diff -Nru juju-core-2.0~beta12/debian/changelog juju-core-2.0~beta15/debian/changelog --- juju-core-2.0~beta12/debian/changelog 2016-07-22 14:31:32.000000000 +0000 +++ juju-core-2.0~beta15/debian/changelog 2016-08-22 12:22:41.000000000 +0000 @@ -1,10 +1,38 @@ -juju-core (2.0~beta12-0ubuntu1.16.04.1) xenial-proposed; urgency=medium +juju-core (2.0~beta15-0ubuntu2.16.04.1) xenial-proposed; urgency=medium + + * Restore all arches to building + * Apply patch for powerpc build + * Re-add jujud to package (LP: #1608538) + + -- Nicholas Skaggs Fri, 18 Aug 2016 10:08:34 -0400 + +juju-core (2.0~beta15-0ubuntu1.16.04.1) xenial-proposed; urgency=medium + + * New upstream release 2.0-beta15 (LP: #1613718) + * Update debian/copyright + * Re-enable current-manual-provider test + * Remove jujud from package + * Remove golang-juju-loggo-dev dependency temporarily (LP: #1612713) + + -- Nicholas Skaggs Fri, 12 Aug 2016 10:08:34 -0400 + +juju-core (2.0~beta12-0ubuntu1.16.10.3) yakkety; urgency=medium + + * Add skip to current-manual-provider for bug 1605313 and bug 1605050 + + -- Nicholas Skaggs Thu, 21 Jul 2016 15:12:52 -0400 + +juju-core (2.0~beta12-0ubuntu1.16.10.2) yakkety; urgency=medium + + * Fix adt test for manual-provider + + -- Nicholas Skaggs Wed, 20 Jul 2016 09:19:42 -0400 + +juju-core (2.0~beta12-0ubuntu1.16.10.1) yakkety; urgency=medium [ Nicholas Skaggs ] * New upstream release 2.0-beta12 (LP: #1604137). * Update debian/copyright. - * Add skip to current-manual-provider for bug 1605313 and bug 1605050 - * Fix adt test for manual-provider [ Martin Packman ] * Allow stripping of go binaries (LP: #1564662). diff -Nru juju-core-2.0~beta12/debian/control juju-core-2.0~beta15/debian/control --- juju-core-2.0~beta12/debian/control 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/debian/control 2016-08-22 12:17:34.000000000 +0000 @@ -13,7 +13,6 @@ golang-golang-x-crypto-dev, golang-golang-x-net-dev, golang-gopkg-tomb.v2-dev, - golang-juju-loggo-dev, golang-websocket-dev, golang-yaml.v2-dev, lsb-release, diff -Nru juju-core-2.0~beta12/debian/copyright juju-core-2.0~beta15/debian/copyright --- juju-core-2.0~beta12/debian/copyright 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/debian/copyright 2016-08-22 12:17:34.000000000 +0000 @@ -118,12 +118,12 @@ Files: src/github.com/juju/bundlechanges/* Copyright: 2015 Canonical Ltd. License: LGPL-3+ -Comment: Last verified commit 8d99dd2a94d7b4fd975a152238d0e19d0c4a6cf1 +Comment: Last verified commit 6791af0ab78efe88ff99c2a0095208b3b7a32055 Files: src/github.com/juju/cmd/* Copyright: 2012-2016 Canonical Ltd. License: LGPL-3 with linking exception -Comment: Last verified commit a11ae7a7436c133e799f025998cbbefd3f6eef7e +Comment: Last verified commit 035efd5daac768531ef240ab9e5ee32e3498fbef Files: src/github.com/juju/errors/* Copyright: 2013-2015 Canonical Ltd. @@ -203,7 +203,7 @@ Files: src/github.com/juju/gomaasapi/* Copyright: 2012-2016, Canonical Ltd. License: LGPL-3+ -Comment: Last verified commit 5bd7212f416a2d801e4a39800b66e1ee4461c42e +Comment: Last verified commit c4008a71e7212cb6a99a9c17bb218034927d82b7 Files: src/github.com/juju/govmomi/* Copyright: 2014-2015 VMware, Inc. All Rights Reserved. @@ -292,9 +292,9 @@ https://github.com/coreos/go-systemd/blob/master/unit/serialize.go Files: src/github.com/juju/loggo/* -Copyright: 2014, 2015 Canonical Ltd. +Copyright: 2014, 2016 Canonical Ltd. License: LGPL-3 with linking exception -Comment: Last verified commit 8477fc936adf0e382d680310047ca27e128a309a +Comment: Last verified commit 15901ae4de786d05edae84a27c93d3fbef66c91e Files: src/github.com/juju/mutex/* Copyright: 2016 Canonical Ltd. @@ -324,17 +324,22 @@ Files: src/github.com/juju/romulus/* Copyright: 2016 Canonical Ltd. License: AGPL-3 -Comment: Last verified commit 6b52a14d619315a31ad4d7069db654c883d6f562 +Comment: Last verified commit f790f93d956741903ce5b1f027df4c9404227d55 Files: src/github.com/juju/schema/* Copyright: 2011-2016 Canonical Ltd. License: LGPL-3 with linking exception Comment: Last verified commit 075de04f9b7d7580d60a1e12a0b3f50bb18e6998 +Files: src/github.com/juju/terms-client/* +Copyright: 2016 Canonical Ltd. +License: GPL-3 +Comment: Last verified commit 9b925afd677234e4146dde3cb1a11e187cbed64e + Files: src/github.com/juju/testing/* Copyright: 2011-2016 Canonical Ltd. License: LGPL-3 with linking exception -Comment: Last verified commit ccf839b5a07a7a05009f8fa3ec41cd05fb2e0b08 +Comment: Last verified commit d325c22badd4ba3a5fde01d479b188c7a06df755 Files: src/github.com/juju/testing/checkers/file_test.go src/github.com/juju/testing/mgo_windows.go @@ -363,7 +368,7 @@ Copyright: 2011-2016 Canonical Ltd. 2014, 2015, 2016 Cloudbase Solutions SRL License: LGPL-3 with linking exception -Comment: Last verified commit 6219812829a3542c827c76cc75f416d4e6c94335 +Comment: Last verified commit 10adcbfe55417518543ed3c3341de2c7db0a3450 Files: src/github.com/juju/utils/du/diskusage.go src/github.com/juju/utils/du/diskusage_windows.go @@ -523,7 +528,7 @@ Files: src/gopkg.in/juju/charm.v6-unstable/* Copyright: 2011-2016 Canonical Ltd. License: LGPL-3 with linking exception -Comment: Last verified commit 8796be6021c9ecb20630950498ec515f7dd24575 +Comment: Last verified commit a3bb92d047b0892452b6a39ece59b4d3a2ac35b9 Files: src/gopkg.in/juju/charmrepo.v2-unstable/* Copyright: 2012-2016 Canonical Ltd. @@ -558,7 +563,7 @@ Files: src/gopkg.in/mgo.v2/* Copyright: 2010-2015 Gustavo Niemeyer License: BSD-2-clause -Comment: Last verified commit 4d04138ffef2791c479c0c8bbffc30b34081b8d9 +Comment: Last verified commit 29cc868a5ca65f401ff318143f9408d02f4799cc Files: src/gopkg.in/mgo.v2/internal/sasl/sspi_windows.* Copyright: 2013-2015 - Christian Amor Kvalheim @@ -571,7 +576,7 @@ Files: src/gopkg.in/juju/names.v2/* Copyright: 2013-2016 Canonical Ltd. License: LGPL-3 with linking exception -Comment: Last verified commit 5426d66579afd36fc63d809dd58806806c2f161f +Comment: Last verified commit 3e0d33a444fec55aea7269b849eb22da41e73072 Files: src/gopkg.in/natefinch/lumberjack.v2/* Copyright: 2014 Nate Finch diff -Nru juju-core-2.0~beta12/debian/patches/gccgo-vsphere-storage.diff juju-core-2.0~beta15/debian/patches/gccgo-vsphere-storage.diff --- juju-core-2.0~beta12/debian/patches/gccgo-vsphere-storage.diff 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/debian/patches/gccgo-vsphere-storage.diff 2016-08-22 12:17:34.000000000 +0000 @@ -0,0 +1,13 @@ +Index: quilt/src/github.com/juju/juju/provider/vsphere/storage.go +=================================================================== +--- quilt.orig/src/github.com/juju/juju/provider/vsphere/storage.go ++++ quilt/src/github.com/juju/juju/provider/vsphere/storage.go +@@ -1,6 +1,8 @@ + // Copyright 2016 Canonical Ltd. + // Licensed under the AGPLv3, see LICENCE file for details. + ++// +build !gccgo ++ + package vsphere + + import ( diff -Nru juju-core-2.0~beta12/debian/patches/series juju-core-2.0~beta15/debian/patches/series --- juju-core-2.0~beta12/debian/patches/series 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/debian/patches/series 2016-08-22 12:17:34.000000000 +0000 @@ -0,0 +1 @@ +gccgo-vsphere-storage.diff diff -Nru juju-core-2.0~beta12/debian/tests/current-manual-provider juju-core-2.0~beta15/debian/tests/current-manual-provider --- juju-core-2.0~beta12/debian/tests/current-manual-provider 2016-07-21 19:14:52.000000000 +0000 +++ juju-core-2.0~beta15/debian/tests/current-manual-provider 2016-08-22 12:17:34.000000000 +0000 @@ -1,9 +1,6 @@ #!/bin/sh set -ex -echo "SKIP: bug 1605313 and bug 1605050" -exit 0 - if ! apt-cache show juju-mongodb3.2 >/dev/null 2>&1; then echo "SKIP: 32-bit state servers not supported after xenial." exit 0 diff -Nru juju-core-2.0~beta12/src/github.com/juju/bundlechanges/changes.go juju-core-2.0~beta15/src/github.com/juju/bundlechanges/changes.go --- juju-core-2.0~beta12/src/github.com/juju/bundlechanges/changes.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/bundlechanges/changes.go 2016-08-16 08:56:25.000000000 +0000 @@ -90,7 +90,7 @@ // GUIArgs implements Change.GUIArgs. func (ch *AddCharmChange) GUIArgs() []interface{} { - return []interface{}{ch.Params.Charm} + return []interface{}{ch.Params.Charm, ch.Params.Series} } // AddCharmParams holds parameters for adding a charm to the environment. @@ -223,9 +223,20 @@ if endpointBindings == nil { endpointBindings = make(map[string]string, 0) } - // TODO(ericsnow) Add resources to the result (from - // ch.Params.Resources) once the GUI is ready. - return []interface{}{ch.Params.Charm, ch.Params.Application, options, ch.Params.Constraints, storage, endpointBindings} + resources := ch.Params.Resources + if resources == nil { + resources = make(map[string]int, 0) + } + return []interface{}{ + ch.Params.Charm, + ch.Params.Series, + ch.Params.Application, + options, + ch.Params.Constraints, + storage, + endpointBindings, + resources, + } } // AddApplicationParams holds parameters for deploying a Juju application. diff -Nru juju-core-2.0~beta12/src/github.com/juju/bundlechanges/changes_test.go juju-core-2.0~beta15/src/github.com/juju/bundlechanges/changes_test.go --- juju-core-2.0~beta12/src/github.com/juju/bundlechanges/changes_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/bundlechanges/changes_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -56,7 +56,7 @@ Params: bundlechanges.AddCharmParams{ Charm: "django", }, - GUIArgs: []interface{}{"django"}, + GUIArgs: []interface{}{"django", ""}, }, { Id: "deploy-1", Method: "deploy", @@ -66,11 +66,13 @@ }, GUIArgs: []interface{}{ "$addCharm-0", + "", "django", map[string]interface{}{}, "", map[string]string{}, map[string]string{}, + map[string]int{}, }, Requires: []string{"addCharm-0"}, }}, @@ -104,7 +106,7 @@ Charm: "cs:precise/mediawiki-10", Series: "precise", }, - GUIArgs: []interface{}{"cs:precise/mediawiki-10"}, + GUIArgs: []interface{}{"cs:precise/mediawiki-10", "precise"}, }, { Id: "deploy-1", Method: "deploy", @@ -117,11 +119,13 @@ }, GUIArgs: []interface{}{ "$addCharm-0", + "precise", "mediawiki", map[string]interface{}{"debug": false}, "", map[string]string{}, map[string]string{}, + map[string]int{"data": 3}, }, Requires: []string{"addCharm-0"}, }, { @@ -153,7 +157,7 @@ Charm: "cs:precise/mysql-28", Series: "precise", }, - GUIArgs: []interface{}{"cs:precise/mysql-28"}, + GUIArgs: []interface{}{"cs:precise/mysql-28", "precise"}, }, { Id: "deploy-5", Method: "deploy", @@ -164,11 +168,13 @@ }, GUIArgs: []interface{}{ "$addCharm-4", + "precise", "mysql", map[string]interface{}{}, "", map[string]string{}, map[string]string{}, + map[string]int{}, }, Requires: []string{"addCharm-4"}, }, { @@ -214,7 +220,7 @@ Charm: "precise/mediawiki-10", Series: "precise", }, - GUIArgs: []interface{}{"precise/mediawiki-10"}, + GUIArgs: []interface{}{"precise/mediawiki-10", "precise"}, }, { Id: "deploy-1", Method: "deploy", @@ -225,11 +231,13 @@ }, GUIArgs: []interface{}{ "$addCharm-0", + "precise", "mediawiki", map[string]interface{}{}, "", map[string]string{}, map[string]string{}, + map[string]int{}, }, Requires: []string{"addCharm-0"}, }, { @@ -242,11 +250,13 @@ }, GUIArgs: []interface{}{ "$addCharm-0", + "precise", "otherwiki", map[string]interface{}{}, "", map[string]string{}, map[string]string{}, + map[string]int{}, }, Requires: []string{"addCharm-0"}, }, { @@ -291,7 +301,7 @@ Charm: "cs:trusty/django-42", Series: "trusty", }, - GUIArgs: []interface{}{"cs:trusty/django-42"}, + GUIArgs: []interface{}{"cs:trusty/django-42", "trusty"}, }, { Id: "deploy-1", Method: "deploy", @@ -303,11 +313,13 @@ }, GUIArgs: []interface{}{ "$addCharm-0", + "trusty", "django", map[string]interface{}{}, "cpu-cores=4 cpu-power=42", map[string]string{}, map[string]string{}, + map[string]int{}, }, Requires: []string{"addCharm-0"}, }, { @@ -317,7 +329,7 @@ Charm: "cs:trusty/haproxy-47", Series: "trusty", }, - GUIArgs: []interface{}{"cs:trusty/haproxy-47"}, + GUIArgs: []interface{}{"cs:trusty/haproxy-47", "trusty"}, }, { Id: "deploy-3", Method: "deploy", @@ -329,11 +341,13 @@ }, GUIArgs: []interface{}{ "$addCharm-2", + "trusty", "haproxy", map[string]interface{}{"bad": "wolf", "number": 42.47}, "", map[string]string{}, map[string]string{}, + map[string]int{}, }, Requires: []string{"addCharm-2"}, }, { @@ -463,7 +477,7 @@ Charm: "cs:trusty/django-42", Series: "trusty", }, - GUIArgs: []interface{}{"cs:trusty/django-42"}, + GUIArgs: []interface{}{"cs:trusty/django-42", "trusty"}, }, { Id: "deploy-1", Method: "deploy", @@ -474,11 +488,13 @@ }, GUIArgs: []interface{}{ "$addCharm-0", + "trusty", "django", map[string]interface{}{}, "", map[string]string{}, map[string]string{}, + map[string]int{}, }, Requires: []string{"addCharm-0"}, }, { @@ -556,7 +572,7 @@ Charm: "cs:precise/mediawiki-10", Series: "precise", }, - GUIArgs: []interface{}{"cs:precise/mediawiki-10"}, + GUIArgs: []interface{}{"cs:precise/mediawiki-10", "precise"}, }, { Id: "deploy-1", Method: "deploy", @@ -567,11 +583,13 @@ }, GUIArgs: []interface{}{ "$addCharm-0", + "precise", "mediawiki", map[string]interface{}{}, "", map[string]string{}, map[string]string{}, + map[string]int{}, }, Requires: []string{"addCharm-0"}, }, { @@ -581,7 +599,7 @@ Charm: "cs:precise/mysql-28", Series: "precise", }, - GUIArgs: []interface{}{"cs:precise/mysql-28"}, + GUIArgs: []interface{}{"cs:precise/mysql-28", "precise"}, }, { Id: "deploy-3", Method: "deploy", @@ -593,11 +611,13 @@ }, GUIArgs: []interface{}{ "$addCharm-2", + "precise", "mysql", map[string]interface{}{}, "mem=42G", map[string]string{}, map[string]string{}, + map[string]int{}, }, Requires: []string{"addCharm-2"}, }, { @@ -629,7 +649,7 @@ Charm: "cs:trusty/django-42", Series: "trusty", }, - GUIArgs: []interface{}{"cs:trusty/django-42"}, + GUIArgs: []interface{}{"cs:trusty/django-42", "trusty"}, }, { Id: "deploy-1", Method: "deploy", @@ -640,11 +660,13 @@ }, GUIArgs: []interface{}{ "$addCharm-0", + "trusty", "django", map[string]interface{}{}, "", map[string]string{}, map[string]string{}, + map[string]int{}, }, Requires: []string{"addCharm-0"}, }, { @@ -653,7 +675,7 @@ Params: bundlechanges.AddCharmParams{ Charm: "wordpress", }, - GUIArgs: []interface{}{"wordpress"}, + GUIArgs: []interface{}{"wordpress", ""}, }, { Id: "deploy-3", Method: "deploy", @@ -663,11 +685,13 @@ }, GUIArgs: []interface{}{ "$addCharm-2", + "", "wordpress", map[string]interface{}{}, "", map[string]string{}, map[string]string{}, + map[string]int{}, }, Requires: []string{"addCharm-2"}, }, { @@ -746,7 +770,7 @@ Charm: "cs:trusty/django-42", Series: "trusty", }, - GUIArgs: []interface{}{"cs:trusty/django-42"}, + GUIArgs: []interface{}{"cs:trusty/django-42", "trusty"}, }, { Id: "deploy-1", Method: "deploy", @@ -757,11 +781,13 @@ }, GUIArgs: []interface{}{ "$addCharm-0", + "trusty", "django", map[string]interface{}{}, "", map[string]string{}, map[string]string{}, + map[string]int{}, }, Requires: []string{"addCharm-0"}, }, { @@ -771,7 +797,7 @@ Charm: "cs:trusty/mem-47", Series: "trusty", }, - GUIArgs: []interface{}{"cs:trusty/mem-47"}, + GUIArgs: []interface{}{"cs:trusty/mem-47", "trusty"}, }, { Id: "deploy-3", Method: "deploy", @@ -782,11 +808,13 @@ }, GUIArgs: []interface{}{ "$addCharm-2", + "trusty", "memcached", map[string]interface{}{}, "", map[string]string{}, map[string]string{}, + map[string]int{}, }, Requires: []string{"addCharm-2"}, }, { @@ -796,7 +824,7 @@ Charm: "vivid/rails", Series: "vivid", }, - GUIArgs: []interface{}{"vivid/rails"}, + GUIArgs: []interface{}{"vivid/rails", "vivid"}, }, { Id: "deploy-5", Method: "deploy", @@ -807,11 +835,13 @@ }, GUIArgs: []interface{}{ "$addCharm-4", + "vivid", "ror", map[string]interface{}{}, "", map[string]string{}, map[string]string{}, + map[string]int{}, }, Requires: []string{"addCharm-4"}, }, { @@ -1039,7 +1069,7 @@ Charm: "cs:trusty/django-42", Series: "trusty", }, - GUIArgs: []interface{}{"cs:trusty/django-42"}, + GUIArgs: []interface{}{"cs:trusty/django-42", "trusty"}, }, { Id: "deploy-1", Method: "deploy", @@ -1050,11 +1080,13 @@ }, GUIArgs: []interface{}{ "$addCharm-0", + "trusty", "django", map[string]interface{}{}, "", map[string]string{}, map[string]string{}, + map[string]int{}, }, Requires: []string{"addCharm-0"}, }, { @@ -1196,7 +1228,7 @@ Charm: "cs:trusty/django-42", Series: "trusty", }, - GUIArgs: []interface{}{"cs:trusty/django-42"}, + GUIArgs: []interface{}{"cs:trusty/django-42", "trusty"}, }, { Id: "deploy-1", Method: "deploy", @@ -1211,6 +1243,7 @@ }, GUIArgs: []interface{}{ "$addCharm-0", + "trusty", "django", map[string]interface{}{}, "", @@ -1219,6 +1252,7 @@ "tmpfs": "tmpfs,1G", }, map[string]string{}, + map[string]int{}, }, Requires: []string{"addCharm-0"}, }, { @@ -1253,7 +1287,7 @@ Params: bundlechanges.AddCharmParams{ Charm: "django", }, - GUIArgs: []interface{}{"django"}, + GUIArgs: []interface{}{"django", ""}, }, { Id: "deploy-1", Method: "deploy", @@ -1264,11 +1298,13 @@ }, GUIArgs: []interface{}{ "$addCharm-0", + "", "django", map[string]interface{}{}, "", map[string]string{}, map[string]string{"foo": "bar"}, + map[string]int{}, }, Requires: []string{"addCharm-0"}, }}, @@ -1293,7 +1329,7 @@ Charm: "cs:precise/juju-gui", Series: "precise", }, - GUIArgs: []interface{}{"cs:precise/juju-gui"}, + GUIArgs: []interface{}{"cs:precise/juju-gui", "precise"}, }, { Id: "deploy-1", Method: "deploy", @@ -1304,11 +1340,13 @@ }, GUIArgs: []interface{}{ "$addCharm-0", + "precise", "gui3", map[string]interface{}{}, "", map[string]string{}, map[string]string{}, + map[string]int{}, }, Requires: []string{"addCharm-0"}, }, { @@ -1408,7 +1446,7 @@ func (s *changesSuite) TestFromData(c *gc.C) { for i, test := range fromDataTests { - c.Logf("test %d: %s", i, test.about) + c.Logf("\ntest %d: %s", i, test.about) s.assertParseData(c, test.content, test.expected) } } @@ -1421,7 +1459,7 @@ Charm: charmDir, Series: series, }, - GUIArgs: []interface{}{charmDir}, + GUIArgs: []interface{}{charmDir, series}, }, { Id: "deploy-1", Method: "deploy", @@ -1432,11 +1470,13 @@ }, GUIArgs: []interface{}{ "$addCharm-0", + series, "django", - map[string]interface{}{}, - "", - map[string]string{}, - map[string]string{}, + map[string]interface{}{}, // options. + "", // constraints. + map[string]string{}, // storage. + map[string]string{}, // endpoint bindings. + map[string]int{}, // resources. }, Requires: []string{"addCharm-0"}, }} diff -Nru juju-core-2.0~beta12/src/github.com/juju/cmd/logging.go juju-core-2.0~beta15/src/github.com/juju/cmd/logging.go --- juju-core-2.0~beta12/src/github.com/juju/cmd/logging.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/cmd/logging.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,7 +7,6 @@ "fmt" "io" "os" - "time" "github.com/juju/loggo" "launchpad.net/gnuflag" @@ -35,7 +34,7 @@ if l.NewWriter != nil { return l.NewWriter(target) } - return loggo.NewSimpleWriter(target, &loggo.DefaultFormatter{}) + return loggo.NewSimpleWriter(target, loggo.DefaultFormatter) } // AddFlags adds appropriate flags to f. @@ -64,7 +63,7 @@ return err } writer := log.GetLogWriter(target) - err = loggo.RegisterWriter("logfile", writer, loggo.TRACE) + err = loggo.RegisterWriter("logfile", writer) if err != nil { return err } @@ -93,8 +92,9 @@ loggo.RemoveWriter("default") // Create a simple writer that doesn't show filenames, or timestamps, // and only shows warning or above. - writer := loggo.NewSimpleWriter(ctx.Stderr, &warningFormatter{}) - err := loggo.RegisterWriter("warning", writer, loggo.WARNING) + writer := loggo.NewSimpleWriter(ctx.Stderr, warningFormatter) + writer = loggo.NewMinimumLevelWriter(writer, loggo.WARNING) + err := loggo.RegisterWriter("warning", writer) if err != nil { return err } @@ -109,10 +109,8 @@ // warningFormatter is a simple loggo formatter that produces something like: // WARNING The message... -type warningFormatter struct{} - -func (*warningFormatter) Format(level loggo.Level, _, _ string, _ int, _ time.Time, message string) string { - return fmt.Sprintf("%s %s", level, message) +func warningFormatter(entry loggo.Entry) string { + return fmt.Sprintf("%s %s", entry.Level, entry.Message) } // NewCommandLogWriter creates a loggo writer for registration @@ -130,12 +128,12 @@ } // Write implements loggo's Writer interface. -func (s *commandLogWriter) Write(level loggo.Level, name, filename string, line int, timestamp time.Time, message string) { - if name == s.name { - if level <= loggo.INFO { - fmt.Fprintf(s.out, "%s\n", message) +func (s *commandLogWriter) Write(entry loggo.Entry) { + if entry.Module == s.name { + if entry.Level <= loggo.INFO { + fmt.Fprintf(s.out, "%s\n", entry.Message) } else { - fmt.Fprintf(s.err, "%s\n", message) + fmt.Fprintf(s.err, "%s\n", entry.Message) } } } diff -Nru juju-core-2.0~beta12/src/github.com/juju/cmd/logging_test.go juju-core-2.0~beta15/src/github.com/juju/cmd/logging_test.go --- juju-core-2.0~beta12/src/github.com/juju/cmd/logging_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/cmd/logging_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -18,19 +18,11 @@ var logger = loggo.GetLogger("juju.test") type LogSuite struct { - testing.CleanupSuite + testing.LoggingCleanupSuite } var _ = gc.Suite(&LogSuite{}) -func (s *LogSuite) SetUpTest(c *gc.C) { - s.CleanupSuite.SetUpTest(c) - s.AddCleanup(func(_ *gc.C) { - loggo.ResetLoggers() - loggo.ResetWriters() - }) -} - func newLogWithFlags(c *gc.C, defaultConfig string, flags ...string) *cmd.Log { log := &cmd.Log{ DefaultConfig: defaultConfig, diff -Nru juju-core-2.0~beta12/src/github.com/juju/gomaasapi/testservice.go juju-core-2.0~beta15/src/github.com/juju/gomaasapi/testservice.go --- juju-core-2.0~beta12/src/github.com/juju/gomaasapi/testservice.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/gomaasapi/testservice.go 2016-08-16 08:56:25.000000000 +0000 @@ -95,7 +95,7 @@ versionJSON string // devices is a map of device UUIDs to devices. - devices map[string]*testDevice + devices map[string]*TestDevice subnets map[uint]TestSubnet subnetNameToID map[string]uint @@ -107,7 +107,7 @@ nextVLAN int } -type testDevice struct { +type TestDevice struct { IPAddresses []string SystemId string MACAddress string @@ -227,7 +227,7 @@ server.nodegroupsInterfaces = make(map[string][]JSONObject) server.zones = make(map[string]JSONObject) server.versionJSON = `{"capabilities": ["networks-management","static-ipaddresses","devices-management","network-deployment-ubuntu"]}` - server.devices = make(map[string]*testDevice) + server.devices = make(map[string]*TestDevice) server.subnets = make(map[uint]TestSubnet) server.subnetNameToID = make(map[string]uint) server.nextSubnet = 1 @@ -542,6 +542,14 @@ server.zones[name] = obj } +func (server *TestServer) AddDevice(device *TestDevice) { + server.devices[device.SystemId] = device +} + +func (server *TestServer) Devices() map[string]*TestDevice { + return server.devices +} + // NewTestServer starts and returns a new MAAS test server. The caller should call Close when finished, to shut it down. func NewTestServer(version string) *TestServer { server := &TestServer{version: version} @@ -655,7 +663,7 @@ } } -func macMatches(device *testDevice, macs []string, hasMac bool) bool { +func macMatches(device *TestDevice, macs []string, hasMac bool) bool { if !hasMac { return true } @@ -714,7 +722,7 @@ }` ) -func renderDevice(device *testDevice) string { +func renderDevice(device *TestDevice) string { t := template.New("Device template") t = t.Funcs(templateFuncs) t, err := t.Parse(deviceTemplate) @@ -756,7 +764,7 @@ return } - device := &testDevice{ + device := &TestDevice{ MACAddress: mac, APIVersion: server.version, Parent: parent, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/agent/agentbootstrap/bootstrap.go juju-core-2.0~beta15/src/github.com/juju/juju/agent/agentbootstrap/bootstrap.go --- juju-core-2.0~beta12/src/github.com/juju/juju/agent/agentbootstrap/bootstrap.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/agent/agentbootstrap/bootstrap.go 2016-08-16 08:56:25.000000000 +0000 @@ -15,12 +15,14 @@ "github.com/juju/juju/cloud" "github.com/juju/juju/cloudconfig/instancecfg" "github.com/juju/juju/controller/modelmanager" + "github.com/juju/juju/environs" "github.com/juju/juju/environs/config" "github.com/juju/juju/instance" "github.com/juju/juju/mongo" "github.com/juju/juju/network" "github.com/juju/juju/state" "github.com/juju/juju/state/multiwatcher" + "github.com/juju/juju/storage" ) var logger = loggo.GetLogger("juju.agent.agentbootstrap") @@ -39,6 +41,13 @@ // SharedSecret is the Mongo replica set shared secret (keyfile). SharedSecret string + + // Provider is called to obtain an EnvironProvider. + Provider func(string) (environs.EnvironProvider, error) + + // StorageProviderRegistry is used to determine and store the + // details of the default storage pools. + StorageProviderRegistry storage.ProviderRegistry } // InitializeState should be called on the bootstrap machine's agent @@ -59,7 +68,7 @@ c agent.ConfigSetter, args InitializeStateParams, dialOpts mongo.DialOpts, - policy state.Policy, + newPolicy state.NewPolicyFunc, ) (_ *state.State, _ *state.Machine, resultErr error) { if c.Tag() != names.NewMachineTag(agent.BootstrapMachineId) { return nil, nil, errors.Errorf("InitializeState not called with bootstrap machine's configuration") @@ -89,12 +98,13 @@ logger.Debugf("initializing address %v", info.Addrs) st, err := state.Initialize(state.InitializeParams{ ControllerModelArgs: state.ModelArgs{ - Owner: adminUser, - Config: args.ControllerModelConfig, - Constraints: args.ModelConstraints, - CloudName: args.ControllerCloudName, - CloudRegion: args.ControllerCloudRegion, - CloudCredential: args.ControllerCloudCredentialName, + Owner: adminUser, + Config: args.ControllerModelConfig, + Constraints: args.ModelConstraints, + CloudName: args.ControllerCloudName, + CloudRegion: args.ControllerCloudRegion, + CloudCredential: args.ControllerCloudCredentialName, + StorageProviderRegistry: args.StorageProviderRegistry, }, CloudName: args.ControllerCloudName, Cloud: args.ControllerCloud, @@ -103,7 +113,7 @@ ControllerInheritedConfig: args.ControllerInheritedConfig, MongoInfo: info, MongoDialOpts: dialOpts, - Policy: policy, + NewPolicy: newPolicy, }) if err != nil { return nil, nil, errors.Errorf("failed to initialize state: %v", err) @@ -142,6 +152,17 @@ } attrs[config.AuthorizedKeysKey] = args.ControllerModelConfig.AuthorizedKeys() + // Construct a CloudSpec to pass on to NewModelConfig below. + cloudSpec, err := environs.MakeCloudSpec( + args.ControllerCloud, + args.ControllerCloudName, + args.ControllerCloudRegion, + args.ControllerCloudCredential, + ) + if err != nil { + return nil, nil, errors.Trace(err) + } + // TODO(axw) we shouldn't be adding credentials to model config. if args.ControllerCloudCredential != nil { for k, v := range args.ControllerCloudCredential.Attributes() { @@ -149,19 +170,38 @@ } } controllerUUID := args.ControllerConfig.ControllerUUID() - hostedModelConfig, err := modelmanager.ModelConfigCreator{}.NewModelConfig( - modelmanager.IsAdmin, controllerUUID, args.ControllerModelConfig, attrs, + creator := modelmanager.ModelConfigCreator{Provider: args.Provider} + hostedModelConfig, err := creator.NewModelConfig( + cloudSpec, controllerUUID, args.ControllerModelConfig, attrs, ) if err != nil { return nil, nil, errors.Annotate(err, "creating hosted model config") } + provider, err := args.Provider(cloudSpec.Type) + if err != nil { + return nil, nil, errors.Annotate(err, "getting environ provider") + } + hostedModelEnv, err := provider.Open(environs.OpenParams{ + Cloud: cloudSpec, + Config: hostedModelConfig, + }) + if err != nil { + return nil, nil, errors.Annotate(err, "opening hosted model environment") + } + if err := hostedModelEnv.Create(environs.CreateParams{ + ControllerUUID: controllerUUID, + }); err != nil { + return nil, nil, errors.Annotate(err, "creating hosted model environment") + } + _, hostedModelState, err := st.NewModel(state.ModelArgs{ - Owner: adminUser, - Config: hostedModelConfig, - Constraints: args.ModelConstraints, - CloudName: args.ControllerCloudName, - CloudRegion: args.ControllerCloudRegion, - CloudCredential: args.ControllerCloudCredentialName, + Owner: adminUser, + Config: hostedModelConfig, + Constraints: args.ModelConstraints, + CloudName: args.ControllerCloudName, + CloudRegion: args.ControllerCloudRegion, + CloudCredential: args.ControllerCloudCredentialName, + StorageProviderRegistry: args.StorageProviderRegistry, }) if err != nil { return nil, nil, errors.Annotate(err, "creating hosted model") diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/agent/agentbootstrap/bootstrap_test.go juju-core-2.0~beta15/src/github.com/juju/juju/agent/agentbootstrap/bootstrap_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/agent/agentbootstrap/bootstrap_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/agent/agentbootstrap/bootstrap_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -31,6 +31,7 @@ "github.com/juju/juju/provider/dummy" "github.com/juju/juju/state" "github.com/juju/juju/state/multiwatcher" + "github.com/juju/juju/storage/provider" "github.com/juju/juju/testing" jujuversion "github.com/juju/juju/version" ) @@ -132,25 +133,13 @@ "10.0.4.5", ) - // Prepare bootstrap config, so we can use it in the state policy. - provider, err := environs.Provider("dummy") - c.Assert(err, jc.ErrorIsNil) - modelAttrs := dummy.SampleConfig().Delete("admin-secret").Merge(testing.Attrs{ + modelAttrs := testing.FakeConfig().Merge(testing.Attrs{ "agent-version": jujuversion.Current.String(), "not-for-hosted": "foo", }) modelCfg, err := config.New(config.NoDefaults, modelAttrs) c.Assert(err, jc.ErrorIsNil) controllerCfg := testing.FakeControllerConfig() - modelCfg, err = provider.BootstrapConfig(environs.BootstrapConfigParams{ - ControllerUUID: controllerCfg.ControllerUUID(), - Config: modelCfg, - }) - c.Assert(err, jc.ErrorIsNil) - // Dummy provider uses a random port, which is added to cfg used to create environment. - apiPort := dummy.ApiPort(provider) - controllerCfg["api-port"] = apiPort - defer dummy.Reset(c) hostedModelUUID := utils.MustNewUUID().String() hostedModelConfigAttrs := map[string]interface{}{ @@ -161,6 +150,7 @@ "apt-mirror": "http://mirror", } + var envProvider fakeProvider args := agentbootstrap.InitializeStateParams{ StateInitializationParams: instancecfg.StateInitializationParams{ BootstrapMachineConstraints: expectBootstrapConstraints, @@ -182,11 +172,16 @@ BootstrapMachineAddresses: initialAddrs, BootstrapMachineJobs: []multiwatcher.MachineJob{multiwatcher.JobManageModel}, SharedSecret: "abc123", + Provider: func(t string) (environs.EnvironProvider, error) { + c.Assert(t, gc.Equals, "dummy") + return &envProvider, nil + }, + StorageProviderRegistry: provider.CommonStorageProviders(), } adminUser := names.NewLocalUserTag("agent-admin") st, m, err := agentbootstrap.InitializeState( - adminUser, cfg, args, mongotest.DialOpts(), environs.NewStatePolicy(), + adminUser, cfg, args, mongotest.DialOpts(), state.NewPolicyFunc(nil), ) c.Assert(err, jc.ErrorIsNil) defer st.Close() @@ -213,7 +208,7 @@ "controller-uuid": testing.ModelTag.Id(), "ca-cert": testing.CACert, "state-port": 1234, - "api-port": apiPort, + "api-port": 17777, "set-numa-control-policy": false, }) @@ -222,7 +217,9 @@ newModelCfg, err := st.ModelConfig() c.Assert(err, jc.ErrorIsNil) // Add in the cloud attributes. - expectedAttrs := modelCfg.AllAttrs() + expectedCfg, err := config.New(config.UseDefaults, modelAttrs) + c.Assert(err, jc.ErrorIsNil) + expectedAttrs := expectedCfg.AllAttrs() expectedAttrs["apt-mirror"] = "http://mirror" c.Assert(newModelCfg.AllAttrs(), jc.DeepEquals, expectedAttrs) @@ -292,9 +289,29 @@ info, ok := cfg.MongoInfo() c.Assert(ok, jc.IsTrue) c.Assert(info.Password, gc.Not(gc.Equals), testing.DefaultMongoPassword) - st1, err := state.Open(newCfg.Model(), info, mongotest.DialOpts(), environs.NewStatePolicy()) + st1, err := state.Open(newCfg.Model(), info, mongotest.DialOpts(), nil) c.Assert(err, jc.ErrorIsNil) defer st1.Close() + + // Make sure that the hosted model Environ's Create method is called. + envProvider.CheckCallNames(c, + "RestrictedConfigAttributes", + "PrepareConfig", + "Validate", + "Open", + "Create", + ) + envProvider.CheckCall(c, 3, "Open", environs.OpenParams{ + Cloud: environs.CloudSpec{ + Type: "dummy", + Name: "dummy", + Region: "some-region", + }, + Config: hostedCfg, + }) + envProvider.CheckCall(c, 4, "Create", environs.CreateParams{ + ControllerUUID: controllerCfg.ControllerUUID(), + }) } func (s *bootstrapSuite) TestInitializeStateWithStateServingInfoNotAvailable(c *gc.C) { @@ -316,7 +333,7 @@ args := agentbootstrap.InitializeStateParams{} adminUser := names.NewLocalUserTag("agent-admin") - _, _, err = agentbootstrap.InitializeState(adminUser, cfg, args, mongotest.DialOpts(), environs.NewStatePolicy()) + _, _, err = agentbootstrap.InitializeState(adminUser, cfg, args, mongotest.DialOpts(), nil) // InitializeState will fail attempting to get the api port information c.Assert(err, gc.ErrorMatches, "state serving information not available") } @@ -368,17 +385,21 @@ }, BootstrapMachineJobs: []multiwatcher.MachineJob{multiwatcher.JobManageModel}, SharedSecret: "abc123", + Provider: func(t string) (environs.EnvironProvider, error) { + return &fakeProvider{}, nil + }, + StorageProviderRegistry: provider.CommonStorageProviders(), } adminUser := names.NewLocalUserTag("agent-admin") st, _, err := agentbootstrap.InitializeState( - adminUser, cfg, args, mongotest.DialOpts(), state.Policy(nil), + adminUser, cfg, args, mongotest.DialOpts(), state.NewPolicyFunc(nil), ) c.Assert(err, jc.ErrorIsNil) st.Close() st, _, err = agentbootstrap.InitializeState( - adminUser, cfg, args, mongotest.DialOpts(), state.Policy(nil), + adminUser, cfg, args, mongotest.DialOpts(), state.NewPolicyFunc(nil), ) if err == nil { st.Close() @@ -420,9 +441,44 @@ Tag: nil, // admin user Password: password, } - st, err := state.Open(modelTag, info, mongotest.DialOpts(), environs.NewStatePolicy()) + st, err := state.Open(modelTag, info, mongotest.DialOpts(), state.NewPolicyFunc(nil)) c.Assert(err, jc.ErrorIsNil) defer st.Close() _, err = st.Machine("0") c.Assert(err, jc.ErrorIsNil) } + +type fakeProvider struct { + environs.EnvironProvider + gitjujutesting.Stub +} + +func (p *fakeProvider) RestrictedConfigAttributes() []string { + p.MethodCall(p, "RestrictedConfigAttributes") + return []string{} +} + +func (p *fakeProvider) PrepareConfig(args environs.PrepareConfigParams) (*config.Config, error) { + p.MethodCall(p, "PrepareConfig", args) + return args.Config, p.NextErr() +} + +func (p *fakeProvider) Validate(newCfg, oldCfg *config.Config) (*config.Config, error) { + p.MethodCall(p, "Validate", newCfg, oldCfg) + return newCfg, p.NextErr() +} + +func (p *fakeProvider) Open(args environs.OpenParams) (environs.Environ, error) { + p.MethodCall(p, "Open", args) + return &fakeEnviron{Stub: &p.Stub}, p.NextErr() +} + +type fakeEnviron struct { + environs.Environ + *gitjujutesting.Stub +} + +func (e *fakeEnviron) Create(args environs.CreateParams) error { + e.MethodCall(e, "Create", args) + return e.NextErr() +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/agent/agent.go juju-core-2.0~beta15/src/github.com/juju/juju/agent/agent.go --- juju-core-2.0~beta12/src/github.com/juju/juju/agent/agent.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/agent/agent.go 2016-08-16 08:56:25.000000000 +0000 @@ -159,15 +159,14 @@ const SystemIdentity = "system-identity" const ( - LxcBridge = "LXC_BRIDGE" - LxdBridge = "LXD_BRIDGE" - ProviderType = "PROVIDER_TYPE" - ContainerType = "CONTAINER_TYPE" - Namespace = "NAMESPACE" - AgentServiceName = "AGENT_SERVICE_NAME" - MongoOplogSize = "MONGO_OPLOG_SIZE" - NumaCtlPreference = "NUMA_CTL_PREFERENCE" - AllowsSecureConnection = "SECURE_CONTROLLER_CONNECTION" + LxcBridge = "LXC_BRIDGE" + LxdBridge = "LXD_BRIDGE" + ProviderType = "PROVIDER_TYPE" + ContainerType = "CONTAINER_TYPE" + Namespace = "NAMESPACE" + AgentServiceName = "AGENT_SERVICE_NAME" + MongoOplogSize = "MONGO_OPLOG_SIZE" + NumaCtlPreference = "NUMA_CTL_PREFERENCE" ) // The Config interface is the sole way that the agent gets access to the diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/agent/machine_test.go juju-core-2.0~beta15/src/github.com/juju/juju/api/agent/machine_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/agent/machine_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/agent/machine_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -17,7 +17,6 @@ apiagent "github.com/juju/juju/api/agent" apiserveragent "github.com/juju/juju/apiserver/agent" "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/environs" "github.com/juju/juju/juju/testing" "github.com/juju/juju/mongo" "github.com/juju/juju/mongo/mongotest" @@ -181,7 +180,7 @@ } func tryOpenState(modelTag names.ModelTag, info *mongo.MongoInfo) error { - st, err := state.Open(modelTag, info, mongotest.DialOpts(), environs.NewStatePolicy()) + st, err := state.Open(modelTag, info, mongotest.DialOpts(), nil) if err == nil { st.Close() } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/agent/state.go juju-core-2.0~beta15/src/github.com/juju/juju/api/agent/state.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/agent/state.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/agent/state.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,6 +10,7 @@ "github.com/juju/juju/api/base" "github.com/juju/juju/api/common" + "github.com/juju/juju/api/common/cloudspec" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/instance" "github.com/juju/juju/state/multiwatcher" @@ -19,6 +20,7 @@ type State struct { facade base.FacadeCaller *common.ModelWatcher + *cloudspec.CloudSpecAPI *common.ControllerConfigAPI } @@ -29,6 +31,7 @@ return &State{ facade: facadeCaller, ModelWatcher: common.NewModelWatcher(facadeCaller), + CloudSpecAPI: cloudspec.NewCloudSpecAPI(facadeCaller), ControllerConfigAPI: common.NewControllerConfig(facadeCaller), } } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/apiclient.go juju-core-2.0~beta15/src/github.com/juju/juju/api/apiclient.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/apiclient.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/apiclient.go 2016-08-16 08:56:25.000000000 +0000 @@ -18,7 +18,9 @@ "github.com/juju/errors" "github.com/juju/loggo" + "github.com/juju/retry" "github.com/juju/utils" + "github.com/juju/utils/clock" "github.com/juju/utils/parallel" "github.com/juju/version" "golang.org/x/net/websocket" @@ -48,10 +50,16 @@ PingTimeout = 30 * time.Second ) +type rpcConnection interface { + Call(req rpc.Request, params, response interface{}) error + Close() error +} + // state is the internal implementation of the Connection interface. type state struct { - client *rpc.Conn + client rpcConnection conn *websocket.Conn + clock clock.Clock // addr is the address used to connect to the API server. addr string @@ -153,7 +161,7 @@ // // See Connect for details of the connection mechanics. func Open(info *Info, opts DialOpts) (Connection, error) { - return open(info, opts, (*state).Login) + return open(info, opts, clock.WallClock, (*state).Login) } // This unexported open method is used both directly above in the Open @@ -162,12 +170,16 @@ func open( info *Info, opts DialOpts, + clock clock.Clock, loginFunc func(st *state, tag names.Tag, pwd, nonce string, ms []macaroon.Slice) error, ) (Connection, error) { if err := info.Validate(); err != nil { return nil, errors.Annotate(err, "validating info for opening an API connection") } + if clock == nil { + return nil, errors.NotValidf("nil clock") + } conn, tlsConfig, err := connectWebsocket(info, opts) if err != nil { return nil, errors.Trace(err) @@ -187,6 +199,9 @@ bakeryClient.Client = &httpc } apiHost := conn.Config().Location.Host + // Technically when there's no CACert, we don't need this + // machinery, because we could just use http.DefaultTransport + // for everything, but it's easier just to leave it in place. bakeryClient.Client.Transport = &hostSwitchingTransport{ primaryHost: apiHost, primary: utils.NewHttpTLSTransport(tlsConfig), @@ -196,6 +211,7 @@ st := &state{ client: client, conn: conn, + clock: clock, addr: apiHost, cookieURL: &url.URL{ Scheme: "https", @@ -260,7 +276,7 @@ default: return nil, errors.NotSupportedf("loginVersion %d", loginVersion) } - return open(info, opts, loginFunc) + return open(info, opts, clock.WallClock, loginFunc) } // connectWebsocket establishes a websocket connection to the RPC @@ -274,12 +290,12 @@ return nil, nil, errors.New("no API addresses to connect to") } tlsConfig := utils.SecureTLSConfig() - // We want to be specific here (rather than just using "anything". - // See commit 7fc118f015d8480dfad7831788e4b8c0432205e8 (PR 899). - tlsConfig.ServerName = "juju-apiserver" tlsConfig.InsecureSkipVerify = opts.InsecureSkipVerify - if !tlsConfig.InsecureSkipVerify { + if info.CACert != "" && !tlsConfig.InsecureSkipVerify { + // We want to be specific here (rather than just using "anything". + // See commit 7fc118f015d8480dfad7831788e4b8c0432205e8 (PR 899). + tlsConfig.ServerName = "juju-apiserver" certPool, err := CreateCertPool(info.CACert) if err != nil { return nil, nil, errors.Annotate(err, "cert pool creation failed") @@ -506,6 +522,7 @@ var newWebsocketDialer = createWebsocketDialer func createWebsocketDialer(cfg *websocket.Config, opts DialOpts) func(<-chan struct{}) (io.Closer, error) { + // TODO(katco): 2016-08-09: lp:1611427 openAttempt := utils.AttemptStrategy{ Total: opts.Timeout, Delay: opts.RetryDelay, @@ -522,9 +539,10 @@ if err == nil { return conn, nil } - if a.HasNext() { - logger.Debugf("error dialing %q, will retry: %v", cfg.Location, err) - } else { + if !a.HasNext() || isX509Error(err) { + // We won't reconnect when there's an X509 error + // because we're not going to succeed if we retry + // in that case. logger.Infof("error dialing %q: %v", cfg.Location, err) return nil, errors.Annotatef(err, "unable to connect to API") } @@ -533,6 +551,30 @@ } } +// isX509Error reports whether the given websocket error +// results from an X509 problem. +func isX509Error(err error) bool { + wsErr, ok := errors.Cause(err).(*websocket.DialError) + if !ok { + return false + } + switch wsErr.Err.(type) { + case x509.HostnameError, + x509.InsecureAlgorithmError, + x509.UnhandledCriticalExtension, + x509.UnknownAuthorityError, + x509.ConstraintViolationError, + x509.SystemRootsError: + return true + } + switch err { + case x509.ErrUnsupportedAlgorithm, + x509.IncorrectPasswordError: + return true + } + return false +} + func callWithTimeout(f func() error, timeout time.Duration) bool { result := make(chan error, 1) go func() { @@ -569,18 +611,39 @@ return s.APICall("Pinger", s.BestFacadeVersion("Pinger"), "", "Ping", nil, nil) } +type hasErrorCode interface { + ErrorCode() string +} + // APICall places a call to the remote machine. // // This fills out the rpc.Request on the given facade, version for a given // object id, and the specific RPC method. It marshalls the Arguments, and will // unmarshall the result into the response object that is supplied. func (s *state) APICall(facade string, version int, id, method string, args, response interface{}) error { - err := s.client.Call(rpc.Request{ - Type: facade, - Version: version, - Id: id, - Action: method, - }, args, response) + retrySpec := retry.CallArgs{ + Func: func() error { + return s.client.Call(rpc.Request{ + Type: facade, + Version: version, + Id: id, + Action: method, + }, args, response) + }, + IsFatalError: func(err error) bool { + ec, ok := err.(hasErrorCode) + if !ok { + return true + } + return ec.ErrorCode() != params.CodeRetry + }, + Delay: 100 * time.Millisecond, + MaxDelay: 1500 * time.Millisecond, + MaxDuration: 10 * time.Second, + BackoffFunc: retry.DoubleDelay, + Clock: s.clock, + } + err := retry.Call(retrySpec) return errors.Trace(err) } @@ -600,13 +663,6 @@ return s.broken } -// RPCClient returns the RPC client for the state, so that testing -// functions can tickle parts of the API that the conventional entry -// points don't reach. This is exported for testing purposes only. -func (s *state) RPCClient() *rpc.Conn { - return s.client -} - // Addr returns the address used to connect to the API server. func (s *state) Addr() string { return s.addr diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/apiclient_test.go juju-core-2.0~beta15/src/github.com/juju/juju/api/apiclient_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/apiclient_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/apiclient_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,10 +6,13 @@ import ( "net" "sync/atomic" + "time" "github.com/juju/errors" + "github.com/juju/retry" "github.com/juju/testing" jc "github.com/juju/testing/checkers" + "github.com/juju/utils/clock" "github.com/juju/utils/parallel" "golang.org/x/net/websocket" gc "gopkg.in/check.v1" @@ -126,9 +129,9 @@ _, err := api.Open(info, api.DialOpts{}) c.Assert(errors.Cause(err), gc.DeepEquals, &rpc.RequestError{ Message: `unknown model: "bad-tag"`, - Code: "not found", + Code: "model not found", }) - c.Check(params.ErrCode(err), gc.Equals, params.CodeNotFound) + c.Check(params.ErrCode(err), gc.Equals, params.CodeModelNotFound) // Now set it to the right tag, and we should succeed. info.ModelTag = s.State.ModelTag() @@ -158,6 +161,28 @@ c.Assert(result, gc.IsNil) } +func (s *apiclientSuite) TestOpenWithNoCACert(c *gc.C) { + // This is hard to test as we have no way of affecting the system roots, + // so instead we check that the error that we get implies that + // we're using the system roots. + + info := s.APIInfo(c) + info.CACert = "" + + t0 := time.Now() + // Use a long timeout so that we can check that the retry + // logic doesn't retry. + _, err := api.Open(info, api.DialOpts{ + Timeout: 20 * time.Second, + RetryDelay: 2 * time.Second, + }) + c.Assert(err, gc.ErrorMatches, `unable to connect to API: websocket.Dial wss://.*/api: x509: certificate signed by unknown authority`) + + if time.Since(t0) > 5*time.Second { + c.Errorf("looks like API is retrying on connection when there is an X509 error") + } +} + func (s *apiclientSuite) TestOpenWithRedirect(c *gc.C) { redirectToHosts := []string{"0.1.2.3:1234", "0.1.2.4:1235"} redirectToCACert := "fake CA cert" @@ -185,6 +210,120 @@ }) } +func (s *apiclientSuite) TestAPICallNoError(c *gc.C) { + clock := &fakeClock{} + conn := api.NewTestingState(api.TestingStateParams{ + RPCConnection: &fakeRPCConnection{}, + Clock: clock, + }) + + err := conn.APICall("facade", 1, "id", "method", nil, nil) + c.Check(err, jc.ErrorIsNil) + c.Check(clock.waits, gc.HasLen, 0) +} + +func (s *apiclientSuite) TestAPICallError(c *gc.C) { + clock := &fakeClock{} + conn := api.NewTestingState(api.TestingStateParams{ + RPCConnection: &fakeRPCConnection{ + errors: []error{errors.BadRequestf("boom")}, + }, + Clock: clock, + }) + + err := conn.APICall("facade", 1, "id", "method", nil, nil) + c.Check(err.Error(), gc.Equals, "boom") + c.Check(err, jc.Satisfies, errors.IsBadRequest) + c.Check(clock.waits, gc.HasLen, 0) +} + +func (s *apiclientSuite) TestAPICallRetries(c *gc.C) { + clock := &fakeClock{} + conn := api.NewTestingState(api.TestingStateParams{ + RPCConnection: &fakeRPCConnection{ + errors: []error{ + &rpc.RequestError{ + Message: "hmm...", + Code: params.CodeRetry, + }, + }, + }, + Clock: clock, + }) + + err := conn.APICall("facade", 1, "id", "method", nil, nil) + c.Check(err, jc.ErrorIsNil) + c.Check(clock.waits, jc.DeepEquals, []time.Duration{100 * time.Millisecond}) +} + +func (s *apiclientSuite) TestAPICallRetriesLimit(c *gc.C) { + clock := &fakeClock{} + retryError := &rpc.RequestError{Message: "hmm...", Code: params.CodeRetry} + var errors []error + for i := 0; i < 10; i++ { + errors = append(errors, retryError) + } + conn := api.NewTestingState(api.TestingStateParams{ + RPCConnection: &fakeRPCConnection{ + errors: errors, + }, + Clock: clock, + }) + + err := conn.APICall("facade", 1, "id", "method", nil, nil) + c.Check(err, jc.Satisfies, retry.IsDurationExceeded) + c.Check(err, gc.ErrorMatches, `.*hmm... \(retry\)`) + c.Check(clock.waits, jc.DeepEquals, []time.Duration{ + 100 * time.Millisecond, + 200 * time.Millisecond, + 400 * time.Millisecond, + 800 * time.Millisecond, + 1500 * time.Millisecond, + 1500 * time.Millisecond, + 1500 * time.Millisecond, + 1500 * time.Millisecond, + 1500 * time.Millisecond, + }) +} + +type fakeClock struct { + clock.Clock + + now time.Time + waits []time.Duration +} + +func (f *fakeClock) Now() time.Time { + if f.now.IsZero() { + f.now = time.Now() + } + return f.now +} + +func (f *fakeClock) After(d time.Duration) <-chan time.Time { + f.waits = append(f.waits, d) + f.now = f.now.Add(d) + return time.After(0) +} + +type fakeRPCConnection struct { + pos int + errors []error +} + +func (f *fakeRPCConnection) Close() error { + return nil +} + +func (f *fakeRPCConnection) Call(req rpc.Request, params, response interface{}) error { + if f.pos >= len(f.errors) { + return nil + } + err := f.errors[f.pos] + f.pos++ + return err +} + type redirectAPI struct { redirected bool modelUUID string diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/backups/restore.go juju-core-2.0~beta15/src/github.com/juju/juju/api/backups/restore.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/backups/restore.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/backups/restore.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,7 +5,6 @@ import ( "io" - "reflect" "time" "github.com/juju/errors" @@ -21,20 +20,14 @@ var ( // restoreStrategy is the attempt strategy for api server calls re-attempts in case // the server is upgrading. + // + // TODO(katco): 2016-08-09: lp:1611427 restoreStrategy = utils.AttemptStrategy{ Delay: 10 * time.Second, Min: 1, } ) -// isUpgradeInProgressErr returns whether or not the error -// is an "upgrade in progress" error. This is necessary as -// the error type returned from a facade call is rpc.RequestError -// and we cannot use params.IsCodeUpgradeInProgress -func isUpgradeInProgressErr(err error) bool { - return reflect.DeepEqual(errors.Cause(err), &rpc.RequestError{Message: params.CodeUpgradeInProgress, Code: params.CodeUpgradeInProgress}) -} - // ClientConnection type represents a function capable of spawning a new Client connection // it is used to pass around connection factories when necessary. // TODO(perrito666) This is a workaround for lp:1399722 . @@ -67,7 +60,7 @@ if err == nil && remoteError == nil { return nil } - if !isUpgradeInProgressErr(err) || remoteError != nil { + if !params.IsCodeUpgradeInProgress(err) || remoteError != nil { return errors.Annotatef(err, "could not start prepare restore mode, server returned: %v", remoteError) } } @@ -138,7 +131,7 @@ cleanExit = true break } - if remoteError != nil || !isUpgradeInProgressErr(err) { + if !params.IsCodeUpgradeInProgress(err) || remoteError != nil { finishErr := finishRestore(newClient) logger.Errorf("could not clean up after failed restore attempt: %v", finishErr) return errors.Annotatef(err, "cannot perform restore: %v", remoteError) @@ -185,7 +178,7 @@ return nil } - if !isUpgradeInProgressErr(err) || remoteError != nil { + if !params.IsCodeUpgradeInProgress(err) || remoteError != nil { return errors.Annotatef(err, "cannot complete restore: %v", remoteError) } } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/base/caller.go juju-core-2.0~beta15/src/github.com/juju/juju/api/base/caller.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/base/caller.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/base/caller.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,22 +4,13 @@ package base import ( - "fmt" "io" "net/url" - "github.com/juju/errors" "github.com/juju/httprequest" "gopkg.in/juju/names.v2" ) -// OldAgentError is returned when an api call is not supported -// by the Juju agent. -func OldAgentError(operation string, vers string) error { - return errors.NewNotSupported( - nil, fmt.Sprintf("%s not supported. Please upgrade API server to Juju %v or later", operation, vers)) -} - // APICaller is implemented by the client-facing State object. // It defines the lowest level of API calls and is used by // the various API implementations to actually make diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/certpool_test.go juju-core-2.0~beta15/src/github.com/juju/juju/api/certpool_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/certpool_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/certpool_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -30,7 +30,7 @@ s.BaseSuite.SetUpTest(c) s.logs = &certLogs{} loggo.GetLogger("juju.api").SetLogLevel(loggo.TRACE) - loggo.RegisterWriter("api-certs", s.logs, loggo.TRACE) + loggo.RegisterWriter("api-certs", s.logs) } func (*certPoolSuite) TestCreateCertPoolNoCert(c *gc.C) { @@ -137,8 +137,8 @@ messages []string } -func (c *certLogs) Write(level loggo.Level, name, filename string, line int, timestamp time.Time, message string) { - if strings.HasSuffix(filename, "certpool.go") { - c.messages = append(c.messages, fmt.Sprintf("%s %s", level, message)) +func (c *certLogs) Write(entry loggo.Entry) { + if strings.HasSuffix(entry.Filename, "certpool.go") { + c.messages = append(c.messages, fmt.Sprintf("%s %s", entry.Level, entry.Message)) } } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/client.go juju-core-2.0~beta15/src/github.com/juju/juju/api/client.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/client.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/client.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,6 +10,7 @@ "net/http" "net/url" "os" + "strconv" "strings" "github.com/juju/errors" @@ -25,7 +26,6 @@ "github.com/juju/juju/apiserver/params" "github.com/juju/juju/constraints" "github.com/juju/juju/downloader" - "github.com/juju/juju/environs/config" "github.com/juju/juju/network" "github.com/juju/juju/status" "github.com/juju/juju/tools" @@ -236,44 +236,6 @@ return c.st.Close() } -// ModelGet returns all model settings. -func (c *Client) ModelGet() (map[string]interface{}, error) { - result := params.ModelConfigResults{} - err := c.facade.FacadeCall("ModelGet", nil, &result) - values := make(map[string]interface{}) - for name, val := range result.Config { - values[name] = val.Value - } - return values, err -} - -// ModelGetWithMetadata returns all model settings along with extra -// metadata like the source of the setting value. -func (c *Client) ModelGetWithMetadata() (config.ConfigValues, error) { - result := params.ModelConfigResults{} - err := c.facade.FacadeCall("ModelGet", nil, &result) - values := make(config.ConfigValues) - for name, val := range result.Config { - values[name] = config.ConfigValue{ - Value: val.Value, - Source: val.Source, - } - } - return values, err -} - -// ModelSet sets the given key-value pairs in the model. -func (c *Client) ModelSet(config map[string]interface{}) error { - args := params.ModelSet{Config: config} - return c.facade.FacadeCall("ModelSet", args, nil) -} - -// ModelUnset sets the given key-value pairs in the model. -func (c *Client) ModelUnset(keys ...string) error { - args := params.ModelUnset{Keys: keys} - return c.facade.FacadeCall("ModelUnset", args, nil) -} - // SetModelAgentVersion sets the model agent-version setting // to the given value. func (c *Client) SetModelAgentVersion(version version.Number) error { @@ -346,10 +308,15 @@ // UploadCharm sends the content to the API server using an HTTP post. func (c *Client) UploadCharm(curl *charm.URL, content io.ReadSeeker) (*charm.URL, error) { - endpoint := "/charms?series=" + curl.Series + args := url.Values{} + args.Add("series", curl.Series) + args.Add("schema", curl.Schema) + args.Add("revision", strconv.Itoa(curl.Revision)) + apiURI := url.URL{Path: "/charms", RawQuery: args.Encode()} + contentType := "application/zip" var resp params.CharmsResponse - if err := c.httpPost(content, endpoint, contentType, &resp); err != nil { + if err := c.httpPost(content, apiURI.String(), contentType, &resp); err != nil { return nil, errors.Trace(err) } @@ -453,25 +420,20 @@ // OpenCharm streams out the identified charm from the controller via // the API. func (c *Client) OpenCharm(curl *charm.URL) (io.ReadCloser, error) { + query := make(url.Values) + query.Add("url", curl.String()) + query.Add("file", "*") + return c.OpenURI("/charms", query) +} + +// OpenURI performs a GET on a Juju HTTP endpoint returning the +func (c *Client) OpenURI(uri string, query url.Values) (io.ReadCloser, error) { // The returned httpClient sets the base url to /model/ if it can. httpClient, err := c.st.HTTPClient() if err != nil { return nil, errors.Trace(err) } - blob, err := openCharm(httpClient, curl) - if err != nil { - return nil, errors.Trace(err) - } - return blob, nil -} - -// openCharm streams out the identified charm from the controller via -// the API. -func openCharm(httpClient HTTPDoer, curl *charm.URL) (io.ReadCloser, error) { - query := make(url.Values) - query.Add("url", curl.String()) - query.Add("file", "*") - blob, err := openBlob(httpClient, "/charms", query) + blob, err := openBlob(httpClient, uri, query) if err != nil { return nil, errors.Trace(err) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/client_macaroon_test.go juju-core-2.0~beta15/src/github.com/juju/juju/api/client_macaroon_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/client_macaroon_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/client_macaroon_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -51,7 +51,7 @@ fmt.Sprintf("local:quantal/%s-%d", charmArchive.Meta().Name, charmArchive.Revision()), ) savedURL, err := s.client.AddLocalCharm(curl, charmArchive) - c.Assert(err, gc.ErrorMatches, `POST https://.*/model/deadbeef-0bad-400d-8000-4b1d0d06f00d/charms\?series=quantal: cannot get discharge from "https://.*": third party refused discharge: cannot discharge: login denied by discharger`) + c.Assert(err, gc.ErrorMatches, `POST https://.+: cannot get discharge from "https://.*": third party refused discharge: cannot discharge: login denied by discharger`) c.Assert(savedURL, gc.IsNil) } @@ -76,5 +76,5 @@ ) // Upload an archive with its original revision. _, err := s.client.AddLocalCharm(curl, charmArchive) - c.Assert(err, gc.ErrorMatches, `POST https://.*/model/deadbeef-0bad-400d-8000-4b1d0d06f00d/charms\?series=quantal: invalid entity name or password`) + c.Assert(err, gc.ErrorMatches, `POST https://.+: invalid entity name or password`) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/client_test.go juju-core-2.0~beta15/src/github.com/juju/juju/api/client_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/client_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/client_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -168,7 +168,7 @@ ) _, err := client.AddLocalCharm(curl, charmArchive) - c.Assert(err, gc.ErrorMatches, `POST http://.*/model/deadbeef-0bad-400d-8000-4b1d0d06f00d/charms\?series=quantal: the POST method is not allowed`) + c.Assert(err, gc.ErrorMatches, `POST http://.+: the POST method is not allowed`) } func (s *clientSuite) TestMinVersionLocalCharm(c *gc.C) { @@ -234,6 +234,28 @@ } } +func (s *clientSuite) TestOpenURIFound(c *gc.C) { + // Use tools download to test OpenURI + const toolsVersion = "2.0.0-xenial-ppc64" + s.AddToolsToState(c, version.MustParseBinary(toolsVersion)) + + client := s.APIState.Client() + reader, err := client.OpenURI("/tools/"+toolsVersion, nil) + c.Assert(err, jc.ErrorIsNil) + defer reader.Close() + + // The fake tools content will be the version number. + content, err := ioutil.ReadAll(reader) + c.Assert(err, jc.ErrorIsNil) + c.Assert(string(content), gc.Equals, toolsVersion) +} + +func (s *clientSuite) TestOpenURIError(c *gc.C) { + client := s.APIState.Client() + _, err := client.OpenURI("/tools/foobar", nil) + c.Assert(err, gc.ErrorMatches, ".+error parsing version.+") +} + func (s *clientSuite) TestOpenCharmFound(c *gc.C) { client := s.APIState.Client() curl, ch := addLocalCharm(c, client, "dummy") @@ -434,7 +456,7 @@ c.Assert(connectURL.Path, gc.Matches, fmt.Sprintf("/model/%s/path", environ.UUID())) } -func (s *clientSuite) TestOpenUsesEnvironUUIDPaths(c *gc.C) { +func (s *clientSuite) TestOpenUsesModelUUIDPaths(c *gc.C) { info := s.APIInfo(c) // Passing in the correct model UUID should work @@ -450,13 +472,13 @@ apistate, err = api.Open(info, api.DialOpts{}) c.Assert(errors.Cause(err), gc.DeepEquals, &rpc.RequestError{ Message: `unknown model: "dead-beef-123456"`, - Code: "not found", + Code: "model not found", }) - c.Check(err, jc.Satisfies, params.IsCodeNotFound) + c.Check(err, jc.Satisfies, params.IsCodeModelNotFound) c.Assert(apistate, gc.IsNil) } -func (s *clientSuite) TestSetEnvironAgentVersionDuringUpgrade(c *gc.C) { +func (s *clientSuite) TestSetModelAgentVersionDuringUpgrade(c *gc.C) { // This is an integration test which ensure that a test with the // correct error code is seen by the client from the // SetModelAgentVersion call when an upgrade is in progress. @@ -498,45 +520,6 @@ c.Assert(err, gc.Equals, someErr) // Confirms that the correct facade was called } -func (s *clientSuite) TestEnvironmentGet(c *gc.C) { - client := s.APIState.Client() - env, err := client.ModelGet() - c.Assert(err, jc.ErrorIsNil) - // Check a known value, just checking that there is something there. - c.Assert(env["type"], gc.Equals, "dummy") -} - -func (s *clientSuite) TestEnvironmentSet(c *gc.C) { - client := s.APIState.Client() - err := client.ModelSet(map[string]interface{}{ - "some-name": "value", - "other-name": true, - }) - c.Assert(err, jc.ErrorIsNil) - // Check them using ModelGet. - env, err := client.ModelGet() - c.Assert(err, jc.ErrorIsNil) - c.Assert(env["some-name"], gc.Equals, "value") - c.Assert(env["other-name"], gc.Equals, true) -} - -func (s *clientSuite) TestEnvironmentUnset(c *gc.C) { - client := s.APIState.Client() - err := client.ModelSet(map[string]interface{}{ - "some-name": "value", - }) - c.Assert(err, jc.ErrorIsNil) - - // Now unset it and make sure it isn't there. - err = client.ModelUnset("some-name") - c.Assert(err, jc.ErrorIsNil) - - env, err := client.ModelGet() - c.Assert(err, jc.ErrorIsNil) - _, found := env["some-name"] - c.Assert(found, jc.IsFalse) -} - // badReader raises err when Read is called. type badReader struct { err error diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/common/cloudspec/cloudspec.go juju-core-2.0~beta15/src/github.com/juju/juju/api/common/cloudspec/cloudspec.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/common/cloudspec/cloudspec.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/common/cloudspec/cloudspec.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,64 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package cloudspec + +import ( + "github.com/juju/errors" + names "gopkg.in/juju/names.v2" + + "github.com/juju/juju/api/base" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cloud" + "github.com/juju/juju/environs" +) + +// CloudSpecAPI provides common client-side API functions +// to call into apiserver/common/cloudspec.CloudSpec. +type CloudSpecAPI struct { + facade base.FacadeCaller +} + +// NewCloudSpecAPI creates a CloudSpecAPI using the provided +// FacadeCaller. +func NewCloudSpecAPI(facade base.FacadeCaller) *CloudSpecAPI { + return &CloudSpecAPI{facade} +} + +// CloudSpec returns the cloud specification for the model +// with the given tag. +func (api *CloudSpecAPI) CloudSpec(tag names.ModelTag) (environs.CloudSpec, error) { + var results params.CloudSpecResults + args := params.Entities{Entities: []params.Entity{{tag.String()}}} + err := api.facade.FacadeCall("CloudSpec", args, &results) + if err != nil { + return environs.CloudSpec{}, err + } + if n := len(results.Results); n != 1 { + return environs.CloudSpec{}, errors.Errorf("expected 1 result, got %d", n) + } + result := results.Results[0] + if result.Error != nil { + return environs.CloudSpec{}, errors.Annotate(result.Error, "API request failed") + } + var credential *cloud.Credential + if result.Result.Credential != nil { + credentialValue := cloud.NewCredential( + cloud.AuthType(result.Result.Credential.AuthType), + result.Result.Credential.Attributes, + ) + credential = &credentialValue + } + spec := environs.CloudSpec{ + Type: result.Result.Type, + Name: result.Result.Name, + Region: result.Result.Region, + Endpoint: result.Result.Endpoint, + StorageEndpoint: result.Result.StorageEndpoint, + Credential: credential, + } + if err := spec.Validate(); err != nil { + return environs.CloudSpec{}, errors.Annotate(err, "validating CloudSpec") + } + return spec, nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/common/cloudspec/cloudspec_test.go juju-core-2.0~beta15/src/github.com/juju/juju/api/common/cloudspec/cloudspec_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/common/cloudspec/cloudspec_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/common/cloudspec/cloudspec_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,127 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package cloudspec_test + +import ( + "errors" + + "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + apitesting "github.com/juju/juju/api/base/testing" + "github.com/juju/juju/api/common/cloudspec" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cloud" + "github.com/juju/juju/environs" + coretesting "github.com/juju/juju/testing" +) + +var _ = gc.Suite(&CloudSpecSuite{}) + +type CloudSpecSuite struct { + testing.IsolationSuite +} + +func (s *CloudSpecSuite) TestNewCloudSpecAPI(c *gc.C) { + api := cloudspec.NewCloudSpecAPI(nil) + c.Check(api, gc.NotNil) +} + +func (s *CloudSpecSuite) TestCloudSpec(c *gc.C) { + facadeCaller := apitesting.StubFacadeCaller{Stub: &testing.Stub{}} + facadeCaller.FacadeCallFn = func(name string, args, response interface{}) error { + c.Assert(name, gc.Equals, "CloudSpec") + c.Assert(args, jc.DeepEquals, params.Entities{[]params.Entity{ + {coretesting.ModelTag.String()}, + }}) + *(response.(*params.CloudSpecResults)) = params.CloudSpecResults{ + []params.CloudSpecResult{{ + Result: ¶ms.CloudSpec{ + Type: "type", + Name: "name", + Region: "region", + Endpoint: "endpoint", + StorageEndpoint: "storage-endpoint", + Credential: ¶ms.CloudCredential{ + AuthType: "auth-type", + Attributes: map[string]string{"k": "v"}, + }, + }, + }}, + } + return nil + } + api := cloudspec.NewCloudSpecAPI(&facadeCaller) + cloudSpec, err := api.CloudSpec(coretesting.ModelTag) + c.Assert(err, jc.ErrorIsNil) + + credential := cloud.NewCredential( + "auth-type", + map[string]string{"k": "v"}, + ) + c.Assert(cloudSpec, jc.DeepEquals, environs.CloudSpec{ + Type: "type", + Name: "name", + Region: "region", + Endpoint: "endpoint", + StorageEndpoint: "storage-endpoint", + Credential: &credential, + }) +} + +func (s *CloudSpecSuite) TestCloudSpecOverallError(c *gc.C) { + expect := errors.New("bewm") + facadeCaller := apitesting.StubFacadeCaller{Stub: &testing.Stub{}} + facadeCaller.FacadeCallFn = func(name string, args, response interface{}) error { + return expect + } + api := cloudspec.NewCloudSpecAPI(&facadeCaller) + _, err := api.CloudSpec(coretesting.ModelTag) + c.Assert(err, gc.Equals, expect) +} + +func (s *CloudSpecSuite) TestCloudSpecResultCountMismatch(c *gc.C) { + facadeCaller := apitesting.StubFacadeCaller{Stub: &testing.Stub{}} + facadeCaller.FacadeCallFn = func(name string, args, response interface{}) error { + return nil + } + api := cloudspec.NewCloudSpecAPI(&facadeCaller) + _, err := api.CloudSpec(coretesting.ModelTag) + c.Assert(err, gc.ErrorMatches, "expected 1 result, got 0") +} + +func (s *CloudSpecSuite) TestCloudSpecResultError(c *gc.C) { + facadeCaller := apitesting.StubFacadeCaller{Stub: &testing.Stub{}} + facadeCaller.FacadeCallFn = func(name string, args, response interface{}) error { + *(response.(*params.CloudSpecResults)) = params.CloudSpecResults{ + []params.CloudSpecResult{{ + Error: ¶ms.Error{ + Code: params.CodeUnauthorized, + Message: "dang", + }, + }}, + } + return nil + } + api := cloudspec.NewCloudSpecAPI(&facadeCaller) + _, err := api.CloudSpec(coretesting.ModelTag) + c.Assert(err, jc.Satisfies, params.IsCodeUnauthorized) + c.Assert(err, gc.ErrorMatches, "API request failed: dang") +} + +func (s *CloudSpecSuite) TestCloudSpecInvalidCloudSpec(c *gc.C) { + facadeCaller := apitesting.StubFacadeCaller{Stub: &testing.Stub{}} + facadeCaller.FacadeCallFn = func(name string, args, response interface{}) error { + *(response.(*params.CloudSpecResults)) = params.CloudSpecResults{[]params.CloudSpecResult{{ + Result: ¶ms.CloudSpec{ + Type: "", + }, + }}} + return nil + } + api := cloudspec.NewCloudSpecAPI(&facadeCaller) + _, err := api.CloudSpec(coretesting.ModelTag) + c.Assert(err, gc.ErrorMatches, "validating CloudSpec: empty Type not valid") +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/common/cloudspec/package_test.go juju-core-2.0~beta15/src/github.com/juju/juju/api/common/cloudspec/package_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/common/cloudspec/package_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/common/cloudspec/package_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,14 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package cloudspec_test + +import ( + "testing" + + gc "gopkg.in/check.v1" +) + +func TestAll(t *testing.T) { + gc.TestingT(t) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/controller/controller.go juju-core-2.0~beta15/src/github.com/juju/juju/api/controller/controller.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/controller/controller.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/controller/controller.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,6 +10,7 @@ "github.com/juju/juju/api" "github.com/juju/juju/api/base" "github.com/juju/juju/api/common" + "github.com/juju/juju/api/common/cloudspec" "github.com/juju/juju/apiserver/params" ) @@ -19,6 +20,7 @@ base.ClientFacade facade base.FacadeCaller *common.ControllerConfigAPI + *cloudspec.CloudSpecAPI } // NewClient creates a new `Client` based on an existing authenticated API @@ -29,6 +31,7 @@ ClientFacade: frontend, facade: backend, ControllerConfigAPI: common.NewControllerConfig(backend), + CloudSpecAPI: cloudspec.NewCloudSpecAPI(backend), } } @@ -208,5 +211,5 @@ if result.Error != nil { return "", errors.Trace(result.Error) } - return result.Id, nil + return result.MigrationId, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/controller/controller_test.go juju-core-2.0~beta15/src/github.com/juju/juju/api/controller/controller_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/controller/controller_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/controller/controller_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -180,7 +180,7 @@ st := s.Factory.MakeModel(c, nil) defer st.Close() - _, err := st.GetModelMigration() + _, err := st.LatestModelMigration() c.Assert(errors.IsNotFound(err), jc.IsTrue) spec := controller.ModelMigrationSpec{ @@ -199,7 +199,7 @@ c.Check(id, gc.Equals, expectedId) // Check database. - mig, err := st.GetModelMigration() + mig, err := st.LatestModelMigration() c.Assert(err, jc.ErrorIsNil) c.Check(mig.Id(), gc.Equals, expectedId) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/api/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/export_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,6 +7,7 @@ "github.com/juju/errors" "github.com/juju/juju/api/base" "github.com/juju/juju/network" + "github.com/juju/utils/clock" ) var ( @@ -20,6 +21,9 @@ ConnectWebsocket = connectWebsocket ) +// RPCConnection defines the methods that are called on the rpc.Conn instance. +type RPCConnection rpcConnection + // SetServerAddress allows changing the URL to the internal API server // that AddLocalCharm uses in order to test NotImplementedError. func SetServerAddress(c *Client, scheme, addr string) { @@ -41,13 +45,17 @@ FacadeVersions map[string][]int ServerScheme string ServerRoot string + RPCConnection RPCConnection + Clock clock.Clock } // NewTestingState creates an api.State object that can be used for testing. It // isn't backed onto an actual API server, so actual RPC methods can't be -// called on it. But it can be used for testing general behavior. +// called on it. But it can be used for testing general behaviour. func NewTestingState(params TestingStateParams) Connection { st := &state{ + client: params.RPCConnection, + clock: params.Clock, addr: params.Address, modelTag: params.ModelTag, hostPorts: params.APIHostPorts, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/facadeversions.go juju-core-2.0~beta15/src/github.com/juju/juju/api/facadeversions.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/facadeversions.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/facadeversions.go 2016-08-16 08:56:25.000000000 +0000 @@ -56,6 +56,7 @@ "MigrationMinion": 1, "MigrationStatusWatcher": 1, "MigrationTarget": 1, + "ModelConfig": 1, "ModelManager": 2, "NotifyWatcher": 1, "Payloads": 1, @@ -73,8 +74,8 @@ "Spaces": 2, "SSHClient": 1, "StatusHistory": 2, - "Storage": 2, - "StorageProvisioner": 2, + "Storage": 3, + "StorageProvisioner": 3, "StringsWatcher": 1, "Subnets": 2, "Undertaker": 1, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/firewaller/firewaller.go juju-core-2.0~beta15/src/github.com/juju/juju/api/firewaller/firewaller.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/firewaller/firewaller.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/firewaller/firewaller.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,6 +9,7 @@ "github.com/juju/juju/api/base" "github.com/juju/juju/api/common" + "github.com/juju/juju/api/common/cloudspec" apiwatcher "github.com/juju/juju/api/watcher" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/watcher" @@ -20,6 +21,7 @@ type State struct { facade base.FacadeCaller *common.ModelWatcher + *cloudspec.CloudSpecAPI } // NewState creates a new client-side Firewaller API facade. @@ -28,6 +30,7 @@ return &State{ facade: facadeCaller, ModelWatcher: common.NewModelWatcher(facadeCaller), + CloudSpecAPI: cloudspec.NewCloudSpecAPI(facadeCaller), } } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/firewaller/firewaller_test.go juju-core-2.0~beta15/src/github.com/juju/juju/api/firewaller/firewaller_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/firewaller/firewaller_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/firewaller/firewaller_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -69,6 +69,6 @@ } // Create the firewaller API facade. - s.firewaller = s.st.Firewaller() + s.firewaller = firewaller.NewState(s.st) c.Assert(s.firewaller, gc.NotNil) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/highavailability/client.go juju-core-2.0~beta15/src/github.com/juju/juju/api/highavailability/client.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/highavailability/client.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/highavailability/client.go 2016-08-16 08:56:25.000000000 +0000 @@ -36,7 +36,7 @@ // EnableHA ensures the availability of Juju controllers. func (c *Client) EnableHA( - numControllers int, cons constraints.Value, series string, placement []string, + numControllers int, cons constraints.Value, placement []string, ) (params.ControllersChanges, error) { var results params.ControllersChangeResults @@ -45,7 +45,6 @@ ModelTag: c.modelTag.String(), NumControllers: numControllers, Constraints: cons, - Series: series, Placement: placement, }}} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/highavailability/client_test.go juju-core-2.0~beta15/src/github.com/juju/juju/api/highavailability/client_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/highavailability/client_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/highavailability/client_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -56,7 +56,7 @@ emptyCons := constraints.Value{} client := highavailability.NewClient(s.APIState) - result, err := client.EnableHA(3, emptyCons, "", nil) + result, err := client.EnableHA(3, emptyCons, nil) c.Assert(err, jc.ErrorIsNil) c.Assert(result.Maintained, gc.DeepEquals, []string{"machine-0"}) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/http.go juju-core-2.0~beta15/src/github.com/juju/juju/api/http.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/http.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/http.go 2016-08-16 08:56:25.000000000 +0000 @@ -77,6 +77,12 @@ // indicates that we're using macaroon authentication. req.SetBasicAuth(doer.st.tag, doer.st.password) } + + // Set the machine nonce if it was provided. + if doer.st.nonce != "" { + req.Header.Set(params.MachineNonceHeader, doer.st.nonce) + } + // Add any explicitly-specified macaroons. for _, ms := range doer.st.macaroons { encoded, err := encodeMacaroonSlice(ms) @@ -202,7 +208,6 @@ return nil, errors.Trace(err) } apiURL.RawQuery = args.Encode() - req, err := http.NewRequest("GET", apiURL.String(), nil) if err != nil { return nil, errors.Annotate(err, "cannot create HTTP request") diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/http_test.go juju-core-2.0~beta15/src/github.com/juju/juju/api/http_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/http_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/http_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -13,8 +13,11 @@ jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" + "github.com/juju/juju/api" "github.com/juju/juju/apiserver/params" jujutesting "github.com/juju/juju/juju/testing" + "github.com/juju/juju/state" + "github.com/juju/juju/testing/factory" ) type httpSuite struct { @@ -164,6 +167,54 @@ } } +func (s *httpSuite) TestControllerMachineAuthForHostedModel(c *gc.C) { + // Create a controller machine & hosted model. + const nonce = "gary" + m, password := s.Factory.MakeMachineReturningPassword(c, &factory.MachineParams{ + Jobs: []state.MachineJob{state.JobManageModel}, + Nonce: nonce, + }) + hostedState := s.Factory.MakeModel(c, nil) + defer hostedState.Close() + + // Connect to the hosted model using the credentials of the + // controller machine. + apiInfo := s.APIInfo(c) + apiInfo.Tag = m.Tag() + apiInfo.Password = password + apiInfo.ModelTag = hostedState.ModelTag() + apiInfo.Nonce = nonce + conn, err := api.Open(apiInfo, api.DialOpts{}) + c.Assert(err, jc.ErrorIsNil) + httpClient, err := conn.HTTPClient() + c.Assert(err, jc.ErrorIsNil) + + // Test with a dummy HTTP server returns the auth related headers used. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + username, password, ok := req.BasicAuth() + if ok { + httprequest.WriteJSON(w, http.StatusOK, map[string]string{ + "username": username, + "password": password, + "nonce": req.Header.Get(params.MachineNonceHeader), + }) + } else { + httprequest.WriteJSON(w, http.StatusUnauthorized, params.Error{ + Message: "no auth header", + }) + } + })) + defer srv.Close() + httpClient.BaseURL = srv.URL + var out map[string]string + c.Assert(httpClient.Get("/", &out), jc.ErrorIsNil) + c.Assert(out, gc.DeepEquals, map[string]string{ + "username": m.Tag().String(), + "password": password, + "nonce": nonce, + }) +} + // Note: the fact that the code works against the actual API server is // well tested by some of the other API tests. // This suite focuses on less reachable paths by changing diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/interface.go juju-core-2.0~beta15/src/github.com/juju/juju/api/interface.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/interface.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/interface.go 2016-08-16 08:56:25.000000000 +0000 @@ -16,16 +16,13 @@ "github.com/juju/juju/api/charmrevisionupdater" "github.com/juju/juju/api/cleaner" "github.com/juju/juju/api/discoverspaces" - "github.com/juju/juju/api/firewaller" "github.com/juju/juju/api/imagemetadata" "github.com/juju/juju/api/instancepoller" - "github.com/juju/juju/api/provisioner" "github.com/juju/juju/api/reboot" "github.com/juju/juju/api/unitassigner" "github.com/juju/juju/api/uniter" "github.com/juju/juju/api/upgrader" "github.com/juju/juju/network" - "github.com/juju/juju/rpc" "github.com/juju/utils/set" ) @@ -40,6 +37,8 @@ // CACert holds the CA certificate that will be used // to validate the controller's certificate, in PEM format. + // If this is empty, the standard system root certificates + // will be used. CACert string // ModelTag holds the model tag for the model we are @@ -92,9 +91,6 @@ if _, err := network.ParseHostPorts(info.Addrs...); err != nil { return errors.NotValidf("host addresses: %v", err) } - if info.CACert == "" { - return errors.NotValidf("missing CA certificate") - } if info.SkipLogin { if info.Tag != nil { return errors.NotValidf("specifying Tag and SkipLogin") @@ -188,10 +184,6 @@ // either expose Ping() or Broken() but not both. Ping() error - // RPCClient is apparently exported for testing purposes only, but this - // seems to indicate *some* sort of layering confusion. - RPCClient() *rpc.Conn - // I think this is actually dead code. It's tested, at least, so I'm // keeping it for now, but it's not apparently used anywhere else. AllFacadeVersions() map[string][]int @@ -210,9 +202,7 @@ // will be easy to remove, but until we're using them via manifolds it's // prohibitively ugly to do so. Client() *Client - Provisioner() *provisioner.State Uniter() (*uniter.State, error) - Firewaller() *firewaller.State Upgrader() *upgrader.State Reboot() (reboot.State, error) DiscoverSpaces() *discoverspaces.API diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/migrationmaster/client.go juju-core-2.0~beta15/src/github.com/juju/juju/api/migrationmaster/client.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/migrationmaster/client.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/migrationmaster/client.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,56 +5,36 @@ import ( "github.com/juju/errors" + "github.com/juju/version" "gopkg.in/juju/names.v2" "github.com/juju/juju/api/base" - apiwatcher "github.com/juju/juju/api/watcher" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/core/migration" "github.com/juju/juju/watcher" ) -// Client describes the client side API for the MigrationMaster facade -// (used by the migration master worker). -type Client interface { - // Watch returns a watcher which reports when a migration is - // active for the model associated with the API connection. - Watch() (watcher.NotifyWatcher, error) - - // GetMigrationStatus returns the details and progress of the - // latest model migration. - GetMigrationStatus() (MigrationStatus, error) - - // SetPhase updates the phase of the currently active model - // migration. - SetPhase(migration.Phase) error - - // Export returns a serialized representation of the model - // associated with the API connection. - Export() ([]byte, error) -} - -// MigrationStatus returns the details for a migration as needed by -// the migration master worker. -type MigrationStatus struct { - ModelUUID string - Attempt int - Phase migration.Phase - TargetInfo migration.TargetInfo -} +// NewWatcherFunc exists to let us unit test Facade without patching. +type NewWatcherFunc func(base.APICaller, params.NotifyWatchResult) watcher.NotifyWatcher // NewClient returns a new Client based on an existing API connection. -func NewClient(caller base.APICaller) Client { - return &client{base.NewFacadeCaller(caller, "MigrationMaster")} +func NewClient(caller base.APICaller, newWatcher NewWatcherFunc) *Client { + return &Client{ + caller: base.NewFacadeCaller(caller, "MigrationMaster"), + newWatcher: newWatcher, + } } -// client implements Client. -type client struct { - caller base.FacadeCaller +// Client describes the client side API for the MigrationMaster facade +// (used by the migrationmaster worker). +type Client struct { + caller base.FacadeCaller + newWatcher NewWatcherFunc } -// Watch implements Client. -func (c *client) Watch() (watcher.NotifyWatcher, error) { +// Watch returns a watcher which reports when a migration is active +// for the model associated with the API connection. +func (c *Client) Watch() (watcher.NotifyWatcher, error) { var result params.NotifyWatchResult err := c.caller.FacadeCall("Watch", nil, &result) if err != nil { @@ -63,14 +43,14 @@ if result.Error != nil { return nil, result.Error } - w := apiwatcher.NewNotifyWatcher(c.caller.RawAPICaller(), result) - return w, nil + return c.newWatcher(c.caller.RawAPICaller(), result), nil } -// GetMigrationStatus implements Client. -func (c *client) GetMigrationStatus() (MigrationStatus, error) { - var empty MigrationStatus - var status params.FullMigrationStatus +// GetMigrationStatus returns the details and progress of the latest +// model migration. +func (c *Client) GetMigrationStatus() (migration.MigrationStatus, error) { + var empty migration.MigrationStatus + var status params.MasterMigrationStatus err := c.caller.FacadeCall("GetMigrationStatus", nil, &status) if err != nil { return empty, errors.Trace(err) @@ -97,10 +77,11 @@ return empty, errors.Annotatef(err, "unable to parse auth tag") } - return MigrationStatus{ - ModelUUID: modelTag.Id(), - Attempt: status.Attempt, - Phase: phase, + return migration.MigrationStatus{ + MigrationId: status.MigrationId, + ModelUUID: modelTag.Id(), + Phase: phase, + PhaseChangedTime: status.PhaseChangedTime, TargetInfo: migration.TargetInfo{ ControllerTag: controllerTag, Addrs: target.Addrs, @@ -111,20 +92,122 @@ }, nil } -// SetPhase implements Client. -func (c *client) SetPhase(phase migration.Phase) error { +// SetPhase updates the phase of the currently active model migration. +func (c *Client) SetPhase(phase migration.Phase) error { args := params.SetMigrationPhaseArgs{ Phase: phase.String(), } return c.caller.FacadeCall("SetPhase", args, nil) } -// Export implements Client. -func (c *client) Export() ([]byte, error) { +// SetStatusMessage sets a human readable message regarding the +// progress of a migration. +func (c *Client) SetStatusMessage(message string) error { + args := params.SetMigrationStatusMessageArgs{ + Message: message, + } + return c.caller.FacadeCall("SetStatusMessage", args, nil) +} + +// Export returns a serialized representation of the model associated +// with the API connection. The charms used by the model are also +// returned. +func (c *Client) Export() (migration.SerializedModel, error) { var serialized params.SerializedModel err := c.caller.FacadeCall("Export", nil, &serialized) if err != nil { - return nil, err + return migration.SerializedModel{}, err + } + + // Convert tools info to output map. + tools := make(map[version.Binary]string) + for _, toolsInfo := range serialized.Tools { + v, err := version.ParseBinary(toolsInfo.Version) + if err != nil { + return migration.SerializedModel{}, errors.Annotate(err, "error parsing tools version") + } + tools[v] = toolsInfo.URI + } + + return migration.SerializedModel{ + Bytes: serialized.Bytes, + Charms: serialized.Charms, + Tools: tools, + }, nil +} + +// Reap removes the documents for the model associated with the API +// connection. +func (c *Client) Reap() error { + return c.caller.FacadeCall("Reap", nil, nil) +} + +// WatchMinionReports returns a watcher which reports when a migration +// minion has made a report for the current migration phase. +func (c *Client) WatchMinionReports() (watcher.NotifyWatcher, error) { + var result params.NotifyWatchResult + err := c.caller.FacadeCall("WatchMinionReports", nil, &result) + if err != nil { + return nil, errors.Trace(err) + } + if result.Error != nil { + return nil, result.Error + } + return c.newWatcher(c.caller.RawAPICaller(), result), nil +} + +// GetMinionReports returns details of the reports made by migration +// minions to the controller for the current migration phase. +func (c *Client) GetMinionReports() (migration.MinionReports, error) { + var in params.MinionReports + var out migration.MinionReports + + err := c.caller.FacadeCall("GetMinionReports", nil, &in) + if err != nil { + return out, errors.Trace(err) + } + + out.MigrationId = in.MigrationId + + phase, ok := migration.ParsePhase(in.Phase) + if !ok { + return out, errors.Errorf("invalid phase: %q", in.Phase) + } + out.Phase = phase + + out.SuccessCount = in.SuccessCount + out.UnknownCount = in.UnknownCount + + out.SomeUnknownMachines, out.SomeUnknownUnits, err = groupTagIds(in.UnknownSample) + if err != nil { + return out, errors.Annotate(err, "processing unknown agents") + } + + out.FailedMachines, out.FailedUnits, err = groupTagIds(in.Failed) + if err != nil { + return out, errors.Annotate(err, "processing failed agents") + } + + return out, nil +} + +func groupTagIds(tagStrs []string) ([]string, []string, error) { + var machines []string + var units []string + + for i := 0; i < len(tagStrs); i++ { + tag, err := names.ParseTag(tagStrs[i]) + if err != nil { + return nil, nil, errors.Trace(err) + } + switch t := tag.(type) { + case names.MachineTag: + machines = append(machines, t.Id()) + case names.UnitTag: + units = append(units, t.Id()) + default: + return nil, nil, errors.Errorf("unsupported tag: %q", tag) + } } - return serialized.Bytes, nil + return machines, units, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/migrationmaster/client_test.go juju-core-2.0~beta15/src/github.com/juju/juju/api/migrationmaster/client_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/migrationmaster/client_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/migrationmaster/client_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,15 +10,16 @@ jujutesting "github.com/juju/testing" jc "github.com/juju/testing/checkers" "github.com/juju/utils" + "github.com/juju/version" gc "gopkg.in/check.v1" "gopkg.in/juju/names.v2" + "github.com/juju/juju/api/base" apitesting "github.com/juju/juju/api/base/testing" "github.com/juju/juju/api/migrationmaster" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/core/migration" - coretesting "github.com/juju/juju/testing" - "github.com/juju/juju/worker" + "github.com/juju/juju/watcher" ) type ClientSuite struct { @@ -31,56 +32,30 @@ var stub jujutesting.Stub apiCaller := apitesting.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error { stub.AddCall(objType+"."+request, id, arg) - switch request { - case "Watch": - *(result.(*params.NotifyWatchResult)) = params.NotifyWatchResult{ - NotifyWatcherId: "abc", - } - case "Next": - // The full success case is tested in api/watcher. - return errors.New("boom") - case "Stop": + *(result.(*params.NotifyWatchResult)) = params.NotifyWatchResult{ + NotifyWatcherId: "123", } return nil }) + expectWatch := &struct{ watcher.NotifyWatcher }{} + newWatcher := func(caller base.APICaller, result params.NotifyWatchResult) watcher.NotifyWatcher { + c.Check(caller, gc.NotNil) + c.Check(result, jc.DeepEquals, params.NotifyWatchResult{NotifyWatcherId: "123"}) + return expectWatch + } + client := migrationmaster.NewClient(apiCaller, newWatcher) - client := migrationmaster.NewClient(apiCaller) w, err := client.Watch() - c.Assert(err, jc.ErrorIsNil) - defer worker.Stop(w) - - errC := make(chan error) - go func() { - errC <- w.Wait() - }() - - select { - case err := <-errC: - c.Assert(err, gc.ErrorMatches, "boom") - expectedCalls := []jujutesting.StubCall{ - {"MigrationMaster.Watch", []interface{}{"", nil}}, - {"NotifyWatcher.Next", []interface{}{"abc", nil}}, - {"NotifyWatcher.Stop", []interface{}{"abc", nil}}, - } - // The Stop API call happens in a separate goroutine which - // might execute after the worker has exited so wait for the - // expected calls to arrive. - for a := coretesting.LongAttempt.Start(); a.Next(); { - if len(stub.Calls()) >= len(expectedCalls) { - return - } - } - stub.CheckCalls(c, expectedCalls) - case <-time.After(coretesting.LongWait): - c.Fatal("timed out waiting for watcher to die") - } + c.Check(err, jc.ErrorIsNil) + c.Check(w, gc.Equals, expectWatch) + stub.CheckCalls(c, []jujutesting.StubCall{{"MigrationMaster.Watch", []interface{}{"", nil}}}) } -func (s *ClientSuite) TestWatchErr(c *gc.C) { +func (s *ClientSuite) TestWatchCallError(c *gc.C) { apiCaller := apitesting.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error { return errors.New("boom") }) - client := migrationmaster.NewClient(apiCaller) + client := migrationmaster.NewClient(apiCaller, nil) _, err := client.Watch() c.Assert(err, gc.ErrorMatches, "boom") } @@ -88,9 +63,10 @@ func (s *ClientSuite) TestGetMigrationStatus(c *gc.C) { modelUUID := utils.MustNewUUID().String() controllerUUID := utils.MustNewUUID().String() + timestamp := time.Date(2016, 6, 22, 16, 42, 44, 0, time.UTC) apiCaller := apitesting.APICallerFunc(func(_ string, _ int, _, _ string, _, result interface{}) error { - out := result.(*params.FullMigrationStatus) - *out = params.FullMigrationStatus{ + out := result.(*params.MasterMigrationStatus) + *out = params.MasterMigrationStatus{ Spec: params.ModelMigrationSpec{ ModelTag: names.NewModelTag(modelUUID).String(), TargetInfo: params.ModelMigrationTargetInfo{ @@ -101,19 +77,21 @@ Password: "secret", }, }, - Attempt: 3, - Phase: "READONLY", + MigrationId: "id", + Phase: "PRECHECK", + PhaseChangedTime: timestamp, } return nil }) - client := migrationmaster.NewClient(apiCaller) + client := migrationmaster.NewClient(apiCaller, nil) status, err := client.GetMigrationStatus() c.Assert(err, jc.ErrorIsNil) - c.Assert(status, gc.DeepEquals, migrationmaster.MigrationStatus{ - ModelUUID: modelUUID, - Attempt: 3, - Phase: migration.READONLY, + c.Assert(status, gc.DeepEquals, migration.MigrationStatus{ + MigrationId: "id", + ModelUUID: modelUUID, + Phase: migration.PRECHECK, + PhaseChangedTime: timestamp, TargetInfo: migration.TargetInfo{ ControllerTag: names.NewModelTag(controllerUUID), Addrs: []string{"2.2.2.2:2"}, @@ -130,7 +108,7 @@ stub.AddCall(objType+"."+request, id, arg) return nil }) - client := migrationmaster.NewClient(apiCaller) + client := migrationmaster.NewClient(apiCaller, nil) err := client.SetPhase(migration.QUIESCE) c.Assert(err, jc.ErrorIsNil) expectedArg := params.SetMigrationPhaseArgs{Phase: "QUIESCE"} @@ -143,33 +121,217 @@ apiCaller := apitesting.APICallerFunc(func(string, int, string, string, interface{}, interface{}) error { return errors.New("boom") }) - client := migrationmaster.NewClient(apiCaller) + client := migrationmaster.NewClient(apiCaller, nil) err := client.SetPhase(migration.QUIESCE) c.Assert(err, gc.ErrorMatches, "boom") } +func (s *ClientSuite) TestSetStatusMessage(c *gc.C) { + var stub jujutesting.Stub + apiCaller := apitesting.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error { + stub.AddCall(objType+"."+request, id, arg) + return nil + }) + client := migrationmaster.NewClient(apiCaller, nil) + err := client.SetStatusMessage("foo") + c.Assert(err, jc.ErrorIsNil) + expectedArg := params.SetMigrationStatusMessageArgs{Message: "foo"} + stub.CheckCalls(c, []jujutesting.StubCall{ + {"MigrationMaster.SetStatusMessage", []interface{}{"", expectedArg}}, + }) +} + +func (s *ClientSuite) TestSetStatusMessageError(c *gc.C) { + apiCaller := apitesting.APICallerFunc(func(string, int, string, string, interface{}, interface{}) error { + return errors.New("boom") + }) + client := migrationmaster.NewClient(apiCaller, nil) + err := client.SetStatusMessage("foo") + c.Assert(err, gc.ErrorMatches, "boom") +} + func (s *ClientSuite) TestExport(c *gc.C) { var stub jujutesting.Stub apiCaller := apitesting.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error { stub.AddCall(objType+"."+request, id, arg) out := result.(*params.SerializedModel) - *out = params.SerializedModel{Bytes: []byte("foo")} + *out = params.SerializedModel{ + Bytes: []byte("foo"), + Charms: []string{"cs:foo-1"}, + Tools: []params.SerializedModelTools{{ + Version: "2.0.0-trusty-amd64", + URI: "/tools/0", + }}, + } return nil }) - client := migrationmaster.NewClient(apiCaller) - bytes, err := client.Export() + client := migrationmaster.NewClient(apiCaller, nil) + out, err := client.Export() c.Assert(err, jc.ErrorIsNil) stub.CheckCalls(c, []jujutesting.StubCall{ {"MigrationMaster.Export", []interface{}{"", nil}}, }) - c.Assert(string(bytes), gc.Equals, "foo") + c.Assert(out, gc.DeepEquals, migration.SerializedModel{ + Bytes: []byte("foo"), + Charms: []string{"cs:foo-1"}, + Tools: map[version.Binary]string{ + version.MustParseBinary("2.0.0-trusty-amd64"): "/tools/0", + }, + }) } func (s *ClientSuite) TestExportError(c *gc.C) { apiCaller := apitesting.APICallerFunc(func(string, int, string, string, interface{}, interface{}) error { return errors.New("blam") }) - client := migrationmaster.NewClient(apiCaller) + client := migrationmaster.NewClient(apiCaller, nil) _, err := client.Export() c.Assert(err, gc.ErrorMatches, "blam") } + +func (s *ClientSuite) TestReap(c *gc.C) { + var stub jujutesting.Stub + apiCaller := apitesting.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error { + stub.AddCall(objType+"."+request, id, arg) + return nil + }) + client := migrationmaster.NewClient(apiCaller, nil) + err := client.Reap() + c.Check(err, jc.ErrorIsNil) + stub.CheckCalls(c, []jujutesting.StubCall{ + {"MigrationMaster.Reap", []interface{}{"", nil}}, + }) +} + +func (s *ClientSuite) TestReapError(c *gc.C) { + apiCaller := apitesting.APICallerFunc(func(string, int, string, string, interface{}, interface{}) error { + return errors.New("blam") + }) + client := migrationmaster.NewClient(apiCaller, nil) + err := client.Reap() + c.Assert(err, gc.ErrorMatches, "blam") +} + +func (s *ClientSuite) TestWatchMinionReports(c *gc.C) { + var stub jujutesting.Stub + apiCaller := apitesting.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error { + stub.AddCall(objType+"."+request, id, arg) + *(result.(*params.NotifyWatchResult)) = params.NotifyWatchResult{ + NotifyWatcherId: "123", + } + return nil + }) + + expectWatch := &struct{ watcher.NotifyWatcher }{} + newWatcher := func(caller base.APICaller, result params.NotifyWatchResult) watcher.NotifyWatcher { + c.Check(caller, gc.NotNil) + c.Check(result, jc.DeepEquals, params.NotifyWatchResult{NotifyWatcherId: "123"}) + return expectWatch + } + client := migrationmaster.NewClient(apiCaller, newWatcher) + + w, err := client.WatchMinionReports() + c.Check(err, jc.ErrorIsNil) + c.Check(w, gc.Equals, expectWatch) + stub.CheckCalls(c, []jujutesting.StubCall{{"MigrationMaster.WatchMinionReports", []interface{}{"", nil}}}) +} + +func (s *ClientSuite) TestWatchMinionReportsError(c *gc.C) { + apiCaller := apitesting.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error { + return errors.New("boom") + }) + client := migrationmaster.NewClient(apiCaller, nil) + _, err := client.WatchMinionReports() + c.Assert(err, gc.ErrorMatches, "boom") +} + +func (s *ClientSuite) TestGetMinionReports(c *gc.C) { + var stub jujutesting.Stub + apiCaller := apitesting.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error { + stub.AddCall(objType+"."+request, id, arg) + out := result.(*params.MinionReports) + *out = params.MinionReports{ + MigrationId: "id", + Phase: "PRECHECK", + SuccessCount: 4, + UnknownCount: 3, + UnknownSample: []string{ + names.NewMachineTag("3").String(), + names.NewMachineTag("4").String(), + names.NewUnitTag("foo/0").String(), + }, + Failed: []string{ + names.NewMachineTag("5").String(), + names.NewUnitTag("foo/1").String(), + names.NewUnitTag("foo/2").String(), + }, + } + return nil + }) + client := migrationmaster.NewClient(apiCaller, nil) + out, err := client.GetMinionReports() + c.Assert(err, jc.ErrorIsNil) + stub.CheckCalls(c, []jujutesting.StubCall{ + {"MigrationMaster.GetMinionReports", []interface{}{"", nil}}, + }) + c.Assert(out, gc.DeepEquals, migration.MinionReports{ + MigrationId: "id", + Phase: migration.PRECHECK, + SuccessCount: 4, + UnknownCount: 3, + SomeUnknownMachines: []string{"3", "4"}, + SomeUnknownUnits: []string{"foo/0"}, + FailedMachines: []string{"5"}, + FailedUnits: []string{"foo/1", "foo/2"}, + }) +} + +func (s *ClientSuite) TestGetMinionReportsFailedCall(c *gc.C) { + apiCaller := apitesting.APICallerFunc(func(string, int, string, string, interface{}, interface{}) error { + return errors.New("blam") + }) + client := migrationmaster.NewClient(apiCaller, nil) + _, err := client.GetMinionReports() + c.Assert(err, gc.ErrorMatches, "blam") +} + +func (s *ClientSuite) TestGetMinionReportsInvalidPhase(c *gc.C) { + apiCaller := apitesting.APICallerFunc(func(_ string, _ int, _ string, _ string, _ interface{}, result interface{}) error { + out := result.(*params.MinionReports) + *out = params.MinionReports{ + Phase: "BLARGH", + } + return nil + }) + client := migrationmaster.NewClient(apiCaller, nil) + _, err := client.GetMinionReports() + c.Assert(err, gc.ErrorMatches, `invalid phase: "BLARGH"`) +} + +func (s *ClientSuite) TestGetMinionReportsBadUnknownTag(c *gc.C) { + apiCaller := apitesting.APICallerFunc(func(_ string, _ int, _ string, _ string, _ interface{}, result interface{}) error { + out := result.(*params.MinionReports) + *out = params.MinionReports{ + Phase: "PRECHECK", + UnknownSample: []string{"carl"}, + } + return nil + }) + client := migrationmaster.NewClient(apiCaller, nil) + _, err := client.GetMinionReports() + c.Assert(err, gc.ErrorMatches, `processing unknown agents: "carl" is not a valid tag`) +} + +func (s *ClientSuite) TestGetMinionReportsBadFailedTag(c *gc.C) { + apiCaller := apitesting.APICallerFunc(func(_ string, _ int, _ string, _ string, _ interface{}, result interface{}) error { + out := result.(*params.MinionReports) + *out = params.MinionReports{ + Phase: "PRECHECK", + Failed: []string{"dave"}, + } + return nil + }) + client := migrationmaster.NewClient(apiCaller, nil) + _, err := client.GetMinionReports() + c.Assert(err, gc.ErrorMatches, `processing failed agents: "dave" is not a valid tag`) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/migrationminion/client.go juju-core-2.0~beta15/src/github.com/juju/juju/api/migrationminion/client.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/migrationminion/client.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/migrationminion/client.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,30 +9,22 @@ "github.com/juju/juju/api/base" apiwatcher "github.com/juju/juju/api/watcher" "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/core/migration" "github.com/juju/juju/watcher" ) -// Client describes the client side API for the MigrationMinion facade -// (used by the migration minion worker). -type Client interface { - // Watch returns a watcher which reports when the status changes - // for the migration for the model associated with the API - // connection. - Watch() (watcher.MigrationStatusWatcher, error) -} - // NewClient returns a new Client based on an existing API connection. -func NewClient(caller base.APICaller) Client { - return &client{base.NewFacadeCaller(caller, "MigrationMinion")} +func NewClient(caller base.APICaller) *Client { + return &Client{base.NewFacadeCaller(caller, "MigrationMinion")} } -// client implements Client. -type client struct { +type Client struct { caller base.FacadeCaller } -// Watch implements Client. -func (c *client) Watch() (watcher.MigrationStatusWatcher, error) { +// Watch returns a watcher which reports when the status changes for +// the migration for the model associated with the API connection. +func (c *Client) Watch() (watcher.MigrationStatusWatcher, error) { var result params.NotifyWatchResult err := c.caller.FacadeCall("Watch", nil, &result) if err != nil { @@ -44,3 +36,15 @@ w := apiwatcher.NewMigrationStatusWatcher(c.caller.RawAPICaller(), result.NotifyWatcherId) return w, nil } + +// Report allows a migration minion to report if it successfully +// completed its activities for a given migration phase. +func (c *Client) Report(migrationId string, phase migration.Phase, success bool) error { + args := params.MinionReport{ + MigrationId: migrationId, + Phase: phase.String(), + Success: success, + } + err := c.caller.FacadeCall("Report", args, nil) + return errors.Trace(err) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/migrationminion/client_test.go juju-core-2.0~beta15/src/github.com/juju/juju/api/migrationminion/client_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/migrationminion/client_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/migrationminion/client_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -14,6 +14,7 @@ apitesting "github.com/juju/juju/api/base/testing" "github.com/juju/juju/api/migrationminion" "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/core/migration" coretesting "github.com/juju/juju/testing" "github.com/juju/juju/worker" ) @@ -81,3 +82,33 @@ _, err := client.Watch() c.Assert(err, gc.ErrorMatches, "boom") } + +func (s *ClientSuite) TestReport(c *gc.C) { + var stub jujutesting.Stub + apiCaller := apitesting.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error { + stub.AddCall(objType+"."+request, arg) + return nil + }) + + client := migrationminion.NewClient(apiCaller) + err := client.Report("id", migration.IMPORT, true) + c.Assert(err, jc.ErrorIsNil) + + stub.CheckCalls(c, []jujutesting.StubCall{ + {"MigrationMinion.Report", []interface{}{params.MinionReport{ + MigrationId: "id", + Phase: "IMPORT", + Success: true, + }}}, + }) +} + +func (s *ClientSuite) TestReportError(c *gc.C) { + apiCaller := apitesting.APICallerFunc(func(string, int, string, string, interface{}, interface{}) error { + return errors.New("boom") + }) + + client := migrationminion.NewClient(apiCaller) + err := client.Report("id", migration.IMPORT, true) + c.Assert(err, gc.ErrorMatches, "boom") +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/modelconfig/modelconfig.go juju-core-2.0~beta15/src/github.com/juju/juju/api/modelconfig/modelconfig.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/modelconfig/modelconfig.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/modelconfig/modelconfig.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,104 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package modelconfig + +import ( + "github.com/juju/errors" + + "github.com/juju/juju/api/base" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/environs/config" +) + +// Client provides methods that the Juju client command uses to interact +// with models stored in the Juju Server. +type Client struct { + base.ClientFacade + facade base.FacadeCaller +} + +// NewClient creates a new `Client` based on an existing authenticated API +// connection. +func NewClient(st base.APICallCloser) *Client { + frontend, backend := base.NewClientFacade(st, "ModelConfig") + return &Client{ClientFacade: frontend, facade: backend} +} + +// Close closes the api connection. +func (c *Client) Close() error { + return c.ClientFacade.Close() +} + +// ModelGet returns all model settings. +func (c *Client) ModelGet() (map[string]interface{}, error) { + result := params.ModelConfigResults{} + err := c.facade.FacadeCall("ModelGet", nil, &result) + if err != nil { + return nil, errors.Trace(err) + } + values := make(map[string]interface{}) + for name, val := range result.Config { + values[name] = val.Value + } + return values, nil +} + +// ModelGetWithMetadata returns all model settings along with extra +// metadata like the source of the setting value. +func (c *Client) ModelGetWithMetadata() (config.ConfigValues, error) { + result := params.ModelConfigResults{} + err := c.facade.FacadeCall("ModelGet", nil, &result) + if err != nil { + return nil, errors.Trace(err) + } + values := make(config.ConfigValues) + for name, val := range result.Config { + values[name] = config.ConfigValue{ + Value: val.Value, + Source: val.Source, + } + } + return values, nil +} + +// ModelSet sets the given key-value pairs in the model. +func (c *Client) ModelSet(config map[string]interface{}) error { + args := params.ModelSet{Config: config} + return c.facade.FacadeCall("ModelSet", args, nil) +} + +// ModelUnset sets the given key-value pairs in the model. +func (c *Client) ModelUnset(keys ...string) error { + args := params.ModelUnset{Keys: keys} + return c.facade.FacadeCall("ModelUnset", args, nil) +} + +// ModelDefaults returns the default config values used when creating a new model. +func (c *Client) ModelDefaults() (config.ConfigValues, error) { + result := params.ModelConfigResults{} + err := c.facade.FacadeCall("ModelDefaults", nil, &result) + if err != nil { + return nil, errors.Trace(err) + } + values := make(config.ConfigValues) + for name, val := range result.Config { + values[name] = config.ConfigValue{ + Value: val.Value, + Source: val.Source, + } + } + return values, nil +} + +// SetModelDefaults updates the specified default model config values. +func (c *Client) SetModelDefaults(config map[string]interface{}) error { + args := params.ModelSet{Config: config} + return c.facade.FacadeCall("SetModelDefaults", args, nil) +} + +// UnsetModelDefaults removes the specified default model config values. +func (c *Client) UnsetModelDefaults(keys ...string) error { + args := params.ModelUnset{Keys: keys} + return c.facade.FacadeCall("UnsetModelDefaults", args, nil) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/modelconfig/modelconfig_test.go juju-core-2.0~beta15/src/github.com/juju/juju/api/modelconfig/modelconfig_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/modelconfig/modelconfig_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/modelconfig/modelconfig_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,210 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package modelconfig_test + +import ( + gitjujutesting "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + basetesting "github.com/juju/juju/api/base/testing" + "github.com/juju/juju/api/modelconfig" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/environs/config" +) + +type modelconfigSuite struct { + gitjujutesting.IsolationSuite +} + +var _ = gc.Suite(&modelconfigSuite{}) + +func (s *modelconfigSuite) TestModelGet(c *gc.C) { + apiCaller := basetesting.APICallerFunc( + func(objType string, + version int, + id, request string, + a, result interface{}, + ) error { + c.Check(objType, gc.Equals, "ModelConfig") + c.Check(id, gc.Equals, "") + c.Check(request, gc.Equals, "ModelGet") + c.Check(a, gc.IsNil) + c.Assert(result, gc.FitsTypeOf, ¶ms.ModelConfigResults{}) + results := result.(*params.ModelConfigResults) + results.Config = map[string]params.ConfigValue{ + "foo": {"bar", "model"}, + } + return nil + }, + ) + client := modelconfig.NewClient(apiCaller) + result, err := client.ModelGet() + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, map[string]interface{}{ + "foo": "bar", + }) +} + +func (s *modelconfigSuite) TestModelGetWithMetadata(c *gc.C) { + apiCaller := basetesting.APICallerFunc( + func(objType string, + version int, + id, request string, + a, result interface{}, + ) error { + c.Check(objType, gc.Equals, "ModelConfig") + c.Check(id, gc.Equals, "") + c.Check(request, gc.Equals, "ModelGet") + c.Check(a, gc.IsNil) + c.Assert(result, gc.FitsTypeOf, ¶ms.ModelConfigResults{}) + results := result.(*params.ModelConfigResults) + results.Config = map[string]params.ConfigValue{ + "foo": {"bar", "model"}, + } + return nil + }, + ) + client := modelconfig.NewClient(apiCaller) + result, err := client.ModelGetWithMetadata() + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, config.ConfigValues{ + "foo": {"bar", "model"}, + }) +} + +func (s *modelconfigSuite) TestModelSet(c *gc.C) { + called := false + apiCaller := basetesting.APICallerFunc( + func(objType string, + version int, + id, request string, + a, result interface{}, + ) error { + c.Check(objType, gc.Equals, "ModelConfig") + c.Check(id, gc.Equals, "") + c.Check(request, gc.Equals, "ModelSet") + c.Check(a, jc.DeepEquals, params.ModelSet{ + Config: map[string]interface{}{ + "some-name": "value", + "other-name": true, + }, + }) + called = true + return nil + }, + ) + client := modelconfig.NewClient(apiCaller) + err := client.ModelSet(map[string]interface{}{ + "some-name": "value", + "other-name": true, + }) + c.Assert(err, jc.ErrorIsNil) + c.Assert(called, jc.IsTrue) +} + +func (s *modelconfigSuite) TestModelUnset(c *gc.C) { + called := false + apiCaller := basetesting.APICallerFunc( + func(objType string, + version int, + id, request string, + a, result interface{}, + ) error { + c.Check(objType, gc.Equals, "ModelConfig") + c.Check(id, gc.Equals, "") + c.Check(request, gc.Equals, "ModelUnset") + c.Check(a, jc.DeepEquals, params.ModelUnset{ + Keys: []string{"foo", "bar"}, + }) + called = true + return nil + }, + ) + client := modelconfig.NewClient(apiCaller) + err := client.ModelUnset("foo", "bar") + c.Assert(err, jc.ErrorIsNil) + c.Assert(called, jc.IsTrue) +} + +func (s *modelconfigSuite) TestModelDefaults(c *gc.C) { + apiCaller := basetesting.APICallerFunc( + func(objType string, + version int, + id, request string, + a, result interface{}, + ) error { + c.Check(objType, gc.Equals, "ModelConfig") + c.Check(id, gc.Equals, "") + c.Check(request, gc.Equals, "ModelDefaults") + c.Check(a, gc.IsNil) + c.Assert(result, gc.FitsTypeOf, ¶ms.ModelConfigResults{}) + results := result.(*params.ModelConfigResults) + results.Config = map[string]params.ConfigValue{ + "foo": {"bar", "model"}, + } + return nil + }, + ) + client := modelconfig.NewClient(apiCaller) + result, err := client.ModelDefaults() + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, config.ConfigValues{ + "foo": {"bar", "model"}, + }) +} + +func (s *modelconfigSuite) TestSetModelDefaults(c *gc.C) { + called := false + apiCaller := basetesting.APICallerFunc( + func(objType string, + version int, + id, request string, + a, result interface{}, + ) error { + c.Check(objType, gc.Equals, "ModelConfig") + c.Check(id, gc.Equals, "") + c.Check(request, gc.Equals, "SetModelDefaults") + c.Check(a, jc.DeepEquals, params.ModelSet{ + Config: map[string]interface{}{ + "some-name": "value", + "other-name": true, + }, + }) + called = true + return nil + }, + ) + client := modelconfig.NewClient(apiCaller) + err := client.SetModelDefaults(map[string]interface{}{ + "some-name": "value", + "other-name": true, + }) + c.Assert(err, jc.ErrorIsNil) + c.Assert(called, jc.IsTrue) +} + +func (s *modelconfigSuite) TestUnsetModelDefaults(c *gc.C) { + called := false + apiCaller := basetesting.APICallerFunc( + func(objType string, + version int, + id, request string, + a, result interface{}, + ) error { + c.Check(objType, gc.Equals, "ModelConfig") + c.Check(id, gc.Equals, "") + c.Check(request, gc.Equals, "UnsetModelDefaults") + c.Check(a, jc.DeepEquals, params.ModelUnset{ + Keys: []string{"foo", "bar"}, + }) + called = true + return nil + }, + ) + client := modelconfig.NewClient(apiCaller) + err := client.UnsetModelDefaults("foo", "bar") + c.Assert(err, jc.ErrorIsNil) + c.Assert(called, jc.IsTrue) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/modelconfig/package_test.go juju-core-2.0~beta15/src/github.com/juju/juju/api/modelconfig/package_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/modelconfig/package_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/modelconfig/package_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,14 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package modelconfig_test + +import ( + "testing" + + gc "gopkg.in/check.v1" +) + +func Test(t *testing.T) { + gc.TestingT(t) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/modelmanager/modelmanager.go juju-core-2.0~beta15/src/github.com/juju/juju/api/modelmanager/modelmanager.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/modelmanager/modelmanager.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/modelmanager/modelmanager.go 2016-08-16 08:56:25.000000000 +0000 @@ -105,6 +105,27 @@ return results.Results, nil } +// DumpModel returns the serialized database agnostic model representation. +func (c *Client) DumpModel(model names.ModelTag) (map[string]interface{}, error) { + var results params.MapResults + entities := params.Entities{ + Entities: []params.Entity{{Tag: model.String()}}, + } + + err := c.facade.FacadeCall("DumpModels", entities, &results) + if err != nil { + return nil, errors.Trace(err) + } + if count := len(results.Results); count != 1 { + return nil, errors.Errorf("unexpected result count: %d", count) + } + result := results.Results[0] + if result.Error != nil { + return nil, result.Error + } + return result.Result, nil +} + // DestroyModel puts the model into a "dying" state, // and removes all non-manager machine instances. DestroyModel // will fail if there are any manually-provisioned non-manager machines @@ -115,14 +136,14 @@ // ParseModelAccess parses an access permission argument into // a type suitable for making an API facade call. -func ParseModelAccess(access string) (params.ModelAccessPermission, error) { - var fail params.ModelAccessPermission +func ParseModelAccess(access string) (params.UserAccessPermission, error) { + var fail params.UserAccessPermission modelAccess, err := permission.ParseModelAccess(access) if err != nil { return fail, errors.Trace(err) } - var accessPermission params.ModelAccessPermission + var accessPermission params.UserAccessPermission switch modelAccess { case permission.ModelReadAccess: accessPermission = params.ModelReadAccess diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/modelmanager/modelmanager_test.go juju-core-2.0~beta15/src/github.com/juju/juju/api/modelmanager/modelmanager_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/modelmanager/modelmanager_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/modelmanager/modelmanager_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,8 +9,11 @@ gc "gopkg.in/check.v1" "gopkg.in/juju/names.v2" + basetesting "github.com/juju/juju/api/base/testing" "github.com/juju/juju/api/modelmanager" + "github.com/juju/juju/apiserver/params" jujutesting "github.com/juju/juju/juju/testing" + "github.com/juju/juju/testing" "github.com/juju/juju/testing/factory" ) @@ -88,3 +91,52 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(called, jc.IsTrue) } + +type dumpModelSuite struct { + testing.BaseSuite +} + +var _ = gc.Suite(&dumpModelSuite{}) + +func (s *dumpModelSuite) TestDumpModel(c *gc.C) { + expected := map[string]interface{}{ + "model-uuid": "some-uuid", + "other-key": "special", + } + results := params.MapResults{Results: []params.MapResult{{ + Result: expected, + }}} + apiCaller := basetesting.APICallerFunc( + func(objType string, version int, id, request string, args, result interface{}) error { + c.Check(objType, gc.Equals, "ModelManager") + c.Check(request, gc.Equals, "DumpModels") + in, ok := args.(params.Entities) + c.Assert(ok, jc.IsTrue) + c.Assert(in, gc.DeepEquals, params.Entities{[]params.Entity{{testing.ModelTag.String()}}}) + res, ok := result.(*params.MapResults) + c.Assert(ok, jc.IsTrue) + *res = results + return nil + }) + client := modelmanager.NewClient(apiCaller) + out, err := client.DumpModel(testing.ModelTag) + c.Assert(err, jc.ErrorIsNil) + c.Assert(out, jc.DeepEquals, expected) +} + +func (s *dumpModelSuite) TestDumpModelError(c *gc.C) { + results := params.MapResults{Results: []params.MapResult{{ + Error: ¶ms.Error{Message: "fake error"}, + }}} + apiCaller := basetesting.APICallerFunc( + func(objType string, version int, id, request string, args, result interface{}) error { + res, ok := result.(*params.MapResults) + c.Assert(ok, jc.IsTrue) + *res = results + return nil + }) + client := modelmanager.NewClient(apiCaller) + out, err := client.DumpModel(testing.ModelTag) + c.Assert(err, gc.ErrorMatches, "fake error") + c.Assert(out, gc.IsNil) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/provisioner/provisioner.go juju-core-2.0~beta15/src/github.com/juju/juju/api/provisioner/provisioner.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/provisioner/provisioner.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/provisioner/provisioner.go 2016-08-16 08:56:25.000000000 +0000 @@ -35,7 +35,8 @@ ModelWatcher: common.NewModelWatcher(facadeCaller), APIAddresser: common.NewAPIAddresser(facadeCaller), ControllerConfigAPI: common.NewControllerConfig(facadeCaller), - facade: facadeCaller} + facade: facadeCaller, + } } // machineLife requests the lifecycle of the given machine from the server. diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/provisioner/provisioner_test.go juju-core-2.0~beta15/src/github.com/juju/juju/api/provisioner/provisioner_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/provisioner/provisioner_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/provisioner/provisioner_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -71,7 +71,7 @@ c.Assert(err, jc.ErrorIsNil) // Create the provisioner API facade. - s.provisioner = s.st.Provisioner() + s.provisioner = provisioner.NewState(s.st) c.Assert(s.provisioner, gc.NotNil) s.ModelWatcherTests = apitesting.NewModelWatcherTests(s.provisioner, s.BackingState, apitesting.HasSecrets) @@ -233,7 +233,7 @@ } func (s *provisionerSuite) TestSetInstanceInfo(c *gc.C) { - pm := poolmanager.New(state.NewStateSettings(s.State)) + pm := poolmanager.New(state.NewStateSettings(s.State), provider.CommonStorageProviders()) _, err := pm.Create("loop-pool", provider.LoopProviderType, map[string]interface{}{"foo": "bar"}) c.Assert(err, jc.ErrorIsNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/state.go juju-core-2.0~beta15/src/github.com/juju/juju/api/state.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/state.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/state.go 2016-08-16 08:56:25.000000000 +0000 @@ -17,11 +17,9 @@ "github.com/juju/juju/api/charmrevisionupdater" "github.com/juju/juju/api/cleaner" "github.com/juju/juju/api/discoverspaces" - "github.com/juju/juju/api/firewaller" "github.com/juju/juju/api/imagemetadata" "github.com/juju/juju/api/instancepoller" "github.com/juju/juju/api/keyupdater" - "github.com/juju/juju/api/provisioner" "github.com/juju/juju/api/reboot" "github.com/juju/juju/api/unitassigner" "github.com/juju/juju/api/uniter" @@ -236,12 +234,6 @@ return unitassigner.New(st) } -// Provisioner returns a version of the state that provides functionality -// required by the provisioner worker. -func (st *state) Provisioner() *provisioner.State { - return provisioner.NewState(st) -} - // Uniter returns a version of the state that provides functionality // required by the uniter worker. func (st *state) Uniter() (*uniter.State, error) { @@ -252,12 +244,6 @@ return uniter.NewState(st, unitTag), nil } -// Firewaller returns a version of the state that provides functionality -// required by the firewaller worker. -func (st *state) Firewaller() *firewaller.State { - return firewaller.NewState(st) -} - // Upgrader returns access to the Upgrader API func (st *state) Upgrader() *upgrader.State { return upgrader.NewState(st) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/storageprovisioner/provisioner.go juju-core-2.0~beta15/src/github.com/juju/juju/api/storageprovisioner/provisioner.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/storageprovisioner/provisioner.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/storageprovisioner/provisioner.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,7 +8,6 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/api/base" - "github.com/juju/juju/api/common" apiwatcher "github.com/juju/juju/api/watcher" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/watcher" @@ -20,8 +19,6 @@ type State struct { facade base.FacadeCaller scope names.Tag - - *common.ModelWatcher } // NewState creates a new client-side StorageProvisioner facade. @@ -33,11 +30,7 @@ return nil, errors.Errorf("expected ModelTag or MachineTag, got %T", scope) } facadeCaller := base.NewFacadeCaller(caller, storageProvisionerFacade) - return &State{ - facadeCaller, - scope, - common.NewModelWatcher(facadeCaller), - }, nil + return &State{facadeCaller, scope}, nil } // WatchBlockDevices watches for changes to the specified machine's block devices. diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/storageprovisioner/provisioner_test.go juju-core-2.0~beta15/src/github.com/juju/juju/api/storageprovisioner/provisioner_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/storageprovisioner/provisioner_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/storageprovisioner/provisioner_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -914,41 +914,3 @@ c.Assert(results, gc.HasLen, 1) c.Check(results[0].Error, gc.ErrorMatches, "MSG") } - -func (s *provisionerSuite) TestWatchForModelConfigChanges(c *gc.C) { - apiCaller := testing.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error { - c.Check(objType, gc.Equals, "StorageProvisioner") - c.Check(version, gc.Equals, 0) - c.Check(id, gc.Equals, "") - c.Check(request, gc.Equals, "WatchForModelConfigChanges") - c.Assert(result, gc.FitsTypeOf, ¶ms.NotifyWatchResult{}) - *(result.(*params.NotifyWatchResult)) = params.NotifyWatchResult{ - NotifyWatcherId: "abc", - } - return errors.New("FAIL") - }) - st, err := storageprovisioner.NewState(apiCaller, names.NewMachineTag("123")) - c.Assert(err, jc.ErrorIsNil) - _, err = st.WatchForModelConfigChanges() - c.Assert(err, gc.ErrorMatches, "FAIL") -} - -func (s *provisionerSuite) TestModelConfig(c *gc.C) { - inputCfg := coretesting.ModelConfig(c) - apiCaller := testing.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error { - c.Check(objType, gc.Equals, "StorageProvisioner") - c.Check(version, gc.Equals, 0) - c.Check(id, gc.Equals, "") - c.Check(request, gc.Equals, "ModelConfig") - c.Assert(result, gc.FitsTypeOf, ¶ms.ModelConfigResult{}) - *(result.(*params.ModelConfigResult)) = params.ModelConfigResult{ - Config: inputCfg.AllAttrs(), - } - return nil - }) - st, err := storageprovisioner.NewState(apiCaller, names.NewMachineTag("123")) - c.Assert(err, jc.ErrorIsNil) - outputCfg, err := st.ModelConfig() - c.Assert(err, jc.ErrorIsNil) - c.Assert(outputCfg.AllAttrs(), jc.DeepEquals, inputCfg.AllAttrs()) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/testing/environwatcher.go juju-core-2.0~beta15/src/github.com/juju/juju/api/testing/environwatcher.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/testing/environwatcher.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/testing/environwatcher.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,6 +10,7 @@ "github.com/juju/juju/environs" "github.com/juju/juju/environs/config" "github.com/juju/juju/state" + "github.com/juju/juju/state/stateenvirons" "github.com/juju/juju/watcher" "github.com/juju/juju/watcher/watchertest" ) @@ -51,7 +52,7 @@ // If the facade doesn't have secrets, we need to replace the config // values in our model to compare against with the secrets replaced. if !s.hasSecrets { - env, err := environs.New(envConfig) + env, err := stateenvirons.GetNewEnvironFunc(environs.New)(s.state) c.Assert(err, jc.ErrorIsNil) secretAttrs, err := env.Provider().SecretAttrs(envConfig) c.Assert(err, jc.ErrorIsNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/usermanager/client.go juju-core-2.0~beta15/src/github.com/juju/juju/api/usermanager/client.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/usermanager/client.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/usermanager/client.go 2016-08-16 08:56:25.000000000 +0000 @@ -46,7 +46,7 @@ modelTags[i] = names.NewModelTag(uuid).String() } - var accessPermission params.ModelAccessPermission + var accessPermission params.UserAccessPermission var err error if len(modelTags) > 0 { accessPermission, err = modelmanager.ParseModelAccess(access) @@ -101,17 +101,23 @@ } // DisableUser disables a user. If the user is already disabled, the action -// is consided a success. +// is considered a success. func (c *Client) DisableUser(username string) error { return c.userCall(username, "DisableUser") } // EnableUser enables a users. If the user is already enabled, the action is -// consided a success. +// considered a success. func (c *Client) EnableUser(username string) error { return c.userCall(username, "EnableUser") } +// RemoveUser deletes a user. That is it permanently removes the user, while +// retaining the record of the user to maintain provenance. +func (c *Client) RemoveUser(username string) error { + return c.userCall(username, "RemoveUser") +} + // IncludeDisabled is a type alias to avoid bare true/false values // in calls to the client method. type IncludeDisabled bool diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/usermanager/client_test.go juju-core-2.0~beta15/src/github.com/juju/juju/api/usermanager/client_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/usermanager/client_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/usermanager/client_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,6 +11,7 @@ "github.com/juju/juju/api/usermanager" "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/core/description" jujutesting "github.com/juju/juju/juju/testing" "github.com/juju/juju/testing/factory" ) @@ -58,11 +59,11 @@ c.Assert(users, gc.HasLen, 3) var modelUserTags = make([]names.UserTag, len(users)) for i, u := range users { - modelUserTags[i] = u.UserTag() - if u.UserTag().Name() == "foobar" { - c.Assert(u.IsReadOnly(), jc.IsTrue) + modelUserTags[i] = u.UserTag + if u.UserTag.Name() == "foobar" { + c.Assert(u.Access, gc.Equals, description.ReadAccess) } else { - c.Assert(u.IsReadOnly(), jc.IsFalse) + c.Assert(u.Access, gc.Not(gc.Equals), description.ReadAccess) } } c.Assert(modelUserTags, jc.SameContents, []names.UserTag{ @@ -103,6 +104,29 @@ c.Assert(err, gc.ErrorMatches, "expected 1 result, got 2") } +func (s *usermanagerSuite) TestRemoveUser(c *gc.C) { + tag, _, err := s.usermanager.AddUser("jjam", "Jimmy Jam", "password", "") + c.Assert(err, jc.ErrorIsNil) + + // Ensure the user exists. + user, err := s.State.User(tag) + c.Assert(err, jc.ErrorIsNil) + c.Assert(user.Name(), gc.Equals, "jjam") + c.Assert(user.DisplayName(), gc.Equals, "Jimmy Jam") + + // Delete the user. + err = s.usermanager.RemoveUser(tag.Name()) + c.Assert(err, jc.ErrorIsNil) + + // Assert that the user is gone. + _, err = s.State.User(tag) + c.Assert(err, jc.Satisfies, errors.IsUserNotFound) + + err = user.Refresh() + c.Check(err, jc.ErrorIsNil) + c.Assert(user.IsDeleted(), jc.IsTrue) +} + func (s *usermanagerSuite) TestDisableUser(c *gc.C) { user := s.Factory.MakeUser(c, &factory.UserParams{Name: "foobar"}) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/watcher/watcher.go juju-core-2.0~beta15/src/github.com/juju/juju/api/watcher/watcher.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/watcher/watcher.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/watcher/watcher.go 2016-08-16 08:56:25.000000000 +0000 @@ -492,6 +492,7 @@ return errors.Errorf("invalid phase %q", inStatus.Phase) } outStatus := watcher.MigrationStatus{ + MigrationId: inStatus.MigrationId, Attempt: inStatus.Attempt, Phase: phase, SourceAPIAddrs: inStatus.SourceAPIAddrs, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/api/watcher/watcher_test.go juju-core-2.0~beta15/src/github.com/juju/juju/api/watcher/watcher_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/api/watcher/watcher_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/api/watcher/watcher_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -18,9 +18,6 @@ "github.com/juju/juju/core/migration" "github.com/juju/juju/juju/testing" "github.com/juju/juju/state" - "github.com/juju/juju/storage" - "github.com/juju/juju/storage/provider/dummy" - "github.com/juju/juju/storage/provider/registry" coretesting "github.com/juju/juju/testing" "github.com/juju/juju/testing/factory" corewatcher "github.com/juju/juju/watcher" @@ -182,20 +179,11 @@ // TODO(fwereade): 2015-11-18 lp:1517391 func (s *watcherSuite) TestWatchMachineStorage(c *gc.C) { - registry.RegisterProvider( - "envscoped", - &dummy.StorageProvider{ - StorageScope: storage.ScopeEnviron, - }, - ) - registry.RegisterEnvironStorageProviders("dummy", "envscoped") - defer registry.RegisterProvider("envscoped", nil) - f := factory.NewFactory(s.BackingState) f.MakeMachine(c, &factory.MachineParams{ Volumes: []state.MachineVolumeParams{{ Volume: state.VolumeParams{ - Pool: "envscoped", + Pool: "environscoped", Size: 1024, }, }}, @@ -315,12 +303,13 @@ } } - assertChange := func(phase migration.Phase) { + assertChange := func(id string, phase migration.Phase) { s.startSync(c, hostedState) select { case status, ok := <-w.Changes(): c.Assert(ok, jc.IsTrue) - c.Assert(status.Phase, gc.Equals, phase) + c.Check(status.MigrationId, gc.Equals, id) + c.Check(status.Phase, gc.Equals, phase) case <-time.After(coretesting.LongWait): c.Fatalf("watcher didn't emit an event") } @@ -328,7 +317,7 @@ } // Initial event with no migration in progress. - assertChange(migration.NONE) + assertChange("", migration.NONE) // Now create a migration, should trigger watcher. spec := state.ModelMigrationSpec{ @@ -343,16 +332,16 @@ } mig, err := hostedState.CreateModelMigration(spec) c.Assert(err, jc.ErrorIsNil) - assertChange(migration.QUIESCE) + assertChange(mig.Id(), migration.QUIESCE) // Now abort the migration, this should be reported too. c.Assert(mig.SetPhase(migration.ABORT), jc.ErrorIsNil) - assertChange(migration.ABORT) + assertChange(mig.Id(), migration.ABORT) c.Assert(mig.SetPhase(migration.ABORTDONE), jc.ErrorIsNil) - assertChange(migration.ABORTDONE) + assertChange(mig.Id(), migration.ABORTDONE) // Start a new migration, this should also trigger. - _, err = hostedState.CreateModelMigration(spec) + mig2, err := hostedState.CreateModelMigration(spec) c.Assert(err, jc.ErrorIsNil) - assertChange(migration.QUIESCE) + assertChange(mig2.Id(), migration.QUIESCE) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/action/action.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/action/action.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/action/action.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/action/action.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,6 +8,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" ) @@ -19,13 +20,13 @@ // ActionAPI implements the client API for interacting with Actions type ActionAPI struct { state *state.State - resources *common.Resources - authorizer common.Authorizer + resources facade.Resources + authorizer facade.Authorizer check *common.BlockChecker } // NewActionAPI returns an initialized ActionAPI -func NewActionAPI(st *state.State, resources *common.Resources, authorizer common.Authorizer) (*ActionAPI, error) { +func NewActionAPI(st *state.State, resources facade.Resources, authorizer facade.Authorizer) (*ActionAPI, error) { if !authorizer.AuthClient() { return nil, common.ErrPerm } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/admin.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/admin.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/admin.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/admin.go 2016-08-16 08:56:25.000000000 +0000 @@ -16,6 +16,7 @@ "github.com/juju/juju/apiserver/observer" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/apiserver/presence" + "github.com/juju/juju/core/description" "github.com/juju/juju/rpc" "github.com/juju/juju/rpc/rpcreflect" "github.com/juju/juju/state" @@ -71,7 +72,6 @@ } } - var agentPingerNeeded = true isUser := true kind := names.UserTagKind if req.AuthTag != "" { @@ -89,6 +89,7 @@ } serverOnlyLogin := a.root.modelUUID == "" + controllerMachineLogin := false entity, lastConnection, err := doCheckCreds(a.root.state, req, !serverOnlyLogin, a.srv.authCtxt) if err != nil { @@ -120,7 +121,7 @@ if kind != names.MachineTagKind { return fail, errors.Trace(err) } - entity, err = a.checkCredsOfControllerMachine(req) + entity, err = a.checkControllerMachineCreds(req) if err != nil { return fail, errors.Trace(err) } @@ -128,35 +129,35 @@ // machine in the controller model, and we don't need a pinger // for it as we already have one running in the machine agent api // worker for the controller model. - agentPingerNeeded = false + controllerMachineLogin = true } a.root.entity = entity - a.apiObserver.Login(entity.Tag().String()) + a.apiObserver.Login(entity.Tag(), a.root.state.ModelTag(), controllerMachineLogin, req.UserData) // We have authenticated the user; enable the appropriate API // to serve to them. a.loggedIn = true - if agentPingerNeeded { + if !controllerMachineLogin { if err := startPingerIfAgent(a.root, entity); err != nil { return fail, errors.Trace(err) } } var maybeUserInfo *params.AuthUserInfo - var modelUser *state.ModelUser + var modelUser description.UserAccess // Send back user info if user if isUser && !serverOnlyLogin { maybeUserInfo = ¶ms.AuthUserInfo{ Identity: entity.Tag().String(), LastConnection: lastConnection, } - modelUser, err = a.root.state.ModelUser(entity.Tag().(names.UserTag)) + modelUser, err = a.root.state.UserAccess(entity.Tag().(names.UserTag), a.root.state.ModelTag()) if err != nil { return fail, errors.Annotatef(err, "missing ModelUser for logged in user %s", entity.Tag()) } - maybeUserInfo.ReadOnly = modelUser.IsReadOnly() + maybeUserInfo.ReadOnly = modelUser.Access == description.ReadAccess if maybeUserInfo.ReadOnly { logger.Debugf("model user %s is READ ONLY", entity.Tag()) } @@ -199,8 +200,8 @@ } loginResult.Facades = facades } - - if modelUser != nil { + emptyUserAccess := description.UserAccess{} + if modelUser != emptyUserAccess { authedApi = newClientAuthRoot(authedApi, modelUser) } @@ -209,27 +210,8 @@ return loginResult, nil } -// checkCredsOfControllerMachine checks the special case of a controller -// machine creating an API connection for a different model so it can -// run API workers for that model to do things like provisioning -// machines. -func (a *admin) checkCredsOfControllerMachine(req params.LoginRequest) (state.Entity, error) { - entity, _, err := doCheckCreds(a.srv.state, req, false, a.srv.authCtxt) - if err != nil { - return nil, errors.Trace(err) - } - machine, ok := entity.(*state.Machine) - if !ok { - return nil, errors.Errorf("entity should be a machine, but is %T", entity) - } - for _, job := range machine.Jobs() { - if job == state.JobManageModel { - return entity, nil - } - } - // The machine does exist in the controller model, but it - // doesn't manage models, so reject it. - return nil, errors.Trace(common.ErrPerm) +func (a *admin) checkControllerMachineCreds(req params.LoginRequest) (state.Entity, error) { + return checkControllerMachineCreds(a.srv.state, req, a.srv.authCtxt) } func (a *admin) maintenanceInProgress() bool { @@ -297,6 +279,28 @@ return entity, lastLogin, nil } +// checkControllerMachineCreds checks the special case of a controller +// machine creating an API connection for a different model so it can +// run workers that act on behalf of a hosted model. +func checkControllerMachineCreds( + controllerSt *state.State, + req params.LoginRequest, + authenticator authentication.EntityAuthenticator, +) (state.Entity, error) { + entity, _, err := doCheckCreds(controllerSt, req, false, authenticator) + if err != nil { + return nil, errors.Trace(err) + } + if machine, ok := entity.(*state.Machine); !ok { + return nil, errors.Errorf("entity should be a machine, but is %T", entity) + } else if !machine.IsManager() { + // The machine exists in the controller model, but it doesn't + // manage models, so reject it. + return nil, errors.Trace(common.ErrPerm) + } + return entity, nil +} + // loginEntity defines the interface needed to log in as a user. // Notable implementations are *state.User and *modelUserEntity. type loginEntity interface { @@ -320,11 +324,12 @@ if !ok { return f.st.FindEntity(tag) } - modelUser, err := f.st.ModelUser(utag) + modelUser, err := f.st.UserAccess(utag, f.st.ModelTag()) if err != nil { return nil, err } u := &modelUserEntity{ + st: f.st, modelUser: modelUser, } if utag.IsLocal() { @@ -345,7 +350,9 @@ // in such a way that the authentication mechanisms // can work without knowing these details. type modelUserEntity struct { - modelUser *state.ModelUser + st *state.State + + modelUser description.UserAccess user *state.User } @@ -376,14 +383,14 @@ // Tag implements state.Entity.Tag. func (u *modelUserEntity) Tag() names.Tag { - return u.modelUser.UserTag() + return u.modelUser.UserTag } // LastLogin implements loginEntity.LastLogin. func (u *modelUserEntity) LastLogin() (time.Time, error) { // The last connection for the model takes precedence over // the local user last login time. - t, err := u.modelUser.LastConnection() + t, err := u.st.LastModelConnection(u.modelUser.UserTag) if state.IsNeverConnectedError(err) { if u.user != nil { // There's a global user, so use that login time instead. @@ -398,13 +405,19 @@ // UpdateLastLogin implements loginEntity.UpdateLastLogin. func (u *modelUserEntity) UpdateLastLogin() error { - err := u.modelUser.UpdateLastConnection() + + if u.modelUser.Object.Kind() != names.ModelTagKind { + return errors.NotValidf("%s as model user", u.modelUser.Object.Kind()) + } + + err := u.st.UpdateLastModelConnection(u.modelUser.UserTag) if u.user != nil { err1 := u.user.UpdateLastLogin() if err == nil { - err = err1 + return err1 } } + return err } @@ -460,11 +473,12 @@ // // We should have picked better names... action := func() { + logger.Debugf("closing connection due to ping timout") if err := root.getRpcConn().Close(); err != nil { logger.Errorf("error closing the RPC connection: %v", err) } } - pingTimeout := newPingTimeout(action, maxClientPingInterval) + pingTimeout := newPingTimeout(action, clock.WallClock, maxClientPingInterval) return root.getResources().RegisterNamed("pingTimeout", pingTimeout) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/admin_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/admin_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/admin_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/admin_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -55,10 +55,10 @@ } func (s *baseLoginSuite) setupServer(c *gc.C) (api.Connection, func()) { - return s.setupServerForEnvironment(c, s.State.ModelTag()) + return s.setupServerForModel(c, s.State.ModelTag()) } -func (s *baseLoginSuite) setupServerForEnvironment(c *gc.C, modelTag names.ModelTag) (api.Connection, func()) { +func (s *baseLoginSuite) setupServerForModel(c *gc.C, modelTag names.ModelTag) (api.Connection, func()) { info, cleanup := s.setupServerForEnvironmentWithValidator(c, modelTag, nil) st, err := api.Open(info, fastDialOpts) c.Assert(err, jc.ErrorIsNil) @@ -520,7 +520,7 @@ c.Assert(err, jc.ErrorIsNil) err = st.APICall("Client", 1, "", "ModelSet", params.ModelSet{}, nil) - c.Assert(errors.Cause(err), gc.DeepEquals, &rpc.RequestError{Message: params.CodeUpgradeInProgress, Code: params.CodeUpgradeInProgress}) + c.Assert(err, jc.Satisfies, params.IsCodeUpgradeInProgress) } s.checkLoginWithValidator(c, validator, checker) } @@ -623,7 +623,7 @@ err := st.Login(adminUser, "dummy-secret", "", nil) c.Assert(err, jc.ErrorIsNil) - s.assertRemoteEnvironment(c, st, s.State.ModelTag()) + s.assertRemoteModel(c, st, s.State.ModelTag()) } func (s *loginSuite) TestControllerModelBadCreds(c *gc.C) { @@ -642,7 +642,7 @@ }) } -func (s *loginSuite) TestNonExistentEnvironment(c *gc.C) { +func (s *loginSuite) TestNonExistentModel(c *gc.C) { info, cleanup := s.setupServerWithValidator(c, nil) defer cleanup() @@ -656,11 +656,11 @@ err = st.Login(adminUser, "dummy-secret", "", nil) c.Assert(errors.Cause(err), gc.DeepEquals, &rpc.RequestError{ Message: fmt.Sprintf("unknown model: %q", uuid), - Code: "not found", + Code: "model not found", }) } -func (s *loginSuite) TestInvalidEnvironment(c *gc.C) { +func (s *loginSuite) TestInvalidModel(c *gc.C) { info, cleanup := s.setupServerWithValidator(c, nil) defer cleanup() @@ -672,11 +672,11 @@ err := st.Login(adminUser, "dummy-secret", "", nil) c.Assert(errors.Cause(err), gc.DeepEquals, &rpc.RequestError{ Message: `unknown model: "rubbish"`, - Code: "not found", + Code: "model not found", }) } -func (s *loginSuite) TestOtherEnvironment(c *gc.C) { +func (s *loginSuite) TestOtherModel(c *gc.C) { info, cleanup := s.setupServerWithValidator(c, nil) defer cleanup() @@ -691,10 +691,10 @@ err := st.Login(envOwner.UserTag(), "password", "", nil) c.Assert(err, jc.ErrorIsNil) - s.assertRemoteEnvironment(c, st, envState.ModelTag()) + s.assertRemoteModel(c, st, envState.ModelTag()) } -func (s *loginSuite) TestMachineLoginOtherEnvironment(c *gc.C) { +func (s *loginSuite) TestMachineLoginOtherModel(c *gc.C) { // User credentials are checked against a global user list. // Machine credentials are checked against environment specific // machines, so this makes sure that the credential checking is @@ -761,7 +761,7 @@ }) } -func (s *loginSuite) assertRemoteEnvironment(c *gc.C, st api.Connection, expected names.ModelTag) { +func (s *loginSuite) assertRemoteModel(c *gc.C, st api.Connection, expected names.ModelTag) { // Look at what the api thinks it has. tag, err := st.ModelTag() c.Assert(err, jc.ErrorIsNil) @@ -809,9 +809,9 @@ c.Assert(lastLogin.After(startTime), jc.IsTrue) // The env user is also updated. - modelUser, err := s.State.ModelUser(user.UserTag()) + modelUser, err := s.State.UserAccess(user.UserTag(), s.State.ModelTag()) c.Assert(err, jc.ErrorIsNil) - when, err := modelUser.LastConnection() + when, err := s.State.LastModelConnection(modelUser.UserTag) c.Assert(err, jc.ErrorIsNil) c.Assert(when, gc.NotNil) c.Assert(when.After(startTime), jc.IsTrue) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/agent/agent.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/agent/agent.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/agent/agent.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/agent/agent.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,10 +11,13 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/common/cloudspec" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/mongo" "github.com/juju/juju/state" "github.com/juju/juju/state/multiwatcher" + "github.com/juju/juju/state/stateenvirons" ) func init() { @@ -27,14 +30,15 @@ *common.RebootFlagClearer *common.ModelWatcher *common.ControllerConfigAPI + cloudspec.CloudSpecAPI st *state.State - auth common.Authorizer + auth facade.Authorizer } // NewAgentAPIV2 returns an object implementing version 2 of the Agent API // with the given authorizer representing the currently logged in client. -func NewAgentAPIV2(st *state.State, resources *common.Resources, auth common.Authorizer) (*AgentAPIV2, error) { +func NewAgentAPIV2(st *state.State, resources facade.Resources, auth facade.Authorizer) (*AgentAPIV2, error) { // Agents are defined to be any user that's not a client user. if !auth.AuthMachineAgent() && !auth.AuthUnitAgent() { return nil, common.ErrPerm @@ -42,11 +46,13 @@ getCanChange := func() (common.AuthFunc, error) { return auth.AuthOwner, nil } + environConfigGetter := stateenvirons.EnvironConfigGetter{st} return &AgentAPIV2{ PasswordChanger: common.NewPasswordChanger(st, getCanChange), RebootFlagClearer: common.NewRebootFlagClearer(st, getCanChange), ModelWatcher: common.NewModelWatcher(st, resources, auth), ControllerConfigAPI: common.NewControllerConfig(st), + CloudSpecAPI: cloudspec.NewCloudSpec(environConfigGetter.CloudSpec, common.AuthFuncForTag(st.ModelTag())), st: st, auth: auth, }, nil diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/agenttools/agenttools.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/agenttools/agenttools.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/agenttools/agenttools.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/agenttools/agenttools.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,15 +9,17 @@ "github.com/juju/version" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/environs" "github.com/juju/juju/environs/config" "github.com/juju/juju/environs/tools" "github.com/juju/juju/state" + "github.com/juju/juju/state/stateenvirons" coretools "github.com/juju/juju/tools" ) func init() { - common.RegisterStandardFacade("AgentTools", 1, NewAgentToolsAPI) + common.RegisterStandardFacade("AgentTools", 1, newAgentToolsAPI) } var logger = loggo.GetLogger("juju.apiserver.model") @@ -26,24 +28,35 @@ findTools = tools.FindTools ) -type stateInterface interface { - ModelGetter - environs.EnvironConfigGetter -} - // AgentToolsAPI implements the API used by the machine model worker. type AgentToolsAPI struct { - st stateInterface - authorizer common.Authorizer + modelGetter ModelGetter + newEnviron newEnvironFunc + authorizer facade.Authorizer // tools lookup findTools toolsFinder envVersionUpdate envVersionUpdater } +func newAgentToolsAPI(st *state.State, resources facade.Resources, authorizer facade.Authorizer) (*AgentToolsAPI, error) { + newEnviron := func() (environs.Environ, error) { + newEnviron := stateenvirons.GetNewEnvironFunc(environs.New) + return newEnviron(st) + } + return NewAgentToolsAPI(st, newEnviron, findTools, envVersionUpdate, authorizer) +} + // NewAgentToolsAPI creates a new instance of the Model API. -func NewAgentToolsAPI(st *state.State, resources *common.Resources, authorizer common.Authorizer) (*AgentToolsAPI, error) { +func NewAgentToolsAPI( + modelGetter ModelGetter, + newEnviron func() (environs.Environ, error), + findTools func(environs.Environ, int, int, string, coretools.Filter) (coretools.List, error), + envVersionUpdate func(*state.Model, version.Number) error, + authorizer facade.Authorizer, +) (*AgentToolsAPI, error) { return &AgentToolsAPI{ - st: st, + modelGetter: modelGetter, + newEnviron: newEnviron, authorizer: authorizer, findTools: findTools, envVersionUpdate: envVersionUpdate, @@ -55,18 +68,17 @@ Model() (*state.Model, error) } +type newEnvironFunc func() (environs.Environ, error) type toolsFinder func(environs.Environ, int, int, string, coretools.Filter) (coretools.List, error) type envVersionUpdater func(*state.Model, version.Number) error -var newEnvirons = environs.New - -func checkToolsAvailability(getter environs.EnvironConfigGetter, modelCfg *config.Config, finder toolsFinder) (version.Number, error) { +func checkToolsAvailability(newEnviron newEnvironFunc, modelCfg *config.Config, finder toolsFinder) (version.Number, error) { currentVersion, ok := modelCfg.AgentVersion() if !ok || currentVersion == version.Zero { return version.Zero, nil } - env, err := environs.GetEnviron(getter, newEnvirons) + env, err := newEnviron() if err != nil { return version.Zero, errors.Annotatef(err, "cannot make model") } @@ -98,8 +110,8 @@ return env.UpdateLatestToolsVersion(ver) } -func updateToolsAvailability(st stateInterface, finder toolsFinder, update envVersionUpdater) error { - model, err := st.Model() +func updateToolsAvailability(modelGetter ModelGetter, newEnviron newEnvironFunc, finder toolsFinder, update envVersionUpdater) error { + model, err := modelGetter.Model() if err != nil { return errors.Annotate(err, "cannot get model") } @@ -107,7 +119,7 @@ if err != nil { return errors.Annotate(err, "cannot get config") } - ver, err := checkToolsAvailability(st, cfg, finder) + ver, err := checkToolsAvailability(newEnviron, cfg, finder) if err != nil { if errors.IsNotFound(err) { // No newer tools, so exit silently. @@ -128,5 +140,5 @@ if !api.authorizer.AuthModelManager() { return common.ErrPerm } - return updateToolsAvailability(api.st, api.findTools, api.envVersionUpdate) + return updateToolsAvailability(api.modelGetter, api.newEnviron, api.findTools, api.envVersionUpdate) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/agenttools/agenttools_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/agenttools/agenttools_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/agenttools/agenttools_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/agenttools/agenttools_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -41,10 +41,6 @@ }) cfg, err := config.New(config.NoDefaults, sConfig) c.Assert(err, jc.ErrorIsNil) - fakeNewEnvirons := func(*config.Config) (environs.Environ, error) { - return dummyEnviron{}, nil - } - s.PatchValue(&newEnvirons, fakeNewEnvirons) var ( calledWithMajor, calledWithMinor int ) @@ -59,7 +55,7 @@ return coretools.List{&t}, nil } - ver, err := checkToolsAvailability(&configGetter{cfg: cfg}, cfg, fakeToolFinder) + ver, err := checkToolsAvailability(getDummyEnviron, cfg, fakeToolFinder) c.Assert(err, jc.ErrorIsNil) c.Assert(ver, gc.Not(gc.Equals), version.Zero) c.Assert(ver, gc.Equals, version.Number{Major: 2, Minor: 5, Patch: 0}) @@ -73,10 +69,6 @@ }) cfg, err := config.New(config.NoDefaults, sConfig) c.Assert(err, jc.ErrorIsNil) - fakeNewEnvirons := func(*config.Config) (environs.Environ, error) { - return dummyEnviron{}, nil - } - s.PatchValue(&newEnvirons, fakeNewEnvirons) var ( calledWithMajor, calledWithMinor int calledWithStreams []string @@ -94,7 +86,7 @@ c.Assert(calledWithMinor, gc.Equals, 5) return coretools.List{&t}, nil } - ver, err := checkToolsAvailability(&configGetter{cfg: cfg}, cfg, fakeToolFinder) + ver, err := checkToolsAvailability(getDummyEnviron, cfg, fakeToolFinder) c.Assert(err, jc.ErrorIsNil) c.Assert(calledWithStreams, gc.DeepEquals, []string{"released", "proposed"}) c.Assert(ver, gc.Not(gc.Equals), version.Zero) @@ -110,11 +102,6 @@ } func (s *AgentToolsSuite) TestUpdateToolsAvailability(c *gc.C) { - fakeNewEnvirons := func(*config.Config) (environs.Environ, error) { - return dummyEnviron{}, nil - } - s.PatchValue(&newEnvirons, fakeNewEnvirons) - fakeModelConfig := func(_ *state.Model) (*config.Config, error) { sConfig := coretesting.FakeConfig() sConfig = sConfig.Merge(coretesting.Attrs{ @@ -140,7 +127,7 @@ cfg, err := config.New(config.NoDefaults, coretesting.FakeConfig()) c.Assert(err, jc.ErrorIsNil) - err = updateToolsAvailability(&mockState{configGetter{cfg}}, fakeToolFinder, fakeUpdate) + err = updateToolsAvailability(&mockState{configGetter{cfg}}, getDummyEnviron, fakeToolFinder, fakeUpdate) c.Assert(err, jc.ErrorIsNil) c.Assert(ver, gc.Not(gc.Equals), version.Zero) @@ -148,11 +135,6 @@ } func (s *AgentToolsSuite) TestUpdateToolsAvailabilityNoMatches(c *gc.C) { - fakeNewEnvirons := func(*config.Config) (environs.Environ, error) { - return dummyEnviron{}, nil - } - s.PatchValue(&newEnvirons, fakeNewEnvirons) - fakeModelConfig := func(_ *state.Model) (*config.Config, error) { sConfig := coretesting.FakeConfig() sConfig = sConfig.Merge(coretesting.Attrs{ @@ -175,6 +157,10 @@ cfg, err := config.New(config.NoDefaults, coretesting.FakeConfig()) c.Assert(err, jc.ErrorIsNil) - err = updateToolsAvailability(&mockState{configGetter{cfg}}, fakeToolFinder, fakeUpdate) + err = updateToolsAvailability(&mockState{configGetter{cfg}}, getDummyEnviron, fakeToolFinder, fakeUpdate) c.Assert(err, jc.ErrorIsNil) } + +func getDummyEnviron() (environs.Environ, error) { + return dummyEnviron{}, nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/allfacades.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/allfacades.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/allfacades.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/allfacades.go 2016-08-16 08:56:25.000000000 +0000 @@ -50,6 +50,7 @@ _ "github.com/juju/juju/apiserver/migrationmaster" _ "github.com/juju/juju/apiserver/migrationminion" _ "github.com/juju/juju/apiserver/migrationtarget" + _ "github.com/juju/juju/apiserver/modelconfig" _ "github.com/juju/juju/apiserver/modelmanager" _ "github.com/juju/juju/apiserver/provisioner" _ "github.com/juju/juju/apiserver/proxyupdater" diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/annotations/client.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/annotations/client.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/annotations/client.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/annotations/client.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,6 +8,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" ) @@ -30,14 +31,14 @@ // implementation of the api end point. type API struct { access annotationAccess - authorizer common.Authorizer + authorizer facade.Authorizer } // NewAPI returns a new charm annotator API facade. func NewAPI( st *state.State, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, ) (*API, error) { if !authorizer.AuthClient() { return nil, common.ErrPerm diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/apiserver.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/apiserver.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/apiserver.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/apiserver.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,6 +10,7 @@ "net/http" "strings" "sync" + "sync/atomic" "time" "github.com/bmizerany/pat" @@ -50,11 +51,9 @@ adminApiFactories map[int]adminApiFactory modelUUID string authCtxt *authContext + lastConnectionID uint64 newObserver observer.ObserverFactory - connCount struct { - sync.RWMutex - value int64 - } + connCount int64 } // LoginValidator functions are used to decide whether login requests @@ -83,7 +82,7 @@ func (c *ServerConfig) Validate() error { if c.NewObserver == nil { - return errors.NotAssignedf("NewObserver") + return errors.NotValidf("missing NewObserver") } return nil @@ -242,9 +241,7 @@ } func (srv *Server) ConnectionCount() int64 { - srv.connCount.RLock() - defer srv.connCount.Unlock() - return srv.connCount.value + return atomic.LoadInt64(&srv.connCount) } // Dead returns a channel that signals when the server has exited. @@ -479,16 +476,16 @@ func (srv *Server) apiHandler(w http.ResponseWriter, req *http.Request) { addCount := func(delta int64) { - srv.connCount.Lock() - srv.connCount.value += delta - srv.connCount.Unlock() + atomic.AddInt64(&srv.connCount, delta) } addCount(1) defer addCount(-1) + connectionID := atomic.AddUint64(&srv.lastConnectionID, 1) + apiObserver := srv.newObserver() - apiObserver.Join(req) + apiObserver.Join(req, connectionID) defer apiObserver.Leave() wsServer := websocket.Server{ diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/application/application.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/application/application.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/application/application.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/application/application.go 2016-08-16 08:56:25.000000000 +0000 @@ -14,6 +14,7 @@ goyaml "gopkg.in/yaml.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/instance" jjj "github.com/juju/juju/juju" @@ -41,14 +42,14 @@ type API struct { check *common.BlockChecker state *state.State - authorizer common.Authorizer + authorizer facade.Authorizer } // NewAPI returns a new application API facade. func NewAPI( st *state.State, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, ) (*API, error) { if !authorizer.AuthClient() { return nil, common.ErrPerm diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/application/application_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/application/application_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/application/application_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/application/application_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -34,9 +34,6 @@ statestorage "github.com/juju/juju/state/storage" "github.com/juju/juju/status" "github.com/juju/juju/storage" - "github.com/juju/juju/storage/poolmanager" - "github.com/juju/juju/storage/provider" - "github.com/juju/juju/storage/provider/registry" "github.com/juju/juju/testcharms" "github.com/juju/juju/testing/factory" jujuversion "github.com/juju/juju/version" @@ -188,18 +185,7 @@ c.Assert(err, gc.ErrorMatches, `unknown option "yummy"`) } -func setupStoragePool(c *gc.C, st *state.State) { - pm := poolmanager.New(state.NewStateSettings(st)) - _, err := pm.Create("loop-pool", provider.LoopProviderType, map[string]interface{}{}) - c.Assert(err, jc.ErrorIsNil) - err = st.UpdateModelConfig(map[string]interface{}{ - "storage-default-block-source": "loop-pool", - }, nil, nil) - c.Assert(err, jc.ErrorIsNil) -} - func (s *serviceSuite) TestServiceDeployWithStorage(c *gc.C) { - setupStoragePool(c, s.State) curl, ch := s.UploadCharm(c, "utopic/storage-block-10", "storage-block") err := application.AddCharmWithAuthorization(s.State, params.AddCharmWithAuthorization{ URL: curl.String(), @@ -209,7 +195,7 @@ "data": { Count: 1, Size: 1024, - Pool: "loop-pool", + Pool: "environscoped-block", }, } @@ -235,7 +221,7 @@ "data": { Count: 1, Size: 1024, - Pool: "loop-pool", + Pool: "environscoped-block", }, "allecto": { Count: 0, @@ -255,7 +241,6 @@ } func (s *serviceSuite) TestServiceDeployWithInvalidStoragePool(c *gc.C) { - setupStoragePool(c, s.State) curl, _ := s.UploadCharm(c, "utopic/storage-block-0", "storage-block") err := application.AddCharmWithAuthorization(s.State, params.AddCharmWithAuthorization{ URL: curl.String(), @@ -285,44 +270,7 @@ c.Assert(results.Results[0].Error, gc.ErrorMatches, `.* pool "foo" not found`) } -func (s *serviceSuite) TestServiceDeployWithUnsupportedStoragePool(c *gc.C) { - registry.RegisterProvider("hostloop", &mockStorageProvider{kind: storage.StorageKindBlock}) - pm := poolmanager.New(state.NewStateSettings(s.State)) - _, err := pm.Create("host-loop-pool", provider.HostLoopProviderType, map[string]interface{}{}) - c.Assert(err, jc.ErrorIsNil) - - curl, _ := s.UploadCharm(c, "utopic/storage-block-0", "storage-block") - err = application.AddCharmWithAuthorization(s.State, params.AddCharmWithAuthorization{ - URL: curl.String(), - }) - c.Assert(err, jc.ErrorIsNil) - storageConstraints := map[string]storage.Constraints{ - "data": storage.Constraints{ - Pool: "host-loop-pool", - Count: 1, - Size: 1024, - }, - } - - var cons constraints.Value - args := params.ApplicationDeploy{ - ApplicationName: "application", - CharmUrl: curl.String(), - NumUnits: 1, - Constraints: cons, - Storage: storageConstraints, - } - results, err := s.applicationApi.Deploy(params.ApplicationsDeploy{ - Applications: []params.ApplicationDeploy{args}}, - ) - c.Assert(err, jc.ErrorIsNil) - c.Assert(results.Results, gc.HasLen, 1) - c.Assert(results.Results[0].Error, gc.ErrorMatches, - `.*pool "host-loop-pool" uses storage provider "hostloop" which is not supported for models of type "dummy"`) -} - func (s *serviceSuite) TestServiceDeployDefaultFilesystemStorage(c *gc.C) { - setupStoragePool(c, s.State) curl, ch := s.UploadCharm(c, "trusty/storage-filesystem-1", "storage-filesystem") err := application.AddCharmWithAuthorization(s.State, params.AddCharmWithAuthorization{ URL: curl.String(), diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/applicationscaler/facade.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/applicationscaler/facade.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/applicationscaler/facade.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/applicationscaler/facade.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,6 +6,7 @@ import ( "github.com/juju/errors" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" "github.com/juju/juju/state/watcher" @@ -27,11 +28,11 @@ // Facade allows model-manager clients to watch and rescale services. type Facade struct { backend Backend - resources *common.Resources + resources facade.Resources } // NewFacade creates a new authorized Facade. -func NewFacade(backend Backend, res *common.Resources, auth common.Authorizer) (*Facade, error) { +func NewFacade(backend Backend, res facade.Resources, auth facade.Authorizer) (*Facade, error) { if !auth.AuthModelManager() { return nil, common.ErrPerm } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/applicationscaler/shim.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/applicationscaler/shim.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/applicationscaler/shim.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/applicationscaler/shim.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,6 +7,7 @@ "github.com/juju/errors" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/state" ) @@ -20,7 +21,7 @@ } // newFacade wraps the supplied *state.State for the use of the Facade. -func newFacade(st *state.State, res *common.Resources, auth common.Authorizer) (*Facade, error) { +func newFacade(st *state.State, res facade.Resources, auth facade.Authorizer) (*Facade, error) { return NewFacade(backendShim{st}, res, auth) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/applicationscaler/util_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/applicationscaler/util_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/applicationscaler/util_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/applicationscaler/util_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,13 +10,14 @@ "github.com/juju/juju/apiserver/applicationscaler" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" ) -// mockAuth implements common.Authorizer for the tests' convenience. +// mockAuth implements facade.Authorizer for the tests' convenience. type mockAuth struct { - common.Authorizer + facade.Authorizer modelManager bool } @@ -25,7 +26,7 @@ } // auth is a convenience constructor for a mockAuth. -func auth(modelManager bool) common.Authorizer { +func auth(modelManager bool) facade.Authorizer { return mockAuth{modelManager: modelManager} } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/authcontext.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/authcontext.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/authcontext.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/authcontext.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,6 +9,7 @@ "time" "github.com/juju/errors" + "github.com/juju/utils/clock" "gopkg.in/juju/names.v2" "gopkg.in/macaroon-bakery.v1/bakery" "gopkg.in/macaroon-bakery.v1/httpbakery" @@ -49,8 +50,8 @@ return nil, errors.Trace(err) } ctxt.userAuth.Service = &expirableStorageBakeryService{bakeryService, key, store, nil} - // TODO(fwereade): 2016-03-17 lp:1558657 - ctxt.userAuth.Clock = state.GetClock() + // TODO(fwereade) 2016-07-21 there should be a clock parameter + ctxt.userAuth.Clock = clock.WallClock return ctxt, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/authhttp_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/authhttp_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/authhttp_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/authhttp_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -24,6 +24,7 @@ apitesting "github.com/juju/juju/api/testing" "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/core/description" "github.com/juju/juju/state" "github.com/juju/juju/testing" "github.com/juju/juju/testing/factory" @@ -93,15 +94,15 @@ } } -func (s *authHttpSuite) dialWebsocketFromURL(c *gc.C, server string, header http.Header) *websocket.Conn { - config := s.makeWebsocketConfigFromURL(c, server, header) +func dialWebsocketFromURL(c *gc.C, server string, header http.Header) *websocket.Conn { + config := makeWebsocketConfigFromURL(c, server, header) c.Logf("dialing %v", server) conn, err := websocket.DialConfig(config) c.Assert(err, jc.ErrorIsNil) return conn } -func (s *authHttpSuite) makeWebsocketConfigFromURL(c *gc.C, server string, header http.Header) *websocket.Config { +func makeWebsocketConfigFromURL(c *gc.C, server string, header http.Header) *websocket.Config { config, err := websocket.NewConfig(server, "http://localhost/") c.Assert(err, jc.ErrorIsNil) config.Header = header @@ -113,7 +114,7 @@ return config } -func (s *authHttpSuite) assertWebsocketClosed(c *gc.C, reader *bufio.Reader) { +func assertWebsocketClosed(c *gc.C, reader *bufio.Reader) { _, err := reader.ReadByte() c.Assert(err, gc.Equals, io.EOF) } @@ -234,9 +235,10 @@ envState := s.Factory.MakeModel(c, nil) s.AddCleanup(func(*gc.C) { envState.Close() }) user := s.Factory.MakeUser(c, nil) - _, err := envState.AddModelUser(state.ModelUserSpec{ + _, err := envState.AddModelUser(state.UserAccessSpec{ User: user.UserTag(), - CreatedBy: s.userTag}) + CreatedBy: s.userTag, + Access: description.ReadAccess}) c.Assert(err, jc.ErrorIsNil) s.userTag = user.UserTag() s.password = "password" diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/backups/backups.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/backups/backups.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/backups/backups.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/backups/backups.go 2016-08-16 08:56:25.000000000 +0000 @@ -12,6 +12,7 @@ "gopkg.in/mgo.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/controller" "github.com/juju/juju/environs/config" @@ -46,7 +47,7 @@ } // NewAPI creates a new instance of the Backups API facade. -func NewAPI(backend Backend, resources *common.Resources, authorizer common.Authorizer) (*API, error) { +func NewAPI(backend Backend, resources facade.Resources, authorizer facade.Authorizer) (*API, error) { if !authorizer.AuthClient() { return nil, errors.Trace(common.ErrPerm) } @@ -83,7 +84,7 @@ return &b, nil } -func extractResourceValue(resources *common.Resources, key string) (string, error) { +func extractResourceValue(resources facade.Resources, key string) (string, error) { res := resources.Get(key) strRes, ok := res.(common.StringResource) if !ok { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/backups/shim.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/backups/shim.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/backups/shim.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/backups/shim.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,6 +6,7 @@ import ( "github.com/juju/errors" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/state" ) @@ -31,6 +32,6 @@ return m.Series(), nil } -func newAPI(st *state.State, resources *common.Resources, authorizer common.Authorizer) (*API, error) { +func newAPI(st *state.State, resources facade.Resources, authorizer facade.Authorizer) (*API, error) { return NewAPI(&stateShim{st}, resources, authorizer) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/block/client.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/block/client.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/block/client.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/block/client.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,6 +7,7 @@ "github.com/juju/errors" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" ) @@ -33,14 +34,14 @@ // implementation of the api end point. type API struct { access blockAccess - authorizer common.Authorizer + authorizer facade.Authorizer } // NewAPI returns a new block API facade. func NewAPI( st *state.State, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, ) (*API, error) { if !authorizer.AuthClient() { return nil, common.ErrPerm diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/charmrevisionupdater/updater.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/charmrevisionupdater/updater.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/charmrevisionupdater/updater.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/charmrevisionupdater/updater.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,6 +8,7 @@ "github.com/juju/loggo" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/charmstore" "github.com/juju/juju/state" @@ -28,8 +29,8 @@ // implementation of the api end point. type CharmRevisionUpdaterAPI struct { state *state.State - resources *common.Resources - authorizer common.Authorizer + resources facade.Resources + authorizer facade.Authorizer } var _ CharmRevisionUpdater = (*CharmRevisionUpdaterAPI)(nil) @@ -37,8 +38,8 @@ // NewCharmRevisionUpdaterAPI creates a new server-side charmrevisionupdater API end point. func NewCharmRevisionUpdaterAPI( st *state.State, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, ) (*CharmRevisionUpdaterAPI, error) { if !authorizer.AuthMachineAgent() && !authorizer.AuthModelManager() { return nil, common.ErrPerm diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/charms/client.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/charms/client.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/charms/client.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/charms/client.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,6 +10,7 @@ "gopkg.in/juju/charm.v6-unstable/resource" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" ) @@ -33,14 +34,14 @@ // implementation of the api end point. type API struct { access charmsAccess - authorizer common.Authorizer + authorizer facade.Authorizer } // NewAPI returns a new charms API facade. func NewAPI( st *state.State, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, ) (*API, error) { if !authorizer.AuthClient() { return nil, common.ErrPerm diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/charms.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/charms.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/charms.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/charms.go 2016-08-16 08:56:25.000000000 +0000 @@ -61,8 +61,7 @@ if err != nil { return errors.Trace(err) } - // Add a local charm to the store provider. - // Requires a "series" query specifying the series to use for the charm. + // Add a charm to the store provider. charmURL, err := h.processPost(r, st) if err != nil { return errors.NewBadRequest(err, "") @@ -210,51 +209,79 @@ // processPost handles a charm upload POST request after authentication. func (h *charmsHandler) processPost(r *http.Request, st *state.State) (*charm.URL, error) { query := r.URL.Query() - series := query.Get("series") - if series == "" { - return nil, fmt.Errorf("expected series=URL argument") + schema := query.Get("schema") + if schema == "" { + schema = "local" } + series := query.Get("series") + // Make sure the content type is zip. contentType := r.Header.Get("Content-Type") if contentType != "application/zip" { return nil, fmt.Errorf("expected Content-Type: application/zip, got: %v", contentType) } - tempFile, err := ioutil.TempFile("", "charm") + + charmFileName, err := writeCharmToTempFile(r.Body) if err != nil { - return nil, fmt.Errorf("cannot create temp file: %v", err) - } - defer tempFile.Close() - defer os.Remove(tempFile.Name()) - if _, err := io.Copy(tempFile, r.Body); err != nil { - return nil, fmt.Errorf("error processing file upload: %v", err) + return nil, errors.Trace(err) } - err = h.processUploadedArchive(tempFile.Name()) + defer os.Remove(charmFileName) + + err = h.processUploadedArchive(charmFileName) if err != nil { return nil, err } - archive, err := charm.ReadCharmArchive(tempFile.Name()) + archive, err := charm.ReadCharmArchive(charmFileName) if err != nil { return nil, fmt.Errorf("invalid charm archive: %v", err) } + // We got it, now let's reserve a charm URL for it in state. - archiveURL := &charm.URL{ - Schema: "local", + curl := &charm.URL{ + Schema: schema, Name: archive.Meta().Name, Revision: archive.Revision(), Series: series, } - preparedURL, err := st.PrepareLocalCharmUpload(archiveURL) - if err != nil { - return nil, err + if schema == "local" { + curl, err = st.PrepareLocalCharmUpload(curl) + if err != nil { + return nil, err + } + } else { + // "cs:" charms may only be uploaded into models which are + // being imported during model migrations. There's currently + // no other time where it makes sense to accept charm store + // charms through this endpoint. + if isImporting, err := modelIsImporting(st); err != nil { + return nil, errors.Trace(err) + } else if !isImporting { + return nil, errors.New("cs charms may only be uploaded during model migration import") + } + + // If a revision argument is provided, it takes precedence + // over the revision in the charm archive. This is required to + // handle the revision differences between unpublished and + // published charms in the charm store. + revisionStr := query.Get("revision") + if revisionStr != "" { + curl.Revision, err = strconv.Atoi(revisionStr) + if err != nil { + return nil, errors.NotValidf("revision") + } + } + if _, err := st.PrepareStoreCharmUpload(curl); err != nil { + return nil, errors.Trace(err) + } } + // Now we need to repackage it with the reserved URL, upload it to // provider storage and update the state. - err = h.repackageAndUploadCharm(st, archive, preparedURL) + err = h.repackageAndUploadCharm(st, archive, curl) if err != nil { return nil, err } - // All done. - return preparedURL, nil + return curl, nil } // processUploadedArchive opens the given charm archive from path, @@ -482,3 +509,23 @@ file.Close() os.Remove(file.Name()) } + +func writeCharmToTempFile(r io.Reader) (string, error) { + tempFile, err := ioutil.TempFile("", "charm") + if err != nil { + return "", errors.Annotate(err, "creating temp file") + } + defer tempFile.Close() + if _, err := io.Copy(tempFile, r); err != nil { + return "", errors.Annotate(err, "processing upload") + } + return tempFile.Name(), nil +} + +func modelIsImporting(st *state.State) (bool, error) { + model, err := st.Model() + if err != nil { + return false, errors.Trace(err) + } + return model.MigrationMode() == state.MigrationModeImporting, nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/charms_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/charms_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/charms_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/charms_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -25,6 +25,7 @@ "github.com/juju/juju/state" "github.com/juju/juju/state/storage" "github.com/juju/juju/testcharms" + "github.com/juju/juju/testing/factory" ) // charmsCommonSuite wraps authHttpSuite and adds @@ -82,6 +83,13 @@ return charmResponse } +func (s *charmsCommonSuite) setModelImporting(c *gc.C) { + model, err := s.State.Model() + c.Assert(err, jc.ErrorIsNil) + err = model.SetMigrationMode(state.MigrationModeImporting) + c.Assert(err, jc.ErrorIsNil) +} + type charmsSuite struct { charmsCommonSuite } @@ -121,34 +129,24 @@ s.assertErrorResponse(c, resp, http.StatusMethodNotAllowed, `unsupported method: "PUT"`) } -func (s *charmsSuite) TestAuthRequiresUser(c *gc.C) { +func (s *charmsSuite) TestPOSTRequiresUserAuth(c *gc.C) { // Add a machine and try to login. - machine, err := s.State.AddMachine("quantal", state.JobHostUnits) - c.Assert(err, jc.ErrorIsNil) - err = machine.SetProvisioned("foo", "fake_nonce", nil) - c.Assert(err, jc.ErrorIsNil) - password, err := utils.RandomPassword() - c.Assert(err, jc.ErrorIsNil) - err = machine.SetPassword(password) - c.Assert(err, jc.ErrorIsNil) - + machine, password := s.Factory.MakeMachineReturningPassword(c, &factory.MachineParams{ + Nonce: "noncy", + }) resp := s.sendRequest(c, httpRequestParams{ - tag: machine.Tag().String(), - password: password, - method: "POST", - url: s.charmsURI(c, ""), - nonce: "fake_nonce", + tag: machine.Tag().String(), + password: password, + method: "POST", + url: s.charmsURI(c, ""), + nonce: "noncy", + contentType: "foo/bar", }) s.assertErrorResponse(c, resp, http.StatusInternalServerError, "tag kind machine not valid") // Now try a user login. resp = s.authRequest(c, httpRequestParams{method: "POST", url: s.charmsURI(c, "")}) - s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected series=URL argument") -} - -func (s *charmsSuite) TestUploadRequiresSeries(c *gc.C) { - resp := s.authRequest(c, httpRequestParams{method: "POST", url: s.charmsURI(c, "")}) - s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected series=URL argument") + s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected Content-Type: application/zip.+") } func (s *charmsSuite) TestUploadFailsWithInvalidZip(c *gc.C) { @@ -237,6 +235,13 @@ c.Assert(downloadedSHA256, gc.Equals, expectedSHA256) } +func (s *charmsSuite) TestUploadWithMultiSeriesCharm(c *gc.C) { + ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") + resp := s.uploadRequest(c, s.charmsURL(c, "").String(), "application/zip", ch.Path) + expectedURL := charm.MustParseURL("local:dummy-1") + s.assertUploadResponse(c, resp, expectedURL.String()) +} + func (s *charmsSuite) TestUploadAllowsTopLevelPath(c *gc.C) { ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") // Backwards compatibility check, that we can upload charms to @@ -331,6 +336,56 @@ c.Assert(bundle.Config(), jc.DeepEquals, sch.Config()) } +func (s *charmsSuite) TestNonLocalCharmUploadFailsIfNotMigrating(c *gc.C) { + ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") + curl := charm.MustParseURL( + fmt.Sprintf("cs:quantal/%s-%d", ch.Meta().Name, ch.Revision()), + ) + info := state.CharmInfo{ + Charm: ch, + ID: curl, + StoragePath: "dummy-storage-path", + SHA256: "dummy-1-sha256", + } + _, err := s.State.AddCharm(info) + c.Assert(err, jc.ErrorIsNil) + + resp := s.uploadRequest(c, s.charmsURI(c, "?schema=cs&series=quantal"), "application/zip", ch.Path) + s.assertErrorResponse(c, resp, 400, "cs charms may only be uploaded during model migration import") +} + +func (s *charmsSuite) TestNonLocalCharmUpload(c *gc.C) { + // Check that upload of charms with the "cs:" schema works (for + // model migrations). + s.setModelImporting(c) + ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") + + resp := s.uploadRequest(c, s.charmsURI(c, "?schema=cs&series=quantal"), "application/zip", ch.Path) + + expectedURL := charm.MustParseURL("cs:quantal/dummy-1") + s.assertUploadResponse(c, resp, expectedURL.String()) + sch, err := s.State.Charm(expectedURL) + c.Assert(err, jc.ErrorIsNil) + c.Assert(sch.URL(), gc.DeepEquals, expectedURL) + c.Assert(sch.Revision(), gc.Equals, 1) + c.Assert(sch.IsUploaded(), jc.IsTrue) +} + +func (s *charmsSuite) TestNonLocalCharmUploadWithRevisionOverride(c *gc.C) { + s.setModelImporting(c) + ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") + + resp := s.uploadRequest(c, s.charmsURI(c, "?schema=cs&revision=99"), "application/zip", ch.Path) + + expectedURL := charm.MustParseURL("cs:dummy-99") + s.assertUploadResponse(c, resp, expectedURL.String()) + sch, err := s.State.Charm(expectedURL) + c.Assert(err, jc.ErrorIsNil) + c.Assert(sch.URL(), gc.DeepEquals, expectedURL) + c.Assert(sch.Revision(), gc.Equals, 99) + c.Assert(sch.IsUploaded(), jc.IsTrue) +} + func (s *charmsSuite) TestGetRequiresCharmURL(c *gc.C) { uri := s.charmsURI(c, "?file=hooks/install") resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri}) @@ -407,6 +462,35 @@ } } +func (s *charmsSuite) TestGetWorksForControllerMachines(c *gc.C) { + // Make a controller machine. + const nonce = "noncey" + m, password := s.Factory.MakeMachineReturningPassword(c, &factory.MachineParams{ + Jobs: []state.MachineJob{state.JobManageModel}, + Nonce: nonce, + }) + + // Create a hosted model and upload a charm for it. + envState := s.setupOtherModel(c) + ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") + s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path) + + // Controller machine should be able to download the charm from + // the hosted model. This is required for controller workers which + // are acting on behalf of a particular hosted model. + url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision") + url.Path = fmt.Sprintf("/model/%s/charms", envState.ModelUUID()) + params := httpRequestParams{ + method: "GET", + url: url.String(), + tag: m.Tag().String(), + password: password, + nonce: nonce, + } + resp := s.sendRequest(c, params) + s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8") +} + func (s *charmsSuite) TestGetStarReturnsArchiveBytes(c *gc.C) { // Add the dummy charm. ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") @@ -531,11 +615,12 @@ return s.userTag.Id() } resp := s.sendRequest(c, httpRequestParams{ - do: s.doer(), - method: "POST", - url: s.charmsURI(c, ""), + do: s.doer(), + method: "POST", + url: s.charmsURI(c, ""), + contentType: "foo/bar", }) - s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected series=URL argument") + s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected Content-Type: application/zip.+") c.Assert(checkCount, gc.Equals, 1) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/cleaner/cleaner.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/cleaner/cleaner.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/cleaner/cleaner.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/cleaner/cleaner.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,6 +8,7 @@ import ( "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" "github.com/juju/juju/state/watcher" @@ -20,14 +21,14 @@ // CleanerAPI implements the API used by the cleaner worker. type CleanerAPI struct { st StateInterface - resources *common.Resources + resources facade.Resources } // NewCleanerAPI creates a new instance of the Cleaner API. func NewCleanerAPI( st *state.State, - res *common.Resources, - authorizer common.Authorizer, + res facade.Resources, + authorizer facade.Authorizer, ) (*CleanerAPI, error) { if !authorizer.AuthModelManager() { return nil, common.ErrPerm diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/api_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/api_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/api_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/api_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -16,7 +16,6 @@ commontesting "github.com/juju/juju/apiserver/common/testing" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/constraints" - "github.com/juju/juju/environs" "github.com/juju/juju/environs/config" "github.com/juju/juju/instance" "github.com/juju/juju/juju/testing" @@ -24,8 +23,6 @@ "github.com/juju/juju/state" "github.com/juju/juju/state/multiwatcher" "github.com/juju/juju/status" - "github.com/juju/juju/storage/poolmanager" - "github.com/juju/juju/storage/provider" coretesting "github.com/juju/juju/testing" "github.com/juju/juju/testing/factory" "github.com/juju/juju/worker" @@ -129,7 +126,7 @@ stateInfo.Password = password st, err := state.Open(s.State.ModelTag(), stateInfo, mongo.DialOpts{ Timeout: 25 * time.Millisecond, - }, environs.NewStatePolicy()) + }, nil) if err == nil { st.Close() } @@ -477,16 +474,6 @@ return } -func (s *baseSuite) setupStoragePool(c *gc.C) { - pm := poolmanager.New(state.NewStateSettings(s.State)) - _, err := pm.Create("loop-pool", provider.LoopProviderType, map[string]interface{}{}) - c.Assert(err, jc.ErrorIsNil) - err = s.State.UpdateModelConfig(map[string]interface{}{ - "storage-default-block-source": "loop-pool", - }, nil, nil) - c.Assert(err, jc.ErrorIsNil) -} - func (s *baseSuite) setAgentPresence(c *gc.C, u *state.Unit) { pinger, err := u.SetAgentPresence() c.Assert(err, jc.ErrorIsNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/backend.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/backend.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/backend.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/backend.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,85 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package client + +import ( + "github.com/juju/version" + "gopkg.in/juju/charm.v6-unstable" + "gopkg.in/juju/names.v2" + + "github.com/juju/juju/constraints" + "github.com/juju/juju/core/description" + "github.com/juju/juju/environs/config" + "github.com/juju/juju/instance" + "github.com/juju/juju/network" + "github.com/juju/juju/state" + "github.com/juju/juju/status" +) + +// Unit represents a state.Unit. +type Unit interface { + status.StatusHistoryGetter + Life() state.Life + Destroy() (err error) + IsPrincipal() bool + PublicAddress() (network.Address, error) + PrivateAddress() (network.Address, error) + Resolve(retryHooks bool) error + AgentHistory() status.StatusHistoryGetter +} + +// Backend contains the state.State methods used in this package, +// allowing stubs to be created for testing. +type Backend interface { + FindEntity(names.Tag) (state.Entity, error) + Unit(string) (Unit, error) + Application(string) (*state.Application, error) + Machine(string) (*state.Machine, error) + AllMachines() ([]*state.Machine, error) + AllApplications() ([]*state.Application, error) + AllRelations() ([]*state.Relation, error) + AddOneMachine(state.MachineTemplate) (*state.Machine, error) + AddMachineInsideMachine(state.MachineTemplate, string, instance.ContainerType) (*state.Machine, error) + AddMachineInsideNewMachine(template, parentTemplate state.MachineTemplate, containerType instance.ContainerType) (*state.Machine, error) + ModelConstraints() (constraints.Value, error) + ModelConfig() (*config.Config, error) + ModelConfigValues() (config.ConfigValues, error) + UpdateModelConfig(map[string]interface{}, []string, state.ValidateConfigFunc) error + SetModelConstraints(constraints.Value) error + ModelUUID() string + ModelTag() names.ModelTag + Model() (*state.Model, error) + ForModel(tag names.ModelTag) (*state.State, error) + SetModelAgentVersion(version.Number) error + SetAnnotations(state.GlobalEntity, map[string]string) error + Annotations(state.GlobalEntity) (map[string]string, error) + InferEndpoints(...string) ([]state.Endpoint, error) + EndpointsRelation(...state.Endpoint) (*state.Relation, error) + Charm(*charm.URL) (*state.Charm, error) + LatestPlaceholderCharm(*charm.URL) (*state.Charm, error) + AddRelation(...state.Endpoint) (*state.Relation, error) + AddModelUser(state.UserAccessSpec) (description.UserAccess, error) + AddControllerUser(state.UserAccessSpec) (description.UserAccess, error) + RemoveUserAccess(names.UserTag, names.Tag) error + Watch() *state.Multiwatcher + AbortCurrentUpgrade() error + APIHostPorts() ([][]network.HostPort, error) + LatestModelMigration() (state.ModelMigration, error) +} + +func NewStateBackend(st *state.State) Backend { + return stateShim{st} +} + +type stateShim struct { + *state.State +} + +func (s stateShim) Unit(name string) (Unit, error) { + u, err := s.State.Unit(name) + if err != nil { + return nil, err + } + return u, nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/bundles_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/bundles_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/bundles_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/bundles_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -101,29 +101,37 @@ c.Assert(r.Changes, jc.DeepEquals, []*params.BundleChangesChange{{ Id: "addCharm-0", Method: "addCharm", - Args: []interface{}{"django"}, + Args: []interface{}{"django", ""}, }, { Id: "deploy-1", Method: "deploy", Args: []interface{}{ - "$addCharm-0", "django", - map[string]interface{}{"debug": true}, "", + "$addCharm-0", + "", + "django", + map[string]interface{}{"debug": true}, + "", map[string]string{"tmpfs": "tmpfs,1G"}, map[string]string{}, + map[string]int{}, }, Requires: []string{"addCharm-0"}, }, { Id: "addCharm-2", Method: "addCharm", - Args: []interface{}{"cs:trusty/haproxy-42"}, + Args: []interface{}{"cs:trusty/haproxy-42", "trusty"}, }, { Id: "deploy-3", Method: "deploy", Args: []interface{}{ - "$addCharm-2", "haproxy", - map[string]interface{}{}, "", + "$addCharm-2", + "trusty", + "haproxy", + map[string]interface{}{}, + "", map[string]string{}, map[string]string{}, + map[string]int{}, }, Requires: []string{"addCharm-2"}, }, { @@ -156,11 +164,13 @@ Method: "deploy", Args: []interface{}{ "$addCharm-0", + "", "django", map[string]interface{}{}, "", map[string]string{}, map[string]string{"url": "public"}, + map[string]int{}, }, Requires: []string{"addCharm-0"}, }) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/client.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/client.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/client.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/client.go 2016-08-16 08:56:25.000000000 +0000 @@ -12,26 +12,30 @@ "github.com/juju/juju/apiserver/application" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" + "github.com/juju/juju/apiserver/modelconfig" "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/core/description" "github.com/juju/juju/environs" "github.com/juju/juju/environs/config" "github.com/juju/juju/environs/manual" "github.com/juju/juju/instance" "github.com/juju/juju/network" "github.com/juju/juju/state" + "github.com/juju/juju/state/stateenvirons" jujuversion "github.com/juju/juju/version" ) func init() { - common.RegisterStandardFacade("Client", 1, NewClient) + common.RegisterStandardFacade("Client", 1, newClient) } var logger = loggo.GetLogger("juju.apiserver.client") type API struct { - stateAccessor stateInterface - auth common.Authorizer - resources *common.Resources + stateAccessor Backend + auth facade.Authorizer + resources facade.Resources client *Client // statusSetter provides common methods for updating an entity's provisioning status. statusSetter *common.StatusSetter @@ -43,35 +47,71 @@ // Until all code is refactored to use interfaces, we // need this helper to keep older code happy. func (api *API) state() *state.State { - return api.stateAccessor.(*stateShim).State + return api.stateAccessor.(stateShim).State } // Client serves client-specific API methods. type Client struct { - api *API - check *common.BlockChecker -} - -var getState = func(st *state.State) stateInterface { - return &stateShim{st} + // TODO(wallyworld) - we'll retain model config facade methods + // on the client facade until GUI and Python client library are updated. + *modelconfig.ModelConfigAPI + + api *API + newEnviron func() (environs.Environ, error) + check *common.BlockChecker +} + +func newClient(st *state.State, resources facade.Resources, authorizer facade.Authorizer) (*Client, error) { + urlGetter := common.NewToolsURLGetter(st.ModelUUID(), st) + configGetter := stateenvirons.EnvironConfigGetter{st} + statusSetter := common.NewStatusSetter(st, common.AuthAlways()) + toolsFinder := common.NewToolsFinder(configGetter, st, urlGetter) + newEnviron := func() (environs.Environ, error) { + return environs.GetEnviron(configGetter, environs.New) + } + blockChecker := common.NewBlockChecker(st) + modelConfigAPI, err := modelconfig.NewModelConfigAPI(st, authorizer) + if err != nil { + return nil, errors.Trace(err) + } + return NewClient( + NewStateBackend(st), + modelConfigAPI, + resources, + authorizer, + statusSetter, + toolsFinder, + newEnviron, + blockChecker, + ) } // NewClient creates a new instance of the Client Facade. -func NewClient(st *state.State, resources *common.Resources, authorizer common.Authorizer) (*Client, error) { +func NewClient( + st Backend, + modelConfigAPI *modelconfig.ModelConfigAPI, + resources facade.Resources, + authorizer facade.Authorizer, + statusSetter *common.StatusSetter, + toolsFinder *common.ToolsFinder, + newEnviron func() (environs.Environ, error), + blockChecker *common.BlockChecker, +) (*Client, error) { if !authorizer.AuthClient() { return nil, common.ErrPerm } - apiState := getState(st) - urlGetter := common.NewToolsURLGetter(apiState.ModelUUID(), apiState) client := &Client{ - api: &API{ - stateAccessor: apiState, + modelConfigAPI, + &API{ + stateAccessor: st, auth: authorizer, resources: resources, - statusSetter: common.NewStatusSetter(st, common.AuthAlways()), - toolsFinder: common.NewToolsFinder(st, st, urlGetter), + statusSetter: statusSetter, + toolsFinder: toolsFinder, }, - check: common.NewBlockChecker(st)} + newEnviron, + blockChecker, + } return client, nil } @@ -341,6 +381,10 @@ return info, nil } +func modelInfo(st *state.State, user description.UserAccess) (params.ModelUserInfo, error) { + return common.ModelUserInfo(user, st) +} + // ModelUserInfo returns information on all users in the model. func (c *Client) ModelUserInfo() (params.ModelUserInfoResults, error) { var results params.ModelUserInfoResults @@ -355,7 +399,7 @@ for _, user := range users { var result params.ModelUserInfoResult - userInfo, err := common.ModelUserInfo(user) + userInfo, err := modelInfo(c.api.state(), user) if err != nil { result.Error = common.ServerError(err) } else { @@ -371,72 +415,14 @@ return params.AgentVersionResult{Version: jujuversion.Current}, nil } -// ModelGet implements the server-side part of the -// get-model-config CLI command. -func (c *Client) ModelGet() (params.ModelConfigResults, error) { - result := params.ModelConfigResults{} - values, err := c.api.stateAccessor.ModelConfigValues() - if err != nil { - return result, err - } - result.Config = make(map[string]params.ConfigValue) - for attr, val := range values { - result.Config[attr] = params.ConfigValue{ - Value: val.Value, - Source: val.Source, - } - } - return result, nil -} - -// ModelSet implements the server-side part of the -// set-model-config CLI command. -func (c *Client) ModelSet(args params.ModelSet) error { - if err := c.check.ChangeAllowed(); err != nil { - return errors.Trace(err) - } - // Make sure we don't allow changing agent-version. - checkAgentVersion := func(updateAttrs map[string]interface{}, removeAttrs []string, oldConfig *config.Config) error { - if v, found := updateAttrs["agent-version"]; found { - oldVersion, _ := oldConfig.AgentVersion() - if v != oldVersion.String() { - return fmt.Errorf("agent-version cannot be changed") - } - } - return nil - } - // Replace any deprecated attributes with their new values. - attrs := config.ProcessDeprecatedAttributes(args.Config) - // 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.stateAccessor.UpdateModelConfig(attrs, nil, checkAgentVersion) -} - -// ModelUnset implements the server-side part of the -// set-model-config CLI command. -func (c *Client) ModelUnset(args params.ModelUnset) error { - if err := c.check.ChangeAllowed(); err != nil { - return errors.Trace(err) - } - // 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.stateAccessor.UpdateModelConfig(nil, args.Keys, nil) -} - // SetModelAgentVersion sets the model agent version. func (c *Client) SetModelAgentVersion(args params.SetModelAgentVersion) error { if err := c.check.ChangeAllowed(); err != nil { return errors.Trace(err) } // Before changing the agent version to trigger an upgrade or downgrade, - // we'll do a very basic check to ensure the - cfg, err := c.api.stateAccessor.ModelConfig() - if err != nil { - return errors.Trace(err) - } - env, err := getEnvironment(cfg) + // we'll do a very basic check to ensure the environment is accessible. + env, err := c.newEnviron() if err != nil { return errors.Trace(err) } @@ -446,14 +432,6 @@ return c.api.stateAccessor.SetModelAgentVersion(args.Version) } -var getEnvironment = func(cfg *config.Config) (environs.Environ, error) { - env, err := environs.New(cfg) - if err != nil { - return nil, err - } - return env, nil -} - // AbortCurrentUpgrade aborts and archives the current upgrade // synchronisation record, if any. func (c *Client) AbortCurrentUpgrade() error { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/client_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/client_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/client_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/client_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -22,9 +22,11 @@ "github.com/juju/juju/agent" "github.com/juju/juju/apiserver/client" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/modelconfig" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/apiserver/testing" "github.com/juju/juju/constraints" + "github.com/juju/juju/core/description" "github.com/juju/juju/environs" "github.com/juju/juju/environs/config" "github.com/juju/juju/environs/manual" @@ -35,6 +37,7 @@ "github.com/juju/juju/state" "github.com/juju/juju/state/multiwatcher" "github.com/juju/juju/state/presence" + "github.com/juju/juju/state/stateenvirons" "github.com/juju/juju/status" coretesting "github.com/juju/juju/testing" "github.com/juju/juju/testing/factory" @@ -44,12 +47,16 @@ type serverSuite struct { baseSuite - client *client.Client + client *client.Client + newEnviron func() (environs.Environ, error) } var _ = gc.Suite(&serverSuite{}) func (s *serverSuite) SetUpTest(c *gc.C) { + s.ConfigAttrs = map[string]interface{}{ + "authorized-keys": coretesting.FakeAuthKeys, + } s.baseSuite.SetUpTest(c) var err error @@ -57,7 +64,29 @@ Tag: s.AdminUserTag(c), EnvironManager: true, } - s.client, err = client.NewClient(s.State, common.NewResources(), auth) + urlGetter := common.NewToolsURLGetter(s.State.ModelUUID(), s.State) + configGetter := stateenvirons.EnvironConfigGetter{s.State} + statusSetter := common.NewStatusSetter(s.State, common.AuthAlways()) + toolsFinder := common.NewToolsFinder(configGetter, s.State, urlGetter) + s.newEnviron = func() (environs.Environ, error) { + return environs.GetEnviron(configGetter, environs.New) + } + newEnviron := func() (environs.Environ, error) { + return s.newEnviron() + } + blockChecker := common.NewBlockChecker(s.State) + modelConfigAPI, err := modelconfig.NewModelConfigAPI(s.State, auth) + c.Assert(err, jc.ErrorIsNil) + s.client, err = client.NewClient( + client.NewStateBackend(s.State), + modelConfigAPI, + common.NewResources(), + auth, + statusSetter, + toolsFinder, + newEnviron, + blockChecker, + ) c.Assert(err, jc.ErrorIsNil) } @@ -77,26 +106,26 @@ func (s *serverSuite) TestModelUsersInfo(c *gc.C) { testAdmin := s.AdminUserTag(c) - owner, err := s.State.ModelUser(testAdmin) + owner, err := s.State.UserAccess(testAdmin, s.State.ModelTag()) c.Assert(err, jc.ErrorIsNil) localUser1 := s.makeLocalModelUser(c, "ralphdoe", "Ralph Doe") localUser2 := s.makeLocalModelUser(c, "samsmith", "Sam Smith") - remoteUser1 := s.Factory.MakeModelUser(c, &factory.ModelUserParams{User: "bobjohns@ubuntuone", DisplayName: "Bob Johns", Access: state.WriteAccess}) - remoteUser2 := s.Factory.MakeModelUser(c, &factory.ModelUserParams{User: "nicshaw@idprovider", DisplayName: "Nic Shaw", Access: state.WriteAccess}) + remoteUser1 := s.Factory.MakeModelUser(c, &factory.ModelUserParams{User: "bobjohns@ubuntuone", DisplayName: "Bob Johns", Access: description.WriteAccess}) + remoteUser2 := s.Factory.MakeModelUser(c, &factory.ModelUserParams{User: "nicshaw@idprovider", DisplayName: "Nic Shaw", Access: description.WriteAccess}) results, err := s.client.ModelUserInfo() c.Assert(err, jc.ErrorIsNil) var expected params.ModelUserInfoResults for _, r := range []struct { - user *state.ModelUser + user description.UserAccess info *params.ModelUserInfo }{ { owner, ¶ms.ModelUserInfo{ - UserName: owner.UserName(), - DisplayName: owner.DisplayName(), + UserName: owner.UserName, + DisplayName: owner.DisplayName, Access: "admin", }, }, { @@ -129,7 +158,7 @@ }, }, } { - r.info.LastConnection = lastConnPointer(c, r.user) + r.info.LastConnection = lastConnPointer(c, r.user, s.State) expected.Results = append(expected.Results, params.ModelUserInfoResult{Result: r.info}) } @@ -138,8 +167,8 @@ c.Assert(results, jc.DeepEquals, expected) } -func lastConnPointer(c *gc.C, modelUser *state.ModelUser) *time.Time { - lastConn, err := modelUser.LastConnection() +func lastConnPointer(c *gc.C, modelUser description.UserAccess, st *state.State) *time.Time { + lastConn, err := st.LastModelConnection(modelUser.UserTag) if err != nil { if state.IsNeverConnectedError(err) { return nil @@ -157,10 +186,10 @@ func (a ByUserName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByUserName) Less(i, j int) bool { return a[i].Result.UserName < a[j].Result.UserName } -func (s *serverSuite) makeLocalModelUser(c *gc.C, username, displayname string) *state.ModelUser { +func (s *serverSuite) makeLocalModelUser(c *gc.C, username, displayname string) description.UserAccess { // factory.MakeUser will create an ModelUser for a local user by defalut user := s.Factory.MakeUser(c, &factory.UserParams{Name: username, DisplayName: displayname}) - modelUser, err := s.State.ModelUser(user.UserTag()) + modelUser, err := s.State.UserAccess(user.UserTag(), s.State.ModelTag()) c.Assert(err, jc.ErrorIsNil) return modelUser } @@ -192,9 +221,9 @@ func (s *serverSuite) assertCheckProviderAPI(c *gc.C, envError error, expectErr string) { env := &mockEnviron{err: envError} - s.PatchValue(client.GetEnvironment, func(cfg *config.Config) (environs.Environ, error) { + s.newEnviron = func() (environs.Environ, error) { return env, nil - }) + } args := params.SetModelAgentVersion{ Version: version.MustParse("9.8.7"), } @@ -214,7 +243,7 @@ } func (s *serverSuite) TestCheckProviderAPIFail(c *gc.C) { - s.assertCheckProviderAPI(c, fmt.Errorf("instances error"), "cannot make API call to provider: instances error") + s.assertCheckProviderAPI(c, errors.New("instances error"), "cannot make API call to provider: instances error") } func (s *serverSuite) assertSetEnvironAgentVersion(c *gc.C) { @@ -727,130 +756,6 @@ c.Assert(addr, gc.Equals, "private") } -func (s *serverSuite) TestClientModelGet(c *gc.C) { - modelConfig, err := s.State.ModelConfig() - c.Assert(err, jc.ErrorIsNil) - result, err := s.client.ModelGet() - c.Assert(err, jc.ErrorIsNil) - cfg := make(map[string]params.ConfigValue) - for name, val := range modelConfig.AllAttrs() { - cfg[name] = params.ConfigValue{ - Value: val, - Source: "model", - } - } - c.Assert(result.Config, gc.DeepEquals, cfg) -} - -func (s *serverSuite) assertEnvValue(c *gc.C, key string, expected interface{}) { - modelConfig, err := s.State.ModelConfig() - c.Assert(err, jc.ErrorIsNil) - value, found := modelConfig.AllAttrs()[key] - c.Assert(found, jc.IsTrue) - c.Assert(value, gc.Equals, expected) -} - -func (s *serverSuite) assertEnvValueMissing(c *gc.C, key string) { - modelConfig, err := s.State.ModelConfig() - c.Assert(err, jc.ErrorIsNil) - _, found := modelConfig.AllAttrs()[key] - c.Assert(found, jc.IsFalse) -} - -func (s *serverSuite) TestClientModelSet(c *gc.C) { - modelConfig, err := s.State.ModelConfig() - c.Assert(err, jc.ErrorIsNil) - _, found := modelConfig.AllAttrs()["some-key"] - c.Assert(found, jc.IsFalse) - - params := params.ModelSet{ - Config: map[string]interface{}{ - "some-key": "value", - "other-key": "other value"}, - } - err = s.client.ModelSet(params) - c.Assert(err, jc.ErrorIsNil) - s.assertEnvValue(c, "some-key", "value") - s.assertEnvValue(c, "other-key", "other value") -} - -func (s *serverSuite) TestClientModelSetImmutable(c *gc.C) { - // The various immutable config values are tested in - // environs/config/config_test.go, so just choosing one here. - params := params.ModelSet{ - Config: map[string]interface{}{"firewall-mode": "global"}, - } - err := s.client.ModelSet(params) - c.Check(err, gc.ErrorMatches, `cannot change firewall-mode from .* to "global"`) -} - -func (s *serverSuite) assertModelSetBlocked(c *gc.C, args map[string]interface{}, msg string) { - err := s.client.ModelSet(params.ModelSet{args}) - s.AssertBlocked(c, err, msg) -} - -func (s *serverSuite) TestBlockChangesClientModelSet(c *gc.C) { - s.BlockAllChanges(c, "TestBlockChangesClientModelSet") - args := map[string]interface{}{"some-key": "value"} - s.assertModelSetBlocked(c, args, "TestBlockChangesClientModelSet") -} - -func (s *serverSuite) TestClientModelSetCannotChangeAgentVersion(c *gc.C) { - args := params.ModelSet{ - map[string]interface{}{"agent-version": "9.9.9"}, - } - err := s.client.ModelSet(args) - c.Assert(err, gc.ErrorMatches, "agent-version cannot be changed") - - // It's okay to pass env back with the same agent-version. - result, err := s.client.ModelGet() - c.Assert(err, jc.ErrorIsNil) - c.Assert(result.Config["agent-version"], gc.NotNil) - args.Config["agent-version"] = result.Config["agent-version"].Value - err = s.client.ModelSet(args) - c.Assert(err, jc.ErrorIsNil) -} - -func (s *serverSuite) TestClientModelUnset(c *gc.C) { - err := s.State.UpdateModelConfig(map[string]interface{}{"abc": 123}, nil, nil) - c.Assert(err, jc.ErrorIsNil) - - args := params.ModelUnset{[]string{"abc"}} - err = s.client.ModelUnset(args) - c.Assert(err, jc.ErrorIsNil) - s.assertEnvValueMissing(c, "abc") -} - -func (s *serverSuite) TestBlockClientModelUnset(c *gc.C) { - err := s.State.UpdateModelConfig(map[string]interface{}{"abc": 123}, nil, nil) - c.Assert(err, jc.ErrorIsNil) - s.BlockAllChanges(c, "TestBlockClientModelUnset") - - args := params.ModelUnset{[]string{"abc"}} - err = s.client.ModelUnset(args) - s.AssertBlocked(c, err, "TestBlockClientModelUnset") -} - -func (s *serverSuite) TestClientModelUnsetMissing(c *gc.C) { - // It's okay to unset a non-existent attribute. - args := params.ModelUnset{[]string{"not_there"}} - err := s.client.ModelUnset(args) - c.Assert(err, jc.ErrorIsNil) -} - -func (s *serverSuite) TestClientModelUnsetError(c *gc.C) { - err := s.State.UpdateModelConfig(map[string]interface{}{"abc": 123}, nil, nil) - c.Assert(err, jc.ErrorIsNil) - - // "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. - args := params.ModelUnset{[]string{"abc", "type"}} - err = s.client.ModelUnset(args) - c.Assert(err, gc.ErrorMatches, "type: expected string, got nothing") - s.assertEnvValue(c, "abc", 123) -} - func (s *clientSuite) TestClientFindTools(c *gc.C) { result, err := s.APIState.Client().FindTools(99, -1, "", "") c.Assert(err, jc.ErrorIsNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/export_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -3,8 +3,6 @@ package client -import "github.com/juju/juju/state" - // Filtering exports var ( MatchPortRanges = matchPortRanges @@ -18,19 +16,3 @@ ) type MachineAndContainers machineAndContainers - -var ( - GetEnvironment = &getEnvironment -) - -type StateInterface stateInterface - -type Patcher interface { - PatchValue(ptr, value interface{}) -} - -func PatchState(p Patcher, st StateInterface) { - p.PatchValue(&getState, func(*state.State) stateInterface { - return st - }) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/instanceconfig.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/instanceconfig.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/instanceconfig.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/instanceconfig.go 2016-08-16 08:56:25.000000000 +0000 @@ -15,6 +15,7 @@ "github.com/juju/juju/cloudconfig/instancecfg" "github.com/juju/juju/controller/authentication" "github.com/juju/juju/state" + "github.com/juju/juju/state/stateenvirons" ) // InstanceConfig returns information from the environment config that @@ -52,7 +53,8 @@ return nil, errors.Annotate(err, "getting state model") } urlGetter := common.NewToolsURLGetter(environment.UUID(), st) - toolsFinder := common.NewToolsFinder(st, st, urlGetter) + configGetter := stateenvirons.EnvironConfigGetter{st} + toolsFinder := common.NewToolsFinder(configGetter, st, urlGetter) findToolsResult, err := toolsFinder.FindTools(params.FindToolsParams{ Number: agentVersion, MajorVersion: -1, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/instanceconfig_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/instanceconfig_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/instanceconfig_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/instanceconfig_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -12,14 +12,12 @@ "github.com/juju/version" gc "gopkg.in/check.v1" - "github.com/juju/juju/agent" "github.com/juju/juju/apiserver/client" "github.com/juju/juju/apiserver/params" envtools "github.com/juju/juju/environs/tools" "github.com/juju/juju/instance" "github.com/juju/juju/juju/testing" "github.com/juju/juju/network" - "github.com/juju/juju/state" "github.com/juju/juju/state/multiwatcher" jujutesting "github.com/juju/juju/testing" coretools "github.com/juju/juju/tools" @@ -59,34 +57,6 @@ c.Assert(instanceConfig.ToolsList().URLs(), jc.DeepEquals, map[version.Binary][]string{ instanceConfig.AgentVersion(): []string{toolsURL}, }) - c.Assert(instanceConfig.AgentEnvironment[agent.AllowsSecureConnection], gc.Equals, "true") -} - -func (s *machineConfigSuite) TestSecureConnectionDisallowed(c *gc.C) { - // StateServingInfo without CAPrivateKey will not allow secure connections. - servingInfo := state.StateServingInfo{ - PrivateKey: jujutesting.ServerKey, - Cert: jujutesting.ServerCert, - SharedSecret: "really, really secret", - APIPort: 4321, - StatePort: 1234, - } - s.State.SetStateServingInfo(servingInfo) - hc := instance.MustParseHardware("mem=4G arch=amd64") - apiParams := params.AddMachineParams{ - Jobs: []multiwatcher.MachineJob{multiwatcher.JobHostUnits}, - InstanceId: instance.Id("1234"), - Nonce: "foo", - HardwareCharacteristics: hc, - } - machines, err := s.APIState.Client().AddMachines([]params.AddMachineParams{apiParams}) - c.Assert(err, jc.ErrorIsNil) - c.Assert(len(machines), gc.Equals, 1) - - machineId := machines[0].Machine - instanceConfig, err := client.InstanceConfig(s.State, machineId, apiParams.Nonce, "") - c.Assert(err, jc.ErrorIsNil) - c.Assert(instanceConfig.AgentEnvironment[agent.AllowsSecureConnection], gc.Equals, "false") } func (s *machineConfigSuite) TestMachineConfigNoArch(c *gc.C) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/perm_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/perm_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/perm_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/perm_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -16,6 +16,7 @@ "github.com/juju/juju/api" "github.com/juju/juju/api/annotations" "github.com/juju/juju/api/application" + "github.com/juju/juju/api/modelconfig" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/charmstore" "github.com/juju/juju/constraints" @@ -138,15 +139,15 @@ allow: []names.Tag{userAdmin, userOther}, }, { about: "Client.ModelGet", - op: opClientEnvironmentGet, + op: opClientModelGet, allow: []names.Tag{userAdmin, userOther}, }, { about: "Client.ModelSet", - op: opClientEnvironmentSet, + op: opClientModelSet, allow: []names.Tag{userAdmin, userOther}, }, { about: "Client.SetModelAgentVersion", - op: opClientSetEnvironAgentVersion, + op: opClientSetModelAgentVersion, allow: []names.Tag{userAdmin, userOther}, }, { about: "Client.WatchAll", @@ -379,28 +380,28 @@ return func() {}, nil } -func opClientEnvironmentGet(c *gc.C, st api.Connection, mst *state.State) (func(), error) { - _, err := st.Client().ModelGet() +func opClientModelGet(c *gc.C, st api.Connection, mst *state.State) (func(), error) { + _, err := modelconfig.NewClient(st).ModelGet() if err != nil { return func() {}, err } return func() {}, nil } -func opClientEnvironmentSet(c *gc.C, st api.Connection, mst *state.State) (func(), error) { +func opClientModelSet(c *gc.C, st api.Connection, mst *state.State) (func(), error) { args := map[string]interface{}{"some-key": "some-value"} - err := st.Client().ModelSet(args) + err := modelconfig.NewClient(st).ModelSet(args) if err != nil { return func() {}, err } return func() { args["some-key"] = nil - st.Client().ModelSet(args) + modelconfig.NewClient(st).ModelSet(args) }, nil } -func opClientSetEnvironAgentVersion(c *gc.C, st api.Connection, mst *state.State) (func(), error) { - attrs, err := st.Client().ModelGet() +func opClientSetModelAgentVersion(c *gc.C, st api.Connection, mst *state.State) (func(), error) { + attrs, err := modelconfig.NewClient(st).ModelGet() if err != nil { return func() {}, err } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/state.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/state.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/state.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/state.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,78 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package client - -import ( - "github.com/juju/version" - "gopkg.in/juju/charm.v6-unstable" - "gopkg.in/juju/names.v2" - - "github.com/juju/juju/constraints" - "github.com/juju/juju/environs/config" - "github.com/juju/juju/instance" - "github.com/juju/juju/network" - "github.com/juju/juju/state" - "github.com/juju/juju/status" -) - -// Unit represents a state.Unit. -type Unit interface { - status.StatusHistoryGetter - Life() state.Life - Destroy() (err error) - IsPrincipal() bool - PublicAddress() (network.Address, error) - PrivateAddress() (network.Address, error) - Resolve(retryHooks bool) error - AgentHistory() status.StatusHistoryGetter -} - -// stateInterface contains the state.State methods used in this package, -// allowing stubs to be created for testing. -type stateInterface interface { - FindEntity(names.Tag) (state.Entity, error) - Unit(string) (Unit, error) - Application(string) (*state.Application, error) - Machine(string) (*state.Machine, error) - AllMachines() ([]*state.Machine, error) - AllApplications() ([]*state.Application, error) - AllRelations() ([]*state.Relation, error) - AddOneMachine(state.MachineTemplate) (*state.Machine, error) - AddMachineInsideMachine(state.MachineTemplate, string, instance.ContainerType) (*state.Machine, error) - AddMachineInsideNewMachine(template, parentTemplate state.MachineTemplate, containerType instance.ContainerType) (*state.Machine, error) - ModelConstraints() (constraints.Value, error) - ModelConfig() (*config.Config, error) - ModelConfigValues() (config.ConfigValues, error) - UpdateModelConfig(map[string]interface{}, []string, state.ValidateConfigFunc) error - SetModelConstraints(constraints.Value) error - ModelUUID() string - ModelTag() names.ModelTag - Model() (*state.Model, error) - ForModel(tag names.ModelTag) (*state.State, error) - SetModelAgentVersion(version.Number) error - SetAnnotations(state.GlobalEntity, map[string]string) error - Annotations(state.GlobalEntity) (map[string]string, error) - InferEndpoints(...string) ([]state.Endpoint, error) - EndpointsRelation(...state.Endpoint) (*state.Relation, error) - Charm(*charm.URL) (*state.Charm, error) - LatestPlaceholderCharm(*charm.URL) (*state.Charm, error) - AddRelation(...state.Endpoint) (*state.Relation, error) - AddModelUser(state.ModelUserSpec) (*state.ModelUser, error) - RemoveModelUser(names.UserTag) error - Watch() *state.Multiwatcher - AbortCurrentUpgrade() error - APIHostPorts() ([][]network.HostPort, error) -} - -type stateShim struct { - *state.State -} - -func (s *stateShim) Unit(name string) (Unit, error) { - u, err := s.State.Unit(name) - if err != nil { - return nil, err - } - return u, nil -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/status.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/status.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/status.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/status.go 2016-08-16 08:56:25.000000000 +0000 @@ -293,9 +293,41 @@ } } + migStatus, err := c.getMigrationStatus() + if err != nil { + // It's not worth killing the entire status out if migration + // status can't be retrieved. + logger.Errorf("error retrieving migration status: %v", err) + info.Migration = "error retrieving migration status" + } else { + info.Migration = migStatus + } + return info, nil } +func (c *Client) getMigrationStatus() (string, error) { + mig, err := c.api.stateAccessor.LatestModelMigration() + if err != nil { + if errors.IsNotFound(err) { + return "", nil + } + return "", errors.Trace(err) + } + + phase, err := mig.Phase() + if err != nil { + return "", errors.Trace(err) + } + if phase.IsTerminal() { + // There has been a migration attempt but it's no longer + // active - don't include this in status. + return "", nil + } + + return mig.StatusMessage(), nil +} + type statusContext struct { // machines: top-level machine id -> list of machines nested in // this machine. @@ -311,7 +343,7 @@ // machine and machines[1..n] are any containers (including nested ones). // // If machineIds is non-nil, only machines whose IDs are in the set are returned. -func fetchMachines(st stateInterface, machineIds set.Strings) (map[string][]*state.Machine, error) { +func fetchMachines(st Backend, machineIds set.Strings) (map[string][]*state.Machine, error) { v := make(map[string][]*state.Machine) machines, err := st.AllMachines() if err != nil { @@ -342,7 +374,7 @@ // fetchAllApplicationsAndUnits returns a map from service name to service, // a map from service name to unit name to unit, and a map from base charm URL to latest URL. func fetchAllApplicationsAndUnits( - st stateInterface, + st Backend, matchAny bool, ) (map[string]*state.Application, map[string]map[string]*state.Unit, map[charm.URL]*state.Charm, error) { @@ -393,7 +425,7 @@ // to have the relations for each service. Reading them once here // avoids the repeated DB hits to retrieve the relations for each // service that used to happen in processServiceRelations(). -func fetchRelations(st stateInterface) (map[string][]*state.Relation, error) { +func fetchRelations(st Backend) (map[string][]*state.Relation, error) { relations, err := st.AllRelations() if err != nil { return nil, err diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/statushistory_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/statushistory_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/statushistory_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/statushistory_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -28,11 +28,19 @@ func (s *statusHistoryTestSuite) SetUpTest(c *gc.C) { s.st = &mockState{} - client.PatchState(s, s.st) tag := names.NewUserTag("user") authorizer := &apiservertesting.FakeAuthorizer{Tag: tag} var err error - s.api, err = client.NewClient(nil, nil, authorizer) + s.api, err = client.NewClient( + s.st, + nil, // modelconfig API + nil, // resources + authorizer, + nil, // statusSetter + nil, // toolsFinder + nil, // newEnviron + nil, // blockChecker + ) c.Assert(err, jc.ErrorIsNil) } @@ -211,7 +219,7 @@ } type mockState struct { - client.StateInterface + client.Backend unitHistory []status.StatusInfo agentHistory []status.StatusInfo } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/status_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/status_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client/status_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client/status_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,14 +7,18 @@ "time" jc "github.com/juju/testing/checkers" + "github.com/juju/utils" gc "gopkg.in/check.v1" + "gopkg.in/juju/names.v2" + "github.com/juju/juju/api" "github.com/juju/juju/apiserver/charmrevisionupdater" "github.com/juju/juju/apiserver/charmrevisionupdater/testing" "github.com/juju/juju/apiserver/client" "github.com/juju/juju/apiserver/common" "github.com/juju/juju/apiserver/params" apiservertesting "github.com/juju/juju/apiserver/testing" + "github.com/juju/juju/core/migration" "github.com/juju/juju/instance" jujutesting "github.com/juju/juju/juju/testing" "github.com/juju/juju/state" @@ -228,6 +232,54 @@ checkUnitVersion(c, appStatus, unit, "") } +func (s *statusUnitTestSuite) TestMigrationInProgress(c *gc.C) { + + // Create a host model because controller models can't be migrated. + state2 := s.Factory.MakeModel(c, nil) + defer state2.Close() + + // Get API connection to hosted model. + apiInfo := s.APIInfo(c) + apiInfo.ModelTag = state2.ModelTag() + conn, err := api.Open(apiInfo, api.DialOpts{}) + c.Assert(err, jc.ErrorIsNil) + client := conn.Client() + + checkMigStatus := func(expected string) { + status, err := client.Status(nil) + c.Assert(err, jc.ErrorIsNil) + c.Check(status.Model.Migration, gc.Equals, expected) + } + + // Migration status should be empty when no migration is happening. + checkMigStatus("") + + // Start it migrating. + mig, err := state2.CreateModelMigration(state.ModelMigrationSpec{ + InitiatedBy: names.NewUserTag("admin"), + TargetInfo: migration.TargetInfo{ + ControllerTag: names.NewModelTag(utils.MustNewUUID().String()), + Addrs: []string{"1.2.3.4:5555", "4.3.2.1:6666"}, + CACert: "cert", + AuthTag: names.NewUserTag("user"), + Password: "password", + }, + }) + c.Assert(err, jc.ErrorIsNil) + + // Check initial message. + checkMigStatus("starting") + + // Check status is reported when set. + setAndCheckMigStatus := func(message string) { + err := mig.SetStatusMessage(message) + c.Assert(err, jc.ErrorIsNil) + checkMigStatus(message) + } + setAndCheckMigStatus("proceeding swimmingly") + setAndCheckMigStatus("oh noes") +} + type statusUpgradeUnitSuite struct { testing.CharmSuite jujutesting.JujuConnSuite diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client_auth_root.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client_auth_root.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client_auth_root.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client_auth_root.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,11 +5,12 @@ import ( "github.com/juju/errors" + "github.com/juju/utils/set" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/core/description" "github.com/juju/juju/rpc" "github.com/juju/juju/rpc/rpcreflect" - "github.com/juju/juju/state" ) // clientAuthRoot restricts API calls for users of a model. Initially the @@ -17,11 +18,11 @@ // near future, full ACL support is desirable. type clientAuthRoot struct { finder rpc.MethodFinder - user *state.ModelUser + user description.UserAccess } // newClientAuthRoot returns a new restrictedRoot. -func newClientAuthRoot(finder rpc.MethodFinder, user *state.ModelUser) *clientAuthRoot { +func newClientAuthRoot(finder rpc.MethodFinder, user description.UserAccess) *clientAuthRoot { return &clientAuthRoot{finder, user} } @@ -35,7 +36,7 @@ return nil, err } // ReadOnly User - if r.user.IsReadOnly() { + if r.user.Access == description.ReadAccess { canCall := isCallAllowableByReadOnlyUser(rootName, methodName) || isCallReadOnly(rootName, methodName) if !canCall { @@ -44,7 +45,7 @@ } // Check if our call requires higher access than the user has. - if doesCallRequireAdmin(rootName, methodName) && !r.user.IsAdmin() { + if doesCallRequireAdmin(rootName, methodName) && r.user.Access != description.AdminAccess { return nil, errors.Trace(common.ErrPerm) } @@ -61,11 +62,26 @@ return restrictedRootNames.Contains(facade) } +var modelManagerMethods = set.NewStrings( + "ModifyModelAccess", + "CreateModel", +) + +var controllerMethods = set.NewStrings( + "DestroyController", +) + func doesCallRequireAdmin(facade, method string) bool { // TODO(perrito666) This should filter adding users to controllers. // TODO(perrito666) Add an exaustive list of facades/methods that are // admin only and put them in an authoritative source to be re-used. // TODO(perrito666) This is a stub, the idea is to maintain the current // status of permissions until we decide what goes to admin only. + switch facade { + case "ModelManager": + return modelManagerMethods.Contains(method) + case "Controller": + return controllerMethods.Contains(method) + } return false } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client_auth_root_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client_auth_root_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/client_auth_root_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/client_auth_root_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,13 +8,13 @@ "github.com/juju/errors" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/core/description" "github.com/juju/juju/testing/factory" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" "github.com/juju/juju/rpc/rpcreflect" - "github.com/juju/juju/state" "github.com/juju/juju/state/testing" ) @@ -51,8 +51,23 @@ s.AssertCallNotImplemented(c, client, "Unknown", 1, "Method") } +func (s *clientAuthRootSuite) TestAdminUser(c *gc.C) { + modelUser := s.Factory.MakeModelUser(c, &factory.ModelUserParams{Access: description.WriteAccess}) + client := newClientAuthRoot(&fakeFinder{}, modelUser) + s.AssertCallGood(c, client, "Client", 1, "FullStatus") + s.AssertCallErrPerm(c, client, "ModelManager", 2, "ModifyModelAccess") + s.AssertCallErrPerm(c, client, "ModelManager", 2, "CreateModel") + s.AssertCallErrPerm(c, client, "Controller", 3, "DestroyController") + + modelUser = s.Factory.MakeModelUser(c, &factory.ModelUserParams{Access: description.AdminAccess}) + client = newClientAuthRoot(&fakeFinder{}, modelUser) + s.AssertCallGood(c, client, "ModelManager", 2, "ModifyModelAccess") + s.AssertCallGood(c, client, "ModelManager", 2, "CreateModel") + s.AssertCallGood(c, client, "Controller", 3, "DestroyController") +} + func (s *clientAuthRootSuite) TestReadOnlyUser(c *gc.C) { - modelUser := s.Factory.MakeModelUser(c, &factory.ModelUserParams{Access: state.ReadAccess}) + modelUser := s.Factory.MakeModelUser(c, &factory.ModelUserParams{Access: description.ReadAccess}) client := newClientAuthRoot(&fakeFinder{}, modelUser) // deploys are bad s.AssertCallErrPerm(c, client, "Application", 1, "Deploy") diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/cloud/cloud.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/cloud/cloud.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/cloud/cloud.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/cloud/cloud.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,6 +10,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/cloud" "github.com/juju/juju/state" @@ -25,19 +26,19 @@ // the concrete implementation of the api end point. type CloudAPI struct { backend Backend - authorizer common.Authorizer + authorizer facade.Authorizer apiUser names.UserTag getCredentialsAuthFunc common.GetAuthFunc getCloudDefaultsAuthFunc common.GetAuthFunc } -func newFacade(st *state.State, resources *common.Resources, auth common.Authorizer) (*CloudAPI, error) { +func newFacade(st *state.State, resources facade.Resources, auth facade.Authorizer) (*CloudAPI, error) { return NewCloudAPI(NewStateBackend(st), auth) } // NewCloudAPI creates a new API server endpoint for managing the controller's // cloud definition and cloud credentials. -func NewCloudAPI(backend Backend, authorizer common.Authorizer) (*CloudAPI, error) { +func NewCloudAPI(backend Backend, authorizer facade.Authorizer) (*CloudAPI, error) { if !authorizer.AuthClient() { return nil, common.ErrPerm } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/action.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/action.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/action.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/action.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,6 +8,7 @@ "github.com/juju/errors" "gopkg.in/juju/names.v2" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" "github.com/juju/juju/state/watcher" @@ -162,7 +163,7 @@ // It needs a tagToActionReceiver function and a registerFunc to register // resources. // It's a helper function currently used by the uniter and by machineactions -func WatchOneActionReceiverNotifications(tagToActionReceiver func(tag string) (state.ActionReceiver, error), registerFunc func(r Resource) string) func(names.Tag) (params.StringsWatchResult, error) { +func WatchOneActionReceiverNotifications(tagToActionReceiver func(tag string) (state.ActionReceiver, error), registerFunc func(r facade.Resource) string) func(names.Tag) (params.StringsWatchResult, error) { return func(tag names.Tag) (params.StringsWatchResult, error) { nothing := params.StringsWatchResult{} receiver, err := tagToActionReceiver(tag.String()) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/action_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/action_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/action_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/action_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -12,6 +12,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" "github.com/juju/juju/testing" @@ -211,7 +212,7 @@ func (s *actionsSuite) TestWatchOneActionReceiverNotifications(c *gc.C) { expectErr := errors.New("zwoosh") - registerFunc := func(common.Resource) string { return "bambalam" } + registerFunc := func(facade.Resource) string { return "bambalam" } tagToActionReceiver := common.TagToActionReceiverFn(makeFindEntity(map[string]state.Entity{ "machine-1": &fakeActionReceiver{watcher: &fakeWatcher{}}, "machine-2": &fakeActionReceiver{watcher: &fakeWatcher{err: expectErr}}, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/addresses.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/addresses.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/addresses.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/addresses.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,6 +4,7 @@ package common import ( + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/network" "github.com/juju/juju/state" @@ -23,13 +24,13 @@ // APIAddresser implements the APIAddresses method type APIAddresser struct { - resources *Resources + resources facade.Resources getter AddressAndCertGetter } // NewAPIAddresser returns a new APIAddresser that uses the given getter to // fetch its addresses. -func NewAPIAddresser(getter AddressAndCertGetter, resources *Resources) *APIAddresser { +func NewAPIAddresser(getter AddressAndCertGetter, resources facade.Resources) *APIAddresser { return &APIAddresser{ getter: getter, resources: resources, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/cloudspec/cloudspec.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/cloudspec/cloudspec.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/cloudspec/cloudspec.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/cloudspec/cloudspec.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,91 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package cloudspec + +import ( + "gopkg.in/juju/names.v2" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/environs" +) + +// CloudSpecAPI implements common methods for use by various +// facades for querying the cloud spec of models. +type CloudSpecAPI struct { + getCloudSpec func(names.ModelTag) (environs.CloudSpec, error) + getAuthFunc common.GetAuthFunc +} + +// NewCloudSpec returns a new CloudSpecAPI. +func NewCloudSpec( + getCloudSpec func(names.ModelTag) (environs.CloudSpec, error), + getAuthFunc common.GetAuthFunc, +) CloudSpecAPI { + return CloudSpecAPI{getCloudSpec, getAuthFunc} +} + +// NewCloudSpecForModel returns a new CloudSpecAPI that permits access to only +// one model. +func NewCloudSpecForModel( + modelTag names.ModelTag, + getCloudSpec func() (environs.CloudSpec, error), +) CloudSpecAPI { + return CloudSpecAPI{ + func(names.ModelTag) (environs.CloudSpec, error) { + // The tag passed in is guaranteed to be the + // same as "modelTag", as the authorizer below + // would have failed otherwise. + return getCloudSpec() + }, + func() (common.AuthFunc, error) { + return func(tag names.Tag) bool { + return tag == modelTag + }, nil + }, + } +} + +// CloudSpec returns the model's cloud spec. +func (s CloudSpecAPI) CloudSpec(args params.Entities) (params.CloudSpecResults, error) { + authFunc, err := s.getAuthFunc() + if err != nil { + return params.CloudSpecResults{}, err + } + results := params.CloudSpecResults{ + Results: make([]params.CloudSpecResult, len(args.Entities)), + } + for i, arg := range args.Entities { + tag, err := names.ParseModelTag(arg.Tag) + if err != nil { + results.Results[i].Error = common.ServerError(err) + continue + } + if !authFunc(tag) { + results.Results[i].Error = common.ServerError(common.ErrPerm) + continue + } + spec, err := s.getCloudSpec(tag) + if err != nil { + results.Results[i].Error = common.ServerError(err) + continue + } + var paramsCloudCredential *params.CloudCredential + if spec.Credential != nil && spec.Credential.AuthType() != "" { + paramsCloudCredential = ¶ms.CloudCredential{ + string(spec.Credential.AuthType()), + spec.Credential.Attributes(), + } + } + results.Results[i].Result = ¶ms.CloudSpec{ + spec.Type, + spec.Name, + spec.Region, + spec.Endpoint, + spec.StorageEndpoint, + paramsCloudCredential, + } + } + return results, nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/cloudspec/cloudspec_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/cloudspec/cloudspec_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/cloudspec/cloudspec_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/cloudspec/cloudspec_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,139 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package cloudspec_test + +import ( + "errors" + + "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + "github.com/juju/utils" + gc "gopkg.in/check.v1" + names "gopkg.in/juju/names.v2" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/common/cloudspec" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cloud" + "github.com/juju/juju/environs" + coretesting "github.com/juju/juju/testing" +) + +type CloudSpecSuite struct { + testing.IsolationSuite + testing.Stub + result environs.CloudSpec + authFunc common.AuthFunc + api cloudspec.CloudSpecAPI +} + +var _ = gc.Suite(&CloudSpecSuite{}) + +func (s *CloudSpecSuite) SetUpTest(c *gc.C) { + s.IsolationSuite.SetUpTest(c) + s.Stub.ResetCalls() + + s.authFunc = func(tag names.Tag) bool { + s.AddCall("Auth", tag) + return tag == coretesting.ModelTag + } + s.api = cloudspec.NewCloudSpec(func(tag names.ModelTag) (environs.CloudSpec, error) { + s.AddCall("CloudSpec", tag) + return s.result, s.NextErr() + }, func() (common.AuthFunc, error) { + s.AddCall("GetAuthFunc") + return s.authFunc, s.NextErr() + }) + + credential := cloud.NewCredential( + "auth-type", + map[string]string{"k": "v"}, + ) + s.result = environs.CloudSpec{ + "type", + "name", + "region", + "endpoint", + "storage-endpoint", + &credential, + } +} + +func (s *CloudSpecSuite) TestCloudSpec(c *gc.C) { + otherModelTag := names.NewModelTag(utils.MustNewUUID().String()) + machineTag := names.NewMachineTag("42") + result, err := s.api.CloudSpec(params.Entities{Entities: []params.Entity{ + {coretesting.ModelTag.String()}, + {otherModelTag.String()}, + {machineTag.String()}, + }}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result.Results, jc.DeepEquals, []params.CloudSpecResult{{ + Result: ¶ms.CloudSpec{ + "type", + "name", + "region", + "endpoint", + "storage-endpoint", + ¶ms.CloudCredential{ + "auth-type", + map[string]string{"k": "v"}, + }, + }, + }, { + Error: ¶ms.Error{ + Code: params.CodeUnauthorized, + Message: "permission denied", + }, + }, { + Error: ¶ms.Error{ + Message: `"machine-42" is not a valid model tag`, + }, + }}) + s.CheckCalls(c, []testing.StubCall{ + {"GetAuthFunc", nil}, + {"Auth", []interface{}{coretesting.ModelTag}}, + {"CloudSpec", []interface{}{coretesting.ModelTag}}, + {"Auth", []interface{}{otherModelTag}}, + }) +} + +func (s *CloudSpecSuite) TestCloudSpecNilCredential(c *gc.C) { + s.result.Credential = nil + result, err := s.api.CloudSpec(params.Entities{ + Entities: []params.Entity{{coretesting.ModelTag.String()}}, + }) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result.Results, jc.DeepEquals, []params.CloudSpecResult{{ + Result: ¶ms.CloudSpec{ + "type", + "name", + "region", + "endpoint", + "storage-endpoint", + nil, + }, + }}) +} + +func (s *CloudSpecSuite) TestCloudSpecGetAuthFuncError(c *gc.C) { + expect := errors.New("bewm") + s.SetErrors(expect) + result, err := s.api.CloudSpec(params.Entities{ + Entities: []params.Entity{{coretesting.ModelTag.String()}}, + }) + c.Assert(err, gc.Equals, expect) + c.Assert(result, jc.DeepEquals, params.CloudSpecResults{}) +} + +func (s *CloudSpecSuite) TestCloudSpecCloudSpecError(c *gc.C) { + s.SetErrors(nil, errors.New("bewm")) + result, err := s.api.CloudSpec(params.Entities{ + Entities: []params.Entity{{coretesting.ModelTag.String()}}, + }) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, params.CloudSpecResults{Results: []params.CloudSpecResult{{ + Error: ¶ms.Error{Message: "bewm"}, + }}}) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/cloudspec/package_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/cloudspec/package_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/cloudspec/package_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/cloudspec/package_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,14 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package cloudspec_test + +import ( + "testing" + + gc "gopkg.in/check.v1" +) + +func TestPackage(t *testing.T) { + gc.TestingT(t) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/errors.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/errors.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/errors.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/errors.go 2016-08-16 08:56:25.000000000 +0000 @@ -33,7 +33,7 @@ } func NoAddressSetError(unitTag names.UnitTag, addressName string) error { - return &noAddressSetError{unitTag, addressName} + return &noAddressSetError{unitTag: unitTag, addressName: addressName} } func isNoAddressSetError(err error) bool { @@ -93,7 +93,6 @@ ErrPerm = errors.New("permission denied") ErrNotLoggedIn = errors.New("not logged in") ErrUnknownWatcher = errors.New("unknown watcher id") - ErrUnknownPinger = errors.New("unknown pinger id") ErrStoppedWatcher = errors.New("watcher has been stopped") ErrBadRequest = errors.New("invalid request") ErrTryAgain = errors.New("try again") @@ -165,7 +164,9 @@ switch err1.Code { case params.CodeUnauthorized: status = http.StatusUnauthorized - case params.CodeNotFound: + case params.CodeNotFound, + params.CodeUserNotFound, + params.CodeModelNotFound: status = http.StatusNotFound case params.CodeBadRequest: status = http.StatusBadRequest @@ -179,6 +180,8 @@ status = http.StatusForbidden case params.CodeDischargeRequired: status = http.StatusUnauthorized + case params.CodeRetry: + status = http.StatusServiceUnavailable } return err1, status } @@ -197,10 +200,14 @@ var info *params.ErrorInfo switch { case ok: + case isIOTimeout(err): + code = params.CodeRetry case errors.IsUnauthorized(err): code = params.CodeUnauthorized case errors.IsNotFound(err): code = params.CodeNotFound + case errors.IsUserNotFound(err): + code = params.CodeUserNotFound case errors.IsAlreadyExists(err): code = params.CodeAlreadyExists case errors.IsNotAssigned(err): @@ -218,7 +225,7 @@ case state.IsHasAttachmentsError(err): code = params.CodeMachineHasAttachedStorage case isUnknownModelError(err): - code = params.CodeNotFound + code = params.CodeModelNotFound case errors.IsNotSupported(err): code = params.CodeNotSupported case errors.IsBadRequest(err): @@ -244,6 +251,17 @@ } } +// Unfortunately there is no specific type of error for i/o timeout, +// and the error that bubbles up from mgo is annotated and a string type, +// so all we can do is look at the error suffix and see if it matches. +func isIOTimeout(err error) bool { + // Perhaps sometime in the future, we'll have additional ways to tell if + // the error is an i/o timeout type error, but for now this is all we + // have. + msg := err.Error() + return strings.HasSuffix(msg, "i/o timeout") +} + func DestroyErr(desc string, ids, errs []string) error { // TODO(waigani) refactor DestroyErr to take a map of ids to errors. if len(errs) == 0 { @@ -284,6 +302,8 @@ // TODO(ericsnow) UnknownModelError should be handled here too. // ...by parsing msg? return errors.NewNotFound(nil, msg) + case params.IsCodeUserNotFound(err): + return errors.NewUserNotFound(nil, msg) case params.IsCodeAlreadyExists(err): return errors.NewAlreadyExists(nil, msg) case params.IsCodeNotAssigned(err): diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/errors_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/errors_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/errors_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/errors_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -39,6 +39,11 @@ status: http.StatusNotFound, helperFunc: params.IsCodeNotFound, }, { + err: errors.UserNotFoundf("xxxx"), + code: params.CodeUserNotFound, + status: http.StatusNotFound, + helperFunc: params.IsCodeUserNotFound, +}, { err: errors.Unauthorizedf("hello"), code: params.CodeUnauthorized, status: http.StatusUnauthorized, @@ -182,9 +187,13 @@ code: "", }, { err: common.UnknownModelError("dead-beef-123456"), - code: params.CodeNotFound, + code: params.CodeModelNotFound, status: http.StatusNotFound, - helperFunc: params.IsCodeNotFound, + helperFunc: params.IsCodeModelNotFound, +}, { + err: errors.Annotate(errors.New("i/o timeout"), "annotated"), + code: params.CodeRetry, + status: http.StatusServiceUnavailable, }, { err: nil, code: "", @@ -232,12 +241,10 @@ params.CodeNoAddressSet, params.CodeUpgradeInProgress, params.CodeMachineHasAttachedStorage, - params.CodeDischargeRequired: + params.CodeDischargeRequired, + params.CodeModelNotFound, + params.CodeRetry: continue - case params.CodeNotFound: - if common.IsUnknownModelError(t.err) { - continue - } case params.CodeOperationBlocked: // ServerError doesn't actually have a case for this code. continue diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/export_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -3,15 +3,15 @@ package common +import "github.com/juju/juju/apiserver/facade" + var ( MachineJobFromParams = machineJobFromParams ValidateNewFacade = validateNewFacade WrapNewFacade = wrapNewFacade - NilFacadeRecord = facadeRecord{} EnvtoolsFindTools = &envtoolsFindTools SendMetrics = &sendMetrics MockableDestroyMachines = destroyMachines - IsUnknownModelError = isUnknownModelError ) type Patcher interface { @@ -22,12 +22,6 @@ // a clean slate to work from, and will not accidentally overrite/mutate the // real facade registry. func SanitizeFacades(patcher Patcher) { - emptyFacades := &FacadeRegistry{} + emptyFacades := &facade.Registry{} patcher.PatchValue(&Facades, emptyFacades) } - -type Versions versions - -func DescriptionFromVersions(name string, vers Versions) FacadeDescription { - return descriptionFromVersions(name, versions(vers)) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/interfaces.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/interfaces.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/interfaces.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/interfaces.go 2016-08-16 08:56:25.000000000 +0000 @@ -14,35 +14,6 @@ // GetAuthFunc returns an AuthFunc. type GetAuthFunc func() (AuthFunc, error) -// Authorizer represents a value that can be asked for authorization -// information on its associated authenticated entity. It is -// implemented by an API server to allow an API implementation to ask -// questions about the client that is currently connected. -type Authorizer interface { - // AuthMachineAgent returns whether the authenticated entity is a - // machine agent. - AuthMachineAgent() bool - - // AuthUnitAgent returns whether the authenticated entity is a - // unit agent. - AuthUnitAgent() bool - - // AuthOwner returns whether the authenticated entity is the same - // as the given entity. - AuthOwner(tag names.Tag) bool - - // AuthModelManager returns whether the authenticated entity is - // a machine running the environment manager job. - AuthModelManager() bool - - // AuthClient returns whether the authenticated entity - // is a client user. - AuthClient() bool - - // GetAuthTag returns the tag of the authenticated entity. - GetAuthTag() names.Tag -} - // AuthEither returns an AuthFunc generator that returns an AuthFunc // that accepts any tag authorized by either of its arguments. func AuthEither(a, b GetAuthFunc) GetAuthFunc { @@ -78,6 +49,15 @@ }, nil } } + +// AuthFuncForTag returns an authentication function that always returns true iff it is passed a specific tag. +func AuthFuncForTag(valid names.Tag) GetAuthFunc { + return func() (AuthFunc, error) { + return func(tag names.Tag) bool { + return tag == valid + }, nil + } +} // AuthFuncForTagKind returns a GetAuthFunc which creates an AuthFunc // allowing only the given tag kind and denies all others. Passing an diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/modeldestroy_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/modeldestroy_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/modeldestroy_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/modeldestroy_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -13,11 +13,9 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/api" - "github.com/juju/juju/apiserver/client" "github.com/juju/juju/apiserver/common" commontesting "github.com/juju/juju/apiserver/common/testing" "github.com/juju/juju/apiserver/metricsender" - apiservertesting "github.com/juju/juju/apiserver/testing" "github.com/juju/juju/instance" "github.com/juju/juju/juju/testing" "github.com/juju/juju/state" @@ -216,9 +214,8 @@ type destroyTwoModelsSuite struct { testing.JujuConnSuite - otherState *state.State - otherModelOwner names.UserTag - otherModelClient *client.Client + otherState *state.State + otherModelOwner names.UserTag modelManager common.ModelManagerBackend otherModelManager common.ModelManagerBackend @@ -240,15 +237,6 @@ s.modelManager = common.NewModelManagerBackend(s.State) s.otherModelManager = common.NewModelManagerBackend(s.otherState) s.AddCleanup(func(*gc.C) { s.otherState.Close() }) - - // get the client for the other model - auth := apiservertesting.FakeAuthorizer{ - Tag: s.otherModelOwner, - EnvironManager: false, - } - s.otherModelClient, err = client.NewClient(s.otherState, common.NewResources(), auth) - c.Assert(err, jc.ErrorIsNil) - } func (s *destroyTwoModelsSuite) TestCleanupModelResources(c *gc.C) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/modelmachineswatcher.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/modelmachineswatcher.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/modelmachineswatcher.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/modelmachineswatcher.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,6 +6,7 @@ import ( "fmt" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" "github.com/juju/juju/state/watcher" @@ -15,14 +16,14 @@ // method for use by various facades. type ModelMachinesWatcher struct { st state.ModelMachinesWatcher - resources *Resources - authorizer Authorizer + resources facade.Resources + authorizer facade.Authorizer } // NewModelMachinesWatcher returns a new ModelMachinesWatcher. The // GetAuthFunc will be used on each invocation of WatchUnits to // determine current permissions. -func NewModelMachinesWatcher(st state.ModelMachinesWatcher, resources *Resources, authorizer Authorizer) *ModelMachinesWatcher { +func NewModelMachinesWatcher(st state.ModelMachinesWatcher, resources facade.Resources, authorizer facade.Authorizer) *ModelMachinesWatcher { return &ModelMachinesWatcher{ st: st, resources: resources, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/modelmanagerinterface.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/modelmanagerinterface.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/modelmanagerinterface.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/modelmanagerinterface.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,12 +4,13 @@ package common import ( + "time" + "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/metricsender" - "github.com/juju/juju/cloud" "github.com/juju/juju/controller" - "github.com/juju/juju/environs" + "github.com/juju/juju/core/description" "github.com/juju/juju/environs/config" "github.com/juju/juju/state" "github.com/juju/juju/status" @@ -20,28 +21,31 @@ // All the interface methods are defined directly on state.State // and are reproduced here for use in tests. type ModelManagerBackend interface { - environs.EnvironConfigGetter APIHostPortsGetter ToolsStorageGetter BlockGetter metricsender.MetricsSenderBackend + state.CloudAccessor - Cloud(name string) (cloud.Cloud, error) - CloudCredentials(user names.UserTag, cloudName string) (map[string]cloud.Credential, error) ModelUUID() string ModelsForUser(names.UserTag) ([]*state.UserModel, error) IsControllerAdministrator(user names.UserTag) (bool, error) NewModel(state.ModelArgs) (Model, ModelManagerBackend, error) + ComposeNewModelConfig(modelAttr map[string]interface{}) (map[string]interface{}, error) ControllerModel() (Model, error) ControllerConfig() (controller.Config, error) ForModel(tag names.ModelTag) (ModelManagerBackend, error) Model() (Model, error) AllModels() ([]Model, error) - AddModelUser(state.ModelUserSpec) (*state.ModelUser, error) - RemoveModelUser(names.UserTag) error - ModelUser(names.UserTag) (*state.ModelUser, error) + AddModelUser(state.UserAccessSpec) (description.UserAccess, error) + AddControllerUser(state.UserAccessSpec) (description.UserAccess, error) + RemoveUserAccess(names.UserTag, names.Tag) error + UserAccess(names.UserTag, names.Tag) (description.UserAccess, error) ModelTag() names.ModelTag + Export() (description.Model, error) + SetUserAccess(subject names.UserTag, target names.Tag, access description.Access) (description.UserAccess, error) + LastModelConnection(user names.UserTag) (time.Time, error) Close() error } @@ -57,19 +61,24 @@ Cloud() string CloudCredential() string CloudRegion() string - Users() ([]ModelUser, error) + Users() ([]description.UserAccess, error) Destroy() error DestroyIncludingHosted() error } +var _ ModelManagerBackend = (*modelManagerStateShim)(nil) + type modelManagerStateShim struct { *state.State } +// NewModelManagerBackend returns a modelManagerStateShim wrapping the passed +// state, which implements ModelManagerBackend. func NewModelManagerBackend(st *state.State) ModelManagerBackend { return modelManagerStateShim{st} } +// ControllerModel implements ModelManagerBackend. func (st modelManagerStateShim) ControllerModel() (Model, error) { m, err := st.State.ControllerModel() if err != nil { @@ -78,6 +87,7 @@ return modelShim{m}, nil } +// NewModel implements ModelManagerBackend. func (st modelManagerStateShim) NewModel(args state.ModelArgs) (Model, ModelManagerBackend, error) { m, otherState, err := st.State.NewModel(args) if err != nil { @@ -86,6 +96,7 @@ return modelShim{m}, modelManagerStateShim{otherState}, nil } +// ForModel implements ModelManagerBackend. func (st modelManagerStateShim) ForModel(tag names.ModelTag) (ModelManagerBackend, error) { otherState, err := st.State.ForModel(tag) if err != nil { @@ -94,6 +105,7 @@ return modelManagerStateShim{otherState}, nil } +// Model implements ModelManagerBackend. func (st modelManagerStateShim) Model() (Model, error) { m, err := st.State.Model() if err != nil { @@ -102,6 +114,7 @@ return modelShim{m}, nil } +// AllModels implements ModelManagerBackend. func (st modelManagerStateShim) AllModels() ([]Model, error) { allStateModels, err := st.State.AllModels() if err != nil { @@ -118,12 +131,13 @@ *state.Model } -func (m modelShim) Users() ([]ModelUser, error) { +// Users implements ModelManagerBackend. +func (m modelShim) Users() ([]description.UserAccess, error) { stateUsers, err := m.Model.Users() if err != nil { return nil, err } - users := make([]ModelUser, len(stateUsers)) + users := make([]description.UserAccess, len(stateUsers)) for i, user := range stateUsers { users[i] = user } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/modeluser.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/modeluser.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/modeluser.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/modeluser.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,48 +6,55 @@ import ( "time" - "github.com/juju/errors" "gopkg.in/juju/names.v2" + "github.com/juju/errors" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/core/description" "github.com/juju/juju/state" ) -// ModelUser defines the subset of the state.ModelUser type -// that we require to convert to a params.ModelUserInfo. -type ModelUser interface { - DisplayName() string - LastConnection() (time.Time, error) - UserName() string - UserTag() names.UserTag - IsReadOnly() bool - IsReadWrite() bool - IsAdmin() bool +type modelConnectionAbleBackend interface { + LastModelConnection(names.UserTag) (time.Time, error) } -// ModelUserInfo converts *state.ModelUser to params.ModelUserInfo. -func ModelUserInfo(user ModelUser) (params.ModelUserInfo, error) { - var lastConn *time.Time - userLastConn, err := user.LastConnection() - if err == nil { - lastConn = &userLastConn - } else if !state.IsNeverConnectedError(err) { +// ModelUserInfo converts description.UserAccess to params.ModelUserInfo. +func ModelUserInfo(user description.UserAccess, st modelConnectionAbleBackend) (params.ModelUserInfo, error) { + access, err := StateToParamsUserAccessPermission(user.Access) + if err != nil { return params.ModelUserInfo{}, errors.Trace(err) } - access := params.ModelReadAccess - switch { - case user.IsAdmin(): - access = params.ModelAdminAccess - case user.IsReadWrite(): - access = params.ModelWriteAccess + userLastConn, err := st.LastModelConnection(user.UserTag) + if err != nil && !state.IsNeverConnectedError(err) { + return params.ModelUserInfo{}, errors.Trace(err) + } + var lastConn *time.Time + if err == nil { + lastConn = &userLastConn } userInfo := params.ModelUserInfo{ - UserName: user.UserName(), - DisplayName: user.DisplayName(), + UserName: user.UserName, + DisplayName: user.DisplayName, LastConnection: lastConn, Access: access, } return userInfo, nil } + +// StateToParamsUserAccessPermission converts description.Access to params.AccessPermission. +func StateToParamsUserAccessPermission(descriptionAccess description.Access) (params.UserAccessPermission, error) { + switch descriptionAccess { + case description.ReadAccess: + return params.ModelReadAccess, nil + case description.WriteAccess: + return params.ModelWriteAccess, nil + case description.AdminAccess: + return params.ModelAdminAccess, nil + } + + return "", errors.NotValidf("model access permission %q", descriptionAccess) + +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/modelwatcher.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/modelwatcher.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/modelwatcher.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/modelwatcher.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,6 +4,7 @@ package common import ( + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/environs" "github.com/juju/juju/state" @@ -14,8 +15,8 @@ // facades - WatchForModelConfigChanges and ModelConfig. type ModelWatcher struct { st state.ModelAccessor - resources *Resources - authorizer Authorizer + resources facade.Resources + authorizer facade.Authorizer } // NewModelWatcher returns a new ModelWatcher. Active watchers @@ -24,7 +25,7 @@ // determine current permissions. // Right now, environment tags are not used, so both created AuthFuncs // are called with "" for tag, which means "the current environment". -func NewModelWatcher(st state.ModelAccessor, resources *Resources, authorizer Authorizer) *ModelWatcher { +func NewModelWatcher(st state.ModelAccessor, resources facade.Resources, authorizer facade.Authorizer) *ModelWatcher { return &ModelWatcher{ st: st, resources: resources, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/modelwatcher_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/modelwatcher_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/modelwatcher_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/modelwatcher_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -126,8 +126,8 @@ bootstrap.PrepareParams{ ControllerConfig: testing.FakeControllerConfig(), ControllerName: "dummycontroller", - BaseConfig: dummy.SampleConfig(), - CloudName: "dummy", + ModelConfig: dummy.SampleConfig(), + Cloud: dummy.SampleCloudSpec(), AdminSecret: "admin-secret", }, ) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/networkingcommon/shims.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/networkingcommon/shims.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/networkingcommon/shims.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/networkingcommon/shims.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,10 +7,10 @@ "github.com/juju/errors" "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/environs/config" "github.com/juju/juju/network" providercommon "github.com/juju/juju/provider/common" "github.com/juju/juju/state" + "github.com/juju/juju/state/stateenvirons" ) // NOTE: All of the following code is only tested with a feature test. @@ -81,20 +81,16 @@ } func NewStateShim(st *state.State) *stateShim { - return &stateShim{st: st} + return &stateShim{stateenvirons.EnvironConfigGetter{st}, st} } // stateShim forwards and adapts state.State methods to Backing // method. type stateShim struct { - NetworkBacking + stateenvirons.EnvironConfigGetter st *state.State } -func (s *stateShim) ModelConfig() (*config.Config, error) { - return s.st.ModelConfig() -} - func (s *stateShim) AddSpace(name string, providerId network.Id, subnetIds []string, public bool) error { _, err := s.st.AddSpace(name, providerId, subnetIds, public) return err diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/networkingcommon/spaces.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/networkingcommon/spaces.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/networkingcommon/spaces.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/networkingcommon/spaces.go 2016-08-16 08:56:25.000000000 +0000 @@ -16,23 +16,11 @@ // SupportsSpaces checks if the environment implements NetworkingEnviron // and also if it supports spaces. func SupportsSpaces(backing environs.EnvironConfigGetter) error { - config, err := backing.ModelConfig() + env, err := environs.GetEnviron(backing, environs.New) if err != nil { - return errors.Annotate(err, "getting model config") + return errors.Annotate(err, "getting environ") } - env, err := environs.New(config) - if err != nil { - return errors.Annotate(err, "validating model config") - } - netEnv, ok := environs.SupportsNetworking(env) - if !ok { - return errors.NotSupportedf("networking") - } - ok, err = netEnv.SupportsSpaces() - if !ok { - if err != nil && !errors.IsNotSupported(err) { - logger.Errorf("checking model spaces support failed with: %v", err) - } + if !environs.SupportsSpaces(env) { return errors.NotSupportedf("spaces") } return nil diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/networkingcommon/spaces_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/networkingcommon/spaces_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/networkingcommon/spaces_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/networkingcommon/spaces_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -81,6 +81,7 @@ baseCalls := []apiservertesting.StubMethodCall{ apiservertesting.BackingCall("ModelConfig"), + apiservertesting.BackingCall("CloudSpec"), apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), apiservertesting.ZonedNetworkingEnvironCall("SupportsSpaces"), } @@ -153,23 +154,25 @@ spaces := params.CreateSpacesParams{} _, err := networkingcommon.CreateSpaces(apiservertesting.BackingInstance, spaces) - c.Assert(err, gc.ErrorMatches, "getting model config: boom") + c.Assert(err, gc.ErrorMatches, "getting environ: boom") } func (s *SpacesSuite) TestCreateSpacesProviderOpenError(c *gc.C) { apiservertesting.SharedStub.SetErrors( nil, // Backing.ModelConfig() + nil, // Backing.CloudSpec() errors.New("boom"), // Provider.Open() ) spaces := params.CreateSpacesParams{} _, err := networkingcommon.CreateSpaces(apiservertesting.BackingInstance, spaces) - c.Assert(err, gc.ErrorMatches, "validating model config: boom") + c.Assert(err, gc.ErrorMatches, "getting environ: boom") } func (s *SpacesSuite) TestCreateSpacesNotSupportedError(c *gc.C) { apiservertesting.SharedStub.SetErrors( nil, // Backing.ModelConfig() + nil, // Backing.CloudSpec() nil, // Provider.Open() errors.NotSupportedf("spaces"), // ZonedNetworkingEnviron.SupportsSpaces() ) @@ -185,17 +188,18 @@ ) err := networkingcommon.SupportsSpaces(apiservertesting.BackingInstance) - c.Assert(err, gc.ErrorMatches, "getting model config: boom") + c.Assert(err, gc.ErrorMatches, "getting environ: boom") } func (s *SpacesSuite) TestSuppportsSpacesEnvironNewError(c *gc.C) { apiservertesting.SharedStub.SetErrors( nil, // Backing.ModelConfig() + nil, // Backing.CloudSpec() errors.New("boom"), // environs.New() ) err := networkingcommon.SupportsSpaces(apiservertesting.BackingInstance) - c.Assert(err, gc.ErrorMatches, "validating model config: boom") + c.Assert(err, gc.ErrorMatches, "getting environ: boom") } func (s *SpacesSuite) TestSuppportsSpacesWithoutNetworking(c *gc.C) { @@ -220,6 +224,7 @@ apiservertesting.SharedStub.SetErrors( nil, // Backing.ModelConfig() + nil, // Backing.CloudSpec() nil, // environs.New() errors.New("boom"), // Backing.SupportsSpaces() ) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/networkingcommon/subnets.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/networkingcommon/subnets.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/networkingcommon/subnets.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/networkingcommon/subnets.go 2016-08-16 08:56:25.000000000 +0000 @@ -432,8 +432,8 @@ // current model config, if supported. If the model does not support // environs.Networking, an error satisfying errors.IsNotSupported() will be // returned. -func networkingEnviron(api NetworkBacking) (environs.NetworkingEnviron, error) { - env, err := environs.GetEnviron(api, environs.New) +func networkingEnviron(getter environs.EnvironConfigGetter) (environs.NetworkingEnviron, error) { + env, err := environs.GetEnviron(getter, environs.New) if err != nil { return nil, errors.Annotate(err, "opening environment") } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/networkingcommon/subnets_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/networkingcommon/subnets_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/networkingcommon/subnets_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/networkingcommon/subnets_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -96,6 +96,7 @@ apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub, apiservertesting.BackingCall("AvailabilityZones"), apiservertesting.BackingCall("ModelConfig"), + apiservertesting.BackingCall("CloudSpec"), apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), apiservertesting.ZonedEnvironCall("AvailabilityZones"), apiservertesting.BackingCall("SetAvailabilityZones", apiservertesting.ProviderInstance.Zones), @@ -113,6 +114,7 @@ apiservertesting.SharedStub.SetErrors( nil, // Backing.AvailabilityZones nil, // Backing.ModelConfig + nil, // Backing.CloudSpec nil, // Provider.Open nil, // ZonedEnviron.AvailabilityZones errors.NotSupportedf("setting"), // Backing.SetAvailabilityZones @@ -129,6 +131,7 @@ apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub, apiservertesting.BackingCall("AvailabilityZones"), apiservertesting.BackingCall("ModelConfig"), + apiservertesting.BackingCall("CloudSpec"), apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), apiservertesting.ZonedEnvironCall("AvailabilityZones"), apiservertesting.BackingCall("SetAvailabilityZones", apiservertesting.ProviderInstance.Zones), @@ -146,6 +149,7 @@ apiservertesting.SharedStub.SetErrors( nil, // Backing.AvailabilityZones nil, // Backing.ModelConfig + nil, // Backing.CloudSpec nil, // Provider.Open errors.NotValidf("foo"), // ZonedEnviron.AvailabilityZones ) @@ -161,6 +165,7 @@ apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub, apiservertesting.BackingCall("AvailabilityZones"), apiservertesting.BackingCall("ModelConfig"), + apiservertesting.BackingCall("CloudSpec"), apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), apiservertesting.ZonedEnvironCall("AvailabilityZones"), ) @@ -204,6 +209,7 @@ apiservertesting.SharedStub.SetErrors( nil, // Backing.AvailabilityZones nil, // Backing.ModelConfig + nil, // Backing.CloudSpec errors.NotValidf("config"), // Provider.Open ) @@ -218,6 +224,7 @@ apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub, apiservertesting.BackingCall("AvailabilityZones"), apiservertesting.BackingCall("ModelConfig"), + apiservertesting.BackingCall("CloudSpec"), apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), ) } @@ -242,6 +249,7 @@ apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub, apiservertesting.BackingCall("AvailabilityZones"), apiservertesting.BackingCall("ModelConfig"), + apiservertesting.BackingCall("CloudSpec"), apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), ) } @@ -401,15 +409,18 @@ // caching subnets (2nd attepmt): fails nil, // BackingInstance.ModelConfig (2nd call) + nil, // BackingInstance.CloudSpec (1st call) errors.NotFoundf("provider"), // ProviderInstance.Open (1st call) // caching subnets (3rd attempt): fails nil, // BackingInstance.ModelConfig (3rd call) + nil, // BackingInstance.CloudSpec (2nd call) nil, // ProviderInstance.Open (2nd call) errors.NotFoundf("subnets"), // NetworkingEnvironInstance.Subnets (1st call) // caching subnets (4th attempt): succeeds nil, // BackingInstance.ModelConfig (4th call) + nil, // BackingInstance.CloudSpec (3rd call) nil, // ProviderInstance.Open (3rd call) nil, // NetworkingEnvironInstance.Subnets (2nd call) @@ -519,15 +530,18 @@ // caching subnets (2nd attepmt): fails apiservertesting.BackingCall("ModelConfig"), + apiservertesting.BackingCall("CloudSpec"), apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), // caching subnets (3rd attempt): fails apiservertesting.BackingCall("ModelConfig"), + apiservertesting.BackingCall("CloudSpec"), apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), apiservertesting.NetworkingEnvironCall("Subnets", instance.UnknownId, []network.Id(nil)), // caching subnets (4th attempt): succeeds apiservertesting.BackingCall("ModelConfig"), + apiservertesting.BackingCall("CloudSpec"), apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), apiservertesting.NetworkingEnvironCall("Subnets", instance.UnknownId, []network.Id(nil)), @@ -567,6 +581,7 @@ // These calls always happen. expectedCalls := []apiservertesting.StubMethodCall{ apiservertesting.BackingCall("ModelConfig"), + apiservertesting.BackingCall("CloudSpec"), apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), } @@ -625,6 +640,7 @@ // updateZones tries to constructs a ZonedEnviron with these calls. zoneCalls := append([]apiservertesting.StubMethodCall{}, apiservertesting.BackingCall("ModelConfig"), + apiservertesting.BackingCall("CloudSpec"), apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), ) // Receiver can differ according to envName, but diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/networkingcommon/types.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/networkingcommon/types.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/networkingcommon/types.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/networkingcommon/types.go 2016-08-16 08:56:25.000000000 +0000 @@ -17,7 +17,6 @@ "github.com/juju/juju/apiserver/params" "github.com/juju/juju/cloudconfig/instancecfg" "github.com/juju/juju/environs" - "github.com/juju/juju/environs/config" "github.com/juju/juju/network" providercommon "github.com/juju/juju/provider/common" "github.com/juju/juju/state" @@ -106,8 +105,7 @@ // retrieve information from the underlying persistency layer (state // DB). type NetworkBacking interface { - // ModelConfig returns the current environment config. - ModelConfig() (*config.Config, error) + environs.EnvironConfigGetter // AvailabilityZones returns all cached availability zones (i.e. // not from the provider, but in state). diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/registry.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/registry.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/registry.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/registry.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,48 +7,44 @@ "fmt" "reflect" "runtime" - "sort" "strings" "github.com/juju/errors" - "github.com/juju/utils/featureflag" "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common/apihttp" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/state" ) -// FacadeFactory represent a way of creating a Facade from the current -// connection to the State. -type FacadeFactory func( - st *state.State, resources *Resources, authorizer Authorizer, id string, -) ( - interface{}, error, -) - -type facadeRecord struct { - factory FacadeFactory - facadeType reflect.Type - // If the feature is not the empty string, then this facade - // is only returned when that feature flag is set. - feature string -} +// Facades is the registry that tracks all of the Facades that will be +// exposed in the API. It can be used to List/Get/Register facades. +// +// Most implementers of a facade will probably want to use +// RegisterStandardFacade rather than Facades.Register, as it provides +// much cleaner syntax and semantics for calling during init(). +// +// Developers in a happy future will not want to use Facades at all, +// eschewing globals in favour of explicitly building apis and supplying +// them directly to an apiserver. +var Facades = &facade.Registry{} // RegisterFacade updates the global facade registry with a new version of a new type. -func RegisterFacade(name string, version int, factory FacadeFactory, facadeType reflect.Type) { +func RegisterFacade(name string, version int, factory facade.Factory, facadeType reflect.Type) { RegisterFacadeForFeature(name, version, factory, facadeType, "") } // RegisterFacadeForFeature updates the global facade registry with a new // version of a new type. If the feature is non-empty, this facade is only // available when the specified feature flag is set. -func RegisterFacadeForFeature(name string, version int, factory FacadeFactory, facadeType reflect.Type, feature string) { +func RegisterFacadeForFeature(name string, version int, factory facade.Factory, facadeType reflect.Type, feature string) { err := Facades.Register(name, version, factory, facadeType, feature) if err != nil { // This is meant to be called during init() so errors should be // considered fatal. panic(err) } + logger.Tracef("Registered facade %q v%d", name, version) } // validateNewFacade ensures that the facade factory we have has the right @@ -66,7 +62,15 @@ return fmt.Errorf("function %q does not take 3 parameters and return 2", funcName) } - facadeType := reflect.TypeOf((*FacadeFactory)(nil)).Elem() + + type nastyFactory func( + st *state.State, + resources facade.Resources, + authorizer facade.Authorizer, + ) ( + interface{}, error, + ) + facadeType := reflect.TypeOf((*nastyFactory)(nil)).Elem() isSame := true for i := 0; i < 3; i++ { if funcType.In(i) != facadeType.In(i) { @@ -78,30 +82,29 @@ isSame = false } if !isSame { - return fmt.Errorf("function %q does not have the signature func (*state.State, *common.Resources, common.Authorizer) (*Type, error)", + return fmt.Errorf("function %q does not have the signature func (*state.State, facade.Resources, facade.Authorizer) (*Type, error)", funcName) } return nil } // wrapNewFacade turns a given NewFoo(st, resources, authorizer) (*Instance, error) -// function and wraps it into a proper FacadeFactory function. -func wrapNewFacade(newFunc interface{}) (FacadeFactory, reflect.Type, error) { +// function and wraps it into a proper facade.Factory function. +func wrapNewFacade(newFunc interface{}) (facade.Factory, reflect.Type, error) { funcValue := reflect.ValueOf(newFunc) err := validateNewFacade(funcValue) if err != nil { return nil, reflect.TypeOf(nil), err } // So we know newFunc is a func with the right args in and out, so - // wrap it into a helper function that matches the FacadeFactory. - wrapped := func( - st *state.State, resources *Resources, auth Authorizer, id string, - ) ( - interface{}, error, - ) { - if id != "" { + // wrap it into a helper function that matches the facade.Factory. + wrapped := func(context facade.Context) (facade.Facade, error) { + if context.ID() != "" { return nil, ErrBadId } + st := context.State() + auth := context.Auth() + resources := context.Resources() // st, resources, or auth is nil, then reflect.Call dies // because reflect.ValueOf(anynil) is the Zero Value. // So we use &obj.Elem() which gives us a concrete Value object @@ -130,9 +133,17 @@ // hook-context-facade to a standard facade so the caller's factory // method can elide unnecessary arguments. This function also handles // any necessary authorization for the client. +// +// XXX(fwereade): this is fundamentally broken, because it (1) +// arbitrarily creates a new facade for a tiny fragment of a specific +// client worker's reponsibilities and (2) actively conceals necessary +// auth information from the facade. Don't call it; actively work to +// delete code that uses it, and rewrite it properly. func RegisterHookContextFacade(name string, version int, newHookContextFacade NewHookContextFacadeFn, facadeType reflect.Type) { - newFacade := func(st *state.State, _ *Resources, authorizer Authorizer, _ string) (interface{}, error) { + newFacade := func(context facade.Context) (facade.Facade, error) { + authorizer := context.Auth() + st := context.State() if !authorizer.AuthUnitAgent() { return nil, ErrPerm @@ -158,7 +169,7 @@ // RegisterStandardFacade registers a factory function for a normal New* style // function. This requires that the function has the form: -// NewFoo(*state.State, *common.Resources, common.Authorizer) (*Type, error) +// NewFoo(*state.State, facade.Resources, facade.Authorizer) (*Type, error) // With that syntax, we will create a helper function that wraps calling NewFoo // with the right parameters, and returns the *Type correctly. func RegisterStandardFacade(name string, version int, newFunc interface{}) { @@ -167,7 +178,7 @@ // RegisterStandardFacadeForFeature registers a factory function for a normal // New* style function. This requires that the function has the form: -// NewFoo(*state.State, *common.Resources, common.Authorizer) (*Type, error) +// NewFoo(*state.State, facade.Resources, facade.Authorizer) (*Type, error) // With that syntax, we will create a helper function that wraps calling // NewFoo with the right parameters, and returns the *Type correctly. If the // feature is non-empty, this facade is only available when the specified @@ -180,137 +191,6 @@ RegisterFacadeForFeature(name, version, wrapped, facadeType, feature) } -// Facades is the registry that tracks all of the Facades that will be exposed in the API. -// It can be used to List/Get/Register facades. -// Most implementers of a facade will probably want to use -// RegisterStandardFacade rather than Facades.Register, as it provides much -// cleaner syntax and semantics for calling during init(). -var Facades = &FacadeRegistry{} - -// versions is our internal structure for tracking specific versions of a -// single facade. We use a map to be able to quickly lookup a version. -type versions map[int]facadeRecord - -// FacadeRegistry is responsible for tracking what Facades are going to be exported in the API. -// See the variable "Facades" for the singleton that tracks them. -// It would be possible to have multiple registries if we decide to change how -// the API exposes methods based on Login information. -type FacadeRegistry struct { - facades map[string]versions -} - -// Register adds a single named facade at a given version to the registry. -// FacadeFactory will be called when someone wants to instantiate an object of -// this facade, and facadeType defines the concrete type that the returned object will be. -// The Type information is used to define what methods will be exported in the -// API, and it must exactly match the actual object returned by the factory. -func (f *FacadeRegistry) Register(name string, version int, factory FacadeFactory, facadeType reflect.Type, feature string) error { - if f.facades == nil { - f.facades = make(map[string]versions, 1) - } - record := facadeRecord{ - factory: factory, - facadeType: facadeType, - feature: feature, - } - if vers, ok := f.facades[name]; ok { - if _, ok := vers[version]; ok { - fullname := fmt.Sprintf("%s(%d)", name, version) - return fmt.Errorf("object %q already registered", fullname) - } - vers[version] = record - } else { - f.facades[name] = versions{version: record} - } - logger.Tracef("Registered facade %q v%d", name, version) - return nil -} - -// lookup translates a facade name and version into a facadeRecord. -func (f *FacadeRegistry) lookup(name string, version int) (facadeRecord, error) { - if versions, ok := f.facades[name]; ok { - if record, ok := versions[version]; ok { - if featureflag.Enabled(record.feature) { - return record, nil - } - } - } - return facadeRecord{}, errors.NotFoundf("%s(%d)", name, version) -} - -// GetFactory returns just the FacadeFactory for a given Facade name and version. -// See also GetType for getting the type information instead of the creation factory. -func (f *FacadeRegistry) GetFactory(name string, version int) (FacadeFactory, error) { - record, err := f.lookup(name, version) - if err != nil { - return nil, err - } - return record.factory, nil -} - -// GetType returns the type information for a given Facade name and version. -// This can be used for introspection purposes (to determine what methods are -// available, etc). -func (f *FacadeRegistry) GetType(name string, version int) (reflect.Type, error) { - record, err := f.lookup(name, version) - if err != nil { - return nil, err - } - return record.facadeType, nil -} - -// FacadeDescription describes the name and what versions of a facade have been -// registered. -type FacadeDescription struct { - Name string - Versions []int -} - -// descriptionFromVersions aggregates the information in a versions map into a -// more friendly form for List(). -func descriptionFromVersions(name string, vers versions) FacadeDescription { - intVersions := make([]int, 0, len(vers)) - for version, record := range vers { - if featureflag.Enabled(record.feature) { - intVersions = append(intVersions, version) - } - } - sort.Ints(intVersions) - return FacadeDescription{ - Name: name, - Versions: intVersions, - } -} - -// List returns a slice describing each of the registered Facades. -func (f *FacadeRegistry) List() []FacadeDescription { - names := make([]string, 0, len(f.facades)) - for name := range f.facades { - names = append(names, name) - } - sort.Strings(names) - descriptions := make([]FacadeDescription, 0, len(f.facades)) - for _, name := range names { - facades := f.facades[name] - description := descriptionFromVersions(name, facades) - if len(description.Versions) > 0 { - descriptions = append(descriptions, description) - } - } - return descriptions -} - -// Discard gets rid of a registration that has already been done. Calling -// discard on an entry that is not present is not considered an error. -func (f *FacadeRegistry) Discard(name string, version int) { - if versions, ok := f.facades[name]; ok { - delete(versions, version) - if len(versions) == 0 { - delete(f.facades, name) - } - } -} - var endpointRegistry = map[string]apihttp.HandlerSpec{} var endpointRegistryOrder []string diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/registry_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/registry_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/registry_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/registry_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,6 +11,8 @@ gc "gopkg.in/check.v1" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" + "github.com/juju/juju/apiserver/facade/facadetest" apiservertesting "github.com/juju/juju/apiserver/testing" "github.com/juju/juju/rpc/rpcreflect" "github.com/juju/juju/state" @@ -23,32 +25,17 @@ var _ = gc.Suite(&facadeRegistrySuite{}) -func testFacade( - st *state.State, resources *common.Resources, - authorizer common.Authorizer, id string, -) (interface{}, error) { - return "myobject", nil -} - func (s *facadeRegistrySuite) TestRegister(c *gc.C) { common.SanitizeFacades(s) var v interface{} common.RegisterFacade("myfacade", 0, testFacade, reflect.TypeOf(&v).Elem()) f, err := common.Facades.GetFactory("myfacade", 0) c.Assert(err, jc.ErrorIsNil) - val, err := f(nil, nil, nil, "") + val, err := f(nil) c.Assert(err, jc.ErrorIsNil) c.Check(val, gc.Equals, "myobject") } -func (*facadeRegistrySuite) TestGetFactoryUnknown(c *gc.C) { - r := &common.FacadeRegistry{} - f, err := r.GetFactory("name", 0) - c.Check(err, jc.Satisfies, errors.IsNotFound) - c.Check(err, gc.ErrorMatches, `name\(0\) not found`) - c.Check(f, gc.IsNil) -} - func (s *facadeRegistrySuite) TestRegisterForFeature(c *gc.C) { common.SanitizeFacades(s) var v interface{} @@ -60,20 +47,11 @@ f, err = common.Facades.GetFactory("myfacade", 0) c.Assert(err, jc.ErrorIsNil) - val, err := f(nil, nil, nil, "") + val, err := f(nil) c.Assert(err, jc.ErrorIsNil) c.Check(val, gc.Equals, "myobject") } -func (*facadeRegistrySuite) TestGetFactoryUnknownVersion(c *gc.C) { - r := &common.FacadeRegistry{} - c.Assert(r.Register("name", 0, validIdFactory, intPtrType, ""), gc.IsNil) - f, err := r.GetFactory("name", 1) - c.Check(err, jc.Satisfies, errors.IsNotFound) - c.Check(err, gc.ErrorMatches, `name\(1\) not found`) - c.Check(f, gc.IsNil) -} - func (s *facadeRegistrySuite) TestRegisterFacadePanicsOnDoubleRegistry(c *gc.C) { var v interface{} doRegister := func() { @@ -83,36 +61,6 @@ c.Assert(doRegister, gc.PanicMatches, `object "myfacade\(0\)" already registered`) } -func checkValidateNewFacadeFailsWith(c *gc.C, obj interface{}, errMsg string) { - err := common.ValidateNewFacade(reflect.ValueOf(obj)) - c.Check(err, gc.NotNil) - c.Check(err, gc.ErrorMatches, errMsg) -} - -func noArgs() { -} - -func badCountIn(a string) (*int, error) { - return nil, nil -} - -func badCountOut(a, b, c string) error { - return nil -} - -func wrongIn(a, b, c string) (*int, error) { - return nil, nil -} - -func wrongOut(*state.State, *common.Resources, common.Authorizer) (error, *int) { - return nil, nil -} - -func validFactory(*state.State, *common.Resources, common.Authorizer) (*int, error) { - var i = 100 - return &i, nil -} - func (*facadeRegistrySuite) TestValidateNewFacade(c *gc.C) { checkValidateNewFacadeFailsWith(c, nil, `cannot wrap nil`) @@ -125,9 +73,9 @@ checkValidateNewFacadeFailsWith(c, badCountOut, `function ".*badCountOut" does not take 3 parameters and return 2`) checkValidateNewFacadeFailsWith(c, wrongIn, - `function ".*wrongIn" does not have the signature func \(\*state.State, \*common.Resources, common.Authorizer\) \(\*Type, error\)`) + `function ".*wrongIn" does not have the signature func \(\*state.State, facade.Resources, facade.Authorizer\) \(\*Type, error\)`) checkValidateNewFacadeFailsWith(c, wrongOut, - `function ".*wrongOut" does not have the signature func \(\*state.State, \*common.Resources, common.Authorizer\) \(\*Type, error\)`) + `function ".*wrongOut" does not have the signature func \(\*state.State, facade.Resources, facade.Authorizer\) \(\*Type, error\)`) err := common.ValidateNewFacade(reflect.ValueOf(validFactory)) c.Assert(err, jc.ErrorIsNil) } @@ -140,7 +88,9 @@ func (*facadeRegistrySuite) TestWrapNewFacadeHandlesId(c *gc.C) { wrapped, _, err := common.WrapNewFacade(validFactory) c.Assert(err, jc.ErrorIsNil) - val, err := wrapped(nil, nil, nil, "badId") + val, err := wrapped(facadetest.Context{ + ID_: "badId", + }) c.Check(err, gc.ErrorMatches, "id not found") c.Check(val, gc.Equals, nil) } @@ -148,30 +98,29 @@ func (*facadeRegistrySuite) TestWrapNewFacadeCallsFunc(c *gc.C) { wrapped, _, err := common.WrapNewFacade(validFactory) c.Assert(err, jc.ErrorIsNil) - val, err := wrapped(nil, nil, nil, "") + val, err := wrapped(facadetest.Context{}) c.Assert(err, jc.ErrorIsNil) c.Check(*(val.(*int)), gc.Equals, 100) } -type myResult struct { - st *state.State - resources *common.Resources - auth common.Authorizer -} - func (*facadeRegistrySuite) TestWrapNewFacadeCallsWithRightParams(c *gc.C) { authorizer := apiservertesting.FakeAuthorizer{} resources := common.NewResources() testFunc := func( - st *state.State, resources *common.Resources, - authorizer common.Authorizer, + st *state.State, + resources facade.Resources, + authorizer facade.Authorizer, ) (*myResult, error) { return &myResult{st, resources, authorizer}, nil } wrapped, facadeType, err := common.WrapNewFacade(testFunc) c.Assert(err, jc.ErrorIsNil) c.Check(facadeType, gc.Equals, reflect.TypeOf((*myResult)(nil))) - val, err := wrapped(nil, resources, authorizer, "") + + val, err := wrapped(facadetest.Context{ + Resources_: resources, + Auth_: authorizer, + }) c.Assert(err, jc.ErrorIsNil) asResult := val.(*myResult) c.Check(asResult.st, gc.IsNil) @@ -184,7 +133,7 @@ common.RegisterStandardFacade("testing", 0, validFactory) wrapped, err := common.Facades.GetFactory("testing", 0) c.Assert(err, jc.ErrorIsNil) - val, err := wrapped(nil, nil, nil, "") + val, err := wrapped(facadetest.Context{}) c.Assert(err, jc.ErrorIsNil) c.Check(*(val.(*int)), gc.Equals, 100) } @@ -218,151 +167,42 @@ } } -func validIdFactory(*state.State, *common.Resources, common.Authorizer, string) (interface{}, error) { - var i = 100 - return &i, nil -} - -var intPtr = new(int) -var intPtrType = reflect.TypeOf(&intPtr).Elem() - -func (*facadeRegistrySuite) TestDescriptionFromVersions(c *gc.C) { - facades := common.Versions{0: common.NilFacadeRecord} - c.Check(common.DescriptionFromVersions("name", facades), - gc.DeepEquals, - common.FacadeDescription{ - Name: "name", - Versions: []int{0}, - }) - facades[2] = common.NilFacadeRecord - c.Check(common.DescriptionFromVersions("name", facades), - gc.DeepEquals, - common.FacadeDescription{ - Name: "name", - Versions: []int{0, 2}, - }) -} - -func (*facadeRegistrySuite) TestDescriptionFromVersionsAreSorted(c *gc.C) { - facades := common.Versions{ - 10: common.NilFacadeRecord, - 5: common.NilFacadeRecord, - 0: common.NilFacadeRecord, - 18: common.NilFacadeRecord, - 6: common.NilFacadeRecord, - 4: common.NilFacadeRecord, - } - c.Check(common.DescriptionFromVersions("name", facades), - gc.DeepEquals, - common.FacadeDescription{ - Name: "name", - Versions: []int{0, 4, 5, 6, 10, 18}, - }) -} - -func (*facadeRegistrySuite) TestRegisterAndList(c *gc.C) { - r := &common.FacadeRegistry{} - c.Assert(r.Register("name", 0, validIdFactory, intPtrType, ""), gc.IsNil) - c.Check(r.List(), gc.DeepEquals, []common.FacadeDescription{ - {Name: "name", Versions: []int{0}}, - }) +func testFacade(facade.Context) (facade.Facade, error) { + return "myobject", nil } -func (*facadeRegistrySuite) TestRegisterAndListMultiple(c *gc.C) { - r := &common.FacadeRegistry{} - c.Assert(r.Register("other", 0, validIdFactory, intPtrType, ""), gc.IsNil) - c.Assert(r.Register("name", 0, validIdFactory, intPtrType, ""), gc.IsNil) - c.Assert(r.Register("third", 2, validIdFactory, intPtrType, ""), gc.IsNil) - c.Assert(r.Register("third", 3, validIdFactory, intPtrType, ""), gc.IsNil) - c.Check(r.List(), gc.DeepEquals, []common.FacadeDescription{ - {Name: "name", Versions: []int{0}}, - {Name: "other", Versions: []int{0}}, - {Name: "third", Versions: []int{2, 3}}, - }) +type myResult struct { + st *state.State + resources facade.Resources + auth facade.Authorizer } -func (s *facadeRegistrySuite) TestRegisterAndListMultipleWithFeatures(c *gc.C) { - r := &common.FacadeRegistry{} - c.Assert(r.Register("other", 0, validIdFactory, intPtrType, "special"), gc.IsNil) - c.Assert(r.Register("name", 0, validIdFactory, intPtrType, ""), gc.IsNil) - c.Assert(r.Register("name", 1, validIdFactory, intPtrType, "special"), gc.IsNil) - c.Assert(r.Register("third", 2, validIdFactory, intPtrType, ""), gc.IsNil) - c.Assert(r.Register("third", 3, validIdFactory, intPtrType, "magic"), gc.IsNil) - s.SetFeatureFlags("magic") - c.Check(r.List(), gc.DeepEquals, []common.FacadeDescription{ - {Name: "name", Versions: []int{0}}, - {Name: "third", Versions: []int{2, 3}}, - }) +func checkValidateNewFacadeFailsWith(c *gc.C, obj interface{}, errMsg string) { + err := common.ValidateNewFacade(reflect.ValueOf(obj)) + c.Check(err, gc.NotNil) + c.Check(err, gc.ErrorMatches, errMsg) } -func (*facadeRegistrySuite) TestRegisterAlreadyPresent(c *gc.C) { - r := &common.FacadeRegistry{} - err := r.Register("name", 0, validIdFactory, intPtrType, "") - c.Assert(err, jc.ErrorIsNil) - secondIdFactory := func(*state.State, *common.Resources, common.Authorizer, string) (interface{}, error) { - var i = 200 - return &i, nil - } - err = r.Register("name", 0, secondIdFactory, intPtrType, "") - c.Check(err, gc.ErrorMatches, `object "name\(0\)" already registered`) - f, err := r.GetFactory("name", 0) - c.Assert(err, jc.ErrorIsNil) - c.Assert(f, gc.NotNil) - val, err := f(nil, nil, nil, "") - c.Assert(err, jc.ErrorIsNil) - asIntPtr := val.(*int) - c.Check(*asIntPtr, gc.Equals, 100) +func noArgs() { } -func (*facadeRegistrySuite) TestGetFactory(c *gc.C) { - r := &common.FacadeRegistry{} - c.Assert(r.Register("name", 0, validIdFactory, intPtrType, ""), gc.IsNil) - f, err := r.GetFactory("name", 0) - c.Assert(err, jc.ErrorIsNil) - c.Assert(f, gc.NotNil) - res, err := f(nil, nil, nil, "") - c.Assert(err, jc.ErrorIsNil) - c.Assert(res, gc.NotNil) - asIntPtr := res.(*int) - c.Check(*asIntPtr, gc.Equals, 100) +func badCountIn(a string) (*int, error) { + return nil, nil } -func (*facadeRegistrySuite) TestGetType(c *gc.C) { - r := &common.FacadeRegistry{} - c.Assert(r.Register("name", 0, validIdFactory, intPtrType, ""), gc.IsNil) - typ, err := r.GetType("name", 0) - c.Assert(err, jc.ErrorIsNil) - c.Check(typ, gc.Equals, intPtrType) +func badCountOut(a, b, c string) error { + return nil } -func (*facadeRegistrySuite) TestDiscardHandlesNotPresent(c *gc.C) { - r := &common.FacadeRegistry{} - r.Discard("name", 1) +func wrongIn(a, b, c string) (*int, error) { + return nil, nil } -func (*facadeRegistrySuite) TestDiscardRemovesEntry(c *gc.C) { - r := &common.FacadeRegistry{} - c.Assert(r.Register("name", 0, validIdFactory, intPtrType, ""), gc.IsNil) - _, err := r.GetFactory("name", 0) - c.Assert(err, jc.ErrorIsNil) - r.Discard("name", 0) - f, err := r.GetFactory("name", 0) - c.Check(err, jc.Satisfies, errors.IsNotFound) - c.Check(err, gc.ErrorMatches, `name\(0\) not found`) - c.Check(f, gc.IsNil) - c.Check(r.List(), gc.DeepEquals, []common.FacadeDescription{}) +func wrongOut(*state.State, facade.Resources, facade.Authorizer) (error, *int) { + return nil, nil } -func (*facadeRegistrySuite) TestDiscardLeavesOtherVersions(c *gc.C) { - r := &common.FacadeRegistry{} - c.Assert(r.Register("name", 0, validIdFactory, intPtrType, ""), gc.IsNil) - c.Assert(r.Register("name", 1, validIdFactory, intPtrType, ""), gc.IsNil) - r.Discard("name", 0) - _, err := r.GetFactory("name", 0) - c.Check(err, jc.Satisfies, errors.IsNotFound) - _, err = r.GetFactory("name", 1) - c.Check(err, jc.ErrorIsNil) - c.Check(r.List(), gc.DeepEquals, []common.FacadeDescription{ - {Name: "name", Versions: []int{1}}, - }) +func validFactory(*state.State, facade.Resources, facade.Authorizer) (*int, error) { + var i = 100 + return &i, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/resource.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/resource.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/resource.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/resource.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,14 +7,9 @@ "fmt" "strconv" "sync" -) -// Resource represents any resource that should be cleaned up when an -// API connection terminates. The Stop method will be called when -// that happens. -type Resource interface { - Stop() error -} + "github.com/juju/juju/apiserver/facade" +) // Resources holds all the resources for a connection. // It allows the registration of resources that will be cleaned @@ -22,21 +17,25 @@ type Resources struct { mu sync.Mutex maxId uint64 - resources map[string]Resource + resources map[string]facade.Resource + // The stack is used to control the order of destruction. // last registered, first stopped. + // XXX(fwereade): is this necessary only because we have + // Resource instead of Worker (which would let us kill them all, + // and wait for them all, without danger of races)? stack []string } func NewResources() *Resources { return &Resources{ - resources: make(map[string]Resource), + resources: make(map[string]facade.Resource), } } // Get returns the resource for the given id, or // nil if there is no such resource. -func (rs *Resources) Get(id string) Resource { +func (rs *Resources) Get(id string) facade.Resource { rs.mu.Lock() defer rs.mu.Unlock() return rs.resources[id] @@ -45,7 +44,7 @@ // Register registers the given resource. It returns a unique // identifier for the resource which can then be used in // subsequent API requests to refer to the resource. -func (rs *Resources) Register(r Resource) string { +func (rs *Resources) Register(r facade.Resource) string { rs.mu.Lock() defer rs.mu.Unlock() rs.maxId++ @@ -63,7 +62,7 @@ // replaced, but we don't have a need for that yet.) // It is also an error to supply a name that is an integer string, since that // collides with the auto-naming from Register. -func (rs *Resources) RegisterNamed(name string, r Resource) error { +func (rs *Resources) RegisterNamed(name string, r facade.Resource) error { rs.mu.Lock() defer rs.mu.Unlock() if _, err := strconv.Atoi(name); err == nil { @@ -119,7 +118,7 @@ logger.Errorf("error stopping %T resource: %v", r, err) } } - rs.resources = make(map[string]Resource) + rs.resources = make(map[string]facade.Resource) rs.stack = nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/storagecommon/filesystems.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/storagecommon/filesystems.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/storagecommon/filesystems.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/storagecommon/filesystems.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,6 +10,7 @@ "github.com/juju/juju/apiserver/params" "github.com/juju/juju/environs/config" "github.com/juju/juju/state" + "github.com/juju/juju/storage" "github.com/juju/juju/storage/poolmanager" ) @@ -21,6 +22,7 @@ modelUUID, controllerUUID string, environConfig *config.Config, poolManager poolmanager.PoolManager, + registry storage.ProviderRegistry, ) (params.FilesystemParams, error) { var pool string @@ -42,7 +44,7 @@ return params.FilesystemParams{}, errors.Annotate(err, "computing storage tags") } - providerType, cfg, err := StoragePoolConfig(pool, poolManager) + providerType, cfg, err := StoragePoolConfig(pool, poolManager, registry) if err != nil { return params.FilesystemParams{}, errors.Trace(err) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/storagecommon/volumes.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/storagecommon/volumes.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/storagecommon/volumes.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/storagecommon/volumes.go 2016-08-16 08:56:25.000000000 +0000 @@ -12,7 +12,6 @@ "github.com/juju/juju/state" "github.com/juju/juju/storage" "github.com/juju/juju/storage/poolmanager" - "github.com/juju/juju/storage/provider/registry" ) type volumeAlreadyProvisionedError struct { @@ -34,6 +33,7 @@ modelUUID, controllerUUID string, environConfig *config.Config, poolManager poolmanager.PoolManager, + registry storage.ProviderRegistry, ) (params.VolumeParams, error) { var pool string @@ -55,7 +55,7 @@ return params.VolumeParams{}, errors.Annotate(err, "computing storage tags") } - providerType, cfg, err := StoragePoolConfig(pool, poolManager) + providerType, cfg, err := StoragePoolConfig(pool, poolManager, registry) if err != nil { return params.VolumeParams{}, errors.Trace(err) } @@ -74,7 +74,7 @@ // such pool with the specified name, but it identifies a // storage provider, then that type will be returned with a // nil configuration. -func StoragePoolConfig(name string, poolManager poolmanager.PoolManager) (storage.ProviderType, *storage.Config, error) { +func StoragePoolConfig(name string, poolManager poolmanager.PoolManager, registry storage.ProviderRegistry) (storage.ProviderType, *storage.Config, error) { pool, err := poolManager.Get(name) if errors.IsNotFound(err) { // If not a storage pool, then maybe a provider type. diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/storagecommon/volumes_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/storagecommon/volumes_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/storagecommon/volumes_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/storagecommon/volumes_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -12,6 +12,7 @@ "github.com/juju/juju/apiserver/params" "github.com/juju/juju/environs/tags" "github.com/juju/juju/state" + "github.com/juju/juju/storage/provider" "github.com/juju/juju/testing" ) @@ -44,6 +45,7 @@ "resource-tags": "a=b c=", }), &fakePoolManager{}, + provider.CommonStorageProviders(), ) c.Assert(err, jc.ErrorIsNil) c.Assert(p, jc.DeepEquals, params.VolumeParams{ @@ -72,6 +74,7 @@ testing.ModelTag.Id(), testing.CustomModelConfig(c, nil), &fakePoolManager{}, + provider.CommonStorageProviders(), ) c.Assert(err, jc.ErrorIsNil) c.Assert(p, jc.DeepEquals, params.VolumeParams{ diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/testing/modelwatcher.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/testing/modelwatcher.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/testing/modelwatcher.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/testing/modelwatcher.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,6 +11,7 @@ "github.com/juju/juju/apiserver/params" "github.com/juju/juju/environs" "github.com/juju/juju/state" + "github.com/juju/juju/state/stateenvirons" statetesting "github.com/juju/juju/state/testing" ) @@ -45,6 +46,7 @@ func (s *ModelWatcherTest) AssertModelConfig(c *gc.C, envWatcher ModelWatcher, hasSecrets bool) { envConfig, err := s.st.ModelConfig() c.Assert(err, jc.ErrorIsNil) + newEnviron := stateenvirons.GetNewEnvironFunc(environs.New) result, err := envWatcher.ModelConfig() c.Assert(err, jc.ErrorIsNil) @@ -53,7 +55,7 @@ // If the implementor doesn't provide secrets, we need to replace the config // values in our environment to compare against with the secrets replaced. if !hasSecrets { - env, err := environs.New(envConfig) + env, err := newEnviron(s.st) c.Assert(err, jc.ErrorIsNil) secretAttrs, err := env.Provider().SecretAttrs(envConfig) c.Assert(err, jc.ErrorIsNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/tools_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/tools_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/tools_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/tools_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -22,6 +22,7 @@ "github.com/juju/juju/network" "github.com/juju/juju/state" "github.com/juju/juju/state/binarystorage" + "github.com/juju/juju/state/stateenvirons" coretools "github.com/juju/juju/tools" jujuversion "github.com/juju/juju/version" ) @@ -53,7 +54,9 @@ return tag == names.NewMachineTag("0") || tag == names.NewMachineTag("42") }, nil } - tg := common.NewToolsGetter(s.State, s.State, s.State, sprintfURLGetter("tools:%s"), getCanRead) + tg := common.NewToolsGetter( + s.State, stateenvirons.EnvironConfigGetter{s.State}, s.State, sprintfURLGetter("tools:%s"), getCanRead, + ) c.Assert(tg, gc.NotNil) err := s.machine0.SetAgentVersion(current) @@ -82,7 +85,9 @@ getCanRead := func() (common.AuthFunc, error) { return nil, fmt.Errorf("splat") } - tg := common.NewToolsGetter(s.State, s.State, s.State, sprintfURLGetter("%s"), getCanRead) + tg := common.NewToolsGetter( + s.State, stateenvirons.EnvironConfigGetter{s.State}, s.State, sprintfURLGetter("%s"), getCanRead, + ) args := params.Entities{ Entities: []params.Entity{{Tag: "machine-42"}}, } @@ -175,7 +180,9 @@ c.Assert(filter.Arch, gc.Equals, "alpha") return envtoolsList, nil }) - toolsFinder := common.NewToolsFinder(s.State, &mockToolsStorage{metadata: storageMetadata}, sprintfURLGetter("tools:%s")) + toolsFinder := common.NewToolsFinder( + stateenvirons.EnvironConfigGetter{s.State}, &mockToolsStorage{metadata: storageMetadata}, sprintfURLGetter("tools:%s"), + ) result, err := toolsFinder.FindTools(params.FindToolsParams{ MajorVersion: 123, MinorVersion: 456, @@ -202,7 +209,7 @@ s.PatchValue(common.EnvtoolsFindTools, func(e environs.Environ, major, minor int, stream string, filter coretools.Filter) (list coretools.List, err error) { return nil, errors.NotFoundf("tools") }) - toolsFinder := common.NewToolsFinder(s.State, s.State, sprintfURLGetter("%s")) + toolsFinder := common.NewToolsFinder(stateenvirons.EnvironConfigGetter{s.State}, s.State, sprintfURLGetter("%s")) result, err := toolsFinder.FindTools(params.FindToolsParams{}) c.Assert(err, jc.ErrorIsNil) c.Assert(result.Error, jc.Satisfies, params.IsCodeNotFound) @@ -246,7 +253,7 @@ } return nil, errors.NotFoundf("tools") }) - toolsFinder := common.NewToolsFinder(s.State, t, sprintfURLGetter("tools:%s")) + toolsFinder := common.NewToolsFinder(stateenvirons.EnvironConfigGetter{s.State}, t, sprintfURLGetter("tools:%s")) result, err := toolsFinder.FindTools(params.FindToolsParams{ Number: jujuversion.Current, MajorVersion: -1, @@ -270,7 +277,7 @@ called = true return nil, errors.NotFoundf("tools") }) - toolsFinder := common.NewToolsFinder(s.State, &mockToolsStorage{ + toolsFinder := common.NewToolsFinder(stateenvirons.EnvironConfigGetter{s.State}, &mockToolsStorage{ err: errors.New("AllMetadata failed"), }, sprintfURLGetter("tools:%s")) result, err := toolsFinder.FindTools(params.FindToolsParams{ diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/unitswatcher.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/unitswatcher.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/unitswatcher.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/unitswatcher.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,6 +7,7 @@ "github.com/juju/errors" "gopkg.in/juju/names.v2" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" "github.com/juju/juju/state/watcher" @@ -16,14 +17,14 @@ // various facades. type UnitsWatcher struct { st state.EntityFinder - resources *Resources + resources facade.Resources getCanWatch GetAuthFunc } // NewUnitsWatcher returns a new UnitsWatcher. The GetAuthFunc will be // used on each invocation of WatchUnits to determine current // permissions. -func NewUnitsWatcher(st state.EntityFinder, resources *Resources, getCanWatch GetAuthFunc) *UnitsWatcher { +func NewUnitsWatcher(st state.EntityFinder, resources facade.Resources, getCanWatch GetAuthFunc) *UnitsWatcher { return &UnitsWatcher{ st: st, resources: resources, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/watch.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/watch.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/common/watch.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/common/watch.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,6 +11,7 @@ "gopkg.in/juju/names.v2" "launchpad.net/tomb" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" "github.com/juju/juju/state/watcher" @@ -20,14 +21,14 @@ // various facades. type AgentEntityWatcher struct { st state.EntityFinder - resources *Resources + resources facade.Resources getCanWatch GetAuthFunc } // NewAgentEntityWatcher returns a new AgentEntityWatcher. The // GetAuthFunc will be used on each invocation of Watch to determine // current permissions. -func NewAgentEntityWatcher(st state.EntityFinder, resources *Resources, getCanWatch GetAuthFunc) *AgentEntityWatcher { +func NewAgentEntityWatcher(st state.EntityFinder, resources facade.Resources, getCanWatch GetAuthFunc) *AgentEntityWatcher { return &AgentEntityWatcher{ st: st, resources: resources, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/controller/controller.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/controller/controller.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/controller/controller.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/controller/controller.go 2016-08-16 08:56:25.000000000 +0000 @@ -14,9 +14,12 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/common/cloudspec" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/core/migration" "github.com/juju/juju/state" + "github.com/juju/juju/state/stateenvirons" ) var logger = loggo.GetLogger("juju.apiserver.controller") @@ -42,10 +45,12 @@ // the concrete implementation of the api end point. type ControllerAPI struct { *common.ControllerConfigAPI + cloudspec.CloudSpecAPI + state *state.State - authorizer common.Authorizer + authorizer facade.Authorizer apiUser names.UserTag - resources *common.Resources + resources facade.Resources } var _ Controller = (*ControllerAPI)(nil) @@ -54,8 +59,8 @@ // environments. func NewControllerAPI( st *state.State, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, ) (*ControllerAPI, error) { if !authorizer.AuthClient() { return nil, errors.Trace(common.ErrPerm) @@ -73,8 +78,10 @@ return nil, errors.Trace(common.ErrPerm) } + environConfigGetter := stateenvirons.EnvironConfigGetter{st} return &ControllerAPI{ ControllerConfigAPI: common.NewControllerConfig(st), + CloudSpecAPI: cloudspec.NewCloudSpec(environConfigGetter.CloudSpec, common.AuthFuncForTag(st.ModelTag())), state: st, authorizer: authorizer, apiUser: apiUser, @@ -282,7 +289,7 @@ if err != nil { result.Error = common.ServerError(err) } else { - result.Id = id + result.MigrationId = id } } return out, nil diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/controller/controller_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/controller/controller_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/controller/controller_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/controller/controller_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -15,8 +15,10 @@ "github.com/juju/juju/apiserver" "github.com/juju/juju/apiserver/common" "github.com/juju/juju/apiserver/controller" + "github.com/juju/juju/apiserver/facade/facadetest" "github.com/juju/juju/apiserver/params" apiservertesting "github.com/juju/juju/apiserver/testing" + "github.com/juju/juju/core/description" jujutesting "github.com/juju/juju/juju/testing" "github.com/juju/juju/state" "github.com/juju/juju/state/multiwatcher" @@ -84,10 +86,11 @@ st := s.Factory.MakeModel(c, &factory.ModelParams{ Name: "user", Owner: remoteUserTag}) defer st.Close() - st.AddModelUser(state.ModelUserSpec{ + st.AddModelUser(state.UserAccessSpec{ User: admin.UserTag(), CreatedBy: remoteUserTag, - DisplayName: "Foo Bar"}) + DisplayName: "Foo Bar", + Access: description.ReadAccess}) s.Factory.MakeModel(c, &factory.ModelParams{ Name: "no-access", Owner: remoteUserTag}).Close() @@ -221,7 +224,12 @@ watcherId, err := s.controller.WatchAllModels() c.Assert(err, jc.ErrorIsNil) - watcherAPI_, err := apiserver.NewAllWatcher(s.State, s.resources, s.authorizer, watcherId.AllWatcherId) + watcherAPI_, err := apiserver.NewAllWatcher(facadetest.Context{ + State_: s.State, + Resources_: s.resources, + Auth_: s.authorizer, + ID_: watcherId.AllWatcherId, + }) c.Assert(err, jc.ErrorIsNil) watcherAPI := watcherAPI_.(*apiserver.SrvAllWatcher) defer func() { @@ -252,7 +260,7 @@ otherEnvOwner := s.Factory.MakeModelUser(c, nil) otherSt := s.Factory.MakeModel(c, &factory.ModelParams{ Name: "dummytoo", - Owner: otherEnvOwner.UserTag(), + Owner: otherEnvOwner.UserTag, ConfigAttrs: testing.Attrs{ "controller": false, }, @@ -290,7 +298,7 @@ ModelTag: hostedEnvTag, HostedMachineCount: 2, ApplicationCount: 1, - OwnerTag: otherEnvOwner.UserTag().String(), + OwnerTag: otherEnvOwner.UserTag.String(), Life: params.Alive, }}) } @@ -339,10 +347,10 @@ c.Check(result.Error, gc.IsNil) c.Check(result.ModelTag, gc.Equals, spec.ModelTag) expectedId := st.ModelUUID() + ":0" - c.Check(result.Id, gc.Equals, expectedId) + c.Check(result.MigrationId, gc.Equals, expectedId) // Ensure the migration made it into the DB correctly. - mig, err := st.GetModelMigration() + mig, err := st.LatestModelMigration() c.Assert(err, jc.ErrorIsNil) c.Check(mig.Id(), gc.Equals, expectedId) c.Check(mig.ModelUUID(), gc.Equals, st.ModelUUID()) @@ -374,7 +382,7 @@ c.Assert(out.Results, gc.HasLen, 1) result := out.Results[0] c.Check(result.ModelTag, gc.Equals, args.Specs[0].ModelTag) - c.Check(result.Id, gc.Equals, "") + c.Check(result.MigrationId, gc.Equals, "") c.Check(result.Error, gc.ErrorMatches, "controller tag: .+ is not a valid tag") } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/debuglog_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/debuglog_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/debuglog_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/debuglog_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -25,7 +25,7 @@ func (s *debugLogBaseSuite) TestBadParams(c *gc.C) { reader := s.openWebsocket(c, url.Values{"maxLines": {"foo"}}) assertJSONError(c, reader, `maxLines value "foo" is not a valid unsigned number`) - s.assertWebsocketClosed(c, reader) + assertWebsocketClosed(c, reader) } func (s *debugLogBaseSuite) TestWithHTTP(c *gc.C) { @@ -49,7 +49,7 @@ reader := bufio.NewReader(conn) assertJSONError(c, reader, "no credentials provided") - s.assertWebsocketClosed(c, reader) + assertWebsocketClosed(c, reader) } func (s *debugLogBaseSuite) TestAgentLoginsRejected(c *gc.C) { @@ -63,7 +63,7 @@ reader := bufio.NewReader(conn) assertJSONError(c, reader, "tag kind machine not valid") - s.assertWebsocketClosed(c, reader) + assertWebsocketClosed(c, reader) } func (s *debugLogBaseSuite) openWebsocket(c *gc.C, values url.Values) *bufio.Reader { @@ -76,7 +76,7 @@ server := s.logURL(c, "wss", nil) server.Path = path header := utils.BasicAuthHeader(s.userTag.String(), s.password) - conn := s.dialWebsocketFromURL(c, server.String(), header) + conn := dialWebsocketFromURL(c, server.String(), header) s.AddCleanup(func(_ *gc.C) { conn.Close() }) return bufio.NewReader(conn) } @@ -92,5 +92,5 @@ func (s *debugLogBaseSuite) dialWebsocketInternal(c *gc.C, queryParams url.Values, header http.Header) *websocket.Conn { server := s.logURL(c, "wss", queryParams).String() - return s.dialWebsocketFromURL(c, server, header) + return dialWebsocketFromURL(c, server, header) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/deployer/deployer.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/deployer/deployer.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/deployer/deployer.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/deployer/deployer.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,6 +9,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" ) @@ -27,15 +28,15 @@ *common.UnitsWatcher st *state.State - resources *common.Resources - authorizer common.Authorizer + resources facade.Resources + authorizer facade.Authorizer } // NewDeployerAPI creates a new server-side DeployerAPI facade. func NewDeployerAPI( st *state.State, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, ) (*DeployerAPI, error) { if !authorizer.AuthMachineAgent() { return nil, common.ErrPerm diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/discoverspaces/discoverspaces.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/discoverspaces/discoverspaces.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/discoverspaces/discoverspaces.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/discoverspaces/discoverspaces.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,6 +7,7 @@ "github.com/juju/errors" "github.com/juju/juju/apiserver/common" "github.com/juju/juju/apiserver/common/networkingcommon" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" ) @@ -18,16 +19,16 @@ // DiscoverSpacesAPI implements the API used by the discoverspaces worker. type DiscoverSpacesAPI struct { st networkingcommon.NetworkBacking - resources *common.Resources - authorizer common.Authorizer + resources facade.Resources + authorizer facade.Authorizer } // NewDiscoverSpacesAPI creates a new instance of the DiscoverSpaces API. -func NewDiscoverSpacesAPI(st *state.State, resources *common.Resources, authorizer common.Authorizer) (*DiscoverSpacesAPI, error) { +func NewDiscoverSpacesAPI(st *state.State, resources facade.Resources, authorizer facade.Authorizer) (*DiscoverSpacesAPI, error) { return NewDiscoverSpacesAPIWithBacking(networkingcommon.NewStateShim(st), resources, authorizer) } -func NewDiscoverSpacesAPIWithBacking(st networkingcommon.NetworkBacking, resources *common.Resources, authorizer common.Authorizer) (*DiscoverSpacesAPI, error) { +func NewDiscoverSpacesAPIWithBacking(st networkingcommon.NetworkBacking, resources facade.Resources, authorizer facade.Authorizer) (*DiscoverSpacesAPI, error) { if !authorizer.AuthModelManager() { return nil, common.ErrPerm } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/diskmanager/diskmanager.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/diskmanager/diskmanager.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/diskmanager/diskmanager.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/diskmanager/diskmanager.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,6 +7,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" "github.com/juju/juju/storage" @@ -19,7 +20,7 @@ // DiskManagerAPI provides access to the DiskManager API facade. type DiskManagerAPI struct { st stateInterface - authorizer common.Authorizer + authorizer facade.Authorizer getAuthFunc common.GetAuthFunc } @@ -30,8 +31,8 @@ // NewDiskManagerAPI creates a new server-side DiskManager API facade. func NewDiskManagerAPI( st *state.State, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, ) (*DiskManagerAPI, error) { if !authorizer.AuthMachineAgent() { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/export_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -17,6 +17,7 @@ "github.com/juju/juju/apiserver/common" "github.com/juju/juju/apiserver/observer" "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/core/description" "github.com/juju/juju/rpc" "github.com/juju/juju/state" ) @@ -30,6 +31,7 @@ BZMimeType = bzMimeType JSMimeType = jsMimeType SpritePath = spritePath + HasPermission = hasPermission ) func ServerMacaroon(srv *Server) (*macaroon.Macaroon, error) { @@ -104,6 +106,15 @@ return h, h.getResources() } +// TestingApiHandlerWithEntity gives you the sane kind of ApiHandler as +// TestingApiHandler but sets the passed entity as the apiHandler +// entity. +func TestingApiHandlerWithEntity(c *gc.C, srvSt, st *state.State, entity state.Entity) (*apiHandler, *common.Resources) { + h, hr := TestingApiHandler(c, srvSt, st) + h.entity = entity + return h, hr +} + // TestingUpgradingRoot returns a limited srvRoot // in an upgrade scenario. func TestingUpgradingRoot(st *state.State) rpc.MethodFinder { @@ -214,3 +225,9 @@ type Patcher interface { PatchValue(ptr, value interface{}) } + +func AssertHasPermission(c *gc.C, handler *apiHandler, access description.Access, tag names.Tag, expect bool) { + hasPermission, err := handler.HasPermission(access, tag) + c.Assert(err, jc.ErrorIsNil) + c.Assert(hasPermission, gc.Equals, expect) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/facade/facadetest/context.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/facade/facadetest/context.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/facade/facadetest/context.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/facade/facadetest/context.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,43 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package facadetest + +import ( + "github.com/juju/juju/apiserver/facade" + "github.com/juju/juju/state" +) + +// Context implements facade.Context in the simplest possible way. +type Context struct { + Abort_ <-chan struct{} + Auth_ facade.Authorizer + Resources_ facade.Resources + State_ *state.State + ID_ string +} + +// Abort is part of the facade.Context interface. +func (context Context) Abort() <-chan struct{} { + return context.Abort_ +} + +// Auth is part of the facade.Context interface. +func (context Context) Auth() facade.Authorizer { + return context.Auth_ +} + +// Resources is part of the facade.Context interface. +func (context Context) Resources() facade.Resources { + return context.Resources_ +} + +// State is part of the facade.Context interface. +func (context Context) State() *state.State { + return context.State_ +} + +// ID is part of the facade.Context interface. +func (context Context) ID() string { + return context.ID_ +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/facade/interface.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/facade/interface.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/facade/interface.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/facade/interface.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,119 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package facade + +import ( + "gopkg.in/juju/names.v2" + + "github.com/juju/juju/core/description" + "github.com/juju/juju/state" +) + +// Facade could be anything; it will be interpreted by the apiserver +// machinery such that certain exported methods will be made available +// as facade methods to connected clients. +type Facade interface{} + +// Factory is a callback used to create a Facade. +type Factory func(Context) (Facade, error) + +// Context exposes useful capabilities to a Facade. +type Context interface { + + // Abort will be closed with the client connection. Any long- + // running methods should pay attention to Abort, and terminate + // with a sensible (non-nil) error when requested. + Abort() <-chan struct{} + + // Auth represents information about the connected client. You + // should always be checking individual requests against Auth: + // both state changes *and* data retrieval should be blocked + // with common.ErrPerm for any targets for which the client is + // not *known* to have a responsibility or requirement. + Auth() Authorizer + + // Resources exposes per-connection capabilities. By adding a + // resource, you make it accessible by (returned) id to all + // other facades used by this connection. It's mostly used to + // pass watcher ids over to watcher-specific facades, but that + // seems to be an antipattern: it breaks the separate-facades- + // by-role advice, and makes it inconvenient to track a given + // worker's watcher activity alongside its other communications. + // + // It's also used to hold some config strings used by various + // consumers, because it's convenient; and the Pinger that + // reports client presence in state, because every Resource gets + // Stop()ped on conn close. Not all of these uses are + // necessarily a great idea. + Resources() Resources + + // State returns, /sigh, a *State. As yet, there is no way + // around this; in the not-too-distant future, we hope, its + // capabilities will migrate towards access via Resources. + State() *state.State + + // ID returns a string that should almost always be "", unless + // this is a watcher facade, in which case it exists in lieu of + // actual arguments in the Next() call, and is used as a key + // into Resources to get the watcher in play. This is not really + // a good idea; see Resources. + ID() string +} + +// Authorizer represents the authenticated entity using the API server. +type Authorizer interface { + + // GetAuthTag returns the entity's tag. + GetAuthTag() names.Tag + + // AuthModelManager returns whether the authenticated entity is + // a machine running the environment manager job. Can't be + // removed from this interface without introducing a dependency + // on something else to look up that property: it's not inherent + // in the result of GetAuthTag, as the other methods all are. + AuthModelManager() bool + + // AuthMachineAgent returns true if the entity is a machine + // agent. Doesn't need to be on this interface, should be a + // utility func if anything. + AuthMachineAgent() bool + + // AuthUnitAgent returns true if the entity is a unit agent. + // Doesn't need to be on this interface, should be a utility + // func if anything. + AuthUnitAgent() bool + + // AuthOwner returns true if tag == .GetAuthTag(). Doesn't need + // to be on this interface, should be a utility fun if anything. + AuthOwner(tag names.Tag) bool + + // AuthClient returns true if the entity is an external user. + // Doesn't need to be on this interface, should be a utility + // func if anything. + AuthClient() bool + + // HasPermission returns true if the given access is allowed for the given + // target by the authenticated entity. + HasPermission(operation description.Access, target names.Tag) (bool, error) +} + +// Resources allows you to store and retrieve Resource implementations. +// +// The lack of error returns are in deference to the existing +// implementation, not because they're a good idea. +type Resources interface { + Register(Resource) string + Get(string) Resource + Stop(string) error +} + +// Resource should almost certainly be worker.Worker: the current +// implementation renders the apiserver vulnerable to deadlock when +// shutting down. (See common.Resources.StopAll -- *that* should be a +// Kill() and a Wait(), so that connection cleanup can kill the +// resources early, along with everything else, and then just wait for +// all those things to finish.) +type Resource interface { + Stop() error +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/facade/package_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/facade/package_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/facade/package_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/facade/package_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,14 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package facade_test + +import ( + "testing" + + gc "gopkg.in/check.v1" +) + +func TestPackage(t *testing.T) { + gc.TestingT(t) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/facade/registry.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/facade/registry.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/facade/registry.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/facade/registry.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,150 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package facade + +import ( + "fmt" + "reflect" + "sort" + + "github.com/juju/errors" + "github.com/juju/utils/featureflag" +) + +// record represents an entry in a Registry. +type record struct { + factory Factory + facadeType reflect.Type + // If the feature is not the empty string, then this facade + // is only returned when that feature flag is set. + // + // It is not a good thing that we depend on yet another flavour + // of global in the implementation of the Registry that itself + // only meaningfully exists as a global. + feature string +} + +// versions is our internal structure for tracking specific versions of a +// single facade. We use a map to be able to quickly lookup a version. +type versions map[int]record + +// Registry describes Facades the facades exposed by some API server. +// +// It's only actually used as a global -- `apiserver/common.Facades` -- +// but if we were smarter we could just create a Registry directly and +// pass it into the apiserver. +type Registry struct { + facades map[string]versions +} + +// Register adds a single named facade at a given version to the registry. +// Factory will be called when someone wants to instantiate an object of +// this facade, and facadeType defines the concrete type that the returned object will be. +// The Type information is used to define what methods will be exported in the +// API, and it must exactly match the actual object returned by the factory. +func (f *Registry) Register(name string, version int, factory Factory, facadeType reflect.Type, feature string) error { + if f.facades == nil { + f.facades = make(map[string]versions, 1) + } + record := record{ + factory: factory, + facadeType: facadeType, + feature: feature, + } + if vers, ok := f.facades[name]; ok { + if _, ok := vers[version]; ok { + fullname := fmt.Sprintf("%s(%d)", name, version) + return fmt.Errorf("object %q already registered", fullname) + } + vers[version] = record + } else { + f.facades[name] = versions{version: record} + } + return nil +} + +// lookup translates a facade name and version into a record. +func (f *Registry) lookup(name string, version int) (record, error) { + if versions, ok := f.facades[name]; ok { + if record, ok := versions[version]; ok { + if featureflag.Enabled(record.feature) { + return record, nil + } + } + } + return record{}, errors.NotFoundf("%s(%d)", name, version) +} + +// GetFactory returns just the Factory for a given Facade name and version. +// See also GetType for getting the type information instead of the creation factory. +func (f *Registry) GetFactory(name string, version int) (Factory, error) { + record, err := f.lookup(name, version) + if err != nil { + return nil, err + } + return record.factory, nil +} + +// GetType returns the type information for a given Facade name and version. +// This can be used for introspection purposes (to determine what methods are +// available, etc). +func (f *Registry) GetType(name string, version int) (reflect.Type, error) { + record, err := f.lookup(name, version) + if err != nil { + return nil, err + } + return record.facadeType, nil +} + +// Description describes the name and what versions of a facade have been +// registered. +type Description struct { + Name string + Versions []int +} + +// descriptionFromVersions aggregates the information in a versions map into a +// more friendly form for List(). +func descriptionFromVersions(name string, vers versions) Description { + intVersions := make([]int, 0, len(vers)) + for version, record := range vers { + if featureflag.Enabled(record.feature) { + intVersions = append(intVersions, version) + } + } + sort.Ints(intVersions) + return Description{ + Name: name, + Versions: intVersions, + } +} + +// List returns a slice describing each of the registered Facades. +func (f *Registry) List() []Description { + names := make([]string, 0, len(f.facades)) + for name := range f.facades { + names = append(names, name) + } + sort.Strings(names) + descriptions := make([]Description, 0, len(f.facades)) + for _, name := range names { + facades := f.facades[name] + description := descriptionFromVersions(name, facades) + if len(description.Versions) > 0 { + descriptions = append(descriptions, description) + } + } + return descriptions +} + +// Discard gets rid of a registration that has already been done. Calling +// discard on an entry that is not present is not considered an error. +func (f *Registry) Discard(name string, version int) { + if versions, ok := f.facades[name]; ok { + delete(versions, version) + if len(versions) == 0 { + delete(f.facades, name) + } + } +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/facade/registry_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/facade/registry_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/facade/registry_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/facade/registry_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,198 @@ +// Copyright 2014-2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package facade_test + +import ( + "reflect" + + "github.com/juju/errors" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/facade" + "github.com/juju/juju/testing" +) + +type RegistrySuite struct { + testing.BaseSuite +} + +var _ = gc.Suite(&RegistrySuite{}) + +func (s *RegistrySuite) TestRegister(c *gc.C) { + registry := &facade.Registry{} + var v interface{} + facadeType := reflect.TypeOf(&v).Elem() + err := registry.Register("myfacade", 123, testFacade, facadeType, "") + c.Assert(err, jc.ErrorIsNil) + + factory, err := registry.GetFactory("myfacade", 123) + c.Assert(err, jc.ErrorIsNil) + val, err := factory(nil) + c.Assert(err, jc.ErrorIsNil) + c.Check(val, gc.Equals, "myobject") +} + +func (*RegistrySuite) TestGetFactoryUnknown(c *gc.C) { + registry := &facade.Registry{} + factory, err := registry.GetFactory("name", 0) + c.Check(err, jc.Satisfies, errors.IsNotFound) + c.Check(err, gc.ErrorMatches, `name\(0\) not found`) + c.Check(factory, gc.IsNil) +} + +func (*RegistrySuite) TestGetFactoryUnknownVersion(c *gc.C) { + registry := &facade.Registry{} + assertRegister(c, registry, "name", 0) + + factory, err := registry.GetFactory("name", 1) + c.Check(err, jc.Satisfies, errors.IsNotFound) + c.Check(err, gc.ErrorMatches, `name\(1\) not found`) + c.Check(factory, gc.IsNil) +} + +func (*RegistrySuite) TestRegisterAndList(c *gc.C) { + registry := &facade.Registry{} + assertRegister(c, registry, "name", 0) + + c.Check(registry.List(), jc.DeepEquals, []facade.Description{ + {Name: "name", Versions: []int{0}}, + }) +} + +func (*RegistrySuite) TestRegisterAndListSorted(c *gc.C) { + registry := &facade.Registry{} + assertRegister(c, registry, "name", 10) + assertRegister(c, registry, "name", 0) + assertRegister(c, registry, "name", 101) + + c.Check(registry.List(), jc.DeepEquals, []facade.Description{ + {Name: "name", Versions: []int{0, 10, 101}}, + }) +} + +func (*RegistrySuite) TestRegisterAndListMultiple(c *gc.C) { + registry := &facade.Registry{} + assertRegister(c, registry, "other", 0) + assertRegister(c, registry, "name", 0) + assertRegister(c, registry, "third", 2) + assertRegister(c, registry, "third", 3) + + c.Check(registry.List(), jc.DeepEquals, []facade.Description{ + {Name: "name", Versions: []int{0}}, + {Name: "other", Versions: []int{0}}, + {Name: "third", Versions: []int{2, 3}}, + }) +} + +func (s *RegistrySuite) TestRegisterAndListMultipleWithFeatures(c *gc.C) { + registry := &facade.Registry{} + assertRegisterFlag(c, registry, "other", 0, "special") + assertRegister(c, registry, "name", 0) + assertRegisterFlag(c, registry, "name", 1, "special") + assertRegister(c, registry, "third", 2) + assertRegisterFlag(c, registry, "third", 3, "magic") + + s.SetFeatureFlags("magic") + c.Check(registry.List(), jc.DeepEquals, []facade.Description{ + {Name: "name", Versions: []int{0}}, + {Name: "third", Versions: []int{2, 3}}, + }) +} + +func (*RegistrySuite) TestRegisterAlreadyPresent(c *gc.C) { + registry := &facade.Registry{} + assertRegister(c, registry, "name", 0) + secondIdFactory := func(context facade.Context) (facade.Facade, error) { + var i = 200 + return &i, nil + } + err := registry.Register("name", 0, secondIdFactory, intPtrType, "") + c.Assert(err, gc.ErrorMatches, `object "name\(0\)" already registered`) + + factory, err := registry.GetFactory("name", 0) + c.Assert(err, jc.ErrorIsNil) + c.Assert(factory, gc.NotNil) + val, err := factory(nil) + c.Assert(err, jc.ErrorIsNil) + asIntPtr := val.(*int) + c.Check(*asIntPtr, gc.Equals, 100) +} + +func (*RegistrySuite) TestGetFactory(c *gc.C) { + registry := &facade.Registry{} + assertRegister(c, registry, "name", 0) + + factory, err := registry.GetFactory("name", 0) + c.Assert(err, jc.ErrorIsNil) + c.Assert(factory, gc.NotNil) + res, err := factory(nil) + c.Assert(err, jc.ErrorIsNil) + c.Assert(res, gc.NotNil) + asIntPtr := res.(*int) + c.Check(*asIntPtr, gc.Equals, 100) +} + +func (*RegistrySuite) TestGetType(c *gc.C) { + registry := &facade.Registry{} + assertRegister(c, registry, "name", 0) + + typ, err := registry.GetType("name", 0) + c.Assert(err, jc.ErrorIsNil) + c.Check(typ, gc.Equals, intPtrType) +} + +func (*RegistrySuite) TestDiscardHandlesNotPresent(c *gc.C) { + registry := &facade.Registry{} + registry.Discard("name", 1) +} + +func (*RegistrySuite) TestDiscardRemovesEntry(c *gc.C) { + registry := &facade.Registry{} + assertRegister(c, registry, "name", 0) + _, err := registry.GetFactory("name", 0) + c.Assert(err, jc.ErrorIsNil) + + registry.Discard("name", 0) + factory, err := registry.GetFactory("name", 0) + c.Check(err, jc.Satisfies, errors.IsNotFound) + c.Check(err, gc.ErrorMatches, `name\(0\) not found`) + c.Check(factory, gc.IsNil) + c.Check(registry.List(), jc.DeepEquals, []facade.Description{}) +} + +func (*RegistrySuite) TestDiscardLeavesOtherVersions(c *gc.C) { + registry := &facade.Registry{} + assertRegister(c, registry, "name", 0) + assertRegister(c, registry, "name", 1) + + registry.Discard("name", 0) + _, err := registry.GetFactory("name", 1) + c.Check(err, jc.ErrorIsNil) + c.Check(registry.List(), jc.DeepEquals, []facade.Description{ + {Name: "name", Versions: []int{1}}, + }) +} + +func testFacade(facade.Context) (facade.Facade, error) { + return "myobject", nil +} + +func validIdFactory(facade.Context) (facade.Facade, error) { + var i = 100 + return &i, nil +} + +var intPtr = new(int) +var intPtrType = reflect.TypeOf(&intPtr).Elem() + +func assertRegister(c *gc.C, registry *facade.Registry, name string, version int) { + assertRegisterFlag(c, registry, name, version, "") +} + +func assertRegisterFlag(c *gc.C, registry *facade.Registry, name string, version int, flag string) { + + err := registry.Register(name, version, validIdFactory, intPtrType, flag) + c.Assert(err, gc.IsNil) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/firewaller/firewaller_base_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/firewaller/firewaller_base_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/firewaller/firewaller_base_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/firewaller/firewaller_base_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,6 +9,7 @@ gc "gopkg.in/check.v1" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" apiservertesting "github.com/juju/juju/apiserver/testing" "github.com/juju/juju/instance" @@ -71,7 +72,7 @@ func (s *firewallerBaseSuite) testFirewallerFailsWithNonEnvironManagerUser( c *gc.C, - factory func(_ *state.State, _ *common.Resources, _ common.Authorizer) error, + factory func(_ *state.State, _ facade.Resources, _ facade.Authorizer) error, ) { anAuthorizer := s.authorizer anAuthorizer.EnvironManager = false @@ -211,7 +212,7 @@ func (s *firewallerBaseSuite) testWatch( c *gc.C, - facade interface { + watcher interface { Watch(args params.Entities) (params.NotifyWatchResults, error) }, allowUnits bool, @@ -223,7 +224,7 @@ {Tag: s.service.Tag().String()}, {Tag: s.units[0].Tag().String()}, }}) - result, err := facade.Watch(args) + result, err := watcher.Watch(args) c.Assert(err, jc.ErrorIsNil) if allowUnits { c.Assert(result, jc.DeepEquals, params.NotifyWatchResults{ @@ -264,7 +265,7 @@ c.Assert(result.Results[1].NotifyWatcherId, gc.Equals, "1") watcher1 := s.resources.Get("1") defer statetesting.AssertStop(c, watcher1) - var watcher2 common.Resource + var watcher2 facade.Resource if allowUnits { c.Assert(result.Results[2].NotifyWatcherId, gc.Equals, "2") watcher2 = s.resources.Get("2") diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/firewaller/firewaller.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/firewaller/firewaller.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/firewaller/firewaller.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/firewaller/firewaller.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,9 +8,12 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/common/cloudspec" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/network" "github.com/juju/juju/state" + "github.com/juju/juju/state/stateenvirons" "github.com/juju/juju/state/watcher" ) @@ -27,10 +30,11 @@ *common.UnitsWatcher *common.ModelMachinesWatcher *common.InstanceIdGetter + cloudspec.CloudSpecAPI st *state.State - resources *common.Resources - authorizer common.Authorizer + resources facade.Resources + authorizer facade.Authorizer accessUnit common.GetAuthFunc accessService common.GetAuthFunc accessMachine common.GetAuthFunc @@ -40,8 +44,8 @@ // NewFirewallerAPI creates a new server-side FirewallerAPI facade. func NewFirewallerAPI( st *state.State, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, ) (*FirewallerAPI, error) { if !authorizer.AuthModelManager() { // Firewaller must run as environment manager. @@ -90,6 +94,9 @@ accessMachine, ) + environConfigGetter := stateenvirons.EnvironConfigGetter{st} + cloudSpecAPI := cloudspec.NewCloudSpec(environConfigGetter.CloudSpec, common.AuthFuncForTag(st.ModelTag())) + return &FirewallerAPI{ LifeGetter: lifeGetter, ModelWatcher: modelWatcher, @@ -97,6 +104,7 @@ UnitsWatcher: unitsWatcher, ModelMachinesWatcher: machinesWatcher, InstanceIdGetter: instanceIdGetter, + CloudSpecAPI: cloudSpecAPI, st: st, resources: resources, authorizer: authorizer, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/firewaller/firewaller_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/firewaller/firewaller_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/firewaller/firewaller_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/firewaller/firewaller_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,8 +10,8 @@ gc "gopkg.in/check.v1" "gopkg.in/juju/names.v2" - "github.com/juju/juju/apiserver/common" commontesting "github.com/juju/juju/apiserver/common/testing" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/firewaller" "github.com/juju/juju/apiserver/params" apiservertesting "github.com/juju/juju/apiserver/testing" @@ -46,7 +46,7 @@ } func (s *firewallerSuite) TestFirewallerFailsWithNonEnvironManagerUser(c *gc.C) { - constructor := func(st *state.State, res *common.Resources, auth common.Authorizer) error { + constructor := func(st *state.State, res facade.Resources, auth facade.Authorizer) error { _, err := firewaller.NewFirewallerAPI(st, res, auth) return err } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/highavailability/highavailability.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/highavailability/highavailability.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/highavailability/highavailability.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/highavailability/highavailability.go 2016-08-16 08:56:25.000000000 +0000 @@ -12,6 +12,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/constraints" "github.com/juju/juju/state" @@ -32,14 +33,14 @@ // implementation of the api end point. type HighAvailabilityAPI struct { state *state.State - resources *common.Resources - authorizer common.Authorizer + resources facade.Resources + authorizer facade.Authorizer } var _ HighAvailability = (*HighAvailabilityAPI)(nil) // NewHighAvailabilityAPI creates a new server-side highavailability API end point. -func NewHighAvailabilityAPI(st *state.State, resources *common.Resources, authorizer common.Authorizer) (*HighAvailabilityAPI, error) { +func NewHighAvailabilityAPI(st *state.State, resources facade.Resources, authorizer facade.Authorizer) (*HighAvailabilityAPI, error) { // Only clients and environment managers can access the high availability service. if !authorizer.AuthClient() && !authorizer.AuthModelManager() { return nil, common.ErrPerm diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/hostkeyreporter/facade.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/hostkeyreporter/facade.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/hostkeyreporter/facade.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/hostkeyreporter/facade.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,6 +9,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" ) @@ -29,7 +30,7 @@ } // New returns a new API facade for the hostkeyreporter worker. -func New(backend Backend, _ *common.Resources, authorizer common.Authorizer) (*Facade, error) { +func New(backend Backend, _ facade.Resources, authorizer facade.Authorizer) (*Facade, error) { return &Facade{ backend: backend, getCanModify: func() (common.AuthFunc, error) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/hostkeyreporter/shim.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/hostkeyreporter/shim.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/hostkeyreporter/shim.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/hostkeyreporter/shim.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,11 +4,16 @@ package hostkeyreporter import ( - "github.com/juju/juju/apiserver/common" + "github.com/juju/errors" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/state" ) // newFacade wraps New to express the supplied *state.State as a Backend. -func newFacade(st *state.State, res *common.Resources, auth common.Authorizer) (*Facade, error) { - return New(st, res, auth) +func newFacade(st *state.State, res facade.Resources, auth facade.Authorizer) (*Facade, error) { + facade, err := New(st, res, auth) + if err != nil { + return nil, errors.Trace(err) + } + return facade, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/httpcontext.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/httpcontext.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/httpcontext.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/httpcontext.go 2016-08-16 08:56:25.000000000 +0000 @@ -64,16 +64,32 @@ } entity, _, err := checkCreds(st, req, true, ctxt.srv.authCtxt) if err != nil { - // All errors other than a macaroon-discharge error count as - // unauthorized at this point. - if !common.IsDischargeRequiredError(err) { - err = errors.NewUnauthorized(err, "") + if common.IsDischargeRequiredError(err) { + return nil, nil, errors.Trace(err) } - return nil, nil, errors.Trace(err) + + // Handle the special case of a worker on a controller machine + // acting on behalf of a hosted model. + if isMachineTag(req.AuthTag) { + entity, err := checkControllerMachineCreds(ctxt.srv.state, req, ctxt.srv.authCtxt) + if err != nil { + return nil, nil, errors.NewUnauthorized(err, "") + } + return st, entity, nil + } + + // Any other error at this point should be treated as + // "unauthorized". + return nil, nil, errors.Trace(errors.NewUnauthorized(err, "")) } return st, entity, nil } +func isMachineTag(tag string) bool { + kind, err := names.TagKind(tag) + return err == nil && kind == names.MachineTagKind +} + // checkPermissions verifies that given tag passes authentication check. // For example, if only user tags are accepted, all other tags will be denied access. func checkPermissions(tag names.Tag, acceptFunc common.GetAuthFunc) (bool, error) { @@ -100,8 +116,8 @@ return st, entity, nil } -// stateForRequestAuthenticatedUser is like stateForRequestAuthenticated -// except that it also verifies that the authenticated entity is a user. +// stateForRequestAuthenticatedAgent is like stateForRequestAuthenticated +// except that it also verifies that the authenticated entity is an agent. func (ctxt *httpContext) stateForRequestAuthenticatedAgent(r *http.Request) (*state.State, state.Entity, error) { authFunc := common.AuthEither( common.AuthFuncForTagKind(names.MachineTagKind), diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/imagemanager/imagemanager.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/imagemanager/imagemanager.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/imagemanager/imagemanager.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/imagemanager/imagemanager.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,6 +8,7 @@ "github.com/juju/loggo" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" "github.com/juju/juju/state/imagestorage" @@ -29,8 +30,8 @@ // implementation of the api end point. type ImageManagerAPI struct { state stateInterface - resources *common.Resources - authorizer common.Authorizer + resources facade.Resources + authorizer facade.Authorizer check *common.BlockChecker } @@ -41,7 +42,7 @@ } // NewImageManagerAPI creates a new server-side imagemanager API end point. -func NewImageManagerAPI(st *state.State, resources *common.Resources, authorizer common.Authorizer) (*ImageManagerAPI, error) { +func NewImageManagerAPI(st *state.State, resources facade.Resources, authorizer facade.Authorizer) (*ImageManagerAPI, error) { // Only clients can access the image manager service. if !authorizer.AuthClient() { return nil, common.ErrPerm diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/imagemetadata/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/imagemetadata/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/imagemetadata/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/imagemetadata/export_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,7 +5,7 @@ import ( "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/environs" + "github.com/juju/juju/environs/config" "github.com/juju/juju/state/cloudimagemetadata" ) @@ -14,6 +14,6 @@ ProcessErrors = processErrors ) -func ParseMetadataFromParams(api *API, p params.CloudImageMetadata, env environs.Environ) (cloudimagemetadata.Metadata, error) { - return api.parseMetadataFromParams(p, env) +func ParseMetadataFromParams(api *API, p params.CloudImageMetadata, cfg *config.Config, cloudRegion string) (cloudimagemetadata.Metadata, error) { + return api.parseMetadataFromParams(p, cfg, cloudRegion) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/imagemetadata/functions_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/imagemetadata/functions_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/imagemetadata/functions_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/imagemetadata/functions_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -12,10 +12,7 @@ "github.com/juju/juju/apiserver/imagemetadata" "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/environs" - "github.com/juju/juju/environs/bootstrap" - envtesting "github.com/juju/juju/environs/testing" - "github.com/juju/juju/jujuclient/jujuclienttesting" + "github.com/juju/juju/environs/config" "github.com/juju/juju/state/cloudimagemetadata" "github.com/juju/juju/testing" ) @@ -23,7 +20,7 @@ type funcSuite struct { baseImageMetadataSuite - env environs.Environ + cfg *config.Config expected cloudimagemetadata.Metadata } @@ -32,20 +29,10 @@ func (s *funcSuite) SetUpTest(c *gc.C) { s.baseImageMetadataSuite.SetUpTest(c) - var err error - s.env, err = bootstrap.Prepare( - envtesting.BootstrapContext(c), - jujuclienttesting.NewMemStore(), - bootstrap.PrepareParams{ - ControllerConfig: testing.FakeControllerConfig(), - ControllerName: "dummycontroller", - BaseConfig: mockConfig(), - CloudName: "dummy", - AdminSecret: "admin-secret", - }, - ) + cfg, err := config.New(config.NoDefaults, mockConfig()) c.Assert(err, jc.ErrorIsNil) - s.state = s.constructState(s.env.Config()) + s.cfg = cfg + s.state = s.constructState(s.cfg, nil) s.expected = cloudimagemetadata.Metadata{ cloudimagemetadata.MetadataAttributes{ @@ -61,14 +48,14 @@ } func (s *funcSuite) TestParseMetadataNoSource(c *gc.C) { - m, err := imagemetadata.ParseMetadataFromParams(s.api, params.CloudImageMetadata{}, s.env) + m, err := imagemetadata.ParseMetadataFromParams(s.api, params.CloudImageMetadata{}, s.cfg, "dummy_region") c.Assert(err, jc.ErrorIsNil) c.Assert(m, gc.DeepEquals, s.expected) } func (s *funcSuite) TestParseMetadataAnySource(c *gc.C) { s.expected.Source = "any" - m, err := imagemetadata.ParseMetadataFromParams(s.api, params.CloudImageMetadata{Source: "any"}, s.env) + m, err := imagemetadata.ParseMetadataFromParams(s.api, params.CloudImageMetadata{Source: "any"}, s.cfg, "dummy_region") c.Assert(err, jc.ErrorIsNil) c.Assert(m, gc.DeepEquals, s.expected) } @@ -77,13 +64,13 @@ stream := "happy stream" s.expected.Stream = stream - m, err := imagemetadata.ParseMetadataFromParams(s.api, params.CloudImageMetadata{Stream: stream}, s.env) + m, err := imagemetadata.ParseMetadataFromParams(s.api, params.CloudImageMetadata{Stream: stream}, s.cfg, "dummy_region") c.Assert(err, jc.ErrorIsNil) c.Assert(m, gc.DeepEquals, s.expected) } func (s *funcSuite) TestParseMetadataDefaultStream(c *gc.C) { - m, err := imagemetadata.ParseMetadataFromParams(s.api, params.CloudImageMetadata{}, s.env) + m, err := imagemetadata.ParseMetadataFromParams(s.api, params.CloudImageMetadata{}, s.cfg, "dummy_region") c.Assert(err, jc.ErrorIsNil) c.Assert(m, gc.DeepEquals, s.expected) } @@ -92,7 +79,7 @@ region := "region" s.expected.Region = region - m, err := imagemetadata.ParseMetadataFromParams(s.api, params.CloudImageMetadata{Region: region}, s.env) + m, err := imagemetadata.ParseMetadataFromParams(s.api, params.CloudImageMetadata{Region: region}, s.cfg, "dummy_region") c.Assert(err, jc.ErrorIsNil) c.Assert(m, gc.DeepEquals, s.expected) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/imagemetadata/metadata.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/imagemetadata/metadata.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/imagemetadata/metadata.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/imagemetadata/metadata.go 2016-08-16 08:56:25.000000000 +0000 @@ -12,6 +12,7 @@ "github.com/juju/utils/series" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/environs" "github.com/juju/juju/environs/config" @@ -19,6 +20,7 @@ "github.com/juju/juju/environs/simplestreams" "github.com/juju/juju/state" "github.com/juju/juju/state/cloudimagemetadata" + "github.com/juju/juju/state/stateenvirons" ) var logger = loggo.GetLogger("juju.apiserver.imagemetadata") @@ -31,14 +33,16 @@ // for loud image metadata manipulations. type API struct { metadata metadataAcess - authorizer common.Authorizer + newEnviron func() (environs.Environ, error) + authorizer facade.Authorizer } // createAPI returns a new image metadata API facade. func createAPI( st metadataAcess, - resources *common.Resources, - authorizer common.Authorizer, + newEnviron func() (environs.Environ, error), + resources facade.Resources, + authorizer facade.Authorizer, ) (*API, error) { if !authorizer.AuthClient() && !authorizer.AuthModelManager() { return nil, common.ErrPerm @@ -46,6 +50,7 @@ return &API{ metadata: st, + newEnviron: newEnviron, authorizer: authorizer, }, nil } @@ -53,10 +58,13 @@ // NewAPI returns a new cloud image metadata API facade. func NewAPI( st *state.State, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, ) (*API, error) { - return createAPI(getState(st), resources, authorizer) + newEnviron := func() (environs.Environ, error) { + return stateenvirons.GetNewEnvironFunc(environs.New)(st) + } + return createAPI(getState(st), newEnviron, resources, authorizer) } // List returns all found cloud image metadata that satisfy @@ -94,16 +102,19 @@ // It supports bulk calls. func (api *API) Save(metadata params.MetadataSaveParams) (params.ErrorResults, error) { all := make([]params.ErrorResult, len(metadata.Metadata)) - envCfg, err := api.metadata.ModelConfig() + if len(metadata.Metadata) == 0 { + return params.ErrorResults{Results: all}, nil + } + modelCfg, err := api.metadata.ModelConfig() if err != nil { - return params.ErrorResults{}, errors.Annotatef(err, "getting environ config") + return params.ErrorResults{}, errors.Annotatef(err, "getting model config") } - env, err := environs.New(envCfg) + model, err := api.metadata.Model() if err != nil { - return params.ErrorResults{}, errors.Annotatef(err, "getting environ") + return params.ErrorResults{}, errors.Annotatef(err, "getting model") } for i, one := range metadata.Metadata { - md, err := api.parseMetadataListFromParams(one, env) + md, err := api.parseMetadataListFromParams(one, modelCfg, model.CloudRegion()) if err != nil { all[i] = params.ErrorResult{Error: common.ServerError(err)} continue @@ -143,11 +154,11 @@ } func (api *API) parseMetadataListFromParams( - p params.CloudImageMetadataList, env environs.Environ, + p params.CloudImageMetadataList, cfg *config.Config, cloudRegion string, ) ([]cloudimagemetadata.Metadata, error) { results := make([]cloudimagemetadata.Metadata, len(p.Metadata)) for i, metadata := range p.Metadata { - result, err := api.parseMetadataFromParams(metadata, env) + result, err := api.parseMetadataFromParams(metadata, cfg, cloudRegion) if err != nil { return nil, errors.Trace(err) } @@ -156,7 +167,7 @@ return results, nil } -func (api *API) parseMetadataFromParams(p params.CloudImageMetadata, env environs.Environ) (cloudimagemetadata.Metadata, error) { +func (api *API) parseMetadataFromParams(p params.CloudImageMetadata, cfg *config.Config, cloudRegion string) (cloudimagemetadata.Metadata, error) { result := cloudimagemetadata.Metadata{ cloudimagemetadata.MetadataAttributes{ Stream: p.Stream, @@ -175,7 +186,7 @@ // Fill in any required default values. if p.Stream == "" { - result.Stream = env.Config().ImageStream() + result.Stream = cfg.ImageStream() } if p.Source == "" { result.Source = "custom" @@ -184,17 +195,10 @@ result.Arch = "amd64" } if result.Series == "" { - result.Series = config.PreferredSeries(env.Config()) + result.Series = config.PreferredSeries(cfg) } if result.Region == "" { - // If the env supports regions, use the env default. - if r, ok := env.(simplestreams.HasRegion); ok { - spec, err := r.Region() - if err != nil { - return cloudimagemetadata.Metadata{}, errors.Annotatef(err, "getting cloud region") - } - result.Region = spec.Region - } + result.Region = cloudRegion } return result, nil } @@ -206,11 +210,7 @@ } func (api *API) retrievePublished() error { - envCfg, err := api.metadata.ModelConfig() - if err != nil { - return errors.Annotatef(err, "getting environ config") - } - env, err := environs.New(envCfg) + env, err := api.newEnviron() if err != nil { return errors.Annotatef(err, "getting environ") } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/imagemetadata/metadata_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/imagemetadata/metadata_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/imagemetadata/metadata_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/imagemetadata/metadata_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -99,8 +99,7 @@ errs, err := s.api.Save(params.MetadataSaveParams{}) c.Assert(err, jc.ErrorIsNil) c.Assert(errs.Results, gc.HasLen, 0) - // not expected to call state :D - s.assertCalls(c, environConfig) + s.assertCalls(c) } func (s *metadataSuite) TestSave(c *gc.C) { @@ -131,14 +130,13 @@ c.Assert(errs.Results, gc.HasLen, 2) c.Assert(errs.Results[0].Error, gc.IsNil) c.Assert(errs.Results[1].Error, gc.ErrorMatches, msg) - s.assertCalls(c, environConfig, saveMetadata, saveMetadata) + s.assertCalls(c, environConfig, "Model", saveMetadata, saveMetadata) } func (s *metadataSuite) TestDeleteEmpty(c *gc.C) { errs, err := s.api.Delete(params.MetadataImageIds{}) c.Assert(err, jc.ErrorIsNil) c.Assert(errs.Results, gc.HasLen, 0) - // not expected to call state :D s.assertCalls(c) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/imagemetadata/package_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/imagemetadata/package_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/imagemetadata/package_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/imagemetadata/package_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -15,11 +15,8 @@ "github.com/juju/juju/apiserver/imagemetadata" "github.com/juju/juju/apiserver/testing" "github.com/juju/juju/environs" - "github.com/juju/juju/environs/bootstrap" "github.com/juju/juju/environs/config" imagetesting "github.com/juju/juju/environs/imagemetadata/testing" - envtesting "github.com/juju/juju/environs/testing" - "github.com/juju/juju/jujuclient/jujuclienttesting" "github.com/juju/juju/state/cloudimagemetadata" coretesting "github.com/juju/juju/testing" ) @@ -28,11 +25,6 @@ gc.TestingT(t) } -func init() { - provider := mockEnvironProvider{} - environs.RegisterProvider("mock", provider) -} - type baseImageMetadataSuite struct { coretesting.BaseSuite @@ -53,10 +45,12 @@ s.resources = common.NewResources() s.authorizer = testing.FakeAuthorizer{names.NewUserTag("testuser"), true} - s.state = s.constructState(testConfig(c)) + s.state = s.constructState(testConfig(c), &mockModel{"meep"}) var err error - s.api, err = imagemetadata.CreateAPI(s.state, s.resources, s.authorizer) + s.api, err = imagemetadata.CreateAPI(s.state, func() (environs.Environ, error) { + return &mockEnviron{}, nil + }, s.resources, s.authorizer) c.Assert(err, jc.ErrorIsNil) } @@ -71,7 +65,7 @@ environConfig = "environConfig" ) -func (s *baseImageMetadataSuite) constructState(cfg *config.Config) *mockState { +func (s *baseImageMetadataSuite) constructState(cfg *config.Config, model imagemetadata.Model) *mockState { return &mockState{ Stub: &gitjujutesting.Stub{}, findMetadata: func(f cloudimagemetadata.MetadataFilter) (map[string][]cloudimagemetadata.Metadata, error) { @@ -86,6 +80,9 @@ environConfig: func() (*config.Config, error) { return cfg, nil }, + model: func() (imagemetadata.Model, error) { + return model, nil + }, } } @@ -96,6 +93,7 @@ saveMetadata func(m []cloudimagemetadata.Metadata) error deleteMetadata func(imageId string) error environConfig func() (*config.Config, error) + model func() (imagemetadata.Model, error) } func (st *mockState) FindMetadata(f cloudimagemetadata.MetadataFilter) (map[string][]cloudimagemetadata.Metadata, error) { @@ -118,22 +116,23 @@ return st.environConfig() } +func (st *mockState) Model() (imagemetadata.Model, error) { + st.Stub.MethodCall(st, "Model") + return st.model() +} + +type mockModel struct { + cloudRegion string +} + +func (m *mockModel) CloudRegion() string { + return m.cloudRegion +} + func testConfig(c *gc.C) *config.Config { - attrs := coretesting.FakeConfig().Merge(coretesting.Attrs{ - "type": "mock", - "controller": true, - }) - env, err := bootstrap.Prepare( - envtesting.BootstrapContext(c), - jujuclienttesting.NewMemStore(), - bootstrap.PrepareParams{ - ControllerConfig: coretesting.FakeControllerConfig(), - ControllerName: "dummycontroller", - BaseConfig: attrs, - CloudName: "dummy", - AdminSecret: "admin-secret", - }, - ) + cfg, err := config.New(config.UseDefaults, coretesting.FakeConfig().Merge(coretesting.Attrs{ + "type": "mock", + })) c.Assert(err, jc.ErrorIsNil) - return env.Config() + return cfg } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/imagemetadata/state.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/imagemetadata/state.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/imagemetadata/state.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/imagemetadata/state.go 2016-08-16 08:56:25.000000000 +0000 @@ -13,9 +13,14 @@ FindMetadata(cloudimagemetadata.MetadataFilter) (map[string][]cloudimagemetadata.Metadata, error) SaveMetadata([]cloudimagemetadata.Metadata) error DeleteMetadata(imageId string) error + Model() (Model, error) ModelConfig() (*config.Config, error) } +type Model interface { + CloudRegion() string +} + var getState = func(st *state.State) metadataAcess { return stateShim{st} } @@ -35,3 +40,15 @@ func (s stateShim) DeleteMetadata(imageId string) error { return s.State.CloudImageMetadataStorage.DeleteMetadata(imageId) } + +func (s stateShim) Model() (Model, error) { + m, err := s.State.Model() + if err != nil { + return nil, err + } + return modelShim{m}, nil +} + +type modelShim struct { + *state.Model +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/imagemetadata/updatefrompublished_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/imagemetadata/updatefrompublished_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/imagemetadata/updatefrompublished_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/imagemetadata/updatefrompublished_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -12,16 +12,13 @@ jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" - "github.com/juju/juju/cmd/modelcmd" "github.com/juju/juju/environs" - "github.com/juju/juju/environs/bootstrap" "github.com/juju/juju/environs/config" "github.com/juju/juju/environs/imagemetadata" imagetesting "github.com/juju/juju/environs/imagemetadata/testing" "github.com/juju/juju/environs/simplestreams" sstesting "github.com/juju/juju/environs/simplestreams/testing" "github.com/juju/juju/juju/keys" - "github.com/juju/juju/jujuclient/jujuclienttesting" "github.com/juju/juju/provider/dummy" "github.com/juju/juju/state/cloudimagemetadata" "github.com/juju/juju/testing" @@ -165,19 +162,7 @@ // testingEnvConfig prepares an environment configuration using // the dummy provider since it doesn't implement simplestreams.HasRegion. s.state.environConfig = func() (*config.Config, error) { - env, err := bootstrap.Prepare( - modelcmd.BootstrapContext(testing.Context(c)), - jujuclienttesting.NewMemStore(), - bootstrap.PrepareParams{ - ControllerConfig: testing.FakeControllerConfig(), - ControllerName: "dummycontroller", - BaseConfig: dummy.SampleConfig(), - CloudName: "dummy", - AdminSecret: "admin-secret", - }, - ) - c.Assert(err, jc.ErrorIsNil) - return env.Config(), err + return config.New(config.UseDefaults, dummy.SampleConfig()) } s.state.saveMetadata = func(m []cloudimagemetadata.Metadata) error { @@ -187,7 +172,6 @@ err := s.api.UpdateFromPublishedImages() c.Assert(err, jc.ErrorIsNil) - s.assertCalls(c, environConfig) c.Assert(saved, jc.SameContents, []cloudimagemetadata.Metadata{}) } @@ -220,24 +204,6 @@ }, nil } -// mockEnvironProvider is the smallest possible provider to -// test image metadata retrieval with region support. -type mockEnvironProvider struct { - environs.EnvironProvider -} - -func (p mockEnvironProvider) BootstrapConfig(args environs.BootstrapConfigParams) (*config.Config, error) { - return args.Config, nil -} - -func (p mockEnvironProvider) PrepareForBootstrap(environs.BootstrapContext, *config.Config) (environs.Environ, error) { - return &mockEnviron{}, nil -} - -func (p mockEnvironProvider) Open(*config.Config) (environs.Environ, error) { - return &mockEnviron{}, nil -} - var _ = gc.Suite(®ionMetadataSuite{}) type regionMetadataSuite struct { @@ -316,7 +282,7 @@ func (s *regionMetadataSuite) checkStoredPublished(c *gc.C) { err := s.api.UpdateFromPublishedImages() c.Assert(err, jc.ErrorIsNil) - s.assertCalls(c, environConfig, environConfig, saveMetadata) + s.assertCalls(c, environConfig, "Model", saveMetadata) c.Assert(s.saved, jc.SameContents, s.expected) } @@ -431,7 +397,7 @@ err = s.api.UpdateFromPublishedImages() c.Assert(err, jc.ErrorIsNil) - s.assertCalls(c, environConfig, environConfig, saveMetadata, environConfig, saveMetadata) + s.assertCalls(c, environConfig, "Model", saveMetadata, environConfig, "Model", saveMetadata) c.Assert(s.saved, jc.SameContents, s.expected) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/instancepoller/instancepoller.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/instancepoller/instancepoller.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/instancepoller/instancepoller.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/instancepoller/instancepoller.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,6 +10,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" "github.com/juju/juju/status" @@ -28,8 +29,8 @@ *common.StatusGetter st StateInterface - resources *common.Resources - authorizer common.Authorizer + resources facade.Resources + authorizer facade.Authorizer accessMachine common.GetAuthFunc clock clock.Clock } @@ -37,8 +38,8 @@ // newInstancePollerAPI wraps NewInstancePollerAPI for RegisterStandardFacade. func newInstancePollerAPI( st *state.State, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, ) (*InstancePollerAPI, error) { return NewInstancePollerAPI(st, resources, authorizer, clock.WallClock) } @@ -47,8 +48,8 @@ // facade. func NewInstancePollerAPI( st *state.State, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, clock clock.Clock, ) (*InstancePollerAPI, error) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/keymanager/keymanager.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/keymanager/keymanager.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/keymanager/keymanager.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/keymanager/keymanager.go 2016-08-16 08:56:25.000000000 +0000 @@ -15,6 +15,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/environs/config" "github.com/juju/juju/state" @@ -38,8 +39,8 @@ // implementation of the api end point. type KeyManagerAPI struct { state *state.State - resources *common.Resources - authorizer common.Authorizer + resources facade.Resources + authorizer facade.Authorizer canRead func(string) bool canWrite func(string) bool check *common.BlockChecker @@ -48,7 +49,7 @@ var _ KeyManager = (*KeyManagerAPI)(nil) // NewKeyManagerAPI creates a new server-side keyupdater API end point. -func NewKeyManagerAPI(st *state.State, resources *common.Resources, authorizer common.Authorizer) (*KeyManagerAPI, error) { +func NewKeyManagerAPI(st *state.State, resources facade.Resources, authorizer facade.Authorizer) (*KeyManagerAPI, error) { // Only clients and environment managers can access the key manager service. if !authorizer.AuthClient() && !authorizer.AuthModelManager() { return nil, common.ErrPerm diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/keyupdater/authorisedkeys.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/keyupdater/authorisedkeys.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/keyupdater/authorisedkeys.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/keyupdater/authorisedkeys.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,6 +9,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" "github.com/juju/juju/state/watcher" @@ -28,8 +29,8 @@ // implementation of the api end point. type KeyUpdaterAPI struct { state *state.State - resources *common.Resources - authorizer common.Authorizer + resources facade.Resources + authorizer facade.Authorizer getCanRead common.GetAuthFunc } @@ -38,8 +39,8 @@ // NewKeyUpdaterAPI creates a new server-side keyupdater API end point. func NewKeyUpdaterAPI( st *state.State, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, ) (*KeyUpdaterAPI, error) { // Only machine agents have access to the keyupdater service. if !authorizer.AuthMachineAgent() { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/leadership/leadership.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/leadership/leadership.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/leadership/leadership.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/leadership/leadership.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,6 +10,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/core/leadership" "github.com/juju/juju/state" @@ -41,14 +42,14 @@ // NewLeadershipServiceFacade constructs a new LeadershipService and presents // a signature that can be used with RegisterStandardFacade. func NewLeadershipServiceFacade( - state *state.State, resources *common.Resources, authorizer common.Authorizer, + state *state.State, resources facade.Resources, authorizer facade.Authorizer, ) (LeadershipService, error) { return NewLeadershipService(state.LeadershipClaimer(), authorizer) } // NewLeadershipService constructs a new LeadershipService. func NewLeadershipService( - claimer leadership.Claimer, authorizer common.Authorizer, + claimer leadership.Claimer, authorizer facade.Authorizer, ) (LeadershipService, error) { if !authorizer.AuthUnitAgent() { @@ -65,7 +66,7 @@ // is the concrete implementation of the API endpoint. type leadershipService struct { claimer leadership.Claimer - authorizer common.Authorizer + authorizer facade.Authorizer } // ClaimLeadership is part of the LeadershipService interface. diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/leadership/leadership_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/leadership/leadership_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/leadership/leadership_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/leadership/leadership_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -18,7 +18,7 @@ gc "gopkg.in/check.v1" "gopkg.in/juju/names.v2" - "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/leadership" "github.com/juju/juju/apiserver/params" coreleadership "github.com/juju/juju/core/leadership" @@ -55,7 +55,7 @@ } type stubAuthorizer struct { - common.Authorizer + facade.Authorizer tag names.Tag } @@ -80,7 +80,7 @@ } func newLeadershipService( - c *gc.C, claimer coreleadership.Claimer, authorizer common.Authorizer, + c *gc.C, claimer coreleadership.Claimer, authorizer facade.Authorizer, ) leadership.LeadershipService { if authorizer == nil { authorizer = stubAuthorizer{tag: names.NewUnitTag(StubUnitNm)} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/leadership/settings.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/leadership/settings.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/leadership/settings.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/leadership/settings.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,6 +7,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/core/leadership" ) @@ -14,7 +15,7 @@ // NewLeadershipSettingsAccessor creates a new // LeadershipSettingsAccessor. func NewLeadershipSettingsAccessor( - authorizer common.Authorizer, + authorizer facade.Authorizer, registerWatcherFn RegisterWatcherFn, getSettingsFn GetSettingsFn, leaderCheckFn LeaderCheckFn, @@ -52,7 +53,7 @@ // LeadershipSettingsAccessor provides a type which can read, write, // and watch leadership settings. type LeadershipSettingsAccessor struct { - authorizer common.Authorizer + authorizer facade.Authorizer registerWatcherFn RegisterWatcherFn getSettingsFn GetSettingsFn leaderCheckFn LeaderCheckFn diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/lifeflag/facade.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/lifeflag/facade.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/lifeflag/facade.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/lifeflag/facade.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,6 +7,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/state" ) @@ -15,7 +16,7 @@ state.EntityFinder } -func NewFacade(backend Backend, resources *common.Resources, authorizer common.Authorizer) (*Facade, error) { +func NewFacade(backend Backend, resources facade.Resources, authorizer facade.Authorizer) (*Facade, error) { if !authorizer.AuthModelManager() { return nil, common.ErrPerm } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/lifeflag/shim.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/lifeflag/shim.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/lifeflag/shim.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/lifeflag/shim.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,13 +5,14 @@ import ( "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/state" ) func init() { common.RegisterStandardFacade( "LifeFlag", 1, - func(st *state.State, resources *common.Resources, authorizer common.Authorizer) (*Facade, error) { + func(st *state.State, resources facade.Resources, authorizer facade.Authorizer) (*Facade, error) { return NewFacade(st, resources, authorizer) }, ) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/lifeflag/util_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/lifeflag/util_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/lifeflag/util_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/lifeflag/util_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,15 +7,15 @@ "github.com/juju/errors" "gopkg.in/juju/names.v2" - "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" coretesting "github.com/juju/juju/testing" ) -// mockAuth implements common.Authorizer for the tests' convenience. +// mockAuth implements facade.Authorizer for the tests' convenience. type mockAuth struct { - common.Authorizer + facade.Authorizer modelManager bool } @@ -24,7 +24,7 @@ } // auth is a convenience constructor for a mockAuth. -func auth(modelManager bool) common.Authorizer { +func auth(modelManager bool) facade.Authorizer { return mockAuth{modelManager: modelManager} } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/logfwd/lastsent.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/logfwd/lastsent.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/logfwd/lastsent.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/logfwd/lastsent.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,12 +10,13 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" ) func init() { - common.RegisterStandardFacade("LogForwarding", 1, func(st *state.State, _ *common.Resources, auth common.Authorizer) (*LogForwardingAPI, error) { + common.RegisterStandardFacade("LogForwarding", 1, func(st *state.State, _ facade.Resources, auth facade.Authorizer) (*LogForwardingAPI, error) { return NewLogForwardingAPI(&stateAdapter{st}, auth) }) } @@ -45,7 +46,7 @@ } // NewLogForwardingAPI creates a new server-side logger API end point. -func NewLogForwardingAPI(st LogForwardingState, auth common.Authorizer) (*LogForwardingAPI, error) { +func NewLogForwardingAPI(st LogForwardingState, auth facade.Authorizer) (*LogForwardingAPI, error) { if !auth.AuthMachineAgent() { // the controller's machine agent return nil, common.ErrPerm } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/logger/logger.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/logger/logger.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/logger/logger.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/logger/logger.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,6 +7,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" "github.com/juju/juju/state/watcher" @@ -29,8 +30,8 @@ // implementation of the api end point. type LoggerAPI struct { state *state.State - resources *common.Resources - authorizer common.Authorizer + resources facade.Resources + authorizer facade.Authorizer } var _ Logger = (*LoggerAPI)(nil) @@ -38,8 +39,8 @@ // NewLoggerAPI creates a new server-side logger API end point. func NewLoggerAPI( st *state.State, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, ) (*LoggerAPI, error) { if !authorizer.AuthMachineAgent() && !authorizer.AuthUnitAgent() { return nil, common.ErrPerm diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/logsink_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/logsink_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/logsink_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/logsink_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -60,13 +60,14 @@ s.password = password s.logs.Clear() - c.Assert(loggo.RegisterWriter("logsink-tests", &s.logs, loggo.INFO), jc.ErrorIsNil) + writer := loggo.NewMinimumLevelWriter(&s.logs, loggo.INFO) + c.Assert(loggo.RegisterWriter("logsink-tests", writer), jc.ErrorIsNil) } func (s *logsinkSuite) TestRejectsBadEnvironUUID(c *gc.C) { reader := s.openWebsocketCustomPath(c, "/model/does-not-exist/logsink") assertJSONError(c, reader, `unknown model: "does-not-exist"`) - s.assertWebsocketClosed(c, reader) + assertWebsocketClosed(c, reader) } func (s *logsinkSuite) TestNoAuth(c *gc.C) { @@ -100,7 +101,7 @@ defer conn.Close() reader := bufio.NewReader(conn) assertJSONError(c, reader, message) - s.assertWebsocketClosed(c, reader) + assertWebsocketClosed(c, reader) } func (s *logsinkSuite) TestLogging(c *gc.C) { @@ -207,13 +208,13 @@ func (s *logsinkSuite) dialWebsocketInternal(c *gc.C, header http.Header) *websocket.Conn { server := s.logsinkURL(c, "wss").String() - return s.dialWebsocketFromURL(c, server, header) + return dialWebsocketFromURL(c, server, header) } func (s *logsinkSuite) openWebsocketCustomPath(c *gc.C, path string) *bufio.Reader { server := s.logsinkURL(c, "wss") server.Path = path - conn := s.dialWebsocketFromURL(c, server.String(), s.makeAuthHeader()) + conn := dialWebsocketFromURL(c, server.String(), s.makeAuthHeader()) s.AddCleanup(func(_ *gc.C) { conn.Close() }) return bufio.NewReader(conn) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/machine/machiner.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/machine/machiner.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/machine/machiner.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/machine/machiner.go 2016-08-16 08:56:25.000000000 +0000 @@ -12,9 +12,11 @@ "github.com/juju/juju/apiserver/common" "github.com/juju/juju/apiserver/common/networkingcommon" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" "github.com/juju/juju/state/multiwatcher" + "github.com/juju/juju/state/stateenvirons" ) var logger = loggo.GetLogger("juju.apiserver.machine") @@ -32,13 +34,13 @@ *common.APIAddresser st *state.State - auth common.Authorizer + auth facade.Authorizer getCanModify common.GetAuthFunc getCanRead common.GetAuthFunc } // NewMachinerAPI creates a new instance of the Machiner API. -func NewMachinerAPI(st *state.State, resources *common.Resources, authorizer common.Authorizer) (*MachinerAPI, error) { +func NewMachinerAPI(st *state.State, resources facade.Resources, authorizer facade.Authorizer) (*MachinerAPI, error) { if !authorizer.AuthMachineAgent() { return nil, common.ErrPerm } @@ -260,7 +262,9 @@ return nil, errors.Trace(err) } - netEnviron, err := networkingcommon.NetworkingEnvironFromModelConfig(api.st) + netEnviron, err := networkingcommon.NetworkingEnvironFromModelConfig( + stateenvirons.EnvironConfigGetter{api.st}, + ) if errors.IsNotSupported(err) { logger.Infof("not updating provider network config: %v", err) return nil, nil diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/machineactions/machineactions.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/machineactions/machineactions.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/machineactions/machineactions.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/machineactions/machineactions.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,6 +8,7 @@ import ( "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" "gopkg.in/juju/names.v2" @@ -24,15 +25,15 @@ // implementation of the api end point. type Facade struct { backend Backend - resources *common.Resources + resources facade.Resources accessMachine common.AuthFunc } // NewFacade creates a new server-side machineactions API end point. func NewFacade( backend Backend, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, ) (*Facade, error) { if !authorizer.AuthMachineAgent() { return nil, common.ErrPerm diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/machineactions/machineactions_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/machineactions/machineactions_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/machineactions/machineactions_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/machineactions/machineactions_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -13,6 +13,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/machineactions" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" @@ -84,13 +85,13 @@ return entities } -// agentAuth implements common.Authorizer for use in the tests. +// agentAuth implements facade.Authorizer for use in the tests. type agentAuth struct { - common.Authorizer + facade.Authorizer machine bool } -// AuthMachineAgent is part of the common.Authorizer interface. +// AuthMachineAgent is part of the facade.Authorizer interface. func (auth agentAuth) AuthMachineAgent() bool { return auth.machine } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/machineactions/shim.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/machineactions/shim.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/machineactions/shim.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/machineactions/shim.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,6 +6,7 @@ import ( "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" "gopkg.in/juju/names.v2" @@ -15,7 +16,7 @@ common.RegisterStandardFacade("MachineActions", 1, newFacade) } -func newFacade(st *state.State, res *common.Resources, auth common.Authorizer) (*Facade, error) { +func newFacade(st *state.State, res facade.Resources, auth facade.Authorizer) (*Facade, error) { return NewFacade(backendShim{st}, res, auth) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/machinemanager/machinemanager.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/machinemanager/machinemanager.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/machinemanager/machinemanager.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/machinemanager/machinemanager.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,6 +9,7 @@ "github.com/juju/errors" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/environs/config" "github.com/juju/juju/instance" @@ -22,7 +23,7 @@ // MachineManagerAPI provides access to the MachineManager API facade. type MachineManagerAPI struct { st stateInterface - authorizer common.Authorizer + authorizer facade.Authorizer check *common.BlockChecker } @@ -33,8 +34,8 @@ // NewMachineManagerAPI creates a new server-side MachineManager API facade. func NewMachineManagerAPI( st *state.State, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, ) (*MachineManagerAPI, error) { if !authorizer.AuthClient() { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/meterstatus/meterstatus.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/meterstatus/meterstatus.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/meterstatus/meterstatus.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/meterstatus/meterstatus.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,6 +8,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" "github.com/juju/juju/state/watcher" @@ -28,14 +29,14 @@ type MeterStatusAPI struct { state *state.State accessUnit common.GetAuthFunc - resources *common.Resources + resources facade.Resources } // NewMeterStatusAPI creates a new API endpoint for dealing with unit meter status. func NewMeterStatusAPI( st *state.State, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, ) (*MeterStatusAPI, error) { if !authorizer.AuthUnitAgent() { return nil, common.ErrPerm diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/metricsadder/metricsadder.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/metricsadder/metricsadder.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/metricsadder/metricsadder.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/metricsadder/metricsadder.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,6 +7,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" ) @@ -32,8 +33,8 @@ // NewMetricsAdderAPI creates a new API endpoint for adding metrics to state. func NewMetricsAdderAPI( st *state.State, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, ) (*MetricsAdderAPI, error) { // TODO(cmars): remove unit agent auth, once worker/metrics/sender manifold // can be righteously relocated to machine agent. diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/metricsdebug/metricsdebug.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/metricsdebug/metricsdebug.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/metricsdebug/metricsdebug.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/metricsdebug/metricsdebug.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,6 +10,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" ) @@ -52,8 +53,8 @@ // NewMetricsDebugAPI creates a new API endpoint for calling metrics debug functions. func NewMetricsDebugAPI( st *state.State, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, ) (*MetricsDebugAPI, error) { if !authorizer.AuthClient() { return nil, common.ErrPerm diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/metricsmanager/metricsmanager.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/metricsmanager/metricsmanager.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/metricsmanager/metricsmanager.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/metricsmanager/metricsmanager.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,6 +11,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/metricsender" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" @@ -46,8 +47,8 @@ // NewMetricsManagerAPI creates a new API endpoint for calling metrics manager functions. func NewMetricsManagerAPI( st *state.State, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, ) (*MetricsManagerAPI, error) { if !(authorizer.AuthMachineAgent() && authorizer.AuthModelManager()) { return nil, common.ErrPerm diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationflag/facade.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationflag/facade.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationflag/facade.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationflag/facade.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,6 +8,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/core/migration" "github.com/juju/juju/state" @@ -18,19 +19,19 @@ type Backend interface { ModelUUID() string MigrationPhase() (migration.Phase, error) - WatchMigrationPhase() (state.NotifyWatcher, error) + WatchMigrationPhase() state.NotifyWatcher } // Facade lets clients watch and get models' migration phases. type Facade struct { backend Backend - resources *common.Resources + resources facade.Resources } // New creates a Facade backed by backend and resources. If auth // doesn't identity the client as a machine agent or a unit agent, // it will return common.ErrPerm. -func New(backend Backend, resources *common.Resources, auth common.Authorizer) (*Facade, error) { +func New(backend Backend, resources facade.Resources, auth facade.Authorizer) (*Facade, error) { if !auth.AuthMachineAgent() && !auth.AuthUnitAgent() { return nil, common.ErrPerm } @@ -101,10 +102,7 @@ if err := facade.auth(tagString); err != nil { return "", errors.Trace(err) } - watch, err := facade.backend.WatchMigrationPhase() - if err != nil { - return "", errors.Trace(err) - } + watch := facade.backend.WatchMigrationPhase() if _, ok := <-watch.Changes(); ok { return facade.resources.Register(watch), nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationflag/facade_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationflag/facade_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationflag/facade_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationflag/facade_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -116,21 +116,20 @@ func (*FacadeSuite) TestWatchErrors(c *gc.C) { stub := &testing.Stub{} - stub.SetErrors(errors.New("blort"), nil, errors.New("squish")) + stub.SetErrors(errors.New("blort")) // trigger channel closed error backend := newMockBackend(stub) resources := common.NewResources() facade, err := migrationflag.New(backend, resources, authOK) c.Assert(err, jc.ErrorIsNil) - // 4 entities: unparseable, unauthorized, watch error, closed chan. + // 3 entities: unparseable, unauthorized, closed channel. results := facade.Watch(entities( "urgle", unknownModel, coretesting.ModelTag.String(), - coretesting.ModelTag.String(), )) - c.Assert(results.Results, gc.HasLen, 4) - stub.CheckCallNames(c, "WatchMigrationPhase", "WatchMigrationPhase") + c.Assert(results.Results, gc.HasLen, 3) + stub.CheckCallNames(c, "WatchMigrationPhase") c.Check(results.Results, jc.DeepEquals, []params.NotifyWatchResult{{ Error: ¶ms.Error{ @@ -142,10 +141,7 @@ }}, { Error: ¶ms.Error{ Message: "blort", - }}, { - Error: ¶ms.Error{ - Message: "squish", - }, - }}) + }}, + }) c.Check(resources.Count(), gc.Equals, 0) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationflag/shim.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationflag/shim.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationflag/shim.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationflag/shim.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,6 +7,7 @@ "github.com/juju/errors" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/core/migration" "github.com/juju/juju/state" ) @@ -16,7 +17,7 @@ } // newFacade wraps New to express the supplied *state.State as a Backend. -func newFacade(st *state.State, resources *common.Resources, auth common.Authorizer) (*Facade, error) { +func newFacade(st *state.State, resources facade.Resources, auth facade.Authorizer) (*Facade, error) { facade, err := New(&backend{st}, resources, auth) if err != nil { return nil, errors.Trace(err) @@ -35,17 +36,13 @@ } // WatchMigrationPhase is part of the Backend interface. -func (shim *backend) WatchMigrationPhase() (state.NotifyWatcher, error) { - watcher, err := shim.st.WatchMigrationStatus() - if err != nil { - return nil, errors.Trace(err) - } - return watcher, nil +func (shim *backend) WatchMigrationPhase() state.NotifyWatcher { + return shim.st.WatchMigrationStatus() } // MigrationPhase is part of the Backend interface. func (shim *backend) MigrationPhase() (migration.Phase, error) { - mig, err := shim.st.GetModelMigration() + mig, err := shim.st.LatestModelMigration() if errors.IsNotFound(err) { return migration.NONE, nil } else if err != nil { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationflag/util_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationflag/util_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationflag/util_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationflag/util_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,26 +6,26 @@ import ( "github.com/juju/testing" - "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/core/migration" "github.com/juju/juju/state" coretesting "github.com/juju/juju/testing" ) -// agentAuth implements common.Authorizer for use in the tests. +// agentAuth implements facade.Authorizer for use in the tests. type agentAuth struct { - common.Authorizer + facade.Authorizer machine bool unit bool } -// AuthMachineAgent is part of the common.Authorizer interface. +// AuthMachineAgent is part of the facade.Authorizer interface. func (auth agentAuth) AuthMachineAgent() bool { return auth.machine } -// AuthUnitAgent is part of the common.Authorizer interface. +// AuthUnitAgent is part of the facade.Authorizer interface. func (auth agentAuth) AuthUnitAgent() bool { return auth.unit } @@ -59,12 +59,9 @@ } // WatchMigrationPhase is part of the migrationflag.Backend interface. -func (mock *mockBackend) WatchMigrationPhase() (state.NotifyWatcher, error) { +func (mock *mockBackend) WatchMigrationPhase() state.NotifyWatcher { mock.stub.AddCall("WatchMigrationPhase") - if err := mock.stub.NextErr(); err != nil { - return nil, err - } - return newMockWatcher(mock.stub), nil + return newMockWatcher(mock.stub) } // newMockWatcher consumes an error from the supplied testing.Stub, and diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationmaster/backend.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationmaster/backend.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationmaster/backend.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationmaster/backend.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,19 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package migrationmaster + +import ( + "github.com/juju/juju/migration" + "github.com/juju/juju/state" +) + +// Backend defines the state functionality required by the +// migrationmaster facade. +type Backend interface { + migration.StateExporter + + WatchForModelMigration() state.NotifyWatcher + LatestModelMigration() (state.ModelMigration, error) + RemoveExportingModelDocs() error +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationmaster/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationmaster/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationmaster/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationmaster/export_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,23 +0,0 @@ -// Copyright 2016 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package migrationmaster - -import ( - "github.com/juju/juju/migration" - "github.com/juju/juju/state" -) - -func PatchState(p Patcher, st Backend) { - p.PatchValue(&getBackend, func(*state.State) Backend { - return st - }) -} - -func PatchExportModel(p Patcher, f func(migration.StateExporter) ([]byte, error)) { - p.PatchValue(&exportModel, f) -} - -type Patcher interface { - PatchValue(ptr, value interface{}) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationmaster/facade.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationmaster/facade.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationmaster/facade.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationmaster/facade.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,268 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package migrationmaster + +import ( + "github.com/juju/errors" + "github.com/juju/utils" + "github.com/juju/utils/set" + "github.com/juju/version" + "gopkg.in/juju/names.v2" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/core/description" + coremigration "github.com/juju/juju/core/migration" + "github.com/juju/juju/state/watcher" +) + +func init() { + common.RegisterStandardFacade("MigrationMaster", 1, newAPIForRegistration) +} + +// API implements the API required for the model migration +// master worker. +type API struct { + backend Backend + authorizer facade.Authorizer + resources facade.Resources +} + +// NewAPI creates a new API server endpoint for the model migration +// master worker. +func NewAPI( + backend Backend, + resources facade.Resources, + authorizer facade.Authorizer, +) (*API, error) { + if !authorizer.AuthModelManager() { + return nil, common.ErrPerm + } + return &API{ + backend: backend, + authorizer: authorizer, + resources: resources, + }, nil +} + +// Watch starts watching for an active migration for the model +// associated with the API connection. The returned id should be used +// with the NotifyWatcher facade to receive events. +func (api *API) Watch() params.NotifyWatchResult { + watch := api.backend.WatchForModelMigration() + if _, ok := <-watch.Changes(); ok { + return params.NotifyWatchResult{ + NotifyWatcherId: api.resources.Register(watch), + } + } + return params.NotifyWatchResult{ + Error: common.ServerError(watcher.EnsureErr(watch)), + } +} + +// GetMigrationStatus returns the details and progress of the latest +// model migration. +func (api *API) GetMigrationStatus() (params.MasterMigrationStatus, error) { + empty := params.MasterMigrationStatus{} + + mig, err := api.backend.LatestModelMigration() + if err != nil { + return empty, errors.Annotate(err, "retrieving model migration") + } + + target, err := mig.TargetInfo() + if err != nil { + return empty, errors.Annotate(err, "retrieving target info") + } + + phase, err := mig.Phase() + if err != nil { + return empty, errors.Annotate(err, "retrieving phase") + } + + return params.MasterMigrationStatus{ + Spec: params.ModelMigrationSpec{ + ModelTag: names.NewModelTag(mig.ModelUUID()).String(), + TargetInfo: params.ModelMigrationTargetInfo{ + ControllerTag: target.ControllerTag.String(), + Addrs: target.Addrs, + CACert: target.CACert, + AuthTag: target.AuthTag.String(), + Password: target.Password, + }, + }, + MigrationId: mig.Id(), + Phase: phase.String(), + PhaseChangedTime: mig.PhaseChangedTime(), + }, nil +} + +// SetPhase sets the phase of the active model migration. The provided +// phase must be a valid phase value, for example QUIESCE" or +// "ABORT". See the core/migration package for the complete list. +func (api *API) SetPhase(args params.SetMigrationPhaseArgs) error { + mig, err := api.backend.LatestModelMigration() + if err != nil { + return errors.Annotate(err, "could not get migration") + } + + phase, ok := coremigration.ParsePhase(args.Phase) + if !ok { + return errors.Errorf("invalid phase: %q", args.Phase) + } + + err = mig.SetPhase(phase) + return errors.Annotate(err, "failed to set phase") +} + +// SetStatusMessage sets a human readable status message containing +// information about the migration's progress. This will be shown in +// status output shown to the end user. +func (api *API) SetStatusMessage(args params.SetMigrationStatusMessageArgs) error { + mig, err := api.backend.LatestModelMigration() + if err != nil { + return errors.Annotate(err, "could not get migration") + } + err = mig.SetStatusMessage(args.Message) + return errors.Annotate(err, "failed to set status message") +} + +// Export serializes the model associated with the API connection. +func (api *API) Export() (params.SerializedModel, error) { + var serialized params.SerializedModel + + model, err := api.backend.Export() + if err != nil { + return serialized, err + } + + bytes, err := description.Serialize(model) + if err != nil { + return serialized, err + } + serialized.Bytes = bytes + serialized.Charms = getUsedCharms(model) + serialized.Tools = getUsedTools(model) + return serialized, nil +} + +// Reap removes all documents for the model associated with the API +// connection. +func (api *API) Reap() error { + return api.backend.RemoveExportingModelDocs() +} + +// WatchMinionReports sets up a watcher which reports when a report +// for a migration minion has arrived. +func (api *API) WatchMinionReports() params.NotifyWatchResult { + mig, err := api.backend.LatestModelMigration() + if err != nil { + return params.NotifyWatchResult{Error: common.ServerError(err)} + } + + watch, err := mig.WatchMinionReports() + if err != nil { + return params.NotifyWatchResult{Error: common.ServerError(err)} + } + + if _, ok := <-watch.Changes(); ok { + return params.NotifyWatchResult{ + NotifyWatcherId: api.resources.Register(watch), + } + } + return params.NotifyWatchResult{ + Error: common.ServerError(watcher.EnsureErr(watch)), + } +} + +// GetMinionReports returns details of the reports made by migration +// minions to the controller for the current migration phase. +func (api *API) GetMinionReports() (params.MinionReports, error) { + var out params.MinionReports + + mig, err := api.backend.LatestModelMigration() + if err != nil { + return out, errors.Trace(err) + } + + reports, err := mig.GetMinionReports() + if err != nil { + return out, errors.Trace(err) + } + + out.MigrationId = mig.Id() + phase, err := mig.Phase() + if err != nil { + return out, errors.Trace(err) + } + out.Phase = phase.String() + + out.SuccessCount = len(reports.Succeeded) + + out.Failed = make([]string, len(reports.Failed)) + for i := 0; i < len(out.Failed); i++ { + out.Failed[i] = reports.Failed[i].String() + } + utils.SortStringsNaturally(out.Failed) + + out.UnknownCount = len(reports.Unknown) + + unknown := make([]string, len(reports.Unknown)) + for i := 0; i < len(unknown); i++ { + unknown[i] = reports.Unknown[i].String() + } + utils.SortStringsNaturally(unknown) + + // Limit the number of unknowns reported + numSamples := out.UnknownCount + if numSamples > 10 { + numSamples = 10 + } + out.UnknownSample = unknown[:numSamples] + + return out, nil +} + +func getUsedCharms(model description.Model) []string { + result := set.NewStrings() + for _, application := range model.Applications() { + result.Add(application.CharmURL()) + } + return result.Values() +} + +func getUsedTools(model description.Model) []params.SerializedModelTools { + // Iterate through the model for all tools, and make a map of them. + usedVersions := make(map[version.Binary]bool) + // It is most likely that the preconditions will limit the number of + // tools versions in use, but that is not relied on here. + for _, machine := range model.Machines() { + addToolsVersionForMachine(machine, usedVersions) + } + + for _, application := range model.Applications() { + for _, unit := range application.Units() { + tools := unit.Tools() + usedVersions[tools.Version()] = true + } + } + + out := make([]params.SerializedModelTools, 0, len(usedVersions)) + for v := range usedVersions { + out = append(out, params.SerializedModelTools{ + Version: v.String(), + URI: common.ToolsURL("", v), + }) + } + return out +} + +func addToolsVersionForMachine(machine description.Machine, usedVersions map[version.Binary]bool) { + tools := machine.Tools() + usedVersions[tools.Version()] = true + for _, container := range machine.Containers() { + addToolsVersionForMachine(container, usedVersions) + } +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationmaster/facade_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationmaster/facade_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationmaster/facade_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationmaster/facade_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,393 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package migrationmaster_test + +import ( + "fmt" + "time" + + "github.com/juju/errors" + "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + "github.com/juju/utils" + "github.com/juju/version" + gc "gopkg.in/check.v1" + "gopkg.in/juju/names.v2" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/migrationmaster" + "github.com/juju/juju/apiserver/params" + apiservertesting "github.com/juju/juju/apiserver/testing" + "github.com/juju/juju/core/description" + coremigration "github.com/juju/juju/core/migration" + "github.com/juju/juju/state" + coretesting "github.com/juju/juju/testing" + jujuversion "github.com/juju/juju/version" +) + +type Suite struct { + coretesting.BaseSuite + + model description.Model + stub *testing.Stub + backend *stubBackend + resources *common.Resources + authorizer apiservertesting.FakeAuthorizer +} + +var _ = gc.Suite(&Suite{}) + +func (s *Suite) SetUpTest(c *gc.C) { + s.BaseSuite.SetUpTest(c) + + s.model = description.NewModel(description.ModelArgs{ + Config: map[string]interface{}{"uuid": modelUUID}, + Owner: names.NewUserTag("admin"), + LatestToolsVersion: jujuversion.Current, + }) + s.stub = new(testing.Stub) + s.backend = &stubBackend{ + migration: &stubMigration{stub: s.stub}, + stub: s.stub, + model: s.model, + } + + s.resources = common.NewResources() + s.AddCleanup(func(*gc.C) { s.resources.StopAll() }) + + s.authorizer = apiservertesting.FakeAuthorizer{ + EnvironManager: true, + } +} + +func (s *Suite) TestNotEnvironManager(c *gc.C) { + s.authorizer.EnvironManager = false + + api, err := s.makeAPI() + c.Assert(api, gc.IsNil) + c.Assert(err, gc.Equals, common.ErrPerm) +} + +func (s *Suite) TestWatch(c *gc.C) { + api := s.mustMakeAPI(c) + + result := api.Watch() + c.Assert(result.Error, gc.IsNil) + + resource := s.resources.Get(result.NotifyWatcherId) + watcher, _ := resource.(state.NotifyWatcher) + c.Assert(watcher, gc.NotNil) + + select { + case <-watcher.Changes(): + c.Fatalf("initial event not consumed") + case <-time.After(coretesting.ShortWait): + } +} + +func (s *Suite) TestGetMigrationStatus(c *gc.C) { + api := s.mustMakeAPI(c) + + status, err := api.GetMigrationStatus() + c.Assert(err, jc.ErrorIsNil) + c.Assert(status, gc.DeepEquals, params.MasterMigrationStatus{ + Spec: params.ModelMigrationSpec{ + ModelTag: names.NewModelTag(modelUUID).String(), + TargetInfo: params.ModelMigrationTargetInfo{ + ControllerTag: names.NewModelTag(controllerUUID).String(), + Addrs: []string{"1.1.1.1:1", "2.2.2.2:2"}, + CACert: "trust me", + AuthTag: names.NewUserTag("admin").String(), + Password: "secret", + }, + }, + MigrationId: "id", + Phase: "PRECHECK", + PhaseChangedTime: s.backend.migration.PhaseChangedTime(), + }) +} + +func (s *Suite) TestSetPhase(c *gc.C) { + api := s.mustMakeAPI(c) + + err := api.SetPhase(params.SetMigrationPhaseArgs{Phase: "ABORT"}) + c.Assert(err, jc.ErrorIsNil) + + c.Assert(s.backend.migration.phaseSet, gc.Equals, coremigration.ABORT) +} + +func (s *Suite) TestSetPhaseNoMigration(c *gc.C) { + s.backend.getErr = errors.New("boom") + api := s.mustMakeAPI(c) + + err := api.SetPhase(params.SetMigrationPhaseArgs{Phase: "ABORT"}) + c.Assert(err, gc.ErrorMatches, "could not get migration: boom") +} + +func (s *Suite) TestSetPhaseBadPhase(c *gc.C) { + api := s.mustMakeAPI(c) + + err := api.SetPhase(params.SetMigrationPhaseArgs{Phase: "wat"}) + c.Assert(err, gc.ErrorMatches, `invalid phase: "wat"`) +} + +func (s *Suite) TestSetPhaseError(c *gc.C) { + s.backend.migration.setPhaseErr = errors.New("blam") + api := s.mustMakeAPI(c) + + err := api.SetPhase(params.SetMigrationPhaseArgs{Phase: "ABORT"}) + c.Assert(err, gc.ErrorMatches, "failed to set phase: blam") +} + +func (s *Suite) TestSetStatusMessage(c *gc.C) { + api := s.mustMakeAPI(c) + + err := api.SetStatusMessage(params.SetMigrationStatusMessageArgs{Message: "foo"}) + c.Assert(err, jc.ErrorIsNil) + c.Check(s.backend.migration.messageSet, gc.Equals, "foo") +} + +func (s *Suite) TestSetStatusMessageNoMigration(c *gc.C) { + s.backend.getErr = errors.New("boom") + api := s.mustMakeAPI(c) + + err := api.SetStatusMessage(params.SetMigrationStatusMessageArgs{Message: "foo"}) + c.Check(err, gc.ErrorMatches, "could not get migration: boom") +} + +func (s *Suite) TestSetStatusMessageError(c *gc.C) { + s.backend.migration.setMessageErr = errors.New("blam") + api := s.mustMakeAPI(c) + + err := api.SetStatusMessage(params.SetMigrationStatusMessageArgs{Message: "foo"}) + c.Assert(err, gc.ErrorMatches, "failed to set status message: blam") +} + +func (s *Suite) TestExport(c *gc.C) { + s.model.AddApplication(description.ApplicationArgs{ + Tag: names.NewApplicationTag("foo"), + CharmURL: "cs:foo-0", + }) + const tools = "2.0.0-xenial-amd64" + m := s.model.AddMachine(description.MachineArgs{Id: names.NewMachineTag("9")}) + m.SetTools(description.AgentToolsArgs{ + Version: version.MustParseBinary(tools), + }) + api := s.mustMakeAPI(c) + + serialized, err := api.Export() + + c.Assert(err, jc.ErrorIsNil) + // We don't want to tie this test the serialisation output (that's + // tested elsewhere). Just check that at least one thing we expect + // is in the serialised output. + c.Assert(string(serialized.Bytes), jc.Contains, jujuversion.Current.String()) + c.Assert(serialized.Charms, gc.DeepEquals, []string{"cs:foo-0"}) + c.Assert(serialized.Tools, gc.DeepEquals, []params.SerializedModelTools{ + {tools, "/tools/" + tools}, + }) +} + +func (s *Suite) TestReap(c *gc.C) { + api := s.mustMakeAPI(c) + + err := api.Reap() + c.Check(err, jc.ErrorIsNil) + s.backend.stub.CheckCalls(c, []testing.StubCall{ + {"RemoveExportingModelDocs", []interface{}{}}, + }) +} + +func (s *Suite) TestReapError(c *gc.C) { + s.backend.removeErr = errors.New("boom") + api := s.mustMakeAPI(c) + + err := api.Reap() + c.Check(err, gc.ErrorMatches, "boom") +} + +func (s *Suite) TestWatchMinionReports(c *gc.C) { + api := s.mustMakeAPI(c) + + result := api.WatchMinionReports() + c.Assert(result.Error, gc.IsNil) + + s.stub.CheckCallNames(c, + "LatestModelMigration", + "ModelMigration.WatchMinionReports", + ) + + resource := s.resources.Get(result.NotifyWatcherId) + watcher, _ := resource.(state.NotifyWatcher) + c.Assert(watcher, gc.NotNil) + + select { + case <-watcher.Changes(): + c.Fatalf("initial event not consumed") + case <-time.After(coretesting.ShortWait): + } +} + +func (s *Suite) TestGetMinionReports(c *gc.C) { + // Report 16 unknowns. These are in reverse order in order to test + // sorting. + unknown := make([]names.Tag, 0, 16) + for i := cap(unknown) - 1; i >= 0; i-- { + unknown = append(unknown, names.NewMachineTag(fmt.Sprintf("%d", i))) + } + m50c0 := names.NewMachineTag("50/lxd/0") + m50c1 := names.NewMachineTag("50/lxd/1") + m50 := names.NewMachineTag("50") + m51 := names.NewMachineTag("51") + m52 := names.NewMachineTag("52") + u0 := names.NewUnitTag("foo/0") + u1 := names.NewUnitTag("foo/1") + s.backend.migration.minionReports = &state.MinionReports{ + Succeeded: []names.Tag{m50, m51, u0}, + Failed: []names.Tag{u1, m52, m50c1, m50c0}, + Unknown: unknown, + } + + api := s.mustMakeAPI(c) + reports, err := api.GetMinionReports() + c.Assert(err, jc.ErrorIsNil) + + // Expect the sample of unknowns to be in order and be limited to + // the first 10. + expectedSample := make([]string, 0, 10) + for i := 0; i < cap(expectedSample); i++ { + expectedSample = append(expectedSample, names.NewMachineTag(fmt.Sprintf("%d", i)).String()) + } + c.Assert(reports, gc.DeepEquals, params.MinionReports{ + MigrationId: "id", + Phase: "PRECHECK", + SuccessCount: 3, + UnknownCount: len(unknown), + UnknownSample: expectedSample, + Failed: []string{ + // Note sorting + m50c0.String(), + m50c1.String(), + m52.String(), + u1.String(), + }, + }) +} + +func (s *Suite) makeAPI() (*migrationmaster.API, error) { + return migrationmaster.NewAPI(s.backend, s.resources, s.authorizer) +} + +func (s *Suite) mustMakeAPI(c *gc.C) *migrationmaster.API { + api, err := migrationmaster.NewAPI(s.backend, s.resources, s.authorizer) + c.Assert(err, jc.ErrorIsNil) + return api +} + +type stubBackend struct { + migrationmaster.Backend + + stub *testing.Stub + getErr error + removeErr error + migration *stubMigration + model description.Model +} + +func (b *stubBackend) WatchForModelMigration() state.NotifyWatcher { + b.stub.AddCall("WatchForModelMigration") + return apiservertesting.NewFakeNotifyWatcher() +} + +func (b *stubBackend) LatestModelMigration() (state.ModelMigration, error) { + b.stub.AddCall("LatestModelMigration") + if b.getErr != nil { + return nil, b.getErr + } + return b.migration, nil +} + +func (b *stubBackend) RemoveExportingModelDocs() error { + b.stub.AddCall("RemoveExportingModelDocs") + return b.removeErr +} + +func (b *stubBackend) Export() (description.Model, error) { + b.stub.AddCall("Export") + return b.model, nil +} + +type stubMigration struct { + state.ModelMigration + + stub *testing.Stub + setPhaseErr error + phaseSet coremigration.Phase + setMessageErr error + messageSet string + minionReports *state.MinionReports +} + +func (m *stubMigration) Id() string { + return "id" +} + +func (m *stubMigration) Phase() (coremigration.Phase, error) { + return coremigration.PRECHECK, nil +} + +func (m *stubMigration) PhaseChangedTime() time.Time { + return time.Date(2016, 6, 22, 16, 38, 0, 0, time.UTC) +} + +func (m *stubMigration) Attempt() (int, error) { + return 1, nil +} + +func (m *stubMigration) ModelUUID() string { + return modelUUID +} + +func (m *stubMigration) TargetInfo() (*coremigration.TargetInfo, error) { + return &coremigration.TargetInfo{ + ControllerTag: names.NewModelTag(controllerUUID), + Addrs: []string{"1.1.1.1:1", "2.2.2.2:2"}, + CACert: "trust me", + AuthTag: names.NewUserTag("admin"), + Password: "secret", + }, nil +} + +func (m *stubMigration) SetPhase(phase coremigration.Phase) error { + if m.setPhaseErr != nil { + return m.setPhaseErr + } + m.phaseSet = phase + return nil +} + +func (m *stubMigration) SetStatusMessage(message string) error { + if m.setMessageErr != nil { + return m.setMessageErr + } + m.messageSet = message + return nil +} + +func (m *stubMigration) WatchMinionReports() (state.NotifyWatcher, error) { + m.stub.AddCall("ModelMigration.WatchMinionReports") + return apiservertesting.NewFakeNotifyWatcher(), nil +} + +func (m *stubMigration) GetMinionReports() (*state.MinionReports, error) { + return m.minionReports, nil +} + +var modelUUID string +var controllerUUID string + +func init() { + modelUUID = utils.MustNewUUID().String() + controllerUUID = utils.MustNewUUID().String() +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationmaster/migrationmaster.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationmaster/migrationmaster.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationmaster/migrationmaster.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationmaster/migrationmaster.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,134 +0,0 @@ -// Copyright 2016 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package migrationmaster - -import ( - "github.com/juju/errors" - "gopkg.in/juju/names.v2" - - "github.com/juju/juju/apiserver/common" - "github.com/juju/juju/apiserver/params" - coremigration "github.com/juju/juju/core/migration" - "github.com/juju/juju/migration" - "github.com/juju/juju/state" - "github.com/juju/juju/state/watcher" -) - -func init() { - common.RegisterStandardFacade("MigrationMaster", 1, NewAPI) -} - -// API implements the API required for the model migration -// master worker. -type API struct { - backend Backend - authorizer common.Authorizer - resources *common.Resources -} - -// NewAPI creates a new API server endpoint for the model migration -// master worker. -func NewAPI( - st *state.State, - resources *common.Resources, - authorizer common.Authorizer, -) (*API, error) { - if !authorizer.AuthModelManager() { - return nil, common.ErrPerm - } - return &API{ - backend: getBackend(st), - authorizer: authorizer, - resources: resources, - }, nil -} - -// Watch starts watching for an active migration for the model -// associated with the API connection. The returned id should be used -// with the NotifyWatcher facade to receive events. -func (api *API) Watch() params.NotifyWatchResult { - watch := api.backend.WatchForModelMigration() - if _, ok := <-watch.Changes(); ok { - return params.NotifyWatchResult{ - NotifyWatcherId: api.resources.Register(watch), - } - } - return params.NotifyWatchResult{ - Error: common.ServerError(watcher.EnsureErr(watch)), - } -} - -// GetMigrationStatus returns the details and progress of the latest -// model migration. -func (api *API) GetMigrationStatus() (params.FullMigrationStatus, error) { - empty := params.FullMigrationStatus{} - - mig, err := api.backend.GetModelMigration() - if err != nil { - return empty, errors.Annotate(err, "retrieving model migration") - } - - target, err := mig.TargetInfo() - if err != nil { - return empty, errors.Annotate(err, "retrieving target info") - } - - attempt, err := mig.Attempt() - if err != nil { - return empty, errors.Annotate(err, "retrieving attempt") - } - - phase, err := mig.Phase() - if err != nil { - return empty, errors.Annotate(err, "retrieving phase") - } - - return params.FullMigrationStatus{ - Spec: params.ModelMigrationSpec{ - ModelTag: names.NewModelTag(mig.ModelUUID()).String(), - TargetInfo: params.ModelMigrationTargetInfo{ - ControllerTag: target.ControllerTag.String(), - Addrs: target.Addrs, - CACert: target.CACert, - AuthTag: target.AuthTag.String(), - Password: target.Password, - }, - }, - Attempt: attempt, - Phase: phase.String(), - }, nil -} - -// SetPhase sets the phase of the active model migration. The provided -// phase must be a valid phase value, for example QUIESCE" or -// "ABORT". See the core/migration package for the complete list. -func (api *API) SetPhase(args params.SetMigrationPhaseArgs) error { - mig, err := api.backend.GetModelMigration() - if err != nil { - return errors.Annotate(err, "could not get migration") - } - - phase, ok := coremigration.ParsePhase(args.Phase) - if !ok { - return errors.Errorf("invalid phase: %q", args.Phase) - } - - err = mig.SetPhase(phase) - return errors.Annotate(err, "failed to set phase") -} - -var exportModel = migration.ExportModel - -// Export serializes the model associated with the API connection. -func (api *API) Export() (params.SerializedModel, error) { - var serialized params.SerializedModel - - bytes, err := exportModel(api.backend) - if err != nil { - return serialized, err - } - - serialized.Bytes = bytes - return serialized, nil -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationmaster/migrationmaster_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationmaster/migrationmaster_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationmaster/migrationmaster_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationmaster/migrationmaster_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,217 +0,0 @@ -// Copyright 2016 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package migrationmaster_test - -import ( - "time" - - "github.com/juju/errors" - jc "github.com/juju/testing/checkers" - "github.com/juju/utils" - gc "gopkg.in/check.v1" - "gopkg.in/juju/names.v2" - - "github.com/juju/juju/apiserver/common" - "github.com/juju/juju/apiserver/migrationmaster" - "github.com/juju/juju/apiserver/params" - apiservertesting "github.com/juju/juju/apiserver/testing" - coremigration "github.com/juju/juju/core/migration" - "github.com/juju/juju/migration" - "github.com/juju/juju/state" - "github.com/juju/juju/testing" -) - -// Ensure that Backend remains compatible with *state.State -var _ migrationmaster.Backend = (*state.State)(nil) - -type Suite struct { - testing.BaseSuite - - backend *stubBackend - resources *common.Resources - authorizer apiservertesting.FakeAuthorizer -} - -var _ = gc.Suite(&Suite{}) - -func (s *Suite) SetUpTest(c *gc.C) { - s.BaseSuite.SetUpTest(c) - - s.backend = &stubBackend{ - migration: new(stubMigration), - } - migrationmaster.PatchState(s, s.backend) - - s.resources = common.NewResources() - s.AddCleanup(func(*gc.C) { s.resources.StopAll() }) - - s.authorizer = apiservertesting.FakeAuthorizer{ - EnvironManager: true, - } -} - -func (s *Suite) TestNotEnvironManager(c *gc.C) { - s.authorizer.EnvironManager = false - - api, err := s.makeAPI() - c.Assert(api, gc.IsNil) - c.Assert(err, gc.Equals, common.ErrPerm) -} - -func (s *Suite) TestWatch(c *gc.C) { - api := s.mustMakeAPI(c) - - result := api.Watch() - c.Assert(result.Error, gc.IsNil) - - resource := s.resources.Get(result.NotifyWatcherId) - watcher, _ := resource.(state.NotifyWatcher) - c.Assert(watcher, gc.NotNil) - - select { - case <-watcher.Changes(): - c.Fatalf("initial event not consumed") - case <-time.After(testing.ShortWait): - } -} - -func (s *Suite) TestGetMigrationStatus(c *gc.C) { - api := s.mustMakeAPI(c) - - status, err := api.GetMigrationStatus() - c.Assert(err, jc.ErrorIsNil) - c.Assert(status, gc.DeepEquals, params.FullMigrationStatus{ - Spec: params.ModelMigrationSpec{ - ModelTag: names.NewModelTag(modelUUID).String(), - TargetInfo: params.ModelMigrationTargetInfo{ - ControllerTag: names.NewModelTag(controllerUUID).String(), - Addrs: []string{"1.1.1.1:1", "2.2.2.2:2"}, - CACert: "trust me", - AuthTag: names.NewUserTag("admin").String(), - Password: "secret", - }, - }, - Attempt: 1, - Phase: "READONLY", - }) -} - -func (s *Suite) TestSetPhase(c *gc.C) { - api := s.mustMakeAPI(c) - - err := api.SetPhase(params.SetMigrationPhaseArgs{Phase: "ABORT"}) - c.Assert(err, jc.ErrorIsNil) - - c.Assert(s.backend.migration.phaseSet, gc.Equals, coremigration.ABORT) -} - -func (s *Suite) TestSetPhaseNoMigration(c *gc.C) { - s.backend.getErr = errors.New("boom") - api := s.mustMakeAPI(c) - - err := api.SetPhase(params.SetMigrationPhaseArgs{Phase: "ABORT"}) - c.Assert(err, gc.ErrorMatches, "could not get migration: boom") -} - -func (s *Suite) TestSetPhaseBadPhase(c *gc.C) { - api := s.mustMakeAPI(c) - - err := api.SetPhase(params.SetMigrationPhaseArgs{Phase: "wat"}) - c.Assert(err, gc.ErrorMatches, `invalid phase: "wat"`) -} - -func (s *Suite) TestSetPhaseError(c *gc.C) { - s.backend.migration.setPhaseErr = errors.New("blam") - api := s.mustMakeAPI(c) - - err := api.SetPhase(params.SetMigrationPhaseArgs{Phase: "ABORT"}) - c.Assert(err, gc.ErrorMatches, "failed to set phase: blam") -} - -func (s *Suite) TestExport(c *gc.C) { - exportModel := func(migration.StateExporter) ([]byte, error) { - return []byte("foo"), nil - } - migrationmaster.PatchExportModel(s, exportModel) - api := s.mustMakeAPI(c) - - serialized, err := api.Export() - - c.Assert(err, jc.ErrorIsNil) - c.Assert(serialized, gc.DeepEquals, params.SerializedModel{ - Bytes: []byte("foo"), - }) -} - -func (s *Suite) makeAPI() (*migrationmaster.API, error) { - return migrationmaster.NewAPI(nil, s.resources, s.authorizer) -} - -func (s *Suite) mustMakeAPI(c *gc.C) *migrationmaster.API { - api, err := migrationmaster.NewAPI(nil, s.resources, s.authorizer) - c.Assert(err, jc.ErrorIsNil) - return api -} - -type stubBackend struct { - migrationmaster.Backend - - getErr error - migration *stubMigration -} - -func (b *stubBackend) WatchForModelMigration() state.NotifyWatcher { - return apiservertesting.NewFakeNotifyWatcher() -} - -func (b *stubBackend) GetModelMigration() (state.ModelMigration, error) { - if b.getErr != nil { - return nil, b.getErr - } - return b.migration, nil -} - -type stubMigration struct { - state.ModelMigration - setPhaseErr error - phaseSet coremigration.Phase -} - -func (m *stubMigration) Phase() (coremigration.Phase, error) { - return coremigration.READONLY, nil -} - -func (m *stubMigration) Attempt() (int, error) { - return 1, nil -} - -func (m *stubMigration) ModelUUID() string { - return modelUUID -} - -func (m *stubMigration) TargetInfo() (*coremigration.TargetInfo, error) { - return &coremigration.TargetInfo{ - ControllerTag: names.NewModelTag(controllerUUID), - Addrs: []string{"1.1.1.1:1", "2.2.2.2:2"}, - CACert: "trust me", - AuthTag: names.NewUserTag("admin"), - Password: "secret", - }, nil -} - -func (m *stubMigration) SetPhase(phase coremigration.Phase) error { - if m.setPhaseErr != nil { - return m.setPhaseErr - } - m.phaseSet = phase - return nil -} - -var modelUUID string -var controllerUUID string - -func init() { - modelUUID = utils.MustNewUUID().String() - controllerUUID = utils.MustNewUUID().String() -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationmaster/shim.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationmaster/shim.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationmaster/shim.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationmaster/shim.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,19 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package migrationmaster + +import ( + "github.com/juju/juju/apiserver/facade" + "github.com/juju/juju/state" +) + +// newAPIForRegistration exists to provide the required signature for +// RegisterStandardFacade, converting st to backend. +func newAPIForRegistration( + st *state.State, + resources facade.Resources, + authorizer facade.Authorizer, +) (*API, error) { + return NewAPI(st, resources, authorizer) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationmaster/state.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationmaster/state.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationmaster/state.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationmaster/state.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,22 +0,0 @@ -// Copyright 2016 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package migrationmaster - -import ( - "github.com/juju/juju/migration" - "github.com/juju/juju/state" -) - -// Backend defines the state functionality required by the -// migrationmaster facade. -type Backend interface { - migration.StateExporter - - WatchForModelMigration() state.NotifyWatcher - GetModelMigration() (state.ModelMigration, error) -} - -var getBackend = func(st *state.State) Backend { - return st -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationminion/backend.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationminion/backend.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationminion/backend.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationminion/backend.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,13 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package migrationminion + +import "github.com/juju/juju/state" + +// Backend defines the state functionality required by the +// MigrationMinion facade. +type Backend interface { + WatchMigrationStatus() state.NotifyWatcher + ModelMigration(string) (state.ModelMigration, error) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationminion/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationminion/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationminion/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationminion/export_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,16 +0,0 @@ -// Copyright 2016 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package migrationminion - -import "github.com/juju/juju/state" - -func PatchState(p Patcher, st Backend) { - p.PatchValue(&getBackend, func(*state.State) Backend { - return st - }) -} - -type Patcher interface { - PatchValue(ptr, value interface{}) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationminion/migrationminion.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationminion/migrationminion.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationminion/migrationminion.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationminion/migrationminion.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,34 +7,31 @@ "github.com/juju/errors" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/state" + "github.com/juju/juju/core/migration" ) -func init() { - common.RegisterStandardFacade("MigrationMinion", 1, NewAPI) -} - // API implements the API required for the model migration // master worker. type API struct { backend Backend - authorizer common.Authorizer - resources *common.Resources + authorizer facade.Authorizer + resources facade.Resources } // NewAPI creates a new API server endpoint for the model migration // master worker. func NewAPI( - st *state.State, - resources *common.Resources, - authorizer common.Authorizer, + backend Backend, + resources facade.Resources, + authorizer facade.Authorizer, ) (*API, error) { if !(authorizer.AuthMachineAgent() || authorizer.AuthUnitAgent()) { return nil, common.ErrPerm } return &API{ - backend: getBackend(st), + backend: backend, authorizer: authorizer, resources: resources, }, nil @@ -48,11 +45,25 @@ // The MigrationStatusWatcher facade must be used to receive events // from the watcher. func (api *API) Watch() (params.NotifyWatchResult, error) { - w, err := api.backend.WatchMigrationStatus() - if err != nil { - return params.NotifyWatchResult{}, errors.Trace(err) - } + w := api.backend.WatchMigrationStatus() return params.NotifyWatchResult{ NotifyWatcherId: api.resources.Register(w), }, nil } + +// Report allows a migration minion to submit whether it succeeded or +// failed for a specific migration phase. +func (api *API) Report(info params.MinionReport) error { + phase, ok := migration.ParsePhase(info.Phase) + if !ok { + return errors.New("unable to parse phase") + } + + mig, err := api.backend.ModelMigration(info.MigrationId) + if err != nil { + return errors.Trace(err) + } + + err = mig.MinionReport(api.authorizer.GetAuthTag(), phase, info.Success) + return errors.Trace(err) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationminion/migrationminion_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationminion/migrationminion_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationminion/migrationminion_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationminion/migrationminion_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,23 +5,27 @@ import ( "github.com/juju/errors" + "github.com/juju/testing" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" "github.com/juju/juju/apiserver/migrationminion" + "github.com/juju/juju/apiserver/params" apiservertesting "github.com/juju/juju/apiserver/testing" + "github.com/juju/juju/core/migration" "github.com/juju/juju/state" - "github.com/juju/juju/testing" + coretesting "github.com/juju/juju/testing" ) // Ensure that Backend remains compatible with *state.State var _ migrationminion.Backend = (*state.State)(nil) type Suite struct { - testing.BaseSuite + coretesting.BaseSuite + stub *testing.Stub backend *stubBackend resources *common.Resources authorizer apiservertesting.FakeAuthorizer @@ -32,8 +36,8 @@ func (s *Suite) SetUpTest(c *gc.C) { s.BaseSuite.SetUpTest(c) - s.backend = &stubBackend{} - migrationminion.PatchState(s, s.backend) + s.stub = &testing.Stub{} + s.backend = &stubBackend{stub: s.stub} s.resources = common.NewResources() s.AddCleanup(func(*gc.C) { s.resources.StopAll() }) @@ -59,14 +63,6 @@ c.Assert(err, gc.Equals, common.ErrPerm) } -func (s *Suite) TestWatchError(c *gc.C) { - s.backend.watchError = errors.New("boom") - api := s.mustMakeAPI(c) - _, err := api.Watch() - c.Assert(err, gc.ErrorMatches, "boom") - c.Assert(s.resources.Count(), gc.Equals, 0) -} - func (s *Suite) TestWatch(c *gc.C) { api := s.mustMakeAPI(c) result, err := api.Watch() @@ -74,24 +70,77 @@ c.Assert(s.resources.Get(result.NotifyWatcherId), gc.NotNil) } +func (s *Suite) TestReport(c *gc.C) { + api := s.mustMakeAPI(c) + err := api.Report(params.MinionReport{ + MigrationId: "id", + Phase: "PRECHECK", + Success: true, + }) + c.Assert(err, jc.ErrorIsNil) + s.stub.CheckCalls(c, []testing.StubCall{ + {"ModelMigration", []interface{}{"id"}}, + {"Report", []interface{}{s.authorizer.Tag, migration.PRECHECK, true}}, + }) +} + +func (s *Suite) TestReportInvalidPhase(c *gc.C) { + api := s.mustMakeAPI(c) + err := api.Report(params.MinionReport{ + MigrationId: "id", + Phase: "WTF", + Success: true, + }) + c.Assert(err, gc.ErrorMatches, "unable to parse phase") +} + +func (s *Suite) TestReportNoSuchMigration(c *gc.C) { + failure := errors.NotFoundf("model") + s.backend.modelLookupErr = failure + api := s.mustMakeAPI(c) + err := api.Report(params.MinionReport{ + MigrationId: "id", + Phase: "QUIESCE", + Success: false, + }) + c.Assert(errors.Cause(err), gc.Equals, failure) +} + func (s *Suite) makeAPI() (*migrationminion.API, error) { - return migrationminion.NewAPI(nil, s.resources, s.authorizer) + return migrationminion.NewAPI(s.backend, s.resources, s.authorizer) } func (s *Suite) mustMakeAPI(c *gc.C) *migrationminion.API { - api, err := migrationminion.NewAPI(nil, s.resources, s.authorizer) + api, err := s.makeAPI() c.Assert(err, jc.ErrorIsNil) return api } type stubBackend struct { migrationminion.Backend - watchError error + stub *testing.Stub + modelLookupErr error +} + +func (b *stubBackend) WatchMigrationStatus() state.NotifyWatcher { + b.stub.AddCall("WatchMigrationStatus") + return apiservertesting.NewFakeNotifyWatcher() } -func (b *stubBackend) WatchMigrationStatus() (state.NotifyWatcher, error) { - if b.watchError != nil { - return nil, b.watchError +func (b *stubBackend) ModelMigration(id string) (state.ModelMigration, error) { + b.stub.AddCall("ModelMigration", id) + if b.modelLookupErr != nil { + return nil, b.modelLookupErr } - return apiservertesting.NewFakeNotifyWatcher(), nil + return &stubModelMigration{stub: b.stub}, nil +} + +type stubModelMigration struct { + state.ModelMigration + stub *testing.Stub +} + +func (m *stubModelMigration) MinionReport(tag names.Tag, phase migration.Phase, success bool) error { + m.stub.AddCall("Report", tag, phase, success) + return nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationminion/register.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationminion/register.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationminion/register.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationminion/register.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,22 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package migrationminion + +import ( + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" + "github.com/juju/juju/state" +) + +func init() { + common.RegisterStandardFacade("MigrationMinion", 1, newAPIShim) +} + +func newAPIShim( + st *state.State, + resources facade.Resources, + authorizer facade.Authorizer, +) (*API, error) { + return NewAPI(st, resources, authorizer) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationminion/state.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationminion/state.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationminion/state.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationminion/state.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,16 +0,0 @@ -// Copyright 2016 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package migrationminion - -import "github.com/juju/juju/state" - -// Backend defines the state functionality required by the -// MigrationMinion facade. -type Backend interface { - WatchMigrationStatus() (state.NotifyWatcher, error) -} - -var getBackend = func(st *state.State) Backend { - return st -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationtarget/migrationtarget.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationtarget/migrationtarget.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationtarget/migrationtarget.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationtarget/migrationtarget.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,6 +8,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/migration" "github.com/juju/juju/state" @@ -21,15 +22,15 @@ // master worker when communicating with the target controller. type API struct { state *state.State - authorizer common.Authorizer - resources *common.Resources + authorizer facade.Authorizer + resources facade.Resources } // NewAPI returns a new API. func NewAPI( st *state.State, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, ) (*API, error) { if err := checkAuth(authorizer, st); err != nil { return nil, errors.Trace(err) @@ -41,7 +42,7 @@ }, nil } -func checkAuth(authorizer common.Authorizer, st *state.State) error { +func checkAuth(authorizer facade.Authorizer, st *state.State) error { if !authorizer.AuthClient() { return errors.Trace(common.ErrPerm) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationtarget/migrationtarget_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationtarget/migrationtarget_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/migrationtarget/migrationtarget_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/migrationtarget/migrationtarget_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,13 +11,11 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade/facadetest" "github.com/juju/juju/apiserver/migrationtarget" "github.com/juju/juju/apiserver/params" apiservertesting "github.com/juju/juju/apiserver/testing" - "github.com/juju/juju/cmd/modelcmd" "github.com/juju/juju/core/description" - "github.com/juju/juju/environs/bootstrap" - "github.com/juju/juju/jujuclient/jujuclienttesting" "github.com/juju/juju/provider/dummy" "github.com/juju/juju/state" statetesting "github.com/juju/juju/state/testing" @@ -35,19 +33,7 @@ func (s *Suite) SetUpTest(c *gc.C) { // Set up InitialConfig with a dummy provider configuration. This // is required to allow model import test to work. - env, err := bootstrap.Prepare( - modelcmd.BootstrapContext(testing.Context(c)), - jujuclienttesting.NewMemStore(), - bootstrap.PrepareParams{ - ControllerConfig: testing.FakeControllerConfig(), - ControllerName: "dummycontroller", - BaseConfig: dummy.SampleConfig(), - CloudName: "dummy", - AdminSecret: "admin-secret", - }, - ) - c.Assert(err, jc.ErrorIsNil) - s.InitialConfig = testing.CustomModelConfig(c, env.Config().AllAttrs()) + s.InitialConfig = testing.CustomModelConfig(c, dummy.SampleConfig()) // The call up to StateSuite's SetUpTest uses s.InitialConfig so // it has to happen here. @@ -65,7 +51,11 @@ factory, err := common.Facades.GetFactory("MigrationTarget", 1) c.Assert(err, jc.ErrorIsNil) - api, err := factory(s.State, s.resources, s.authorizer, "") + api, err := factory(facadetest.Context{ + State_: s.State, + Resources_: s.resources, + Auth_: s.authorizer, + }) c.Assert(err, jc.ErrorIsNil) c.Assert(api, gc.FitsTypeOf, new(migrationtarget.API)) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/modelconfig/backend.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/modelconfig/backend.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/modelconfig/backend.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/modelconfig/backend.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,29 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package modelconfig + +import ( + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/environs/config" + "github.com/juju/juju/state" +) + +// Backend contains the state.State methods used in this package, +// allowing stubs to be created for testing. +type Backend interface { + common.BlockGetter + ModelConfigValues() (config.ConfigValues, error) + ModelConfigDefaultValues() (config.ConfigValues, error) + UpdateModelConfigDefaultValues(map[string]interface{}, []string) error + UpdateModelConfig(map[string]interface{}, []string, state.ValidateConfigFunc) error +} + +type stateShim struct { + *state.State +} + +// NewStateBackend creates a backend for the facade to use. +func NewStateBackend(st *state.State) Backend { + return stateShim{st} +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/modelconfig/modelconfig.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/modelconfig/modelconfig.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/modelconfig/modelconfig.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/modelconfig/modelconfig.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,161 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package modelconfig + +import ( + "github.com/juju/errors" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/environs" + "github.com/juju/juju/environs/config" + "github.com/juju/juju/state" +) + +func init() { + common.RegisterStandardFacade("ModelConfig", 1, newFacade) +} + +func newFacade(st *state.State, _ facade.Resources, auth facade.Authorizer) (*ModelConfigAPI, error) { + return NewModelConfigAPI(NewStateBackend(st), auth) +} + +// ModelConfigAPI is the endpoint which implements the model config facade. +type ModelConfigAPI struct { + backend Backend + auth facade.Authorizer + check *common.BlockChecker +} + +// NewModelConfigAPI creates a new instance of the ModelConfig Facade. +func NewModelConfigAPI(backend Backend, authorizer facade.Authorizer) (*ModelConfigAPI, error) { + if !authorizer.AuthClient() { + return nil, common.ErrPerm + } + client := &ModelConfigAPI{ + backend: backend, + auth: authorizer, + check: common.NewBlockChecker(backend), + } + return client, nil +} + +// ModelGet implements the server-side part of the +// get-model-config CLI command. +func (c *ModelConfigAPI) ModelGet() (params.ModelConfigResults, error) { + result := params.ModelConfigResults{} + values, err := c.backend.ModelConfigValues() + if err != nil { + return result, err + } + + // TODO(wallyworld) - this can be removed once credentials are properly + // managed outside of model config. + // Strip out any model config attributes that are credential attributes. + provider, err := environs.Provider(values[config.TypeKey].Value.(string)) + if err != nil { + return result, err + } + credSchemas := provider.CredentialSchemas() + var allCredentialAttributes []string + for _, schema := range credSchemas { + for _, attr := range schema { + allCredentialAttributes = append(allCredentialAttributes, attr.Name) + } + } + isCredentialAttribute := func(attr string) bool { + for _, a := range allCredentialAttributes { + if a == attr { + return true + } + } + return false + } + + result.Config = make(map[string]params.ConfigValue) + for attr, val := range values { + if isCredentialAttribute(attr) { + continue + } + // Authorized keys are able to be listed using + // juju ssh-keys and including them here just + // clutters everything. + if attr == config.AuthorizedKeysKey { + continue + } + result.Config[attr] = params.ConfigValue{ + Value: val.Value, + Source: val.Source, + } + } + return result, nil +} + +// ModelSet implements the server-side part of the +// set-model-config CLI command. +func (c *ModelConfigAPI) ModelSet(args params.ModelSet) error { + if err := c.check.ChangeAllowed(); err != nil { + return errors.Trace(err) + } + // Make sure we don't allow changing agent-version. + checkAgentVersion := func(updateAttrs map[string]interface{}, removeAttrs []string, oldConfig *config.Config) error { + if v, found := updateAttrs["agent-version"]; found { + oldVersion, _ := oldConfig.AgentVersion() + if v != oldVersion.String() { + return errors.New("agent-version cannot be changed") + } + } + return nil + } + // Replace any deprecated attributes with their new values. + attrs := config.ProcessDeprecatedAttributes(args.Config) + return c.backend.UpdateModelConfig(attrs, nil, checkAgentVersion) +} + +// ModelUnset implements the server-side part of the +// set-model-config CLI command. +func (c *ModelConfigAPI) ModelUnset(args params.ModelUnset) error { + if err := c.check.ChangeAllowed(); err != nil { + return errors.Trace(err) + } + return c.backend.UpdateModelConfig(nil, args.Keys, nil) +} + +// ModelDefaults returns the default config values used when creating a new model. +func (c *ModelConfigAPI) ModelDefaults() (params.ModelConfigResults, error) { + result := params.ModelConfigResults{} + values, err := c.backend.ModelConfigDefaultValues() + if err != nil { + return result, err + } + result.Config = make(map[string]params.ConfigValue) + for attr, val := range values { + result.Config[attr] = params.ConfigValue{ + Value: val.Value, + Source: val.Source, + } + } + return result, nil +} + +// SetModelDefaults writes new values for the specified default model settings. +func (c *ModelConfigAPI) SetModelDefaults(args params.ModelSet) error { + if err := c.check.ChangeAllowed(); err != nil { + return errors.Trace(err) + } + // Make sure we don't allow changing agent-version. + if _, found := args.Config["agent-version"]; found { + return errors.New("agent-version cannot have a default value") + } + return c.backend.UpdateModelConfigDefaultValues(args.Config, nil) +} + +// UnsetModelDefaults removes the specified default model settings. +func (c *ModelConfigAPI) UnsetModelDefaults(args params.ModelUnset) error { + if err := c.check.ChangeAllowed(); err != nil { + return errors.Trace(err) + } + return c.backend.UpdateModelConfigDefaultValues(nil, args.Keys) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/modelconfig/modelconfig_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/modelconfig/modelconfig_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/modelconfig/modelconfig_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/modelconfig/modelconfig_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,278 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package modelconfig_test + +import ( + "github.com/juju/errors" + gitjujutesting "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/juju/names.v2" + + "github.com/juju/juju/apiserver/modelconfig" + "github.com/juju/juju/apiserver/params" + apiservertesting "github.com/juju/juju/apiserver/testing" + "github.com/juju/juju/environs/config" + "github.com/juju/juju/provider/dummy" + _ "github.com/juju/juju/provider/dummy" + "github.com/juju/juju/state" + "github.com/juju/juju/testing" +) + +type modelconfigSuite struct { + gitjujutesting.IsolationSuite + backend *mockBackend + authorizer apiservertesting.FakeAuthorizer + api *modelconfig.ModelConfigAPI +} + +var _ = gc.Suite(&modelconfigSuite{}) + +func (s *modelconfigSuite) SetUpTest(c *gc.C) { + s.IsolationSuite.SetUpTest(c) + s.authorizer = apiservertesting.FakeAuthorizer{ + Tag: names.NewUserTag("bruce@local"), + } + s.backend = &mockBackend{ + cfg: config.ConfigValues{ + "type": {"dummy", "model"}, + "agent-version": {"1.2.3.4", "model"}, + "ftp-proxy": {"http://proxy", "model"}, + "authorized-keys": {testing.FakeAuthKeys, "model"}, + }, + cfgDefaults: config.ConfigValues{ + "attr": {Value: "val", Source: "controller"}, + "attr2": {Value: "val2", Source: "controller"}, + }, + } + var err error + s.api, err = modelconfig.NewModelConfigAPI(s.backend, &s.authorizer) + c.Assert(err, jc.ErrorIsNil) +} + +func (s *modelconfigSuite) TestModelGet(c *gc.C) { + result, err := s.api.ModelGet() + c.Assert(err, jc.ErrorIsNil) + c.Assert(result.Config, jc.DeepEquals, map[string]params.ConfigValue{ + "type": {"dummy", "model"}, + "ftp-proxy": {"http://proxy", "model"}, + "agent-version": {Value: "1.2.3.4", Source: "model"}, + }) +} + +func (s *modelconfigSuite) assertConfigValue(c *gc.C, key string, expected interface{}) { + value, found := s.backend.cfg[key] + c.Assert(found, jc.IsTrue) + c.Assert(value.Value, gc.Equals, expected) +} + +func (s *modelconfigSuite) assertConfigValueMissing(c *gc.C, key string) { + _, found := s.backend.cfg[key] + c.Assert(found, jc.IsFalse) +} + +func (s *modelconfigSuite) TestModelSet(c *gc.C) { + params := params.ModelSet{ + Config: map[string]interface{}{ + "some-key": "value", + "other-key": "other value"}, + } + err := s.api.ModelSet(params) + c.Assert(err, jc.ErrorIsNil) + s.assertConfigValue(c, "some-key", "value") + s.assertConfigValue(c, "other-key", "other value") +} + +func (s *modelconfigSuite) blockAllChanges(c *gc.C, msg string) { + s.backend.msg = msg + s.backend.b = state.ChangeBlock +} + +func (s *modelconfigSuite) assertBlocked(c *gc.C, err error, msg string) { + c.Assert(params.IsCodeOperationBlocked(err), jc.IsTrue, gc.Commentf("error: %#v", err)) + c.Assert(errors.Cause(err), jc.DeepEquals, ¶ms.Error{ + Message: msg, + Code: "operation is blocked", + }) +} + +func (s *modelconfigSuite) assertModelSetBlocked(c *gc.C, args map[string]interface{}, msg string) { + err := s.api.ModelSet(params.ModelSet{args}) + s.assertBlocked(c, err, msg) +} + +func (s *modelconfigSuite) TestBlockChangesModelSet(c *gc.C) { + s.blockAllChanges(c, "TestBlockChangesModelSet") + args := map[string]interface{}{"some-key": "value"} + s.assertModelSetBlocked(c, args, "TestBlockChangesModelSet") +} + +func (s *modelconfigSuite) TestModelSetCannotChangeAgentVersion(c *gc.C) { + old, err := config.New(config.UseDefaults, dummy.SampleConfig().Merge(testing.Attrs{ + "agent-version": "1.2.3.4", + })) + c.Assert(err, jc.ErrorIsNil) + s.backend.old = old + args := params.ModelSet{ + map[string]interface{}{"agent-version": "9.9.9"}, + } + err = s.api.ModelSet(args) + c.Assert(err, gc.ErrorMatches, "agent-version cannot be changed") + + // It's okay to pass config back with the same agent-version. + result, err := s.api.ModelGet() + c.Assert(err, jc.ErrorIsNil) + c.Assert(result.Config["agent-version"], gc.NotNil) + args.Config["agent-version"] = result.Config["agent-version"].Value + err = s.api.ModelSet(args) + c.Assert(err, jc.ErrorIsNil) +} + +func (s *modelconfigSuite) TestModelUnset(c *gc.C) { + err := s.backend.UpdateModelConfig(map[string]interface{}{"abc": 123}, nil, nil) + c.Assert(err, jc.ErrorIsNil) + + args := params.ModelUnset{[]string{"abc"}} + err = s.api.ModelUnset(args) + c.Assert(err, jc.ErrorIsNil) + s.assertConfigValueMissing(c, "abc") +} + +func (s *modelconfigSuite) TestBlockModelUnset(c *gc.C) { + err := s.backend.UpdateModelConfig(map[string]interface{}{"abc": 123}, nil, nil) + c.Assert(err, jc.ErrorIsNil) + s.blockAllChanges(c, "TestBlockModelUnset") + + args := params.ModelUnset{[]string{"abc"}} + err = s.api.ModelUnset(args) + s.assertBlocked(c, err, "TestBlockModelUnset") +} + +func (s *modelconfigSuite) TestModelUnsetMissing(c *gc.C) { + // It's okay to unset a non-existent attribute. + args := params.ModelUnset{[]string{"not_there"}} + err := s.api.ModelUnset(args) + c.Assert(err, jc.ErrorIsNil) +} + +func (s *modelconfigSuite) TestModelDefaults(c *gc.C) { + result, err := s.api.ModelDefaults() + c.Assert(err, jc.ErrorIsNil) + expectedValues := map[string]params.ConfigValue{ + "attr": {Value: "val", Source: "controller"}, + "attr2": {Value: "val2", Source: "controller"}, + } + c.Assert(result.Config, jc.DeepEquals, expectedValues) +} + +func (s *modelconfigSuite) TestSetModelDefaults(c *gc.C) { + params := params.ModelSet{ + Config: map[string]interface{}{ + "attr3": "val3", + "attr4": "val4"}, + } + err := s.api.SetModelDefaults(params) + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.backend.cfgDefaults, jc.DeepEquals, config.ConfigValues{ + "attr": {Value: "val", Source: "controller"}, + "attr2": {Value: "val2", Source: "controller"}, + "attr3": {Value: "val3", Source: "controller"}, + "attr4": {Value: "val4", Source: "controller"}, + }) +} + +func (s *modelconfigSuite) TestBlockChangesSetModelDefaults(c *gc.C) { + s.blockAllChanges(c, "TestBlockChangesSetModelDefaults") + err := s.api.SetModelDefaults(params.ModelSet{}) + s.assertBlocked(c, err, "TestBlockChangesSetModelDefaults") +} + +func (s *modelconfigSuite) TestUnsetModelDefaults(c *gc.C) { + args := params.ModelUnset{[]string{"attr"}} + err := s.api.UnsetModelDefaults(args) + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.backend.cfgDefaults, jc.DeepEquals, config.ConfigValues{ + "attr2": {Value: "val2", Source: "controller"}, + }) +} + +func (s *modelconfigSuite) TestBlockUnsetModelDefaults(c *gc.C) { + s.blockAllChanges(c, "TestBlockUnsetModelDefaults") + args := params.ModelUnset{[]string{"abc"}} + err := s.api.UnsetModelDefaults(args) + s.assertBlocked(c, err, "TestBlockUnsetModelDefaults") +} + +func (s *modelconfigSuite) TestUnsetModelDefaultsMissing(c *gc.C) { + // It's okay to unset a non-existent attribute. + args := params.ModelUnset{[]string{"not_there"}} + err := s.api.UnsetModelDefaults(args) + c.Assert(err, jc.ErrorIsNil) +} + +type mockBackend struct { + cfg config.ConfigValues + cfgDefaults config.ConfigValues + old *config.Config + b state.BlockType + msg string +} + +func (m *mockBackend) ModelConfigValues() (config.ConfigValues, error) { + return m.cfg, nil +} + +func (m *mockBackend) ModelConfigDefaultValues() (config.ConfigValues, error) { + return m.cfgDefaults, nil +} + +func (m *mockBackend) UpdateModelConfig(update map[string]interface{}, remove []string, validate state.ValidateConfigFunc) error { + if validate != nil { + err := validate(update, remove, m.old) + if err != nil { + return err + } + } + for k, v := range update { + m.cfg[k] = config.ConfigValue{v, "model"} + } + for _, n := range remove { + delete(m.cfg, n) + } + return nil +} + +func (m *mockBackend) UpdateModelConfigDefaultValues(update map[string]interface{}, remove []string) error { + for k, v := range update { + m.cfgDefaults[k] = config.ConfigValue{v, "controller"} + } + for _, n := range remove { + delete(m.cfgDefaults, n) + } + return nil +} + +func (m *mockBackend) GetBlockForType(t state.BlockType) (state.Block, bool, error) { + if m.b == t { + return &mockBlock{t: t, m: m.msg}, true, nil + } else { + return nil, false, nil + } +} + +type mockBlock struct { + state.Block + t state.BlockType + m string +} + +func (m mockBlock) Id() string { return "" } + +func (m mockBlock) Tag() (names.Tag, error) { return names.NewModelTag("mocktesting"), nil } + +func (m mockBlock) Type() state.BlockType { return m.t } + +func (m mockBlock) Message() string { return m.m } + +func (m mockBlock) ModelUUID() string { return "" } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/modelconfig/package_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/modelconfig/package_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/modelconfig/package_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/modelconfig/package_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,14 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package modelconfig_test + +import ( + "testing" + + gc "gopkg.in/check.v1" +) + +func Test(t *testing.T) { + gc.TestingT(t) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/modelmanager/modelinfo_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/modelmanager/modelinfo_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/modelmanager/modelinfo_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/modelmanager/modelinfo_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,6 +4,7 @@ package modelmanager_test import ( + "strings" "time" "github.com/juju/errors" @@ -20,6 +21,7 @@ apiservertesting "github.com/juju/juju/apiserver/testing" "github.com/juju/juju/cloud" "github.com/juju/juju/controller" + "github.com/juju/juju/core/description" "github.com/juju/juju/environs" "github.com/juju/juju/environs/config" "github.com/juju/juju/state" @@ -58,34 +60,30 @@ }, users: []*mockModelUser{{ userName: "admin", - access: state.AdminAccess, + access: description.AdminAccess, }, { userName: "bob@local", displayName: "Bob", - access: state.ReadAccess, + access: description.ReadAccess, }, { userName: "charlotte@local", displayName: "Charlotte", - access: state.ReadAccess, + access: description.ReadAccess, }}, } var err error - s.modelmanager, err = modelmanager.NewModelManagerAPI(s.st, &s.authorizer) + s.modelmanager, err = modelmanager.NewModelManagerAPI(s.st, nil, &s.authorizer) c.Assert(err, jc.ErrorIsNil) } func (s *modelInfoSuite) setAPIUser(c *gc.C, user names.UserTag) { s.authorizer.Tag = user - modelmanager, err := modelmanager.NewModelManagerAPI(s.st, s.authorizer) + modelmanager, err := modelmanager.NewModelManagerAPI(s.st, nil, s.authorizer) c.Assert(err, jc.ErrorIsNil) s.modelmanager = modelmanager } func (s *modelInfoSuite) TestModelInfo(c *gc.C) { - s.st.model.users[1].SetErrors( - state.NeverConnectedError("never connected"), - nil, nil, nil, nil, - ) info := s.getModelInfo(c) c.Assert(info, jc.DeepEquals, params.ModelInfo{ Name: "testenv", @@ -109,7 +107,7 @@ }, { UserName: "bob@local", DisplayName: "Bob", - LastConnection: nil, // never connected + LastConnection: &time.Time{}, Access: params.ModelReadAccess, }, { UserName: "charlotte@local", @@ -124,11 +122,17 @@ {"ForModel", []interface{}{names.NewModelTag(s.st.model.cfg.UUID())}}, {"Model", nil}, {"ControllerConfig", nil}, + {"LastModelConnection", []interface{}{names.NewUserTag("admin")}}, + {"LastModelConnection", []interface{}{names.NewLocalUserTag("bob")}}, + {"LastModelConnection", []interface{}{names.NewLocalUserTag("charlotte")}}, {"Close", nil}, }) s.st.model.CheckCalls(c, []gitjujutesting.StubCall{ {"Config", nil}, {"Users", nil}, + {"ModelTag", nil}, + {"ModelTag", nil}, + {"ModelTag", nil}, {"Status", nil}, {"Owner", nil}, {"Life", nil}, @@ -216,10 +220,20 @@ cloud cloud.Cloud model *mockModel controllerModel *mockModel - users []*state.ModelUser + users []description.UserAccess creds map[string]cloud.Credential } +type fakeModelDescription struct { + description.Model `yaml:"-"` + + UUID string `yaml:"model-uuid"` +} + +func (st *mockState) Export() (description.Model, error) { + return &fakeModelDescription{UUID: st.uuid}, nil +} + func (st *mockState) ModelUUID() string { st.MethodCall(st, "ModelUUID") return st.uuid @@ -240,7 +254,7 @@ } for _, u := range st.controllerModel.users { - if user.Name() == u.UserName() && u.access == state.AdminAccess { + if user.Name() == u.userName && u.access == description.AdminAccess { nextErr := st.NextErr() if user.Name() != "admin" { panic(user.Name()) @@ -262,6 +276,16 @@ return st.controllerModel, st.NextErr() } +func (st *mockState) ComposeNewModelConfig(modelAttr map[string]interface{}) (map[string]interface{}, error) { + st.MethodCall(st, "ComposeNewModelConfig") + attr := make(map[string]interface{}) + for attrName, val := range modelAttr { + attr[attrName] = val + } + attr["something"] = "value" + return attr, st.NextErr() +} + func (st *mockState) ControllerConfig() (controller.Config, error) { st.MethodCall(st, "ControllerConfig") return controller.Config{ @@ -304,9 +328,14 @@ return st.NextErr() } -func (st *mockState) AddModelUser(spec state.ModelUserSpec) (*state.ModelUser, error) { +func (st *mockState) AddModelUser(spec state.UserAccessSpec) (description.UserAccess, error) { st.MethodCall(st, "AddModelUser", spec) - return nil, st.NextErr() + return description.UserAccess{}, st.NextErr() +} + +func (st *mockState) AddControllerUser(spec state.UserAccessSpec) (description.UserAccess, error) { + st.MethodCall(st, "AddControllerUser", spec) + return description.UserAccess{}, st.NextErr() } func (st *mockState) RemoveModelUser(tag names.UserTag) error { @@ -314,9 +343,24 @@ return st.NextErr() } -func (st *mockState) ModelUser(tag names.UserTag) (*state.ModelUser, error) { - st.MethodCall(st, "ModelUser", tag) - return nil, st.NextErr() +func (st *mockState) UserAccess(tag names.UserTag, target names.Tag) (description.UserAccess, error) { + st.MethodCall(st, "ModelUser", tag, target) + return description.UserAccess{}, st.NextErr() +} + +func (st *mockState) LastModelConnection(user names.UserTag) (time.Time, error) { + st.MethodCall(st, "LastModelConnection", user) + return time.Time{}, st.NextErr() +} + +func (st *mockState) RemoveUserAccess(subject names.UserTag, target names.Tag) error { + st.MethodCall(st, "RemoveUserAccess", subject, target) + return st.NextErr() +} + +func (st *mockState) SetUserAccess(subject names.UserTag, target names.Tag, access description.Access) (description.UserAccess, error) { + st.MethodCall(st, "SetUserAccess", subject, target, access) + return description.UserAccess{}, st.NextErr() } type mockModel struct { @@ -375,14 +419,21 @@ return "some-credential" } -func (m *mockModel) Users() ([]common.ModelUser, error) { +func (m *mockModel) Users() ([]description.UserAccess, error) { m.MethodCall(m, "Users") if err := m.NextErr(); err != nil { return nil, err } - users := make([]common.ModelUser, len(m.users)) + users := make([]description.UserAccess, len(m.users)) for i, user := range m.users { - users[i] = user + users[i] = description.UserAccess{ + UserID: strings.ToLower(user.userName), + UserTag: names.NewUserTag(user.userName), + Object: m.ModelTag(), + Access: user.access, + DisplayName: user.displayName, + UserName: user.userName, + } } return users, nil } @@ -402,46 +453,5 @@ userName string displayName string lastConnection time.Time - access state.Access -} - -func (u *mockModelUser) IsAdmin() bool { - u.MethodCall(u, "IsAdmin") - u.PopNoErr() - return u.access == state.AdminAccess -} - -func (u *mockModelUser) IsReadOnly() bool { - u.MethodCall(u, "IsReadOnly") - u.PopNoErr() - return u.access == state.ReadAccess -} - -func (u *mockModelUser) IsReadWrite() bool { - u.MethodCall(u, "IsReadWrite") - u.PopNoErr() - return u.access == state.WriteAccess -} - -func (u *mockModelUser) DisplayName() string { - u.MethodCall(u, "DisplayName") - u.PopNoErr() - return u.displayName -} - -func (u *mockModelUser) LastConnection() (time.Time, error) { - u.MethodCall(u, "LastConnection") - return u.lastConnection, u.NextErr() -} - -func (u *mockModelUser) UserName() string { - u.MethodCall(u, "UserName") - u.PopNoErr() - return u.userName -} - -func (u *mockModelUser) UserTag() names.UserTag { - u.MethodCall(u, "UserTag") - u.PopNoErr() - return names.NewUserTag(u.userName) + access description.Access } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/modelmanager/modelmanager.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/modelmanager/modelmanager.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/modelmanager/modelmanager.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/modelmanager/modelmanager.go 2016-08-16 08:56:25.000000000 +0000 @@ -14,16 +14,23 @@ "github.com/juju/errors" "github.com/juju/loggo" "github.com/juju/txn" + "github.com/juju/utils" "github.com/juju/version" "gopkg.in/juju/names.v2" + "gopkg.in/yaml.v1" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/cloud" + jujucloud "github.com/juju/juju/cloud" "github.com/juju/juju/controller/modelmanager" + "github.com/juju/juju/core/description" + "github.com/juju/juju/environs" "github.com/juju/juju/environs/config" "github.com/juju/juju/juju/permission" + "github.com/juju/juju/migration" "github.com/juju/juju/state" + "github.com/juju/juju/state/stateenvirons" "github.com/juju/juju/tools" ) @@ -36,6 +43,7 @@ // ModelManager defines the methods on the modelmanager API endpoint. type ModelManager interface { CreateModel(args params.ModelCreateArgs) (params.ModelInfo, error) + DumpModels(args params.Entities) params.MapResults ListModels(user params.Entity) (params.UserModelList, error) DestroyModel() error } @@ -44,7 +52,7 @@ // the concrete implementation of the api end point. type ModelManagerAPI struct { state common.ModelManagerBackend - authorizer common.Authorizer + authorizer facade.Authorizer toolsFinder *common.ToolsFinder apiUser names.UserTag isAdmin bool @@ -52,13 +60,18 @@ var _ ModelManager = (*ModelManagerAPI)(nil) -func newFacade(st *state.State, _ *common.Resources, auth common.Authorizer) (*ModelManagerAPI, error) { - return NewModelManagerAPI(common.NewModelManagerBackend(st), auth) +func newFacade(st *state.State, _ facade.Resources, auth facade.Authorizer) (*ModelManagerAPI, error) { + configGetter := stateenvirons.EnvironConfigGetter{st} + return NewModelManagerAPI(common.NewModelManagerBackend(st), configGetter, auth) } // NewModelManagerAPI creates a new api server endpoint for managing // models. -func NewModelManagerAPI(st common.ModelManagerBackend, authorizer common.Authorizer) (*ModelManagerAPI, error) { +func NewModelManagerAPI( + st common.ModelManagerBackend, + configGetter environs.EnvironConfigGetter, + authorizer facade.Authorizer, +) (*ModelManagerAPI, error) { if !authorizer.AuthClient() { return nil, common.ErrPerm } @@ -75,7 +88,7 @@ return &ModelManagerAPI{ state: st, authorizer: authorizer, - toolsFinder: common.NewToolsFinder(st, st, urlGetter), + toolsFinder: common.NewToolsFinder(configGetter, st, urlGetter), apiUser: apiUser, isAdmin: isAdmin, }, nil @@ -105,7 +118,10 @@ } func (mm *ModelManagerAPI) newModelConfig( - args params.ModelCreateArgs, controllerUUID string, source ConfigSource, credential *cloud.Credential, + cloudSpec environs.CloudSpec, + args params.ModelCreateArgs, + controllerUUID string, + source ConfigSource, ) (*config.Config, error) { // For now, we just smash to the two maps together as we store // the account values and the model config together in the @@ -127,8 +143,8 @@ // Copy credential attributes across to model config. // TODO(axw) credentials should not be going into model config. - if credential != nil { - for key, value := range credential.Attributes() { + if cloudSpec.Credential != nil { + for key, value := range cloudSpec.Credential.Attributes() { joint[key] = value } } @@ -137,7 +153,11 @@ if err != nil { return nil, errors.Trace(err) } + if joint, err = mm.state.ComposeNewModelConfig(joint); err != nil { + return nil, errors.Trace(err) + } creator := modelmanager.ModelConfigCreator{ + Provider: environs.Provider, FindTools: func(n version.Number) (tools.List, error) { result, err := mm.toolsFinder.FindTools(params.FindToolsParams{ Number: n, @@ -148,7 +168,7 @@ return result.List, nil }, } - return creator.NewModelConfig(mm.isAdmin, controllerUUID, baseConfig, joint) + return creator.NewModelConfig(cloudSpec, controllerUUID, baseConfig, joint) } // CreateModel creates a new model using the account and @@ -181,6 +201,11 @@ } cloudName := controllerModel.Cloud() + cloud, err := mm.state.Cloud(cloudName) + if err != nil { + return result, errors.Annotate(err, "getting cloud definition") + } + cloudCredentialName := args.CloudCredential if cloudCredentialName == "" { if ownerTag.Canonical() == controllerModel.Owner().Canonical() { @@ -190,13 +215,9 @@ // cloud credential, and if so, use it? For now, we // require the user to specify a credential unless // the cloud does not require one. - controllerCloud, err := mm.state.Cloud(controllerModel.Cloud()) - if err != nil { - return result, errors.Trace(err) - } var hasEmpty bool - for _, authType := range controllerCloud.AuthTypes { - if authType != cloud.EmptyAuthType { + for _, authType := range cloud.AuthTypes { + if authType != jujucloud.EmptyAuthType { continue } hasEmpty = true @@ -208,12 +229,12 @@ } } - cloudRegion := args.CloudRegion - if cloudRegion == "" { - cloudRegion = controllerModel.CloudRegion() + cloudRegionName := args.CloudRegion + if cloudRegionName == "" { + cloudRegionName = controllerModel.CloudRegion() } - var credential *cloud.Credential + var credential *jujucloud.Credential if cloudCredentialName != "" { ownerCredentials, err := mm.state.CloudCredentials(ownerTag, controllerModel.Cloud()) if err != nil { @@ -228,25 +249,46 @@ credential = &elem } + cloudSpec, err := environs.MakeCloudSpec(cloud, cloudName, cloudRegionName, credential) + if err != nil { + return result, errors.Trace(err) + } + controllerCfg, err := mm.state.ControllerConfig() if err != nil { return result, errors.Trace(err) } - newConfig, err := mm.newModelConfig(args, controllerCfg.ControllerUUID(), controllerModel, credential) + newConfig, err := mm.newModelConfig(cloudSpec, args, controllerCfg.ControllerUUID(), controllerModel) if err != nil { return result, errors.Annotate(err, "failed to create config") } + // Create the Environ. + env, err := environs.New(environs.OpenParams{ + Cloud: cloudSpec, + Config: newConfig, + }) + if err != nil { + return result, errors.Annotate(err, "failed to open environ") + } + if err := env.Create(environs.CreateParams{ + ControllerUUID: controllerCfg.ControllerUUID(), + }); err != nil { + return result, errors.Annotate(err, "failed to create environ") + } + storageProviderRegistry := stateenvirons.NewStorageProviderRegistry(env) + // NOTE: check the agent-version of the config, and if it is > the current // version, it is not supported, also check existing tools, and if we don't // have tools for that version, also die. model, st, err := mm.state.NewModel(state.ModelArgs{ CloudName: cloudName, - CloudRegion: cloudRegion, + CloudRegion: cloudRegionName, CloudCredential: cloudCredentialName, Config: newConfig, Owner: ownerTag, + StorageProviderRegistry: storageProviderRegistry, }) if err != nil { return result, errors.Annotate(err, "failed to create new model") @@ -256,6 +298,77 @@ return mm.getModelInfo(model.ModelTag()) } +func (mm *ModelManagerAPI) dumpModel(args params.Entity) (map[string]interface{}, error) { + modelTag, err := names.ParseModelTag(args.Tag) + if err != nil { + return nil, errors.Trace(err) + } + + st := mm.state + if st.ModelTag() != modelTag { + st, err = mm.state.ForModel(modelTag) + if err != nil { + if errors.IsNotFound(err) { + return nil, errors.Trace(common.ErrBadId) + } + return nil, errors.Trace(err) + } + defer st.Close() + } + + // Check model permissions if the user isn't a controller admin. + if !mm.isAdmin { + user, err := st.UserAccess(mm.apiUser, mm.state.ModelTag()) + if err != nil { + if errors.IsNotFound(err) { + return nil, errors.Trace(common.ErrPerm) + } + // Something weird went on. + return nil, errors.Trace(err) + } + if user.Access != description.AdminAccess { + return nil, errors.Trace(common.ErrPerm) + } + } + + bytes, err := migration.ExportModel(st) + if err != nil { + return nil, errors.Trace(err) + } + // Now read it back into a map. + var asMap map[string]interface{} + err = yaml.Unmarshal(bytes, &asMap) + if err != nil { + return nil, errors.Trace(err) + } + // In order to serialize the map through JSON, we need to make sure + // that all the embedded maps are map[string]interface{}, not + // map[interface{}]interface{} which is what YAML gives by default. + out, err := utils.ConformYAML(asMap) + if err != nil { + return nil, errors.Trace(err) + } + return out.(map[string]interface{}), nil +} + +// DumpModels will export the models into the database agnostic +// representation. The user needs to either be a controller admin, or have +// admin privileges on the model itself. +func (mm *ModelManagerAPI) DumpModels(args params.Entities) params.MapResults { + results := params.MapResults{ + Results: make([]params.MapResult, len(args.Entities)), + } + for i, entity := range args.Entities { + dumped, err := mm.dumpModel(entity) + if err != nil { + results.Results[i].Error = common.ServerError(err) + continue + } + results.Results[i].Result = dumped + } + return results +} + // ListModels returns the models that the specified user // has access to in the current server. Only that controller owner // can list models for any user (at this stage). Other users @@ -393,13 +506,14 @@ authorizedOwner := m.authCheck(owner) == nil for _, user := range users { - if !authorizedOwner && m.authCheck(user.UserTag()) != nil { + if !authorizedOwner && m.authCheck(user.UserTag) != nil { // The authenticated user is neither the owner // nor administrator, nor the model user, so // has no business knowing about the model user. continue } - userInfo, err := common.ModelUserInfo(user) + + userInfo, err := common.ModelUserInfo(user, st) if err != nil { return params.ModelInfo{}, errors.Trace(err) } @@ -449,17 +563,17 @@ return result, nil } -// resolveStateAccess returns the state representation of the logical model +// resolveDescriptionAccess returns the state representation of the logical model // access type. -func resolveStateAccess(access permission.ModelAccess) (state.Access, error) { - var fail state.Access +func resolveDescriptionAccess(access permission.ModelAccess) (description.Access, error) { + var fail description.Access switch access { case permission.ModelAdminAccess: - return state.AdminAccess, nil + return description.AdminAccess, nil case permission.ModelReadAccess: - return state.ReadAccess, nil + return description.ReadAccess, nil case permission.ModelWriteAccess: - return state.WriteAccess, nil + return description.WriteAccess, nil } logger.Errorf("invalid access permission: %+v", access) return fail, errors.Errorf("invalid access permission") @@ -477,7 +591,7 @@ // Get the current user's ModelUser for the Model to see if the user has // permission to grant or revoke permissions on the model. - currentUser, err := st.ModelUser(userTag) + currentUser, err := st.UserAccess(userTag, st.ModelTag()) if err != nil { if errors.IsNotFound(err) { // No, this user doesn't have permission. @@ -485,7 +599,7 @@ } return errors.Annotate(err, "could not retrieve user") } - if !currentUser.IsAdmin() { + if currentUser.Access != description.AdminAccess { return common.ErrPerm } return nil @@ -504,16 +618,20 @@ return errors.Trace(err) } - stateAccess, err := resolveStateAccess(access) + descriptionAccess, err := resolveDescriptionAccess(access) if err != nil { return errors.Annotate(err, "could not resolve model access") } + if descriptionAccess == description.UndefinedAccess { + return errors.NotValidf("changing model access to %q", description.UndefinedAccess) + } + switch action { case params.GrantModelAccess: - _, err = st.AddModelUser(state.ModelUserSpec{User: targetUserTag, CreatedBy: apiUser, Access: stateAccess}) + _, err = st.AddModelUser(state.UserAccessSpec{User: targetUserTag, CreatedBy: apiUser, Access: descriptionAccess}) if errors.IsAlreadyExists(err) { - modelUser, err := st.ModelUser(targetUserTag) + modelUser, err := st.UserAccess(targetUserTag, st.ModelTag()) if errors.IsNotFound(err) { // Conflicts with prior check, must be inconsistent state. err = txn.ErrExcessiveContention @@ -523,43 +641,41 @@ } // Only set access if greater access is being granted. - if modelUser.IsGreaterAccess(stateAccess) { - err = modelUser.SetAccess(stateAccess) - if err != nil { - return errors.Annotate(err, "could not set model access for user") - } - } else { - return errors.Errorf("user already has %q access or greater", stateAccess) + if modelUser.Access.EqualOrGreaterModelAccessThan(descriptionAccess) { + return errors.Errorf("user already has %q access or greater", descriptionAccess) + } + if _, err = st.SetUserAccess(modelUser.UserTag, modelUser.Object, descriptionAccess); err != nil { + return errors.Annotate(err, "could not set model access for user") } return nil } return errors.Annotate(err, "could not grant model access") case params.RevokeModelAccess: - switch stateAccess { - case state.ReadAccess: + switch descriptionAccess { + case description.ReadAccess: // Revoking read access removes all access. - err := st.RemoveModelUser(targetUserTag) + err := st.RemoveUserAccess(targetUserTag, st.ModelTag()) return errors.Annotate(err, "could not revoke model access") - case state.WriteAccess: + case description.WriteAccess: // Revoking write access sets read-only. - modelUser, err := st.ModelUser(targetUserTag) + modelUser, err := st.UserAccess(targetUserTag, st.ModelTag()) if err != nil { return errors.Annotate(err, "could not look up model access for user") } - err = modelUser.SetAccess(state.ReadAccess) + _, err = st.SetUserAccess(modelUser.UserTag, modelUser.Object, description.ReadAccess) return errors.Annotate(err, "could not set model access to read-only") - case state.AdminAccess: + case description.AdminAccess: // Revoking admin access sets read-write. - modelUser, err := st.ModelUser(targetUserTag) + modelUser, err := st.UserAccess(targetUserTag, st.ModelTag()) if err != nil { return errors.Annotate(err, "could not look up model access for user") } - err = modelUser.SetAccess(state.WriteAccess) + _, err = st.SetUserAccess(modelUser.UserTag, modelUser.Object, description.WriteAccess) return errors.Annotate(err, "could not set model access to read-write") default: - return errors.Errorf("don't know how to revoke %q access", stateAccess) + return errors.Errorf("don't know how to revoke %q access", descriptionAccess) } default: @@ -568,7 +684,7 @@ } // FromModelAccessParam returns the logical model access type from the API wireformat type. -func FromModelAccessParam(paramAccess params.ModelAccessPermission) (permission.ModelAccess, error) { +func FromModelAccessParam(paramAccess params.UserAccessPermission) (permission.ModelAccess, error) { var fail permission.ModelAccess switch paramAccess { case params.ModelReadAccess: diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/modelmanager/modelmanager_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/modelmanager/modelmanager_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/modelmanager/modelmanager_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/modelmanager/modelmanager_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,6 +5,7 @@ import ( "regexp" + "runtime" "time" "github.com/juju/errors" @@ -18,14 +19,15 @@ "github.com/juju/juju/apiserver/params" apiservertesting "github.com/juju/juju/apiserver/testing" "github.com/juju/juju/cloud" + "github.com/juju/juju/core/description" "github.com/juju/juju/environs" "github.com/juju/juju/environs/config" jujutesting "github.com/juju/juju/juju/testing" + "github.com/juju/juju/state/stateenvirons" "github.com/juju/juju/status" jujuversion "github.com/juju/juju/version" // Register the providers for the field check test "github.com/juju/juju/apiserver/common" - "github.com/juju/juju/controller" _ "github.com/juju/juju/provider/azure" "github.com/juju/juju/provider/dummy" _ "github.com/juju/juju/provider/ec2" @@ -62,6 +64,10 @@ cloud: cloud.Cloud{ Type: "dummy", AuthTypes: []cloud.AuthType{cloud.EmptyAuthType}, + Regions: []cloud.Region{ + {Name: "some-region"}, + {Name: "qux"}, + }, }, controllerModel: &mockModel{ owner: names.NewUserTag("admin@local"), @@ -73,15 +79,16 @@ }, users: []*mockModelUser{{ userName: "admin", - access: state.AdminAccess, + access: description.AdminAccess, }, { userName: "otheruser", - access: state.AdminAccess, + access: description.AdminAccess, }}, }, model: &mockModel{ owner: names.NewUserTag("admin@local"), life: state.Alive, + tag: coretesting.ModelTag, cfg: cfg, status: status.StatusInfo{ Status: status.StatusAvailable, @@ -89,10 +96,10 @@ }, users: []*mockModelUser{{ userName: "admin", - access: state.AdminAccess, + access: description.AdminAccess, }, { userName: "otheruser", - access: state.AdminAccess, + access: description.AdminAccess, }}, }, creds: map[string]cloud.Credential{ @@ -102,7 +109,7 @@ s.authoriser = apiservertesting.FakeAuthorizer{ Tag: names.NewUserTag("admin@local"), } - api, err := modelmanager.NewModelManagerAPI(&s.st, s.authoriser) + api, err := modelmanager.NewModelManagerAPI(&s.st, nil, s.authoriser) c.Assert(err, jc.ErrorIsNil) s.api = api } @@ -123,12 +130,16 @@ "IsControllerAdministrator", "ModelUUID", "ControllerModel", + "Cloud", "CloudCredentials", "ControllerConfig", + "ComposeNewModelConfig", "NewModel", "ForModel", "Model", "ControllerConfig", + "LastModelConnection", + "LastModelConnection", "Close", // close new model's state "Close", // close controller model's state ) @@ -136,7 +147,7 @@ // We cannot predict the UUID, because it's generated, // so we just extract it and ensure that it's not the // same as the controller UUID. - newModelArgs := s.st.Calls()[5].Args[0].(state.ModelArgs) + newModelArgs := s.st.Calls()[7].Args[0].(state.ModelArgs) uuid := newModelArgs.Config.UUID() c.Assert(uuid, gc.Not(gc.Equals), s.st.controllerModel.cfg.UUID()) @@ -149,12 +160,12 @@ "controller": false, "broken": "", "secret": "pork", + "something": "value", }) c.Assert(err, jc.ErrorIsNil) - // TODO(wallyworld) - we need to separate controller and model schemas - // Remove any remaining controller attributes from the env config. - cfg, err = cfg.Remove(controller.ControllerOnlyConfigAttributes) - c.Assert(err, jc.ErrorIsNil) + + c.Assert(newModelArgs.StorageProviderRegistry, gc.NotNil) + newModelArgs.StorageProviderRegistry = nil c.Assert(newModelArgs, jc.DeepEquals, state.ModelArgs{ Owner: names.NewUserTag("admin@local"), @@ -173,7 +184,7 @@ _, err := s.api.CreateModel(args) c.Assert(err, jc.ErrorIsNil) - newModelArgs := s.st.Calls()[5].Args[0].(state.ModelArgs) + newModelArgs := s.st.Calls()[7].Args[0].(state.ModelArgs) c.Assert(newModelArgs.CloudRegion, gc.Equals, "some-region") } @@ -194,7 +205,7 @@ _, err := s.api.CreateModel(args) c.Assert(err, jc.ErrorIsNil) - newModelArgs := s.st.Calls()[5].Args[0].(state.ModelArgs) + newModelArgs := s.st.Calls()[7].Args[0].(state.ModelArgs) c.Assert(newModelArgs.CloudCredential, gc.Equals, "some-credential") } @@ -206,7 +217,7 @@ _, err := s.api.CreateModel(args) c.Assert(err, jc.ErrorIsNil) - newModelArgs := s.st.Calls()[5].Args[0].(state.ModelArgs) + newModelArgs := s.st.Calls()[6].Args[0].(state.ModelArgs) c.Assert(newModelArgs.CloudCredential, gc.Equals, "") } @@ -230,6 +241,70 @@ c.Assert(err, gc.ErrorMatches, `no such credential "bar"`) } +func (s *modelManagerSuite) TestDumpModel(c *gc.C) { + results := s.api.DumpModels(params.Entities{[]params.Entity{{ + Tag: "bad-tag", + }, { + Tag: "application-foo", + }, { + Tag: s.st.ModelTag().String(), + }}}) + + c.Assert(results.Results, gc.HasLen, 3) + bad, notApp, good := results.Results[0], results.Results[1], results.Results[2] + c.Check(bad.Result, gc.IsNil) + c.Check(bad.Error.Message, gc.Equals, `"bad-tag" is not a valid tag`) + + c.Check(notApp.Result, gc.IsNil) + c.Check(notApp.Error.Message, gc.Equals, `"application-foo" is not a valid model tag`) + + c.Check(good.Error, gc.IsNil) + c.Check(good.Result, jc.DeepEquals, map[string]interface{}{ + "model-uuid": "deadbeef-0bad-400d-8000-4b1d0d06f00d", + }) +} + +func (s *modelManagerSuite) TestDumpModelMissingModel(c *gc.C) { + s.st.SetErrors(errors.NotFoundf("boom")) + tag := names.NewModelTag("deadbeef-0bad-400d-8000-4b1d0d06f000") + models := params.Entities{[]params.Entity{{Tag: tag.String()}}} + results := s.api.DumpModels(models) + + calls := s.st.Calls() + c.Logf("%#v", calls) + lastCall := calls[len(calls)-1] + c.Check(lastCall.FuncName, gc.Equals, "ForModel") + + c.Assert(results.Results, gc.HasLen, 1) + result := results.Results[0] + c.Assert(result.Result, gc.IsNil) + c.Assert(result.Error, gc.NotNil) + c.Check(result.Error.Code, gc.Equals, `not found`) + c.Check(result.Error.Message, gc.Equals, `id not found`) +} + +func (s *modelManagerSuite) TestDumpModelMissingUser(c *gc.C) { + s.st.SetErrors(nil, errors.New("boom")) + + authoriser := apiservertesting.FakeAuthorizer{ + Tag: names.NewUserTag("other@local"), + } + api, err := modelmanager.NewModelManagerAPI(&s.st, nil, authoriser) + c.Assert(err, jc.ErrorIsNil) + + models := params.Entities{[]params.Entity{{Tag: s.st.ModelTag().String()}}} + results := api.DumpModels(models) + + calls := s.st.Calls() + lastCall := calls[len(calls)-1] + c.Check(lastCall.FuncName, gc.Equals, "ModelUser") + + result := results.Results[0] + c.Assert(result.Result, gc.IsNil) + c.Assert(result.Error, gc.NotNil) + c.Check(result.Error.Message, gc.Equals, `boom`) +} + // modelManagerStateSuite contains end-to-end tests. // Prefer adding tests to modelManagerSuite above. type modelManagerStateSuite struct { @@ -240,6 +315,14 @@ var _ = gc.Suite(&modelManagerStateSuite{}) +func (s *modelManagerStateSuite) SetUpSuite(c *gc.C) { + // TODO(anastasiamac 2016-07-19): Fix this on windows + if runtime.GOOS != "linux" { + c.Skip("bug 1603585: Skipping this on windows for now") + } + s.JujuConnSuite.SetUpSuite(c) +} + func (s *modelManagerStateSuite) SetUpTest(c *gc.C) { s.JujuConnSuite.SetUpTest(c) s.authoriser = apiservertesting.FakeAuthorizer{ @@ -251,7 +334,9 @@ func (s *modelManagerStateSuite) setAPIUser(c *gc.C, user names.UserTag) { s.authoriser.Tag = user modelmanager, err := modelmanager.NewModelManagerAPI( - common.NewModelManagerBackend(s.State), s.authoriser, + common.NewModelManagerBackend(s.State), + stateenvirons.EnvironConfigGetter{s.State}, + s.authoriser, ) c.Assert(err, jc.ErrorIsNil) s.modelmanager = modelmanager @@ -261,7 +346,7 @@ anAuthoriser := s.authoriser anAuthoriser.Tag = names.NewUserTag("external@remote") endPoint, err := modelmanager.NewModelManagerAPI( - common.NewModelManagerBackend(s.State), anAuthoriser, + common.NewModelManagerBackend(s.State), nil, anAuthoriser, ) c.Assert(err, jc.ErrorIsNil) c.Assert(endPoint, gc.NotNil) @@ -271,7 +356,7 @@ anAuthoriser := s.authoriser anAuthoriser.Tag = names.NewUnitTag("mysql/0") endPoint, err := modelmanager.NewModelManagerAPI( - common.NewModelManagerBackend(s.State), anAuthoriser, + common.NewModelManagerBackend(s.State), nil, anAuthoriser, ) c.Assert(endPoint, gc.IsNil) c.Assert(err, gc.ErrorMatches, "permission denied") @@ -320,7 +405,7 @@ newModel, err := newState.Model() c.Assert(err, jc.ErrorIsNil) c.Assert(newModel.Owner(), gc.Equals, owner) - _, err = newState.ModelUser(owner) + _, err = newState.UserAccess(owner, newState.ModelTag()) c.Assert(err, jc.ErrorIsNil) } @@ -345,7 +430,7 @@ args.Config["controller"] = "maybe" _, err := s.modelmanager.CreateModel(args) c.Assert(err, gc.ErrorMatches, - "failed to create config: provider validation failed: controller: expected bool, got string\\(\"maybe\"\\)", + "failed to create config: provider config preparation failed: controller: expected bool, got string\\(\"maybe\"\\)", ) } @@ -499,7 +584,7 @@ defer st.Close() s.modelmanager, err = modelmanager.NewModelManagerAPI( - common.NewModelManagerBackend(st), s.authoriser, + common.NewModelManagerBackend(st), nil, s.authoriser, ) c.Assert(err, jc.ErrorIsNil) @@ -523,7 +608,7 @@ defer st.Close() s.modelmanager, err = modelmanager.NewModelManagerAPI( - common.NewModelManagerBackend(st), s.authoriser, + common.NewModelManagerBackend(st), nil, s.authoriser, ) c.Assert(err, jc.ErrorIsNil) @@ -548,7 +633,7 @@ defer st.Close() s.modelmanager, err = modelmanager.NewModelManagerAPI( - common.NewModelManagerBackend(st), s.authoriser, + common.NewModelManagerBackend(st), nil, s.authoriser, ) c.Assert(err, jc.ErrorIsNil) @@ -563,7 +648,7 @@ c.Assert(model.Life(), gc.Equals, state.Alive) } -func (s *modelManagerStateSuite) modifyAccess(c *gc.C, user names.UserTag, action params.ModelAction, access params.ModelAccessPermission, model names.ModelTag) error { +func (s *modelManagerStateSuite) modifyAccess(c *gc.C, user names.UserTag, action params.ModelAction, access params.UserAccessPermission, model names.ModelTag) error { args := params.ModifyModelAccessRequest{ Changes: []params.ModifyModelAccess{{ UserTag: user.String(), @@ -576,11 +661,11 @@ return result.OneError() } -func (s *modelManagerStateSuite) grant(c *gc.C, user names.UserTag, access params.ModelAccessPermission, model names.ModelTag) error { +func (s *modelManagerStateSuite) grant(c *gc.C, user names.UserTag, access params.UserAccessPermission, model names.ModelTag) error { return s.modifyAccess(c, user, params.GrantModelAccess, access, model) } -func (s *modelManagerStateSuite) revoke(c *gc.C, user names.UserTag, access params.ModelAccessPermission, model names.ModelTag) error { +func (s *modelManagerStateSuite) revoke(c *gc.C, user names.UserTag, access params.UserAccessPermission, model names.ModelTag) error { return s.modifyAccess(c, user, params.RevokeModelAccess, access, model) } @@ -599,31 +684,31 @@ s.setAPIUser(c, s.AdminUserTag(c)) user := s.Factory.MakeModelUser(c, nil) model := names.NewModelTag("17e4bd2d-3e08-4f3d-b945-087be7ebdce4") - err := s.grant(c, user.UserTag(), params.ModelReadAccess, model) + err := s.grant(c, user.UserTag, params.ModelReadAccess, model) expectedErr := `.*model not found` c.Assert(err, gc.ErrorMatches, expectedErr) } func (s *modelManagerStateSuite) TestRevokeAdminLeavesReadAccess(c *gc.C) { s.setAPIUser(c, s.AdminUserTag(c)) - user := s.Factory.MakeModelUser(c, &factory.ModelUserParams{Access: state.WriteAccess}) + user := s.Factory.MakeModelUser(c, &factory.ModelUserParams{Access: description.WriteAccess}) - err := s.revoke(c, user.UserTag(), params.ModelWriteAccess, user.ModelTag()) + err := s.revoke(c, user.UserTag, params.ModelWriteAccess, user.Object.(names.ModelTag)) c.Assert(err, gc.IsNil) - modelUser, err := s.State.ModelUser(user.UserTag()) + modelUser, err := s.State.UserAccess(user.UserTag, user.Object) c.Assert(err, jc.ErrorIsNil) - c.Assert(modelUser.IsReadOnly(), jc.IsTrue) + c.Assert(modelUser.Access, gc.Equals, description.ReadAccess) } func (s *modelManagerStateSuite) TestRevokeReadRemovesModelUser(c *gc.C) { s.setAPIUser(c, s.AdminUserTag(c)) user := s.Factory.MakeModelUser(c, nil) - err := s.revoke(c, user.UserTag(), params.ModelReadAccess, user.ModelTag()) + err := s.revoke(c, user.UserTag, params.ModelReadAccess, user.Object.(names.ModelTag)) c.Assert(err, gc.IsNil) - _, err = s.State.ModelUser(user.UserTag()) + _, err = s.State.UserAccess(user.UserTag, user.Object) c.Assert(errors.IsNotFound(err), jc.IsTrue) } @@ -636,7 +721,7 @@ err := s.revoke(c, user, params.ModelReadAccess, st.ModelTag()) c.Assert(err, gc.ErrorMatches, `could not revoke model access: model user "bob@local" does not exist`) - _, err = st.ModelUser(user) + _, err = st.UserAccess(user, st.ModelTag()) c.Assert(errors.IsNotFound(err), jc.IsTrue) } @@ -653,10 +738,10 @@ c.Assert(err, gc.ErrorMatches, `user already has "read" access or greater`) } -func (s *modelManagerStateSuite) assertNewUser(c *gc.C, modelUser *state.ModelUser, userTag, creatorTag names.UserTag) { - c.Assert(modelUser.UserTag(), gc.Equals, userTag) - c.Assert(modelUser.CreatedBy(), gc.Equals, creatorTag.Canonical()) - _, err := modelUser.LastConnection() +func (s *modelManagerStateSuite) assertNewUser(c *gc.C, modelUser description.UserAccess, userTag, creatorTag names.UserTag) { + c.Assert(modelUser.UserTag, gc.Equals, userTag) + c.Assert(modelUser.CreatedBy, gc.Equals, creatorTag) + _, err := s.State.LastModelConnection(modelUser.UserTag) c.Assert(err, jc.Satisfies, state.IsNeverConnectedError) } @@ -670,10 +755,10 @@ err := s.grant(c, user.UserTag(), params.ModelReadAccess, st.ModelTag()) c.Assert(err, jc.ErrorIsNil) - modelUser, err := st.ModelUser(user.UserTag()) + modelUser, err := st.UserAccess(user.UserTag(), st.ModelTag()) c.Assert(err, jc.ErrorIsNil) s.assertNewUser(c, modelUser, user.UserTag(), apiUser) - c.Assert(modelUser.IsReadOnly(), jc.IsTrue) + c.Assert(modelUser.Access, gc.Equals, description.ReadAccess) } func (s *modelManagerStateSuite) TestGrantModelAddRemoteUser(c *gc.C) { @@ -686,11 +771,11 @@ err := s.grant(c, userTag, params.ModelReadAccess, st.ModelTag()) c.Assert(err, jc.ErrorIsNil) - modelUser, err := st.ModelUser(userTag) + modelUser, err := st.UserAccess(userTag, st.ModelTag()) c.Assert(err, jc.ErrorIsNil) s.assertNewUser(c, modelUser, userTag, apiUser) - c.Assert(modelUser.IsReadOnly(), jc.IsTrue) + c.Assert(modelUser.Access, gc.Equals, description.ReadAccess) } func (s *modelManagerStateSuite) TestGrantModelAddAdminUser(c *gc.C) { @@ -702,10 +787,10 @@ err := s.grant(c, user.UserTag(), params.ModelWriteAccess, st.ModelTag()) - modelUser, err := st.ModelUser(user.UserTag()) + modelUser, err := st.UserAccess(user.UserTag(), st.ModelTag()) c.Assert(err, jc.ErrorIsNil) s.assertNewUser(c, modelUser, user.UserTag(), apiUser) - c.Assert(modelUser.IsReadOnly(), jc.IsFalse) + c.Assert(modelUser.Access, gc.Equals, description.WriteAccess) } func (s *modelManagerStateSuite) TestGrantModelIncreaseAccess(c *gc.C) { @@ -713,14 +798,14 @@ st := s.Factory.MakeModel(c, nil) defer st.Close() stFactory := factory.NewFactory(st) - user := stFactory.MakeModelUser(c, &factory.ModelUserParams{Access: state.ReadAccess}) + user := stFactory.MakeModelUser(c, &factory.ModelUserParams{Access: description.ReadAccess}) - err := s.grant(c, user.UserTag(), params.ModelWriteAccess, st.ModelTag()) + err := s.grant(c, user.UserTag, params.ModelWriteAccess, st.ModelTag()) c.Assert(err, jc.ErrorIsNil) - modelUser, err := st.ModelUser(user.UserTag()) + modelUser, err := st.UserAccess(user.UserTag, st.ModelTag()) c.Assert(err, jc.ErrorIsNil) - c.Assert(modelUser.IsReadWrite(), jc.IsTrue) + c.Assert(modelUser.Access, gc.Equals, description.WriteAccess) } func (s *modelManagerStateSuite) TestGrantToModelNoAccess(c *gc.C) { @@ -743,7 +828,7 @@ defer st.Close() stFactory := factory.NewFactory(st) stFactory.MakeModelUser(c, &factory.ModelUserParams{ - User: apiUser.Canonical(), Access: state.ReadAccess}) + User: apiUser.Canonical(), Access: description.ReadAccess}) other := names.NewUserTag("other@remote") err := s.grant(c, other, params.ModelReadAccess, st.ModelTag()) @@ -758,16 +843,16 @@ defer st.Close() stFactory := factory.NewFactory(st) stFactory.MakeModelUser(c, &factory.ModelUserParams{ - User: apiUser.Canonical(), Access: state.AdminAccess}) + User: apiUser.Canonical(), Access: description.AdminAccess}) other := names.NewUserTag("other@remote") err := s.grant(c, other, params.ModelReadAccess, st.ModelTag()) c.Assert(err, jc.ErrorIsNil) - modelUser, err := st.ModelUser(other) + modelUser, err := st.UserAccess(other, st.ModelTag()) c.Assert(err, jc.ErrorIsNil) s.assertNewUser(c, modelUser, other, apiUser) - c.Assert(modelUser.IsReadOnly(), jc.IsTrue) + c.Assert(modelUser.Access, gc.Equals, description.ReadAccess) } func (s *modelManagerStateSuite) TestGrantModelInvalidUserTag(c *gc.C) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/network.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/network.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/network.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/network.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,56 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package apiserver - -import ( - "net" - "time" - - "github.com/juju/errors" - "github.com/juju/utils" -) - -var ( - // The defaults below are best suited to retries associated - // with disk I/O timeouts, eg database operations. - // Use the NetworkOperationWithRetries() variant to explicitly - // use retry values better suited to different scenarios. - - // defaultNetworkOperationRetryDelay is the default time - // to wait between operation retries. - defaultNetworkOperationRetryDelay = 30 * time.Second - - // defaultNetworkOperationAttempts is the default number - // of attempts before giving up. - defaultNetworkOperationAttempts = 10 -) - -// networkOperationWithDefaultRetries calls the supplied function and if it returns a -// network error which is temporary, will retry a number of times before giving up. -// A default attempt strategy is used. -func networkOperationWitDefaultRetries(networkOp func() error, description string) func() error { - attempt := utils.AttemptStrategy{ - Delay: defaultNetworkOperationRetryDelay, - Min: defaultNetworkOperationAttempts, - } - return networkOperationWithRetries(attempt, networkOp, description) -} - -// networkOperationWithRetries calls the supplied function and if it returns a -// network error which is temporary, will retry a number of times before giving up. -func networkOperationWithRetries(strategy utils.AttemptStrategy, networkOp func() error, description string) func() error { - return func() error { - for a := strategy.Start(); ; { - a.Next() - err := networkOp() - if !a.HasNext() || err == nil { - return errors.Trace(err) - } - if networkErr, ok := errors.Cause(err).(net.Error); !ok || !networkErr.Temporary() { - return errors.Trace(err) - } - logger.Debugf("%q error, will retry: %v", description, err) - } - } -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/network_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/network_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/network_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/network_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,102 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package apiserver - -import ( - "time" - - "github.com/juju/errors" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/testing" -) - -type networkSuite struct { - testing.BaseSuite -} - -var _ = gc.Suite(&networkSuite{}) - -func (s *networkSuite) TestOpSuccess(c *gc.C) { - isCalled := false - f := func() error { - isCalled = true - return nil - } - err := networkOperationWitDefaultRetries(f, "do it")() - c.Assert(err, jc.ErrorIsNil) - c.Assert(isCalled, jc.IsTrue) -} - -func (s *networkSuite) TestOpFailureNoRetry(c *gc.C) { - s.PatchValue(&defaultNetworkOperationRetryDelay, 1*time.Millisecond) - netErr := &netError{false} - callCount := 0 - f := func() error { - callCount++ - return netErr - } - err := networkOperationWitDefaultRetries(f, "do it")() - c.Assert(errors.Cause(err), gc.Equals, netErr) - c.Assert(callCount, gc.Equals, 1) -} - -func (s *networkSuite) TestOpFailureRetries(c *gc.C) { - s.PatchValue(&defaultNetworkOperationRetryDelay, 1*time.Millisecond) - netErr := &netError{true} - callCount := 0 - f := func() error { - callCount++ - return netErr - } - err := networkOperationWitDefaultRetries(f, "do it")() - c.Assert(errors.Cause(err), gc.Equals, netErr) - c.Assert(callCount, gc.Equals, 10) -} - -func (s *networkSuite) TestOpNestedFailureRetries(c *gc.C) { - s.PatchValue(&defaultNetworkOperationRetryDelay, 1*time.Millisecond) - netErr := &netError{true} - callCount := 0 - f := func() error { - callCount++ - return errors.Annotate(errors.Trace(netErr), "create a wrapped error") - } - err := networkOperationWitDefaultRetries(f, "do it")() - c.Assert(errors.Cause(err), gc.Equals, netErr) - c.Assert(callCount, gc.Equals, 10) -} - -func (s *networkSuite) TestOpSucceedsAfterRetries(c *gc.C) { - s.PatchValue(&defaultNetworkOperationRetryDelay, 1*time.Millisecond) - netErr := &netError{true} - callCount := 0 - f := func() error { - callCount++ - if callCount == 5 { - return nil - } - return netErr - } - err := networkOperationWitDefaultRetries(f, "do it")() - c.Assert(err, jc.ErrorIsNil) - c.Assert(callCount, gc.Equals, 5) -} - -type netError struct { - temporary bool -} - -func (e *netError) Error() string { - return "network error" -} - -func (e *netError) Temporary() bool { - return e.temporary -} - -func (e *netError) Timeout() bool { - return false -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/observer/audit.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/observer/audit.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/observer/audit.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/observer/audit.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,6 +9,7 @@ "github.com/juju/errors" "github.com/juju/version" + "gopkg.in/juju/names.v2" "github.com/juju/juju/audit" "github.com/juju/juju/rpc" @@ -58,12 +59,12 @@ } // Login implements Observer. -func (a *Audit) Login(tag string) { - a.state.authenticatedTag = tag +func (a *Audit) Login(entity names.Tag, _ names.ModelTag, _ bool, _ string) { + a.state.authenticatedTag = entity.String() } // Join implements Observer. -func (a *Audit) Join(req *http.Request) { +func (a *Audit) Join(req *http.Request, _ uint64) { a.state.remoteAddress = req.RemoteAddr } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/observer/fakeobserver/instance.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/observer/fakeobserver/instance.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/observer/fakeobserver/instance.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/observer/fakeobserver/instance.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,6 +7,8 @@ "net/http" "runtime" + "gopkg.in/juju/names.v2" + "strings" "github.com/juju/juju/rpc" @@ -19,8 +21,8 @@ } // Join implements Observer. -func (f *Instance) Join(req *http.Request) { - f.AddCall(funcName(), req) +func (f *Instance) Join(req *http.Request, connectionID uint64) { + f.AddCall(funcName(), req, connectionID) } // Leave implements Observer. @@ -29,8 +31,8 @@ } // Login implements Observer. -func (f *Instance) Login(entityName string) { - f.AddCall(funcName(), entityName) +func (f *Instance) Login(entity names.Tag, model names.ModelTag, fromController bool, userData string) { + f.AddCall(funcName(), entity, model, fromController, userData) } // RPCObserver implements Observer. diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/observer/observer.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/observer/observer.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/observer/observer.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/observer/observer.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,6 +8,7 @@ "sync" "github.com/juju/juju/rpc" + "gopkg.in/juju/names.v2" ) // Observer defines a type which will observe API server events as @@ -16,11 +17,11 @@ rpc.ObserverFactory // Login informs an Observer that an entity has logged in. - Login(string) + Login(entity names.Tag, model names.ModelTag, fromController bool, userData string) // Join is called when the connection to the API server's // WebSocket is opened. - Join(req *http.Request) + Join(req *http.Request, connectionID uint64) // Leave is called when the connection to the API server's // WebSocket is closed. @@ -75,8 +76,8 @@ // Join is called when the connection to the API server's WebSocket is // opened. -func (m *Multiplexer) Join(req *http.Request) { - mapConcurrent(func(o Observer) { o.Join(req) }, m.observers) +func (m *Multiplexer) Join(req *http.Request, connectionID uint64) { + mapConcurrent(func(o Observer) { o.Join(req, connectionID) }, m.observers) } // Leave implements Observer. @@ -85,8 +86,8 @@ } // Login implements Observer. -func (m *Multiplexer) Login(entityName string) { - mapConcurrent(func(o Observer) { o.Login(entityName) }, m.observers) +func (m *Multiplexer) Login(entity names.Tag, model names.ModelTag, fromController bool, userData string) { + mapConcurrent(func(o Observer) { o.Login(entity, model, fromController, userData) }, m.observers) } // RPCObserver implements Observer. It will create an diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/observer/observer_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/observer/observer_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/observer/observer_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/observer/observer_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,6 +8,7 @@ "github.com/juju/testing" gc "gopkg.in/check.v1" + "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/observer" "github.com/juju/juju/apiserver/observer/fakeobserver" @@ -42,10 +43,10 @@ o := observer.NewMultiplexer(observers[0], observers[1]) var req http.Request - o.Join(&req) + o.Join(&req, 1234) for _, f := range observers { - f.CheckCall(c, 0, "Join", &req) + f.CheckCall(c, 0, "Join", &req, uint64(1234)) } } @@ -84,10 +85,13 @@ } o := observer.NewMultiplexer(observers[0], observers[1]) - tag := "foo" - o.Login(tag) + entity := names.NewMachineTag("42") + model := names.NewModelTag("fake-uuid") + fromController := false + userData := "foo" + o.Login(entity, model, fromController, userData) for _, f := range observers { - f.CheckCall(c, 0, "Login", tag) + f.CheckCall(c, 0, "Login", entity, model, fromController, userData) } } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/observer/request_notifier.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/observer/request_notifier.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/observer/request_notifier.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/observer/request_notifier.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,6 +6,8 @@ "net/http" "time" + "gopkg.in/juju/names.v2" + "github.com/juju/loggo" "github.com/juju/utils/clock" @@ -19,7 +21,6 @@ clock clock.Clock logger loggo.Logger apiConnectionCount func() int64 - id int64 // state represents information that's built up as methods on this // type are called. We segregate this to ensure it's clear what @@ -27,6 +28,7 @@ // later. It's an anonymous struct so this doesn't leak outside // this type. state struct { + id uint64 websocketConnected time.Time tag string } @@ -44,26 +46,26 @@ } // NewRequestObserver returns a new RPCObserver. -func NewRequestObserver(ctx RequestObserverContext, id int64) *RequestObserver { +func NewRequestObserver(ctx RequestObserverContext) *RequestObserver { return &RequestObserver{ clock: ctx.Clock, logger: ctx.Logger, - id: id, } } // Login implements Observer. -func (n *RequestObserver) Login(tag string) { - n.state.tag = tag +func (n *RequestObserver) Login(entity names.Tag, _ names.ModelTag, _ bool, _ string) { + n.state.tag = entity.String() } // Join implements Observer. -func (n *RequestObserver) Join(req *http.Request) { +func (n *RequestObserver) Join(req *http.Request, connectionID uint64) { + n.state.id = connectionID n.state.websocketConnected = n.clock.Now() n.logger.Infof( "[%X] API connection from %s", - n.id, + n.state.id, req.RemoteAddr, ) } @@ -72,7 +74,7 @@ func (n *RequestObserver) Leave() { n.logger.Infof( "[%X] %s API connection terminated after %v", - n.id, + n.state.id, n.state.tag, time.Since(n.state.websocketConnected), ) @@ -83,7 +85,7 @@ return &rpcObserver{ clock: n.clock, logger: n.logger, - id: n.id, + id: n.state.id, tag: n.state.tag, } } @@ -92,7 +94,7 @@ type rpcObserver struct { clock clock.Clock logger loggo.Logger - id int64 + id uint64 tag string requestStart time.Time } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/package_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/package_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/package_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/package_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,11 +4,11 @@ package apiserver_test import ( - stdtesting "testing" + "testing" coretesting "github.com/juju/juju/testing" ) -func TestPackage(t *stdtesting.T) { +func TestPackage(t *testing.T) { coretesting.MgoTestPackage(t) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/params/apierror.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/params/apierror.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/params/apierror.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/params/apierror.go 2016-08-16 08:56:25.000000000 +0000 @@ -58,6 +58,8 @@ // The Code constants hold error codes for some kinds of error. const ( CodeNotFound = "not found" + CodeUserNotFound = "user not found" + CodeModelNotFound = "model not found" CodeUnauthorized = "unauthorized access" CodeLoginExpired = "login expired" CodeCannotEnterScope = "cannot enter scope" @@ -86,6 +88,7 @@ CodeForbidden = "forbidden" CodeDischargeRequired = "macaroon discharge required" CodeRedirect = "redirection required" + CodeRetry = "retry" ) // ErrCode returns the error code associated with @@ -111,6 +114,14 @@ return ErrCode(err) == CodeNotFound } +func IsCodeUserNotFound(err error) bool { + return ErrCode(err) == CodeUserNotFound +} + +func IsCodeModelNotFound(err error) bool { + return ErrCode(err) == CodeModelNotFound +} + func IsCodeUnauthorized(err error) bool { return ErrCode(err) == CodeUnauthorized } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/params/cloud.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/params/cloud.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/params/cloud.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/params/cloud.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,7 +8,7 @@ Type string `json:"type"` AuthTypes []string `json:"auth-types,omitempty"` Endpoint string `json:"endpoint,omitempty"` - StorageEndpoint string `json:"endpoint,omitempty"` + StorageEndpoint string `json:"storage-endpoint,omitempty"` Regions []CloudRegion `json:"regions,omitempty"` } @@ -16,7 +16,7 @@ type CloudRegion struct { Name string `json:"name"` Endpoint string `json:"endpoint,omitempty"` - StorageEndpoint string `json:"endpoint,omitempty"` + StorageEndpoint string `json:"storage-endpoint,omitempty"` } // CloudResult contains a cloud definition or an error. @@ -83,10 +83,31 @@ // CloudDefaultsResult contains a CloudDefaults or an error. type CloudDefaultsResult struct { Result *CloudDefaults `json:"result,omitempty"` - Error *Error `json:"error"` + Error *Error `json:"error,omitempty"` } // CloudDefaultsResults contains a set of CloudDefaultsResults. type CloudDefaultsResults struct { Results []CloudDefaultsResult `json:"results,omitempty"` } + +// CloudSpec holds a cloud specification. +type CloudSpec struct { + Type string `json:"type"` + Name string `json:"name"` + Region string `json:"region,omitempty"` + Endpoint string `json:"endpoint,omitempty"` + StorageEndpoint string `json:"storage-endpoint,omitempty"` + Credential *CloudCredential `json:"credential,omitempty"` +} + +// CloudSpecResult contains a CloudSpec or an error. +type CloudSpecResult struct { + Result *CloudSpec `json:"result,omitempty"` + Error *Error `json:"error,omitempty"` +} + +// CloudSpecResults contains a set of CloudSpecResults. +type CloudSpecResults struct { + Results []CloudSpecResult `json:"results,omitempty"` +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/params/internal.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/params/internal.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/params/internal.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/params/internal.go 2016-08-16 08:56:25.000000000 +0000 @@ -75,6 +75,18 @@ Results []StringResult `json:"results"` } +// MapResult holds a generic map or an error. +type MapResult struct { + Result map[string]interface{} `json:"result"` + Error *Error `json:"error,omitempty"` +} + +// MapResults holds the bulk operation result of an API call +// that returns a map or an error. +type MapResults struct { + Results []MapResult `json:"results"` +} + // ModelResult holds the result of an API call returning a name and UUID // for a model. type ModelResult struct { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/params/migration.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/params/migration.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/params/migration.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/params/migration.go 2016-08-16 08:56:25.000000000 +0000 @@ -3,6 +3,8 @@ package params +import "time" + // InitiateModelMigrationArgs holds the details required to start one // or more model migrations. type InitiateModelMigrationArgs struct { @@ -35,9 +37,9 @@ // InitiateModelMigrationResult is used to return the result of one // model migration initiation attempt. type InitiateModelMigrationResult struct { - ModelTag string `json:"model-tag"` - Error *Error `json:"error,omitempty"` - Id string `json:"id"` // the ID for the migration attempt + ModelTag string `json:"model-tag"` + Error *Error `json:"error,omitempty"` + MigrationId string `json:"migration-id"` } // SetMigrationPhaseArgs provides a migration phase to the @@ -46,9 +48,30 @@ Phase string `json:"phase"` } -// SerializedModel wraps a buffer contain a serialised Juju model. +// SetMigrationStatusMessageArgs provides a migration status message +// to the migrationmaster.SetStatusMessage API method. +type SetMigrationStatusMessageArgs struct { + Message string `json:"message"` +} + +// SerializedModel wraps a buffer contain a serialised Juju model. It +// also contains lists of the charms and tools used in the model. type SerializedModel struct { - Bytes []byte `json:"bytes"` + Bytes []byte `json:"bytes"` + Charms []string `json:"charms"` + Tools []SerializedModelTools `json:"tools"` +} + +// SerializedModelTools holds the version and URI for a given tools +// version. +type SerializedModelTools struct { + Version string `json:"version"` + + // URI holds the URI were a client can download the tools + // (e.g. "/tools/1.2.3-xenial-amd64"). It will need to prefixed + // with the API server scheme, address and model prefix before it + // can be used. + URI string `json:"uri"` } // ModelArgs wraps a simple model tag. @@ -56,10 +79,21 @@ ModelTag string `json:"model-tag"` } +// MasterMigrationStatus is used to report the current status of a +// model migration for the migrationmaster. It includes authentication +// details for the remote controller. +type MasterMigrationStatus struct { + Spec ModelMigrationSpec `json:"spec"` + MigrationId string `json:"migration-id"` + Phase string `json:"phase"` + PhaseChangedTime time.Time `json:"phase-changed-time"` +} + // MigrationStatus reports the current status of a model migration. type MigrationStatus struct { - Attempt int `json:"attempt"` - Phase string `json:"phase"` + MigrationId string `json:"migration-id"` + Attempt int `json:"attempt"` + Phase string `json:"phase"` // TODO(mjs): I'm not convinced these Source fields will get used. SourceAPIAddrs []string `json:"source-api-addrs"` @@ -69,20 +103,57 @@ TargetCACert string `json:"target-ca-cert"` } -// FullMigrationStatus reports the current status of a model -// migration, including authentication details for the remote -// controller. -type FullMigrationStatus struct { - Spec ModelMigrationSpec `json:"spec"` - Attempt int `json:"attempt"` - Phase string `json:"phase"` +// PhasesResults holds the phase of one or more model migrations. +type PhaseResults struct { + Results []PhaseResult `json:"results"` } +// PhaseResult holds the phase of a single model migration, or an +// error if the phase could not be determined. type PhaseResult struct { - Phase string `json:"phase"` + Phase string `json:"phase,omitempty"` Error *Error `json:"error,omitempty"` } -type PhaseResults struct { - Results []PhaseResult `json:"results"` +// MinionReport holds the details of whether a migration minion +// succeeded or failed for a specific migration phase. +type MinionReport struct { + // MigrationId holds the id of the migration the agent is + // reporting about. + MigrationId string `json:"migration-id"` + + // Phase holds the phase of the migration the agent is + // reporting about. + Phase string `json:"phase"` + + // Success is true if the agent successfully completed its actions + // for the migration phase, false otherwise. + Success bool `json:"success"` +} + +// MinionReports holds the details of whether a migration minion +// succeeded or failed for a specific migration phase. +type MinionReports struct { + // MigrationId holds the id of the migration the reports related to. + MigrationId string `json:"migration-id"` + + // Phase holds the phase of the migration the reports related to. + Phase string `json:"phase"` + + // SuccessCount holds the number of agents which have successfully + // completed a given migration phase. + SuccessCount int `json:"success-count"` + + // UnknownCount holds the number of agents still to report for a + // given migration phase. + UnknownCount int `json:"unknown-count"` + + // UnknownSample holds the tags of a limited number of agents + // that are still to report for a given migration phase (for + // logging or showing in a user interface). + UnknownSample []string `json:"unknown-sample"` + + // Failed contains the tags of all agents which have reported a + // failed to complete a given migration phase. + Failed []string `json:"failed"` } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/params/model.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/params/model.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/params/model.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/params/model.go 2016-08-16 08:56:25.000000000 +0000 @@ -102,10 +102,10 @@ // model. Owners of a model can see this information for all users // who have access, so it should not include sensitive information. type ModelUserInfo struct { - UserName string `json:"user"` - DisplayName string `json:"display-name"` - LastConnection *time.Time `json:"last-connection"` - Access ModelAccessPermission `json:"access"` + UserName string `json:"user"` + DisplayName string `json:"display-name"` + LastConnection *time.Time `json:"last-connection"` + Access UserAccessPermission `json:"access"` } // ModelUserInfoResult holds the result of an ModelUserInfo call. @@ -125,10 +125,10 @@ } type ModifyModelAccess struct { - UserTag string `json:"user-tag"` - Action ModelAction `json:"action"` - Access ModelAccessPermission `json:"access"` - ModelTag string `json:"model-tag"` + UserTag string `json:"user-tag"` + Action ModelAction `json:"action"` + Access UserAccessPermission `json:"access"` + ModelTag string `json:"model-tag"` } // ModelAction is an action that can be performed on a model. @@ -140,13 +140,13 @@ RevokeModelAccess ModelAction = "revoke" ) -// ModelAccessPermission is the type of permission that a user has to access a +// UserAccessPermission is the type of permission that a user has to access a // model. -type ModelAccessPermission string +type UserAccessPermission string // Model access permissions that may be set on a user. const ( - ModelAdminAccess ModelAccessPermission = "admin" - ModelReadAccess ModelAccessPermission = "read" - ModelWriteAccess ModelAccessPermission = "write" + ModelAdminAccess UserAccessPermission = "admin" + ModelReadAccess UserAccessPermission = "read" + ModelWriteAccess UserAccessPermission = "write" ) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/params/params.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/params/params.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/params/params.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/params/params.go 2016-08-16 08:56:25.000000000 +0000 @@ -381,6 +381,7 @@ Credentials string `json:"credentials"` Nonce string `json:"nonce"` Macaroons []macaroon.Slice `json:"macaroons"` + UserData string `json:"user-data"` } // LoginRequestCompat holds credentials for identifying an entity to the Login v1 diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/params/status.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/params/status.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/params/status.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/params/status.go 2016-08-16 08:56:25.000000000 +0000 @@ -34,6 +34,7 @@ CloudRegion string `json:"region,omitempty"` Version string `json:"version"` AvailableVersion string `json:"available-version"` + Migration string `json:"migration,omitempty"` } // MachineStatus holds status info about a machine. diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/params/usermanager.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/params/usermanager.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/params/usermanager.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/params/usermanager.go 2016-08-16 08:56:25.000000000 +0000 @@ -54,7 +54,7 @@ Password string `json:"password,omitempty"` // ModelAccess is the permission that the user will have to access the models. - ModelAccess ModelAccessPermission `json:"model-access-permission,omitempty"` + ModelAccess UserAccessPermission `json:"model-access-permission,omitempty"` } // AddUserResults holds the results of the bulk AddUser API call. diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/pinger.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/pinger.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/pinger.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/pinger.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,7 +10,9 @@ "launchpad.net/tomb" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/state" + "github.com/juju/utils/clock" ) func init() { @@ -20,7 +22,7 @@ // NewPinger returns an object that can be pinged by calling its Ping method. // If this method is not called frequently enough, the connection will be // dropped. -func NewPinger(st *state.State, resources *common.Resources, authorizer common.Authorizer) (Pinger, error) { +func NewPinger(st *state.State, resources facade.Resources, authorizer facade.Authorizer) (Pinger, error) { pingTimeout, ok := resources.Get("pingTimeout").(*pingTimeout) if !ok { return nullPinger{}, nil @@ -40,19 +42,21 @@ type pingTimeout struct { tomb tomb.Tomb action func() + clock clock.Clock timeout time.Duration - reset chan time.Duration + reset chan struct{} } // newPingTimeout returns a new pingTimeout instance // that invokes the given action asynchronously if there // is more than the given timeout interval between calls // to its Ping method. -func newPingTimeout(action func(), timeout time.Duration) Pinger { +func newPingTimeout(action func(), clock clock.Clock, timeout time.Duration) Pinger { pt := &pingTimeout{ action: action, + clock: clock, timeout: timeout, - reset: make(chan time.Duration), + reset: make(chan struct{}), } go func() { defer pt.tomb.Done() @@ -66,7 +70,7 @@ func (pt *pingTimeout) Ping() { select { case <-pt.tomb.Dying(): - case pt.reset <- pt.timeout: + case pt.reset <- struct{}{}: } } @@ -79,18 +83,14 @@ // loop waits for a reset signal, otherwise it performs // the initially passed action. func (pt *pingTimeout) loop() error { - // TODO(fwereade): 2016-03-17 lp:1558657 - timer := time.NewTimer(pt.timeout) - defer timer.Stop() for { select { case <-pt.tomb.Dying(): - return nil - case <-timer.C: + return tomb.ErrDying + case <-pt.reset: + case <-pt.clock.After(pt.timeout): go pt.action() return errors.New("ping timeout") - case duration := <-pt.reset: - timer.Reset(duration) } } } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/pinger_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/pinger_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/pinger_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/pinger_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -59,7 +59,7 @@ func (s *pingerSuite) TestPing(c *gc.C) { tw := &loggo.TestWriter{} - c.Assert(loggo.RegisterWriter("ping-tester", tw, loggo.DEBUG), gc.IsNil) + c.Assert(loggo.RegisterWriter("ping-tester", tw), gc.IsNil) defer loggo.RemoveWriter("ping-tester") st, _ := s.OpenAPIAsNewMachine(c) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/provisioner/provisioner.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/provisioner/provisioner.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/provisioner/provisioner.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/provisioner/provisioner.go 2016-08-16 08:56:25.000000000 +0000 @@ -14,6 +14,7 @@ "github.com/juju/juju/apiserver/common" "github.com/juju/juju/apiserver/common/networkingcommon" "github.com/juju/juju/apiserver/common/storagecommon" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/constraints" "github.com/juju/juju/container" @@ -21,8 +22,11 @@ "github.com/juju/juju/instance" "github.com/juju/juju/network" "github.com/juju/juju/state" + "github.com/juju/juju/state/stateenvirons" "github.com/juju/juju/state/watcher" "github.com/juju/juju/status" + "github.com/juju/juju/storage" + "github.com/juju/juju/storage/poolmanager" ) var logger = loggo.GetLogger("juju.apiserver.provisioner") @@ -48,14 +52,17 @@ *common.ToolsFinder *common.ToolsGetter - st *state.State - resources *common.Resources - authorizer common.Authorizer - getAuthFunc common.GetAuthFunc + st *state.State + resources facade.Resources + authorizer facade.Authorizer + storageProviderRegistry storage.ProviderRegistry + storagePoolManager poolmanager.PoolManager + configGetter environs.EnvironConfigGetter + getAuthFunc common.GetAuthFunc } // NewProvisionerAPI creates a new server-side ProvisionerAPI facade. -func NewProvisionerAPI(st *state.State, resources *common.Resources, authorizer common.Authorizer) (*ProvisionerAPI, error) { +func NewProvisionerAPI(st *state.State, resources facade.Resources, authorizer facade.Authorizer) (*ProvisionerAPI, error) { if !authorizer.AuthMachineAgent() && !authorizer.AuthModelManager() { return nil, common.ErrPerm } @@ -91,30 +98,39 @@ getAuthOwner := func() (common.AuthFunc, error) { return authorizer.AuthOwner, nil } - env, err := st.Model() + model, err := st.Model() if err != nil { return nil, err } - urlGetter := common.NewToolsURLGetter(env.UUID(), st) + configGetter := stateenvirons.EnvironConfigGetter{st} + env, err := environs.GetEnviron(configGetter, environs.New) + if err != nil { + return nil, err + } + urlGetter := common.NewToolsURLGetter(model.UUID(), st) + storageProviderRegistry := stateenvirons.NewStorageProviderRegistry(env) return &ProvisionerAPI{ - Remover: common.NewRemover(st, false, getAuthFunc), - StatusSetter: common.NewStatusSetter(st, getAuthFunc), - StatusGetter: common.NewStatusGetter(st, getAuthFunc), - DeadEnsurer: common.NewDeadEnsurer(st, getAuthFunc), - PasswordChanger: common.NewPasswordChanger(st, getAuthFunc), - LifeGetter: common.NewLifeGetter(st, getAuthFunc), - StateAddresser: common.NewStateAddresser(st), - APIAddresser: common.NewAPIAddresser(st, resources), - ModelWatcher: common.NewModelWatcher(st, resources, authorizer), - ModelMachinesWatcher: common.NewModelMachinesWatcher(st, resources, authorizer), - ControllerConfigAPI: common.NewControllerConfig(st), - InstanceIdGetter: common.NewInstanceIdGetter(st, getAuthFunc), - ToolsFinder: common.NewToolsFinder(st, st, urlGetter), - ToolsGetter: common.NewToolsGetter(st, st, st, urlGetter, getAuthOwner), - st: st, - resources: resources, - authorizer: authorizer, - getAuthFunc: getAuthFunc, + Remover: common.NewRemover(st, false, getAuthFunc), + StatusSetter: common.NewStatusSetter(st, getAuthFunc), + StatusGetter: common.NewStatusGetter(st, getAuthFunc), + DeadEnsurer: common.NewDeadEnsurer(st, getAuthFunc), + PasswordChanger: common.NewPasswordChanger(st, getAuthFunc), + LifeGetter: common.NewLifeGetter(st, getAuthFunc), + StateAddresser: common.NewStateAddresser(st), + APIAddresser: common.NewAPIAddresser(st, resources), + ModelWatcher: common.NewModelWatcher(st, resources, authorizer), + ModelMachinesWatcher: common.NewModelMachinesWatcher(st, resources, authorizer), + ControllerConfigAPI: common.NewControllerConfig(st), + InstanceIdGetter: common.NewInstanceIdGetter(st, getAuthFunc), + ToolsFinder: common.NewToolsFinder(configGetter, st, urlGetter), + ToolsGetter: common.NewToolsGetter(st, configGetter, st, urlGetter, getAuthOwner), + st: st, + resources: resources, + authorizer: authorizer, + configGetter: configGetter, + storageProviderRegistry: storageProviderRegistry, + storagePoolManager: poolmanager.New(state.NewStateSettings(st), storageProviderRegistry), + getAuthFunc: getAuthFunc, }, nil } @@ -708,7 +724,7 @@ // prepareContainerAccessEnvironment retrieves the environment, host machine, and access // for working with containers. func (p *ProvisionerAPI) prepareContainerAccessEnvironment() (environs.NetworkingEnviron, *state.Machine, common.AuthFunc, error) { - netEnviron, err := networkingcommon.NetworkingEnvironFromModelConfig(p.st) + netEnviron, err := networkingcommon.NetworkingEnvironFromModelConfig(p.configGetter) if err != nil { return nil, nil, nil, errors.Trace(err) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/provisioner/provisioner_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/provisioner/provisioner_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/provisioner/provisioner_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/provisioner/provisioner_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -24,6 +24,7 @@ "github.com/juju/juju/instance" "github.com/juju/juju/juju/testing" "github.com/juju/juju/network" + "github.com/juju/juju/provider/dummy" "github.com/juju/juju/state" statetesting "github.com/juju/juju/state/testing" "github.com/juju/juju/status" @@ -854,9 +855,7 @@ } func (s *withoutControllerSuite) TestSetInstanceInfo(c *gc.C) { - s.registerStorageProviders(c, "static") - - pm := poolmanager.New(state.NewStateSettings(s.State)) + pm := poolmanager.New(state.NewStateSettings(s.State), dummy.StorageProviders()) _, err := pm.Create("static-pool", "static", map[string]interface{}{"foo": "bar"}) c.Assert(err, jc.ErrorIsNil) err = s.State.UpdateModelConfig(map[string]interface{}{ diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/provisioner/provisioninginfo.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/provisioner/provisioninginfo.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/provisioner/provisioninginfo.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/provisioner/provisioninginfo.go 2016-08-16 08:56:25.000000000 +0000 @@ -18,7 +18,6 @@ "github.com/juju/juju/apiserver/params" "github.com/juju/juju/cloudconfig/instancecfg" "github.com/juju/juju/environs" - "github.com/juju/juju/environs/config" "github.com/juju/juju/environs/imagemetadata" "github.com/juju/juju/environs/simplestreams" "github.com/juju/juju/environs/tags" @@ -26,8 +25,6 @@ "github.com/juju/juju/state/cloudimagemetadata" "github.com/juju/juju/state/multiwatcher" "github.com/juju/juju/storage" - "github.com/juju/juju/storage/poolmanager" - "github.com/juju/juju/storage/provider/registry" ) // ProvisioningInfo returns the provisioning information for each given machine entity. @@ -126,7 +123,6 @@ if err != nil { return nil, err } - poolManager := poolmanager.New(state.NewStateSettings(p.st)) allVolumeParams := make([]params.VolumeParams, 0, len(volumeAttachments)) for _, volumeAttachment := range volumeAttachments { volumeTag := volumeAttachment.Volume() @@ -141,11 +137,13 @@ return nil, errors.Annotatef(err, "getting volume %q storage instance", volumeTag.Id()) } volumeParams, err := storagecommon.VolumeParams( - volume, storageInstance, modelConfig.UUID(), controllerCfg.ControllerUUID(), modelConfig, poolManager) + volume, storageInstance, modelConfig.UUID(), controllerCfg.ControllerUUID(), + modelConfig, p.storagePoolManager, p.storageProviderRegistry, + ) if err != nil { return nil, errors.Annotatef(err, "getting volume %q parameters", volumeTag.Id()) } - provider, err := registry.StorageProvider(storage.ProviderType(volumeParams.Provider)) + provider, err := p.storageProviderRegistry.StorageProvider(storage.ProviderType(volumeParams.Provider)) if err != nil { return nil, errors.Annotate(err, "getting storage provider") } @@ -376,14 +374,14 @@ func (p *ProvisionerAPI) constructImageConstraint(m *state.Machine) (*imagemetadata.ImageConstraint, environs.Environ, error) { // If we can determine current region, // we want only metadata specific to this region. - cloud, cfg, env, err := p.obtainEnvCloudConfig() + cloud, env, err := p.obtainEnvCloudConfig() if err != nil { return nil, nil, errors.Trace(err) } lookup := simplestreams.LookupParams{ Series: []string{m.Series()}, - Stream: cfg.ImageStream(), + Stream: env.Config().ImageStream(), } mcons, err := m.Constraints() @@ -403,15 +401,10 @@ // obtainEnvCloudConfig returns environment specific cloud information // to be used in search for compatible images and their metadata. -func (p *ProvisionerAPI) obtainEnvCloudConfig() (*simplestreams.CloudSpec, *config.Config, environs.Environ, error) { - cfg, err := p.st.ModelConfig() - if err != nil { - return nil, nil, nil, errors.Annotate(err, "could not get model config") - } - - env, err := environs.GetEnviron(p.st, environs.New) +func (p *ProvisionerAPI) obtainEnvCloudConfig() (*simplestreams.CloudSpec, environs.Environ, error) { + env, err := environs.GetEnviron(p.configGetter, environs.New) if err != nil { - return nil, nil, nil, errors.Annotate(err, "could not get model") + return nil, nil, errors.Annotate(err, "could not get model") } if inst, ok := env.(simplestreams.HasRegion); ok { @@ -419,11 +412,11 @@ if err != nil { // can't really find images if we cannot determine cloud region // TODO (anastasiamac 2015-12-03) or can we? - return nil, nil, nil, errors.Annotate(err, "getting provider region information (cloud spec)") + return nil, nil, errors.Annotate(err, "getting provider region information (cloud spec)") } - return &cloud, cfg, env, nil + return &cloud, env, nil } - return nil, cfg, env, nil + return nil, env, nil } // findImageMetadata returns all image metadata or an error fetching them. diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/provisioner/provisioninginfo_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/provisioner/provisioninginfo_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/provisioner/provisioninginfo_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/provisioner/provisioninginfo_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -16,17 +16,12 @@ "github.com/juju/juju/provider/dummy" "github.com/juju/juju/state" "github.com/juju/juju/state/multiwatcher" - "github.com/juju/juju/storage" "github.com/juju/juju/storage/poolmanager" - storagedummy "github.com/juju/juju/storage/provider/dummy" - "github.com/juju/juju/storage/provider/registry" coretesting "github.com/juju/juju/testing" ) func (s *withoutControllerSuite) TestProvisioningInfoWithStorage(c *gc.C) { - s.registerStorageProviders(c, "static") - - pm := poolmanager.New(state.NewStateSettings(s.State)) + pm := poolmanager.New(state.NewStateSettings(s.State), dummy.StorageProviders()) _, err := pm.Create("static-pool", "static", map[string]interface{}{"foo": "bar"}) c.Assert(err, jc.ErrorIsNil) @@ -118,35 +113,6 @@ c.Assert(result, jc.DeepEquals, expected) } -func (s *withoutControllerSuite) registerStorageProviders(c *gc.C, names ...string) { - types := make([]storage.ProviderType, len(names)) - for i, name := range names { - types[i] = storage.ProviderType(name) - if name == "dynamic" { - s.registerDynamicStorageProvider(c) - } else if name == "static" { - s.registerStaticStorageProvider(c) - } else { - c.Fatalf("unknown storage provider type: %q, expected static or dynamic", name) - } - } - registry.RegisterEnvironStorageProviders("dummy", types...) -} - -func (s *withoutControllerSuite) registerDynamicStorageProvider(c *gc.C) { - registry.RegisterProvider("dynamic", &storagedummy.StorageProvider{IsDynamic: true}) - s.AddCleanup(func(*gc.C) { - registry.RegisterProvider("dynamic", nil) - }) -} - -func (s *withoutControllerSuite) registerStaticStorageProvider(c *gc.C) { - registry.RegisterProvider("static", &storagedummy.StorageProvider{IsDynamic: false}) - s.AddCleanup(func(*gc.C) { - registry.RegisterProvider("static", nil) - }) -} - func (s *withoutControllerSuite) TestProvisioningInfoWithSingleNegativeAndPositiveSpaceInConstraints(c *gc.C) { s.addSpacesAndSubnets(c) @@ -304,14 +270,12 @@ } func (s *withoutControllerSuite) TestStorageProviderFallbackToType(c *gc.C) { - s.registerStorageProviders(c, "dynamic", "static") - template := state.MachineTemplate{ Series: "quantal", Jobs: []state.MachineJob{state.JobHostUnits}, Placement: "valid", Volumes: []state.MachineVolumeParams{ - {Volume: state.VolumeParams{Size: 1000, Pool: "dynamic"}}, + {Volume: state.VolumeParams{Size: 1000, Pool: "environscoped"}}, {Volume: state.VolumeParams{Size: 1000, Pool: "static"}}, }, } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/proxyupdater/model.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/proxyupdater/model.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/proxyupdater/model.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/proxyupdater/model.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,11 +4,11 @@ package proxyupdater import ( - "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/state" ) // NewAPI creates a new API server-side facade with a state.State backing. -func NewAPI(st *state.State, res *common.Resources, auth common.Authorizer) (*ProxyUpdaterAPI, error) { +func NewAPI(st *state.State, res facade.Resources, auth facade.Authorizer) (*ProxyUpdaterAPI, error) { return NewAPIWithBacking(&stateShim{st: st}, res, auth) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/proxyupdater/proxyupdater.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/proxyupdater/proxyupdater.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/proxyupdater/proxyupdater.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/proxyupdater/proxyupdater.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,6 +7,7 @@ "strings" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/environs/config" "github.com/juju/juju/network" @@ -28,12 +29,12 @@ type ProxyUpdaterAPI struct { backend Backend - resources *common.Resources - authorizer common.Authorizer + resources facade.Resources + authorizer facade.Authorizer } // NewAPIWithBacking creates a new server-side API facade with the given Backing. -func NewAPIWithBacking(st Backend, resources *common.Resources, authorizer common.Authorizer) (*ProxyUpdaterAPI, error) { +func NewAPIWithBacking(st Backend, resources facade.Resources, authorizer facade.Authorizer) (*ProxyUpdaterAPI, error) { if !(authorizer.AuthMachineAgent() || authorizer.AuthUnitAgent()) { return &ProxyUpdaterAPI{}, common.ErrPerm } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/reboot/reboot.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/reboot/reboot.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/reboot/reboot.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/reboot/reboot.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,6 +9,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" "github.com/juju/juju/state/watcher" @@ -23,10 +24,10 @@ *common.RebootRequester *common.RebootFlagClearer - auth common.Authorizer + auth facade.Authorizer st *state.State machine *state.Machine - resources *common.Resources + resources facade.Resources } func init() { @@ -34,7 +35,7 @@ } // NewRebootAPI creates a new server-side RebootAPI facade. -func NewRebootAPI(st *state.State, resources *common.Resources, auth common.Authorizer) (*RebootAPI, error) { +func NewRebootAPI(st *state.State, resources facade.Resources, auth facade.Authorizer) (*RebootAPI, error) { if !auth.AuthMachineAgent() { return nil, common.ErrPerm } @@ -71,11 +72,8 @@ var result params.NotifyWatchResult if r.auth.AuthOwner(r.machine.Tag()) { - watch, err = r.machine.WatchForRebootEvent() - if err != nil { - result.Error = common.ServerError(err) - return result, nil - } + watch = r.machine.WatchForRebootEvent() + err = nil // Consume the initial event. Technically, API // calls to Watch 'transmit' the initial event // in the Watch response. But NotifyWatchers diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/resumer/resumer.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/resumer/resumer.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/resumer/resumer.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/resumer/resumer.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,6 +7,7 @@ import ( "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/state" ) @@ -17,11 +18,11 @@ // ResumerAPI implements the API used by the resumer worker. type ResumerAPI struct { st stateInterface - auth common.Authorizer + auth facade.Authorizer } // NewResumerAPI creates a new instance of the Resumer API. -func NewResumerAPI(st *state.State, _ *common.Resources, authorizer common.Authorizer) (*ResumerAPI, error) { +func NewResumerAPI(st *state.State, _ facade.Resources, authorizer facade.Authorizer) (*ResumerAPI, error) { if !authorizer.AuthModelManager() { return nil, common.ErrPerm } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/retrystrategy/retrystrategy.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/retrystrategy/retrystrategy.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/retrystrategy/retrystrategy.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/retrystrategy/retrystrategy.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,6 +11,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" "github.com/juju/juju/state/watcher" @@ -39,7 +40,7 @@ type RetryStrategyAPI struct { st *state.State accessUnit common.GetAuthFunc - resources *common.Resources + resources facade.Resources } var _ RetryStrategy = (*RetryStrategyAPI)(nil) @@ -47,8 +48,8 @@ // NewRetryStrategyAPI creates a new API endpoint for getting retry strategies. func NewRetryStrategyAPI( st *state.State, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, ) (*RetryStrategyAPI, error) { if !authorizer.AuthUnitAgent() { return nil, common.ErrPerm diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/root.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/root.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/root.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/root.go 2016-08-16 08:56:25.000000000 +0000 @@ -12,7 +12,9 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/core/description" "github.com/juju/juju/rpc" "github.com/juju/juju/rpc/rpcreflect" "github.com/juju/juju/state" @@ -130,13 +132,13 @@ type apiRoot struct { state *state.State resources *common.Resources - authorizer common.Authorizer + authorizer facade.Authorizer objectMutex sync.RWMutex objectCache map[objectKey]reflect.Value } // newApiRoot returns a new apiRoot. -func newApiRoot(st *state.State, resources *common.Resources, authorizer common.Authorizer) *apiRoot { +func newApiRoot(st *state.State, resources *common.Resources, authorizer facade.Authorizer) *apiRoot { r := &apiRoot{ state: st, resources: resources, @@ -185,7 +187,7 @@ // check. return reflect.Value{}, err } - obj, err := factory(r.state, r.resources, r.authorizer, id) + obj, err := factory(r.facadeContext(id)) if err != nil { return reflect.Value{}, err } @@ -214,6 +216,39 @@ }, nil } +func (r *apiRoot) facadeContext(id string) *facadeContext { + return &facadeContext{ + r: r, + id: id, + } +} + +// facadeContext implements facade.Context +type facadeContext struct { + r *apiRoot + id string +} + +func (ctx *facadeContext) Abort() <-chan struct{} { + return nil +} + +func (ctx *facadeContext) Auth() facade.Authorizer { + return ctx.r.authorizer +} + +func (ctx *facadeContext) Resources() facade.Resources { + return ctx.r.resources +} + +func (ctx *facadeContext) State() *state.State { + return ctx.r.state +} + +func (ctx *facadeContext) ID() string { + return ctx.id +} + func lookupMethod(rootName string, version int, methodName string) (reflect.Type, rpcreflect.ObjMethod, error) { noMethod := rpcreflect.ObjMethod{} goType, err := common.Facades.GetType(rootName, version) @@ -314,6 +349,44 @@ return r.entity } +// HasPermission returns true if the logged in user can perform on . +func (r *apiHandler) HasPermission(operation description.Access, target names.Tag) (bool, error) { + return hasPermission(r.state.UserAccess, r.entity.Tag(), operation, target) +} + +type userAccessFunc func(names.UserTag, names.Tag) (description.UserAccess, error) + +func hasPermission(userGetter userAccessFunc, entity names.Tag, + operation description.Access, target names.Tag) (bool, error) { + validForKind := false + + switch operation { + case description.LoginAccess, description.AddModelAccess, description.SuperuserAccess: + validForKind = target.Kind() == names.ControllerTagKind + case description.ReadAccess, description.WriteAccess, description.AdminAccess: + validForKind = target.Kind() == names.ModelTagKind + } + if !validForKind { + return false, nil + } + + userTag, ok := entity.(names.UserTag) + if !ok { + return false, errors.NotValidf("obtaining permission for subject kind %q", entity.Kind()) + } + + user, err := userGetter(userTag, target) + if err != nil { + return false, errors.Annotatef(err, "while obtaining %s user", target.Kind()) + } + modelPermission := user.Access.EqualOrGreaterModelAccessThan(operation) && target.Kind() == names.ModelTagKind + controllerPermission := user.Access.EqualOrGreaterControllerAccessThan(operation) && target.Kind() == names.ControllerTagKind + if !controllerPermission && !modelPermission { + return false, nil + } + return true, nil +} + // DescribeFacades returns the list of available Facades and their Versions func DescribeFacades() []params.FacadeVersions { facades := common.Facades.List() diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/root_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/root_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/root_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/root_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,72 +9,83 @@ "sync" "time" + "github.com/juju/errors" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" + "github.com/juju/juju/core/description" "github.com/juju/juju/rpc/rpcreflect" "github.com/juju/juju/state" "github.com/juju/juju/testing" ) -type rootSuite struct { +type pingSuite struct { testing.BaseSuite } -var _ = gc.Suite(&rootSuite{}) - -var allowedDiscardedMethods = []string{ - "AuthClient", - "AuthModelManager", - "AuthMachineAgent", - "AuthOwner", - "AuthUnitAgent", - "FindMethod", - "GetAuthEntity", - "GetAuthTag", -} +var _ = gc.Suite(&pingSuite{}) -func (r *rootSuite) TestPingTimeout(c *gc.C) { - closedc := make(chan time.Time, 1) +func (r *pingSuite) TestPingTimeout(c *gc.C) { + triggered := make(chan struct{}) action := func() { - closedc <- time.Now() + close(triggered) } - timeout := apiserver.NewPingTimeout(action, 50*time.Millisecond) + clock := testing.NewClock(time.Now()) + timeout := apiserver.NewPingTimeout(action, clock, 50*time.Millisecond) for i := 0; i < 2; i++ { - time.Sleep(10 * time.Millisecond) + waitAlarm(c, clock) + clock.Advance(10 * time.Millisecond) timeout.Ping() } - // Expect action to be executed about 50ms after last ping. - broken := time.Now() - var closed time.Time + + waitAlarm(c, clock) + clock.Advance(49 * time.Millisecond) + select { + case <-triggered: + c.Fatalf("action triggered early") + case <-time.After(testing.ShortWait): + } + + clock.Advance(time.Millisecond) select { - case closed = <-closedc: + case <-triggered: case <-time.After(testing.LongWait): - c.Fatalf("action never executed") + c.Fatalf("action never triggered") } - closeDiff := closed.Sub(broken) / time.Millisecond - c.Assert(50 <= closeDiff && closeDiff <= 100, jc.IsTrue) } -func (r *rootSuite) TestPingTimeoutStopped(c *gc.C) { - closedc := make(chan time.Time, 1) +func (r *pingSuite) TestPingTimeoutStopped(c *gc.C) { + triggered := make(chan struct{}) action := func() { - closedc <- time.Now() + close(triggered) } - timeout := apiserver.NewPingTimeout(action, 20*time.Millisecond) - timeout.Ping() + clock := testing.NewClock(time.Now()) + timeout := apiserver.NewPingTimeout(action, clock, 20*time.Millisecond) + + waitAlarm(c, clock) timeout.Stop() + clock.Advance(time.Hour) + // The action should never trigger select { - case <-closedc: + case <-triggered: c.Fatalf("action triggered after Stop()") case <-time.After(testing.ShortWait): } } +func waitAlarm(c *gc.C, clock *testing.Clock) { + select { + case <-time.After(testing.LongWait): + c.Fatalf("alarm never set") + case <-clock.Alarms(): + } +} + type errRootSuite struct { testing.BaseSuite } @@ -101,6 +112,12 @@ return fmt.Errorf("badType.Exposed was not to be exposed") } +type rootSuite struct { + testing.BaseSuite +} + +var _ = gc.Suite(&rootSuite{}) + func (r *rootSuite) TestFindMethodUnknownFacade(c *gc.C) { root := apiserver.TestingApiRoot(nil) caller, err := root.FindMethod("unknown-testing-facade", 0, "Method") @@ -113,7 +130,7 @@ srvRoot := apiserver.TestingApiRoot(nil) defer common.Facades.Discard("my-testing-facade", 0) myGoodFacade := func( - *state.State, *common.Resources, common.Authorizer, + *state.State, facade.Resources, facade.Authorizer, ) ( *testingType, error, ) { @@ -131,25 +148,13 @@ defer common.Facades.Discard("my-testing-facade", 0) defer common.Facades.Discard("my-testing-facade", 1) defer common.Facades.Discard("my-testing-facade", 2) - myBadFacade := func( - *state.State, *common.Resources, common.Authorizer, string, - ) ( - interface{}, error, - ) { + myBadFacade := func(facade.Context) (facade.Facade, error) { return &badType{}, nil } - myGoodFacade := func( - *state.State, *common.Resources, common.Authorizer, string, - ) ( - interface{}, error, - ) { + myGoodFacade := func(facade.Context) (facade.Facade, error) { return &testingType{}, nil } - myErrFacade := func( - *state.State, *common.Resources, common.Authorizer, string, - ) ( - interface{}, error, - ) { + myErrFacade := func(context facade.Context) (facade.Facade, error) { return nil, fmt.Errorf("you shall not pass") } expectedType := reflect.TypeOf((*testingType)(nil)) @@ -207,7 +212,7 @@ defer common.Facades.Discard("my-counting-facade", 1) var count int64 newCounter := func( - *state.State, *common.Resources, common.Authorizer, + *state.State, facade.Resources, facade.Authorizer, ) ( *countingType, error, ) { @@ -243,11 +248,9 @@ var count int64 // like newCounter, but also tracks the "id" that was requested for // this counter - newIdCounter := func( - _ *state.State, _ *common.Resources, _ common.Authorizer, id string, - ) (interface{}, error) { + newIdCounter := func(context facade.Context) (facade.Facade, error) { count += 1 - return &countingType{count: count, id: id}, nil + return &countingType{count: count, id: context.ID()}, nil } reflectType := reflect.TypeOf((*countingType)(nil)) common.RegisterFacade("my-counting-facade", 0, newIdCounter, reflectType) @@ -275,11 +278,9 @@ srvRoot := apiserver.TestingApiRoot(nil) defer common.Facades.Discard("my-counting-facade", 0) var count int64 - newIdCounter := func( - _ *state.State, _ *common.Resources, _ common.Authorizer, id string, - ) (interface{}, error) { + newIdCounter := func(context facade.Context) (facade.Facade, error) { count += 1 - return &countingType{count: count, id: id}, nil + return &countingType{count: count, id: context.ID()}, nil } reflectType := reflect.TypeOf((*countingType)(nil)) common.RegisterFacade("my-counting-facade", 0, newIdCounter, reflectType) @@ -329,14 +330,14 @@ defer common.Facades.Discard("my-interface-facade", 0) defer common.Facades.Discard("my-interface-facade", 1) common.RegisterStandardFacade("my-interface-facade", 0, func( - *state.State, *common.Resources, common.Authorizer, + *state.State, facade.Resources, facade.Authorizer, ) ( smallInterface, error, ) { return &firstImpl{}, nil }) common.RegisterStandardFacade("my-interface-facade", 1, func( - *state.State, *common.Resources, common.Authorizer, + *state.State, facade.Resources, facade.Authorizer, ) ( smallInterface, error, ) { @@ -401,3 +402,111 @@ c.Check(authorized, jc.IsFalse) } + +type fakeUserAccess struct { + subjects []names.UserTag + objects []names.Tag + user description.UserAccess + err error +} + +func (f *fakeUserAccess) call(subject names.UserTag, object names.Tag) (description.UserAccess, error) { + f.subjects = append(f.subjects, subject) + f.objects = append(f.objects, object) + return f.user, f.err +} + +func (r *rootSuite) TestNoUserTagLacksPermission(c *gc.C) { + nonUser := names.NewModelTag("beef1beef1-0000-0000-000011112222") + target := names.NewModelTag("beef1beef2-0000-0000-000011112222") + hasPermission, err := apiserver.HasPermission((&fakeUserAccess{}).call, nonUser, description.ReadAccess, target) + c.Assert(hasPermission, jc.IsFalse) + c.Assert(err, gc.ErrorMatches, "obtaining permission for subject kind \"model\" not valid") +} + +func (r *rootSuite) TestHasPermission(c *gc.C) { + testCases := []struct { + title string + userGetterAccess description.Access + user names.UserTag + target names.Tag + access description.Access + expected bool + }{ + { + title: "user has lesser permissions than required", + userGetterAccess: description.ReadAccess, + user: names.NewUserTag("validuser"), + target: names.NewModelTag("beef1beef2-0000-0000-000011112222"), + access: description.WriteAccess, + expected: false, + }, + { + title: "user has equal permission than required", + userGetterAccess: description.WriteAccess, + user: names.NewUserTag("validuser"), + target: names.NewModelTag("beef1beef2-0000-0000-000011112222"), + access: description.WriteAccess, + expected: true, + }, + { + title: "user has greater permission than required", + userGetterAccess: description.AdminAccess, + user: names.NewUserTag("validuser"), + target: names.NewModelTag("beef1beef2-0000-0000-000011112222"), + access: description.WriteAccess, + expected: true, + }, + { + title: "user requests model permission on controller", + userGetterAccess: description.AdminAccess, + user: names.NewUserTag("validuser"), + target: names.NewModelTag("beef1beef2-0000-0000-000011112222"), + access: description.AddModelAccess, + expected: false, + }, + { + title: "user requests controller permission on model", + userGetterAccess: description.AdminAccess, + user: names.NewUserTag("validuser"), + target: names.NewControllerTag("beef1beef2-0000-0000-000011112222"), + access: description.AdminAccess, // notice user has this permission for model. + expected: false, + }, + { + title: "controller permissions also work", + userGetterAccess: description.AddModelAccess, + user: names.NewUserTag("validuser"), + target: names.NewControllerTag("beef1beef2-0000-0000-000011112222"), + access: description.AddModelAccess, + expected: true, + }, + } + for i, t := range testCases { + userGetter := &fakeUserAccess{ + user: description.UserAccess{ + Access: t.userGetterAccess, + }} + c.Logf("HasPermission test n %d: %s", i, t.title) + hasPermission, err := apiserver.HasPermission(userGetter.call, t.user, t.access, t.target) + c.Assert(hasPermission, gc.Equals, t.expected) + c.Assert(err, jc.ErrorIsNil) + } + +} + +func (r *rootSuite) TestUserGetterErrorReturns(c *gc.C) { + user := names.NewUserTag("validuser") + target := names.NewModelTag("beef1beef2-0000-0000-000011112222") + userGetter := &fakeUserAccess{ + user: description.UserAccess{}, + err: errors.NotFoundf("a user"), + } + hasPermission, err := apiserver.HasPermission(userGetter.call, user, description.ReadAccess, target) + c.Assert(hasPermission, jc.IsFalse) + c.Assert(err, gc.ErrorMatches, "while obtaining model user: a user not found") + c.Assert(userGetter.subjects, gc.HasLen, 1) + c.Assert(userGetter.subjects[0], gc.DeepEquals, user) + c.Assert(userGetter.objects, gc.HasLen, 1) + c.Assert(userGetter.objects[0], gc.DeepEquals, target) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/server_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/server_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/server_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/server_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,6 +9,7 @@ "fmt" "net" "net/http" + "time" "github.com/juju/loggo" "github.com/juju/testing" @@ -30,8 +31,9 @@ "github.com/juju/juju/apiserver/params" "github.com/juju/juju/cert" "github.com/juju/juju/controller" + "github.com/juju/juju/core/description" jujutesting "github.com/juju/juju/juju/testing" - "github.com/juju/juju/mongo/mongotest" + "github.com/juju/juju/mongo" "github.com/juju/juju/network" "github.com/juju/juju/state" "github.com/juju/juju/state/presence" @@ -190,7 +192,11 @@ proxy := testing.NewTCPProxy(c, mongoInfo.Addrs[0]) mongoInfo.Addrs = []string{proxy.Addr()} - st, err := state.Open(s.State.ModelTag(), mongoInfo, mongotest.DialOpts(), nil) + dialOpts := mongo.DialOpts{ + Timeout: 5 * time.Second, + SocketTimeout: 5 * time.Second, + } + st, err := state.Open(s.State.ModelTag(), mongoInfo, dialOpts, nil) c.Assert(err, gc.IsNil) defer st.Close() @@ -425,6 +431,62 @@ return nil } +func (s *serverSuite) bootstrapHasPermissionTest(c *gc.C) (*state.User, names.ControllerTag) { + u, err := s.State.AddUser("foobar", "Foo Bar", "password", "read") + c.Assert(err, jc.ErrorIsNil) + user := u.UserTag() + + ctag, err := names.ParseControllerTag("controller-" + s.State.ControllerUUID()) + c.Assert(err, jc.ErrorIsNil) + cu, err := s.State.UserAccess(user, ctag) + c.Assert(err, jc.ErrorIsNil) + c.Assert(cu.Access, gc.Equals, description.LoginAccess) + return u, ctag +} + +func (s *serverSuite) TestApiHandlerHasPermissionLogin(c *gc.C) { + u, ctag := s.bootstrapHasPermissionTest(c) + + handler, _ := apiserver.TestingApiHandlerWithEntity(c, s.State, s.State, u) + defer handler.Kill() + + apiserver.AssertHasPermission(c, handler, description.LoginAccess, ctag, true) + apiserver.AssertHasPermission(c, handler, description.AddModelAccess, ctag, false) + apiserver.AssertHasPermission(c, handler, description.SuperuserAccess, ctag, false) +} + +func (s *serverSuite) TestApiHandlerHasPermissionAdmodel(c *gc.C) { + u, ctag := s.bootstrapHasPermissionTest(c) + user := u.UserTag() + + handler, _ := apiserver.TestingApiHandlerWithEntity(c, s.State, s.State, u) + defer handler.Kill() + + ua, err := s.State.SetUserAccess(user, ctag, description.AddModelAccess) + c.Assert(err, jc.ErrorIsNil) + c.Assert(ua.Access, gc.Equals, description.AddModelAccess) + + apiserver.AssertHasPermission(c, handler, description.LoginAccess, ctag, true) + apiserver.AssertHasPermission(c, handler, description.AddModelAccess, ctag, true) + apiserver.AssertHasPermission(c, handler, description.SuperuserAccess, ctag, false) +} + +func (s *serverSuite) TestApiHandlerHasPermissionSuperUser(c *gc.C) { + u, ctag := s.bootstrapHasPermissionTest(c) + user := u.UserTag() + + handler, _ := apiserver.TestingApiHandlerWithEntity(c, s.State, s.State, u) + defer handler.Kill() + + ua, err := s.State.SetUserAccess(user, ctag, description.SuperuserAccess) + c.Assert(err, jc.ErrorIsNil) + c.Assert(ua.Access, gc.Equals, description.SuperuserAccess) + + apiserver.AssertHasPermission(c, handler, description.LoginAccess, ctag, true) + apiserver.AssertHasPermission(c, handler, description.AddModelAccess, ctag, true) + apiserver.AssertHasPermission(c, handler, description.SuperuserAccess, ctag, true) +} + func (s *serverSuite) TestApiHandlerTeardownInitialEnviron(c *gc.C) { s.checkApiHandlerTeardown(c, s.State, s.State) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/singular/fixture_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/singular/fixture_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/singular/fixture_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/singular/fixture_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,23 +9,23 @@ "github.com/juju/testing" "gopkg.in/juju/names.v2" - "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/core/lease" coretesting "github.com/juju/juju/testing" ) // mockAuth represents a machine which may or may not be an environ manager. type mockAuth struct { - common.Authorizer + facade.Authorizer nonManager bool } -// AuthModelManager is part of the common.Authorizer interface. +// AuthModelManager is part of the facade.Authorizer interface. func (mock mockAuth) AuthModelManager() bool { return !mock.nonManager } -// GetAuthTag is part of the common.Authorizer interface. +// GetAuthTag is part of the facade.Authorizer interface. func (mockAuth) GetAuthTag() names.Tag { return names.NewMachineTag("123") } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/singular/singular.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/singular/singular.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/singular/singular.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/singular/singular.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,6 +9,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/core/lease" "github.com/juju/juju/state" @@ -17,7 +18,7 @@ func init() { common.RegisterStandardFacade( "Singular", 1, - func(st *state.State, _ *common.Resources, auth common.Authorizer) (*Facade, error) { + func(st *state.State, _ facade.Resources, auth facade.Authorizer) (*Facade, error) { return NewFacade(st, auth) }, ) @@ -35,7 +36,7 @@ // NewFacade returns a singular-controller API facade, backed by the supplied // state, so long as the authorizer represents a controller machine. -func NewFacade(backend Backend, auth common.Authorizer) (*Facade, error) { +func NewFacade(backend Backend, auth facade.Authorizer) (*Facade, error) { if !auth.AuthModelManager() { return nil, common.ErrPerm } @@ -49,7 +50,7 @@ // Facade allows controller machines to request exclusive rights to administer // some specific model for a limited time. type Facade struct { - auth common.Authorizer + auth facade.Authorizer model names.ModelTag claimer lease.Claimer } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/spaces/spaces.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/spaces/spaces.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/spaces/spaces.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/spaces/spaces.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,6 +8,7 @@ "github.com/juju/juju/apiserver/common" "github.com/juju/juju/apiserver/common/networkingcommon" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" ) @@ -25,19 +26,19 @@ // spacesAPI implements the API interface. type spacesAPI struct { backing networkingcommon.NetworkBacking - resources *common.Resources - authorizer common.Authorizer + resources facade.Resources + authorizer facade.Authorizer } // NewAPI creates a new Space API server-side facade with a // state.State backing. -func NewAPI(st *state.State, res *common.Resources, auth common.Authorizer) (API, error) { +func NewAPI(st *state.State, res facade.Resources, auth facade.Authorizer) (API, error) { return newAPIWithBacking(networkingcommon.NewStateShim(st), res, auth) } // newAPIWithBacking creates a new server-side Spaces API facade with // the given Backing. -func newAPIWithBacking(backing networkingcommon.NetworkBacking, resources *common.Resources, authorizer common.Authorizer) (API, error) { +func newAPIWithBacking(backing networkingcommon.NetworkBacking, resources facade.Resources, authorizer facade.Authorizer) (API, error) { // Only clients can access the Spaces facade. if !authorizer.AuthClient() { return nil, common.ErrPerm diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/spaces/spaces_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/spaces/spaces_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/spaces/spaces_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/spaces/spaces_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -121,6 +121,7 @@ baseCalls := []apiservertesting.StubMethodCall{ apiservertesting.BackingCall("ModelConfig"), + apiservertesting.BackingCall("CloudSpec"), apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), apiservertesting.ZonedNetworkingEnvironCall("SupportsSpaces"), } @@ -163,6 +164,7 @@ func (s *SpacesSuite) TestAddSpacesAPIError(c *gc.C) { apiservertesting.SharedStub.SetErrors( nil, // Backing.ModelConfig() + nil, // Backing.CloudSpec() nil, // Provider.Open() nil, // ZonedNetworkingEnviron.SupportsSpaces() errors.AlreadyExistsf("space-foo"), // Backing.AddSpace() @@ -263,12 +265,13 @@ boom := errors.New("backing boom") apiservertesting.BackingInstance.SetErrors(boom) _, err := s.facade.ListSpaces() - c.Assert(err, gc.ErrorMatches, "getting model config: backing boom") + c.Assert(err, gc.ErrorMatches, "getting environ: backing boom") } func (s *SpacesSuite) TestListSpacesSubnetsError(c *gc.C) { apiservertesting.SharedStub.SetErrors( nil, // Backing.ModelConfig() + nil, // Backing.CloudSpec() nil, // Provider.Open() nil, // ZonedNetworkingEnviron.SupportsSpaces() nil, // Backing.AllSpaces() @@ -289,6 +292,7 @@ boom := errors.New("boom") apiservertesting.SharedStub.SetErrors( nil, // Backing.ModelConfig() + nil, // Backing.CloudSpec() nil, // Provider.Open() nil, // ZonedNetworkingEnviron.SupportsSpaces() nil, // Backing.AllSpaces() @@ -314,23 +318,25 @@ spaces := params.CreateSpacesParams{} _, err := s.facade.CreateSpaces(spaces) - c.Assert(err, gc.ErrorMatches, "getting model config: boom") + c.Assert(err, gc.ErrorMatches, "getting environ: boom") } func (s *SpacesSuite) TestCreateSpacesProviderOpenError(c *gc.C) { apiservertesting.SharedStub.SetErrors( nil, // Backing.ModelConfig() + nil, // Backing.CloudSpec() errors.New("boom"), // Provider.Open() ) spaces := params.CreateSpacesParams{} _, err := s.facade.CreateSpaces(spaces) - c.Assert(err, gc.ErrorMatches, "validating model config: boom") + c.Assert(err, gc.ErrorMatches, "getting environ: boom") } func (s *SpacesSuite) TestCreateSpacesNotSupportedError(c *gc.C) { apiservertesting.SharedStub.SetErrors( nil, // Backing.ModelConfig() + nil, // Backing.CloudSpec() nil, // Provider.Open() errors.NotSupportedf("spaces"), // ZonedNetworkingEnviron.SupportsSpaces() ) @@ -343,6 +349,7 @@ func (s *SpacesSuite) TestListSpacesNotSupportedError(c *gc.C) { apiservertesting.SharedStub.SetErrors( nil, // Backing.ModelConfig() + nil, // Backing.CloudSpec() nil, // Provider.Open errors.NotSupportedf("spaces"), // ZonedNetworkingEnviron.SupportsSpaces() ) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/sshclient/facade.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/sshclient/facade.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/sshclient/facade.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/sshclient/facade.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,6 +7,7 @@ import ( "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/network" ) @@ -21,7 +22,7 @@ } // New returns a new API facade for the sshclient worker. -func New(backend Backend, _ *common.Resources, authorizer common.Authorizer) (*Facade, error) { +func New(backend Backend, _ facade.Resources, authorizer facade.Authorizer) (*Facade, error) { if !authorizer.AuthClient() { return nil, common.ErrPerm } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/sshclient/shim.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/sshclient/shim.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/sshclient/shim.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/sshclient/shim.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,7 +7,7 @@ "github.com/juju/errors" "gopkg.in/juju/names.v2" - "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/environs/config" "github.com/juju/juju/network" "github.com/juju/juju/state" @@ -29,7 +29,7 @@ } // newFacade wraps New to express the supplied *state.State as a Backend. -func newFacade(st *state.State, res *common.Resources, auth common.Authorizer) (*Facade, error) { +func newFacade(st *state.State, res facade.Resources, auth facade.Authorizer) (*Facade, error) { return New(&backend{st}, res, auth) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/statushistory/pruner.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/statushistory/pruner.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/statushistory/pruner.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/statushistory/pruner.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,6 +5,7 @@ import ( "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" ) @@ -16,11 +17,11 @@ // API is the concrete implementation of the Pruner endpoint.. type API struct { st *state.State - authorizer common.Authorizer + authorizer facade.Authorizer } // NewAPI returns an API Instance. -func NewAPI(st *state.State, _ *common.Resources, auth common.Authorizer) (*API, error) { +func NewAPI(st *state.State, _ facade.Resources, auth facade.Authorizer) (*API, error) { return &API{ st: st, authorizer: auth, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/storage/base_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/storage/base_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/storage/base_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/storage/base_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -40,6 +40,7 @@ filesystemAttachment *mockFilesystemAttachment calls []string + registry jujustorage.StaticProviderRegistry poolManager *mockPoolManager pools map[string]*jujustorage.Config @@ -53,11 +54,12 @@ s.calls = []string{} s.state = s.constructState() + s.registry = jujustorage.StaticProviderRegistry{map[jujustorage.ProviderType]jujustorage.Provider{}} s.pools = make(map[string]*jujustorage.Config) s.poolManager = s.constructPoolManager() var err error - s.api, err = storage.CreateAPI(s.state, s.poolManager, s.resources, s.authorizer) + s.api, err = storage.NewAPI(s.state, s.registry, s.poolManager, s.resources, s.authorizer) c.Assert(err, jc.ErrorIsNil) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/storage/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/storage/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/storage/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/storage/export_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,6 +7,4 @@ ValidatePoolListFilter = (*API).validatePoolListFilter ValidateNameCriteria = (*API).validateNameCriteria ValidateProviderCriteria = (*API).validateProviderCriteria - - CreateAPI = createAPI ) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/storage/poollist_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/storage/poollist_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/storage/poollist_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/storage/poollist_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -14,7 +14,6 @@ apiserverstorage "github.com/juju/juju/apiserver/storage" "github.com/juju/juju/storage" "github.com/juju/juju/storage/provider" - "github.com/juju/juju/storage/provider/registry" ) type poolSuite struct { @@ -50,13 +49,11 @@ } func (s *poolSuite) TestListManyResults(c *gc.C) { + s.registry.Providers["static"] = nil s.createPools(c, 2) results, err := s.api.ListPools(params.StoragePoolFilters{[]params.StoragePoolFilter{{}}}) c.Assert(err, jc.ErrorIsNil) - assertPoolNames(c, results.Results[0].Result, - "testpool0", "testpool1", - "dummy", "loop", - "tmpfs", "rootfs") + assertPoolNames(c, results.Results[0].Result, "testpool0", "testpool1", "static") } func (s *poolSuite) TestListByName(c *gc.C) { @@ -162,10 +159,11 @@ } func (s *poolSuite) TestListNoPools(c *gc.C) { + s.registry.Providers["static"] = nil results, err := s.api.ListPools(params.StoragePoolFilters{[]params.StoragePoolFilter{{}}}) c.Assert(err, jc.ErrorIsNil) c.Assert(results.Results, gc.HasLen, 1) - assertPoolNames(c, results.Results[0].Result, "dummy", "rootfs", "loop", "tmpfs") + assertPoolNames(c, results.Results[0].Result, "static") } func (s *poolSuite) TestListFilterEmpty(c *gc.C) { @@ -189,11 +187,10 @@ } func (s *poolSuite) TestListFilterUnregisteredProvider(c *gc.C) { - s.state.modelName = "noprovidersregistered" err := apiserverstorage.ValidateProviderCriteria( s.api, []string{validProvider}) - c.Assert(err, gc.ErrorMatches, ".*not supported.*") + c.Assert(err, gc.ErrorMatches, `storage provider "loop" not found`) } func (s *poolSuite) TestListFilterUnknownProvider(c *gc.C) { @@ -201,7 +198,7 @@ err := apiserverstorage.ValidateProviderCriteria( s.api, []string{invalidProvider}) - c.Assert(err, gc.ErrorMatches, ".*not supported.*") + c.Assert(err, gc.ErrorMatches, `storage provider "invalid" not found`) } func (s *poolSuite) TestListFilterValidNames(c *gc.C) { @@ -244,7 +241,7 @@ params.StoragePoolFilter{ Providers: []string{invalidProvider}, Names: []string{validName}}) - c.Assert(err, gc.ErrorMatches, ".*not supported.*") + c.Assert(err, gc.ErrorMatches, `storage provider "invalid" not found`) } func (s *poolSuite) TestListFilterInvalidProvidersAndNames(c *gc.C) { @@ -253,9 +250,14 @@ params.StoragePoolFilter{ Providers: []string{invalidProvider}, Names: []string{invalidName}}) - c.Assert(err, gc.ErrorMatches, ".*not supported.*") + c.Assert(err, gc.ErrorMatches, `storage provider "invalid" not found`) } func (s *poolSuite) registerProviders(c *gc.C) { - registry.RegisterEnvironStorageProviders(s.state.modelName, "dummy") + common := provider.CommonStorageProviders() + for _, providerType := range common.StorageProviderTypes() { + p, err := common.StorageProvider(providerType) + c.Assert(err, jc.ErrorIsNil) + s.registry.Providers[providerType] = p + } } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/storage/shim.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/storage/shim.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/storage/shim.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/storage/shim.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,146 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package storage + +import ( + "github.com/juju/errors" + "gopkg.in/juju/names.v2" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" + "github.com/juju/juju/environs" + "github.com/juju/juju/state" + "github.com/juju/juju/state/stateenvirons" + "github.com/juju/juju/storage/poolmanager" +) + +// This file contains untested shims to let us wrap state in a sensible +// interface and avoid writing tests that depend on mongodb. If you were +// to change any part of it so that it were no longer *obviously* and +// *trivially* correct, you would be Doing It Wrong. + +func init() { + common.RegisterStandardFacade("Storage", 3, newAPI) +} + +func newAPI( + st *state.State, + resources facade.Resources, + authorizer facade.Authorizer, +) (*API, error) { + env, err := stateenvirons.GetNewEnvironFunc(environs.New)(st) + if err != nil { + return nil, errors.Annotate(err, "getting environ") + } + registry := stateenvirons.NewStorageProviderRegistry(env) + pm := poolmanager.New(state.NewStateSettings(st), registry) + return NewAPI(getState(st), registry, pm, resources, authorizer) +} + +type storageAccess interface { + // StorageInstance is required for storage functionality. + StorageInstance(names.StorageTag) (state.StorageInstance, error) + + // AllStorageInstances is required for storage functionality. + AllStorageInstances() ([]state.StorageInstance, error) + + // StorageAttachments is required for storage functionality. + StorageAttachments(names.StorageTag) ([]state.StorageAttachment, error) + + // UnitAssignedMachine is required for storage functionality. + UnitAssignedMachine(names.UnitTag) (names.MachineTag, error) + + // FilesystemAttachment is required for storage functionality. + FilesystemAttachment(names.MachineTag, names.FilesystemTag) (state.FilesystemAttachment, error) + + // StorageInstanceFilesystem is required for storage functionality. + StorageInstanceFilesystem(names.StorageTag) (state.Filesystem, error) + + // StorageInstanceVolume is required for storage functionality. + StorageInstanceVolume(names.StorageTag) (state.Volume, error) + + // VolumeAttachment is required for storage functionality. + VolumeAttachment(names.MachineTag, names.VolumeTag) (state.VolumeAttachment, error) + + // WatchStorageAttachment is required for storage functionality. + WatchStorageAttachment(names.StorageTag, names.UnitTag) state.NotifyWatcher + + // WatchFilesystemAttachment is required for storage functionality. + WatchFilesystemAttachment(names.MachineTag, names.FilesystemTag) state.NotifyWatcher + + // WatchVolumeAttachment is required for storage functionality. + WatchVolumeAttachment(names.MachineTag, names.VolumeTag) state.NotifyWatcher + + // WatchBlockDevices is required for storage functionality. + WatchBlockDevices(names.MachineTag) state.NotifyWatcher + + // BlockDevices is required for storage functionality. + BlockDevices(names.MachineTag) ([]state.BlockDeviceInfo, error) + + // ModelName is required for pool functionality. + ModelName() (string, error) + + // AllVolumes is required for volume functionality. + AllVolumes() ([]state.Volume, error) + + // VolumeAttachments is required for volume functionality. + VolumeAttachments(volume names.VolumeTag) ([]state.VolumeAttachment, error) + + // MachineVolumeAttachments is required for volume functionality. + MachineVolumeAttachments(machine names.MachineTag) ([]state.VolumeAttachment, error) + + // Volume is required for volume functionality. + Volume(tag names.VolumeTag) (state.Volume, error) + + // AllFilesystems is required for filesystem functionality. + AllFilesystems() ([]state.Filesystem, error) + + // FilesystemAttachments is required for filesystem functionality. + FilesystemAttachments(filesystem names.FilesystemTag) ([]state.FilesystemAttachment, error) + + // MachineFilesystemAttachments is required for filesystem functionality. + MachineFilesystemAttachments(machine names.MachineTag) ([]state.FilesystemAttachment, error) + + // Filesystem is required for filesystem functionality. + Filesystem(tag names.FilesystemTag) (state.Filesystem, error) + + // AddStorageForUnit is required for storage add functionality. + AddStorageForUnit(tag names.UnitTag, name string, cons state.StorageConstraints) error + + // GetBlockForType is required to block operations. + GetBlockForType(t state.BlockType) (state.Block, bool, error) +} + +var getState = func(st *state.State) storageAccess { + return stateShim{st} +} + +type stateShim struct { + *state.State +} + +// UnitAssignedMachine returns the tag of the machine that the unit +// is assigned to, or an error if the unit cannot be obtained or is +// not assigned to a machine. +func (s stateShim) UnitAssignedMachine(tag names.UnitTag) (names.MachineTag, error) { + unit, err := s.Unit(tag.Id()) + if err != nil { + return names.MachineTag{}, errors.Trace(err) + } + mid, err := unit.AssignedMachineId() + if err != nil { + return names.MachineTag{}, errors.Trace(err) + } + return names.NewMachineTag(mid), nil +} + +// ModelName returns the name of Juju environment, +// or an error if environment configuration is not retrievable. +func (s stateShim) ModelName() (string, error) { + cfg, err := s.State.ModelConfig() + if err != nil { + return "", errors.Trace(err) + } + return cfg.Name(), nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/storage/state.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/storage/state.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/storage/state.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/storage/state.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,118 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package storage - -import ( - "github.com/juju/errors" - "gopkg.in/juju/names.v2" - - "github.com/juju/juju/state" -) - -type storageAccess interface { - // StorageInstance is required for storage functionality. - StorageInstance(names.StorageTag) (state.StorageInstance, error) - - // AllStorageInstances is required for storage functionality. - AllStorageInstances() ([]state.StorageInstance, error) - - // StorageAttachments is required for storage functionality. - StorageAttachments(names.StorageTag) ([]state.StorageAttachment, error) - - // UnitAssignedMachine is required for storage functionality. - UnitAssignedMachine(names.UnitTag) (names.MachineTag, error) - - // FilesystemAttachment is required for storage functionality. - FilesystemAttachment(names.MachineTag, names.FilesystemTag) (state.FilesystemAttachment, error) - - // StorageInstanceFilesystem is required for storage functionality. - StorageInstanceFilesystem(names.StorageTag) (state.Filesystem, error) - - // StorageInstanceVolume is required for storage functionality. - StorageInstanceVolume(names.StorageTag) (state.Volume, error) - - // VolumeAttachment is required for storage functionality. - VolumeAttachment(names.MachineTag, names.VolumeTag) (state.VolumeAttachment, error) - - // WatchStorageAttachment is required for storage functionality. - WatchStorageAttachment(names.StorageTag, names.UnitTag) state.NotifyWatcher - - // WatchFilesystemAttachment is required for storage functionality. - WatchFilesystemAttachment(names.MachineTag, names.FilesystemTag) state.NotifyWatcher - - // WatchVolumeAttachment is required for storage functionality. - WatchVolumeAttachment(names.MachineTag, names.VolumeTag) state.NotifyWatcher - - // WatchBlockDevices is required for storage functionality. - WatchBlockDevices(names.MachineTag) state.NotifyWatcher - - // BlockDevices is required for storage functionality. - BlockDevices(names.MachineTag) ([]state.BlockDeviceInfo, error) - - // ModelName is required for pool functionality. - ModelName() (string, error) - - // AllVolumes is required for volume functionality. - AllVolumes() ([]state.Volume, error) - - // VolumeAttachments is required for volume functionality. - VolumeAttachments(volume names.VolumeTag) ([]state.VolumeAttachment, error) - - // MachineVolumeAttachments is required for volume functionality. - MachineVolumeAttachments(machine names.MachineTag) ([]state.VolumeAttachment, error) - - // Volume is required for volume functionality. - Volume(tag names.VolumeTag) (state.Volume, error) - - // AllFilesystems is required for filesystem functionality. - AllFilesystems() ([]state.Filesystem, error) - - // FilesystemAttachments is required for filesystem functionality. - FilesystemAttachments(filesystem names.FilesystemTag) ([]state.FilesystemAttachment, error) - - // MachineFilesystemAttachments is required for filesystem functionality. - MachineFilesystemAttachments(machine names.MachineTag) ([]state.FilesystemAttachment, error) - - // Filesystem is required for filesystem functionality. - Filesystem(tag names.FilesystemTag) (state.Filesystem, error) - - // AddStorageForUnit is required for storage add functionality. - AddStorageForUnit(tag names.UnitTag, name string, cons state.StorageConstraints) error - - // GetBlockForType is required to block operations. - GetBlockForType(t state.BlockType) (state.Block, bool, error) -} - -var getState = func(st *state.State) storageAccess { - return stateShim{st} -} - -type stateShim struct { - *state.State -} - -// UnitAssignedMachine returns the tag of the machine that the unit -// is assigned to, or an error if the unit cannot be obtained or is -// not assigned to a machine. -func (s stateShim) UnitAssignedMachine(tag names.UnitTag) (names.MachineTag, error) { - unit, err := s.Unit(tag.Id()) - if err != nil { - return names.MachineTag{}, errors.Trace(err) - } - mid, err := unit.AssignedMachineId() - if err != nil { - return names.MachineTag{}, errors.Trace(err) - } - return names.NewMachineTag(mid), nil -} - -// ModelName returns the name of Juju environment, -// or an error if environment configuration is not retrievable. -func (s stateShim) ModelName() (string, error) { - cfg, err := s.State.ModelConfig() - if err != nil { - return "", errors.Trace(err) - } - return cfg.Name(), nil -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/storage/storage.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/storage/storage.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/storage/storage.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/storage/storage.go 2016-08-16 08:56:25.000000000 +0000 @@ -12,56 +12,43 @@ "github.com/juju/juju/apiserver/common" "github.com/juju/juju/apiserver/common/storagecommon" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" "github.com/juju/juju/status" "github.com/juju/juju/storage" "github.com/juju/juju/storage/poolmanager" - "github.com/juju/juju/storage/provider/registry" ) -func init() { - common.RegisterStandardFacade("Storage", 2, NewAPI) -} - // API implements the storage interface and is the concrete // implementation of the api end point. type API struct { storage storageAccess + registry storage.ProviderRegistry poolManager poolmanager.PoolManager - authorizer common.Authorizer + authorizer facade.Authorizer } -// createAPI returns a new storage API facade. -func createAPI( +// NewAPI returns a new storage API facade. +func NewAPI( st storageAccess, + registry storage.ProviderRegistry, pm poolmanager.PoolManager, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, ) (*API, error) { if !authorizer.AuthClient() { return nil, common.ErrPerm } + return &API{ storage: st, + registry: registry, poolManager: pm, authorizer: authorizer, }, nil } -// NewAPI returns a new storage API facade. -func NewAPI( - st *state.State, - resources *common.Resources, - authorizer common.Authorizer, -) (*API, error) { - return createAPI(getState(st), poolManager(st), resources, authorizer) -} - -func poolManager(st *state.State) poolmanager.PoolManager { - return poolmanager.New(state.NewStateSettings(st)) -} - // StorageDetails retrieves and returns detailed information about desired // storage identified by supplied tags. If specified storage cannot be // retrieved, individual error is returned instead of storage information. @@ -239,10 +226,7 @@ if err != nil { return nil, err } - providers, err := a.allProviders() - if err != nil { - return nil, err - } + providers := a.registry.StorageProviderTypes() matches := buildFilter(filter) results := append( filterPools(pools, matches), @@ -307,17 +291,6 @@ return all } -func (a *API) allProviders() ([]storage.ProviderType, error) { - envName, err := a.storage.ModelName() - if err != nil { - return nil, errors.Annotate(err, "getting env name") - } - if providers, ok := registry.EnvironStorageProviders(envName); ok { - return providers, nil - } - return nil, nil -} - func (a *API) validatePoolListFilter(filter params.StoragePoolFilter) error { if err := a.validateProviderCriteria(filter.Providers); err != nil { return errors.Trace(err) @@ -338,13 +311,10 @@ } func (a *API) validateProviderCriteria(providers []string) error { - envName, err := a.storage.ModelName() - if err != nil { - return errors.Annotate(err, "getting model name") - } for _, p := range providers { - if !registry.IsProviderSupported(envName, storage.ProviderType(p)) { - return errors.NotSupportedf("%q", p) + _, err := a.registry.StorageProvider(storage.ProviderType(p)) + if err != nil { + return errors.Trace(err) } } return nil diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/storageprovisioner/shim.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/storageprovisioner/shim.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/storageprovisioner/shim.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/storageprovisioner/shim.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,103 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package storageprovisioner + +import ( + "github.com/juju/errors" + "gopkg.in/juju/names.v2" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" + "github.com/juju/juju/controller" + "github.com/juju/juju/environs" + "github.com/juju/juju/instance" + "github.com/juju/juju/state" + "github.com/juju/juju/state/stateenvirons" + "github.com/juju/juju/storage/poolmanager" +) + +// This file contains untested shims to let us wrap state in a sensible +// interface and avoid writing tests that depend on mongodb. If you were +// to change any part of it so that it were no longer *obviously* and +// *trivially* correct, you would be Doing It Wrong. + +func init() { + common.RegisterStandardFacade("StorageProvisioner", 3, newStorageProvisionerAPI) +} + +func newStorageProvisionerAPI(st *state.State, resources facade.Resources, authorizer facade.Authorizer) (*StorageProvisionerAPI, error) { + env, err := stateenvirons.GetNewEnvironFunc(environs.New)(st) + if err != nil { + return nil, errors.Annotate(err, "getting environ") + } + registry := stateenvirons.NewStorageProviderRegistry(env) + pm := poolmanager.New(state.NewStateSettings(st), registry) + return NewStorageProvisionerAPI(stateShim{st}, resources, authorizer, registry, pm) +} + +type Backend interface { + state.EntityFinder + state.ModelAccessor + + ControllerConfig() (controller.Config, error) + MachineInstanceId(names.MachineTag) (instance.Id, error) + ModelTag() names.ModelTag + BlockDevices(names.MachineTag) ([]state.BlockDeviceInfo, error) + + WatchBlockDevices(names.MachineTag) state.NotifyWatcher + WatchMachine(names.MachineTag) (state.NotifyWatcher, error) + WatchModelFilesystems() state.StringsWatcher + WatchEnvironFilesystemAttachments() state.StringsWatcher + WatchMachineFilesystems(names.MachineTag) state.StringsWatcher + WatchMachineFilesystemAttachments(names.MachineTag) state.StringsWatcher + WatchModelVolumes() state.StringsWatcher + WatchEnvironVolumeAttachments() state.StringsWatcher + WatchMachineVolumes(names.MachineTag) state.StringsWatcher + WatchMachineVolumeAttachments(names.MachineTag) state.StringsWatcher + WatchVolumeAttachment(names.MachineTag, names.VolumeTag) state.NotifyWatcher + + StorageInstance(names.StorageTag) (state.StorageInstance, error) + + Filesystem(names.FilesystemTag) (state.Filesystem, error) + FilesystemAttachment(names.MachineTag, names.FilesystemTag) (state.FilesystemAttachment, error) + + Volume(names.VolumeTag) (state.Volume, error) + VolumeAttachment(names.MachineTag, names.VolumeTag) (state.VolumeAttachment, error) + VolumeAttachments(names.VolumeTag) ([]state.VolumeAttachment, error) + + RemoveFilesystem(names.FilesystemTag) error + RemoveFilesystemAttachment(names.MachineTag, names.FilesystemTag) error + RemoveVolume(names.VolumeTag) error + RemoveVolumeAttachment(names.MachineTag, names.VolumeTag) error + + SetFilesystemInfo(names.FilesystemTag, state.FilesystemInfo) error + SetFilesystemAttachmentInfo(names.MachineTag, names.FilesystemTag, state.FilesystemAttachmentInfo) error + SetVolumeInfo(names.VolumeTag, state.VolumeInfo) error + SetVolumeAttachmentInfo(names.MachineTag, names.VolumeTag, state.VolumeAttachmentInfo) error +} + +type stateShim struct { + *state.State +} + +// NewStateBackend creates a Backend from the given *state.State. +func NewStateBackend(st *state.State) Backend { + return stateShim{st} +} + +func (s stateShim) MachineInstanceId(tag names.MachineTag) (instance.Id, error) { + m, err := s.Machine(tag.Id()) + if err != nil { + return "", errors.Trace(err) + } + return m.InstanceId() +} + +func (s stateShim) WatchMachine(tag names.MachineTag) (state.NotifyWatcher, error) { + m, err := s.Machine(tag.Id()) + if err != nil { + return nil, errors.Trace(err) + } + return m.Watch(), nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/storageprovisioner/state.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/storageprovisioner/state.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/storageprovisioner/state.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/storageprovisioner/state.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,73 +0,0 @@ -// Copyright 2014 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package storageprovisioner - -import ( - "github.com/juju/errors" - "gopkg.in/juju/names.v2" - - "github.com/juju/juju/controller" - "github.com/juju/juju/instance" - "github.com/juju/juju/state" -) - -type provisionerState interface { - state.EntityFinder - state.ModelAccessor - - ControllerConfig() (controller.Config, error) - MachineInstanceId(names.MachineTag) (instance.Id, error) - BlockDevices(names.MachineTag) ([]state.BlockDeviceInfo, error) - - WatchBlockDevices(names.MachineTag) state.NotifyWatcher - WatchMachine(names.MachineTag) (state.NotifyWatcher, error) - WatchModelFilesystems() state.StringsWatcher - WatchEnvironFilesystemAttachments() state.StringsWatcher - WatchMachineFilesystems(names.MachineTag) state.StringsWatcher - WatchMachineFilesystemAttachments(names.MachineTag) state.StringsWatcher - WatchModelVolumes() state.StringsWatcher - WatchEnvironVolumeAttachments() state.StringsWatcher - WatchMachineVolumes(names.MachineTag) state.StringsWatcher - WatchMachineVolumeAttachments(names.MachineTag) state.StringsWatcher - WatchVolumeAttachment(names.MachineTag, names.VolumeTag) state.NotifyWatcher - - StorageInstance(names.StorageTag) (state.StorageInstance, error) - - Filesystem(names.FilesystemTag) (state.Filesystem, error) - FilesystemAttachment(names.MachineTag, names.FilesystemTag) (state.FilesystemAttachment, error) - - Volume(names.VolumeTag) (state.Volume, error) - VolumeAttachment(names.MachineTag, names.VolumeTag) (state.VolumeAttachment, error) - VolumeAttachments(names.VolumeTag) ([]state.VolumeAttachment, error) - - RemoveFilesystem(names.FilesystemTag) error - RemoveFilesystemAttachment(names.MachineTag, names.FilesystemTag) error - RemoveVolume(names.VolumeTag) error - RemoveVolumeAttachment(names.MachineTag, names.VolumeTag) error - - SetFilesystemInfo(names.FilesystemTag, state.FilesystemInfo) error - SetFilesystemAttachmentInfo(names.MachineTag, names.FilesystemTag, state.FilesystemAttachmentInfo) error - SetVolumeInfo(names.VolumeTag, state.VolumeInfo) error - SetVolumeAttachmentInfo(names.MachineTag, names.VolumeTag, state.VolumeAttachmentInfo) error -} - -type stateShim struct { - *state.State -} - -func (s stateShim) MachineInstanceId(tag names.MachineTag) (instance.Id, error) { - m, err := s.Machine(tag.Id()) - if err != nil { - return "", errors.Trace(err) - } - return m.InstanceId() -} - -func (s stateShim) WatchMachine(tag names.MachineTag) (state.NotifyWatcher, error) { - m, err := s.Machine(tag.Id()) - if err != nil { - return nil, errors.Trace(err) - } - return m.Watch(), nil -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/storageprovisioner/storageprovisioner.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/storageprovisioner/storageprovisioner.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/storageprovisioner/storageprovisioner.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/storageprovisioner/storageprovisioner.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,6 +10,7 @@ "github.com/juju/juju/apiserver/common" "github.com/juju/juju/apiserver/common/storagecommon" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" "github.com/juju/juju/state/watcher" @@ -19,22 +20,18 @@ var logger = loggo.GetLogger("juju.apiserver.storageprovisioner") -func init() { - common.RegisterStandardFacade("StorageProvisioner", 2, NewStorageProvisionerAPI) -} - // StorageProvisionerAPI provides access to the Provisioner API facade. type StorageProvisionerAPI struct { *common.LifeGetter *common.DeadEnsurer - *common.ModelWatcher *common.InstanceIdGetter *common.StatusSetter - st provisionerState - settings poolmanager.SettingsManager - resources *common.Resources - authorizer common.Authorizer + st Backend + resources facade.Resources + authorizer facade.Authorizer + registry storage.ProviderRegistry + poolManager poolmanager.PoolManager getScopeAuthFunc common.GetAuthFunc getStorageEntityAuthFunc common.GetAuthFunc getMachineAuthFunc common.GetAuthFunc @@ -42,16 +39,14 @@ getAttachmentAuthFunc func() (func(names.MachineTag, names.Tag) bool, error) } -var getState = func(st *state.State) provisionerState { - return stateShim{st} -} - -var getSettingsManager = func(st *state.State) poolmanager.SettingsManager { - return state.NewStateSettings(st) -} - // NewStorageProvisionerAPI creates a new server-side StorageProvisionerAPI facade. -func NewStorageProvisionerAPI(st *state.State, resources *common.Resources, authorizer common.Authorizer) (*StorageProvisionerAPI, error) { +func NewStorageProvisionerAPI( + st Backend, + resources facade.Resources, + authorizer facade.Authorizer, + registry storage.ProviderRegistry, + poolManager poolmanager.PoolManager, +) (*StorageProvisionerAPI, error) { if !authorizer.AuthMachineAgent() { return nil, common.ErrPerm } @@ -159,19 +154,17 @@ return false }, nil } - stateInterface := getState(st) - settings := getSettingsManager(st) return &StorageProvisionerAPI{ - LifeGetter: common.NewLifeGetter(stateInterface, getLifeAuthFunc), - DeadEnsurer: common.NewDeadEnsurer(stateInterface, getStorageEntityAuthFunc), - ModelWatcher: common.NewModelWatcher(stateInterface, resources, authorizer), + LifeGetter: common.NewLifeGetter(st, getLifeAuthFunc), + DeadEnsurer: common.NewDeadEnsurer(st, getStorageEntityAuthFunc), InstanceIdGetter: common.NewInstanceIdGetter(st, getMachineAuthFunc), StatusSetter: common.NewStatusSetter(st, getStorageEntityAuthFunc), - st: stateInterface, - settings: settings, + st: st, resources: resources, authorizer: authorizer, + registry: registry, + poolManager: poolManager, getScopeAuthFunc: getScopeAuthFunc, getStorageEntityAuthFunc: getStorageEntityAuthFunc, getAttachmentAuthFunc: getAttachmentAuthFunc, @@ -555,7 +548,6 @@ results := params.VolumeParamsResults{ Results: make([]params.VolumeParamsResult, len(args.Entities)), } - poolManager := poolmanager.New(s.settings) one := func(arg params.Entity) (params.VolumeParams, error) { tag, err := names.ParseVolumeTag(arg.Tag) if err != nil || !canAccess(tag) { @@ -579,7 +571,9 @@ return params.VolumeParams{}, err } volumeParams, err := storagecommon.VolumeParams( - volume, storageInstance, modelCfg.UUID(), controllerCfg.ControllerUUID(), modelCfg, poolManager) + volume, storageInstance, modelCfg.UUID(), controllerCfg.ControllerUUID(), + modelCfg, s.poolManager, s.registry, + ) if err != nil { return params.VolumeParams{}, err } @@ -646,7 +640,6 @@ results := params.FilesystemParamsResults{ Results: make([]params.FilesystemParamsResult, len(args.Entities)), } - poolManager := poolmanager.New(s.settings) one := func(arg params.Entity) (params.FilesystemParams, error) { tag, err := names.ParseFilesystemTag(arg.Tag) if err != nil || !canAccess(tag) { @@ -666,7 +659,8 @@ return params.FilesystemParams{}, err } filesystemParams, err := storagecommon.FilesystemParams( - filesystem, storageInstance, modelConfig.UUID(), controllerCfg.ControllerUUID(), modelConfig, poolManager, + filesystem, storageInstance, modelConfig.UUID(), controllerCfg.ControllerUUID(), + modelConfig, s.poolManager, s.registry, ) if err != nil { return params.FilesystemParams{}, err @@ -698,7 +692,6 @@ results := params.VolumeAttachmentParamsResults{ Results: make([]params.VolumeAttachmentParamsResult, len(args.Ids)), } - poolManager := poolmanager.New(s.settings) one := func(arg params.MachineStorageId) (params.VolumeAttachmentParams, error) { volumeAttachment, err := s.oneVolumeAttachment(arg, canAccess) if err != nil { @@ -727,7 +720,7 @@ volumeId = volumeInfo.VolumeId pool = volumeInfo.Pool } - providerType, _, err := storagecommon.StoragePoolConfig(pool, poolManager) + providerType, _, err := storagecommon.StoragePoolConfig(pool, s.poolManager, s.registry) if err != nil { return params.VolumeAttachmentParams{}, errors.Trace(err) } @@ -777,7 +770,6 @@ results := params.FilesystemAttachmentParamsResults{ Results: make([]params.FilesystemAttachmentParamsResult, len(args.Ids)), } - poolManager := poolmanager.New(s.settings) one := func(arg params.MachineStorageId) (params.FilesystemAttachmentParams, error) { filesystemAttachment, err := s.oneFilesystemAttachment(arg, canAccess) if err != nil { @@ -806,7 +798,7 @@ filesystemId = filesystemInfo.FilesystemId pool = filesystemInfo.Pool } - providerType, _, err := storagecommon.StoragePoolConfig(pool, poolManager) + providerType, _, err := storagecommon.StoragePoolConfig(pool, s.poolManager, s.registry) if err != nil { return params.FilesystemAttachmentParams{}, errors.Trace(err) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/storageprovisioner/storageprovisioner_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/storageprovisioner/storageprovisioner_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/storageprovisioner/storageprovisioner_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/storageprovisioner/storageprovisioner_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -15,14 +15,15 @@ "github.com/juju/juju/apiserver/params" "github.com/juju/juju/apiserver/storageprovisioner" apiservertesting "github.com/juju/juju/apiserver/testing" + "github.com/juju/juju/environs" "github.com/juju/juju/environs/tags" "github.com/juju/juju/instance" jujutesting "github.com/juju/juju/juju/testing" "github.com/juju/juju/state" + "github.com/juju/juju/state/stateenvirons" statetesting "github.com/juju/juju/state/testing" "github.com/juju/juju/storage" - "github.com/juju/juju/storage/provider/dummy" - "github.com/juju/juju/storage/provider/registry" + "github.com/juju/juju/storage/poolmanager" "github.com/juju/juju/testing" "github.com/juju/juju/testing/factory" ) @@ -39,24 +40,6 @@ api *storageprovisioner.StorageProvisionerAPI } -func (s *provisionerSuite) SetUpSuite(c *gc.C) { - s.JujuConnSuite.SetUpSuite(c) - - registry.RegisterProvider("environscoped", &dummy.StorageProvider{ - StorageScope: storage.ScopeEnviron, - }) - registry.RegisterProvider("machinescoped", &dummy.StorageProvider{ - StorageScope: storage.ScopeMachine, - }) - registry.RegisterEnvironStorageProviders( - "dummy", "environscoped", "machinescoped", - ) - s.AddCleanup(func(c *gc.C) { - registry.RegisterProvider("environscoped", nil) - registry.RegisterProvider("machinescoped", nil) - }) -} - func (s *provisionerSuite) SetUpTest(c *gc.C) { s.JujuConnSuite.SetUpTest(c) s.factory = factory.NewFactory(s.State) @@ -66,19 +49,25 @@ s.resources = common.NewResources() s.AddCleanup(func(_ *gc.C) { s.resources.StopAll() }) - var err error + env, err := stateenvirons.GetNewEnvironFunc(environs.New)(s.State) + c.Assert(err, jc.ErrorIsNil) + registry := stateenvirons.NewStorageProviderRegistry(env) + pm := poolmanager.New(state.NewStateSettings(s.State), registry) + s.authorizer = &apiservertesting.FakeAuthorizer{ Tag: names.NewMachineTag("0"), EnvironManager: true, } - s.api, err = storageprovisioner.NewStorageProvisionerAPI(s.State, s.resources, s.authorizer) + backend := storageprovisioner.NewStateBackend(s.State) + s.api, err = storageprovisioner.NewStorageProvisionerAPI(backend, s.resources, s.authorizer, registry, pm) c.Assert(err, jc.ErrorIsNil) } func (s *provisionerSuite) TestNewStorageProvisionerAPINonMachine(c *gc.C) { tag := names.NewUnitTag("mysql/0") authorizer := &apiservertesting.FakeAuthorizer{Tag: tag} - _, err := storageprovisioner.NewStorageProvisionerAPI(s.State, common.NewResources(), authorizer) + backend := storageprovisioner.NewStateBackend(s.State) + _, err := storageprovisioner.NewStorageProvisionerAPI(backend, common.NewResources(), authorizer, nil, nil) c.Assert(err, gc.ErrorMatches, "permission denied") } @@ -1034,36 +1023,6 @@ }) } -func (s *provisionerSuite) TestWatchForModelConfigChanges(c *gc.C) { - result, err := s.api.WatchForModelConfigChanges() - c.Assert(err, jc.ErrorIsNil) - c.Assert(result.NotifyWatcherId, gc.Equals, "1") - - // Verify the resource was registered and stop it when done. - c.Assert(s.resources.Count(), gc.Equals, 1) - watcher := s.resources.Get("1") - defer statetesting.AssertStop(c, watcher) - - // Check that the Watch has consumed the initial events ("returned" in - // the Watch call) - wc := statetesting.NewNotifyWatcherC(c, s.State, watcher.(state.NotifyWatcher)) - wc.AssertNoChange() - - // Updating config should trigger the watcher. - err = s.State.UpdateModelConfig(map[string]interface{}{"what": "ever"}, nil, nil) - c.Assert(err, jc.ErrorIsNil) - wc.AssertOneChange() -} - -func (s *provisionerSuite) TestModelConfig(c *gc.C) { - stateModelConfig, err := s.State.ModelConfig() - c.Assert(err, jc.ErrorIsNil) - - result, err := s.api.ModelConfig() - c.Assert(err, jc.ErrorIsNil) - c.Assert(result.Config, jc.DeepEquals, params.ModelConfig(stateModelConfig.AllAttrs())) -} - func (s *provisionerSuite) TestRemoveVolumesEnvironManager(c *gc.C) { s.setupVolumes(c) args := params.Entities{Entities: []params.Entity{ diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/subnets/subnets.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/subnets/subnets.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/subnets/subnets.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/subnets/subnets.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,6 +9,7 @@ "github.com/juju/juju/apiserver/common" "github.com/juju/juju/apiserver/common/networkingcommon" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" ) @@ -38,19 +39,19 @@ // subnetsAPI implements the SubnetsAPI interface. type subnetsAPI struct { backing networkingcommon.NetworkBacking - resources *common.Resources - authorizer common.Authorizer + resources facade.Resources + authorizer facade.Authorizer } // NewAPI creates a new Subnets API server-side facade with a // state.State backing. -func NewAPI(st *state.State, res *common.Resources, auth common.Authorizer) (SubnetsAPI, error) { +func NewAPI(st *state.State, res facade.Resources, auth facade.Authorizer) (SubnetsAPI, error) { return newAPIWithBacking(networkingcommon.NewStateShim(st), res, auth) } // newAPIWithBacking creates a new server-side Subnets API facade with // a common.NetworkBacking -func newAPIWithBacking(backing networkingcommon.NetworkBacking, resources *common.Resources, authorizer common.Authorizer) (SubnetsAPI, error) { +func newAPIWithBacking(backing networkingcommon.NetworkBacking, resources facade.Resources, authorizer facade.Authorizer) (SubnetsAPI, error) { // Only clients can access the Subnets facade. if !authorizer.AuthClient() { return nil, common.ErrPerm diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/subnets/subnets_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/subnets/subnets_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/subnets/subnets_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/subnets/subnets_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -148,6 +148,7 @@ apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub, apiservertesting.BackingCall("AvailabilityZones"), apiservertesting.BackingCall("ModelConfig"), + apiservertesting.BackingCall("CloudSpec"), apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), apiservertesting.ZonedEnvironCall("AvailabilityZones"), apiservertesting.BackingCall("SetAvailabilityZones", apiservertesting.ProviderInstance.Zones), @@ -159,6 +160,7 @@ apiservertesting.SharedStub.SetErrors( nil, // Backing.AvailabilityZones nil, // Backing.ModelConfig + nil, // Backing.CloudSpec nil, // Provider.Open nil, // ZonedEnviron.AvailabilityZones errors.NotSupportedf("setting"), // Backing.SetAvailabilityZones @@ -175,6 +177,7 @@ apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub, apiservertesting.BackingCall("AvailabilityZones"), apiservertesting.BackingCall("ModelConfig"), + apiservertesting.BackingCall("CloudSpec"), apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), apiservertesting.ZonedEnvironCall("AvailabilityZones"), apiservertesting.BackingCall("SetAvailabilityZones", apiservertesting.ProviderInstance.Zones), @@ -186,6 +189,7 @@ apiservertesting.SharedStub.SetErrors( nil, // Backing.AvailabilityZones nil, // Backing.ModelConfig + nil, // Backing.CloudSpec nil, // Provider.Open errors.NotValidf("foo"), // ZonedEnviron.AvailabilityZones ) @@ -201,6 +205,7 @@ apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub, apiservertesting.BackingCall("AvailabilityZones"), apiservertesting.BackingCall("ModelConfig"), + apiservertesting.BackingCall("CloudSpec"), apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), apiservertesting.ZonedEnvironCall("AvailabilityZones"), ) @@ -232,6 +237,7 @@ apiservertesting.SharedStub.SetErrors( nil, // Backing.AvailabilityZones nil, // Backing.ModelConfig + nil, // Backing.CloudSpec errors.NotValidf("config"), // Provider.Open ) @@ -246,6 +252,7 @@ apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub, apiservertesting.BackingCall("AvailabilityZones"), apiservertesting.BackingCall("ModelConfig"), + apiservertesting.BackingCall("CloudSpec"), apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), ) } @@ -265,6 +272,7 @@ apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub, apiservertesting.BackingCall("AvailabilityZones"), apiservertesting.BackingCall("ModelConfig"), + apiservertesting.BackingCall("CloudSpec"), apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), ) } @@ -453,15 +461,18 @@ // caching subnets (2nd attepmt): fails nil, // BackingInstance.ModelConfig (2nd call) + nil, // BackingInstance.CloudSpec (1st call) errors.NotFoundf("provider"), // ProviderInstance.Open (1st call) // caching subnets (3rd attempt): fails nil, // BackingInstance.ModelConfig (3rd call) + nil, // BackingInstance.CloudSpec (2nd call) nil, // ProviderInstance.Open (2nd call) errors.NotFoundf("subnets"), // NetworkingEnvironInstance.Subnets (1st call) // caching subnets (4th attempt): succeeds nil, // BackingInstance.ModelConfig (4th call) + nil, // BackingInstance.CloudSpec (3rd call) nil, // ProviderInstance.Open (3rd call) nil, // NetworkingEnvironInstance.Subnets (2nd call) @@ -571,15 +582,18 @@ // caching subnets (2nd attepmt): fails apiservertesting.BackingCall("ModelConfig"), + apiservertesting.BackingCall("CloudSpec"), apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), // caching subnets (3rd attempt): fails apiservertesting.BackingCall("ModelConfig"), + apiservertesting.BackingCall("CloudSpec"), apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), apiservertesting.NetworkingEnvironCall("Subnets", instance.UnknownId, []network.Id(nil)), // caching subnets (4th attempt): succeeds apiservertesting.BackingCall("ModelConfig"), + apiservertesting.BackingCall("CloudSpec"), apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), apiservertesting.NetworkingEnvironCall("Subnets", instance.UnknownId, []network.Id(nil)), @@ -619,6 +633,7 @@ // These calls always happen. expectedCalls := []apiservertesting.StubMethodCall{ apiservertesting.BackingCall("ModelConfig"), + apiservertesting.BackingCall("CloudSpec"), apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), } @@ -677,6 +692,7 @@ // updateZones tries to constructs a ZonedEnviron with these calls. zoneCalls := append([]apiservertesting.StubMethodCall{}, apiservertesting.BackingCall("ModelConfig"), + apiservertesting.BackingCall("CloudSpec"), apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), ) // Receiver can differ according to envName, but diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/testing/fakeauthorizer.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/testing/fakeauthorizer.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/testing/fakeauthorizer.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/testing/fakeauthorizer.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,9 +5,11 @@ import ( "gopkg.in/juju/names.v2" + + "github.com/juju/juju/core/description" ) -// FakeAuthorizer implements the common.Authorizer interface. +// FakeAuthorizer implements the facade.Authorizer interface. type FakeAuthorizer struct { Tag names.Tag EnvironManager bool @@ -43,3 +45,8 @@ func (fa FakeAuthorizer) GetAuthTag() names.Tag { return fa.Tag } + +func (fa FakeAuthorizer) HasPermission(operation description.Access, target names.Tag) (bool, error) { + // TODO(perrito666) provide a way to pre-set the desired result here. + return fa.Tag == target, nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/testing/stub_network.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/testing/stub_network.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/testing/stub_network.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/testing/stub_network.go 2016-08-16 08:56:25.000000000 +0000 @@ -20,6 +20,7 @@ "github.com/juju/utils" "github.com/juju/utils/set" gc "gopkg.in/check.v1" + names "gopkg.in/juju/names.v2" ) type StubNetwork struct { @@ -336,6 +337,7 @@ *testing.Stub EnvConfig *config.Config + Cloud environs.CloudSpec Zones []providercommon.AvailabilityZone Spaces []networkingcommon.BackingSpace @@ -368,6 +370,12 @@ "name": envName, } sb.EnvConfig = coretesting.CustomModelConfig(c, extraAttrs) + sb.Cloud = environs.CloudSpec{ + Type: StubProviderType, + Name: "cloud-name", + Endpoint: "endpoint", + StorageEndpoint: "storage-endpoint", + } sb.Zones = []providercommon.AvailabilityZone{} if withZones { sb.Zones = make([]providercommon.AvailabilityZone, len(ProviderInstance.Zones)) @@ -426,6 +434,14 @@ return sb.EnvConfig, nil } +func (sb *StubBacking) CloudSpec(names.ModelTag) (environs.CloudSpec, error) { + sb.MethodCall(sb, "CloudSpec") + if err := sb.NextErr(); err != nil { + return environs.CloudSpec{}, err + } + return sb.Cloud, nil +} + func (sb *StubBacking) AvailabilityZones() ([]providercommon.AvailabilityZone, error) { sb.MethodCall(sb, "AvailabilityZones") if err := sb.NextErr(); err != nil { @@ -515,12 +531,12 @@ var _ environs.EnvironProvider = (*StubProvider)(nil) -func (sp *StubProvider) Open(cfg *config.Config) (environs.Environ, error) { - sp.MethodCall(sp, "Open", cfg) +func (sp *StubProvider) Open(args environs.OpenParams) (environs.Environ, error) { + sp.MethodCall(sp, "Open", args.Config) if err := sp.NextErr(); err != nil { return nil, err } - switch cfg.Name() { + switch args.Config.Name() { case StubEnvironName: return EnvironInstance, nil case StubZonedEnvironName: @@ -530,7 +546,7 @@ case StubZonedNetworkingEnvironName: return ZonedNetworkingEnvironInstance, nil } - panic("unexpected model name: " + cfg.Name()) + panic("unexpected model name: " + args.Config.Name()) } // GoString implements fmt.GoStringer. diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/tools.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/tools.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/tools.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/tools.go 2016-08-16 08:56:25.000000000 +0000 @@ -23,6 +23,7 @@ envtools "github.com/juju/juju/environs/tools" "github.com/juju/juju/state" "github.com/juju/juju/state/binarystorage" + "github.com/juju/juju/state/stateenvirons" "github.com/juju/juju/tools" ) @@ -119,7 +120,8 @@ // in simplestreams and GETting it, caching the result in tools storage before returning // to the caller. func (h *toolsDownloadHandler) fetchAndCacheTools(v version.Binary, stor binarystorage.Storage, st *state.State) (io.ReadCloser, error) { - env, err := environs.GetEnviron(st, environs.New) + newEnviron := stateenvirons.GetNewEnvironFunc(environs.New) + env, err := newEnviron(st) if err != nil { return nil, err } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/undertaker/undertaker.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/undertaker/undertaker.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/undertaker/undertaker.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/undertaker/undertaker.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,6 +8,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" "github.com/juju/juju/state/watcher" @@ -20,16 +21,16 @@ // UndertakerAPI implements the API used by the machine undertaker worker. type UndertakerAPI struct { st State - resources *common.Resources + resources facade.Resources *common.StatusSetter } // NewUndertakerAPI creates a new instance of the undertaker API. -func NewUndertakerAPI(st *state.State, resources *common.Resources, authorizer common.Authorizer) (*UndertakerAPI, error) { +func NewUndertakerAPI(st *state.State, resources facade.Resources, authorizer facade.Authorizer) (*UndertakerAPI, error) { return newUndertakerAPI(&stateShim{st}, resources, authorizer) } -func newUndertakerAPI(st State, resources *common.Resources, authorizer common.Authorizer) (*UndertakerAPI, error) { +func newUndertakerAPI(st State, resources facade.Resources, authorizer facade.Authorizer) (*UndertakerAPI, error) { if !authorizer.AuthMachineAgent() || !authorizer.AuthModelManager() { return nil, common.ErrPerm } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/unitassigner/unitassigner.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/unitassigner/unitassigner.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/unitassigner/unitassigner.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/unitassigner/unitassigner.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,6 +8,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" "github.com/juju/juju/state/watcher" @@ -31,12 +32,12 @@ // API implements the functionality for assigning units to machines. type API struct { st assignerState - res *common.Resources + res facade.Resources statusSetter statusSetter } // New returns a new unitAssigner api instance. -func New(st *state.State, res *common.Resources, _ common.Authorizer) (*API, error) { +func New(st *state.State, res facade.Resources, _ facade.Authorizer) (*API, error) { setter := common.NewStatusSetter(&common.UnitAgentFinder{st}, common.AuthAlways()) return &API{ st: st, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/uniter/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/uniter/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/uniter/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/uniter/export_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,6 +5,7 @@ import ( "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/meterstatus" ) @@ -18,7 +19,7 @@ func NewStorageAPI( st StorageStateInterface, - resources *common.Resources, + resources facade.Resources, accessUnit common.GetAuthFunc, ) (*StorageAPI, error) { return newStorageAPI(storageStateInterface(st), resources, accessUnit) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/uniter/storage.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/uniter/storage.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/uniter/storage.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/uniter/storage.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,6 +9,7 @@ "github.com/juju/juju/apiserver/common" "github.com/juju/juju/apiserver/common/storagecommon" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" "github.com/juju/juju/state/watcher" @@ -17,14 +18,14 @@ // StorageAPI provides access to the Storage API facade. type StorageAPI struct { st storageStateInterface - resources *common.Resources + resources facade.Resources accessUnit common.GetAuthFunc } // newStorageAPI creates a new server-side Storage API facade. func newStorageAPI( st storageStateInterface, - resources *common.Resources, + resources facade.Resources, accessUnit common.GetAuthFunc, ) (*StorageAPI, error) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/uniter/uniter.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/uniter/uniter.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/uniter/uniter.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/uniter/uniter.go 2016-08-16 08:56:25.000000000 +0000 @@ -14,6 +14,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" leadershipapiserver "github.com/juju/juju/apiserver/leadership" "github.com/juju/juju/apiserver/meterstatus" "github.com/juju/juju/apiserver/params" @@ -43,8 +44,8 @@ meterstatus.MeterStatus st *state.State - auth common.Authorizer - resources *common.Resources + auth facade.Authorizer + resources facade.Resources accessUnit common.GetAuthFunc accessService common.GetAuthFunc unit *state.Unit @@ -53,7 +54,7 @@ } // NewUniterAPIV4 creates a new instance of the Uniter API, version 3. -func NewUniterAPIV4(st *state.State, resources *common.Resources, authorizer common.Authorizer) (*UniterAPIV3, error) { +func NewUniterAPIV4(st *state.State, resources facade.Resources, authorizer facade.Authorizer) (*UniterAPIV3, error) { if !authorizer.AuthUnitAgent() { return nil, common.ErrPerm } @@ -1474,8 +1475,8 @@ func leadershipSettingsAccessorFactory( st *state.State, - resources *common.Resources, - auth common.Authorizer, + resources facade.Resources, + auth facade.Authorizer, ) *leadershipapiserver.LeadershipSettingsAccessor { registerWatcher := func(serviceId string) (string, error) { service, err := st.Application(serviceId) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/upgrader/unitupgrader.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/upgrader/unitupgrader.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/upgrader/unitupgrader.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/upgrader/unitupgrader.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,6 +8,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" "github.com/juju/juju/state/watcher" @@ -19,15 +20,15 @@ *common.ToolsSetter st *state.State - resources *common.Resources - authorizer common.Authorizer + resources facade.Resources + authorizer facade.Authorizer } // NewUnitUpgraderAPI creates a new server-side UnitUpgraderAPI facade. func NewUnitUpgraderAPI( st *state.State, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, ) (*UnitUpgraderAPI, error) { if !authorizer.AuthUnitAgent() { return nil, common.ErrPerm diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/upgrader/upgrader.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/upgrader/upgrader.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/upgrader/upgrader.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/upgrader/upgrader.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,9 +10,11 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/environs/config" "github.com/juju/juju/state" + "github.com/juju/juju/state/stateenvirons" "github.com/juju/juju/state/watcher" jujuversion "github.com/juju/juju/version" ) @@ -28,7 +30,7 @@ // returned depends on who is calling. // Both of them conform to the exact Upgrader API, so the actual calls that are // available do not depend on who is currently connected. -func upgraderFacade(st *state.State, resources *common.Resources, auth common.Authorizer) (Upgrader, error) { +func upgraderFacade(st *state.State, resources facade.Resources, auth facade.Authorizer) (Upgrader, error) { // The type of upgrader we return depends on who is asking. // Machines get an UpgraderAPI, units get a UnitUpgraderAPI. // This is tested in the api/upgrader package since there @@ -61,15 +63,15 @@ *common.ToolsSetter st *state.State - resources *common.Resources - authorizer common.Authorizer + resources facade.Resources + authorizer facade.Authorizer } // NewUpgraderAPI creates a new server-side UpgraderAPI facade. func NewUpgraderAPI( st *state.State, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, ) (*UpgraderAPI, error) { if !authorizer.AuthMachineAgent() { return nil, common.ErrPerm @@ -82,8 +84,9 @@ return nil, err } urlGetter := common.NewToolsURLGetter(env.UUID(), st) + configGetter := stateenvirons.EnvironConfigGetter{st} return &UpgraderAPI{ - ToolsGetter: common.NewToolsGetter(st, st, st, urlGetter, getCanReadWrite), + ToolsGetter: common.NewToolsGetter(st, configGetter, st, urlGetter, getCanReadWrite), ToolsSetter: common.NewToolsSetter(st, getCanReadWrite), st: st, resources: resources, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/usermanager/usermanager.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/usermanager/usermanager.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/usermanager/usermanager.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/usermanager/usermanager.go 2016-08-16 08:56:25.000000000 +0000 @@ -13,6 +13,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/modelmanager" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" @@ -28,7 +29,7 @@ // implementation of the api end point. type UserManagerAPI struct { state *state.State - authorizer common.Authorizer + authorizer facade.Authorizer createLocalLoginMacaroon func(names.UserTag) (*macaroon.Macaroon, error) check *common.BlockChecker apiUser names.UserTag @@ -37,8 +38,8 @@ func NewUserManagerAPI( st *state.State, - resources *common.Resources, - authorizer common.Authorizer, + resources facade.Resources, + authorizer facade.Authorizer, ) (*UserManagerAPI, error) { if !authorizer.AuthClient() { return nil, common.ErrPerm @@ -76,9 +77,8 @@ // AddUser adds a user with a username, and either a password or // a randomly generated secret key which will be returned. func (api *UserManagerAPI) AddUser(args params.AddUsers) (params.AddUserResults, error) { - result := params.AddUserResults{ - Results: make([]params.AddUserResult, len(args.Users)), - } + var result params.AddUserResults + if err := api.check.ChangeAllowed(); err != nil { return result, errors.Trace(err) } @@ -86,8 +86,16 @@ if len(args.Users) == 0 { return result, nil } + + // Create the results list to populate. + result.Results = make([]params.AddUserResult, len(args.Users)) + + // Make sure we have admin. If not fail each of the requests and return w/o a top level error. if !api.isAdmin { - return result, common.ErrPerm + for i, _ := range result.Results { + result.Results[i].Error = common.ServerError(common.ErrPerm) + } + return result, nil } for i, arg := range args.Users { @@ -138,6 +146,63 @@ return result, nil } +// RemoveUser permanently removes a user from the current controller for each +// entity provided. While the user is permanently removed we keep it's +// information around for auditing purposes. +// TODO(redir): Add information about getting deleted user information when we +// add that capability. +func (api *UserManagerAPI) RemoveUser(entities params.Entities) (params.ErrorResults, error) { + var deletions params.ErrorResults + + if err := api.check.ChangeAllowed(); err != nil { + return deletions, errors.Trace(err) + } + + // Get a handle on the controller model. + controllerModel, err := api.state.ControllerModel() + if err != nil { + return deletions, errors.Trace(err) + } + + // Create the results list to populate. + deletions.Results = make([]params.ErrorResult, len(entities.Entities)) + + // Make sure we have admin. If not fail each of the requests and return w/o a top level error. + if !api.isAdmin { + for i, _ := range deletions.Results { + deletions.Results[i].Error = common.ServerError(common.ErrPerm) + } + return deletions, nil + } + + // Remove the entities. + for i, e := range entities.Entities { + user, err := names.ParseUserTag(e.Tag) + if err != nil { + deletions.Results[i].Error = common.ServerError(err) + continue + } + + if controllerModel.Owner().Id() == user.Id() { + deletions.Results[i].Error = common.ServerError( + errors.Errorf("cannot delete controller owner %q", user.Name())) + continue + } + err = api.state.RemoveUser(user) + if err != nil { + if errors.IsUserNotFound(err) { + deletions.Results[i].Error = common.ServerError(err) + } else { + deletions.Results[i].Error = common.ServerError( + errors.Annotatef(err, "failed to delete user %q", user.Name())) + } + continue + } + deletions.Results[i].Error = nil + } + return deletions, nil +} + func (api *UserManagerAPI) getUser(tag string) (*state.User, error) { userTag, err := names.ParseUserTag(tag) if err != nil { @@ -151,7 +216,7 @@ } // EnableUser enables one or more users. If the user is already enabled, -// the action is consided a success. +// the action is considered a success. func (api *UserManagerAPI) EnableUser(users params.Entities) (params.ErrorResults, error) { if err := api.check.ChangeAllowed(); err != nil { return params.ErrorResults{}, errors.Trace(err) @@ -160,7 +225,7 @@ } // DisableUser disables one or more users. If the user is already disabled, -// the action is consided a success. +// the action is considered a success. func (api *UserManagerAPI) DisableUser(users params.Entities) (params.ErrorResults, error) { if err := api.check.ChangeAllowed(); err != nil { return params.ErrorResults{}, errors.Trace(err) @@ -169,14 +234,20 @@ } func (api *UserManagerAPI) enableUserImpl(args params.Entities, action string, method func(*state.User) error) (params.ErrorResults, error) { - result := params.ErrorResults{ - Results: make([]params.ErrorResult, len(args.Entities)), - } + var result params.ErrorResults + if len(args.Entities) == 0 { return result, nil } + + // Create the results list to populate. + result.Results = make([]params.ErrorResult, len(args.Entities)) + if !api.isAdmin { - return result, common.ErrPerm + for i, _ := range result.Results { + result.Results[i].Error = common.ServerError(common.ErrPerm) + } + return result, nil } for i, arg := range args.Entities { @@ -196,6 +267,7 @@ // UserInfo returns information on a user. func (api *UserManagerAPI) UserInfo(request params.UserInfoRequest) (params.UserInfoResults, error) { var results params.UserInfoResults + var infoForUser = func(user *state.User) params.UserInfoResult { var lastLogin *time.Time userLastLogin, err := user.LastLogin() @@ -230,6 +302,7 @@ return results, nil } + // Create the results list to populate. results.Results = make([]params.UserInfoResult, argCount) for i, arg := range request.Entities { user, err := api.getUser(arg.Tag) @@ -248,12 +321,15 @@ if err := api.check.ChangeAllowed(); err != nil { return params.ErrorResults{}, errors.Trace(err) } - result := params.ErrorResults{ - Results: make([]params.ErrorResult, len(args.Changes)), - } + + var result params.ErrorResults + if len(args.Changes) == 0 { return result, nil } + + // Create the results list to populate. + result.Results = make([]params.ErrorResult, len(args.Changes)) for i, arg := range args.Changes { if err := api.setPassword(arg); err != nil { result.Results[i].Error = common.ServerError(err) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/usermanager/usermanager_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/usermanager/usermanager_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/usermanager/usermanager_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/usermanager/usermanager_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,6 +4,7 @@ package usermanager_test import ( + "fmt" "time" "github.com/juju/errors" @@ -17,6 +18,7 @@ "github.com/juju/juju/apiserver/params" apiservertesting "github.com/juju/juju/apiserver/testing" "github.com/juju/juju/apiserver/usermanager" + "github.com/juju/juju/core/description" jujutesting "github.com/juju/juju/juju/testing" "github.com/juju/juju/state" "github.com/juju/juju/testing/factory" @@ -70,7 +72,7 @@ c.Assert(err, gc.ErrorMatches, "permission denied") } -func (s *userManagerSuite) assertAddUser(c *gc.C, access params.ModelAccessPermission, sharedModelTags []string) { +func (s *userManagerSuite) assertAddUser(c *gc.C, access params.UserAccessPermission, sharedModelTags []string) { sharedModelState := s.Factory.MakeModel(c, nil) defer sharedModelState.Close() @@ -99,7 +101,7 @@ } func (s *userManagerSuite) TestAddUser(c *gc.C) { - s.assertAddUser(c, params.ModelAccessPermission(""), nil) + s.assertAddUser(c, params.UserAccessPermission(""), nil) } func (s *userManagerSuite) TestAddUserWithSecretKey(c *gc.C) { @@ -141,7 +143,7 @@ s.addUserWithSharedModel(c, params.ModelWriteAccess) } -func (s *userManagerSuite) addUserWithSharedModel(c *gc.C, access params.ModelAccessPermission) { +func (s *userManagerSuite) addUserWithSharedModel(c *gc.C, access params.UserAccessPermission) { sharedModelState := s.Factory.MakeModel(c, nil) defer sharedModelState.Close() @@ -154,11 +156,11 @@ c.Assert(err, jc.ErrorIsNil) var modelUserTags = make([]names.UserTag, len(users)) for i, u := range users { - modelUserTags[i] = u.UserTag() - if u.UserName() == "foobar" { - c.Assert(u.IsReadOnly(), gc.Equals, access == params.ModelReadAccess) - } else if u.UserName() == "admin" { - c.Assert(u.IsReadOnly(), gc.Equals, false) + modelUserTags[i] = u.UserTag + if u.UserName == "foobar" { + c.Assert(u.Access, gc.Equals, description.ReadAccess) + } else if u.UserName == "admin" { + c.Assert(u.Access, gc.Equals, description.AdminAccess) } } c.Assert(modelUserTags, jc.SameContents, []names.UserTag{ @@ -177,13 +179,13 @@ s.BlockAllChanges(c, "TestBlockAddUser") result, err := s.usermanager.AddUser(args) - // Check that the call is blocked + // Check that the call is blocked. s.AssertBlocked(c, err, "TestBlockAddUser") - c.Assert(result.Results, gc.HasLen, 1) - //check that user is not created + // Check that there's no results. + c.Assert(result.Results, gc.HasLen, 0) + //check that user is not created. foobarTag := names.NewLocalUserTag("foobar") - c.Assert(result.Results[0], gc.DeepEquals, params.AddUserResult{}) - // Check that the call results in a new user being created + // Check that the call results in a new user being created. _, err = s.State.User(foobarTag) c.Assert(err, gc.ErrorMatches, `user "foobar" not found`) } @@ -201,8 +203,14 @@ Password: "password", }}} - _, err = usermanager.AddUser(args) - c.Assert(err, gc.ErrorMatches, "permission denied") + got, err := usermanager.AddUser(args) + + for _, result := range got.Results { + c.Check(errors.Cause(result.Error), jc.DeepEquals, + ¶ms.Error{Message: "permission denied", Code: "unauthorized access"}) + } + c.Assert(got.Results, gc.HasLen, 1) + c.Assert(err, jc.ErrorIsNil) _, err = s.State.User(names.NewLocalUserTag("foobar")) c.Assert(err, jc.Satisfies, errors.IsNotFound) @@ -351,8 +359,12 @@ args := params.Entities{ []params.Entity{{barb.Tag().String()}}, } - _, err = usermanager.DisableUser(args) - c.Assert(err, gc.ErrorMatches, "permission denied") + got, err := usermanager.DisableUser(args) + for _, result := range got.Results { + c.Check(errors.Cause(result.Error), jc.DeepEquals, ¶ms.Error{Message: "permission denied", Code: "unauthorized access"}) + } + c.Assert(got.Results, gc.HasLen, 1) + c.Assert(err, jc.ErrorIsNil) err = barb.Refresh() c.Assert(err, jc.ErrorIsNil) @@ -370,8 +382,12 @@ args := params.Entities{ []params.Entity{{barb.Tag().String()}}, } - _, err = usermanager.EnableUser(args) - c.Assert(err, gc.ErrorMatches, "permission denied") + got, err := usermanager.EnableUser(args) + for _, result := range got.Results { + c.Check(errors.Cause(result.Error), jc.DeepEquals, ¶ms.Error{Message: "permission denied", Code: "unauthorized access"}) + } + c.Assert(got.Results, gc.HasLen, 1) + c.Assert(err, jc.ErrorIsNil) err = barb.Refresh() c.Assert(err, jc.ErrorIsNil) @@ -590,3 +606,198 @@ c.Assert(barb.PasswordValid("new-password"), jc.IsFalse) } + +func (s *userManagerSuite) TestRemoveUserBadTag(c *gc.C) { + tag := "not-a-tag" + got, err := s.usermanager.RemoveUser(params.Entities{ + Entities: []params.Entity{{Tag: tag}}}) + c.Assert(got.Results, gc.HasLen, 1) + c.Assert(err, gc.Equals, nil) + c.Check(got.Results[0].Error, jc.DeepEquals, ¶ms.Error{ + Message: "\"not-a-tag\" is not a valid tag", + }) +} + +func (s *userManagerSuite) TestRemoveUserNonExistent(c *gc.C) { + tag := "user-harvey" + got, err := s.usermanager.RemoveUser(params.Entities{ + Entities: []params.Entity{{Tag: tag}}}) + c.Assert(got.Results, gc.HasLen, 1) + c.Assert(err, gc.Equals, nil) + c.Check(got.Results[0].Error, jc.DeepEquals, ¶ms.Error{ + Message: "failed to delete user \"harvey\": user \"harvey\" not found", + Code: "not found", + }) +} + +func (s *userManagerSuite) TestRemoveUser(c *gc.C) { + // Create a user to delete. + jjam := s.Factory.MakeUser(c, &factory.UserParams{Name: "jimmyjam"}) + + expectedError := fmt.Sprintf("%q user not found", jjam.Name()) + + // Remove the user + got, err := s.usermanager.RemoveUser(params.Entities{ + Entities: []params.Entity{{Tag: jjam.Tag().String()}}}) + c.Assert(got.Results, gc.HasLen, 1) + + c.Check(got.Results[0].Error, gc.IsNil) // Uses gc.IsNil as it's a typed nil. + c.Assert(err, jc.ErrorIsNil) + + // Check if deleted. + err = jjam.Refresh() + c.Check(err, jc.ErrorIsNil) + c.Assert(jjam.IsDeleted(), jc.IsTrue) + + // Try again and verify we get the expected error. + got, err = s.usermanager.RemoveUser(params.Entities{ + Entities: []params.Entity{{Tag: jjam.Tag().String()}}}) + c.Check(got.Results, gc.HasLen, 1) + c.Check(got.Results[0].Error, jc.DeepEquals, ¶ms.Error{ + Message: expectedError, + Code: "user not found", + }) + c.Assert(err, jc.ErrorIsNil) +} + +func (s *userManagerSuite) TestRemoveUserAsNormalUser(c *gc.C) { + // Create a user to delete. + jjam := s.Factory.MakeUser(c, &factory.UserParams{Name: "jimmyjam"}) + // Create a user to delete jjam. + chuck := s.Factory.MakeUser(c, &factory.UserParams{ + Name: "chuck", + NoModelUser: true, + }) + + // Authenticate as chuck. + usermanager, err := usermanager.NewUserManagerAPI( + s.State, s.resources, apiservertesting.FakeAuthorizer{ + Tag: chuck.Tag(), + }) + c.Assert(err, jc.ErrorIsNil) + + // Make sure the user exists. + ui, err := s.usermanager.UserInfo(params.UserInfoRequest{ + Entities: []params.Entity{{Tag: jjam.Tag().String()}}, + }) + c.Check(err, jc.ErrorIsNil) + c.Check(ui.Results, gc.HasLen, 1) + c.Assert(ui.Results[0].Result.Username, gc.DeepEquals, jjam.Name()) + + // Remove jjam as chuck and fail. + got, err := usermanager.RemoveUser(params.Entities{ + Entities: []params.Entity{{Tag: jjam.Tag().String()}}}) + c.Check(got.Results, gc.HasLen, 1) + c.Check(errors.Cause(got.Results[0].Error), jc.DeepEquals, + ¶ms.Error{Message: "permission denied", Code: "unauthorized access"}) + c.Assert(err, jc.ErrorIsNil) + + // Make sure jjam is still around. + err = jjam.Refresh() + c.Assert(err, jc.ErrorIsNil) +} + +func (s *userManagerSuite) TestRemoveUserSelfAsNormalUser(c *gc.C) { + // Create a user to delete. + jjam := s.Factory.MakeUser(c, &factory.UserParams{ + Name: "jimmyjam", + NoModelUser: true, + }) + usermanager, err := usermanager.NewUserManagerAPI( + s.State, s.resources, apiservertesting.FakeAuthorizer{ + Tag: jjam.Tag(), + }) + c.Assert(err, jc.ErrorIsNil) + + // Make sure the user exists. + ui, err := s.usermanager.UserInfo(params.UserInfoRequest{ + Entities: []params.Entity{{Tag: jjam.Tag().String()}}, + }) + c.Assert(err, jc.ErrorIsNil) + c.Check(ui.Results, gc.HasLen, 1) + c.Assert(ui.Results[0].Result.Username, gc.DeepEquals, jjam.Name()) + + // Remove the user as the user + got, err := usermanager.RemoveUser(params.Entities{ + Entities: []params.Entity{{Tag: jjam.Tag().String()}}}) + c.Assert(got.Results, gc.HasLen, 1) + c.Check(errors.Cause(got.Results[0].Error), jc.DeepEquals, + ¶ms.Error{Message: "permission denied", Code: "unauthorized access"}) + c.Assert(err, jc.ErrorIsNil) + + // Check if deleted. + err = jjam.Refresh() + c.Assert(err, jc.ErrorIsNil) +} + +func (s *userManagerSuite) TestRemoveUserAsSelfAdmin(c *gc.C) { + + expectedError := "cannot delete controller owner \"admin\"" + + // Remove admin as admin. + got, err := s.usermanager.RemoveUser(params.Entities{ + Entities: []params.Entity{{Tag: s.AdminUserTag(c).String()}}}) + c.Assert(got.Results, gc.HasLen, 1) + c.Check(got.Results[0].Error, jc.DeepEquals, ¶ms.Error{ + Message: expectedError, + }) + c.Assert(err, jc.ErrorIsNil) + + // Try again to see if we succeeded. + got, err = s.usermanager.RemoveUser(params.Entities{ + Entities: []params.Entity{{Tag: s.AdminUserTag(c).String()}}}) + c.Assert(got.Results, gc.HasLen, 1) + c.Check(got.Results[0].Error, jc.DeepEquals, ¶ms.Error{ + Message: expectedError, + }) + c.Assert(err, jc.ErrorIsNil) + + ui, err := s.usermanager.UserInfo(params.UserInfoRequest{}) + c.Check(err, jc.ErrorIsNil) + c.Assert(ui.Results, gc.HasLen, 1) + +} + +func (s *userManagerSuite) TestRemoveUserBulkSharedModels(c *gc.C) { + // Create users. + jjam := s.Factory.MakeUser(c, &factory.UserParams{ + Name: "jimmyjam", + }) + alice := s.Factory.MakeUser(c, &factory.UserParams{ + Name: "alice", + }) + bob := s.Factory.MakeUser(c, &factory.UserParams{ + Name: "bob", + }) + + // Get a handle on the current model. + model, err := s.State.Model() + c.Assert(err, jc.ErrorIsNil) + users, err := model.Users() + + // Make sure the users exist. + var userNames []string + for _, u := range users { + userNames = append(userNames, u.UserTag.Name()) + } + c.Assert(userNames, jc.SameContents, []string{"admin", jjam.Name(), alice.Name(), bob.Name()}) + + // Remove 2 users. + got, err := s.usermanager.RemoveUser(params.Entities{ + Entities: []params.Entity{ + {Tag: jjam.Tag().String()}, + {Tag: alice.Tag().String()}, + }}) + c.Check(got.Results, gc.HasLen, 2) + var paramErr *params.Error + c.Check(got.Results[0].Error, jc.DeepEquals, paramErr) + c.Check(got.Results[1].Error, jc.DeepEquals, paramErr) + c.Assert(err, jc.ErrorIsNil) + + // Make sure users were deleted. + err = jjam.Refresh() + c.Assert(jjam.IsDeleted(), jc.IsTrue) + err = alice.Refresh() + c.Assert(alice.IsDeleted(), jc.IsTrue) + +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/utils.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/utils.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/utils.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/utils.go 2016-08-16 08:56:25.000000000 +0000 @@ -45,12 +45,10 @@ func validateModelUUID(args validateArgs) (string, error) { ssState := args.statePool.SystemState() if args.modelUUID == "" { - // We allow the modelUUID to be empty for 2 cases - // 1) Compatibility with older clients - // 2) TODO: server a limited API at the root (empty modelUUID) - // with just the user manager and model manager - // if the connection comes over a sufficiently up to date - // login command. + // We allow the modelUUID to be empty so that: + // TODO: server a limited API at the root (empty modelUUID) + // just the user manager and model manager are able to accept + // requests that don't require a modelUUID, like add-model. if args.strict { return "", errors.Trace(common.UnknownModelError(args.modelUUID)) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/watcher.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/watcher.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/watcher.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/watcher.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,6 +10,7 @@ "github.com/juju/juju/apiserver/common" "github.com/juju/juju/apiserver/common/storagecommon" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/controller" "github.com/juju/juju/core/migration" @@ -62,11 +63,14 @@ // NewAllWatcher returns a new API server endpoint for interacting // with a watcher created by the WatchAll and WatchAllModels API calls. -func NewAllWatcher(st *state.State, resources *common.Resources, auth common.Authorizer, id string) (interface{}, error) { +func NewAllWatcher(context facade.Context) (facade.Facade, error) { + id := context.ID() + auth := context.Auth() + resources := context.Resources() + if !auth.AuthClient() { return nil, common.ErrPerm } - watcher, ok := resources.Get(id).(*state.Multiwatcher) if !ok { return nil, common.ErrUnknownWatcher @@ -85,7 +89,7 @@ type SrvAllWatcher struct { watcher *state.Multiwatcher id string - resources *common.Resources + resources facade.Resources } func (aw *SrvAllWatcher) Next() (params.AllWatcherNextResults, error) { @@ -104,14 +108,18 @@ type srvNotifyWatcher struct { watcher state.NotifyWatcher id string - resources *common.Resources + resources facade.Resources } -func isAgent(auth common.Authorizer) bool { +func isAgent(auth facade.Authorizer) bool { return auth.AuthMachineAgent() || auth.AuthUnitAgent() } -func newNotifyWatcher(st *state.State, resources *common.Resources, auth common.Authorizer, id string) (interface{}, error) { +func newNotifyWatcher(context facade.Context) (facade.Facade, error) { + id := context.ID() + auth := context.Auth() + resources := context.Resources() + if !isAgent(auth) { return nil, common.ErrPerm } @@ -152,10 +160,14 @@ type srvStringsWatcher struct { watcher state.StringsWatcher id string - resources *common.Resources + resources facade.Resources } -func newStringsWatcher(st *state.State, resources *common.Resources, auth common.Authorizer, id string) (interface{}, error) { +func newStringsWatcher(context facade.Context) (facade.Facade, error) { + id := context.ID() + auth := context.Auth() + resources := context.Resources() + if !isAgent(auth) { return nil, common.ErrPerm } @@ -197,10 +209,14 @@ type srvRelationUnitsWatcher struct { watcher state.RelationUnitsWatcher id string - resources *common.Resources + resources facade.Resources } -func newRelationUnitsWatcher(st *state.State, resources *common.Resources, auth common.Authorizer, id string) (interface{}, error) { +func newRelationUnitsWatcher(context facade.Context) (facade.Facade, error) { + id := context.ID() + auth := context.Auth() + resources := context.Resources() + if !isAgent(auth) { return nil, common.ErrPerm } @@ -246,27 +262,26 @@ type srvMachineStorageIdsWatcher struct { watcher state.StringsWatcher id string - resources *common.Resources + resources facade.Resources parser func([]string) ([]params.MachineStorageId, error) } -func newVolumeAttachmentsWatcher( - st *state.State, - resources *common.Resources, - auth common.Authorizer, - id string, -) (interface{}, error) { +func newVolumeAttachmentsWatcher(context facade.Context) (facade.Facade, error) { + id := context.ID() + auth := context.Auth() + resources := context.Resources() + st := context.State() + return newMachineStorageIdsWatcher( st, resources, auth, id, storagecommon.ParseVolumeAttachmentIds, ) } -func newFilesystemAttachmentsWatcher( - st *state.State, - resources *common.Resources, - auth common.Authorizer, - id string, -) (interface{}, error) { +func newFilesystemAttachmentsWatcher(context facade.Context) (facade.Facade, error) { + id := context.ID() + auth := context.Auth() + resources := context.Resources() + st := context.State() return newMachineStorageIdsWatcher( st, resources, auth, id, storagecommon.ParseFilesystemAttachmentIds, ) @@ -274,11 +289,11 @@ func newMachineStorageIdsWatcher( st *state.State, - resources *common.Resources, - auth common.Authorizer, + resources facade.Resources, + auth facade.Authorizer, id string, parser func([]string) ([]params.MachineStorageId, error), -) (interface{}, error) { +) (facade.Facade, error) { if !isAgent(auth) { return nil, common.ErrPerm } @@ -332,13 +347,16 @@ // sending the changes as a list of strings, which could be transformed // from state entity ids to their corresponding entity tags. type srvEntitiesWatcher struct { - st *state.State - resources *common.Resources + resources facade.Resources id string watcher EntitiesWatcher } -func newEntitiesWatcher(st *state.State, resources *common.Resources, auth common.Authorizer, id string) (interface{}, error) { +func newEntitiesWatcher(context facade.Context) (facade.Facade, error) { + id := context.ID() + auth := context.Auth() + resources := context.Resources() + if !isAgent(auth) { return nil, common.ErrPerm } @@ -347,7 +365,6 @@ return nil, common.ErrUnknownWatcher } return &srvEntitiesWatcher{ - st: st, resources: resources, id: id, watcher: watcher, @@ -386,17 +403,17 @@ // migrationBackend defines State functionality required by the // migration watchers. type migrationBackend interface { - GetModelMigration() (state.ModelMigration, error) + LatestModelMigration() (state.ModelMigration, error) APIHostPorts() ([][]network.HostPort, error) ControllerConfig() (controller.Config, error) } -func newMigrationStatusWatcher( - st *state.State, - resources *common.Resources, - auth common.Authorizer, - id string, -) (interface{}, error) { +func newMigrationStatusWatcher(context facade.Context) (facade.Facade, error) { + id := context.ID() + auth := context.Auth() + resources := context.Resources() + st := context.State() + if !(auth.AuthMachineAgent() || auth.AuthUnitAgent()) { return nil, common.ErrPerm } @@ -415,7 +432,7 @@ type srvMigrationStatusWatcher struct { watcher state.NotifyWatcher id string - resources *common.Resources + resources facade.Resources st migrationBackend } @@ -433,7 +450,7 @@ return empty, err } - mig, err := w.st.GetModelMigration() + mig, err := w.st.LatestModelMigration() if errors.IsNotFound(err) { return params.MigrationStatus{ Phase: migration.NONE.String(), @@ -468,6 +485,7 @@ } return params.MigrationStatus{ + MigrationId: mig.Id(), Attempt: attempt, Phase: phase.String(), SourceAPIAddrs: sourceAddrs, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/watcher_test.go juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/watcher_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/apiserver/watcher_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/apiserver/watcher_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,6 +11,7 @@ "github.com/juju/juju/apiserver" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade/facadetest" "github.com/juju/juju/apiserver/params" apiservertesting "github.com/juju/juju/apiserver/testing" "github.com/juju/juju/controller" @@ -40,7 +41,11 @@ func (s *watcherSuite) getFacade(c *gc.C, name string, version int, id string) interface{} { factory, err := common.Facades.GetFactory(name, version) c.Assert(err, jc.ErrorIsNil) - facade, err := factory(nil, s.resources, s.authorizer, id) + facade, err := factory(facadetest.Context{ + Resources_: s.resources, + Auth_: s.authorizer, + ID_: id, + }) c.Assert(err, jc.ErrorIsNil) return facade } @@ -93,8 +98,9 @@ result, err := facade.Next() c.Assert(err, jc.ErrorIsNil) c.Assert(result, jc.DeepEquals, params.MigrationStatus{ + MigrationId: "id", Attempt: 2, - Phase: "READONLY", + Phase: "PRECHECK", SourceAPIAddrs: []string{"1.2.3.4:5", "2.3.4.5:6", "3.4.5.6:7"}, SourceCACert: "no worries", TargetAPIAddrs: []string{"1.2.3.4:5555"}, @@ -123,7 +129,11 @@ factory, err := common.Facades.GetFactory("MigrationStatusWatcher", 1) c.Assert(err, jc.ErrorIsNil) - _, err = factory(nil, s.resources, s.authorizer, id) + _, err = factory(facadetest.Context{ + Resources_: s.resources, + Auth_: s.authorizer, + ID_: id, + }) c.Assert(err, gc.Equals, common.ErrPerm) } @@ -148,7 +158,7 @@ noMigration bool } -func (b *fakeMigrationBackend) GetModelMigration() (state.ModelMigration, error) { +func (b *fakeMigrationBackend) LatestModelMigration() (state.ModelMigration, error) { if b.noMigration { return nil, errors.NotFoundf("migration") } @@ -182,12 +192,16 @@ state.ModelMigration } +func (m *fakeModelMigration) Id() string { + return "id" +} + func (m *fakeModelMigration) Attempt() (int, error) { return 2, nil } func (m *fakeModelMigration) Phase() (migration.Phase, error) { - return migration.READONLY, nil + return migration.PRECHECK, nil } func (m *fakeModelMigration) TargetInfo() (*migration.TargetInfo, error) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cloud/clouds.go juju-core-2.0~beta15/src/github.com/juju/juju/cloud/clouds.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cloud/clouds.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cloud/clouds.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,6 +10,7 @@ "io/ioutil" "os" "reflect" + "sort" "strings" "github.com/juju/errors" @@ -17,6 +18,7 @@ "gopkg.in/yaml.v2" "github.com/juju/juju/juju/osenv" + "github.com/juju/juju/provider/lxd/lxdnames" ) //go:generate go run ../generate/filetoconst/filetoconst.go fallbackPublicCloudInfo fallback-public-cloud.yaml fallback_public_cloud.go 2015 cloud @@ -24,6 +26,13 @@ // AuthType is the type of authentication used by the cloud. type AuthType string +// AuthTypes is defined to allow sorting AuthType slices. +type AuthTypes []AuthType + +func (a AuthTypes) Len() int { return len(a) } +func (a AuthTypes) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a AuthTypes) Less(i, j int) bool { return a[i] < a[j] } + const ( // AccessKeyAuthType is an authentication type using a key and secret. AccessKeyAuthType AuthType = "access-key" @@ -41,6 +50,9 @@ // a JSON file. JSONFileAuthType AuthType = "jsonfile" + // CertificateAuthType is an authentication type using certificates. + CertificateAuthType AuthType = "certificate" + // EmptyAuthType is the authentication type used for providers // that require no credentials, e.g. "lxd", and "manual". EmptyAuthType AuthType = "empty" @@ -48,11 +60,13 @@ // Cloud is a cloud definition. type Cloud struct { - // Type is the type of cloud, eg aws, openstack etc. + // Type is the type of cloud, eg ec2, openstack etc. + // This is one of the provider names registered with + // environs.RegisterProvider. Type string // AuthTypes are the authentication modes supported by the cloud. - AuthTypes []AuthType + AuthTypes AuthTypes // Endpoint is the default endpoint for the cloud regions, may be // overridden by a region. @@ -128,12 +142,15 @@ StorageEndpoint string `yaml:"storage-endpoint,omitempty"` } +//DefaultLXD is the name of the default lxd cloud. +const DefaultLXD = "localhost" + // BuiltInClouds work out of the box. var BuiltInClouds = map[string]Cloud{ - "localhost": { - Type: "lxd", + DefaultLXD: { + Type: lxdnames.ProviderType, AuthTypes: []AuthType{EmptyAuthType}, - Regions: []Region{{Name: "localhost"}}, + Regions: []Region{{Name: lxdnames.DefaultRegion}}, }, } @@ -175,15 +192,17 @@ } return nil, errors.NewNotFound(nil, fmt.Sprintf( "region %q not found (expected one of %q)", - name, cloudRegionNames(regions), + name, RegionNames(regions), )) } -func cloudRegionNames(regions []Region) []string { +// RegionNames returns a sorted list of the names of the given regions. +func RegionNames(regions []Region) []string { names := make([]string, len(regions)) for i, region := range regions { names[i] = region.Name } + sort.Strings(names) return names } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cloud/clouds_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cloud/clouds_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cloud/clouds_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cloud/clouds_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -61,7 +61,7 @@ func (s *cloudSuite) TestParseCloudsAuthTypes(c *gc.C) { clouds := parsePublicClouds(c) rackspace := clouds["rackspace"] - c.Assert(rackspace.AuthTypes, jc.SameContents, []cloud.AuthType{"access-key", "userpass"}) + c.Assert(rackspace.AuthTypes, jc.SameContents, cloud.AuthTypes{"access-key", "userpass"}) } func (s *cloudSuite) TestParseCloudsConfig(c *gc.C) { @@ -191,3 +191,17 @@ `[1:] s.assertCompareClouds(c, metadata, false) } + +func (s *cloudSuite) TestRegionNames(c *gc.C) { + regions := []cloud.Region{ + {Name: "mars"}, + {Name: "earth"}, + {Name: "jupiter"}, + } + + names := cloud.RegionNames(regions) + c.Assert(names, gc.DeepEquals, []string{"earth", "jupiter", "mars"}) + + c.Assert(cloud.RegionNames([]cloud.Region{}), gc.HasLen, 0) + c.Assert(cloud.RegionNames(nil), gc.HasLen, 0) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cloud/credentials.go juju-core-2.0~beta15/src/github.com/juju/juju/cloud/credentials.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cloud/credentials.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cloud/credentials.go 2016-08-16 08:56:25.000000000 +0000 @@ -180,12 +180,19 @@ if field.FilePath { pathValue, ok := resultMap[name] if ok && pathValue != "" { - if absPath, err := ValidateFileAttrValue(pathValue.(string)); err != nil { + absPath, err := ValidateFileAttrValue(pathValue.(string)) + if err != nil { return nil, errors.Trace(err) - } else { - newAttrs[name] = absPath - continue } + data, err := readFile(absPath) + if err != nil { + return nil, errors.Annotatef(err, "reading file for %q", name) + } + if len(data) == 0 { + return nil, errors.NotValidf("empty file for %q", name) + } + newAttrs[name] = string(data) + continue } } if val, ok := resultMap[name]; ok { @@ -301,7 +308,9 @@ // value used for this attribute. FileAttr string - // FilePath is true is the value of this attribute is a file path. + // FilePath is true is the value of this attribute is a file path. If + // this is true, then the attribute value will be set to the contents + // of the file when the credential is "finalized". FilePath bool // Optional controls whether the attribute is required to have a non-empty diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cloud/credentials_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cloud/credentials_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cloud/credentials_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cloud/credentials_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -318,15 +318,13 @@ "key": "value", }, ) - schema := cloud.CredentialSchema{ - { - "key", - cloud.CredentialAttr{ - Description: "key credential", - Hidden: true, - }, + schema := cloud.CredentialSchema{{ + "key", + cloud.CredentialAttr{ + Description: "key credential", + Hidden: true, }, - } + }} _, err := cloud.FinalizeCredential(cred, map[cloud.AuthType]cloud.CredentialSchema{ cloud.UserPassAuthType: schema, }, readFileNotSupported) @@ -341,18 +339,16 @@ "quay": "value", }, ) - schema := cloud.CredentialSchema{ - { - "key", - cloud.CredentialAttr{ - Description: "key credential", - Hidden: true, - FileAttr: "key-file", - }, - }, { - "quay", cloud.CredentialAttr{FileAttr: "quay-file"}, - }, - } + schema := cloud.CredentialSchema{{ + "key", + cloud.CredentialAttr{ + Description: "key credential", + Hidden: true, + FileAttr: "key-file", + }, + }, { + "quay", cloud.CredentialAttr{FileAttr: "quay-file"}, + }} readFile := func(s string) ([]byte, error) { c.Assert(s, gc.Equals, "path") return []byte("file-value"), nil @@ -374,16 +370,14 @@ "key-file": "path", }, ) - schema := cloud.CredentialSchema{ - { - "key", - cloud.CredentialAttr{ - Description: "key credential", - Hidden: true, - FileAttr: "key-file", - }, + schema := cloud.CredentialSchema{{ + "key", + cloud.CredentialAttr{ + Description: "key credential", + Hidden: true, + FileAttr: "key-file", }, - } + }} readFile := func(string) ([]byte, error) { return nil, nil } @@ -398,16 +392,14 @@ cloud.UserPassAuthType, map[string]string{}, ) - schema := cloud.CredentialSchema{ - { - "key", - cloud.CredentialAttr{ - Description: "key credential", - Hidden: true, - FileAttr: "key-file", - }, + schema := cloud.CredentialSchema{{ + "key", + cloud.CredentialAttr{ + Description: "key credential", + Hidden: true, + FileAttr: "key-file", }, - } + }} _, err := cloud.FinalizeCredential(cred, map[cloud.AuthType]cloud.CredentialSchema{ cloud.UserPassAuthType: schema, }, readFileNotSupported) @@ -422,16 +414,14 @@ "key-file": "path", }, ) - schema := cloud.CredentialSchema{ - { - "key", - cloud.CredentialAttr{ - Description: "key credential", - Hidden: true, - FileAttr: "key-file", - }, + schema := cloud.CredentialSchema{{ + "key", + cloud.CredentialAttr{ + Description: "key credential", + Hidden: true, + FileAttr: "key-file", }, - } + }} _, err := cloud.FinalizeCredential(cred, map[cloud.AuthType]cloud.CredentialSchema{ cloud.UserPassAuthType: schema, }, readFileNotSupported) @@ -443,15 +433,13 @@ cloud.UserPassAuthType, map[string]string{}, ) - schema := cloud.CredentialSchema{ - { - "key", - cloud.CredentialAttr{ - Description: "key credential", - Hidden: true, - }, + schema := cloud.CredentialSchema{{ + "key", + cloud.CredentialAttr{ + Description: "key credential", + Hidden: true, }, - } + }} _, err := cloud.FinalizeCredential(cred, map[cloud.AuthType]cloud.CredentialSchema{ cloud.UserPassAuthType: schema, }, readFileNotSupported) @@ -483,13 +471,9 @@ }, ) schema := cloud.CredentialSchema{ - { - "username", cloud.CredentialAttr{Optional: false}, - }, { - "password", cloud.CredentialAttr{Hidden: true}, - }, { - "domain", cloud.CredentialAttr{}, - }, + {"username", cloud.CredentialAttr{Optional: false}}, + {"password", cloud.CredentialAttr{Hidden: true}}, + {"domain", cloud.CredentialAttr{}}, } _, err := cloud.FinalizeCredential(cred, map[cloud.AuthType]cloud.CredentialSchema{ cloud.UserPassAuthType: schema, @@ -504,16 +488,14 @@ "key-file": "path", }, ) - schema := cloud.CredentialSchema{ - { - "key", - cloud.CredentialAttr{ - Description: "key credential", - Optional: false, - FileAttr: "key-file", - }, + schema := cloud.CredentialSchema{{ + "key", + cloud.CredentialAttr{ + Description: "key credential", + Optional: false, + FileAttr: "key-file", }, - } + }} readFile := func(s string) ([]byte, error) { c.Assert(s, gc.Equals, "path") return []byte("file-value"), nil @@ -538,13 +520,9 @@ }, ) schema := cloud.CredentialSchema{ - { - "username", cloud.CredentialAttr{Optional: false}, - }, { - "password", cloud.CredentialAttr{Hidden: true}, - }, { - "domain", cloud.CredentialAttr{}, - }, + {"username", cloud.CredentialAttr{Optional: false}}, + {"password", cloud.CredentialAttr{Hidden: true}}, + {"domain", cloud.CredentialAttr{}}, } _, err := cloud.FinalizeCredential(cred, map[cloud.AuthType]cloud.CredentialSchema{ cloud.UserPassAuthType: schema, @@ -562,13 +540,9 @@ }, ) schema := cloud.CredentialSchema{ - { - "username", cloud.CredentialAttr{Optional: false}, - }, { - "password", cloud.CredentialAttr{Hidden: true}, - }, { - "algorithm", cloud.CredentialAttr{Options: []interface{}{"bar", "foobar"}}, - }, + {"username", cloud.CredentialAttr{Optional: false}}, + {"password", cloud.CredentialAttr{Hidden: true}}, + {"algorithm", cloud.CredentialAttr{Options: []interface{}{"bar", "foobar"}}}, } _, err := cloud.FinalizeCredential(cred, map[cloud.AuthType]cloud.CredentialSchema{ cloud.UserPassAuthType: schema, @@ -588,17 +562,21 @@ "file": filename, }, ) - schema := cloud.CredentialSchema{ - { - "file", cloud.CredentialAttr{FilePath: true}, - }, + schema := cloud.CredentialSchema{{ + "file", cloud.CredentialAttr{FilePath: true}, + }} + + readFile := func(path string) ([]byte, error) { + c.Assert(path, gc.Equals, filename) + return []byte("file-contents"), nil } + newCred, err := cloud.FinalizeCredential(cred, map[cloud.AuthType]cloud.CredentialSchema{ cloud.JSONFileAuthType: schema, - }, nil) + }, readFile) c.Assert(err, jc.ErrorIsNil) c.Assert(newCred.Attributes(), jc.DeepEquals, map[string]string{ - "file": filename, + "file": "file-contents", }) } @@ -609,11 +587,9 @@ "file": filepath.Join(c.MkDir(), "somefile"), }, ) - schema := cloud.CredentialSchema{ - { - "file", cloud.CredentialAttr{FilePath: true}, - }, - } + schema := cloud.CredentialSchema{{ + "file", cloud.CredentialAttr{FilePath: true}, + }} _, err := cloud.FinalizeCredential(cred, map[cloud.AuthType]cloud.CredentialSchema{ cloud.JSONFileAuthType: schema, }, nil) @@ -627,11 +603,9 @@ "file": "file", }, ) - schema := cloud.CredentialSchema{ - { - "file", cloud.CredentialAttr{FilePath: true}, - }, - } + schema := cloud.CredentialSchema{{ + "file", cloud.CredentialAttr{FilePath: true}, + }} _, err := cloud.FinalizeCredential(cred, map[cloud.AuthType]cloud.CredentialSchema{ cloud.JSONFileAuthType: schema, }, nil) @@ -646,13 +620,11 @@ "password": "secret", }, ) - schema := cloud.CredentialSchema{ - { - "username", cloud.CredentialAttr{}, - }, { - "password", cloud.CredentialAttr{Hidden: true}, - }, - } + schema := cloud.CredentialSchema{{ + "username", cloud.CredentialAttr{}, + }, { + "password", cloud.CredentialAttr{Hidden: true}, + }} sanitisedCred, err := cloud.RemoveSecrets(cred, map[cloud.AuthType]cloud.CredentialSchema{ cloud.UserPassAuthType: schema, }) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cloudconfig/cloudinit/cloudinit_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cloudconfig/cloudinit/cloudinit_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cloudconfig/cloudinit/cloudinit_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cloudconfig/cloudinit/cloudinit_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -252,7 +252,6 @@ // packages (see cloudArchivePackagesUbuntu in juju/utils // repo). "--target-release", "precise-updates/cloud-tools", "cloud-utils", - "--target-release", "precise-updates/cloud-tools", "cloud-image-utils", // Other regular packages. "ubuntu", }}, @@ -274,10 +273,6 @@ cfg.AddPackage("precise-updates/cloud-tools") cfg.AddPackage("cloud-utils") - cfg.AddPackage("--target-release") - cfg.AddPackage("precise-updates/cloud-tools") - cfg.AddPackage("cloud-image-utils") - cfg.AddPackage("ubuntu") }, }, { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cloudconfig/cloudinit/cloudinit_ubuntu.go juju-core-2.0~beta15/src/github.com/juju/juju/cloudconfig/cloudinit/cloudinit_ubuntu.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cloudconfig/cloudinit/cloudinit_ubuntu.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cloudconfig/cloudinit/cloudinit_ubuntu.go 2016-08-16 08:56:25.000000000 +0000 @@ -268,7 +268,6 @@ // leave it to the networker worker. "bridge-utils", "cloud-utils", - "cloud-image-utils", "tmux", } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cloudconfig/containerinit/container_userdata.go juju-core-2.0~beta15/src/github.com/juju/juju/cloudconfig/containerinit/container_userdata.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cloudconfig/containerinit/container_userdata.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cloudconfig/containerinit/container_userdata.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,7 +6,9 @@ import ( "bytes" + "fmt" "io/ioutil" + "net" "path/filepath" "strings" @@ -56,7 +58,10 @@ return userDataFilename, nil } -var networkInterfacesFile = "/etc/network/interfaces" +var ( + systemNetworkInterfacesFile = "/etc/network/interfaces" + networkInterfacesFile = systemNetworkInterfacesFile + "-juju" +) // GenerateNetworkConfig renders a network config for one or more network // interfaces, using the given non-nil networkConfig containing a non-empty @@ -70,7 +75,7 @@ prepared := PrepareNetworkConfigFromInterfaces(networkConfig.Interfaces) var output bytes.Buffer - gatewayWritten := false + gatewayHandled := false for _, name := range prepared.InterfaceNames { output.WriteString("\n") if name == "lo" { @@ -99,20 +104,35 @@ continue } else if address == string(network.ConfigDHCP) { output.WriteString("iface " + name + " inet dhcp\n") + // We're expecting to get a default gateway + // from the DHCP lease. + gatewayHandled = true continue } output.WriteString("iface " + name + " inet static\n") output.WriteString(" address " + address + "\n") - if !gatewayWritten && prepared.GatewayAddress != "" { - output.WriteString(" gateway " + prepared.GatewayAddress + "\n") - gatewayWritten = true // write it only once + if !gatewayHandled && prepared.GatewayAddress != "" { + _, network, err := net.ParseCIDR(address) + if err != nil { + return "", errors.Annotatef(err, "invalid gateway for interface %q with address %q", name, address) + } + + gatewayIP := net.ParseIP(prepared.GatewayAddress) + if network.Contains(gatewayIP) { + output.WriteString(" gateway " + prepared.GatewayAddress + "\n") + gatewayHandled = true // write it only once + } } } generatedConfig := output.String() logger.Debugf("generated network config:\n%s", generatedConfig) + if !gatewayHandled { + logger.Infof("generated network config has no gateway") + } + return generatedConfig, nil } @@ -158,8 +178,7 @@ dnsSearchDomains = dnsSearchDomains.Union(set.NewStrings(info.DNSSearchDomains...)) - if info.InterfaceName == "eth0" && gatewayAddress == "" { - // Only set gateway once for the primary NIC. + if gatewayAddress == "" && info.GatewayAddress.Value != "" { gatewayAddress = info.GatewayAddress.Value } @@ -193,6 +212,8 @@ } cloudConfig.AddBootTextFile(networkInterfacesFile, config, 0644) + cloudConfig.AddRunCmd(raiseJujuNetworkInterfacesScript(systemNetworkInterfacesFile, networkInterfacesFile)) + return cloudConfig, nil } @@ -322,3 +343,22 @@ return cmds, nil } + +// raiseJujuNetworkInterfacesScript returns a cloud-init script to +// raise Juju's network interfaces supplied via cloud-init. +// +// Note: we sleep to mitigate against LP #1337873 and LP #1269921. +func raiseJujuNetworkInterfacesScript(oldInterfacesFile, newInterfacesFile string) string { + return fmt.Sprintf(` +if [ -f %[2]s ]; then + ifdown -a + sleep 1.5 + if ifup -a --interfaces=%[2]s; then + cp %[1]s %[1]s-orig + cp %[2]s %[1]s + else + ifup -a + fi +fi`[1:], + oldInterfacesFile, newInterfacesFile) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cloudconfig/containerinit/container_userdata_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cloudconfig/containerinit/container_userdata_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cloudconfig/containerinit/container_userdata_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cloudconfig/containerinit/container_userdata_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -29,8 +29,10 @@ type UserDataSuite struct { testing.BaseSuite - networkInterfacesFile string - fakeInterfaces []network.InterfaceInfo + networkInterfacesFile string + systemNetworkInterfacesFile string + + fakeInterfaces []network.InterfaceInfo expectedSampleConfig string expectedSampleUserData string @@ -42,9 +44,10 @@ func (s *UserDataSuite) SetUpTest(c *gc.C) { s.BaseSuite.SetUpTest(c) - s.networkInterfacesFile = filepath.Join(c.MkDir(), "interfaces") + s.networkInterfacesFile = filepath.Join(c.MkDir(), "juju-interfaces") + s.systemNetworkInterfacesFile = filepath.Join(c.MkDir(), "system-interfaces") s.fakeInterfaces = []network.InterfaceInfo{{ - InterfaceName: "eth0", + InterfaceName: "any0", CIDR: "0.1.2.0/24", ConfigType: network.ConfigStatic, NoAutoStart: false, @@ -54,47 +57,47 @@ GatewayAddress: network.NewAddress("0.1.2.1"), MACAddress: "aa:bb:cc:dd:ee:f0", }, { - InterfaceName: "eth1", - CIDR: "0.1.2.0/24", + InterfaceName: "any1", + CIDR: "0.2.2.0/24", ConfigType: network.ConfigStatic, NoAutoStart: false, - Address: network.NewAddress("0.1.2.4"), + Address: network.NewAddress("0.2.2.4"), DNSServers: network.NewAddresses("ns1.invalid", "ns2.invalid"), DNSSearchDomains: []string{"foo", "bar"}, - GatewayAddress: network.NewAddress("0.1.2.1"), - MACAddress: "aa:bb:cc:dd:ee:f0", + GatewayAddress: network.NewAddress("0.2.2.1"), + MACAddress: "aa:bb:cc:dd:ee:f1", }, { - InterfaceName: "eth2", + InterfaceName: "any2", ConfigType: network.ConfigDHCP, NoAutoStart: true, }, { - InterfaceName: "eth3", + InterfaceName: "any3", ConfigType: network.ConfigDHCP, NoAutoStart: false, }, { - InterfaceName: "eth4", + InterfaceName: "any4", ConfigType: network.ConfigManual, NoAutoStart: true, }} s.expectedSampleConfig = ` -auto eth0 eth1 eth3 lo +auto any0 any1 any3 lo iface lo inet loopback dns-nameservers ns1.invalid ns2.invalid dns-search bar foo -iface eth0 inet static +iface any0 inet static address 0.1.2.3/24 gateway 0.1.2.1 -iface eth1 inet static - address 0.1.2.4/24 +iface any1 inet static + address 0.2.2.4/24 -iface eth2 inet dhcp +iface any2 inet dhcp -iface eth3 inet dhcp +iface any3 inet dhcp -iface eth4 inet manual +iface any4 inet manual ` s.expectedSampleUserData = ` #cloud-config @@ -102,25 +105,37 @@ - install -D -m 644 /dev/null '%[1]s' - |- printf '%%s\n' ' - auto eth0 eth1 eth3 lo + auto any0 any1 any3 lo iface lo inet loopback dns-nameservers ns1.invalid ns2.invalid dns-search bar foo - iface eth0 inet static + iface any0 inet static address 0.1.2.3/24 gateway 0.1.2.1 - iface eth1 inet static - address 0.1.2.4/24 + iface any1 inet static + address 0.2.2.4/24 - iface eth2 inet dhcp + iface any2 inet dhcp - iface eth3 inet dhcp + iface any3 inet dhcp - iface eth4 inet manual + iface any4 inet manual ' > '%[1]s' +runcmd: +- |- + if [ -f %[1]s ]; then + ifdown -a + sleep 1.5 + if ifup -a --interfaces=%[1]s; then + cp %[2]s %[2]s-orig + cp %[1]s %[2]s + else + ifup -a + fi + fi `[1:] s.expectedFallbackConfig = ` @@ -142,9 +157,22 @@ iface eth0 inet dhcp ' > '%[1]s' +runcmd: +- |- + if [ -f %[1]s ]; then + ifdown -a + sleep 1.5 + if ifup -a --interfaces=%[1]s; then + cp %[2]s %[2]s-orig + cp %[1]s %[2]s + else + ifup -a + fi + fi `[1:] s.PatchValue(containerinit.NetworkInterfacesFile, s.networkInterfacesFile) + s.PatchValue(containerinit.SystemNetworkInterfacesFile, s.systemNetworkInterfacesFile) } func (s *UserDataSuite) TestGenerateNetworkConfig(c *gc.C) { @@ -170,7 +198,7 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(cloudConf, gc.NotNil) - expected := fmt.Sprintf(s.expectedSampleUserData, s.networkInterfacesFile) + expected := fmt.Sprintf(s.expectedSampleUserData, s.networkInterfacesFile, s.systemNetworkInterfacesFile) assertUserData(c, cloudConf, expected) } @@ -179,8 +207,7 @@ cloudConf, err := containerinit.NewCloudInitConfigWithNetworks("quantal", netConfig) c.Assert(err, jc.ErrorIsNil) c.Assert(cloudConf, gc.NotNil) - - expected := fmt.Sprintf(s.expectedFallbackUserData, s.networkInterfacesFile) + expected := fmt.Sprintf(s.expectedFallbackUserData, s.networkInterfacesFile, s.systemNetworkInterfacesFile) assertUserData(c, cloudConf, expected) } @@ -192,9 +219,16 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(data, gc.NotNil) - // Extract the "#cloud-config" header and all lines between from the - // "bootcmd" section up to (but not including) the "output" sections to - // match against expected. + // Extract the "#cloud-config" header and all lines between + // from the "bootcmd" section up to (but not including) the + // "output" sections to match against expected. But we cannot + // possibly handle all the /other/ output that may be added by + // CloudInitUserData() in the future, so we also truncate at + // the first runcmd which now happens to include the runcmd's + // added for raising the network interfaces captured in + // expectedFallbackUserData. However, the other tests above do + // check for that output. + var linesToMatch []string seenBootcmd := false for _, line := range strings.Split(string(data), "\n") { @@ -215,8 +249,18 @@ linesToMatch = append(linesToMatch, line) } } - expected := fmt.Sprintf(s.expectedFallbackUserData, s.networkInterfacesFile) - c.Assert(strings.Join(linesToMatch, "\n")+"\n", gc.Equals, expected) + expected := fmt.Sprintf(s.expectedFallbackUserData, s.networkInterfacesFile, s.systemNetworkInterfacesFile) + + var expectedLinesToMatch []string + + for _, line := range strings.Split(expected, "\n") { + if strings.HasPrefix(line, "runcmd:") { + break + } + expectedLinesToMatch = append(expectedLinesToMatch, line) + } + + c.Assert(strings.Join(linesToMatch, "\n")+"\n", gc.Equals, strings.Join(expectedLinesToMatch, "\n")+"\n") } func assertUserData(c *gc.C, cloudConf cloudinit.CloudConfig, expected string) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cloudconfig/containerinit/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cloudconfig/containerinit/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cloudconfig/containerinit/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cloudconfig/containerinit/export_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,5 +6,6 @@ var ( NetworkInterfacesFile = &networkInterfacesFile + SystemNetworkInterfacesFile = &systemNetworkInterfacesFile NewCloudInitConfigWithNetworks = newCloudInitConfigWithNetworks ) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cloudconfig/instancecfg/instancecfg.go juju-core-2.0~beta15/src/github.com/juju/juju/cloudconfig/instancecfg/instancecfg.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cloudconfig/instancecfg/instancecfg.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cloudconfig/instancecfg/instancecfg.go 2016-08-16 08:56:25.000000000 +0000 @@ -671,9 +671,6 @@ MachineNonce: machineNonce, APIInfo: apiInfo, ImageStream: imageStream, - AgentEnvironment: map[string]string{ - agent.AllowsSecureConnection: strconv.FormatBool(secureServerConnections), - }, } return icfg, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cloudconfig/userdatacfg_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cloudconfig/userdatacfg_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cloudconfig/userdatacfg_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cloudconfig/userdatacfg_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -330,6 +330,8 @@ printf '%s\\n' 'FAKE_NONCE' > '/var/lib/juju/nonce.txt' test -e /proc/self/fd/9 \|\| exec 9>&2 \(\[ ! -e /home/ubuntu/.profile \] \|\| grep -q '.juju-proxy' /home/ubuntu/.profile\) \|\| printf .* >> /home/ubuntu/.profile +install -D -m 644 /dev/null '/etc/profile.d/juju-introspection.sh' +printf '%s\\n' '.*' > '/etc/profile.d/juju-introspection.sh' mkdir -p /var/lib/juju/locks \(id ubuntu &> /dev/null\) && chown ubuntu:ubuntu /var/lib/juju/locks mkdir -p /var/log/juju @@ -387,6 +389,8 @@ printf '%s\\n' 'FAKE_NONCE' > '/var/lib/juju/nonce.txt' test -e /proc/self/fd/9 \|\| exec 9>&2 \(\[ ! -e /home/ubuntu/\.profile \] \|\| grep -q '.juju-proxy' /home/ubuntu/.profile\) \|\| printf .* >> /home/ubuntu/.profile +install -D -m 644 /dev/null '/etc/profile.d/juju-introspection.sh' +printf '%s\\n' '.*' > '/etc/profile.d/juju-introspection.sh' mkdir -p /var/lib/juju/locks \(id ubuntu &> /dev/null\) && chown ubuntu:ubuntu /var/lib/juju/locks mkdir -p /var/log/juju diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cloudconfig/userdatacfg_unix.go juju-core-2.0~beta15/src/github.com/juju/juju/cloudconfig/userdatacfg_unix.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cloudconfig/userdatacfg_unix.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cloudconfig/userdatacfg_unix.go 2016-08-16 08:56:25.000000000 +0000 @@ -210,6 +210,9 @@ w.conf.AddRunTextFile(keyFile, w.icfg.Controller.PublicImageSigningKey, 0644) } + // Write out the introspection helper bash functions in /etc/profile.d. + w.conf.AddRunTextFile("/etc/profile.d/juju-introspection.sh", introspectionWorkerBashFuncs, 0644) + // Make the lock dir and change the ownership of the lock dir itself to // ubuntu:ubuntu from root:root so the juju-run command run as the ubuntu // user is able to get access to the hook execution lock (like the uniter @@ -265,6 +268,7 @@ return errors.Trace(err) } } + return w.addMachineAgentToBoot() } @@ -445,3 +449,37 @@ } return base64.StdEncoding.EncodeToString(data) } + +const introspectionWorkerBashFuncs = ` +jujuAgentCall () { + local agent=$1 + shift + local path= + for i in "$@"; do + path="$path/$i" + done + echo -e "GET $path HTTP/1.0\r\n" | socat abstract-connect:jujud-$agent STDIO +} + +jujuMachineAgentName () { + local machine=` + "`ls -d /var/lib/juju/agents/machine*`" + ` + machine=` + "`basename $machine`" + ` + echo $machine +} + +juju-goroutines () { + if [ "$#" -gt 1 ]; then + echo "expected no args (for machine agent) or one (unit agent)" + return 1 + fi + local agent=$(jujuMachineAgentName) + if [ "$#" -eq 1 ]; then + agent=$1 + fi + jujuAgentCall $agent debug/pprof/goroutine?debug=1 +} + +export -f jujuAgentCall +export -f jujuMachineAgentName +export -f juju-goroutines +` diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/action/list.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/action/list.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/action/list.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/action/list.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,11 +11,11 @@ "github.com/juju/cmd" errors "github.com/juju/errors" + "github.com/juju/utils" "gopkg.in/juju/names.v2" "launchpad.net/gnuflag" "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/cmd/juju/common" "github.com/juju/juju/cmd/modelcmd" ) @@ -35,7 +35,7 @@ List the actions available to run on the target application, with a short description. To show the full schema for the actions, use --schema. -For more information, see also the 'run-ation' command, which executes actions. +For more information, see also the 'run-action' command, which executes actions. ` // Set up the output. @@ -107,7 +107,7 @@ } sortedNames = append(sortedNames, name) } - sortedNames = common.SortStringsNaturally(sortedNames) + utils.SortStringsNaturally(sortedNames) return c.printTabular(ctx, shortOutput, sortedNames) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/action/run.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/action/run.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/action/run.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/action/run.go 2016-08-16 08:56:25.000000000 +0000 @@ -43,7 +43,7 @@ 'juju show-action-status '. Params are validated according to the charm for the unit's application. The -valid params can be seen using "juju action defined --schema". +valid params can be seen using "juju actions --schema". Params may be in a yaml file which is passed with the --params flag, or they may be specified by a key.key.key...=value format (see examples below.) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/application/bundle.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/application/bundle.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/application/bundle.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/application/bundle.go 2016-08-16 08:56:25.000000000 +0000 @@ -24,6 +24,7 @@ apiannotations "github.com/juju/juju/api/annotations" "github.com/juju/juju/api/application" "github.com/juju/juju/api/charms" + "github.com/juju/juju/api/modelconfig" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/charmstore" "github.com/juju/juju/constraints" @@ -110,11 +111,16 @@ } defer watcher.Stop() - serviceClient, err := serviceDeployer.newApplicationAPIClient() + applicationClient, err := serviceDeployer.newApplicationAPIClient() if err != nil { return nil, errors.Annotate(err, "cannot get application client") } + modelConfigClient, err := serviceDeployer.newModelConfigAPIClient() + if err != nil { + return nil, errors.Annotate(err, "cannot get model config client") + } + annotationsClient, err := serviceDeployer.newAnnotationsAPIClient() if err != nil { return nil, errors.Annotate(err, "cannot get annotations client") @@ -131,7 +137,8 @@ results: make(map[string]string, numChanges), channel: channel, client: client, - serviceClient: serviceClient, + modelConfigClient: modelConfigClient, + applicationClient: applicationClient, annotationsClient: annotationsClient, charmsClient: charmsClient, serviceDeployer: serviceDeployer, @@ -214,11 +221,14 @@ // client is used to interact with the environment. client *api.Client + // modelConfigClient is used to get model config information. + modelConfigClient *modelconfig.Client + // charmsClient is used to get charm information. charmsClient *charms.Client - // serviceClient is used to interact with services. - serviceClient *application.Client + // applicationClient is used to interact with applications. + applicationClient *application.Client // annotationsClient is used to interact with annotations. annotationsClient *apiannotations.Client @@ -366,7 +376,7 @@ } // Figure out what series we need to deploy with. - conf, err := getClientConfig(h.client) + conf, err := getModelConfig(h.modelConfigClient) if err != nil { return err } @@ -414,7 +424,7 @@ } // Update application configuration. if configYAML != "" { - if err := h.serviceClient.Update(params.ApplicationUpdate{ + if err := h.applicationClient.Update(params.ApplicationUpdate{ ApplicationName: p.Application, SettingsYAML: configYAML, }); err != nil { @@ -426,7 +436,7 @@ } // Update application constraints. if p.Constraints != "" { - if err := h.serviceClient.SetConstraints(p.Application, cons); err != nil { + if err := h.applicationClient.SetConstraints(p.Application, cons); err != nil { // This should never happen, as the bundle is already verified. return errors.Annotatef(err, "cannot update constraints for application %q", p.Application) } @@ -525,7 +535,7 @@ func (h *bundleHandler) addRelation(id string, p bundlechanges.AddRelationParams) error { ep1 := resolveRelation(p.Endpoint1, h.results) ep2 := resolveRelation(p.Endpoint2, h.results) - _, err := h.serviceClient.AddRelation(ep1, ep2) + _, err := h.applicationClient.AddRelation(ep1, ep2) if err == nil { // A new relation has been established. h.log.Infof("related %s and %s", ep1, ep2) @@ -574,7 +584,7 @@ } placementArg = append(placementArg, placement) } - r, err := h.serviceClient.AddUnits(application, 1, placementArg) + r, err := h.applicationClient.AddUnits(application, 1, placementArg) if err != nil { return errors.Annotatef(err, "cannot add unit for application %q", application) } @@ -599,7 +609,7 @@ // exposeService exposes an application. func (h *bundleHandler) exposeService(id string, p bundlechanges.ExposeParams) error { application := resolve(p.Application, h.results) - if err := h.serviceClient.Expose(application); err != nil { + if err := h.applicationClient.Expose(application); err != nil { return errors.Annotatef(err, "cannot expose application %s", application) } h.log.Infof("application %s exposed", application) @@ -820,7 +830,7 @@ // incompatible, meaning an upgrade from one to the other is not allowed. func (h *bundleHandler) upgradeCharm(applicationName string, chID charmstore.CharmID, csMac *macaroon.Macaroon, resources map[string]string) error { id := chID.URL.String() - existing, err := h.serviceClient.GetCharmURL(applicationName) + existing, err := h.applicationClient.GetCharmURL(applicationName) if err != nil { return errors.Annotatef(err, "cannot retrieve info for application %q", applicationName) } @@ -852,7 +862,7 @@ CharmID: chID, ResourceIDs: resNames2IDs, } - if err := h.serviceClient.SetCharm(cfg); err != nil { + if err := h.applicationClient.SetCharm(cfg); err != nil { return errors.Annotatef(err, "cannot upgrade charm to %q", id) } h.log.Infof("upgraded charm for existing application %s (from %s to %s)", applicationName, existing, id) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/application/cmd_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/application/cmd_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/application/cmd_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/application/cmd_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -54,7 +54,7 @@ if com.NumUnits == 0 { com.NumUnits = 1 } - com.SetClientStore(store) + com.SetClientStore(modelcmd.QualifyingClientStore{store}) com.SetModelName("controller") } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/application/deploy.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/application/deploy.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/application/deploy.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/application/deploy.go 2016-08-16 08:56:25.000000000 +0000 @@ -25,6 +25,7 @@ apiannotations "github.com/juju/juju/api/annotations" "github.com/juju/juju/api/application" apicharms "github.com/juju/juju/api/charms" + "github.com/juju/juju/api/modelconfig" "github.com/juju/juju/charmstore" "github.com/juju/juju/cmd/juju/block" "github.com/juju/juju/cmd/modelcmd" @@ -276,7 +277,7 @@ ModelGet() (map[string]interface{}, error) } -var getClientConfig = func(client ModelConfigGetter) (*config.Config, error) { +var getModelConfig = func(client ModelConfigGetter) (*config.Config, error) { // Separated into a variable for easy overrides attrs, err := client.ModelGet() if err != nil { @@ -311,7 +312,7 @@ return bundleData, bundleFile, bundleFilePath, err } -func (c *DeployCommand) deployCharmOrBundle(ctx *cmd.Context, client *api.Client) error { +func (c *DeployCommand) deployCharmOrBundle(ctx *cmd.Context, client *api.Client, modelConfigClient *modelconfig.Client) error { deployer := applicationDeployer{ctx, c} // We may have been given a local bundle file. @@ -373,7 +374,7 @@ return err } - conf, err := getClientConfig(client) + conf, err := getModelConfig(modelConfigClient) if err != nil { return err } @@ -673,6 +674,14 @@ return application.NewClient(root), nil } +func (d *applicationDeployer) newModelConfigAPIClient() (*modelconfig.Client, error) { + root, err := d.api.NewAPIRoot() + if err != nil { + return nil, errors.Trace(err) + } + return modelconfig.NewClient(root), nil +} + func (d *applicationDeployer) newAnnotationsAPIClient() (*apiannotations.Client, error) { root, err := d.api.NewAPIRoot() if err != nil { @@ -721,11 +730,18 @@ func (c *DeployCommand) Run(ctx *cmd.Context) error { client, err := c.NewAPIClient() if err != nil { - return err + return errors.Trace(err) } defer client.Close() - err = c.deployCharmOrBundle(ctx, client) + api, err := c.NewAPIRoot() + if err != nil { + return errors.Trace(err) + } + modelConfigClient := modelconfig.NewClient(api) + defer modelConfigClient.Close() + + err = c.deployCharmOrBundle(ctx, client, modelConfigClient) return block.ProcessBlockedError(err, block.BlockChange) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/application/deploy_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/application/deploy_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/application/deploy_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/application/deploy_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -42,8 +42,6 @@ "github.com/juju/juju/instance" "github.com/juju/juju/juju/testing" "github.com/juju/juju/state" - "github.com/juju/juju/storage/poolmanager" - "github.com/juju/juju/storage/provider" "github.com/juju/juju/testcharms" coretesting "github.com/juju/juju/testing" ) @@ -309,12 +307,8 @@ // TODO(wallyworld) - add another test that deploy with storage fails for older environments // (need deploy client to be refactored to use API stub) func (s *DeploySuite) TestStorage(c *gc.C) { - pm := poolmanager.New(state.NewStateSettings(s.State)) - _, err := pm.Create("loop-pool", provider.LoopProviderType, map[string]interface{}{"foo": "bar"}) - c.Assert(err, jc.ErrorIsNil) - ch := testcharms.Repo.CharmArchivePath(s.CharmsPath, "storage-block") - err = runDeploy(c, ch, "--storage", "data=loop-pool,1G", "--series", "trusty") + err := runDeploy(c, ch, "--storage", "data=machinescoped,1G", "--series", "trusty") c.Assert(err, jc.ErrorIsNil) curl := charm.MustParseURL("local:trusty/storage-block-1") application, _ := s.AssertService(c, "storage-block", curl, 1, 0) @@ -323,7 +317,7 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(cons, jc.DeepEquals, map[string]state.StorageConstraints{ "data": { - Pool: "loop-pool", + Pool: "machinescoped", Count: 1, Size: 1024, }, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/application/get.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/application/get.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/application/get.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/application/get.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,6 +4,8 @@ package application import ( + "fmt" + "github.com/juju/cmd" "github.com/juju/errors" "launchpad.net/gnuflag" @@ -17,6 +19,8 @@ Displays configuration settings for a deployed application.`[1:] var usageGetConfigDetails = ` +By default, all configuration (keys, values, metadata) for the application are +displayed if a key is not specified. Output includes the name of the charm used to deploy the application and a listing of the application-specific configuration settings. See `[1:] + "`juju status`" + ` for application names. @@ -24,6 +28,7 @@ Examples: juju get-config mysql juju get-config mysql-testing + juju get-config mysql wait-timeout See also: set-config @@ -38,7 +43,8 @@ // getCommand retrieves the configuration of an application. type getCommand struct { modelcmd.ModelCommandBase - ApplicationName string + applicationName string + key string out cmd.Output api getServiceAPI } @@ -46,7 +52,7 @@ func (c *getCommand) Info() *cmd.Info { return &cmd.Info{ Name: "get-config", - Args: "", + Args: " [attribute-key]", Purpose: usageGetConfigSummary, Doc: usageGetConfigDetails, Aliases: []string{"get-configs"}, @@ -65,8 +71,12 @@ if len(args) == 0 { return errors.New("no application name specified") } - c.ApplicationName = args[0] - return cmd.CheckEmpty(args[1:]) + c.applicationName = args[0] + if len(args) == 1 { + return nil + } + c.key = args[1] + return cmd.CheckEmpty(args[2:]) } // getServiceAPI defines the methods on the client API @@ -96,10 +106,22 @@ } defer apiclient.Close() - results, err := apiclient.Get(c.ApplicationName) + results, err := apiclient.Get(c.applicationName) if err != nil { return err } + if c.key != "" { + info, found := results.Config[c.key].(map[string]interface{}) + if !found { + return fmt.Errorf("key %q not found in %q application settings.", c.key, c.applicationName) + } + out, err := cmd.FormatSmart(info["value"]) + if err != nil { + return err + } + fmt.Fprintf(ctx.Stdout, "%v\n", string(out)) + return nil + } resultsMap := map[string]interface{}{ "application": results.Application, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/application/get_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/application/get_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/application/get_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/application/get_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -77,6 +77,23 @@ c.Assert(err, gc.ErrorMatches, "no application name specified") } +func (s *GetSuite) TestGetCommandInitWithApplication(c *gc.C) { + err := coretesting.InitCommand(application.NewGetCommandForTest(s.fake), []string{"app"}) + // everything ok + c.Assert(err, jc.ErrorIsNil) +} + +func (s *GetSuite) TestGetCommandInitWithKey(c *gc.C) { + err := coretesting.InitCommand(application.NewGetCommandForTest(s.fake), []string{"app", "key"}) + // everything ok + c.Assert(err, jc.ErrorIsNil) +} + +func (s *GetSuite) TestGetCommandInitTooManyArgs(c *gc.C) { + err := coretesting.InitCommand(application.NewGetCommandForTest(s.fake), []string{"app", "key", "another"}) + c.Assert(err, gc.ErrorMatches, `unrecognized args: \["another"\]`) +} + func (s *GetSuite) TestGetConfig(c *gc.C) { for _, t := range getTests { ctx := coretesting.Context(c) @@ -98,3 +115,19 @@ c.Assert(actual, gc.DeepEquals, expected) } } + +func (s *GetSuite) TestGetConfigKey(c *gc.C) { + ctx := coretesting.Context(c) + code := cmd.Main(application.NewGetCommandForTest(s.fake), ctx, []string{"dummy-application", "title"}) + c.Check(code, gc.Equals, 0) + c.Assert(ctx.Stderr.(*bytes.Buffer).String(), gc.Equals, "") + c.Assert(ctx.Stdout.(*bytes.Buffer).String(), gc.Equals, "Nearly There\n") +} + +func (s *GetSuite) TestGetConfigKeyNotFound(c *gc.C) { + ctx := coretesting.Context(c) + code := cmd.Main(application.NewGetCommandForTest(s.fake), ctx, []string{"dummy-application", "invalid"}) + c.Check(code, gc.Equals, 1) + c.Assert(ctx.Stderr.(*bytes.Buffer).String(), gc.Equals, "error: key \"invalid\" not found in \"dummy-application\" application settings.\n") + c.Assert(ctx.Stdout.(*bytes.Buffer).String(), gc.Equals, "") +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/application/upgradecharm.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/application/upgradecharm.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/application/upgradecharm.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/application/upgradecharm.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,6 +7,7 @@ "fmt" "os" "path/filepath" + "strings" "github.com/juju/cmd" "github.com/juju/errors" @@ -21,6 +22,7 @@ "github.com/juju/juju/api" "github.com/juju/juju/api/application" "github.com/juju/juju/api/charms" + "github.com/juju/juju/api/modelconfig" "github.com/juju/juju/charmstore" "github.com/juju/juju/cmd/juju/block" "github.com/juju/juju/cmd/modelcmd" @@ -162,6 +164,14 @@ return application.NewClient(root), nil } +func (c *upgradeCharmCommand) newModelConfigAPIClient() (*modelconfig.Client, error) { + root, err := c.NewAPIRoot() + if err != nil { + return nil, errors.Trace(err) + } + return modelconfig.NewClient(root), nil +} + // Run connects to the specified environment and starts the charm // upgrade process. func (c *upgradeCharmCommand) Run(ctx *cmd.Context) error { @@ -175,6 +185,7 @@ if err != nil { return err } + defer serviceClient.Close() oldURL, err := serviceClient.GetCharmURL(c.ApplicationName) if err != nil { @@ -201,13 +212,22 @@ } csClient := newCharmStoreClient(bakeryClient).WithChannel(c.Channel) - conf, err := getClientConfig(client) + modelConfigClient, err := c.newModelConfigAPIClient() + if err != nil { + return err + } + defer modelConfigClient.Close() + conf, err := getModelConfig(modelConfigClient) if err != nil { return errors.Trace(err) } resolver := newCharmURLResolver(conf, csClient) chID, csMac, err := c.addCharm(oldURL, newRef, client, resolver) if err != nil { + if err1, ok := errors.Cause(err).(*termsRequiredError); ok { + terms := strings.Join(err1.Terms, " ") + return errors.Errorf(`Declined: please agree to the following terms %s. Try: "juju agree %s"`, terms, terms) + } return block.ProcessBlockedError(err, block.BlockChange) } ctx.Infof("Added charm %q to the model.", chID.URL) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/application/upgradecharm_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/application/upgradecharm_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/application/upgradecharm_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/application/upgradecharm_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -428,3 +428,21 @@ "wordpress": {charm: "cs:~client-username/trusty/wordpress-1"}, }) } + +func (s *UpgradeCharmCharmStoreSuite) TestUpgradeWithTermsNotSigned(c *gc.C) { + id, ch := testcharms.UploadCharm(c, s.client, "quantal/terms1-1", "terms1") + err := runDeploy(c, "quantal/terms1") + c.Assert(err, jc.ErrorIsNil) + id.Revision = id.Revision + 1 + err = s.client.UploadCharmWithRevision(id, ch, -1) + c.Assert(err, gc.IsNil) + err = s.client.Publish(id, []csclientparams.Channel{csclientparams.StableChannel}, nil) + c.Assert(err, gc.IsNil) + s.termsDischargerError = &httpbakery.Error{ + Message: "term agreement required: term/1 term/2", + Code: "term agreement required", + } + expectedError := `Declined: please agree to the following terms term/1 term/2. Try: "juju agree term/1 term/2"` + err = runUpgradeCharm(c, "terms1") + c.Assert(err, gc.ErrorMatches, expectedError) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/backups/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/backups/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/backups/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/backups/export_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -71,39 +71,52 @@ store jujuclient.ClientStore, api RestoreAPI, getArchive func(string) (ArchiveReader, *params.BackupsMetadataResult, error), - getEnviron func(string, *params.BackupsMetadataResult) (environs.Environ, *restoreBootstrapParams, error), + newEnviron func(environs.OpenParams) (environs.Environ, error), + getRebootstrapParams func(string, *params.BackupsMetadataResult) (*restoreBootstrapParams, error), ) cmd.Command { c := &restoreCommand{ - getArchiveFunc: getArchive, - getEnvironFunc: getEnviron, + getArchiveFunc: getArchive, + newEnvironFunc: newEnviron, + getRebootstrapParamsFunc: getRebootstrapParams, newAPIClientFunc: func() (RestoreAPI, error) { return api, nil }, waitForAgentFunc: func(ctx *cmd.Context, c *modelcmd.ModelCommandBase, controllerName string) error { return nil - }} - if getEnviron == nil { - c.getEnvironFunc = func(controllerNme string, meta *params.BackupsMetadataResult) (environs.Environ, *restoreBootstrapParams, error) { - return c.getEnviron(controllerNme, meta) - } + }, + } + if getRebootstrapParams == nil { + c.getRebootstrapParamsFunc = c.getRebootstrapParams + } + if newEnviron == nil { + c.newEnvironFunc = environs.New } c.Log = &cmd.Log{} c.SetClientStore(store) return modelcmd.Wrap(c) } -func GetEnvironFunc(e environs.Environ, cloud string) func(string, *params.BackupsMetadataResult) (environs.Environ, *restoreBootstrapParams, error) { - return func(string, *params.BackupsMetadataResult) (environs.Environ, *restoreBootstrapParams, error) { - return e, &restoreBootstrapParams{ +func GetEnvironFunc(e environs.Environ) func(environs.OpenParams) (environs.Environ, error) { + return func(environs.OpenParams) (environs.Environ, error) { + return e, nil + } +} + +func GetRebootstrapParamsFunc(cloud string) func(string, *params.BackupsMetadataResult) (*restoreBootstrapParams, error) { + return func(string, *params.BackupsMetadataResult) (*restoreBootstrapParams, error) { + return &restoreBootstrapParams{ ControllerConfig: testing.FakeControllerConfig(), - CloudName: cloud, - CloudRegion: "a-region", + Cloud: environs.CloudSpec{ + Type: "lxd", + Name: cloud, + Region: "a-region", + }, }, nil } } -func GetEnvironFuncWithError() func(string, *params.BackupsMetadataResult) (environs.Environ, *restoreBootstrapParams, error) { - return func(string, *params.BackupsMetadataResult) (environs.Environ, *restoreBootstrapParams, error) { - return nil, nil, errors.New("failed") +func GetRebootstrapParamsFuncWithError() func(string, *params.BackupsMetadataResult) (*restoreBootstrapParams, error) { + return func(string, *params.BackupsMetadataResult) (*restoreBootstrapParams, error) { + return nil, errors.New("failed") } } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/backups/restore.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/backups/restore.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/backups/restore.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/backups/restore.go 2016-08-16 08:56:25.000000000 +0000 @@ -34,7 +34,8 @@ // NewRestoreCommand returns a command used to restore a backup. func NewRestoreCommand() cmd.Command { restoreCmd := &restoreCommand{} - restoreCmd.getEnvironFunc = restoreCmd.getEnviron + restoreCmd.newEnvironFunc = environs.New + restoreCmd.getRebootstrapParamsFunc = restoreCmd.getRebootstrapParams restoreCmd.newAPIClientFunc = func() (RestoreAPI, error) { return restoreCmd.newClient() } @@ -53,10 +54,11 @@ bootstrap bool uploadTools bool - newAPIClientFunc func() (RestoreAPI, error) - getEnvironFunc func(string, *params.BackupsMetadataResult) (environs.Environ, *restoreBootstrapParams, error) - getArchiveFunc func(string) (ArchiveReader, *params.BackupsMetadataResult, error) - waitForAgentFunc func(ctx *cmd.Context, c *modelcmd.ModelCommandBase, controllerName string) error + newAPIClientFunc func() (RestoreAPI, error) + newEnvironFunc func(environs.OpenParams) (environs.Environ, error) + getRebootstrapParamsFunc func(string, *params.BackupsMetadataResult) (*restoreBootstrapParams, error) + getArchiveFunc func(string) (ArchiveReader, *params.BackupsMetadataResult, error) + waitForAgentFunc func(ctx *cmd.Context, c *modelcmd.ModelCommandBase, controllerName string) error } // RestoreAPI is used to invoke various API calls. @@ -135,37 +137,35 @@ type restoreBootstrapParams struct { ControllerConfig controller.Config - CloudName string - CloudRegion string + Cloud environs.CloudSpec CredentialName string - Credential cloud.Credential AdminSecret string - CAPrivateKey string + ModelConfig *config.Config } -// getEnviron returns the environ for the specified controller, or -// mocked out environ for testing. -func (c *restoreCommand) getEnviron( +// getRebootstrapParams returns the params for rebootstrapping the +// specified controller. +func (c *restoreCommand) getRebootstrapParams( controllerName string, meta *params.BackupsMetadataResult, -) (environs.Environ, *restoreBootstrapParams, error) { - // TODO(axw) delete this and -b in 2.0-beta2. We will update bootstrap - // with a flag to specify a restore file. When we do that, we'll need - // to extract the CA cert from the backup, and we'll need to reset the - // password after restore so the admin user can login. - // We also need to store things like the admin-secret, controller - // certificate etc with the backup. +) (*restoreBootstrapParams, error) { + // TODO(axw) delete this and -b. We will update bootstrap with a flag + // to specify a restore file. When we do that, we'll need to extract + // the CA cert from the backup, and we'll need to reset the password + // after restore so the admin user can login. We also need to store + // things like the admin-secret, controller certificate etc with the + // backup. store := c.ClientStore() config, params, err := modelcmd.NewGetBootstrapConfigParamsFunc(store)(controllerName) if err != nil { - return nil, nil, errors.Trace(err) + return nil, errors.Trace(err) } - provider, err := environs.Provider(params.Config.Type()) + provider, err := environs.Provider(config.CloudType) if err != nil { - return nil, nil, errors.Trace(err) + return nil, errors.Trace(err) } - cfg, err := provider.BootstrapConfig(*params) + cfg, err := provider.PrepareConfig(*params) if err != nil { - return nil, nil, errors.Trace(err) + return nil, errors.Trace(err) } // Get the local admin user so we can use the password as the admin secret. @@ -178,11 +178,11 @@ // No relevant local admin user so generate a new secret. buf := make([]byte, 16) if _, err := io.ReadFull(rand.Reader, buf); err != nil { - return nil, nil, errors.Annotate(err, "generating new admin secret") + return nil, errors.Annotate(err, "generating new admin secret") } adminSecret = fmt.Sprintf("%x", buf) } else { - return nil, nil, errors.Trace(err) + return nil, errors.Trace(err) } // Turn on safe mode so that the newly bootstrapped instance @@ -192,31 +192,82 @@ "provisioner-safe-mode": true, }) if err != nil { - return nil, nil, errors.Annotatef(err, "cannot enable provisioner-safe-mode") + return nil, errors.Annotatef(err, "cannot enable provisioner-safe-mode") } - controllerCfg := controller.Config{ - controller.ControllerUUIDKey: params.ControllerUUID, - controller.CACertKey: meta.CACert, - } - env, err := environs.New(cfg) - return env, &restoreBootstrapParams{ - ControllerConfig: controllerCfg, - CloudName: config.Cloud, - CloudRegion: config.CloudRegion, - CredentialName: config.Credential, - Credential: params.Credentials, - AdminSecret: adminSecret, - }, err + controllerCfg := make(controller.Config) + for k, v := range config.ControllerConfig { + controllerCfg[k] = v + } + controllerCfg[controller.ControllerUUIDKey] = params.ControllerUUID + controllerCfg[controller.CACertKey] = meta.CACert + + return &restoreBootstrapParams{ + controllerCfg, + params.Cloud, + config.Credential, + adminSecret, + cfg, + }, nil } // rebootstrap will bootstrap a new server in safe-mode (not killing any other agent) // if there is no current server available to restore to. func (c *restoreCommand) rebootstrap(ctx *cmd.Context, meta *params.BackupsMetadataResult) error { - env, params, err := c.getEnvironFunc(c.ControllerName(), meta) + params, err := c.getRebootstrapParamsFunc(c.ControllerName(), meta) if err != nil { return errors.Trace(err) } + + cloudParam, err := cloud.CloudByName(params.Cloud.Name) + if errors.IsNotFound(err) { + provider, err := environs.Provider(params.Cloud.Type) + if errors.IsNotFound(err) { + return errors.NewNotFound(nil, fmt.Sprintf("unknown cloud %q, please try %q", params.Cloud.Name, "juju update-clouds")) + } else if err != nil { + return errors.Trace(err) + } + detector, ok := provider.(environs.CloudRegionDetector) + if !ok { + return errors.Errorf("provider %q does not support detecting regions", params.Cloud.Type) + } + var cloudEndpoint string + regions, err := detector.DetectRegions() + if errors.IsNotFound(err) { + // It's not an error to have no regions. If the + // provider does not support regions, then we + // reinterpret the supplied region name as the + // cloud's endpoint. This enables the user to + // supply, for example, maas/ or manual/. + if params.Cloud.Region != "" { + cloudEndpoint = params.Cloud.Region + } + } else if err != nil { + return errors.Annotatef(err, "detecting regions for %q cloud provider", params.Cloud.Type) + } + schemas := provider.CredentialSchemas() + authTypes := make([]cloud.AuthType, 0, len(schemas)) + for authType := range schemas { + authTypes = append(authTypes, authType) + } + cloudParam = &cloud.Cloud{ + Type: params.Cloud.Type, + AuthTypes: authTypes, + Endpoint: cloudEndpoint, + Regions: regions, + } + } else if err != nil { + return errors.Trace(err) + } + + env, err := c.newEnvironFunc(environs.OpenParams{ + Cloud: params.Cloud, + Config: params.ModelConfig, + }) + if err != nil { + return errors.Annotate(err, "opening environ for rebootstrapping") + } + instanceIds, err := env.ControllerInstances(params.ControllerConfig.ControllerUUID()) if err != nil && errors.Cause(err) != environs.ErrNotBootstrapped { return errors.Annotatef(err, "cannot determine controller instances") @@ -242,11 +293,6 @@ config.UUIDKey: hostedModelUUID.String(), } - cloudParam, err := cloud.CloudByName(params.CloudName) - if err != nil { - return errors.Trace(err) - } - // We may have previous controller metadata. We need to replace that so it // will contain the new CA Cert and UUID required to connect to the newly // bootstrapped controller API. @@ -254,8 +300,8 @@ details := jujuclient.ControllerDetails{ ControllerUUID: params.ControllerConfig.ControllerUUID(), CACert: meta.CACert, - Cloud: params.CloudName, - CloudRegion: params.CloudRegion, + Cloud: params.Cloud.Name, + CloudRegion: params.Cloud.Region, } err = store.UpdateController(c.ControllerName(), details) if err != nil { @@ -263,16 +309,12 @@ } bootVers := version.Current - var cred *cloud.Credential - if params.Credential.AuthType() != cloud.EmptyAuthType { - cred = ¶ms.Credential - } args := bootstrap.BootstrapParams{ Cloud: *cloudParam, - CloudName: params.CloudName, - CloudRegion: params.CloudRegion, + CloudName: params.Cloud.Name, + CloudRegion: params.Cloud.Region, CloudCredentialName: params.CredentialName, - CloudCredential: cred, + CloudCredential: params.Cloud.Credential, ModelConstraints: c.constraints, UploadTools: c.uploadTools, BuildToolsTarball: sync.BuildToolsTarball, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/backups/restore_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/backups/restore_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/backups/restore_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/backups/restore_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,6 +5,7 @@ import ( "io" + "sort" "github.com/juju/errors" jc "github.com/juju/testing/checkers" @@ -14,6 +15,7 @@ "github.com/juju/juju/apiserver/params" "github.com/juju/juju/cloud" "github.com/juju/juju/cmd/juju/backups" + "github.com/juju/juju/controller" "github.com/juju/juju/environs" "github.com/juju/juju/environs/bootstrap" "github.com/juju/juju/instance" @@ -21,6 +23,7 @@ "github.com/juju/juju/jujuclient/jujuclienttesting" "github.com/juju/juju/network" _ "github.com/juju/juju/provider/dummy" + _ "github.com/juju/juju/provider/lxd" "github.com/juju/juju/testing" ) @@ -68,11 +71,16 @@ } s.store.BootstrapConfig["testing"] = jujuclient.BootstrapConfig{ Cloud: "mycloud", + CloudType: "dummy", CloudRegion: "a-region", Config: map[string]interface{}{ "type": "dummy", "name": "admin", }, + ControllerConfig: controller.Config{ + "api-port": 17070, + "state-port": 37017, + }, } s.store.Credentials["dummy"] = cloud.CloudCredential{ AuthCredentials: map[string]cloud.Credential{ @@ -85,7 +93,7 @@ } func (s *restoreSuite) TestRestoreArgs(c *gc.C) { - s.command = backups.NewRestoreCommandForTest(s.store, nil, nil, nil) + s.command = backups.NewRestoreCommandForTest(s.store, nil, nil, nil, nil) _, err := testing.RunCommand(c, s.command, "restore") c.Assert(err, gc.ErrorMatches, "you must specify either a file or a backup id.") @@ -124,7 +132,8 @@ func(string) (backups.ArchiveReader, *params.BackupsMetadataResult, error) { return &mockArchiveReader{}, ¶ms.BackupsMetadataResult{}, nil }, - backups.GetEnvironFunc(fakeEnv, "mycloud"), + backups.GetEnvironFunc(fakeEnv), + backups.GetRebootstrapParamsFunc("mycloud"), ) _, err := testing.RunCommand(c, s.command, "restore", "--file", "afile", "-b") c.Assert(err, gc.ErrorMatches, ".*still seems to exist.*") @@ -139,7 +148,8 @@ CACert: testing.CACert, }, nil }, - backups.GetEnvironFunc(fakeEnv, "mycloud"), + backups.GetEnvironFunc(fakeEnv), + backups.GetRebootstrapParamsFunc("mycloud"), ) s.PatchValue(&backups.BootstrapFunc, func(ctx environs.BootstrapContext, environ environs.Environ, args bootstrap.BootstrapParams) error { return errors.New("failed to bootstrap new controller") @@ -159,7 +169,9 @@ func(string) (backups.ArchiveReader, *params.BackupsMetadataResult, error) { return &mockArchiveReader{}, &metadata, nil }, - nil) + backups.GetEnvironFunc(fakeEnviron{}), + backups.GetRebootstrapParamsFunc("mycloud"), + ) s.PatchValue(&backups.BootstrapFunc, func(ctx environs.BootstrapContext, environ environs.Environ, args bootstrap.BootstrapParams) error { return errors.New("failed to bootstrap new controller") }) @@ -178,7 +190,8 @@ func(string) (backups.ArchiveReader, *params.BackupsMetadataResult, error) { return &mockArchiveReader{}, &metadata, nil }, - backups.GetEnvironFuncWithError(), + nil, + backups.GetRebootstrapParamsFuncWithError(), ) s.PatchValue(&backups.BootstrapFunc, func(ctx environs.BootstrapContext, environ environs.Environ, args bootstrap.BootstrapParams) error { // We should not call bootstrap. @@ -210,14 +223,25 @@ func(string) (backups.ArchiveReader, *params.BackupsMetadataResult, error) { return &mockArchiveReader{}, &metadata, nil }, - backups.GetEnvironFunc(fakeEnv, "mycloud"), + backups.GetEnvironFunc(fakeEnv), + backups.GetRebootstrapParamsFunc("mycloud"), ) + boostrapped := false s.PatchValue(&backups.BootstrapFunc, func(ctx environs.BootstrapContext, environ environs.Environ, args bootstrap.BootstrapParams) error { + c.Assert(args.ControllerConfig, jc.DeepEquals, controller.Config{ + "controller-uuid": "deadbeef-0bad-400d-8000-4b1d0d06f00d", + "ca-cert": testing.CACert, + "state-port": 1234, + "api-port": 17777, + "set-numa-control-policy": false, + }) + boostrapped = true return nil }) _, err := testing.RunCommand(c, s.command, "restore", "-m", "testing:test1", "--file", "afile", "-b") c.Assert(err, jc.ErrorIsNil) + c.Assert(boostrapped, jc.IsTrue) c.Assert(s.store.Controllers["testing"], jc.DeepEquals, jujuclient.ControllerDetails{ Cloud: "mycloud", CloudRegion: "a-region", @@ -228,6 +252,37 @@ }) } +func (s *restoreSuite) TestRestoreReboostrapBuiltInProvider(c *gc.C) { + metadata := params.BackupsMetadataResult{ + CACert: testing.CACert, + CAPrivateKey: testing.CAKey, + } + fakeEnv := fakeEnviron{} + s.command = backups.NewRestoreCommandForTest( + s.store, &mockRestoreAPI{}, + func(string) (backups.ArchiveReader, *params.BackupsMetadataResult, error) { + return &mockArchiveReader{}, &metadata, nil + }, + backups.GetEnvironFunc(fakeEnv), + backups.GetRebootstrapParamsFunc("lxd"), + ) + boostrapped := false + s.PatchValue(&backups.BootstrapFunc, func(ctx environs.BootstrapContext, environ environs.Environ, args bootstrap.BootstrapParams) error { + boostrapped = true + sort.Sort(args.Cloud.AuthTypes) + c.Assert(args.Cloud, jc.DeepEquals, cloud.Cloud{ + Type: "lxd", + AuthTypes: []cloud.AuthType{"certificate", "empty"}, + Regions: []cloud.Region{{Name: "localhost"}}, + }) + return nil + }) + + _, err := testing.RunCommand(c, s.command, "restore", "-m", "testing:test1", "--file", "afile", "-b") + c.Assert(err, jc.ErrorIsNil) + c.Assert(boostrapped, jc.IsTrue) +} + type fakeInstance struct { instance.Instance id instance.Id diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/cloud/addcredential.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/cloud/addcredential.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/cloud/addcredential.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/cloud/addcredential.go 2016-08-16 08:56:25.000000000 +0000 @@ -158,7 +158,7 @@ if err != nil { return err } - fmt.Fprintf(ctxt.Stdout, "credentials updated for cloud %s\n", c.CloudName) + fmt.Fprintf(ctxt.Stdout, "Credentials updated for cloud %q.\n", c.CloudName) return nil } @@ -182,7 +182,7 @@ return errors.Trace(err) } if credentialName == "" { - fmt.Fprintln(ctxt.Stderr, "credentials entry aborted") + fmt.Fprintln(ctxt.Stderr, "Credentials entry aborted.") return nil } @@ -220,12 +220,12 @@ if err != nil { return errors.Trace(err) } - fmt.Fprintf(ctxt.Stdout, "credentials added for cloud %s\n\n", c.CloudName) + fmt.Fprintf(ctxt.Stdout, "Credentials added for cloud %s.\n\n", c.CloudName) return nil } func (c *addCredentialCommand) promptCredentialName(out io.Writer, in io.Reader) (string, error) { - fmt.Fprint(out, " credential name: ") + fmt.Fprint(out, "Enter credential name: ") input, err := readLine(in) if err != nil { return "", errors.Trace(err) @@ -234,7 +234,11 @@ } func (c *addCredentialCommand) promptReplace(out io.Writer, in io.Reader) (bool, error) { - fmt.Fprint(out, " replace existing credential? [y/N]: ") + fmt.Fprint(out, ` +A credential with that name already exists. + +Replace the existing credential? (y/N): `[1:]) + input, err := readLine(in) if err != nil { return false, errors.Trace(err) @@ -244,7 +248,7 @@ func (c *addCredentialCommand) promptAuthType(out io.Writer, in io.Reader, authTypes []jujucloud.AuthType) (jujucloud.AuthType, error) { if len(authTypes) == 1 { - fmt.Fprintf(out, " auth-type: %v\n", authTypes[0]) + fmt.Fprintf(out, "Using auth-type %q.\n", authTypes[0]) return authTypes[0], nil } authType := "" @@ -256,7 +260,8 @@ } } for { - fmt.Fprintf(out, " select auth-type [%v]: ", strings.Join(choices, ", ")) + fmt.Fprintf(out, "Auth Types\n%s\n\nSelect auth-type: ", + strings.Join(choices, "\n")) input, err := readLine(in) if err != nil { return "", errors.Trace(err) @@ -275,7 +280,7 @@ if isValid { break } - fmt.Fprintf(out, " ...invalid auth type %q\n", authType) + fmt.Fprintf(out, "Invalid auth type %q.\n", authType) } return jujucloud.AuthType(authType), nil } @@ -311,7 +316,7 @@ } } if !isValid { - fmt.Fprintf(out, " ...invalid value %q\n", value) + fmt.Fprintf(out, "Invalid value %q.\n", value) continue } if value == "" && !currentAttr.Optional { @@ -337,7 +342,7 @@ if value != "" && currentAttr.FilePath { value, err = jujucloud.ValidateFileAttrValue(value) if err != nil { - fmt.Fprintf(out, " ...%s\n", err.Error()) + fmt.Fprintf(out, "Invalid file attribute %q.\n", err.Error()) continue } } @@ -373,7 +378,7 @@ } // Prompt for and accept input for field value. - fmt.Fprintf(out, " %s%s: ", name, optionsPrompt) + fmt.Fprintf(out, "Enter %s%s: ", name, optionsPrompt) var input string var err error if attr.Hidden { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/cloud/detectcredentials.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/cloud/detectcredentials.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/cloud/detectcredentials.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/cloud/detectcredentials.go 2016-08-16 08:56:25.000000000 +0000 @@ -37,38 +37,44 @@ cloudByNameFunc func(string) (*jujucloud.Cloud, error) } -var detectCredentialsDoc = ` -The autoload-credentials command looks for well known locations for supported clouds and -allows the user to interactively save these into the Juju credentials store to make these -available when bootstrapping new controllers and creating new models. - -The resulting credentials may be viewed with ` + "`juju credentials`" + `. +const detectCredentialsSummary = `Attempts to automatically add or replace credentials for a cloud.` -The clouds for which credentials may be autoloaded are: - -AWS - Credentials and regions are located in: - 1. On Linux, $HOME/.aws/credentials and $HOME/.aws/config +var detectCredentialsDoc = ` +Well known locations for specific clouds are searched and any found +information is presented interactively to the user. +An alternative to this command is ` + "`juju add-credential`" + `. +Below are the cloud types for which credentials may be autoloaded, +including the locations searched. + +EC2 + Credentials and regions: + 1. On Linux, $HOME/.aws/credentials and $HOME/.aws/config 2. Environment variables AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY - + GCE - Credentials are located in: - 1. A JSON file whose path is specified by the GOOGLE_APPLICATION_CREDENTIALS environment variable - 2. A JSON file in a knowm location eg on Linux $HOME/.config/gcloud/application_default_credentials.json - Default region is specified by the CLOUDSDK_COMPUTE_REGION environment variable. - + Credentials: + 1. A JSON file whose path is specified by the + GOOGLE_APPLICATION_CREDENTIALS environment variable + 2. On Linux, $HOME/.config/gcloud/application_default_credentials.json + Default region is specified by the CLOUDSDK_COMPUTE_REGION environment + variable. + 3. On Windows, %APPDATA%\gcloud\application_default_credentials.json + OpenStack - Credentials are located in: + Credentials: 1. On Linux, $HOME/.novarc - 2. Environment variables OS_USERNAME, OS_PASSWORD, OS_TENANT_NAME - + 2. Environment variables OS_USERNAME, OS_PASSWORD, OS_TENANT_NAME, + OS_DOMAIN_NAME + Example: - juju autoload-credentials + juju autoload-credentials See Also: - juju credentials - juju add-credential -` + list-credentials + remove-credential + set-default-credential + add-credential +`[1:] // NewDetectCredentialsCommand returns a command to add credential information to credentials.yaml. func NewDetectCredentialsCommand() cmd.Command { @@ -86,7 +92,7 @@ func (c *detectCredentialsCommand) Info() *cmd.Info { return &cmd.Info{ Name: "autoload-credentials", - Purpose: "Looks for cloud credentials and caches those for use by Juju when bootstrapping.", + Purpose: detectCredentialsSummary, Doc: detectCredentialsDoc, } } @@ -329,7 +335,7 @@ } func (c *detectCredentialsCommand) promptCredentialNumber(out io.Writer, in io.Reader) (string, error) { - fmt.Fprint(out, "Save any? Type number, or Q to quit, then enter. ") + fmt.Fprint(out, "Select a credential to save by number, or type Q to quit: ") defer out.Write([]byte{'\n'}) input, err := readLine(in) if err != nil { @@ -339,7 +345,7 @@ } func (c *detectCredentialsCommand) promptCloudName(out io.Writer, in io.Reader, defaultCloudName, cloudType string) (string, error) { - text := fmt.Sprintf(`Enter cloud to which the credential belongs, or Q to quit [%s] `, defaultCloudName) + text := fmt.Sprintf(`Select the cloud it belongs to, or type Q to quit [%s]: `, defaultCloudName) fmt.Fprint(out, text) defer out.Write([]byte{'\n'}) input, err := readLine(in) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/bootstrap.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/bootstrap.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/bootstrap.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/bootstrap.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,8 +4,10 @@ package commands import ( + "bufio" "fmt" "os" + "os/user" "strings" "github.com/juju/cmd" @@ -29,7 +31,6 @@ "github.com/juju/juju/instance" "github.com/juju/juju/juju/osenv" "github.com/juju/juju/jujuclient" - "github.com/juju/juju/provider/gce" jujuversion "github.com/juju/juju/version" ) @@ -43,7 +44,7 @@ Initializes a cloud environment.`[1:] var usageBootstrapDetails = ` -Used without arguments, bootstrap will step you through the process of +Used without arguments, bootstrap will step you through the process of initializing a Juju cloud environment. Initialization consists of creating a 'controller' model and provisioning a machine to act as controller. @@ -51,7 +52,7 @@ See --clouds for a list of clouds and credentials. See --regions for a list of available regions for a given cloud. -Credentials are set beforehand and are distinct from any other +Credentials are set beforehand and are distinct from any other configuration (see `[1:] + "`juju add-credential`" + `). The 'controller' model typically does not run workloads. It should remain pristine to run and manage Juju's own infrastructure for the corresponding @@ -63,26 +64,26 @@ If '--bootstrap-constraints' is used, its values will also apply to any future controllers provisioned for high availability (HA). -If '--constraints' is used, its values will be set as the default -constraints for all future workload machines in the model, exactly as if +If '--constraints' is used, its values will be set as the default +constraints for all future workload machines in the model, exactly as if the constraints were set with ` + "`juju set-model-constraints`" + `. It is possible to override constraints and the automatic machine selection algorithm by assigning a "placement directive" via the '--to' option. This -dictates what machine to use for the controller. This would typically be +dictates what machine to use for the controller. This would typically be used with the MAAS provider ('--to .maas'). -You can change the default timeout and retry delays used during the +You can change the default timeout and retry delays used during the bootstrap by changing the following settings in your configuration (all values represent number of seconds): # How long to wait for a connection to the controller bootstrap-timeout: 600 # default: 10 minutes - # How long to wait between connection attempts to a controller + # How long to wait between connection attempts to a controller address. bootstrap-retry-delay: 5 # default: 5 seconds # How often to refresh controller addresses from the API server. bootstrap-addresses-delay: 10 # default: 10 seconds - + Private clouds may need to specify their own custom image metadata and tools/agent. Use '--metadata-source' whose value is a local directory. The value of '--agent-version' will become the default tools version to @@ -99,7 +100,7 @@ juju bootstrap --config agent-version=1.25.3 joe-us-east-1 aws juju bootstrap --config bootstrap-timeout=1200 joe-eastus azure -See also: +See also: add-credentials add-model set-constraints` @@ -141,6 +142,9 @@ Cloud string Region string noGUI bool + interactive bool + + flagset *gnuflag.FlagSet } func (c *bootstrapCommand) Info() *cmd.Info { @@ -153,6 +157,9 @@ } func (c *bootstrapCommand) SetFlags(f *gnuflag.FlagSet) { + // we need to store this so that later we can easily check how many flags + // have been set (for interactive mode). + c.flagset = f f.Var(constraints.ConstraintsValue{Target: &c.Constraints}, "constraints", "Set model constraints") f.Var(constraints.ConstraintsValue{Target: &c.BootstrapConstraints}, "bootstrap-constraints", "Specify bootstrap machine constraints") f.StringVar(&c.BootstrapSeries, "bootstrap-series", "", "Specify the series of the bootstrap machine") @@ -175,6 +182,21 @@ } func (c *bootstrapCommand) Init(args []string) (err error) { + if len(args) == 0 { + switch c.flagset.NFlag() { + case 0: + // no args or flags, go interactive. + c.interactive = true + return nil + case 1: + if c.UploadTools { + // juju bootstrap --upload-tools is ok for interactive, too. + c.interactive = true + return nil + } + // some other flag was set, which means non-interactive. + } + } if c.showClouds && c.showRegionsForCloud != "" { return fmt.Errorf("--clouds and --regions can't be used together") } @@ -279,6 +301,12 @@ // a juju in that environment if none already exists. If there is as yet no environments.yaml file, // the user is informed how to create one. func (c *bootstrapCommand) Run(ctx *cmd.Context) (resultErr error) { + if c.interactive { + if err := c.runInteractive(ctx); err != nil { + return errors.Trace(err) + } + // now run normal bootstrap using info gained above. + } if c.showClouds { return printClouds(ctx, c.ClientStore()) } @@ -319,7 +347,7 @@ // cloud's endpoint. This enables the user to // supply, for example, maas/ or manual/. if c.Region != "" { - ctx.Verbosef("interpreting %q as the cloud endpoint") + ctx.Verbosef("interpreting %q as the cloud endpoint", c.Region) cloudEndpoint = c.Region c.Region = "" } @@ -415,17 +443,6 @@ return errors.Trace(err) } - // TODO(axw) this is a dirty hack to get 2.0-beta10 over the line. - // We need to pull this out immediately after, and then update - // everything to remove credentials from model config. - if cloud.Type == "gce" && credential.AuthType() == jujucloud.JSONFileAuthType { - cred, err := gce.ParseJSONAuthFile(credential.Attributes()["file"]) - if err != nil { - return errors.Trace(err) - } - *credential = cred - } - // Create a model config, and split out any controller // and bootstrap config attributes. modelConfigAttrs := map[string]interface{}{ @@ -433,9 +450,6 @@ "name": bootstrap.ControllerModelName, config.UUIDKey: controllerUUID.String(), } - for k, v := range cloud.Config { - modelConfigAttrs[k] = v - } userConfigAttrs, err := c.config.ReadAttrs(ctx) if err != nil { return errors.Trace(err) @@ -445,6 +459,20 @@ } bootstrapConfigAttrs := make(map[string]interface{}) controllerConfigAttrs := make(map[string]interface{}) + // Based on the attribute names in clouds.yaml, create + // a map of shared config for all models on this cloud. + inheritedControllerAttrs := make(map[string]interface{}) + for k, v := range cloud.Config { + switch { + case bootstrap.IsBootstrapAttribute(k): + bootstrapConfigAttrs[k] = v + continue + case controller.ControllerOnlyAttribute(k): + controllerConfigAttrs[k] = v + continue + } + inheritedControllerAttrs[k] = v + } for k, v := range modelConfigAttrs { switch { case bootstrap.IsBootstrapAttribute(k): @@ -499,19 +527,38 @@ } }() + bootstrapModelConfig := make(map[string]interface{}) + for k, v := range inheritedControllerAttrs { + bootstrapModelConfig[k] = v + } + for k, v := range modelConfigAttrs { + bootstrapModelConfig[k] = v + } + // Add in any default attribute values if not already + // specified, making the recorded bootstrap config + // immutable to changes in Juju. + for k, v := range config.ConfigDefaults() { + if _, ok := bootstrapModelConfig[k]; !ok { + bootstrapModelConfig[k] = v + } + } + environ, err := bootstrapPrepare( modelcmd.BootstrapContext(ctx), store, bootstrap.PrepareParams{ - BaseConfig: modelConfigAttrs, - ControllerConfig: controllerConfig, - ControllerName: c.controllerName, - CloudName: c.Cloud, - CloudRegion: region.Name, - CloudEndpoint: region.Endpoint, - CloudStorageEndpoint: region.StorageEndpoint, - Credential: *credential, - CredentialName: credentialName, - AdminSecret: bootstrapConfig.AdminSecret, + ModelConfig: bootstrapModelConfig, + ControllerConfig: controllerConfig, + ControllerName: c.controllerName, + Cloud: environs.CloudSpec{ + Type: cloud.Type, + Name: c.Cloud, + Region: region.Name, + Endpoint: region.Endpoint, + StorageEndpoint: region.StorageEndpoint, + Credential: credential, + }, + CredentialName: credentialName, + AdminSecret: bootstrapConfig.AdminSecret, }, ) if err != nil { @@ -597,6 +644,9 @@ "name": c.hostedModelName, config.UUIDKey: hostedModelUUID.String(), } + for k, v := range inheritedControllerAttrs { + hostedModelConfig[k] = v + } // We copy across any user supplied attributes to the hosted model config. // But only if the attributes have not been removed from the controller @@ -614,15 +664,6 @@ delete(hostedModelConfig, config.AuthorizedKeysKey) delete(hostedModelConfig, config.AgentVersionKey) - // Based on the attribute names in clouds.yaml, create - // a map of shared config for all models on this cloud. - inheritedControllerAttrs := make(map[string]interface{}) - for k := range cloud.Config { - if v, ok := controllerModelConfigAttrs[k]; ok { - inheritedControllerAttrs[k] = v - } - } - // Check whether the Juju GUI must be installed in the controller. // Leaving this value empty means no GUI will be installed. var guiDataSourceBaseURL string @@ -668,7 +709,7 @@ return errors.Annotate(err, "failed to bootstrap model") } - if err := c.SetModelName(c.hostedModelName); err != nil { + if err := c.SetModelName(modelcmd.JoinModelName(c.controllerName, c.hostedModelName)); err != nil { return errors.Trace(err) } @@ -683,6 +724,49 @@ return waitForAgentInitialisation(ctx, &c.ModelCommandBase, c.controllerName) } +// runInteractive queries the user about bootstrap config interactively at the +// command prompt. +func (c *bootstrapCommand) runInteractive(ctx *cmd.Context) error { + scanner := bufio.NewScanner(ctx.Stdin) + clouds, err := assembleClouds() + if err != nil { + return errors.Trace(err) + } + c.Cloud, err = queryCloud(clouds, jujucloud.DefaultLXD, scanner, ctx.Stdout) + if err != nil { + return errors.Trace(err) + } + cloud, err := jujucloud.CloudByName(c.Cloud) + if err != nil { + return errors.Trace(err) + } + + switch len(cloud.Regions) { + case 0: + // No region to choose, nothing to do. + case 1: + // If there's just one, don't prompt, just use it. + c.Region = cloud.Regions[0].Name + default: + c.Region, err = queryRegion(c.Cloud, cloud.Regions, scanner, ctx.Stdout) + if err != nil { + return errors.Trace(err) + } + } + + var username string + if u, err := user.Current(); err == nil { + username = u.Username + } + defName := defaultControllerName(username, c.Cloud, c.Region, cloud) + + c.controllerName, err = queryName(defName, scanner, ctx.Stdout) + if err != nil { + return errors.Trace(err) + } + return nil +} + // getRegion returns the cloud.Region to use, based on the specified // region name. If no region name is specified, and there is at least // one region, we use the first region in the list. diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/bootstrap_interactive.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/bootstrap_interactive.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/bootstrap_interactive.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/bootstrap_interactive.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,126 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "bufio" + "fmt" + "io" + "sort" + "strings" + + "github.com/juju/errors" + jujucloud "github.com/juju/juju/cloud" + "github.com/juju/juju/cmd/juju/common" + "github.com/juju/juju/cmd/juju/interact" +) + +// assembleClouds +func assembleClouds() ([]string, error) { + public, _, err := jujucloud.PublicCloudMetadata(jujucloud.JujuPublicCloudsPath()) + if err != nil { + return nil, errors.Trace(err) + } + + personal, err := jujucloud.PersonalCloudMetadata() + if err != nil { + return nil, errors.Trace(err) + } + + return sortClouds(public, common.BuiltInClouds(), personal), nil +} + +// queryCloud asks the user to choose a cloud. +func queryCloud(clouds []string, defCloud string, scanner *bufio.Scanner, w io.Writer) (string, error) { + list := strings.Join(clouds, "\n") + if _, err := fmt.Fprint(w, "Clouds\n", list, "\n\n"); err != nil { + return "", errors.Trace(err) + } + + // add support for a default (empty) selection. + clouds = append(clouds, "") + + verify := interact.MatchOptions(clouds, errors.Errorf("Invalid cloud.")) + + query := fmt.Sprintf("Select a cloud [%s]: ", defCloud) + cloud, err := interact.QueryVerify([]byte(query), scanner, w, verify) + if err != nil { + return "", errors.Trace(err) + } + if cloud == "" { + return defCloud, nil + } + + cloudName, ok := interact.FindMatch(cloud, clouds) + if !ok { + // should be impossible + return "", errors.Errorf("invalid cloud name chosen: %s", cloud) + } + + return cloudName, nil +} + +// queryRegion asks the user to pick a region of the ones passed in. The first +// region in the list will be the default. +func queryRegion(cloud string, regions []jujucloud.Region, scanner *bufio.Scanner, w io.Writer) (string, error) { + fmt.Fprintf(w, "Regions in %s:\n", cloud) + names := jujucloud.RegionNames(regions) + // add an empty string to allow for a default value. Also gives us an extra + // line return after the list of names. + names = append(names, "") + if _, err := fmt.Fprintln(w, strings.Join(names, "\n")); err != nil { + return "", errors.Trace(err) + } + verify := interact.MatchOptions(names, errors.Errorf("Invalid region.")) + defaultRegion := regions[0].Name + query := fmt.Sprintf("Select a region in %s [%s]: ", cloud, defaultRegion) + region, err := interact.QueryVerify([]byte(query), scanner, w, verify) + if err != nil { + return "", errors.Trace(err) + } + if region == "" { + return defaultRegion, nil + } + regionName, ok := interact.FindMatch(region, names) + if !ok { + // should be impossible + return "", errors.Errorf("invalid region name chosen: %s", region) + } + + return regionName, nil +} + +func defaultControllerName(username, cloudname, region string, cloud *jujucloud.Cloud) string { + name := cloudname + if len(cloud.Regions) > 1 { + name = region + } + if username == "" { + return name + } + return username + "-" + name +} + +func queryName(defName string, scanner *bufio.Scanner, w io.Writer) (string, error) { + query := fmt.Sprintf("Enter a name for the Controller [%s]: ", defName) + name, err := interact.QueryVerify([]byte(query), scanner, w, nil) + if err != nil { + return "", errors.Trace(err) + } + if name == "" { + return defName, nil + } + return name, nil +} + +func sortClouds(maps ...map[string]jujucloud.Cloud) []string { + var clouds []string + for _, m := range maps { + for name := range m { + clouds = append(clouds, name) + } + } + sort.Strings(clouds) + return clouds +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/bootstrap_interactive_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/bootstrap_interactive_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/bootstrap_interactive_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/bootstrap_interactive_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,161 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "bufio" + "bytes" + "io/ioutil" + "strings" + + "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + jujucloud "github.com/juju/juju/cloud" + jujutesting "github.com/juju/juju/testing" +) + +type BSInteractSuite struct { + testing.IsolationSuite +} + +var _ = gc.Suite(BSInteractSuite{}) + +func (BSInteractSuite) TestInitEmpty(c *gc.C) { + cmd := &bootstrapCommand{} + err := jujutesting.InitCommand(cmd, nil) + c.Assert(err, jc.ErrorIsNil) + c.Assert(cmd.interactive, jc.IsTrue) +} + +func (BSInteractSuite) TestInitUploadTools(c *gc.C) { + cmd := &bootstrapCommand{} + err := jujutesting.InitCommand(cmd, []string{"--upload-tools"}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(cmd.interactive, jc.IsTrue) + c.Assert(cmd.UploadTools, jc.IsTrue) +} + +func (BSInteractSuite) TestInitArg(c *gc.C) { + cmd := &bootstrapCommand{} + err := jujutesting.InitCommand(cmd, []string{"foo"}) + c.Assert(err, gc.ErrorMatches, "controller name and cloud name are required") + c.Assert(cmd.interactive, jc.IsFalse) +} + +func (BSInteractSuite) TestInitTwoArgs(c *gc.C) { + cmd := &bootstrapCommand{} + err := jujutesting.InitCommand(cmd, []string{"foo", "bar"}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(cmd.interactive, jc.IsFalse) +} + +func (BSInteractSuite) TestInitOtherFlag(c *gc.C) { + cmd := &bootstrapCommand{} + err := jujutesting.InitCommand(cmd, []string{"--clouds"}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(cmd.interactive, jc.IsFalse) +} + +func (BSInteractSuite) TestQueryCloud(c *gc.C) { + input := "search\n" + + scanner := bufio.NewScanner(strings.NewReader(input)) + clouds := []string{"books", "books-china", "search", "local"} + + buf := bytes.Buffer{} + cloud, err := queryCloud(clouds, "local", scanner, &buf) + c.Assert(err, jc.ErrorIsNil) + c.Assert(cloud, gc.Equals, "search") + + // clouds should be printed out in the same order as they're given. + expected := ` +Clouds +books +books-china +search +local + +Select a cloud [local]: +`[1:] + c.Assert(buf.String(), gc.Equals, expected) +} + +func (BSInteractSuite) TestQueryCloudDefault(c *gc.C) { + input := "\n" + + scanner := bufio.NewScanner(strings.NewReader(input)) + clouds := []string{"books", "local"} + + cloud, err := queryCloud(clouds, "local", scanner, ioutil.Discard) + c.Assert(err, jc.ErrorIsNil) + c.Assert(cloud, gc.Equals, "local") +} + +func (BSInteractSuite) TestQueryRegion(c *gc.C) { + input := "mars-west1\n" + + scanner := bufio.NewScanner(strings.NewReader(input)) + regions := []jujucloud.Region{ + {Name: "mars-east1"}, + {Name: "mars-west1"}, + {Name: "jupiter-central"}, + } + + buf := bytes.Buffer{} + region, err := queryRegion("goggles", regions, scanner, &buf) + c.Assert(err, jc.ErrorIsNil) + c.Assert(region, gc.Equals, "mars-west1") + + // regions should be alphabetized, and the first one in the original list + // should be the default. + expected := ` +Regions in goggles: +jupiter-central +mars-east1 +mars-west1 + +Select a region in goggles [mars-east1]: +`[1:] + c.Assert(buf.String(), gc.Equals, expected) +} + +func (BSInteractSuite) TestQueryRegionDefault(c *gc.C) { + input := "\n" + + scanner := bufio.NewScanner(strings.NewReader(input)) + regions := []jujucloud.Region{ + {Name: "mars-east1"}, + {Name: "jupiter-central"}, + } + + region, err := queryRegion("goggles", regions, scanner, ioutil.Discard) + c.Assert(err, jc.ErrorIsNil) + c.Assert(region, gc.Equals, regions[0].Name) +} + +func (BSInteractSuite) TestQueryName(c *gc.C) { + input := "awesome-cloud\n" + + scanner := bufio.NewScanner(strings.NewReader(input)) + buf := bytes.Buffer{} + name, err := queryName("default-cloud", scanner, &buf) + c.Assert(err, jc.ErrorIsNil) + c.Assert(name, gc.Equals, "awesome-cloud") + + expected := ` +Enter a name for the Controller [default-cloud]: +`[1:] + c.Assert(buf.String(), gc.Equals, expected) +} + +func (BSInteractSuite) TestQueryNameDefault(c *gc.C) { + input := "\n" + + scanner := bufio.NewScanner(strings.NewReader(input)) + name, err := queryName("default-cloud", scanner, ioutil.Discard) + c.Assert(err, jc.ErrorIsNil) + c.Assert(name, gc.Equals, "default-cloud") +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/bootstrap_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/bootstrap_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/bootstrap_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/bootstrap_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,6 +4,7 @@ package commands import ( + "bytes" "fmt" "io/ioutil" "os" @@ -29,6 +30,7 @@ "github.com/juju/juju/constraints" "github.com/juju/juju/environs" "github.com/juju/juju/environs/bootstrap" + "github.com/juju/juju/environs/config" "github.com/juju/juju/environs/filestorage" "github.com/juju/juju/environs/gui" "github.com/juju/juju/environs/imagemetadata" @@ -231,7 +233,7 @@ c.Assert(controller.APIEndpoints, gc.DeepEquals, addrConnectedTo) c.Assert(utils.IsValidUUIDString(controller.ControllerUUID), jc.IsTrue) - controllerModel, err := s.store.ModelByName(controllerName, bootstrap.ControllerModelName) + controllerModel, err := s.store.ModelByName(controllerName, "admin@local/controller") c.Assert(err, jc.ErrorIsNil) c.Assert(controllerModel.ModelUUID, gc.Equals, controller.ControllerUUID) @@ -241,12 +243,18 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(bootstrapConfig.Cloud, gc.Equals, "dummy") c.Assert(bootstrapConfig.Credential, gc.Equals, "") - c.Assert(bootstrapConfig.Config, jc.DeepEquals, map[string]interface{}{ + expected := map[string]interface{}{ "name": bootstrap.ControllerModelName, "type": "dummy", "default-series": "raring", "authorized-keys": "public auth key\n", - }) + } + for k, v := range config.ConfigDefaults() { + if _, ok := expected[k]; !ok { + expected[k] = v + } + } + c.Assert(bootstrapConfig.Config, jc.DeepEquals, expected) return restore } @@ -340,11 +348,6 @@ err: `--clouds and --regions can't be used together`, }} -func (s *BootstrapSuite) TestRunControllerNameMissing(c *gc.C) { - _, err := coretesting.RunCommand(c, s.newBootstrapCommand()) - c.Check(err, gc.ErrorMatches, "controller name and cloud name are required") -} - func (s *BootstrapSuite) TestRunCloudNameMissing(c *gc.C) { _, err := coretesting.RunCommand(c, s.newBootstrapCommand(), "my-controller") c.Check(err, gc.ErrorMatches, "controller name and cloud name are required") @@ -395,7 +398,7 @@ c.Assert(currentController, gc.Equals, "devcontroller") modelName, err := s.store.CurrentModel(currentController) c.Assert(err, jc.ErrorIsNil) - c.Assert(modelName, gc.Equals, "default") + c.Assert(modelName, gc.Equals, "admin@local/default") } func (s *BootstrapSuite) TestBootstrapDefaultModel(c *gc.C) { @@ -591,11 +594,11 @@ jujuclient.ClientStore, bootstrap.PrepareParams, ) (environs.Environ, error) { - s.writeControllerModelAccountInfo(c, "foo", "bar", "foobar@local") + s.writeControllerModelAccountInfo(c, "foo", "foobar@local/bar", "foobar@local") return nil, fmt.Errorf("mock-prepare") }) - s.writeControllerModelAccountInfo(c, "olddevcontroller", "fredmodel", "fred@local") + s.writeControllerModelAccountInfo(c, "olddevcontroller", "fred@local/fredmodel", "fred@local") _, err := coretesting.RunCommand(c, s.newBootstrapCommand(), "devcontroller", "dummy", "--auto-upgrade") c.Assert(err, gc.ErrorMatches, "mock-prepare") @@ -606,14 +609,14 @@ c.Assert(accountDetails.User, gc.Equals, "fred@local") currentModel, err := s.store.CurrentModel(currentController) c.Assert(err, jc.ErrorIsNil) - c.Assert(currentModel, gc.Equals, "fredmodel") + c.Assert(currentModel, gc.Equals, "fred@local/fredmodel") } func (s *BootstrapSuite) TestBootstrapAlreadyExists(c *gc.C) { const controllerName = "devcontroller" s.patchVersionAndSeries(c, "raring") - s.writeControllerModelAccountInfo(c, "devcontroller", "fredmodel", "fred@local") + s.writeControllerModelAccountInfo(c, "devcontroller", "fred@local/fredmodel", "fred@local") ctx := coretesting.Context(c) _, errc := cmdtesting.RunCommand(ctx, s.newBootstrapCommand(), controllerName, "dummy", "--auto-upgrade") @@ -627,7 +630,7 @@ c.Assert(accountDetails.User, gc.Equals, "fred@local") currentModel, err := s.store.CurrentModel(currentController) c.Assert(err, jc.ErrorIsNil) - c.Assert(currentModel, gc.Equals, "fredmodel") + c.Assert(currentModel, gc.Equals, "fred@local/fredmodel") } func (s *BootstrapSuite) TestInvalidLocalSource(c *gc.C) { @@ -741,21 +744,56 @@ // are automatically synchronized. _, err := coretesting.RunCommand( c, s.newBootstrapCommand(), "--metadata-source", sourceDir, - "devcontroller", "dummy-cloud/region-1", + "devcontroller", "dummy-cloud/region-1", "--config", "default-series=trusty", ) c.Assert(err, jc.ErrorIsNil) - p, err := environs.Provider("dummy") + bootstrapConfig, params, err := modelcmd.NewGetBootstrapConfigParamsFunc(s.store)("devcontroller") + c.Assert(err, jc.ErrorIsNil) + provider, err := environs.Provider(bootstrapConfig.CloudType) c.Assert(err, jc.ErrorIsNil) - cfg, err := modelcmd.NewGetBootstrapConfigFunc(s.store)("devcontroller") + cfg, err := provider.PrepareConfig(*params) + c.Assert(err, jc.ErrorIsNil) + + env, err := environs.New(environs.OpenParams{ + Cloud: params.Cloud, + Config: cfg, + }) c.Assert(err, jc.ErrorIsNil) - env, err := p.PrepareForBootstrap(envtesting.BootstrapContext(c), cfg) + err = env.PrepareForBootstrap(envtesting.BootstrapContext(c)) c.Assert(err, jc.ErrorIsNil) // Now check the available tools which are the 1.2.0 envtools. checkTools(c, env, v120All) } +func (s *BootstrapSuite) TestInteractiveBootstrap(c *gc.C) { + s.patchVersionAndSeries(c, "raring") + + cmd := s.newBootstrapCommand() + err := coretesting.InitCommand(cmd, nil) + c.Assert(err, jc.ErrorIsNil) + ctx := coretesting.Context(c) + out := bytes.Buffer{} + ctx.Stdin = strings.NewReader(` +dummy-cloud +region-1 +my-dummy-cloud +`[1:]) + ctx.Stdout = &out + err = cmd.Run(ctx) + if err != nil { + c.Logf(out.String()) + } + c.Assert(err, jc.ErrorIsNil) + + name := s.store.CurrentControllerName + c.Assert(name, gc.Equals, "my-dummy-cloud") + controller := s.store.Controllers[name] + c.Assert(controller.Cloud, gc.Equals, "dummy-cloud") + c.Assert(controller.CloudRegion, gc.Equals, "region-1") +} + func (s *BootstrapSuite) setupAutoUploadTest(c *gc.C, vers, ser string) { s.PatchValue(&envtools.BundleTools, toolstesting.GetMockBundleTools(c)) sourceDir := createToolsSource(c, vAll) @@ -976,7 +1014,7 @@ "many-credentials-no-auth-types", "--credential", "one", ) - c.Assert(bootstrap.args.Cloud.AuthTypes, jc.SameContents, []cloud.AuthType{"one", "two"}) + c.Assert(bootstrap.args.Cloud.AuthTypes, jc.SameContents, cloud.AuthTypes{"one", "two"}) } func (s *BootstrapSuite) TestBootstrapProviderDetectRegions(c *gc.C) { @@ -1036,7 +1074,7 @@ _, err := coretesting.RunCommand(c, s.newBootstrapCommand(), "ctrl", "dummy/DUMMY") c.Assert(err, gc.ErrorMatches, "mock-prepare") - c.Assert(prepareParams.CloudRegion, gc.Equals, "dummy") + c.Assert(prepareParams.Cloud.Region, gc.Equals, "dummy") } func (s *BootstrapSuite) TestBootstrapConfigFile(c *gc.C) { @@ -1181,6 +1219,91 @@ c.Assert(err, gc.ErrorMatches, "cloud foo not found") } +func (s *BootstrapSuite) TestBootstrapSetsControllerOnBase(c *gc.C) { + // This test ensures that the controller name is correctly set on + // on the bootstrap commands embedded ModelCommandBase. Without + // this, the concurrent bootstraps fail. + // See https://pad.lv/1604223 + + resetJujuXDGDataHome(c) + s.patchVersionAndSeries(c, "raring") + + const controllerName = "dev" + + // Record the controller name seen by ModelCommandBase at the end of bootstrap. + var seenControllerName string + s.PatchValue(&waitForAgentInitialisation, func(_ *cmd.Context, base *modelcmd.ModelCommandBase, _ string) error { + seenControllerName = base.ControllerName() + return nil + }) + + // Run the bootstrap command in another goroutine, sending the + // dummy provider ops to opc. + errc := make(chan error, 1) + opc := make(chan dummy.Operation) + dummy.Listen(opc) + go func() { + defer func() { + dummy.Listen(nil) + close(opc) + }() + com := s.newBootstrapCommand() + args := []string{controllerName, "dummy", "--auto-upgrade"} + if err := coretesting.InitCommand(com, args); err != nil { + errc <- err + return + } + errc <- com.Run(cmdtesting.NullContext(c)) + }() + + // Wait for bootstrap to start. + select { + case op := <-opc: + _, ok := op.(dummy.OpBootstrap) + c.Assert(ok, jc.IsTrue) + case <-time.After(coretesting.LongWait): + c.Fatal("timed out") + } + + // Simulate another controller being bootstrapped during the + // bootstrap. Changing the current controller shouldn't affect the + // bootstrap process. + c.Assert(s.store.UpdateController("another", jujuclient.ControllerDetails{ + ControllerUUID: "uuid", + CACert: "cert", + }), jc.ErrorIsNil) + c.Assert(s.store.SetCurrentController("another"), jc.ErrorIsNil) + + // Let bootstrap finish. + select { + case op := <-opc: + _, ok := op.(dummy.OpFinalizeBootstrap) + c.Assert(ok, jc.IsTrue) + case <-time.After(coretesting.LongWait): + c.Fatal("timed out") + } + + // Ensure there were no errors reported. + select { + case err := <-errc: + c.Assert(err, jc.ErrorIsNil) + case <-time.After(coretesting.LongWait): + c.Fatal("timed out") + } + + // Wait for the ops channel to close. + select { + case _, ok := <-opc: + c.Assert(ok, jc.IsFalse) + case <-time.After(coretesting.LongWait): + c.Fatal("timed out") + } + + // Expect to see that the correct controller was in use at the end + // of bootstrap. + c.Assert(seenControllerName, gc.Equals, controllerName) +} + // createToolsSource writes the mock tools and metadata into a temporary // directory and returns it. func createToolsSource(c *gc.C, versions []version.Binary) string { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/enableha.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/enableha.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/enableha.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/enableha.go 2016-08-16 08:56:25.000000000 +0000 @@ -45,17 +45,17 @@ // NumControllers specifies the number of controllers to make available. NumControllers int - // Series is used for newly created machines, if specified. - // Otherwise, the environment's default-series is used. - Series string + // Constraints, if specified, will be merged with those already // in the environment when creating new machines. Constraints constraints.Value + // Placement specifies specific machine(s) which will be used to host // new controllers. If there are more controllers required than // machines specified, new machines will be created. // Placement is passed verbatim to the API, to be evaluated and used server-side. Placement []string + // PlacementSpec holds the unparsed placement directives argument (--to). PlacementSpec string } @@ -74,18 +74,16 @@ # then that number will be ensured. juju enable-ha - # Ensure that 5 controllers are available, with newly created - # controller machines having the "trusty" series. - juju enable-ha -n 5 --series=trusty + # Ensure that 5 controllers are available. + juju enable-ha -n 5 # Ensure that 7 controllers are available, with newly created - # controller machines having the default series, and at least - # 8GB RAM. + # controller machines having at least 8GB RAM. juju enable-ha -n 7 --constraints mem=8G # Ensure that 7 controllers are available, with machines server1 and # server2 used first, and if necessary, newly created controller - # machines having the default series, and at least 8GB RAM. + # machines having at least 8GB RAM. juju enable-ha -n 7 --to server1,server2 --constraints mem=8G ` @@ -149,7 +147,6 @@ func (c *enableHACommand) SetFlags(f *gnuflag.FlagSet) { f.IntVar(&c.NumControllers, "n", 0, "Number of controllers to make available") - f.StringVar(&c.Series, "series", "", "The charm series") f.StringVar(&c.PlacementSpec, "to", "", "The machine(s) to become controllers, bypasses constraints") f.Var(constraints.ConstraintsValue{&c.Constraints}, "constraints", "Additional machine constraints") c.out.AddFlags(f, "simple", map[string]cmd.Formatter{ @@ -201,7 +198,7 @@ type MakeHAClient interface { Close() error EnableHA( - numControllers int, cons constraints.Value, series string, + numControllers int, cons constraints.Value, placement []string) (params.ControllersChanges, error) } @@ -217,7 +214,6 @@ enableHAResult, err := haClient.EnableHA( c.NumControllers, c.Constraints, - c.Series, c.Placement, ) if err != nil { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/enableha_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/enableha_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/enableha_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/enableha_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -50,7 +50,6 @@ numControllers int cons constraints.Value err error - series string placement []string result params.ControllersChanges } @@ -59,12 +58,10 @@ return nil } -func (f *fakeHAClient) EnableHA(numControllers int, cons constraints.Value, - series string, placement []string) (params.ControllersChanges, error) { +func (f *fakeHAClient) EnableHA(numControllers int, cons constraints.Value, placement []string) (params.ControllersChanges, error) { f.numControllers = numControllers f.cons = cons - f.series = series f.placement = placement if f.err != nil { @@ -112,7 +109,6 @@ c.Assert(s.fake.numControllers, gc.Equals, 1) c.Assert(&s.fake.cons, jc.Satisfies, constraints.IsEmpty) - c.Assert(s.fake.series, gc.Equals, "") c.Assert(len(s.fake.placement), gc.Equals, 0) } @@ -137,7 +133,6 @@ c.Assert(s.fake.numControllers, gc.Equals, 3) c.Assert(&s.fake.cons, jc.Satisfies, constraints.IsEmpty) - c.Assert(s.fake.series, gc.Equals, "") c.Assert(len(s.fake.placement), gc.Equals, 0) var result map[string][]string @@ -157,7 +152,6 @@ c.Assert(s.fake.numControllers, gc.Equals, 3) c.Assert(&s.fake.cons, jc.Satisfies, constraints.IsEmpty) - c.Assert(s.fake.series, gc.Equals, "") c.Assert(len(s.fake.placement), gc.Equals, 0) var result map[string][]string @@ -166,9 +160,9 @@ c.Assert(result, gc.DeepEquals, expected) } -func (s *EnableHASuite) TestEnableHAWithSeries(c *gc.C) { +func (s *EnableHASuite) TestEnableHAWithFive(c *gc.C) { // Also test with -n 5 to validate numbers other than 1 and 3 - ctx, err := s.runEnableHA(c, "--series", "series", "-n", "5") + ctx, err := s.runEnableHA(c, "-n", "5") c.Assert(err, jc.ErrorIsNil) c.Assert(coretesting.Stdout(ctx), gc.Equals, "maintaining machines: 0\n"+ @@ -176,7 +170,6 @@ c.Assert(s.fake.numControllers, gc.Equals, 5) c.Assert(&s.fake.cons, jc.Satisfies, constraints.IsEmpty) - c.Assert(s.fake.series, gc.Equals, "series") c.Assert(len(s.fake.placement), gc.Equals, 0) } @@ -190,7 +183,6 @@ c.Assert(s.fake.numControllers, gc.Equals, 3) expectedCons := constraints.MustParse("mem=4G") c.Assert(s.fake.cons, gc.DeepEquals, expectedCons) - c.Assert(s.fake.series, gc.Equals, "") c.Assert(len(s.fake.placement), gc.Equals, 0) } @@ -203,7 +195,6 @@ c.Assert(s.fake.numControllers, gc.Equals, 3) c.Assert(&s.fake.cons, jc.Satisfies, constraints.IsEmpty) - c.Assert(s.fake.series, gc.Equals, "") expectedPlacement := []string{"valid"} c.Assert(s.fake.placement, gc.DeepEquals, expectedPlacement) } @@ -229,7 +220,6 @@ c.Assert(s.fake.numControllers, gc.Equals, 0) c.Assert(&s.fake.cons, jc.Satisfies, constraints.IsEmpty) - c.Assert(s.fake.series, gc.Equals, "") c.Assert(len(s.fake.placement), gc.Equals, 0) } @@ -244,7 +234,6 @@ c.Assert(s.fake.numControllers, gc.Equals, 0) c.Assert(&s.fake.cons, jc.Satisfies, constraints.IsEmpty) - c.Assert(s.fake.series, gc.Equals, "") c.Assert(len(s.fake.placement), gc.Equals, 0) } @@ -272,6 +261,13 @@ c.Check(s.fake.numControllers, gc.Equals, 0) c.Check(&s.fake.cons, jc.Satisfies, constraints.IsEmpty) - c.Check(s.fake.series, gc.Equals, "") c.Check(len(s.fake.placement), gc.Equals, 2) } + +func (s *EnableHASuite) TestEnableHADisallowsSeries(c *gc.C) { + // We don't allow --series as an argument. This test ensures it is not + // inadvertantly added back. + ctx, err := s.runEnableHA(c, "-n", "0", "--series", "xenian") + c.Assert(err, gc.ErrorMatches, "flag provided but not defined: --series") + c.Assert(coretesting.Stdout(ctx), gc.Equals, "") +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/main.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/main.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/main.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/main.go 2016-08-16 08:56:25.000000000 +0000 @@ -248,9 +248,6 @@ r.Register(newUpgradeJujuCommand(nil)) r.Register(application.NewUpgradeCharmCommand()) - // Charm publishing commands. - r.Register(newPublishCommand()) - // Charm tool commands. r.Register(newHelpToolCommand()) r.Register(charmcmd.NewSuperCommand()) @@ -279,6 +276,7 @@ r.Register(user.NewDisableCommand()) r.Register(user.NewLoginCommand()) r.Register(user.NewLogoutCommand()) + r.Register(user.NewRemoveCommand()) // Manage cached images r.Register(cachedimages.NewRemoveCommand()) @@ -292,6 +290,9 @@ // Manage model r.Register(model.NewGetCommand()) + r.Register(model.NewModelDefaultsCommand()) + r.Register(model.NewSetModelDefaultsCommand()) + r.Register(model.NewUnsetModelDefaultsCommand()) r.Register(model.NewSetCommand()) r.Register(model.NewUnsetCommand()) r.Register(model.NewRetryProvisioningCommand()) @@ -304,6 +305,9 @@ if featureflag.Enabled(feature.Migration) { r.Register(newMigrateCommand()) } + if featureflag.Enabled(feature.DeveloperMode) { + r.Register(model.NewDumpCommand()) + } // Manage and control actions r.Register(action.NewStatusCommand()) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/main_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/main_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/main_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/main_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -389,6 +389,7 @@ "debug-hooks", "debug-log", "debug-metrics", + "remove-user", "deploy", "destroy-controller", "destroy-model", @@ -439,9 +440,10 @@ "logout", "machine", "machines", + "model-config", + "model-defaults", "models", "plans", - "publish", "register", "relate", //alias for add-relation "remove-all-blocks", @@ -472,6 +474,7 @@ "set-meter-status", "set-model-config", "set-model-constraints", + "set-model-default", "set-plan", "ssh-key", "ssh-keys", @@ -504,6 +507,7 @@ "upload-backup", "unregister", "unset-model-config", + "unset-model-default", "update-clouds", "upgrade-charm", "upgrade-gui", diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/migrate.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/migrate.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/migrate.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/migrate.go 2016-08-16 08:56:25.000000000 +0000 @@ -51,10 +51,10 @@ completion. The progress of a migration can be tracked using the "status" command and by consulting the logs. -See Also: - juju help login - juju help controllers - juju help status +See also: + login + controllers + status ` // Info implements cmd.Command. @@ -62,7 +62,7 @@ return &cmd.Info{ Name: "migrate", Args: " ", - Purpose: "migrate a hosted model to another controller", + Purpose: "Migrate a hosted model to another controller.", Doc: migrateDoc, } } @@ -87,10 +87,11 @@ func (c *migrateCommand) getMigrationSpec() (*controller.ModelMigrationSpec, error) { store := c.ClientStore() - modelInfo, err := store.ModelByName(c.ControllerName(), c.model) + modelUUIDs, err := c.ModelUUIDs([]string{c.model}) if err != nil { return nil, err } + modelUUID := modelUUIDs[0] controllerInfo, err := store.ControllerByName(c.targetController) if err != nil { @@ -103,7 +104,7 @@ } return &controller.ModelMigrationSpec{ - ModelUUID: modelInfo.ModelUUID, + ModelUUID: modelUUID, TargetControllerUUID: controllerInfo.ControllerUUID, TargetAddrs: controllerInfo.APIEndpoints, TargetCACert: controllerInfo.CACert, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/migrate_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/migrate_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/migrate_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/migrate_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,6 +8,7 @@ jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" + "github.com/juju/juju/api/base" "github.com/juju/juju/api/controller" "github.com/juju/juju/cmd/modelcmd" "github.com/juju/juju/feature" @@ -49,7 +50,7 @@ c.Assert(err, jc.ErrorIsNil) // Define the model to migrate in the config. - err = s.store.UpdateModel("source", "model", jujuclient.ModelDetails{ + err = s.store.UpdateModel("source", "source@local/model", jujuclient.ModelDetails{ ModelUUID: modelUUID, }) c.Assert(err, jc.ErrorIsNil) @@ -73,22 +74,22 @@ } func (s *MigrateSuite) TestMissingModel(c *gc.C) { - _, err := s.runCommand(c) + _, err := s.makeAndRun(c) c.Assert(err, gc.ErrorMatches, "model not specified") } func (s *MigrateSuite) TestMissingTargetController(c *gc.C) { - _, err := s.runCommand(c, "mymodel") + _, err := s.makeAndRun(c, "mymodel") c.Assert(err, gc.ErrorMatches, "target controller not specified") } func (s *MigrateSuite) TestTooManyArgs(c *gc.C) { - _, err := s.runCommand(c, "one", "too", "many") + _, err := s.makeAndRun(c, "one", "too", "many") c.Assert(err, gc.ErrorMatches, "too many arguments specified") } func (s *MigrateSuite) TestSuccess(c *gc.C) { - ctx, err := s.runCommand(c, "model", "target") + ctx, err := s.makeAndRun(c, "model", "target") c.Assert(err, jc.ErrorIsNil) c.Check(testing.Stderr(ctx), gc.Matches, "Migration started with ID \"uuid:0\"\n") @@ -103,22 +104,40 @@ } func (s *MigrateSuite) TestModelDoesntExist(c *gc.C) { - _, err := s.runCommand(c, "wat", "target") + cmd := s.makeCommand() + cmd.SetModelApi(&fakeModelAPI{}) + _, err := s.run(c, cmd, "wat", "target") c.Check(err, gc.ErrorMatches, "model .+ not found") c.Check(s.api.specSeen, gc.IsNil) // API shouldn't have been called } +func (s *MigrateSuite) TestModelDoesntExistBeforeRefresh(c *gc.C) { + cmd := s.makeCommand() + cmd.SetModelApi(&fakeModelAPI{model: "wat"}) // Model is available after refresh + _, err := s.run(c, cmd, "wat", "target") + c.Check(err, jc.ErrorIsNil) + c.Check(s.api.specSeen, gc.NotNil) +} + func (s *MigrateSuite) TestControllerDoesntExist(c *gc.C) { - _, err := s.runCommand(c, "model", "wat") + _, err := s.makeAndRun(c, "model", "wat") c.Check(err, gc.ErrorMatches, "controller wat not found") c.Check(s.api.specSeen, gc.IsNil) // API shouldn't have been called } -func (s *MigrateSuite) runCommand(c *gc.C, args ...string) (*cmd.Context, error) { +func (s *MigrateSuite) makeAndRun(c *gc.C, args ...string) (*cmd.Context, error) { + return s.run(c, s.makeCommand(), args...) +} + +func (s *MigrateSuite) makeCommand() *migrateCommand { cmd := &migrateCommand{ api: s.api, } cmd.SetClientStore(s.store) + return cmd +} + +func (s *MigrateSuite) run(c *gc.C, cmd *migrateCommand, args ...string) (*cmd.Context, error) { return testing.RunCommand(c, modelcmd.WrapController(cmd), args...) } @@ -130,3 +149,22 @@ a.specSeen = &spec return "uuid:0", nil } + +type fakeModelAPI struct { + model string +} + +func (m *fakeModelAPI) ListModels(user string) ([]base.UserModel, error) { + if m.model == "" { + return []base.UserModel{}, nil + } + return []base.UserModel{{ + Name: m.model, + UUID: modelUUID, + Owner: "source@local", + }}, nil +} + +func (m *fakeModelAPI) Close() error { + return nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/publish.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/publish.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/publish.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/publish.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,192 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package commands - -import ( - "fmt" - "os" - "strings" - "time" - - "github.com/juju/cmd" - "github.com/juju/utils/bzr" - "gopkg.in/juju/charm.v6-unstable" - "gopkg.in/juju/charmrepo.v2-unstable" - "launchpad.net/gnuflag" - - "github.com/juju/juju/cmd/modelcmd" -) - -func newPublishCommand() cmd.Command { - return modelcmd.Wrap(&publishCommand{}) -} - -type publishCommand struct { - modelcmd.ModelCommandBase - URL string - CharmPath string - - // changePushLocation allows translating the branch location - // for testing purposes. - changePushLocation func(loc string) string - - pollDelay time.Duration -} - -const publishDoc = ` - can be a charm URL, or an unambiguously condensed form of it; -the following forms are accepted: - -For cs:precise/mysql - cs:precise/mysql - precise/mysql - -For cs:~user/precise/mysql - cs:~user/precise/mysql - -There is no default series, so one must be provided explicitly when -informing a charm URL. If the URL isn't provided, an attempt will be -made to infer it from the current branch push URL. -` - -func (c *publishCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "publish", - Args: "[]", - Purpose: "Publish charm to the store.", - Doc: publishDoc, - } -} - -func (c *publishCommand) SetFlags(f *gnuflag.FlagSet) { - f.StringVar(&c.CharmPath, "from", ".", "Path for charm to be published") -} - -func (c *publishCommand) Init(args []string) error { - if len(args) == 0 { - return nil - } - c.URL = args[0] - return cmd.CheckEmpty(args[1:]) -} - -func (c *publishCommand) ChangePushLocation(change func(string) string) { - c.changePushLocation = change -} - -func (c *publishCommand) SetPollDelay(delay time.Duration) { - c.pollDelay = delay -} - -// Wording guideline to avoid confusion: charms have *URLs*, branches have *locations*. - -func (c *publishCommand) Run(ctx *cmd.Context) (err error) { - branch := bzr.New(ctx.AbsPath(c.CharmPath)) - if _, err := os.Stat(branch.Join(".bzr")); err != nil { - return fmt.Errorf("not a charm branch: %s", branch.Location()) - } - if err := branch.CheckClean(); err != nil { - return err - } - - var curl *charm.URL - if c.URL == "" { - loc, err := branch.PushLocation() - if err != nil { - return fmt.Errorf("no charm URL provided and cannot infer from current directory (no push location)") - } - curl, err = charmrepo.LegacyStore.CharmURL(loc) - if err != nil { - return fmt.Errorf("cannot infer charm URL from branch location: %q", loc) - } - } else { - curl, err = charm.ParseURL(c.URL) - if err != nil { - return err - } - } - - pushLocation := charmrepo.LegacyStore.BranchLocation(curl) - if c.changePushLocation != nil { - pushLocation = c.changePushLocation(pushLocation) - } - - repo, err := charmrepo.LegacyInferRepository(curl, "/not/important") - if err != nil { - return err - } - if repo != charmrepo.LegacyStore { - return fmt.Errorf("charm URL must reference the juju charm store") - } - - localDigest, err := branch.RevisionId() - if err != nil { - return fmt.Errorf("cannot obtain local digest: %v", err) - } - logger.Infof("local digest is %s", localDigest) - - ch, err := charm.ReadCharmDir(branch.Location()) - if err != nil { - return err - } - if ch.Meta().Name != curl.Name { - return fmt.Errorf("charm name in metadata must match name in URL: %q != %q", ch.Meta().Name, curl.Name) - } - - oldEvent, err := charmrepo.LegacyStore.Event(curl, localDigest) - if _, ok := err.(*charmrepo.NotFoundError); ok { - oldEvent, err = charmrepo.LegacyStore.Event(curl, "") - if _, ok := err.(*charmrepo.NotFoundError); ok { - logger.Infof("charm %s is not yet in the store", curl) - err = nil - } - } - if err != nil { - return fmt.Errorf("cannot obtain event details from the store: %s", err) - } - - if oldEvent != nil && oldEvent.Digest == localDigest { - return handleEvent(ctx, curl, oldEvent) - } - - logger.Infof("sending charm to the charm store...") - - err = branch.Push(&bzr.PushAttr{Location: pushLocation, Remember: true}) - if err != nil { - return err - } - logger.Infof("charm sent; waiting for it to be published...") - for { - time.Sleep(c.pollDelay) - newEvent, err := charmrepo.LegacyStore.Event(curl, "") - if _, ok := err.(*charmrepo.NotFoundError); ok { - continue - } - if err != nil { - return fmt.Errorf("cannot obtain event details from the store: %s", err) - } - if oldEvent != nil && oldEvent.Digest == newEvent.Digest { - continue - } - if newEvent.Digest != localDigest { - // TODO Check if the published digest is in the local history. - return fmt.Errorf("charm changed but not to local charm digest; publishing race?") - } - return handleEvent(ctx, curl, newEvent) - } -} - -func handleEvent(ctx *cmd.Context, curl *charm.URL, event *charmrepo.EventResponse) error { - switch event.Kind { - case "published": - curlRev := curl.WithRevision(event.Revision) - logger.Infof("charm published at %s as %s", event.Time, curlRev) - fmt.Fprintln(ctx.Stdout, curlRev) - case "publish-error": - return fmt.Errorf("charm could not be published: %s", strings.Join(event.Errors, "; ")) - default: - return fmt.Errorf("unknown event kind %q for charm %s", event.Kind, curl) - } - return nil -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/publish_nix_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/publish_nix_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/publish_nix_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/publish_nix_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,9 +0,0 @@ -// Copyright 2014 Canonical Ltd. -// Copyright 2014 Cloudbase Solutions SRL -// Licensed under the AGPLv3, see LICENCE file for details. - -// +build !windows - -package commands - -var bzrHomeFile = ".bazaar/bazaar.conf" diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/publish_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/publish_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/publish_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/publish_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,398 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package commands - -import ( - "fmt" - "os" - - "github.com/juju/cmd" - gitjujutesting "github.com/juju/testing" - jc "github.com/juju/testing/checkers" - "github.com/juju/utils" - "github.com/juju/utils/bzr" - gc "gopkg.in/check.v1" - "gopkg.in/juju/charmrepo.v2-unstable" - - "github.com/juju/juju/cmd/modelcmd" - "github.com/juju/juju/testing" -) - -// Sadly, this is a very slow test suite, heavily dominated by calls to bzr. - -type PublishSuite struct { - testing.FakeJujuXDGDataHomeSuite - gitjujutesting.HTTPSuite - - dir string - oldBaseURL string - branch *bzr.Branch -} - -var _ = gc.Suite(&PublishSuite{}) - -func touch(c *gc.C, filename string) { - f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0644) - c.Assert(err, jc.ErrorIsNil) - f.Close() -} - -func addMeta(c *gc.C, branch *bzr.Branch, meta string) { - if meta == "" { - meta = "name: wordpress\nsummary: Some summary\ndescription: Some description.\n" - } - f, err := os.Create(branch.Join("metadata.yaml")) - c.Assert(err, jc.ErrorIsNil) - _, err = f.Write([]byte(meta)) - f.Close() - c.Assert(err, jc.ErrorIsNil) - err = branch.Add("metadata.yaml") - c.Assert(err, jc.ErrorIsNil) - err = branch.Commit("Added metadata.yaml.") - c.Assert(err, jc.ErrorIsNil) -} - -func (s *PublishSuite) runPublish(c *gc.C, args ...string) (*cmd.Context, error) { - return testing.RunCommandInDir(c, newPublishCommand(), args, s.dir) -} - -const pollDelay = testing.ShortWait - -func (s *PublishSuite) SetUpSuite(c *gc.C) { - s.FakeJujuXDGDataHomeSuite.SetUpSuite(c) - s.HTTPSuite.SetUpSuite(c) - - s.oldBaseURL = charmrepo.LegacyStore.BaseURL - charmrepo.LegacyStore.BaseURL = s.URL("") -} - -func (s *PublishSuite) TearDownSuite(c *gc.C) { - s.FakeJujuXDGDataHomeSuite.TearDownSuite(c) - s.HTTPSuite.TearDownSuite(c) - - charmrepo.LegacyStore.BaseURL = s.oldBaseURL -} - -func (s *PublishSuite) SetUpTest(c *gc.C) { - s.FakeJujuXDGDataHomeSuite.SetUpTest(c) - s.HTTPSuite.SetUpTest(c) - s.PatchEnvironment("BZR_HOME", utils.Home()) - s.FakeJujuXDGDataHomeSuite.Home.AddFiles(c, gitjujutesting.TestFile{ - Name: bzrHomeFile, - Data: "[DEFAULT]\nemail = Test \n", - }) - - s.dir = c.MkDir() - s.branch = bzr.New(s.dir) - err := s.branch.Init() - c.Assert(err, jc.ErrorIsNil) -} - -func (s *PublishSuite) TearDownTest(c *gc.C) { - s.HTTPSuite.TearDownTest(c) - s.FakeJujuXDGDataHomeSuite.TearDownTest(c) -} - -func (s *PublishSuite) TestNoBranch(c *gc.C) { - dir := c.MkDir() - _, err := testing.RunCommandInDir(c, newPublishCommand(), []string{"cs:precise/wordpress"}, dir) - // We need to do this here because \U is outputed on windows - // and it's an invalid regex escape sequence - c.Assert(err.Error(), gc.Equals, fmt.Sprintf("not a charm branch: %s", dir)) -} - -func (s *PublishSuite) TestEmpty(c *gc.C) { - _, err := s.runPublish(c, "cs:precise/wordpress") - c.Assert(err, gc.ErrorMatches, `cannot obtain local digest: branch has no content`) -} - -func (s *PublishSuite) TestFrom(c *gc.C) { - _, err := testing.RunCommandInDir(c, newPublishCommand(), []string{"--from", s.dir, "cs:precise/wordpress"}, c.MkDir()) - c.Assert(err, gc.ErrorMatches, `cannot obtain local digest: branch has no content`) -} - -func (s *PublishSuite) TestNotClean(c *gc.C) { - touch(c, s.branch.Join("file")) - _, err := s.runPublish(c, "cs:precise/wordpress") - c.Assert(err, gc.ErrorMatches, `branch is not clean \(bzr status\)`) -} - -func (s *PublishSuite) TestNoPushLocation(c *gc.C) { - addMeta(c, s.branch, "") - _, err := s.runPublish(c) - c.Assert(err, gc.ErrorMatches, `no charm URL provided and cannot infer from current directory \(no push location\)`) -} - -func (s *PublishSuite) TestUnknownPushLocation(c *gc.C) { - addMeta(c, s.branch, "") - err := s.branch.Push(&bzr.PushAttr{Location: c.MkDir() + "/foo", Remember: true}) - c.Assert(err, jc.ErrorIsNil) - _, err = s.runPublish(c) - c.Assert(err, gc.ErrorMatches, `cannot infer charm URL from branch location: ".*/foo"`) -} - -func (s *PublishSuite) TestWrongRepository(c *gc.C) { - addMeta(c, s.branch, "") - _, err := s.runPublish(c, "local:precise/wordpress") - c.Assert(err, gc.ErrorMatches, "charm URL must reference the juju charm store") -} - -func (s *PublishSuite) TestParseReference(c *gc.C) { - addMeta(c, s.branch, "") - - cmd := &publishCommand{} - cmd.ChangePushLocation(func(location string) string { - c.Assert(location, gc.Equals, "lp:charms/precise/wordpress") - c.SucceedNow() - panic("unreachable") - }) - - _, err := testing.RunCommandInDir(c, modelcmd.Wrap(cmd), []string{"precise/wordpress"}, s.dir) - c.Assert(err, jc.ErrorIsNil) - c.Fatal("shouldn't get here; location closure didn't run?") -} - -func (s *PublishSuite) TestBrokenCharm(c *gc.C) { - addMeta(c, s.branch, "name: wordpress\nsummary: Some summary\n") - _, err := s.runPublish(c, "cs:precise/wordpress") - c.Assert(err, gc.ErrorMatches, "metadata: description: expected string, got nothing") -} - -func (s *PublishSuite) TestWrongName(c *gc.C) { - addMeta(c, s.branch, "") - _, err := s.runPublish(c, "cs:precise/mysql") - c.Assert(err, gc.ErrorMatches, `charm name in metadata must match name in URL: "wordpress" != "mysql"`) -} - -func (s *PublishSuite) TestPreExistingPublished(c *gc.C) { - addMeta(c, s.branch, "") - - // Pretend the store has seen the digest before, and it has succeeded. - digest, err := s.branch.RevisionId() - c.Assert(err, jc.ErrorIsNil) - body := `{"cs:precise/wordpress": {"kind": "published", "digest": %q, "revision": 42}}` - gitjujutesting.Server.Response(200, nil, []byte(fmt.Sprintf(body, digest))) - - ctx, err := s.runPublish(c, "cs:precise/wordpress") - c.Assert(err, jc.ErrorIsNil) - c.Assert(testing.Stdout(ctx), gc.Equals, "cs:precise/wordpress-42\n") - - req := gitjujutesting.Server.WaitRequest() - c.Assert(req.URL.Path, gc.Equals, "/charm-event") - c.Assert(req.Form.Get("charms"), gc.Equals, "cs:precise/wordpress@"+digest) -} - -func (s *PublishSuite) TestPreExistingPublishedEdge(c *gc.C) { - addMeta(c, s.branch, "") - - // If it doesn't find the right digest on the first try, it asks again for - // any digest at all to keep the tip in mind. There's a small chance that - // on the second request the tip has changed and matches the digest we're - // looking for, in which case we have the answer already. - digest, err := s.branch.RevisionId() - c.Assert(err, jc.ErrorIsNil) - var body string - body = `{"cs:precise/wordpress": {"errors": ["entry not found"]}}` - gitjujutesting.Server.Response(200, nil, []byte(body)) - body = `{"cs:precise/wordpress": {"kind": "published", "digest": %q, "revision": 42}}` - gitjujutesting.Server.Response(200, nil, []byte(fmt.Sprintf(body, digest))) - - ctx, err := s.runPublish(c, "cs:precise/wordpress") - c.Assert(err, jc.ErrorIsNil) - c.Assert(testing.Stdout(ctx), gc.Equals, "cs:precise/wordpress-42\n") - - req := gitjujutesting.Server.WaitRequest() - c.Assert(req.URL.Path, gc.Equals, "/charm-event") - c.Assert(req.Form.Get("charms"), gc.Equals, "cs:precise/wordpress@"+digest) - - req = gitjujutesting.Server.WaitRequest() - c.Assert(req.URL.Path, gc.Equals, "/charm-event") - c.Assert(req.Form.Get("charms"), gc.Equals, "cs:precise/wordpress") -} - -func (s *PublishSuite) TestPreExistingPublishError(c *gc.C) { - addMeta(c, s.branch, "") - - // Pretend the store has seen the digest before, and it has failed. - digest, err := s.branch.RevisionId() - c.Assert(err, jc.ErrorIsNil) - body := `{"cs:precise/wordpress": {"kind": "publish-error", "digest": %q, "errors": ["an error"]}}` - gitjujutesting.Server.Response(200, nil, []byte(fmt.Sprintf(body, digest))) - - _, err = s.runPublish(c, "cs:precise/wordpress") - c.Assert(err, gc.ErrorMatches, "charm could not be published: an error") - - req := gitjujutesting.Server.WaitRequest() - c.Assert(req.URL.Path, gc.Equals, "/charm-event") - c.Assert(req.Form.Get("charms"), gc.Equals, "cs:precise/wordpress@"+digest) -} - -func (s *PublishSuite) TestFullPublish(c *gc.C) { - addMeta(c, s.branch, "") - - digest, err := s.branch.RevisionId() - c.Assert(err, jc.ErrorIsNil) - - pushBranch := bzr.New(c.MkDir()) - err = pushBranch.Init() - c.Assert(err, jc.ErrorIsNil) - - cmd := &publishCommand{} - cmd.ChangePushLocation(func(location string) string { - c.Assert(location, gc.Equals, "lp:~user/charms/precise/wordpress/trunk") - return pushBranch.Location() - }) - cmd.SetPollDelay(testing.ShortWait) - - var body string - - // The local digest isn't found. - body = `{"cs:~user/precise/wordpress": {"kind": "", "errors": ["entry not found"]}}` - gitjujutesting.Server.Response(200, nil, []byte(body)) - - // But the charm exists with an arbitrary non-matching digest. - body = `{"cs:~user/precise/wordpress": {"kind": "published", "digest": "other-digest"}}` - gitjujutesting.Server.Response(200, nil, []byte(body)) - - // After the branch is pushed we fake the publishing delay. - body = `{"cs:~user/precise/wordpress": {"kind": "published", "digest": "other-digest"}}` - gitjujutesting.Server.Response(200, nil, []byte(body)) - - // And finally report success. - body = `{"cs:~user/precise/wordpress": {"kind": "published", "digest": %q, "revision": 42}}` - gitjujutesting.Server.Response(200, nil, []byte(fmt.Sprintf(body, digest))) - - ctx, err := testing.RunCommandInDir(c, modelcmd.Wrap(cmd), []string{"cs:~user/precise/wordpress"}, s.dir) - c.Assert(err, jc.ErrorIsNil) - c.Assert(testing.Stdout(ctx), gc.Equals, "cs:~user/precise/wordpress-42\n") - - // Ensure the branch was actually pushed. - pushDigest, err := pushBranch.RevisionId() - c.Assert(err, jc.ErrorIsNil) - c.Assert(pushDigest, gc.Equals, digest) - - // And that all the requests were sent with the proper data. - req := gitjujutesting.Server.WaitRequest() - c.Assert(req.URL.Path, gc.Equals, "/charm-event") - c.Assert(req.Form.Get("charms"), gc.Equals, "cs:~user/precise/wordpress@"+digest) - - for i := 0; i < 3; i++ { - // The second request grabs tip to see the current state, and the - // following requests are done after pushing to see when it changes. - req = gitjujutesting.Server.WaitRequest() - c.Assert(req.URL.Path, gc.Equals, "/charm-event") - c.Assert(req.Form.Get("charms"), gc.Equals, "cs:~user/precise/wordpress") - } -} - -func (s *PublishSuite) TestFullPublishError(c *gc.C) { - addMeta(c, s.branch, "") - - digest, err := s.branch.RevisionId() - c.Assert(err, jc.ErrorIsNil) - - pushBranch := bzr.New(c.MkDir()) - err = pushBranch.Init() - c.Assert(err, jc.ErrorIsNil) - - cmd := &publishCommand{} - cmd.ChangePushLocation(func(location string) string { - c.Assert(location, gc.Equals, "lp:~user/charms/precise/wordpress/trunk") - return pushBranch.Location() - }) - cmd.SetPollDelay(pollDelay) - - var body string - - // The local digest isn't found. - body = `{"cs:~user/precise/wordpress": {"kind": "", "errors": ["entry not found"]}}` - gitjujutesting.Server.Response(200, nil, []byte(body)) - - // And tip isn't found either, meaning the charm was never published. - gitjujutesting.Server.Response(200, nil, []byte(body)) - - // After the branch is pushed we fake the publishing delay. - gitjujutesting.Server.Response(200, nil, []byte(body)) - - // And finally report success. - body = `{"cs:~user/precise/wordpress": {"kind": "published", "digest": %q, "revision": 42}}` - gitjujutesting.Server.Response(200, nil, []byte(fmt.Sprintf(body, digest))) - - ctx, err := testing.RunCommandInDir(c, modelcmd.Wrap(cmd), []string{"cs:~user/precise/wordpress"}, s.dir) - c.Assert(err, jc.ErrorIsNil) - c.Assert(testing.Stdout(ctx), gc.Equals, "cs:~user/precise/wordpress-42\n") - - // Ensure the branch was actually pushed. - pushDigest, err := pushBranch.RevisionId() - c.Assert(err, jc.ErrorIsNil) - c.Assert(pushDigest, gc.Equals, digest) - - // And that all the requests were sent with the proper data. - req := gitjujutesting.Server.WaitRequest() - c.Assert(req.URL.Path, gc.Equals, "/charm-event") - c.Assert(req.Form.Get("charms"), gc.Equals, "cs:~user/precise/wordpress@"+digest) - - for i := 0; i < 3; i++ { - // The second request grabs tip to see the current state, and the - // following requests are done after pushing to see when it changes. - req = gitjujutesting.Server.WaitRequest() - c.Assert(req.URL.Path, gc.Equals, "/charm-event") - c.Assert(req.Form.Get("charms"), gc.Equals, "cs:~user/precise/wordpress") - } -} - -func (s *PublishSuite) TestFullPublishRace(c *gc.C) { - addMeta(c, s.branch, "") - - digest, err := s.branch.RevisionId() - c.Assert(err, jc.ErrorIsNil) - - pushBranch := bzr.New(c.MkDir()) - err = pushBranch.Init() - c.Assert(err, jc.ErrorIsNil) - - cmd := &publishCommand{} - cmd.ChangePushLocation(func(location string) string { - c.Assert(location, gc.Equals, "lp:~user/charms/precise/wordpress/trunk") - return pushBranch.Location() - }) - cmd.SetPollDelay(pollDelay) - - var body string - - // The local digest isn't found. - body = `{"cs:~user/precise/wordpress": {"kind": "", "errors": ["entry not found"]}}` - gitjujutesting.Server.Response(200, nil, []byte(body)) - - // And tip isn't found either, meaning the charm was never published. - gitjujutesting.Server.Response(200, nil, []byte(body)) - - // After the branch is pushed we fake the publishing delay. - gitjujutesting.Server.Response(200, nil, []byte(body)) - - // But, surprisingly, the digest changed to something else entirely. - body = `{"cs:~user/precise/wordpress": {"kind": "published", "digest": "surprising-digest", "revision": 42}}` - gitjujutesting.Server.Response(200, nil, []byte(body)) - - _, err = testing.RunCommandInDir(c, modelcmd.Wrap(cmd), []string{"cs:~user/precise/wordpress"}, s.dir) - c.Assert(err, gc.ErrorMatches, `charm changed but not to local charm digest; publishing race\?`) - - // Ensure the branch was actually pushed. - pushDigest, err := pushBranch.RevisionId() - c.Assert(err, jc.ErrorIsNil) - c.Assert(pushDigest, gc.Equals, digest) - - // And that all the requests were sent with the proper data. - req := gitjujutesting.Server.WaitRequest() - c.Assert(req.URL.Path, gc.Equals, "/charm-event") - c.Assert(req.Form.Get("charms"), gc.Equals, "cs:~user/precise/wordpress@"+digest) - - for i := 0; i < 3; i++ { - // The second request grabs tip to see the current state, and the - // following requests are done after pushing to see when it changes. - req = gitjujutesting.Server.WaitRequest() - c.Assert(req.URL.Path, gc.Equals, "/charm-event") - c.Assert(req.Form.Get("charms"), gc.Equals, "cs:~user/precise/wordpress") - } -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/publish_windows_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/publish_windows_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/publish_windows_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/publish_windows_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,9 +0,0 @@ -// Copyright 2014 Canonical Ltd. -// Copyright 2014 Cloudbase Solutions SRL -// Licensed under the AGPLv3, see LICENCE file for details. - -// +build windows - -package commands - -var bzrHomeFile = "Bazaar/2.0/bazaar.conf" diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/ssh_common.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/ssh_common.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/ssh_common.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/ssh_common.go 2016-08-16 08:56:25.000000000 +0000 @@ -65,6 +65,8 @@ } // attemptStarter is an interface corresponding to utils.AttemptStrategy +// +// TODO(katco): 2016-08-09: lp:1611427 type attemptStarter interface { Start() attempt } @@ -73,9 +75,11 @@ Next() bool } +// TODO(katco): 2016-08-09: lp:1611427 type attemptStrategy utils.AttemptStrategy func (s attemptStrategy) Start() attempt { + // TODO(katco): 2016-08-09: lp:1611427 return utils.AttemptStrategy(s).Start() } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/switch.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/switch.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/switch.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/switch.go 2016-08-16 08:56:25.000000000 +0000 @@ -71,17 +71,18 @@ } func (c *switchCommand) Run(ctx *cmd.Context) (resultErr error) { + store := modelcmd.QualifyingClientStore{c.Store} // Get the current name for logging the transition or printing // the current controller/model. - currentControllerName, err := c.Store.CurrentController() + currentControllerName, err := store.CurrentController() if errors.IsNotFound(err) { currentControllerName = "" } else if err != nil { return errors.Trace(err) } if c.Target == "" { - currentName, err := c.name(currentControllerName, true) + currentName, err := c.name(store, currentControllerName, true) if err != nil { return errors.Trace(err) } @@ -91,7 +92,7 @@ fmt.Fprintf(ctx.Stdout, "%s\n", currentName) return nil } - currentName, err := c.name(currentControllerName, false) + currentName, err := c.name(store, currentControllerName, false) if err != nil { return errors.Trace(err) } @@ -114,16 +115,16 @@ // If the target identifies a controller, then set that as the current controller. var newControllerName = c.Target - if _, err = c.Store.ControllerByName(c.Target); err == nil { + if _, err = store.ControllerByName(c.Target); err == nil { if newControllerName == currentControllerName { newName = currentName return nil } else { - newName, err = c.name(newControllerName, false) + newName, err = c.name(store, newControllerName, false) if err != nil { return errors.Trace(err) } - return errors.Trace(c.Store.SetCurrentController(newControllerName)) + return errors.Trace(store.SetCurrentController(newControllerName)) } } else if !errors.IsNotFound(err) { return errors.Trace(err) @@ -135,25 +136,28 @@ // case, the model must exist in the current controller. newControllerName, modelName := modelcmd.SplitModelName(c.Target) if newControllerName != "" { - if _, err = c.Store.ControllerByName(newControllerName); err != nil { + if _, err = store.ControllerByName(newControllerName); err != nil { return errors.Trace(err) } - newName = modelcmd.JoinModelName(newControllerName, modelName) } else { if currentControllerName == "" { return unknownSwitchTargetError(c.Target) } newControllerName = currentControllerName - newName = modelcmd.JoinModelName(newControllerName, modelName) } + modelName, err = store.QualifiedModelName(newControllerName, modelName) + if err != nil { + return errors.Trace(err) + } + newName = modelcmd.JoinModelName(newControllerName, modelName) - err = c.Store.SetCurrentModel(newControllerName, modelName) + err = store.SetCurrentModel(newControllerName, modelName) if errors.IsNotFound(err) { // The model isn't known locally, so we must query the controller. - if err := c.RefreshModels(c.Store, newControllerName); err != nil { + if err := c.RefreshModels(store, newControllerName); err != nil { return errors.Annotate(err, "refreshing models cache") } - err := c.Store.SetCurrentModel(newControllerName, modelName) + err := store.SetCurrentModel(newControllerName, modelName) if errors.IsNotFound(err) { return unknownSwitchTargetError(c.Target) } else if err != nil { @@ -163,7 +167,7 @@ return errors.Trace(err) } if currentControllerName != newControllerName { - if err := c.Store.SetCurrentController(newControllerName); err != nil { + if err := store.SetCurrentController(newControllerName); err != nil { return errors.Trace(err) } } @@ -185,11 +189,11 @@ // name returns the name of the current model for the specified controller // if one is set, otherwise the controller name with an indicator that it // is the name of a controller and not a model. -func (c *switchCommand) name(controllerName string, machineReadable bool) (string, error) { +func (c *switchCommand) name(store jujuclient.ModelGetter, controllerName string, machineReadable bool) (string, error) { if controllerName == "" { return "", nil } - modelName, err := c.Store.CurrentModel(controllerName) + modelName, err := store.CurrentModel(controllerName) if err == nil { return modelcmd.JoinModelName(controllerName, modelName), nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/switch_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/switch_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/switch_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/switch_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -77,12 +77,12 @@ s.addController(c, "a-controller") s.store.CurrentControllerName = "a-controller" s.store.Models["a-controller"] = &jujuclient.ControllerModels{ - Models: map[string]jujuclient.ModelDetails{"mymodel": {}}, - CurrentModel: "mymodel", + Models: map[string]jujuclient.ModelDetails{"admin@local/mymodel": {}}, + CurrentModel: "admin@local/mymodel", } ctx, err := s.run(c) c.Assert(err, jc.ErrorIsNil) - c.Assert(coretesting.Stdout(ctx), gc.Equals, "a-controller:mymodel\n") + c.Assert(coretesting.Stdout(ctx), gc.Equals, "a-controller:admin@local/mymodel\n") } func (s *SwitchSimpleSuite) TestSwitchWritesCurrentController(c *gc.C) { @@ -131,80 +131,100 @@ s.store.CurrentControllerName = "ctrl" s.addController(c, "ctrl") s.store.Models["ctrl"] = &jujuclient.ControllerModels{ - Models: map[string]jujuclient.ModelDetails{"mymodel": {}}, + Models: map[string]jujuclient.ModelDetails{"admin@local/mymodel": {}}, } context, err := s.run(c, "mymodel") c.Assert(err, jc.ErrorIsNil) - c.Assert(coretesting.Stderr(context), gc.Equals, "ctrl (controller) -> ctrl:mymodel\n") + c.Assert(coretesting.Stderr(context), gc.Equals, "ctrl (controller) -> ctrl:admin@local/mymodel\n") s.stubStore.CheckCalls(c, []testing.StubCall{ {"CurrentController", nil}, {"CurrentModel", []interface{}{"ctrl"}}, {"ControllerByName", []interface{}{"mymodel"}}, - {"SetCurrentModel", []interface{}{"ctrl", "mymodel"}}, + {"AccountDetails", []interface{}{"ctrl"}}, + {"SetCurrentModel", []interface{}{"ctrl", "admin@local/mymodel"}}, }) - c.Assert(s.store.Models["ctrl"].CurrentModel, gc.Equals, "mymodel") + c.Assert(s.store.Models["ctrl"].CurrentModel, gc.Equals, "admin@local/mymodel") } func (s *SwitchSimpleSuite) TestSwitchControllerToModelDifferentController(c *gc.C) { s.store.CurrentControllerName = "old" s.addController(c, "new") s.store.Models["new"] = &jujuclient.ControllerModels{ - Models: map[string]jujuclient.ModelDetails{"mymodel": {}}, + Models: map[string]jujuclient.ModelDetails{"admin@local/mymodel": {}}, } context, err := s.run(c, "new:mymodel") c.Assert(err, jc.ErrorIsNil) - c.Assert(coretesting.Stderr(context), gc.Equals, "old (controller) -> new:mymodel\n") + c.Assert(coretesting.Stderr(context), gc.Equals, "old (controller) -> new:admin@local/mymodel\n") s.stubStore.CheckCalls(c, []testing.StubCall{ {"CurrentController", nil}, {"CurrentModel", []interface{}{"old"}}, {"ControllerByName", []interface{}{"new:mymodel"}}, {"ControllerByName", []interface{}{"new"}}, - {"SetCurrentModel", []interface{}{"new", "mymodel"}}, + {"AccountDetails", []interface{}{"new"}}, + {"SetCurrentModel", []interface{}{"new", "admin@local/mymodel"}}, {"SetCurrentController", []interface{}{"new"}}, }) - c.Assert(s.store.Models["new"].CurrentModel, gc.Equals, "mymodel") + c.Assert(s.store.Models["new"].CurrentModel, gc.Equals, "admin@local/mymodel") } func (s *SwitchSimpleSuite) TestSwitchLocalControllerToModelDifferentController(c *gc.C) { s.store.CurrentControllerName = "old" s.addController(c, "new") s.store.Models["new"] = &jujuclient.ControllerModels{ - Models: map[string]jujuclient.ModelDetails{"mymodel": {}}, + Models: map[string]jujuclient.ModelDetails{"admin@local/mymodel": {}}, } context, err := s.run(c, "new:mymodel") c.Assert(err, jc.ErrorIsNil) - c.Assert(coretesting.Stderr(context), gc.Equals, "old (controller) -> new:mymodel\n") + c.Assert(coretesting.Stderr(context), gc.Equals, "old (controller) -> new:admin@local/mymodel\n") s.stubStore.CheckCalls(c, []testing.StubCall{ {"CurrentController", nil}, {"CurrentModel", []interface{}{"old"}}, {"ControllerByName", []interface{}{"new:mymodel"}}, {"ControllerByName", []interface{}{"new"}}, - {"SetCurrentModel", []interface{}{"new", "mymodel"}}, + {"AccountDetails", []interface{}{"new"}}, + {"SetCurrentModel", []interface{}{"new", "admin@local/mymodel"}}, {"SetCurrentController", []interface{}{"new"}}, }) - c.Assert(s.store.Models["new"].CurrentModel, gc.Equals, "mymodel") + c.Assert(s.store.Models["new"].CurrentModel, gc.Equals, "admin@local/mymodel") } func (s *SwitchSimpleSuite) TestSwitchControllerToDifferentControllerCurrentModel(c *gc.C) { s.store.CurrentControllerName = "old" s.addController(c, "new") s.store.Models["new"] = &jujuclient.ControllerModels{ - Models: map[string]jujuclient.ModelDetails{"mymodel": {}}, - CurrentModel: "mymodel", + Models: map[string]jujuclient.ModelDetails{"admin@local/mymodel": {}}, + CurrentModel: "admin@local/mymodel", } context, err := s.run(c, "new:mymodel") c.Assert(err, jc.ErrorIsNil) - c.Assert(coretesting.Stderr(context), gc.Equals, "old (controller) -> new:mymodel\n") + c.Assert(coretesting.Stderr(context), gc.Equals, "old (controller) -> new:admin@local/mymodel\n") s.stubStore.CheckCalls(c, []testing.StubCall{ {"CurrentController", nil}, {"CurrentModel", []interface{}{"old"}}, {"ControllerByName", []interface{}{"new:mymodel"}}, {"ControllerByName", []interface{}{"new"}}, - {"SetCurrentModel", []interface{}{"new", "mymodel"}}, + {"AccountDetails", []interface{}{"new"}}, + {"SetCurrentModel", []interface{}{"new", "admin@local/mymodel"}}, {"SetCurrentController", []interface{}{"new"}}, }) } +func (s *SwitchSimpleSuite) TestSwitchToModelDifferentOwner(c *gc.C) { + s.store.CurrentControllerName = "same" + s.addController(c, "same") + s.store.Models["same"] = &jujuclient.ControllerModels{ + Models: map[string]jujuclient.ModelDetails{ + "admin@local/mymodel": {}, + "bianca@local/mymodel": {}, + }, + CurrentModel: "admin@local/mymodel", + } + context, err := s.run(c, "bianca/mymodel") + c.Assert(err, jc.ErrorIsNil) + c.Assert(coretesting.Stderr(context), gc.Equals, "same:admin@local/mymodel -> same:bianca@local/mymodel\n") + c.Assert(s.store.Models["same"].CurrentModel, gc.Equals, "bianca@local/mymodel") +} + func (s *SwitchSimpleSuite) TestSwitchUnknownNoCurrentController(c *gc.C) { _, err := s.run(c, "unknown") c.Assert(err, gc.ErrorMatches, `"unknown" is not the name of a model or controller`) @@ -219,15 +239,13 @@ s.addController(c, "ctrl") s.onRefresh = func() { s.store.Models["ctrl"] = &jujuclient.ControllerModels{ - Models: map[string]jujuclient.ModelDetails{"unknown": {}}, + Models: map[string]jujuclient.ModelDetails{"admin@local/unknown": {}}, } } ctx, err := s.run(c, "unknown") c.Assert(err, jc.ErrorIsNil) - c.Assert(coretesting.Stderr(ctx), gc.Equals, "ctrl (controller) -> ctrl:unknown\n") - s.CheckCalls(c, []testing.StubCall{ - {"RefreshModels", []interface{}{s.stubStore, "ctrl"}}, - }) + c.Assert(coretesting.Stderr(ctx), gc.Equals, "ctrl (controller) -> ctrl:admin@local/unknown\n") + s.CheckCallNames(c, "RefreshModels") } func (s *SwitchSimpleSuite) TestSwitchUnknownCurrentControllerRefreshModelsStillUnknown(c *gc.C) { @@ -235,9 +253,7 @@ s.addController(c, "ctrl") _, err := s.run(c, "unknown") c.Assert(err, gc.ErrorMatches, `"unknown" is not the name of a model or controller`) - s.CheckCalls(c, []testing.StubCall{ - {"RefreshModels", []interface{}{s.stubStore, "ctrl"}}, - }) + s.CheckCallNames(c, "RefreshModels") } func (s *SwitchSimpleSuite) TestSwitchUnknownCurrentControllerRefreshModelsFails(c *gc.C) { @@ -246,9 +262,7 @@ s.SetErrors(errors.New("not very refreshing")) _, err := s.run(c, "unknown") c.Assert(err, gc.ErrorMatches, "refreshing models cache: not very refreshing") - s.CheckCalls(c, []testing.StubCall{ - {"RefreshModels", []interface{}{s.stubStore, "ctrl"}}, - }) + s.CheckCallNames(c, "RefreshModels") } func (s *SwitchSimpleSuite) TestSettingWhenEnvVarSet(c *gc.C) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/synctools.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/synctools.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/synctools.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/synctools.go 2016-08-16 08:56:25.000000000 +0000 @@ -122,7 +122,10 @@ func (c *syncToolsCommand) Run(ctx *cmd.Context) (resultErr error) { // Register writer for output on screen. - loggo.RegisterWriter("synctools", cmd.NewCommandLogWriter("juju.environs.sync", ctx.Stdout, ctx.Stderr), loggo.INFO) + writer := loggo.NewMinimumLevelWriter( + cmd.NewCommandLogWriter("juju.environs.sync", ctx.Stdout, ctx.Stderr), + loggo.INFO) + loggo.RegisterWriter("synctools", writer) defer loggo.RemoveWriter("synctools") sctx := &sync.SyncContext{ diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/synctools_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/synctools_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/synctools_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/synctools_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -198,7 +198,7 @@ } // Register writer. var tw loggo.TestWriter - c.Assert(loggo.RegisterWriter("deprecated-tester", &tw, loggo.DEBUG), gc.IsNil) + c.Assert(loggo.RegisterWriter("deprecated-tester", &tw), gc.IsNil) defer loggo.RemoveWriter("deprecated-tester") // Add deprecated message to be checked. messages := []jc.SimpleMessage{ diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/upgradejuju.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/upgradejuju.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/upgradejuju.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/upgradejuju.go 2016-08-16 08:56:25.000000000 +0000 @@ -18,6 +18,7 @@ "github.com/juju/version" "launchpad.net/gnuflag" + "github.com/juju/juju/api/modelconfig" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/cmd/juju/block" "github.com/juju/juju/cmd/modelcmd" @@ -113,7 +114,7 @@ // behaviour live, so the only restriction is that Build cannot // be used (because its value needs to be chosen internally so as // not to collide with existing tools). - return fmt.Errorf("cannot specify build number when uploading tools") + return errors.New("cannot specify build number when uploading tools") } c.Version = vers } @@ -164,7 +165,6 @@ } type upgradeJujuAPI interface { - ModelGet() (map[string]interface{}, error) FindTools(majorVersion, minorVersion int, series, arch string) (result params.FindToolsResult, err error) UploadTools(r io.ReadSeeker, vers version.Binary, additionalSeries ...string) (coretools.List, error) AbortCurrentUpgrade() error @@ -172,10 +172,23 @@ Close() error } +type modelConfigAPI interface { + ModelGet() (map[string]interface{}, error) + Close() error +} + var getUpgradeJujuAPI = func(c *upgradeJujuCommand) (upgradeJujuAPI, error) { return c.NewAPIClient() } +var getModelConfigAPI = func(c *upgradeJujuCommand) (modelConfigAPI, error) { + api, err := c.NewAPIRoot() + if err != nil { + return nil, errors.Trace(err) + } + return modelconfig.NewClient(api), nil +} + // Run changes the version proposed for the juju envtools. func (c *upgradeJujuCommand) Run(ctx *cmd.Context) (err error) { @@ -184,6 +197,11 @@ return err } defer client.Close() + modelConfigClient, err := getModelConfigAPI(c) + if err != nil { + return err + } + defer modelConfigClient.Close() defer func() { if err == errUpToDate { ctx.Infof(err.Error()) @@ -192,7 +210,7 @@ }() // Determine the version to upgrade to, uploading tools if necessary. - attrs, err := client.ModelGet() + attrs, err := modelConfigClient.ModelGet() if err != nil { return err } @@ -214,7 +232,7 @@ agentVersion, ok := cfg.AgentVersion() if !ok { // Can't happen. In theory. - return fmt.Errorf("incomplete model configuration") + return errors.New("incomplete model configuration") } if c.UploadTools && c.Version == version.Zero { @@ -228,12 +246,12 @@ case !canUpgradeRunningVersion(agentVersion): // This version of upgrade-juju cannot upgrade the running // environment version (can't guarantee API compatibility). - return fmt.Errorf("cannot upgrade a %s model with a %s client", + return errors.Errorf("cannot upgrade a %s model with a %s client", agentVersion, jujuversion.Current) case c.Version != version.Zero && c.Version.Major < agentVersion.Major: // The specified version would downgrade the environment. // Don't upgrade and return an error. - return fmt.Errorf(downgradeErrMsg, agentVersion, c.Version) + return errors.Errorf(downgradeErrMsg, agentVersion, c.Version) case agentVersion.Major != jujuversion.Current.Major: // Running environment is the previous major version (a higher major // version wouldn't have passed the check in canUpgradeRunningVersion). @@ -272,7 +290,7 @@ retErr = true } if retErr { - return fmt.Errorf("unable to upgrade to requested version") + return errors.New("unable to upgrade to requested version") } } @@ -486,9 +504,9 @@ context.chosen = newestCurrent } else { if context.agent.Major != context.client.Major { - return fmt.Errorf("no compatible tools available") + return errors.New("no compatible tools available") } else { - return fmt.Errorf("no more recent supported versions available") + return errors.New("no more recent supported versions available") } } } @@ -513,7 +531,7 @@ // any of our tools detect an incompatible version, they should act to // minimize damage: the CLI should abort politely, and the agents should // run an Upgrader but no other tasks. - return fmt.Errorf(downgradeErrMsg, context.agent, context.chosen) + return errors.Errorf(downgradeErrMsg, context.agent, context.chosen) } return nil diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/upgradejuju_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/upgradejuju_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/commands/upgradejuju_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/commands/upgradejuju_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -473,6 +473,9 @@ s.PatchValue(&getUpgradeJujuAPI, func(*upgradeJujuCommand) (upgradeJujuAPI, error) { return fakeAPI, nil }) + s.PatchValue(&getModelConfigAPI, func(*upgradeJujuCommand) (modelConfigAPI, error) { + return fakeAPI, nil + }) cmd := newUpgradeJujuCommand(nil) _, err := coretesting.RunCommand(c, cmd, "--upload-tools", "-m", "dummy-model") c.Assert(err, gc.ErrorMatches, "--upload-tools can only be used with the controller model") @@ -878,6 +881,9 @@ s.PatchValue(&getUpgradeJujuAPI, func(*upgradeJujuCommand) (upgradeJujuAPI, error) { return a, nil }) + s.PatchValue(&getModelConfigAPI, func(*upgradeJujuCommand) (modelConfigAPI, error) { + return a, nil + }) } func (a *fakeUpgradeJujuAPI) addTools(tools ...string) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/common/controller.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/common/controller.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/common/controller.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/common/controller.go 2016-08-16 08:56:25.000000000 +0000 @@ -88,6 +88,7 @@ // command which will fail until the controller is fully initialised. // TODO(wallyworld) - add a bespoke command to maybe the admin facade for this purpose. func WaitForAgentInitialisation(ctx *cmd.Context, c *modelcmd.ModelCommandBase, controllerName string) error { + // TODO(katco): 2016-08-09: lp:1611427 attempts := utils.AttemptStrategy{ Min: bootstrapReadyPollCount, Delay: bootstrapReadyPollDelay, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/common/naturalsort.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/common/naturalsort.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/common/naturalsort.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/common/naturalsort.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,57 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package common - -import ( - "fmt" - "sort" - "strconv" - "strings" - "unicode" -) - -// SortStringsNaturally sorts strings according to their natural sort order. -func SortStringsNaturally(s []string) []string { - sort.Sort(naturally(s)) - return s -} - -type naturally []string - -func (n naturally) Len() int { - return len(n) -} - -func (n naturally) Swap(a, b int) { - n[a], n[b] = n[b], n[a] -} - -// Less sorts by non-numeric prefix and numeric suffix -// when one exists. -func (n naturally) Less(a, b int) bool { - aPrefix, aNumber := splitAtNumber(n[a]) - bPrefix, bNumber := splitAtNumber(n[b]) - if aPrefix == bPrefix { - return aNumber < bNumber - } - return n[a] < n[b] -} - -// splitAtNumber splits given string into prefix and numeric suffix. -// If no numeric suffix exists, full original string is returned as -// prefix with -1 as a suffix. -func splitAtNumber(str string) (string, int) { - i := strings.LastIndexFunc(str, func(r rune) bool { - return !unicode.IsDigit(r) - }) + 1 - if i == len(str) { - // no numeric suffix - return str, -1 - } - n, err := strconv.Atoi(str[i:]) - if err != nil { - panic(fmt.Sprintf("parsing number %v: %v", str[i:], err)) // should never happen - } - return str[:i], n -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/common/naturalsort_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/common/naturalsort_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/common/naturalsort_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/common/naturalsort_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,113 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package common - -import ( - "sort" - - gc "gopkg.in/check.v1" - - "github.com/juju/juju/testing" -) - -type naturalSortSuite struct { - testing.BaseSuite -} - -var _ = gc.Suite(&naturalSortSuite{}) - -func (s *naturalSortSuite) TestNaturallyEmpty(c *gc.C) { - s.assertNaturallySort( - c, - []string{}, - []string{}, - ) -} - -func (s *naturalSortSuite) TestNaturallyAlpha(c *gc.C) { - s.assertNaturallySort( - c, - []string{"bac", "cba", "abc"}, - []string{"abc", "bac", "cba"}, - ) -} - -func (s *naturalSortSuite) TestNaturallyAlphaNumeric(c *gc.C) { - s.assertNaturallySort( - c, - []string{"a1", "a10", "a100", "a11"}, - []string{"a1", "a10", "a11", "a100"}, - ) -} - -func (s *naturalSortSuite) TestNaturallySpecial(c *gc.C) { - s.assertNaturallySort( - c, - []string{"a1", "a10", "a100", "a1/1", "1a"}, - []string{"1a", "a1", "a1/1", "a10", "a100"}, - ) -} - -func (s *naturalSortSuite) TestNaturallyTagLike(c *gc.C) { - s.assertNaturallySort( - c, - []string{"a1/1", "a1/11", "a1/2", "a1/7", "a1/100"}, - []string{"a1/1", "a1/2", "a1/7", "a1/11", "a1/100"}, - ) -} - -func (s *naturalSortSuite) TestNaturallySeveralNumericParts(c *gc.C) { - s.assertNaturallySort( - c, - []string{"x2-y08", "x2-g8", "x8-y8", "x2-y7"}, - []string{"x2-g8", "x2-y7", "x2-y08", "x8-y8"}, - ) -} - -func (s *naturalSortSuite) TestNaturallyFoo(c *gc.C) { - s.assertNaturallySort( - c, - []string{"foo2", "foo01"}, - []string{"foo01", "foo2"}, - ) -} - -func (s *naturalSortSuite) TestNaturallyIPs(c *gc.C) { - s.assertNaturallySort( - c, - []string{"100.001.010.123", "001.001.010.123", "001.002.010.123"}, - []string{"001.001.010.123", "001.002.010.123", "100.001.010.123"}, - ) -} - -func (s *naturalSortSuite) TestNaturallyJuju(c *gc.C) { - s.assertNaturallySort( - c, - []string{ - "ubuntu/0", - "ubuntu/1", - "ubuntu/10", - "ubuntu/100", - "ubuntu/101", - "ubuntu/102", - "ubuntu/103", - "ubuntu/104", - "ubuntu/11"}, - []string{ - "ubuntu/0", - "ubuntu/1", - "ubuntu/10", - "ubuntu/11", - "ubuntu/100", - "ubuntu/101", - "ubuntu/102", - "ubuntu/103", - "ubuntu/104"}, - ) -} - -func (s *naturalSortSuite) assertNaturallySort(c *gc.C, sample, expected []string) { - sort.Sort(naturally(sample)) - c.Assert(sample, gc.DeepEquals, expected) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/addmodel.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/addmodel.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/addmodel.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/addmodel.go 2016-08-16 08:56:25.000000000 +0000 @@ -99,7 +99,7 @@ return errors.Errorf("%q is not a valid user", c.Owner) } - return nil + return cmd.CheckEmpty(args) } type AddModelAPI interface { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/addmodel_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/addmodel_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/addmodel_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/addmodel_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -117,6 +117,9 @@ args: []string{"new-model", "--config", "key=value", "--config", "key2=value2"}, name: "new-model", values: map[string]interface{}{"key": "value", "key2": "value2"}, + }, { + args: []string{"new-model", "extra", "args"}, + err: `unrecognized args: \["extra" "args"\]`, }, } { c.Logf("test %d", i) @@ -145,7 +148,7 @@ // controller will error out if the model already exists. Overwriting // means we'll replace any stale details from an previously existing // model with the same name. - err := s.store.UpdateModel("test-master", "test", jujuclient.ModelDetails{ + err := s.store.UpdateModel("test-master", "bob@local/test", jujuclient.ModelDetails{ "stale-uuid", }) c.Assert(err, jc.ErrorIsNil) @@ -153,7 +156,7 @@ _, err = s.run(c, "test") c.Assert(err, jc.ErrorIsNil) - details, err := s.store.ModelByName("test-master", "test") + details, err := s.store.ModelByName("test-master", "bob@local/test") c.Assert(err, jc.ErrorIsNil) c.Assert(details, jc.DeepEquals, &jujuclient.ModelDetails{"fake-model-uuid"}) } @@ -275,7 +278,7 @@ _, err := s.run(c, "test") c.Assert(err, gc.ErrorMatches, "bah humbug") - _, err = s.store.ModelByName("test-master", "test") + _, err = s.store.ModelByName("test-master", "bob@local/test") c.Assert(err, jc.Satisfies, errors.IsNotFound) } @@ -283,7 +286,7 @@ _, err := s.run(c, "test") c.Assert(err, jc.ErrorIsNil) - model, err := s.store.ModelByName("test-master", "test") + model, err := s.store.ModelByName("test-master", "bob@local/test") c.Assert(err, jc.ErrorIsNil) c.Assert(model, jc.DeepEquals, &jujuclient.ModelDetails{"fake-model-uuid"}) } @@ -293,7 +296,7 @@ c.Assert(err, jc.ErrorIsNil) // Creating a model for another user does not update the model cache. - _, err = s.store.ModelByName("test-master", "test") + _, err = s.store.ModelByName("test-master", "bob@local/test") c.Assert(err, jc.Satisfies, errors.IsNotFound) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/destroy.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/destroy.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/destroy.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/destroy.go 2016-08-16 08:56:25.000000000 +0000 @@ -68,13 +68,14 @@ WARNING! This command will destroy the %q controller. This includes all machines, applications, data and other resources. -Continue [y/N]? `[1:] +Continue? (y/N):`[1:] // destroyControllerAPI defines the methods on the controller API endpoint // that the destroy command calls. type destroyControllerAPI interface { Close() error ModelConfig() (map[string]interface{}, error) + CloudSpec(names.ModelTag) (environs.CloudSpec, error) DestroyController(destroyModels bool) error ListBlockedModels() ([]params.ModelBlockInfo, error) ModelStatus(models ...names.ModelTag) ([]base.ModelStatus, error) @@ -340,27 +341,66 @@ // Environ by first checking the config store, then querying the // API if the information is not in the store. func (c *destroyCommandBase) getControllerEnviron( - store jujuclient.ClientStore, controllerName string, sysAPI destroyControllerAPI, -) (_ environs.Environ, err error) { - cfg, err := modelcmd.NewGetBootstrapConfigFunc(store)(controllerName) + store jujuclient.ClientStore, + controllerName string, + sysAPI destroyControllerAPI, +) (environs.Environ, error) { + env, err := c.getControllerEnvironFromStore(store, controllerName) if errors.IsNotFound(err) { - if sysAPI == nil { - return nil, errors.New( - "unable to get bootstrap information from client store or API", - ) - } - bootstrapConfig, err := sysAPI.ModelConfig() - if err != nil { - return nil, errors.Annotate(err, "getting bootstrap config from API") - } - cfg, err = config.New(config.NoDefaults, bootstrapConfig) - if err != nil { - return nil, errors.Trace(err) - } + return c.getControllerEnvironFromAPI(sysAPI, controllerName) } else if err != nil { - return nil, errors.Annotate(err, "getting bootstrap config from client store") + return nil, errors.Annotate(err, "getting environ using bootstrap config from client store") + } + return env, nil +} + +func (c *destroyCommandBase) getControllerEnvironFromStore( + store jujuclient.ClientStore, + controllerName string, +) (environs.Environ, error) { + bootstrapConfig, params, err := modelcmd.NewGetBootstrapConfigParamsFunc(store)(controllerName) + if err != nil { + return nil, errors.Trace(err) + } + provider, err := environs.Provider(bootstrapConfig.CloudType) + if err != nil { + return nil, errors.Trace(err) + } + cfg, err := provider.PrepareConfig(*params) + if err != nil { + return nil, errors.Trace(err) + } + return environs.New(environs.OpenParams{ + Cloud: params.Cloud, + Config: cfg, + }) +} + +func (c *destroyCommandBase) getControllerEnvironFromAPI( + api destroyControllerAPI, + controllerName string, +) (environs.Environ, error) { + if api == nil { + return nil, errors.New( + "unable to get bootstrap information from client store or API", + ) + } + attrs, err := api.ModelConfig() + if err != nil { + return nil, errors.Annotate(err, "getting model config from API") + } + cfg, err := config.New(config.NoDefaults, attrs) + if err != nil { + return nil, errors.Trace(err) + } + cloudSpec, err := api.CloudSpec(names.NewModelTag(cfg.UUID())) + if err != nil { + return nil, errors.Annotate(err, "getting cloud spec from API") } - return environs.New(cfg) + return environs.New(environs.OpenParams{ + Cloud: cloudSpec, + Config: cfg, + }) } func confirmDestruction(ctx *cmd.Context, controllerName string) error { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/destroy_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/destroy_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/destroy_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/destroy_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -19,11 +19,11 @@ "github.com/juju/juju/cmd/juju/controller" "github.com/juju/juju/cmd/modelcmd" cmdtesting "github.com/juju/juju/cmd/testing" - jujucontroller "github.com/juju/juju/controller" + "github.com/juju/juju/environs" "github.com/juju/juju/environs/config" "github.com/juju/juju/jujuclient" "github.com/juju/juju/jujuclient/jujuclienttesting" - _ "github.com/juju/juju/provider/dummy" + "github.com/juju/juju/provider/dummy" "github.com/juju/juju/testing" ) @@ -50,6 +50,7 @@ // fakeDestroyAPI mocks out the controller API type fakeDestroyAPI struct { gitjujutesting.Stub + cloud environs.CloudSpec env map[string]interface{} destroyAll bool blocks []params.ModelBlockInfo @@ -62,6 +63,14 @@ return f.NextErr() } +func (f *fakeDestroyAPI) CloudSpec(tag names.ModelTag) (environs.CloudSpec, error) { + f.MethodCall(f, "CloudSpec", tag) + if err := f.NextErr(); err != nil { + return environs.CloudSpec{}, err + } + return f.cloud, nil +} + func (f *fakeDestroyAPI) ModelConfig() (map[string]interface{}, error) { f.MethodCall(f, "ModelConfig") if err := f.NextErr(); err != nil { @@ -125,10 +134,6 @@ "controller": "true", }) c.Assert(err, jc.ErrorIsNil) - - // TODO(wallyworld) - we need to separate controller and model schemas - cfg, err = cfg.Remove(jujucontroller.ControllerOnlyConfigAttributes) - c.Assert(err, jc.ErrorIsNil) return cfg.AllAttrs() } @@ -137,6 +142,7 @@ s.clientapi = &fakeDestroyAPIClient{} owner := names.NewUserTag("owner") s.api = &fakeDestroyAPI{ + cloud: dummy.SampleCloudSpec(), envStatus: map[string]base.ModelStatus{}, } s.apierror = nil @@ -189,7 +195,8 @@ }) if model.bootstrapCfg != nil { s.store.BootstrapConfig[controllerName] = jujuclient.BootstrapConfig{ - Config: createBootstrapInfo(c, "admin"), + Config: createBootstrapInfo(c, "admin"), + CloudType: "dummy", } } @@ -290,7 +297,7 @@ s.api.SetErrors(errors.NotFoundf(`controller "test3"`)) _, err := s.runDestroyCommand(c, "test3", "-y") c.Assert(err, gc.ErrorMatches, - "getting controller environ: getting bootstrap config from API: controller \"test3\" not found", + "getting controller environ: getting model config from API: controller \"test3\" not found", ) checkControllerExistsInStore(c, "test3", s.store) } @@ -356,7 +363,8 @@ User: "admin@local", } s.store.BootstrapConfig["test1"] = jujuclient.BootstrapConfig{ - Config: createBootstrapInfo(c, "admin"), + Config: createBootstrapInfo(c, "admin"), + CloudType: "dummy", } } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/export_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -67,8 +67,8 @@ // NewRegisterCommandForTest returns a RegisterCommand with the function used // to open the API connection mocked out. -func NewRegisterCommandForTest(apiOpen api.OpenFunc, refreshModels func(jujuclient.ClientStore, string) error, store jujuclient.ClientStore) *registerCommand { - return ®isterCommand{apiOpen: apiOpen, refreshModels: refreshModels, store: store} +func NewRegisterCommandForTest(apiOpen api.OpenFunc, listModels func(jujuclient.ClientStore, string, string) ([]base.UserModel, error), store jujuclient.ClientStore) *registerCommand { + return ®isterCommand{apiOpen: apiOpen, listModelsFunc: listModels, store: store} } // NewRemoveBlocksCommandForTest returns a RemoveBlocksCommand with the diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/kill_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/kill_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/kill_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/kill_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -91,7 +91,7 @@ s.api.SetErrors(errors.NotFoundf(`controller "test3"`)) _, err := s.runKillCommand(c, "test3", "-y") c.Assert(err, gc.ErrorMatches, - "getting controller environ: getting bootstrap config from API: controller \"test3\" not found", + "getting controller environ: getting model config from API: controller \"test3\" not found", ) checkControllerExistsInStore(c, "test3", s.store) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/listcontrollersconverters.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/listcontrollersconverters.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/listcontrollersconverters.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/listcontrollersconverters.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,6 +7,7 @@ "fmt" "github.com/juju/errors" + "gopkg.in/juju/names.v2" "github.com/juju/juju/jujuclient" ) @@ -73,6 +74,14 @@ } } else { modelName = currentModel + if userName != "" { + // There's a user logged in, so display the + // model name relative to that user. + if unqualifiedModelName, owner, err := jujuclient.SplitModelName(modelName); err == nil { + user := names.NewUserTag(userName) + modelName = ownerQualifiedModelName(unqualifiedModelName, owner, user) + } + } } controllers[controllerName] = ControllerItem{ diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/listmodels.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/listmodels.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/listmodels.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/listmodels.go 2016-08-16 08:56:25.000000000 +0000 @@ -18,6 +18,7 @@ "github.com/juju/juju/apiserver/params" "github.com/juju/juju/cmd/juju/common" "github.com/juju/juju/cmd/modelcmd" + "github.com/juju/juju/jujuclient" ) // NewListModelsCommand returns a command to list models. @@ -29,13 +30,14 @@ // current user can access on the current controller. type modelsCommand struct { modelcmd.ControllerCommandBase - out cmd.Output - all bool - user string - listUUID bool - exactTime bool - modelAPI ModelManagerAPI - sysAPI ModelsSysAPI + out cmd.Output + all bool + loggedInUser string + user string + listUUID bool + exactTime bool + modelAPI ModelManagerAPI + sysAPI ModelsSysAPI } var listModelsDoc = ` @@ -109,28 +111,35 @@ // ModelSet contains the set of models known to the client, // and UUID of the current model. type ModelSet struct { - Models []common.ModelInfo `yaml:"models" json:"models"` - CurrentModel string `yaml:"current-model,omitempty" json:"current-model,omitempty"` + Models []common.ModelInfo `yaml:"models" json:"models"` + + // CurrentModel is the name of the current model, qualified for the + // user for which we're listing models. i.e. for the user admin@local, + // and the model admin@local/foo, this field will contain "foo"; for + // bob@local and the same model, the field will contain "admin/foo". + CurrentModel string `yaml:"current-model,omitempty" json:"current-model,omitempty"` + + // CurrentModelQualified is the fully qualified name for the current + // model, i.e. having the format $owner/$model. + CurrentModelQualified string `yaml:"-" json:"-"` } // Run implements Command.Run func (c *modelsCommand) Run(ctx *cmd.Context) error { - if c.user == "" { - accountDetails, err := c.ClientStore().AccountDetails( - c.ControllerName(), - ) - if err != nil { - return err - } - c.user = accountDetails.User + accountDetails, err := c.ClientStore().AccountDetails(c.ControllerName()) + if err != nil { + return err } + c.loggedInUser = accountDetails.User // First get a list of the models. var models []base.UserModel - var err error if c.all { models, err = c.getAllModels() } else { + if c.user == "" { + c.user = accountDetails.User + } models, err = c.getUserModels() } if err != nil { @@ -159,11 +168,21 @@ if err != nil && !errors.IsNotFound(err) { return err } + modelSet.CurrentModelQualified = current modelSet.CurrentModel = current + if c.user != "" { + userForListing := names.NewUserTag(c.user) + unqualifiedModelName, owner, err := jujuclient.SplitModelName(current) + if err == nil { + modelSet.CurrentModel = ownerQualifiedModelName( + unqualifiedModelName, owner, userForListing, + ) + } + } + if err := c.out.Write(ctx, modelSet); err != nil { return err } - if len(models) == 0 && c.out.Name() == "tabular" { // When the output is tabular, we inform the user when there // are no models available, and tell them how to go about @@ -232,6 +251,18 @@ if !ok { return nil, errors.Errorf("expected value of type %T, got %T", modelSet, value) } + + // We need the tag of the user for which we're listing models, + // and for the logged-in user. We use these below when formatting + // the model display names. + loggedInUser := names.NewUserTag(c.loggedInUser) + userForLastConn := loggedInUser + var userForListing names.UserTag + if c.user != "" { + userForListing = names.NewUserTag(c.user) + userForLastConn = userForListing + } + var out bytes.Buffer const ( // To format things into columns. @@ -248,16 +279,16 @@ } fmt.Fprintf(tw, "\tOWNER\tSTATUS\tLAST CONNECTION\n") for _, model := range modelSet.Models { - name := model.Name - if name == modelSet.CurrentModel { + owner := names.NewUserTag(model.Owner) + name := ownerQualifiedModelName(model.Name, owner, userForListing) + if jujuclient.JoinOwnerModelName(owner, model.Name) == modelSet.CurrentModelQualified { name += "*" } fmt.Fprintf(tw, "%s", name) if c.listUUID { fmt.Fprintf(tw, "\t%s", model.UUID) } - user := names.NewUserTag(c.user).Canonical() - lastConnection := model.Users[user].LastConnection + lastConnection := model.Users[userForLastConn.Canonical()].LastConnection if lastConnection == "" { lastConnection = "never connected" } @@ -266,3 +297,19 @@ tw.Flush() return out.Bytes(), nil } + +// ownerQualifiedModelName returns the model name qualified with the +// model owner if the owner is not the same as the given canonical +// user name. If the owner is a local user, we omit the domain. +func ownerQualifiedModelName(modelName string, owner, user names.UserTag) string { + if owner.Canonical() == user.Canonical() { + return modelName + } + var ownerName string + if owner.IsLocal() { + ownerName = owner.Name() + } else { + ownerName = owner.Canonical() + } + return fmt.Sprintf("%s/%s", ownerName, modelName) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/listmodels_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/listmodels_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/listmodels_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/listmodels_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -103,15 +103,15 @@ models := []base.UserModel{ { Name: "test-model1", - Owner: "user-admin@local", + Owner: "admin@local", UUID: "test-model1-UUID", }, { Name: "test-model2", - Owner: "user-admin@local", + Owner: "carlotta@local", UUID: "test-model2-UUID", }, { Name: "test-model3", - Owner: "user-admin@local", + Owner: "daiwik@external", UUID: "test-model3-UUID", }, } @@ -123,7 +123,7 @@ s.store.CurrentControllerName = "fake" s.store.Controllers["fake"] = jujuclient.ControllerDetails{} s.store.Models["fake"] = &jujuclient.ControllerModels{ - CurrentModel: "test-model1", + CurrentModel: "admin@local/test-model1", } s.store.Accounts["fake"] = jujuclient.AccountDetails{ User: "admin@local", @@ -135,21 +135,28 @@ return controller.NewListModelsCommandForTest(s.api, s.api, s.store) } -func (s *ModelsSuite) checkSuccess(c *gc.C, user string, args ...string) { - context, err := testing.RunCommand(c, s.newCommand(), args...) +func (s *ModelsSuite) TestModelsOwner(c *gc.C) { + context, err := testing.RunCommand(c, s.newCommand()) c.Assert(err, jc.ErrorIsNil) - c.Assert(s.api.user, gc.Equals, user) + c.Assert(s.api.user, gc.Equals, "admin@local") c.Assert(testing.Stdout(context), gc.Equals, ""+ - "MODEL OWNER STATUS LAST CONNECTION\n"+ - "test-model1* user-admin@local active 2015-03-20\n"+ - "test-model2 user-admin@local active 2015-03-01\n"+ - "test-model3 user-admin@local destroying never connected\n"+ + "MODEL OWNER STATUS LAST CONNECTION\n"+ + "test-model1* admin@local active 2015-03-20\n"+ + "carlotta/test-model2 carlotta@local active 2015-03-01\n"+ + "daiwik@external/test-model3 daiwik@external destroying never connected\n"+ "\n") } -func (s *ModelsSuite) TestModels(c *gc.C) { - s.checkSuccess(c, "admin@local") - s.checkSuccess(c, "bob", "--user", "bob") +func (s *ModelsSuite) TestModelsNonOwner(c *gc.C) { + context, err := testing.RunCommand(c, s.newCommand(), "--user", "bob") + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.api.user, gc.Equals, "bob") + c.Assert(testing.Stdout(context), gc.Equals, ""+ + "MODEL OWNER STATUS LAST CONNECTION\n"+ + "admin/test-model1* admin@local active 2015-03-20\n"+ + "carlotta/test-model2 carlotta@local active 2015-03-01\n"+ + "daiwik@external/test-model3 daiwik@external destroying never connected\n"+ + "\n") } func (s *ModelsSuite) TestAllModels(c *gc.C) { @@ -157,10 +164,10 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(s.api.all, jc.IsTrue) c.Assert(testing.Stdout(context), gc.Equals, ""+ - "MODEL OWNER STATUS LAST CONNECTION\n"+ - "test-model1* user-admin@local active 2015-03-20\n"+ - "test-model2 user-admin@local active 2015-03-01\n"+ - "test-model3 user-admin@local destroying never connected\n"+ + "MODEL OWNER STATUS LAST CONNECTION\n"+ + "admin/test-model1* admin@local active 2015-03-20\n"+ + "carlotta/test-model2 carlotta@local active 2015-03-01\n"+ + "daiwik@external/test-model3 daiwik@external destroying never connected\n"+ "\n") } @@ -169,10 +176,10 @@ context, err := testing.RunCommand(c, s.newCommand()) c.Assert(err, jc.ErrorIsNil) c.Assert(testing.Stdout(context), gc.Equals, ""+ - "MODEL OWNER STATUS LAST CONNECTION\n"+ - "test-model1 user-admin@local active 2015-03-20\n"+ - "test-model2 user-admin@local active 2015-03-01\n"+ - "test-model3 user-admin@local destroying never connected\n"+ + "MODEL OWNER STATUS LAST CONNECTION\n"+ + "test-model1 admin@local active 2015-03-20\n"+ + "carlotta/test-model2 carlotta@local active 2015-03-01\n"+ + "daiwik@external/test-model3 daiwik@external destroying never connected\n"+ "\n") } @@ -181,10 +188,10 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(s.api.user, gc.Equals, "admin@local") c.Assert(testing.Stdout(context), gc.Equals, ""+ - "MODEL MODEL UUID OWNER STATUS LAST CONNECTION\n"+ - "test-model1* test-model1-UUID user-admin@local active 2015-03-20\n"+ - "test-model2 test-model2-UUID user-admin@local active 2015-03-01\n"+ - "test-model3 test-model3-UUID user-admin@local destroying never connected\n"+ + "MODEL MODEL UUID OWNER STATUS LAST CONNECTION\n"+ + "test-model1* test-model1-UUID admin@local active 2015-03-20\n"+ + "carlotta/test-model2 test-model2-UUID carlotta@local active 2015-03-01\n"+ + "daiwik@external/test-model3 test-model3-UUID daiwik@external destroying never connected\n"+ "\n") } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/register.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/register.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/register.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/register.go 2016-08-16 08:56:25.000000000 +0000 @@ -25,6 +25,8 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/api" + "github.com/juju/juju/api/base" + "github.com/juju/juju/api/modelmanager" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/cmd/modelcmd" "github.com/juju/juju/jujuclient" @@ -39,7 +41,7 @@ func NewRegisterCommand() cmd.Command { cmd := ®isterCommand{} cmd.apiOpen = cmd.APIOpen - cmd.refreshModels = cmd.RefreshModels + cmd.listModelsFunc = cmd.listModels cmd.store = jujuclient.NewFileClientStore() return modelcmd.WrapBase(cmd) } @@ -48,10 +50,10 @@ // information. type registerCommand struct { modelcmd.JujuCommandBase - apiOpen api.OpenFunc - refreshModels func(_ jujuclient.ClientStore, controller string) error - store jujuclient.ClientStore - EncodedData string + apiOpen api.OpenFunc + listModelsFunc func(_ jujuclient.ClientStore, controller, user string) ([]base.UserModel, error) + store jujuclient.ClientStore + EncodedData string } var usageRegisterSummary = ` @@ -102,11 +104,12 @@ func (c *registerCommand) Run(ctx *cmd.Context) error { - registrationParams, err := c.getParameters(ctx) + store := modelcmd.QualifyingClientStore{c.store} + registrationParams, err := c.getParameters(ctx, store) if err != nil { return errors.Trace(err) } - _, err = c.store.ControllerByName(registrationParams.controllerName) + _, err = store.ControllerByName(registrationParams.controllerName) if err == nil { return errors.AlreadyExistsf("controller %q", registrationParams.controllerName) } else if !errors.IsNotFound(err) { @@ -159,7 +162,7 @@ ControllerUUID: responsePayload.ControllerUUID, CACert: responsePayload.CACert, } - if err := c.store.UpdateController(registrationParams.controllerName, controllerDetails); err != nil { + if err := store.UpdateController(registrationParams.controllerName, controllerDetails); err != nil { return errors.Trace(err) } macaroonJSON, err := responsePayload.Macaroon.MarshalJSON() @@ -170,16 +173,27 @@ User: registrationParams.userTag.Canonical(), Macaroon: string(macaroonJSON), } - if err := c.store.UpdateAccount(registrationParams.controllerName, accountDetails); err != nil { + if err := store.UpdateAccount(registrationParams.controllerName, accountDetails); err != nil { return errors.Trace(err) } // Log into the controller to verify the credentials, and - // refresh the connection information. - if err := c.refreshModels(c.store, registrationParams.controllerName); err != nil { + // list the models available. + models, err := c.listModelsFunc(store, registrationParams.controllerName, accountDetails.User) + if err != nil { return errors.Trace(err) } - if err := c.store.SetCurrentController(registrationParams.controllerName); err != nil { + for _, model := range models { + owner := names.NewUserTag(model.Owner) + if err := store.UpdateModel( + registrationParams.controllerName, + jujuclient.JoinOwnerModelName(owner, model.Name), + jujuclient.ModelDetails{model.UUID}, + ); err != nil { + return errors.Annotate(err, "storing model details") + } + } + if err := store.SetCurrentController(registrationParams.controllerName); err != nil { return errors.Trace(err) } @@ -187,41 +201,58 @@ ctx.Stderr, "\nWelcome, %s. You are now logged into %q.\n", registrationParams.userTag.Id(), registrationParams.controllerName, ) - return c.maybeSetCurrentModel(ctx, registrationParams.controllerName) + return c.maybeSetCurrentModel(ctx, store, registrationParams.controllerName, accountDetails.User, models) +} + +func (c *registerCommand) listModels(store jujuclient.ClientStore, controllerName, userName string) ([]base.UserModel, error) { + api, err := c.NewAPIRoot(store, controllerName, "") + if err != nil { + return nil, errors.Trace(err) + } + defer api.Close() + mm := modelmanager.NewClient(api) + return mm.ListModels(userName) } -func (c *registerCommand) maybeSetCurrentModel(ctx *cmd.Context, controllerName string) error { - models, err := c.store.AllModels(controllerName) - if errors.IsNotFound(err) { +func (c *registerCommand) maybeSetCurrentModel(ctx *cmd.Context, store jujuclient.ClientStore, controllerName, userName string, models []base.UserModel) error { + if len(models) == 0 { fmt.Fprintf(ctx.Stderr, "\n%s\n\n", errNoModels.Error()) return nil - } else if err != nil { - return errors.Trace(err) } // If we get to here, there is at least one model. if len(models) == 1 { // There is exactly one model shared, // so set it as the current model. - var modelName string - for modelName = range models { - // Loop exists only to obtain one and only key. - } - err := c.store.SetCurrentModel(controllerName, modelName) + model := models[0] + owner := names.NewUserTag(model.Owner) + modelName := jujuclient.JoinOwnerModelName(owner, model.Name) + err := store.SetCurrentModel(controllerName, modelName) if err != nil { return errors.Trace(err) } - fmt.Fprintf(ctx.Stderr, "\nCurrent model set to %q\n\n", modelName) + fmt.Fprintf(ctx.Stderr, "\nCurrent model set to %q.\n\n", modelName) } else { fmt.Fprintf(ctx.Stderr, ` There are %d models available. Use "juju switch" to select one of them: `, len(models)) - modelNames := make(set.Strings) - for modelName := range models { - modelNames.Add(modelName) + user := names.NewUserTag(userName) + ownerModelNames := make(set.Strings) + otherModelNames := make(set.Strings) + for _, model := range models { + if model.Owner == userName { + ownerModelNames.Add(model.Name) + continue + } + owner := names.NewUserTag(model.Owner) + modelName := ownerQualifiedModelName(model.Name, owner, user) + otherModelNames.Add(modelName) + } + for _, modelName := range ownerModelNames.SortedValues() { + fmt.Fprintf(ctx.Stderr, " - juju switch %s\n", modelName) } - for _, modelName := range modelNames.SortedValues() { + for _, modelName := range otherModelNames.SortedValues() { fmt.Fprintf(ctx.Stderr, " - juju switch %s\n", modelName) } fmt.Fprintln(ctx.Stderr) @@ -240,7 +271,7 @@ // getParameters gets all of the parameters required for registering, prompting // the user as necessary. -func (c *registerCommand) getParameters(ctx *cmd.Context) (*registrationParams, error) { +func (c *registerCommand) getParameters(ctx *cmd.Context, store jujuclient.ClientStore) (*registrationParams, error) { // Decode key, username, controller addresses from the string supplied // on the command line. @@ -263,7 +294,7 @@ copy(params.key[:], info.SecretKey) // Prompt the user for the controller name. - controllerName, err := c.promptControllerName(info.ControllerName, ctx.Stderr, ctx.Stdin) + controllerName, err := c.promptControllerName(store, info.ControllerName, ctx.Stderr, ctx.Stdin) if err != nil { return nil, errors.Trace(err) } @@ -362,22 +393,23 @@ return password, nil } -const errControllerConflicts = `WARNING: the controller proposed %q which clashes with an` + - ` existing controller. The two controllers are entirely different. +const errControllerConflicts = `WARNING: The controller proposed %q which clashes with an existing` + + ` controller. The two controllers are entirely different. ` -func (c *registerCommand) promptControllerName(suggestedName string, stderr io.Writer, stdin io.Reader) (string, error) { - _, err := c.store.ControllerByName(suggestedName) +func (c *registerCommand) promptControllerName(store jujuclient.ClientStore, suggestedName string, stderr io.Writer, stdin io.Reader) (string, error) { + _, err := store.ControllerByName(suggestedName) if err == nil { fmt.Fprintf(stderr, errControllerConflicts, suggestedName) suggestedName = "" } - setMsg := "Please set a name for this controller" + var setMsg string + setMsg = "Enter a name for this controller: " if suggestedName != "" { - setMsg = setMsg + " (" + suggestedName + ")" + setMsg = fmt.Sprintf("Enter a name for this controller [%s]: ", + suggestedName) } - setMsg = setMsg + ":" fmt.Fprintf(stderr, setMsg) defer stderr.Write([]byte{'\n'}) name, err := c.readLine(stdin) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/register_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/register_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/register_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/register_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -23,6 +23,7 @@ "gopkg.in/macaroon.v1" "github.com/juju/juju/api" + "github.com/juju/juju/api/base" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/cmd/juju/controller" "github.com/juju/juju/jujuclient" @@ -32,13 +33,14 @@ type RegisterSuite struct { testing.FakeJujuXDGDataHomeSuite - apiConnection *mockAPIConnection - store *jujuclienttesting.MemStore - apiOpenError error - refreshModels func(jujuclient.ClientStore, string) error - refreshModelsControllerName string - server *httptest.Server - httpHandler http.Handler + apiConnection *mockAPIConnection + store *jujuclienttesting.MemStore + apiOpenError error + listModels func(jujuclient.ClientStore, string, string) ([]base.UserModel, error) + listModelsControllerName string + listModelsUserName string + server *httptest.Server + httpHandler http.Handler } var _ = gc.Suite(&RegisterSuite{}) @@ -58,10 +60,12 @@ controllerTag: testing.ModelTag, addr: serverURL.Host, } - s.refreshModelsControllerName = "" - s.refreshModels = func(store jujuclient.ClientStore, controllerName string) error { - s.refreshModelsControllerName = controllerName - return nil + s.listModelsControllerName = "" + s.listModelsUserName = "" + s.listModels = func(_ jujuclient.ClientStore, controllerName, userName string) ([]base.UserModel, error) { + s.listModelsControllerName = controllerName + s.listModelsUserName = userName + return nil, nil } s.store = jujuclienttesting.NewMemStore() @@ -82,7 +86,7 @@ } func (s *RegisterSuite) run(c *gc.C, stdin io.Reader, args ...string) (*cmd.Context, error) { - command := controller.NewRegisterCommandForTest(s.apiOpen, s.refreshModels, s.store) + command := controller.NewRegisterCommandForTest(s.apiOpen, s.listModels, s.store) err := testing.InitCommand(command, args) c.Assert(err, jc.ErrorIsNil) ctx := testing.Context(c) @@ -143,10 +147,11 @@ func (s *RegisterSuite) TestRegister(c *gc.C) { ctx := s.testRegister(c, "") - c.Assert(s.refreshModelsControllerName, gc.Equals, "controller-name") + c.Assert(s.listModelsControllerName, gc.Equals, "controller-name") + c.Assert(s.listModelsUserName, gc.Equals, "bob@local") stderr := testing.Stderr(ctx) c.Assert(stderr, gc.Equals, ` -Please set a name for this controller (controller-name): +Enter a name for this controller [controller-name]: Enter a new password: Confirm password: @@ -160,29 +165,31 @@ } func (s *RegisterSuite) TestRegisterOneModel(c *gc.C) { - s.refreshModels = func(store jujuclient.ClientStore, controller string) error { - err := store.UpdateModel(controller, "theoneandonly", jujuclient.ModelDetails{ - ModelUUID: "df136476-12e9-11e4-8a70-b2227cce2b54", - }) - c.Assert(err, jc.ErrorIsNil) - return nil + s.listModels = func(_ jujuclient.ClientStore, controllerName, userName string) ([]base.UserModel, error) { + return []base.UserModel{{ + Name: "theoneandonly", + Owner: "carol@local", + UUID: "df136476-12e9-11e4-8a70-b2227cce2b54", + }}, nil } s.testRegister(c, "") c.Assert( s.store.Models["controller-name"].CurrentModel, - gc.Equals, "theoneandonly", + gc.Equals, "carol@local/theoneandonly", ) } func (s *RegisterSuite) TestRegisterMultipleModels(c *gc.C) { - s.refreshModels = func(store jujuclient.ClientStore, controller string) error { - for _, name := range [...]string{"model1", "model2"} { - err := store.UpdateModel(controller, name, jujuclient.ModelDetails{ - ModelUUID: "df136476-12e9-11e4-8a70-b2227cce2b54", - }) - c.Assert(err, jc.ErrorIsNil) - } - return nil + s.listModels = func(_ jujuclient.ClientStore, controllerName, userName string) ([]base.UserModel, error) { + return []base.UserModel{{ + Name: "model1", + Owner: "bob@local", + UUID: "df136476-12e9-11e4-8a70-b2227cce2b54", + }, { + Name: "model2", + Owner: "bob@local", + UUID: "df136476-12e9-11e4-8a70-b2227cce2b55", + }}, nil } ctx := s.testRegister(c, "") @@ -194,7 +201,7 @@ stderr := testing.Stderr(ctx) c.Assert(stderr, gc.Equals, ` -Please set a name for this controller (controller-name): +Enter a name for this controller [controller-name]: Enter a new password: Confirm password: @@ -320,12 +327,12 @@ CACert: testing.CACert, }) - s.refreshModels = func(store jujuclient.ClientStore, controller string) error { - err := store.UpdateModel(controller, "controller-name", jujuclient.ModelDetails{ - ModelUUID: "df136476-12e9-11e4-8a70-b2227cce2b54", - }) - c.Assert(err, jc.ErrorIsNil) - return nil + s.listModels = func(_ jujuclient.ClientStore, controllerName, userName string) ([]base.UserModel, error) { + return []base.UserModel{{ + Name: "model-name", + Owner: "bob@local", + UUID: "df136476-12e9-11e4-8a70-b2227cce2b54", + }}, nil } ctx := s.testRegister(c, "you must specify a non-empty controller name") @@ -337,9 +344,9 @@ c.Assert(err, jc.ErrorIsNil) stderr := testing.Stderr(ctx) c.Assert(stderr, gc.Equals, ` -WARNING: the controller proposed "controller-name" which clashes with an existing controller. The two controllers are entirely different. +WARNING: The controller proposed "controller-name" which clashes with an existing controller. The two controllers are entirely different. -Please set a name for this controller: +Enter a name for this controller: `[1:]) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/showcontroller.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/showcontroller.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/showcontroller.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/showcontroller.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,14 +4,11 @@ package controller import ( - "fmt" - "github.com/juju/cmd" "github.com/juju/errors" "launchpad.net/gnuflag" "github.com/juju/juju/cmd/modelcmd" - "github.com/juju/juju/environs/config" "github.com/juju/juju/jujuclient" ) @@ -100,10 +97,6 @@ // Account is the account details for the user logged into this controller. Account *AccountDetails `yaml:"account,omitempty" json:"account,omitempty"` - // BootstrapConfig contains the bootstrap configuration for this controller. - // This is only available on the client that bootstrapped the controller. - BootstrapConfig *BootstrapConfig `yaml:"bootstrap-config,omitempty" json:"bootstrap-config,omitempty"` - // Errors is a collection of errors related to accessing this controller details. Errors []string `yaml:"errors,omitempty" json:"errors,omitempty"` } @@ -141,17 +134,6 @@ Password string `yaml:"password,omitempty" json:"password,omitempty"` } -// BootstrapConfig holds the configuration used to bootstrap a controller. -type BootstrapConfig struct { - Config map[string]interface{} `yaml:"config,omitempty" json:"config,omitempty"` - Cloud string `yaml:"cloud" json:"cloud"` - CloudType string `yaml:"cloud-type" json:"cloud-type"` - CloudRegion string `yaml:"region,omitempty" json:"region,omitempty"` - CloudEndpoint string `yaml:"endpoint,omitempty" json:"endpoint,omitempty"` - CloudStorageEndpoint string `yaml:"storage-endpoint,omitempty" json:"storage-endpoint,omitempty"` - Credential string `yaml:"credential,omitempty" json:"credential,omitempty"` -} - func (c *showControllerCommand) convertControllerForShow(controllerName string, details *jujuclient.ControllerDetails) ShowControllerDetails { controller := ShowControllerDetails{ Details: ControllerDetails{ @@ -164,7 +146,6 @@ } c.convertModelsForShow(controllerName, &controller) c.convertAccountsForShow(controllerName, &controller) - c.convertBootstrapConfigForShow(controllerName, &controller) return controller } @@ -206,39 +187,6 @@ } } -func (c *showControllerCommand) convertBootstrapConfigForShow(controllerName string, controller *ShowControllerDetails) { - bootstrapConfig, err := c.store.BootstrapConfigForController(controllerName) - if errors.IsNotFound(err) { - return - } else if err != nil { - controller.Errors = append(controller.Errors, err.Error()) - return - } - cfg := make(map[string]interface{}) - var cloudType string - for k, v := range bootstrapConfig.Config { - switch k { - case config.NameKey: - // Name is always "admin" for the admin model, - // which is not interesting to us here. - case config.TypeKey: - // Pull Type up to the top level. - cloudType = fmt.Sprint(v) - default: - cfg[k] = v - } - } - controller.BootstrapConfig = &BootstrapConfig{ - Config: cfg, - Cloud: bootstrapConfig.Cloud, - CloudType: cloudType, - CloudRegion: bootstrapConfig.CloudRegion, - CloudEndpoint: bootstrapConfig.CloudEndpoint, - CloudStorageEndpoint: bootstrapConfig.CloudStorageEndpoint, - Credential: bootstrapConfig.Credential, - } -} - type showControllerCommand struct { modelcmd.JujuCommandBase diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/showcontroller_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/showcontroller_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/controller/showcontroller_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/controller/showcontroller_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -101,6 +101,7 @@ "extra": "value", }, Credential: "my-credential", + CloudType: "maas", Cloud: "mallards", CloudRegion: "mallards1", CloudEndpoint: "http://mallards.local/MAAS", @@ -122,14 +123,6 @@ current-model: my-model account: user: admin@local - bootstrap-config: - config: - extra: value - cloud: mallards - cloud-type: maas - region: mallards1 - endpoint: http://mallards.local/MAAS - credential: my-credential `[1:] s.assertShowController(c, "mallards") diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/interact/doc.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/interact/doc.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/interact/doc.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/interact/doc.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,6 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +// Package interact provides helper methods for interacting with the CLI user at +// command run time. +package interact diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/interact/package_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/interact/package_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/interact/package_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/interact/package_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,14 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package interact + +import ( + "testing" + + gc "gopkg.in/check.v1" +) + +func TestPackage(t *testing.T) { + gc.TestingT(t) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/interact/query.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/interact/query.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/interact/query.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/interact/query.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,75 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package interact + +import ( + "bufio" + "fmt" + "io" + "strings" +) + +// QueryVerify writes a question to w and waits for an answer to be read from +// scanner. It will pass the answer into the verify function. Verify, if +// non-nil, should check the answer for validity, returning an error that will +// be written out to the user, or nil if answer is valid. +// +// This function takes a scanner rather than an io.Reader to avoid the case +// where the scanner reads past the delimiter and thus might lose data. It is +// expected that this method will be used repeatedly with the same scanner if +// multiple queries are required. +func QueryVerify(question []byte, scanner *bufio.Scanner, w io.Writer, verify func(string) error) (answer string, err error) { + defer fmt.Fprint(w, "\n") + for { + if _, err = w.Write(question); err != nil { + return "", err + } + + if !scanner.Scan() { + if err := scanner.Err(); err != nil { + return "", err + } + return "", io.EOF + } + answer = scanner.Text() + if verify == nil { + return answer, nil + } + err := verify(answer) + // valid answer, return it! + if err == nil { + return answer, nil + } + // invalid answer, inform user of problem and retry. + _, err = fmt.Fprint(w, err, "\n\n") + if err != nil { + return "", err + } + } +} + +// MatchOptions returns a function that performs a case insensitive comparison +// against the given list of options. To make a verification function that +// accepts an empty default, include an empty string in the list. +func MatchOptions(options []string, err error) func(string) error { + return func(s string) error { + for _, opt := range options { + if strings.ToLower(opt) == strings.ToLower(s) { + return nil + } + } + return err + } +} + +// FindMatch does a case-insensitive search of the given options and returns the +// matching option. Found reports whether s was found in the options. +func FindMatch(s string, options []string) (match string, found bool) { + for _, opt := range options { + if strings.ToLower(opt) == strings.ToLower(s) { + return opt, true + } + } + return "", false +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/interact/query_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/interact/query_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/interact/query_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/interact/query_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,105 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package interact + +import ( + "bufio" + "bytes" + "errors" + "io/ioutil" + "strings" + + "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" +) + +type Suite struct { + testing.IsolationSuite +} + +var _ = gc.Suite(Suite{}) + +func (Suite) TestAnswer(c *gc.C) { + scanner := bufio.NewScanner(strings.NewReader("hi!\n")) + answer, err := QueryVerify([]byte("boo: "), scanner, ioutil.Discard, nil) + c.Assert(err, jc.ErrorIsNil) + c.Assert(answer, gc.Equals, "hi!") +} + +func (Suite) TestVerify(c *gc.C) { + scanner := bufio.NewScanner(strings.NewReader("hi!\nok!\n")) + out := bytes.Buffer{} + verify := func(s string) error { + if s == "ok!" { + return nil + } + return errors.New("No!") + } + answer, err := QueryVerify([]byte("boo: "), scanner, &out, verify) + c.Assert(err, jc.ErrorIsNil) + c.Assert(answer, gc.Equals, "ok!") + // in practice, "No!" will be on a separate line, since the cursor will get + // moved down by the user hitting return for their answer, but the output + // we generate doesn't do that itself.' + expected := ` +boo: No! + +boo: +`[1:] + c.Assert(out.String(), gc.Equals, expected) +} + +func (Suite) TestQueryMultiple(c *gc.C) { + scanner := bufio.NewScanner(strings.NewReader(` +hi! +ok! +bob +`[1:])) + verify := func(s string) error { + if s == "ok!" { + return nil + } + return errors.New("No!") + } + answer, err := QueryVerify([]byte("boo: "), scanner, ioutil.Discard, verify) + c.Assert(err, jc.ErrorIsNil) + c.Assert(answer, gc.Equals, "ok!") + + answer, err = QueryVerify([]byte("name: "), scanner, ioutil.Discard, nil) + c.Assert(err, jc.ErrorIsNil) + c.Assert(answer, gc.Equals, "bob") +} + +func (Suite) TestMatchOptions(c *gc.C) { + err := errors.New("err") + f := MatchOptions([]string{"foo", "BAR"}, err) + c.Check(f("foo"), jc.ErrorIsNil) + c.Check(f("FOO"), jc.ErrorIsNil) + c.Check(f("BAR"), jc.ErrorIsNil) + c.Check(f("bar"), jc.ErrorIsNil) + c.Check(f("baz"), gc.Equals, err) +} + +func (Suite) TestFindMatch(c *gc.C) { + options := []string{"foo", "BAR"} + m, ok := FindMatch("foo", options) + c.Check(m, gc.Equals, "foo") + c.Check(ok, jc.IsTrue) + + m, ok = FindMatch("FOO", options) + c.Check(m, gc.Equals, "foo") + c.Check(ok, jc.IsTrue) + + m, ok = FindMatch("bar", options) + c.Check(m, gc.Equals, "BAR") + c.Check(ok, jc.IsTrue) + + m, ok = FindMatch("BAR", options) + c.Check(m, gc.Equals, "BAR") + c.Check(ok, jc.IsTrue) + + m, ok = FindMatch("baz", options) + c.Check(ok, jc.IsFalse) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/machine/add.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/machine/add.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/machine/add.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/machine/add.go 2016-08-16 08:56:25.000000000 +0000 @@ -13,6 +13,7 @@ "launchpad.net/gnuflag" "github.com/juju/juju/api/machinemanager" + "github.com/juju/juju/api/modelconfig" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/cmd/juju/block" "github.com/juju/juju/cmd/juju/common" @@ -93,6 +94,7 @@ type addCommand struct { modelcmd.ModelCommandBase api AddMachineAPI + modelConfigAPI ModelConfigAPI machineManagerAPI MachineManagerAPI // If specified, use this series, else use the model default-series Series string @@ -140,7 +142,7 @@ return err } if c.NumMachines > 1 && c.Placement != nil && c.Placement.Directive != "" { - return fmt.Errorf("cannot use -n when specifying a placement directive") + return errors.New("cannot use -n when specifying a placement directive") } return nil } @@ -149,11 +151,15 @@ AddMachines([]params.AddMachineParams) ([]params.AddMachinesResult, error) Close() error ForceDestroyMachines(machines ...string) error - ModelGet() (map[string]interface{}, error) ModelUUID() (string, error) ProvisioningScript(params.ProvisioningScriptParams) (script string, err error) } +type ModelConfigAPI interface { + ModelGet() (map[string]interface{}, error) + Close() error +} + type MachineManagerAPI interface { AddMachines([]params.AddMachineParams) ([]params.AddMachinesResult, error) BestAPIVersion() int @@ -169,6 +175,18 @@ return c.NewAPIClient() } +func (c *addCommand) getModelConfigAPI() (ModelConfigAPI, error) { + if c.modelConfigAPI != nil { + return c.modelConfigAPI, nil + } + api, err := c.NewAPIRoot() + if err != nil { + return nil, errors.Annotate(err, "opening API connection") + } + return modelconfig.NewClient(api), nil + +} + func (c *addCommand) NewMachineManagerClient() (*machinemanager.Client, error) { root, err := c.NewAPIRoot() if err != nil { @@ -204,7 +222,12 @@ } logger.Infof("load config") - configAttrs, err := client.ModelGet() + modelConfigClient, err := c.getModelConfigAPI() + if err != nil { + return errors.Trace(err) + } + defer modelConfigClient.Close() + configAttrs, err := modelConfigClient.ModelGet() if err != nil { return errors.Trace(err) } @@ -295,7 +318,7 @@ } } if len(errs) == 1 { - fmt.Fprintf(ctx.Stderr, "failed to create 1 machine\n") + fmt.Fprint(ctx.Stderr, "failed to create 1 machine\n") return errs[0] } if len(errs) > 1 { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/machine/add_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/machine/add_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/machine/add_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/machine/add_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -95,7 +95,7 @@ }, } { c.Logf("test %d", i) - wrappedCommand, addCmd := machine.NewAddCommandForTest(s.fakeAddMachine, s.fakeMachineManager) + wrappedCommand, addCmd := machine.NewAddCommandForTest(s.fakeAddMachine, s.fakeAddMachine, s.fakeMachineManager) err := testing.InitCommand(wrappedCommand, test.args) if test.errorString == "" { c.Check(err, jc.ErrorIsNil) @@ -114,7 +114,7 @@ } func (s *AddMachineSuite) run(c *gc.C, args ...string) (*cmd.Context, error) { - add, _ := machine.NewAddCommandForTest(s.fakeAddMachine, s.fakeMachineManager) + add, _ := machine.NewAddCommandForTest(s.fakeAddMachine, s.fakeAddMachine, s.fakeMachineManager) return testing.RunCommand(c, add, args...) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/machine/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/machine/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/machine/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/machine/export_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -19,10 +19,11 @@ } // NewAddCommand returns an AddCommand with the api provided as specified. -func NewAddCommandForTest(api AddMachineAPI, mmApi MachineManagerAPI) (cmd.Command, *AddCommand) { +func NewAddCommandForTest(api AddMachineAPI, mcApi ModelConfigAPI, mmApi MachineManagerAPI) (cmd.Command, *AddCommand) { cmd := &addCommand{ api: api, machineManagerAPI: mmApi, + modelConfigAPI: mcApi, } return modelcmd.Wrap(cmd), &AddCommand{cmd} } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/constraints.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/constraints.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/constraints.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/constraints.go 2016-08-16 08:56:25.000000000 +0000 @@ -30,10 +30,11 @@ juju get-model-constraints juju get-model-constraints -m mymodel -See also: models - set-model-constraints - set-constraints - get-constraints +See also: + models + get-constraints + set-constraints + set-model-constraints ` // setConstraintsDoc is multi-line since we need to use ` to denote @@ -53,10 +54,11 @@ juju set-model-constraints cpu-cores=8 mem=16G juju set-model-constraints -m mymodel root-disk=64G -See also: models - get-model-constraints - set-constraints - get-constraints +See also: + models + get-model-constraints + get-constraints + set-constraints ` // ConstraintsAPI defines methods on the client API that diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/destroy_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/destroy_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/destroy_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/destroy_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -52,8 +52,8 @@ s.store.Controllers["test1"] = jujuclient.ControllerDetails{ControllerUUID: "test1-uuid"} s.store.Models["test1"] = &jujuclient.ControllerModels{ Models: map[string]jujuclient.ModelDetails{ - "test1": {"test1-uuid"}, - "test2": {"test2-uuid"}, + "admin@local/test1": {"test1-uuid"}, + "admin@local/test2": {"test2-uuid"}, }, } s.store.Accounts["test1"] = jujuclient.AccountDetails{ @@ -99,7 +99,7 @@ func (s *DestroySuite) TestDestroyUnknownModel(c *gc.C) { _, err := s.runDestroyCommand(c, "foo") - c.Assert(err, gc.ErrorMatches, `cannot read model info: model test1:foo not found`) + c.Assert(err, gc.ErrorMatches, `cannot read model info: model test1:admin@local/foo not found`) } func (s *DestroySuite) TestDestroyCannotConnectToAPI(c *gc.C) { @@ -107,34 +107,34 @@ _, err := s.runDestroyCommand(c, "test2", "-y") c.Assert(err, gc.ErrorMatches, "cannot destroy model: connection refused") c.Check(c.GetTestLog(), jc.Contains, "failed to destroy model \"test2\"") - checkModelExistsInStore(c, "test1:test2", s.store) + checkModelExistsInStore(c, "test1:admin@local/test2", s.store) } func (s *DestroySuite) TestSystemDestroyFails(c *gc.C) { _, err := s.runDestroyCommand(c, "test1", "-y") c.Assert(err, gc.ErrorMatches, `"test1" is a controller; use 'juju destroy-controller' to destroy it`) - checkModelExistsInStore(c, "test1:test1", s.store) + checkModelExistsInStore(c, "test1:admin@local/test1", s.store) } func (s *DestroySuite) TestDestroy(c *gc.C) { - checkModelExistsInStore(c, "test1:test2", s.store) + checkModelExistsInStore(c, "test1:admin@local/test2", s.store) _, err := s.runDestroyCommand(c, "test2", "-y") c.Assert(err, jc.ErrorIsNil) - checkModelRemovedFromStore(c, "test1:test2", s.store) + checkModelRemovedFromStore(c, "test1:admin@local/test2", s.store) } func (s *DestroySuite) TestFailedDestroyModel(c *gc.C) { s.api.err = errors.New("permission denied") _, err := s.runDestroyCommand(c, "test1:test2", "-y") c.Assert(err, gc.ErrorMatches, "cannot destroy model: permission denied") - checkModelExistsInStore(c, "test1:test2", s.store) + checkModelExistsInStore(c, "test1:admin@local/test2", s.store) } func (s *DestroySuite) resetModel(c *gc.C) { s.store.Models["test1"] = &jujuclient.ControllerModels{ Models: map[string]jujuclient.ModelDetails{ - "test1": {"test1-uuid"}, - "test2": {"test2-uuid"}, + "admin@local/test1": {"test1-uuid"}, + "admin@local/test2": {"test2-uuid"}, }, } } @@ -156,7 +156,7 @@ c.Fatalf("command took too long") } c.Check(testing.Stdout(ctx), gc.Matches, "WARNING!.*test2(.|\n)*") - checkModelExistsInStore(c, "test1:test1", s.store) + checkModelExistsInStore(c, "test1:admin@local/test1", s.store) // EOF on stdin: equivalent to answering no. stdin.Reset() @@ -169,7 +169,7 @@ c.Fatalf("command took too long") } c.Check(testing.Stdout(ctx), gc.Matches, "WARNING!.*test2(.|\n)*") - checkModelExistsInStore(c, "test1:test2", s.store) + checkModelExistsInStore(c, "test1:admin@local/test2", s.store) for _, answer := range []string{"y", "Y", "yes", "YES"} { stdin.Reset() @@ -182,7 +182,7 @@ case <-time.After(testing.LongWait): c.Fatalf("command took too long") } - checkModelRemovedFromStore(c, "test1:test2", s.store) + checkModelRemovedFromStore(c, "test1:admin@local/test2", s.store) // Add the test2 model back into the store for the next test s.resetModel(c) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/dump.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/dump.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/dump.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/dump.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,103 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package model + +import ( + "github.com/juju/cmd" + "github.com/juju/errors" + "gopkg.in/juju/names.v2" + "launchpad.net/gnuflag" + + "github.com/juju/juju/api/modelmanager" + "github.com/juju/juju/cmd/modelcmd" +) + +// NewDumpCommand returns a fully constructed dump-model command. +func NewDumpCommand() cmd.Command { + return modelcmd.Wrap(&dumpCommand{}) +} + +type dumpCommand struct { + modelcmd.ModelCommandBase + out cmd.Output + api DumpModelAPI +} + +const dumpModelHelpDoc = ` +Calls export on the model's database representation and writes the +resulting YAML to stdout. + +Examples: + + juju dump-model + juju dump-model -m mymodel + +See also: + models +` + +// Info implements Command. +func (c *dumpCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "dump-model", + Purpose: "Displays the database agnostic representation of the model.", + Doc: dumpModelHelpDoc, + } +} + +// SetFlags implements Command. +func (c *dumpCommand) SetFlags(f *gnuflag.FlagSet) { + c.out.AddFlags(f, "yaml", map[string]cmd.Formatter{ + "yaml": cmd.FormatYaml, + "json": cmd.FormatJson, + }) +} + +// Init implements Command. +func (c *dumpCommand) Init(args []string) (err error) { + return cmd.CheckEmpty(args) +} + +// DumpModelAPI specifies the used function calls of the ModelManager. +type DumpModelAPI interface { + Close() error + DumpModel(names.ModelTag) (map[string]interface{}, error) +} + +func (c *dumpCommand) getAPI() (DumpModelAPI, error) { + if c.api != nil { + return c.api, nil + } + root, err := c.NewAPIRoot() + if err != nil { + return nil, errors.Trace(err) + } + return modelmanager.NewClient(root), nil +} + +// Run implements Command. +func (c *dumpCommand) Run(ctx *cmd.Context) error { + client, err := c.getAPI() + if err != nil { + return err + } + defer client.Close() + + store := c.ClientStore() + modelDetails, err := store.ModelByName( + c.ControllerName(), + c.ModelName(), + ) + if err != nil { + return errors.Annotate(err, "getting model details") + } + + modelTag := names.NewModelTag(modelDetails.ModelUUID) + results, err := client.DumpModel(modelTag) + if err != nil { + return err + } + + return c.out.Write(ctx, results) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/dump_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/dump_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/dump_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/dump_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,72 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for info. + +package model_test + +import ( + gitjujutesting "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/juju/names.v2" + + "github.com/juju/juju/cmd/juju/model" + "github.com/juju/juju/jujuclient" + "github.com/juju/juju/jujuclient/jujuclienttesting" + "github.com/juju/juju/testing" +) + +type DumpCommandSuite struct { + testing.FakeJujuXDGDataHomeSuite + fake fakeDumpClient + store *jujuclienttesting.MemStore +} + +var _ = gc.Suite(&DumpCommandSuite{}) + +type fakeDumpClient struct { + gitjujutesting.Stub +} + +func (f *fakeDumpClient) Close() error { + f.MethodCall(f, "Close") + return f.NextErr() +} + +func (f *fakeDumpClient) DumpModel(model names.ModelTag) (map[string]interface{}, error) { + f.MethodCall(f, "DumpModel", model) + err := f.NextErr() + if err != nil { + return nil, err + } + return map[string]interface{}{ + "model-uuid": "fake uuid", + }, nil +} + +func (s *DumpCommandSuite) SetUpTest(c *gc.C) { + s.FakeJujuXDGDataHomeSuite.SetUpTest(c) + s.fake.ResetCalls() + s.store = jujuclienttesting.NewMemStore() + s.store.CurrentControllerName = "testing" + s.store.Controllers["testing"] = jujuclient.ControllerDetails{} + s.store.Accounts["testing"] = jujuclient.AccountDetails{ + User: "admin@local", + } + err := s.store.UpdateModel("testing", "admin@local/mymodel", jujuclient.ModelDetails{ + testing.ModelTag.Id(), + }) + c.Assert(err, jc.ErrorIsNil) + s.store.Models["testing"].CurrentModel = "admin@local/mymodel" +} + +func (s *DumpCommandSuite) TestDump(c *gc.C) { + ctx, err := testing.RunCommand(c, model.NewDumpCommandForTest(&s.fake, s.store)) + c.Assert(err, jc.ErrorIsNil) + s.fake.CheckCalls(c, []gitjujutesting.StubCall{ + {"DumpModel", []interface{}{testing.ModelTag}}, + {"Close", nil}, + }) + + out := testing.Stdout(ctx) + c.Assert(out, gc.Equals, "model-uuid: fake uuid\n") +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/export_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -34,6 +34,30 @@ return modelcmd.Wrap(cmd) } +// NewGetDefaultsCommandForTest returns a GetDefaultsCommand with the api provided as specified. +func NewGetDefaultsCommandForTest(api modelDefaultsAPI) cmd.Command { + cmd := &getDefaultsCommand{ + newAPIFunc: func() (modelDefaultsAPI, error) { return api, nil }, + } + return modelcmd.Wrap(cmd) +} + +// NewSetDefaultsCommandForTest returns a SetDefaultsCommand with the api provided as specified. +func NewSetDefaultsCommandForTest(api setModelDefaultsAPI) cmd.Command { + cmd := &setDefaultsCommand{ + newAPIFunc: func() (setModelDefaultsAPI, error) { return api, nil }, + } + return modelcmd.Wrap(cmd) +} + +// NewUnsetDefaultsCommandForTest returns a UnsetDefaultsCommand with the api provided as specified. +func NewUnsetDefaultsCommandForTest(api unsetModelDefaultsAPI) cmd.Command { + cmd := &unsetDefaultsCommand{ + newAPIFunc: func() (unsetModelDefaultsAPI, error) { return api, nil }, + } + return modelcmd.Wrap(cmd) +} + // NewRetryProvisioningCommandForTest returns a RetryProvisioningCommand with the api provided as specified. func NewRetryProvisioningCommandForTest(api RetryProvisioningAPI) cmd.Command { cmd := &retryProvisioningCommand{ @@ -55,6 +79,13 @@ cmd.SetClientStore(store) return modelcmd.Wrap(cmd) } + +// NewDumpCommandForTest returns a DumpCommand with the api provided as specified. +func NewDumpCommandForTest(api DumpModelAPI, store jujuclient.ClientStore) cmd.Command { + cmd := &dumpCommand{api: api} + cmd.SetClientStore(store) + return modelcmd.Wrap(cmd) +} // NewDestroyCommandForTest returns a DestroyCommand with the api provided as specified. func NewDestroyCommandForTest(api DestroyModelAPI, store jujuclient.ClientStore) cmd.Command { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/fakeenv_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/fakeenv_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/fakeenv_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/fakeenv_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -23,13 +23,18 @@ "special": "special value", "running": true, }, + defaults: config.ConfigValues{ + "attr": {Value: "foo", Source: "default"}, + "attr2": {Value: "bar", Source: "controller"}, + }, } } type fakeEnvAPI struct { - values map[string]interface{} - err error - keys []string + values map[string]interface{} + defaults config.ConfigValues + err error + keys []string } func (f *fakeEnvAPI) Close() error { @@ -48,6 +53,30 @@ return result, nil } +func (f *fakeEnvAPI) ModelDefaults() (config.ConfigValues, error) { + return f.defaults, nil +} + +func (f *fakeEnvAPI) SetModelDefaults(cfg map[string]interface{}) error { + if f.err != nil { + return f.err + } + for name, val := range cfg { + f.defaults[name] = config.ConfigValue{Value: val, Source: "controller"} + } + return nil +} + +func (f *fakeEnvAPI) UnsetModelDefaults(keys ...string) error { + if f.err != nil { + return f.err + } + for _, key := range keys { + delete(f.defaults, key) + } + return nil +} + func (f *fakeEnvAPI) ModelSet(config map[string]interface{}) error { f.values = config return f.err diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/getdefaults.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/getdefaults.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/getdefaults.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/getdefaults.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,165 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package model + +import ( + "bytes" + "fmt" + "sort" + "strings" + "text/tabwriter" + + "github.com/juju/cmd" + "github.com/juju/errors" + "launchpad.net/gnuflag" + + "github.com/juju/juju/api/modelconfig" + "github.com/juju/juju/cmd/modelcmd" + "github.com/juju/juju/environs/config" +) + +// NewModelDefaultsCommand returns a command used to print the +// default model config attributes. +func NewModelDefaultsCommand() cmd.Command { + c := &getDefaultsCommand{} + c.newAPIFunc = func() (modelDefaultsAPI, error) { + api, err := c.NewAPIRoot() + if err != nil { + return nil, errors.Annotate(err, "opening API connection") + } + return modelconfig.NewClient(api), nil + } + return modelcmd.Wrap(c) +} + +type getDefaultsCommand struct { + modelcmd.ModelCommandBase + newAPIFunc func() (modelDefaultsAPI, error) + key string + out cmd.Output +} + +const modelDefaultsHelpDoc = ` +By default, all default configuration (keys and values) are +displayed if a key is not specified. +By default, the model is the current model. + +Examples: + + juju model-defaults + juju model-defaults http-proxy + juju model-defaults -m mymodel type + +See also: + models + set-model-defaults + unset-model-defaults + set-model-config + get-model-config + unset-model-config +` + +func (c *getDefaultsCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "model-defaults", + Args: "[]", + Purpose: "Displays default configuration settings for a model.", + Doc: modelDefaultsHelpDoc, + } +} + +func (c *getDefaultsCommand) SetFlags(f *gnuflag.FlagSet) { + c.out.AddFlags(f, "tabular", map[string]cmd.Formatter{ + "yaml": cmd.FormatYaml, + "json": cmd.FormatJson, + "tabular": formatDefaultConfigTabular, + }) +} + +func (c *getDefaultsCommand) Init(args []string) (err error) { + c.key, err = cmd.ZeroOrOneArgs(args) + return +} + +// modelDefaultsAPI defines the api methods used by this command. +type modelDefaultsAPI interface { + // Close closes the api connection. + Close() error + + // ModelDefaults returns the default config values used when creating a new model. + ModelDefaults() (config.ConfigValues, error) +} + +func (c *getDefaultsCommand) Run(ctx *cmd.Context) error { + client, err := c.newAPIFunc() + if err != nil { + return err + } + defer client.Close() + + attrs, err := client.ModelDefaults() + if err != nil { + return err + } + + if c.key != "" { + if value, ok := attrs[c.key]; ok { + attrs = config.ConfigValues{ + c.key: value, + } + } else { + return errors.Errorf("key %q not found in %q model defaults.", c.key, attrs["name"]) + } + } + // If key is empty, write out the whole lot. + return c.out.Write(ctx, attrs) +} + +// formatConfigTabular returns a tabular summary of default config information. +func formatDefaultConfigTabular(value interface{}) ([]byte, error) { + configValues, ok := value.(config.ConfigValues) + if !ok { + return nil, errors.Errorf("expected value of type %T, got %T", configValues, value) + } + + var out bytes.Buffer + const ( + // To format things into columns. + minwidth = 0 + tabwidth = 1 + padding = 2 + padchar = ' ' + flags = 0 + ) + tw := tabwriter.NewWriter(&out, minwidth, tabwidth, padding, padchar, flags) + p := func(values ...string) { + text := strings.Join(values, "\t") + fmt.Fprintln(tw, text) + } + var valueNames []string + for name := range configValues { + valueNames = append(valueNames, name) + } + sort.Strings(valueNames) + p("ATTRIBUTE\tDEFAULT\tCONTROLLER") + + for _, name := range valueNames { + info := configValues[name] + val, err := cmd.FormatSmart(info.Value) + if err != nil { + return nil, errors.Annotatef(err, "formatting value for %q", name) + } + d := "-" + c := "-" + if info.Source == "default" { + d = string(val) + } else { + c = string(val) + } + p(name, d, c) + } + + tw.Flush() + return out.Bytes(), nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/getdefaults_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/getdefaults_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/getdefaults_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/getdefaults_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,92 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package model_test + +import ( + "strings" + + "github.com/juju/cmd" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/juju/model" + "github.com/juju/juju/testing" +) + +type getDefaultsSuite struct { + fakeEnvSuite +} + +var _ = gc.Suite(&getDefaultsSuite{}) + +func (s *getDefaultsSuite) run(c *gc.C, args ...string) (*cmd.Context, error) { + command := model.NewGetDefaultsCommandForTest(s.fake) + return testing.RunCommand(c, command, args...) +} + +func (s *getDefaultsSuite) TestInitArgCount(c *gc.C) { + // zero or one args is fine. + err := testing.InitCommand(model.NewGetDefaultsCommandForTest(s.fake), nil) + c.Check(err, jc.ErrorIsNil) + err = testing.InitCommand(model.NewGetDefaultsCommandForTest(s.fake), []string{"one"}) + c.Check(err, jc.ErrorIsNil) + // More than one is not allowed. + err = testing.InitCommand(model.NewGetDefaultsCommandForTest(s.fake), []string{"one", "two"}) + c.Check(err, gc.ErrorMatches, `unrecognized args: \["two"\]`) +} + +func (s *getDefaultsSuite) TestSingleValue(c *gc.C) { + context, err := s.run(c, "attr") + c.Assert(err, jc.ErrorIsNil) + + output := strings.TrimSpace(testing.Stdout(context)) + expected := "" + + "ATTRIBUTE DEFAULT CONTROLLER\n" + + "attr foo -" + c.Assert(output, gc.Equals, expected) +} + +func (s *getDefaultsSuite) TestSingleValueJSON(c *gc.C) { + context, err := s.run(c, "--format=json", "attr") + c.Assert(err, jc.ErrorIsNil) + + output := strings.TrimSpace(testing.Stdout(context)) + c.Assert(output, gc.Equals, `{"attr":{"Value":"foo","Source":"default"}}`) +} + +func (s *getDefaultsSuite) TestAllValuesYAML(c *gc.C) { + context, err := s.run(c, "--format=yaml") + c.Assert(err, jc.ErrorIsNil) + + output := strings.TrimSpace(testing.Stdout(context)) + expected := "" + + "attr:\n" + + " value: foo\n" + + " source: default\n" + + "attr2:\n" + + " value: bar\n" + + " source: controller" + c.Assert(output, gc.Equals, expected) +} + +func (s *getDefaultsSuite) TestAllValuesJSON(c *gc.C) { + context, err := s.run(c, "--format=json") + c.Assert(err, jc.ErrorIsNil) + + output := strings.TrimSpace(testing.Stdout(context)) + expected := `{"attr":{"Value":"foo","Source":"default"},"attr2":{"Value":"bar","Source":"controller"}}` + c.Assert(output, gc.Equals, expected) +} + +func (s *getDefaultsSuite) TestAllValuesTabular(c *gc.C) { + context, err := s.run(c) + c.Assert(err, jc.ErrorIsNil) + + output := strings.TrimSpace(testing.Stdout(context)) + expected := "" + + "ATTRIBUTE DEFAULT CONTROLLER\n" + + "attr foo -\n" + + "attr2 - bar" + c.Assert(output, gc.Equals, expected) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/get.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/get.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/get.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/get.go 2016-08-16 08:56:25.000000000 +0000 @@ -14,6 +14,7 @@ "github.com/juju/errors" "launchpad.net/gnuflag" + "github.com/juju/juju/api/modelconfig" "github.com/juju/juju/cmd/modelcmd" "github.com/juju/juju/environs/config" ) @@ -41,17 +42,19 @@ juju get-model-config default-series juju get-model-config -m mymodel type -See also: models - set-model-config - unset-model-config +See also: + models + set-model-config + unset-model-config ` func (c *getCommand) Info() *cmd.Info { return &cmd.Info{ Name: "get-model-config", + Aliases: []string{"model-config"}, Args: "[]", Purpose: "Displays configuration settings for a model.", - Doc: strings.TrimSpace(getModelHelpDoc), + Doc: getModelHelpDoc, } } @@ -78,7 +81,19 @@ if c.api != nil { return c.api, nil } - return c.NewAPIClient() + api, err := c.NewAPIRoot() + if err != nil { + return nil, errors.Annotate(err, "opening API connection") + } + return modelconfig.NewClient(api), nil +} + +func (c *getCommand) isModelAttrbute(attr string) bool { + switch attr { + case config.NameKey, config.TypeKey, config.UUIDKey: + return true + } + return false } func (c *getCommand) Run(ctx *cmd.Context) error { @@ -93,6 +108,14 @@ return err } + for attrName := range attrs { + // We don't want model attributes included, these are available + // via show-model. + if c.isModelAttrbute(attrName) { + delete(attrs, attrName) + } + } + if c.key != "" { if value, found := attrs[c.key]; found { out, err := cmd.FormatSmart(value.Value) @@ -142,7 +165,10 @@ if err != nil { return nil, errors.Annotatef(err, "formatting value for %q", name) } - p(name, info.Source, string(val)) + // Some attribute values have a newline appended + // which makes the output messy. + valString := strings.TrimSuffix(string(val), "\n") + p(name, info.Source, valString) } tw.Flush() diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/get_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/get_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/get_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/get_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -37,19 +37,19 @@ } func (s *GetSuite) TestSingleValue(c *gc.C) { - context, err := s.run(c, "name") + context, err := s.run(c, "special") c.Assert(err, jc.ErrorIsNil) output := strings.TrimSpace(testing.Stdout(context)) - c.Assert(output, gc.Equals, "test-model") + c.Assert(output, gc.Equals, "special value") } func (s *GetSuite) TestSingleValueJSON(c *gc.C) { - context, err := s.run(c, "--format=json", "name") + context, err := s.run(c, "--format=json", "special") c.Assert(err, jc.ErrorIsNil) output := strings.TrimSpace(testing.Stdout(context)) - c.Assert(output, gc.Equals, "test-model") + c.Assert(output, gc.Equals, "special value") } func (s *GetSuite) TestAllValuesYAML(c *gc.C) { @@ -58,9 +58,6 @@ output := strings.TrimSpace(testing.Stdout(context)) expected := "" + - "name:\n" + - " value: test-model\n" + - " source: model\n" + "running:\n" + " value: true\n" + " source: model\n" + @@ -75,7 +72,7 @@ c.Assert(err, jc.ErrorIsNil) output := strings.TrimSpace(testing.Stdout(context)) - expected := `{"name":{"Value":"test-model","Source":"model"},"running":{"Value":true,"Source":"model"},"special":{"Value":"special value","Source":"model"}}` + expected := `{"running":{"Value":true,"Source":"model"},"special":{"Value":"special value","Source":"model"}}` c.Assert(output, gc.Equals, expected) } @@ -86,7 +83,6 @@ output := strings.TrimSpace(testing.Stdout(context)) expected := "" + "ATTRIBUTE FROM VALUE\n" + - "name model test-model\n" + "running model True\n" + "special model special value" c.Assert(output, gc.Equals, expected) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/grantrevoke_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/grantrevoke_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/grantrevoke_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/grantrevoke_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -47,11 +47,11 @@ s.store.Models = map[string]*jujuclient.ControllerModels{ controllerName: { Models: map[string]jujuclient.ModelDetails{ - "foo": jujuclient.ModelDetails{fooModelUUID}, - "bar": jujuclient.ModelDetails{barModelUUID}, - "baz": jujuclient.ModelDetails{bazModelUUID}, - "model1": jujuclient.ModelDetails{model1ModelUUID}, - "model2": jujuclient.ModelDetails{model2ModelUUID}, + "bob@local/foo": jujuclient.ModelDetails{fooModelUUID}, + "bob@local/bar": jujuclient.ModelDetails{barModelUUID}, + "bob@local/baz": jujuclient.ModelDetails{bazModelUUID}, + "bob@local/model1": jujuclient.ModelDetails{model1ModelUUID}, + "bob@local/model2": jujuclient.ModelDetails{model2ModelUUID}, }, }, } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/setdefaults.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/setdefaults.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/setdefaults.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/setdefaults.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,107 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package model + +import ( + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/utils/keyvalues" + + "github.com/juju/juju/api/modelconfig" + "github.com/juju/juju/cmd/juju/block" + "github.com/juju/juju/cmd/modelcmd" + "github.com/juju/juju/environs/config" +) + +// NewSetModelDefaultsCommand returns a command used to set default +// model attributes used when creating a new model. +func NewSetModelDefaultsCommand() cmd.Command { + c := &setDefaultsCommand{} + c.newAPIFunc = func() (setModelDefaultsAPI, error) { + api, err := c.NewAPIRoot() + if err != nil { + return nil, errors.Annotate(err, "opening API connection") + } + return modelconfig.NewClient(api), nil + } + return modelcmd.Wrap(c) +} + +type setDefaultsCommand struct { + modelcmd.ModelCommandBase + newAPIFunc func() (setModelDefaultsAPI, error) + values attributes +} + +const setModelDefaultsHelpDoc = ` +A shared model configuration attribute is set so that all newly created +models use this value unless overridden. +Consult the online documentation for a list of keys and possible values. + +Examples: + + juju set-model-default logging-config='=WARNING;unit=INFO' + juju set-model-default -m mymodel ftp-proxy=http://proxy default-series=xenial + +See also: + models + model-defaults + unset-model-default +` + +func (c *setDefaultsCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "set-model-default", + Args: "= ...", + Purpose: "Sets default configuration keys on a model.", + Doc: setModelDefaultsHelpDoc, + } +} + +func (c *setDefaultsCommand) Init(args []string) (err error) { + if len(args) == 0 { + return errors.New("no key, value pairs specified") + } + + options, err := keyvalues.Parse(args, true) + if err != nil { + return err + } + + c.values = make(attributes) + for key, value := range options { + if key == "agent-version" { + return errors.New("agent-version must be set via upgrade-juju") + } + c.values[key] = value + } + for key := range c.values { + // check if the key exists in the known config + // and warn the user if the key is not defined + if _, exists := config.ConfigDefaults()[key]; !exists { + logger.Warningf("key %q is not defined in the known model configuration: possible misspelling", key) + } + } + + return nil +} + +type setModelDefaultsAPI interface { + // Close closes the api connection. + Close() error + + // SetModelDefaults sets the default config values to use + // when creating new models. + SetModelDefaults(config map[string]interface{}) error +} + +func (c *setDefaultsCommand) Run(ctx *cmd.Context) error { + client, err := c.newAPIFunc() + if err != nil { + return err + } + defer client.Close() + + return block.ProcessBlockedError(client.SetModelDefaults(c.values), block.BlockChange) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/setdefaults_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/setdefaults_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/setdefaults_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/setdefaults_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,77 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package model_test + +import ( + "github.com/juju/cmd" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/cmd/juju/model" + "github.com/juju/juju/environs/config" + "github.com/juju/juju/testing" +) + +type SetDefaultsSuite struct { + fakeEnvSuite +} + +var _ = gc.Suite(&SetDefaultsSuite{}) + +func (s *SetDefaultsSuite) run(c *gc.C, args ...string) (*cmd.Context, error) { + command := model.NewSetDefaultsCommandForTest(s.fake) + return testing.RunCommand(c, command, args...) +} + +func (s *SetDefaultsSuite) TestInitKeyArgs(c *gc.C) { + for i, test := range []struct { + args []string + errorMatch string + }{ + { + errorMatch: "no key, value pairs specified", + }, { + args: []string{"special"}, + errorMatch: `expected "key=value", got "special"`, + }, { + args: []string{"special=extra", "special=other"}, + errorMatch: `key "special" specified more than once`, + }, { + args: []string{"agent-version=2.0.0"}, + errorMatch: "agent-version must be set via upgrade-juju", + }, + } { + c.Logf("test %d", i) + setCmd := model.NewSetCommandForTest(s.fake) + err := testing.InitCommand(setCmd, test.args) + c.Check(err, gc.ErrorMatches, test.errorMatch) + } +} + +func (s *SetDefaultsSuite) TestInitUnknownValue(c *gc.C) { + unsetCmd := model.NewUnsetDefaultsCommandForTest(s.fake) + err := testing.InitCommand(unsetCmd, []string{"attr", "weird"}) + c.Assert(err, jc.ErrorIsNil) + expected := `key "weird" is not defined in the known model configuration: possible misspelling` + c.Check(c.GetTestLog(), jc.Contains, expected) +} + +func (s *SetDefaultsSuite) TestSet(c *gc.C) { + _, err := s.run(c, "special=extra", "attr=baz") + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.fake.defaults, jc.DeepEquals, config.ConfigValues{ + "attr": {Value: "baz", Source: "controller"}, + "attr2": {Value: "bar", Source: "controller"}, + "special": {Value: "extra", Source: "controller"}, + }) +} + +func (s *SetDefaultsSuite) TestBlockedError(c *gc.C) { + s.fake.err = common.OperationBlockedError("TestBlockedError") + _, err := s.run(c, "special=extra") + c.Assert(err, gc.Equals, cmd.ErrSilent) + // msg is logged + c.Check(c.GetTestLog(), jc.Contains, "TestBlockedError") +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/set.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/set.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/set.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/set.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,12 +4,11 @@ package model import ( - "fmt" - "strings" - "github.com/juju/cmd" + "github.com/juju/errors" "github.com/juju/utils/keyvalues" + "github.com/juju/juju/api/modelconfig" "github.com/juju/juju/cmd/juju/block" "github.com/juju/juju/cmd/modelcmd" ) @@ -34,11 +33,12 @@ Examples: juju set-model-config logging-config='=WARNING;unit=INFO' - juju set-model-config -m mymodel api-port=17071 default-series=xenial + juju set-model-config -m mymodel ftp-proxy=http://proxy default-series=xenial -See also: models - get-model-config - unset-model-config +See also: + models + get-model-config + unset-model-config ` func (c *setCommand) Info() *cmd.Info { @@ -46,13 +46,13 @@ Name: "set-model-config", Args: "= ...", Purpose: "Sets configuration keys on a model.", - Doc: strings.TrimSpace(setModelHelpDoc), + Doc: setModelHelpDoc, } } func (c *setCommand) Init(args []string) (err error) { if len(args) == 0 { - return fmt.Errorf("no key, value pairs specified") + return errors.New("no key, value pairs specified") } options, err := keyvalues.Parse(args, true) @@ -63,7 +63,7 @@ c.values = make(attributes) for key, value := range options { if key == "agent-version" { - return fmt.Errorf("agent-version must be set via upgrade-juju") + return errors.New("agent-version must be set via upgrade-juju") } c.values[key] = value } @@ -81,7 +81,11 @@ if c.api != nil { return c.api, nil } - return c.NewAPIClient() + api, err := c.NewAPIRoot() + if err != nil { + return nil, errors.Annotate(err, "opening API connection") + } + return modelconfig.NewClient(api), nil } func (c *setCommand) Run(ctx *cmd.Context) error { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/show_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/show_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/show_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/show_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -120,11 +120,11 @@ s.store.Accounts["testing"] = jujuclient.AccountDetails{ User: "admin@local", } - err := s.store.UpdateModel("testing", "mymodel", jujuclient.ModelDetails{ + err := s.store.UpdateModel("testing", "admin@local/mymodel", jujuclient.ModelDetails{ testing.ModelTag.Id(), }) c.Assert(err, jc.ErrorIsNil) - s.store.Models["testing"].CurrentModel = "mymodel" + s.store.Models["testing"].CurrentModel = "admin@local/mymodel" } func (s *ShowCommandSuite) TestShow(c *gc.C) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/unsetdefaults.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/unsetdefaults.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/unsetdefaults.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/unsetdefaults.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,96 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package model + +import ( + "github.com/juju/cmd" + "github.com/juju/errors" + + "github.com/juju/juju/api/modelconfig" + "github.com/juju/juju/cmd/juju/block" + "github.com/juju/juju/cmd/modelcmd" + "github.com/juju/juju/environs/config" +) + +// NewUnsetModelDefaultsCommand returns a command used to reset default +// model attributes used when creating a new model. +func NewUnsetModelDefaultsCommand() cmd.Command { + c := &unsetDefaultsCommand{} + c.newAPIFunc = func() (unsetModelDefaultsAPI, error) { + api, err := c.NewAPIRoot() + if err != nil { + return nil, errors.Annotate(err, "opening API connection") + } + return modelconfig.NewClient(api), nil + } + return modelcmd.Wrap(c) + +} + +type unsetDefaultsCommand struct { + modelcmd.ModelCommandBase + newAPIFunc func() (unsetModelDefaultsAPI, error) + keys []string +} + +// unsetModelDefaultsHelpDoc is multi-line since we need to use ` to denote +// commands for ease in markdown. +const unsetModelDefaultsHelpDoc = ` +A shared model configuration attribute is unset so that all newly created +models will use any Juju defined default. +Consult the online documentation for a list of keys and possible values. + +Examples: + + juju unset-model-default ftp-proxy test-mode + +See also: + set-model-config + get-model-config +` + +func (c *unsetDefaultsCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "unset-model-default", + Args: " ...", + Purpose: "Unsets default model configuration.", + Doc: unsetModelDefaultsHelpDoc, + } +} + +func (c *unsetDefaultsCommand) Init(args []string) error { + if len(args) == 0 { + return errors.New("no keys specified") + } + c.keys = args + + for _, key := range c.keys { + // check if the key exists in the known config + // and warn the user if the key is not defined + if _, exists := config.ConfigDefaults()[key]; !exists { + logger.Warningf("key %q is not defined in the known model configuration: possible misspelling", key) + } + } + + return nil +} + +type unsetModelDefaultsAPI interface { + // Close closes the api connection. + Close() error + + // UnsetModelDefaults clears the default model + // configuration values. + UnsetModelDefaults(keys ...string) error +} + +func (c *unsetDefaultsCommand) Run(ctx *cmd.Context) error { + client, err := c.newAPIFunc() + if err != nil { + return err + } + defer client.Close() + + return block.ProcessBlockedError(client.UnsetModelDefaults(c.keys...), block.BlockChange) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/unsetdefaults_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/unsetdefaults_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/unsetdefaults_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/unsetdefaults_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,60 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package model_test + +import ( + "github.com/juju/cmd" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/cmd/juju/model" + "github.com/juju/juju/environs/config" + "github.com/juju/juju/testing" +) + +type UnsetDefaultsSuite struct { + fakeEnvSuite +} + +var _ = gc.Suite(&UnsetDefaultsSuite{}) + +func (s *UnsetDefaultsSuite) run(c *gc.C, args ...string) (*cmd.Context, error) { + command := model.NewUnsetDefaultsCommandForTest(s.fake) + return testing.RunCommand(c, command, args...) +} + +func (s *UnsetDefaultsSuite) TestInitArgCount(c *gc.C) { + unsetCmd := model.NewUnsetDefaultsCommandForTest(s.fake) + // Only empty is a problem. + err := testing.InitCommand(unsetCmd, []string{}) + c.Assert(err, gc.ErrorMatches, "no keys specified") + // Everything else is fine. + err = testing.InitCommand(unsetCmd, []string{"something", "weird"}) + c.Assert(err, jc.ErrorIsNil) +} + +func (s *UnsetDefaultsSuite) TestInitUnknownValue(c *gc.C) { + unsetCmd := model.NewUnsetDefaultsCommandForTest(s.fake) + err := testing.InitCommand(unsetCmd, []string{"attr", "weird"}) + c.Assert(err, jc.ErrorIsNil) + expected := `key "weird" is not defined in the known model configuration: possible misspelling` + c.Check(c.GetTestLog(), jc.Contains, expected) +} + +func (s *UnsetDefaultsSuite) TestUnset(c *gc.C) { + _, err := s.run(c, "attr", "unknown") + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.fake.defaults, jc.DeepEquals, config.ConfigValues{ + "attr2": {Value: "bar", Source: "controller"}, + }) +} + +func (s *UnsetDefaultsSuite) TestBlockedError(c *gc.C) { + s.fake.err = common.OperationBlockedError("TestBlockedError") + _, err := s.run(c, "attr") + c.Assert(err, gc.Equals, cmd.ErrSilent) + // msg is logged + c.Check(c.GetTestLog(), jc.Contains, "TestBlockedError") +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/unset.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/unset.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/model/unset.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/model/unset.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,11 +4,10 @@ package model import ( - "fmt" - "strings" - "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/juju/api/modelconfig" "github.com/juju/juju/cmd/juju/block" "github.com/juju/juju/cmd/modelcmd" ) @@ -23,23 +22,24 @@ keys []string } -// unsetEnvHelpDoc is multi-line since we need to use ` to denote +// unsetModelHelpDoc is multi-line since we need to use ` to denote // commands for ease in markdown. -const unsetEnvHelpDoc = "" + +const unsetModelHelpDoc = "" + "A model key is reset to its default value. If it does not have such a\n" + "value defined then it is removed.\n" + "Attempting to remove a required key with no default value will result\n" + "in an error.\n" + "By default, the model is the current model.\n" + - "Model configuration key values can be viewed with `juju get-model-config`.\n" + unsetEnvHelpDocExamples + "Model configuration key values can be viewed with `juju get-model-config`.\n" + unsetModelHelpDocExamples -const unsetEnvHelpDocExamples = ` +const unsetModelHelpDocExamples = ` Examples: - juju unset-model-config api-port test-mode + juju unset-model-config ftp-proxy test-mode -See also: set-model-config - get-model-config +See also: + set-model-config + get-model-config ` func (c *unsetCommand) Info() *cmd.Info { @@ -47,13 +47,13 @@ Name: "unset-model-config", Args: " ...", Purpose: "Unsets model configuration.", - Doc: strings.TrimSpace(unsetEnvHelpDoc), + Doc: unsetModelHelpDoc, } } -func (c *unsetCommand) Init(args []string) (err error) { +func (c *unsetCommand) Init(args []string) error { if len(args) == 0 { - return fmt.Errorf("no keys specified") + return errors.New("no keys specified") } c.keys = args return nil @@ -69,7 +69,11 @@ if c.api != nil { return c.api, nil } - return c.NewAPIClient() + api, err := c.NewAPIRoot() + if err != nil { + return nil, errors.Annotate(err, "opening API connection") + } + return modelconfig.NewClient(api), nil } func (c *unsetCommand) Run(ctx *cmd.Context) error { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/status/formatted.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/status/formatted.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/status/formatted.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/status/formatted.go 2016-08-16 08:56:25.000000000 +0000 @@ -32,6 +32,7 @@ CloudRegion string `json:"region,omitempty" yaml:"region,omitempty"` Version string `json:"version" yaml:"version"` AvailableVersion string `json:"upgrade-available,omitempty" yaml:"upgrade-available,omitempty"` + Migration string `json:"migration,omitempty" yaml:"migration,omitempty"` } type machineStatus struct { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/status/formatter.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/status/formatter.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/status/formatter.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/status/formatter.go 2016-08-16 08:56:25.000000000 +0000 @@ -53,6 +53,7 @@ CloudRegion: sf.status.Model.CloudRegion, Version: sf.status.Model.Version, AvailableVersion: sf.status.Model.AvailableVersion, + Migration: sf.status.Model.Migration, }, Machines: make(map[string]machineStatus), Applications: make(map[string]applicationStatus), diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/status/output_oneline.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/status/output_oneline.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/status/output_oneline.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/status/output_oneline.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,8 +9,7 @@ "strings" "github.com/juju/errors" - - "github.com/juju/juju/cmd/juju/common" + "github.com/juju/utils" ) // FormatOneline returns a brief list of units and their subordinates. @@ -49,9 +48,9 @@ printf(&out, format, uName, u, level) } - for _, svcName := range common.SortStringsNaturally(stringKeysFromMap(fs.Applications)) { + for _, svcName := range utils.SortStringsNaturally(stringKeysFromMap(fs.Applications)) { svc := fs.Applications[svcName] - for _, uName := range common.SortStringsNaturally(stringKeysFromMap(svc.Units)) { + for _, uName := range utils.SortStringsNaturally(stringKeysFromMap(svc.Units)) { unit := svc.Units[uName] pprint(uName, unit, 0) recurseUnits(unit, 1, pprint) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/status/output_summary.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/status/output_summary.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/status/output_summary.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/status/output_summary.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,9 +11,9 @@ "text/tabwriter" "github.com/juju/errors" + "github.com/juju/utils" "github.com/juju/utils/set" - "github.com/juju/juju/cmd/juju/common" "github.com/juju/juju/status" ) @@ -54,7 +54,7 @@ p(" ") p("# APPLICATIONS:", fmt.Sprintf(" (%d)", len(fs.Applications))) - for _, svcName := range common.SortStringsNaturally(stringKeysFromMap(svcExposure)) { + for _, svcName := range utils.SortStringsNaturally(stringKeysFromMap(svcExposure)) { s := svcExposure[svcName] p(svcName, fmt.Sprintf("%d/%d\texposed", s[true], s[true]+s[false])) } @@ -122,7 +122,7 @@ } func (f *summaryFormatter) printStateToCount(m map[status.Status]int) { - for _, stateToCount := range common.SortStringsNaturally(stringKeysFromMap(m)) { + for _, stateToCount := range utils.SortStringsNaturally(stringKeysFromMap(m)) { numInStatus := m[status.Status(stateToCount)] f.delimitValuesWithTabs(stateToCount+":", fmt.Sprintf(" %d ", numInStatus)) } @@ -156,7 +156,7 @@ func (f *summaryFormatter) aggregateMachineStates(machines map[string]machineStatus) map[status.Status]int { stateToMachine := make(map[status.Status]int) - for _, name := range common.SortStringsNaturally(stringKeysFromMap(machines)) { + for _, name := range utils.SortStringsNaturally(stringKeysFromMap(machines)) { m := machines[name] f.resolveAndTrackIp(m.DNSName) @@ -171,10 +171,10 @@ func (f *summaryFormatter) aggregateServiceAndUnitStates(services map[string]applicationStatus) map[string]map[bool]int { svcExposure := make(map[string]map[bool]int) - for _, name := range common.SortStringsNaturally(stringKeysFromMap(services)) { + for _, name := range utils.SortStringsNaturally(stringKeysFromMap(services)) { s := services[name] // Grab unit states - for _, un := range common.SortStringsNaturally(stringKeysFromMap(s.Units)) { + for _, un := range utils.SortStringsNaturally(stringKeysFromMap(s.Units)) { u := s.Units[un] f.trackUnit(un, u, 0) recurseUnits(u, 1, f.trackUnit) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/status/output_tabular.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/status/output_tabular.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/status/output_tabular.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/status/output_tabular.go 2016-08-16 08:56:25.000000000 +0000 @@ -13,10 +13,10 @@ "text/tabwriter" "github.com/juju/errors" + "github.com/juju/utils" "github.com/juju/utils/set" "gopkg.in/juju/charm.v6-unstable/hooks" - "github.com/juju/juju/cmd/juju/common" "github.com/juju/juju/instance" "github.com/juju/juju/status" ) @@ -134,7 +134,7 @@ metering := false relations := newRelationFormatter() outputHeaders("APP", "VERSION", "STATUS", "EXPOSED", "ORIGIN", "CHARM", "REV", "OS") - for _, appName := range common.SortStringsNaturally(stringKeysFromMap(fs.Applications)) { + for _, appName := range utils.SortStringsNaturally(stringKeysFromMap(fs.Applications)) { app := fs.Applications[appName] version := app.Version // Don't let a long version push out the version column. @@ -193,14 +193,14 @@ u.WorkloadStatusInfo.Current, u.JujuStatusInfo.Current, u.Machine, - strings.Join(u.OpenedPorts, ","), u.PublicAddress, + strings.Join(u.OpenedPorts, ","), message, ) } - outputHeaders("UNIT", "WORKLOAD", "AGENT", "MACHINE", "PORTS", "PUBLIC-ADDRESS", "MESSAGE") - for _, name := range common.SortStringsNaturally(stringKeysFromMap(units)) { + outputHeaders("UNIT", "WORKLOAD", "AGENT", "MACHINE", "PUBLIC-ADDRESS", "PORTS", "MESSAGE") + for _, name := range utils.SortStringsNaturally(stringKeysFromMap(units)) { u := units[name] pUnit(name, u, 0) const indentationLevel = 1 @@ -209,7 +209,7 @@ if metering { outputHeaders("METER", "STATUS", "MESSAGE") - for _, name := range common.SortStringsNaturally(stringKeysFromMap(units)) { + for _, name := range utils.SortStringsNaturally(stringKeysFromMap(units)) { u := units[name] if u.MeterStatus != nil { p(name, u.MeterStatus.Color, u.MeterStatus.Message) @@ -229,7 +229,7 @@ az = *hw.AvailabilityZone } p(m.Id, m.JujuStatus.Current, m.DNSName, m.InstanceId, m.Series, az) - for _, name := range common.SortStringsNaturally(stringKeysFromMap(m.Containers)) { + for _, name := range utils.SortStringsNaturally(stringKeysFromMap(m.Containers)) { pMachine(m.Containers[name]) } } @@ -243,7 +243,7 @@ func printMachines(tw *tabwriter.Writer, machines map[string]machineStatus) { p := printHelper(tw) p("MACHINE", "STATE", "DNS", "INS-ID", "SERIES", "AZ") - for _, name := range common.SortStringsNaturally(stringKeysFromMap(machines)) { + for _, name := range utils.SortStringsNaturally(stringKeysFromMap(machines)) { printMachine(p, machines[name], "") } } @@ -259,7 +259,7 @@ az = *hw.AvailabilityZone } p(prefix+m.Id, m.JujuStatus.Current, m.DNSName, m.InstanceId, m.Series, az) - for _, name := range common.SortStringsNaturally(stringKeysFromMap(m.Containers)) { + for _, name := range utils.SortStringsNaturally(stringKeysFromMap(m.Containers)) { printMachine(p, m.Containers[name], prefix+" ") } } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/status/status_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/status/status_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/status/status_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/status/status_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -15,14 +15,17 @@ "github.com/juju/cmd" jc "github.com/juju/testing/checkers" + "github.com/juju/utils" "github.com/juju/version" gc "gopkg.in/check.v1" "gopkg.in/juju/charm.v6-unstable" + "gopkg.in/juju/names.v2" goyaml "gopkg.in/yaml.v2" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/cmd/modelcmd" "github.com/juju/juju/constraints" + "github.com/juju/juju/core/migration" "github.com/juju/juju/environs" "github.com/juju/juju/instance" "github.com/juju/juju/juju/osenv" @@ -34,6 +37,7 @@ "github.com/juju/juju/status" "github.com/juju/juju/testcharms" coretesting "github.com/juju/juju/testing" + "github.com/juju/juju/testing/factory" coreversion "github.com/juju/juju/version" ) @@ -3149,6 +3153,63 @@ } } +func (s *StatusSuite) TestMigrationInProgress(c *gc.C) { + // This test isn't part of statusTests because migrations can't be + // run on controller models. + + const hostedModelName = "hosted" + const statusText = "foo bar" + + f := factory.NewFactory(s.BackingState) + hostedSt := f.MakeModel(c, &factory.ModelParams{ + Name: hostedModelName, + }) + defer hostedSt.Close() + + mig, err := hostedSt.CreateModelMigration(state.ModelMigrationSpec{ + InitiatedBy: names.NewUserTag("admin"), + TargetInfo: migration.TargetInfo{ + ControllerTag: names.NewModelTag(utils.MustNewUUID().String()), + Addrs: []string{"1.2.3.4:5555", "4.3.2.1:6666"}, + CACert: "cert", + AuthTag: names.NewUserTag("user"), + Password: "password", + }, + }) + c.Assert(err, jc.ErrorIsNil) + err = mig.SetStatusMessage(statusText) + c.Assert(err, jc.ErrorIsNil) + + expected := M{ + "model": M{ + "name": hostedModelName, + "controller": "kontroll", + "cloud": "dummy", + "version": "1.2.3", + "migration": statusText, + }, + "machines": M{}, + "applications": M{}, + } + + for _, format := range statusFormats { + code, stdout, stderr := runStatus(c, "-m", hostedModelName, "--format", format.name) + c.Check(code, gc.Equals, 0) + c.Assert(stderr, gc.HasLen, 0, gc.Commentf("status failed: %s", stderr)) + + // Roundtrip expected through format so that types will match. + buf, err := format.marshal(expected) + c.Assert(err, jc.ErrorIsNil) + var expectedForFormat M + err = format.unmarshal(buf, &expectedForFormat) + c.Assert(err, jc.ErrorIsNil) + + var actual M + c.Assert(format.unmarshal(stdout, &actual), jc.ErrorIsNil) + c.Check(actual, jc.DeepEquals, expectedForFormat) + } +} + type fakeApiClient struct { statusReturn *params.FullStatus patternsUsed []string @@ -3388,11 +3449,11 @@ db mysql wordpress regular logging-directory wordpress logging subordinate -UNIT WORKLOAD AGENT MACHINE PORTS PUBLIC-ADDRESS MESSAGE -mysql/0 maintenance idle 2 controller-2.dns installing all the things - logging/1 error idle controller-2.dns somehow lost in all those logs -wordpress/0 active idle 1 controller-1.dns - logging/0 active idle controller-1.dns +UNIT WORKLOAD AGENT MACHINE PUBLIC-ADDRESS PORTS MESSAGE +mysql/0 maintenance idle 2 controller-2.dns installing all the things + logging/1 error idle controller-2.dns somehow lost in all those logs +wordpress/0 active idle 1 controller-1.dns + logging/0 active idle controller-1.dns MACHINE STATE DNS INS-ID SERIES AZ 0 started controller-0.dns controller-0 quantal us-east-1a @@ -3445,7 +3506,7 @@ APP VERSION STATUS EXPOSED ORIGIN CHARM REV OS foo false 0 -UNIT WORKLOAD AGENT MACHINE PORTS PUBLIC-ADDRESS MESSAGE +UNIT WORKLOAD AGENT MACHINE PUBLIC-ADDRESS PORTS MESSAGE foo/0 maintenance executing (config-changed) doing some work foo/1 maintenance executing (backup database) doing some work @@ -3537,7 +3598,7 @@ APP VERSION STATUS EXPOSED ORIGIN CHARM REV OS foo false 0 -UNIT WORKLOAD AGENT MACHINE PORTS PUBLIC-ADDRESS MESSAGE +UNIT WORKLOAD AGENT MACHINE PUBLIC-ADDRESS PORTS MESSAGE foo/0 foo/1 diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/status/utils.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/status/utils.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/status/utils.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/status/utils.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,7 +7,7 @@ "fmt" "reflect" - "github.com/juju/juju/cmd/juju/common" + "github.com/juju/utils" ) // stringKeysFromMap takes a map with keys which are strings and returns @@ -25,7 +25,7 @@ if len(u.Subordinates) == 0 { return } - for _, uName := range common.SortStringsNaturally(stringKeysFromMap(u.Subordinates)) { + for _, uName := range utils.SortStringsNaturally(stringKeysFromMap(u.Subordinates)) { unit := u.Subordinates[uName] recurseMap(uName, unit, il) recurseUnits(unit, il+1, recurseMap) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/user/add.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/user/add.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/user/add.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/user/add.go 2016-08-16 08:56:25.000000000 +0000 @@ -43,7 +43,8 @@ show-user disable-user enable-user - change-user-password`[1:] + change-user-password + remove-user`[1:] // AddUserAPI defines the usermanager API methods that the add command uses. type AddUserAPI interface { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/user/add_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/user/add_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/user/add_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/user/add_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -146,7 +146,7 @@ type mockModelApi struct{} func (m *mockModelApi) ListModels(user string) ([]base.UserModel, error) { - return []base.UserModel{{Name: "model", UUID: "modeluuid"}}, nil + return []base.UserModel{{Name: "model", UUID: "modeluuid", Owner: "current-user@local"}}, nil } func (m *mockModelApi) Close() error { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/user/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/user/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/user/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/user/export_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -15,6 +15,10 @@ *addCommand } +type RemoveCommand struct { + *removeCommand +} + type ChangePasswordCommand struct { *changePasswordCommand } @@ -38,6 +42,12 @@ return modelcmd.WrapController(c), &AddCommand{c} } +func NewRemoveCommandForTest(api RemoveUserAPI, store jujuclient.ClientStore) (cmd.Command, *RemoveCommand) { + c := &removeCommand{api: api} + c.SetClientStore(store) + return modelcmd.WrapController(c), &RemoveCommand{c} +} + func NewShowUserCommandForTest(api UserInfoAPI, store jujuclient.ClientStore) cmd.Command { cmd := &infoCommand{infoCommandBase: infoCommandBase{api: api}} cmd.SetClientStore(store) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/user/remove.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/user/remove.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/user/remove.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/user/remove.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,138 @@ +// Copyright 2012-2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. +package user + +import ( + "bufio" + "fmt" + "io" + "strings" + + "github.com/juju/cmd" + "github.com/juju/errors" + "launchpad.net/gnuflag" + + "github.com/juju/juju/cmd/juju/block" + "github.com/juju/juju/cmd/modelcmd" +) + +var removeUsageSummary = ` +Deletes a Juju user from a controller.`[1:] + +// TODO(redir): Get updated copy for add-user as that may need updates, too. +var removeUsageDetails = ` +This removes a user permanently. + +By default, the controller is the current controller. + +Examples: + juju remove-user bob + juju remove-user bob --yes + +See also: + unregister + revoke + show-user + list-users + switch-user + disable-user + enable-user + change-user-password`[1:] + +var removeUserMsg = ` +WARNING! This command will remove the user %q from the %q controller. + +Continue (y/N)? `[1:] + +// RemoveUserAPI defines the usermanager API methods that the remove command +// uses. +type RemoveUserAPI interface { + RemoveUser(username string) error + Close() error +} + +// NewRemoveCommand constructs a wrapped unexported removeCommand. +func NewRemoveCommand() cmd.Command { + return modelcmd.WrapController(&removeCommand{}) +} + +// removeCommand deletes a user from a Juju controller. +type removeCommand struct { + modelcmd.ControllerCommandBase + api RemoveUserAPI + UserName string + ConfirmDelete bool +} + +// SetFlags adds command specific flags and then returns the flagset. +func (c *removeCommand) SetFlags(f *gnuflag.FlagSet) { + c.ControllerCommandBase.SetFlags(f) + f.BoolVar(&c.ConfirmDelete, "y", false, "Confirm deletion of the user") + f.BoolVar(&c.ConfirmDelete, "yes", false, "") +} + +// Info implements Command.Info. +func (c *removeCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "remove-user", + Args: "", + Purpose: removeUsageSummary, + Doc: removeUsageDetails, + } +} + +// Init implements Command.Init. +func (c *removeCommand) Init(args []string) error { + if len(args) == 0 { + return fmt.Errorf("no username supplied") + } + c.UserName = args[0] + return cmd.CheckEmpty(args[1:]) +} + +// Run implements Command.Run. +func (c *removeCommand) Run(ctx *cmd.Context) error { + api := c.api // This is for testing. + + if api == nil { // The real McCoy. + var err error + api, err = c.NewUserManagerAPIClient() + if err != nil { + return errors.Trace(err) + } + defer api.Close() + } + + // Confirm deletion if the user didn't specify -y/--yes in the command. + if !c.ConfirmDelete { + if err := confirmDelete(ctx, c.ControllerName(), c.UserName); err != nil { + return err + } + } + + err := api.RemoveUser(c.UserName) + if err != nil { + return block.ProcessBlockedError(err, block.BlockChange) + } + + fmt.Fprintf(ctx.Stdout, "User %q removed\n", c.UserName) + + return nil +} + +func confirmDelete(ctx *cmd.Context, controller, username string) error { + // Get confirmation from the user that they want to continue + fmt.Fprintf(ctx.Stdout, removeUserMsg, username, controller) + + scanner := bufio.NewScanner(ctx.Stdin) + scanner.Scan() + err := scanner.Err() + if err != nil && err != io.EOF { + return errors.Annotate(err, "user deletion aborted") + } + answer := strings.ToLower(scanner.Text()) + if answer != "y" && answer != "yes" { + return errors.New("user deletion aborted") + } + return nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/user/remove_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/user/remove_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/juju/user/remove_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/juju/user/remove_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,88 @@ +// Copyright 2012-2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. +package user_test + +import ( + "github.com/juju/cmd" + "github.com/juju/juju/cmd/juju/user" + "github.com/juju/juju/testing" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" +) + +type RemoveUserCommandSuite struct { + BaseSuite + mockAPI *mockRemoveUserAPI +} + +var _ = gc.Suite(&RemoveUserCommandSuite{}) + +func (s *RemoveUserCommandSuite) SetUpTest(c *gc.C) { + s.BaseSuite.SetUpTest(c) + s.mockAPI = &mockRemoveUserAPI{} +} + +type mockRemoveUserAPI struct { + username string +} + +func (*mockRemoveUserAPI) Close() error { return nil } + +func (m *mockRemoveUserAPI) RemoveUser(username string) error { + m.username = username + return nil +} + +func (s *RemoveUserCommandSuite) run(c *gc.C, name string) (*cmd.Context, error) { + removeCommand, _ := user.NewRemoveCommandForTest(s.mockAPI, s.store) + return testing.RunCommand(c, removeCommand, name) +} + +func (s *RemoveUserCommandSuite) TestInit(c *gc.C) { + table := []struct { + args []string + confirm bool + errorString string + }{{ + confirm: false, + errorString: "no username supplied", + }, { + args: []string{"--yes"}, + confirm: true, + errorString: "no username supplied", + }, { + args: []string{"--yes", "jjam"}, + confirm: true, + }} + for _, test := range table { + wrappedCommand, command := user.NewRemoveCommandForTest(s.mockAPI, s.store) + err := testing.InitCommand(wrappedCommand, test.args) + c.Check(command.ConfirmDelete, jc.DeepEquals, test.confirm) + if test.errorString == "" { + c.Check(err, jc.ErrorIsNil) + } else { + c.Check(err, gc.ErrorMatches, test.errorString) + } + } +} + +func (s *RemoveUserCommandSuite) TestRemove(c *gc.C) { + username := "testing" + command, _ := user.NewRemoveCommandForTest(s.mockAPI, s.store) + _, err := testing.RunCommand(c, command, "-y", username) + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.mockAPI.username, gc.Equals, username) + +} + +func (s *RemoveUserCommandSuite) TestRemovePrompts(c *gc.C) { + username := "testing" + expected := ` +WARNING! This command will remove the user "testing" from the "testing" controller. + +Continue (y/N)? `[1:] + command, _ := user.NewRemoveCommandForTest(s.mockAPI, s.store) + ctx, _ := testing.RunCommand(c, command, username) + c.Assert(testing.Stdout(ctx), jc.DeepEquals, expected) + +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/agenttest/agent.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/agenttest/agent.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/agenttest/agent.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/agenttest/agent.go 2016-08-16 08:56:25.000000000 +0000 @@ -31,6 +31,7 @@ "github.com/juju/juju/mongo/mongotest" "github.com/juju/juju/network" "github.com/juju/juju/state" + "github.com/juju/juju/state/stateenvirons" coretesting "github.com/juju/juju/testing" coretools "github.com/juju/juju/tools" jujuversion "github.com/juju/juju/version" @@ -134,6 +135,7 @@ apiInfo := s.APIInfo(c) paths := agent.DefaultPaths paths.DataDir = s.DataDir() + paths.MetricsSpoolDir = c.MkDir() conf, err := agent.NewAgentConfig( agent.AgentConfigParams{ Paths: paths, @@ -245,7 +247,9 @@ c.Assert(err, jc.ErrorIsNil) info, ok := config.MongoInfo() c.Assert(ok, jc.IsTrue) - st, err := state.Open(config.Model(), info, mongotest.DialOpts(), environs.NewStatePolicy()) + st, err := state.Open(config.Model(), info, mongotest.DialOpts(), stateenvirons.GetNewPolicyFunc( + stateenvirons.GetNewEnvironFunc(environs.New), + )) c.Assert(err, jc.ErrorIsNil) st.Close() } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/engine/housing.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/engine/housing.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/engine/housing.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/engine/housing.go 2016-08-16 08:56:25.000000000 +0000 @@ -103,7 +103,7 @@ return nil, errors.Trace(err) } if !flag.Check() { - return nil, dependency.ErrMissing + return nil, errors.Annotatef(dependency.ErrMissing, "%q not set", name) } return inner(context) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/engine_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/engine_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/engine_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/engine_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,14 +4,19 @@ package agent import ( + "fmt" + "runtime" "sync" + "time" "github.com/juju/errors" - jc "github.com/juju/testing/checkers" "github.com/juju/utils/set" gc "gopkg.in/check.v1" + goyaml "gopkg.in/yaml.v2" + "github.com/juju/juju/cmd/jujud/agent/machine" "github.com/juju/juju/cmd/jujud/agent/model" + "github.com/juju/juju/cmd/jujud/agent/unit" coretesting "github.com/juju/juju/testing" "github.com/juju/juju/worker" "github.com/juju/juju/worker/dependency" @@ -40,6 +45,7 @@ "instance-poller", "metric-worker", "migration-fortress", + "migration-inactive-flag", "migration-master", "application-scaler", "space-importer", @@ -48,106 +54,288 @@ "storage-provisioner", "unit-assigner", } - + migratingModelWorkers = []string{ + "environ-tracker", + "migration-fortress", + "migration-inactive-flag", + "migration-master", + } // ReallyLongTimeout should be long enough for the model-tracker // tests that depend on a hosted model; its backing state is not // accessible for StartSyncs, so we generally have to wait for at // least two 5s ticks to pass, and should expect rare circumstances // to take even longer. ReallyLongWait = coretesting.LongWait * 3 + + alwaysUnitWorkers = []string{ + "agent", + "api-caller", + "api-config-watcher", + "log-sender", + "migration-fortress", + "migration-inactive-flag", + "migration-minion", + "upgrader", + } + notMigratingUnitWorkers = []string{ + "api-address-updater", + "charm-dir", + "hook-retry-strategy", + "leadership-tracker", + "logging-config-updater", + "meter-status", + "metric-collect", + "metric-sender", + "metric-spool", + "proxy-config-updater", + "uniter", + } + + alwaysMachineWorkers = []string{ + "agent", + "api-caller", + "api-config-watcher", + "log-forwarder", + "migration-fortress", + "migration-inactive-flag", + "migration-minion", + "state-config-watcher", + "termination-signal-handler", + "upgrade-check-flag", + "upgrade-check-gate", + "upgrade-steps-flag", + "upgrade-steps-gate", + "upgrader", + } + notMigratingMachineWorkers = []string{ + "api-address-updater", + "disk-manager", + // "host-key-reporter", not stable, exits when done + "log-sender", + "logging-config-updater", + "machine-action-runner", + "machiner", + "proxy-config-updater", + "reboot-executor", + "ssh-authkeys-updater", + "storage-provisioner", + "unconverted-api-workers", + "unit-agent-deployer", + } ) -// modelMatchFunc returns a func that will return whether the current -// set of workers running for the supplied model matches those supplied; -// and will log what it saw in some detail. -func modelMatchFunc(c *gc.C, tracker *modelTracker, workers []string) func(string) bool { - expect := set.NewStrings(workers...) - return func(uuid string) bool { - actual := tracker.Workers(uuid) - c.Logf("\n%s: has workers %v", uuid, actual.SortedValues()) - extras := actual.Difference(expect) - missed := expect.Difference(actual) - if len(extras) == 0 && len(missed) == 0 { - return true - } - c.Logf("%s: waiting for %v", uuid, missed.SortedValues()) - c.Logf("%s: unexpected %v", uuid, extras.SortedValues()) - return false +// Some workers only exist on certain operating systems. +func init() { + if runtime.GOOS == "linux" { + alwaysMachineWorkers = append(alwaysMachineWorkers, "introspection") + alwaysUnitWorkers = append(alwaysUnitWorkers, "introspection") } } -// newModelTracker creates a type whose Manifolds method can -// be patched over modelManifolds, and whose Workers method -// will tell you what workers are currently running stably -// within the requested model's dependency engine. -func newModelTracker(c *gc.C) *modelTracker { - return &modelTracker{ +type ModelManifoldsFunc func(config model.ManifoldsConfig) dependency.Manifolds + +func TrackModels(c *gc.C, tracker *engineTracker, inner ModelManifoldsFunc) ModelManifoldsFunc { + return func(config model.ManifoldsConfig) dependency.Manifolds { + raw := inner(config) + id := config.Agent.CurrentConfig().Model().Id() + if err := tracker.Install(raw, id); err != nil { + c.Errorf("cannot install tracker: %v", err) + } + return raw + } +} + +type MachineManifoldsFunc func(config machine.ManifoldsConfig) dependency.Manifolds + +func TrackMachines(c *gc.C, tracker *engineTracker, inner MachineManifoldsFunc) MachineManifoldsFunc { + return func(config machine.ManifoldsConfig) dependency.Manifolds { + raw := inner(config) + id := config.Agent.CurrentConfig().Tag().String() + if err := tracker.Install(raw, id); err != nil { + c.Errorf("cannot install tracker: %v", err) + } + return raw + } +} + +type UnitManifoldsFunc func(config unit.ManifoldsConfig) dependency.Manifolds + +func TrackUnits(c *gc.C, tracker *engineTracker, inner UnitManifoldsFunc) UnitManifoldsFunc { + return func(config unit.ManifoldsConfig) dependency.Manifolds { + raw := inner(config) + id := config.Agent.CurrentConfig().Tag().String() + if err := tracker.Install(raw, id); err != nil { + c.Errorf("cannot install tracker: %v", err) + } + return raw + } +} + +// NewWorkerManager takes an engineTracker, an engine manager id to +// monitor and the workers that are expected to be running and sets up +// a WorkerManager. +func NewWorkerMatcher(c *gc.C, tracker *engineTracker, id string, workers []string) *WorkerMatcher { + return &WorkerMatcher{ c: c, + tracker: tracker, + id: id, + expect: set.NewStrings(workers...), + } +} + +// WorkerMatcher monitors the workers of a single engine manager, +// using an engineTracker, for a given set of workers to be running. +type WorkerMatcher struct { + c *gc.C + tracker *engineTracker + id string + expect set.Strings + matchTime time.Time +} + +// Check returns true if the workers which are expected to be running +// (as specified in the call to NewWorkerMatcher) are running and have +// been running for a short period (i.e. some indication of stability). +func (m *WorkerMatcher) Check() bool { + if m.checkOnce() { + now := time.Now() + if m.matchTime.IsZero() { + m.matchTime = now + return false + } + // Only return that the required workers have started if they + // have been stable for a little while. + return now.Sub(m.matchTime) >= time.Second + } + // Required workers not running, reset the timestamp. + m.matchTime = time.Time{} + return false +} + +func (m *WorkerMatcher) checkOnce() bool { + actual := m.tracker.Workers(m.id) + m.c.Logf("\n%s: has workers %v", m.id, actual.SortedValues()) + extras := actual.Difference(m.expect) + missed := m.expect.Difference(actual) + if len(extras) == 0 && len(missed) == 0 { + return true + } + m.c.Logf("%s: waiting for %v", m.id, missed.SortedValues()) + m.c.Logf("%s: unexpected %v", m.id, extras.SortedValues()) + report, _ := goyaml.Marshal(m.tracker.Report(m.id)) + m.c.Logf("%s: report: \n%s\n", m.id, report) + return false +} + +// WaitMatch returns only when the match func succeeds, or it times out. +func WaitMatch(c *gc.C, match func() bool, maxWait time.Duration, sync func()) { + timeout := time.After(maxWait) + for { + if match() { + return + } + select { + case <-time.After(coretesting.ShortWait): + sync() + case <-timeout: + c.Fatalf("timed out waiting for workers") + } + } +} + +// NewEngineTracker creates a type that can Install itself into a +// Manifolds map, and expose recent snapshots of running Workers. +func NewEngineTracker() *engineTracker { + return &engineTracker{ current: make(map[string]set.Strings), + reports: make(map[string]map[string]interface{}), } } -type modelTracker struct { - c *gc.C +type engineTracker struct { mu sync.Mutex current map[string]set.Strings + reports map[string]map[string]interface{} } -func (tracker *modelTracker) Workers(model string) set.Strings { +// Workers returns the most-recently-reported set of running workers. +func (tracker *engineTracker) Workers(id string) set.Strings { tracker.mu.Lock() defer tracker.mu.Unlock() - return tracker.current[model] + return tracker.current[id] } -func (tracker *modelTracker) Manifolds(config model.ManifoldsConfig) dependency.Manifolds { +// Report returns the most-recently-reported self-report. It will +// only work if you hack up the relevant engine-starting code to +// include: +// +// manifolds["self"] = dependency.SelfManifold(engine) +// +// or otherwise inject a suitable "self" manifold. +func (tracker *engineTracker) Report(id string) map[string]interface{} { + tracker.mu.Lock() + defer tracker.mu.Unlock() + return tracker.reports[id] +} + +// Install injects a manifold named TEST-TRACKER into raw, which will +// depend on all other manifolds in raw and write currently-available +// worker information to the tracker (differentiating it from other +// tracked engines via the id param). +func (tracker *engineTracker) Install(raw dependency.Manifolds, id string) error { const trackerName = "TEST-TRACKER" - raw := model.Manifolds(config) - uuid := config.Agent.CurrentConfig().Model().Id() names := make([]string, 0, len(raw)) for name := range raw { if name == trackerName { - tracker.c.Errorf("manifold tracker used repeatedly") - return raw - } else { - names = append(names, name) + return errors.New("engine tracker installed repeatedly") } + names = append(names, name) } tracker.mu.Lock() defer tracker.mu.Unlock() - if _, exists := tracker.current[uuid]; exists { - tracker.c.Errorf("model %s started repeatedly", uuid) - return raw + if _, exists := tracker.current[id]; exists { + return errors.Errorf("manifolds for %s created repeatedly", id) } - - raw[trackerName] = tracker.manifold(uuid, names) - return raw + raw[trackerName] = dependency.Manifold{ + Inputs: append(names, "self"), + Start: tracker.startFunc(id, names), + } + return nil } -func (tracker *modelTracker) manifold(uuid string, names []string) dependency.Manifold { - return dependency.Manifold{ - Inputs: names, - Start: func(context dependency.Context) (worker.Worker, error) { - seen := set.NewStrings() - for _, name := range names { - err := context.Get(name, nil) - if errors.Cause(err) == dependency.ErrMissing { - continue - } - if tracker.c.Check(err, jc.ErrorIsNil) { - seen.Add(name) - } - } - select { - case <-context.Abort(): - // don't bother to report if it's about to change +func (tracker *engineTracker) startFunc(id string, names []string) dependency.StartFunc { + return func(context dependency.Context) (worker.Worker, error) { + + seen := set.NewStrings() + for _, name := range names { + err := context.Get(name, nil) + switch errors.Cause(err) { + case nil: + case dependency.ErrMissing: + continue default: - tracker.mu.Lock() - defer tracker.mu.Unlock() - tracker.current[uuid] = seen + name = fmt.Sprintf("%s [%v]", name, err) } - return nil, dependency.ErrMissing - }, + seen.Add(name) + } + + var report map[string]interface{} + var reporter dependency.Reporter + if err := context.Get("self", &reporter); err == nil { + report = reporter.Report() + } + + select { + case <-context.Abort(): + // don't bother to report if it's about to change + default: + tracker.mu.Lock() + defer tracker.mu.Unlock() + tracker.current[id] = seen + tracker.reports[id] = report + } + return nil, dependency.ErrMissing } } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/imagemetadataworker_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/imagemetadataworker_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/imagemetadataworker_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/imagemetadataworker_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,7 +9,6 @@ "github.com/juju/juju/api/imagemetadata" "github.com/juju/juju/environs" - "github.com/juju/juju/environs/config" "github.com/juju/juju/environs/simplestreams" "github.com/juju/juju/state" "github.com/juju/juju/worker" @@ -31,7 +30,7 @@ return worker.NewNoOpWorker() } s.PatchValue(&newMetadataUpdater, newWorker) - s.PatchValue(&newEnvirons, func(config *config.Config) (environs.Environ, error) { + s.PatchValue(&newEnvirons, func(environs.OpenParams) (environs.Environ, error) { return &dummyEnviron{}, nil }) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/machine/manifolds.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/machine/manifolds.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/machine/manifolds.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/machine/manifolds.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,13 +4,18 @@ package machine import ( + "runtime" + "time" + "github.com/juju/errors" + "github.com/juju/utils/proxy" "github.com/juju/utils/voyeur" coreagent "github.com/juju/juju/agent" "github.com/juju/juju/api" apideployer "github.com/juju/juju/api/deployer" "github.com/juju/juju/cmd/jujud/agent/engine" + "github.com/juju/juju/container/lxd" "github.com/juju/juju/state" "github.com/juju/juju/worker" "github.com/juju/juju/worker/agent" @@ -25,12 +30,14 @@ "github.com/juju/juju/worker/gate" "github.com/juju/juju/worker/hostkeyreporter" "github.com/juju/juju/worker/identityfilewriter" + "github.com/juju/juju/worker/introspection" "github.com/juju/juju/worker/logforwarder" "github.com/juju/juju/worker/logforwarder/sinks" "github.com/juju/juju/worker/logger" "github.com/juju/juju/worker/logsender" "github.com/juju/juju/worker/machineactions" "github.com/juju/juju/worker/machiner" + "github.com/juju/juju/worker/migrationflag" "github.com/juju/juju/worker/migrationminion" "github.com/juju/juju/worker/proxyupdater" "github.com/juju/juju/worker/reboot" @@ -111,7 +118,7 @@ // otherwise be restricted. NewDeployContext func(st *apideployer.State, agentConfig coreagent.Config) deployer.Context - // Clock is used by the storageprovisioner worker. + // Clock supplies timekeeping services to various workers. Clock clock.Clock } @@ -140,12 +147,23 @@ } return err } + var externalUpdateProxyFunc func(proxy.Settings) error + if runtime.GOOS == "linux" { + externalUpdateProxyFunc = lxd.ConfigureLXDProxies + } return dependency.Manifolds{ // The agent manifold references the enclosing agent, and is the // foundation stone on which most other manifolds ultimately depend. agentName: agent.Manifold(config.Agent), + // The introspection worker provides debugging information over + // an abstract domain socket - linux only (for now). + introspectionName: introspection.Manifold(introspection.ManifoldConfig{ + AgentName: agentName, + WorkerFunc: introspection.NewWorker, + }), + // The termination worker returns ErrTerminateAgent if a // termination signal is received by the process it's running // in. It has no inputs and its only output is the error it @@ -257,21 +275,38 @@ PreUpgradeSteps: config.PreUpgradeSteps, }), - // The migration minion handles the agent side aspects of model migrations. + // The migration workers collaborate to run migrations; + // and to create a mechanism for running other workers + // so they can't accidentally interfere with a migration + // in progress. Such a manifold should (1) depend on the + // migration-inactive flag, to know when to start or die; + // and (2) occupy the migration-fortress, so as to avoid + // possible interference with the minion (which will not + // take action until it's gained sole control of the + // fortress). + // + // Note that the fortress itself will not be created + // until the upgrade process is complete; this frees all + // its dependencies from upgrade concerns. migrationFortressName: ifFullyUpgraded(fortress.Manifold()), - migrationMinionName: ifFullyUpgraded(migrationminion.Manifold(migrationminion.ManifoldConfig{ + migrationInactiveFlagName: migrationflag.Manifold(migrationflag.ManifoldConfig{ + APICallerName: apiCallerName, + Check: migrationflag.IsTerminal, + NewFacade: migrationflag.NewFacade, + NewWorker: migrationflag.NewWorker, + }), + migrationMinionName: migrationminion.Manifold(migrationminion.ManifoldConfig{ AgentName: agentName, APICallerName: apiCallerName, FortressName: migrationFortressName, - - NewFacade: migrationminion.NewFacade, - NewWorker: migrationminion.NewWorker, - })), + NewFacade: migrationminion.NewFacade, + NewWorker: migrationminion.NewWorker, + }), // The serving-info-setter manifold sets grabs the state // serving info from the API connection and writes it to the // agent config. - servingInfoSetterName: ifFullyUpgraded(ServingInfoSetterManifold(ServingInfoSetterConfig{ + servingInfoSetterName: ifNotMigrating(ServingInfoSetterManifold(ServingInfoSetterConfig{ AgentName: agentName, APICallerName: apiCallerName, })), @@ -280,7 +315,7 @@ // machine agent's API connection but have not been converted // to work directly under the dependency engine. It waits for // upgrades to be finished before starting these workers. - apiWorkersName: ifFullyUpgraded(APIWorkersManifold(APIWorkersConfig{ + apiWorkersName: ifNotMigrating(APIWorkersManifold(APIWorkersConfig{ APICallerName: apiCallerName, StartAPIWorkers: config.StartAPIWorkers, })), @@ -288,7 +323,7 @@ // The reboot manifold manages a worker which will reboot the // machine when requested. It needs an API connection and // waits for upgrades to be complete. - rebootName: ifFullyUpgraded(reboot.Manifold(reboot.ManifoldConfig{ + rebootName: ifNotMigrating(reboot.Manifold(reboot.ManifoldConfig{ AgentName: agentName, APICallerName: apiCallerName, MachineLockName: coreagent.MachineLockName, @@ -299,7 +334,7 @@ // controls the messages sent via the log sender or rsyslog, // according to changes in environment config. We should only need // one of these in a consolidated agent. - loggingConfigUpdaterName: ifFullyUpgraded(logger.Manifold(logger.ManifoldConfig{ + loggingConfigUpdaterName: ifNotMigrating(logger.Manifold(logger.ManifoldConfig{ AgentName: agentName, APICallerName: apiCallerName, })), @@ -307,22 +342,24 @@ // The diskmanager worker periodically lists block devices on the // machine it runs on. This worker will be run on all Juju-managed // machines (one per machine agent). - diskManagerName: ifFullyUpgraded(diskmanager.Manifold(diskmanager.ManifoldConfig{ + diskManagerName: ifNotMigrating(diskmanager.Manifold(diskmanager.ManifoldConfig{ AgentName: agentName, APICallerName: apiCallerName, })), // The proxy config updater is a leaf worker that sets http/https/apt/etc // proxy settings. - proxyConfigUpdater: ifFullyUpgraded(proxyupdater.Manifold(proxyupdater.ManifoldConfig{ - AgentName: agentName, - APICallerName: apiCallerName, + proxyConfigUpdater: ifNotMigrating(proxyupdater.Manifold(proxyupdater.ManifoldConfig{ + AgentName: agentName, + APICallerName: apiCallerName, + WorkerFunc: proxyupdater.NewWorker, + ExternalUpdate: externalUpdateProxyFunc, })), // The api address updater is a leaf worker that rewrites agent config // as the state server addresses change. We should only need one of // these in a consolidated agent. - apiAddressUpdaterName: ifFullyUpgraded(apiaddressupdater.Manifold(apiaddressupdater.ManifoldConfig{ + apiAddressUpdaterName: ifNotMigrating(apiaddressupdater.Manifold(apiaddressupdater.ManifoldConfig{ AgentName: agentName, APICallerName: apiCallerName, })), @@ -330,7 +367,7 @@ // The machiner Worker will wait for the identified machine to become // Dying and make it Dead; or until the machine becomes Dead by other // means. - machinerName: ifFullyUpgraded(machiner.Manifold(machiner.ManifoldConfig{ + machinerName: ifNotMigrating(machiner.Manifold(machiner.ManifoldConfig{ AgentName: agentName, APICallerName: apiCallerName, })), @@ -343,7 +380,7 @@ // runs; it currently seems better to fill the buffer and send when stable, // optimising for stable controller upgrades rather than up-to-the-moment // observable normal-machine upgrades. - logSenderName: ifFullyUpgraded(logsender.Manifold(logsender.ManifoldConfig{ + logSenderName: ifNotMigrating(logsender.Manifold(logsender.ManifoldConfig{ APICallerName: apiCallerName, LogSource: config.LogSource, })), @@ -352,13 +389,13 @@ // agents, according to changes in a set of state units; and for the // final removal of its agents' units from state when they are no // longer needed. - deployerName: ifFullyUpgraded(deployer.Manifold(deployer.ManifoldConfig{ + deployerName: ifNotMigrating(deployer.Manifold(deployer.ManifoldConfig{ NewDeployContext: config.NewDeployContext, AgentName: agentName, APICallerName: apiCallerName, })), - authenticationWorkerName: ifFullyUpgraded(authenticationworker.Manifold(authenticationworker.ManifoldConfig{ + authenticationWorkerName: ifNotMigrating(authenticationworker.Manifold(authenticationworker.ManifoldConfig{ AgentName: agentName, APICallerName: apiCallerName, })), @@ -366,35 +403,39 @@ // The storageProvisioner worker manages provisioning // (deprovisioning), and attachment (detachment) of first-class // volumes and filesystems. - storageProvisionerName: ifFullyUpgraded(storageprovisioner.MachineManifold(storageprovisioner.MachineManifoldConfig{ + storageProvisionerName: ifNotMigrating(storageprovisioner.MachineManifold(storageprovisioner.MachineManifoldConfig{ AgentName: agentName, APICallerName: apiCallerName, Clock: config.Clock, })), - resumerName: ifFullyUpgraded(resumer.Manifold(resumer.ManifoldConfig{ + resumerName: ifNotMigrating(resumer.Manifold(resumer.ManifoldConfig{ AgentName: agentName, APICallerName: apiCallerName, + Clock: config.Clock, + Interval: time.Minute, + NewFacade: resumer.NewFacade, + NewWorker: resumer.NewWorker, })), - identityFileWriterName: ifFullyUpgraded(identityfilewriter.Manifold(identityfilewriter.ManifoldConfig{ + identityFileWriterName: ifNotMigrating(identityfilewriter.Manifold(identityfilewriter.ManifoldConfig{ AgentName: agentName, APICallerName: apiCallerName, })), - toolsVersionCheckerName: ifFullyUpgraded(toolsversionchecker.Manifold(toolsversionchecker.ManifoldConfig{ + toolsVersionCheckerName: ifNotMigrating(toolsversionchecker.Manifold(toolsversionchecker.ManifoldConfig{ AgentName: agentName, APICallerName: apiCallerName, })), - machineActionName: ifFullyUpgraded(machineactions.Manifold(machineactions.ManifoldConfig{ + machineActionName: ifNotMigrating(machineactions.Manifold(machineactions.ManifoldConfig{ AgentName: agentName, APICallerName: apiCallerName, NewFacade: machineactions.NewFacade, NewWorker: machineactions.NewMachineActionsWorker, })), - hostKeyReporterName: ifFullyUpgraded(hostkeyreporter.Manifold(hostkeyreporter.ManifoldConfig{ + hostKeyReporterName: ifNotMigrating(hostkeyreporter.Manifold(hostkeyreporter.ManifoldConfig{ AgentName: agentName, APICallerName: apiCallerName, RootDir: config.RootDir, @@ -419,6 +460,13 @@ }, }.Decorate +var ifNotMigrating = engine.Housing{ + Flags: []string{ + migrationInactiveFlagName, + }, + Occupy: migrationFortressName, +}.Decorate + const ( agentName = "agent" terminationName = "termination-signal-handler" @@ -426,6 +474,7 @@ stateName = "state" stateWorkersName = "unconverted-state-workers" apiCallerName = "api-caller" + apiConfigWatcherName = "api-config-watcher" upgraderName = "upgrader" upgradeStepsName = "upgrade-steps-runner" @@ -434,9 +483,11 @@ upgradeCheckGateName = "upgrade-check-gate" upgradeCheckFlagName = "upgrade-check-flag" - migrationFortressName = "migration-fortress" - migrationMinionName = "migration-minion" + migrationFortressName = "migration-fortress" + migrationInactiveFlagName = "migration-inactive-flag" + migrationMinionName = "migration-minion" + introspectionName = "introspection" servingInfoSetterName = "serving-info-setter" apiWorkersName = "unconverted-api-workers" rebootName = "reboot-executor" @@ -452,7 +503,6 @@ resumerName = "mgo-txn-resumer" identityFileWriterName = "ssh-identity-writer" toolsVersionCheckerName = "tools-version-checker" - apiConfigWatcherName = "api-config-watcher" machineActionName = "machine-action-runner" hostKeyReporterName = "host-key-reporter" logForwarderName = "log-forwarder" diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/machine/manifolds_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/machine/manifolds_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/machine/manifolds_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/machine/manifolds_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -48,6 +48,7 @@ "api-config-watcher", "disk-manager", "host-key-reporter", + "introspection", "log-forwarder", "log-sender", "logging-config-updater", @@ -56,6 +57,7 @@ "mgo-txn-resumer", "migration-fortress", "migration-minion", + "migration-inactive-flag", "proxy-config-updater", "reboot-executor", "serving-info-setter", @@ -79,15 +81,29 @@ c.Assert(keys, jc.SameContents, expectedKeys) } -func (*ManifoldsSuite) TestUpgradeGuardsUsed(c *gc.C) { +func (*ManifoldsSuite) TestUpgradesBlockMigration(c *gc.C) { + manifolds := machine.Manifolds(machine.ManifoldsConfig{}) + manifold, ok := manifolds["migration-fortress"] + c.Assert(ok, jc.IsTrue) + + checkContains(c, manifold.Inputs, "upgrade-check-flag") + checkContains(c, manifold.Inputs, "upgrade-steps-flag") +} + +func (*ManifoldsSuite) TestMigrationGuardsUsed(c *gc.C) { exempt := set.NewStrings( "agent", "api-caller", "api-config-watcher", + "introspection", + "log-forwarder", "state", "state-config-watcher", "termination-signal-handler", "unconverted-state-workers", + "migration-fortress", + "migration-inactive-flag", + "migration-minion", "upgrade-check-flag", "upgrade-check-gate", "upgrade-steps-flag", @@ -96,26 +112,22 @@ "upgrader", ) manifolds := machine.Manifolds(machine.ManifoldsConfig{}) - keys := make([]string, 0, len(manifolds)) - for key := range manifolds { - if !exempt.Contains(key) { - keys = append(keys, key) + for name, manifold := range manifolds { + c.Logf(name) + if !exempt.Contains(name) { + checkContains(c, manifold.Inputs, "migration-fortress") + checkContains(c, manifold.Inputs, "migration-inactive-flag") } } - for _, key := range keys { - c.Logf("checking %s...", key) - var sawCheck, sawSteps bool - for _, name := range manifolds[key].Inputs { - if name == "upgrade-check-flag" { - sawCheck = true - } - if name == "upgrade-steps-flag" { - sawSteps = true - } +} + +func checkContains(c *gc.C, names []string, seek string) { + for _, name := range names { + if name == seek { + return } - c.Check(sawSteps, jc.IsTrue) - c.Check(sawCheck, jc.IsTrue) } + c.Errorf("%q not found in %v", seek, names) } func (*ManifoldsSuite) TestUpgradeGates(c *gc.C) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/machine.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/machine.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/machine.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/machine.go 2016-08-16 08:56:25.000000000 +0000 @@ -12,13 +12,13 @@ "strconv" "strings" "sync" - "sync/atomic" "time" "github.com/juju/cmd" "github.com/juju/errors" apiagent "github.com/juju/juju/api/agent" apimachiner "github.com/juju/juju/api/machiner" + "github.com/juju/juju/controller" "github.com/juju/loggo" "github.com/juju/replicaset" "github.com/juju/utils" @@ -41,6 +41,7 @@ "github.com/juju/juju/api" apideployer "github.com/juju/juju/api/deployer" "github.com/juju/juju/api/metricsmanager" + apiprovisioner "github.com/juju/juju/api/provisioner" "github.com/juju/juju/apiserver" "github.com/juju/juju/apiserver/observer" "github.com/juju/juju/apiserver/params" @@ -62,6 +63,7 @@ "github.com/juju/juju/service/common" "github.com/juju/juju/state" "github.com/juju/juju/state/multiwatcher" + "github.com/juju/juju/state/stateenvirons" "github.com/juju/juju/storage/looputil" "github.com/juju/juju/upgrades" jujuversion "github.com/juju/juju/version" @@ -91,15 +93,20 @@ jujuDumpLogs = paths.MustSucceed(paths.JujuDumpLogs(series.HostSeries())) // The following are defined as variables to allow the tests to - // intercept calls to the functions. + // intercept calls to the functions. In every case, they should + // be expressed as explicit dependencies, but nobody has yet had + // the intestinal fortitude to untangle this package. Be that + // person! Juju Needs You. useMultipleCPUs = utils.UseMultipleCPUs - modelManifolds = model.Manifolds newSingularRunner = singular.New peergrouperNew = peergrouper.New newCertificateUpdater = certupdater.NewCertificateUpdater newMetadataUpdater = imagemetadataworker.NewWorker newUpgradeMongoWorker = mongoupgrader.New reportOpenedState = func(*state.State) {} + + modelManifolds = model.Manifolds + machineManifolds = machine.Manifolds ) // Variable to override in tests, default is true @@ -441,7 +448,7 @@ if err != nil { return nil, err } - manifolds := machine.Manifolds(machine.ManifoldsConfig{ + manifolds := machineManifolds(machine.ManifoldsConfig{ PreviousAgentVersion: previousAgentVersion, Agent: agent.APIHostPortsSetter{Agent: a}, RootDir: a.rootDir, @@ -737,7 +744,12 @@ if !ok { return nil, errors.New("no state info available") } - st, err := state.Open(agentConfig.Model(), info, mongo.DefaultDialOpts(), environs.NewStatePolicy()) + st, err := state.Open( + agentConfig.Model(), info, mongo.DefaultDialOpts(), + stateenvirons.GetNewPolicyFunc( + stateenvirons.GetNewEnvironFunc(environs.New), + ), + ) if err != nil { return nil, errors.Trace(err) } @@ -774,7 +786,7 @@ containers []instance.ContainerType, agentConfig agent.Config, ) error { - pr := st.Provisioner() + pr := apiprovisioner.NewState(st) tag := agentConfig.Tag().(names.MachineTag) machine, err := pr.Machine(tag) if errors.IsNotFound(err) || err == nil && machine.Life() == params.Dead { @@ -793,37 +805,11 @@ return errors.Annotatef(err, "setting supported containers for %s", tag) } // Start the watcher to fire when a container is first requested on the machine. - modelUUID, err := st.ModelTag() - if err != nil { - return err - } watcherName := fmt.Sprintf("%s-container-watcher", machine.Id()) - // There may not be a CA certificate private key available, and without - // it we can't ensure that other Juju nodes can connect securely, so only - // use an image URL getter if there's a private key. - var imageURLGetter container.ImageURLGetter - if agentConfig.Value(agent.AllowsSecureConnection) == "true" { - cfg, err := pr.ModelConfig() - if err != nil { - return errors.Annotate(err, "unable to get environ config") - } - imageURLGetter = container.NewImageURLGetter( - // Explicitly call the non-named constructor so if anyone - // adds additional fields, this fails. - container.ImageURLGetterConfig{ - ServerRoot: st.Addr(), - ModelUUID: modelUUID.Id(), - CACert: []byte(agentConfig.CACert()), - CloudimgBaseUrl: cfg.CloudImageBaseURL(), - Stream: cfg.ImageStream(), - ImageDownloadFunc: container.ImageDownloadURL, - }) - } params := provisioner.ContainerSetupParams{ Runner: runner, WorkerName: watcherName, SupportedContainers: containers, - ImageURLGetter: imageURLGetter, Machine: machine, Provisioner: pr, Config: agentConfig, @@ -892,7 +878,12 @@ return w, nil }) a.startWorkerAfterUpgrade(runner, "peergrouper", func() (worker.Worker, error) { - w, err := peergrouperNew(st) + env, err := stateenvirons.GetNewEnvironFunc(environs.New)(st) + if err != nil { + return nil, errors.Annotate(err, "getting environ from state") + } + supportsSpaces := environs.SupportsSpaces(env) + w, err := peergrouperNew(st, supportsSpaces) if err != nil { return nil, errors.Annotate(err, "cannot start peergrouper worker") } @@ -995,6 +986,7 @@ StatusHistoryPrunerMaxHistoryMB: 5120, // 5G StatusHistoryPrunerInterval: 5 * time.Minute, SpacesImportedGate: a.discoverSpacesComplete, + NewEnvironFunc: newEnvirons, }) if err := dependency.Install(engine, manifolds); err != nil { if err := worker.Stop(engine); err != nil { @@ -1057,6 +1049,11 @@ logger.Criticalf("%v", err) } + controllerConfig, err := st.ControllerConfig() + if err != nil { + return nil, errors.Annotate(err, "cannot fetch the controller config") + } + server, err := apiserver.NewServer(st, listener, apiserver.ServerConfig{ Cert: cert, Key: key, @@ -1066,6 +1063,7 @@ Validator: a.limitLogins, CertChanged: certChanged, NewObserver: newObserverFn( + controllerConfig, clock.WallClock, jujuversion.Current, agentConfig.Model().Id(), @@ -1105,31 +1103,40 @@ } func newObserverFn( + controllerConfig controller.Config, clock clock.Clock, jujuServerVersion version.Number, modelUUID string, persistAuditEntry audit.AuditEntrySinkFn, auditErrorHandler observer.ErrorHandler, ) observer.ObserverFactory { - var connectionID int64 - return observer.ObserverFactoryMultiplexer( - func() observer.Observer { - logger := loggo.GetLogger("juju.apiserver") - ctx := observer.RequestObserverContext{ - Clock: clock, - Logger: logger, - } - return observer.NewRequestObserver(ctx, atomic.AddInt64(&connectionID, 1)) - }, - func() observer.Observer { + + var observerFactories []observer.ObserverFactory + + // Common logging of RPC requests + observerFactories = append(observerFactories, func() observer.Observer { + logger := loggo.GetLogger("juju.apiserver") + ctx := observer.RequestObserverContext{ + Clock: clock, + Logger: logger, + } + return observer.NewRequestObserver(ctx) + }) + + // Auditing observer + // TODO(katco): Auditing needs feature tests (lp:1604551) + if controllerConfig.AuditingEnabled() { + observerFactories = append(observerFactories, func() observer.Observer { ctx := &observer.AuditContext{ JujuServerVersion: jujuServerVersion, ModelUUID: modelUUID, } - // TODO(katco): Pass in an error channel return observer.NewAudit(ctx, persistAuditEntry, auditErrorHandler) - }, - ) + }) + } + + return observer.ObserverFactoryMultiplexer(observerFactories...) + } // limitLogins is called by the API server for each login attempt. @@ -1263,7 +1270,11 @@ if !ok { return nil, nil, fmt.Errorf("no state info available") } - st, err := state.Open(agentConfig.Model(), info, dialOpts, environs.NewStatePolicy()) + st, err := state.Open(agentConfig.Model(), info, dialOpts, + stateenvirons.GetNewPolicyFunc( + stateenvirons.GetNewEnvironFunc(environs.New), + ), + ) if err != nil { return nil, nil, err } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/machine_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/machine_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/machine_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/machine_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -35,6 +35,8 @@ apimachiner "github.com/juju/juju/api/machiner" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/cert" + "github.com/juju/juju/core/migration" + "github.com/juju/juju/environs" envtesting "github.com/juju/juju/environs/testing" "github.com/juju/juju/instance" "github.com/juju/juju/juju" @@ -54,7 +56,6 @@ "github.com/juju/juju/worker/instancepoller" "github.com/juju/juju/worker/machiner" "github.com/juju/juju/worker/mongoupgrader" - "github.com/juju/juju/worker/resumer" "github.com/juju/juju/worker/storageprovisioner" "github.com/juju/juju/worker/upgrader" "github.com/juju/juju/worker/workertest" @@ -359,22 +360,6 @@ c.Logf("test agent stopped successfully.") } -func (s *MachineSuite) TestManageModelRunsResumer(c *gc.C) { - started := newSignal() - s.AgentSuite.PatchValue(&resumer.NewResumer, func(st resumer.TransactionResumer) worker.Worker { - started.trigger() - return newDummyWorker() - }) - - m, _, _ := s.primeAgent(c, state.JobManageModel) - a := s.newAgent(c, m) - defer a.Stop() - go func() { - c.Check(a.Run(nil), jc.ErrorIsNil) - }() - started.assertTriggered(c, "resumer worker to start") -} - func (s *MachineSuite) TestManageModelRunsInstancePoller(c *gc.C) { s.AgentSuite.PatchValue(&instancepoller.ShortPoll, 500*time.Millisecond) usefulVersion := version.Binary{ @@ -428,7 +413,7 @@ func (s *MachineSuite) TestManageModelRunsPeergrouper(c *gc.C) { started := newSignal() - s.AgentSuite.PatchValue(&peergrouperNew, func(st *state.State) (worker.Worker, error) { + s.AgentSuite.PatchValue(&peergrouperNew, func(st *state.State, _ bool) (worker.Worker, error) { c.Check(st, gc.NotNil) started.trigger() return newDummyWorker(), nil @@ -1241,64 +1226,94 @@ c.Assert(a.IsRestoreRunning(), jc.IsFalse) } -func (s *MachineSuite) TestControllerModelWorkers(c *gc.C) { - tracker := newModelTracker(c) - check := modelMatchFunc(c, tracker, append( - alwaysModelWorkers, aliveModelWorkers..., - )) - s.PatchValue(&modelManifolds, tracker.Manifolds) +func (s *MachineSuite) TestMachineWorkers(c *gc.C) { + tracker := NewEngineTracker() + instrumented := TrackMachines(c, tracker, machineManifolds) + s.PatchValue(&machineManifolds, instrumented) + + m, _, _ := s.primeAgent(c, state.JobHostUnits) + a := s.newAgent(c, m) + go func() { c.Check(a.Run(nil), jc.ErrorIsNil) }() + defer func() { c.Check(a.Stop(), jc.ErrorIsNil) }() + + // Wait for it to stabilise, running as normal. + matcher := NewWorkerMatcher(c, tracker, a.Tag().String(), + append(alwaysMachineWorkers, notMigratingMachineWorkers...)) + WaitMatch(c, matcher.Check, coretesting.LongWait, s.BackingState.StartSync) +} +func (s *MachineSuite) TestControllerModelWorkers(c *gc.C) { uuid := s.BackingState.ModelUUID() - timeout := time.After(coretesting.LongWait) - s.assertJobWithState(c, state.JobManageModel, func(_ agent.Config, _ *state.State) { - for { - if check(uuid) { - break - } - select { - case <-time.After(coretesting.ShortWait): - s.BackingState.StartSync() - case <-timeout: - c.Fatalf("timed out waiting for workers") - } - } + tracker := NewEngineTracker() + instrumented := TrackModels(c, tracker, modelManifolds) + s.PatchValue(&modelManifolds, instrumented) + + matcher := NewWorkerMatcher(c, tracker, uuid, + append(alwaysModelWorkers, aliveModelWorkers...)) + s.assertJobWithState(c, state.JobManageModel, func(agent.Config, *state.State) { + WaitMatch(c, matcher.Check, coretesting.LongWait, s.BackingState.StartSync) }) } func (s *MachineSuite) TestHostedModelWorkers(c *gc.C) { - tracker := newModelTracker(c) - check := modelMatchFunc(c, tracker, append( - alwaysModelWorkers, aliveModelWorkers..., - )) - s.PatchValue(&modelManifolds, tracker.Manifolds) + // The dummy provider blows up in the face of multi-model + // scenarios so patch in a minimal environs.Environ that's good + // enough to allow the model workers to run. + s.PatchValue(&newEnvirons, func(environs.OpenParams) (environs.Environ, error) { + return &minModelWorkersEnviron{}, nil + }) st, closer := s.setUpNewModel(c) defer closer() uuid := st.ModelUUID() - timeout := time.After(ReallyLongWait) - s.assertJobWithState(c, state.JobManageModel, func(_ agent.Config, _ *state.State) { - for { - if check(uuid) { - break - } - select { - case <-time.After(coretesting.ShortWait): - s.BackingState.StartSync() - case <-timeout: - c.Fatalf("timed out waiting for workers") - } - } + tracker := NewEngineTracker() + instrumented := TrackModels(c, tracker, modelManifolds) + s.PatchValue(&modelManifolds, instrumented) + + matcher := NewWorkerMatcher(c, tracker, uuid, + append(alwaysModelWorkers, aliveModelWorkers...)) + s.assertJobWithState(c, state.JobManageModel, func(agent.Config, *state.State) { + WaitMatch(c, matcher.Check, ReallyLongWait, st.StartSync) + }) +} + +func (s *MachineSuite) TestMigratingModelWorkers(c *gc.C) { + st, closer := s.setUpNewModel(c) + defer closer() + uuid := st.ModelUUID() + + tracker := NewEngineTracker() + instrumented := TrackModels(c, tracker, modelManifolds) + s.PatchValue(&modelManifolds, instrumented) + + targetControllerTag := names.NewModelTag(utils.MustNewUUID().String()) + _, err := st.CreateModelMigration(state.ModelMigrationSpec{ + InitiatedBy: names.NewUserTag("admin"), + TargetInfo: migration.TargetInfo{ + ControllerTag: targetControllerTag, + Addrs: []string{"1.2.3.4:5555", "4.3.2.1:6666"}, + CACert: "cert", + AuthTag: names.NewUserTag("user"), + Password: "password", + }, + }) + c.Assert(err, jc.ErrorIsNil) + + matcher := NewWorkerMatcher(c, tracker, uuid, + append(alwaysModelWorkers, migratingModelWorkers...)) + s.assertJobWithState(c, state.JobManageModel, func(agent.Config, *state.State) { + WaitMatch(c, matcher.Check, ReallyLongWait, st.StartSync) }) } func (s *MachineSuite) TestDyingModelCleanedUp(c *gc.C) { st, closer := s.setUpNewModel(c) defer closer() - timeout := time.After(ReallyLongWait) - s.assertJobWithState(c, state.JobManageModel, func(_ agent.Config, _ *state.State) { + timeout := time.After(ReallyLongWait) + s.assertJobWithState(c, state.JobManageModel, func(agent.Config, *state.State) { model, err := st.Model() c.Assert(err, jc.ErrorIsNil) watch := model.Watch() @@ -1306,8 +1321,6 @@ err = model.Destroy() c.Assert(err, jc.ErrorIsNil) - - // Wait for the model to go away. for { select { case <-watch.Changes(): @@ -1320,7 +1333,7 @@ } c.Assert(err, jc.ErrorIsNil) // guaranteed fail case <-time.After(coretesting.ShortWait): - s.BackingState.StartSync() + st.StartSync() case <-timeout: c.Fatalf("timed out waiting for workers") } @@ -1338,23 +1351,13 @@ // Then run a normal model-tracking test, just checking for // a different set of workers. - tracker := newModelTracker(c) - check := modelMatchFunc(c, tracker, alwaysModelWorkers) - s.PatchValue(&modelManifolds, tracker.Manifolds) - - timeout := time.After(coretesting.LongWait) - s.assertJobWithState(c, state.JobManageModel, func(_ agent.Config, _ *state.State) { - for { - if check(uuid) { - break - } - select { - case <-time.After(coretesting.ShortWait): - s.BackingState.StartSync() - case <-timeout: - c.Fatalf("timed out waiting for workers") - } - } + tracker := NewEngineTracker() + instrumented := TrackModels(c, tracker, modelManifolds) + s.PatchValue(&modelManifolds, instrumented) + + matcher := NewWorkerMatcher(c, tracker, uuid, alwaysModelWorkers) + s.assertJobWithState(c, state.JobManageModel, func(agent.Config, *state.State) { + WaitMatch(c, matcher.Check, coretesting.LongWait, s.BackingState.StartSync) }) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/model/manifolds.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/model/manifolds.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/model/manifolds.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/model/manifolds.go 2016-08-16 08:56:25.000000000 +0000 @@ -30,6 +30,7 @@ "github.com/juju/juju/worker/instancepoller" "github.com/juju/juju/worker/lifeflag" "github.com/juju/juju/worker/metricworker" + "github.com/juju/juju/worker/migrationflag" "github.com/juju/juju/worker/migrationmaster" "github.com/juju/juju/worker/provisioner" "github.com/juju/juju/worker/singular" @@ -83,6 +84,10 @@ // SpacesImportedGate will be unlocked when spaces are known to // have been imported. SpacesImportedGate gate.Lock + + // NewEnvironFunc is a function opens a provider "environment" + // (typically environs.New). + NewEnvironFunc environs.NewEnvironFunc } // Manifolds returns a set of interdependent dependency manifolds that will @@ -144,42 +149,63 @@ NewWorker: singular.NewWorker, }), + // The migration workers collaborate to run migrations; + // and to create a mechanism for running other workers + // so they can't accidentally interfere with a migration + // in progress. Such a manifold should (1) depend on the + // migration-inactive flag, to know when to start or die; + // and (2) occupy the migration-fortress, so as to avoid + // possible interference with the minion (which will not + // take action until it's gained sole control of the + // fortress). + // + // Note that the fortress and flag will only exist while + // the model is not dead; this frees their dependencies + // from model-lifetime concerns. migrationFortressName: ifNotDead(fortress.Manifold()), + migrationInactiveFlagName: ifNotDead(migrationflag.Manifold(migrationflag.ManifoldConfig{ + APICallerName: apiCallerName, + Check: migrationflag.IsTerminal, + NewFacade: migrationflag.NewFacade, + NewWorker: migrationflag.NewWorker, + })), migrationMasterName: ifNotDead(migrationmaster.Manifold(migrationmaster.ManifoldConfig{ + AgentName: agentName, APICallerName: apiCallerName, FortressName: migrationFortressName, - - NewFacade: migrationmaster.NewFacade, - NewWorker: migrationmaster.NewWorker, + Clock: config.Clock, + NewFacade: migrationmaster.NewFacade, + NewWorker: migrationmaster.NewWorker, })), // Everything else should be wrapped in ifResponsible, - // ifNotAlive, or ifNotDead, to ensure that only a single - // controller is administering this model at a time. + // ifNotAlive, ifNotDead, or ifNotMigrating (which also + // implies NotDead), to ensure that only a single + // controller is attempting to administer this model at + // any one time. // - // NOTE: not perfectly reliable at this stage? i.e. a worker - // that ignores its stop signal for "too long" might continue - // to take admin actions after the window of responsibility - // closes. This *is* a pre-existing problem, but demands some - // thought/care: e.g. should we make sure the apiserver also - // closes any connections that lose responsibility..? can we - // make sure all possible environ operations are either time- + // NOTE: not perfectly reliable at this stage? i.e. a + // worker that ignores its stop signal for "too long" + // might continue to take admin actions after the window + // of responsibility closes. This *is* a pre-existing + // problem, but demands some thought/care: e.g. should + // we make sure the apiserver also closes any + // connections that lose responsibility..? can we make + // sure all possible environ operations are either time- // bounded or interruptible? etc // - // On the other hand, all workers *should* be written in the - // expectation of dealing with a sucky infrastructure running - // things in parallel unexpectedly, just because the universe - // hates us and will engineer matters such that it happens - // sometimes, even when we try to avoid it. + // On the other hand, all workers *should* be written in + // the expectation of dealing with sucky infrastructure + // running things in parallel unexpectedly, just because + // the universe hates us and will engineer matters such + // that it happens sometimes, even when we try to avoid + // it. // The environ tracker could/should be used by several other // workers (firewaller, provisioners, address-cleaner?). environTrackerName: ifResponsible(environ.Manifold(environ.ManifoldConfig{ APICallerName: apiCallerName, - NewEnvironFunc: environs.New, - })), - stateCleanerName: ifResponsible(cleaner.Manifold(cleaner.ManifoldConfig{ - APICallerName: apiCallerName, + NewEnvironFunc: config.NewEnvironFunc, })), // The undertaker is currently the only ifNotAlive worker. @@ -191,8 +217,8 @@ NewWorker: undertaker.NewWorker, })), - // All the rest depend on ifNotDead. - spaceImporterName: ifNotDead(discoverspaces.Manifold(discoverspaces.ManifoldConfig{ + // All the rest depend on ifNotMigrating. + spaceImporterName: ifNotMigrating(discoverspaces.Manifold(discoverspaces.ManifoldConfig{ EnvironName: environTrackerName, APICallerName: apiCallerName, UnlockerName: spacesImportedGateName, @@ -200,34 +226,36 @@ NewFacade: discoverspaces.NewFacade, NewWorker: discoverspaces.NewWorker, })), - - computeProvisionerName: ifNotDead(provisioner.Manifold(provisioner.ManifoldConfig{ - AgentName: agentName, - APICallerName: apiCallerName, + computeProvisionerName: ifNotMigrating(provisioner.Manifold(provisioner.ManifoldConfig{ + AgentName: agentName, + APICallerName: apiCallerName, + EnvironName: environTrackerName, + NewProvisionerFunc: provisioner.NewEnvironProvisioner, })), - storageProvisionerName: ifNotDead(storageprovisioner.ModelManifold(storageprovisioner.ModelManifoldConfig{ + storageProvisionerName: ifNotMigrating(storageprovisioner.ModelManifold(storageprovisioner.ModelManifoldConfig{ APICallerName: apiCallerName, ClockName: clockName, + EnvironName: environTrackerName, Scope: modelTag, })), - firewallerName: ifNotDead(firewaller.Manifold(firewaller.ManifoldConfig{ + firewallerName: ifNotMigrating(firewaller.Manifold(firewaller.ManifoldConfig{ APICallerName: apiCallerName, })), - unitAssignerName: ifNotDead(unitassigner.Manifold(unitassigner.ManifoldConfig{ + unitAssignerName: ifNotMigrating(unitassigner.Manifold(unitassigner.ManifoldConfig{ APICallerName: apiCallerName, })), - applicationscalerName: ifNotDead(applicationscaler.Manifold(applicationscaler.ManifoldConfig{ + applicationScalerName: ifNotMigrating(applicationscaler.Manifold(applicationscaler.ManifoldConfig{ APICallerName: apiCallerName, NewFacade: applicationscaler.NewFacade, NewWorker: applicationscaler.New, })), - instancePollerName: ifNotDead(instancepoller.Manifold(instancepoller.ManifoldConfig{ - ClockName: clockName, - Delay: config.InstPollerAggregationDelay, + instancePollerName: ifNotMigrating(instancepoller.Manifold(instancepoller.ManifoldConfig{ APICallerName: apiCallerName, EnvironName: environTrackerName, + ClockName: clockName, + Delay: config.InstPollerAggregationDelay, })), - charmRevisionUpdaterName: ifNotDead(charmrevisionmanifold.Manifold(charmrevisionmanifold.ManifoldConfig{ + charmRevisionUpdaterName: ifNotMigrating(charmrevisionmanifold.Manifold(charmrevisionmanifold.ManifoldConfig{ APICallerName: apiCallerName, ClockName: clockName, Period: config.CharmRevisionUpdateInterval, @@ -235,10 +263,13 @@ NewFacade: charmrevisionmanifold.NewAPIFacade, NewWorker: charmrevision.NewWorker, })), - metricWorkerName: ifNotDead(metricworker.Manifold(metricworker.ManifoldConfig{ + metricWorkerName: ifNotMigrating(metricworker.Manifold(metricworker.ManifoldConfig{ APICallerName: apiCallerName, })), - statusHistoryPrunerName: ifNotDead(statushistorypruner.Manifold(statushistorypruner.ManifoldConfig{ + stateCleanerName: ifNotMigrating(cleaner.Manifold(cleaner.ManifoldConfig{ + APICallerName: apiCallerName, + })), + statusHistoryPrunerName: ifNotMigrating(statushistorypruner.Manifold(statushistorypruner.ManifoldConfig{ APICallerName: apiCallerName, MaxHistoryTime: config.StatusHistoryPrunerMaxHistoryTime, MaxHistoryMB: config.StatusHistoryPrunerMaxHistoryMB, @@ -285,6 +316,19 @@ notDeadFlagName, }, }.Decorate + + // ifNotMigrating wraps a manifold such that it only runs if the + // migration-inactive flag is set; and then runs workers only + // within Visits to the migration fortress. To avoid redundancy, + // it takes advantage of the fact that those migration manifolds + // themselves depend on ifNotDead, and eschews repeating those + // dependencies. + ifNotMigrating = engine.Housing{ + Flags: []string{ + migrationInactiveFlagName, + }, + Occupy: migrationFortressName, + }.Decorate ) const ( @@ -298,8 +342,9 @@ notDeadFlagName = "not-dead-flag" notAliveFlagName = "not-alive-flag" - migrationFortressName = "migration-fortress" - migrationMasterName = "migration-master" + migrationFortressName = "migration-fortress" + migrationInactiveFlagName = "migration-inactive-flag" + migrationMasterName = "migration-master" environTrackerName = "environ-tracker" undertakerName = "undertaker" @@ -308,7 +353,7 @@ storageProvisionerName = "storage-provisioner" firewallerName = "firewaller" unitAssignerName = "unit-assigner" - applicationscalerName = "application-scaler" + applicationScalerName = "application-scaler" instancePollerName = "instance-poller" charmRevisionUpdaterName = "charm-revision-updater" metricWorkerName = "metric-worker" diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/model/manifolds_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/model/manifolds_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/model/manifolds_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/model/manifolds_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -30,10 +30,11 @@ } // NOTE: if this test failed, the cmd/jujud/agent tests will // also fail. Search for 'ModelWorkers' to find affected vars. - c.Check(actual.Values(), jc.SameContents, []string{ + c.Check(actual.SortedValues(), jc.DeepEquals, []string{ "agent", "api-caller", "api-config-watcher", + "application-scaler", "charm-revision-updater", "clock", "compute-provisioner", @@ -43,10 +44,10 @@ "is-responsible-flag", "metric-worker", "migration-fortress", + "migration-inactive-flag", "migration-master", "not-alive-flag", "not-dead-flag", - "application-scaler", "space-importer", "spaces-imported-gate", "state-cleaner", @@ -57,7 +58,7 @@ }) } -func (s *ManifoldsSuite) TestResponsibleFlagDependencies(c *gc.C) { +func (s *ManifoldsSuite) TestFlagDependencies(c *gc.C) { exclusions := set.NewStrings( "agent", "api-caller", @@ -77,7 +78,10 @@ continue } inputs := set.NewStrings(manifold.Inputs...) - c.Check(inputs.Contains("is-responsible-flag"), jc.IsTrue) + if !inputs.Contains("is-responsible-flag") { + c.Check(inputs.Contains("migration-fortress"), jc.IsTrue) + c.Check(inputs.Contains("migration-inactive-flag"), jc.IsTrue) + } } } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/unit/manifolds.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/unit/manifolds.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/unit/manifolds.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/unit/manifolds.go 2016-08-16 08:56:25.000000000 +0000 @@ -12,6 +12,7 @@ coreagent "github.com/juju/juju/agent" msapi "github.com/juju/juju/api/meterstatus" + "github.com/juju/juju/cmd/jujud/agent/engine" "github.com/juju/juju/worker" "github.com/juju/juju/worker/agent" "github.com/juju/juju/worker/apiaddressupdater" @@ -19,6 +20,7 @@ "github.com/juju/juju/worker/apiconfigwatcher" "github.com/juju/juju/worker/dependency" "github.com/juju/juju/worker/fortress" + "github.com/juju/juju/worker/introspection" "github.com/juju/juju/worker/leadership" "github.com/juju/juju/worker/logger" "github.com/juju/juju/worker/logsender" @@ -26,6 +28,7 @@ "github.com/juju/juju/worker/metrics/collect" "github.com/juju/juju/worker/metrics/sender" "github.com/juju/juju/worker/metrics/spool" + "github.com/juju/juju/worker/migrationflag" "github.com/juju/juju/worker/migrationminion" "github.com/juju/juju/worker/proxyupdater" "github.com/juju/juju/worker/retrystrategy" @@ -79,6 +82,13 @@ // (Currently, that is "all manifolds", but consider a shared clock.) agentName: agent.Manifold(config.Agent), + // The introspection worker provides debugging information over + // an abstract domain socket - linux only (for now). + introspectionName: introspection.Manifold(introspection.ManifoldConfig{ + AgentName: agentName, + WorkerFunc: introspection.NewWorker, + }), + // The api-config-watcher manifold monitors the API server // addresses in the agent config and bounces when they // change. It's required as part of model migrations. @@ -119,34 +129,46 @@ APICallerName: apiCallerName, }), + // The migration workers collaborate to run migrations; + // and to create a mechanism for running other workers + // so they can't accidentally interfere with a migration + // in progress. Such a manifold should (1) depend on the + // migration-inactive flag, to know when to start or die; + // and (2) occupy the migration-fortress, so as to avoid + // possible interference with the minion (which will not + // take action until it's gained sole control of the + // fortress). migrationFortressName: fortress.Manifold(), - - // The migration minion handles the agent side aspects of model migrations. + migrationInactiveFlagName: migrationflag.Manifold(migrationflag.ManifoldConfig{ + APICallerName: apiCallerName, + Check: migrationflag.IsTerminal, + NewFacade: migrationflag.NewFacade, + NewWorker: migrationflag.NewWorker, + }), migrationMinionName: migrationminion.Manifold(migrationminion.ManifoldConfig{ AgentName: agentName, APICallerName: apiCallerName, FortressName: migrationFortressName, - - NewFacade: migrationminion.NewFacade, - NewWorker: migrationminion.NewWorker, + NewFacade: migrationminion.NewFacade, + NewWorker: migrationminion.NewWorker, }), // The logging config updater is a leaf worker that indirectly // controls the messages sent via the log sender according to // changes in environment config. We should only need one of // these in a consolidated agent. - loggingConfigUpdaterName: logger.Manifold(logger.ManifoldConfig{ + loggingConfigUpdaterName: ifNotMigrating(logger.Manifold(logger.ManifoldConfig{ AgentName: agentName, APICallerName: apiCallerName, - }), + })), // The api address updater is a leaf worker that rewrites agent config // as the controller addresses change. We should only need one of // these in a consolidated agent. - apiAddressUpdaterName: apiaddressupdater.Manifold(apiaddressupdater.ManifoldConfig{ + apiAddressUpdaterName: ifNotMigrating(apiaddressupdater.Manifold(apiaddressupdater.ManifoldConfig{ AgentName: agentName, APICallerName: apiCallerName, - }), + })), // The proxy config updater is a leaf worker that sets http/https/apt/etc // proxy settings. @@ -154,40 +176,42 @@ // code trying to run this early; if that ever helped, it was only by // coincidence. Probably we ought to be making components that might // need proxy config into explicit dependencies of the proxy updater... - proxyConfigUpdaterName: proxyupdater.Manifold(proxyupdater.ManifoldConfig{ + proxyConfigUpdaterName: ifNotMigrating(proxyupdater.Manifold(proxyupdater.ManifoldConfig{ + AgentName: agentName, APICallerName: apiCallerName, - }), + WorkerFunc: proxyupdater.NewWorker, + })), // The charmdir resource coordinates whether the charm directory is // available or not; after 'start' hook and before 'stop' hook // executes, and not during upgrades. - charmDirName: fortress.Manifold(), + charmDirName: ifNotMigrating(fortress.Manifold()), // The leadership tracker attempts to secure and retain leadership of // the unit's service, and is consulted on such matters by the // uniter. As it stannds today, we'll need one per unit in a // consolidated agent. - leadershipTrackerName: leadership.Manifold(leadership.ManifoldConfig{ + leadershipTrackerName: ifNotMigrating(leadership.Manifold(leadership.ManifoldConfig{ AgentName: agentName, APICallerName: apiCallerName, LeadershipGuarantee: config.LeadershipGuarantee, - }), + })), // HookRetryStrategy uses a retrystrategy worker to get a // retry strategy that will be used by the uniter to run its hooks. - hookRetryStrategyName: retrystrategy.Manifold(retrystrategy.ManifoldConfig{ + hookRetryStrategyName: ifNotMigrating(retrystrategy.Manifold(retrystrategy.ManifoldConfig{ AgentName: agentName, APICallerName: apiCallerName, NewFacade: retrystrategy.NewFacade, NewWorker: retrystrategy.NewRetryStrategyWorker, - }), + })), // The uniter installs charms; manages the unit's presence in its // relations; creates suboordinate units; runs all the hooks; sends // metrics; etc etc etc. We expect to break it up further in the // coming weeks, and to need one per unit in a consolidated agent // (and probably one for each component broken out). - uniterName: uniter.Manifold(uniter.ManifoldConfig{ + uniterName: ifNotMigrating(uniter.Manifold(uniter.ManifoldConfig{ AgentName: agentName, APICallerName: apiCallerName, MachineLockName: coreagent.MachineLockName, @@ -195,24 +219,24 @@ LeadershipTrackerName: leadershipTrackerName, CharmDirName: charmDirName, HookRetryStrategyName: hookRetryStrategyName, - }), + })), // TODO (mattyw) should be added to machine agent. - metricSpoolName: spool.Manifold(spool.ManifoldConfig{ + metricSpoolName: ifNotMigrating(spool.Manifold(spool.ManifoldConfig{ AgentName: agentName, - }), + })), // The metric collect worker executes the collect-metrics hook in a // restricted context that can safely run concurrently with other hooks. - metricCollectName: collect.Manifold(collect.ManifoldConfig{ + metricCollectName: ifNotMigrating(collect.Manifold(collect.ManifoldConfig{ AgentName: agentName, MetricSpoolName: metricSpoolName, CharmDirName: charmDirName, - }), + })), // The meter status worker executes the meter-status-changed hook when it detects // that the meter status has changed. - meterStatusName: meterstatus.Manifold(meterstatus.ManifoldConfig{ + meterStatusName: ifNotMigrating(meterstatus.Manifold(meterstatus.ManifoldConfig{ AgentName: agentName, APICallerName: apiCallerName, MachineLockName: coreagent.MachineLockName, @@ -221,17 +245,24 @@ NewMeterStatusAPIClient: msapi.NewClient, NewConnectedStatusWorker: meterstatus.NewConnectedStatusWorker, NewIsolatedStatusWorker: meterstatus.NewIsolatedStatusWorker, - }), + })), // The metric sender worker periodically sends accumulated metrics to the controller. - metricSenderName: sender.Manifold(sender.ManifoldConfig{ + metricSenderName: ifNotMigrating(sender.Manifold(sender.ManifoldConfig{ AgentName: agentName, APICallerName: apiCallerName, MetricSpoolName: metricSpoolName, - }), + })), } } +var ifNotMigrating = engine.Housing{ + Flags: []string{ + migrationInactiveFlagName, + }, + Occupy: migrationFortressName, +}.Decorate + const ( agentName = "agent" apiConfigWatcherName = "api-config-watcher" @@ -239,9 +270,11 @@ logSenderName = "log-sender" upgraderName = "upgrader" - migrationFortressName = "migration-fortress" - migrationMinionName = "migration-minion" + migrationFortressName = "migration-fortress" + migrationInactiveFlagName = "migration-inactive-flag" + migrationMinionName = "migration-minion" + introspectionName = "introspection" loggingConfigUpdaterName = "logging-config-updater" proxyConfigUpdaterName = "proxy-config-updater" apiAddressUpdaterName = "api-address-updater" diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/unit/manifolds_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/unit/manifolds_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/unit/manifolds_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/unit/manifolds_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,6 +5,7 @@ import ( jc "github.com/juju/testing/checkers" + "github.com/juju/utils/set" gc "gopkg.in/check.v1" "github.com/juju/juju/agent" @@ -30,21 +31,18 @@ } func (s *ManifoldsSuite) TestManifoldNames(c *gc.C) { - config := unit.ManifoldsConfig{ - Agent: nil, - LogSource: nil, - LeadershipGuarantee: 0, - } - + config := unit.ManifoldsConfig{} manifolds := unit.Manifolds(config) expectedKeys := []string{ "agent", "api-config-watcher", "api-caller", + "introspection", "log-sender", "upgrader", "migration-fortress", "migration-minion", + "migration-inactive-flag", "logging-config-updater", "proxy-config-updater", "api-address-updater", @@ -64,6 +62,39 @@ c.Assert(expectedKeys, jc.SameContents, keys) } +func (*ManifoldsSuite) TestMigrationGuards(c *gc.C) { + exempt := set.NewStrings( + "agent", + "machine-lock", + "api-config-watcher", + "api-caller", + "introspection", + "log-sender", + "upgrader", + "migration-fortress", + "migration-minion", + "migration-inactive-flag", + ) + config := unit.ManifoldsConfig{} + manifolds := unit.Manifolds(config) + for name, manifold := range manifolds { + c.Logf(name) + if !exempt.Contains(name) { + checkContains(c, manifold.Inputs, "migration-inactive-flag") + checkContains(c, manifold.Inputs, "migration-fortress") + } + } +} + +func checkContains(c *gc.C, names []string, seek string) { + for _, name := range names { + if name == seek { + return + } + } + c.Errorf("%q not present in %v", seek, names) +} + type fakeAgent struct { agent.Agent } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/unit.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/unit.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/unit.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/unit.go 2016-08-16 08:56:25.000000000 +0000 @@ -29,6 +29,9 @@ var ( agentLogger = loggo.GetLogger("juju.jujud") + + // should be an explicit dependency, can't do it cleanly yet + unitManifolds = unit.Manifolds ) // UnitAgent is a cmd.Command responsible for running a unit agent. @@ -132,7 +135,7 @@ // APIWorkers returns a dependency.Engine running the unit agent's responsibilities. func (a *UnitAgent) APIWorkers() (worker.Worker, error) { - manifolds := unit.Manifolds(unit.ManifoldsConfig{ + manifolds := unitManifolds(unit.ManifoldsConfig{ Agent: agent.APIHostPortsSetter{a}, LogSource: a.bufferedLogs, LeadershipGuarantee: 30 * time.Second, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/unit_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/unit_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/unit_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/unit_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -12,6 +12,7 @@ "time" "github.com/juju/cmd" + "github.com/juju/cmd/cmdtesting" jc "github.com/juju/testing/checkers" "github.com/juju/utils/arch" "github.com/juju/utils/series" @@ -24,11 +25,11 @@ agenttools "github.com/juju/juju/agent/tools" "github.com/juju/juju/cmd/jujud/agent/agenttest" envtesting "github.com/juju/juju/environs/testing" - jujutesting "github.com/juju/juju/juju/testing" "github.com/juju/juju/network" "github.com/juju/juju/state" "github.com/juju/juju/status" coretesting "github.com/juju/juju/testing" + "github.com/juju/juju/testing/factory" "github.com/juju/juju/tools" jujuversion "github.com/juju/juju/version" "github.com/juju/juju/worker/upgrader" @@ -64,22 +65,13 @@ // primeAgent creates a unit, and sets up the unit agent's directory. // It returns the assigned machine, new unit and the agent's configuration. func (s *UnitSuite) primeAgent(c *gc.C) (*state.Machine, *state.Unit, agent.Config, *tools.Tools) { - jujutesting.AddControllerMachine(c, s.State) - svc := s.AddTestingService(c, "wordpress", s.AddTestingCharm(c, "wordpress")) - unit, err := svc.AddUnit() - c.Assert(err, jc.ErrorIsNil) - err = unit.SetPassword(initialUnitPassword) - c.Assert(err, jc.ErrorIsNil) - // Assign the unit to a machine. - err = unit.AssignToNewMachine() - c.Assert(err, jc.ErrorIsNil) - id, err := unit.AssignedMachineId() - c.Assert(err, jc.ErrorIsNil) - machine, err := s.State.Machine(id) - c.Assert(err, jc.ErrorIsNil) - inst, md := jujutesting.AssertStartInstance(c, s.Environ, s.ControllerConfig.ControllerUUID(), id) - err = machine.SetProvisioned(inst.Id(), agent.BootstrapNonce, md) - c.Assert(err, jc.ErrorIsNil) + machine := s.Factory.MakeMachine(c, nil) + app := s.AddTestingService(c, "wordpress", s.AddTestingCharm(c, "wordpress")) + unit := s.Factory.MakeUnit(c, &factory.UnitParams{ + Application: app, + Machine: machine, + Password: initialUnitPassword, + }) conf, tools := s.PrimeAgent(c, unit.Tag(), initialUnitPassword) return machine, unit, conf, tools } @@ -403,3 +395,21 @@ c.Fatal("timed out waiting for config changed signal") } } + +func (s *UnitSuite) TestWorkers(c *gc.C) { + tracker := NewEngineTracker() + instrumented := TrackUnits(c, tracker, unitManifolds) + s.PatchValue(&unitManifolds, instrumented) + + _, unit, _, _ := s.primeAgent(c) + ctx := cmdtesting.Context(c) + a := NewUnitAgent(ctx, nil) + s.InitAgent(c, a, "--unit-name", unit.Name()) + + go func() { c.Check(a.Run(nil), gc.IsNil) }() + defer func() { c.Check(a.Stop(), gc.IsNil) }() + + matcher := NewWorkerMatcher(c, tracker, a.Tag().String(), + append(alwaysUnitWorkers, notMigratingUnitWorkers...)) + WaitMatch(c, matcher.Check, coretesting.LongWait, s.BackingState.StartSync) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/util_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/util_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/agent/util_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/agent/util_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -26,6 +26,8 @@ apideployer "github.com/juju/juju/api/deployer" "github.com/juju/juju/cmd/jujud/agent/agenttest" cmdutil "github.com/juju/juju/cmd/jujud/util" + "github.com/juju/juju/environs" + "github.com/juju/juju/environs/config" "github.com/juju/juju/instance" jujutesting "github.com/juju/juju/juju/testing" "github.com/juju/juju/mongo/mongotest" @@ -87,7 +89,7 @@ s.singularRecord = newSingularRunnerRecord() s.AgentSuite.PatchValue(&newSingularRunner, s.singularRecord.newSingularRunner) - s.AgentSuite.PatchValue(&peergrouperNew, func(st *state.State) (worker.Worker, error) { + s.AgentSuite.PatchValue(&peergrouperNew, func(*state.State, bool) (worker.Worker, error) { return newDummyWorker(), nil }) @@ -533,3 +535,26 @@ } func (FakeAgentConfig) CheckArgs([]string) error { return nil } + +// minModelWorkersEnviron implements just enough of environs.Environ +// to allow model workers to run. +type minModelWorkersEnviron struct { + environs.Environ +} + +func (e *minModelWorkersEnviron) Config() *config.Config { + attrs := coretesting.FakeConfig() + cfg, err := config.New(config.NoDefaults, attrs) + if err != nil { + panic(err) + } + return cfg +} + +func (e *minModelWorkersEnviron) SetConfig(*config.Config) error { + return nil +} + +func (e *minModelWorkersEnviron) AllInstances() ([]instance.Instance, error) { + return nil, nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/bootstrap.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/bootstrap.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/bootstrap.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/bootstrap.go 2016-08-16 08:56:25.000000000 +0000 @@ -41,7 +41,7 @@ "github.com/juju/juju/state/binarystorage" "github.com/juju/juju/state/cloudimagemetadata" "github.com/juju/juju/state/multiwatcher" - "github.com/juju/juju/storage/poolmanager" + "github.com/juju/juju/state/stateenvirons" "github.com/juju/juju/tools" jujuversion "github.com/juju/juju/version" "github.com/juju/juju/worker/peergrouper" @@ -127,7 +127,19 @@ } // Get the bootstrap machine's addresses from the provider. - env, err := environs.New(args.ControllerModelConfig) + cloudSpec, err := environs.MakeCloudSpec( + args.ControllerCloud, + args.ControllerCloudName, + args.ControllerCloudRegion, + args.ControllerCloudCredential, + ) + if err != nil { + return errors.Trace(err) + } + env, err := environs.New(environs.OpenParams{ + Cloud: cloudSpec, + Config: args.ControllerModelConfig, + }) if err != nil { return err } @@ -246,10 +258,17 @@ adminTag, agentConfig, agentbootstrap.InitializeStateParams{ - args, addrs, jobs, sharedSecret, + StateInitializationParams: args, + BootstrapMachineAddresses: addrs, + BootstrapMachineJobs: jobs, + SharedSecret: sharedSecret, + Provider: environs.Provider, + StorageProviderRegistry: stateenvirons.NewStorageProviderRegistry(env), }, dialOpts, - environs.NewStatePolicy(), + stateenvirons.GetNewPolicyFunc( + stateenvirons.GetNewEnvironFunc(environs.New), + ), ) return stateErr }) @@ -278,11 +297,6 @@ } } - // Populate the storage pools. - if err = c.populateDefaultStoragePools(st); err != nil { - return err - } - // bootstrap machine always gets the vote return m.SetHasVote(true) } @@ -339,12 +353,6 @@ return nil } -// populateDefaultStoragePools creates the default storage pools. -func (c *BootstrapCommand) populateDefaultStoragePools(st *state.State) error { - settings := state.NewStateSettings(st) - return poolmanager.AddDefaultStoragePools(settings) -} - // populateTools stores uploaded tools in provider storage // and updates the tools metadata. func (c *BootstrapCommand) populateTools(st *state.State, env environs.Environ) error { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/bootstrap_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/bootstrap_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/bootstrap_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/bootstrap_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -57,7 +57,6 @@ "github.com/juju/juju/state" "github.com/juju/juju/state/cloudimagemetadata" "github.com/juju/juju/state/multiwatcher" - "github.com/juju/juju/storage/poolmanager" "github.com/juju/juju/testing" "github.com/juju/juju/tools" jujuversion "github.com/juju/juju/version" @@ -178,7 +177,7 @@ c.Assert(err, jc.ErrorIsNil) var tw loggo.TestWriter - err = loggo.RegisterWriter("bootstrap-test", &tw, loggo.DEBUG) + err = loggo.RegisterWriter("bootstrap-test", &tw) c.Assert(err, jc.ErrorIsNil) defer loggo.RemoveWriter("bootstrap-test") @@ -205,7 +204,7 @@ c.Assert(err, jc.ErrorIsNil) var tw loggo.TestWriter - err = loggo.RegisterWriter("bootstrap-test", &tw, loggo.DEBUG) + err = loggo.RegisterWriter("bootstrap-test", &tw) c.Assert(err, jc.ErrorIsNil) defer loggo.RemoveWriter("bootstrap-test") @@ -226,7 +225,7 @@ c.Assert(err, jc.ErrorIsNil) var tw loggo.TestWriter - err = loggo.RegisterWriter("bootstrap-test", &tw, loggo.DEBUG) + err = loggo.RegisterWriter("bootstrap-test", &tw) c.Assert(err, jc.ErrorIsNil) defer loggo.RemoveWriter("bootstrap-test") @@ -242,7 +241,7 @@ c.Assert(err, jc.ErrorIsNil) var tw loggo.TestWriter - err = loggo.RegisterWriter("bootstrap-test", &tw, loggo.DEBUG) + err = loggo.RegisterWriter("bootstrap-test", &tw) c.Assert(err, jc.ErrorIsNil) defer loggo.RemoveWriter("bootstrap-test") @@ -260,7 +259,7 @@ CACert: testing.CACert, }, Password: testPassword, - }, mongotest.DialOpts(), environs.NewStatePolicy()) + }, mongotest.DialOpts(), nil) c.Assert(err, jc.ErrorIsNil) defer st.Close() @@ -369,7 +368,7 @@ CACert: testing.CACert, }, Password: testPassword, - }, mongotest.DialOpts(), environs.NewStatePolicy()) + }, mongotest.DialOpts(), nil) c.Assert(err, jc.ErrorIsNil) defer st.Close() machines, err := st.AllMachines() @@ -422,7 +421,7 @@ CACert: testing.CACert, }, Password: testPassword, - }, mongotest.DialOpts(), environs.NewStatePolicy()) + }, mongotest.DialOpts(), nil) c.Assert(err, jc.ErrorIsNil) defer st.Close() @@ -449,7 +448,7 @@ CACert: testing.CACert, }, Password: testPassword, - }, mongotest.DialOpts(), environs.NewStatePolicy()) + }, mongotest.DialOpts(), nil) c.Assert(err, jc.ErrorIsNil) defer st.Close() @@ -485,7 +484,7 @@ CACert: testing.CACert, }, Password: testPassword, - }, mongotest.DialOpts(), environs.NewStatePolicy()) + }, mongotest.DialOpts(), nil) c.Assert(err, jc.ErrorIsNil) defer st.Close() m, err := st.Machine("0") @@ -506,7 +505,7 @@ CACert: testing.CACert, }, Password: testPassword, - }, mongotest.DialOpts(), environs.NewStatePolicy()) + }, mongotest.DialOpts(), nil) c.Assert(err, jc.ErrorIsNil) defer st.Close() m, err := st.Machine("0") @@ -529,9 +528,8 @@ } // Check we can log in to mongo as admin. - // TODO(dfc) does passing nil for the admin user name make your skin crawl ? mine too. info.Tag, info.Password = nil, testPassword - st, err := state.Open(testing.ModelTag, info, mongotest.DialOpts(), environs.NewStatePolicy()) + st, err := state.Open(testing.ModelTag, info, mongotest.DialOpts(), nil) c.Assert(err, jc.ErrorIsNil) defer st.Close() @@ -558,7 +556,7 @@ stateinfo, ok := machineConf1.MongoInfo() c.Assert(ok, jc.IsTrue) - st, err = state.Open(testing.ModelTag, stateinfo, mongotest.DialOpts(), environs.NewStatePolicy()) + st, err = state.Open(testing.ModelTag, stateinfo, mongotest.DialOpts(), nil) c.Assert(err, jc.ErrorIsNil) defer st.Close() @@ -599,7 +597,7 @@ func (s *BootstrapSuite) TestInitializeStateArgs(c *gc.C) { var called int - initializeState := func(_ names.UserTag, _ agent.ConfigSetter, args agentbootstrap.InitializeStateParams, dialOpts mongo.DialOpts, _ state.Policy) (_ *state.State, _ *state.Machine, resultErr error) { + initializeState := func(_ names.UserTag, _ agent.ConfigSetter, args agentbootstrap.InitializeStateParams, dialOpts mongo.DialOpts, _ state.NewPolicyFunc) (_ *state.State, _ *state.Machine, resultErr error) { called++ c.Assert(dialOpts.Direct, jc.IsTrue) c.Assert(dialOpts.Timeout, gc.Equals, 30*time.Second) @@ -620,7 +618,7 @@ func (s *BootstrapSuite) TestInitializeStateMinSocketTimeout(c *gc.C) { var called int - initializeState := func(_ names.UserTag, _ agent.ConfigSetter, _ agentbootstrap.InitializeStateParams, dialOpts mongo.DialOpts, _ state.Policy) (_ *state.State, _ *state.Machine, resultErr error) { + initializeState := func(_ names.UserTag, _ agent.ConfigSetter, _ agentbootstrap.InitializeStateParams, dialOpts mongo.DialOpts, _ state.NewPolicyFunc) (_ *state.State, _ *state.Machine, resultErr error) { called++ c.Assert(dialOpts.Direct, jc.IsTrue) c.Assert(dialOpts.SocketTimeout, gc.Equals, 1*time.Minute) @@ -690,7 +688,7 @@ CACert: testing.CACert, }, Password: testPassword, - }, mongotest.DialOpts(), environs.NewStatePolicy()) + }, mongotest.DialOpts(), nil) c.Assert(err, jc.ErrorIsNil) defer st.Close() expectedSeries := make(set.Strings) @@ -739,7 +737,7 @@ CACert: testing.CACert, }, Password: testPassword, - }, mongotest.DialOpts(), environs.NewStatePolicy()) + }, mongotest.DialOpts(), nil) c.Assert(err, jc.ErrorIsNil) defer st.Close() @@ -799,12 +797,17 @@ c.Assert(err, jc.ErrorIsNil) controllerCfg := testing.FakeControllerConfig() controllerCfg["controller-uuid"] = cfg.UUID() - cfg, err = provider.BootstrapConfig(environs.BootstrapConfigParams{ + cfg, err = provider.PrepareConfig(environs.PrepareConfigParams{ ControllerUUID: controllerCfg.ControllerUUID(), Config: cfg, }) c.Assert(err, jc.ErrorIsNil) - env, err := provider.PrepareForBootstrap(nullContext(), cfg) + env, err := provider.Open(environs.OpenParams{ + Cloud: dummy.SampleCloudSpec(), + Config: cfg, + }) + c.Assert(err, jc.ErrorIsNil) + err = env.PrepareForBootstrap(nullContext()) c.Assert(err, jc.ErrorIsNil) s.PatchValue(&keys.JujuPublicKey, sstesting.SignedMetadataPublicKey) @@ -861,27 +864,3 @@ } return base64.StdEncoding.EncodeToString(data) } - -func (s *BootstrapSuite) TestDefaultStoragePools(c *gc.C) { - _, cmd, err := s.initBootstrapCommand(c, nil) - c.Assert(err, jc.ErrorIsNil) - err = cmd.Run(nil) - c.Assert(err, jc.ErrorIsNil) - - st, err := state.Open(testing.ModelTag, &mongo.MongoInfo{ - Info: mongo.Info{ - Addrs: []string{gitjujutesting.MgoServer.Addr()}, - CACert: testing.CACert, - }, - Password: testPassword, - }, mongotest.DialOpts(), environs.NewStatePolicy()) - c.Assert(err, jc.ErrorIsNil) - defer st.Close() - - settings := state.NewStateSettings(st) - pm := poolmanager.New(settings) - for _, p := range []string{"ebs-ssd"} { - _, err = pm.Get(p) - c.Assert(err, jc.ErrorIsNil) - } -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/dumplogs/dumplogs.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/dumplogs/dumplogs.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/dumplogs/dumplogs.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/dumplogs/dumplogs.go 2016-08-16 08:56:25.000000000 +0000 @@ -23,7 +23,6 @@ "github.com/juju/juju/agent" jujudagent "github.com/juju/juju/cmd/jujud/agent" - "github.com/juju/juju/environs" corenames "github.com/juju/juju/juju/names" "github.com/juju/juju/mongo" "github.com/juju/juju/state" @@ -109,7 +108,7 @@ return errors.New("no database connection info available (is this a controller host?)") } - st0, err := state.Open(config.Model(), info, mongo.DefaultDialOpts(), environs.NewStatePolicy()) + st0, err := state.Open(config.Model(), info, mongo.DefaultDialOpts(), nil) if err != nil { return errors.Annotate(err, "failed to connect to database") } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/main.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/main.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/main.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/main.go 2016-08-16 08:56:25.000000000 +0000 @@ -22,7 +22,6 @@ jujucmd "github.com/juju/juju/cmd" agentcmd "github.com/juju/juju/cmd/jujud/agent" "github.com/juju/juju/cmd/jujud/dumplogs" - "github.com/juju/juju/cmd/pprof" components "github.com/juju/juju/component/all" "github.com/juju/juju/juju/names" "github.com/juju/juju/juju/sockets" @@ -192,11 +191,6 @@ commandName := filepath.Base(args[0]) switch commandName { case names.Jujud: - // start pprof server and defer cleanup - pprofSocketPath := filepath.Join(os.TempDir(), pprof.Filename) - stop := pprof.Start(pprofSocketPath) - defer stop() - code, err = jujuDMain(args, ctx) case names.Jujuc: fmt.Fprint(os.Stderr, jujudDoc) @@ -219,25 +213,21 @@ } type jujudWriter struct { - target io.Writer - unitFormatter simpleFormatter - defaultFormatter loggo.DefaultFormatter + target io.Writer } -func (w *jujudWriter) Write(level loggo.Level, module, filename string, line int, timestamp time.Time, message string) { - if strings.HasPrefix(module, "unit.") { - fmt.Fprintln(w.target, w.unitFormatter.Format(level, module, timestamp, message)) +func (w *jujudWriter) Write(entry loggo.Entry) { + if strings.HasPrefix(entry.Module, "unit.") { + fmt.Fprintln(w.target, w.unitFormat(entry)) } else { - fmt.Fprintln(w.target, w.defaultFormatter.Format(level, module, filename, line, timestamp, message)) + fmt.Fprintln(w.target, loggo.DefaultFormatter(entry)) } } -type simpleFormatter struct{} - -func (*simpleFormatter) Format(level loggo.Level, module string, timestamp time.Time, message string) string { - ts := timestamp.In(time.UTC).Format("2006-01-02 15:04:05") +func (w *jujudWriter) unitFormat(entry loggo.Entry) string { + ts := entry.Timestamp.In(time.UTC).Format("2006-01-02 15:04:05") // Just show the last element of the module. - lastDot := strings.LastIndex(module, ".") - module = module[lastDot+1:] - return fmt.Sprintf("%s %s %s %s", ts, level, module, message) + lastDot := strings.LastIndex(entry.Module, ".") + module := entry.Module[lastDot+1:] + return fmt.Sprintf("%s %s %s %s", ts, entry.Level, module, entry.Message) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/reboot/reboot.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/reboot/reboot.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/reboot/reboot.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/reboot/reboot.go 2016-08-16 08:56:25.000000000 +0000 @@ -85,7 +85,7 @@ managerConfig := container.ManagerConfig{ container.ConfigModelUUID: modelUUID} cfg := container.ManagerConfig(managerConfig) - manager, err := factory.NewContainerManager(val, cfg, nil) + manager, err := factory.NewContainerManager(val, cfg) if err != nil { return nil, errors.Annotatef(err, "failed to get manager for container type %v", val) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/run.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/run.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/jujud/run.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/jujud/run.go 2016-08-16 08:56:25.000000000 +0000 @@ -181,10 +181,15 @@ Clock: clock.WallClock, Delay: 250 * time.Millisecond, } + logger.Debugf("acquire lock %q for juju-run", c.MachineLockName) releaser, err := mutex.Acquire(spec) if err != nil { return nil, errors.Trace(err) } + logger.Debugf("lock %q acquired", c.MachineLockName) + + // Defer the logging first so it is executed after the Release. LIFO. + defer logger.Debugf("release lock %q for juju-run", c.MachineLockName) defer releaser.Release() runCmd := c.appendProxyToCommands() diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/modelcmd/base.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/modelcmd/base.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/modelcmd/base.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/modelcmd/base.go 2016-08-16 08:56:25.000000000 +0000 @@ -105,13 +105,42 @@ accountDetails = &jujuclient.AccountDetails{} } } - params, err := c.NewAPIConnectionParams( + param, err := c.NewAPIConnectionParams( store, controllerName, modelName, accountDetails, ) if err != nil { return nil, errors.Trace(err) } - return juju.NewAPIConnection(params) + conn, err := juju.NewAPIConnection(param) + if modelName != "" && params.ErrCode(err) == params.CodeModelNotFound { + return nil, c.missingModelError(store, controllerName, modelName) + } + return conn, err +} + +func (c *JujuCommandBase) missingModelError(store jujuclient.ClientStore, controllerName, modelName string) error { + // First, we'll try and clean up the missing model from the local cache. + err := store.RemoveModel(controllerName, modelName) + if err != nil { + logger.Warningf("cannot remove unknown model from cache: %v", err) + } + currentModel, err := store.CurrentModel(controllerName) + if err != nil { + logger.Warningf("cannot read current model: %v", err) + } else if currentModel == modelName { + if err := store.SetCurrentModel(controllerName, ""); err != nil { + logger.Warningf("cannot reset current model: %v", err) + } + } + errorMessage := "model %q has been removed from the controller, run 'juju models' and switch to one of them." + modelInfoMessage := "\nThere are %d accessible models on controller %q." + models, err := store.AllModels(controllerName) + if err == nil { + modelInfoMessage = fmt.Sprintf(modelInfoMessage, len(models), controllerName) + } else { + modelInfoMessage = "" + } + return errors.Errorf(errorMessage+modelInfoMessage, modelName) } // NewAPIConnectionParams returns a juju.NewAPIConnectionParams with the @@ -183,7 +212,9 @@ } for _, model := range models { modelDetails := jujuclient.ModelDetails{model.UUID} - if err := store.UpdateModel(controllerName, model.Name, modelDetails); err != nil { + owner := names.NewUserTag(model.Owner) + modelName := jujuclient.JoinOwnerModelName(owner, model.Name) + if err := store.UpdateModel(controllerName, modelName, modelDetails); err != nil { return errors.Trace(err) } } @@ -313,7 +344,7 @@ // NewGetBootstrapConfigParamsFunc returns a function that, given a controller name, // returns the params needed to bootstrap a fresh copy of that controller in the given client store. -func NewGetBootstrapConfigParamsFunc(store jujuclient.ClientStore) func(string) (*jujuclient.BootstrapConfig, *environs.BootstrapConfigParams, error) { +func NewGetBootstrapConfigParamsFunc(store jujuclient.ClientStore) func(string) (*jujuclient.BootstrapConfig, *environs.PrepareConfigParams, error) { return bootstrapConfigGetter{store}.getBootstrapConfigParams } @@ -322,18 +353,18 @@ } func (g bootstrapConfigGetter) getBootstrapConfig(controllerName string) (*config.Config, error) { - _, params, err := g.getBootstrapConfigParams(controllerName) + bootstrapConfig, params, err := g.getBootstrapConfigParams(controllerName) if err != nil { return nil, errors.Trace(err) } - provider, err := environs.Provider(params.Config.Type()) + provider, err := environs.Provider(bootstrapConfig.CloudType) if err != nil { return nil, errors.Trace(err) } - return provider.BootstrapConfig(*params) + return provider.PrepareConfig(*params) } -func (g bootstrapConfigGetter) getBootstrapConfigParams(controllerName string) (*jujuclient.BootstrapConfig, *environs.BootstrapConfigParams, error) { +func (g bootstrapConfigGetter) getBootstrapConfigParams(controllerName string) (*jujuclient.BootstrapConfig, *environs.PrepareConfigParams, error) { if _, err := g.ClientStore.ControllerByName(controllerName); err != nil { return nil, nil, errors.Annotate(err, "resolving controller name") } @@ -341,10 +372,6 @@ if err != nil { return nil, nil, errors.Annotate(err, "getting bootstrap config") } - cloudType, ok := bootstrapConfig.Config["type"].(string) - if !ok { - return nil, nil, errors.NotFoundf("cloud type in bootstrap config") - } var credential *cloud.Credential if bootstrapConfig.Credential != "" { @@ -353,7 +380,7 @@ bootstrapConfig.CloudRegion, bootstrapConfig.Credential, bootstrapConfig.Cloud, - cloudType, + bootstrapConfig.CloudType, ) if err != nil { return nil, nil, errors.Trace(err) @@ -361,7 +388,8 @@ } else { // The credential was auto-detected; run auto-detection again. cloudCredential, err := DetectCredential( - bootstrapConfig.Cloud, cloudType, + bootstrapConfig.Cloud, + bootstrapConfig.CloudType, ) if err != nil { return nil, nil, errors.Trace(err) @@ -380,23 +408,20 @@ } bootstrapConfig.Config[config.UUIDKey] = controllerDetails.ControllerUUID - cfg, err := config.New(config.UseDefaults, bootstrapConfig.Config) + cfg, err := config.New(config.NoDefaults, bootstrapConfig.Config) if err != nil { return nil, nil, errors.Trace(err) } - return &jujuclient.BootstrapConfig{ - Credential: bootstrapConfig.Credential, - Cloud: bootstrapConfig.Cloud, - CloudRegion: bootstrapConfig.CloudRegion, - Config: bootstrapConfig.Config, - CloudEndpoint: bootstrapConfig.CloudEndpoint, - CloudStorageEndpoint: bootstrapConfig.CloudStorageEndpoint, - }, - &environs.BootstrapConfigParams{ - controllerDetails.ControllerUUID, - cfg, *credential, + return bootstrapConfig, &environs.PrepareConfigParams{ + controllerDetails.ControllerUUID, + environs.CloudSpec{ + bootstrapConfig.CloudType, + bootstrapConfig.Cloud, bootstrapConfig.CloudRegion, bootstrapConfig.CloudEndpoint, bootstrapConfig.CloudStorageEndpoint, - }, nil + credential, + }, + cfg, + }, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/modelcmd/base_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/modelcmd/base_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/modelcmd/base_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/modelcmd/base_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,8 +4,11 @@ package modelcmd_test import ( + "strings" + gc "gopkg.in/check.v1" + "github.com/juju/errors" "github.com/juju/juju/api" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/cmd/modelcmd" @@ -23,13 +26,19 @@ func (s *BaseCommandSuite) SetUpTest(c *gc.C) { s.FakeJujuXDGDataHomeSuite.SetUpTest(c) - //s.PatchEnvironment("JUJU_CLI_VERSION", "") s.store = jujuclienttesting.NewMemStore() s.store.CurrentControllerName = "foo" s.store.Controllers["foo"] = jujuclient.ControllerDetails{ APIEndpoints: []string{"testing.invalid:1234"}, } + s.store.Models["foo"] = &jujuclient.ControllerModels{ + Models: map[string]jujuclient.ModelDetails{ + "admin@local/badmodel": {"deadbeef"}, + "admin@local/goodmodel": {"deadbeef2"}, + }, + CurrentModel: "admin@local/badmodel", + } s.store.Accounts["foo"] = jujuclient.AccountDetails{ User: "bar@local", Password: "hunter2", } @@ -51,3 +60,27 @@ juju login bar `) } + +func (s *BaseCommandSuite) assertUnknownModel(c *gc.C, current, expectedCurrent string) { + s.store.Models["foo"].CurrentModel = current + apiOpen := func(*api.Info, api.DialOpts) (api.Connection, error) { + return nil, errors.Trace(¶ms.Error{Code: params.CodeModelNotFound, Message: "model deaddeaf not found"}) + } + cmd := modelcmd.NewModelCommandBase(s.store, "foo", "admin@local/badmodel") + cmd.SetAPIOpen(apiOpen) + conn, err := cmd.NewAPIRoot() + c.Assert(conn, gc.IsNil) + msg := strings.Replace(err.Error(), "\n", "", -1) + c.Assert(msg, gc.Equals, `model "admin@local/badmodel" has been removed from the controller, run 'juju models' and switch to one of them.There are 1 accessible models on controller "foo".`) + c.Assert(s.store.Models["foo"].Models, gc.HasLen, 1) + c.Assert(s.store.Models["foo"].Models["admin@local/goodmodel"], gc.DeepEquals, jujuclient.ModelDetails{"deadbeef2"}) + c.Assert(s.store.Models["foo"].CurrentModel, gc.Equals, expectedCurrent) +} + +func (s *BaseCommandSuite) TestUnknownModel(c *gc.C) { + s.assertUnknownModel(c, "admin@local/badmodel", "") +} + +func (s *BaseCommandSuite) TestUnknownModelNotCurrent(c *gc.C) { + s.assertUnknownModel(c, "admin@local/goodmodel", "admin@local/goodmodel") +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/modelcmd/clientstore.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/modelcmd/clientstore.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/modelcmd/clientstore.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/modelcmd/clientstore.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,79 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package modelcmd + +import ( + "github.com/juju/errors" + "gopkg.in/juju/names.v2" + + "github.com/juju/juju/jujuclient" +) + +// QualifyingClientStore wraps a jujuclient.ClientStore, modifying +// model-related methods such that they accept unqualified model +// names, and automatically qualify them with the logged-in user +// name as necessary. +type QualifyingClientStore struct { + jujuclient.ClientStore +} + +// QualifiedModelName returns a Qualified model name, given either +// an unqualified or qualified model name. If the input is a +// fully qualified name, it is returned untouched; otherwise it is +// return qualified with the logged-in user name. +func (s QualifyingClientStore) QualifiedModelName(controllerName, modelName string) (string, error) { + if !jujuclient.IsQualifiedModelName(modelName) { + details, err := s.ClientStore.AccountDetails(controllerName) + if err != nil { + return "", errors.Annotate(err, "getting account details for qualifying model name") + } + owner := names.NewUserTag(details.User) + modelName = jujuclient.JoinOwnerModelName(owner, modelName) + } else { + unqualifiedModelName, owner, err := jujuclient.SplitModelName(modelName) + if err != nil { + return "", errors.Trace(err) + } + // Make sure that the user name is canonical. + owner = names.NewUserTag(owner.Canonical()) + modelName = jujuclient.JoinOwnerModelName(owner, unqualifiedModelName) + } + return modelName, nil +} + +// Implements jujuclient.ModelGetter. +func (s QualifyingClientStore) ModelByName(controllerName, modelName string) (*jujuclient.ModelDetails, error) { + modelName, err := s.QualifiedModelName(controllerName, modelName) + if err != nil { + return nil, errors.Annotatef(err, "getting model %q", modelName) + } + return s.ClientStore.ModelByName(controllerName, modelName) +} + +// Implements jujuclient.ModelUpdater. +func (s QualifyingClientStore) UpdateModel(controllerName, modelName string, details jujuclient.ModelDetails) error { + modelName, err := s.QualifiedModelName(controllerName, modelName) + if err != nil { + return errors.Annotatef(err, "updating model %q", modelName) + } + return s.ClientStore.UpdateModel(controllerName, modelName, details) +} + +// Implements jujuclient.ModelUpdater. +func (s QualifyingClientStore) SetCurrentModel(controllerName, modelName string) error { + modelName, err := s.QualifiedModelName(controllerName, modelName) + if err != nil { + return errors.Annotatef(err, "setting current model to %q", modelName) + } + return s.ClientStore.SetCurrentModel(controllerName, modelName) +} + +// Implements jujuclient.ModelRemover. +func (s QualifyingClientStore) RemoveModel(controllerName, modelName string) error { + modelName, err := s.QualifiedModelName(controllerName, modelName) + if err != nil { + return errors.Annotatef(err, "removing model %q", modelName) + } + return s.ClientStore.RemoveModel(controllerName, modelName) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/modelcmd/controller.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/modelcmd/controller.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/modelcmd/controller.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/modelcmd/controller.go 2016-08-16 08:56:25.000000000 +0000 @@ -239,8 +239,9 @@ store := w.ClientStore() if store == nil { store = jujuclient.NewFileClientStore() - w.SetClientStore(store) } + store = QualifyingClientStore{store} + w.SetClientStore(store) if w.setFlags { if w.controllerName == "" && w.useDefaultControllerName { currentController, err := store.CurrentController() diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/modelcmd/modelcommand.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/modelcmd/modelcommand.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/modelcmd/modelcommand.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/modelcmd/modelcommand.go 2016-08-16 08:56:25.000000000 +0000 @@ -275,8 +275,9 @@ store := w.ClientStore() if store == nil { store = jujuclient.NewFileClientStore() - w.SetClientStore(store) } + store = QualifyingClientStore{store} + w.SetClientStore(store) if !w.skipFlags { if w.modelName == "" && w.useDefaultModel { // Look for the default. diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/modelcmd/modelcommand_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/modelcmd/modelcommand_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/modelcmd/modelcommand_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/modelcmd/modelcommand_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -54,34 +54,34 @@ } func (s *ModelCommandSuite) TestGetCurrentModelCurrentControllerModel(c *gc.C) { - err := s.store.UpdateModel("foo", "mymodel", jujuclient.ModelDetails{"uuid"}) + err := s.store.UpdateModel("foo", "admin@local/mymodel", jujuclient.ModelDetails{"uuid"}) c.Assert(err, jc.ErrorIsNil) - err = s.store.SetCurrentModel("foo", "mymodel") + err = s.store.SetCurrentModel("foo", "admin@local/mymodel") c.Assert(err, jc.ErrorIsNil) env, err := modelcmd.GetCurrentModel(s.store) c.Assert(err, jc.ErrorIsNil) - c.Assert(env, gc.Equals, "foo:mymodel") + c.Assert(env, gc.Equals, "foo:admin@local/mymodel") } func (s *ModelCommandSuite) TestGetCurrentModelJujuEnvSet(c *gc.C) { - os.Setenv(osenv.JujuModelEnvKey, "magic") + os.Setenv(osenv.JujuModelEnvKey, "admin@local/magic") env, err := modelcmd.GetCurrentModel(s.store) - c.Assert(env, gc.Equals, "magic") + c.Assert(env, gc.Equals, "admin@local/magic") c.Assert(err, jc.ErrorIsNil) } func (s *ModelCommandSuite) TestGetCurrentModelBothSet(c *gc.C) { - os.Setenv(osenv.JujuModelEnvKey, "magic") + os.Setenv(osenv.JujuModelEnvKey, "admin@local/magic") - err := s.store.UpdateModel("foo", "mymodel", jujuclient.ModelDetails{"uuid"}) + err := s.store.UpdateModel("foo", "admin@local/mymodel", jujuclient.ModelDetails{"uuid"}) c.Assert(err, jc.ErrorIsNil) - err = s.store.SetCurrentModel("foo", "mymodel") + err = s.store.SetCurrentModel("foo", "admin@local/mymodel") c.Assert(err, jc.ErrorIsNil) env, err := modelcmd.GetCurrentModel(s.store) c.Assert(err, jc.ErrorIsNil) - c.Assert(env, gc.Equals, "magic") + c.Assert(env, gc.Equals, "admin@local/magic") } func (s *ModelCommandSuite) TestModelCommandInitExplicit(c *gc.C) { @@ -95,11 +95,11 @@ } func (s *ModelCommandSuite) TestModelCommandInitEnvFile(c *gc.C) { - err := s.store.UpdateModel("foo", "mymodel", jujuclient.ModelDetails{"uuid"}) + err := s.store.UpdateModel("foo", "admin@local/mymodel", jujuclient.ModelDetails{"uuid"}) c.Assert(err, jc.ErrorIsNil) - err = s.store.SetCurrentModel("foo", "mymodel") + err = s.store.SetCurrentModel("foo", "admin@local/mymodel") c.Assert(err, jc.ErrorIsNil) - s.testEnsureModelName(c, "mymodel") + s.testEnsureModelName(c, "admin@local/mymodel") } func (s *ModelCommandSuite) TestBootstrapContext(c *gc.C) { @@ -191,7 +191,7 @@ s.MacaroonSuite.AddModelUser(c, testUser) s.controllerName = "my-controller" - s.modelName = "my-model" + s.modelName = testUser + "/my-model" modelTag := names.NewModelTag(s.State.ModelUUID()) apiInfo := s.APIInfo(c) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/plugins/juju-metadata/imagemetadata.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/plugins/juju-metadata/imagemetadata.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/plugins/juju-metadata/imagemetadata.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/plugins/juju-metadata/imagemetadata.go 2016-08-16 08:56:25.000000000 +0000 @@ -31,7 +31,15 @@ // NOTE(axw) this is a work-around for the TODO below. This // means that the command will only work if you've bootstrapped // the specified environment. - cfg, err := modelcmd.NewGetBootstrapConfigFunc(c.ClientStore())(c.ControllerName()) + bootstrapConfig, params, err := modelcmd.NewGetBootstrapConfigParamsFunc(c.ClientStore())(c.ControllerName()) + if err != nil { + return nil, errors.Trace(err) + } + provider, err := environs.Provider(bootstrapConfig.CloudType) + if err != nil { + return nil, errors.Trace(err) + } + cfg, err := provider.PrepareConfig(*params) if err != nil { return nil, errors.Trace(err) } @@ -41,7 +49,10 @@ // identify region and endpoint info that we need. Not sure what // we'll do about simplestreams.MetadataValidator yet. Probably // move it to the EnvironProvider interface. - return environs.New(cfg) + return environs.New(environs.OpenParams{ + Cloud: params.Cloud, + Config: cfg, + }) } func newImageMetadataCommand() cmd.Command { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/plugins/juju-metadata/signmetadata.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/plugins/juju-metadata/signmetadata.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/plugins/juju-metadata/signmetadata.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/plugins/juju-metadata/signmetadata.go 2016-08-16 08:56:25.000000000 +0000 @@ -64,7 +64,10 @@ } func (c *signMetadataCommand) Run(context *cmd.Context) error { - loggo.RegisterWriter("signmetadata", cmd.NewCommandLogWriter("juju.plugins.metadata", context.Stdout, context.Stderr), loggo.INFO) + writer := loggo.NewMinimumLevelWriter( + cmd.NewCommandLogWriter("juju.plugins.metadata", context.Stdout, context.Stderr), + loggo.INFO) + loggo.RegisterWriter("signmetadata", writer) defer loggo.RemoveWriter("signmetadata") keyData, err := ioutil.ReadFile(c.keyFile) if err != nil { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/plugins/juju-metadata/signmetadata_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/plugins/juju-metadata/signmetadata_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/plugins/juju-metadata/signmetadata_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/plugins/juju-metadata/signmetadata_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -20,18 +20,17 @@ coretesting "github.com/juju/juju/testing" ) -type SignMetadataSuite struct{} +type SignMetadataSuite struct { + coretesting.BaseSuite +} var _ = gc.Suite(&SignMetadataSuite{}) func (s *SignMetadataSuite) SetUpTest(c *gc.C) { + s.BaseSuite.SetUpTest(c) loggo.GetLogger("").SetLogLevel(loggo.INFO) } -func (s *SignMetadataSuite) TearDownTest(c *gc.C) { - loggo.ResetLoggers() -} - var expectedLoggingOutput = `signing 2 file\(s\) in .*subdir1.* signing file .*file1\.json.* signing file .*file2\.json.* diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/plugins/juju-metadata/toolsmetadata.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/plugins/juju-metadata/toolsmetadata.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/plugins/juju-metadata/toolsmetadata.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/plugins/juju-metadata/toolsmetadata.go 2016-08-16 08:56:25.000000000 +0000 @@ -95,7 +95,10 @@ } func (c *toolsMetadataCommand) Run(context *cmd.Context) error { - loggo.RegisterWriter("toolsmetadata", cmd.NewCommandLogWriter("juju.environs.tools", context.Stdout, context.Stderr), loggo.INFO) + writer := loggo.NewMinimumLevelWriter( + cmd.NewCommandLogWriter("juju.environs.tools", context.Stdout, context.Stderr), + loggo.INFO) + loggo.RegisterWriter("toolsmetadata", writer) defer loggo.RemoveWriter("toolsmetadata") if c.metadataDir == "" { c.metadataDir = osenv.JujuXDGDataHome() diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/plugins/juju-metadata/toolsmetadata_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/plugins/juju-metadata/toolsmetadata_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/plugins/juju-metadata/toolsmetadata_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/plugins/juju-metadata/toolsmetadata_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -43,9 +43,6 @@ func (s *ToolsMetadataSuite) SetUpTest(c *gc.C) { s.FakeJujuXDGDataHomeSuite.SetUpTest(c) s.AddCleanup(dummy.Reset) - s.AddCleanup(func(*gc.C) { - loggo.ResetLoggers() - }) cfg, err := config.New(config.UseDefaults, map[string]interface{}{ "name": "erewhemos", "type": "dummy", @@ -60,8 +57,8 @@ bootstrap.PrepareParams{ ControllerConfig: coretesting.FakeControllerConfig(), ControllerName: cfg.Name(), - BaseConfig: cfg.AllAttrs(), - CloudName: "dummy", + ModelConfig: cfg.AllAttrs(), + Cloud: dummy.SampleCloudSpec(), AdminSecret: "admin-secret", }, ) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/plugins/juju-metadata/validateimagemetadata_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/plugins/juju-metadata/validateimagemetadata_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/plugins/juju-metadata/validateimagemetadata_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/plugins/juju-metadata/validateimagemetadata_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -113,6 +113,7 @@ store.BootstrapConfig["ec2-controller"] = jujuclient.BootstrapConfig{ Config: ec2Config.AllAttrs(), Cloud: "ec2", + CloudType: "ec2", CloudRegion: "us-east-1", } @@ -140,6 +141,7 @@ store.BootstrapConfig["azure-controller"] = jujuclient.BootstrapConfig{ Config: azureConfig.AllAttrs(), Cloud: "azure", + CloudType: "azure", CloudRegion: "West US", CloudEndpoint: "https://management.azure.com", CloudStorageEndpoint: "https://core.windows.net", diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/pprof/pprof.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/pprof/pprof.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/pprof/pprof.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/pprof/pprof.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,226 +0,0 @@ -// Copyright 2010 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package pprof is a fork of net/http/pprof modified to communicate -// over a unix socket. -// -// Changes from the original version -// -// - This fork does not automatically register itself with the default -// net/http ServeMux. -// - To start the pprof handler, see the Start method in socket.go. -// - For compatability with Go 1.2.1, support for obtaining trace data -// has been removed. -// -// --------------------------------------------------------------- -// -// Package pprof serves via its HTTP server runtime profiling data -// in the format expected by the pprof visualization tool. -// For more information about pprof, see -// http://code.google.com/p/google-perftools/. -// -// The package is typically only imported for the side effect of -// registering its HTTP handlers. -// The handled paths all begin with /debug/pprof/. -// -// To use pprof, link this package into your program: -// import _ "net/http/pprof" -// -// If your application is not already running an http server, you -// need to start one. Add "net/http" and "log" to your imports and -// the following code to your main function: -// -// go func() { -// log.Println(http.ListenAndServe("localhost:6060", nil)) -// }() -// -// Then use the pprof tool to look at the heap profile: -// -// go tool pprof http://localhost:6060/debug/pprof/heap -// -// Or to look at a 30-second CPU profile: -// -// go tool pprof http://localhost:6060/debug/pprof/profile -// -// Or to look at the goroutine blocking profile: -// -// go tool pprof http://localhost:6060/debug/pprof/block -// -// To view all available profiles, open http://localhost:6060/debug/pprof/ -// in your browser. -// -// For a study of the facility in action, visit -// -// https://blog.golang.org/2011/06/profiling-go-programs.html -// -package pprof - -import ( - "bufio" - "bytes" - "fmt" - "html/template" - "io" - "log" - "net/http" - "os" - "runtime" - "runtime/pprof" - "strconv" - "strings" - "time" -) - -// Cmdline responds with the running program's -// command line, with arguments separated by NUL bytes. -// The package initialization registers it as /debug/pprof/cmdline. -func Cmdline(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - fmt.Fprintf(w, strings.Join(os.Args, "\x00")) -} - -func sleep(w http.ResponseWriter, d time.Duration) { - var clientGone <-chan bool - if cn, ok := w.(http.CloseNotifier); ok { - clientGone = cn.CloseNotify() - } - select { - case <-time.After(d): - case <-clientGone: - } -} - -// Profile responds with the pprof-formatted cpu profile. -// The package initialization registers it as /debug/pprof/profile. -func Profile(w http.ResponseWriter, r *http.Request) { - sec, _ := strconv.ParseInt(r.FormValue("seconds"), 10, 64) - if sec == 0 { - sec = 30 - } - - // Set Content Type assuming StartCPUProfile will work, - // because if it does it starts writing. - w.Header().Set("Content-Type", "application/octet-stream") - if err := pprof.StartCPUProfile(w); err != nil { - // StartCPUProfile failed, so no writes yet. - // Can change header back to text content - // and send error code. - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, "Could not enable CPU profiling: %s\n", err) - return - } - sleep(w, time.Duration(sec)*time.Second) - pprof.StopCPUProfile() -} - -// Symbol looks up the program counters listed in the request, -// responding with a table mapping program counters to function names. -// The package initialization registers it as /debug/pprof/symbol. -func Symbol(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - - // We have to read the whole POST body before - // writing any output. Buffer the output here. - var buf bytes.Buffer - - // We don't know how many symbols we have, but we - // do have symbol information. Pprof only cares whether - // this number is 0 (no symbols available) or > 0. - fmt.Fprintf(&buf, "num_symbols: 1\n") - - var b *bufio.Reader - if r.Method == "POST" { - b = bufio.NewReader(r.Body) - } else { - b = bufio.NewReader(strings.NewReader(r.URL.RawQuery)) - } - - for { - word, err := b.ReadSlice('+') - if err == nil { - word = word[0 : len(word)-1] // trim + - } - pc, _ := strconv.ParseUint(string(word), 0, 64) - if pc != 0 { - f := runtime.FuncForPC(uintptr(pc)) - if f != nil { - fmt.Fprintf(&buf, "%#x %s\n", pc, f.Name()) - } - } - - // Wait until here to check for err; the last - // symbol will have an err because it doesn't end in +. - if err != nil { - if err != io.EOF { - fmt.Fprintf(&buf, "reading request: %v\n", err) - } - break - } - } - - w.Write(buf.Bytes()) -} - -// Handler returns an HTTP handler that serves the named profile. -func Handler(name string) http.Handler { - return handler(name) -} - -type handler string - -func (name handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - debug, _ := strconv.Atoi(r.FormValue("debug")) - p := pprof.Lookup(string(name)) - if p == nil { - w.WriteHeader(404) - fmt.Fprintf(w, "Unknown profile: %s\n", name) - return - } - gc, _ := strconv.Atoi(r.FormValue("gc")) - if name == "heap" && gc > 0 { - runtime.GC() - } - p.WriteTo(w, debug) - return -} - -// Index responds with the pprof-formatted profile named by the request. -// For example, "/debug/pprof/heap" serves the "heap" profile. -// Index responds to a request for "/debug/pprof/" with an HTML page -// listing the available profiles. -func Index(w http.ResponseWriter, r *http.Request) { - if strings.HasPrefix(r.URL.Path, "/debug/pprof/") { - name := strings.TrimPrefix(r.URL.Path, "/debug/pprof/") - if name != "" { - handler(name).ServeHTTP(w, r) - return - } - } - - profiles := pprof.Profiles() - if err := indexTmpl.Execute(w, profiles); err != nil { - log.Print(err) - } -} - -var indexTmpl = template.Must(template.New("index").Parse(` - -/debug/pprof/ - - -/debug/pprof/
-
-profiles:
- -{{range .}} -
{{.Count}}{{.Name}} -{{end}} -
-
-full goroutine stack dump
- - -`)) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/pprof/socket.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/pprof/socket.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/pprof/socket.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/pprof/socket.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,69 +0,0 @@ -// Copyright 2016 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package pprof - -import ( - "fmt" - "net" - "net/http" - "os" - "path/filepath" - "runtime" - - "github.com/juju/loggo" -) - -var logger = loggo.GetLogger("juju.cmd.pprof") - -// Filename contains the filename to use for the pprof unix socket for this -// process. The name is derived from the process name, as provided in -// os.Args[0], and the process ID. -var Filename = fmt.Sprintf( - "pprof.%s.%d", - filepath.Base(os.Args[0]), - os.Getpid(), -) - -// Start starts a pprof server listening on a unix socket which will be -// created at the specified path. -func Start(path string) func() error { - if runtime.GOOS != "linux" { - logger.Infof("pprof debugging not supported on %q", runtime.GOOS) - return func() error { return nil } - } - - mux := http.NewServeMux() - mux.Handle("/debug/pprof/", http.HandlerFunc(Index)) - mux.Handle("/debug/pprof/cmdline", http.HandlerFunc(Cmdline)) - mux.Handle("/debug/pprof/profile", http.HandlerFunc(Profile)) - mux.Handle("/debug/pprof/symbol", http.HandlerFunc(Symbol)) - - srv := http.Server{ - Handler: mux, - } - - addr, err := net.ResolveUnixAddr("unix", path) - if err != nil { - logger.Errorf("unable to resolve unix socket: %v", err) - return func() error { return nil } - } - - // Try to remove the socket if already present. - os.Remove(path) - - l, err := net.ListenUnix("unix", addr) - if err != nil { - logger.Errorf("unable to listen on unix socket: %v", err) - return func() error { return nil } - } - - go func() { - defer os.Remove(path) - - // Ignore the error from calling l.Close. - srv.Serve(l) - }() - - return l.Close -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/cmd/pprof/socket_test.go juju-core-2.0~beta15/src/github.com/juju/juju/cmd/pprof/socket_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/cmd/pprof/socket_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/cmd/pprof/socket_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,133 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package pprof - -import ( - "bufio" - "bytes" - "fmt" - "io/ioutil" - "net" - "os" - "path/filepath" - "regexp" - "runtime" - "testing" - - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" -) - -type suite struct { -} - -var _ = gc.Suite(&suite{}) - -func TestSuite(t *testing.T) { - if runtime.GOOS != "linux" { - t.Skipf("skipping pprof tests, %q not supported", runtime.GOOS) - } - gc.TestingT(t) -} - -func (s *suite) TestFilename(c *gc.C) { - got := Filename - want := fmt.Sprintf("pprof.pprof.test.%d", os.Getpid()) - c.Assert(got, gc.Equals, want) -} - -func (s *suite) TestPprofStartReturnsNonNilShutdownFn(c *gc.C) { - stop := Start(filepath.Join(c.MkDir(), Filename)) - c.Assert(stop, gc.NotNil) - c.Assert(stop(), jc.ErrorIsNil) -} - -func (s *suite) TestPprofStart(c *gc.C) { - path := filepath.Join(c.MkDir(), Filename) - _, err := os.Stat(path) - c.Assert(os.IsNotExist(err), jc.IsTrue) - - stop := Start(path) - _, err = os.Stat(path) - c.Assert(err, jc.ErrorIsNil) - - err = stop() - c.Assert(err, jc.ErrorIsNil) - _, err = os.Stat(path) - c.Assert(os.IsNotExist(err), jc.IsTrue) -} - -func (s *suite) TestPprofStartWithExistingSocketFile(c *gc.C) { - path := filepath.Join(c.MkDir(), Filename) - w, err := os.Create(path) - c.Assert(err, jc.ErrorIsNil) - - w.Write([]byte("not a socket")) - err = w.Close() // can ignore error from w.Write - c.Assert(err, jc.ErrorIsNil) - - stop := Start(path) - defer stop() - fi, err := os.Stat(path) - c.Assert(err, jc.ErrorIsNil) - c.Assert(fi.Mode()&os.ModeSocket != 0, jc.IsTrue) -} - -type pprofSuite struct { - stop func() error - path string -} - -var _ = gc.Suite(&pprofSuite{}) - -func (s *pprofSuite) SetUpSuite(c *gc.C) { - s.path = filepath.Join(c.MkDir(), Filename) - s.stop = Start(s.path) -} - -func (s *pprofSuite) TearDownSuite(c *gc.C) { - err := s.stop() - c.Assert(err, jc.ErrorIsNil) -} - -func (s *pprofSuite) call(c *gc.C, url string) []byte { - conn, err := net.Dial("unix", s.path) - c.Assert(err, jc.ErrorIsNil) - defer conn.Close() - - _, err = fmt.Fprintf(conn, "GET %s HTTP/1.0\r\n\r\n", url) - c.Assert(err, jc.ErrorIsNil) - - buf, err := ioutil.ReadAll(conn) - c.Assert(err, jc.ErrorIsNil) - return buf -} - -func (s *pprofSuite) TestCmdLine(c *gc.C) { - buf := s.call(c, "/debug/pprof/cmdline") - c.Assert(buf, gc.NotNil) - matches(c, buf, ".*github.com/juju/juju/cmd/pprof/_test/pprof.test") -} - -func (s *pprofSuite) TestGoroutineProfile(c *gc.C) { - buf := s.call(c, "/debug/pprof/goroutine") - c.Assert(buf, gc.NotNil) - matches(c, buf, `^goroutine profile: total \d+`) -} - -// matches fails if regex is not found in the contents of b. -// b is expected to be the response from the pprof http server, and will -// contain some HTTP preamble that should be ignored. -func matches(c *gc.C, b []byte, regex string) { - re, err := regexp.Compile(regex) - c.Assert(err, jc.ErrorIsNil) - r := bytes.NewReader(b) - sc := bufio.NewScanner(r) - for sc.Scan() { - if re.MatchString(sc.Text()) { - return - } - } - c.Fatalf("%q did not match regex %q", string(b), regex) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/component/all/payload.go juju-core-2.0~beta15/src/github.com/juju/juju/component/all/payload.go --- juju-core-2.0~beta12/src/github.com/juju/juju/component/all/payload.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/component/all/payload.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,6 +11,7 @@ "github.com/juju/juju/api/base" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/cmd/juju/commands" "github.com/juju/juju/cmd/modelcmd" "github.com/juju/juju/payload" @@ -19,8 +20,6 @@ internalserver "github.com/juju/juju/payload/api/private/server" "github.com/juju/juju/payload/api/server" "github.com/juju/juju/payload/context" - "github.com/juju/juju/payload/persistence" - payloadstate "github.com/juju/juju/payload/state" "github.com/juju/juju/payload/status" "github.com/juju/juju/state" unitercontext "github.com/juju/juju/worker/uniter/runner/context" @@ -32,7 +31,6 @@ type payloads struct{} func (c payloads) registerForServer() error { - c.registerState() c.registerPublicFacade() c.registerHookContext() return nil @@ -43,8 +41,8 @@ return nil } -func (payloads) newPublicFacade(st *state.State, resources *common.Resources, authorizer common.Authorizer) (*server.PublicAPI, error) { - up, err := st.EnvPayloads() +func (payloads) newPublicFacade(st *state.State, resources facade.Resources, authorizer facade.Authorizer) (*server.PublicAPI, error) { + up, err := st.ModelPayloads() if err != nil { return nil, errors.Trace(err) } @@ -191,25 +189,3 @@ return cmd, nil }) } - -func (payloads) registerState() { - if !markRegistered(payload.ComponentName, "state") { - return - } - - newUnitPayloads := func(db state.Persistence, unit, machine string) (state.UnitPayloads, error) { - persist := persistence.NewPersistence(db) - unitPersist := persistence.NewUnitPersistence(persist, unit) - return payloadstate.NewUnitPayloads(unitPersist, unit, machine), nil - } - - newEnvPayloads := func(db state.Persistence) (state.EnvPayloads, error) { - persist := persistence.NewPersistence(db) - envPayloads := payloadstate.EnvPayloads{ - Persist: persist, - } - return envPayloads, nil - } - - state.SetPayloadsComponent(newEnvPayloads, newUnitPayloads) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/container/factory/factory.go juju-core-2.0~beta15/src/github.com/juju/juju/container/factory/factory.go --- juju-core-2.0~beta12/src/github.com/juju/juju/container/factory/factory.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/container/factory/factory.go 2016-08-16 08:56:25.000000000 +0000 @@ -16,7 +16,7 @@ // NewContainerManager creates the appropriate container.Manager for the // specified container type. -func NewContainerManager(forType instance.ContainerType, conf container.ManagerConfig, imageURLGetter container.ImageURLGetter) (container.Manager, error) { +func NewContainerManager(forType instance.ContainerType, conf container.ManagerConfig) (container.Manager, error) { switch forType { case instance.LXD: return lxd.NewContainerManager(conf) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/container/factory/factory_test.go juju-core-2.0~beta15/src/github.com/juju/juju/container/factory/factory_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/container/factory/factory_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/container/factory/factory_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -46,7 +46,7 @@ } conf := container.ManagerConfig{container.ConfigModelUUID: testing.ModelTag.Id()} - manager, err := factory.NewContainerManager(test.containerType, conf, nil) + manager, err := factory.NewContainerManager(test.containerType, conf) if test.valid { c.Assert(err, jc.ErrorIsNil) c.Assert(manager, gc.NotNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/container/image.go juju-core-2.0~beta15/src/github.com/juju/juju/container/image.go --- juju-core-2.0~beta12/src/github.com/juju/juju/container/image.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/container/image.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,92 +0,0 @@ -// Copyright 2014 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package container - -import ( - "fmt" - "os" - "os/exec" - "path" - "strings" - - "github.com/juju/errors" - - "github.com/juju/juju/instance" -) - -// ImageURLGetter implementations provide a getter which returns a URL which -// can be used to fetch an image blob. -type ImageURLGetter interface { - // ImageURL returns a URL which can be used to fetch an image of the - // specified kind, series, and arch. - ImageURL(kind instance.ContainerType, series, arch string) (string, error) - - // CACert returns the ca certificate used to validate the controller - // certificate when using wget. - CACert() []byte -} - -type ImageURLGetterConfig struct { - ServerRoot string - ModelUUID string - CACert []byte - CloudimgBaseUrl string - Stream string - ImageDownloadFunc func(kind instance.ContainerType, series, arch, stream, cloudimgBaseUrl string) (string, error) -} - -type imageURLGetter struct { - config ImageURLGetterConfig -} - -// NewImageURLGetter returns an ImageURLGetter for the specified state -// server address and environment UUID. -func NewImageURLGetter(config ImageURLGetterConfig) ImageURLGetter { - return &imageURLGetter{config} -} - -// ImageURL is specified on the NewImageURLGetter interface. -func (ug *imageURLGetter) ImageURL(kind instance.ContainerType, series, arch string) (string, error) { - imageURL, err := ug.config.ImageDownloadFunc(kind, series, arch, ug.config.Stream, ug.config.CloudimgBaseUrl) - if err != nil { - return "", errors.Annotatef(err, "cannot determine LXC image URL: %v", err) - } - imageFilename := path.Base(imageURL) - - imageUrl := fmt.Sprintf( - "https://%s/model/%s/images/%v/%s/%s/%s", ug.config.ServerRoot, ug.config.ModelUUID, kind, series, arch, imageFilename, - ) - return imageUrl, nil -} - -// CACert is specified on the NewImageURLGetter interface. -func (ug *imageURLGetter) CACert() []byte { - return ug.config.CACert -} - -// ImageDownloadURL determines the public URL which can be used to obtain an -// image blob with the specified parameters. -func ImageDownloadURL(kind instance.ContainerType, series, arch, stream, cloudimgBaseUrl string) (string, error) { - // TODO - we currently only need to support LXC images - kind is ignored. - if kind != instance.LXD { - return "", errors.Errorf("unsupported container type: %v", kind) - } - - // Use the ubuntu-cloudimg-query command to get the url from which to fetch the image. - // This will be somewhere on http://cloud-images.ubuntu.com. - cmd := exec.Command("ubuntu-cloudimg-query", series, stream, arch, "--format", "%{url}") - if cloudimgBaseUrl != "" { - // If the base url isn't specified, we don't need to copy the current - // environment, because this is the default behaviour of the exec package. - cmd.Env = append(os.Environ(), fmt.Sprintf("UBUNTU_CLOUDIMG_QUERY_BASEURL=%s", cloudimgBaseUrl)) - } - urlBytes, err := cmd.CombinedOutput() - if err != nil { - stderr := string(urlBytes) - return "", errors.Annotatef(err, "cannot determine LXC image URL: %v", stderr) - } - logger.Debugf("%s image for %s (%s) is %s", kind, series, arch, urlBytes) - imageURL := strings.Replace(string(urlBytes), ".tar.gz", "-root.tar.gz", -1) - return imageURL, nil -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/container/image_test.go juju-core-2.0~beta15/src/github.com/juju/juju/container/image_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/container/image_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/container/image_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,89 +0,0 @@ -// Copyright 2014 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -// +build !windows - -package container_test - -import ( - "fmt" - - "github.com/juju/testing" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/container" - containertesting "github.com/juju/juju/container/testing" - "github.com/juju/juju/instance" - coretesting "github.com/juju/juju/testing" -) - -type imageURLSuite struct { - coretesting.BaseSuite -} - -var _ = gc.Suite(&imageURLSuite{}) - -func (s *imageURLSuite) SetUpTest(c *gc.C) { - testing.PatchExecutable(c, s, "ubuntu-cloudimg-query", containertesting.FakeLxcURLScript) -} - -func (s *imageURLSuite) TestImageURL(c *gc.C) { - s.assertImageURLForStream(c, "released") - s.assertImageURLForStream(c, "daily") -} - -func (s *imageURLSuite) assertImageURLForStream(c *gc.C, stream string) { - imageURLGetter := container.NewImageURLGetter( - container.ImageURLGetterConfig{ - ServerRoot: "host:port", - ModelUUID: "12345", - CACert: []byte("cert"), - CloudimgBaseUrl: "", - Stream: stream, - ImageDownloadFunc: container.ImageDownloadURL, - }) - imageURL, err := imageURLGetter.ImageURL(instance.LXD, "trusty", "amd64") - c.Assert(err, gc.IsNil) - c.Assert(imageURL, gc.Equals, fmt.Sprintf("https://host:port/model/12345/images/lxd/trusty/amd64/trusty-%s-amd64-root.tar.gz", stream)) - c.Assert(imageURLGetter.CACert(), gc.DeepEquals, []byte("cert")) -} - -func (s *imageURLSuite) TestImageURLOtherBase(c *gc.C) { - var calledBaseURL string - baseURL := "other://cloud-images" - mockFunc := func(kind instance.ContainerType, series, arch, stream, cloudimgBaseUrl string) (string, error) { - calledBaseURL = cloudimgBaseUrl - return "omg://wat/trusty-released-amd64-root.tar.gz", nil - } - imageURLGetter := container.NewImageURLGetter( - container.ImageURLGetterConfig{ - ServerRoot: "host:port", - ModelUUID: "12345", - CACert: []byte("cert"), - CloudimgBaseUrl: baseURL, - Stream: "released", - ImageDownloadFunc: mockFunc, - }) - imageURL, err := imageURLGetter.ImageURL(instance.LXD, "trusty", "amd64") - c.Assert(err, gc.IsNil) - c.Assert(imageURL, gc.Equals, "https://host:port/model/12345/images/lxd/trusty/amd64/trusty-released-amd64-root.tar.gz") - c.Assert(imageURLGetter.CACert(), gc.DeepEquals, []byte("cert")) - c.Assert(calledBaseURL, gc.Equals, baseURL) -} - -func (s *imageURLSuite) TestImageDownloadURL(c *gc.C) { - imageDownloadURL, err := container.ImageDownloadURL(instance.LXD, "trusty", "amd64", "released", "") - c.Assert(err, gc.IsNil) - c.Assert(imageDownloadURL, gc.Equals, "test://cloud-images/trusty-released-amd64-root.tar.gz") -} - -func (s *imageURLSuite) TestImageDownloadURLOtherBase(c *gc.C) { - imageDownloadURL, err := container.ImageDownloadURL(instance.LXD, "trusty", "amd64", "released", "other://cloud-images") - c.Assert(err, gc.IsNil) - c.Assert(imageDownloadURL, gc.Equals, "other://cloud-images/trusty-released-amd64-root.tar.gz") -} - -func (s *imageURLSuite) TestImageDownloadURLUnsupportedContainer(c *gc.C) { - _, err := container.ImageDownloadURL(instance.KVM, "trusty", "amd64", "released", "") - c.Assert(err, gc.ErrorMatches, "unsupported container .*") -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/container/interface.go juju-core-2.0~beta15/src/github.com/juju/juju/container/interface.go --- juju-core-2.0~beta12/src/github.com/juju/juju/container/interface.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/container/interface.go 2016-08-16 08:56:25.000000000 +0000 @@ -13,8 +13,6 @@ const ( ConfigModelUUID = "model-uuid" ConfigLogDir = "log-dir" - - DefaultNamespace = "juju" ) // ManagerConfig contains the initialization parameters for the ContainerManager. diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/container/kvm/kvm_test.go juju-core-2.0~beta15/src/github.com/juju/juju/container/kvm/kvm_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/container/kvm/kvm_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/container/kvm/kvm_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -348,7 +348,7 @@ }, }} { var tw loggo.TestWriter - c.Assert(loggo.RegisterWriter("constraint-tester", &tw, loggo.DEBUG), gc.IsNil) + c.Assert(loggo.RegisterWriter("constraint-tester", &tw), gc.IsNil) cons := constraints.MustParse(test.cons) params := kvm.ParseConstraintsToStartParams(cons) c.Check(params, gc.DeepEquals, test.expected) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/container/lxd/initialisation.go juju-core-2.0~beta15/src/github.com/juju/juju/container/lxd/initialisation.go --- juju-core-2.0~beta12/src/github.com/juju/juju/container/lxd/initialisation.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/container/lxd/initialisation.go 2016-08-16 08:56:25.000000000 +0000 @@ -15,9 +15,9 @@ "strings" "github.com/juju/errors" - "github.com/juju/utils/packaging/config" "github.com/juju/utils/packaging/manager" + "github.com/juju/utils/proxy" "github.com/juju/juju/container" ) @@ -56,7 +56,14 @@ if err != nil { return err } + proxies := proxy.DetectProxies() + err = ConfigureLXDProxies(proxies) + if err != nil { + return err + } + // Well... this will need to change soon once we are passed 17.04 as who + // knows what the series name will be. if ci.series >= "xenial" { configureZFS() } @@ -76,11 +83,49 @@ return config.NewPackagingConfigurer(series) } +// ConfigureLXDProxies will try to set the lxc config core.proxy_http and core.proxy_https +// configuration values based on the current environment. +func ConfigureLXDProxies(proxies proxy.Settings) error { + setter, err := getLXDConfigSetter() + if err != nil { + return errors.Trace(err) + } + return errors.Trace(configureLXDProxies(setter, proxies)) +} + +var getLXDConfigSetter = getConfigSetterConnect + +func getConfigSetterConnect() (configSetter, error) { + return ConnectLocal() +} + +type configSetter interface { + SetConfig(key, value string) error +} + +func configureLXDProxies(setter configSetter, proxies proxy.Settings) error { + err := setter.SetConfig("core.proxy_http", proxies.Http) + if err != nil { + return errors.Trace(err) + } + err = setter.SetConfig("core.proxy_https", proxies.Https) + if err != nil { + return errors.Trace(err) + } + err = setter.SetConfig("core.proxy_ignore_hosts", proxies.NoProxy) + if err != nil { + return errors.Trace(err) + } + return nil +} + +var execCommand = exec.Command + var configureZFS = func() { /* create a 100 GB pool by default (sparse, so it won't actually fill * that immediately) */ - output, err := exec.Command( + output, err := execCommand( "lxd", "init", "--auto", diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/container/lxd/initialisation_test.go juju-core-2.0~beta15/src/github.com/juju/juju/container/lxd/initialisation_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/container/lxd/initialisation_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/container/lxd/initialisation_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,19 +9,23 @@ "errors" "fmt" "net" + "runtime" + "github.com/juju/testing" jc "github.com/juju/testing/checkers" "github.com/juju/utils/packaging/commands" "github.com/juju/utils/packaging/manager" + "github.com/juju/utils/proxy" "github.com/juju/utils/series" gc "gopkg.in/check.v1" - "github.com/juju/juju/testing" + coretesting "github.com/juju/juju/testing" ) type InitialiserSuite struct { - testing.BaseSuite + coretesting.BaseSuite calledCmds []string + testing.PatchExecHelper } var _ = gc.Suite(&InitialiserSuite{}) @@ -77,6 +81,11 @@ s.PatchValue(&manager.RunCommandWithRetry, getMockRunCommandWithRetry(&s.calledCmds)) s.PatchValue(&configureZFS, func() {}) s.PatchValue(&configureLXDBridge, func() error { return nil }) + s.PatchValue(&getLXDConfigSetter, func() (configSetter, error) { + return &mockConfigSetter{}, nil + }) + // Fake the lxc executable for all the tests. + testing.PatchExecutableAsEchoArgs(c, s, "lxc") } func (s *InitialiserSuite) TestLTSSeriesPackages(c *gc.C) { @@ -113,6 +122,66 @@ }) } +type mockConfigSetter struct { + keys []string + values []string +} + +func (m *mockConfigSetter) SetConfig(key, value string) error { + m.keys = append(m.keys, key) + m.values = append(m.values, value) + return nil +} + +func (s *InitialiserSuite) TestConfigureProxies(c *gc.C) { + // This test is safe on windows because it mocks out all lxd moving parts. + setter := &mockConfigSetter{} + s.PatchValue(&getLXDConfigSetter, func() (configSetter, error) { + return setter, nil + }) + + proxies := proxy.Settings{ + Http: "http://test.local/http/proxy", + Https: "http://test.local/https/proxy", + NoProxy: "test.local,localhost", + } + err := ConfigureLXDProxies(proxies) + c.Assert(err, jc.ErrorIsNil) + + c.Check(setter.keys, jc.DeepEquals, []string{ + "core.proxy_http", "core.proxy_https", "core.proxy_ignore_hosts", + }) + c.Check(setter.values, jc.DeepEquals, []string{ + "http://test.local/http/proxy", "http://test.local/https/proxy", "test.local,localhost", + }) +} + +func (s *InitialiserSuite) TestInitializeSetsProxies(c *gc.C) { + if runtime.GOOS == "windows" { + c.Skip("no lxd on windows") + } + + setter := &mockConfigSetter{} + s.PatchValue(&getLXDConfigSetter, func() (configSetter, error) { + return setter, nil + }) + + s.PatchEnvironment("http_proxy", "http://test.local/http/proxy") + s.PatchEnvironment("https_proxy", "http://test.local/https/proxy") + s.PatchEnvironment("no_proxy", "test.local,localhost") + + container := NewContainerInitialiser("") + err := container.Initialise() + c.Assert(err, jc.ErrorIsNil) + + c.Check(setter.keys, jc.DeepEquals, []string{ + "core.proxy_http", "core.proxy_https", "core.proxy_ignore_hosts", + }) + c.Check(setter.values, jc.DeepEquals, []string{ + "http://test.local/http/proxy", "http://test.local/https/proxy", "test.local,localhost", + }) +} + func (s *InitialiserSuite) TestFindAvailableSubnetWithInterfaceAddrsError(c *gc.C) { s.PatchValue(&interfaceAddrs, func() ([]net.Addr, error) { return nil, errors.New("boom!") diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/container/testing/common.go juju-core-2.0~beta15/src/github.com/juju/juju/container/testing/common.go --- juju-core-2.0~beta12/src/github.com/juju/juju/container/testing/common.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/container/testing/common.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,11 +5,7 @@ import ( "io/ioutil" - "os" - "path/filepath" - "github.com/juju/errors" - "github.com/juju/loggo" jc "github.com/juju/testing/checkers" "github.com/juju/version" gc "gopkg.in/check.v1" @@ -24,8 +20,6 @@ "github.com/juju/juju/tools" ) -var logger = loggo.GetLogger("juju.container.testing") - func MockMachineConfig(machineId string) (*instancecfg.InstanceConfig, error) { apiInfo := jujutesting.FakeAPIInfo(machineId) @@ -79,18 +73,6 @@ return inst } -func EnsureContainerRootFSEtcNetwork(c *gc.C, containerName string) { - // Pre-create the mock rootfs dir for the container and - // /etc/network/ inside it, where the interfaces file will be - // pre-rendered (unless AUFS is used). - etcNetwork := filepath.Join(container.ContainerDir, containerName, "rootfs", "etc", "network") - logger.Debugf("ensuring root fs /etc/network in %s", etcNetwork) - err := os.MkdirAll(etcNetwork, 0755) - c.Assert(err, jc.ErrorIsNil) - err = ioutil.WriteFile(filepath.Join(etcNetwork, "interfaces"), []byte("#empty"), 0644) - c.Assert(err, jc.ErrorIsNil) -} - func AssertCloudInit(c *gc.C, filename string) []byte { c.Assert(filename, jc.IsNonEmptyFile) data, err := ioutil.ReadFile(filename) @@ -98,50 +80,3 @@ c.Assert(string(data), jc.HasPrefix, "#cloud-config\n") return data } - -// CreateContainerTest tries to create a container and returns any errors encountered along the -// way -func CreateContainerTest(c *gc.C, manager container.Manager, machineId string) (instance.Instance, error) { - instanceConfig, err := MockMachineConfig(machineId) - if err != nil { - return nil, errors.Trace(err) - } - - network := container.BridgeNetworkConfig("nic42", 0, nil) - storage := &container.StorageConfig{} - - name, err := manager.Namespace().Hostname(instanceConfig.MachineId) - c.Assert(err, jc.ErrorIsNil) - EnsureContainerRootFSEtcNetwork(c, name) - - callback := func(settableStatus status.Status, info string, data map[string]interface{}) error { return nil } - inst, hardware, err := manager.CreateContainer(instanceConfig, constraints.Value{}, "quantal", network, storage, callback) - - if err != nil { - return nil, errors.Trace(err) - } - if hardware == nil { - return nil, errors.New("nil hardware characteristics") - } - if hardware.String() == "" { - return nil, errors.New("empty hardware characteristics") - } - return inst, nil - -} - -// FakeLxcURLScript is used to replace ubuntu-cloudimg-query in tests. -var FakeLxcURLScript = `#!/bin/bash -baseurl="${UBUNTU_CLOUDIMG_QUERY_BASEURL:-test://cloud-images}" -echo -n ${baseurl}/$1-$2-$3.tar.gz` - -// MockURLGetter implements ImageURLGetter. -type MockURLGetter struct{} - -func (ug *MockURLGetter) ImageURL(kind instance.ContainerType, series, arch string) (string, error) { - return "imageURL", nil -} - -func (ug *MockURLGetter) CACert() []byte { - return []byte("cert") -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/CONTRIBUTING.md juju-core-2.0~beta15/src/github.com/juju/juju/CONTRIBUTING.md --- juju-core-2.0~beta12/src/github.com/juju/juju/CONTRIBUTING.md 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/CONTRIBUTING.md 2016-08-16 08:56:25.000000000 +0000 @@ -146,7 +146,7 @@ After getting the juju code, you need to get `godeps`: ```shell -go get launchpad.net/godeps +go get github.com/rogpeppe/godeps ``` This installs the `godeps` application. You can then run `godeps` from the diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/controller/config.go juju-core-2.0~beta15/src/github.com/juju/juju/controller/config.go --- juju-core-2.0~beta12/src/github.com/juju/juju/controller/config.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/controller/config.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,7 +4,6 @@ package controller import ( - "fmt" "net/url" "github.com/juju/errors" @@ -22,6 +21,10 @@ // ApiPort is the port used for api connections. ApiPort = "api-port" + // AuditingEnabled determines whether the controller will record + // auditing information. + AuditingEnabled = "auditing-enabled" + // StatePort is the port used for mongo connections. StatePort = "state-port" @@ -42,6 +45,10 @@ // Attribute Defaults + // DefaultAuditingEnabled contains the default value for the + // AuditingEnabled config value. + DefaultAuditingEnabled = false + // DefaultNumaControlPolicy should not be used by default. // Only use numactl if user specifically requests it DefaultNumaControlPolicy = false @@ -111,7 +118,7 @@ } value, _ := c[name].(int) if value == 0 { - panic(fmt.Errorf("empty value for %q found in configuration", name)) + panic(errors.Errorf("empty value for %q found in configuration", name)) } return value } @@ -129,7 +136,7 @@ func (c Config) mustString(name string) string { value, _ := c[name].(string) if value == "" { - panic(fmt.Errorf("empty value for %q found in configuration (type %T, val %v)", name, c[name], c[name])) + panic(errors.Errorf("empty value for %q found in configuration (type %T, val %v)", name, c[name], c[name])) } return value } @@ -144,6 +151,15 @@ return c.mustInt(ApiPort) } +// AuditingEnabled returns whether or not auditing has been enabled +// for the environment. The default is false. +func (c Config) AuditingEnabled() bool { + if v, ok := c[AuditingEnabled]; ok { + return v.(bool) + } + return false +} + // ControllerUUID returns the uuid for the model's controller. func (c Config) ControllerUUID() string { return c.mustString(ControllerUUIDKey) @@ -192,10 +208,10 @@ if v, ok := c[IdentityURL].(string); ok { u, err := url.Parse(v) if err != nil { - return fmt.Errorf("invalid identity URL: %v", err) + return errors.Annotate(err, "invalid identity URL") } if u.Scheme != "https" { - return fmt.Errorf("URL needs to be https") + return errors.Errorf("URL needs to be https") } } @@ -203,7 +219,7 @@ if v, ok := c[IdentityPublicKey].(string); ok { var key bakery.PublicKey if err := key.UnmarshalText([]byte(v)); err != nil { - return fmt.Errorf("invalid identity public key: %v", err) + return errors.Annotate(err, "invalid identity public key") } } @@ -229,6 +245,7 @@ } var configChecker = schema.FieldMap(schema.Fields{ + AuditingEnabled: schema.Bool(), ApiPort: schema.ForceInt(), StatePort: schema.ForceInt(), IdentityURL: schema.String(), @@ -236,6 +253,7 @@ SetNumaControlPolicyKey: schema.Bool(), }, schema.Defaults{ ApiPort: DefaultAPIPort, + AuditingEnabled: DefaultAuditingEnabled, StatePort: DefaultStatePort, IdentityURL: schema.Omit, IdentityPublicKey: schema.Omit, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/controller/config_test.go juju-core-2.0~beta15/src/github.com/juju/juju/controller/config_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/controller/config_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/controller/config_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -32,7 +32,7 @@ s.FakeJujuXDGDataHomeSuite.SetUpTest(c) // Make sure that the defaults are used, which // is =WARNING - loggo.ResetLoggers() + loggo.DefaultContext().ResetLoggerLevels() } func (s *ConfigSuite) TestGenerateControllerCertAndKey(c *gc.C) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/controller/modelmanager/createmodel.go juju-core-2.0~beta15/src/github.com/juju/juju/controller/modelmanager/createmodel.go --- juju-core-2.0~beta12/src/github.com/juju/juju/controller/modelmanager/createmodel.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/controller/modelmanager/createmodel.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,7 +11,6 @@ "github.com/juju/utils" "github.com/juju/version" - "github.com/juju/juju/controller" "github.com/juju/juju/environs" "github.com/juju/juju/environs/config" "github.com/juju/juju/tools" @@ -21,19 +20,15 @@ logger = loggo.GetLogger("juju.controller.modelmanager") ) -const ( - // IsAdmin is used when generating a model config for an admin user. - IsAdmin = true - - // IsNotAdmin is used when generating a model config for a non admin user. - IsNotAdmin = false -) - // ModelConfigCreator provides a method of creating a new model config. // // The zero value of ModelConfigCreator is usable with the limitations // noted on each struct field. type ModelConfigCreator struct { + // Provider will be used to obtain EnvironProviders for preparing + // and validating configuration. + Provider func(string) (environs.EnvironProvider, error) + // FindTools, if non-nil, will be used to validate the agent-version // value in NewModelConfig if it differs from the base configuration. // @@ -52,7 +47,7 @@ // // The config will be validated with the provider before being returned. func (c ModelConfigCreator) NewModelConfig( - isAdmin bool, + cloud environs.CloudSpec, controllerUUID string, base *config.Config, attrs map[string]interface{}, @@ -61,6 +56,10 @@ if err := c.checkVersion(base, attrs); err != nil { return nil, errors.Trace(err) } + provider, err := c.Provider(cloud.Type) + if err != nil { + return nil, errors.Trace(err) + } // Before comparing any values, we need to push the config through // the provider validation code. One of the reasons for this is that @@ -69,7 +68,7 @@ // However, before we can create a valid config, we need to make sure // we copy across fields from the main config that aren't there. baseAttrs := base.AllAttrs() - restrictedFields, err := RestrictedProviderFields(base.Type()) + restrictedFields, err := RestrictedProviderFields(provider) if err != nil { return nil, errors.Trace(err) } @@ -90,21 +89,15 @@ } attrs[config.UUIDKey] = uuid.String() } - cfg, err := finalizeConfig(isAdmin, controllerUUID, base, attrs) + cfg, err := finalizeConfig(provider, cloud, controllerUUID, attrs) if err != nil { return nil, errors.Trace(err) } - attrs = cfg.AllAttrs() - // TODO(wallyworld) - we need to separate controller and model schemas - for _, attr := range controller.ControllerOnlyConfigAttributes { - if _, ok := attrs[attr]; ok { - return nil, errors.Errorf("unexpected controller attribute %q in model config", attr) - } - } // Any values that would normally be copied from the controller // config can also be defined, but if they differ from the controller // values, an error is returned. + attrs = cfg.AllAttrs() for _, field := range restrictedFields { if value, ok := attrs[field]; ok { if serverValue := baseAttrs[field]; value != serverValue { @@ -179,11 +172,7 @@ // specific config, since models should be independent of each other; and // anything that should not change across models should be in the controller // config. -func RestrictedProviderFields(providerType string) ([]string, error) { - provider, err := environs.Provider(providerType) - if err != nil { - return nil, errors.Trace(err) - } +func RestrictedProviderFields(provider environs.EnvironProvider) ([]string, error) { var fields []string // For now, all models in a controller must be of the same type. fields = append(fields, config.TypeKey) @@ -191,34 +180,29 @@ return fields, nil } -// finalizeConfig creates the config object from attributes, calls -// PrepareForCreateEnvironment, and then finally validates the config -// before returning it. -func finalizeConfig(isAdmin bool, controllerUUID string, controllerModelCfg *config.Config, attrs map[string]interface{}) (*config.Config, error) { - provider, err := environs.Provider(controllerModelCfg.Type()) - if err != nil { - return nil, errors.Trace(err) - } - +// finalizeConfig creates the config object from attributes, +// and calls EnvironProvider.PrepareConfig. +func finalizeConfig( + provider environs.EnvironProvider, + cloud environs.CloudSpec, + controllerUUID string, + attrs map[string]interface{}, +) (*config.Config, error) { cfg, err := config.New(config.UseDefaults, attrs) if err != nil { return nil, errors.Annotate(err, "creating config from values failed") } - - // TODO(wallyworld) - we need to separate controller and model schemas - // Remove any remaining controller attributes from the env config. - cfg, err = cfg.Remove(controller.ControllerOnlyConfigAttributes) - if err != nil { - return nil, errors.Annotate(err, "cannot remove controller attributes") - } - - cfg, err = provider.PrepareForCreateEnvironment(controllerUUID, cfg) + cfg, err = provider.PrepareConfig(environs.PrepareConfigParams{ + ControllerUUID: controllerUUID, + Cloud: cloud, + Config: cfg, + }) if err != nil { - return nil, errors.Trace(err) + return nil, errors.Annotate(err, "provider config preparation failed") } cfg, err = provider.Validate(cfg, nil) if err != nil { - return nil, errors.Annotate(err, "provider validation failed") + return nil, errors.Annotate(err, "provider config validation failed") } return cfg, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/controller/modelmanager/createmodel_test.go juju-core-2.0~beta15/src/github.com/juju/juju/controller/modelmanager/createmodel_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/controller/modelmanager/createmodel_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/controller/modelmanager/createmodel_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -29,6 +29,7 @@ type ModelConfigCreatorSuite struct { coretesting.BaseSuite + fake fakeProvider creator modelmanager.ModelConfigCreator baseConfig *config.Config } @@ -37,7 +38,17 @@ func (s *ModelConfigCreatorSuite) SetUpTest(c *gc.C) { s.BaseSuite.SetUpTest(c) - s.creator = modelmanager.ModelConfigCreator{} + s.fake = fakeProvider{ + restrictedConfigAttributes: []string{"restricted"}, + } + s.creator = modelmanager.ModelConfigCreator{ + Provider: func(provider string) (environs.EnvironProvider, error) { + if provider != "fake" { + return nil, errors.Errorf("expected fake, got %s", provider) + } + return &s.fake, nil + }, + } baseConfig, err := config.New( config.UseDefaults, coretesting.FakeConfig().Merge(coretesting.Attrs{ @@ -53,15 +64,11 @@ baseConfig, err = baseConfig.Remove(controller.ControllerOnlyConfigAttributes) c.Assert(err, jc.ErrorIsNil) s.baseConfig = baseConfig - fake.Reset() } func (s *ModelConfigCreatorSuite) newModelConfig(attrs map[string]interface{}) (*config.Config, error) { - return s.creator.NewModelConfig(modelmanager.IsNotAdmin, coretesting.ModelTag.Id(), s.baseConfig, attrs) -} - -func (s *ModelConfigCreatorSuite) newModelConfigAdmin(attrs map[string]interface{}) (*config.Config, error) { - return s.creator.NewModelConfig(modelmanager.IsAdmin, coretesting.ModelTag.Id(), s.baseConfig, attrs) + cloudSpec := environs.CloudSpec{Type: "fake"} + return s.creator.NewModelConfig(cloudSpec, coretesting.ModelTag.Id(), s.baseConfig, attrs) } func (s *ModelConfigCreatorSuite) TestCreateModelValidatesConfig(c *gc.C) { @@ -80,53 +87,12 @@ expected["uuid"] = newModelUUID c.Assert(cfg.AllAttrs(), jc.DeepEquals, expected) - fake.Stub.CheckCallNames(c, + s.fake.Stub.CheckCallNames(c, "RestrictedConfigAttributes", - "PrepareForCreateEnvironment", + "PrepareConfig", "Validate", ) - validateCall := fake.Stub.Calls()[2] - c.Assert(validateCall.Args, gc.HasLen, 2) - c.Assert(validateCall.Args[0], gc.Equals, cfg) - c.Assert(validateCall.Args[1], gc.IsNil) -} - -func (s *ModelConfigCreatorSuite) TestCreateModelForAdminUserPrefersUserSecrets(c *gc.C) { - var err error - s.baseConfig, err = s.baseConfig.Apply(coretesting.Attrs{ - "username": "user", - "password": "password", - }) - c.Assert(err, jc.ErrorIsNil) - newModelUUID := utils.MustNewUUID().String() - newAttrs := coretesting.Attrs{ - "name": "new-model", - "additional": "value", - "uuid": newModelUUID, - "username": "anotheruser", - "password": "anotherpassword", - } - cfg, err := s.newModelConfigAdmin(newAttrs) - c.Assert(err, jc.ErrorIsNil) - expectedCfg, err := config.New(config.UseDefaults, newAttrs) - c.Assert(err, jc.ErrorIsNil) - - // TODO(wallyworld) - we need to separate controller and model schemas - // Remove any remaining controller attributes from the env config. - expectedCfg, err = expectedCfg.Remove(controller.ControllerOnlyConfigAttributes) - c.Assert(err, jc.ErrorIsNil) - - expected := expectedCfg.AllAttrs() - c.Assert(expected["username"], gc.Equals, "anotheruser") - c.Assert(expected["password"], gc.Equals, "anotherpassword") - c.Assert(cfg.AllAttrs(), jc.DeepEquals, expected) - - fake.Stub.CheckCallNames(c, - "RestrictedConfigAttributes", - "PrepareForCreateEnvironment", - "Validate", - ) - validateCall := fake.Stub.Calls()[2] + validateCall := s.fake.Stub.Calls()[2] c.Assert(validateCall.Args, gc.HasLen, 2) c.Assert(validateCall.Args[0], gc.Equals, cfg) c.Assert(validateCall.Args[1], gc.IsNil) @@ -241,41 +207,30 @@ expected []string }{{ provider: "azure", - expected: []string{ - "type", - "location", "endpoint", "storage-endpoint", - }, + expected: []string{"type"}, }, { provider: "dummy", - expected: []string{ - "type", - }, + expected: []string{"type"}, }, { provider: "joyent", - expected: []string{ - "type", "sdc-url", - }, + expected: []string{"type"}, }, { provider: "maas", - expected: []string{ - "type", - "maas-server", - }, + expected: []string{"type"}, }, { provider: "openstack", - expected: []string{ - "type", - "region", "auth-url", "auth-mode", - }, + expected: []string{"type"}, }, { provider: "ec2", expected: []string{ "type", - "region", "vpc-id-force", + "vpc-id-force", }, }} { c.Logf("%d: %s provider", i, test.provider) - fields, err := modelmanager.RestrictedProviderFields(test.provider) + provider, err := environs.Provider(test.provider) + c.Check(err, jc.ErrorIsNil) + fields, err := modelmanager.RestrictedProviderFields(provider) c.Check(err, jc.ErrorIsNil) c.Check(fields, jc.SameContents, test.expected) } @@ -287,11 +242,6 @@ restrictedConfigAttributes []string } -func (p *fakeProvider) Reset() { - p.Stub.ResetCalls() - p.restrictedConfigAttributes = []string{"restricted"} -} - func (p *fakeProvider) RestrictedConfigAttributes() []string { p.MethodCall(p, "RestrictedConfigAttributes") return p.restrictedConfigAttributes @@ -302,9 +252,9 @@ return cfg, p.NextErr() } -func (p *fakeProvider) PrepareForCreateEnvironment(controllerUUID string, cfg *config.Config) (*config.Config, error) { - p.MethodCall(p, "PrepareForCreateEnvironment", controllerUUID, cfg) - return cfg, p.NextErr() +func (p *fakeProvider) PrepareConfig(args environs.PrepareConfigParams) (*config.Config, error) { + p.MethodCall(p, "PrepareConfig", args) + return args.Config, p.NextErr() } func (p *fakeProvider) CredentialSchemas() map[cloud.AuthType]cloud.CredentialSchema { @@ -325,10 +275,3 @@ func (p *fakeProvider) DetectCredentials() (*cloud.CloudCredential, error) { return nil, errors.NotFoundf("credentials") } - -var fake fakeProvider - -func init() { - fake.Reset() - environs.RegisterProvider("fake", &fake) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/access.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/access.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/access.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/access.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,133 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package description + +import ( + "github.com/juju/errors" + "github.com/juju/schema" +) + +// Access represents a level of access. +type Access string + +const ( + // UndefinedAccess is not a valid access type. It is the value + // used when access is not defined at all. + UndefinedAccess Access = "" + + // Model Permissions + + // ReadAccess allows a user to read information about a permission subject, + // without being able to make any changes. + ReadAccess Access = "read" + + // WriteAccess allows a user to make changes to a permission subject. + WriteAccess Access = "write" + + // AdminAccess allows a user full control over the subject. + AdminAccess Access = "admin" + + // Controller permissions + + // LoginAccess allows a user to log-ing into the subject. + LoginAccess Access = "login" + + // AddModelAccess allows user to add new models in subjects supporting it. + AddModelAccess Access = "addmodel" + + // SuperuserAccess allows user unrestricted permissions in the subject. + SuperuserAccess Access = "superuser" +) + +// Validate returns error if the current is not a valid access level. +func (a Access) Validate() error { + switch a { + case UndefinedAccess, AdminAccess, ReadAccess, WriteAccess, + LoginAccess, AddModelAccess, SuperuserAccess: + return nil + } + return errors.NotValidf("access level %s", a) +} + +// ValidateModelAccess returns error if the passed access is not a valid +// model access level. +func ValidateModelAccess(access Access) error { + switch access { + case ReadAccess, WriteAccess, AdminAccess: + return nil + } + return errors.NotValidf("%q model access", access) +} + +//ValidateControllerAccess returns error if the passed access is not a valid +// controller access level. +func ValidateControllerAccess(access Access) error { + switch access { + case LoginAccess, AddModelAccess, SuperuserAccess: + return nil + } + return errors.NotValidf("%q controller access", access) +} + +// EqualOrGreaterAccessThan returns true if the provided access is equal or +// less than the current. +func (a Access) EqualOrGreaterModelAccessThan(access Access) bool { + if a == access { + return true + } + switch a { + case UndefinedAccess: + return false + case ReadAccess: + return access == UndefinedAccess + case WriteAccess: + return access == ReadAccess || + access == UndefinedAccess + case AdminAccess: + return access == ReadAccess || + access == WriteAccess + } + return false +} + +func (a Access) EqualOrGreaterControllerAccessThan(access Access) bool { + if a == access { + return true + } + switch a { + case UndefinedAccess: + return false + case LoginAccess: + return access == UndefinedAccess + case AddModelAccess: + return access == UndefinedAccess || + access == LoginAccess + case SuperuserAccess: + return access == UndefinedAccess || + access == LoginAccess || + access == AddModelAccess + } + return false +} + +// accessField returns a Checker that accepts a string value only +// and returns a valid Access or an error. +func accessField() schema.Checker { + return accessC{} +} + +type accessC struct{} + +func (c accessC) Coerce(v interface{}, path []string) (interface{}, error) { + s := schema.String() + in, err := s.Coerce(v, path) + if err != nil { + return nil, err + } + access := Access(in.(string)) + if err := access.Validate(); err != nil { + return nil, errors.Trace(err) + } + return access, nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/blockdevice.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/blockdevice.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/blockdevice.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/blockdevice.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,215 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package description + +import ( + "github.com/juju/errors" + "github.com/juju/schema" +) + +// BlockDevice represents a block device on a machine. +type BlockDevice interface { + Name() string + Links() []string + Label() string + UUID() string + HardwareID() string + BusAddress() string + Size() uint64 + FilesystemType() string + InUse() bool + MountPoint() string +} + +type blockdevices struct { + Version int `yaml:"version"` + BlockDevices_ []*blockdevice `yaml:"block-devices"` +} + +func (d *blockdevices) add(args BlockDeviceArgs) *blockdevice { + dev := newBlockDevice(args) + d.BlockDevices_ = append(d.BlockDevices_, dev) + return dev +} + +type blockdevice struct { + Name_ string `yaml:"name"` + Links_ []string `yaml:"links,omitempty"` + Label_ string `yaml:"label,omitempty"` + UUID_ string `yaml:"uuid,omitempty"` + HardwareID_ string `yaml:"hardware-id,omitempty"` + BusAddress_ string `yaml:"bus-address,omitempty"` + Size_ uint64 `yaml:"size"` + FilesystemType_ string `yaml:"fs-type,omitempty"` + InUse_ bool `yaml:"in-use"` + MountPoint_ string `yaml:"mount-point,omitempty"` +} + +// BlockDeviceArgs is an argument struct used to add a block device to a Machine. +type BlockDeviceArgs struct { + Name string + Links []string + Label string + UUID string + HardwareID string + BusAddress string + Size uint64 + FilesystemType string + InUse bool + MountPoint string +} + +func newBlockDevice(args BlockDeviceArgs) *blockdevice { + bd := &blockdevice{ + Name_: args.Name, + Links_: make([]string, len(args.Links)), + Label_: args.Label, + UUID_: args.UUID, + HardwareID_: args.HardwareID, + BusAddress_: args.BusAddress, + Size_: args.Size, + FilesystemType_: args.FilesystemType, + InUse_: args.InUse, + MountPoint_: args.MountPoint, + } + copy(bd.Links_, args.Links) + return bd +} + +// Name implements BlockDevice. +func (b *blockdevice) Name() string { + return b.Name_ +} + +// Links implements BlockDevice. +func (b *blockdevice) Links() []string { + return b.Links_ +} + +// Label implements BlockDevice. +func (b *blockdevice) Label() string { + return b.Label_ +} + +// UUID implements BlockDevice. +func (b *blockdevice) UUID() string { + return b.UUID_ +} + +// HardwareID implements BlockDevice. +func (b *blockdevice) HardwareID() string { + return b.HardwareID_ +} + +// BusAddress implements BlockDevice. +func (b *blockdevice) BusAddress() string { + return b.BusAddress_ +} + +// Size implements BlockDevice. +func (b *blockdevice) Size() uint64 { + return b.Size_ +} + +// FilesystemType implements BlockDevice. +func (b *blockdevice) FilesystemType() string { + return b.FilesystemType_ +} + +// InUse implements BlockDevice. +func (b *blockdevice) InUse() bool { + return b.InUse_ +} + +// MountPoint implements BlockDevice. +func (b *blockdevice) MountPoint() string { + return b.MountPoint_ +} + +func importBlockDevices(source interface{}) ([]*blockdevice, error) { + checker := versionedChecker("block-devices") + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "block devices version schema check failed") + } + valid := coerced.(map[string]interface{}) + + version := int(valid["version"].(int64)) + importFunc, ok := blockdeviceDeserializationFuncs[version] + if !ok { + return nil, errors.NotValidf("version %d", version) + } + sourceList := valid["block-devices"].([]interface{}) + return importBlockDeviceList(sourceList, importFunc) +} + +func importBlockDeviceList(sourceList []interface{}, importFunc blockdeviceDeserializationFunc) ([]*blockdevice, error) { + result := make([]*blockdevice, 0, len(sourceList)) + for i, value := range sourceList { + source, ok := value.(map[string]interface{}) + if !ok { + return nil, errors.Errorf("unexpected value for block device %d, %T", i, value) + } + device, err := importFunc(source) + if err != nil { + return nil, errors.Annotatef(err, "block device %d", i) + } + result = append(result, device) + } + return result, nil +} + +type blockdeviceDeserializationFunc func(map[string]interface{}) (*blockdevice, error) + +var blockdeviceDeserializationFuncs = map[int]blockdeviceDeserializationFunc{ + 1: importBlockDeviceV1, +} + +func importBlockDeviceV1(source map[string]interface{}) (*blockdevice, error) { + fields := schema.Fields{ + "name": schema.String(), + "links": schema.List(schema.String()), + "label": schema.String(), + "uuid": schema.String(), + "hardware-id": schema.String(), + "bus-address": schema.String(), + "size": schema.Uint(), + "fs-type": schema.String(), + "in-use": schema.Bool(), + "mount-point": schema.String(), + } + + defaults := schema.Defaults{ + "links": schema.Omit, + "label": "", + "uuid": "", + "hardware-id": "", + "bus-address": "", + "fs-type": "", + "mount-point": "", + } + checker := schema.FieldMap(fields, defaults) + + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "block device v1 schema check failed") + } + valid := coerced.(map[string]interface{}) + // From here we know that the map returned from the schema coercion + // contains fields of the right type. + result := &blockdevice{ + Name_: valid["name"].(string), + Links_: convertToStringSlice(valid["links"]), + Label_: valid["label"].(string), + UUID_: valid["uuid"].(string), + HardwareID_: valid["hardware-id"].(string), + BusAddress_: valid["bus-address"].(string), + Size_: valid["size"].(uint64), + FilesystemType_: valid["fs-type"].(string), + InUse_: valid["in-use"].(bool), + MountPoint_: valid["mount-point"].(string), + } + + return result, nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/blockdevice_test.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/blockdevice_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/blockdevice_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/blockdevice_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,95 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package description + +import ( + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/yaml.v2" +) + +type BlockDeviceSerializationSuite struct { + SliceSerializationSuite +} + +var _ = gc.Suite(&BlockDeviceSerializationSuite{}) + +func (s *BlockDeviceSerializationSuite) SetUpTest(c *gc.C) { + s.SliceSerializationSuite.SetUpTest(c) + s.importName = "block devices" + s.sliceName = "block-devices" + s.importFunc = func(m map[string]interface{}) (interface{}, error) { + return importBlockDevices(m) + } + s.testFields = func(m map[string]interface{}) { + m["block-devices"] = []interface{}{} + } +} + +func allBlockDeviceArgs() BlockDeviceArgs { + return BlockDeviceArgs{ + Name: "/dev/sda", + Links: []string{"some", "data"}, + Label: "sda", + UUID: "some-uuid", + HardwareID: "magic", + BusAddress: "bus stop", + Size: 16 * 1024 * 1024 * 1024, + FilesystemType: "ext4", + InUse: true, + MountPoint: "/", + } +} + +func (s *BlockDeviceSerializationSuite) TestNewBlockDevice(c *gc.C) { + d := newBlockDevice(allBlockDeviceArgs()) + c.Check(d.Name(), gc.Equals, "/dev/sda") + c.Check(d.Links(), jc.DeepEquals, []string{"some", "data"}) + c.Check(d.Label(), gc.Equals, "sda") + c.Check(d.UUID(), gc.Equals, "some-uuid") + c.Check(d.HardwareID(), gc.Equals, "magic") + c.Check(d.BusAddress(), gc.Equals, "bus stop") + c.Check(d.Size(), gc.Equals, uint64(16*1024*1024*1024)) + c.Check(d.FilesystemType(), gc.Equals, "ext4") + c.Check(d.InUse(), jc.IsTrue) + c.Check(d.MountPoint(), gc.Equals, "/") +} + +func (s *BlockDeviceSerializationSuite) exportImport(c *gc.C, dev *blockdevice) *blockdevice { + initial := blockdevices{ + Version: 1, + BlockDevices_: []*blockdevice{dev}, + } + + bytes, err := yaml.Marshal(initial) + c.Assert(err, jc.ErrorIsNil) + + var source map[string]interface{} + err = yaml.Unmarshal(bytes, &source) + c.Assert(err, jc.ErrorIsNil) + + devices, err := importBlockDevices(source) + c.Assert(err, jc.ErrorIsNil) + c.Assert(devices, gc.HasLen, 1) + return devices[0] +} + +func (s *BlockDeviceSerializationSuite) TestParsingSerializedData(c *gc.C) { + initial := newBlockDevice(allBlockDeviceArgs()) + imported := s.exportImport(c, initial) + c.Assert(imported, jc.DeepEquals, initial) +} + +func (s *BlockDeviceSerializationSuite) TestImportEmpty(c *gc.C) { + devices, err := importBlockDevices(emptyBlockDeviceMap()) + c.Assert(err, jc.ErrorIsNil) + c.Assert(devices, gc.HasLen, 0) +} + +func emptyBlockDeviceMap() map[interface{}]interface{} { + return map[interface{}]interface{}{ + "version": 1, + "block-devices": []interface{}{}, + } +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/constraints.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/constraints.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/constraints.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/constraints.go 2016-08-16 08:56:25.000000000 +0000 @@ -20,6 +20,8 @@ Spaces []string Tags []string + + VirtType string } func newConstraints(args ConstraintsArgs) *constraints { @@ -44,6 +46,7 @@ RootDisk_: args.RootDisk, Spaces_: spaces, Tags_: tags, + VirtType_: args.VirtType, } } @@ -60,6 +63,8 @@ Spaces_ []string `yaml:"spaces,omitempty"` Tags_ []string `yaml:"tags,omitempty"` + + VirtType_ string `yaml:"virt-type,omitempty"` } // Architecture implements Constraints. @@ -117,6 +122,11 @@ return tags } +// VirtType implements Constraints. +func (c *constraints) VirtType() string { + return c.VirtType_ +} + func importConstraints(source map[string]interface{}) (*constraints, error) { version, err := getVersion(source) if err != nil { @@ -149,6 +159,8 @@ "spaces": schema.List(schema.String()), "tags": schema.List(schema.String()), + + "virt-type": schema.String(), } // Some values don't have to be there. defaults := schema.Defaults{ @@ -162,6 +174,8 @@ "spaces": schema.Omit, "tags": schema.Omit, + + "virt-type": "", } checker := schema.FieldMap(fields, defaults) @@ -185,6 +199,8 @@ Spaces_: convertToStringSlice(valid["spaces"]), Tags_: convertToStringSlice(valid["tags"]), + + VirtType_: valid["virt-type"].(string), }, nil } @@ -202,5 +218,6 @@ c.Memory == 0 && c.RootDisk == 0 && c.Spaces == nil && - c.Tags == nil + c.Tags == nil && + c.VirtType == "" } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/constraints_test.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/constraints_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/constraints_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/constraints_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -65,6 +65,13 @@ c.Assert(instance.Tags(), jc.DeepEquals, []string{"much", "strong"}) } +func (s *ConstraintsSerializationSuite) TestNewConstraintsWithVirt(c *gc.C) { + args := s.allArgs() + args.VirtType = "kvm" + instance := newConstraints(args) + c.Assert(instance.VirtType(), gc.Equals, args.VirtType) +} + func (s *ConstraintsSerializationSuite) TestNewConstraintsEmpty(c *gc.C) { instance := newConstraints(ConstraintsArgs{}) c.Assert(instance, gc.IsNil) @@ -77,8 +84,16 @@ c.Assert(instance.Spaces(), gc.IsNil) } +func (s *ConstraintsSerializationSuite) TestEmptyVirt(c *gc.C) { + instance := newConstraints(ConstraintsArgs{Architecture: "amd64"}) + c.Assert(instance.VirtType(), gc.Equals, "") +} + func (s *ConstraintsSerializationSuite) TestParsingSerializedData(c *gc.C) { - initial := newConstraints(s.allArgs()) + s.assertParsingSerializedConstraints(c, newConstraints(s.allArgs())) +} + +func (s *ConstraintsSerializationSuite) assertParsingSerializedConstraints(c *gc.C, initial Constraints) { bytes, err := yaml.Marshal(initial) c.Assert(err, jc.ErrorIsNil) @@ -90,3 +105,9 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(instance, jc.DeepEquals, initial) } + +func (s *ConstraintsSerializationSuite) TestParsingSerializedVirt(c *gc.C) { + args := s.allArgs() + args.VirtType = "kvm" + s.assertParsingSerializedConstraints(c, newConstraints(args)) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/filesystem.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/filesystem.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/filesystem.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/filesystem.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,385 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package description + +import ( + "github.com/juju/errors" + "github.com/juju/schema" + "gopkg.in/juju/names.v2" +) + +type filesystems struct { + Version int `yaml:"version"` + Filesystems_ []*filesystem `yaml:"filesystems"` +} + +type filesystem struct { + ID_ string `yaml:"id"` + StorageID_ string `yaml:"storage-id,omitempty"` + VolumeID_ string `yaml:"volume-id,omitempty"` + Binding_ string `yaml:"binding,omitempty"` + + Provisioned_ bool `yaml:"provisioned"` + Size_ uint64 `yaml:"size"` + Pool_ string `yaml:"pool,omitempty"` + FilesystemID_ string `yaml:"filesystem-id,omitempty"` + + Status_ *status `yaml:"status"` + StatusHistory_ `yaml:"status-history"` + + Attachments_ filesystemAttachments `yaml:"attachments"` +} + +type filesystemAttachments struct { + Version int `yaml:"version"` + Attachments_ []*filesystemAttachment `yaml:"attachments"` +} + +type filesystemAttachment struct { + MachineID_ string `yaml:"machine-id"` + Provisioned_ bool `yaml:"provisioned"` + MountPoint_ string `yaml:"mount-point,omitempty"` + ReadOnly_ bool `yaml:"read-only"` +} + +// FilesystemArgs is an argument struct used to add a filesystem to the Model. +type FilesystemArgs struct { + Tag names.FilesystemTag + Storage names.StorageTag + Volume names.VolumeTag + Binding names.Tag + Provisioned bool + Size uint64 + Pool string + FilesystemID string +} + +func newFilesystem(args FilesystemArgs) *filesystem { + f := &filesystem{ + ID_: args.Tag.Id(), + StorageID_: args.Storage.Id(), + VolumeID_: args.Volume.Id(), + Provisioned_: args.Provisioned, + Size_: args.Size, + Pool_: args.Pool, + FilesystemID_: args.FilesystemID, + StatusHistory_: newStatusHistory(), + } + if args.Binding != nil { + f.Binding_ = args.Binding.String() + } + f.setAttachments(nil) + return f +} + +// Tag implements Filesystem. +func (f *filesystem) Tag() names.FilesystemTag { + return names.NewFilesystemTag(f.ID_) +} + +// Volume implements Filesystem. +func (f *filesystem) Volume() names.VolumeTag { + if f.VolumeID_ == "" { + return names.VolumeTag{} + } + return names.NewVolumeTag(f.VolumeID_) +} + +// Storage implements Filesystem. +func (f *filesystem) Storage() names.StorageTag { + if f.StorageID_ == "" { + return names.StorageTag{} + } + return names.NewStorageTag(f.StorageID_) +} + +// Binding implements Filesystem. +func (f *filesystem) Binding() (names.Tag, error) { + if f.Binding_ == "" { + return nil, nil + } + tag, err := names.ParseTag(f.Binding_) + if err != nil { + return nil, errors.Trace(err) + } + return tag, nil +} + +// Provisioned implements Filesystem. +func (f *filesystem) Provisioned() bool { + return f.Provisioned_ +} + +// Size implements Filesystem. +func (f *filesystem) Size() uint64 { + return f.Size_ +} + +// Pool implements Filesystem. +func (f *filesystem) Pool() string { + return f.Pool_ +} + +// FilesystemID implements Filesystem. +func (f *filesystem) FilesystemID() string { + return f.FilesystemID_ +} + +// Status implements Filesystem. +func (f *filesystem) Status() Status { + // To avoid typed nils check nil here. + if f.Status_ == nil { + return nil + } + return f.Status_ +} + +// SetStatus implements Filesystem. +func (f *filesystem) SetStatus(args StatusArgs) { + f.Status_ = newStatus(args) +} + +func (f *filesystem) setAttachments(attachments []*filesystemAttachment) { + f.Attachments_ = filesystemAttachments{ + Version: 1, + Attachments_: attachments, + } +} + +// Attachments implements Filesystem. +func (f *filesystem) Attachments() []FilesystemAttachment { + var result []FilesystemAttachment + for _, attachment := range f.Attachments_.Attachments_ { + result = append(result, attachment) + } + return result +} + +// AddAttachment implements Filesystem. +func (f *filesystem) AddAttachment(args FilesystemAttachmentArgs) FilesystemAttachment { + a := newFilesystemAttachment(args) + f.Attachments_.Attachments_ = append(f.Attachments_.Attachments_, a) + return a +} + +// Validate implements Filesystem. +func (f *filesystem) Validate() error { + if f.ID_ == "" { + return errors.NotValidf("filesystem missing id") + } + if f.Size_ == 0 { + return errors.NotValidf("filesystem %q missing size", f.ID_) + } + if f.Status_ == nil { + return errors.NotValidf("filesystem %q missing status", f.ID_) + } + return nil +} + +func importFilesystems(source map[string]interface{}) ([]*filesystem, error) { + checker := versionedChecker("filesystems") + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "filesystems version schema check failed") + } + valid := coerced.(map[string]interface{}) + + version := int(valid["version"].(int64)) + importFunc, ok := filesystemDeserializationFuncs[version] + if !ok { + return nil, errors.NotValidf("version %d", version) + } + sourceList := valid["filesystems"].([]interface{}) + return importFilesystemList(sourceList, importFunc) +} + +func importFilesystemList(sourceList []interface{}, importFunc filesystemDeserializationFunc) ([]*filesystem, error) { + result := make([]*filesystem, 0, len(sourceList)) + for i, value := range sourceList { + source, ok := value.(map[string]interface{}) + if !ok { + return nil, errors.Errorf("unexpected value for filesystem %d, %T", i, value) + } + filesystem, err := importFunc(source) + if err != nil { + return nil, errors.Annotatef(err, "filesystem %d", i) + } + result = append(result, filesystem) + } + return result, nil +} + +type filesystemDeserializationFunc func(map[string]interface{}) (*filesystem, error) + +var filesystemDeserializationFuncs = map[int]filesystemDeserializationFunc{ + 1: importFilesystemV1, +} + +func importFilesystemV1(source map[string]interface{}) (*filesystem, error) { + fields := schema.Fields{ + "id": schema.String(), + "storage-id": schema.String(), + "volume-id": schema.String(), + "binding": schema.String(), + "provisioned": schema.Bool(), + "size": schema.ForceUint(), + "pool": schema.String(), + "filesystem-id": schema.String(), + "status": schema.StringMap(schema.Any()), + "attachments": schema.StringMap(schema.Any()), + } + + defaults := schema.Defaults{ + "storage-id": "", + "volume-id": "", + "binding": "", + "pool": "", + "filesystem-id": "", + "attachments": schema.Omit, + } + addStatusHistorySchema(fields) + checker := schema.FieldMap(fields, defaults) + + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "filesystem v1 schema check failed") + } + valid := coerced.(map[string]interface{}) + // From here we know that the map returned from the schema coercion + // contains fields of the right type. + result := &filesystem{ + ID_: valid["id"].(string), + StorageID_: valid["storage-id"].(string), + VolumeID_: valid["volume-id"].(string), + Binding_: valid["binding"].(string), + Provisioned_: valid["provisioned"].(bool), + Size_: valid["size"].(uint64), + Pool_: valid["pool"].(string), + FilesystemID_: valid["filesystem-id"].(string), + StatusHistory_: newStatusHistory(), + } + if err := result.importStatusHistory(valid); err != nil { + return nil, errors.Trace(err) + } + + status, err := importStatus(valid["status"].(map[string]interface{})) + if err != nil { + return nil, errors.Trace(err) + } + result.Status_ = status + + attachments, err := importFilesystemAttachments(valid["attachments"].(map[string]interface{})) + if err != nil { + return nil, errors.Trace(err) + } + result.setAttachments(attachments) + + return result, nil +} + +// FilesystemAttachmentArgs is an argument struct used to add information about the +// cloud instance to a Filesystem. +type FilesystemAttachmentArgs struct { + Machine names.MachineTag + Provisioned bool + ReadOnly bool + MountPoint string +} + +func newFilesystemAttachment(args FilesystemAttachmentArgs) *filesystemAttachment { + return &filesystemAttachment{ + MachineID_: args.Machine.Id(), + Provisioned_: args.Provisioned, + ReadOnly_: args.ReadOnly, + MountPoint_: args.MountPoint, + } +} + +// Machine implements FilesystemAttachment +func (a *filesystemAttachment) Machine() names.MachineTag { + return names.NewMachineTag(a.MachineID_) +} + +// Provisioned implements FilesystemAttachment +func (a *filesystemAttachment) Provisioned() bool { + return a.Provisioned_ +} + +// ReadOnly implements FilesystemAttachment +func (a *filesystemAttachment) ReadOnly() bool { + return a.ReadOnly_ +} + +// MountPoint implements FilesystemAttachment +func (a *filesystemAttachment) MountPoint() string { + return a.MountPoint_ +} + +func importFilesystemAttachments(source map[string]interface{}) ([]*filesystemAttachment, error) { + checker := versionedChecker("attachments") + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "filesystem attachments version schema check failed") + } + valid := coerced.(map[string]interface{}) + + version := int(valid["version"].(int64)) + importFunc, ok := filesystemAttachmentDeserializationFuncs[version] + if !ok { + return nil, errors.NotValidf("version %d", version) + } + sourceList := valid["attachments"].([]interface{}) + return importFilesystemAttachmentList(sourceList, importFunc) +} + +func importFilesystemAttachmentList(sourceList []interface{}, importFunc filesystemAttachmentDeserializationFunc) ([]*filesystemAttachment, error) { + result := make([]*filesystemAttachment, 0, len(sourceList)) + for i, value := range sourceList { + source, ok := value.(map[string]interface{}) + if !ok { + return nil, errors.Errorf("unexpected value for filesystemAttachment %d, %T", i, value) + } + filesystemAttachment, err := importFunc(source) + if err != nil { + return nil, errors.Annotatef(err, "filesystemAttachment %d", i) + } + result = append(result, filesystemAttachment) + } + return result, nil +} + +type filesystemAttachmentDeserializationFunc func(map[string]interface{}) (*filesystemAttachment, error) + +var filesystemAttachmentDeserializationFuncs = map[int]filesystemAttachmentDeserializationFunc{ + 1: importFilesystemAttachmentV1, +} + +func importFilesystemAttachmentV1(source map[string]interface{}) (*filesystemAttachment, error) { + fields := schema.Fields{ + "machine-id": schema.String(), + "provisioned": schema.Bool(), + "read-only": schema.Bool(), + "mount-point": schema.String(), + } + defaults := schema.Defaults{ + "mount-point": "", + } + checker := schema.FieldMap(fields, defaults) + + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "filesystemAttachment v1 schema check failed") + } + valid := coerced.(map[string]interface{}) + // From here we know that the map returned from the schema coercion + // contains fields of the right type. + + result := &filesystemAttachment{ + MachineID_: valid["machine-id"].(string), + Provisioned_: valid["provisioned"].(bool), + ReadOnly_: valid["read-only"].(bool), + MountPoint_: valid["mount-point"].(string), + } + return result, nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/filesystem_test.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/filesystem_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/filesystem_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/filesystem_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,271 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package description + +import ( + "github.com/juju/errors" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/juju/names.v2" + "gopkg.in/yaml.v2" +) + +type FilesystemSerializationSuite struct { + SliceSerializationSuite + StatusHistoryMixinSuite +} + +var _ = gc.Suite(&FilesystemSerializationSuite{}) + +func (s *FilesystemSerializationSuite) SetUpTest(c *gc.C) { + s.SliceSerializationSuite.SetUpTest(c) + s.importName = "filesystems" + s.sliceName = "filesystems" + s.importFunc = func(m map[string]interface{}) (interface{}, error) { + return importFilesystems(m) + } + s.testFields = func(m map[string]interface{}) { + m["filesystems"] = []interface{}{} + } + s.StatusHistoryMixinSuite.creator = func() HasStatusHistory { + return testFilesystem() + } + s.StatusHistoryMixinSuite.serializer = func(c *gc.C, initial interface{}) HasStatusHistory { + return s.exportImport(c, initial.(*filesystem)) + } +} + +func testFilesystemMap() map[interface{}]interface{} { + return map[interface{}]interface{}{ + "id": "1234", + "storage-id": "test/1", + "volume-id": "4321", + "binding": "machine-42", + "provisioned": true, + "size": int(20 * gig), + "pool": "swimming", + "filesystem-id": "some filesystem id", + "status": minimalStatusMap(), + "status-history": emptyStatusHistoryMap(), + "attachments": map[interface{}]interface{}{ + "version": 1, + "attachments": []interface{}{}, + }, + } +} + +func testFilesystem() *filesystem { + v := newFilesystem(testFilesystemArgs()) + v.SetStatus(minimalStatusArgs()) + return v +} + +func testFilesystemArgs() FilesystemArgs { + return FilesystemArgs{ + Tag: names.NewFilesystemTag("1234"), + Storage: names.NewStorageTag("test/1"), + Volume: names.NewVolumeTag("4321"), + Binding: names.NewMachineTag("42"), + Provisioned: true, + Size: 20 * gig, + Pool: "swimming", + FilesystemID: "some filesystem id", + } +} + +func (s *FilesystemSerializationSuite) TestNewFilesystem(c *gc.C) { + filesystem := testFilesystem() + + c.Check(filesystem.Tag(), gc.Equals, names.NewFilesystemTag("1234")) + c.Check(filesystem.Storage(), gc.Equals, names.NewStorageTag("test/1")) + c.Check(filesystem.Volume(), gc.Equals, names.NewVolumeTag("4321")) + binding, err := filesystem.Binding() + c.Check(err, jc.ErrorIsNil) + c.Check(binding, gc.Equals, names.NewMachineTag("42")) + c.Check(filesystem.Provisioned(), jc.IsTrue) + c.Check(filesystem.Size(), gc.Equals, 20*gig) + c.Check(filesystem.Pool(), gc.Equals, "swimming") + c.Check(filesystem.FilesystemID(), gc.Equals, "some filesystem id") + + c.Check(filesystem.Attachments(), gc.HasLen, 0) +} + +func (s *FilesystemSerializationSuite) TestFilesystemValid(c *gc.C) { + filesystem := testFilesystem() + c.Assert(filesystem.Validate(), jc.ErrorIsNil) +} + +func (s *FilesystemSerializationSuite) TestFilesystemValidMissingID(c *gc.C) { + v := newFilesystem(FilesystemArgs{}) + err := v.Validate() + c.Check(err, gc.ErrorMatches, `filesystem missing id not valid`) + c.Check(err, jc.Satisfies, errors.IsNotValid) +} + +func (s *FilesystemSerializationSuite) TestFilesystemValidMissingSize(c *gc.C) { + v := newFilesystem(FilesystemArgs{ + Tag: names.NewFilesystemTag("123"), + }) + err := v.Validate() + c.Check(err, gc.ErrorMatches, `filesystem "123" missing size not valid`) + c.Check(err, jc.Satisfies, errors.IsNotValid) +} + +func (s *FilesystemSerializationSuite) TestFilesystemValidMissingStatus(c *gc.C) { + v := newFilesystem(FilesystemArgs{ + Tag: names.NewFilesystemTag("123"), + Size: 5, + }) + err := v.Validate() + c.Check(err, gc.ErrorMatches, `filesystem "123" missing status not valid`) + c.Check(err, jc.Satisfies, errors.IsNotValid) +} + +func (s *FilesystemSerializationSuite) TestFilesystemValidMinimal(c *gc.C) { + v := newFilesystem(FilesystemArgs{ + Tag: names.NewFilesystemTag("123"), + Size: 5, + }) + v.SetStatus(minimalStatusArgs()) + err := v.Validate() + c.Check(err, jc.ErrorIsNil) +} + +func (s *FilesystemSerializationSuite) TestFilesystemMatches(c *gc.C) { + bytes, err := yaml.Marshal(testFilesystem()) + c.Assert(err, jc.ErrorIsNil) + + var source map[interface{}]interface{} + err = yaml.Unmarshal(bytes, &source) + c.Assert(err, jc.ErrorIsNil) + c.Assert(source, jc.DeepEquals, testFilesystemMap()) +} + +func (s *FilesystemSerializationSuite) exportImport(c *gc.C, filesystem_ *filesystem) *filesystem { + initial := filesystems{ + Version: 1, + Filesystems_: []*filesystem{filesystem_}, + } + + bytes, err := yaml.Marshal(initial) + c.Assert(err, jc.ErrorIsNil) + + var source map[string]interface{} + err = yaml.Unmarshal(bytes, &source) + c.Assert(err, jc.ErrorIsNil) + + filesystems, err := importFilesystems(source) + c.Assert(err, jc.ErrorIsNil) + c.Assert(filesystems, gc.HasLen, 1) + return filesystems[0] +} + +func (s *FilesystemSerializationSuite) TestAddingAttachments(c *gc.C) { + // The core code does not care about duplicates, so we'll just add + // the same attachment twice. + original := testFilesystem() + attachment1 := original.AddAttachment(testFilesystemAttachmentArgs("1")) + attachment2 := original.AddAttachment(testFilesystemAttachmentArgs("2")) + filesystem := s.exportImport(c, original) + c.Assert(filesystem, jc.DeepEquals, original) + attachments := filesystem.Attachments() + c.Assert(attachments, gc.HasLen, 2) + c.Check(attachments[0], jc.DeepEquals, attachment1) + c.Check(attachments[1], jc.DeepEquals, attachment2) +} + +func (s *FilesystemSerializationSuite) TestParsingSerializedData(c *gc.C) { + original := testFilesystem() + original.AddAttachment(testFilesystemAttachmentArgs()) + filesystem := s.exportImport(c, original) + c.Assert(filesystem, jc.DeepEquals, original) +} + +type FilesystemAttachmentSerializationSuite struct { + SliceSerializationSuite +} + +var _ = gc.Suite(&FilesystemAttachmentSerializationSuite{}) + +func (s *FilesystemAttachmentSerializationSuite) SetUpTest(c *gc.C) { + s.SliceSerializationSuite.SetUpTest(c) + s.importName = "filesystem attachments" + s.sliceName = "attachments" + s.importFunc = func(m map[string]interface{}) (interface{}, error) { + return importFilesystemAttachments(m) + } + s.testFields = func(m map[string]interface{}) { + m["attachments"] = []interface{}{} + } +} + +func testFilesystemAttachmentMap() map[interface{}]interface{} { + return map[interface{}]interface{}{ + "machine-id": "42", + "provisioned": true, + "read-only": true, + "mount-point": "/some/dir", + } +} + +func testFilesystemAttachment() *filesystemAttachment { + return newFilesystemAttachment(testFilesystemAttachmentArgs()) +} + +func testFilesystemAttachmentArgs(id ...string) FilesystemAttachmentArgs { + machineID := "42" + if len(id) > 0 { + machineID = id[0] + } + return FilesystemAttachmentArgs{ + Machine: names.NewMachineTag(machineID), + Provisioned: true, + ReadOnly: true, + MountPoint: "/some/dir", + } +} + +func (s *FilesystemAttachmentSerializationSuite) TestNewFilesystemAttachment(c *gc.C) { + attachment := testFilesystemAttachment() + + c.Check(attachment.Machine(), gc.Equals, names.NewMachineTag("42")) + c.Check(attachment.Provisioned(), jc.IsTrue) + c.Check(attachment.ReadOnly(), jc.IsTrue) + c.Check(attachment.MountPoint(), gc.Equals, "/some/dir") +} + +func (s *FilesystemAttachmentSerializationSuite) TestFilesystemAttachmentMatches(c *gc.C) { + bytes, err := yaml.Marshal(testFilesystemAttachment()) + c.Assert(err, jc.ErrorIsNil) + + var source map[interface{}]interface{} + err = yaml.Unmarshal(bytes, &source) + c.Assert(err, jc.ErrorIsNil) + c.Assert(source, jc.DeepEquals, testFilesystemAttachmentMap()) +} + +func (s *FilesystemAttachmentSerializationSuite) exportImport(c *gc.C, attachment *filesystemAttachment) *filesystemAttachment { + initial := filesystemAttachments{ + Version: 1, + Attachments_: []*filesystemAttachment{attachment}, + } + + bytes, err := yaml.Marshal(initial) + c.Assert(err, jc.ErrorIsNil) + + var source map[string]interface{} + err = yaml.Unmarshal(bytes, &source) + c.Assert(err, jc.ErrorIsNil) + + attachments, err := importFilesystemAttachments(source) + c.Assert(err, jc.ErrorIsNil) + c.Assert(attachments, gc.HasLen, 1) + return attachments[0] +} + +func (s *FilesystemAttachmentSerializationSuite) TestParsingSerializedData(c *gc.C) { + original := testFilesystemAttachment() + attachment := s.exportImport(c, original) + c.Assert(attachment, jc.DeepEquals, original) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/interfaces.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/interfaces.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/interfaces.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/interfaces.go 2016-08-16 08:56:25.000000000 +0000 @@ -24,6 +24,13 @@ SetConstraints(ConstraintsArgs) } +// HasStatus defines the common methods for setting and getting status +// entries for the various entities. +type HasStatus interface { + Status() Status + SetStatus(StatusArgs) +} + // HasStatusHistory defines the common methods for setting and // getting historical status entries for the various entities. type HasStatusHistory interface { @@ -63,9 +70,30 @@ Relations() []Relation AddRelation(RelationArgs) Relation + Spaces() []Space + AddSpace(SpaceArgs) Space + + LinkLayerDevices() []LinkLayerDevice + AddLinkLayerDevice(LinkLayerDeviceArgs) LinkLayerDevice + + Subnets() []Subnet + AddSubnet(SubnetArgs) Subnet + + IPAddresses() []IPAddress + AddIPAddress(IPAddressArgs) IPAddress + + SSHHostKeys() []SSHHostKey + AddSSHHostKey(SSHHostKeyArgs) SSHHostKey + Sequences() map[string]int SetSequence(name string, value int) + Volumes() []Volume + AddVolume(VolumeArgs) Volume + + Filesystems() []Filesystem + AddFilesystem(FilesystemArgs) Filesystem + Validate() error } @@ -77,9 +105,7 @@ CreatedBy() names.UserTag DateCreated() time.Time LastConnection() time.Time - IsReadOnly() bool - IsReadWrite() bool - IsAdmin() bool + Access() Access } // Address represents an IP Address of some form. @@ -104,6 +130,7 @@ type Machine interface { HasAnnotations HasConstraints + HasStatus HasStatusHistory Id() string @@ -134,12 +161,12 @@ Containers() []Machine AddContainer(MachineArgs) Machine - Status() Status - SetStatus(StatusArgs) - // TODO: // Storage + BlockDevices() []BlockDevice + AddBlockDevice(BlockDeviceArgs) BlockDevice + OpenedPorts() []OpenedPorts AddOpenedPorts(OpenedPortsArgs) OpenedPorts @@ -196,6 +223,8 @@ Spaces() []string Tags() []string + + VirtType() string } // Status represents an agent, application, or workload status. @@ -210,6 +239,7 @@ type Application interface { HasAnnotations HasConstraints + HasStatus HasStatusHistory Tag() names.ApplicationTag @@ -231,9 +261,6 @@ MetricsCredentials() []byte - Status() Status - SetStatus(StatusArgs) - Units() []Unit AddUnit(UnitArgs) Unit @@ -314,3 +341,115 @@ Settings(unitName string) map[string]interface{} SetUnitSettings(unitName string, settings map[string]interface{}) } + +// Space represents a network space, which is a named collection of subnets. +type Space interface { + Name() string + Public() bool + ProviderID() string +} + +// LinkLayerDevice represents a link layer device. +type LinkLayerDevice interface { + Name() string + MTU() uint + ProviderID() string + MachineID() string + Type() string + MACAddress() string + IsAutoStart() bool + IsUp() bool + ParentName() string +} + +// Subnet represents a network subnet. +type Subnet interface { + ProviderId() string + CIDR() string + VLANTag() int + AvailabilityZone() string + SpaceName() string + AllocatableIPHigh() string + AllocatableIPLow() string +} + +// IPAddress represents an IP address. +type IPAddress interface { + ProviderID() string + DeviceName() string + MachineID() string + SubnetCIDR() string + ConfigMethod() string + Value() string + DNSServers() []string + DNSSearchDomains() []string + GatewayAddress() string +} + +// SSHHostKey represents an ssh host key. +type SSHHostKey interface { + MachineID() string + Keys() []string +} + +// Volume represents a volume (disk, logical volume, etc.) in the model. +type Volume interface { + HasStatus + HasStatusHistory + + Tag() names.VolumeTag + Storage() names.StorageTag + + Binding() (names.Tag, error) + + Provisioned() bool + + Size() uint64 + Pool() string + + HardwareID() string + VolumeID() string + Persistent() bool + + Attachments() []VolumeAttachment + AddAttachment(VolumeAttachmentArgs) VolumeAttachment +} + +// VolumeAttachment represents a volume attached to a machine. +type VolumeAttachment interface { + Machine() names.MachineTag + Provisioned() bool + ReadOnly() bool + DeviceName() string + DeviceLink() string + BusAddress() string +} + +// Filesystem represents a filesystem in the model. +type Filesystem interface { + HasStatus + HasStatusHistory + + Tag() names.FilesystemTag + Volume() names.VolumeTag + Storage() names.StorageTag + Binding() (names.Tag, error) + + Provisioned() bool + + Size() uint64 + Pool() string + + FilesystemID() string + + Attachments() []FilesystemAttachment + AddAttachment(FilesystemAttachmentArgs) FilesystemAttachment +} + +// FilesystemAttachment represents a filesystem attached to a machine. +type FilesystemAttachment interface { + Machine() names.MachineTag + Provisioned() bool + MountPoint() string + ReadOnly() bool +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/ipaddress.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/ipaddress.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/ipaddress.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/ipaddress.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,184 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package description + +import ( + "github.com/juju/errors" + "github.com/juju/schema" +) + +type ipaddresses struct { + Version int `yaml:"version"` + IPAddresses_ []*ipaddress `yaml:"ipaddresses"` +} + +type ipaddress struct { + ProviderID_ string `yaml:"provider-id,omitempty"` + DeviceName_ string `yaml:"devicename"` + MachineID_ string `yaml:"machineid"` + SubnetCIDR_ string `yaml:"subnetcidr"` + ConfigMethod_ string `yaml:"configmethod"` + Value_ string `yaml:"value"` + DNSServers_ []string `yaml:"dnsservers"` + DNSSearchDomains_ []string `yaml:"dnssearchdomains"` + GatewayAddress_ string `yaml:"gatewayaddress"` +} + +// ProviderID implements IPAddress. +func (i *ipaddress) ProviderID() string { + return i.ProviderID_ +} + +// DeviceName implements IPAddress. +func (i *ipaddress) DeviceName() string { + return i.DeviceName_ +} + +// MachineID implements IPAddress. +func (i *ipaddress) MachineID() string { + return i.MachineID_ +} + +// SubnetCIDR implements IPAddress. +func (i *ipaddress) SubnetCIDR() string { + return i.SubnetCIDR_ +} + +// ConfigMethod implements IPAddress. +func (i *ipaddress) ConfigMethod() string { + return i.ConfigMethod_ +} + +// Value implements IPAddress. +func (i *ipaddress) Value() string { + return i.Value_ +} + +// DNSServers implements IPAddress. +func (i *ipaddress) DNSServers() []string { + return i.DNSServers_ +} + +// DNSSearchDomains implements IPAddress. +func (i *ipaddress) DNSSearchDomains() []string { + return i.DNSSearchDomains_ +} + +// GatewayAddress implements IPAddress. +func (i *ipaddress) GatewayAddress() string { + return i.GatewayAddress_ +} + +// IPAddressArgs is an argument struct used to create a +// new internal ipaddress type that supports the IPAddress interface. +type IPAddressArgs struct { + ProviderID string + DeviceName string + MachineID string + SubnetCIDR string + ConfigMethod string + Value string + DNSServers []string + DNSSearchDomains []string + GatewayAddress string +} + +func newIPAddress(args IPAddressArgs) *ipaddress { + return &ipaddress{ + ProviderID_: args.ProviderID, + DeviceName_: args.DeviceName, + MachineID_: args.MachineID, + SubnetCIDR_: args.SubnetCIDR, + ConfigMethod_: args.ConfigMethod, + Value_: args.Value, + DNSServers_: args.DNSServers, + DNSSearchDomains_: args.DNSSearchDomains, + GatewayAddress_: args.GatewayAddress, + } +} + +func importIPAddresses(source map[string]interface{}) ([]*ipaddress, error) { + checker := versionedChecker("ipaddresses") + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "ipaddresses version schema check failed") + } + valid := coerced.(map[string]interface{}) + + version := int(valid["version"].(int64)) + importFunc, ok := ipaddressDeserializationFuncs[version] + if !ok { + return nil, errors.NotValidf("version %d", version) + } + sourceList := valid["ipaddresses"].([]interface{}) + return importIPAddressList(sourceList, importFunc) +} + +func importIPAddressList(sourceList []interface{}, importFunc ipaddressDeserializationFunc) ([]*ipaddress, error) { + result := make([]*ipaddress, 0, len(sourceList)) + for i, value := range sourceList { + source, ok := value.(map[string]interface{}) + if !ok { + return nil, errors.Errorf("unexpected value for ipaddress %d, %T", i, value) + } + ipaddress, err := importFunc(source) + if err != nil { + return nil, errors.Annotatef(err, "ipaddress %d", i) + } + result = append(result, ipaddress) + } + return result, nil +} + +type ipaddressDeserializationFunc func(map[string]interface{}) (*ipaddress, error) + +var ipaddressDeserializationFuncs = map[int]ipaddressDeserializationFunc{ + 1: importIPAddressV1, +} + +func importIPAddressV1(source map[string]interface{}) (*ipaddress, error) { + fields := schema.Fields{ + "provider-id": schema.String(), + "devicename": schema.String(), + "machineid": schema.String(), + "subnetcidr": schema.String(), + "configmethod": schema.String(), + "value": schema.String(), + "dnsservers": schema.List(schema.String()), + "dnssearchdomains": schema.List(schema.String()), + "gatewayaddress": schema.String(), + } + // Some values don't have to be there. + defaults := schema.Defaults{ + "provider-id": "", + } + checker := schema.FieldMap(fields, defaults) + + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "ipaddress v1 schema check failed") + } + valid := coerced.(map[string]interface{}) + dnsserversInterface := valid["dnsservers"].([]interface{}) + dnsservers := make([]string, len(dnsserversInterface)) + for i, d := range dnsserversInterface { + dnsservers[i] = d.(string) + } + dnssearchInterface := valid["dnssearchdomains"].([]interface{}) + dnssearch := make([]string, len(dnssearchInterface)) + for i, d := range dnssearchInterface { + dnssearch[i] = d.(string) + } + return &ipaddress{ + ProviderID_: valid["provider-id"].(string), + DeviceName_: valid["devicename"].(string), + MachineID_: valid["machineid"].(string), + SubnetCIDR_: valid["subnetcidr"].(string), + ConfigMethod_: valid["configmethod"].(string), + Value_: valid["value"].(string), + DNSServers_: dnsservers, + DNSSearchDomains_: dnssearch, + GatewayAddress_: valid["gatewayaddress"].(string), + }, nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/ipaddress_test.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/ipaddress_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/ipaddress_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/ipaddress_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,84 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package description + +import ( + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/yaml.v2" +) + +type IPAddressSerializationSuite struct { + SliceSerializationSuite +} + +var _ = gc.Suite(&IPAddressSerializationSuite{}) + +func (s *IPAddressSerializationSuite) SetUpTest(c *gc.C) { + s.SliceSerializationSuite.SetUpTest(c) + s.importName = "ipaddresses" + s.sliceName = "ipaddresses" + s.importFunc = func(m map[string]interface{}) (interface{}, error) { + return importIPAddresses(m) + } + s.testFields = func(m map[string]interface{}) { + m["ipaddresses"] = []interface{}{} + } +} + +func (s *IPAddressSerializationSuite) TestNewIPAddress(c *gc.C) { + args := IPAddressArgs{ + SubnetCIDR: "10.0.0.0/24", + ProviderID: "magic", + DeviceName: "foo", + MachineID: "bar", + ConfigMethod: "static", + Value: "10.0.0.4", + DNSServers: []string{"10.1.0.1", "10.2.0.1"}, + DNSSearchDomains: []string{"bam", "mam"}, + GatewayAddress: "10.0.0.1", + } + address := newIPAddress(args) + c.Assert(address.SubnetCIDR(), gc.Equals, args.SubnetCIDR) + c.Assert(address.ProviderID(), gc.Equals, args.ProviderID) + c.Assert(address.DeviceName(), gc.Equals, args.DeviceName) + c.Assert(address.MachineID(), gc.Equals, args.MachineID) + c.Assert(address.ConfigMethod(), gc.Equals, args.ConfigMethod) + c.Assert(address.Value(), gc.Equals, args.Value) + c.Assert(address.DNSServers(), jc.DeepEquals, args.DNSServers) + c.Assert(address.DNSSearchDomains(), jc.DeepEquals, args.DNSSearchDomains) + c.Assert(address.GatewayAddress(), gc.Equals, args.GatewayAddress) +} + +func (s *IPAddressSerializationSuite) TestParsingSerializedData(c *gc.C) { + initial := ipaddresses{ + Version: 1, + IPAddresses_: []*ipaddress{ + newIPAddress(IPAddressArgs{ + SubnetCIDR: "10.0.0.0/24", + ProviderID: "magic", + DeviceName: "foo", + MachineID: "bar", + ConfigMethod: "static", + Value: "10.0.0.4", + DNSServers: []string{"10.1.0.1", "10.2.0.1"}, + DNSSearchDomains: []string{"bam", "mam"}, + GatewayAddress: "10.0.0.1", + }), + newIPAddress(IPAddressArgs{Value: "10.0.0.5"}), + }, + } + + bytes, err := yaml.Marshal(initial) + c.Assert(err, jc.ErrorIsNil) + + var source map[string]interface{} + err = yaml.Unmarshal(bytes, &source) + c.Assert(err, jc.ErrorIsNil) + + addresses, err := importIPAddresses(source) + c.Assert(err, jc.ErrorIsNil) + + c.Assert(addresses, jc.DeepEquals, initial.IPAddresses_) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/linklayerdevice.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/linklayerdevice.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/linklayerdevice.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/linklayerdevice.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,174 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package description + +import ( + "github.com/juju/errors" + "github.com/juju/schema" +) + +type linklayerdevices struct { + Version int `yaml:"version"` + LinkLayerDevices_ []*linklayerdevice `yaml:"linklayerdevices"` +} + +type linklayerdevice struct { + Name_ string `yaml:"name"` + MTU_ uint `yaml:"mtu"` + ProviderID_ string `yaml:"provider-id,omitempty"` + MachineID_ string `yaml:"machineid"` + Type_ string `yaml:"type"` + MACAddress_ string `yaml:"macaddress"` + IsAutoStart_ bool `yaml:"isautostart"` + IsUp_ bool `yaml:"isup"` + ParentName_ string `yaml:"parentname"` +} + +// ProviderID implements LinkLayerDevice. +func (i *linklayerdevice) ProviderID() string { + return i.ProviderID_ +} + +// MachineID implements LinkLayerDevice. +func (i *linklayerdevice) MachineID() string { + return i.MachineID_ +} + +// Name implements LinkLayerDevice. +func (i *linklayerdevice) Name() string { + return i.Name_ +} + +// MTU implements LinkLayerDevice. +func (i *linklayerdevice) MTU() uint { + return i.MTU_ +} + +// Type implements LinkLayerDevice. +func (i *linklayerdevice) Type() string { + return i.Type_ +} + +// MACAddress implements LinkLayerDevice. +func (i *linklayerdevice) MACAddress() string { + return i.MACAddress_ +} + +// IsAutoStart implements LinkLayerDevice. +func (i *linklayerdevice) IsAutoStart() bool { + return i.IsAutoStart_ +} + +// IsUp implements LinkLayerDevice. +func (i *linklayerdevice) IsUp() bool { + return i.IsUp_ +} + +// ParentName implements LinkLayerDevice. +func (i *linklayerdevice) ParentName() string { + return i.ParentName_ +} + +// LinkLayerDeviceArgs is an argument struct used to create a +// new internal linklayerdevice type that supports the LinkLayerDevice interface. +type LinkLayerDeviceArgs struct { + Name string + MTU uint + ProviderID string + MachineID string + Type string + MACAddress string + IsAutoStart bool + IsUp bool + ParentName string +} + +func newLinkLayerDevice(args LinkLayerDeviceArgs) *linklayerdevice { + return &linklayerdevice{ + ProviderID_: args.ProviderID, + MachineID_: args.MachineID, + Name_: args.Name, + MTU_: args.MTU, + Type_: args.Type, + MACAddress_: args.MACAddress, + IsAutoStart_: args.IsAutoStart, + IsUp_: args.IsUp, + ParentName_: args.ParentName, + } +} + +func importLinkLayerDevices(source map[string]interface{}) ([]*linklayerdevice, error) { + checker := versionedChecker("linklayerdevices") + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "linklayerdevices version schema check failed") + } + valid := coerced.(map[string]interface{}) + + version := int(valid["version"].(int64)) + importFunc, ok := linklayerdeviceDeserializationFuncs[version] + if !ok { + return nil, errors.NotValidf("version %d", version) + } + sourceList := valid["linklayerdevices"].([]interface{}) + return importLinkLayerDeviceList(sourceList, importFunc) +} + +func importLinkLayerDeviceList(sourceList []interface{}, importFunc linklayerdeviceDeserializationFunc) ([]*linklayerdevice, error) { + result := make([]*linklayerdevice, 0, len(sourceList)) + for i, value := range sourceList { + source, ok := value.(map[string]interface{}) + if !ok { + return nil, errors.Errorf("unexpected value for linklayerdevice %d, %T", i, value) + } + linklayerdevice, err := importFunc(source) + if err != nil { + return nil, errors.Annotatef(err, "linklayerdevice %d", i) + } + result = append(result, linklayerdevice) + } + return result, nil +} + +type linklayerdeviceDeserializationFunc func(map[string]interface{}) (*linklayerdevice, error) + +var linklayerdeviceDeserializationFuncs = map[int]linklayerdeviceDeserializationFunc{ + 1: importLinkLayerDeviceV1, +} + +func importLinkLayerDeviceV1(source map[string]interface{}) (*linklayerdevice, error) { + fields := schema.Fields{ + "provider-id": schema.String(), + "machineid": schema.String(), + "name": schema.String(), + "mtu": schema.Int(), + "type": schema.String(), + "macaddress": schema.String(), + "isautostart": schema.Bool(), + "isup": schema.Bool(), + "parentname": schema.String(), + } + // Some values don't have to be there. + defaults := schema.Defaults{ + "provider-id": "", + } + checker := schema.FieldMap(fields, defaults) + + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "linklayerdevice v1 schema check failed") + } + valid := coerced.(map[string]interface{}) + return &linklayerdevice{ + ProviderID_: valid["provider-id"].(string), + MachineID_: valid["machineid"].(string), + Name_: valid["name"].(string), + MTU_: uint(valid["mtu"].(int64)), + Type_: valid["type"].(string), + MACAddress_: valid["macaddress"].(string), + IsAutoStart_: valid["isautostart"].(bool), + IsUp_: valid["isup"].(bool), + ParentName_: valid["parentname"].(string), + }, nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/linklayerdevice_test.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/linklayerdevice_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/linklayerdevice_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/linklayerdevice_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,84 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package description + +import ( + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/yaml.v2" +) + +type LinkLayerDeviceSerializationSuite struct { + SliceSerializationSuite +} + +var _ = gc.Suite(&LinkLayerDeviceSerializationSuite{}) + +func (s *LinkLayerDeviceSerializationSuite) SetUpTest(c *gc.C) { + s.SliceSerializationSuite.SetUpTest(c) + s.importName = "linklayerdevices" + s.sliceName = "linklayerdevices" + s.importFunc = func(m map[string]interface{}) (interface{}, error) { + return importLinkLayerDevices(m) + } + s.testFields = func(m map[string]interface{}) { + m["linklayerdevices"] = []interface{}{} + } +} + +func (s *LinkLayerDeviceSerializationSuite) TestNewLinkLayerDevice(c *gc.C) { + args := LinkLayerDeviceArgs{ + ProviderID: "magic", + MachineID: "bar", + Name: "foo", + MTU: 54, + Type: "loopback", + MACAddress: "DEADBEEF", + IsAutoStart: true, + IsUp: true, + ParentName: "bam", + } + device := newLinkLayerDevice(args) + c.Assert(device.ProviderID(), gc.Equals, args.ProviderID) + c.Assert(device.MachineID(), gc.Equals, args.MachineID) + c.Assert(device.Name(), gc.Equals, args.Name) + c.Assert(device.MTU(), gc.Equals, args.MTU) + c.Assert(device.Type(), gc.Equals, args.Type) + c.Assert(device.MACAddress(), gc.Equals, args.MACAddress) + c.Assert(device.IsAutoStart(), gc.Equals, args.IsAutoStart) + c.Assert(device.IsUp(), gc.Equals, args.IsUp) + c.Assert(device.ParentName(), gc.Equals, args.ParentName) +} + +func (s *LinkLayerDeviceSerializationSuite) TestParsingSerializedData(c *gc.C) { + initial := linklayerdevices{ + Version: 1, + LinkLayerDevices_: []*linklayerdevice{ + newLinkLayerDevice(LinkLayerDeviceArgs{ + ProviderID: "magic", + MachineID: "bar", + Name: "foo", + MTU: 54, + Type: "loopback", + MACAddress: "DEADBEEF", + IsAutoStart: true, + IsUp: true, + ParentName: "bam", + }), + newLinkLayerDevice(LinkLayerDeviceArgs{Name: "weeee"}), + }, + } + + bytes, err := yaml.Marshal(initial) + c.Assert(err, jc.ErrorIsNil) + + var source map[string]interface{} + err = yaml.Unmarshal(bytes, &source) + c.Assert(err, jc.ErrorIsNil) + + devices, err := importLinkLayerDevices(source) + c.Assert(err, jc.ErrorIsNil) + + c.Assert(devices, jc.DeepEquals, initial.LinkLayerDevices_) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/machine.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/machine.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/machine.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/machine.go 2016-08-16 08:56:25.000000000 +0000 @@ -45,6 +45,8 @@ Annotations_ `yaml:"annotations,omitempty"` Constraints_ *constraints `yaml:"constraints,omitempty"` + + BlockDevices_ blockdevices `yaml:"block-devices,omitempty"` } // MachineArgs is an argument struct used to add a machine to the Model. @@ -82,6 +84,7 @@ copy(supported, *args.SupportedContainers) m.SupportedContainers_ = &supported } + m.setBlockDevices(nil) return m } @@ -246,6 +249,27 @@ return result } +// BlockDevices implements Machine. +func (m *machine) BlockDevices() []BlockDevice { + var result []BlockDevice + for _, device := range m.BlockDevices_.BlockDevices_ { + result = append(result, device) + } + return result +} + +// AddBlockDevice implements Machine. +func (m *machine) AddBlockDevice(args BlockDeviceArgs) BlockDevice { + return m.BlockDevices_.add(args) +} + +func (m *machine) setBlockDevices(devices []*blockdevice) { + m.BlockDevices_ = blockdevices{ + Version: 1, + BlockDevices_: devices, + } +} + // AddContainer implements Machine. func (m *machine) AddContainer(args MachineArgs) Machine { container := newMachine(args) @@ -379,6 +403,8 @@ "machine-addresses": schema.List(schema.StringMap(schema.Any())), "preferred-public-address": schema.StringMap(schema.Any()), "preferred-private-address": schema.StringMap(schema.Any()), + + "block-devices": schema.StringMap(schema.Any()), } defaults := schema.Defaults{ @@ -389,6 +415,7 @@ "instance": schema.Omit, "supported-containers": schema.Omit, "opened-ports": schema.Omit, + "block-devices": schema.Omit, "provider-addresses": schema.Omit, "machine-addresses": schema.Omit, "preferred-public-address": schema.Omit, @@ -414,6 +441,7 @@ Series_: valid["series"].(string), ContainerType_: valid["container-type"].(string), StatusHistory_: newStatusHistory(), + Jobs_: convertToStringSlice(valid["jobs"]), } result.importAnnotations(valid) if err := result.importStatusHistory(valid); err != nil { @@ -428,11 +456,6 @@ result.Constraints_ = constraints } - if jobs := valid["jobs"].([]interface{}); len(jobs) > 0 { - for _, job := range jobs { - result.Jobs_ = append(result.Jobs_, job.(string)) - } - } if supported, ok := valid["supported-containers"]; ok { supportedList := supported.([]interface{}) s := make([]string, len(supportedList)) @@ -450,6 +473,16 @@ result.Instance_ = instance } + if blockDeviceMap, ok := valid["block-devices"]; ok { + devices, err := importBlockDevices(blockDeviceMap.(map[string]interface{})) + if err != nil { + return nil, errors.Trace(err) + } + result.setBlockDevices(devices) + } else { + result.setBlockDevices(nil) + } + // Tools and status are required, so we expect them to be there. tools, err := importAgentTools(valid["tools"].(map[string]interface{})) if err != nil { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/machine_test.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/machine_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/machine_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/machine_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -49,6 +49,7 @@ "containers": containers, "status": minimalStatusMap(), "status-history": emptyStatusHistoryMap(), + "block-devices": emptyBlockDeviceMap(), } } @@ -283,6 +284,7 @@ m.SetTools(minimalAgentToolsArgs()) m.SetStatus(minimalStatusArgs()) m.SetInstance(minimalCloudInstanceArgs()) + m.AddBlockDevice(allBlockDeviceArgs()) s.addOpenedPorts(m) // Just use one set of address args for both machine and provider. diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/model.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/model.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/model.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/model.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,7 +4,9 @@ package description import ( + "net" "sort" + "strings" "github.com/juju/errors" "github.com/juju/schema" @@ -43,6 +45,13 @@ m.setMachines(nil) m.setApplications(nil) m.setRelations(nil) + m.setSpaces(nil) + m.setLinkLayerDevices(nil) + m.setSubnets(nil) + m.setIPAddresses(nil) + m.setSSHHostKeys(nil) + m.setVolumes(nil) + m.setFilesystems(nil) return m } @@ -69,6 +78,33 @@ return model, nil } +// parseLinkLayerDeviceGlobalKey is used to validate that the parent device +// referenced by a LinkLayerDevice exists. Copied from state to avoid exporting +// and will be replaced by device.ParentMachineID() at some point. +func parseLinkLayerDeviceGlobalKey(globalKey string) (machineID, deviceName string, canBeGlobalKey bool) { + if !strings.Contains(globalKey, "#") { + // Can't be a global key. + return "", "", false + } + keyParts := strings.Split(globalKey, "#") + if len(keyParts) != 4 || (keyParts[0] != "m" && keyParts[2] != "d") { + // Invalid global key format. + return "", "", true + } + machineID, deviceName = keyParts[1], keyParts[3] + return machineID, deviceName, true +} + +// parentId returns the id of the host machine if machineId a container id, or "" +// if machineId is not for a container. +func parentId(machineId string) string { + idParts := strings.Split(machineId, "/") + if len(idParts) < 3 { + return "" + } + return strings.Join(idParts[:len(idParts)-2], "/") +} + type model struct { Version int `yaml:"version"` @@ -78,10 +114,16 @@ LatestToolsVersion_ version.Number `yaml:"latest-tools,omitempty"` - Users_ users `yaml:"users"` - Machines_ machines `yaml:"machines"` - Applications_ applications `yaml:"applications"` - Relations_ relations `yaml:"relations"` + Users_ users `yaml:"users"` + Machines_ machines `yaml:"machines"` + Applications_ applications `yaml:"applications"` + Relations_ relations `yaml:"relations"` + Spaces_ spaces `yaml:"spaces"` + LinkLayerDevices_ linklayerdevices `yaml:"linklayerdevices"` + IPAddresses_ ipaddresses `yaml:"ipaddresses"` + Subnets_ subnets `yaml:"subnets"` + + SSHHostKeys_ sshHostKeys `yaml:"sshhostkeys"` Sequences_ map[string]int `yaml:"sequences"` @@ -94,8 +136,9 @@ CloudCredential_ string `yaml:"cloud-credential,omitempty"` // TODO: - // Spaces - // Storage + // Storage... + Volumes_ volumes `yaml:"volumes"` + Filesystems_ filesystems `yaml:"filesystems"` } func (m *model) Tag() names.ModelTag { @@ -244,6 +287,121 @@ } } +// Spaces implements Model. +func (m *model) Spaces() []Space { + var result []Space + for _, space := range m.Spaces_.Spaces_ { + result = append(result, space) + } + return result +} + +// AddSpace implements Model. +func (m *model) AddSpace(args SpaceArgs) Space { + space := newSpace(args) + m.Spaces_.Spaces_ = append(m.Spaces_.Spaces_, space) + return space +} + +func (m *model) setSpaces(spaceList []*space) { + m.Spaces_ = spaces{ + Version: 1, + Spaces_: spaceList, + } +} + +// LinkLayerDevices implements Model. +func (m *model) LinkLayerDevices() []LinkLayerDevice { + var result []LinkLayerDevice + for _, device := range m.LinkLayerDevices_.LinkLayerDevices_ { + result = append(result, device) + } + return result +} + +// AddLinkLayerDevice implements Model. +func (m *model) AddLinkLayerDevice(args LinkLayerDeviceArgs) LinkLayerDevice { + device := newLinkLayerDevice(args) + m.LinkLayerDevices_.LinkLayerDevices_ = append(m.LinkLayerDevices_.LinkLayerDevices_, device) + return device +} + +func (m *model) setLinkLayerDevices(devicesList []*linklayerdevice) { + m.LinkLayerDevices_ = linklayerdevices{ + Version: 1, + LinkLayerDevices_: devicesList, + } +} + +// Subnets implements Model. +func (m *model) Subnets() []Subnet { + var result []Subnet + for _, subnet := range m.Subnets_.Subnets_ { + result = append(result, subnet) + } + return result +} + +// AddSubnet implemets Model. +func (m *model) AddSubnet(args SubnetArgs) Subnet { + subnet := newSubnet(args) + m.Subnets_.Subnets_ = append(m.Subnets_.Subnets_, subnet) + return subnet +} + +func (m *model) setSubnets(subnetList []*subnet) { + m.Subnets_ = subnets{ + Version: 1, + Subnets_: subnetList, + } +} + +// IPAddresses implements Model. +func (m *model) IPAddresses() []IPAddress { + var result []IPAddress + for _, addr := range m.IPAddresses_.IPAddresses_ { + result = append(result, addr) + } + return result +} + +// AddIPAddress implements Model. +func (m *model) AddIPAddress(args IPAddressArgs) IPAddress { + addr := newIPAddress(args) + m.IPAddresses_.IPAddresses_ = append(m.IPAddresses_.IPAddresses_, addr) + return addr +} + +func (m *model) setIPAddresses(addressesList []*ipaddress) { + m.IPAddresses_ = ipaddresses{ + Version: 1, + IPAddresses_: addressesList, + } +} + +// SSHHostKeys implements Model. +func (m *model) SSHHostKeys() []SSHHostKey { + var result []SSHHostKey + for _, addr := range m.SSHHostKeys_.SSHHostKeys_ { + result = append(result, addr) + } + return result +} + +// AddSSHHostKey implements Model. +func (m *model) AddSSHHostKey(args SSHHostKeyArgs) SSHHostKey { + addr := newSSHHostKey(args) + m.SSHHostKeys_.SSHHostKeys_ = append(m.SSHHostKeys_.SSHHostKeys_, addr) + return addr +} + +func (m *model) setSSHHostKeys(addressesList []*sshHostKey) { + m.SSHHostKeys_ = sshHostKeys{ + Version: 1, + SSHHostKeys_: addressesList, + } +} + // Sequences implements Model. func (m *model) Sequences() map[string]int { return m.Sequences_ @@ -282,6 +440,52 @@ return m.CloudCredential_ } +// Volumes implements Model. +func (m *model) Volumes() []Volume { + var result []Volume + for _, volume := range m.Volumes_.Volumes_ { + result = append(result, volume) + } + return result +} + +// AddVolume implemets Model. +func (m *model) AddVolume(args VolumeArgs) Volume { + volume := newVolume(args) + m.Volumes_.Volumes_ = append(m.Volumes_.Volumes_, volume) + return volume +} + +func (m *model) setVolumes(volumeList []*volume) { + m.Volumes_ = volumes{ + Version: 1, + Volumes_: volumeList, + } +} + +// Filesystems implements Model. +func (m *model) Filesystems() []Filesystem { + var result []Filesystem + for _, filesystem := range m.Filesystems_.Filesystems_ { + result = append(result, filesystem) + } + return result +} + +// AddFilesystem implemets Model. +func (m *model) AddFilesystem(args FilesystemArgs) Filesystem { + filesystem := newFilesystem(args) + m.Filesystems_.Filesystems_ = append(m.Filesystems_.Filesystems_, filesystem) + return filesystem +} + +func (m *model) setFilesystems(filesystemList []*filesystem) { + m.Filesystems_ = filesystems{ + Version: 1, + Filesystems_: filesystemList, + } +} + // Validate implements Model. func (m *model) Validate() error { // A model needs an owner. @@ -314,7 +518,177 @@ return errors.Errorf("unknown unit names in open ports: %s", unknownUnitsWithPorts.SortedValues()) } - return m.validateRelations() + err := m.validateRelations() + if err != nil { + return errors.Trace(err) + } + + err = m.validateSubnets() + if err != nil { + return errors.Trace(err) + } + + err = m.validateLinkLayerDevices() + if err != nil { + return errors.Trace(err) + } + err = m.validateAddresses() + if err != nil { + return errors.Trace(err) + } + err = m.validateVolumes() + if err != nil { + return errors.Trace(err) + } + err = m.validateFilesystems() + if err != nil { + return errors.Trace(err) + } + + return nil +} + +func (m *model) validateVolumes() error { + for i, volume := range m.Volumes_.Volumes_ { + if err := volume.Validate(); err != nil { + return errors.Annotatef(err, "volume[%d]", i) + } + } + return nil +} + +func (m *model) validateFilesystems() error { + for i, filesystem := range m.Filesystems_.Filesystems_ { + if err := filesystem.Validate(); err != nil { + return errors.Annotatef(err, "filesystem[%d]", i) + } + } + return nil +} + +// validateSubnets makes sure that any spaces referenced by subnets exist. +func (m *model) validateSubnets() error { + spaceNames := set.NewStrings() + for _, space := range m.Spaces_.Spaces_ { + spaceNames.Add(space.Name()) + } + for _, subnet := range m.Subnets_.Subnets_ { + if subnet.SpaceName() == "" { + continue + } + if !spaceNames.Contains(subnet.SpaceName()) { + return errors.Errorf("subnet %q references non-existent space %q", subnet.CIDR(), subnet.SpaceName()) + } + } + + return nil +} + +func (m *model) machineMaps() (map[string]Machine, map[string]map[string]LinkLayerDevice) { + machineIDs := make(map[string]Machine) + for _, machine := range m.Machines_.Machines_ { + addMachinesToMap(machine, machineIDs) + } + + // Build a map of all devices for each machine. + machineDevices := make(map[string]map[string]LinkLayerDevice) + for _, device := range m.LinkLayerDevices_.LinkLayerDevices_ { + _, ok := machineDevices[device.MachineID()] + if !ok { + machineDevices[device.MachineID()] = make(map[string]LinkLayerDevice) + } + machineDevices[device.MachineID()][device.Name()] = device + } + return machineIDs, machineDevices +} + +func addMachinesToMap(machine Machine, machineIDs map[string]Machine) { + machineIDs[machine.Id()] = machine + for _, container := range machine.Containers() { + addMachinesToMap(container, machineIDs) + } +} + +// validateAddresses makes sure that the machine and device referenced by IP +// addresses exist. +func (m *model) validateAddresses() error { + machineIDs, machineDevices := m.machineMaps() + for _, addr := range m.IPAddresses_.IPAddresses_ { + _, ok := machineIDs[addr.MachineID()] + if !ok { + return errors.Errorf("ip address %q references non-existent machine %q", addr.Value(), addr.MachineID()) + } + _, ok = machineDevices[addr.MachineID()][addr.DeviceName()] + if !ok { + return errors.Errorf("ip address %q references non-existent device %q", addr.Value(), addr.DeviceName()) + } + if ip := net.ParseIP(addr.Value()); ip == nil { + return errors.Errorf("ip address has invalid value %q", addr.Value()) + } + if addr.SubnetCIDR() == "" { + return errors.Errorf("ip address %q has empty subnet CIDR", addr.Value()) + } + if _, _, err := net.ParseCIDR(addr.SubnetCIDR()); err != nil { + return errors.Errorf("ip address %q has invalid subnet CIDR %q", addr.Value(), addr.SubnetCIDR()) + } + + if addr.GatewayAddress() != "" { + if ip := net.ParseIP(addr.GatewayAddress()); ip == nil { + return errors.Errorf("ip address %q has invalid gateway address %q", addr.Value(), addr.GatewayAddress()) + } + } + } + return nil +} + +// validateLinkLayerDevices makes sure that any machines referenced by link +// layer devices exist. +func (m *model) validateLinkLayerDevices() error { + machineIDs, machineDevices := m.machineMaps() + for _, device := range m.LinkLayerDevices_.LinkLayerDevices_ { + machine, ok := machineIDs[device.MachineID()] + if !ok { + return errors.Errorf("device %q references non-existent machine %q", device.Name(), device.MachineID()) + } + if device.Name() == "" { + return errors.Errorf("device has empty name: %#v", device) + } + if device.MACAddress() != "" { + if _, err := net.ParseMAC(device.MACAddress()); err != nil { + return errors.Errorf("device %q has invalid MACAddress %q", device.Name(), device.MACAddress()) + } + } + if device.ParentName() == "" { + continue + } + hostMachineID, parentDeviceName, canBeGlobalKey := parseLinkLayerDeviceGlobalKey(device.ParentName()) + if !canBeGlobalKey { + hostMachineID = device.MachineID() + parentDeviceName = device.ParentName() + } + parentDevice, ok := machineDevices[hostMachineID][parentDeviceName] + if !ok { + return errors.Errorf("device %q has non-existent parent %q", device.Name(), parentDeviceName) + } + if !canBeGlobalKey { + if device.Name() == parentDeviceName { + return errors.Errorf("device %q is its own parent", device.Name()) + } + continue + } + // The device is on a container. + if parentDevice.Type() != "bridge" { + return errors.Errorf("device %q on a container but not a bridge", device.Name()) + } + parentId := parentId(machine.Id()) + if parentId == "" { + return errors.Errorf("ParentName %q for non-container machine %q", device.ParentName(), machine.Id()) + } + if parentDevice.MachineID() != parentId { + return errors.Errorf("parent machine of device %q not host machine %q", device.Name(), parentId) + } + } + return nil } // validateRelations makes sure that for each endpoint in each relation there @@ -367,17 +741,24 @@ func importModelV1(source map[string]interface{}) (*model, error) { fields := schema.Fields{ - "owner": schema.String(), - "cloud": schema.String(), - "cloud-region": schema.String(), - "config": schema.StringMap(schema.Any()), - "latest-tools": schema.String(), - "blocks": schema.StringMap(schema.String()), - "users": schema.StringMap(schema.Any()), - "machines": schema.StringMap(schema.Any()), - "applications": schema.StringMap(schema.Any()), - "relations": schema.StringMap(schema.Any()), - "sequences": schema.StringMap(schema.Int()), + "owner": schema.String(), + "cloud": schema.String(), + "cloud-region": schema.String(), + "config": schema.StringMap(schema.Any()), + "latest-tools": schema.String(), + "blocks": schema.StringMap(schema.String()), + "users": schema.StringMap(schema.Any()), + "machines": schema.StringMap(schema.Any()), + "applications": schema.StringMap(schema.Any()), + "relations": schema.StringMap(schema.Any()), + "sshhostkeys": schema.StringMap(schema.Any()), + "ipaddresses": schema.StringMap(schema.Any()), + "spaces": schema.StringMap(schema.Any()), + "subnets": schema.StringMap(schema.Any()), + "linklayerdevices": schema.StringMap(schema.Any()), + "volumes": schema.StringMap(schema.Any()), + "filesystems": schema.StringMap(schema.Any()), + "sequences": schema.StringMap(schema.Int()), } // Some values don't have to be there. defaults := schema.Defaults{ @@ -463,5 +844,52 @@ } result.setRelations(relations) + spaceMap := valid["spaces"].(map[string]interface{}) + spaces, err := importSpaces(spaceMap) + if err != nil { + return nil, errors.Annotate(err, "spaces") + } + result.setSpaces(spaces) + + deviceMap := valid["linklayerdevices"].(map[string]interface{}) + devices, err := importLinkLayerDevices(deviceMap) + if err != nil { + return nil, errors.Annotate(err, "linklayerdevices") + } + result.setLinkLayerDevices(devices) + + subnetsMap := valid["subnets"].(map[string]interface{}) + subnets, err := importSubnets(subnetsMap) + if err != nil { + return nil, errors.Annotate(err, "subnets") + } + result.setSubnets(subnets) + + addressMap := valid["ipaddresses"].(map[string]interface{}) + addresses, err := importIPAddresses(addressMap) + if err != nil { + return nil, errors.Annotate(err, "ipaddresses") + } + result.setIPAddresses(addresses) + + sshHostKeyMap := valid["sshhostkeys"].(map[string]interface{}) + hostKeys, err := importSSHHostKeys(sshHostKeyMap) + if err != nil { + return nil, errors.Annotate(err, "sshhostkeys") + } + result.setSSHHostKeys(hostKeys) + + volumes, err := importVolumes(valid["volumes"].(map[string]interface{})) + if err != nil { + return nil, errors.Annotate(err, "volumes") + } + result.setVolumes(volumes) + + filesystems, err := importFilesystems(valid["filesystems"].(map[string]interface{})) + if err != nil { + return nil, errors.Annotate(err, "filesystems") + } + result.setFilesystems(filesystems) + return result, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/model_test.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/model_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/model_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/model_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -331,3 +331,353 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(model, jc.DeepEquals, initial) } + +func (s *ModelSerializationSuite) TestModelValidationChecksSubnets(c *gc.C) { + model := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + model.AddSubnet(SubnetArgs{CIDR: "10.0.0.0/24", SpaceName: "foo"}) + model.AddSubnet(SubnetArgs{CIDR: "10.0.1.0/24"}) + err := model.Validate() + c.Assert(err, gc.ErrorMatches, `subnet "10.0.0.0/24" references non-existent space "foo"`) + model.AddSpace(SpaceArgs{Name: "foo"}) + err = model.Validate() + c.Assert(err, jc.ErrorIsNil) +} + +func (s *ModelSerializationSuite) TestModelValidationChecksAddressMachineID(c *gc.C) { + model := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + model.AddIPAddress(IPAddressArgs{Value: "192.168.1.0", MachineID: "42"}) + err := model.Validate() + c.Assert(err, gc.ErrorMatches, `ip address "192.168.1.0" references non-existent machine "42"`) +} + +func (s *ModelSerializationSuite) TestModelValidationChecksAddressDeviceName(c *gc.C) { + model := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + args := IPAddressArgs{Value: "192.168.1.0", MachineID: "42", DeviceName: "foo"} + model.AddIPAddress(args) + s.addMachineToModel(model, "42") + err := model.Validate() + c.Assert(err, gc.ErrorMatches, `ip address "192.168.1.0" references non-existent device "foo"`) +} + +func (s *ModelSerializationSuite) TestModelValidationChecksAddressValueEmpty(c *gc.C) { + model := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + args := IPAddressArgs{MachineID: "42", DeviceName: "foo"} + model.AddIPAddress(args) + s.addMachineToModel(model, "42") + model.AddLinkLayerDevice(LinkLayerDeviceArgs{Name: "foo", MachineID: "42"}) + err := model.Validate() + c.Assert(err, gc.ErrorMatches, `ip address has invalid value ""`) +} + +func (s *ModelSerializationSuite) TestModelValidationChecksAddressValueInvalid(c *gc.C) { + model := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + args := IPAddressArgs{MachineID: "42", DeviceName: "foo", Value: "foobar"} + model.AddIPAddress(args) + s.addMachineToModel(model, "42") + model.AddLinkLayerDevice(LinkLayerDeviceArgs{Name: "foo", MachineID: "42"}) + err := model.Validate() + c.Assert(err, gc.ErrorMatches, `ip address has invalid value "foobar"`) +} + +func (s *ModelSerializationSuite) TestModelValidationChecksAddressSubnetEmpty(c *gc.C) { + model := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + args := IPAddressArgs{MachineID: "42", DeviceName: "foo", Value: "192.168.1.1"} + model.AddIPAddress(args) + s.addMachineToModel(model, "42") + model.AddLinkLayerDevice(LinkLayerDeviceArgs{Name: "foo", MachineID: "42"}) + err := model.Validate() + c.Assert(err, gc.ErrorMatches, `ip address "192.168.1.1" has empty subnet CIDR`) +} + +func (s *ModelSerializationSuite) TestModelValidationChecksAddressSubnetInvalid(c *gc.C) { + model := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + args := IPAddressArgs{ + MachineID: "42", + DeviceName: "foo", + Value: "192.168.1.1", + SubnetCIDR: "foo", + } + model.AddIPAddress(args) + s.addMachineToModel(model, "42") + model.AddLinkLayerDevice(LinkLayerDeviceArgs{Name: "foo", MachineID: "42"}) + err := model.Validate() + c.Assert(err, gc.ErrorMatches, `ip address "192.168.1.1" has invalid subnet CIDR "foo"`) +} + +func (s *ModelSerializationSuite) TestModelValidationChecksAddressSucceeds(c *gc.C) { + model := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + args := IPAddressArgs{ + MachineID: "42", + DeviceName: "foo", + Value: "192.168.1.1", + SubnetCIDR: "192.168.1.0/24", + } + model.AddIPAddress(args) + s.addMachineToModel(model, "42") + model.AddLinkLayerDevice(LinkLayerDeviceArgs{Name: "foo", MachineID: "42"}) + err := model.Validate() + c.Assert(err, jc.ErrorIsNil) +} + +func (s *ModelSerializationSuite) TestModelValidationChecksAddressGatewayAddressInvalid(c *gc.C) { + model := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + args := IPAddressArgs{ + MachineID: "42", + DeviceName: "foo", + Value: "192.168.1.1", + SubnetCIDR: "192.168.1.0/24", + GatewayAddress: "foo", + } + model.AddIPAddress(args) + s.addMachineToModel(model, "42") + model.AddLinkLayerDevice(LinkLayerDeviceArgs{Name: "foo", MachineID: "42"}) + err := model.Validate() + c.Assert(err, gc.ErrorMatches, `ip address "192.168.1.1" has invalid gateway address "foo"`) +} + +func (s *ModelSerializationSuite) TestModelValidationChecksAddressGatewayAddressValid(c *gc.C) { + model := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + args := IPAddressArgs{ + MachineID: "42", + DeviceName: "foo", + Value: "192.168.1.2", + SubnetCIDR: "192.168.1.0/24", + GatewayAddress: "192.168.1.1", + } + model.AddIPAddress(args) + s.addMachineToModel(model, "42") + model.AddLinkLayerDevice(LinkLayerDeviceArgs{Name: "foo", MachineID: "42"}) + err := model.Validate() + c.Assert(err, jc.ErrorIsNil) +} + +func (s *ModelSerializationSuite) TestModelValidationChecksLinkLayerDeviceMachineId(c *gc.C) { + model := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + model.AddLinkLayerDevice(LinkLayerDeviceArgs{Name: "foo", MachineID: "42"}) + err := model.Validate() + c.Assert(err, gc.ErrorMatches, `device "foo" references non-existent machine "42"`) + s.addMachineToModel(model, "42") + err = model.Validate() + c.Assert(err, jc.ErrorIsNil) +} + +func (s *ModelSerializationSuite) TestModelValidationChecksLinkLayerName(c *gc.C) { + model := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + model.AddLinkLayerDevice(LinkLayerDeviceArgs{MachineID: "42"}) + s.addMachineToModel(model, "42") + err := model.Validate() + c.Assert(err, gc.ErrorMatches, "device has empty name.*") +} + +func (s *ModelSerializationSuite) TestModelValidationChecksLinkLayerMACAddress(c *gc.C) { + model := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + args := LinkLayerDeviceArgs{MachineID: "42", Name: "foo", MACAddress: "DEADBEEF"} + model.AddLinkLayerDevice(args) + s.addMachineToModel(model, "42") + err := model.Validate() + c.Assert(err, gc.ErrorMatches, `device "foo" has invalid MACAddress "DEADBEEF"`) +} + +func (s *ModelSerializationSuite) TestModelValidationChecksParentExists(c *gc.C) { + model := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + args := LinkLayerDeviceArgs{MachineID: "42", Name: "foo", ParentName: "bar", MACAddress: "01:23:45:67:89:ab"} + model.AddLinkLayerDevice(args) + s.addMachineToModel(model, "42") + err := model.Validate() + c.Assert(err, gc.ErrorMatches, `device "foo" has non-existent parent "bar"`) + model.AddLinkLayerDevice(LinkLayerDeviceArgs{Name: "bar", MachineID: "42"}) + err = model.Validate() + c.Assert(err, jc.ErrorIsNil) +} + +func (s *ModelSerializationSuite) TestModelValidationChecksParentIsNotItself(c *gc.C) { + model := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + args := LinkLayerDeviceArgs{MachineID: "42", Name: "foo", ParentName: "foo"} + model.AddLinkLayerDevice(args) + s.addMachineToModel(model, "42") + err := model.Validate() + c.Assert(err, gc.ErrorMatches, `device "foo" is its own parent`) +} + +func (s *ModelSerializationSuite) TestModelValidationChecksParentIsABridge(c *gc.C) { + model := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + args := LinkLayerDeviceArgs{MachineID: "42", Name: "foo", ParentName: "m#43#d#bar"} + model.AddLinkLayerDevice(args) + args2 := LinkLayerDeviceArgs{MachineID: "43", Name: "bar"} + model.AddLinkLayerDevice(args2) + s.addMachineToModel(model, "42") + s.addMachineToModel(model, "43") + err := model.Validate() + c.Assert(err, gc.ErrorMatches, `device "foo" on a container but not a bridge`) +} + +func (s *ModelSerializationSuite) TestModelValidationChecksChildDeviceContained(c *gc.C) { + model := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + args := LinkLayerDeviceArgs{MachineID: "42", Name: "foo", ParentName: "m#43#d#bar"} + model.AddLinkLayerDevice(args) + args2 := LinkLayerDeviceArgs{MachineID: "43", Name: "bar", Type: "bridge"} + model.AddLinkLayerDevice(args2) + s.addMachineToModel(model, "42") + s.addMachineToModel(model, "43") + err := model.Validate() + c.Assert(err, gc.ErrorMatches, `ParentName "m#43#d#bar" for non-container machine "42"`) +} + +func (s *ModelSerializationSuite) TestModelValidationChecksParentOnHost(c *gc.C) { + model := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + args := LinkLayerDeviceArgs{MachineID: "41/lxd/0", Name: "foo", ParentName: "m#43#d#bar"} + model.AddLinkLayerDevice(args) + args2 := LinkLayerDeviceArgs{MachineID: "43", Name: "bar", Type: "bridge"} + model.AddLinkLayerDevice(args2) + machine := s.addMachineToModel(model, "41") + container := machine.AddContainer(MachineArgs{Id: names.NewMachineTag("41/lxd/0")}) + container.SetInstance(CloudInstanceArgs{InstanceId: "magic"}) + container.SetTools(minimalAgentToolsArgs()) + container.SetStatus(minimalStatusArgs()) + s.addMachineToModel(model, "43") + err := model.Validate() + c.Assert(err, gc.ErrorMatches, `parent machine of device "foo" not host machine "41"`) +} + +func (s *ModelSerializationSuite) TestModelValidationLinkLayerDeviceContainer(c *gc.C) { + model := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + args := LinkLayerDeviceArgs{MachineID: "43/lxd/0", Name: "foo", ParentName: "m#43#d#bar"} + model.AddLinkLayerDevice(args) + args2 := LinkLayerDeviceArgs{MachineID: "43", Name: "bar", Type: "bridge"} + model.AddLinkLayerDevice(args2) + machine := s.addMachineToModel(model, "43") + container := machine.AddContainer(MachineArgs{Id: names.NewMachineTag("43/lxd/0")}) + container.SetInstance(CloudInstanceArgs{InstanceId: "magic"}) + container.SetTools(minimalAgentToolsArgs()) + container.SetStatus(minimalStatusArgs()) + err := model.Validate() + c.Assert(err, jc.ErrorIsNil) +} + +func (s *ModelSerializationSuite) TestSpaces(c *gc.C) { + initial := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + space := initial.AddSpace(SpaceArgs{Name: "special"}) + c.Assert(space.Name(), gc.Equals, "special") + spaces := initial.Spaces() + c.Assert(spaces, gc.HasLen, 1) + c.Assert(spaces[0], gc.Equals, space) + + bytes, err := yaml.Marshal(initial) + c.Assert(err, jc.ErrorIsNil) + + model, err := Deserialize(bytes) + c.Assert(err, jc.ErrorIsNil) + c.Assert(model.Spaces(), jc.DeepEquals, spaces) + +} + +func (s *ModelSerializationSuite) TestLinkLayerDevice(c *gc.C) { + initial := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + device := initial.AddLinkLayerDevice(LinkLayerDeviceArgs{Name: "foo"}) + c.Assert(device.Name(), gc.Equals, "foo") + devices := initial.LinkLayerDevices() + c.Assert(devices, gc.HasLen, 1) + c.Assert(devices[0], jc.DeepEquals, device) + + bytes, err := yaml.Marshal(initial) + c.Assert(err, jc.ErrorIsNil) + + model, err := Deserialize(bytes) + c.Assert(err, jc.ErrorIsNil) + c.Assert(model.LinkLayerDevices(), jc.DeepEquals, devices) +} + +func (s *ModelSerializationSuite) TestSubnets(c *gc.C) { + initial := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + subnet := initial.AddSubnet(SubnetArgs{CIDR: "10.0.0.0/24"}) + c.Assert(subnet.CIDR(), gc.Equals, "10.0.0.0/24") + subnets := initial.Subnets() + c.Assert(subnets, gc.HasLen, 1) + c.Assert(subnets[0], jc.DeepEquals, subnet) + + bytes, err := yaml.Marshal(initial) + c.Assert(err, jc.ErrorIsNil) + + model, err := Deserialize(bytes) + c.Assert(err, jc.ErrorIsNil) + c.Assert(model.Subnets(), jc.DeepEquals, subnets) +} + +func (s *ModelSerializationSuite) TestIPAddress(c *gc.C) { + initial := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + addr := initial.AddIPAddress(IPAddressArgs{Value: "10.0.0.4"}) + c.Assert(addr.Value(), gc.Equals, "10.0.0.4") + addresses := initial.IPAddresses() + c.Assert(addresses, gc.HasLen, 1) + c.Assert(addresses[0], jc.DeepEquals, addr) + + bytes, err := yaml.Marshal(initial) + c.Assert(err, jc.ErrorIsNil) + + model, err := Deserialize(bytes) + c.Assert(err, jc.ErrorIsNil) + c.Assert(model.IPAddresses(), jc.DeepEquals, addresses) +} + +func (s *ModelSerializationSuite) TestSSHHostKey(c *gc.C) { + initial := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + key := initial.AddSSHHostKey(SSHHostKeyArgs{MachineID: "foo"}) + c.Assert(key.MachineID(), gc.Equals, "foo") + keys := initial.SSHHostKeys() + c.Assert(keys, gc.HasLen, 1) + c.Assert(keys[0], jc.DeepEquals, key) + + bytes, err := yaml.Marshal(initial) + c.Assert(err, jc.ErrorIsNil) + + model, err := Deserialize(bytes) + c.Assert(err, jc.ErrorIsNil) + c.Assert(model.SSHHostKeys(), jc.DeepEquals, keys) +} + +func (s *ModelSerializationSuite) TestVolumeValidation(c *gc.C) { + model := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + model.AddVolume(testVolumeArgs()) + err := model.Validate() + c.Assert(err, gc.ErrorMatches, `volume\[0\]: volume "1234" missing status not valid`) +} + +func (s *ModelSerializationSuite) TestVolumes(c *gc.C) { + initial := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + volume := initial.AddVolume(testVolumeArgs()) + volume.SetStatus(minimalStatusArgs()) + volumes := initial.Volumes() + c.Assert(volumes, gc.HasLen, 1) + c.Assert(volumes[0], gc.Equals, volume) + + bytes, err := yaml.Marshal(initial) + c.Assert(err, jc.ErrorIsNil) + + model, err := Deserialize(bytes) + c.Assert(err, jc.ErrorIsNil) + c.Assert(model.Volumes(), jc.DeepEquals, volumes) +} + +func (s *ModelSerializationSuite) TestFilesystemValidation(c *gc.C) { + model := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + model.AddFilesystem(testFilesystemArgs()) + err := model.Validate() + c.Assert(err, gc.ErrorMatches, `filesystem\[0\]: filesystem "1234" missing status not valid`) +} + +func (s *ModelSerializationSuite) TestFilesystems(c *gc.C) { + initial := NewModel(ModelArgs{Owner: names.NewUserTag("owner")}) + filesystem := initial.AddFilesystem(testFilesystemArgs()) + filesystem.SetStatus(minimalStatusArgs()) + filesystem.AddAttachment(testFilesystemAttachmentArgs()) + filesystems := initial.Filesystems() + c.Assert(filesystems, gc.HasLen, 1) + c.Assert(filesystems[0], gc.Equals, filesystem) + + bytes, err := yaml.Marshal(initial) + c.Assert(err, jc.ErrorIsNil) + + model, err := Deserialize(bytes) + c.Assert(err, jc.ErrorIsNil) + c.Assert(model.Filesystems(), jc.DeepEquals, filesystems) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/space.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/space.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/space.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/space.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,117 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package description + +import ( + "github.com/juju/errors" + "github.com/juju/schema" +) + +type spaces struct { + Version int `yaml:"version"` + Spaces_ []*space `yaml:"spaces"` +} + +type space struct { + Name_ string `yaml:"name"` + Public_ bool `yaml:"public"` + ProviderID_ string `yaml:"provider-id,omitempty"` +} + +// SpaceArgs is an argument struct used to create a new internal space +// type that supports the Space interface. +type SpaceArgs struct { + Name string + Public bool + ProviderID string +} + +func newSpace(args SpaceArgs) *space { + return &space{ + Name_: args.Name, + Public_: args.Public, + ProviderID_: args.ProviderID, + } +} + +// Name implements Space. +func (s *space) Name() string { + return s.Name_ +} + +// Public implements Space. +func (s *space) Public() bool { + return s.Public_ +} + +// ProviderID implements Space. +func (s *space) ProviderID() string { + return s.ProviderID_ +} + +func importSpaces(source map[string]interface{}) ([]*space, error) { + checker := versionedChecker("spaces") + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "spaces version schema check failed") + } + valid := coerced.(map[string]interface{}) + + version := int(valid["version"].(int64)) + importFunc, ok := spaceDeserializationFuncs[version] + if !ok { + return nil, errors.NotValidf("version %d", version) + } + sourceList := valid["spaces"].([]interface{}) + return importSpaceList(sourceList, importFunc) +} + +func importSpaceList(sourceList []interface{}, importFunc spaceDeserializationFunc) ([]*space, error) { + result := make([]*space, 0, len(sourceList)) + for i, value := range sourceList { + source, ok := value.(map[string]interface{}) + if !ok { + return nil, errors.Errorf("unexpected value for space %d, %T", i, value) + } + space, err := importFunc(source) + if err != nil { + return nil, errors.Annotatef(err, "space %d", i) + } + result = append(result, space) + } + return result, nil +} + +type spaceDeserializationFunc func(map[string]interface{}) (*space, error) + +var spaceDeserializationFuncs = map[int]spaceDeserializationFunc{ + 1: importSpaceV1, +} + +func importSpaceV1(source map[string]interface{}) (*space, error) { + fields := schema.Fields{ + "name": schema.String(), + "public": schema.Bool(), + "provider-id": schema.String(), + } + // Some values don't have to be there. + defaults := schema.Defaults{ + "provider-id": "", + } + checker := schema.FieldMap(fields, defaults) + + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "space v1 schema check failed") + } + valid := coerced.(map[string]interface{}) + // From here we know that the map returned from the schema coercion + // contains fields of the right type. + + return &space{ + Name_: valid["name"].(string), + Public_: valid["public"].(bool), + ProviderID_: valid["provider-id"].(string), + }, nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/space_test.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/space_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/space_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/space_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,66 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package description + +import ( + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/yaml.v2" +) + +type SpaceSerializationSuite struct { + SliceSerializationSuite +} + +var _ = gc.Suite(&SpaceSerializationSuite{}) + +func (s *SpaceSerializationSuite) SetUpTest(c *gc.C) { + s.SliceSerializationSuite.SetUpTest(c) + s.importName = "spaces" + s.sliceName = "spaces" + s.importFunc = func(m map[string]interface{}) (interface{}, error) { + return importSpaces(m) + } + s.testFields = func(m map[string]interface{}) { + m["spaces"] = []interface{}{} + } +} + +func (s *SpaceSerializationSuite) TestNewSpace(c *gc.C) { + args := SpaceArgs{ + Name: "special", + Public: true, + ProviderID: "magic", + } + space := newSpace(args) + c.Assert(space.Name(), gc.Equals, args.Name) + c.Assert(space.Public(), gc.Equals, args.Public) + c.Assert(space.ProviderID(), gc.Equals, args.ProviderID) +} + +func (s *SpaceSerializationSuite) TestParsingSerializedData(c *gc.C) { + initial := spaces{ + Version: 1, + Spaces_: []*space{ + newSpace(SpaceArgs{ + Name: "special", + Public: true, + ProviderID: "magic", + }), + newSpace(SpaceArgs{Name: "foo"}), + }, + } + + bytes, err := yaml.Marshal(initial) + c.Assert(err, jc.ErrorIsNil) + + var source map[string]interface{} + err = yaml.Unmarshal(bytes, &source) + c.Assert(err, jc.ErrorIsNil) + + spaces, err := importSpaces(source) + c.Assert(err, jc.ErrorIsNil) + + c.Assert(spaces, jc.DeepEquals, initial.Spaces_) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/sshhostkeys.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/sshhostkeys.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/sshhostkeys.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/sshhostkeys.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,107 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package description + +import ( + "github.com/juju/errors" + "github.com/juju/schema" +) + +type sshHostKeys struct { + Version int `yaml:"version"` + SSHHostKeys_ []*sshHostKey `yaml:"sshhostkeys"` +} + +type sshHostKey struct { + MachineID_ string `yaml:"machineid"` + Keys_ []string `yaml:"keys"` +} + +// MachineID implements SSHHostKey. +func (i *sshHostKey) MachineID() string { + return i.MachineID_ +} + +// Keys implements SSHHostKey. +func (i *sshHostKey) Keys() []string { + return i.Keys_ +} + +// SSHHostKeyArgs is an argument struct used to create a +// new internal sshHostKey type that supports the SSHHostKey interface. +type SSHHostKeyArgs struct { + MachineID string + Keys []string +} + +func newSSHHostKey(args SSHHostKeyArgs) *sshHostKey { + return &sshHostKey{ + MachineID_: args.MachineID, + Keys_: args.Keys, + } +} + +func importSSHHostKeys(source map[string]interface{}) ([]*sshHostKey, error) { + checker := versionedChecker("sshhostkeys") + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "sshhostkeys version schema check failed") + } + valid := coerced.(map[string]interface{}) + + version := int(valid["version"].(int64)) + importFunc, ok := sshHostKeyDeserializationFuncs[version] + if !ok { + return nil, errors.NotValidf("version %d", version) + } + sourceList := valid["sshhostkeys"].([]interface{}) + return importSSHHostKeyList(sourceList, importFunc) +} + +func importSSHHostKeyList(sourceList []interface{}, importFunc sshHostKeyDeserializationFunc) ([]*sshHostKey, error) { + result := make([]*sshHostKey, 0, len(sourceList)) + for i, value := range sourceList { + source, ok := value.(map[string]interface{}) + if !ok { + return nil, errors.Errorf("unexpected value for sshhostkey %d, %T", i, value) + } + sshHostKey, err := importFunc(source) + if err != nil { + return nil, errors.Annotatef(err, "sshhostkey %d", i) + } + result = append(result, sshHostKey) + } + return result, nil +} + +type sshHostKeyDeserializationFunc func(map[string]interface{}) (*sshHostKey, error) + +var sshHostKeyDeserializationFuncs = map[int]sshHostKeyDeserializationFunc{ + 1: importSSHHostKeyV1, +} + +func importSSHHostKeyV1(source map[string]interface{}) (*sshHostKey, error) { + fields := schema.Fields{ + "machineid": schema.String(), + "keys": schema.List(schema.String()), + } + // Some values don't have to be there. + defaults := schema.Defaults{} + checker := schema.FieldMap(fields, defaults) + + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "sshhostkey v1 schema check failed") + } + valid := coerced.(map[string]interface{}) + keysInterface := valid["keys"].([]interface{}) + keys := make([]string, len(keysInterface)) + for i, d := range keysInterface { + keys[i] = d.(string) + } + return &sshHostKey{ + MachineID_: valid["machineid"].(string), + Keys_: keys, + }, nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/sshhostkey_test.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/sshhostkey_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/sshhostkey_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/sshhostkey_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,63 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package description + +import ( + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/yaml.v2" +) + +type SSHHostKeySerializationSuite struct { + SliceSerializationSuite +} + +var _ = gc.Suite(&SSHHostKeySerializationSuite{}) + +func (s *SSHHostKeySerializationSuite) SetUpTest(c *gc.C) { + s.SliceSerializationSuite.SetUpTest(c) + s.importName = "sshhostkeys" + s.sliceName = "sshhostkeys" + s.importFunc = func(m map[string]interface{}) (interface{}, error) { + return importSSHHostKeys(m) + } + s.testFields = func(m map[string]interface{}) { + m["sshhostkeys"] = []interface{}{} + } +} + +func (s *SSHHostKeySerializationSuite) TestNewSSHHostKey(c *gc.C) { + args := SSHHostKeyArgs{ + MachineID: "foo", + Keys: []string{"one", "two", "three"}, + } + key := newSSHHostKey(args) + c.Assert(key.MachineID(), gc.Equals, args.MachineID) + c.Assert(key.Keys(), jc.DeepEquals, args.Keys) +} + +func (s *SSHHostKeySerializationSuite) TestParsingSerializedData(c *gc.C) { + initial := sshHostKeys{ + Version: 1, + SSHHostKeys_: []*sshHostKey{ + newSSHHostKey(SSHHostKeyArgs{ + MachineID: "foo", + Keys: []string{"one", "two", "three"}, + }), + newSSHHostKey(SSHHostKeyArgs{MachineID: "bar"}), + }, + } + + bytes, err := yaml.Marshal(initial) + c.Assert(err, jc.ErrorIsNil) + + var source map[string]interface{} + err = yaml.Unmarshal(bytes, &source) + c.Assert(err, jc.ErrorIsNil) + + keys, err := importSSHHostKeys(source) + c.Assert(err, jc.ErrorIsNil) + + c.Assert(keys, jc.DeepEquals, initial.SSHHostKeys_) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/subnet.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/subnet.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/subnet.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/subnet.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,166 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package description + +import ( + "github.com/juju/errors" + "github.com/juju/schema" +) + +type subnets struct { + Version int `yaml:"version"` + Subnets_ []*subnet `yaml:"subnets"` +} + +type subnet struct { + ProviderId_ string `yaml:"provider-id,omitempty"` + CIDR_ string `yaml:"cidr"` + VLANTag_ int `yaml:"vlantag"` + + AvailabilityZone_ string `yaml:"availabilityzone"` + SpaceName_ string `yaml:"spacename"` + + // These will be deprecated once the address allocation strategy for + // EC2 is changed. They are unused already on MAAS. + AllocatableIPHigh_ string `yaml:"allocatableiphigh,omitempty"` + AllocatableIPLow_ string `yaml:"allocatableiplow,omitempty"` +} + +// SubnetArgs is an argument struct used to create a +// new internal subnet type that supports the Subnet interface. +type SubnetArgs struct { + ProviderId string + CIDR string + VLANTag int + AvailabilityZone string + SpaceName string + + // These will be deprecated once the address allocation strategy for + // EC2 is changed. They are unused already on MAAS. + AllocatableIPHigh string + AllocatableIPLow string +} + +func newSubnet(args SubnetArgs) *subnet { + return &subnet{ + ProviderId_: string(args.ProviderId), + SpaceName_: args.SpaceName, + CIDR_: args.CIDR, + VLANTag_: args.VLANTag, + AvailabilityZone_: args.AvailabilityZone, + AllocatableIPHigh_: args.AllocatableIPHigh, + AllocatableIPLow_: args.AllocatableIPLow, + } +} + +// ProviderId implements Subnet. +func (s *subnet) ProviderId() string { + return s.ProviderId_ +} + +// SpaceName implements Subnet. +func (s *subnet) SpaceName() string { + return s.SpaceName_ +} + +// CIDR implements Subnet. +func (s *subnet) CIDR() string { + return s.CIDR_ +} + +// VLANTag implements Subnet. +func (s *subnet) VLANTag() int { + return s.VLANTag_ +} + +// AvailabilityZone implements Subnet. +func (s *subnet) AvailabilityZone() string { + return s.AvailabilityZone_ +} + +// AllocatableIPHigh implements Subnet. +func (s *subnet) AllocatableIPHigh() string { + return s.AllocatableIPHigh_ +} + +// AllocatableIPLow implements Subnet. +func (s *subnet) AllocatableIPLow() string { + return s.AllocatableIPLow_ +} + +func importSubnets(source map[string]interface{}) ([]*subnet, error) { + checker := versionedChecker("subnets") + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "subnets version schema check failed") + } + valid := coerced.(map[string]interface{}) + + version := int(valid["version"].(int64)) + importFunc, ok := subnetDeserializationFuncs[version] + if !ok { + return nil, errors.NotValidf("version %d", version) + } + sourceList := valid["subnets"].([]interface{}) + return importSubnetList(sourceList, importFunc) +} + +func importSubnetList(sourceList []interface{}, importFunc subnetDeserializationFunc) ([]*subnet, error) { + result := make([]*subnet, 0, len(sourceList)) + for i, value := range sourceList { + source, ok := value.(map[string]interface{}) + if !ok { + return nil, errors.Errorf("unexpected value for subnet %d, %T", i, value) + } + subnet, err := importFunc(source) + if err != nil { + return nil, errors.Annotatef(err, "subnet %d", i) + } + result = append(result, subnet) + } + return result, nil +} + +type subnetDeserializationFunc func(map[string]interface{}) (*subnet, error) + +var subnetDeserializationFuncs = map[int]subnetDeserializationFunc{ + 1: importSubnetV1, +} + +func importSubnetV1(source map[string]interface{}) (*subnet, error) { + fields := schema.Fields{ + "cidr": schema.String(), + "provider-id": schema.String(), + "vlantag": schema.Int(), + "spacename": schema.String(), + "availabilityzone": schema.String(), + "allocatableiphigh": schema.String(), + "allocatableiplow": schema.String(), + } + + defaults := schema.Defaults{ + "provider-id": "", + "allocatableiphigh": "", + "allocatableiplow": "", + } + checker := schema.FieldMap(fields, defaults) + + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "subnet v1 schema check failed") + } + valid := coerced.(map[string]interface{}) + + // From here we know that the map returned from the schema coercion + // contains fields of the right type. + return &subnet{ + CIDR_: valid["cidr"].(string), + ProviderId_: valid["provider-id"].(string), + VLANTag_: int(valid["vlantag"].(int64)), + SpaceName_: valid["spacename"].(string), + AvailabilityZone_: valid["availabilityzone"].(string), + AllocatableIPHigh_: valid["allocatableiphigh"].(string), + AllocatableIPLow_: valid["allocatableiplow"].(string), + }, nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/subnet_test.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/subnet_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/subnet_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/subnet_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,78 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package description + +import ( + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/yaml.v2" +) + +type SubnetSerializationSuite struct { + SliceSerializationSuite +} + +var _ = gc.Suite(&SubnetSerializationSuite{}) + +func (s *SubnetSerializationSuite) SetUpTest(c *gc.C) { + s.SliceSerializationSuite.SetUpTest(c) + s.importName = "subnets" + s.sliceName = "subnets" + s.importFunc = func(m map[string]interface{}) (interface{}, error) { + return importSubnets(m) + } + s.testFields = func(m map[string]interface{}) { + m["subnets"] = []interface{}{} + } +} + +func (s *SubnetSerializationSuite) TestNewSubnet(c *gc.C) { + args := SubnetArgs{ + CIDR: "10.0.0.0/24", + ProviderId: "magic", + VLANTag: 64, + SpaceName: "foo", + AvailabilityZone: "bar", + AllocatableIPHigh: "10.0.0.255", + AllocatableIPLow: "10.0.0.0", + } + subnet := newSubnet(args) + c.Assert(subnet.CIDR(), gc.Equals, args.CIDR) + c.Assert(subnet.ProviderId(), gc.Equals, args.ProviderId) + c.Assert(subnet.VLANTag(), gc.Equals, args.VLANTag) + c.Assert(subnet.SpaceName(), gc.Equals, args.SpaceName) + c.Assert(subnet.AvailabilityZone(), gc.Equals, args.AvailabilityZone) + c.Assert(subnet.AllocatableIPHigh(), gc.Equals, args.AllocatableIPHigh) + c.Assert(subnet.AllocatableIPLow(), gc.Equals, args.AllocatableIPLow) +} + +func (s *SubnetSerializationSuite) TestParsingSerializedData(c *gc.C) { + initial := subnets{ + Version: 1, + Subnets_: []*subnet{ + newSubnet(SubnetArgs{ + CIDR: "10.0.0.0/24", + ProviderId: "magic", + VLANTag: 64, + SpaceName: "foo", + AvailabilityZone: "bar", + AllocatableIPHigh: "10.0.0.255", + AllocatableIPLow: "10.0.0.0", + }), + newSubnet(SubnetArgs{CIDR: "10.0.1.0/24"}), + }, + } + + bytes, err := yaml.Marshal(initial) + c.Assert(err, jc.ErrorIsNil) + + var source map[string]interface{} + err = yaml.Unmarshal(bytes, &source) + c.Assert(err, jc.ErrorIsNil) + + subnets, err := importSubnets(source) + c.Assert(err, jc.ErrorIsNil) + + c.Assert(subnets, jc.DeepEquals, initial.Subnets_) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/useraccess.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/useraccess.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/useraccess.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/useraccess.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,35 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package description + +import ( + "time" + + "gopkg.in/juju/names.v2" +) + +// UserAccess represents a user access to a target whereas the user +// could represent a remote user or a user across multiple models the +// user access always represents a single user for a single target. +// There should be no more than one UserAccess per target/user pair. +// Many of these fields are storage artifacts but generate them from +// other fields implies out of band knowledge of other packages. +type UserAccess struct { + // UserID is the stored ID of the user. + UserID string + // UserTag is the tag for the user. + UserTag names.UserTag + // Object is the tag for the object of this access grant. + Object names.Tag + // Access represents the level of access subjec has over object. + Access Access + // CreatedBy is the tag of the user that granted the access. + CreatedBy names.UserTag + // DateCreated is the date the user was created in UTC. + DateCreated time.Time + // DisplayName is the name we are showing for this user. + DisplayName string + // UserName is the actual username for this access. + UserName string +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/user.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/user.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/user.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/user.go 2016-08-16 08:56:25.000000000 +0000 @@ -22,7 +22,7 @@ CreatedBy names.UserTag DateCreated time.Time LastConnection time.Time - Access string + Access Access } func newUser(args UserArgs) *user { @@ -45,7 +45,7 @@ DisplayName_ string `yaml:"display-name,omitempty"` CreatedBy_ string `yaml:"created-by"` DateCreated_ time.Time `yaml:"date-created"` - Access_ string `yaml:"access"` + Access_ Access `yaml:"access"` // Can't use omitempty with time.Time, it just doesn't work, // so use a pointer in the struct. LastConnection_ *time.Time `yaml:"last-connection,omitempty"` @@ -80,22 +80,9 @@ return *u.LastConnection_ } -// IsRead implements User. -func (u *user) IsReadOnly() bool { - // string used here to avoid dependency on state. - return u.Access_ == "read" -} - -// IsReadWrite implements User. -func (u *user) IsReadWrite() bool { - // string used here to avoid dependency on state. - return u.Access_ == "write" -} - -// IsAdmin implements User. -func (u *user) IsAdmin() bool { - // string used here to avoid dependency on state. - return u.Access_ == "admin" +// Access implements User. +func (u *user) Access() Access { + return u.Access_ } func importUsers(source map[string]interface{}) ([]*user, error) { @@ -145,7 +132,7 @@ "read-only": schema.Bool(), "date-created": schema.Time(), "last-connection": schema.Time(), - "access": schema.String(), + "access": accessField(), } // Some values don't have to be there. @@ -168,7 +155,7 @@ DisplayName_: valid["display-name"].(string), CreatedBy_: valid["created-by"].(string), DateCreated_: valid["date-created"].(time.Time), - Access_: valid["access"].(string), + Access_: valid["access"].(Access), } lastConn := valid["last-connection"].(time.Time) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/user_test.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/user_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/user_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/user_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -70,7 +70,8 @@ DisplayName_: "A read only user", CreatedBy_: "admin@local", DateCreated_: time.Date(2015, 10, 9, 12, 34, 56, 0, time.UTC), - Access_: "read", + // We want to fail if someone breaks ReadAccess definition. + Access_: Access("read"), }, }, } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/volume.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/volume.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/volume.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/volume.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,408 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package description + +import ( + "github.com/juju/errors" + "github.com/juju/schema" + "gopkg.in/juju/names.v2" +) + +type volumes struct { + Version int `yaml:"version"` + Volumes_ []*volume `yaml:"volumes"` +} + +type volume struct { + ID_ string `yaml:"id"` + Binding_ string `yaml:"binding,omitempty"` + StorageID_ string `yaml:"storage-id,omitempty"` + Provisioned_ bool `yaml:"provisioned"` + Size_ uint64 `yaml:"size"` + Pool_ string `yaml:"pool,omitempty"` + HardwareID_ string `yaml:"hardware-id,omitempty"` + VolumeID_ string `yaml:"volume-id,omitempty"` + Persistent_ bool `yaml:"persistent"` + + Status_ *status `yaml:"status"` + StatusHistory_ `yaml:"status-history"` + + Attachments_ volumeAttachments `yaml:"attachments"` +} + +type volumeAttachments struct { + Version int `yaml:"version"` + Attachments_ []*volumeAttachment `yaml:"attachments"` +} + +type volumeAttachment struct { + MachineID_ string `yaml:"machine-id"` + Provisioned_ bool `yaml:"provisioned"` + ReadOnly_ bool `yaml:"read-only"` + DeviceName_ string `yaml:"device-name"` + DeviceLink_ string `yaml:"device-link"` + BusAddress_ string `yaml:"bus-address"` +} + +// VolumeArgs is an argument struct used to add a volume to the Model. +type VolumeArgs struct { + Tag names.VolumeTag + Storage names.StorageTag + Binding names.Tag + Provisioned bool + Size uint64 + Pool string + HardwareID string + VolumeID string + Persistent bool +} + +func newVolume(args VolumeArgs) *volume { + v := &volume{ + ID_: args.Tag.Id(), + StorageID_: args.Storage.Id(), + Provisioned_: args.Provisioned, + Size_: args.Size, + Pool_: args.Pool, + HardwareID_: args.HardwareID, + VolumeID_: args.VolumeID, + Persistent_: args.Persistent, + StatusHistory_: newStatusHistory(), + } + if args.Binding != nil { + v.Binding_ = args.Binding.String() + } + v.setAttachments(nil) + return v +} + +// Tag implements Volume. +func (v *volume) Tag() names.VolumeTag { + return names.NewVolumeTag(v.ID_) +} + +// Storage implements Volume. +func (v *volume) Storage() names.StorageTag { + if v.StorageID_ == "" { + return names.StorageTag{} + } + return names.NewStorageTag(v.StorageID_) +} + +// Binding implements Volume. +func (v *volume) Binding() (names.Tag, error) { + if v.Binding_ == "" { + return nil, nil + } + tag, err := names.ParseTag(v.Binding_) + if err != nil { + return nil, errors.Trace(err) + } + return tag, nil +} + +// Provisioned implements Volume. +func (v *volume) Provisioned() bool { + return v.Provisioned_ +} + +// Size implements Volume. +func (v *volume) Size() uint64 { + return v.Size_ +} + +// Pool implements Volume. +func (v *volume) Pool() string { + return v.Pool_ +} + +// HardwareID implements Volume. +func (v *volume) HardwareID() string { + return v.HardwareID_ +} + +// VolumeID implements Volume. +func (v *volume) VolumeID() string { + return v.VolumeID_ +} + +// Persistent implements Volume. +func (v *volume) Persistent() bool { + return v.Persistent_ +} + +// Status implements Volume. +func (v *volume) Status() Status { + // To avoid typed nils check nil here. + if v.Status_ == nil { + return nil + } + return v.Status_ +} + +// SetStatus implements Volume. +func (v *volume) SetStatus(args StatusArgs) { + v.Status_ = newStatus(args) +} + +func (v *volume) setAttachments(attachments []*volumeAttachment) { + v.Attachments_ = volumeAttachments{ + Version: 1, + Attachments_: attachments, + } +} + +// Attachments implements Volume. +func (v *volume) Attachments() []VolumeAttachment { + var result []VolumeAttachment + for _, attachment := range v.Attachments_.Attachments_ { + result = append(result, attachment) + } + return result +} + +// AddAttachment implements Volume. +func (v *volume) AddAttachment(args VolumeAttachmentArgs) VolumeAttachment { + a := newVolumeAttachment(args) + v.Attachments_.Attachments_ = append(v.Attachments_.Attachments_, a) + return a +} + +// Validate implements Volume. +func (v *volume) Validate() error { + if v.ID_ == "" { + return errors.NotValidf("volume missing id") + } + if v.Size_ == 0 { + return errors.NotValidf("volume %q missing size", v.ID_) + } + if v.Status_ == nil { + return errors.NotValidf("volume %q missing status", v.ID_) + } + return nil +} + +func importVolumes(source map[string]interface{}) ([]*volume, error) { + checker := versionedChecker("volumes") + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "volumes version schema check failed") + } + valid := coerced.(map[string]interface{}) + + version := int(valid["version"].(int64)) + importFunc, ok := volumeDeserializationFuncs[version] + if !ok { + return nil, errors.NotValidf("version %d", version) + } + sourceList := valid["volumes"].([]interface{}) + return importVolumeList(sourceList, importFunc) +} + +func importVolumeList(sourceList []interface{}, importFunc volumeDeserializationFunc) ([]*volume, error) { + result := make([]*volume, 0, len(sourceList)) + for i, value := range sourceList { + source, ok := value.(map[string]interface{}) + if !ok { + return nil, errors.Errorf("unexpected value for volume %d, %T", i, value) + } + volume, err := importFunc(source) + if err != nil { + return nil, errors.Annotatef(err, "volume %d", i) + } + result = append(result, volume) + } + return result, nil +} + +type volumeDeserializationFunc func(map[string]interface{}) (*volume, error) + +var volumeDeserializationFuncs = map[int]volumeDeserializationFunc{ + 1: importVolumeV1, +} + +func importVolumeV1(source map[string]interface{}) (*volume, error) { + fields := schema.Fields{ + "id": schema.String(), + "storage-id": schema.String(), + "binding": schema.String(), + "provisioned": schema.Bool(), + "size": schema.ForceUint(), + "pool": schema.String(), + "hardware-id": schema.String(), + "volume-id": schema.String(), + "persistent": schema.Bool(), + "status": schema.StringMap(schema.Any()), + "attachments": schema.StringMap(schema.Any()), + } + + defaults := schema.Defaults{ + "storage-id": "", + "binding": "", + "pool": "", + "hardware-id": "", + "volume-id": "", + "attachments": schema.Omit, + } + addStatusHistorySchema(fields) + checker := schema.FieldMap(fields, defaults) + + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "volume v1 schema check failed") + } + valid := coerced.(map[string]interface{}) + // From here we know that the map returned from the schema coercion + // contains fields of the right type. + result := &volume{ + ID_: valid["id"].(string), + StorageID_: valid["storage-id"].(string), + Binding_: valid["binding"].(string), + Provisioned_: valid["provisioned"].(bool), + Size_: valid["size"].(uint64), + Pool_: valid["pool"].(string), + HardwareID_: valid["hardware-id"].(string), + VolumeID_: valid["volume-id"].(string), + Persistent_: valid["persistent"].(bool), + StatusHistory_: newStatusHistory(), + } + if err := result.importStatusHistory(valid); err != nil { + return nil, errors.Trace(err) + } + + status, err := importStatus(valid["status"].(map[string]interface{})) + if err != nil { + return nil, errors.Trace(err) + } + result.Status_ = status + + attachments, err := importVolumeAttachments(valid["attachments"].(map[string]interface{})) + if err != nil { + return nil, errors.Trace(err) + } + result.setAttachments(attachments) + + return result, nil +} + +// VolumeAttachmentArgs is an argument struct used to add information about the +// cloud instance to a Volume. +type VolumeAttachmentArgs struct { + Machine names.MachineTag + Provisioned bool + ReadOnly bool + DeviceName string + DeviceLink string + BusAddress string +} + +func newVolumeAttachment(args VolumeAttachmentArgs) *volumeAttachment { + return &volumeAttachment{ + MachineID_: args.Machine.Id(), + Provisioned_: args.Provisioned, + ReadOnly_: args.ReadOnly, + DeviceName_: args.DeviceName, + DeviceLink_: args.DeviceLink, + BusAddress_: args.BusAddress, + } +} + +// Machine implements VolumeAttachment +func (a *volumeAttachment) Machine() names.MachineTag { + return names.NewMachineTag(a.MachineID_) +} + +// Provisioned implements VolumeAttachment +func (a *volumeAttachment) Provisioned() bool { + return a.Provisioned_ +} + +// ReadOnly implements VolumeAttachment +func (a *volumeAttachment) ReadOnly() bool { + return a.ReadOnly_ +} + +// DeviceName implements VolumeAttachment +func (a *volumeAttachment) DeviceName() string { + return a.DeviceName_ +} + +// DeviceLink implements VolumeAttachment +func (a *volumeAttachment) DeviceLink() string { + return a.DeviceLink_ +} + +// BusAddress implements VolumeAttachment +func (a *volumeAttachment) BusAddress() string { + return a.BusAddress_ +} + +func importVolumeAttachments(source map[string]interface{}) ([]*volumeAttachment, error) { + checker := versionedChecker("attachments") + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "volume attachments version schema check failed") + } + valid := coerced.(map[string]interface{}) + + version := int(valid["version"].(int64)) + importFunc, ok := volumeAttachmentDeserializationFuncs[version] + if !ok { + return nil, errors.NotValidf("version %d", version) + } + sourceList := valid["attachments"].([]interface{}) + return importVolumeAttachmentList(sourceList, importFunc) +} + +func importVolumeAttachmentList(sourceList []interface{}, importFunc volumeAttachmentDeserializationFunc) ([]*volumeAttachment, error) { + result := make([]*volumeAttachment, 0, len(sourceList)) + for i, value := range sourceList { + source, ok := value.(map[string]interface{}) + if !ok { + return nil, errors.Errorf("unexpected value for volumeAttachment %d, %T", i, value) + } + volumeAttachment, err := importFunc(source) + if err != nil { + return nil, errors.Annotatef(err, "volumeAttachment %d", i) + } + result = append(result, volumeAttachment) + } + return result, nil +} + +type volumeAttachmentDeserializationFunc func(map[string]interface{}) (*volumeAttachment, error) + +var volumeAttachmentDeserializationFuncs = map[int]volumeAttachmentDeserializationFunc{ + 1: importVolumeAttachmentV1, +} + +func importVolumeAttachmentV1(source map[string]interface{}) (*volumeAttachment, error) { + fields := schema.Fields{ + "machine-id": schema.String(), + "provisioned": schema.Bool(), + "read-only": schema.Bool(), + "device-name": schema.String(), + "device-link": schema.String(), + "bus-address": schema.String(), + } + checker := schema.FieldMap(fields, nil) // no defaults + + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "volumeAttachment v1 schema check failed") + } + valid := coerced.(map[string]interface{}) + // From here we know that the map returned from the schema coercion + // contains fields of the right type. + + result := &volumeAttachment{ + MachineID_: valid["machine-id"].(string), + Provisioned_: valid["provisioned"].(bool), + ReadOnly_: valid["read-only"].(bool), + DeviceName_: valid["device-name"].(string), + DeviceLink_: valid["device-link"].(string), + BusAddress_: valid["bus-address"].(string), + } + return result, nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/description/volume_test.go juju-core-2.0~beta15/src/github.com/juju/juju/core/description/volume_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/description/volume_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/description/volume_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,280 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package description + +import ( + "github.com/juju/errors" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/juju/names.v2" + "gopkg.in/yaml.v2" +) + +type VolumeSerializationSuite struct { + SliceSerializationSuite + StatusHistoryMixinSuite +} + +var _ = gc.Suite(&VolumeSerializationSuite{}) + +func (s *VolumeSerializationSuite) SetUpTest(c *gc.C) { + s.SliceSerializationSuite.SetUpTest(c) + s.importName = "volumes" + s.sliceName = "volumes" + s.importFunc = func(m map[string]interface{}) (interface{}, error) { + return importVolumes(m) + } + s.testFields = func(m map[string]interface{}) { + m["volumes"] = []interface{}{} + } + s.StatusHistoryMixinSuite.creator = func() HasStatusHistory { + return testVolume() + } + s.StatusHistoryMixinSuite.serializer = func(c *gc.C, initial interface{}) HasStatusHistory { + return s.exportImport(c, initial.(*volume)) + } +} + +func testVolumeMap() map[interface{}]interface{} { + return map[interface{}]interface{}{ + "id": "1234", + "storage-id": "test/1", + "binding": "machine-42", + "provisioned": true, + "size": int(20 * gig), + "pool": "swimming", + "hardware-id": "a hardware id", + "volume-id": "some volume id", + "persistent": true, + "status": minimalStatusMap(), + "status-history": emptyStatusHistoryMap(), + "attachments": map[interface{}]interface{}{ + "version": 1, + "attachments": []interface{}{}, + }, + } +} + +func testVolume() *volume { + v := newVolume(testVolumeArgs()) + v.SetStatus(minimalStatusArgs()) + return v +} + +func testVolumeArgs() VolumeArgs { + return VolumeArgs{ + Tag: names.NewVolumeTag("1234"), + Storage: names.NewStorageTag("test/1"), + Binding: names.NewMachineTag("42"), + Provisioned: true, + Size: 20 * gig, + Pool: "swimming", + HardwareID: "a hardware id", + VolumeID: "some volume id", + Persistent: true, + } +} + +func (s *VolumeSerializationSuite) TestNewVolume(c *gc.C) { + volume := testVolume() + + c.Check(volume.Tag(), gc.Equals, names.NewVolumeTag("1234")) + c.Check(volume.Storage(), gc.Equals, names.NewStorageTag("test/1")) + binding, err := volume.Binding() + c.Check(err, jc.ErrorIsNil) + c.Check(binding, gc.Equals, names.NewMachineTag("42")) + c.Check(volume.Provisioned(), jc.IsTrue) + c.Check(volume.Size(), gc.Equals, 20*gig) + c.Check(volume.Pool(), gc.Equals, "swimming") + c.Check(volume.HardwareID(), gc.Equals, "a hardware id") + c.Check(volume.VolumeID(), gc.Equals, "some volume id") + c.Check(volume.Persistent(), jc.IsTrue) + + c.Check(volume.Attachments(), gc.HasLen, 0) +} + +func (s *VolumeSerializationSuite) TestVolumeValid(c *gc.C) { + volume := testVolume() + c.Assert(volume.Validate(), jc.ErrorIsNil) +} + +func (s *VolumeSerializationSuite) TestVolumeValidMissingID(c *gc.C) { + v := newVolume(VolumeArgs{}) + err := v.Validate() + c.Check(err, gc.ErrorMatches, `volume missing id not valid`) + c.Check(err, jc.Satisfies, errors.IsNotValid) +} + +func (s *VolumeSerializationSuite) TestVolumeValidMissingSize(c *gc.C) { + v := newVolume(VolumeArgs{ + Tag: names.NewVolumeTag("123"), + }) + err := v.Validate() + c.Check(err, gc.ErrorMatches, `volume "123" missing size not valid`) + c.Check(err, jc.Satisfies, errors.IsNotValid) +} + +func (s *VolumeSerializationSuite) TestVolumeValidMissingStatus(c *gc.C) { + v := newVolume(VolumeArgs{ + Tag: names.NewVolumeTag("123"), + Size: 5, + }) + err := v.Validate() + c.Check(err, gc.ErrorMatches, `volume "123" missing status not valid`) + c.Check(err, jc.Satisfies, errors.IsNotValid) +} + +func (s *VolumeSerializationSuite) TestVolumeValidMinimal(c *gc.C) { + v := newVolume(VolumeArgs{ + Tag: names.NewVolumeTag("123"), + Size: 5, + }) + v.SetStatus(minimalStatusArgs()) + err := v.Validate() + c.Check(err, jc.ErrorIsNil) +} + +func (s *VolumeSerializationSuite) TestVolumeMatches(c *gc.C) { + bytes, err := yaml.Marshal(testVolume()) + c.Assert(err, jc.ErrorIsNil) + + var source map[interface{}]interface{} + err = yaml.Unmarshal(bytes, &source) + c.Assert(err, jc.ErrorIsNil) + c.Assert(source, jc.DeepEquals, testVolumeMap()) +} + +func (s *VolumeSerializationSuite) exportImport(c *gc.C, volume_ *volume) *volume { + initial := volumes{ + Version: 1, + Volumes_: []*volume{volume_}, + } + + bytes, err := yaml.Marshal(initial) + c.Assert(err, jc.ErrorIsNil) + + var source map[string]interface{} + err = yaml.Unmarshal(bytes, &source) + c.Assert(err, jc.ErrorIsNil) + + volumes, err := importVolumes(source) + c.Assert(err, jc.ErrorIsNil) + c.Assert(volumes, gc.HasLen, 1) + return volumes[0] +} + +func (s *VolumeSerializationSuite) TestAddingAttachments(c *gc.C) { + // The core code does not care about duplicates, so we'll just add + // the same attachment twice. + original := testVolume() + attachment1 := original.AddAttachment(testVolumeAttachmentArgs("1")) + attachment2 := original.AddAttachment(testVolumeAttachmentArgs("2")) + volume := s.exportImport(c, original) + c.Assert(volume, jc.DeepEquals, original) + attachments := volume.Attachments() + c.Assert(attachments, gc.HasLen, 2) + c.Check(attachments[0], jc.DeepEquals, attachment1) + c.Check(attachments[1], jc.DeepEquals, attachment2) +} + +func (s *VolumeSerializationSuite) TestParsingSerializedData(c *gc.C) { + original := testVolume() + original.AddAttachment(testVolumeAttachmentArgs()) + volume := s.exportImport(c, original) + c.Assert(volume, jc.DeepEquals, original) +} + +type VolumeAttachmentSerializationSuite struct { + SliceSerializationSuite +} + +var _ = gc.Suite(&VolumeAttachmentSerializationSuite{}) + +func (s *VolumeAttachmentSerializationSuite) SetUpTest(c *gc.C) { + s.SliceSerializationSuite.SetUpTest(c) + s.importName = "volume attachments" + s.sliceName = "attachments" + s.importFunc = func(m map[string]interface{}) (interface{}, error) { + return importVolumeAttachments(m) + } + s.testFields = func(m map[string]interface{}) { + m["attachments"] = []interface{}{} + } +} + +func testVolumeAttachmentMap() map[interface{}]interface{} { + return map[interface{}]interface{}{ + "machine-id": "42", + "provisioned": true, + "read-only": true, + "device-name": "sdd", + "device-link": "link?", + "bus-address": "nfi", + } +} + +func testVolumeAttachment() *volumeAttachment { + return newVolumeAttachment(testVolumeAttachmentArgs()) +} + +func testVolumeAttachmentArgs(id ...string) VolumeAttachmentArgs { + machineID := "42" + if len(id) > 0 { + machineID = id[0] + } + return VolumeAttachmentArgs{ + Machine: names.NewMachineTag(machineID), + Provisioned: true, + ReadOnly: true, + DeviceName: "sdd", + DeviceLink: "link?", + BusAddress: "nfi", + } +} + +func (s *VolumeAttachmentSerializationSuite) TestNewVolumeAttachment(c *gc.C) { + attachment := testVolumeAttachment() + + c.Check(attachment.Machine(), gc.Equals, names.NewMachineTag("42")) + c.Check(attachment.Provisioned(), jc.IsTrue) + c.Check(attachment.ReadOnly(), jc.IsTrue) + c.Check(attachment.DeviceName(), gc.Equals, "sdd") + c.Check(attachment.DeviceLink(), gc.Equals, "link?") + c.Check(attachment.BusAddress(), gc.Equals, "nfi") +} + +func (s *VolumeAttachmentSerializationSuite) TestVolumeAttachmentMatches(c *gc.C) { + bytes, err := yaml.Marshal(testVolumeAttachment()) + c.Assert(err, jc.ErrorIsNil) + + var source map[interface{}]interface{} + err = yaml.Unmarshal(bytes, &source) + c.Assert(err, jc.ErrorIsNil) + c.Assert(source, jc.DeepEquals, testVolumeAttachmentMap()) +} + +func (s *VolumeAttachmentSerializationSuite) exportImport(c *gc.C, attachment *volumeAttachment) *volumeAttachment { + initial := volumeAttachments{ + Version: 1, + Attachments_: []*volumeAttachment{attachment}, + } + + bytes, err := yaml.Marshal(initial) + c.Assert(err, jc.ErrorIsNil) + + var source map[string]interface{} + err = yaml.Unmarshal(bytes, &source) + c.Assert(err, jc.ErrorIsNil) + + attachments, err := importVolumeAttachments(source) + c.Assert(err, jc.ErrorIsNil) + c.Assert(attachments, gc.HasLen, 1) + return attachments[0] +} + +func (s *VolumeAttachmentSerializationSuite) TestParsingSerializedData(c *gc.C) { + original := testVolumeAttachment() + attachment := s.exportImport(c, original) + c.Assert(attachment, jc.DeepEquals, original) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/migration/migration.go juju-core-2.0~beta15/src/github.com/juju/juju/core/migration/migration.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/migration/migration.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/migration/migration.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,47 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package migration + +import ( + "time" + + "github.com/juju/version" +) + +// MigrationStatus returns the details for a migration as needed by +// the migrationmaster worker. +type MigrationStatus struct { + // MigrationId hold the unique id for the migration. + MigrationId string + + // ModelUUID holds the UUID of the model being migrated. + ModelUUID string + + // Phases indicates the current migration phase. + Phase Phase + + // PhaseChangedTime indicates the time the phase was changed to + // its current value. + PhaseChangedTime time.Time + + // TargetInfo contains the details of how to connect to the target + // controller. + TargetInfo TargetInfo +} + +// SerializedModel wraps a buffer contain a serialised Juju model as +// well as containing metadata about the charms and tools used by the +// model. +type SerializedModel struct { + // Bytes contains the serialized data for the model. + Bytes []byte + + // Charms lists the charm URLs in use in the model. + Charms []string + + // Tools lists the tools versions in use with the model along with + // their URIs. The URIs can be used to download the tools from the + // source controller. + Tools map[version.Binary]string // version -> tools URI +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/migration/minionreports.go juju-core-2.0~beta15/src/github.com/juju/juju/core/migration/minionreports.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/migration/minionreports.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/migration/minionreports.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,43 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package migration + +// MinionReports returns information about the migration minion +// reports received so far for a given migration phase. +type MinionReports struct { + // ModelUUID holds the unique identifier for the model migration. + MigrationId string + + // Phases indicates the migration phase the reports relate to. + Phase Phase + + // SuccesCount indicates how many agents have successfully + // completed the migration phase. + SuccessCount int + + // UnknownCount indicates how many agents are yet to report + // regarding the migration phase. + UnknownCount int + + // SomeUnknownMachines holds the ids of some of the machines which + // have not yet reported in. + SomeUnknownMachines []string + + // SomeUnknownUnits holds the names of some of the units which + // have not yet reported in. + SomeUnknownUnits []string + + // FailedMachines holds the ids of machines which have failed to + // complete the migration phase. + FailedMachines []string + + // FailedUnits holds the names of units which have failed to + // complete the migration phase. + FailedUnits []string +} + +// IsZero returns true if the MinionReports instance hasn't been set. +func (r *MinionReports) IsZero() bool { + return r.MigrationId == "" && r.Phase == UNKNOWN +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/migration/minionreports_test.go juju-core-2.0~beta15/src/github.com/juju/juju/core/migration/minionreports_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/migration/minionreports_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/migration/minionreports_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,37 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package migration_test + +import ( + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/core/migration" + coretesting "github.com/juju/juju/testing" +) + +type MinionReportsSuite struct { + coretesting.BaseSuite +} + +var _ = gc.Suite(new(MinionReportsSuite)) + +func (s *MinionReportsSuite) TestIsZero(c *gc.C) { + reports := migration.MinionReports{} + c.Check(reports.IsZero(), jc.IsTrue) +} + +func (s *MinionReportsSuite) TestIsZeroIdSet(c *gc.C) { + reports := migration.MinionReports{ + MigrationId: "foo", + } + c.Check(reports.IsZero(), jc.IsFalse) +} + +func (s *MinionReportsSuite) TestIsZeroPhaseSet(c *gc.C) { + reports := migration.MinionReports{ + Phase: migration.QUIESCE, + } + c.Check(reports.IsZero(), jc.IsFalse) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/migration/phase.go juju-core-2.0~beta15/src/github.com/juju/juju/core/migration/phase.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/migration/phase.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/migration/phase.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,7 +11,6 @@ UNKNOWN Phase = iota NONE QUIESCE - READONLY PRECHECK IMPORT VALIDATION @@ -28,10 +27,9 @@ "UNKNOWN", // To catch uninitialised fields. "NONE", // For watchers to indicate there's never been a migration attempt. "QUIESCE", - "READONLY", "PRECHECK", - "VALIDATION", "IMPORT", + "VALIDATION", "SUCCESS", "LOGTRANSFER", "REAP", @@ -76,13 +74,28 @@ return false } +// IsRunning returns true if the phase indicates the migration is +// active and up to or at the SUCCESS phase. It returns false if the +// phase is one of the final cleanup phases or indicates an failed +// migration. +func (p Phase) IsRunning() bool { + if p.IsTerminal() { + return false + } + switch p { + case QUIESCE, PRECHECK, IMPORT, VALIDATION, SUCCESS: + return true + default: + return false + } +} + // Define all possible phase transitions. // // The keys are the "from" states and the values enumerate the // possible "to" states. var validTransitions = map[Phase][]Phase{ - QUIESCE: {READONLY, ABORT}, - READONLY: {PRECHECK, ABORT}, + QUIESCE: {PRECHECK, ABORT}, PRECHECK: {IMPORT, ABORT}, IMPORT: {VALIDATION, ABORT}, VALIDATION: {SUCCESS, ABORT}, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/core/migration/phase_test.go juju-core-2.0~beta15/src/github.com/juju/juju/core/migration/phase_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/core/migration/phase_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/core/migration/phase_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -55,10 +55,26 @@ c.Check(migration.DONE.IsTerminal(), jc.IsTrue) } +func (s *PhaseSuite) TestIsRunning(c *gc.C) { + c.Check(migration.UNKNOWN.IsRunning(), jc.IsFalse) + c.Check(migration.NONE.IsRunning(), jc.IsFalse) + + c.Check(migration.QUIESCE.IsRunning(), jc.IsTrue) + c.Check(migration.IMPORT.IsRunning(), jc.IsTrue) + c.Check(migration.SUCCESS.IsRunning(), jc.IsTrue) + + c.Check(migration.LOGTRANSFER.IsRunning(), jc.IsFalse) + c.Check(migration.REAP.IsRunning(), jc.IsFalse) + c.Check(migration.REAPFAILED.IsRunning(), jc.IsFalse) + c.Check(migration.DONE.IsRunning(), jc.IsFalse) + c.Check(migration.ABORT.IsRunning(), jc.IsFalse) + c.Check(migration.ABORTDONE.IsRunning(), jc.IsFalse) +} + func (s *PhaseSuite) TestCanTransitionTo(c *gc.C) { c.Check(migration.QUIESCE.CanTransitionTo(migration.SUCCESS), jc.IsFalse) c.Check(migration.QUIESCE.CanTransitionTo(migration.ABORT), jc.IsTrue) - c.Check(migration.QUIESCE.CanTransitionTo(migration.READONLY), jc.IsTrue) + c.Check(migration.QUIESCE.CanTransitionTo(migration.PRECHECK), jc.IsTrue) c.Check(migration.QUIESCE.CanTransitionTo(migration.Phase(-1)), jc.IsFalse) c.Check(migration.ABORT.CanTransitionTo(migration.QUIESCE), jc.IsFalse) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/dependencies.tsv juju-core-2.0~beta15/src/github.com/juju/juju/dependencies.tsv --- juju-core-2.0~beta12/src/github.com/juju/juju/dependencies.tsv 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/dependencies.tsv 2016-08-16 08:56:25.000000000 +0000 @@ -14,31 +14,32 @@ github.com/joyent/gosdc git 2f11feadd2d9891e92296a1077c3e2e56939547d 2014-05-24T00:08:15Z github.com/joyent/gosign git 0da0d5f1342065321c97812b1f4ac0c2b0bab56c 2014-05-24T00:07:34Z github.com/juju/blobstore git 06056004b3d7b54bbb7984d830c537bad00fec21 2015-07-29T11:18:58Z -github.com/juju/bundlechanges git 8d99dd2a94d7b4fd975a152238d0e19d0c4a6cf1 2016-06-15T07:19:43Z -github.com/juju/cmd git a11ae7a7436c133e799f025998cbbefd3f6eef7e 2016-06-01T03:55:01Z +github.com/juju/bundlechanges git 6791af0ab78efe88ff99c2a0095208b3b7a32055 2016-07-20T09:32:50Z +github.com/juju/cmd git 035efd5daac768531ef240ab9e5ee32e3498fbef 2016-08-02T03:51:17Z github.com/juju/errors git 1b5e39b83d1835fa480e0c2ddefb040ee82d58b3 2015-09-16T12:56:42Z github.com/juju/go4 git 40d72ab9641a2a8c36a9c46a51e28367115c8e59 2016-02-22T16:32:58Z github.com/juju/gojsonpointer git afe8b77aa08f272b49e01b82de78510c11f61500 2015-02-04T19:46:29Z github.com/juju/gojsonreference git f0d24ac5ee330baa21721cdff56d45e4ee42628e 2015-02-04T19:46:33Z github.com/juju/gojsonschema git e1ad140384f254c82f89450d9a7c8dd38a632838 2015-03-12T17:00:16Z -github.com/juju/gomaasapi git 5bd7212f416a2d801e4a39800b66e1ee4461c42e 2016-05-03T13:03:30Z +github.com/juju/gomaasapi git c4008a71e7212cb6a99a9c17bb218034927d82b7 2016-07-28T00:29:23Z github.com/juju/govmomi git 4354a88d4b34abe467215f77c2fc1cb9f78b66f7 2015-04-24T01:54:48Z github.com/juju/httpprof git 14bf14c307672fd2456bdbf35d19cf0ccd3cf565 2014-12-17T16:00:36Z github.com/juju/httprequest git 796aaafaf712f666df58d31a482c51233038bf9f 2016-05-03T15:03:27Z github.com/juju/idmclient git 3dda079a75cccb85083d4c3877e638f5d6ab79c2 2016-05-26T05:00:34Z -github.com/juju/loggo git 8477fc936adf0e382d680310047ca27e128a309a 2015-05-27T03:58:39Z +github.com/juju/loggo git 15901ae4de786d05edae84a27c93d3fbef66c91e 2016-08-04T22:15:26Z github.com/juju/mempool git 24974d6c264fe5a29716e7d56ea24c4bd904b7cc 2016-02-05T10:49:27Z github.com/juju/mutex git 59c26ee163447c5c57f63ff71610d433862013de 2016-06-17T01:09:07Z github.com/juju/persistent-cookiejar git e710b897c13ca52828ca2fc9769465186fd6d15c 2016-03-31T17:12:27Z github.com/juju/replicaset git fb7294cf57a1e2f08a57691f1246d129a87ab7e8 2015-05-08T02:21:43Z github.com/juju/retry git 62c62032529169c7ec02fa48f93349604c345e1f 2015-10-29T02:48:21Z github.com/juju/rfc git ebdbbdb950cd039a531d15cdc2ac2cbd94f068ee 2016-07-11T02:42:13Z -github.com/juju/romulus git 6b52a14d619315a31ad4d7069db654c883d6f562 2016-07-14T11:43:41Z +github.com/juju/romulus git f790f93d956741903ce5b1f027df4c9404227d55 2016-08-08T09:53:44Z github.com/juju/schema git 075de04f9b7d7580d60a1e12a0b3f50bb18e6998 2016-04-20T04:42:03Z -github.com/juju/testing git ccf839b5a07a7a05009f8fa3ec41cd05fb2e0b08 2016-06-24T20:35:24Z +github.com/juju/terms-client git 9b925afd677234e4146dde3cb1a11e187cbed64e 2016-08-09T13:19:00Z +github.com/juju/testing git d325c22badd4ba3a5fde01d479b188c7a06df755 2016-08-02T03:47:59Z github.com/juju/txn git 99ec629d0066a4d73c54d8e021a7fc1dc07df614 2015-06-09T16:58:27Z github.com/juju/usso git 68a59c96c178fbbad65926e7f93db50a2cd14f33 2016-04-01T10:44:24Z -github.com/juju/utils git 6219812829a3542c827c76cc75f416d4e6c94335 2016-07-08T10:00:56Z +github.com/juju/utils git 10adcbfe55417518543ed3c3341de2c7db0a3450 2016-07-29T19:45:31Z github.com/juju/version git 4ae6172c00626779a5a462c3e3d22fc0e889431a 2016-06-03T19:49:58Z github.com/juju/webbrowser git 54b8c57083b4afb7dc75da7f13e2967b2606a507 2016-03-09T14:36:29Z github.com/juju/xml git eb759a627588d35166bc505fceb51b88500e291e 2015-04-13T13:11:21Z @@ -58,12 +59,12 @@ gopkg.in/goose.v1 git 495e6fa2ab89bc5ed2c8e1bbcbc4c9e4a3c97d37 2016-03-17T17:25:46Z gopkg.in/ini.v1 git 776aa739ce9373377cd16f526cdf06cb4c89b40f 2016-02-22T23:24:41Z gopkg.in/juju/blobstore.v2 git 51fa6e26128d74e445c72d3a91af555151cc3654 2016-01-25T02:37:03Z -gopkg.in/juju/charm.v6-unstable git 8796be6021c9ecb20630950498ec515f7dd24575 2016-06-09T14:28:26Z +gopkg.in/juju/charm.v6-unstable git a3bb92d047b0892452b6a39ece59b4d3a2ac35b9 2016-07-22T08:34:31Z gopkg.in/juju/charmrepo.v2-unstable git 6e6733987fb03100f30e494cc1134351fe4a593b 2016-05-30T23:07:41Z gopkg.in/juju/charmstore.v5-unstable git 2cb9f80553dddaae8c5e2161ea45f4be5d9afc00 2016-05-27T11:46:22Z gopkg.in/juju/environschema.v1 git 7359fc7857abe2b11b5b3e23811a9c64cb6b01e0 2015-11-04T11:58:10Z gopkg.in/juju/jujusvg.v1 git cc128825adce31ea13020d24e7b3302bac86a8c3 2016-05-30T22:53:36Z -gopkg.in/juju/names.v2 git 5426d66579afd36fc63d809dd58806806c2f161f 2016-06-23T03:33:52Z +gopkg.in/juju/names.v2 git 3e0d33a444fec55aea7269b849eb22da41e73072 2016-07-18T22:31:20Z gopkg.in/macaroon-bakery.v1 git b097c9d99b2537efaf54492e08f7e148f956ba51 2016-05-24T09:38:11Z gopkg.in/macaroon.v1 git ab3940c6c16510a850e1c2dd628b919f0f3f1464 2015-01-21T11:42:31Z gopkg.in/mgo.v2 git 29cc868a5ca65f401ff318143f9408d02f4799cc 2016-06-09T18:00:28Z diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/bootstrap/bootstrap.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/bootstrap/bootstrap.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/bootstrap/bootstrap.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/bootstrap/bootstrap.go 2016-08-16 08:56:25.000000000 +0000 @@ -93,7 +93,7 @@ ControllerConfig controller.Config // ControllerInheritedConfig is the set of config attributes to be shared - // across all models in the same controller on the bootstrap cloud. + // across all models in the same controller. ControllerInheritedConfig map[string]interface{} // HostedModelConfig is the set of config attributes to be overlaid @@ -321,6 +321,9 @@ if err := instanceConfig.SetTools(selectedToolsList); err != nil { return errors.Trace(err) } + // Make sure we have the most recent environ config as the specified + // tools version has been updated there. + cfg = environ.Config() if err := finalizeInstanceBootstrapConfig(ctx, instanceConfig, args, cfg, customImageMetadata); err != nil { return errors.Annotate(err, "finalizing bootstrap instance config") } @@ -372,7 +375,7 @@ CAPrivateKey: args.CAPrivateKey, } if _, ok := cfg.AgentVersion(); !ok { - return fmt.Errorf("controller model configuration has no agent-version") + return errors.New("controller model configuration has no agent-version") } icfg.Bootstrap.ControllerModelConfig = cfg diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/bootstrap/bootstrap_test.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/bootstrap/bootstrap_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/bootstrap/bootstrap_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/bootstrap/bootstrap_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -276,11 +276,44 @@ } } +func (s *bootstrapSuite) TestBootstrapUploadTools(c *gc.C) { + if runtime.GOOS == "windows" { + c.Skip("issue 1403084: Currently does not work because of jujud problems") + } + + // Patch out HostArch and FindTools to allow the test to pass on other architectures, + // such as s390. + s.PatchValue(&arch.HostArch, func() string { return arch.ARM64 }) + s.PatchValue(bootstrap.FindTools, func(environs.Environ, int, int, string, tools.Filter) (tools.List, error) { + return nil, errors.NotFoundf("tools") + }) + + env := newEnviron("foo", useDefaultKeys, nil) + err := bootstrap.Bootstrap(envtesting.BootstrapContext(c), env, bootstrap.BootstrapParams{ + UploadTools: true, + AdminSecret: "admin-secret", + CAPrivateKey: coretesting.CAKey, + ControllerConfig: coretesting.FakeControllerConfig(), + BuildToolsTarball: func(ver *version.Number, _ string) (*sync.BuiltTools, error) { + c.Logf("BuildToolsTarball version %s", ver) + return &sync.BuiltTools{Dir: c.MkDir()}, nil + }, + }) + c.Assert(err, jc.ErrorIsNil) + // Check that the model config has the correct version set. + cfg := env.instanceConfig.Bootstrap.ControllerModelConfig + agentVersion, valid := cfg.AgentVersion() + c.Check(valid, jc.IsTrue) + c.Check(agentVersion.String(), gc.Equals, "1.99.0.1") +} + func (s *bootstrapSuite) TestBootstrapNoToolsNonReleaseStream(c *gc.C) { if runtime.GOOS == "windows" { c.Skip("issue 1403084: Currently does not work because of jujud problems") } + // Patch out HostArch and FindTools to allow the test to pass on other architectures, + // such as s390. s.PatchValue(&arch.HostArch, func() string { return arch.ARM64 }) s.PatchValue(bootstrap.FindTools, func(environs.Environ, int, int, string, tools.Filter) (tools.List, error) { return nil, errors.NotFoundf("tools") @@ -679,9 +712,10 @@ env := newEnviron("foo", useDefaultKeys, nil) err := bootstrap.Bootstrap(envtesting.BootstrapContext(c), env, bootstrap.BootstrapParams{ - ControllerConfig: coretesting.FakeControllerConfig(), - AdminSecret: password, - CAPrivateKey: coretesting.CAKey, + ControllerConfig: coretesting.FakeControllerConfig(), + ControllerInheritedConfig: map[string]interface{}{"ftp-proxy": "http://proxy"}, + AdminSecret: password, + CAPrivateKey: coretesting.CAKey, }) c.Assert(err, jc.ErrorIsNil) icfg := env.instanceConfig @@ -694,6 +728,7 @@ c.Check(icfg.Controller.MongoInfo, jc.DeepEquals, &mongo.MongoInfo{ Password: password, Info: mongo.Info{CACert: coretesting.CACert}, }) + c.Check(icfg.Bootstrap.ControllerInheritedConfig, gc.DeepEquals, map[string]interface{}{"ftp-proxy": "http://proxy"}) controllerCfg := icfg.Controller.Config c.Check(controllerCfg["ca-private-key"], gc.IsNil) c.Check(icfg.Bootstrap.StateServingInfo.StatePort, gc.Equals, controllerCfg.StatePort()) @@ -839,12 +874,12 @@ environs.Environ // stub out all methods we don't care about. // The following fields are filled in when Bootstrap is called. - bootstrapCount int - finalizerCount int - supportedArchitecturesCount int - args environs.BootstrapParams - instanceConfig *instancecfg.InstanceConfig - storage storage.Storage + bootstrapCount int + finalizerCount int + constraintsValidatorCount int + args environs.BootstrapParams + instanceConfig *instancecfg.InstanceConfig + storage storage.Storage } func newEnviron(name string, defaultKeys bool, extraAttrs map[string]interface{}) *bootstrapEnviron { @@ -903,13 +938,11 @@ return e.storage } -func (e *bootstrapEnviron) SupportedArchitectures() ([]string, error) { - e.supportedArchitecturesCount++ - return []string{arch.AMD64, arch.ARM64}, nil -} - func (e *bootstrapEnviron) ConstraintsValidator() (constraints.Validator, error) { - return constraints.NewValidator(), nil + e.constraintsValidatorCount++ + v := constraints.NewValidator() + v.RegisterVocabulary(constraints.Arch, []string{arch.AMD64, arch.ARM64}) + return v, nil } type bootstrapEnvironWithRegion struct { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/bootstrap/prepare.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/bootstrap/prepare.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/bootstrap/prepare.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/bootstrap/prepare.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,8 +5,8 @@ import ( "github.com/juju/errors" + "gopkg.in/juju/names.v2" - "github.com/juju/juju/cloud" "github.com/juju/juju/controller" "github.com/juju/juju/environs" "github.com/juju/juju/environs/config" @@ -19,11 +19,11 @@ // PrepareParams contains the parameters for preparing a controller Environ // for bootstrapping. type PrepareParams struct { - // BaseConfig contains the base configuration for the controller model. + // ModelConfig contains the base configuration for the controller model. // - // This includes the model name, cloud type, and any user-supplied - // configuration. It does not include any default attributes. - BaseConfig map[string]interface{} + // This includes the model name, cloud type, any user-supplied + // configuration, config inherited from controller, and any defaults. + ModelConfig map[string]interface{} // ControllerConfig is the configuration of the controller being prepared. ControllerConfig controller.Config @@ -31,26 +31,9 @@ // ControllerName is the name of the controller being prepared. ControllerName string - // CloudName is the name of the cloud that the controller is being - // prepared for. - CloudName string - - // CloudRegion is the name of the region of the cloud to create - // the Juju controller in. This will be empty for clouds without - // regions. - CloudRegion string - - // CloudEndpoint is the location of the primary API endpoint to - // use when communicating with the cloud. - CloudEndpoint string - - // CloudStorageEndpoint is the location of the API endpoint to use - // when communicating with the cloud's storage service. This will - // be empty for clouds that have no cloud-specific API endpoint. - CloudStorageEndpoint string - - // Credential is the credential to use to bootstrap. - Credential cloud.Credential + // Cloud is the specification of the cloud that the controller is + // being prepared for. + Cloud environs.CloudSpec // CredentialName is the name of the credential to use to bootstrap. // This will be empty for auto-detected credentials. @@ -68,7 +51,7 @@ if p.ControllerName == "" { return errors.NotValidf("empty controller name") } - if p.CloudName == "" { + if p.Cloud.Name == "" { return errors.NotValidf("empty cloud name") } if p.AdminSecret == "" { @@ -100,7 +83,7 @@ return nil, errors.Annotatef(err, "error reading controller %q info", args.ControllerName) } - cloudType, ok := args.BaseConfig["type"].(string) + cloudType, ok := args.ModelConfig["type"].(string) if !ok { return nil, errors.NotFoundf("cloud type in base configuration") } @@ -130,6 +113,10 @@ details prepareDetails, controllerName, modelName string, ) error { + qualifiedModelName := jujuclient.JoinOwnerModelName( + names.NewUserTag(details.AccountDetails.User), + modelName, + ) if err := store.UpdateController(controllerName, details.ControllerDetails); err != nil { return errors.Trace(err) } @@ -139,10 +126,10 @@ if err := store.UpdateAccount(controllerName, details.AccountDetails); err != nil { return errors.Trace(err) } - if err := store.UpdateModel(controllerName, modelName, details.ModelDetails); err != nil { + if err := store.UpdateModel(controllerName, qualifiedModelName, details.ModelDetails); err != nil { return errors.Trace(err) } - if err := store.SetCurrentModel(controllerName, modelName); err != nil { + if err := store.SetCurrentModel(controllerName, qualifiedModelName); err != nil { return errors.Trace(err) } return nil @@ -155,29 +142,34 @@ ) (environs.Environ, prepareDetails, error) { var details prepareDetails - cfg, err := config.New(config.UseDefaults, args.BaseConfig) + cfg, err := config.New(config.NoDefaults, args.ModelConfig) if err != nil { return nil, details, errors.Trace(err) } - cfg, err = p.BootstrapConfig(environs.BootstrapConfigParams{ - args.ControllerConfig.ControllerUUID(), cfg, args.Credential, args.CloudRegion, - args.CloudEndpoint, args.CloudStorageEndpoint, + cfg, err = p.PrepareConfig(environs.PrepareConfigParams{ + args.ControllerConfig.ControllerUUID(), args.Cloud, cfg, }) if err != nil { return nil, details, errors.Trace(err) } - env, err := p.PrepareForBootstrap(ctx, cfg) + env, err := p.Open(environs.OpenParams{ + Cloud: args.Cloud, + Config: cfg, + }) if err != nil { return nil, details, errors.Trace(err) } + if err := env.PrepareForBootstrap(ctx); err != nil { + return nil, details, errors.Trace(err) + } // We store the base configuration only; we don't want the // default attributes, generated secrets/certificates, or // UUIDs stored in the bootstrap config. Make a copy, so // we don't disturb the caller's config map. details.Config = make(map[string]interface{}) - for k, v := range args.BaseConfig { + for k, v := range args.ModelConfig { details.Config[k] = v } delete(details.Config, config.UUIDKey) @@ -190,17 +182,34 @@ return nil, details, errors.New("controller config is missing CA certificate") } + // We want to store attributes describing how a controller has been configured. + // These do not include the CACert or UUID since they will be replaced with new + // values when/if we need to use this configuration. + details.ControllerConfig = make(controller.Config) + for k, v := range args.ControllerConfig { + if k == controller.CACertKey || k == controller.ControllerUUIDKey { + continue + } + details.ControllerConfig[k] = v + } + for k, v := range args.ControllerConfig { + if k == controller.CACertKey || k == controller.ControllerUUIDKey { + continue + } + details.ControllerConfig[k] = v + } details.CACert = caCert details.ControllerUUID = args.ControllerConfig.ControllerUUID() details.User = environs.AdminUser details.Password = args.AdminSecret details.ModelUUID = cfg.UUID() - details.ControllerDetails.Cloud = args.CloudName - details.ControllerDetails.CloudRegion = args.CloudRegion - details.BootstrapConfig.Cloud = args.CloudName - details.BootstrapConfig.CloudRegion = args.CloudRegion - details.CloudEndpoint = args.CloudEndpoint - details.CloudStorageEndpoint = args.CloudStorageEndpoint + details.ControllerDetails.Cloud = args.Cloud.Name + details.ControllerDetails.CloudRegion = args.Cloud.Region + details.BootstrapConfig.CloudType = args.Cloud.Type + details.BootstrapConfig.Cloud = args.Cloud.Name + details.BootstrapConfig.CloudRegion = args.Cloud.Region + details.CloudEndpoint = args.Cloud.Endpoint + details.CloudStorageEndpoint = args.Cloud.StorageEndpoint details.Credential = args.CredentialName return env, details, nil diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/bootstrap/prepare_test.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/bootstrap/prepare_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/bootstrap/prepare_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/bootstrap/prepare_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -14,6 +14,7 @@ sstesting "github.com/juju/juju/environs/simplestreams/testing" envtesting "github.com/juju/juju/environs/testing" "github.com/juju/juju/juju/keys" + "github.com/juju/juju/jujuclient" "github.com/juju/juju/jujuclient/jujuclienttesting" "github.com/juju/juju/provider/dummy" "github.com/juju/juju/testing" @@ -42,6 +43,7 @@ baselineAttrs := dummy.SampleConfig().Merge(testing.Attrs{ "controller": false, "name": "erewhemos", + "test-mode": true, }).Delete( "admin-secret", ) @@ -50,14 +52,17 @@ controllerStore := jujuclienttesting.NewMemStore() ctx := envtesting.BootstrapContext(c) controllerCfg := controller.Config{ - controller.ControllerUUIDKey: testing.ModelTag.Id(), - controller.CACertKey: testing.CACert, + controller.ControllerUUIDKey: testing.ModelTag.Id(), + controller.CACertKey: testing.CACert, + controller.ApiPort: 17777, + controller.StatePort: 1234, + controller.SetNumaControlPolicyKey: true, } _, err = bootstrap.Prepare(ctx, controllerStore, bootstrap.PrepareParams{ ControllerConfig: controllerCfg, ControllerName: cfg.Name(), - BaseConfig: cfg.AllAttrs(), - CloudName: "dummy", + ModelConfig: cfg.AllAttrs(), + Cloud: dummy.SampleCloudSpec(), AdminSecret: "admin-secret", }) c.Assert(err, jc.ErrorIsNil) @@ -68,12 +73,40 @@ c.Assert(foundController.ControllerUUID, gc.DeepEquals, cfg.UUID()) c.Assert(foundController.Cloud, gc.Equals, "dummy") + // Check that bootstrap config was written + bootstrapCfg, err := controllerStore.BootstrapConfigForController(cfg.Name()) + c.Assert(err, jc.ErrorIsNil) + c.Assert(bootstrapCfg, jc.DeepEquals, &jujuclient.BootstrapConfig{ + ControllerConfig: controller.Config{ + controller.ApiPort: 17777, + controller.StatePort: 1234, + controller.SetNumaControlPolicyKey: true, + }, + Config: map[string]interface{}{ + "default-series": "xenial", + "firewall-mode": "instance", + "ssl-hostname-verification": true, + "logging-config": "=DEBUG;unit=DEBUG", + "secret": "pork", + "authorized-keys": testing.FakeAuthKeys, + "type": "dummy", + "name": "erewhemos", + "controller": false, + "development": false, + "test-mode": true, + }, + Cloud: "dummy", + CloudType: "dummy", + CloudEndpoint: "dummy-endpoint", + CloudStorageEndpoint: "dummy-storage-endpoint", + }) + // Check we cannot call Prepare again. _, err = bootstrap.Prepare(ctx, controllerStore, bootstrap.PrepareParams{ ControllerConfig: controllerCfg, ControllerName: cfg.Name(), - BaseConfig: cfg.AllAttrs(), - CloudName: "dummy", + ModelConfig: cfg.AllAttrs(), + Cloud: dummy.SampleCloudSpec(), AdminSecret: "admin-secret", }) c.Assert(err, jc.Satisfies, errors.IsAlreadyExists) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/bootstrap/tools.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/bootstrap/tools.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/bootstrap/tools.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/bootstrap/tools.go 2016-08-16 08:56:25.000000000 +0000 @@ -13,6 +13,7 @@ "github.com/juju/utils/set" "github.com/juju/version" + "github.com/juju/juju/constraints" "github.com/juju/juju/environs" envtools "github.com/juju/juju/environs/tools" coretools "github.com/juju/juju/tools" @@ -44,21 +45,17 @@ } } // If no architecture is specified, ensure the target provider supports instances matching our architecture. - supportedArchitectures, err := env.SupportedArchitectures() + validator, err := env.ConstraintsValidator() if err != nil { - return fmt.Errorf( - "no packaged tools available and cannot determine model's supported architectures: %v", err) - } - archSupported := false - for _, arch := range supportedArchitectures { - if hostArch == arch { - archSupported = true - break - } - } - if !archSupported { - envType := env.Config().Type() - return errors.Errorf("model %q of type %s does not support instances running on %q", env.Config().Name(), envType, hostArch) + return errors.Annotate(err, + "no packaged tools available and cannot determine model's supported architectures", + ) + } + if _, err := validator.Validate(constraints.Value{Arch: &hostArch}); err != nil { + return errors.Errorf( + "model %q of type %s does not support instances running on %q", + env.Config().Name(), env.Config().Type(), hostArch, + ) } return nil } @@ -116,7 +113,7 @@ // Collate the set of arch+series that are externally available // so we can see if we need to build any locally. If we need // to, only then do we validate that we can upload (which - // involves a potentially expensive SupportedArchitectures call). + // involves a potentially expensive ConstraintsValidator call). archSeries := make(set.Strings) for _, tools := range toolsList { archSeries.Add(tools.Version.Arch + tools.Version.Series) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/bootstrap/tools_test.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/bootstrap/tools_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/bootstrap/tools_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/bootstrap/tools_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -262,7 +262,7 @@ availableTools, err := bootstrap.FindAvailableTools(env, nil, nil, nil, false, true) c.Assert(err, jc.ErrorIsNil) c.Assert(len(availableTools), jc.GreaterThan, 1) - c.Assert(env.supportedArchitecturesCount, gc.Equals, 1) + c.Assert(env.constraintsValidatorCount, gc.Equals, 1) var trustyToolsFound int expectedVersion := jujuversion.Current expectedVersion.Build++ @@ -301,5 +301,5 @@ availableTools, err := bootstrap.FindAvailableTools(env, nil, nil, nil, false, false) c.Assert(err, jc.ErrorIsNil) c.Assert(availableTools, gc.HasLen, len(allTools)) - c.Assert(env.supportedArchitecturesCount, gc.Equals, 0) + c.Assert(env.constraintsValidatorCount, gc.Equals, 0) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/cloudspec.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/cloudspec.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/cloudspec.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/cloudspec.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,70 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package environs + +import ( + "github.com/juju/errors" + names "gopkg.in/juju/names.v2" + + jujucloud "github.com/juju/juju/cloud" +) + +// CloudSpec describes a specific cloud configuration, for the purpose +// of opening an Environ to manage the cloud resources. +type CloudSpec struct { + // Type is the type of cloud, eg aws, openstack etc. + Type string + + // Name is the name of the cloud. + Name string + + // Region is the name of the cloud region, if the cloud supports + // regions. + Region string + + // Endpoint is the endpoint for the cloud (region). + Endpoint string + + // StorageEndpoint is the storage endpoint for the cloud (region). + StorageEndpoint string + + // Credential is the cloud credential to use to authenticate + // with the cloud, or nil if the cloud does not require any + // credentials. + Credential *jujucloud.Credential +} + +// Validate validates that the CloudSpec is well-formed. It does +// not ensure that the cloud type and credentials are valid. +func (cs CloudSpec) Validate() error { + if cs.Type == "" { + return errors.NotValidf("empty Type") + } + if !names.IsValidCloud(cs.Name) { + return errors.NotValidf("cloud name %q", cs.Name) + } + return nil +} + +// MakeCloudSpec returns a CloudSpec from the given +// Cloud, cloud and region names, and credential. +func MakeCloudSpec(cloud jujucloud.Cloud, cloudName, cloudRegionName string, credential *jujucloud.Credential) (CloudSpec, error) { + cloudSpec := CloudSpec{ + Type: cloud.Type, + Name: cloudName, + Region: cloudRegionName, + Endpoint: cloud.Endpoint, + StorageEndpoint: cloud.StorageEndpoint, + Credential: credential, + } + if cloudRegionName != "" { + cloudRegion, err := jujucloud.RegionByName(cloud.Regions, cloudRegionName) + if err != nil { + return CloudSpec{}, errors.Annotate(err, "getting cloud region definition") + } + cloudSpec.Endpoint = cloudRegion.Endpoint + cloudSpec.StorageEndpoint = cloudRegion.StorageEndpoint + } + return cloudSpec, nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/config/config.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/config/config.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/config/config.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/config/config.go 2016-08-16 08:56:25.000000000 +0000 @@ -103,11 +103,6 @@ // of k=v pairs, defining the tags for ResourceTags. ResourceTagsKey = "resource-tags" - // CloudImageBaseURL allows a user to override the default url that the - // 'ubuntu-cloudimg-query' executable uses to find container images. This - // is primarily for enabling Juju to work cleanly in a closed network. - CloudImageBaseURL = "cloudimg-base-url" - // LogForwardEnabled determines whether the log forward functionality is enabled. LogForwardEnabled = "logforward-enabled" @@ -278,6 +273,51 @@ return c, nil } +var defaultConfigValues = map[string]interface{}{ + // Network. + "firewall-mode": FwInstance, + "disable-network-management": false, + IgnoreMachineAddresses: false, + "ssl-hostname-verification": true, + "proxy-ssh": false, + + "default-series": series.LatestLts(), + ProvisionerHarvestModeKey: HarvestDestroyed.String(), + ResourceTagsKey: "", + "logging-config": "", + AutomaticallyRetryHooks: true, + "enable-os-refresh-update": true, + "enable-os-upgrade": true, + "development": false, + "test-mode": false, + + // Image and agent streams and URLs. + "image-stream": "released", + "image-metadata-url": "", + AgentStreamKey: "released", + AgentMetadataURLKey: "", + + // Log forward settings. + LogForwardEnabled: false, + + // Proxy settings. + HttpProxyKey: "", + HttpsProxyKey: "", + FtpProxyKey: "", + NoProxyKey: "", + AptHttpProxyKey: "", + AptHttpsProxyKey: "", + AptFtpProxyKey: "", + "apt-mirror": "", +} + +// ConfigDefaults returns the config default values +// to be used for any new model where there is no +// value yet defined. +func ConfigDefaults() map[string]interface{} { + return defaultConfigValues +} + func (c *Config) ensureUnitLogging() error { loggingConfig := c.asString("logging-config") // If the logging config hasn't been set, then look for the os environment @@ -289,7 +329,7 @@ loggingConfig = loggo.LoggerInfo() } } - levels, err := loggo.ParseConfigurationString(loggingConfig) + levels, err := loggo.ParseConfigString(loggingConfig) if err != nil { return err } @@ -314,6 +354,26 @@ return processedAttrs } +// CoerceForStorage transforms attributes prior to being saved in a persistent store. +func CoerceForStorage(attrs map[string]interface{}) map[string]interface{} { + coercedAttrs := make(map[string]interface{}, len(attrs)) + for attrName, attrValue := range attrs { + if attrName == ResourceTagsKey { + // Resource Tags are specified by the user as a string but transformed + // to a map when config is parsed. We want to store as a string. + var tagsSlice []string + if tags, ok := attrValue.(map[string]string); ok { + for resKey, resValue := range tags { + tagsSlice = append(tagsSlice, fmt.Sprintf("%v=%v", resKey, resValue)) + } + attrValue = strings.Join(tagsSlice, " ") + } + } + coercedAttrs[attrName] = attrValue + } + return coercedAttrs +} + // InvalidConfigValue is an error type for a config value that failed validation. type InvalidConfigValueError struct { // Key is the config key used to access the value. @@ -350,7 +410,7 @@ modelName := cfg.asString(NameKey) if modelName == "" { - return fmt.Errorf("empty name in model configuration") + return errors.New("empty name in model configuration") } if !names.IsValidModelName(modelName) { return fmt.Errorf("%q is not a valid name: model names may only contain lowercase letters, digits and hyphens", modelName) @@ -366,7 +426,7 @@ // If the logging config is set, make sure it is valid. if v, ok := cfg.defined["logging-config"].(string); ok { - if _, err := loggo.ParseConfigurationString(v); err != nil { + if _, err := loggo.ParseConfigString(v); err != nil { return err } } @@ -389,13 +449,17 @@ // Check the immutable config values. These can't change if old != nil { for _, attr := range immutableAttributes { - if newv, oldv := cfg.defined[attr], old.defined[attr]; newv != oldv { + oldv, ok := old.defined[attr] + if !ok { + continue + } + if newv := cfg.defined[attr]; newv != oldv { return fmt.Errorf("cannot change %s from %#v to %#v", attr, oldv, newv) } } if _, oldFound := old.AgentVersion(); oldFound { if _, newFound := cfg.AgentVersion(); !newFound { - return fmt.Errorf("cannot clear agent-version") + return errors.New("cannot clear agent-version") } } } @@ -654,7 +718,8 @@ // Development returns whether the environment is in development mode. func (c *Config) Development() bool { - return c.defined["development"].(bool) + value, _ := c.defined["development"].(bool) + return value } // EnableOSRefreshUpdate returns whether or not newly provisioned @@ -740,7 +805,8 @@ // In this case, accessing the charm store does not affect statistical // data of the store. func (c *Config) TestMode() bool { - return c.defined["test-mode"].(bool) + val, _ := c.defined["test-mode"].(bool) + return val } // DisableNetworkManagement reports whether Juju is allowed to @@ -764,13 +830,6 @@ return bs, bs != "" } -// CloudImageBaseURL returns the specified override url that the 'ubuntu- -// cloudimg-query' executable uses to find container images. The empty string -// means that the default URL is used. -func (c *Config) CloudImageBaseURL() string { - return c.asString(CloudImageBaseURL) -} - // ResourceTags returns a set of tags to set on environment resources // that Juju creates and manages, if the provider supports them. These // tags have no special meaning to Juju, but may be used for existing @@ -857,16 +916,22 @@ // but some fields listed as optional here are actually mandatory // with NoDefaults and are checked at the later Validate stage. var alwaysOptional = schema.Defaults{ - // Model config attributes - AgentVersionKey: schema.Omit, - AuthorizedKeysKey: schema.Omit, + AgentVersionKey: schema.Omit, + AuthorizedKeysKey: schema.Omit, + + LogForwardEnabled: schema.Omit, + LogFwdSyslogHost: schema.Omit, + LogFwdSyslogCACert: schema.Omit, + LogFwdSyslogClientCert: schema.Omit, + LogFwdSyslogClientKey: schema.Omit, + + // Storage related config. + // Environ providers will specify their own defaults. + StorageDefaultBlockSourceKey: schema.Omit, + + "firewall-mode": schema.Omit, "logging-config": schema.Omit, ProvisionerHarvestModeKey: schema.Omit, - LogForwardEnabled: schema.Omit, - LogFwdSyslogHost: schema.Omit, - LogFwdSyslogCACert: schema.Omit, - LogFwdSyslogClientCert: schema.Omit, - LogFwdSyslogClientKey: schema.Omit, HttpProxyKey: schema.Omit, HttpsProxyKey: schema.Omit, FtpProxyKey: schema.Omit, @@ -875,47 +940,38 @@ AptHttpsProxyKey: schema.Omit, AptFtpProxyKey: schema.Omit, "apt-mirror": schema.Omit, - "disable-network-management": schema.Omit, - IgnoreMachineAddresses: schema.Omit, AgentStreamKey: schema.Omit, ResourceTagsKey: schema.Omit, - CloudImageBaseURL: schema.Omit, - - // AutomaticallyRetryHooks is assumed to be true if missing - AutomaticallyRetryHooks: schema.Omit, - - // Storage related config. - // Environ providers will specify their own defaults. - StorageDefaultBlockSourceKey: schema.Omit, - - "proxy-ssh": schema.Omit, - "enable-os-refresh-update": schema.Omit, - "enable-os-upgrade": schema.Omit, - "image-stream": schema.Omit, - "image-metadata-url": schema.Omit, - AgentMetadataURLKey: schema.Omit, - "default-series": "", - "test-mode": false, + "cloudimg-base-url": schema.Omit, + "enable-os-refresh-update": schema.Omit, + "enable-os-upgrade": schema.Omit, + "image-stream": schema.Omit, + "image-metadata-url": schema.Omit, + AgentMetadataURLKey: schema.Omit, + "default-series": schema.Omit, + "development": schema.Omit, + "ssl-hostname-verification": schema.Omit, + "proxy-ssh": schema.Omit, + "disable-network-management": schema.Omit, + IgnoreMachineAddresses: schema.Omit, + AutomaticallyRetryHooks: schema.Omit, + "test-mode": schema.Omit, } func allowEmpty(attr string) bool { - return alwaysOptional[attr] == "" + return alwaysOptional[attr] == "" || alwaysOptional[attr] == schema.Omit } -var defaults = allDefaults() +var defaultsWhenParsing = allDefaults() // allDefaults returns a schema.Defaults that contains // defaults to be used when creating a new config with // UseDefaults. func allDefaults() schema.Defaults { - d := schema.Defaults{ - "firewall-mode": FwInstance, - "development": false, - "ssl-hostname-verification": true, - "proxy-ssh": false, - "disable-network-management": false, - IgnoreMachineAddresses: false, - AutomaticallyRetryHooks: true, + d := schema.Defaults{} + configDefaults := ConfigDefaults() + for attr, val := range configDefaults { + d[attr] = val } for attr, val := range alwaysOptional { if _, ok := d[attr]; !ok { @@ -936,7 +992,7 @@ } var ( - withDefaultsChecker = schema.FieldMap(fields, defaults) + withDefaultsChecker = schema.FieldMap(fields, defaultsWhenParsing) noDefaultsChecker = schema.FieldMap(fields, alwaysOptional) ) @@ -1083,11 +1139,6 @@ Type: environschema.Tstring, Group: environschema.EnvironGroup, }, - CloudImageBaseURL: { - Description: "A URL to use instead of the default 'https://cloud-images.ubuntu.com/query' that the 'ubuntu-cloudimg-query' executable uses to find container images. This is primarily for enabling Juju to work cleanly in a closed network.", - Type: environschema.Tstring, - Group: environschema.EnvironGroup, - }, "default-series": { Description: "The default series of Ubuntu to use for deploying charms", Type: environschema.Tstring, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/config/config_test.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/config/config_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/config/config_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/config/config_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,8 +5,6 @@ import ( "fmt" - "os" - "regexp" "strings" stdtesting "testing" "time" @@ -42,21 +40,26 @@ s.FakeJujuXDGDataHomeSuite.SetUpTest(c) // Make sure that the defaults are used, which // is =WARNING - loggo.ResetLoggers() + loggo.DefaultContext().ResetLoggerLevels() } // sampleConfig holds a configuration with all required // attributes set. var sampleConfig = testing.Attrs{ - "type": "my-type", - "name": "my-name", - "uuid": testing.ModelTag.Id(), - "authorized-keys": testing.FakeAuthKeys, - "firewall-mode": config.FwInstance, - "unknown": "my-unknown", - "ssl-hostname-verification": true, - "development": false, - "default-series": series.LatestLts(), + "type": "my-type", + "name": "my-name", + "uuid": testing.ModelTag.Id(), + "authorized-keys": testing.FakeAuthKeys, + "firewall-mode": config.FwInstance, + "unknown": "my-unknown", + "ssl-hostname-verification": true, + "development": false, + "default-series": series.LatestLts(), + "disable-network-management": false, + "ignore-machine-addresses": false, + "automatically-retry-hooks": true, + "proxy-ssh": false, + "resource-tags": []string{}, } type configTest struct { @@ -72,8 +75,6 @@ "a": "b", "c": "", "d": "e", } -var quotedPathSeparator = regexp.QuoteMeta(string(os.PathSeparator)) - var minimalConfigAttrs = testing.Attrs{ "type": "my-type", "name": "my-name", @@ -108,12 +109,6 @@ "default-series": "my-series", }), }, { - about: "Implicit series with empty value", - useDefaults: config.UseDefaults, - attrs: minimalConfigAttrs.Merge(testing.Attrs{ - "default-series": "", - }), - }, { about: "Explicit logging", useDefaults: config.UseDefaults, attrs: minimalConfigAttrs.Merge(testing.Attrs{ @@ -385,18 +380,23 @@ about: "Config settings from juju actual installation", useDefaults: config.NoDefaults, attrs: map[string]interface{}{ - "name": "sample", - "development": false, - "ssl-hostname-verification": true, - "authorized-keys": "ssh-rsa mykeys rog@rog-x220\n", - "region": "us-east-1", - "default-series": "precise", - "secret-key": "a-secret-key", - "access-key": "an-access-key", - "agent-version": "1.13.2", - "firewall-mode": "instance", - "type": "ec2", - "uuid": testing.ModelTag.Id(), + "name": "sample", + "development": false, + "ssl-hostname-verification": true, + "authorized-keys": "ssh-rsa mykeys rog@rog-x220\n", + "region": "us-east-1", + "default-series": "precise", + "secret-key": "a-secret-key", + "access-key": "an-access-key", + "agent-version": "1.13.2", + "firewall-mode": "instance", + "disable-network-management": false, + "ignore-machine-addresses": false, + "automatically-retry-hooks": true, + "proxy-ssh": false, + "resource-tags": []string{}, + "type": "ec2", + "uuid": testing.ModelTag.Id(), }, }, { about: "TestMode flag specified", @@ -432,8 +432,6 @@ }), err: `empty uuid in model configuration`, }, - missingAttributeNoDefault("development"), - missingAttributeNoDefault("ssl-hostname-verification"), { about: "Explicit apt-mirror", useDefaults: config.UseDefaults, @@ -536,15 +534,6 @@ }, } -func missingAttributeNoDefault(attrName string) configTest { - return configTest{ - about: fmt.Sprintf("No default: missing %s", attrName), - useDefaults: config.NoDefaults, - attrs: sampleConfig.Delete(attrName), - err: fmt.Sprintf("%s: expected [a-z]+, got nothing", attrName), - } -} - type testFile struct { name, data string } @@ -601,12 +590,13 @@ testmode, _ := test.attrs["test-mode"].(bool) c.Assert(cfg.TestMode(), gc.Equals, testmode) - series, _ := test.attrs["default-series"].(string) - if defaultSeries, ok := cfg.DefaultSeries(); ok { - c.Assert(defaultSeries, gc.Equals, series) + seriesAttr, _ := test.attrs["default-series"].(string) + defaultSeries, ok := cfg.DefaultSeries() + c.Assert(ok, jc.IsTrue) + if seriesAttr != "" { + c.Assert(defaultSeries, gc.Equals, seriesAttr) } else { - c.Assert(series, gc.Equals, "") - c.Assert(defaultSeries, gc.Equals, "") + c.Assert(defaultSeries, gc.Equals, series.LatestLts()) } if m, _ := test.attrs["firewall-mode"].(string); m != "" { @@ -694,11 +684,20 @@ c.Assert(agentStreamValue, gc.Equals, expectedAgentStreamAttr) resourceTags, cfgHasResourceTags := cfg.ResourceTags() - if _, ok := test.attrs["resource-tags"]; ok { - c.Assert(cfgHasResourceTags, jc.IsTrue) - c.Assert(resourceTags, jc.DeepEquals, testResourceTagsMap) + c.Assert(cfgHasResourceTags, jc.IsTrue) + if tags, ok := test.attrs["resource-tags"]; ok { + switch tags := tags.(type) { + case []string: + if len(tags) > 0 { + c.Assert(resourceTags, jc.DeepEquals, testResourceTagsMap) + } + case string: + if tags != "" { + c.Assert(resourceTags, jc.DeepEquals, testResourceTagsMap) + } + } } else { - c.Assert(cfgHasResourceTags, jc.IsFalse) + c.Assert(resourceTags, gc.HasLen, 0) } } @@ -715,16 +714,20 @@ // Normally this is handled by gitjujutesting.FakeHome s.PatchEnvironment(osenv.JujuLoggingConfigEnvKey, "") attrs := map[string]interface{}{ - "type": "my-type", - "name": "my-name", - "uuid": "90168e4c-2f10-4e9c-83c2-1fb55a58e5a9", - "authorized-keys": testing.FakeAuthKeys, - "firewall-mode": config.FwInstance, - "unknown": "my-unknown", - "ssl-hostname-verification": true, - "development": false, - "default-series": series.LatestLts(), - "test-mode": false, + "type": "my-type", + "name": "my-name", + "uuid": "90168e4c-2f10-4e9c-83c2-1fb55a58e5a9", + "authorized-keys": testing.FakeAuthKeys, + "firewall-mode": config.FwInstance, + "unknown": "my-unknown", + "ssl-hostname-verification": true, + "default-series": series.LatestLts(), + "disable-network-management": false, + "ignore-machine-addresses": false, + "automatically-retry-hooks": true, + "proxy-ssh": false, + "development": false, + "test-mode": false, } cfg, err := config.New(config.NoDefaults, attrs) c.Assert(err, jc.ErrorIsNil) @@ -964,19 +967,6 @@ c.Assert(config.AutomaticallyRetryHooks(), gc.Equals, true) } -func (s *ConfigSuite) TestCloudImageBaseURL(c *gc.C) { - s.addJujuFiles(c) - config := newTestConfig(c, testing.Attrs{}) - c.Assert(config.CloudImageBaseURL(), gc.Equals, "") -} - -func (s *ConfigSuite) TestCloudImageBaseURLSet(c *gc.C) { - s.addJujuFiles(c) - config := newTestConfig(c, testing.Attrs{ - "cloudimg-base-url": "http://local.foo/query"}) - c.Assert(config.CloudImageBaseURL(), gc.Equals, "http://local.foo/query") -} - func (s *ConfigSuite) TestProxyValuesWithFallback(c *gc.C) { s.addJujuFiles(c) @@ -1123,6 +1113,23 @@ c.Assert(schema, gc.IsNil) } +func (s *ConfigSuite) TestCoerceForStorage(c *gc.C) { + cfg := newTestConfig(c, testing.Attrs{ + "resource-tags": "a=b c=d"}) + tags, ok := cfg.ResourceTags() + c.Assert(ok, jc.IsTrue) + expectedTags := map[string]string{"a": "b", "c": "d"} + c.Assert(tags, gc.DeepEquals, expectedTags) + tagsStr := config.CoerceForStorage(cfg.AllAttrs())["resource-tags"].(string) + tagItems := strings.Split(tagsStr, " ") + tagsMap := make(map[string]string) + for _, kv := range tagItems { + parts := strings.Split(kv, "=") + tagsMap[parts[0]] = parts[1] + } + c.Assert(tagsMap, gc.DeepEquals, expectedTags) +} + var specializeCharmRepoTests = []struct { about string testMode bool diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/config/source.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/config/source.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/config/source.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/config/source.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,6 +7,10 @@ // After a call to UpdateModelConfig, any attributes added/removed // will have a source of JujuModelConfigSource. const ( + // JujuDefaultSource is used to label model config attributes that + // come from hard coded defaults. + JujuDefaultSource = "default" + // JujuControllerSource is used to label model config attributes that // come from those associated with the controller. JujuControllerSource = "controller" diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/config/validator.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/config/validator.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/config/validator.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/config/validator.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,15 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package config + +// Validator is an interface for validating model configuration. +type Validator interface { + // Validate ensures that cfg is a valid configuration. + // If old is not nil, Validate should use it to determine + // whether a configuration change is valid. + // + // TODO(axw) Validate should just return an error. We should + // use a separate mechanism for updating config. + Validate(cfg, old *Config) (valid *Config, _ error) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/environ.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/environ.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/environ.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/environ.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,6 +5,7 @@ import ( "github.com/juju/errors" + "gopkg.in/juju/names.v2" "github.com/juju/juju/environs/config" ) @@ -12,19 +13,30 @@ // EnvironConfigGetter exposes a model configuration to its clients. type EnvironConfigGetter interface { ModelConfig() (*config.Config, error) + CloudSpec(names.ModelTag) (CloudSpec, error) } // NewEnvironFunc is the type of a function that, given a model config, // returns an Environ. This will typically be environs.New. -type NewEnvironFunc func(*config.Config) (Environ, error) +type NewEnvironFunc func(OpenParams) (Environ, error) // GetEnviron returns the environs.Environ ("provider") associated // with the model. func GetEnviron(st EnvironConfigGetter, newEnviron NewEnvironFunc) (Environ, error) { - envcfg, err := st.ModelConfig() + modelConfig, err := st.ModelConfig() if err != nil { return nil, errors.Trace(err) } - env, err := newEnviron(envcfg) - return env, errors.Trace(err) + cloudSpec, err := st.CloudSpec(names.NewModelTag(modelConfig.UUID())) + if err != nil { + return nil, errors.Trace(err) + } + env, err := newEnviron(OpenParams{ + Cloud: cloudSpec, + Config: modelConfig, + }) + if err != nil { + return nil, errors.Trace(err) + } + return env, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/environ_test.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/environ_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/environ_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/environ_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,6 +9,7 @@ "github.com/juju/juju/environs" "github.com/juju/juju/juju/testing" + "github.com/juju/juju/state/stateenvirons" ) type environSuite struct { @@ -18,7 +19,7 @@ var _ = gc.Suite(&environSuite{}) func (s *environSuite) TestGetEnvironment(c *gc.C) { - env, err := environs.GetEnviron(s.State, environs.New) + env, err := stateenvirons.GetNewEnvironFunc(environs.New)(s.State) c.Assert(err, jc.ErrorIsNil) config, err := s.State.ModelConfig() c.Assert(err, jc.ErrorIsNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/filestorage/filestorage.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/filestorage/filestorage.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/filestorage/filestorage.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/filestorage/filestorage.go 2016-08-16 08:56:25.000000000 +0000 @@ -114,6 +114,7 @@ // DefaultConsistencyStrategy implements storage.StorageReader.ConsistencyStrategy. func (f *fileStorageReader) DefaultConsistencyStrategy() utils.AttemptStrategy { + // TODO(katco): 2016-08-09: lp:1611427 return utils.AttemptStrategy{} } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/imagemetadata/generate.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/imagemetadata/generate.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/imagemetadata/generate.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/imagemetadata/generate.go 2016-08-16 08:56:25.000000000 +0000 @@ -55,14 +55,39 @@ return existingMetadata, nil } +// mapKey returns a key that uniquely identifies image metadata. +// The metadata for different images may have similar values +// for some parameters. This key ensures that truly distinct +// metadata is not overwritten by closely related ones. +// This key is similar to image metadata key built in state which combines +// parameter values rather than using image id to ensure record uniqueness. func mapKey(im *ImageMetadata) string { - return fmt.Sprintf("%s-%s", im.productId(), im.RegionName) + return fmt.Sprintf("%s-%s-%s-%s", im.productId(), im.RegionName, im.VirtType, im.Storage) } // mergeMetadata merges the newMetadata into existingMetadata, overwriting existing matching image records. func mergeMetadata(seriesVersion string, cloudSpec *simplestreams.CloudSpec, newMetadata, existingMetadata []*ImageMetadata) ([]*ImageMetadata, []simplestreams.CloudSpec) { + regions := make(map[string]bool) + var allCloudSpecs = []simplestreams.CloudSpec{} + // Each metadata item defines its own cloud specification. + // However, when we combine metadata items in the file, we do not want to + // repeat common cloud specifications in index definition. + // Since region name and endpoint have 1:1 correspondence, + // only one distinct cloud specification for each region + // is being collected. + addDistinctCloudSpec := func(im *ImageMetadata) { + if _, ok := regions[im.RegionName]; !ok { + regions[im.RegionName] = true + aCloudSpec := simplestreams.CloudSpec{ + Region: im.RegionName, + Endpoint: im.Endpoint, + } + allCloudSpecs = append(allCloudSpecs, aCloudSpec) + } + } + var toWrite = make([]*ImageMetadata, len(newMetadata)) imageIds := make(map[string]bool) for i, im := range newMetadata { @@ -72,23 +97,13 @@ newRecord.Endpoint = cloudSpec.Endpoint toWrite[i] = &newRecord imageIds[mapKey(&newRecord)] = true + addDistinctCloudSpec(&newRecord) } - regions := make(map[string]bool) - var allCloudSpecs = []simplestreams.CloudSpec{*cloudSpec} for _, im := range existingMetadata { - if _, ok := imageIds[mapKey(im)]; ok { - continue - } - toWrite = append(toWrite, im) - if _, ok := regions[im.RegionName]; ok { - continue - } - regions[im.RegionName] = true - existingCloudSpec := simplestreams.CloudSpec{ - Region: im.RegionName, - Endpoint: im.Endpoint, + if _, ok := imageIds[mapKey(im)]; !ok { + toWrite = append(toWrite, im) + addDistinctCloudSpec(im) } - allCloudSpecs = append(allCloudSpecs, existingCloudSpec) } return toWrite, allCloudSpecs } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/imagemetadata/generate_test.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/imagemetadata/generate_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/imagemetadata/generate_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/imagemetadata/generate_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -21,7 +21,7 @@ coretesting.BaseSuite } -func assertFetch(c *gc.C, stor storage.Storage, series, arch, region, endpoint, id string) { +func assertFetch(c *gc.C, stor storage.Storage, series, arch, region, endpoint string, ids ...string) { cons := imagemetadata.NewImageConstraint(simplestreams.LookupParams{ CloudSpec: simplestreams.CloudSpec{region, endpoint}, Series: []string{series}, @@ -30,8 +30,10 @@ dataSource := storage.NewStorageSimpleStreamsDataSource("test datasource", stor, "images", simplestreams.DEFAULT_CLOUD_DATA, false) metadata, _, err := imagemetadata.Fetch([]simplestreams.DataSource{dataSource}, cons) c.Assert(err, jc.ErrorIsNil) - c.Assert(metadata, gc.HasLen, 1) - c.Assert(metadata[0].Id, gc.Equals, id) + c.Assert(metadata, gc.HasLen, len(ids)) + for i, id := range ids { + c.Assert(metadata[i].Id, gc.Equals, id) + } } func (s *generateSuite) TestWriteMetadata(c *gc.C) { @@ -194,3 +196,121 @@ assertFetch(c, targetStorage, "raring", "amd64", "region", "endpoint", "1234") assertFetch(c, targetStorage, "raring", "amd64", "region2", "endpoint2", "abcd") } + +// lp#1600054 +func (s *generateSuite) TestWriteMetadataMergeDifferentVirtType(c *gc.C) { + existingImageMetadata := []*imagemetadata.ImageMetadata{ + { + Id: "1234", + Arch: "amd64", + Version: "13.04", + VirtType: "kvm", + }, + } + cloudSpec := &simplestreams.CloudSpec{ + Region: "region", + Endpoint: "endpoint", + } + dir := c.MkDir() + targetStorage, err := filestorage.NewFileStorageWriter(dir) + c.Assert(err, jc.ErrorIsNil) + err = imagemetadata.MergeAndWriteMetadata("raring", existingImageMetadata, cloudSpec, targetStorage) + c.Assert(err, jc.ErrorIsNil) + newImageMetadata := []*imagemetadata.ImageMetadata{ + { + Id: "abcd", + Arch: "amd64", + Version: "13.04", + VirtType: "lxd", + }, + } + err = imagemetadata.MergeAndWriteMetadata("raring", newImageMetadata, cloudSpec, targetStorage) + c.Assert(err, jc.ErrorIsNil) + + foundMetadata := testing.ParseMetadataFromDir(c, dir) + + expectedMetadata := append(newImageMetadata, existingImageMetadata...) + c.Assert(len(foundMetadata), gc.Equals, len(expectedMetadata)) + for _, im := range expectedMetadata { + im.RegionName = cloudSpec.Region + im.Endpoint = cloudSpec.Endpoint + } + imagemetadata.Sort(expectedMetadata) + c.Assert(foundMetadata, gc.DeepEquals, expectedMetadata) + assertFetch(c, targetStorage, "raring", "amd64", "region", "endpoint", "1234", "abcd") +} + +func (s *generateSuite) TestWriteIndexRegionOnce(c *gc.C) { + existingImageMetadata := []*imagemetadata.ImageMetadata{ + { + Id: "1234", + Arch: "amd64", + Version: "13.04", + VirtType: "kvm", + }, + } + cloudSpec := &simplestreams.CloudSpec{ + Region: "region", + Endpoint: "endpoint", + } + dir := c.MkDir() + targetStorage, err := filestorage.NewFileStorageWriter(dir) + c.Assert(err, jc.ErrorIsNil) + err = imagemetadata.MergeAndWriteMetadata("raring", existingImageMetadata, cloudSpec, targetStorage) + c.Assert(err, jc.ErrorIsNil) + newImageMetadata := []*imagemetadata.ImageMetadata{ + { + Id: "abcd", + Arch: "amd64", + Version: "13.04", + VirtType: "lxd", + }, + } + err = imagemetadata.MergeAndWriteMetadata("raring", newImageMetadata, cloudSpec, targetStorage) + c.Assert(err, jc.ErrorIsNil) + + foundIndex, _ := testing.ParseIndexMetadataFromStorage(c, targetStorage) + expectedCloudSpecs := []simplestreams.CloudSpec{*cloudSpec} + c.Assert(foundIndex.Clouds, jc.SameContents, expectedCloudSpecs) +} + +func (s *generateSuite) TestWriteDistinctIndexRegions(c *gc.C) { + existingImageMetadata := []*imagemetadata.ImageMetadata{ + { + Id: "1234", + Arch: "amd64", + Version: "13.04", + VirtType: "kvm", + }, + } + cloudSpec := &simplestreams.CloudSpec{ + Region: "region", + Endpoint: "endpoint", + } + dir := c.MkDir() + targetStorage, err := filestorage.NewFileStorageWriter(dir) + c.Assert(err, jc.ErrorIsNil) + err = imagemetadata.MergeAndWriteMetadata("raring", existingImageMetadata, cloudSpec, targetStorage) + c.Assert(err, jc.ErrorIsNil) + + expectedCloudSpecs := []simplestreams.CloudSpec{*cloudSpec} + + newImageMetadata := []*imagemetadata.ImageMetadata{ + { + Id: "abcd", + Arch: "amd64", + Version: "13.04", + VirtType: "lxd", + }, + } + cloudSpec = &simplestreams.CloudSpec{ + Region: "region2", + Endpoint: "endpoint2", + } + err = imagemetadata.MergeAndWriteMetadata("raring", newImageMetadata, cloudSpec, targetStorage) + c.Assert(err, jc.ErrorIsNil) + + foundIndex, _ := testing.ParseIndexMetadataFromStorage(c, targetStorage) + expectedCloudSpecs = append(expectedCloudSpecs, *cloudSpec) + c.Assert(foundIndex.Clouds, jc.SameContents, expectedCloudSpecs) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/imagemetadata/testing/testing.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/imagemetadata/testing/testing.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/imagemetadata/testing/testing.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/imagemetadata/testing/testing.go 2016-08-16 08:56:25.000000000 +0000 @@ -35,8 +35,8 @@ return ParseMetadataFromStorage(c, stor) } -// ParseMetadataFromStorage loads ImageMetadata from the specified storage reader. -func ParseMetadataFromStorage(c *gc.C, stor storage.StorageReader) []*imagemetadata.ImageMetadata { +// ParseIndexMetadataFromStorage loads Indices from the specified storage reader. +func ParseIndexMetadataFromStorage(c *gc.C, stor storage.StorageReader) (*simplestreams.IndexMetadata, simplestreams.DataSource) { source := storage.NewStorageSimpleStreamsDataSource("test storage reader", stor, "images", simplestreams.DEFAULT_CLOUD_DATA, false) // Find the simplestreams index file. @@ -54,6 +54,13 @@ imageIndexMetadata := indexRef.Indexes["com.ubuntu.cloud:custom"] c.Assert(imageIndexMetadata, gc.NotNil) + return imageIndexMetadata, source +} + +// ParseMetadataFromStorage loads ImageMetadata from the specified storage reader. +func ParseMetadataFromStorage(c *gc.C, stor storage.StorageReader) []*imagemetadata.ImageMetadata { + imageIndexMetadata, source := ParseIndexMetadataFromStorage(c, stor) + c.Assert(imageIndexMetadata, gc.NotNil) // Read the products file contents. r, err := stor.Get(path.Join("images", imageIndexMetadata.ProductsFilePath)) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/imagemetadata_test.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/imagemetadata_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/imagemetadata_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/imagemetadata_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -51,8 +51,8 @@ bootstrap.PrepareParams{ ControllerConfig: testing.FakeControllerConfig(), ControllerName: attrs["name"].(string), - BaseConfig: attrs, - CloudName: "dummy", + ModelConfig: attrs, + Cloud: dummy.SampleCloudSpec(), AdminSecret: "admin-secret", }, ) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/interface.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/interface.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/interface.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/interface.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,51 +11,46 @@ "github.com/juju/juju/environs/config" "github.com/juju/juju/instance" "github.com/juju/juju/network" - "github.com/juju/juju/state" + "github.com/juju/juju/storage" ) // A EnvironProvider represents a computing and storage provider. type EnvironProvider interface { + config.Validator + ProviderCredentials + // RestrictedConfigAttributes are provider specific attributes stored in // the config that really cannot or should not be changed across // environments running inside a single juju server. RestrictedConfigAttributes() []string - // PrepareForCreateEnvironment prepares an environment for creation. Any - // additional configuration attributes are added to the config passed in - // and returned. This allows providers to add additional required config - // for new environments that may be created in an existing juju server. - // Note that this is not called in a client context, so environment variables, - // local files, etc are not available. - PrepareForCreateEnvironment(controllerUUID string, cfg *config.Config) (*config.Config, error) - - // PrepareForBootstrap prepares an environment for use. - PrepareForBootstrap(ctx BootstrapContext, cfg *config.Config) (Environ, error) - - // BootstrapConfig produces the configuration for the initial controller - // model, based on the provided arguments. BootstrapConfig is expected - // to produce a deterministic output. Any unique values should be based - // on the "uuid" attribute of the base configuration. - BootstrapConfig(BootstrapConfigParams) (*config.Config, error) - - // Open opens the environment and returns it. - // The configuration must have come from a previously - // prepared environment. - Open(cfg *config.Config) (Environ, error) - - // Validate ensures that config is a valid configuration for this - // provider, applying changes to it if necessary, and returns the - // validated configuration. - // If old is not nil, it holds the previous environment configuration - // for consideration when validating changes. - Validate(cfg, old *config.Config) (valid *config.Config, err error) + // PrepareConfig prepares the configuration for a new model, based on + // the provided arguments. PrepareConfig is expected to produce a + // deterministic output. Any unique values should be based on the + // "uuid" attribute of the base configuration. This is called for the + // controller model during bootstrap, and also for new hosted models. + PrepareConfig(PrepareConfigParams) (*config.Config, error) + + // Open opens the environment and returns it. The configuration must + // have passed through PrepareConfig at some point in its lifecycle. + // + // Open should not perform any expensive operations, such as querying + // the cloud API, as it will be called frequently. + Open(OpenParams) (Environ, error) // SecretAttrs filters the supplied configuration returning only values // which are considered sensitive. All of the values of these secret // attributes need to be strings. SecretAttrs(cfg *config.Config) (map[string]string, error) +} - ProviderCredentials +// OpenParams contains the parameters for EnvironProvider.Open. +type OpenParams struct { + // Cloud is the cloud specification to use to connect to the cloud. + Cloud CloudSpec + + // Config is the base configuration for the provider. + Config *config.Config } // ProviderSchema can be implemented by a provider to provide @@ -69,33 +64,17 @@ Schema() environschema.Fields } -// BootstrapConfigParams contains the parameters for EnvironProvider.BootstrapConfig. -type BootstrapConfigParams struct { +// PrepareConfigParams contains the parameters for EnvironProvider.PrepareConfig. +type PrepareConfigParams struct { // ControllerUUID is the UUID of the controller to be bootstrapped. ControllerUUID string + // Cloud is the cloud specification to use to connect to the cloud. + Cloud CloudSpec + // Config is the base configuration for the provider. This should // be updated with the region, endpoint and credentials. Config *config.Config - - // Credentials is the set of credentials to use to bootstrap. - // - // TODO(axw) rename field to Credential. - Credentials cloud.Credential - - // CloudRegion is the name of the region of the cloud to create - // the Juju controller in. This will be empty for clouds without - // regions. - CloudRegion string - - // CloudEndpoint is the location of the primary API endpoint to - // use when communicating with the cloud. - CloudEndpoint string - - // CloudStorageEndpoint is the location of the API endpoint to use - // when communicating with the cloud's storage service. This will - // be empty for clouds that have no cloud-specific API endpoint. - CloudStorageEndpoint string } // ProviderCredentials is an interface that an EnvironProvider implements @@ -173,11 +152,26 @@ // implementation. The typical provider implementation needs locking to // avoid undefined behaviour when the configuration changes. type Environ interface { - // Bootstrap creates a new instance with the series and architecture - // of its choice, constrained to those of the available tools, and - // returns the instance's architecture, series, and a function that - // must be called to finalize the bootstrap process by transferring - // the tools and installing the initial Juju controller. + // Environ implements storage.ProviderRegistry for acquiring + // environ-scoped storage providers supported by the Environ. + // StorageProviders returned from Environ.StorageProvider will + // be scoped specifically to that Environ. + storage.ProviderRegistry + + // PrepareForBootstrap prepares an environment for bootstrapping. + // + // This will be called very early in the bootstrap procedure, to + // give an Environ a chance to perform interactive operations that + // are required for bootstrapping. + PrepareForBootstrap(ctx BootstrapContext) error + + // Bootstrap creates a new environment, and an instance to host the + // controller for that environment. The instnace will have have the + // series and architecture of the Environ's choice, constrained to + // those of the available tools. Bootstrap will return the instance's + // architecture, series, and a function that must be called to finalize + // the bootstrap process by transferring the tools and installing the + // initial Juju controller. // // It is possible to direct Bootstrap to use a specific architecture // (or fail if it cannot start an instance of that architecture) by @@ -186,6 +180,16 @@ // architecture. Bootstrap(ctx BootstrapContext, params BootstrapParams) (*BootstrapResult, error) + // Create creates the environment for a new hosted model. + // + // This will be called before any workers begin operating on the + // Environ, to give an Environ a chance to perform operations that + // are required for further use. + // + // Create is not called for the initial controller model; it is + // the Bootstrap method's job to create the controller model. + Create(CreateParams) error + // InstanceBroker defines methods for starting and stopping // instances. InstanceBroker @@ -193,9 +197,6 @@ // ConfigGetter allows the retrieval of the configuration data. ConfigGetter - // EnvironCapability allows access to this environment's capabilities. - state.EnvironCapability - // ConstraintsValidator returns a Validator instance which // is used to validate and merge constraints. ConstraintsValidator() (constraints.Validator, error) @@ -244,7 +245,27 @@ // Provider returns the EnvironProvider that created this Environ. Provider() EnvironProvider - state.Prechecker + // PrecheckInstance performs a preflight check on the specified + // series and constraints, ensuring that they are possibly valid for + // creating an instance in this model. + // + // PrecheckInstance is best effort, and not guaranteed to eliminate + // all invalid parameters. If PrecheckInstance returns nil, it is not + // guaranteed that the constraints are valid; if a non-nil error is + // returned, then the constraints are definitely invalid. + // + // TODO(axw) find a home for state.Prechecker that isn't state and + // isn't environs, so both packages can refer to it. Maybe the + // constraints package? Can't be instance, because constraints + // import instance... + PrecheckInstance(series string, cons constraints.Value, placement string) error +} + +// CreateParams contains the parameters for Environ.Create. +type CreateParams struct { + // ControllerUUID is the UUID of the controller to be that is creating + // the Environ. + ControllerUUID string } // Firewaller exposes methods for managing network ports. diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/jujutest/livetests.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/jujutest/livetests.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/jujutest/livetests.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/jujutest/livetests.go 2016-08-16 08:56:25.000000000 +0000 @@ -76,6 +76,8 @@ // Attempt holds a strategy for waiting until the environment // becomes logically consistent. + // + // TODO(katco): 2016-08-09: lp:1611427 Attempt utils.AttemptStrategy // CanOpenState should be true if the testing environment allows @@ -157,13 +159,16 @@ } return bootstrap.PrepareParams{ ControllerConfig: coretesting.FakeControllerConfig(), - BaseConfig: t.TestConfig, - Credential: credential, - CloudEndpoint: t.CloudEndpoint, - CloudRegion: t.CloudRegion, - ControllerName: t.TestConfig["name"].(string), - CloudName: t.TestConfig["type"].(string), - AdminSecret: AdminSecret, + ModelConfig: t.TestConfig, + Cloud: environs.CloudSpec{ + Type: t.TestConfig["type"].(string), + Name: t.TestConfig["type"].(string), + Region: t.CloudRegion, + Endpoint: t.CloudEndpoint, + Credential: &credential, + }, + ControllerName: t.TestConfig["name"].(string), + AdminSecret: AdminSecret, } } @@ -731,6 +736,7 @@ } } +// TODO(katco): 2016-08-09: lp:1611427 var waitAgent = utils.AttemptStrategy{ Total: 30 * time.Second, Delay: 1 * time.Second, @@ -805,7 +811,7 @@ "name": "dummy storage", }) args := t.prepareForBootstrapParams(c) - args.BaseConfig = dummyCfg + args.ModelConfig = dummyCfg dummyenv, err := bootstrap.Prepare(envtesting.BootstrapContext(c), jujuclienttesting.NewMemStore(), args, @@ -819,7 +825,7 @@ "name": "livetests", "default-series": other.Series, }) - args.BaseConfig = attrs + args.ModelConfig = attrs env, err := bootstrap.Prepare(envtesting.BootstrapContext(c), t.ControllerStore, args) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/jujutest/tests.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/jujutest/tests.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/jujutest/tests.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/jujutest/tests.go 2016-08-16 08:56:25.000000000 +0000 @@ -49,29 +49,39 @@ // Open opens an instance of the testing environment. func (t *Tests) Open(c *gc.C, cfg *config.Config) environs.Environ { - e, err := environs.New(cfg) + e, err := environs.New(environs.OpenParams{ + Cloud: t.CloudSpec(), + Config: cfg, + }) c.Assert(err, gc.IsNil, gc.Commentf("opening environ %#v", cfg.AllAttrs())) c.Assert(e, gc.NotNil) return e } +func (t *Tests) CloudSpec() environs.CloudSpec { + credential := t.Credential + if credential.AuthType() == "" { + credential = cloud.NewEmptyCredential() + } + return environs.CloudSpec{ + Type: t.TestConfig["type"].(string), + Name: t.TestConfig["type"].(string), + Region: t.CloudRegion, + Endpoint: t.CloudEndpoint, + Credential: &credential, + } +} + // PrepareParams returns the environs.PrepareParams that will be used to call // environs.Prepare. func (t *Tests) PrepareParams(c *gc.C) bootstrap.PrepareParams { testConfigCopy := t.TestConfig.Merge(nil) - credential := t.Credential - if credential.AuthType() == "" { - credential = cloud.NewEmptyCredential() - } return bootstrap.PrepareParams{ ControllerConfig: coretesting.FakeControllerConfig(), - BaseConfig: testConfigCopy, - Credential: credential, + ModelConfig: testConfigCopy, + Cloud: t.CloudSpec(), ControllerName: t.TestConfig["name"].(string), - CloudName: t.TestConfig["type"].(string), - CloudEndpoint: t.CloudEndpoint, - CloudRegion: t.CloudRegion, AdminSecret: AdminSecret, } } @@ -84,14 +94,14 @@ // PrepareWithParams prepares an instance of the testing environment. func (t *Tests) PrepareWithParams(c *gc.C, params bootstrap.PrepareParams) environs.Environ { e, err := bootstrap.Prepare(envtesting.BootstrapContext(c), t.ControllerStore, params) - c.Assert(err, gc.IsNil, gc.Commentf("preparing environ %#v", params.BaseConfig)) + c.Assert(err, gc.IsNil, gc.Commentf("preparing environ %#v", params.ModelConfig)) c.Assert(e, gc.NotNil) return e } func (t *Tests) AssertPrepareFailsWithConfig(c *gc.C, badConfig coretesting.Attrs, errorMatches string) error { args := t.PrepareParams(c) - args.BaseConfig = coretesting.Attrs(args.BaseConfig).Merge(badConfig) + args.ModelConfig = coretesting.Attrs(args.ModelConfig).Merge(badConfig) e, err := bootstrap.Prepare(envtesting.BootstrapContext(c), t.ControllerStore, args) c.Assert(err, gc.ErrorMatches, errorMatches) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/networking.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/networking.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/networking.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/networking.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,6 +6,7 @@ import ( "gopkg.in/juju/names.v2" + "github.com/juju/errors" "github.com/juju/juju/instance" "github.com/juju/juju/network" ) @@ -45,6 +46,10 @@ // container NICs in preparedInfo, hosted by the hostInstanceID. Returns the // network config including all allocated addresses on success. AllocateContainerAddresses(hostInstanceID instance.Id, containerTag names.MachineTag, preparedInfo []network.InterfaceInfo) ([]network.InterfaceInfo, error) + + // ReleaseContainerAddresses releases the previously allocated + // addresses matching the interface infos passed in. + ReleaseContainerAddresses(interfaces []network.InterfaceInfo) error } // NetworkingEnviron combines the standard Environ interface with the @@ -61,3 +66,20 @@ ne, ok := environ.(NetworkingEnviron) return ne, ok } + +// SupportsSpaces checks if the environment implements NetworkingEnviron +// and also if it supports spaces. +func SupportsSpaces(env Environ) bool { + netEnv, ok := supportsNetworking(env) + if !ok { + return false + } + ok, err := netEnv.SupportsSpaces() + if err != nil { + if !errors.IsNotSupported(err) { + logger.Errorf("checking model spaces support failed with: %v", err) + } + return false + } + return ok +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/open.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/open.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/open.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/open.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,7 +6,6 @@ import ( "github.com/juju/errors" - "github.com/juju/juju/environs/config" "github.com/juju/juju/jujuclient" ) @@ -14,12 +13,12 @@ const AdminUser = "admin@local" // New returns a new environment based on the provided configuration. -func New(config *config.Config) (Environ, error) { - p, err := Provider(config.Type()) +func New(args OpenParams) (Environ, error) { + p, err := Provider(args.Cloud.Type) if err != nil { return nil, errors.Trace(err) } - return p.Open(config) + return p.Open(args) } // Destroy destroys the controller and, if successful, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/open_test.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/open_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/open_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/open_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -54,8 +54,8 @@ env, err := bootstrap.Prepare(ctx, cache, bootstrap.PrepareParams{ ControllerConfig: controllerCfg, ControllerName: cfg.Name(), - BaseConfig: cfg.AllAttrs(), - CloudName: "dummy", + ModelConfig: cfg.AllAttrs(), + Cloud: dummy.SampleCloudSpec(), AdminSecret: "admin-secret", }) c.Assert(err, jc.ErrorIsNil) @@ -93,8 +93,8 @@ _, err = bootstrap.Prepare(ctx, store, bootstrap.PrepareParams{ ControllerConfig: controllerCfg, ControllerName: "controller-name", - BaseConfig: cfg.AllAttrs(), - CloudName: "dummy", + ModelConfig: cfg.AllAttrs(), + Cloud: dummy.SampleCloudSpec(), AdminSecret: "admin-secret", }) c.Assert(err, jc.ErrorIsNil) @@ -103,7 +103,7 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(foundController.ControllerUUID, gc.Not(gc.Equals), "") c.Assert(foundController.CACert, gc.Not(gc.Equals), "") - foundModel, err := store.ModelByName("controller-name", "admin-model") + foundModel, err := store.ModelByName("controller-name", "admin@local/admin-model") c.Assert(err, jc.ErrorIsNil) c.Assert(foundModel, jc.DeepEquals, &jujuclient.ModelDetails{ ModelUUID: cfg.UUID(), @@ -111,13 +111,11 @@ } func (*OpenSuite) TestNewUnknownEnviron(c *gc.C) { - cfg, err := config.New(config.NoDefaults, dummy.SampleConfig().Merge( - testing.Attrs{ - "type": "wondercloud", + env, err := environs.New(environs.OpenParams{ + Cloud: environs.CloudSpec{ + Type: "wondercloud", }, - )) - c.Assert(err, jc.ErrorIsNil) - env, err := environs.New(cfg) + }) c.Assert(err, gc.ErrorMatches, "no registered provider for.*") c.Assert(env, gc.IsNil) } @@ -130,7 +128,10 @@ }, )) c.Assert(err, jc.ErrorIsNil) - e, err := environs.New(cfg) + e, err := environs.New(environs.OpenParams{ + Cloud: dummy.SampleCloudSpec(), + Config: cfg, + }) c.Assert(err, jc.ErrorIsNil) _, err = e.ControllerInstances("uuid") c.Assert(err, gc.ErrorMatches, "model is not prepared") @@ -152,8 +153,8 @@ e, err := bootstrap.Prepare(ctx, store, bootstrap.PrepareParams{ ControllerConfig: controllerCfg, ControllerName: "controller-name", - BaseConfig: cfg.AllAttrs(), - CloudName: "dummy", + ModelConfig: cfg.AllAttrs(), + Cloud: dummy.SampleCloudSpec(), AdminSecret: "admin-secret", }) c.Assert(err, jc.ErrorIsNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/statepolicy.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/statepolicy.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/statepolicy.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/statepolicy.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,87 +0,0 @@ -// Copyright 2014 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package environs - -import ( - "github.com/juju/errors" - - "github.com/juju/juju/constraints" - "github.com/juju/juju/environs/config" - "github.com/juju/juju/environs/simplestreams" - "github.com/juju/juju/state" -) - -// environStatePolicy implements state.Policy in -// terms of environs.Environ and related types. -type environStatePolicy struct{} - -var _ state.Policy = environStatePolicy{} - -// NewStatePolicy returns a state.Policy that is -// implemented in terms of Environ and related -// types. -func NewStatePolicy() state.Policy { - return environStatePolicy{} -} - -func (environStatePolicy) Prechecker(cfg *config.Config) (state.Prechecker, error) { - // Environ implements state.Prechecker. - return New(cfg) -} - -func (environStatePolicy) ConfigValidator(providerType string) (state.ConfigValidator, error) { - // EnvironProvider implements state.ConfigValidator. - return Provider(providerType) -} - -func (environStatePolicy) EnvironCapability(cfg *config.Config) (state.EnvironCapability, error) { - // Environ implements state.EnvironCapability. - return New(cfg) -} - -func (environStatePolicy) ConstraintsValidator(cfg *config.Config, querier state.SupportedArchitecturesQuerier) (constraints.Validator, error) { - env, err := New(cfg) - if err != nil { - return nil, err - } - - // Ensure that supported architectures are filtered based on cloud specification. - // TODO (anastasiamac 2015-12-22) this cries for a test \o/ - region := "" - if cloudEnv, ok := env.(simplestreams.HasRegion); ok { - cloudCfg, err := cloudEnv.Region() - if err != nil { - return nil, err - } - region = cloudCfg.Region - } - arches, err := querier.SupportedArchitectures(env.Config().AgentStream(), region) - if err != nil { - return nil, err - } - - // Construct provider specific validator. - val, err := env.ConstraintsValidator() - if err != nil { - return nil, err - } - - // Update validator architectures with supported architectures from stored - // cloud image metadata. - if len(arches) != 0 { - val.UpdateVocabulary(constraints.Arch, arches) - } - return val, nil -} - -func (environStatePolicy) InstanceDistributor(cfg *config.Config) (state.InstanceDistributor, error) { - env, err := New(cfg) - if err != nil { - return nil, err - } - if p, ok := env.(state.InstanceDistributor); ok { - return p, nil - } - return nil, errors.NotImplementedf("InstanceDistributor") -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/storage/interfaces.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/storage/interfaces.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/storage/interfaces.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/storage/interfaces.go 2016-08-16 08:56:25.000000000 +0000 @@ -32,6 +32,8 @@ // If the storage implementation has immediate consistency, the // strategy won't need to wait at all. But for eventually-consistent // storage backends a few seconds of polling may be needed. + // + // TODO(katco): 2016-08-09: lp:1611427 DefaultConsistencyStrategy() utils.AttemptStrategy // ShouldRetry returns true is the specified error is such that an diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/storage/storage.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/storage/storage.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/storage/storage.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/storage/storage.go 2016-08-16 08:56:25.000000000 +0000 @@ -39,6 +39,8 @@ } // GetWithRetry gets the named file from stor using the specified attempt strategy. +// +// TODO(katco): 2016-08-09: lp:1611427 func GetWithRetry(stor StorageReader, name string, attempt utils.AttemptStrategy) (r io.ReadCloser, err error) { for a := attempt.Start(); a.Next(); { r, err = stor.Get(name) @@ -55,6 +57,8 @@ } // ListWithRetry lists the files matching prefix from stor using the specified attempt strategy. +// +// TODO(katco): 2016-08-09: lp:1611427 func ListWithRetry(stor StorageReader, prefix string, attempt utils.AttemptStrategy) (list []string, err error) { for a := attempt.Start(); a.Next(); { list, err = stor.List(prefix) @@ -116,6 +120,7 @@ if err == nil { dataURL = fullURL } + // TODO(katco): 2016-08-09: lp:1611427 var attempt utils.AttemptStrategy if s.allowRetry { attempt = s.storage.DefaultConsistencyStrategy() diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/storage/storage_test.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/storage/storage_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/storage/storage_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/storage/storage_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -136,6 +136,7 @@ } func (s *fakeStorage) DefaultConsistencyStrategy() utils.AttemptStrategy { + // TODO(katco): 2016-08-09: lp:1611427 return utils.AttemptStrategy{Min: 10} } @@ -145,6 +146,7 @@ func (s *storageSuite) TestGetWithRetry(c *gc.C) { stor := &fakeStorage{shouldRetry: true} + // TODO(katco): 2016-08-09: lp:1611427 attempt := utils.AttemptStrategy{Min: 5} storage.GetWithRetry(stor, "foo", attempt) c.Assert(stor.getName, gc.Equals, "foo") @@ -167,6 +169,7 @@ func (s *storageSuite) TestListWithRetry(c *gc.C) { stor := &fakeStorage{shouldRetry: true} + // TODO(katco): 2016-08-09: lp:1611427 attempt := utils.AttemptStrategy{Min: 5} storage.ListWithRetry(stor, "foo", attempt) c.Assert(stor.listPrefix, gc.Equals, "foo") diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/testing/credentials.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/testing/credentials.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/testing/credentials.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/testing/credentials.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,7 +6,6 @@ import ( "fmt" - "github.com/juju/errors" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" @@ -32,8 +31,8 @@ schema, ok := p.CredentialSchemas()[authType] c.Assert(ok, jc.IsTrue, gc.Commentf("missing schema for %q auth-type", authType)) validate := func(attrs map[string]string) error { - _, err := schema.Finalize(attrs, func(string) ([]byte, error) { - return nil, errors.NotSupportedf("reading files") + _, err := schema.Finalize(attrs, func(path string) ([]byte, error) { + return []byte("contentsOf(" + path + ")"), nil }) return err } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/testing/polling.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/testing/polling.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/testing/polling.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/testing/polling.go 2016-08-16 08:56:25.000000000 +0000 @@ -12,10 +12,14 @@ // impatientAttempt is an extremely short polling time suitable for tests. // It polls at least once, never delays, and times out very quickly. +// +// TODO(katco): 2016-08-09: lp:1611427 var impatientAttempt = utils.AttemptStrategy{} // savedAttemptStrategy holds the state needed to restore an AttemptStrategy's // original setting. +// +// TODO(katco): 2016-08-09: lp:1611427 type savedAttemptStrategy struct { address *utils.AttemptStrategy original utils.AttemptStrategy @@ -23,6 +27,8 @@ // saveAttemptStrategies captures the information required to restore the // given AttemptStrategy objects. +// +// TODO(katco): 2016-08-09: lp:1611427 func saveAttemptStrategies(strategies []*utils.AttemptStrategy) []savedAttemptStrategy { savedStrategies := make([]savedAttemptStrategy, len(strategies)) for index, strategy := range strategies { @@ -49,6 +55,8 @@ // internalPatchAttemptStrategies sets the given AttemptStrategy objects // to the impatientAttempt configuration, and returns a function that restores // the original configurations. +// +// TODO(katco): 2016-08-09: lp:1611427 func internalPatchAttemptStrategies(strategies []*utils.AttemptStrategy) func() { snapshot := saveAttemptStrategies(strategies) for _, strategy := range strategies { @@ -64,6 +72,8 @@ // polling and timeout times so that tests can run fast. // It returns a cleanup function that restores the original settings. You must // call this afterwards. +// +// TODO(katco): 2016-08-09: lp:1611427 func PatchAttemptStrategies(strategies ...*utils.AttemptStrategy) func() { // The one irregularity here is that LongAttempt goes on the list of // strategies that need patching. To keep testing simple, we treat diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/testing/polling_test.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/testing/polling_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/testing/polling_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/testing/polling_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -23,6 +23,7 @@ var _ = gc.Suite(&testingSuite{}) func (*testingSuite) TestSaveAttemptStrategiesSaves(c *gc.C) { + // TODO(katco): 2016-08-09: lp:1611427 attempt := utils.AttemptStrategy{ Total: time.Second, Delay: time.Millisecond, @@ -36,6 +37,7 @@ } func (*testingSuite) TestSaveAttemptStrategiesLeavesOriginalsIntact(c *gc.C) { + // TODO(katco): 2016-08-09: lp:1611427 original := utils.AttemptStrategy{ Total: time.Second, Delay: time.Millisecond, @@ -48,6 +50,7 @@ } func (*testingSuite) TestInternalPatchAttemptStrategiesPatches(c *gc.C) { + // TODO(katco): 2016-08-09: lp:1611427 attempt := utils.AttemptStrategy{ Total: 33 * time.Millisecond, Delay: 99 * time.Microsecond, @@ -64,6 +67,7 @@ // these tests take this as sufficient proof that any strategy that gets // patched, also gets restored by the cleanup function. func (*testingSuite) TestInternalPatchAttemptStrategiesReturnsCleanup(c *gc.C) { + // TODO(katco): 2016-08-09: lp:1611427 original := utils.AttemptStrategy{ Total: 22 * time.Millisecond, Delay: 77 * time.Microsecond, @@ -91,6 +95,7 @@ } func (*testingSuite) TestPatchAttemptStrategiesPatchesGivenAttempts(c *gc.C) { + // TODO(katco): 2016-08-09: lp:1611427 attempt1 := utils.AttemptStrategy{ Total: 33 * time.Millisecond, Delay: 99 * time.Microsecond, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/tools/tools_test.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/tools/tools_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/tools/tools_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/tools/tools_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -103,8 +103,8 @@ bootstrap.PrepareParams{ ControllerConfig: coretesting.FakeControllerConfig(), ControllerName: attrs["name"].(string), - BaseConfig: attrs, - CloudName: "dummy", + ModelConfig: attrs, + Cloud: dummy.SampleCloudSpec(), AdminSecret: "admin-secret", }, ) @@ -195,7 +195,7 @@ func (s *SimpleStreamsToolsSuite) TestFindToolsFiltering(c *gc.C) { var tw loggo.TestWriter - c.Assert(loggo.RegisterWriter("filter-tester", &tw, loggo.TRACE), gc.IsNil) + c.Assert(loggo.RegisterWriter("filter-tester", &tw), gc.IsNil) defer loggo.RemoveWriter("filter-tester") logger := loggo.GetLogger("juju.environs") defer logger.SetLogLevel(logger.LogLevel()) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/environs/tools/urls_test.go juju-core-2.0~beta15/src/github.com/juju/juju/environs/tools/urls_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/environs/tools/urls_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/environs/tools/urls_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -49,8 +49,8 @@ bootstrap.PrepareParams{ ControllerConfig: coretesting.FakeControllerConfig(), ControllerName: attrs["name"].(string), - BaseConfig: attrs, - CloudName: "dummy", + ModelConfig: attrs, + Cloud: dummy.SampleCloudSpec(), AdminSecret: "admin-secret", }, ) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/feature/flags.go juju-core-2.0~beta15/src/github.com/juju/juju/feature/flags.go --- juju-core-2.0~beta12/src/github.com/juju/juju/feature/flags.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/feature/flags.go 2016-08-16 08:56:25.000000000 +0000 @@ -36,3 +36,6 @@ // Migration enables the 'juju migrate' command. const Migration = "migration" + +// DeveloperMode allows access to developer specific commands and behaviour. +const DeveloperMode = "developer-mode" diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/featuretests/api_cloud_test.go juju-core-2.0~beta15/src/github.com/juju/juju/featuretests/api_cloud_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/featuretests/api_cloud_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/featuretests/api_cloud_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,41 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package featuretests + +import ( + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/juju/names.v2" + + apicloud "github.com/juju/juju/api/cloud" + "github.com/juju/juju/cloud" + "github.com/juju/juju/juju/testing" +) + +// NOTE(axw) this suite only exists because nothing exercises +// the cloud API enough to expose serialisation bugs such as +// lp:1607557. If/when we have commands that would expose that +// bug, we should drop this suite and write a new command-based +// one. + +type CloudAPISuite struct { + testing.JujuConnSuite + client *apicloud.Client +} + +func (s *CloudAPISuite) SetUpTest(c *gc.C) { + s.JujuConnSuite.SetUpTest(c) + s.client = apicloud.NewClient(s.APIState) +} + +func (s *CloudAPISuite) TestCloudAPI(c *gc.C) { + result, err := s.client.Cloud(names.NewCloudTag("dummy")) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, cloud.Cloud{ + Type: "dummy", + AuthTypes: []cloud.AuthType{cloud.EmptyAuthType}, + Endpoint: "dummy-endpoint", + StorageEndpoint: "dummy-storage-endpoint", + }) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/featuretests/api_model_test.go juju-core-2.0~beta15/src/github.com/juju/juju/featuretests/api_model_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/featuretests/api_model_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/featuretests/api_model_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -16,6 +16,7 @@ "github.com/juju/juju/api" "github.com/juju/juju/api/modelmanager" "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/core/description" jujunames "github.com/juju/juju/juju/names" "github.com/juju/juju/juju/testing" "github.com/juju/juju/state" @@ -46,10 +47,10 @@ c.Assert(err, jc.ErrorIsNil) user := names.NewUserTag(username) - modelUser, err := s.State.ModelUser(user) + modelUser, err := s.State.UserAccess(user, model.ModelTag()) c.Assert(err, jc.ErrorIsNil) - c.Assert(modelUser.UserName(), gc.Equals, user.Canonical()) - lastConn, err := modelUser.LastConnection() + c.Assert(modelUser.UserName, gc.Equals, user.Canonical()) + lastConn, err := s.State.LastModelConnection(modelUser.UserTag) c.Assert(err, jc.Satisfies, state.IsNeverConnectedError) c.Assert(lastConn.IsZero(), jc.IsTrue) } @@ -61,45 +62,45 @@ c.Assert(err, jc.ErrorIsNil) mm := modelmanager.NewClient(s.APIState) - modelUser, err := s.State.ModelUser(user.UserTag()) + modelUser, err := s.State.UserAccess(user.UserTag, s.State.ModelTag()) c.Assert(err, jc.ErrorIsNil) - c.Assert(modelUser, gc.NotNil) + c.Assert(modelUser, gc.Not(gc.DeepEquals), description.UserAccess{}) // Then test unsharing the environment. - err = mm.RevokeModel(user.UserName(), "read", model.UUID()) + err = mm.RevokeModel(user.UserName, "read", model.UUID()) c.Assert(err, jc.ErrorIsNil) - modelUser, err = s.State.ModelUser(user.UserTag()) + modelUser, err = s.State.UserAccess(user.UserTag, s.State.ModelTag()) c.Assert(errors.IsNotFound(err), jc.IsTrue) - c.Assert(modelUser, gc.IsNil) + c.Assert(modelUser, gc.DeepEquals, description.UserAccess{}) } func (s *apiEnvironmentSuite) TestEnvironmentUserInfo(c *gc.C) { modelUser := s.Factory.MakeModelUser(c, &factory.ModelUserParams{User: "bobjohns@ubuntuone", DisplayName: "Bob Johns"}) - env, err := s.State.Model() + mod, err := s.State.Model() c.Assert(err, jc.ErrorIsNil) - owner, err := s.State.ModelUser(env.Owner()) + owner, err := s.State.UserAccess(mod.Owner(), mod.ModelTag()) c.Assert(err, jc.ErrorIsNil) obtained, err := s.client.ModelUserInfo() c.Assert(err, jc.ErrorIsNil) c.Assert(obtained, gc.DeepEquals, []params.ModelUserInfo{ { - UserName: owner.UserName(), - DisplayName: owner.DisplayName(), + UserName: owner.UserName, + DisplayName: owner.DisplayName, Access: "admin", - LastConnection: lastConnPointer(c, owner), + LastConnection: lastConnPointer(c, s.State, owner), }, { UserName: "bobjohns@ubuntuone", DisplayName: "Bob Johns", Access: "admin", - LastConnection: lastConnPointer(c, modelUser), + LastConnection: lastConnPointer(c, s.State, modelUser), }, }) } -func lastConnPointer(c *gc.C, modelUser *state.ModelUser) *time.Time { - lastConn, err := modelUser.LastConnection() +func lastConnPointer(c *gc.C, st *state.State, modelUser description.UserAccess) *time.Time { + lastConn, err := st.LastModelConnection(modelUser.UserTag) if err != nil { if state.IsNeverConnectedError(err) { return nil diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/featuretests/cmd_juju_controller_test.go juju-core-2.0~beta15/src/github.com/juju/juju/featuretests/cmd_juju_controller_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/featuretests/cmd_juju_controller_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/featuretests/cmd_juju_controller_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,6 +5,7 @@ import ( "fmt" + "os" "reflect" "strings" "time" @@ -87,9 +88,9 @@ s.createModelNormalUser(c, "new-model", false) context := s.run(c, "list-models", "--all") c.Assert(testing.Stdout(context), gc.Equals, ""+ - "MODEL OWNER STATUS LAST CONNECTION\n"+ - "controller* admin@local available just now\n"+ - "new-model test@local available never connected\n"+ + "MODEL OWNER STATUS LAST CONNECTION\n"+ + "admin/controller* admin@local available just now\n"+ + "test/new-model test@local available never connected\n"+ "\n") } @@ -148,20 +149,21 @@ // The JujuConnSuite doesn't set up an ssh key in the fake home dir, // so fake one on the command line. The dummy provider also expects // a config value for 'controller'. - context := s.run(c, "add-model", "new-model", "authorized-keys=fake-key", "controller=false") + context := s.run( + c, "add-model", "new-model", + "--config", "authorized-keys=fake-key", + "--config", "controller=false", + ) c.Check(testing.Stdout(context), gc.Equals, "") c.Check(testing.Stderr(context), gc.Equals, ` -Added 'new-model' model for user 'admin' - -No SSH authorized-keys were found. You must use "juju add-ssh-key" -before "juju ssh", "juju scp", or "juju debug-hooks" will work. +Added 'new-model' model with credential 'cred' for user 'admin' `[1:]) // Make sure that the saved server details are sufficient to connect // to the api server. accountDetails, err := s.ControllerStore.AccountDetails("kontroll") c.Assert(err, jc.ErrorIsNil) - modelDetails, err := s.ControllerStore.ModelByName("kontroll", "new-model") + modelDetails, err := s.ControllerStore.ModelByName("kontroll", "admin@local/new-model") c.Assert(err, jc.ErrorIsNil) api, err := juju.NewAPIConnection(juju.NewAPIConnectionParams{ Store: s.ControllerStore, @@ -176,6 +178,14 @@ } func (s *cmdControllerSuite) TestControllerDestroy(c *gc.C) { + s.testControllerDestroy(c, false) +} + +func (s *cmdControllerSuite) TestControllerDestroyUsingAPI(c *gc.C) { + s.testControllerDestroy(c, true) +} + +func (s *cmdControllerSuite) testControllerDestroy(c *gc.C, forceAPI bool) { st := s.Factory.MakeModel(c, &factory.ModelParams{ Name: "just-a-controller", ConfigAttrs: testing.Attrs{"controller": true}, @@ -214,10 +224,24 @@ } }() + if forceAPI { + // Remove bootstrap config from the client store, + // forcing the command to use the API. + err := os.Remove(jujuclient.JujuBootstrapConfigPath()) + c.Assert(err, jc.ErrorIsNil) + } + + ops := make(chan dummy.Operation, 1) + dummy.Listen(ops) + s.run(c, "destroy-controller", "kontroll", "-y", "--destroy-all-models", "--debug") close(stop) <-done + destroyOp := (<-ops).(dummy.OpDestroy) + c.Assert(destroyOp.Env, gc.Equals, "controller") + c.Assert(destroyOp.Cloud, jc.DeepEquals, dummy.SampleCloudSpec()) + store := jujuclient.NewFileClientStore() _, err := store.ControllerByName("kontroll") c.Assert(err, jc.Satisfies, errors.IsNotFound) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/featuretests/cmd_juju_login_test.go juju-core-2.0~beta15/src/github.com/juju/juju/featuretests/cmd_juju_login_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/featuretests/cmd_juju_login_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/featuretests/cmd_juju_login_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,6 +5,7 @@ import ( "io" + "os" "strings" "github.com/juju/cmd" @@ -13,6 +14,7 @@ gc "gopkg.in/check.v1" "github.com/juju/juju/cmd/juju/commands" + "github.com/juju/juju/juju/osenv" jujutesting "github.com/juju/juju/juju/testing" "github.com/juju/juju/jujuclient" "github.com/juju/juju/testing" @@ -22,6 +24,11 @@ jujutesting.JujuConnSuite } +func (s *cmdLoginSuite) SetUpTest(c *gc.C) { + s.JujuConnSuite.SetUpTest(c) + os.Setenv(osenv.JujuModelEnvKey, "") +} + func (s *cmdLoginSuite) run(c *gc.C, stdin io.Reader, args ...string) *cmd.Context { context := testing.Context(c) if stdin != nil { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/featuretests/cmd_juju_model_test.go juju-core-2.0~beta15/src/github.com/juju/juju/featuretests/cmd_juju_model_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/featuretests/cmd_juju_model_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/featuretests/cmd_juju_model_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -14,6 +14,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/cmd/juju/commands" + "github.com/juju/juju/core/description" jujutesting "github.com/juju/juju/juju/testing" "github.com/juju/juju/state" "github.com/juju/juju/testing" @@ -46,11 +47,11 @@ c.Assert(obtained, gc.Equals, expected) user := names.NewUserTag(username) - modelUser, err := s.State.ModelUser(user) + modelUser, err := s.State.UserAccess(user, s.State.ModelTag()) c.Assert(err, jc.ErrorIsNil) - c.Assert(modelUser.UserName(), gc.Equals, user.Canonical()) - c.Assert(modelUser.CreatedBy(), gc.Equals, s.AdminUserTag(c).Canonical()) - lastConn, err := modelUser.LastConnection() + c.Assert(modelUser.UserName, gc.Equals, user.Canonical()) + c.Assert(modelUser.CreatedBy.Canonical(), gc.Equals, s.AdminUserTag(c).Canonical()) + lastConn, err := s.State.LastModelConnection(modelUser.UserTag) c.Assert(err, jc.Satisfies, state.IsNeverConnectedError) c.Assert(lastConn.IsZero(), jc.IsTrue) } @@ -59,7 +60,7 @@ // Firstly share a model with a user username := "bar@ubuntuone" s.Factory.MakeModelUser(c, &factory.ModelUserParams{ - User: username, Access: state.ReadAccess}) + User: username, Access: description.ReadAccess}) // Because we are calling into juju through the main command, // and the main command adds a warning logging writer, we need @@ -73,9 +74,9 @@ c.Assert(obtained, gc.Equals, expected) user := names.NewUserTag(username) - modelUser, err := s.State.ModelUser(user) + modelUser, err := s.State.UserAccess(user, s.State.ModelTag()) c.Assert(errors.IsNotFound(err), jc.IsTrue) - c.Assert(modelUser, gc.IsNil) + c.Assert(modelUser, gc.DeepEquals, description.UserAccess{}) } func (s *cmdModelSuite) TestModelUsersCmd(c *gc.C) { @@ -83,7 +84,7 @@ username := "bar@ubuntuone" context := s.run(c, "grant", username, "controller") user := names.NewUserTag(username) - modelUser, err := s.State.ModelUser(user) + modelUser, err := s.State.UserAccess(user, s.State.ModelTag()) c.Assert(err, jc.ErrorIsNil) c.Assert(modelUser, gc.NotNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/featuretests/cmd_juju_register_test.go juju-core-2.0~beta15/src/github.com/juju/juju/featuretests/cmd_juju_register_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/featuretests/cmd_juju_register_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/featuretests/cmd_juju_register_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -60,9 +60,9 @@ context = s.run(c, stdin, args...) c.Check(testing.Stdout(context), gc.Equals, "") c.Check(testing.Stderr(context), gc.Equals, ` -WARNING: the controller proposed "kontroll" which clashes with an existing controller. The two controllers are entirely different. +WARNING: The controller proposed "kontroll" which clashes with an existing controller. The two controllers are entirely different. -Please set a name for this controller: +Enter a name for this controller: Enter a new password: Confirm password: diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/featuretests/cmd_juju_user_test.go juju-core-2.0~beta15/src/github.com/juju/juju/featuretests/cmd_juju_user_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/featuretests/cmd_juju_user_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/featuretests/cmd_juju_user_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -69,7 +69,7 @@ c.Assert(err, jc.ErrorIsNil) var modelUserTags = make([]names.UserTag, len(users)) for i, u := range users { - modelUserTags[i] = u.UserTag() + modelUserTags[i] = u.UserTag } c.Assert(modelUserTags, jc.SameContents, []names.UserTag{ user.Tag().(names.UserTag), @@ -116,6 +116,34 @@ c.Assert(user.IsDisabled(), jc.IsFalse) } +func (s *UserSuite) TestRemoveUserPrompt(c *gc.C) { + expected := ` +WARNING! This command will remove the user "jjam" from the "kontroll" controller. + +Continue (y/N)? `[1:] + _ = s.Factory.MakeUser(c, &factory.UserParams{Name: "jjam"}) + ctx, _ := s.RunUserCommand(c, "", "remove-user", "jjam") + c.Assert(testing.Stdout(ctx), jc.DeepEquals, expected) +} + +func (s *UserSuite) TestRemoveUser(c *gc.C) { + user := s.Factory.MakeUser(c, &factory.UserParams{Name: "jjam"}) + _, err := s.RunUserCommand(c, "", "remove-user", "-y", "jjam") + c.Assert(err, jc.ErrorIsNil) + err = user.Refresh() + c.Assert(err, jc.ErrorIsNil) + c.Assert(user.IsDeleted(), jc.IsTrue) +} + +func (s *UserSuite) TestRemoveUserLongForm(c *gc.C) { + user := s.Factory.MakeUser(c, &factory.UserParams{Name: "jjam"}) + _, err := s.RunUserCommand(c, "", "remove-user", "--yes", "jjam") + c.Assert(err, jc.ErrorIsNil) + err = user.Refresh() + c.Assert(err, jc.ErrorIsNil) + c.Assert(user.IsDeleted(), jc.IsTrue) +} + func (s *UserSuite) TestUserList(c *gc.C) { ctx, err := s.RunUserCommand(c, "", "list-users") c.Assert(err, jc.ErrorIsNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/featuretests/package_test.go juju-core-2.0~beta15/src/github.com/juju/juju/featuretests/package_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/featuretests/package_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/featuretests/package_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,6 +5,7 @@ import ( "flag" + "runtime" stdtesting "testing" gc "gopkg.in/check.v1" @@ -22,23 +23,29 @@ return } // Initialize all suites here. - gc.Suite(&cmdJujuSuite{}) gc.Suite(&annotationsSuite{}) + gc.Suite(&CloudAPISuite{}) gc.Suite(&apiEnvironmentSuite{}) + gc.Suite(&BakeryStorageSuite{}) gc.Suite(&blockSuite{}) + gc.Suite(&cmdControllerSuite{}) + gc.Suite(&cmdJujuSuite{}) + gc.Suite(&cmdLoginSuite{}) gc.Suite(&cmdModelSuite{}) + gc.Suite(&cmdRegistrationSuite{}) gc.Suite(&cmdStorageSuite{}) - gc.Suite(&cmdControllerSuite{}) - gc.Suite(&dblogSuite{}) - gc.Suite(&cloudImageMetadataSuite{}) - gc.Suite(&cmdSpaceSuite{}) gc.Suite(&cmdSubnetSuite{}) - gc.Suite(&undertakerSuite{}) + gc.Suite(&dblogSuite{}) gc.Suite(&dumpLogsCommandSuite{}) + gc.Suite(&undertakerSuite{}) gc.Suite(&upgradeSuite{}) - gc.Suite(&cmdRegistrationSuite{}) - gc.Suite(&cmdLoginSuite{}) - gc.Suite(&BakeryStorageSuite{}) + + // TODO (anastasiamac 2016-07-19) Bug#1603585 + // These tests cannot run on windows - they require a bootstrapped controller. + if runtime.GOOS == "linux" { + gc.Suite(&cloudImageMetadataSuite{}) + gc.Suite(&cmdSpaceSuite{}) + } } func TestPackage(t *stdtesting.T) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/featuretests/storage_test.go juju-core-2.0~beta15/src/github.com/juju/juju/featuretests/storage_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/featuretests/storage_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/featuretests/storage_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -15,11 +15,10 @@ "github.com/juju/errors" jujucmd "github.com/juju/juju/cmd/juju/commands" jujutesting "github.com/juju/juju/juju/testing" - "github.com/juju/juju/provider/ec2" + "github.com/juju/juju/provider/dummy" "github.com/juju/juju/state" "github.com/juju/juju/storage/poolmanager" "github.com/juju/juju/storage/provider" - "github.com/juju/juju/storage/provider/registry" "github.com/juju/juju/testing" ) @@ -29,12 +28,9 @@ func setupTestStorageSupport(c *gc.C, s *state.State) { stsetts := state.NewStateSettings(s) - poolManager := poolmanager.New(stsetts) + poolManager := poolmanager.New(stsetts, dummy.StorageProviders()) _, err := poolManager.Create(testPool, provider.LoopProviderType, map[string]interface{}{"it": "works"}) c.Assert(err, jc.ErrorIsNil) - - registry.RegisterEnvironStorageProviders("dummy", ec2.EBS_ProviderType) - registry.RegisterEnvironStorageProviders("controller", ec2.EBS_ProviderType) } func makeStorageCons(pool string, size, count uint64) state.StorageConstraints { @@ -232,12 +228,18 @@ provider: loop attrs: it: works -ebs: - provider: ebs +environscoped: + provider: environscoped +environscoped-block: + provider: environscoped-block loop: provider: loop +machinescoped: + provider: machinescoped rootfs: provider: rootfs +static: + provider: static tmpfs: provider: tmpfs `[1:] @@ -248,12 +250,15 @@ stdout, _, err := runPoolList(c) c.Assert(err, jc.ErrorIsNil) expected := ` -NAME PROVIDER ATTRS -block loop it=works -ebs ebs -loop loop -rootfs rootfs -tmpfs tmpfs +NAME PROVIDER ATTRS +block loop it=works +environscoped environscoped +environscoped-block environscoped-block +loop loop +machinescoped machinescoped +rootfs rootfs +static static +tmpfs tmpfs `[1:] c.Assert(stdout, gc.Equals, expected) @@ -298,14 +303,7 @@ c.Assert(stdout, gc.Equals, expected) } -func (s *cmdStorageSuite) registerTmpProviderType(c *gc.C) { - cfg, err := s.State.ModelConfig() - c.Assert(err, jc.ErrorIsNil) - registry.RegisterEnvironStorageProviders(cfg.Name(), provider.TmpfsProviderType) -} - func (s *cmdStorageSuite) TestListPoolsProviderNoMatch(c *gc.C) { - s.registerTmpProviderType(c) stdout, _, err := runPoolList(c, "--format", "yaml", "--provider", string(provider.TmpfsProviderType)) c.Assert(err, jc.ErrorIsNil) expected := ` @@ -318,7 +316,7 @@ func (s *cmdStorageSuite) TestListPoolsProviderUnregistered(c *gc.C) { _, stderr, err := runPoolList(c, "--provider", "oops") c.Assert(err, gc.NotNil) - c.Assert(stderr, jc.Contains, `"oops" not supported`) + c.Assert(stderr, jc.Contains, `storage provider "oops" not found`) } func (s *cmdStorageSuite) TestListPoolsNameAndProvider(c *gc.C) { @@ -334,14 +332,13 @@ } func (s *cmdStorageSuite) TestListPoolsProviderAndNotName(c *gc.C) { - stdout, _, err := runPoolList(c, "--name", "fluff", "--provider", "ebs") + stdout, _, err := runPoolList(c, "--name", "fluff", "--provider", "environscoped") c.Assert(err, jc.ErrorIsNil) // there is no pool that matches this name AND type c.Assert(stdout, gc.Equals, "") } func (s *cmdStorageSuite) TestListPoolsNameAndNotProvider(c *gc.C) { - s.registerTmpProviderType(c) stdout, _, err := runPoolList(c, "--name", "block", "--provider", string(provider.TmpfsProviderType)) c.Assert(err, jc.ErrorIsNil) // no pool matches this name and this provider @@ -349,7 +346,6 @@ } func (s *cmdStorageSuite) TestListPoolsNotNameAndNotProvider(c *gc.C) { - s.registerTmpProviderType(c) stdout, _, err := runPoolList(c, "--name", "fluff", "--provider", string(provider.TmpfsProviderType)) c.Assert(err, jc.ErrorIsNil) c.Assert(stdout, gc.Equals, "") @@ -409,7 +405,7 @@ func assertPoolExists(c *gc.C, st *state.State, pname, provider, attr string) { stsetts := state.NewStateSettings(st) - poolManager := poolmanager.New(stsetts) + poolManager := poolmanager.New(stsetts, dummy.StorageProviders()) found, err := poolManager.List() c.Assert(err, jc.ErrorIsNil) @@ -538,7 +534,7 @@ func (s *cmdStorageSuite) TestStorageAddToUnitHasVolumes(c *gc.C) { // Reproducing Bug1462146 - u := createUnitWithFileSystemStorage(c, &s.JujuConnSuite, "ebs") + u := createUnitWithFileSystemStorage(c, &s.JujuConnSuite, "environscoped-block") instancesBefore, err := s.State.AllStorageInstances() c.Assert(err, jc.ErrorIsNil) s.assertStorageExist(c, instancesBefore, "data") @@ -556,7 +552,7 @@ `[1:]) c.Assert(testing.Stderr(context), gc.Equals, "") - context, err = runAddToUnit(c, u, "data=ebs,1G") + context, err = runAddToUnit(c, u, "data=environscoped-block,1G") c.Assert(err, jc.ErrorIsNil) c.Assert(testing.Stdout(context), gc.Equals, "added \"data\"\n") c.Assert(testing.Stderr(context), gc.Equals, "") diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/featuretests/upgrade_test.go juju-core-2.0~beta15/src/github.com/juju/juju/featuretests/upgrade_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/featuretests/upgrade_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/featuretests/upgrade_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,11 +7,9 @@ package featuretests import ( - "reflect" "strings" "time" - "github.com/juju/errors" jc "github.com/juju/testing/checkers" "github.com/juju/utils" "github.com/juju/utils/arch" @@ -30,7 +28,6 @@ envtesting "github.com/juju/juju/environs/testing" jujutesting "github.com/juju/juju/juju/testing" "github.com/juju/juju/mongo" - "github.com/juju/juju/rpc" "github.com/juju/juju/state" "github.com/juju/juju/state/watcher" coretesting "github.com/juju/juju/testing" @@ -48,6 +45,7 @@ RestrictedAPIExposed = false ) +// TODO(katco): 2016-08-09: lp:1611427 var ShortAttempt = &utils.AttemptStrategy{ Total: time.Second * 10, Delay: time.Millisecond * 200, @@ -317,7 +315,7 @@ return } case RestrictedAPIExposed: - if reflect.DeepEqual(errors.Cause(err), &rpc.RequestError{Message: params.CodeUpgradeInProgress, Code: params.CodeUpgradeInProgress}) { + if params.IsCodeUpgradeInProgress(err) { return } } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/instance/distribution.go juju-core-2.0~beta15/src/github.com/juju/juju/instance/distribution.go --- juju-core-2.0~beta12/src/github.com/juju/juju/instance/distribution.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/instance/distribution.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,21 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package instance + +// Distributor is an interface that may be used to distribute +// application units across instances for high availability. +type Distributor interface { + // DistributeInstance takes a set of clean, empty + // instances, and a distribution group, and returns + // the subset of candidates which the policy will + // allow entry into the distribution group. + // + // The AssignClean and AssignCleanEmpty unit + // assignment policies will attempt to assign a + // unit to each of the resulting instances until + // one is successful. If no instances can be assigned + // to (e.g. because of concurrent deployments), then + // a new machine will be allocated. + DistributeInstances(candidates, distributionGroup []Id) ([]Id, error) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/instance/instance.go juju-core-2.0~beta15/src/github.com/juju/juju/instance/instance.go --- juju-core-2.0~beta12/src/github.com/juju/juju/instance/instance.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/instance/instance.go 2016-08-16 08:56:25.000000000 +0000 @@ -67,27 +67,6 @@ AvailabilityZone *string `json:"availability-zone,omitempty" yaml:"availabilityzone,omitempty"` } -// An error reporting that an error has occurred during instance creation -// (e.g. due to a failed container from on of previous deploys) and -// that it is safe to restart instance creation -type RetryableCreationError struct { - message string -} - -// Returns the error message -func (e RetryableCreationError) Error() string { return e.message } - -func NewRetryableCreationError(errorMessage string) *RetryableCreationError { - return &RetryableCreationError{errorMessage} -} - -// IsRetryableCreationError returns true if the given error is -// RetryableCreationError -func IsRetryableCreationError(err error) bool { - _, ok := err.(*RetryableCreationError) - return ok -} - func (hc HardwareCharacteristics) String() string { var strs []string if hc.Arch != nil { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/juju/api.go juju-core-2.0~beta15/src/github.com/juju/juju/juju/api.go --- juju-core-2.0~beta12/src/github.com/juju/juju/juju/api.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/juju/api.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,7 +5,6 @@ import ( "encoding/json" - "fmt" "time" "github.com/juju/errors" @@ -33,8 +32,6 @@ cachedInfo *api.Info } -var errAborted = fmt.Errorf("aborted") - // NewAPIConnectionParams contains the parameters for creating a new Juju API // connection. type NewAPIConnectionParams struct { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/juju/api_test.go juju-core-2.0~beta15/src/github.com/juju/juju/juju/api_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/juju/api_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/juju/api_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -90,8 +90,8 @@ env, err := bootstrap.Prepare(ctx, store, bootstrap.PrepareParams{ ControllerConfig: coretesting.FakeControllerConfig(), ControllerName: controllerName, - BaseConfig: dummy.SampleConfig(), - CloudName: "dummy", + ModelConfig: dummy.SampleConfig(), + Cloud: dummy.SampleCloudSpec(), AdminSecret: "admin-secret", }) c.Assert(err, jc.ErrorIsNil) @@ -129,7 +129,7 @@ return expectState, nil } - st, err := newAPIConnectionFromNames(c, "noconfig", "admin", store, apiOpen) + st, err := newAPIConnectionFromNames(c, "noconfig", "admin@local/admin", store, apiOpen) c.Assert(err, jc.ErrorIsNil) c.Assert(st, gc.Equals, expectState) c.Assert(called, gc.Equals, 1) @@ -145,7 +145,7 @@ // If APIHostPorts haven't changed, then the store won't be updated. stubStore := jujuclienttesting.WrapClientStore(store) - st, err = newAPIConnectionFromNames(c, "noconfig", "admin", stubStore, apiOpen) + st, err = newAPIConnectionFromNames(c, "noconfig", "admin@local/admin", stubStore, apiOpen) c.Assert(err, jc.ErrorIsNil) c.Assert(st, gc.Equals, expectState) c.Assert(called, gc.Equals, 2) @@ -206,7 +206,7 @@ return nil, fmt.Errorf("OpenAPI called too many times") } - st0, err := newAPIConnectionFromNames(c, "ctl", "admin", store, redirOpen) + st0, err := newAPIConnectionFromNames(c, "ctl", "admin@local/admin", store, redirOpen) c.Assert(err, jc.ErrorIsNil) c.Assert(openCount, gc.Equals, 2) st := st0.(*mockAPIState) @@ -260,7 +260,7 @@ }) c.Assert(err, jc.ErrorIsNil) - err = store.UpdateModel(controllerName, "admin", jujuclient.ModelDetails{ + err = store.UpdateModel(controllerName, "admin@local/admin", jujuclient.ModelDetails{ fakeUUID, }) c.Assert(err, jc.ErrorIsNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/juju/testing/conn.go juju-core-2.0~beta15/src/github.com/juju/juju/juju/testing/conn.go --- juju-core-2.0~beta12/src/github.com/juju/juju/juju/testing/conn.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/juju/testing/conn.go 2016-08-16 08:56:25.000000000 +0000 @@ -46,6 +46,7 @@ "github.com/juju/juju/provider/dummy" "github.com/juju/juju/state" "github.com/juju/juju/state/binarystorage" + "github.com/juju/juju/state/stateenvirons" statestorage "github.com/juju/juju/state/storage" "github.com/juju/juju/testcharms" "github.com/juju/juju/testing" @@ -272,15 +273,15 @@ for key, value := range s.ControllerConfigAttrs { s.ControllerConfig[key] = value } + cloudSpec := dummy.SampleCloudSpec() environ, err := bootstrap.Prepare( modelcmd.BootstrapContext(ctx), s.ControllerStore, bootstrap.PrepareParams{ ControllerConfig: s.ControllerConfig, - BaseConfig: cfg.AllAttrs(), - Credential: cloud.NewEmptyCredential(), + ModelConfig: cfg.AllAttrs(), + Cloud: cloudSpec, ControllerName: ControllerName, - CloudName: "dummy", AdminSecret: AdminSecret, }, ) @@ -310,13 +311,17 @@ s.ControllerConfig["api-port"] = apiPort err = bootstrap.Bootstrap(modelcmd.BootstrapContext(ctx), environ, bootstrap.BootstrapParams{ ControllerConfig: s.ControllerConfig, - CloudName: "dummy", + CloudName: cloudSpec.Name, Cloud: cloud.Cloud{ - Type: "dummy", - AuthTypes: []cloud.AuthType{cloud.EmptyAuthType}, + Type: cloudSpec.Type, + AuthTypes: []cloud.AuthType{cloud.EmptyAuthType}, + Endpoint: cloudSpec.Endpoint, + StorageEndpoint: cloudSpec.StorageEndpoint, }, - AdminSecret: AdminSecret, - CAPrivateKey: testing.CAKey, + CloudCredential: cloudSpec.Credential, + CloudCredentialName: "cred", + AdminSecret: AdminSecret, + CAPrivateKey: testing.CAKey, }) c.Assert(err, jc.ErrorIsNil) @@ -384,6 +389,7 @@ s.AddToolsToState(c, versions...) } +// TODO(katco): 2016-08-09: lp:1611427 var redialStrategy = utils.AttemptStrategy{ Total: 60 * time.Second, Delay: 250 * time.Millisecond, @@ -401,13 +407,16 @@ mongoInfo.Password = password opts := mongotest.DialOpts() - st, err := state.Open(modelTag, mongoInfo, opts, environs.NewStatePolicy()) + newPolicyFunc := stateenvirons.GetNewPolicyFunc( + stateenvirons.GetNewEnvironFunc(environs.New), + ) + st, err := state.Open(modelTag, mongoInfo, opts, newPolicyFunc) if errors.IsUnauthorized(errors.Cause(err)) { // We try for a while because we might succeed in // connecting to mongo before the state has been // initialized and the initial password set. for a := redialStrategy.Start(); a.Next(); { - st, err = state.Open(modelTag, mongoInfo, opts, environs.NewStatePolicy()) + st, err = state.Open(modelTag, mongoInfo, opts, newPolicyFunc) if !errors.IsUnauthorized(errors.Cause(err)) { break } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/jujuclient/accounts.go juju-core-2.0~beta15/src/github.com/juju/juju/jujuclient/accounts.go --- juju-core-2.0~beta12/src/github.com/juju/juju/jujuclient/accounts.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/jujuclient/accounts.go 2016-08-16 08:56:25.000000000 +0000 @@ -63,8 +63,8 @@ ControllerAccounts map[string]AccountDetails `yaml:"controllers"` } -// TODO(axw) 2016-07-14 #NNN -// Drop this code once we get to 2.0-beta13. +// TODO(axw) 2016-07-14 #1603841 +// Drop this code once we get to 2.0. func migrateLegacyAccounts(data []byte) error { type legacyControllerAccounts struct { Accounts map[string]AccountDetails `yaml:"accounts"` diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/jujuclient/bootstrapconfigfile_test.go juju-core-2.0~beta15/src/github.com/juju/juju/jujuclient/bootstrapconfigfile_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/jujuclient/bootstrapconfigfile_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/jujuclient/bootstrapconfigfile_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,6 +9,7 @@ jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" + "github.com/juju/juju/controller" "github.com/juju/juju/juju/osenv" "github.com/juju/juju/jujuclient" "github.com/juju/juju/testing" @@ -23,38 +24,56 @@ const testBootstrapConfigYAML = ` controllers: aws-test: - config: + controller-config: + api-port: 17070 + state-port: 37017 + model-config: name: admin type: ec2 credential: default cloud: aws + type: ec2 region: us-east-1 endpoint: https://us-east-1.amazonaws.com mallards: - config: + controller-config: + api-port: 17070 + state-port: 37017 + model-config: name: admin type: maas cloud: maas + type: maas region: 127.0.0.1 ` var testBootstrapConfig = map[string]jujuclient.BootstrapConfig{ "aws-test": { + ControllerConfig: controller.Config{ + "api-port": 17070, + "state-port": 37017, + }, Config: map[string]interface{}{ "type": "ec2", "name": "admin", }, Credential: "default", Cloud: "aws", + CloudType: "ec2", CloudRegion: "us-east-1", CloudEndpoint: "https://us-east-1.amazonaws.com", }, "mallards": { + ControllerConfig: controller.Config{ + "api-port": 17070, + "state-port": 37017, + }, Config: map[string]interface{}{ "type": "maas", "name": "admin", }, Cloud: "maas", + CloudType: "maas", CloudRegion: "127.0.0.1", }, } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/jujuclient/bootstrapconfig.go juju-core-2.0~beta15/src/github.com/juju/juju/jujuclient/bootstrapconfig.go --- juju-core-2.0~beta12/src/github.com/juju/juju/jujuclient/bootstrapconfig.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/jujuclient/bootstrapconfig.go 2016-08-16 08:56:25.000000000 +0000 @@ -55,6 +55,14 @@ if err != nil { return nil, errors.Annotate(err, "cannot unmarshal bootstrap config") } + // TODO(wallyworld) - drop when we get to beta 15. + // This is for backwards compatibility with beta 13. + for controllerName, cfg := range result.ControllerBootstrapConfig { + if cfg.Config == nil { + cfg.Config = cfg.OldConfig + result.ControllerBootstrapConfig[controllerName] = cfg + } + } return result.ControllerBootstrapConfig, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/jujuclient/file.go juju-core-2.0~beta15/src/github.com/juju/juju/jujuclient/file.go --- juju-core-2.0~beta12/src/github.com/juju/juju/jujuclient/file.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/jujuclient/file.go 2016-08-16 08:56:25.000000000 +0000 @@ -449,11 +449,12 @@ if all == nil { all = make(map[string]*ControllerModels) } - controllerModels = &ControllerModels{ - Models: make(map[string]ModelDetails), - } + controllerModels = &ControllerModels{} all[controllerName] = controllerModels } + if controllerModels.Models == nil { + controllerModels.Models = make(map[string]ModelDetails) + } updated, err := update(controllerModels) if err != nil { return errors.Trace(err) @@ -635,5 +636,11 @@ if !ok { return nil, errors.NotFoundf("bootstrap config for controller %s", controllerName) } + if cfg.CloudType == "" { + // TODO(axw) 2016-07-25 #1603841 + // Drop this when we get to 2.0. This exists only for + // compatibility with previous beta releases. + cfg.CloudType, _ = cfg.Config["type"].(string) + } return &cfg, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/jujuclient/interface.go juju-core-2.0~beta15/src/github.com/juju/juju/jujuclient/interface.go --- juju-core-2.0~beta12/src/github.com/juju/juju/jujuclient/interface.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/jujuclient/interface.go 2016-08-16 08:56:25.000000000 +0000 @@ -3,7 +3,10 @@ package jujuclient -import "github.com/juju/juju/cloud" +import ( + "github.com/juju/juju/cloud" + "github.com/juju/juju/controller" +) // ControllerDetails holds the details needed to connect to a controller. type ControllerDetails struct { @@ -59,9 +62,16 @@ // bootstrap configuration. A reference to the credential used will be // stored, rather than the credential itself. type BootstrapConfig struct { - // ModelConfig is the base configuration for the provider. This should - // be updated with the region, endpoint and credentials. - Config map[string]interface{} `yaml:"config"` + // ControllerConfig is the controller configuration. + ControllerConfig controller.Config `yaml:"controller-config"` + + // Config is the complete configuration for the provider. + // This should be updated with the region, endpoint and credentials. + Config map[string]interface{} `yaml:"model-config"` + + // TODO(wallyworld) - drop when we get to beta 15. + // This is for backwards compatibility with beta 13. + OldConfig map[string]interface{} `yaml:"base-model-config,omitempty"` // Credential is the name of the credential used to bootstrap. // @@ -71,6 +81,9 @@ // Cloud is the name of the cloud to create the Juju controller in. Cloud string `yaml:"cloud"` + // CloudType is the type of the cloud to create the Juju controller in. + CloudType string `yaml:"type"` + // CloudRegion is the name of the region of the cloud to create // the Juju controller in. This will be empty for clouds without // regions. diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/jujuclient/modelsfile_test.go juju-core-2.0~beta15/src/github.com/juju/juju/jujuclient/modelsfile_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/jujuclient/modelsfile_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/jujuclient/modelsfile_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -24,28 +24,28 @@ controllers: ctrl: models: - admin: + admin@local/admin: uuid: ghi kontroll: models: - admin: + admin@local/admin: uuid: abc - my-model: + admin@local/my-model: uuid: def - current-model: my-model + current-model: admin@local/my-model ` var testControllerModels = map[string]*jujuclient.ControllerModels{ "kontroll": { Models: map[string]jujuclient.ModelDetails{ - "admin": kontrollAdminModelDetails, - "my-model": kontrollMyModelModelDetails, + "admin@local/admin": kontrollAdminModelDetails, + "admin@local/my-model": kontrollMyModelModelDetails, }, - CurrentModel: "my-model", + CurrentModel: "admin@local/my-model", }, "ctrl": { Models: map[string]jujuclient.ModelDetails{ - "admin": ctrlAdminModelDetails, + "admin@local/admin": ctrlAdminModelDetails, }, }, } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/jujuclient/models.go juju-core-2.0~beta15/src/github.com/juju/juju/jujuclient/models.go --- juju-core-2.0~beta12/src/github.com/juju/juju/jujuclient/models.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/jujuclient/models.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,11 +4,14 @@ package jujuclient import ( + "fmt" "io/ioutil" "os" + "strings" "github.com/juju/errors" "github.com/juju/utils" + "gopkg.in/juju/names.v2" "gopkg.in/yaml.v2" "github.com/juju/juju/juju/osenv" @@ -76,8 +79,8 @@ CurrentModel string `yaml:"current-model,omitempty"` } -// TODO(axw) 2016-07-14 #NNN -// Drop this code once we get to 2.0-beta13. +// TODO(axw) 2016-07-14 #1603841 +// Drop this code once we get to 2.0. func migrateLegacyModels(data []byte) error { accounts, err := ReadAccountsFile(JujuAccountsPath()) if err != nil { @@ -122,3 +125,30 @@ } return nil } + +// JoinOwnerModelName returns a model name qualified with the model owner. +func JoinOwnerModelName(owner names.UserTag, modelName string) string { + return fmt.Sprintf("%s/%s", owner.Canonical(), modelName) +} + +// IsQualifiedModelName returns true if the provided model name is qualified +// with an owner. The name is assumed to be either a valid qualified model +// name, or a valid unqualified model name. +func IsQualifiedModelName(name string) bool { + return strings.ContainsRune(name, '/') +} + +// SplitModelName splits a qualified model name into the model and owner +// name components. +func SplitModelName(name string) (string, names.UserTag, error) { + i := strings.IndexRune(name, '/') + if i < 0 { + return "", names.UserTag{}, errors.NotValidf("unqualified model name %q", name) + } + owner := name[:i] + if !names.IsValidUser(owner) { + return "", names.UserTag{}, errors.NotValidf("user name %q", owner) + } + name = name[i+1:] + return name, names.NewUserTag(owner), nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/jujuclient/models_test.go juju-core-2.0~beta15/src/github.com/juju/juju/jujuclient/models_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/jujuclient/models_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/jujuclient/models_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,6 +4,7 @@ package jujuclient_test import ( + "io/ioutil" "os" "github.com/juju/errors" @@ -30,28 +31,28 @@ func (s *ModelsSuite) TestModelByNameNoFile(c *gc.C) { err := os.Remove(jujuclient.JujuModelsPath()) c.Assert(err, jc.ErrorIsNil) - details, err := s.store.ModelByName("not-found", "admin") + details, err := s.store.ModelByName("not-found", "admin@local/admin") c.Assert(err, gc.ErrorMatches, "models for controller not-found not found") c.Assert(details, gc.IsNil) } func (s *ModelsSuite) TestModelByNameControllerNotFound(c *gc.C) { - details, err := s.store.ModelByName("not-found", "admin") + details, err := s.store.ModelByName("not-found", "admin@local/admin") c.Assert(err, gc.ErrorMatches, "models for controller not-found not found") c.Assert(details, gc.IsNil) } func (s *ModelsSuite) TestModelByNameModelNotFound(c *gc.C) { - details, err := s.store.ModelByName("kontroll", "not-found") - c.Assert(err, gc.ErrorMatches, "model kontroll:not-found not found") + details, err := s.store.ModelByName("kontroll", "admin@local/not-found") + c.Assert(err, gc.ErrorMatches, "model kontroll:admin@local/not-found not found") c.Assert(details, gc.IsNil) } func (s *ModelsSuite) TestModelByName(c *gc.C) { - details, err := s.store.ModelByName("kontroll", "admin") + details, err := s.store.ModelByName("kontroll", "admin@local/admin") c.Assert(err, jc.ErrorIsNil) c.Assert(details, gc.NotNil) - c.Assert(*details, jc.DeepEquals, testControllerModels["kontroll"].Models["admin"]) + c.Assert(*details, jc.DeepEquals, testControllerModels["kontroll"].Models["admin@local/admin"]) } func (s *ModelsSuite) TestAllModelsNoFile(c *gc.C) { @@ -71,7 +72,7 @@ func (s *ModelsSuite) TestCurrentModel(c *gc.C) { current, err := s.store.CurrentModel("kontroll") c.Assert(err, jc.ErrorIsNil) - c.Assert(current, gc.Equals, "my-model") + c.Assert(current, gc.Equals, "admin@local/my-model") } func (s *ModelsSuite) TestCurrentModelNotSet(c *gc.C) { @@ -85,44 +86,44 @@ } func (s *ModelsSuite) TestSetCurrentModelControllerNotFound(c *gc.C) { - err := s.store.SetCurrentModel("not-found", "admin") + err := s.store.SetCurrentModel("not-found", "admin@local/admin") c.Assert(err, jc.Satisfies, errors.IsNotFound) } func (s *ModelsSuite) TestSetCurrentModelModelNotFound(c *gc.C) { - err := s.store.SetCurrentModel("kontroll", "not-found") + err := s.store.SetCurrentModel("kontroll", "admin@local/not-found") c.Assert(err, jc.Satisfies, errors.IsNotFound) } func (s *ModelsSuite) TestSetCurrentModel(c *gc.C) { - err := s.store.SetCurrentModel("kontroll", "admin") + err := s.store.SetCurrentModel("kontroll", "admin@local/admin") c.Assert(err, jc.ErrorIsNil) all, err := jujuclient.ReadModelsFile(jujuclient.JujuModelsPath()) c.Assert(err, jc.ErrorIsNil) - c.Assert(all["kontroll"].CurrentModel, gc.Equals, "admin") + c.Assert(all["kontroll"].CurrentModel, gc.Equals, "admin@local/admin") } func (s *ModelsSuite) TestUpdateModelNewController(c *gc.C) { testModelDetails := jujuclient.ModelDetails{"test.uuid"} - err := s.store.UpdateModel("new-controller", "new-model", testModelDetails) + err := s.store.UpdateModel("new-controller", "admin@local/new-model", testModelDetails) c.Assert(err, jc.ErrorIsNil) models, err := s.store.AllModels("new-controller") c.Assert(err, jc.ErrorIsNil) c.Assert(models, jc.DeepEquals, map[string]jujuclient.ModelDetails{ - "new-model": testModelDetails, + "admin@local/new-model": testModelDetails, }) } func (s *ModelsSuite) TestUpdateModelExistingControllerAndModelNewModel(c *gc.C) { testModelDetails := jujuclient.ModelDetails{"test.uuid"} - err := s.store.UpdateModel("kontroll", "new-model", testModelDetails) + err := s.store.UpdateModel("kontroll", "admin@local/new-model", testModelDetails) c.Assert(err, jc.ErrorIsNil) models, err := s.store.AllModels("kontroll") c.Assert(err, jc.ErrorIsNil) c.Assert(models, jc.DeepEquals, map[string]jujuclient.ModelDetails{ - "admin": kontrollAdminModelDetails, - "my-model": kontrollMyModelModelDetails, - "new-model": testModelDetails, + "admin@local/admin": kontrollAdminModelDetails, + "admin@local/my-model": kontrollMyModelModelDetails, + "admin@local/new-model": testModelDetails, }) } @@ -131,35 +132,56 @@ for i := 0; i < 2; i++ { // Twice so we exercise the code path of updating with // identical details. - err := s.store.UpdateModel("kontroll", "admin", testModelDetails) + err := s.store.UpdateModel("kontroll", "admin@local/admin", testModelDetails) c.Assert(err, jc.ErrorIsNil) - details, err := s.store.ModelByName("kontroll", "admin") + details, err := s.store.ModelByName("kontroll", "admin@local/admin") c.Assert(err, jc.ErrorIsNil) c.Assert(*details, jc.DeepEquals, testModelDetails) } } +func (s *ModelsSuite) TestUpdateModelEmptyModels(c *gc.C) { + // This test exists to exercise a bug caused by the + // presence of a file with an empty "models" field, + // that would lead to a panic. + err := ioutil.WriteFile(jujuclient.JujuModelsPath(), []byte(` +controllers: + ctrl: + models: +`[1:]), 0644) + c.Assert(err, jc.ErrorIsNil) + + testModelDetails := jujuclient.ModelDetails{"test.uuid"} + err = s.store.UpdateModel("ctrl", "admin@local/admin", testModelDetails) + c.Assert(err, jc.ErrorIsNil) + models, err := s.store.AllModels("ctrl") + c.Assert(err, jc.ErrorIsNil) + c.Assert(models, jc.DeepEquals, map[string]jujuclient.ModelDetails{ + "admin@local/admin": testModelDetails, + }) +} + func (s *ModelsSuite) TestRemoveModelNoFile(c *gc.C) { err := os.Remove(jujuclient.JujuModelsPath()) c.Assert(err, jc.ErrorIsNil) - err = s.store.RemoveModel("not-found", "admin") + err = s.store.RemoveModel("not-found", "admin@local/admin") c.Assert(err, jc.Satisfies, errors.IsNotFound) } func (s *ModelsSuite) TestRemoveModelControllerNotFound(c *gc.C) { - err := s.store.RemoveModel("not-found", "admin") + err := s.store.RemoveModel("not-found", "admin@local/admin") c.Assert(err, jc.Satisfies, errors.IsNotFound) } func (s *ModelsSuite) TestRemoveModelNotFound(c *gc.C) { - err := s.store.RemoveModel("kontroll", "not-found") + err := s.store.RemoveModel("kontroll", "admin@local/not-found") c.Assert(err, jc.Satisfies, errors.IsNotFound) } func (s *ModelsSuite) TestRemoveModel(c *gc.C) { - err := s.store.RemoveModel("kontroll", "admin") + err := s.store.RemoveModel("kontroll", "admin@local/admin") c.Assert(err, jc.ErrorIsNil) - _, err = s.store.ModelByName("kontroll", "admin") + _, err = s.store.ModelByName("kontroll", "admin@local/admin") c.Assert(err, jc.Satisfies, errors.IsNotFound) } @@ -175,6 +197,6 @@ models, err := jujuclient.ReadModelsFile(jujuclient.JujuModelsPath()) c.Assert(err, jc.ErrorIsNil) - _, ok := models["kontroll"] + _, ok := models["admin@local/kontroll"] c.Assert(ok, jc.IsFalse) // kontroll models are removed } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/jujuclient/modelvalidation_test.go juju-core-2.0~beta15/src/github.com/juju/juju/jujuclient/modelvalidation_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/jujuclient/modelvalidation_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/jujuclient/modelvalidation_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,6 +4,7 @@ package jujuclient_test import ( + jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" "github.com/juju/juju/jujuclient" @@ -25,7 +26,12 @@ var _ = gc.Suite(&ModelValidationSuite{}) func (s *ModelValidationSuite) TestValidateModelName(c *gc.C) { - c.Assert(jujuclient.ValidateModelName(""), gc.ErrorMatches, "empty model name not valid") + c.Assert(jujuclient.ValidateModelName("foo@bar/baz"), jc.ErrorIsNil) + c.Assert(jujuclient.ValidateModelName("foo/bar"), gc.ErrorMatches, `validating model name \"foo/bar\": validating model owner name: unqualified user name "foo" not valid`) + c.Assert(jujuclient.ValidateModelName("foo"), gc.ErrorMatches, `validating model name "foo": unqualified model name "foo" not valid`) + c.Assert(jujuclient.ValidateModelName(""), gc.ErrorMatches, `validating model name "": unqualified model name "" not valid`) + c.Assert(jujuclient.ValidateModelName("!"), gc.ErrorMatches, `validating model name "!": unqualified model name "!" not valid`) + c.Assert(jujuclient.ValidateModelName("!/foo"), gc.ErrorMatches, `validating model name "!/foo": user name "!" not valid`) } func (s *ModelValidationSuite) TestValidateModelDetailsNoModelUUID(c *gc.C) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/jujuclient/validation.go juju-core-2.0~beta15/src/github.com/juju/juju/jujuclient/validation.go --- juju-core-2.0~beta12/src/github.com/juju/juju/jujuclient/validation.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/jujuclient/validation.go 2016-08-16 08:56:25.000000000 +0000 @@ -52,9 +52,16 @@ // ValidateModelName validates the given model name. func ValidateModelName(name string) error { - // TODO(axw) define a regex for valid model names. - if name == "" { - return errors.NotValidf("empty model name") + modelName, owner, err := SplitModelName(name) + if err != nil { + return errors.Annotatef(err, "validating model name %q", name) + } + if err := validateUserTag(owner); err != nil { + err = errors.Annotate(err, "validating model owner name") + return errors.Annotatef(err, "validating model name %q", name) + } + if !names.IsValidModelName(modelName) { + return errors.NotValidf("model name %q", name) } return nil } @@ -70,6 +77,9 @@ if cfg.Cloud == "" { return errors.NotValidf("empty cloud name") } + if cfg.CloudType == "" { + return errors.NotValidf("empty cloud type") + } if len(cfg.Config) == 0 { return errors.NotValidf("empty config") } @@ -78,10 +88,15 @@ func validateUser(name string) error { if !names.IsValidUser(name) { - return errors.NotValidf("account name %q", name) + return errors.NotValidf("user name %q", name) } - if tag := names.NewUserTag(name); tag.Id() != tag.Canonical() { - return errors.NotValidf("unqualified account name %q", name) + tag := names.NewUserTag(name) + return validateUserTag(tag) +} + +func validateUserTag(tag names.UserTag) error { + if tag.Id() != tag.Canonical() { + return errors.NotValidf("unqualified user name %q", tag.Id()) } return nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/logfwd/syslog/config.go juju-core-2.0~beta15/src/github.com/juju/juju/logfwd/syslog/config.go --- juju-core-2.0~beta12/src/github.com/juju/juju/logfwd/syslog/config.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/logfwd/syslog/config.go 2016-08-16 08:56:25.000000000 +0000 @@ -45,10 +45,11 @@ return errors.Trace(err) } - if _, err := cfg.tlsConfig(); err != nil { - return errors.Annotate(err, "validating TLS config") + if cfg.Enabled || cfg.ClientKey != "" || cfg.ClientCert != "" || cfg.CACert != "" { + if _, err := cfg.tlsConfig(); err != nil { + return errors.Annotate(err, "validating TLS config") + } } - return nil } @@ -57,7 +58,7 @@ if err != nil { host = cfg.Host } - if host == "" { + if host == "" && cfg.Enabled { return errors.NotValidf("Host %q", cfg.Host) } return nil diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/logfwd/syslog/config_test.go juju-core-2.0~beta15/src/github.com/juju/juju/logfwd/syslog/config_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/logfwd/syslog/config_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/logfwd/syslog/config_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,7 +4,6 @@ package syslog_test import ( - "github.com/juju/errors" "github.com/juju/testing" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" @@ -47,14 +46,13 @@ func (s *ConfigSuite) TestRawValidateZeroValue(c *gc.C) { var cfg syslog.RawConfig - err := cfg.Validate() - - c.Check(err, jc.Satisfies, errors.IsNotValid) + c.Check(err, jc.ErrorIsNil) } func (s *ConfigSuite) TestRawValidateMissingHost(c *gc.C) { cfg := syslog.RawConfig{ + Enabled: true, Host: "", CACert: validCACert, ClientCert: validCert, @@ -66,8 +64,21 @@ c.Check(err, gc.ErrorMatches, `Host "" not valid`) } +func (s *ConfigSuite) TestRawValidateMissingHostNotEnabled(c *gc.C) { + cfg := syslog.RawConfig{ + Host: "", + CACert: validCACert, + ClientCert: validCert, + ClientKey: validKey, + } + + err := cfg.Validate() + c.Check(err, jc.ErrorIsNil) +} + func (s *ConfigSuite) TestRawValidateMissingHostname(c *gc.C) { cfg := syslog.RawConfig{ + Enabled: true, Host: ":9876", CACert: validCACert, ClientCert: validCert, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/Makefile juju-core-2.0~beta15/src/github.com/juju/juju/Makefile --- juju-core-2.0~beta12/src/github.com/juju/juju/Makefile 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/Makefile 2016-08-16 08:56:25.000000000 +0000 @@ -102,7 +102,7 @@ @sudo install -o root -g root -m 644 etc/bash_completion.d/juju2 /etc/bash_completion.d setup-lxd: -ifeq ($(shell ifconfig lxdbr0 | grep -q "inet addr" && echo true),true) +ifeq ($(shell ifconfig lxdbr0 2>&1 | grep -q "inet addr" && echo true),true) @echo IPv4 networking is already setup for LXD. @echo run "sudo scripts/setup-lxd.sh" to reconfigure IPv4 networking else diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/migration/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/migration/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/migration/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/migration/export_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -3,6 +3,4 @@ package migration -var ( - GetCharmStoragePath = getCharmStoragePath -) +var () diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/migration/migration.go juju-core-2.0~beta15/src/github.com/juju/juju/migration/migration.go --- juju-core-2.0~beta12/src/github.com/juju/juju/migration/migration.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/migration/migration.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,20 +6,18 @@ import ( "io" "io/ioutil" + "net/url" "os" "github.com/juju/errors" "github.com/juju/loggo" - "github.com/juju/utils/set" "github.com/juju/version" "gopkg.in/juju/charm.v6-unstable" "gopkg.in/mgo.v2" - "github.com/juju/juju/api" "github.com/juju/juju/core/description" "github.com/juju/juju/state" "github.com/juju/juju/state/binarystorage" - "github.com/juju/juju/state/storage" "github.com/juju/juju/tools" ) @@ -64,6 +62,12 @@ return dbModel, dbState, nil } +// CharmDownlaoder defines a single method that is used to download a +// charm from the source controller in a migration. +type CharmDownloader interface { + OpenCharm(*charm.URL) (io.ReadCloser, error) +} + // UploadBackend define the methods on *state.State that are needed for // uploading the tools and charms from the current controller to a different // controller. @@ -74,72 +78,50 @@ ToolsStorage() (binarystorage.StorageCloser, error) } -// CharmUploader defines a simple single method interface that is used to -// upload a charm to the target controller +// CharmUploader defines a single method that is used to upload a +// charm to the target controller in a migration. type CharmUploader interface { UploadCharm(*charm.URL, io.ReadSeeker) (*charm.URL, error) } -// ToolsUploader defines a simple single method interface that is used to -// upload tools to the target controller +// ToolsDownloader defines a single method that is used to download +// tools from the source controller in a migration. +type ToolsDownloader interface { + OpenURI(string, url.Values) (io.ReadCloser, error) +} + +// ToolsUploader defines a single method that is used to upload tools +// to the target controller in a migration. type ToolsUploader interface { UploadTools(io.ReadSeeker, version.Binary, ...string) (tools.List, error) } -// UploadBinariesConfig provides all the configuration that the UploadBinaries -// function needs to operate. The functions are configurable for testing -// purposes. To construct the config with the default functions, use -// `NewUploadBinariesConfig`. +// UploadBinariesConfig provides all the configuration that the +// UploadBinaries function needs to operate. To construct the config +// with the default helper functions, use `NewUploadBinariesConfig`. type UploadBinariesConfig struct { - State UploadBackend - Model description.Model - Target api.Connection - - GetCharmUploader func(api.Connection) CharmUploader - GetToolsUploader func(api.Connection) ToolsUploader - - GetStateStorage func(UploadBackend) storage.Storage - GetCharmStoragePath func(UploadBackend, *charm.URL) (string, error) -} - -// NewUploadBinariesConfig constructs a `UploadBinariesConfig` with the default -// functions to get the uploaders for the target api connection, and functions -// used to get the charm data out of the database. -func NewUploadBinariesConfig(backend UploadBackend, model description.Model, target api.Connection) UploadBinariesConfig { - return UploadBinariesConfig{ - State: backend, - Model: model, - Target: target, - - GetCharmUploader: getCharmUploader, - GetStateStorage: getStateStorage, - GetToolsUploader: getToolsUploader, - GetCharmStoragePath: getCharmStoragePath, - } + Charms []string + CharmDownloader CharmDownloader + CharmUploader CharmUploader + + Tools map[version.Binary]string + ToolsDownloader ToolsDownloader + ToolsUploader ToolsUploader } // Validate makes sure that all the config values are non-nil. func (c *UploadBinariesConfig) Validate() error { - if c.State == nil { - return errors.NotValidf("missing UploadBackend") + if c.CharmDownloader == nil { + return errors.NotValidf("missing CharmDownloader") } - if c.Model == nil { - return errors.NotValidf("missing Model") + if c.CharmUploader == nil { + return errors.NotValidf("missing CharmUploader") } - if c.Target == nil { - return errors.NotValidf("missing Target") + if c.ToolsDownloader == nil { + return errors.NotValidf("missing ToolsDownloader") } - if c.GetCharmUploader == nil { - return errors.NotValidf("missing GetCharmUploader") - } - if c.GetStateStorage == nil { - return errors.NotValidf("missing GetStateStorage") - } - if c.GetToolsUploader == nil { - return errors.NotValidf("missing GetToolsUploader") - } - if c.GetCharmStoragePath == nil { - return errors.NotValidf("missing GetCharmStoragePath") + if c.ToolsUploader == nil { + return errors.NotValidf("missing ToolsUploader") } return nil } @@ -150,65 +132,17 @@ if err := config.Validate(); err != nil { return errors.Trace(err) } - if err := uploadTools(config); err != nil { - return errors.Trace(err) - } - if err := uploadCharms(config); err != nil { return errors.Trace(err) } - - return nil -} - -func getStateStorage(backend UploadBackend) storage.Storage { - return storage.NewStorage(backend.ModelUUID(), backend.MongoSession()) -} - -func getToolsUploader(target api.Connection) ToolsUploader { - return target.Client() -} - -func getCharmUploader(target api.Connection) CharmUploader { - return target.Client() -} - -func uploadTools(config UploadBinariesConfig) error { - storage, err := config.State.ToolsStorage() - if err != nil { + if err := uploadTools(config); err != nil { return errors.Trace(err) } - defer storage.Close() - - usedVersions := getUsedToolsVersions(config.Model) - toolsUploader := config.GetToolsUploader(config.Target) - - for toolsVersion := range usedVersions { - logger.Debugf("send tools version %s to target", toolsVersion) - _, reader, err := storage.Open(toolsVersion.String()) - if err != nil { - return errors.Trace(err) - } - defer reader.Close() - - content, cleanup, err := streamThroughTempFile(reader) - if err != nil { - return errors.Trace(err) - } - defer cleanup() - - // UploadTools encapsulates the HTTP POST necessary to send the tools - // to the target API server. - if _, err := toolsUploader.UploadTools(content, toolsVersion); err != nil { - return errors.Trace(err) - } - } - return nil } func streamThroughTempFile(r io.Reader) (_ io.ReadSeeker, cleanup func(), err error) { - tempFile, err := ioutil.TempFile("", "juju-tools") + tempFile, err := ioutil.TempFile("", "juju-migrate-binary") if err != nil { return nil, nil, errors.Trace(err) } @@ -231,53 +165,18 @@ return tempFile, rmTempFile, nil } -func getUsedToolsVersions(model description.Model) map[version.Binary]bool { - // Iterate through the model for all tools, and make a map of them. - usedVersions := make(map[version.Binary]bool) - // It is most likely that the preconditions will limit the number of - // tools versions in use, but that is not depended on here. - for _, machine := range model.Machines() { - addToolsVersionForMachine(machine, usedVersions) - } - - for _, application := range model.Applications() { - for _, unit := range application.Units() { - tools := unit.Tools() - usedVersions[tools.Version()] = true - } - } - return usedVersions -} - -func addToolsVersionForMachine(machine description.Machine, usedVersions map[version.Binary]bool) { - tools := machine.Tools() - usedVersions[tools.Version()] = true - for _, container := range machine.Containers() { - addToolsVersionForMachine(container, usedVersions) - } -} - func uploadCharms(config UploadBinariesConfig) error { - storage := config.GetStateStorage(config.State) - usedCharms := getUsedCharms(config.Model) - charmUploader := config.GetCharmUploader(config.Target) - - for _, charmUrl := range usedCharms.Values() { - logger.Debugf("send charm %s to target", charmUrl) + for _, charmUrl := range config.Charms { + logger.Debugf("sending charm %s to target", charmUrl) curl, err := charm.ParseURL(charmUrl) if err != nil { return errors.Annotate(err, "bad charm URL") } - path, err := config.GetCharmStoragePath(config.State, curl) - if err != nil { - return errors.Trace(err) - } - - reader, _, err := storage.Get(path) + reader, err := config.CharmDownloader.OpenCharm(curl) if err != nil { - return errors.Annotate(err, "cannot get charm from storage") + return errors.Annotate(err, "cannot open charm") } defer reader.Close() @@ -287,28 +186,34 @@ } defer cleanup() - if _, err := charmUploader.UploadCharm(curl, content); err != nil { + if _, err := config.CharmUploader.UploadCharm(curl, content); err != nil { return errors.Annotate(err, "cannot upload charm") } } return nil } -func getUsedCharms(model description.Model) set.Strings { - result := set.NewStrings() - for _, application := range model.Applications() { - result.Add(application.CharmURL()) - } - return result -} +func uploadTools(config UploadBinariesConfig) error { + for v, uri := range config.Tools { + logger.Debugf("sending tools to target: %s", v) -func getCharmStoragePath(st UploadBackend, curl *charm.URL) (string, error) { - ch, err := st.Charm(curl) - if err != nil { - return "", errors.Annotate(err, "cannot get charm from state") - } + reader, err := config.ToolsDownloader.OpenURI(uri, nil) + if err != nil { + return errors.Annotate(err, "cannot open charm") + } + defer reader.Close() - return ch.StoragePath(), nil + content, cleanup, err := streamThroughTempFile(reader) + if err != nil { + return errors.Trace(err) + } + defer cleanup() + + if _, err := config.ToolsUploader.UploadTools(content, v); err != nil { + return errors.Annotate(err, "cannot upload tools") + } + } + return nil } // PrecheckBackend is implemented by *state.State but defined as an interface diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/migration/migration_test.go juju-core-2.0~beta15/src/github.com/juju/juju/migration/migration_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/migration/migration_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/migration/migration_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,6 +8,7 @@ "fmt" "io" "io/ioutil" + "net/url" "github.com/juju/errors" jc "github.com/juju/testing/checkers" @@ -15,20 +16,14 @@ "github.com/juju/version" gc "gopkg.in/check.v1" "gopkg.in/juju/charm.v6-unstable" - "gopkg.in/juju/names.v2" - "gopkg.in/mgo.v2" - "github.com/juju/juju/api" - "github.com/juju/juju/cmd/modelcmd" + "github.com/juju/juju/controller" "github.com/juju/juju/core/description" - "github.com/juju/juju/environs/bootstrap" - "github.com/juju/juju/jujuclient/jujuclienttesting" + "github.com/juju/juju/environs/config" "github.com/juju/juju/migration" "github.com/juju/juju/provider/dummy" _ "github.com/juju/juju/provider/dummy" "github.com/juju/juju/state" - "github.com/juju/juju/state/binarystorage" - "github.com/juju/juju/state/storage" statetesting "github.com/juju/juju/state/testing" "github.com/juju/juju/testing" "github.com/juju/juju/tools" @@ -47,20 +42,7 @@ // is one that isn't registered as a valid provider. For our tests here we // need a real registered provider, so we use the dummy provider. // NOTE: make a better test provider. - env, err := bootstrap.Prepare( - modelcmd.BootstrapContext(testing.Context(c)), - jujuclienttesting.NewMemStore(), - bootstrap.PrepareParams{ - ControllerConfig: testing.FakeControllerConfig(), - ControllerName: "dummycontroller", - BaseConfig: dummy.SampleConfig(), - CloudName: "dummy", - AdminSecret: "admin-secret", - }, - ) - c.Assert(err, jc.ErrorIsNil) - - s.InitialConfig = testing.CustomModelConfig(c, env.Config().AllAttrs()) + s.InitialConfig = testing.CustomModelConfig(c, dummy.SampleConfig()) s.StateSuite.SetUpTest(c) } @@ -98,139 +80,83 @@ c.Assert(dbConfig.Name(), gc.Equals, "new-model") } -func (s *ImportSuite) TestUploadBinariesTools(c *gc.C) { - // Create a model that has three different tools versions: - // one for a machine, one for a container, and one for a unit agent. - // We don't care about the actual validity of the model (it isn't). - model := description.NewModel(description.ModelArgs{ - Owner: names.NewUserTag("me"), - }) - machine := model.AddMachine(description.MachineArgs{ - Id: names.NewMachineTag("0"), - }) - machine.SetTools(description.AgentToolsArgs{ - Version: version.MustParseBinary("2.0.1-trusty-amd64"), - }) - container := machine.AddContainer(description.MachineArgs{ - Id: names.NewMachineTag("0/lxd/0"), - }) - container.SetTools(description.AgentToolsArgs{ - Version: version.MustParseBinary("2.0.5-trusty-amd64"), - }) - application := model.AddApplication(description.ApplicationArgs{ - Tag: names.NewApplicationTag("magic"), - CharmURL: "local:trusty/magic", - }) - unit := application.AddUnit(description.UnitArgs{ - Tag: names.NewUnitTag("magic/0"), - }) - unit.SetTools(description.AgentToolsArgs{ - Version: version.MustParseBinary("2.0.3-trusty-amd64"), - }) +func (s *ImportSuite) TestUploadBinariesConfigValidate(c *gc.C) { + type T migration.UploadBinariesConfig // alias for brevity - uploader := &fakeUploader{tools: make(map[version.Binary]string)} - config := migration.UploadBinariesConfig{ - State: &fakeStateStorage{}, - Model: model, - Target: &fakeAPIConnection{}, - GetCharmUploader: func(api.Connection) migration.CharmUploader { return &noOpUploader{} }, - GetToolsUploader: func(target api.Connection) migration.ToolsUploader { - return uploader - }, - GetStateStorage: func(migration.UploadBackend) storage.Storage { return &fakeCharmsStorage{} }, - GetCharmStoragePath: func(migration.UploadBackend, *charm.URL) (string, error) { return "", nil }, + check := func(modify func(*T), missing string) { + config := T{ + CharmDownloader: struct{ migration.CharmDownloader }{}, + CharmUploader: struct{ migration.CharmUploader }{}, + ToolsDownloader: struct{ migration.ToolsDownloader }{}, + ToolsUploader: struct{ migration.ToolsUploader }{}, + } + modify(&config) + realConfig := migration.UploadBinariesConfig(config) + c.Check(realConfig.Validate(), gc.ErrorMatches, fmt.Sprintf("missing %s not valid", missing)) } - err := migration.UploadBinaries(config) - c.Assert(err, jc.ErrorIsNil) - c.Assert(uploader.tools, jc.DeepEquals, map[version.Binary]string{ - version.MustParseBinary("2.0.1-trusty-amd64"): "fake tools 2.0.1-trusty-amd64", - version.MustParseBinary("2.0.3-trusty-amd64"): "fake tools 2.0.3-trusty-amd64", - version.MustParseBinary("2.0.5-trusty-amd64"): "fake tools 2.0.5-trusty-amd64", - }) + check(func(c *T) { c.CharmDownloader = nil }, "CharmDownloader") + check(func(c *T) { c.CharmUploader = nil }, "CharmUploader") + check(func(c *T) { c.ToolsDownloader = nil }, "ToolsDownloader") + check(func(c *T) { c.ToolsUploader = nil }, "ToolsUploader") } -func (s *ImportSuite) TestStreamCharmsTools(c *gc.C) { - model := description.NewModel(description.ModelArgs{ - Owner: names.NewUserTag("me"), - }) - model.AddApplication(description.ApplicationArgs{ - Tag: names.NewApplicationTag("magic"), - CharmURL: "local:trusty/magic", - }) - model.AddApplication(description.ApplicationArgs{ - Tag: names.NewApplicationTag("magic"), - CharmURL: "cs:trusty/postgresql-42", - }) +func (s *ImportSuite) TestBinariesMigration(c *gc.C) { + downloader := &fakeDownloader{} + uploader := &fakeUploader{ + charms: make(map[string]string), + tools: make(map[version.Binary]string), + } - uploader := &fakeUploader{charms: make(map[string]string)} + toolsMap := map[version.Binary]string{ + version.MustParseBinary("2.1.0-trusty-amd64"): "/tools/0", + version.MustParseBinary("2.0.0-xenial-amd64"): "/tools/1", + } config := migration.UploadBinariesConfig{ - State: &fakeStateStorage{}, - Model: model, - Target: &fakeAPIConnection{}, - GetCharmUploader: func(api.Connection) migration.CharmUploader { return uploader }, - GetToolsUploader: func(target api.Connection) migration.ToolsUploader { return &noOpUploader{} }, - GetStateStorage: func(migration.UploadBackend) storage.Storage { return &fakeCharmsStorage{} }, - GetCharmStoragePath: func(_ migration.UploadBackend, u *charm.URL) (string, error) { - return "/path/for/" + u.String(), nil - }, + Charms: []string{"local:trusty/magic", "cs:trusty/postgresql-42"}, + CharmDownloader: downloader, + CharmUploader: uploader, + Tools: toolsMap, + ToolsDownloader: downloader, + ToolsUploader: uploader, } err := migration.UploadBinaries(config) c.Assert(err, jc.ErrorIsNil) + c.Assert(downloader.charms, jc.DeepEquals, []string{ + "local:trusty/magic", + "cs:trusty/postgresql-42", + }) c.Assert(uploader.charms, jc.DeepEquals, map[string]string{ - "local:trusty/magic": "fake file at /path/for/local:trusty/magic", - "cs:trusty/postgresql-42": "fake file at /path/for/cs:trusty/postgresql-42", + "local:trusty/magic": "local:trusty/magic content", + "cs:trusty/postgresql-42": "cs:trusty/postgresql-42 content", }) + c.Assert(downloader.uris, jc.SameContents, []string{ + "/tools/0", + "/tools/1", + }) + c.Assert(uploader.tools, jc.DeepEquals, toolsMap) } -type fakeStateStorage struct { - tools fakeToolsStorage - charms fakeCharmsStorage -} - -type fakeCharmsStorage struct { - storage.Storage -} - -type fakeAPIConnection struct { - api.Connection -} - -type fakeToolsStorage struct { - binarystorage.Storage - closed bool -} - -func (f *fakeStateStorage) ToolsStorage() (binarystorage.StorageCloser, error) { - return &f.tools, nil -} - -func (f *fakeStateStorage) ModelUUID() string { - return testing.ModelTag.Id() -} - -func (f *fakeStateStorage) MongoSession() *mgo.Session { - return nil -} - -func (f *fakeStateStorage) Charm(*charm.URL) (*state.Charm, error) { - return nil, nil -} - -func (f *fakeToolsStorage) Open(v string) (binarystorage.Metadata, io.ReadCloser, error) { - buff := bytes.NewBufferString(fmt.Sprintf("fake tools %s", v)) - return binarystorage.Metadata{}, ioutil.NopCloser(buff), nil +type fakeDownloader struct { + charms []string + uris []string } -func (f *fakeToolsStorage) Close() error { - f.closed = true - return nil +func (d *fakeDownloader) OpenCharm(curl *charm.URL) (io.ReadCloser, error) { + urlStr := curl.String() + d.charms = append(d.charms, urlStr) + // Return the charm URL string as the fake charm content + return ioutil.NopCloser(bytes.NewReader([]byte(urlStr + " content"))), nil } -func (f *fakeCharmsStorage) Get(path string) (io.ReadCloser, int64, error) { - buff := bytes.NewBufferString(fmt.Sprintf("fake file at %s", path)) - return ioutil.NopCloser(buff), int64(buff.Len()), nil +func (d *fakeDownloader) OpenURI(uri string, query url.Values) (io.ReadCloser, error) { + if query != nil { + panic("query should be empty") + } + d.uris = append(d.uris, uri) + // Return the URI string as fake content + return ioutil.NopCloser(bytes.NewReader([]byte(uri))), nil } type fakeUploader struct { @@ -243,13 +169,8 @@ if err != nil { return nil, errors.Trace(err) } - f.tools[v] = string(data) - - uploaded := &tools.Tools{ - Version: v, - } - return tools.List{uploaded}, nil + return tools.List{&tools.Tools{Version: v}}, nil } func (f *fakeUploader) UploadCharm(u *charm.URL, r io.ReadSeeker) (*charm.URL, error) { @@ -262,16 +183,6 @@ return u, nil } -type noOpUploader struct{} - -func (*noOpUploader) UploadCharm(*charm.URL, io.ReadSeeker) (*charm.URL, error) { - return nil, nil -} - -func (*noOpUploader) UploadTools(io.ReadSeeker, version.Binary, ...string) (tools.List, error) { - return nil, nil -} - type ExportSuite struct { statetesting.StateSuite } @@ -326,16 +237,28 @@ return f.cleanupNeeded, f.cleanupError } -type CharmInternalSuite struct { - statetesting.StateSuite +type InternalSuite struct { + testing.BaseSuite } -var _ = gc.Suite(&CharmInternalSuite{}) +var _ = gc.Suite(&InternalSuite{}) -func (s *CharmInternalSuite) TestCharmStoragePath(c *gc.C) { - charm := s.Factory.MakeCharm(c, nil) +type stateGetter struct { + cfg *config.Config +} - path, err := migration.GetCharmStoragePath(s.State, charm.URL()) - c.Assert(err, jc.ErrorIsNil) - c.Assert(path, gc.Equals, "fake-storage-path") +func (e *stateGetter) Model() (*state.Model, error) { + return &state.Model{}, nil +} + +func (s *stateGetter) ModelConfig() (*config.Config, error) { + return s.cfg, nil +} + +func (s *stateGetter) ControllerConfig() (controller.Config, error) { + return map[string]interface{}{ + controller.ControllerUUIDKey: testing.ModelTag.Id(), + controller.CACertKey: testing.CACert, + controller.ApiPort: 4321, + }, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/mongo/mongo_test.go juju-core-2.0~beta15/src/github.com/juju/juju/mongo/mongo_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/mongo/mongo_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/mongo/mongo_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -476,7 +476,7 @@ s.patchSeries(test.series) var tw loggo.TestWriter - c.Assert(loggo.RegisterWriter("mongosuite", &tw, loggo.INFO), jc.ErrorIsNil) + c.Assert(loggo.RegisterWriter("mongosuite", &tw), jc.ErrorIsNil) defer loggo.RemoveWriter("mongosuite") err := mongo.EnsureServer(makeEnsureServerParams(dataDir)) @@ -534,7 +534,8 @@ s.data.SetStatus(mongo.ServiceName, "installed") var tw loggo.TestWriter - c.Assert(loggo.RegisterWriter("test-writer", &tw, loggo.ERROR), jc.ErrorIsNil) + writer := loggo.NewMinimumLevelWriter(&tw, loggo.ERROR) + c.Assert(loggo.RegisterWriter("test-writer", writer), jc.ErrorIsNil) defer loggo.RemoveWriter("test-writer") dataDir := c.MkDir() diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/patches/001-mgo.v2-issue-277-fix.diff juju-core-2.0~beta15/src/github.com/juju/juju/patches/001-mgo.v2-issue-277-fix.diff --- juju-core-2.0~beta12/src/github.com/juju/juju/patches/001-mgo.v2-issue-277-fix.diff 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/patches/001-mgo.v2-issue-277-fix.diff 2016-08-16 08:56:25.000000000 +0000 @@ -1,28 +1,71 @@ diff --git a/session.go b/session.go -index a8ad115..0949359 100644 +index a8ad115..75cb838 100644 + This applies the minimal changes to fix the mgo duplicate key error, -see https://github.com/go-mgo/mgo/pull/291 +see https://github.com/go-mgo/mgo/pull/291 and https://github.com/go-mgo/mgo/pull/302 + +It also includes logging so we can see that the patch is applied. + +Diff on github: https://github.com/go-mgo/mgo/compare/v2...babbageclunk:fix-277-v2-minimal?expand=1 +Generated with "git diff v2..fix-277-v2-minimal" + +Apply from $GOPATH/src with: patch -p1 < github.com/juju/juju/patches/001-mgo.v2-issue-277-fix.diff + --- a/gopkg.in/mgo.v2/session.go +++ b/gopkg.in/mgo.v2/session.go -@@ -146,7 +146,10 @@ var ( +@@ -41,6 +41,7 @@ import ( + "sync" + "time" + ++ "github.com/juju/loggo" + "gopkg.in/mgo.v2/bson" + ) + +@@ -144,9 +145,18 @@ type Iter struct { + var ( + ErrNotFound = errors.New("not found") ErrCursor = errors.New("invalid cursor") ++ ++ logPatchedOnce sync.Once ++ logger = loggo.GetLogger("mgo") ) -const defaultPrefetch = 0.25 +const ( -+ defaultPrefetch = 0.25 ++ defaultPrefetch = 0.25 ++ ++ // How many times we will retry an upsert if it produces duplicate ++ // key errors. + maxUpsertRetries = 5 +) // Dial establishes a new session to the cluster identified by the given seed // server(s). The session will enable communication with all of the servers in -@@ -2478,7 +2481,15 @@ func (c *Collection) Upsert(selector interface{}, update interface{}) (info *Cha +@@ -410,6 +420,16 @@ func (addr *ServerAddr) TCPAddr() *net.TCPAddr { + + // DialWithInfo establishes a new session to the cluster identified by info. + func DialWithInfo(info *DialInfo) (*Session, error) { ++ // This is using loggo because that can be done here in a ++ // localised patch, while using mgo's logging would need a change ++ // in Juju to call mgo.SetLogger. It's in this short-lived patch ++ // as a stop-gap because it's proving difficult to tell if the ++ // patch is applied in a running system. If you see it in ++ // committed code then something has gone very awry - please ++ // complain loudly! (babbageclunk) ++ logPatchedOnce.Do(func() { ++ logger.Debugf("duplicate key error patch applied") ++ }) + addrs := make([]string, len(info.Addrs)) + for i, addr := range info.Addrs { + p := strings.LastIndexAny(addr, "]:") +@@ -2478,7 +2498,16 @@ func (c *Collection) Upsert(selector interface{}, update interface{}) (info *Cha Flags: 1, Upsert: true, } - lerr, err := c.writeOp(&op, true) + var lerr *LastError -+ for i := 0; i < maxUpsertRetries; i++ { ++ // <= to allow for the first attempt (not a retry). ++ for i := 0; i <= maxUpsertRetries; i++ { + lerr, err = c.writeOp(&op, true) + // Retry duplicate key errors on upserts. + // https://docs.mongodb.com/v3.2/reference/method/db.collection.update/#use-unique-indexes @@ -33,23 +76,31 @@ if err == nil && lerr != nil { info = &ChangeInfo{} if lerr.UpdatedExisting { -@@ -4208,8 +4219,17 @@ func (q *Query) Apply(change Change, result interface{}) (info *ChangeInfo, err +@@ -4208,13 +4237,22 @@ func (q *Query) Apply(change Change, result interface{}) (info *ChangeInfo, err session.SetMode(Strong, false) var doc valueResult - err = session.DB(dbname).Run(&cmd, &doc) - if err != nil { -+ for i := 0; i < maxUpsertRetries; i++ { +- if qerr, ok := err.(*QueryError); ok && qerr.Message == "No matching object found" { +- return nil, ErrNotFound ++ for retries := 0; ; retries++ { + err = session.DB(dbname).Run(&cmd, &doc) -+ -+ if err == nil { -+ break -+ } -+ if change.Upsert && IsDup(err) { -+ // Retry duplicate key errors on upserts. -+ // https://docs.mongodb.com/v3.2/reference/method/db.collection.update/#use-unique-indexes -+ continue -+ } - if qerr, ok := err.(*QueryError); ok && qerr.Message == "No matching object found" { - return nil, ErrNotFound ++ if err != nil { ++ if qerr, ok := err.(*QueryError); ok && qerr.Message == "No matching object found" { ++ return nil, ErrNotFound ++ } ++ if change.Upsert && IsDup(err) && retries < maxUpsertRetries { ++ // Retry duplicate key errors on upserts. ++ // https://docs.mongodb.com/v3.2/reference/method/db.collection.update/#use-unique-indexes ++ continue ++ } ++ return nil, err } +- return nil, err ++ break // No error, so don't retry. + } ++ + if doc.LastError.N == 0 { + return nil, ErrNotFound + } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/payload/persistence/base_test.go juju-core-2.0~beta15/src/github.com/juju/juju/payload/persistence/base_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/payload/persistence/base_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/payload/persistence/base_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,149 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package persistence - -import ( - "fmt" - "reflect" - - "github.com/juju/errors" - "github.com/juju/testing" - jujutxn "github.com/juju/txn" - "github.com/juju/utils" - gc "gopkg.in/check.v1" - "gopkg.in/juju/charm.v6-unstable" - - "github.com/juju/juju/payload" -) - -type PayloadPersistenceFixture struct { - Stub *testing.Stub - DB *StubPersistenceBase - Queries payloadsQueries - StateID string -} - -func NewPayloadPersistenceFixture() *PayloadPersistenceFixture { - stub := &testing.Stub{} - db := &StubPersistenceBase{Stub: stub} - return &PayloadPersistenceFixture{ - Stub: stub, - DB: db, - Queries: payloadsQueries{db}, - StateID: "f47ac10b-58cc-4372-a567-0e02b2c3d479", - } -} - -func (f PayloadPersistenceFixture) NewPersistence() *Persistence { - return NewPersistence(f.DB) -} - -func (f PayloadPersistenceFixture) NewPayload(machine, unit, pType string, id string) payload.FullPayloadInfo { - name, pluginID := payload.ParseID(id) - if pluginID == "" { - pluginID = fmt.Sprintf("%s-%s", name, utils.MustNewUUID()) - } - - return payload.FullPayloadInfo{ - Payload: payload.Payload{ - PayloadClass: charm.PayloadClass{ - Name: name, - Type: pType, - }, - ID: pluginID, - Status: "running", - Unit: unit, - }, - Machine: machine, - } -} - -func (f PayloadPersistenceFixture) NewPayloads(machine, unit, pType string, ids ...string) []payload.FullPayloadInfo { - var payloads []payload.FullPayloadInfo - for _, id := range ids { - pl := f.NewPayload(machine, unit, pType, id) - payloads = append(payloads, pl) - } - return payloads -} - -func (f PayloadPersistenceFixture) SetDocs(payloads ...payload.FullPayloadInfo) { - f.DB.SetDocs(payloads...) -} - -func (f PayloadPersistenceFixture) CheckPayloads(c *gc.C, payloads []payload.FullPayloadInfo, expectedList ...payload.FullPayloadInfo) { - remainder := make([]payload.FullPayloadInfo, len(payloads)) - copy(remainder, payloads) - var noMatch []payload.FullPayloadInfo - for _, expected := range expectedList { - found := false - for i, payload := range remainder { - if reflect.DeepEqual(payload, expected) { - remainder = append(remainder[:i], remainder[i+1:]...) - found = true - break - } - } - if !found { - noMatch = append(noMatch, expected) - } - } - - ok1 := c.Check(noMatch, gc.HasLen, 0) - ok2 := c.Check(remainder, gc.HasLen, 0) - if !ok1 || !ok2 { - c.Logf("<<<<<<<<\nexpected:") - for _, payload := range expectedList { - c.Logf("%#v", payload) - } - c.Logf("--------\ngot:") - for _, payload := range payloads { - c.Logf("%#v", payload) - } - c.Logf(">>>>>>>>") - } -} - -type StubPersistenceBase struct { - *testing.Stub - - ReturnAll []*payloadDoc -} - -func (s *StubPersistenceBase) AddDoc(pl payload.FullPayloadInfo) { - doc := newPayloadDoc(pl) - s.ReturnAll = append(s.ReturnAll, doc) -} - -func (s *StubPersistenceBase) SetDocs(payloads ...payload.FullPayloadInfo) { - docs := make([]*payloadDoc, len(payloads)) - for i, pl := range payloads { - docs[i] = newPayloadDoc(pl) - } - s.ReturnAll = docs -} - -func (s *StubPersistenceBase) All(collName string, query, docs interface{}) error { - s.AddCall("All", collName, query, docs) - if err := s.NextErr(); err != nil { - return errors.Trace(err) - } - - actual := docs.(*[]payloadDoc) - var copied []payloadDoc - for _, doc := range s.ReturnAll { - copied = append(copied, *doc) - } - *actual = copied - return nil -} - -func (s *StubPersistenceBase) Run(transactions jujutxn.TransactionSource) error { - s.AddCall("Run", transactions) - if err := s.NextErr(); err != nil { - return errors.Trace(err) - } - - return nil -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/payload/persistence/mongo.go juju-core-2.0~beta15/src/github.com/juju/juju/payload/persistence/mongo.go --- juju-core-2.0~beta12/src/github.com/juju/juju/payload/persistence/mongo.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/payload/persistence/mongo.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,107 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package persistence - -import ( - "fmt" - - "gopkg.in/juju/charm.v6-unstable" - "gopkg.in/mgo.v2/bson" - - "github.com/juju/juju/payload" -) - -const ( - payloadsC = "payloads" -) - -func payloadID(unit, name string) string { - return fmt.Sprintf("payload#%s#%s", unit, name) -} - -func payloadIDQuery(unit, name string) bson.D { - id := payloadID(unit, name) - return bson.D{{"_id", id}} -} - -// payloadDoc is the top-level document for payloads. -type payloadDoc struct { - DocID string `bson:"_id"` - - // UnitID and Name are encoded in DocID. - UnitID string `bson:"unitid"` - Name string `bson:"name"` - - MachineID string `bson:"machine-id"` - - Type string `bson:"type"` - - // TODO(ericsnow) Store status in the "statuses" collection? - - State string `bson:"state"` - - // TODO(ericsnow) Store labels in the "annotations" collection? - - Labels []string `bson:"labels"` - - RawID string `bson:"rawid"` -} - -func newPayloadDoc(p payload.FullPayloadInfo) *payloadDoc { - id := payloadID(p.Unit, p.Name) - - definition := p.PayloadClass - - labels := make([]string, len(p.Labels)) - copy(labels, p.Labels) - - return &payloadDoc{ - DocID: id, - UnitID: p.Unit, - Name: definition.Name, - - MachineID: p.Machine, - - Type: definition.Type, - - State: p.Status, - - Labels: labels, - - RawID: p.ID, - } -} - -func (d payloadDoc) payload() payload.FullPayloadInfo { - labels := make([]string, len(d.Labels)) - copy(labels, d.Labels) - p := payload.FullPayloadInfo{ - Payload: payload.Payload{ - PayloadClass: d.definition(), - ID: d.RawID, - Status: d.State, - Labels: labels, - Unit: d.UnitID, - }, - Machine: d.MachineID, - } - return p -} - -func (d payloadDoc) definition() charm.PayloadClass { - return charm.PayloadClass{ - Name: d.Name, - Type: d.Type, - } -} - -func (d payloadDoc) match(name, rawID string) bool { - if d.Name != name { - return false - } - if d.RawID != rawID { - return false - } - return true -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/payload/persistence/mongo_queries.go juju-core-2.0~beta15/src/github.com/juju/juju/payload/persistence/mongo_queries.go --- juju-core-2.0~beta12/src/github.com/juju/juju/payload/persistence/mongo_queries.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/payload/persistence/mongo_queries.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,90 +0,0 @@ -// Copyright 2016 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package persistence - -import ( - "github.com/juju/errors" - "gopkg.in/mgo.v2/bson" -) - -type payloadsDBQueryer interface { - // All populates docs with the list of the documents corresponding - // to the provided query. - All(collName string, query, docs interface{}) error -} - -type payloadsQueries struct { - querier payloadsDBQueryer -} - -func (pq payloadsQueries) one(query bson.D) (payloadDoc, error) { - var docs []payloadDoc - if err := pq.querier.All(payloadsC, query, &docs); err != nil { - return payloadDoc{}, errors.Trace(err) - } - if len(docs) > 1 { - return payloadDoc{}, errors.NewNotValid(nil, "query too broad, got more than one doc") - } - if len(docs) == 0 { - return payloadDoc{}, errors.NotFoundf("") - } - return docs[0], nil -} - -func (pq payloadsQueries) all(unit string) ([]payloadDoc, error) { - var docs []payloadDoc - var query bson.D - if unit != "" { - query = bson.D{{"unitid", unit}} - } - if err := pq.querier.All(payloadsC, query, &docs); err != nil { - return nil, errors.Trace(err) - } - return docs, nil -} - -func (pq payloadsQueries) allByName() (map[string]payloadDoc, error) { - docs, err := pq.all("") - if err != nil { - return nil, errors.Trace(err) - } - results := make(map[string]payloadDoc, len(docs)) - for _, doc := range docs { - results[doc.Name] = doc - } - return results, nil -} - -func (pq payloadsQueries) unitPayloadsByName(unit string) (map[string]payloadDoc, error) { - if unit == "" { - return nil, errors.NewNotValid(nil, "missing unit ID") - } - docs, err := pq.all(unit) - if err != nil { - return nil, errors.Trace(err) - } - results := make(map[string]payloadDoc) - for _, doc := range docs { - results[doc.Name] = doc - } - return results, nil -} - -func (pq payloadsQueries) someUnitPayloads(unit string, names []string) ([]payloadDoc, []string, error) { - all, err := pq.unitPayloadsByName(unit) - if err != nil { - return nil, nil, errors.Trace(err) - } - - var results []payloadDoc - var missing []string - for _, name := range names { - if doc, ok := all[name]; ok { - results = append(results, doc) - } else { - missing = append(missing, name) - } - } - return results, missing, nil -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/payload/persistence/mongo_test.go juju-core-2.0~beta15/src/github.com/juju/juju/payload/persistence/mongo_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/payload/persistence/mongo_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/payload/persistence/mongo_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,227 +0,0 @@ -// Copyright 2016 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package persistence - -import ( - "github.com/juju/errors" - "github.com/juju/testing" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - "gopkg.in/mgo.v2/bson" - "gopkg.in/mgo.v2/txn" - - "github.com/juju/juju/payload" -) - -// These tests are a low-level sanity check in support of more complete -// integration testing done in state/payloads_test.go. - -type PayloadsMongoSuite struct { - testing.IsolationSuite -} - -var _ = gc.Suite(&PayloadsMongoSuite{}) - -func (s *PayloadsMongoSuite) TestUpsertOpsMissing(c *gc.C) { - f := NewPayloadPersistenceFixture() - pl := f.NewPayload("0", "a-unit/0", "docker", "payloadA/payloadA-xyz") - utxn := upsertPayloadTxn{ - payload: pl, - exists: false, - } - - ops := utxn.ops() - - f.Stub.CheckNoCalls(c) - id := "payload#a-unit/0#payloadA" - c.Check(ops, jc.DeepEquals, []txn.Op{{ - C: "payloads", - Id: id, - Assert: txn.DocMissing, - Insert: &payloadDoc{ - DocID: id, - UnitID: "a-unit/0", - Name: "payloadA", - MachineID: "0", - Type: "docker", - RawID: "payloadA-xyz", - State: "running", - }, - }}) -} - -func (s *PayloadsMongoSuite) TestUpsertOpsExists(c *gc.C) { - f := NewPayloadPersistenceFixture() - pl := f.NewPayload("0", "a-unit/0", "docker", "payloadA/payloadA-xyz") - utxn := upsertPayloadTxn{ - payload: pl, - exists: true, - } - - ops := utxn.ops() - - f.Stub.CheckNoCalls(c) - id := "payload#a-unit/0#payloadA" - c.Check(ops, jc.DeepEquals, []txn.Op{{ - C: "payloads", - Id: id, - Assert: txn.DocExists, - Remove: true, - }, { - C: "payloads", - Id: id, - Assert: txn.DocMissing, - Insert: &payloadDoc{ - DocID: id, - UnitID: "a-unit/0", - Name: "payloadA", - MachineID: "0", - Type: "docker", - RawID: "payloadA-xyz", - State: "running", - }, - }}) -} - -func (s *PayloadsMongoSuite) TestUpsertCheckAssertsMissing(c *gc.C) { - f := NewPayloadPersistenceFixture() - pl := f.NewPayload("0", "a-unit/0", "docker", "payloadA/payloadA-xyz") - utxn := upsertPayloadTxn{ - payload: pl, - } - - err := utxn.checkAssertsAndUpdate(f.Queries) - c.Assert(err, jc.ErrorIsNil) - - f.Stub.CheckCallNames(c, "All") - c.Check(utxn.exists, jc.IsFalse) -} - -func (s *PayloadsMongoSuite) TestUpsertCheckAssertsWasFound(c *gc.C) { - f := NewPayloadPersistenceFixture() - pl := f.NewPayload("0", "a-unit/0", "docker", "payloadA/payloadA-xyz") - utxn := upsertPayloadTxn{ - payload: pl, - exists: true, - } - - err := utxn.checkAssertsAndUpdate(f.Queries) - c.Assert(err, jc.ErrorIsNil) - - f.Stub.CheckCallNames(c, "All") - c.Check(utxn.exists, jc.IsFalse) -} - -func (s *PayloadsMongoSuite) TestUpsertCheckAssertsAlreadyExists(c *gc.C) { - f := NewPayloadPersistenceFixture() - pl := f.NewPayload("0", "a-unit/0", "docker", "payloadA/payloadA-xyz") - f.SetDocs(pl) - utxn := upsertPayloadTxn{ - payload: pl, - } - - err := utxn.checkAssertsAndUpdate(f.Queries) - c.Assert(err, jc.ErrorIsNil) - - f.Stub.CheckCallNames(c, "All") - c.Check(utxn.exists, jc.IsTrue) -} - -func (s *PayloadsMongoSuite) TestUpsertCheckAssertsWasMissing(c *gc.C) { - f := NewPayloadPersistenceFixture() - pl := f.NewPayload("0", "a-unit/0", "docker", "payloadA/payloadA-xyz") - f.SetDocs(pl) - utxn := upsertPayloadTxn{ - payload: pl, - exists: false, - } - - err := utxn.checkAssertsAndUpdate(f.Queries) - c.Assert(err, jc.ErrorIsNil) - - f.Stub.CheckCallNames(c, "All") - c.Check(utxn.exists, jc.IsTrue) -} - -func (s *PayloadsMongoSuite) TestSetStatusOps(c *gc.C) { - f := NewPayloadPersistenceFixture() - pl := f.NewPayload("0", "a-unit/0", "docker", "payloadA/payloadA-xyz") - stxn := setPayloadStatusTxn{pl.Unit, pl.Name, payload.StateRunning} - - ops := stxn.ops() - - f.Stub.CheckNoCalls(c) - id := "payload#a-unit/0#payloadA" - c.Check(ops, jc.DeepEquals, []txn.Op{{ - C: "payloads", - Id: id, - Assert: txn.DocExists, - Update: bson.D{ - {"$set", bson.D{ - {"state", payload.StateRunning}, - }}, - }, - }}) -} - -func (s *PayloadsMongoSuite) TestSetStatusCheckAssertsExists(c *gc.C) { - f := NewPayloadPersistenceFixture() - pl := f.NewPayload("0", "a-unit/0", "docker", "payloadA/payloadA-xyz") - f.SetDocs(pl) - stxn := setPayloadStatusTxn{pl.Unit, pl.Name, payload.StateRunning} - - err := stxn.checkAssertsAndUpdate(f.Queries) - c.Assert(err, jc.ErrorIsNil) - - f.Stub.CheckCallNames(c, "All") -} - -func (s *PayloadsMongoSuite) TestSetStatusCheckAssertsMissing(c *gc.C) { - f := NewPayloadPersistenceFixture() - stxn := setPayloadStatusTxn{"a-unit/0", "", payload.StateRunning} - - err := stxn.checkAssertsAndUpdate(f.Queries) - - f.Stub.CheckCallNames(c, "All") - c.Check(errors.Cause(err), gc.Equals, payload.ErrNotFound) -} - -func (s *PayloadsMongoSuite) TestRemoveOps(c *gc.C) { - f := NewPayloadPersistenceFixture() - pl := f.NewPayload("0", "a-unit/0", "docker", "payloadA/xyz") - rtxn := removePayloadTxn{pl.Unit, pl.Name} - - ops := rtxn.ops() - - f.Stub.CheckNoCalls(c) - id := "payload#a-unit/0#payloadA" - c.Check(ops, jc.DeepEquals, []txn.Op{{ - C: "payloads", - Id: id, - Assert: txn.DocExists, - Remove: true, - }}) -} - -func (s *PayloadsMongoSuite) TestRemoveCheckAssertsExists(c *gc.C) { - f := NewPayloadPersistenceFixture() - pl := f.NewPayload("0", "a-unit/0", "docker", "payloadA/xyz") - f.SetDocs(pl) - rtxn := removePayloadTxn{pl.Unit, pl.Name} - - err := rtxn.checkAssertsAndUpdate(f.Queries) - c.Assert(err, jc.ErrorIsNil) - - f.Stub.CheckCallNames(c, "All") -} - -func (s *PayloadsMongoSuite) TestRemoveCheckAssertsMissing(c *gc.C) { - f := NewPayloadPersistenceFixture() - rtxn := removePayloadTxn{"a-unit/0", "payloadA"} - - err := rtxn.checkAssertsAndUpdate(f.Queries) - - f.Stub.CheckCallNames(c, "All") - c.Check(errors.Cause(err), gc.Equals, payload.ErrNotFound) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/payload/persistence/mongo_txn.go juju-core-2.0~beta15/src/github.com/juju/juju/payload/persistence/mongo_txn.go --- juju-core-2.0~beta12/src/github.com/juju/juju/payload/persistence/mongo_txn.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/payload/persistence/mongo_txn.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,153 +0,0 @@ -// Copyright 2016 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package persistence - -import ( - "github.com/juju/errors" - jujutxn "github.com/juju/txn" - "gopkg.in/mgo.v2/bson" - "gopkg.in/mgo.v2/txn" - - "github.com/juju/juju/payload" -) - -type payloadsTxnRunner interface { - // Run runs the transaction generated by the provided factory - // function. It may be retried several times. - Run(transactions jujutxn.TransactionSource) error -} - -type payloadsTransaction interface { - checkAssertsAndUpdate(pq payloadsQueries) error - ops() []txn.Op -} - -type payloadsTransactions struct { - queries payloadsQueries - runner payloadsTxnRunner -} - -func (pt payloadsTransactions) run(ptxn payloadsTransaction) error { - buildTxn := pt.newTxnSource(ptxn) - if err := pt.runner.Run(buildTxn); err != nil { - return errors.Trace(err) - } - return nil -} - -func (pt payloadsTransactions) newTxnSource(ptxn payloadsTransaction) jujutxn.TransactionSource { - return func(attempt int) ([]txn.Op, error) { - // We always check the asserts manually before returning ops. - if err := ptxn.checkAssertsAndUpdate(pt.queries); err != nil { - return nil, errors.Trace(err) - } - // If the asserts check out then it was probably a transient - // error, so try again. - - return ptxn.ops(), nil - } -} - -type upsertPayloadTxn struct { - payload payload.FullPayloadInfo - - exists bool -} - -func (utxn *upsertPayloadTxn) checkAssertsAndUpdate(pq payloadsQueries) error { - utxn.exists = false - query := payloadIDQuery(utxn.payload.Unit, utxn.payload.Name) - _, err := pq.one(query) - if err == nil { - utxn.exists = true - } else if !errors.IsNotFound(err) { - return errors.Trace(err) - } - - return nil -} - -func (utxn upsertPayloadTxn) ops() []txn.Op { - doc := newPayloadDoc(utxn.payload) - var ops []txn.Op - if utxn.exists { - ops = append(ops, txn.Op{ - C: payloadsC, - Id: doc.DocID, - Assert: txn.DocExists, - Remove: true, - }) - } - // TODO(ericsnow) Add unitPersistence.newEnsureAliveOp(pp.unit)? - return append(ops, txn.Op{ - C: payloadsC, - Id: doc.DocID, - Assert: txn.DocMissing, - Insert: doc, - }) -} - -type setPayloadStatusTxn struct { - unit string - name string - status string -} - -func (stxn *setPayloadStatusTxn) checkAssertsAndUpdate(pq payloadsQueries) error { - query := payloadIDQuery(stxn.unit, stxn.name) - _, err := pq.one(query) - if errors.IsNotFound(err) { - return errors.Annotatef(payload.ErrNotFound, "(%s)", stxn.name) - } - if err != nil { - return errors.Trace(err) - } - - return nil -} - -func (stxn setPayloadStatusTxn) ops() []txn.Op { - id := payloadID(stxn.unit, stxn.name) - updates := bson.D{ - {"state", stxn.status}, - } - // TODO(ericsnow) Add unitPersistence.newEnsureAliveOp(pp.unit)? - return []txn.Op{{ - C: payloadsC, - Id: id, - Assert: txn.DocExists, - Update: bson.D{{"$set", updates}}, - }} -} - -type removePayloadTxn struct { - unit string - name string -} - -func (rtxn *removePayloadTxn) checkAssertsAndUpdate(pq payloadsQueries) error { - query := payloadIDQuery(rtxn.unit, rtxn.name) - _, err := pq.one(query) - if errors.IsNotFound(err) { - // Must have already beeen removed! The business logic - // (i.e. state) can decide whether or not to ignore this. - return errors.Annotatef(payload.ErrNotFound, "(%s)", rtxn.name) - } - if err != nil { - return errors.Trace(err) - } - - return nil -} - -func (rtxn removePayloadTxn) ops() []txn.Op { - id := payloadID(rtxn.unit, rtxn.name) - // TODO(ericsnow) Add unitPersistence.newEnsureAliveOp(pp.unit)? - return []txn.Op{{ - C: payloadsC, - Id: id, - Assert: txn.DocExists, - Remove: true, - }} -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/payload/persistence/package_test.go juju-core-2.0~beta15/src/github.com/juju/juju/payload/persistence/package_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/payload/persistence/package_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/payload/persistence/package_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,14 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package persistence - -import ( - "testing" - - gc "gopkg.in/check.v1" -) - -func Test(t *testing.T) { - gc.TestingT(t) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/payload/persistence/persistence.go juju-core-2.0~beta15/src/github.com/juju/juju/payload/persistence/persistence.go --- juju-core-2.0~beta12/src/github.com/juju/juju/payload/persistence/persistence.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/payload/persistence/persistence.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,145 +0,0 @@ -// Copyright 2016 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package persistence - -import ( - "github.com/juju/errors" - "github.com/juju/loggo" - - "github.com/juju/juju/payload" -) - -var logger = loggo.GetLogger("juju.payload.persistence") - -// TODO(ericsnow) Store status in the status collection? - -// TODO(ericsnow) Move PersistenceBase to the components package? - -// PersistenceBase exposes the core persistence functionality needed -// for payloads. -type PersistenceBase interface { - payloadsDBQueryer - payloadsTxnRunner -} - -// UnitPersistence exposes the high-level persistence functionality -// related to payloads in Juju. -type Persistence struct { - queries payloadsQueries - txns payloadsTransactions -} - -// NewPersistence wraps the "db" in a new Persistence. -func NewPersistence(db PersistenceBase) *Persistence { - queries := payloadsQueries{ - querier: db, - } - return &Persistence{ - queries: queries, - txns: payloadsTransactions{ - queries: queries, - runner: db, - }, - } -} - -// ListAll returns the list of all payloads in the model. -func (pp *Persistence) ListAll() ([]payload.FullPayloadInfo, error) { - logger.Tracef("listing all payloads") - - docs, err := pp.queries.all("") - if err != nil { - return nil, errors.Trace(err) - } - - fullPayloads := make([]payload.FullPayloadInfo, 0, len(docs)) - for _, doc := range docs { - p := doc.payload() - fullPayloads = append(fullPayloads, p) - } - return fullPayloads, nil -} - -// Track adds records for the payload to persistence. If the payload -// is already there then it is replaced with the new one. -func (pp Persistence) Track(pl payload.FullPayloadInfo) error { - logger.Tracef("inserting %q - %#v", pl.Name, pl) - txn := &upsertPayloadTxn{ - payload: pl, - } - if err := pp.txns.run(txn); err != nil { - return errors.Trace(err) - } - return nil -} - -// SetStatus updates the raw status for the identified payload in -// persistence. If the payload is not there then payload.ErrNotFound -// is returned. -func (pp Persistence) SetStatus(unit, name, status string) error { - logger.Tracef("setting status for %q", name) - txn := &setPayloadStatusTxn{ - unit: unit, - name: name, - status: status, - } - if err := pp.txns.run(txn); err != nil { - return errors.Trace(err) - } - return nil -} - -// List builds the list of payloads found in persistence which match -// the provided IDs. The lists of IDs with missing records is also -// returned. -func (pp Persistence) List(unit string, names ...string) ([]payload.FullPayloadInfo, []string, error) { - // TODO(ericsnow) Ensure that the unit is Alive? - - docs, missing, err := pp.queries.someUnitPayloads(unit, names) - if err != nil { - return nil, nil, errors.Trace(err) - } - - results := make([]payload.FullPayloadInfo, len(docs)) - for i, doc := range docs { - results[i] = doc.payload() - } - return results, missing, nil -} - -// ListAllForUnit builds the list of all payloads found in persistence. -func (pp Persistence) ListAllForUnit(unit string) ([]payload.FullPayloadInfo, error) { - // TODO(ericsnow) Ensure that the unit is Alive? - - docs, err := pp.queries.unitPayloadsByName(unit) - if err != nil { - return nil, errors.Trace(err) - } - - results := make([]payload.FullPayloadInfo, 0, len(docs)) - for _, doc := range docs { - p := doc.payload() - results = append(results, p) - } - return results, nil -} - -// TODO(ericsnow) Add payloads to state/cleanup.go. - -// TODO(ericsnow) How to ensure they are completely removed from state -// (when you factor in status stored in a separate collection)? - -// Untrack removes all records associated with the identified payload -// from persistence. If the payload is not there then -// payload.ErrNotFound is returned. -func (pp Persistence) Untrack(unit, name string) error { - txn := &removePayloadTxn{ - unit: unit, - name: name, - } - if err := pp.txns.run(txn); err != nil { - return errors.Trace(err) - } - return nil -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/payload/persistence/persistence_test.go juju-core-2.0~beta15/src/github.com/juju/juju/payload/persistence/persistence_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/payload/persistence/persistence_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/payload/persistence/persistence_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,234 +0,0 @@ -// Copyright 2016 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package persistence_test - -import ( - "sort" - - "github.com/juju/errors" - "github.com/juju/testing" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/payload" - "github.com/juju/juju/payload/persistence" -) - -type PayloadsPersistenceSuite struct { - testing.IsolationSuite -} - -var _ = gc.Suite(&PayloadsPersistenceSuite{}) - -func (s *PayloadsPersistenceSuite) TestListAllOkay(c *gc.C) { - f := persistence.NewPayloadPersistenceFixture() - p1 := f.NewPayload("0", "a-unit/0", "docker", "spam/spam-xyz") - p1.Labels = []string{"a-tag"} - p2 := f.NewPayload("0", "a-unit/0", "docker", "eggs/eggs-xyz") - p2.Labels = []string{"a-tag"} - f.SetDocs(p1, p2) - persist := f.NewPersistence() - - payloads, err := persist.ListAll() - c.Assert(err, jc.ErrorIsNil) - - f.CheckPayloads(c, payloads, p1, p2) - f.Stub.CheckCallNames(c, "All") -} - -func (s *PayloadsPersistenceSuite) TestListAllEmpty(c *gc.C) { - f := persistence.NewPayloadPersistenceFixture() - persist := f.NewPersistence() - - payloads, err := persist.ListAll() - c.Assert(err, jc.ErrorIsNil) - - c.Check(payloads, gc.HasLen, 0) - f.Stub.CheckCallNames(c, "All") -} - -func (s *PayloadsPersistenceSuite) TestListAllFailed(c *gc.C) { - f := persistence.NewPayloadPersistenceFixture() - failure := errors.Errorf("") - f.Stub.SetErrors(failure) - persist := f.NewPersistence() - - _, err := persist.ListAll() - - c.Check(errors.Cause(err), gc.Equals, failure) -} - -func (s *PayloadsPersistenceSuite) TestTrackOkay(c *gc.C) { - f := persistence.NewPayloadPersistenceFixture() - pl := f.NewPayload("0", "a-unit/0", "docker", "payloadA/payloadA-xyz") - wp := f.NewPersistence() - - err := wp.Track(pl) - c.Assert(err, jc.ErrorIsNil) - - f.Stub.CheckCallNames(c, "Run") -} - -func (s *PayloadsPersistenceSuite) TestTrackFailed(c *gc.C) { - f := persistence.NewPayloadPersistenceFixture() - failure := errors.Errorf("") - f.Stub.SetErrors(failure) - pl := f.NewPayload("0", "a-unit/0", "docker", "payloadA") - pp := f.NewPersistence() - - err := pp.Track(pl) - - c.Check(errors.Cause(err), gc.Equals, failure) - f.Stub.CheckCallNames(c, "Run") -} - -func (s *PayloadsPersistenceSuite) TestSetStatusOkay(c *gc.C) { - f := persistence.NewPayloadPersistenceFixture() - pl := f.NewPayload("0", "a-unit/0", "docker", "payloadA/payloadA-xyz") - f.SetDocs(pl) - pp := f.NewPersistence() - - err := pp.SetStatus(pl.Unit, pl.Name, payload.StateRunning) - c.Assert(err, jc.ErrorIsNil) - - f.Stub.CheckCallNames(c, "Run") -} - -func (s *PayloadsPersistenceSuite) TestSetStatusFailed(c *gc.C) { - f := persistence.NewPayloadPersistenceFixture() - pl := f.NewPayload("0", "a-unit/0", "docker", "payloadA/payloadA-xyz") - f.SetDocs(pl) - failure := errors.Errorf("") - f.Stub.SetErrors(failure) - pp := f.NewPersistence() - - err := pp.SetStatus(pl.Unit, pl.Name, payload.StateRunning) - - c.Check(errors.Cause(err), gc.Equals, failure) - f.Stub.CheckCallNames(c, "Run") -} - -func (s *PayloadsPersistenceSuite) TestListOkay(c *gc.C) { - f := persistence.NewPayloadPersistenceFixture() - pl := f.NewPayload("0", "a-unit/0", "docker", "payloadA/xyz") - other := f.NewPayload("0", "a-unit/0", "docker", "payloadB/abc") - f.SetDocs(pl, other) - pp := f.NewPersistence() - - payloads, missing, err := pp.List(pl.Unit, pl.Name) - c.Assert(err, jc.ErrorIsNil) - - f.Stub.CheckCallNames(c, "All") - c.Check(payloads, jc.DeepEquals, []payload.FullPayloadInfo{pl}) - c.Check(missing, gc.HasLen, 0) -} - -func (s *PayloadsPersistenceSuite) TestListSomeMissing(c *gc.C) { - f := persistence.NewPayloadPersistenceFixture() - pl := f.NewPayload("0", "a-unit/0", "docker", "payloadB/abc") - other := f.NewPayload("0", "a-unit/0", "docker", "payloadA/xyz") - f.SetDocs(pl, other) - missingName := "not-" + pl.Name - pp := f.NewPersistence() - - payloads, missing, err := pp.List(pl.Unit, pl.Name, missingName) - c.Assert(err, jc.ErrorIsNil) - - f.Stub.CheckCallNames(c, "All") - c.Check(payloads, jc.DeepEquals, []payload.FullPayloadInfo{pl}) - c.Check(missing, jc.DeepEquals, []string{missingName}) -} - -func (s *PayloadsPersistenceSuite) TestListEmpty(c *gc.C) { - name := "payloadA" - f := persistence.NewPayloadPersistenceFixture() - pp := f.NewPersistence() - - payloads, missing, err := pp.List("a-unit/0", name) - c.Assert(err, jc.ErrorIsNil) - - f.Stub.CheckCallNames(c, "All") - c.Check(payloads, gc.HasLen, 0) - c.Check(missing, jc.DeepEquals, []string{name}) -} - -func (s *PayloadsPersistenceSuite) TestListFailure(c *gc.C) { - f := persistence.NewPayloadPersistenceFixture() - failure := errors.Errorf("") - f.Stub.SetErrors(failure) - pp := f.NewPersistence() - - _, _, err := pp.List("a-unit/0") - - c.Check(errors.Cause(err), gc.Equals, failure) -} - -func (s *PayloadsPersistenceSuite) TestListAllForUnitOkay(c *gc.C) { - f := persistence.NewPayloadPersistenceFixture() - existing := f.NewPayloads("0", "a-unit/0", "docker", "payloadA/xyz", "payloadB/abc") - f.SetDocs(existing...) - pp := f.NewPersistence() - - payloads, err := pp.ListAllForUnit("a-unit/0") - c.Assert(err, jc.ErrorIsNil) - - f.Stub.CheckCallNames(c, "All") - sort.Sort(byName(payloads)) - sort.Sort(byName(existing)) - c.Check(payloads, jc.DeepEquals, existing) -} - -func (s *PayloadsPersistenceSuite) TestListAllForUnitEmpty(c *gc.C) { - f := persistence.NewPayloadPersistenceFixture() - pp := f.NewPersistence() - - payloads, err := pp.ListAllForUnit("a-unit/0") - c.Assert(err, jc.ErrorIsNil) - - f.Stub.CheckCallNames(c, "All") - c.Check(payloads, gc.HasLen, 0) -} - -type byName []payload.FullPayloadInfo - -func (b byName) Len() int { return len(b) } -func (b byName) Less(i, j int) bool { return b[i].FullID() < b[j].FullID() } -func (b byName) Swap(i, j int) { b[i], b[j] = b[j], b[i] } - -func (s *PayloadsPersistenceSuite) TestListAllForUnitFailed(c *gc.C) { - f := persistence.NewPayloadPersistenceFixture() - failure := errors.Errorf("") - f.Stub.SetErrors(failure) - pp := f.NewPersistence() - - _, err := pp.ListAllForUnit("a-unit/0") - - c.Check(errors.Cause(err), gc.Equals, failure) -} - -func (s *PayloadsPersistenceSuite) TestUntrackOkay(c *gc.C) { - f := persistence.NewPayloadPersistenceFixture() - pl := f.NewPayload("0", "a-unit/0", "docker", "payloadA/xyz") - f.SetDocs(pl) - pp := f.NewPersistence() - - err := pp.Untrack(pl.Unit, pl.Name) - c.Assert(err, jc.ErrorIsNil) - - f.Stub.CheckCallNames(c, "Run") -} - -func (s *PayloadsPersistenceSuite) TestUntrackFailed(c *gc.C) { - f := persistence.NewPayloadPersistenceFixture() - pl := f.NewPayload("0", "a-unit/0", "docker", "payloadA/xyz") - f.SetDocs(pl) - failure := errors.Errorf("") - f.Stub.SetErrors(failure) - pp := f.NewPersistence() - - err := pp.Untrack(pl.Unit, pl.Name) - - c.Check(errors.Cause(err), gc.Equals, failure) - f.Stub.CheckCallNames(c, "Run") -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/payload/persistence/unit.go juju-core-2.0~beta15/src/github.com/juju/juju/payload/persistence/unit.go --- juju-core-2.0~beta12/src/github.com/juju/juju/payload/persistence/unit.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/payload/persistence/unit.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,59 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package persistence - -import ( - "github.com/juju/juju/payload" -) - -// UnitPersistence exposes the high-level persistence functionality -// related to payloads in Juju. -type UnitPersistence struct { - pp *Persistence - unit string -} - -// NewUnitPersistence builds a new Persistence based on the provided info. -func NewUnitPersistence(pp *Persistence, unit string) *UnitPersistence { - return &UnitPersistence{ - pp: pp, - unit: unit, - } -} - -// Track adds records for the payload to persistence. If the payload -// is already there then false gets returned (true if inserted). -// Existing records are not checked for consistency. -func (up UnitPersistence) Track(pl payload.FullPayloadInfo) error { - return up.pp.Track(pl) -} - -// SetStatus updates the raw status for the identified payload in -// persistence. The return value corresponds to whether or not the -// record was found in persistence. Any other problem results in -// an error. The payload is not checked for inconsistent records. -func (up UnitPersistence) SetStatus(name, status string) error { - return up.pp.SetStatus(up.unit, name, status) -} - -// List builds the list of payloads found in persistence which match -// the provided IDs. The lists of IDs with missing records is also -// returned. -func (up UnitPersistence) List(names ...string) ([]payload.FullPayloadInfo, []string, error) { - return up.pp.List(up.unit, names...) -} - -// ListAll builds the list of all payloads found in persistence. -// Inconsistent records result in errors.NotValid. -func (up UnitPersistence) ListAll() ([]payload.FullPayloadInfo, error) { - return up.pp.ListAllForUnit(up.unit) -} - -// Untrack removes all records associated with the identified payload -// from persistence. Also returned is whether or not the payload was -// found. If the records for the payload are not consistent then -// errors.NotValid is returned. -func (up UnitPersistence) Untrack(name string) error { - return up.pp.Untrack(up.unit, name) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/payload/state/base_test.go juju-core-2.0~beta15/src/github.com/juju/juju/payload/state/base_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/payload/state/base_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/payload/state/base_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,144 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package state_test - -import ( - "fmt" - - "github.com/juju/errors" - gitjujutesting "github.com/juju/testing" - jc "github.com/juju/testing/checkers" - "github.com/juju/utils" - gc "gopkg.in/check.v1" - "gopkg.in/juju/charm.v6-unstable" - - "github.com/juju/juju/payload" - "github.com/juju/juju/testing" -) - -type basePayloadsSuite struct { - testing.BaseSuite - - stub *gitjujutesting.Stub - persist *fakePayloadsPersistence -} - -func (s *basePayloadsSuite) SetUpTest(c *gc.C) { - s.BaseSuite.SetUpTest(c) - - s.stub = &gitjujutesting.Stub{} - s.persist = &fakePayloadsPersistence{Stub: s.stub} -} - -func (s *basePayloadsSuite) newPayload(pType string, id string) payload.FullPayloadInfo { - name, rawID := payload.ParseID(id) - if rawID == "" { - rawID = fmt.Sprintf("%s-%s", name, utils.MustNewUUID()) - } - - return payload.FullPayloadInfo{ - Payload: payload.Payload{ - PayloadClass: charm.PayloadClass{ - Name: name, - Type: pType, - }, - Status: payload.StateRunning, - ID: rawID, - Unit: "a-application/0", - }, - Machine: "0", - } -} - -type fakePayloadsPersistence struct { - *gitjujutesting.Stub - payloads map[string]*payload.FullPayloadInfo -} - -func (s *fakePayloadsPersistence) checkPayload(c *gc.C, id string, expected payload.FullPayloadInfo) { - pl, ok := s.payloads[id] - if !ok { - c.Errorf("payload %q not found", id) - } else { - c.Check(pl, jc.DeepEquals, &expected) - } -} - -func (s *fakePayloadsPersistence) setPayload(pl *payload.FullPayloadInfo) { - if s.payloads == nil { - s.payloads = make(map[string]*payload.FullPayloadInfo) - } - s.payloads[pl.Name] = pl -} - -func (s *fakePayloadsPersistence) Track(pl payload.FullPayloadInfo) error { - s.AddCall("Track", pl) - if err := s.NextErr(); err != nil { - return errors.Trace(err) - } - - if _, ok := s.payloads[pl.Name]; ok { - return payload.ErrAlreadyExists - } - s.setPayload(&pl) - return nil -} - -func (s *fakePayloadsPersistence) SetStatus(id, status string) error { - s.AddCall("SetStatus", id, status) - if err := s.NextErr(); err != nil { - return errors.Trace(err) - } - - pl, ok := s.payloads[id] - if !ok { - return payload.ErrNotFound - } - pl.Status = status - return nil -} - -func (s *fakePayloadsPersistence) List(ids ...string) ([]payload.FullPayloadInfo, []string, error) { - s.AddCall("List", ids) - if err := s.NextErr(); err != nil { - return nil, nil, errors.Trace(err) - } - - var payloads []payload.FullPayloadInfo - var missing []string - for _, id := range ids { - if pl, ok := s.payloads[id]; !ok { - missing = append(missing, id) - } else { - payloads = append(payloads, *pl) - } - } - return payloads, missing, nil -} - -func (s *fakePayloadsPersistence) ListAll() ([]payload.FullPayloadInfo, error) { - s.AddCall("ListAll") - if err := s.NextErr(); err != nil { - return nil, errors.Trace(err) - } - - var payloads []payload.FullPayloadInfo - for _, pl := range s.payloads { - payloads = append(payloads, *pl) - } - return payloads, nil -} - -func (s *fakePayloadsPersistence) Untrack(id string) error { - s.AddCall("Untrack", id) - if err := s.NextErr(); err != nil { - return errors.Trace(err) - } - - if _, ok := s.payloads[id]; !ok { - return payload.ErrNotFound - } - delete(s.payloads, id) - return nil -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/payload/state/package_test.go juju-core-2.0~beta15/src/github.com/juju/juju/payload/state/package_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/payload/state/package_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/payload/state/package_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,14 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package state_test - -import ( - "testing" - - gc "gopkg.in/check.v1" -) - -func Test(t *testing.T) { - gc.TestingT(t) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/payload/state/payloads.go juju-core-2.0~beta15/src/github.com/juju/juju/payload/state/payloads.go --- juju-core-2.0~beta12/src/github.com/juju/juju/payload/state/payloads.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/payload/state/payloads.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,34 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package state - -import ( - "github.com/juju/errors" - - "github.com/juju/juju/payload" -) - -// EnvPersistence provides the persistence functionality for the -// Juju environment as a whole. -type EnvPersistence interface { - // ListAll returns the list of all payloads in the environment. - ListAll() ([]payload.FullPayloadInfo, error) -} - -// EnvPayloads provides the functionality related to an env's -// payloads, as needed by state. -type EnvPayloads struct { - Persist EnvPersistence -} - -// ListAll builds the list of payload information that is registered in state. -func (ps EnvPayloads) ListAll() ([]payload.FullPayloadInfo, error) { - logger.Tracef("listing all payloads") - - payloads, err := ps.Persist.ListAll() - if err != nil { - return nil, errors.Trace(err) - } - return payloads, nil -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/payload/state/payloads_test.go juju-core-2.0~beta15/src/github.com/juju/juju/payload/state/payloads_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/payload/state/payloads_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/payload/state/payloads_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,198 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package state_test - -import ( - "reflect" - - "github.com/juju/errors" - "github.com/juju/testing" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - "gopkg.in/juju/charm.v6-unstable" - - "github.com/juju/juju/payload" - "github.com/juju/juju/payload/state" -) - -var _ = gc.Suite(&envPayloadsSuite{}) - -type envPayloadsSuite struct { - basePayloadsSuite - - persists *stubPayloadsPersistence -} - -func (s *envPayloadsSuite) SetUpTest(c *gc.C) { - s.basePayloadsSuite.SetUpTest(c) - - s.persists = &stubPayloadsPersistence{stub: s.stub} -} - -func (s *envPayloadsSuite) newPayload(name string) payload.FullPayloadInfo { - return payload.FullPayloadInfo{ - Payload: payload.Payload{ - PayloadClass: charm.PayloadClass{ - Name: name, - Type: "docker", - }, - ID: "id" + name, - Status: payload.StateRunning, - Labels: []string{"a-tag"}, - Unit: "a-application/0", - }, - Machine: "1", - } -} - -func (s *envPayloadsSuite) TestListAllOkay(c *gc.C) { - p1 := s.newPayload("spam") - p2 := s.newPayload("eggs") - s.persists.setPayload(p1) - s.persists.setPayload(p2) - - ps := state.EnvPayloads{ - Persist: s.persists, - } - payloads, err := ps.ListAll() - c.Assert(err, jc.ErrorIsNil) - - s.stub.CheckCallNames(c, "ListAll", "ListAll") - checkPayloads(c, payloads, []payload.FullPayloadInfo{ - p1, - p2, - }) -} - -func (s *envPayloadsSuite) TestListAllMulti(c *gc.C) { - p1 := s.newPayload("spam") - p2 := s.newPayload("eggs") - p2.Unit = "a-application/1" - p3 := s.newPayload("ham") - p3.Unit = "a-application/2" - p3.Machine = "2" - p4 := s.newPayload("spamspamspam") - p4.Unit = "a-application/1" - s.persists.setPayload(p1) - s.persists.setPayload(p2) - s.persists.setPayload(p3) - s.persists.setPayload(p4) - - ps := state.EnvPayloads{ - Persist: s.persists, - } - payloads, err := ps.ListAll() - c.Assert(err, jc.ErrorIsNil) - - s.stub.CheckCallNames(c, "ListAll", "ListAll", "ListAll", "ListAll") - checkPayloads(c, payloads, []payload.FullPayloadInfo{ - p1, - p2, - p3, - p4, - }) -} - -func (s *envPayloadsSuite) TestListAllFailed(c *gc.C) { - failure := errors.Errorf("") - s.stub.SetErrors(failure) - p1 := s.newPayload("spam") - p2 := s.newPayload("eggs") - s.persists.setPayload(p1) - s.persists.setPayload(p2) - - ps := state.EnvPayloads{ - Persist: s.persists, - } - _, err := ps.ListAll() - - s.stub.CheckCallNames(c, "ListAll") - c.Check(errors.Cause(err), gc.Equals, failure) -} - -func checkPayloads(c *gc.C, payloads, expectedList []payload.FullPayloadInfo) { - remainder := make([]payload.FullPayloadInfo, len(payloads)) - copy(remainder, payloads) - var noMatch []payload.FullPayloadInfo - for _, expected := range expectedList { - found := false - for i, payload := range remainder { - if reflect.DeepEqual(payload, expected) { - remainder = append(remainder[:i], remainder[i+1:]...) - found = true - break - } - } - if !found { - noMatch = append(noMatch, expected) - } - } - - ok1 := c.Check(noMatch, gc.HasLen, 0) - ok2 := c.Check(remainder, gc.HasLen, 0) - if !ok1 || !ok2 { - c.Logf("<<<<<<<<\nexpected:") - for _, payload := range expectedList { - c.Logf("%#v", payload) - } - c.Logf("--------\ngot:") - for _, payload := range payloads { - c.Logf("%#v", payload) - } - c.Logf(">>>>>>>>") - } -} - -type stubPayloadsPersistence struct { - stub *testing.Stub - - persists map[string]map[string]*fakePayloadsPersistence -} - -func (s *stubPayloadsPersistence) ListAll() ([]payload.FullPayloadInfo, error) { - s.stub.AddCall("ListAll") - if err := s.stub.NextErr(); err != nil { - return nil, errors.Trace(err) - } - - var fullPayloads []payload.FullPayloadInfo - for machine, units := range s.persists { - for unit, unitPayloads := range units { - payloads, err := unitPayloads.ListAll() - if err != nil { - return nil, errors.Trace(err) - } - - for _, pl := range payloads { - if pl.Unit == "" { - pl.Unit = unit - } - if pl.Machine == "" { - pl.Machine = machine - } - fullPayloads = append(fullPayloads, pl) - } - } - } - return fullPayloads, nil -} - -func (s *stubPayloadsPersistence) setPayload(pl payload.FullPayloadInfo) { - if s.persists == nil { - s.persists = make(map[string]map[string]*fakePayloadsPersistence) - } - - units := s.persists[pl.Machine] - if units == nil { - units = make(map[string]*fakePayloadsPersistence) - s.persists[pl.Machine] = units - } - unitPayloads := units[pl.Unit] - if unitPayloads == nil { - unitPayloads = &fakePayloadsPersistence{Stub: s.stub} - units[pl.Unit] = unitPayloads - } - - unitPayloads.setPayload(&pl) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/payload/state/unit.go juju-core-2.0~beta15/src/github.com/juju/juju/payload/state/unit.go --- juju-core-2.0~beta12/src/github.com/juju/juju/payload/state/unit.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/payload/state/unit.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,195 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package state - -import ( - "github.com/juju/errors" - "github.com/juju/loggo" - - "github.com/juju/juju/payload" -) - -var logger = loggo.GetLogger("juju.payload.state") - -// TODO(ericsnow) We need a worker to clean up dying payloads. - -// Persistence exposes methods needed for payloads in state. -type Persistence interface { - Track(info payload.FullPayloadInfo) error - // SetStatus updates the status for a payload. - SetStatus(name, status string) error - List(names ...string) ([]payload.FullPayloadInfo, []string, error) - ListAll() ([]payload.FullPayloadInfo, error) - Untrack(name string) error -} - -// UnitPayloads provides the functionality related to a unit's -// payloads, as needed by state. -type UnitPayloads struct { - // Persist is the persistence layer that will be used. - Persist Persistence - - // Unit identifies the unit associated with the payloads. This - // is the "unit ID" of the targeted unit. - Unit string - - // Machine identifies the unit's machine. This is the "machine ID" - // of the machine on which the unit is running. - Machine string -} - -// NewUnitPayloads builds a UnitPayloads for a unit. -func NewUnitPayloads(persist Persistence, unit, machine string) *UnitPayloads { - return &UnitPayloads{ - Persist: persist, - Unit: unit, - Machine: machine, - } -} - -// Track inserts the provided payload info in state. If the payload -// is already in the DB then it is replaced. -func (uw UnitPayloads) Track(pl payload.Payload) error { - logger.Tracef("tracking %#v", pl) - - // TODO(ericsnow) The unit should probably not be a part - // of payload.Payload... - //pl.Unit = uw.Unit - - if err := pl.Validate(); err != nil { - return errors.NewNotValid(err, "bad payload") - } - - if existing, err := uw.lookUp(pl.Name); err != nil { - if errors.Cause(err) != payload.ErrNotFound { - return errors.Trace(err) - } - // Wasn't found, so we're okay. - } else { - logger.Debugf("payload %q (raw: %q) already exists; replacing...", pl.Name, existing.ID) - } - - full := payload.FullPayloadInfo{ - Payload: pl, - Machine: uw.Machine, - } - if err := uw.Persist.Track(full); err != nil { - return errors.Trace(err) - } - - return nil -} - -// SetStatus updates the raw status for the identified payload to the -// provided value. If the payload is missing then payload.ErrNotFound -// is returned. -func (uw UnitPayloads) SetStatus(name, status string) error { - logger.Tracef("setting payload status for %q to %q", name, status) - - if err := payload.ValidateState(status); err != nil { - return errors.Trace(err) - } - - if err := uw.Persist.SetStatus(name, status); err != nil { - return errors.Trace(err) - } - return nil -} - -// List builds the list of payload information for the provided payload -// IDs. If none are provided then the list contains the info for all -// payloads associated with the unit. Missing payloads -// are ignored. -func (uw UnitPayloads) List(names ...string) ([]payload.Result, error) { - logger.Tracef("listing %v", names) - var err error - var payloads []payload.FullPayloadInfo - missingIDs := make(map[string]bool) - if len(names) == 0 { - payloads, err = uw.Persist.ListAll() - if err != nil { - return nil, errors.Trace(err) - } - for _ = range payloads { - names = append(names, "") - } - } else { - var missing []string - payloads, missing, err = uw.Persist.List(names...) - if err != nil { - return nil, errors.Trace(err) - } - for _, id := range missing { - missingIDs[id] = true - } - } - - var results []payload.Result - i := 0 - for _, name := range names { - if missingIDs[name] { - results = append(results, payload.Result{ - ID: name, - NotFound: true, - Error: errors.NotFoundf(name), - }) - continue - } - pl := payloads[i] - i += 1 - - // TODO(ericsnow) Ensure that pl.Name == name? - // TODO(ericsnow) Ensure that pl.Unit == uw.Unit? - - result := payload.Result{ - ID: pl.Name, - Payload: &pl, - } - results = append(results, result) - } - return results, nil -} - -// TODO(ericsnow) Drop LookUp in favor of calling List() directly. - -// LookUp returns the payload ID for the given name/rawID pair. -func (uw UnitPayloads) LookUp(name, rawID string) (string, error) { - logger.Tracef("looking up payload id for %s/%s", name, rawID) - pl, err := uw.lookUp(name) - if err != nil { - return "", errors.Trace(err) - } - return pl.Name, nil -} - -func (uw UnitPayloads) lookUp(name string) (payload.FullPayloadInfo, error) { - var pl payload.FullPayloadInfo - - results, err := uw.List(name) - if err != nil { - return pl, errors.Trace(err) - } - if results[0].NotFound { - return pl, errors.Annotate(payload.ErrNotFound, name) - } - if results[0].Error != nil { - return pl, errors.Trace(results[0].Error) - } - pl = *results[0].Payload - - return pl, nil -} - -// Untrack removes the identified payload from state. It does not -// trigger the actual destruction of the payload. If the payload is -// missing then this is a noop. -func (uw UnitPayloads) Untrack(name string) error { - logger.Tracef("untracking %q", name) - // If the record wasn't found then we're already done. - err := uw.Persist.Untrack(name) - if err != nil && errors.Cause(err) != payload.ErrNotFound { - return errors.Trace(err) - } - return nil -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/payload/state/unit_test.go juju-core-2.0~beta15/src/github.com/juju/juju/payload/state/unit_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/payload/state/unit_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/payload/state/unit_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,271 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package state_test - -import ( - "github.com/juju/errors" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/payload" - "github.com/juju/juju/payload/state" -) - -var _ = gc.Suite(&unitPayloadsSuite{}) - -type unitPayloadsSuite struct { - basePayloadsSuite -} - -func (s *unitPayloadsSuite) TestTrackOkay(c *gc.C) { - pl := s.newPayload("docker", "payloadA/payloadA-xyz") - - ps := state.UnitPayloads{ - Persist: s.persist, - Unit: "a-application/0", - Machine: "0", - } - - err := ps.Track(pl.Payload) - c.Assert(err, jc.ErrorIsNil) - - s.stub.CheckCallNames(c, "List", "Track") - c.Check(s.persist.payloads, gc.HasLen, 1) - s.persist.checkPayload(c, pl.Name, pl) -} - -func (s *unitPayloadsSuite) TestTrackInvalid(c *gc.C) { - pl := s.newPayload("", "payloadA/payloadA-xyz") - - ps := state.UnitPayloads{ - Persist: s.persist, - Unit: "a-application/0", - Machine: "0", - } - - err := ps.Track(pl.Payload) - - c.Check(err, jc.Satisfies, errors.IsNotValid) -} - -func (s *unitPayloadsSuite) TestTrackEnsureDefinitionFailed(c *gc.C) { - failure := errors.Errorf("") - s.stub.SetErrors(failure) - pl := s.newPayload("docker", "payloadA/payloadA-xyz") - - ps := state.UnitPayloads{ - Persist: s.persist, - Unit: "a-application/0", - Machine: "0", - } - - err := ps.Track(pl.Payload) - - c.Check(errors.Cause(err), gc.Equals, failure) -} - -func (s *unitPayloadsSuite) TestTrackInsertFailed(c *gc.C) { - failure := errors.Errorf("") - s.stub.SetErrors(failure) - pl := s.newPayload("docker", "payloadA/payloadA-xyz") - - ps := state.UnitPayloads{ - Persist: s.persist, - Unit: "a-application/0", - Machine: "0", - } - - err := ps.Track(pl.Payload) - - c.Check(errors.Cause(err), gc.Equals, failure) -} - -func (s *unitPayloadsSuite) TestTrackAlreadyExists(c *gc.C) { - pl := s.newPayload("docker", "payloadA/payloadA-xyz") - s.persist.setPayload(&pl) - - ps := state.UnitPayloads{ - Persist: s.persist, - Unit: "a-application/0", - Machine: "0", - } - - err := ps.Track(pl.Payload) - - c.Check(err, jc.Satisfies, errors.IsAlreadyExists) -} - -func (s *unitPayloadsSuite) TestSetStatusOkay(c *gc.C) { - pl := s.newPayload("docker", "payloadA/payloadA-xyz") - s.persist.setPayload(&pl) - - ps := state.UnitPayloads{ - Persist: s.persist, - Unit: "a-application/0", - Machine: "0", - } - - err := ps.SetStatus(pl.Name, payload.StateRunning) - c.Assert(err, jc.ErrorIsNil) - - s.stub.CheckCallNames(c, "SetStatus") - current := s.persist.payloads[pl.Name] - c.Check(current.Status, jc.DeepEquals, payload.StateRunning) -} - -func (s *unitPayloadsSuite) TestSetStatusFailed(c *gc.C) { - failure := errors.Errorf("") - s.stub.SetErrors(failure) - pl := s.newPayload("docker", "payloadA/payloadA-xyz") - s.persist.setPayload(&pl) - - ps := state.UnitPayloads{ - Persist: s.persist, - Unit: "a-application/0", - Machine: "0", - } - err := ps.SetStatus(pl.Name, payload.StateRunning) - - c.Check(errors.Cause(err), gc.Equals, failure) -} - -func (s *unitPayloadsSuite) TestSetStatusMissing(c *gc.C) { - ps := state.UnitPayloads{ - Persist: s.persist, - Unit: "a-application/0", - Machine: "0", - } - err := ps.SetStatus("payloadA", payload.StateRunning) - - c.Check(err, jc.Satisfies, errors.IsNotFound) -} - -func (s *unitPayloadsSuite) TestListOkay(c *gc.C) { - pl := s.newPayload("docker", "payloadA/payloadA-xyz") - other := s.newPayload("docker", "payloadB/payloadB-abc") - s.persist.setPayload(&pl) - s.persist.setPayload(&other) - - ps := state.UnitPayloads{ - Persist: s.persist, - Unit: "a-application/0", - } - results, err := ps.List(pl.Name) - c.Assert(err, jc.ErrorIsNil) - - s.stub.CheckCallNames(c, "List") - c.Check(results, jc.DeepEquals, []payload.Result{{ - ID: pl.Name, - Payload: &pl, - }}) -} - -func (s *unitPayloadsSuite) TestListAll(c *gc.C) { - pl1 := s.newPayload("docker", "payloadA/payloadA-xyz") - pl2 := s.newPayload("docker", "payloadB/payloadB-abc") - s.persist.setPayload(&pl1) - s.persist.setPayload(&pl2) - - ps := state.UnitPayloads{ - Persist: s.persist, - Unit: "a-application/0", - Machine: "0", - } - results, err := ps.List() - c.Assert(err, jc.ErrorIsNil) - - s.stub.CheckCallNames(c, "ListAll") - c.Assert(results, gc.HasLen, 2) - if results[0].Payload.Name == "payloadA" { - c.Check(results[0].Payload, jc.DeepEquals, &pl1) - c.Check(results[1].Payload, jc.DeepEquals, &pl2) - } else { - c.Check(results[0].Payload, jc.DeepEquals, &pl2) - c.Check(results[1].Payload, jc.DeepEquals, &pl1) - } -} - -func (s *unitPayloadsSuite) TestListFailed(c *gc.C) { - failure := errors.Errorf("") - s.stub.SetErrors(failure) - - ps := state.UnitPayloads{ - Persist: s.persist, - Unit: "a-application/0", - Machine: "0", - } - _, err := ps.List() - - s.stub.CheckCallNames(c, "ListAll") - c.Check(errors.Cause(err), gc.Equals, failure) -} - -func (s *unitPayloadsSuite) TestListMissing(c *gc.C) { - pl := s.newPayload("docker", "payloadA/payloadA-xyz") - s.persist.setPayload(&pl) - missingName := "not-" + pl.Name - - ps := state.UnitPayloads{ - Persist: s.persist, - Unit: "a-application/0", - Machine: "0", - } - results, err := ps.List(pl.Name, missingName) - c.Assert(err, jc.ErrorIsNil) - - c.Assert(results, gc.HasLen, 2) - c.Check(results[1].Error, gc.NotNil) - results[1].Error = nil - c.Check(results, jc.DeepEquals, []payload.Result{{ - ID: pl.Name, - Payload: &pl, - }, { - ID: missingName, - NotFound: true, - }}) -} - -func (s *unitPayloadsSuite) TestUntrackOkay(c *gc.C) { - pl := s.newPayload("docker", "payloadA/payloadA-xyz") - s.persist.setPayload(&pl) - - ps := state.UnitPayloads{ - Persist: s.persist, - Unit: "a-application/0", - Machine: "0", - } - err := ps.Untrack(pl.Name) - c.Assert(err, jc.ErrorIsNil) - - s.stub.CheckCallNames(c, "Untrack") - c.Check(s.persist.payloads, gc.HasLen, 0) -} - -func (s *unitPayloadsSuite) TestUntrackMissing(c *gc.C) { - ps := state.UnitPayloads{ - Persist: s.persist, - Unit: "a-application/0", - Machine: "0", - } - err := ps.Untrack("payloadA") - c.Assert(err, jc.ErrorIsNil) - - s.stub.CheckCallNames(c, "Untrack") - c.Check(s.persist.payloads, gc.HasLen, 0) -} - -func (s *unitPayloadsSuite) TestUntrackFailed(c *gc.C) { - failure := errors.Errorf("") - s.stub.SetErrors(failure) - - ps := state.UnitPayloads{ - Persist: s.persist, - Unit: "a-application/0", - Machine: "0", - } - err := ps.Untrack("payloadA") - - s.stub.CheckCallNames(c, "Untrack") - c.Check(errors.Cause(err), gc.Equals, failure) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/config.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/config.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/config.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/config.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,10 +4,8 @@ package azure import ( - "net/url" "strings" - "github.com/Azure/azure-sdk-for-go/Godeps/_workspace/src/github.com/Azure/go-autorest/autorest/azure" "github.com/Azure/azure-sdk-for-go/arm/storage" "github.com/juju/errors" "github.com/juju/schema" @@ -17,83 +15,30 @@ ) const ( - configAttrAppId = "application-id" - configAttrSubscriptionId = "subscription-id" - configAttrTenantId = "tenant-id" - configAttrAppPassword = "application-password" - configAttrLocation = "location" - configAttrEndpoint = "endpoint" - configAttrStorageEndpoint = "storage-endpoint" configAttrStorageAccountType = "storage-account-type" // The below bits are internal book-keeping things, rather than // configuration. Config is just what we have to work with. - // configAttrStorageAccount is the name of the storage account. We - // can't just use a well-defined name for the storage acocunt because - // storage account names must be globally unique; each storage account - // has an associated public DNS entry. - configAttrStorageAccount = "storage-account" - - // configAttrStorageAccountKey is the primary key for the storage - // account. - configAttrStorageAccountKey = "storage-account-key" - // resourceNameLengthMax is the maximum length of resource // names in Azure. resourceNameLengthMax = 80 ) var configFields = schema.Fields{ - configAttrLocation: schema.String(), - configAttrEndpoint: schema.String(), - configAttrStorageEndpoint: schema.String(), - configAttrAppId: schema.String(), - configAttrSubscriptionId: schema.String(), - configAttrTenantId: schema.String(), - configAttrAppPassword: schema.String(), - configAttrStorageAccount: schema.String(), - configAttrStorageAccountKey: schema.String(), configAttrStorageAccountType: schema.String(), } var configDefaults = schema.Defaults{ - configAttrStorageAccount: schema.Omit, - configAttrStorageAccountKey: schema.Omit, configAttrStorageAccountType: string(storage.StandardLRS), } -var requiredConfigAttributes = []string{ - configAttrAppId, - configAttrAppPassword, - configAttrSubscriptionId, - configAttrTenantId, - configAttrLocation, - configAttrEndpoint, - configAttrStorageEndpoint, -} - var immutableConfigAttributes = []string{ - configAttrSubscriptionId, - configAttrTenantId, - configAttrStorageAccount, configAttrStorageAccountType, } -var internalConfigAttributes = []string{ - configAttrStorageAccount, - configAttrStorageAccountKey, -} - type azureModelConfig struct { *config.Config - token *azure.ServicePrincipalToken - subscriptionId string - location string // canonicalized - endpoint string - storageEndpoint string - storageAccount string - storageAccountKey string storageAccountType storage.AccountType } @@ -123,12 +68,6 @@ return nil, err } - // Ensure required configuration is provided. - for _, key := range requiredConfigAttributes { - if value, ok := validated[key].(string); !ok || value == "" { - return nil, errors.Errorf("%q config not specified", key) - } - } if oldCfg != nil { // Ensure immutable configuration isn't changed. oldUnknownAttrs := oldCfg.UnknownAttrs() @@ -150,8 +89,6 @@ } // It's valid to go from not having to having. } - // TODO(axw) figure out how we intend to handle changing - // secrets, such as application key } // Resource group names must not exceed 80 characters. Resource group @@ -169,22 +106,12 @@ ) } - location := canonicalLocation(validated[configAttrLocation].(string)) - endpoint := validated[configAttrEndpoint].(string) - storageEndpoint := validated[configAttrStorageEndpoint].(string) - appId := validated[configAttrAppId].(string) - subscriptionId := validated[configAttrSubscriptionId].(string) - tenantId := validated[configAttrTenantId].(string) - appPassword := validated[configAttrAppPassword].(string) - storageAccount, _ := validated[configAttrStorageAccount].(string) - storageAccountKey, _ := validated[configAttrStorageAccountKey].(string) - storageAccountType := validated[configAttrStorageAccountType].(string) - if newCfg.FirewallMode() == config.FwGlobal { // We do not currently support the "global" firewall mode. return nil, errNoFwGlobal } + storageAccountType := validated[configAttrStorageAccountType].(string) if !isKnownStorageAccountType(storageAccountType) { return nil, errors.Errorf( "invalid storage account type %q, expected one of: %q", @@ -192,32 +119,10 @@ ) } - // The Azure storage code wants the endpoint host only, not the URL. - storageEndpointURL, err := url.Parse(storageEndpoint) - if err != nil { - return nil, errors.Annotate(err, "parsing storage endpoint URL") - } - - token, err := azure.NewServicePrincipalToken( - appId, appPassword, tenantId, - azure.AzureResourceManagerScope, - ) - if err != nil { - return nil, errors.Annotate(err, "constructing service principal token") - } - azureConfig := &azureModelConfig{ newCfg, - token, - subscriptionId, - location, - endpoint, - storageEndpointURL.Host, - storageAccount, - storageAccountKey, storage.AccountType(storageAccountType), } - return azureConfig, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/config_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/config_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/config_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/config_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -32,7 +32,7 @@ func (s *configSuite) SetUpTest(c *gc.C) { s.BaseSuite.SetUpTest(c) - s.provider, _ = newProviders(c, azure.ProviderConfig{ + s.provider = newProvider(c, azure.ProviderConfig{ Sender: mocks.NewSender(), }) } @@ -55,13 +55,6 @@ ) } -func (s *configSuite) TestValidateLocation(c *gc.C) { - s.assertConfigInvalid(c, testing.Attrs{"location": ""}, `"location" config not specified`) - // We don't validate locations, because new locations may be added. - // Azure will complain if the location is invalid anyway. - s.assertConfigValid(c, testing.Attrs{"location": "eurasia"}) -} - func (s *configSuite) TestValidateModelNameLength(c *gc.C) { s.assertConfigInvalid( c, testing.Attrs{"name": "someextremelyoverlylongishmodelname"}, @@ -70,25 +63,14 @@ Please choose a model name of no more than 32 characters.`) } -func (s *configSuite) TestValidateInvalidCredentials(c *gc.C) { - s.assertConfigInvalid(c, testing.Attrs{"application-id": ""}, `"application-id" config not specified`) - s.assertConfigInvalid(c, testing.Attrs{"application-password": ""}, `"application-password" config not specified`) - s.assertConfigInvalid(c, testing.Attrs{"tenant-id": ""}, `"tenant-id" config not specified`) - s.assertConfigInvalid(c, testing.Attrs{"subscription-id": ""}, `"subscription-id" config not specified`) -} - -func (s *configSuite) TestValidateStorageAccountCantChange(c *gc.C) { - cfgOld := makeTestModelConfig(c, testing.Attrs{"storage-account": "abc"}) +func (s *configSuite) TestValidateStorageAccountTypeCantChange(c *gc.C) { + cfgOld := makeTestModelConfig(c, testing.Attrs{"storage-account-type": "Standard_LRS"}) _, err := s.provider.Validate(cfgOld, cfgOld) c.Assert(err, jc.ErrorIsNil) - cfgNew := makeTestModelConfig(c) // no storage-account attribute - _, err = s.provider.Validate(cfgNew, cfgOld) - c.Assert(err, gc.ErrorMatches, `cannot remove immutable "storage-account" config`) - - cfgNew = makeTestModelConfig(c, testing.Attrs{"storage-account": "def"}) + cfgNew := makeTestModelConfig(c, testing.Attrs{"storage-account-type": "Premium_LRS"}) _, err = s.provider.Validate(cfgNew, cfgOld) - c.Assert(err, gc.ErrorMatches, `cannot change immutable "storage-account" config \(abc -> def\)`) + c.Assert(err, gc.ErrorMatches, `cannot change immutable "storage-account-type" config \(Standard_LRS -> Premium_LRS\)`) } func (s *configSuite) assertConfigValid(c *gc.C, attrs testing.Attrs) { @@ -105,15 +87,8 @@ func makeTestModelConfig(c *gc.C, extra ...testing.Attrs) *config.Config { attrs := testing.Attrs{ - "type": "azure", - "application-id": fakeApplicationId, - "tenant-id": fakeTenantId, - "application-password": "opensezme", - "subscription-id": fakeSubscriptionId, - "location": "westus", - "endpoint": "https://api.azurestack.local", - "storage-endpoint": "https://storage.azurestack.local", - "agent-version": "1.2.3", + "type": "azure", + "agent-version": "1.2.3", } for _, extra := range extra { attrs = attrs.Merge(extra) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/credentials.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/credentials.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/credentials.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/credentials.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,6 +9,13 @@ "github.com/juju/juju/cloud" ) +const ( + credAttrAppId = "application-id" + credAttrSubscriptionId = "subscription-id" + credAttrTenantId = "tenant-id" + credAttrAppPassword = "application-password" +) + // environPoviderCredentials is an implementation of // environs.ProviderCredentials for the Azure Resource // Manager cloud provider. @@ -19,13 +26,13 @@ return map[cloud.AuthType]cloud.CredentialSchema{ cloud.UserPassAuthType: { { - configAttrAppId, cloud.CredentialAttr{Description: "Azure Active Directory application ID"}, + credAttrAppId, cloud.CredentialAttr{Description: "Azure Active Directory application ID"}, }, { - configAttrSubscriptionId, cloud.CredentialAttr{Description: "Azure subscription ID"}, + credAttrSubscriptionId, cloud.CredentialAttr{Description: "Azure subscription ID"}, }, { - configAttrTenantId, cloud.CredentialAttr{Description: "Azure Active Directory tenant ID"}, + credAttrTenantId, cloud.CredentialAttr{Description: "Azure Active Directory tenant ID"}, }, { - configAttrAppPassword, cloud.CredentialAttr{ + credAttrAppPassword, cloud.CredentialAttr{ Description: "Azure Active Directory application password", Hidden: true, }, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/credentials_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/credentials_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/credentials_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/credentials_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -23,7 +23,7 @@ func (s *credentialsSuite) SetUpTest(c *gc.C) { s.BaseSuite.SetUpTest(c) - s.provider, _ = newProviders(c, azure.ProviderConfig{}) + s.provider = newProvider(c, azure.ProviderConfig{}) } func (s *credentialsSuite) TestCredentialSchemas(c *gc.C) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/environ.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/environ.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/environ.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/environ.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,11 +6,13 @@ import ( "fmt" "net/http" + "net/url" "sort" "strings" "sync" "github.com/Azure/azure-sdk-for-go/Godeps/_workspace/src/github.com/Azure/go-autorest/autorest" + "github.com/Azure/azure-sdk-for-go/Godeps/_workspace/src/github.com/Azure/go-autorest/autorest/azure" "github.com/Azure/azure-sdk-for-go/Godeps/_workspace/src/github.com/Azure/go-autorest/autorest/to" "github.com/Azure/azure-sdk-for-go/arm/compute" "github.com/Azure/azure-sdk-for-go/arm/network" @@ -41,14 +43,33 @@ "github.com/juju/juju/tools" ) -const jujuMachineNameTag = tags.JujuTagPrefix + "machine-name" +const ( + jujuMachineNameTag = tags.JujuTagPrefix + "machine-name" -type azureEnviron struct { - common.SupportsUnitPlacementPolicy + // defaultRootDiskSize is the default root disk size to give + // to a VM, if none is specified. + defaultRootDiskSize = 30 * 1024 // 30 GiB +) +type azureEnviron struct { // provider is the azureEnvironProvider used to open this environment. provider *azureEnvironProvider + // cloud defines the cloud configuration for this environment. + cloud environs.CloudSpec + + // location is the canonicalized location name. Use this instead + // of cloud.Region in API calls. + location string + + // subscriptionId is the Azure account subscription ID. + subscriptionId string + + // storageEndpoint is the Azure storage endpoint. This is the host + // portion of the storage endpoint URL only; use this instead of + // cloud.StorageEndpoint in API calls. + storageEndpoint string + // resourceGroup is the name of the Resource Group in the Azure // subscription that corresponds to the environment. resourceGroup string @@ -56,26 +77,49 @@ // envName is the name of the environment. envName string - mu sync.Mutex - config *azureModelConfig - instanceTypes map[string]instances.InstanceType - // azure management clients + // azure auth token, and management clients + token *azure.ServicePrincipalToken compute compute.ManagementClient resources resources.ManagementClient storage storage.ManagementClient network network.ManagementClient storageClient azurestorage.Client + + mu sync.Mutex + config *azureModelConfig + instanceTypes map[string]instances.InstanceType + storageAccount *storage.Account + storageAccountKeys *storage.AccountKeys } var _ environs.Environ = (*azureEnviron)(nil) var _ state.Prechecker = (*azureEnviron)(nil) // newEnviron creates a new azureEnviron. -func newEnviron(provider *azureEnvironProvider, cfg *config.Config) (*azureEnviron, error) { - env := azureEnviron{provider: provider} - err := env.SetConfig(cfg) +func newEnviron( + provider *azureEnvironProvider, + cloud environs.CloudSpec, + cfg *config.Config, +) (*azureEnviron, error) { + + // The Azure storage code wants the endpoint host only, not the URL. + storageEndpointURL, err := url.Parse(cloud.StorageEndpoint) if err != nil { - return nil, err + return nil, errors.Annotate(err, "parsing storage endpoint URL") + } + + env := azureEnviron{ + provider: provider, + cloud: cloud, + location: canonicalLocation(cloud.Region), + storageEndpoint: storageEndpointURL.Host, + } + if err := env.initEnviron(); err != nil { + return nil, errors.Trace(err) + } + + if err := env.SetConfig(cfg); err != nil { + return nil, errors.Trace(err) } modelTag := names.NewModelTag(cfg.UUID()) env.resourceGroup = resourceGroupName(modelTag, cfg.Name()) @@ -83,20 +127,84 @@ return &env, nil } -// Bootstrap is specified in the Environ interface. +func (env *azureEnviron) initEnviron() error { + credAttrs := env.cloud.Credential.Attributes() + env.subscriptionId = credAttrs[credAttrSubscriptionId] + tenantId := credAttrs[credAttrTenantId] + appId := credAttrs[credAttrAppId] + appPassword := credAttrs[credAttrAppPassword] + token, err := azure.NewServicePrincipalToken( + appId, + appPassword, + tenantId, + azure.AzureResourceManagerScope, + ) + if err != nil { + return errors.Annotate(err, "constructing service principal token") + } + env.token = token + if env.provider.config.Sender != nil { + env.token.SetSender(env.provider.config.Sender) + } + + env.compute = compute.NewWithBaseURI(env.cloud.Endpoint, env.subscriptionId) + env.resources = resources.NewWithBaseURI(env.cloud.Endpoint, env.subscriptionId) + env.storage = storage.NewWithBaseURI(env.cloud.Endpoint, env.subscriptionId) + env.network = network.NewWithBaseURI(env.cloud.Endpoint, env.subscriptionId) + clients := map[string]*autorest.Client{ + "azure.compute": &env.compute.Client, + "azure.resources": &env.resources.Client, + "azure.storage": &env.storage.Client, + "azure.network": &env.network.Client, + } + for id, client := range clients { + client.Authorizer = env.token + logger := loggo.GetLogger(id) + if env.provider.config.Sender != nil { + client.Sender = env.provider.config.Sender + } + client.ResponseInspector = tracingRespondDecorator(logger) + client.RequestInspector = tracingPrepareDecorator(logger) + if env.provider.config.RequestInspector != nil { + tracer := client.RequestInspector + inspector := env.provider.config.RequestInspector + client.RequestInspector = func(p autorest.Preparer) autorest.Preparer { + p = tracer(p) + p = inspector(p) + return p + } + } + } + return nil +} + +// PrepareForBootstrap is part of the Environ interface. +func (env *azureEnviron) PrepareForBootstrap(ctx environs.BootstrapContext) error { + if ctx.ShouldVerifyCredentials() { + if err := verifyCredentials(env); err != nil { + return errors.Trace(err) + } + } + return nil +} + +// Create is part of the Environ interface. +func (env *azureEnviron) Create(args environs.CreateParams) error { + if err := verifyCredentials(env); err != nil { + return errors.Trace(err) + } + return errors.Trace(env.initResourceGroup(args.ControllerUUID)) +} + +// Bootstrap is part of the Environ interface. func (env *azureEnviron) Bootstrap( ctx environs.BootstrapContext, args environs.BootstrapParams, ) (*environs.BootstrapResult, error) { - cfg, err := env.initResourceGroup(args.ControllerConfig.ControllerUUID()) - if err != nil { + if err := env.initResourceGroup(args.ControllerConfig.ControllerUUID()); err != nil { return nil, errors.Annotate(err, "creating controller resource group") } - if err := env.SetConfig(cfg); err != nil { - return nil, errors.Annotate(err, "updating config") - } - result, err := common.Bootstrap(ctx, env, args) if err != nil { logger.Errorf("bootstrap failed, destroying model: %v", err) @@ -109,17 +217,22 @@ } // initResourceGroup creates and initialises a resource group for this -// environment. The resource group will have a storage account and a -// subnet associated with it (but not necessarily contained within: -// see subnet creation). -func (env *azureEnviron) initResourceGroup(controllerUUID string) (*config.Config, error) { - location := env.config.location +// environment. The resource group will have a storage account, and +// internal network and subnet. +func (env *azureEnviron) initResourceGroup(controllerUUID string) error { + location := env.location + resourceGroupsClient := resources.GroupsClient{env.resources} + networkClient := env.network + storageAccountsClient := storage.AccountsClient{env.storage} + + env.mu.Lock() tags := tags.ResourceTags( names.NewModelTag(env.config.Config.UUID()), names.NewModelTag(controllerUUID), env.config, ) - resourceGroupsClient := resources.GroupsClient{env.resources} + storageAccountType := env.config.storageAccountType + env.mu.Unlock() logger.Debugf("creating resource group %q", env.resourceGroup) if err := env.callAPI(func() (autorest.Response, error) { @@ -129,40 +242,34 @@ }) return group.Response, err }); err != nil { - return nil, errors.Annotate(err, "creating resource group") + return errors.Annotate(err, "creating resource group") } // Create an internal network for all VMs in the // resource group to connect to. vnetPtr, err := createInternalVirtualNetwork( - env.callAPI, env.network, env.resourceGroup, location, tags, + env.callAPI, networkClient, env.resourceGroup, location, tags, ) if err != nil { - return nil, errors.Annotate(err, "creating virtual network") + return errors.Annotate(err, "creating virtual network") } _, err = createInternalSubnet( - env.callAPI, env.network, env.resourceGroup, vnetPtr, location, tags, + env.callAPI, networkClient, env.resourceGroup, vnetPtr, location, tags, ) if err != nil { - return nil, errors.Annotate(err, "creating subnet") + return errors.Annotate(err, "creating subnet") } // Create a storage account for the resource group. - storageAccountsClient := storage.AccountsClient{env.storage} - storageAccountName, storageAccountKey, err := createStorageAccount( - env.callAPI, storageAccountsClient, - env.config.storageAccountType, + if err := createStorageAccount( + env.callAPI, storageAccountsClient, storageAccountType, env.resourceGroup, location, tags, env.provider.config.StorageAccountNameGenerator, - ) - if err != nil { - return nil, errors.Annotate(err, "creating storage account") + ); err != nil { + return errors.Annotate(err, "creating storage account") } - return env.config.Config.Apply(map[string]interface{}{ - configAttrStorageAccount: storageAccountName, - configAttrStorageAccountKey: storageAccountKey, - }) + return nil } func createStorageAccount( @@ -173,7 +280,7 @@ location string, tags map[string]string, accountNameGenerator func() string, -) (string, string, error) { +) error { logger.Debugf("creating storage account (finding available name)") const maxAttempts = 10 for remaining := maxAttempts; remaining > 0; remaining-- { @@ -192,7 +299,7 @@ ) return result.Response, err }); err != nil { - return "", "", errors.Annotate(err, "checking account name availability") + return errors.Annotate(err, "checking account name availability") } if !to.Bool(result.NameAvailable) { logger.Debugf( @@ -216,21 +323,11 @@ result, err := client.Create(resourceGroup, accountName, createParams) return result.Response, err }); err != nil { - return "", "", errors.Trace(err) - } - - logger.Debugf("- listing storage account keys") - var listKeysResult storage.AccountKeys - if err := callAPI(func() (autorest.Response, error) { - var err error - listKeysResult, err = client.ListKeys(resourceGroup, accountName) - return listKeysResult.Response, err - }); err != nil { - return "", "", errors.Annotate(err, "listing storage account keys") + return errors.Trace(err) } - return accountName, to.String(listKeysResult.Key1), nil + return nil } - return "", "", errors.New("could not find available storage account name") + return errors.New("could not find available storage account name") } // ControllerInstances is specified in the Environ interface. @@ -277,59 +374,9 @@ } env.config = ecfg - // Initialise clients. - env.compute = compute.NewWithBaseURI(ecfg.endpoint, env.config.subscriptionId) - env.resources = resources.NewWithBaseURI(ecfg.endpoint, env.config.subscriptionId) - env.storage = storage.NewWithBaseURI(ecfg.endpoint, env.config.subscriptionId) - env.network = network.NewWithBaseURI(ecfg.endpoint, env.config.subscriptionId) - clients := map[string]*autorest.Client{ - "azure.compute": &env.compute.Client, - "azure.resources": &env.resources.Client, - "azure.storage": &env.storage.Client, - "azure.network": &env.network.Client, - } - if env.provider.config.Sender != nil { - env.config.token.SetSender(env.provider.config.Sender) - } - for id, client := range clients { - client.Authorizer = env.config.token - logger := loggo.GetLogger(id) - if env.provider.config.Sender != nil { - client.Sender = env.provider.config.Sender - } - client.ResponseInspector = tracingRespondDecorator(logger) - client.RequestInspector = tracingPrepareDecorator(logger) - if env.provider.config.RequestInspector != nil { - tracer := client.RequestInspector - inspector := env.provider.config.RequestInspector - client.RequestInspector = func(p autorest.Preparer) autorest.Preparer { - p = tracer(p) - p = inspector(p) - return p - } - } - } - - // Invalidate instance types when the location changes. - if old != nil { - oldLocation := old.UnknownAttrs()["location"].(string) - if env.config.location != oldLocation { - env.instanceTypes = nil - } - } - return nil } -// SupportedArchitectures is specified on the EnvironCapability interface. -func (env *azureEnviron) SupportedArchitectures() ([]string, error) { - return env.supportedArchitectures(), nil -} - -func (env *azureEnviron) supportedArchitectures() []string { - return []string{arch.AMD64} -} - // ConstraintsValidator is defined on the Environs interface. func (env *azureEnviron) ConstraintsValidator() (constraints.Validator, error) { instanceTypes, err := env.getInstanceTypes() @@ -350,7 +397,7 @@ }) validator.RegisterVocabulary( constraints.Arch, - env.supportedArchitectures(), + []string{arch.AMD64}, ) validator.RegisterVocabulary( constraints.InstanceType, @@ -399,29 +446,34 @@ if args.ControllerUUID == "" { return nil, errors.New("missing controller UUID") } + + location := env.location + vmClient := compute.VirtualMachinesClient{env.compute} + availabilitySetClient := compute.AvailabilitySetsClient{env.compute} + networkClient := env.network + vmImagesClient := compute.VirtualMachineImagesClient{env.compute} + vmExtensionClient := compute.VirtualMachineExtensionsClient{env.compute} + // Get the required configuration and config-dependent information // required to create the instance. We take the lock just once, to // ensure we obtain all information based on the same configuration. env.mu.Lock() - location := env.config.location envTags := tags.ResourceTags( names.NewModelTag(env.config.Config.UUID()), names.NewModelTag(args.ControllerUUID), env.config, ) - vmClient := compute.VirtualMachinesClient{env.compute} - availabilitySetClient := compute.AvailabilitySetsClient{env.compute} - networkClient := env.network - vmImagesClient := compute.VirtualMachineImagesClient{env.compute} - vmExtensionClient := compute.VirtualMachineExtensionsClient{env.compute} imageStream := env.config.ImageStream() - storageEndpoint := env.config.storageEndpoint - storageAccountName := env.config.storageAccount instanceTypes, err := env.getInstanceTypesLocked() if err != nil { env.mu.Unlock() return nil, errors.Trace(err) } + storageAccount, err := env.getStorageAccountLocked(false) + if err != nil { + env.mu.Unlock() + return nil, errors.Annotate(err, "getting storage account") + } internalNetworkSubnet, err := env.getInternalSubnetLocked() if err != nil { env.mu.Unlock() @@ -429,6 +481,16 @@ } env.mu.Unlock() + // If the user has not specified a root-disk size, then + // set a sensible default. + var rootDisk uint64 + if args.Constraints.RootDisk != nil { + rootDisk = *args.Constraints.RootDisk + } else { + rootDisk = defaultRootDiskSize + args.Constraints.RootDisk = &rootDisk + } + // Identify the instance type and image to provision. instanceSpec, err := findInstanceSpec( vmImagesClient, @@ -444,6 +506,12 @@ if err != nil { return nil, err } + if rootDisk < uint64(instanceSpec.InstanceType.RootDisk) { + // The InstanceType's RootDisk is set to the maximum + // OS disk size; override it with the user-specified + // or default root disk size. + instanceSpec.InstanceType.RootDisk = rootDisk + } // Pick tools by filtering the available tools down to the architecture of // the image that will be provisioned. @@ -491,7 +559,7 @@ args.DistributionGroup, env.Instances, apiPortPtr, internalNetworkSubnet, - storageEndpoint, storageAccountName, + storageAccount, networkClient, vmClient, availabilitySetClient, vmExtensionClient, env.callAPI, @@ -534,7 +602,7 @@ instancesFunc func([]instance.Id) ([]instance.Instance, error), apiPort *int, internalNetworkSubnet *network.Subnet, - storageEndpoint, storageAccountName string, + storageAccount *storage.Account, networkClient network.ManagementClient, vmClient compute.VirtualMachinesClient, availabilitySetClient compute.AvailabilitySetsClient, @@ -543,7 +611,7 @@ ) (compute.VirtualMachine, error) { storageProfile, err := newStorageProfile( - vmName, instanceSpec, storageEndpoint, storageAccountName, + vmName, instanceSpec, storageAccount, ) if err != nil { return compute.VirtualMachine{}, errors.Annotate(err, "creating storage profile") @@ -715,7 +783,7 @@ func newStorageProfile( vmName string, instanceSpec *instances.InstanceSpec, - storageEndpoint, storageAccountName string, + storageAccount *storage.Account, ) (*compute.StorageProfile, error) { logger.Debugf("creating storage profile for %q", vmName) @@ -728,8 +796,9 @@ sku := urnParts[2] version := urnParts[3] - osDisksRoot := osDiskVhdRoot(storageEndpoint, storageAccountName) + osDisksRoot := osDiskVhdRoot(storageAccount) osDiskName := vmName + osDiskSizeGB := mibToGB(instanceSpec.InstanceType.RootDisk) osDisk := &compute.OSDisk{ Name: to.StringPtr(osDiskName), CreateOption: compute.FromImage, @@ -739,6 +808,7 @@ osDisksRoot + osDiskName + vhdExtension, ), }, + DiskSizeGB: to.IntPtr(int(osDiskSizeGB)), } return &compute.StorageProfile{ ImageReference: &compute.ImageReference{ @@ -751,6 +821,11 @@ }, nil } +func mibToGB(mib uint64) uint64 { + b := float64(mib * 1024 * 1024) + return uint64(b / (1000 * 1000 * 1000)) +} + func newOSProfile(vmName string, instanceConfig *instancecfg.InstanceConfig) (*compute.OSProfile, os.OSType, error) { logger.Debugf("creating OS profile for %q", vmName) @@ -801,14 +876,8 @@ // StopInstances is specified in the InstanceBroker interface. func (env *azureEnviron) StopInstances(ids ...instance.Id) error { - env.mu.Lock() computeClient := env.compute networkClient := env.network - env.mu.Unlock() - storageClient, err := env.getStorageClient() - if err != nil { - return errors.Trace(err) - } // Query the instances, so we can inspect the VirtualMachines // and delete related resources. @@ -823,6 +892,11 @@ break } + storageClient, err := env.getStorageClient() + if err != nil { + return errors.Trace(err) + } + for _, inst := range instances { if inst == nil { continue @@ -1001,11 +1075,9 @@ resourceGroup string, refreshAddresses bool, ) ([]instance.Instance, error) { - env.mu.Lock() vmClient := compute.VirtualMachinesClient{env.compute} nicClient := network.InterfacesClient{env.network} pipClient := network.PublicIPAddressesClient{env.network} - env.mu.Unlock() // Due to how deleting instances works, we have to get creative about // listing instances. We list NICs and return an instance for each @@ -1241,7 +1313,7 @@ return env.instanceTypes, nil } - location := env.config.location + location := env.location client := compute.VirtualMachineSizesClient{env.compute} var result compute.VirtualMachineSizeListResult @@ -1288,26 +1360,99 @@ func (env *azureEnviron) getStorageClient() (internalazurestorage.Client, error) { env.mu.Lock() defer env.mu.Unlock() - client, err := getStorageClient(env.provider.config.NewStorageClient, env.config) + storageAccount, err := env.getStorageAccountLocked(false) + if err != nil { + return nil, errors.Annotate(err, "getting storage account") + } + storageAccountKeys, err := env.getStorageAccountKeysLocked( + to.String(storageAccount.Name), false, + ) + if err != nil { + return nil, errors.Annotate(err, "getting storage account keys") + } + client, err := getStorageClient( + env.provider.config.NewStorageClient, + env.storageEndpoint, + storageAccount, + storageAccountKeys, + ) if err != nil { return nil, errors.Annotate(err, "getting storage client") } return client, nil } +// getStorageAccount returns the storage account for this environment's +// resource group. If refresh is true, cached details will be refreshed. +func (env *azureEnviron) getStorageAccount(refresh bool) (*storage.Account, error) { + env.mu.Lock() + defer env.mu.Unlock() + return env.getStorageAccountLocked(refresh) +} + +func (env *azureEnviron) getStorageAccountLocked(refresh bool) (*storage.Account, error) { + if !refresh && env.storageAccount != nil { + return env.storageAccount, nil + } + client := storage.AccountsClient{env.storage} + var result storage.AccountListResult + if err := env.callAPI(func() (autorest.Response, error) { + var err error + result, err = client.List() + return result.Response, err + }); err != nil { + return nil, errors.Annotate(err, "listing storage accounts") + } + if result.Value == nil || len(*result.Value) == 0 { + return nil, errors.NotFoundf("storage account") + } + for _, account := range *result.Value { + if toTags(account.Tags)[tags.JujuModel] != env.config.UUID() { + continue + } + env.storageAccount = &account + return &account, nil + } + return nil, errors.NotFoundf("storage account") +} + +// getStorageAccountKeys returns the storage account keys for this +// environment's storage account. If refresh is true, cached keys +// will be refreshed. +func (env *azureEnviron) getStorageAccountKeys(accountName string, refresh bool) (*storage.AccountKeys, error) { + env.mu.Lock() + defer env.mu.Unlock() + return env.getStorageAccountKeysLocked(accountName, refresh) +} + +func (env *azureEnviron) getStorageAccountKeysLocked(accountName string, refresh bool) (*storage.AccountKeys, error) { + if !refresh && env.storageAccountKeys != nil { + return env.storageAccountKeys, nil + } + client := storage.AccountsClient{env.storage} + var listKeysResult storage.AccountKeys + if err := env.callAPI(func() (autorest.Response, error) { + var err error + listKeysResult, err = client.ListKeys(env.resourceGroup, accountName) + return listKeysResult.Response, err + }); err != nil { + return nil, errors.Annotate(err, "listing storage account keys") + } + env.storageAccountKeys = &listKeysResult + return env.storageAccountKeys, nil +} + // AgentMirror is specified in the tools.HasAgentMirror interface. // // TODO(axw) 2016-04-11 #1568715 // When we have image simplestreams, we should rename this to "Region", // to implement simplestreams.HasRegion. func (env *azureEnviron) AgentMirror() (simplestreams.CloudSpec, error) { - env.mu.Lock() - defer env.mu.Unlock() return simplestreams.CloudSpec{ - Region: env.config.location, + Region: env.location, // The endpoints published in simplestreams // data are the storage endpoints. - Endpoint: fmt.Sprintf("https://%s/", env.config.storageEndpoint), + Endpoint: fmt.Sprintf("https://%s/", env.storageEndpoint), }, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/environprovider.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/environprovider.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/environprovider.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/environprovider.go 2016-08-16 08:56:25.000000000 +0000 @@ -69,94 +69,48 @@ return &azureEnvironProvider{config: config}, nil } -// Open is specified in the EnvironProvider interface. -func (prov *azureEnvironProvider) Open(cfg *config.Config) (environs.Environ, error) { - logger.Debugf("opening model %q", cfg.Name()) - environ, err := newEnviron(prov, cfg) +// Open is part of the EnvironProvider interface. +func (prov *azureEnvironProvider) Open(args environs.OpenParams) (environs.Environ, error) { + logger.Debugf("opening model %q", args.Config.Name()) + if err := validateCloudSpec(args.Cloud); err != nil { + return nil, errors.Annotate(err, "validating cloud spec") + } + environ, err := newEnviron(prov, args.Cloud, args.Config) if err != nil { return nil, errors.Annotate(err, "opening model") } return environ, nil } -// RestrictedConfigAttributes is specified in the EnvironProvider interface. -// -// The result of RestrictedConfigAttributes is the names of attributes that -// will be copied across to a hosted environment's initial configuration. +// RestrictedConfigAttributes is part of the EnvironProvider interface. func (prov *azureEnvironProvider) RestrictedConfigAttributes() []string { - // TODO(axw) there should be no restricted attributes. - return []string{ - configAttrLocation, - configAttrEndpoint, - configAttrStorageEndpoint, - } + return []string{} } -// PrepareForCreateEnvironment is specified in the EnvironProvider interface. -func (prov *azureEnvironProvider) PrepareForCreateEnvironment(controllerUUID string, cfg *config.Config) (*config.Config, error) { - env, err := newEnviron(prov, cfg) - if err != nil { - return nil, errors.Annotate(err, "opening model") +// PrepareConfig is part of the EnvironProvider interface. +func (prov *azureEnvironProvider) PrepareConfig(args environs.PrepareConfigParams) (*config.Config, error) { + if err := validateCloudSpec(args.Cloud); err != nil { + return nil, errors.Annotate(err, "validating cloud spec") } - return env.initResourceGroup(controllerUUID) + return args.Config, nil } -// BootstrapConfig is specified in the EnvironProvider interface. -func (prov *azureEnvironProvider) BootstrapConfig(args environs.BootstrapConfigParams) (*config.Config, error) { - // Ensure that internal configuration is not specified, and then set - // what we can now. We only need to do this during bootstrap. Validate - // will check for changes later. - unknownAttrs := args.Config.UnknownAttrs() - for _, key := range internalConfigAttributes { - if _, ok := unknownAttrs[key]; ok { - return nil, errors.Errorf(`internal config %q must not be specified`, key) - } - } - - attrs := map[string]interface{}{ - configAttrLocation: args.CloudRegion, - configAttrEndpoint: args.CloudEndpoint, - configAttrStorageEndpoint: args.CloudStorageEndpoint, - } - switch authType := args.Credentials.AuthType(); authType { - case cloud.UserPassAuthType: - for k, v := range args.Credentials.Attributes() { - attrs[k] = v - } - default: - return nil, errors.NotSupportedf("%q auth-type", authType) - } - cfg, err := args.Config.Apply(attrs) - if err != nil { - return nil, errors.Annotate(err, "updating config") - } - return cfg, nil +// SecretAttrs is part of the EnvironProvider interface. +func (prov *azureEnvironProvider) SecretAttrs(cfg *config.Config) (map[string]string, error) { + return map[string]string{}, nil } -// PrepareForBootstrap is specified in the EnvironProvider interface. -func (prov *azureEnvironProvider) PrepareForBootstrap(ctx environs.BootstrapContext, cfg *config.Config) (environs.Environ, error) { - env, err := prov.Open(cfg) - if err != nil { - return nil, errors.Trace(err) - } - if ctx.ShouldVerifyCredentials() { - if err := verifyCredentials(env.(*azureEnviron)); err != nil { - return nil, errors.Trace(err) - } +func validateCloudSpec(spec environs.CloudSpec) error { + if err := spec.Validate(); err != nil { + return errors.Trace(err) } - return env, nil -} - -// SecretAttrs is specified in the EnvironProvider interface. -func (prov *azureEnvironProvider) SecretAttrs(cfg *config.Config) (map[string]string, error) { - unknownAttrs := cfg.UnknownAttrs() - secretAttrs := map[string]string{ - configAttrAppPassword: unknownAttrs[configAttrAppPassword].(string), + if spec.Credential == nil { + return errors.NotValidf("missing credential") } - if storageAccountKey, ok := unknownAttrs[configAttrStorageAccountKey].(string); ok { - secretAttrs[configAttrStorageAccountKey] = storageAccountKey + if authType := spec.Credential.AuthType(); authType != cloud.UserPassAuthType { + return errors.NotSupportedf("%q auth-type", authType) } - return secretAttrs, nil + return nil } // verifyCredentials issues a cheap, non-modifying request to Azure to @@ -164,8 +118,6 @@ // error will be returned, and the original error will be logged at debug // level. var verifyCredentials = func(e *azureEnviron) error { - e.mu.Lock() - defer e.mu.Unlock() // TODO(axw) user-friendly error message - return e.config.token.EnsureFresh() + return e.token.EnsureFresh() } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/environprovider_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/environprovider_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/environprovider_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/environprovider_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,7 +5,6 @@ import ( "bytes" - "fmt" "io/ioutil" "net/http" "sync" @@ -19,13 +18,13 @@ "github.com/juju/juju/environs" "github.com/juju/juju/provider/azure" "github.com/juju/juju/provider/azure/internal/azuretesting" - "github.com/juju/juju/storage" "github.com/juju/juju/testing" ) type environProviderSuite struct { testing.BaseSuite provider environs.EnvironProvider + spec environs.CloudSpec requests []*http.Request sender azuretesting.Senders } @@ -34,59 +33,74 @@ func (s *environProviderSuite) SetUpTest(c *gc.C) { s.BaseSuite.SetUpTest(c) - s.provider, _ = newProviders(c, azure.ProviderConfig{ + s.provider = newProvider(c, azure.ProviderConfig{ Sender: &s.sender, RequestInspector: requestRecorder(&s.requests), }) + s.spec = environs.CloudSpec{ + Type: "azure", + Name: "azure", + Region: "westus", + Endpoint: "https://api.azurestack.local", + StorageEndpoint: "https://storage.azurestack.local", + Credential: fakeUserPassCredential(), + } s.sender = nil } -func (s *environProviderSuite) TestBootstrapConfigWithInternalConfig(c *gc.C) { - s.testBootstrapConfigWithInternalConfig(c, "storage-account") -} - -func (s *environProviderSuite) testBootstrapConfigWithInternalConfig(c *gc.C, key string) { - cfg := makeTestModelConfig(c, testing.Attrs{key: "whatever"}) - s.sender = azuretesting.Senders{tokenRefreshSender()} - _, err := s.provider.BootstrapConfig(environs.BootstrapConfigParams{ - Config: cfg, - Credentials: fakeUserPassCredential(), - }) - c.Check(err, gc.ErrorMatches, fmt.Sprintf(`internal config "%s" must not be specified`, key)) -} - -func fakeUserPassCredential() cloud.Credential { - return cloud.NewCredential( +func fakeUserPassCredential() *cloud.Credential { + cred := cloud.NewCredential( cloud.UserPassAuthType, map[string]string{ - "application-id": "application-id", - "subscription-id": "subscription-id", - "tenant-id": "tenant-id", - "application-password": "application-password", + "application-id": fakeApplicationId, + "subscription-id": fakeSubscriptionId, + "tenant-id": fakeTenantId, + "application-password": "opensezme", }, ) + return &cred } -func (s *environProviderSuite) TestBootstrapConfig(c *gc.C) { +func (s *environProviderSuite) TestPrepareConfig(c *gc.C) { cfg := makeTestModelConfig(c) s.sender = azuretesting.Senders{tokenRefreshSender()} - cfg, err := s.provider.BootstrapConfig(environs.BootstrapConfigParams{ - Config: cfg, - CloudRegion: "westus", - CloudEndpoint: "https://api.azurestack.local", - CloudStorageEndpoint: "https://storage.azurestack.local", - Credentials: fakeUserPassCredential(), + cfg, err := s.provider.PrepareConfig(environs.PrepareConfigParams{ + Cloud: s.spec, + Config: cfg, }) - c.Check(err, jc.ErrorIsNil) + c.Assert(err, jc.ErrorIsNil) c.Check(cfg, gc.NotNil) +} - attrs := cfg.UnknownAttrs() - c.Assert(attrs["location"], gc.Equals, "westus") - c.Assert(attrs["endpoint"], gc.Equals, "https://api.azurestack.local") - c.Assert(attrs["storage-endpoint"], gc.Equals, "https://storage.azurestack.local") +func (s *environProviderSuite) TestOpen(c *gc.C) { + env, err := s.provider.Open(environs.OpenParams{ + Cloud: s.spec, + Config: makeTestModelConfig(c), + }) + c.Assert(err, jc.ErrorIsNil) + c.Assert(env, gc.NotNil) +} + +func (s *environProviderSuite) TestOpenMissingCredential(c *gc.C) { + s.spec.Credential = nil + s.testOpenError(c, s.spec, `validating cloud spec: missing credential not valid`) +} + +func (s *environProviderSuite) TestOpenUnsupportedCredential(c *gc.C) { + credential := cloud.NewCredential(cloud.OAuth1AuthType, map[string]string{}) + s.spec.Credential = &credential + s.testOpenError(c, s.spec, `validating cloud spec: "oauth1" auth-type not supported`) +} + +func (s *environProviderSuite) testOpenError(c *gc.C, spec environs.CloudSpec, expect string) { + _, err := s.provider.Open(environs.OpenParams{ + Cloud: spec, + Config: makeTestModelConfig(c), + }) + c.Assert(err, gc.ErrorMatches, expect) } -func newProviders(c *gc.C, config azure.ProviderConfig) (environs.EnvironProvider, storage.Provider) { +func newProvider(c *gc.C, config azure.ProviderConfig) environs.EnvironProvider { if config.NewStorageClient == nil { var storage azuretesting.MockStorageClient config.NewStorageClient = storage.NewClient @@ -99,9 +113,9 @@ if config.RetryClock == nil { config.RetryClock = testing.NewClock(time.Time{}) } - environProvider, storageProvider, err := azure.NewProviders(config) + environProvider, err := azure.NewProvider(config) c.Assert(err, jc.ErrorIsNil) - return environProvider, storageProvider + return environProvider } func requestRecorder(requests *[]*http.Request) autorest.PrepareDecorator { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/environ_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/environ_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/environ_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/environ_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -57,6 +57,7 @@ tags map[string]*string group *resources.ResourceGroup vmSizes *compute.VirtualMachineSizeListResult + storageAccounts []storage.Account storageNameAvailabilityResult *storage.CheckNameAvailabilityResult storageAccount *storage.Account storageAccountKeys *storage.AccountKeys @@ -80,7 +81,7 @@ s.requests = nil s.retryClock = mockClock{Clock: testing.NewClock(time.Time{})} - s.provider, _ = newProviders(c, azure.ProviderConfig{ + s.provider = newProvider(c, azure.ProviderConfig{ Sender: azuretesting.NewSerialSender(&s.sender), RequestInspector: requestRecorder(&s.requests), NewStorageClient: s.storageClient.NewClient, @@ -268,6 +269,8 @@ fakeStorageAccount, )), }, + // 30 GiB is roughly 32 GB. + DiskSizeGB: to.IntPtr(32), }, }, OsProfile: &compute.OSProfile{ @@ -291,7 +294,6 @@ } func (s *environSuite) openEnviron(c *gc.C, attrs ...testing.Attrs) environs.Environ { - attrs = append([]testing.Attrs{{"storage-account": fakeStorageAccount}}, attrs...) return openEnviron(c, s.provider, &s.sender, attrs...) } @@ -304,7 +306,10 @@ // Opening the environment should not incur network communication, // so we don't set s.sender until after opening. cfg := makeTestModelConfig(c, attrs...) - env, err := provider.Open(cfg) + env, err := provider.Open(environs.OpenParams{ + Cloud: fakeCloudSpec(), + Config: cfg, + }) c.Assert(err, jc.ErrorIsNil) // Force an explicit refresh of the access token, so it isn't done @@ -326,19 +331,32 @@ // so we don't set s.sender until after opening. cfg := makeTestModelConfig(c, attrs...) *sender = azuretesting.Senders{tokenRefreshSender()} - cfg, err := provider.BootstrapConfig(environs.BootstrapConfigParams{ - Config: cfg, - CloudRegion: "westus", - CloudEndpoint: "https://management.azure.com", - CloudStorageEndpoint: "https://core.windows.net", - Credentials: fakeUserPassCredential(), + cfg, err := provider.PrepareConfig(environs.PrepareConfigParams{ + Config: cfg, + Cloud: fakeCloudSpec(), + }) + c.Assert(err, jc.ErrorIsNil) + env, err := provider.Open(environs.OpenParams{ + Cloud: fakeCloudSpec(), + Config: cfg, }) c.Assert(err, jc.ErrorIsNil) - env, err := provider.PrepareForBootstrap(ctx, cfg) + err = env.PrepareForBootstrap(ctx) c.Assert(err, jc.ErrorIsNil) return env } +func fakeCloudSpec() environs.CloudSpec { + return environs.CloudSpec{ + Type: "azure", + Name: "azure", + Region: "westus", + Endpoint: "https://api.azurestack.local", + StorageEndpoint: "https://storage.azurestack.local", + Credential: fakeUserPassCredential(), + } +} + func tokenRefreshSender() *azuretesting.MockSender { // lp:1558657 tokenRefreshSender := azuretesting.NewSenderWithValue(&autorestazure.Token{ @@ -359,13 +377,13 @@ s.makeSender(".*/virtualnetworks/juju-internal-network/subnets/juju-internal-subnet", s.subnet), s.makeSender(".*/checkNameAvailability", s.storageNameAvailabilityResult), s.makeSender(".*/storageAccounts/.*", s.storageAccount), - s.makeSender(".*/storageAccounts/.*/listKeys", s.storageAccountKeys), } } func (s *environSuite) startInstanceSenders(controller bool) azuretesting.Senders { senders := azuretesting.Senders{ s.vmSizesSender(), + s.storageAccountsSender(), s.makeSender(".*/subnets/juju-internal-subnet", s.subnet), s.makeSender(".*/Canonical/.*/UbuntuServer/skus", s.ubuntuServerSKUs), s.makeSender(".*/publicIPAddresses/machine-0-public-ip", s.publicIPAddress), @@ -403,6 +421,15 @@ return s.makeSender(".*/vmSizes", s.vmSizes) } +func (s *environSuite) storageAccountsSender() *azuretesting.MockSender { + accounts := []storage.Account{*s.storageAccount} + return s.makeSender(".*/storageAccounts", storage.AccountListResult{Value: &accounts}) +} + +func (s *environSuite) storageAccountKeysSender() *azuretesting.MockSender { + return s.makeSender(".*/storageAccounts/.*/listKeys", s.storageAccountKeys) +} + func (s *environSuite) makeSender(pattern string, v interface{}) *azuretesting.MockSender { sender := azuretesting.NewSenderWithValue(v) sender.PathPattern = pattern @@ -471,9 +498,7 @@ } func (s *environSuite) TestOpen(c *gc.C) { - cfg := makeTestModelConfig(c) - env, err := s.provider.Open(cfg) - c.Assert(err, jc.ErrorIsNil) + env := s.openEnviron(c) c.Assert(env, gc.NotNil) } @@ -504,7 +529,7 @@ arch := "amd64" mem := uint64(3584) - rootDisk := uint64(29495) // ~30 GB + rootDisk := uint64(30 * 1024) // 30 GiB cpuCores := uint64(1) c.Assert(result.Hardware, jc.DeepEquals, &instance.HardwareCharacteristics{ Arch: &arch, @@ -540,12 +565,12 @@ _, err := env.StartInstance(makeStartInstanceParams(c, s.controllerUUID, "quantal")) c.Assert(err, jc.ErrorIsNil) - c.Assert(s.requests, gc.HasLen, 8+failures) - s.assertStartInstanceRequests(c, s.requests[:8]) + c.Assert(s.requests, gc.HasLen, 9+failures) + s.assertStartInstanceRequests(c, s.requests[:9]) // The last two requests should match the third-to-last, which // is checked by assertStartInstanceRequests. - for i := 8; i < 8+failures; i++ { + for i := 9; i < 9+failures; i++ { c.Assert(s.requests[i].Method, gc.Equals, "PUT") assertCreateVirtualMachineRequestBody(c, s.requests[i], s.virtualMachine) } @@ -630,29 +655,31 @@ s.virtualMachine.Properties.ProvisioningState = nil // Validate HTTP request bodies. - c.Assert(requests, gc.HasLen, 8) + c.Assert(requests, gc.HasLen, 9) c.Assert(requests[0].Method, gc.Equals, "GET") // vmSizes - c.Assert(requests[1].Method, gc.Equals, "GET") // juju-testenv-model-deadbeef-0bad-400d-8000-4b1d0d06f00d - c.Assert(requests[2].Method, gc.Equals, "GET") // skus - c.Assert(requests[3].Method, gc.Equals, "PUT") - assertRequestBody(c, requests[3], s.publicIPAddress) - c.Assert(requests[4].Method, gc.Equals, "GET") // NICs - c.Assert(requests[5].Method, gc.Equals, "PUT") - assertRequestBody(c, requests[5], s.newNetworkInterface) + c.Assert(requests[1].Method, gc.Equals, "GET") // storage accounts + c.Assert(requests[2].Method, gc.Equals, "GET") // juju-testenv-model-deadbeef-0bad-400d-8000-4b1d0d06f00d + c.Assert(requests[3].Method, gc.Equals, "GET") // skus + c.Assert(requests[4].Method, gc.Equals, "PUT") + assertRequestBody(c, requests[4], s.publicIPAddress) + c.Assert(requests[5].Method, gc.Equals, "GET") // NICs c.Assert(requests[6].Method, gc.Equals, "PUT") - assertRequestBody(c, requests[6], s.jujuAvailabilitySet) + assertRequestBody(c, requests[6], s.newNetworkInterface) c.Assert(requests[7].Method, gc.Equals, "PUT") - assertCreateVirtualMachineRequestBody(c, requests[7], s.virtualMachine) + assertRequestBody(c, requests[7], s.jujuAvailabilitySet) + c.Assert(requests[8].Method, gc.Equals, "PUT") + assertCreateVirtualMachineRequestBody(c, requests[8], s.virtualMachine) return startInstanceRequests{ vmSizes: requests[0], - subnet: requests[1], - skus: requests[2], - publicIPAddress: requests[3], - nics: requests[4], - networkInterface: requests[5], - availabilitySet: requests[6], - virtualMachine: requests[7], + storageAccounts: requests[1], + subnet: requests[2], + skus: requests[3], + publicIPAddress: requests[4], + nics: requests[5], + networkInterface: requests[6], + availabilitySet: requests[7], + virtualMachine: requests[8], } } @@ -668,6 +695,7 @@ type startInstanceRequests struct { vmSizes *http.Request + storageAccounts *http.Request subnet *http.Request skus *http.Request publicIPAddress *http.Request @@ -704,7 +732,6 @@ c.Assert(s.requests[3].Method, gc.Equals, "PUT") // subnet c.Assert(s.requests[4].Method, gc.Equals, "POST") // check storage account name c.Assert(s.requests[5].Method, gc.Equals, "PUT") // create storage account - c.Assert(s.requests[6].Method, gc.Equals, "POST") // get storage account keys assertRequestBody(c, s.requests[0], &s.group) @@ -797,6 +824,8 @@ s.publicIPAddressesSender( makePublicIPAddress("pip-0", "machine-0", "1.2.3.4"), ), + s.storageAccountsSender(), + s.storageAccountKeysSender(), s.makeSender(".*/virtualMachines/machine-0", nil), // DELETE s.makeSender(".*/networkSecurityGroups/juju-internal-nsg", nsg), // GET s.makeSender(".*/networkSecurityGroups/juju-internal-nsg/securityRules/machine-0-80", nil), // DELETE diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/export_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -13,5 +13,5 @@ } func ForceTokenRefresh(env environs.Environ) error { - return env.(*azureEnviron).config.token.Refresh() + return env.(*azureEnviron).token.Refresh() } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/init.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/init.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/init.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/init.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,27 +9,24 @@ "github.com/juju/juju/environs" "github.com/juju/juju/provider/azure/internal/azurestorage" - "github.com/juju/juju/storage" - "github.com/juju/juju/storage/provider/registry" ) const ( - providerType = "azure" - storageProviderType storage.ProviderType = "azure" + providerType = "azure" ) -// NewProviders instantiates and returns Azure providers using the given -// configuration. -func NewProviders(config ProviderConfig) (environs.EnvironProvider, storage.Provider, error) { +// NewProvider instantiates and returns the Azure EnvironProvider using the +// given configuration. +func NewProvider(config ProviderConfig) (environs.EnvironProvider, error) { environProvider, err := NewEnvironProvider(config) if err != nil { - return nil, nil, errors.Trace(err) + return nil, errors.Trace(err) } - return environProvider, &azureStorageProvider{environProvider}, nil + return environProvider, nil } func init() { - environProvider, storageProvider, err := NewProviders(ProviderConfig{ + environProvider, err := NewProvider(ProviderConfig{ NewStorageClient: azurestorage.NewClient, StorageAccountNameGenerator: RandomStorageAccountName, RetryClock: &clock.WallClock, @@ -39,8 +36,6 @@ } environs.RegisterProvider(providerType, environProvider) - registry.RegisterProvider(storageProviderType, storageProvider) - registry.RegisterEnvironStorageProviders(providerType, storageProviderType) // TODO(axw) register an image metadata data source that queries // the Azure image registry, and introduce a way to disable the diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/instance.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/instance.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/instance.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/instance.go 2016-08-16 08:56:25.000000000 +0000 @@ -153,10 +153,8 @@ // internal virtual network. This address is used to identify the machine in // network security rules. func (inst *azureInstance) internalNetworkAddress() (jujunetwork.Address, error) { - inst.env.mu.Lock() - subscriptionId := inst.env.config.subscriptionId + subscriptionId := inst.env.subscriptionId resourceGroup := inst.env.resourceGroup - inst.env.mu.Unlock() internalSubnetId := internalSubnetId(resourceGroup, subscriptionId) for _, nic := range inst.networkInterfaces { @@ -185,10 +183,8 @@ // OpenPorts is specified in the Instance interface. func (inst *azureInstance) OpenPorts(machineId string, ports []jujunetwork.PortRange) error { - inst.env.mu.Lock() nsgClient := network.SecurityGroupsClient{inst.env.network} securityRuleClient := network.SecurityRulesClient{inst.env.network} - inst.env.mu.Unlock() internalNetworkAddress, err := inst.internalNetworkAddress() if err != nil { return errors.Trace(err) @@ -283,9 +279,7 @@ // ClosePorts is specified in the Instance interface. func (inst *azureInstance) ClosePorts(machineId string, ports []jujunetwork.PortRange) error { - inst.env.mu.Lock() securityRuleClient := network.SecurityRulesClient{inst.env.network} - inst.env.mu.Unlock() securityGroupName := internalSecurityGroupName // Delete rules one at a time; this is necessary to avoid trampling @@ -313,10 +307,7 @@ // Ports is specified in the Instance interface. func (inst *azureInstance) Ports(machineId string) (ports []jujunetwork.PortRange, err error) { - inst.env.mu.Lock() nsgClient := network.SecurityGroupsClient{inst.env.network} - inst.env.mu.Unlock() - securityGroupName := internalSecurityGroupName var nsg network.SecurityGroup if err := inst.env.callAPI(func() (autorest.Response, error) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/instance_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/instance_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/instance_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/instance_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -38,7 +38,7 @@ func (s *instanceSuite) SetUpTest(c *gc.C) { s.BaseSuite.SetUpTest(c) - s.provider, _ = newProviders(c, azure.ProviderConfig{ + s.provider = newProvider(c, azure.ProviderConfig{ Sender: &s.sender, RequestInspector: requestRecorder(&s.requests), }) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/instancetype.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/instancetype.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/instancetype.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/instancetype.go 2016-08-16 08:56:25.000000000 +0000 @@ -107,20 +107,21 @@ Arches: []string{arch.AMD64}, CpuCores: uint64(to.Int(size.NumberOfCores)), Mem: uint64(to.Int(size.MemoryInMB)), - // NOTE(axw) size.OsDiskSizeInMB is the maximum root disk - // size, but the actual disk size is limited to the size - // of the image/VHD that the machine is backed by. The - // Azure Resource Manager APIs do not provide a way of - // determining the image size. - // - // All of the published images that we use are ~30GiB. - RootDisk: uint64(29495), + // NOTE(axw) size.OsDiskSizeInMB is the *maximum* + // OS-disk size. When we create a VM, we can create + // one that is smaller. + RootDisk: mbToMib(uint64(to.Int(size.OsDiskSizeInMB))), Cost: uint64(cost), VirtType: &vtype, // tags are not currently supported by azure } } +func mbToMib(mb uint64) uint64 { + b := mb * 1000 * 1000 + return uint64(float64(b) / 1024 / 1024) +} + // findInstanceSpec returns the InstanceSpec that best satisfies the supplied // InstanceConstraint. // diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/storage.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/storage.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/storage.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/storage.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,6 +10,7 @@ "github.com/Azure/azure-sdk-for-go/Godeps/_workspace/src/github.com/Azure/go-autorest/autorest" "github.com/Azure/azure-sdk-for-go/Godeps/_workspace/src/github.com/Azure/go-autorest/autorest/to" "github.com/Azure/azure-sdk-for-go/arm/compute" + armstorage "github.com/Azure/azure-sdk-for-go/arm/storage" azurestorage "github.com/Azure/azure-sdk-for-go/storage" "github.com/juju/errors" "github.com/juju/schema" @@ -17,13 +18,14 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/environs" - "github.com/juju/juju/environs/config" "github.com/juju/juju/instance" internalazurestorage "github.com/juju/juju/provider/azure/internal/azurestorage" "github.com/juju/juju/storage" ) const ( + azureStorageProviderType = "azure" + // volumeSizeMaxGiB is the maximum disk size (in gibibytes) for Azure disks. // // See: https://azure.microsoft.com/en-gb/documentation/articles/virtual-machines-disks-vhds/ @@ -41,9 +43,22 @@ vhdExtension = ".vhd" ) +// StorageProviderTypes implements storage.ProviderRegistry. +func (env *azureEnviron) StorageProviderTypes() []storage.ProviderType { + return []storage.ProviderType{azureStorageProviderType} +} + +// StorageProvider implements storage.ProviderRegistry. +func (env *azureEnviron) StorageProvider(t storage.ProviderType) (storage.Provider, error) { + if t == azureStorageProviderType { + return &azureStorageProvider{env}, nil + } + return nil, errors.NotFoundf("storage provider %q", t) +} + // azureStorageProvider is a storage provider for Azure disks. type azureStorageProvider struct { - environProvider *azureEnvironProvider + env *azureEnviron } var _ storage.Provider = (*azureStorageProvider)(nil) @@ -67,43 +82,42 @@ return azureStorageConfig, nil } -// ValidateConfig is defined on the Provider interface. +// ValidateConfig is part of the Provider interface. func (e *azureStorageProvider) ValidateConfig(cfg *storage.Config) error { _, err := newAzureStorageConfig(cfg.Attrs()) return errors.Trace(err) } -// Supports is defined on the Provider interface. +// Supports is part of the Provider interface. func (e *azureStorageProvider) Supports(k storage.StorageKind) bool { return k == storage.StorageKindBlock } -// Scope is defined on the Provider interface. +// Scope is part of the Provider interface. func (e *azureStorageProvider) Scope() storage.Scope { return storage.ScopeEnviron } -// Dynamic is defined on the Provider interface. +// Dynamic is part of the Provider interface. func (e *azureStorageProvider) Dynamic() bool { return true } -// VolumeSource is defined on the Provider interface. -func (e *azureStorageProvider) VolumeSource(environConfig *config.Config, cfg *storage.Config) (storage.VolumeSource, error) { +// DefaultPools is part of the Provider interface. +func (e *azureStorageProvider) DefaultPools() []*storage.Config { + return nil +} + +// VolumeSource is part of the Provider interface. +func (e *azureStorageProvider) VolumeSource(cfg *storage.Config) (storage.VolumeSource, error) { if err := e.ValidateConfig(cfg); err != nil { return nil, errors.Trace(err) } - env, err := newEnviron(e.environProvider, environConfig) - if err != nil { - return nil, errors.Trace(err) - } - return &azureVolumeSource{env}, nil + return &azureVolumeSource{e.env}, nil } -// FilesystemSource is defined on the Provider interface. -func (e *azureStorageProvider) FilesystemSource( - environConfig *config.Config, providerConfig *storage.Config, -) (storage.FilesystemSource, error) { +// FilesystemSource is part of the Provider interface. +func (e *azureStorageProvider) FilesystemSource(providerConfig *storage.Config) (storage.FilesystemSource, error) { return nil, errors.NotSupportedf("filesystems") } @@ -131,6 +145,10 @@ if err != nil { return nil, errors.Annotate(err, "getting virtual machines") } + storageAccount, err := v.env.getStorageAccount(false) + if err != nil { + return nil, errors.Trace(err) + } // Update VirtualMachine objects in-memory, // and then perform the updates all at once. @@ -146,7 +164,9 @@ results[i].Error = vm.err continue } - volume, volumeAttachment, err := v.createVolume(vm.vm, p) + volume, volumeAttachment, err := v.createVolume( + vm.vm, p, storageAccount, + ) if err != nil { results[i].Error = err vm.err = err @@ -177,6 +197,7 @@ func (v *azureVolumeSource) createVolume( vm *compute.VirtualMachine, p storage.VolumeParams, + storageAccount *armstorage.Account, ) (*storage.Volume, *storage.VolumeAttachment, error) { lun, err := nextAvailableLUN(vm) @@ -184,7 +205,7 @@ return nil, nil, errors.Annotate(err, "choosing LUN") } - dataDisksRoot := dataDiskVhdRoot(v.env.config.storageEndpoint, v.env.config.storageAccount) + dataDisksRoot := dataDiskVhdRoot(storageAccount) dataDiskName := p.Tag.String() vhdURI := dataDisksRoot + dataDiskName + vhdExtension @@ -347,6 +368,10 @@ if err != nil { return nil, errors.Annotate(err, "getting virtual machines") } + storageAccount, err := v.env.getStorageAccount(false) + if err != nil { + return nil, errors.Trace(err) + } // Update VirtualMachine objects in-memory, // and then perform the updates all at once. @@ -364,7 +389,9 @@ results[i].Error = vm.err continue } - volumeAttachment, updated, err := v.attachVolume(vm.vm, p) + volumeAttachment, updated, err := v.attachVolume( + vm.vm, p, storageAccount, + ) if err != nil { results[i].Error = err vm.err = err @@ -398,9 +425,15 @@ func (v *azureVolumeSource) attachVolume( vm *compute.VirtualMachine, p storage.VolumeAttachmentParams, + storageAccount *armstorage.Account, ) (_ *storage.VolumeAttachment, updated bool, _ error) { - dataDisksRoot := dataDiskVhdRoot(v.env.config.storageEndpoint, v.env.config.storageAccount) + storageAccount, err := v.env.getStorageAccount(false) + if err != nil { + return nil, false, errors.Trace(err) + } + + dataDisksRoot := dataDiskVhdRoot(storageAccount) dataDiskName := p.VolumeId vhdURI := dataDisksRoot + dataDiskName + vhdExtension @@ -465,6 +498,10 @@ if err != nil { return nil, errors.Annotate(err, "getting virtual machines") } + storageAccount, err := v.env.getStorageAccount(false) + if err != nil { + return nil, errors.Annotate(err, "getting storage account") + } // Update VirtualMachine objects in-memory, // and then perform the updates all at once. @@ -482,7 +519,7 @@ results[i] = vm.err continue } - if v.detachVolume(vm.vm, p) { + if v.detachVolume(vm.vm, p, storageAccount) { changed[p.InstanceId] = true } } @@ -508,9 +545,10 @@ func (v *azureVolumeSource) detachVolume( vm *compute.VirtualMachine, p storage.VolumeAttachmentParams, + storageAccount *armstorage.Account, ) (updated bool) { - dataDisksRoot := dataDiskVhdRoot(v.env.config.storageEndpoint, v.env.config.storageAccount) + dataDisksRoot := dataDiskVhdRoot(storageAccount) dataDiskName := p.VolumeId vhdURI := dataDisksRoot + dataDiskName + vhdExtension @@ -650,22 +688,21 @@ // osDiskVhdRoot returns the URL to the blob container in which we store the // VHDs for OS disks for the environment. -func osDiskVhdRoot(storageEndpoint, storageAccountName string) string { - return blobContainerURL(storageEndpoint, storageAccountName, osDiskVHDContainer) +func osDiskVhdRoot(storageAccount *armstorage.Account) string { + return blobContainerURL(storageAccount, osDiskVHDContainer) } // dataDiskVhdRoot returns the URL to the blob container in which we store the // VHDs for data disks for the environment. -func dataDiskVhdRoot(storageEndpoint, storageAccountName string) string { - return blobContainerURL(storageEndpoint, storageAccountName, dataDiskVHDContainer) +func dataDiskVhdRoot(storageAccount *armstorage.Account) string { + return blobContainerURL(storageAccount, dataDiskVHDContainer) } // blobContainer returns the URL to the named blob container. -func blobContainerURL(storageEndpoint, storageAccountName, container string) string { +func blobContainerURL(storageAccount *armstorage.Account, container string) string { return fmt.Sprintf( - "https://%s.blob.%s/%s/", - storageAccountName, - storageEndpoint, + "%s%s/", + to.String(storageAccount.Properties.PrimaryEndpoints.Blob), container, ) } @@ -687,11 +724,12 @@ // and a constructor. func getStorageClient( newClient internalazurestorage.NewClientFunc, - cfg *azureModelConfig, + storageEndpoint string, + storageAccount *armstorage.Account, + storageAccountKeys *armstorage.AccountKeys, ) (internalazurestorage.Client, error) { - storageAccountName := cfg.storageAccount - storageAccountKey := cfg.storageAccountKey - storageEndpoint := cfg.storageEndpoint + storageAccountName := to.String(storageAccount.Name) + storageAccountKey := to.String(storageAccountKeys.Key1) const useHTTPS = true return newClient( storageAccountName, storageAccountKey, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/storage_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/storage_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/azure/storage_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/azure/storage_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,6 +10,7 @@ "github.com/Azure/azure-sdk-for-go/Godeps/_workspace/src/github.com/Azure/go-autorest/autorest/to" "github.com/Azure/azure-sdk-for-go/arm/compute" "github.com/Azure/azure-sdk-for-go/arm/network" + armstorage "github.com/Azure/azure-sdk-for-go/arm/storage" azurestorage "github.com/Azure/azure-sdk-for-go/storage" "github.com/juju/errors" jc "github.com/juju/testing/checkers" @@ -38,24 +39,24 @@ s.BaseSuite.SetUpTest(c) s.storageClient = azuretesting.MockStorageClient{} s.requests = nil - _, s.provider = newProviders(c, azure.ProviderConfig{ + envProvider := newProvider(c, azure.ProviderConfig{ Sender: &s.sender, NewStorageClient: s.storageClient.NewClient, RequestInspector: requestRecorder(&s.requests), }) s.sender = nil + + var err error + env := openEnviron(c, envProvider, &s.sender) + s.provider, err = env.StorageProvider("azure") + c.Assert(err, jc.ErrorIsNil) } func (s *storageSuite) volumeSource(c *gc.C, attrs ...testing.Attrs) storage.VolumeSource { storageConfig, err := storage.NewConfig("azure", "azure", nil) c.Assert(err, jc.ErrorIsNil) - attrs = append([]testing.Attrs{{ - "storage-account": fakeStorageAccount, - "storage-account-key": fakeStorageAccountKey, - }}, attrs...) - cfg := makeTestModelConfig(c, attrs...) - volumeSource, err := s.provider.VolumeSource(cfg, storageConfig) + volumeSource, err := s.provider.VolumeSource(storageConfig) c.Assert(err, jc.ErrorIsNil) // Force an explicit refresh of the access token, so it isn't done @@ -66,6 +67,32 @@ return volumeSource } +func (s *storageSuite) accountsSender() *azuretesting.MockSender { + envTags := map[string]*string{ + "juju-model-uuid": to.StringPtr(testing.ModelTag.Id()), + } + accounts := []armstorage.Account{{ + Name: to.StringPtr(fakeStorageAccount), + Type: to.StringPtr("Standard_LRS"), + Tags: &envTags, + Properties: &armstorage.AccountProperties{ + PrimaryEndpoints: &armstorage.Endpoints{ + Blob: to.StringPtr(fmt.Sprintf("https://%s.blob.storage.azurestack.local/", fakeStorageAccount)), + }, + }, + }} + accountsSender := azuretesting.NewSenderWithValue(armstorage.AccountListResult{Value: &accounts}) + accountsSender.PathPattern = ".*/storageAccounts" + return accountsSender +} + +func (s *storageSuite) accountKeysSender() *azuretesting.MockSender { + keys := armstorage.AccountKeys{Key1: to.StringPtr(fakeStorageAccountKey), Key2: to.StringPtr("key2")} + keysSender := azuretesting.NewSenderWithValue(&keys) + keysSender.PathPattern = ".*/storageAccounts/.*/listKeys" + return keysSender +} + func (s *storageSuite) TestVolumeSource(c *gc.C) { vs := s.volumeSource(c) c.Assert(vs, gc.NotNil) @@ -75,8 +102,7 @@ storageConfig, err := storage.NewConfig("azure", "azure", nil) c.Assert(err, jc.ErrorIsNil) - cfg := makeTestModelConfig(c) - _, err = s.provider.FilesystemSource(cfg, storageConfig) + _, err = s.provider.FilesystemSource(storageConfig) c.Assert(err, gc.ErrorMatches, "filesystems not supported") c.Assert(err, jc.Satisfies, errors.IsNotSupported) } @@ -170,6 +196,7 @@ s.sender = azuretesting.Senders{ nicsSender, virtualMachinesSender, + s.accountsSender(), updateVirtualMachine0Sender, updateVirtualMachine1Sender, } @@ -185,11 +212,12 @@ c.Check(results[4].Error, gc.ErrorMatches, "choosing LUN: all LUNs are in use") // Validate HTTP request bodies. - c.Assert(s.requests, gc.HasLen, 4) + c.Assert(s.requests, gc.HasLen, 5) c.Assert(s.requests[0].Method, gc.Equals, "GET") // list NICs c.Assert(s.requests[1].Method, gc.Equals, "GET") // list virtual machines - c.Assert(s.requests[2].Method, gc.Equals, "PUT") // update machine-0 - c.Assert(s.requests[3].Method, gc.Equals, "PUT") // update machine-1 + c.Assert(s.requests[2].Method, gc.Equals, "GET") // list storage accounts + c.Assert(s.requests[3].Method, gc.Equals, "PUT") // update machine-0 + c.Assert(s.requests[4].Method, gc.Equals, "PUT") // update machine-1 machine0DataDisks := []compute.DataDisk{{ Lun: to.IntPtr(0), @@ -213,7 +241,7 @@ CreateOption: compute.Empty, }} virtualMachines[0].Properties.StorageProfile.DataDisks = &machine0DataDisks - assertRequestBody(c, s.requests[2], &virtualMachines[0]) + assertRequestBody(c, s.requests[3], &virtualMachines[0]) machine1DataDisks = append(machine1DataDisks, compute.DataDisk{ Lun: to.IntPtr(1), @@ -226,7 +254,7 @@ Caching: compute.ReadWrite, CreateOption: compute.Empty, }) - assertRequestBody(c, s.requests[3], &virtualMachines[1]) + assertRequestBody(c, s.requests[4], &virtualMachines[1]) } func (s *storageSuite) TestListVolumes(c *gc.C) { @@ -254,6 +282,10 @@ } volumeSource := s.volumeSource(c) + s.sender = azuretesting.Senders{ + s.accountsSender(), + s.accountKeysSender(), + } volumeIds, err := volumeSource.ListVolumes() c.Assert(err, jc.ErrorIsNil) s.storageClient.CheckCallNames(c, "NewClient", "ListBlobs") @@ -267,6 +299,11 @@ func (s *storageSuite) TestListVolumesErrors(c *gc.C) { volumeSource := s.volumeSource(c) + s.sender = azuretesting.Senders{ + s.accountsSender(), + s.accountKeysSender(), + } + s.storageClient.SetErrors(errors.New("no client for you")) _, err := volumeSource.ListVolumes() c.Assert(err, gc.ErrorMatches, "listing volumes: getting storage client: no client for you") @@ -297,6 +334,10 @@ } volumeSource := s.volumeSource(c) + s.sender = azuretesting.Senders{ + s.accountsSender(), + s.accountKeysSender(), + } results, err := volumeSource.DescribeVolumes([]string{"volume-0", "volume-1", "volume-0", "volume-42"}) c.Assert(err, jc.ErrorIsNil) s.storageClient.CheckCallNames(c, "NewClient", "ListBlobs") @@ -329,6 +370,10 @@ func (s *storageSuite) TestDestroyVolumes(c *gc.C) { volumeSource := s.volumeSource(c) + s.sender = azuretesting.Senders{ + s.accountsSender(), + s.accountKeysSender(), + } results, err := volumeSource.DestroyVolumes([]string{"volume-0", "volume-42"}) c.Assert(err, jc.ErrorIsNil) c.Assert(results, gc.HasLen, 2) @@ -425,6 +470,7 @@ s.sender = azuretesting.Senders{ nicsSender, virtualMachinesSender, + s.accountsSender(), updateVirtualMachine0Sender, } @@ -439,10 +485,11 @@ c.Check(results[4].Error, gc.ErrorMatches, "choosing LUN: all LUNs are in use") // Validate HTTP request bodies. - c.Assert(s.requests, gc.HasLen, 3) + c.Assert(s.requests, gc.HasLen, 4) c.Assert(s.requests[0].Method, gc.Equals, "GET") // list NICs c.Assert(s.requests[1].Method, gc.Equals, "GET") // list virtual machines - c.Assert(s.requests[2].Method, gc.Equals, "PUT") // update machine-0 + c.Assert(s.requests[2].Method, gc.Equals, "GET") // list storage accounts + c.Assert(s.requests[3].Method, gc.Equals, "PUT") // update machine-0 machine0DataDisks := []compute.DataDisk{{ Lun: to.IntPtr(0), @@ -464,7 +511,7 @@ CreateOption: compute.Attach, }} virtualMachines[0].Properties.StorageProfile.DataDisks = &machine0DataDisks - assertRequestBody(c, s.requests[2], &virtualMachines[0]) + assertRequestBody(c, s.requests[3], &virtualMachines[0]) } func (s *storageSuite) TestDetachVolumes(c *gc.C) { @@ -548,6 +595,7 @@ s.sender = azuretesting.Senders{ nicsSender, virtualMachinesSender, + s.accountsSender(), updateVirtualMachine0Sender, } @@ -561,15 +609,16 @@ c.Check(results[3], gc.ErrorMatches, "instance machine-42 not found") // Validate HTTP request bodies. - c.Assert(s.requests, gc.HasLen, 3) + c.Assert(s.requests, gc.HasLen, 4) c.Assert(s.requests[0].Method, gc.Equals, "GET") // list NICs c.Assert(s.requests[1].Method, gc.Equals, "GET") // list virtual machines - c.Assert(s.requests[2].Method, gc.Equals, "PUT") // update machine-0 + c.Assert(s.requests[2].Method, gc.Equals, "GET") // list storage accounts + c.Assert(s.requests[3].Method, gc.Equals, "PUT") // update machine-0 machine0DataDisks = []compute.DataDisk{ machine0DataDisks[0], machine0DataDisks[2], } virtualMachines[0].Properties.StorageProfile.DataDisks = &machine0DataDisks - assertRequestBody(c, s.requests[2], &virtualMachines[0]) + assertRequestBody(c, s.requests[3], &virtualMachines[0]) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/client.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/client.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/client.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/client.go 2016-08-16 08:56:25.000000000 +0000 @@ -17,9 +17,8 @@ ) type environClient struct { - conn *gosigma.Client - uuid string - config *environConfig + conn *gosigma.Client + uuid string } type tracer struct{} @@ -29,12 +28,15 @@ } // newClient returns an instance of the CloudSigma client. -var newClient = func(cfg *environConfig) (client *environClient, err error) { - uuid := cfg.UUID() +var newClient = func(cloud environs.CloudSpec, uuid string) (client *environClient, err error) { logger.Debugf("creating CloudSigma client: id=%q", uuid) + credAttrs := cloud.Credential.Attributes() + username := credAttrs[credAttrUsername] + password := credAttrs[credAttrPassword] + // create connection to CloudSigma - conn, err := gosigma.NewClient(cfg.endpoint(), cfg.username(), cfg.password(), nil) + conn, err := gosigma.NewClient(cloud.Endpoint, username, password, nil) if err != nil { return nil, err } @@ -45,9 +47,8 @@ } client = &environClient{ - conn: conn, - uuid: uuid, - config: cfg, + conn: conn, + uuid: uuid, } return client, nil @@ -150,7 +151,12 @@ } //newInstance creates and starts new instance. -func (c *environClient) newInstance(args environs.StartInstanceParams, img *imagemetadata.ImageMetadata, userData []byte) (srv gosigma.Server, drv gosigma.Drive, ar string, err error) { +func (c *environClient) newInstance( + args environs.StartInstanceParams, + img *imagemetadata.ImageMetadata, + userData []byte, + authorizedKeys string, +) (srv gosigma.Server, drv gosigma.Drive, ar string, err error) { defer func() { if err == nil { @@ -198,7 +204,9 @@ } } - cc, err := c.generateSigmaComponents(baseName, constraints, args, drv, userData) + cc, err := c.generateSigmaComponents( + baseName, constraints, args, drv, userData, authorizedKeys, + ) if err != nil { return nil, nil, "", errors.Trace(err) } @@ -227,7 +235,14 @@ return srv, drv, ar, nil } -func (c *environClient) generateSigmaComponents(baseName string, constraints *sigmaConstraints, args environs.StartInstanceParams, drv gosigma.Drive, userData []byte) (cc gosigma.Components, err error) { +func (c *environClient) generateSigmaComponents( + baseName string, + constraints *sigmaConstraints, + args environs.StartInstanceParams, + drv gosigma.Drive, + userData []byte, + authorizedKeys string, +) (cc gosigma.Components, err error) { cc.SetName(baseName) cc.SetDescription(baseName) cc.SetSMP(constraints.cores) @@ -240,8 +255,8 @@ return } cc.SetVNCPassword(vncpass) - logger.Debugf("Setting ssh key: %s end", c.config.AuthorizedKeys()) - cc.SetSSHPublicKey(c.config.AuthorizedKeys()) + logger.Debugf("Setting ssh key: %s end", authorizedKeys) + cc.SetSSHPublicKey(authorizedKeys) cc.AttachDrive(1, "0:0", "virtio", drv.UUID()) cc.NetworkDHCP4(gosigma.ModelVirtio) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/client_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/client_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/client_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/client_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -17,6 +17,7 @@ "github.com/juju/version" gc "gopkg.in/check.v1" + "github.com/juju/juju/cloud" "github.com/juju/juju/cloudconfig/instancecfg" "github.com/juju/juju/constraints" "github.com/juju/juju/environs" @@ -55,16 +56,16 @@ } func testNewClient(c *gc.C, endpoint, username, password string) (*environClient, error) { - ecfg := &environConfig{ - Config: newConfig(c, testing.Attrs{"name": "client-test", "uuid": "f54aac3a-9dcd-4a0c-86b5-24091478478c"}), - attrs: map[string]interface{}{ - "region": "testregion", - "endpoint": endpoint, - "username": username, - "password": password, - }, + cred := cloud.NewCredential(cloud.UserPassAuthType, map[string]string{ + "username": username, + "password": password, + }) + spec := environs.CloudSpec{ + Region: "testregion", + Endpoint: endpoint, + Credential: &cred, } - return newClient(ecfg) + return newClient(spec, "f54aac3a-9dcd-4a0c-86b5-24091478478c") } func addTestClientServer(c *gc.C, instance, env string) string { @@ -166,7 +167,7 @@ img := &imagemetadata.ImageMetadata{ Id: validImageId, } - server, drive, arch, err := cli.newInstance(params, img, nil) + server, drive, arch, err := cli.newInstance(params, img, nil, "") c.Check(server, gc.IsNil) c.Check(arch, gc.Equals, "") c.Check(drive, gc.IsNil) @@ -192,7 +193,7 @@ img := &imagemetadata.ImageMetadata{ Id: "invalid-id", } - server, drive, arch, err := cli.newInstance(params, img, nil) + server, drive, arch, err := cli.newInstance(params, img, nil, "") c.Check(server, gc.IsNil) c.Check(arch, gc.Equals, "") c.Check(drive, gc.IsNil) @@ -238,7 +239,7 @@ mock.ResetDrives() mock.LibDrives.Add(templateDrive) - server, drive, arch, err := cli.newInstance(params, img, utils.Gzip([]byte{})) + server, drive, arch, err := cli.newInstance(params, img, utils.Gzip([]byte{}), "") c.Check(server, gc.NotNil) c.Check(drive, gc.NotNil) c.Check(arch, gc.NotNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/config.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/config.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/config.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/config.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,23 +10,11 @@ "github.com/juju/juju/environs/config" ) -var configFields = schema.Fields{ - "username": schema.String(), - "password": schema.String(), - "region": schema.String(), - "endpoint": schema.String(), -} +var configFields = schema.Fields{} var configDefaultFields = schema.Defaults{} -var configSecretFields = []string{ - "password", -} - -var configImmutableFields = []string{ - "region", - "endpoint", -} +var configImmutableFields = []string{} func validateConfig(cfg *config.Config, old *environConfig) (*environConfig, error) { // Check sanity of juju-level fields. @@ -85,34 +73,7 @@ return ecfg, nil } -// configChanged checks if CloudSigma client environment configuration is changed -func (c environConfig) clientConfigChanged(newConfig *environConfig) bool { - // compare - if newConfig.region() != c.region() || newConfig.username() != c.username() || - newConfig.password() != c.password() { - return true - } - - return false -} - type environConfig struct { *config.Config attrs map[string]interface{} } - -func (c environConfig) region() string { - return c.attrs["region"].(string) -} - -func (c environConfig) endpoint() string { - return c.attrs["endpoint"].(string) -} - -func (c environConfig) username() string { - return c.attrs["username"].(string) -} - -func (c environConfig) password() string { - return c.attrs["password"].(string) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/config_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/config_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/config_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/config_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,9 +4,10 @@ package cloudsigma import ( - "github.com/juju/schema" gc "gopkg.in/check.v1" + "github.com/altoros/gosigma/mock" + "github.com/juju/juju/cloud" "github.com/juju/juju/environs" "github.com/juju/juju/environs/config" "github.com/juju/juju/testing" @@ -21,12 +22,26 @@ func validAttrs() testing.Attrs { return testing.FakeConfig().Merge(testing.Attrs{ - "type": "cloudsigma", - "username": "user", - "password": "password", - "region": "zrh", - "endpoint": "https://0.1.2.3:2000/api/2.0/", - "uuid": "f54aac3a-9dcd-4a0c-86b5-24091478478c", + "type": "cloudsigma", + "uuid": "f54aac3a-9dcd-4a0c-86b5-24091478478c", + }) +} + +func fakeCloudSpec() environs.CloudSpec { + cred := fakeCredential() + return environs.CloudSpec{ + Type: "cloudsigma", + Name: "cloudsigma", + Region: "testregion", + Endpoint: "https://0.1.2.3:2000/api/2.0/", + Credential: &cred, + } +} + +func fakeCredential() cloud.Credential { + return cloud.NewCredential(cloud.UserPassAuthType, map[string]string{ + "username": mock.TestUser, + "password": mock.TestPassword, }) } @@ -41,7 +56,7 @@ func (s *configSuite) SetUpTest(c *gc.C) { s.BaseSuite.SetUpTest(c) // speed up tests, do not create heavy stuff inside providers created withing this test suite - s.PatchValue(&newClient, func(cfg *environConfig) (*environClient, error) { + s.PatchValue(&newClient, func(environs.CloudSpec, string) (*environClient, error) { return nil, nil }) } @@ -61,37 +76,13 @@ remove []string expect testing.Attrs err string - }{{ - info: "username is required", - remove: []string{"username"}, - err: "username: expected string, got nothing", - }, { - info: "username cannot be empty", - insert: testing.Attrs{"username": ""}, - err: "username: must not be empty", - }, { - info: "password is required", - remove: []string{"password"}, - err: "password: expected string, got nothing", - }, { - info: "password cannot be empty", - insert: testing.Attrs{"password": ""}, - err: "password: must not be empty", - }, { - info: "region cannot be empty", - remove: []string{"region"}, - err: "region: expected string, got nothing", - }, { - info: "region cannot not be empty", - insert: testing.Attrs{"region": ""}, - err: "region: must not be empty", - }} + }{} for i, test := range newConfigTests { c.Logf("test %d: %s", i, test.info) attrs := validAttrs().Merge(test.insert).Delete(test.remove...) testConfig := newConfig(c, attrs) - environ, err := environs.New(testConfig) + environ, err := environs.New(environs.OpenParams{fakeCloudSpec(), testConfig}) if test.err == "" { c.Check(err, gc.IsNil) attrs := environ.Config().AllAttrs() @@ -118,26 +109,6 @@ }{{ info: "no change, no error", expect: validAttrs(), -}, { - info: "can change username", - insert: testing.Attrs{"username": "cloudsigma_user"}, - expect: testing.Attrs{"username": "cloudsigma_user"}, -}, { - info: "can not change username to empty", - insert: testing.Attrs{"username": ""}, - err: "username: must not be empty", -}, { - info: "can change password", - insert: testing.Attrs{"password": "cloudsigma_password"}, - expect: testing.Attrs{"password": "cloudsigma_password"}, -}, { - info: "can not change password to empty", - insert: testing.Attrs{"password": ""}, - err: "password: must not be empty", -}, { - info: "can change region", - insert: testing.Attrs{"region": "lvs"}, - err: "region: cannot change from .* to .*", }} func (s *configSuite) TestValidateChange(c *gc.C) { @@ -177,7 +148,7 @@ baseConfig := newConfig(c, validAttrs()) for i, test := range changeConfigTests { c.Logf("test %d: %s", i, test.info) - environ, err := environs.New(baseConfig) + environ, err := environs.New(environs.OpenParams{fakeCloudSpec(), baseConfig}) c.Assert(err, gc.IsNil) attrs := validAttrs().Merge(test.insert).Delete(test.remove...) testConfig := newConfig(c, attrs) @@ -197,21 +168,11 @@ } } -func (s *configSuite) TestConfigName(c *gc.C) { - baseConfig := newConfig(c, validAttrs().Merge(testing.Attrs{"name": "testname"})) - environ, err := environs.New(baseConfig) - c.Assert(err, gc.IsNil) - c.Check(environ.Config().Name(), gc.Equals, "testname") -} - func (s *configSuite) TestModelConfig(c *gc.C) { testConfig := newConfig(c, validAttrs()) ecfg, err := validateConfig(testConfig, nil) c.Assert(ecfg, gc.NotNil) c.Assert(err, gc.IsNil) - c.Check(ecfg.username(), gc.Equals, "user") - c.Check(ecfg.password(), gc.Equals, "password") - c.Check(ecfg.region(), gc.Equals, "zrh") } func (s *configSuite) TestInvalidConfigChange(c *gc.C) { @@ -227,96 +188,3 @@ c.Assert(newecfg, gc.IsNil) c.Assert(err, gc.NotNil) } - -var secretAttrsConfigTests = []struct { - info string - insert testing.Attrs - remove []string - expect map[string]string - err string -}{{ - info: "no change, no error", - expect: map[string]string{"password": "password"}, -}, { - info: "invalid config", - insert: testing.Attrs{"username": ""}, - err: ".* must not be empty.*", -}} - -func (s *configSuite) TestSecretAttrs(c *gc.C) { - for i, test := range secretAttrsConfigTests { - c.Logf("test %d: %s", i, test.info) - attrs := validAttrs().Merge(test.insert).Delete(test.remove...) - testConfig := newConfig(c, attrs) - sa, err := providerInstance.SecretAttrs(testConfig) - if test.err == "" { - c.Check(sa, gc.HasLen, len(test.expect)) - for field, value := range test.expect { - c.Check(sa[field], gc.Equals, value) - } - c.Check(err, gc.IsNil) - } else { - c.Check(sa, gc.IsNil) - c.Check(err, gc.ErrorMatches, test.err) - } - } -} - -func (s *configSuite) TestSecretAttrsAreStrings(c *gc.C) { - for i, field := range configSecretFields { - c.Logf("test %d: %s", i, field) - attrs := validAttrs().Merge(testing.Attrs{field: 0}) - - if v, ok := configFields[field]; ok { - configFields[field] = schema.ForceInt() - defer func(c schema.Checker) { - configFields[field] = c - }(v) - } else { - c.Errorf("secrect field %s not found in configFields", field) - continue - } - - testConfig := newConfig(c, attrs) - sa, err := providerInstance.SecretAttrs(testConfig) - c.Check(sa, gc.IsNil) - c.Check(err, gc.ErrorMatches, "secret .* field must have a string value; got .*") - } -} - -func (s *configSuite) TestClientConfigChanged(c *gc.C) { - ecfg := &environConfig{ - Config: newConfig(c, testing.Attrs{"name": "client-test"}), - attrs: map[string]interface{}{ - "region": "https://testing.invalid", - "username": "user", - "password": "password", - }, - } - - oldConfig := &environConfig{ - Config: newConfig(c, testing.Attrs{"name": "client-test"}), - attrs: map[string]interface{}{ - "region": "https://testing.invalid", - "username": "user", - "password": "password", - }, - } - - rc := oldConfig.clientConfigChanged(ecfg) - c.Check(rc, gc.Equals, false) - - ecfg.attrs["region"] = "" - rc = oldConfig.clientConfigChanged(ecfg) - c.Check(rc, gc.Equals, true) - - ecfg.attrs["region"] = "https://testing.invalid" - ecfg.attrs["username"] = "user1" - rc = oldConfig.clientConfigChanged(ecfg) - c.Check(rc, gc.Equals, true) - - ecfg.attrs["username"] = "user" - ecfg.attrs["password"] = "password1" - rc = oldConfig.clientConfigChanged(ecfg) - c.Check(rc, gc.Equals, true) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/credentials.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/credentials.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/credentials.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/credentials.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,19 +10,24 @@ type environProviderCredentials struct{} +const ( + credAttrUsername = "username" + credAttrPassword = "password" +) + // CredentialSchemas is part of the environs.ProviderCredentials interface. func (environProviderCredentials) CredentialSchemas() map[cloud.AuthType]cloud.CredentialSchema { return map[cloud.AuthType]cloud.CredentialSchema{ - cloud.UserPassAuthType: { - { - "username", cloud.CredentialAttr{Description: "account username"}, - }, { - "password", cloud.CredentialAttr{ - Description: "account password", - Hidden: true, - }, + cloud.UserPassAuthType: {{ + "username", cloud.CredentialAttr{ + Description: "account username", + }, + }, { + "password", cloud.CredentialAttr{ + Description: "account password", + Hidden: true, }, - }, + }}, } } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/environcaps.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/environcaps.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/environcaps.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/environcaps.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,8 +4,6 @@ package cloudsigma import ( - "github.com/juju/errors" - "github.com/juju/juju/constraints" "github.com/juju/juju/environs/imagemetadata" "github.com/juju/juju/environs/simplestreams" @@ -57,11 +55,3 @@ func (env *environ) SupportNetworks() bool { return false } - -// SupportsUnitAssignment returns an error which, if non-nil, indicates -// that the environment does not support unit placement. If the environment -// does not support unit placement, then machines may not be created -// without units, and units cannot be placed explcitly. -func (env *environ) SupportsUnitPlacement() error { - return errors.NotImplementedf("SupportsUnitPlacement") -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/environ.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/environ.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/environ.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/environ.go 2016-08-16 08:56:25.000000000 +0000 @@ -24,14 +24,14 @@ // This file contains the core of the Environ implementation. type environ struct { - common.SupportsUnitPlacementPolicy - name string + name string + cloud environs.CloudSpec + client *environClient lock sync.Mutex archMutex sync.Mutex ecfg *environConfig - client *environClient supportedArchitectures []string } @@ -57,16 +57,6 @@ if err != nil { return errors.Trace(err) } - - if env.client == nil || env.ecfg == nil || env.ecfg.clientConfigChanged(ecfg) { - client, err := newClient(ecfg) - if err != nil { - return errors.Trace(err) - } - - env.client = client - } - env.ecfg = ecfg return nil @@ -79,6 +69,17 @@ return env.ecfg.Config } +// PrepareForBootstrap is part of the Environ interface. +func (env *environ) PrepareForBootstrap(ctx environs.BootstrapContext) error { + logger.Infof("preparing model %q", env.name) + return nil +} + +// Create is part of the Environ interface. +func (env *environ) Create(environs.CreateParams) error { + return nil +} + // Bootstrap initializes the state for the environment, possibly // starting one or more instances. If the configuration's // AdminSecret is non-empty, the administrator password on the @@ -133,11 +134,9 @@ // Region is specified in the HasRegion interface. func (env *environ) Region() (simplestreams.CloudSpec, error) { - env.lock.Lock() - defer env.lock.Unlock() return simplestreams.CloudSpec{ - Region: env.ecfg.region(), - Endpoint: env.ecfg.endpoint(), + Region: env.cloud.Region, + Endpoint: env.cloud.Endpoint, }, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/environinstance.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/environinstance.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/environinstance.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/environinstance.go 2016-08-16 08:56:25.000000000 +0000 @@ -73,7 +73,8 @@ logger.Debugf("cloudsigma user data; %d bytes", len(userData)) client := env.client - server, rootdrive, arch, err := client.newInstance(args, img, userData) + cfg := env.Config() + server, rootdrive, arch, err := client.newInstance(args, img, userData, cfg.AuthorizedKeys()) if err != nil { return nil, errors.Errorf("failed start instance: %v", err) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/environinstance_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/environinstance_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/environinstance_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/environinstance_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -24,6 +24,7 @@ type environInstanceSuite struct { testing.BaseSuite + cloud environs.CloudSpec baseConfig *config.Config } @@ -34,13 +35,12 @@ mock.Start() + s.cloud = fakeCloudSpec() + s.cloud.Endpoint = mock.Endpoint("") + attrs := testing.Attrs{ - "name": "testname", - "uuid": "f54aac3a-9dcd-4a0c-86b5-24091478478c", - "region": "testregion", - "endpoint": mock.Endpoint(""), - "username": mock.TestUser, - "password": mock.TestPassword, + "name": "testname", + "uuid": "f54aac3a-9dcd-4a0c-86b5-24091478478c", } s.baseConfig = newConfig(c, validAttrs().Merge(attrs)) } @@ -75,7 +75,11 @@ if cfg == nil { cfg = s.baseConfig } - environ, err := environs.New(cfg) + + environ, err := environs.New(environs.OpenParams{ + Cloud: s.cloud, + Config: cfg, + }) c.Assert(err, gc.IsNil) c.Assert(environ, gc.NotNil) @@ -128,25 +132,18 @@ } func (s *environInstanceSuite) TestInstancesFail(c *gc.C) { - attrs := testing.Attrs{ - "name": "testname", - "region": "testregion", - "endpoint": "https://0.1.2.3:2000/api/2.0/", - "username": mock.TestUser, - "password": mock.TestPassword, - } - baseConfig := newConfig(c, validAttrs().Merge(attrs)) newClientFunc := newClient - s.PatchValue(&newClient, func(cfg *environConfig) (*environClient, error) { - cli, err := newClientFunc(cfg) + s.PatchValue(&newClient, func(spec environs.CloudSpec, uuid string) (*environClient, error) { + spec.Endpoint = "https://0.1.2.3:2000/api/2.0/" + cli, err := newClientFunc(spec, uuid) if cli != nil { cli.conn.ConnectTimeout(10 * time.Millisecond) } return cli, err }) - environ := s.createEnviron(c, baseConfig) + environ := s.createEnviron(c, nil) instances, err := environ.AllInstances() c.Assert(instances, gc.IsNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/environ_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/environ_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/environ_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/environ_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -41,12 +41,15 @@ } func (s *environSuite) TestBase(c *gc.C) { - s.PatchValue(&newClient, func(*environConfig) (*environClient, error) { + s.PatchValue(&newClient, func(environs.CloudSpec, string) (*environClient, error) { return nil, nil }) baseConfig := newConfig(c, validAttrs().Merge(testing.Attrs{"name": "testname"})) - env, err := environs.New(baseConfig) + env, err := environs.New(environs.OpenParams{ + Cloud: fakeCloudSpec(), + Config: baseConfig, + }) c.Assert(err, gc.IsNil) env.(*environ).supportedArchitectures = []string{arch.AMD64} @@ -65,17 +68,15 @@ c.Check(cloudSpec.Region, gc.Not(gc.Equals), "") c.Check(cloudSpec.Endpoint, gc.Not(gc.Equals), "") - archs, err := env.SupportedArchitectures() - c.Check(err, gc.IsNil) - c.Assert(archs, gc.NotNil) - c.Assert(archs, gc.HasLen, 1) - c.Check(archs[0], gc.Equals, arch.AMD64) - validator, err := env.ConstraintsValidator() c.Check(validator, gc.NotNil) c.Check(err, gc.IsNil) - c.Check(env.SupportsUnitPlacement(), gc.ErrorMatches, "SupportsUnitPlacement not implemented") + amd64, i386 := arch.AMD64, arch.I386 + _, err = validator.Validate(constraints.Value{Arch: &amd64}) + c.Check(err, gc.IsNil) + _, err = validator.Validate(constraints.Value{Arch: &i386}) + c.Check(err, gc.ErrorMatches, "invalid constraint value: arch=i386\nvalid values are: \\[amd64\\]") c.Check(env.OpenPorts(nil), gc.IsNil) c.Check(env.ClosePorts(nil), gc.IsNil) @@ -86,12 +87,15 @@ } func (s *environSuite) TestUnsupportedConstraints(c *gc.C) { - s.PatchValue(&newClient, func(*environConfig) (*environClient, error) { + s.PatchValue(&newClient, func(environs.CloudSpec, string) (*environClient, error) { return nil, nil }) baseConfig := newConfig(c, validAttrs().Merge(testing.Attrs{"name": "testname"})) - env, err := environs.New(baseConfig) + env, err := environs.New(environs.OpenParams{ + Cloud: fakeCloudSpec(), + Config: baseConfig, + }) c.Assert(err, gc.IsNil) env.(*environ).supportedArchitectures = []string{arch.AMD64} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/provider.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/provider.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/provider.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/provider.go 2016-08-16 08:56:25.000000000 +0000 @@ -16,7 +16,6 @@ "github.com/juju/juju/environs" "github.com/juju/juju/environs/config" "github.com/juju/juju/environs/simplestreams" - "github.com/juju/juju/storage/provider/registry" ) var logger = loggo.GetLogger("juju.provider.cloudsigma") @@ -30,7 +29,13 @@ if !ok { return nil, errors.NotSupportedf("non-cloudsigma model") } - return simplestreams.NewURLDataSource("cloud images", fmt.Sprintf(CloudsigmaCloudImagesURLTemplate, e.ecfg.region()), utils.VerifySSLHostnames, simplestreams.SPECIFIC_CLOUD_DATA, false), nil + return simplestreams.NewURLDataSource( + "cloud images", + fmt.Sprintf(CloudsigmaCloudImagesURLTemplate, e.cloud.Region), + utils.VerifySSLHostnames, + simplestreams.SPECIFIC_CLOUD_DATA, + false, + ), nil } type environProvider struct { @@ -49,17 +54,27 @@ // except in direct tests for that provider. environs.RegisterProvider("cloudsigma", providerInstance) environs.RegisterImageDataSourceFunc("cloud sigma image source", getImageSource) - registry.RegisterEnvironStorageProviders(providerType) } // Open opens the environment and returns it. // The configuration must have come from a previously // prepared environment. -func (environProvider) Open(cfg *config.Config) (environs.Environ, error) { - logger.Infof("opening model %q", cfg.Name()) +func (environProvider) Open(args environs.OpenParams) (environs.Environ, error) { + logger.Infof("opening model %q", args.Config.Name()) + if err := validateCloudSpec(args.Cloud); err != nil { + return nil, errors.Annotate(err, "validating cloud spec") + } - env := &environ{name: cfg.Name()} - if err := env.SetConfig(cfg); err != nil { + client, err := newClient(args.Cloud, args.Config.UUID()) + if err != nil { + return nil, errors.Trace(err) + } + env := &environ{ + name: args.Config.Name(), + cloud: args.Cloud, + client: client, + } + if err := env.SetConfig(args.Config); err != nil { return nil, err } @@ -70,50 +85,15 @@ // the config that really cannot or should not be changed across // environments running inside a single juju server. func (environProvider) RestrictedConfigAttributes() []string { - return []string{"region"} + return []string{} } -// PrepareForCreateEnvironment prepares an environment for creation. Any -// additional configuration attributes are added to the config passed in -// and returned. This allows providers to add additional required config -// for new environments that may be created in an existing juju server. -func (environProvider) PrepareForCreateEnvironment(controllerUUID string, cfg *config.Config) (*config.Config, error) { - // Not even sure if this will ever make sense. - return nil, errors.NotImplementedf("PrepareForCreateEnvironment") -} - -// BootstrapConfig is defined by EnvironProvider. -func (environProvider) BootstrapConfig(args environs.BootstrapConfigParams) (*config.Config, error) { - cfg := args.Config - switch authType := args.Credentials.AuthType(); authType { - case cloud.UserPassAuthType: - var err error - credentialAttributes := args.Credentials.Attributes() - cfg, err = cfg.Apply(map[string]interface{}{ - "username": credentialAttributes["username"], - "password": credentialAttributes["password"], - }) - if err != nil { - return nil, errors.Trace(err) - } - default: - return nil, errors.NotSupportedf("%q auth-type", authType) - } - // Ensure cloud info is in config. - cfg, err := cfg.Apply(map[string]interface{}{ - "region": args.CloudRegion, - "endpoint": args.CloudEndpoint, - }) - if err != nil { - return nil, errors.Trace(err) +// PrepareConfig is defined by EnvironProvider. +func (environProvider) PrepareConfig(args environs.PrepareConfigParams) (*config.Config, error) { + if err := validateCloudSpec(args.Cloud); err != nil { + return nil, errors.Annotate(err, "validating cloud spec") } - return cfg, nil -} - -// PrepareForBootstrap is defined by EnvironProvider. -func (environProvider) PrepareForBootstrap(ctx environs.BootstrapContext, cfg *config.Config) (environs.Environ, error) { - logger.Infof("preparing model %q", cfg.Name()) - return providerInstance.Open(cfg) + return args.Config, nil } // Validate ensures that config is a valid configuration for this @@ -148,29 +128,18 @@ // which are considered sensitive. All of the values of these secret // attributes need to be strings. func (environProvider) SecretAttrs(cfg *config.Config) (map[string]string, error) { - logger.Infof("filtering secret attributes for model %q", cfg.Name()) + return map[string]string{}, nil +} - // If you keep configSecretFields up to date, this method should Just Work. - ecfg, err := validateConfig(cfg, nil) - if err != nil { - return nil, err +func validateCloudSpec(spec environs.CloudSpec) error { + if err := spec.Validate(); err != nil { + return errors.Trace(err) } - secretAttrs := map[string]string{} - for _, field := range configSecretFields { - if value, ok := ecfg.attrs[field]; ok { - if stringValue, ok := value.(string); ok { - secretAttrs[field] = stringValue - } else { - // All your secret attributes must be strings at the moment. Sorry. - // It's an expedient and hopefully temporary measure that helps us - // plug a security hole in the API. - return nil, errors.Errorf( - "secret %q field must have a string value; got %v", - field, value, - ) - } - } + if spec.Credential == nil { + return errors.NotValidf("missing credential") } - - return secretAttrs, nil + if authType := spec.Credential.AuthType(); authType != cloud.UserPassAuthType { + return errors.NotSupportedf("%q auth-type", authType) + } + return nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/provider_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/provider_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/provider_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/provider_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,19 +4,67 @@ package cloudsigma import ( - "testing" + stdtesting "testing" + "github.com/juju/testing" + jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" - tt "github.com/juju/juju/testing" + "github.com/juju/juju/cloud" + "github.com/juju/juju/environs" ) -func TestCloudSigma(t *testing.T) { +func TestCloudSigma(t *stdtesting.T) { gc.TestingT(t) } type providerSuite struct { - tt.BaseSuite + testing.IsolationSuite + + provider environs.EnvironProvider + spec environs.CloudSpec } var _ = gc.Suite(&providerSuite{}) + +func (s *providerSuite) SetUpTest(c *gc.C) { + s.IsolationSuite.SetUpTest(c) + + provider, err := environs.Provider("cloudsigma") + c.Assert(err, jc.ErrorIsNil) + s.provider = provider + s.spec = fakeCloudSpec() +} + +func (s *providerSuite) TestOpen(c *gc.C) { + env, err := s.provider.Open(environs.OpenParams{ + Cloud: s.spec, + Config: newConfig(c, nil), + }) + c.Assert(err, jc.ErrorIsNil) + c.Assert(env, gc.NotNil) +} + +func (s *providerSuite) TestOpenInvalidCloudSpec(c *gc.C) { + s.spec.Name = "" + s.testOpenError(c, s.spec, `validating cloud spec: cloud name "" not valid`) +} + +func (s *providerSuite) TestOpenMissingCredential(c *gc.C) { + s.spec.Credential = nil + s.testOpenError(c, s.spec, `validating cloud spec: missing credential not valid`) +} + +func (s *providerSuite) TestOpenUnsupportedCredential(c *gc.C) { + credential := cloud.NewCredential(cloud.OAuth1AuthType, map[string]string{}) + s.spec.Credential = &credential + s.testOpenError(c, s.spec, `validating cloud spec: "oauth1" auth-type not supported`) +} + +func (s *providerSuite) testOpenError(c *gc.C, spec environs.CloudSpec, expect string) { + _, err := s.provider.Open(environs.OpenParams{ + Cloud: spec, + Config: newConfig(c, nil), + }) + c.Assert(err, gc.ErrorMatches, expect) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/storage.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/storage.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/cloudsigma/storage.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/cloudsigma/storage.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,20 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package cloudsigma + +import ( + "github.com/juju/errors" + + "github.com/juju/juju/storage" +) + +// StorageProviderTypes implements storage.ProviderRegistry. +func (*environ) StorageProviderTypes() []storage.ProviderType { + return nil +} + +// StorageProvider implements storage.ProviderRegistry. +func (*environ) StorageProvider(t storage.ProviderType) (storage.Provider, error) { + return nil, errors.NotFoundf("storage provider %q", t) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/common/destroy.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/common/destroy.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/common/destroy.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/common/destroy.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,10 +9,8 @@ "github.com/juju/errors" "github.com/juju/juju/environs" - "github.com/juju/juju/environs/config" "github.com/juju/juju/instance" "github.com/juju/juju/storage" - "github.com/juju/juju/storage/provider/registry" ) // Destroy is a common implementation of the Destroy method defined on @@ -51,13 +49,8 @@ func destroyStorage(env environs.Environ) error { logger.Infof("destroying storage") - environConfig := env.Config() - storageProviderTypes, ok := registry.EnvironStorageProviders(environConfig.Type()) - if !ok { - return nil - } - for _, storageProviderType := range storageProviderTypes { - storageProvider, err := registry.StorageProvider(storageProviderType) + for _, storageProviderType := range env.StorageProviderTypes() { + storageProvider, err := env.StorageProvider(storageProviderType) if err != nil { return errors.Trace(err) } @@ -67,7 +60,7 @@ if storageProvider.Scope() != storage.ScopeEnviron { continue } - if err := destroyVolumes(environConfig, storageProviderType, storageProvider); err != nil { + if err := destroyVolumes(storageProviderType, storageProvider); err != nil { return errors.Trace(err) } // TODO(axw) destroy env-level filesystems when we have them. @@ -76,7 +69,6 @@ } func destroyVolumes( - environConfig *config.Config, storageProviderType storage.ProviderType, storageProvider storage.Provider, ) error { @@ -93,7 +85,7 @@ return errors.Trace(err) } - volumeSource, err := storageProvider.VolumeSource(environConfig, storageConfig) + volumeSource, err := storageProvider.VolumeSource(storageConfig) if err != nil { return errors.Annotate(err, "getting volume source") } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/common/destroy_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/common/destroy_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/common/destroy_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/common/destroy_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -13,12 +13,10 @@ gc "gopkg.in/check.v1" "github.com/juju/juju/environs" - "github.com/juju/juju/environs/config" "github.com/juju/juju/instance" "github.com/juju/juju/provider/common" "github.com/juju/juju/storage" "github.com/juju/juju/storage/provider/dummy" - "github.com/juju/juju/storage/provider/registry" "github.com/juju/juju/testing" jujuversion "github.com/juju/juju/version" ) @@ -138,29 +136,30 @@ return make([]error, len(ids)), nil }, } - staticProvider := &dummy.StorageProvider{ + storageProvider := &dummy.StorageProvider{ IsDynamic: true, StorageScope: storage.ScopeEnviron, - VolumeSourceFunc: func(*config.Config, *storage.Config) (storage.VolumeSource, error) { + VolumeSourceFunc: func(*storage.Config) (storage.VolumeSource, error) { return volumeSource, nil }, } - registry.RegisterProvider("environ", staticProvider) - defer registry.RegisterProvider("environ", nil) - registry.RegisterEnvironStorageProviders("anything, really", "environ") - defer registry.ResetEnvironStorageProviders("anything, really") env := &mockEnviron{ config: configGetter(c), allInstances: func() ([]instance.Instance, error) { return nil, environs.ErrNoInstances }, + storageProviders: storage.StaticProviderRegistry{ + map[storage.ProviderType]storage.Provider{ + "environ": storageProvider, + }, + }, } err := common.Destroy(env) c.Assert(err, jc.ErrorIsNil) // common.Destroy will ignore machine-scoped storage providers. - staticProvider.CheckCallNames(c, "Dynamic", "Scope", "Supports", "VolumeSource") + storageProvider.CheckCallNames(c, "Dynamic", "Scope", "Supports", "VolumeSource") volumeSource.CheckCalls(c, []gitjujutesting.StubCall{ {"ListVolumes", nil}, {"DestroyVolumes", []interface{}{[]string{"vol-0", "vol-1", "vol-2"}}}, @@ -181,23 +180,24 @@ }, } - staticProvider := &dummy.StorageProvider{ + storageProvider := &dummy.StorageProvider{ IsDynamic: true, StorageScope: storage.ScopeEnviron, - VolumeSourceFunc: func(*config.Config, *storage.Config) (storage.VolumeSource, error) { + VolumeSourceFunc: func(*storage.Config) (storage.VolumeSource, error) { return volumeSource, nil }, } - registry.RegisterProvider("environ", staticProvider) - defer registry.RegisterProvider("environ", nil) - registry.RegisterEnvironStorageProviders("anything, really", "environ") - defer registry.ResetEnvironStorageProviders("anything, really") env := &mockEnviron{ config: configGetter(c), allInstances: func() ([]instance.Instance, error) { return nil, environs.ErrNoInstances }, + storageProviders: storage.StaticProviderRegistry{ + map[storage.ProviderType]storage.Provider{ + "environ": storageProvider, + }, + }, } err := common.Destroy(env) c.Assert(err, gc.ErrorMatches, "destroying storage: destroying volumes: cannot destroy vol-1, cannot destroy vol-2") @@ -208,16 +208,17 @@ IsDynamic: false, StorageScope: storage.ScopeEnviron, } - registry.RegisterProvider("static", staticProvider) - defer registry.RegisterProvider("static", nil) - registry.RegisterEnvironStorageProviders("anything, really", "static") - defer registry.ResetEnvironStorageProviders("anything, really") env := &mockEnviron{ config: configGetter(c), allInstances: func() ([]instance.Instance, error) { return nil, environs.ErrNoInstances }, + storageProviders: storage.StaticProviderRegistry{ + map[storage.ProviderType]storage.Provider{ + "static": staticProvider, + }, + }, } err := common.Destroy(env) c.Assert(err, jc.ErrorIsNil) @@ -231,16 +232,17 @@ IsDynamic: true, StorageScope: storage.ScopeMachine, } - registry.RegisterProvider("machine", staticProvider) - defer registry.RegisterProvider("machine", nil) - registry.RegisterEnvironStorageProviders("anything, really", "machine") - defer registry.ResetEnvironStorageProviders("anything, really") env := &mockEnviron{ config: configGetter(c), allInstances: func() ([]instance.Instance, error) { return nil, environs.ErrNoInstances }, + storageProviders: storage.StaticProviderRegistry{ + map[storage.ProviderType]storage.Provider{ + "static": staticProvider, + }, + }, } err := common.Destroy(env) c.Assert(err, jc.ErrorIsNil) @@ -257,16 +259,17 @@ return false }, } - registry.RegisterProvider("filesystem", staticProvider) - defer registry.RegisterProvider("filesystem", nil) - registry.RegisterEnvironStorageProviders("anything, really", "filesystem") - defer registry.ResetEnvironStorageProviders("anything, really") env := &mockEnviron{ config: configGetter(c), allInstances: func() ([]instance.Instance, error) { return nil, environs.ErrNoInstances }, + storageProviders: storage.StaticProviderRegistry{ + map[storage.ProviderType]storage.Provider{ + "static": staticProvider, + }, + }, } err := common.Destroy(env) c.Assert(err, jc.ErrorIsNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/common/mock_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/common/mock_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/common/mock_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/common/mock_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -15,6 +15,7 @@ "github.com/juju/juju/instance" "github.com/juju/juju/network" "github.com/juju/juju/provider/common" + jujustorage "github.com/juju/juju/storage" "github.com/juju/juju/tools" ) @@ -33,6 +34,7 @@ getToolsSources getToolsSourcesFunc config configFunc setConfig setConfigFunc + storageProviders jujustorage.StaticProviderRegistry environs.Environ // stub out other methods with panics } @@ -88,6 +90,14 @@ return []simplestreams.DataSource{datasource}, nil } +func (env *mockEnviron) StorageProviderTypes() []jujustorage.ProviderType { + return env.storageProviders.StorageProviderTypes() +} + +func (env *mockEnviron) StorageProvider(t jujustorage.ProviderType) (jujustorage.Provider, error) { + return env.storageProviders.StorageProvider(t) +} + type availabilityZonesFunc func() ([]common.AvailabilityZone, error) type instanceAvailabilityZoneNamesFunc func([]instance.Id) ([]string, error) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/common/policies.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/common/policies.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/common/policies.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/common/policies.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,15 +0,0 @@ -// Copyright 2014 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package common - -// SupportsUnitPlacementPolicy provides an -// implementation of SupportsUnitPlacement -// that never returns an error, and is -// intended for embedding in environs.Environ -// implementations. -type SupportsUnitPlacementPolicy struct{} - -func (*SupportsUnitPlacementPolicy) SupportsUnitPlacement() error { - return nil -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/common/polling.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/common/polling.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/common/polling.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/common/polling.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,6 +11,8 @@ // Use ShortAttempt to poll for short-term events. // TODO: This may need tuning for different providers (or even environments). +// +// TODO(katco): 2016-08-09: lp:1611427 var ShortAttempt = utils.AttemptStrategy{ Total: 5 * time.Second, Delay: 200 * time.Millisecond, @@ -22,6 +24,8 @@ // Other requests fail due to a slow state transition (e.g. an instance taking // a while to release a security group after termination). If you need to // poll for the latter kind, use LongAttempt. +// +// TODO(katco): 2016-08-09: lp:1611427 var LongAttempt = utils.AttemptStrategy{ Total: 3 * time.Minute, Delay: 1 * time.Second, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/common/supportedarchitectures.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/common/supportedarchitectures.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/common/supportedarchitectures.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/common/supportedarchitectures.go 2016-08-16 08:56:25.000000000 +0000 @@ -24,5 +24,5 @@ for _, im := range matchingImages { arches.Add(im.Arch) } - return arches.Values(), nil + return arches.SortedValues(), nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/dummy/config_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/dummy/config_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/dummy/config_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/dummy/config_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -33,9 +33,9 @@ ctx, jujuclienttesting.NewMemStore(), bootstrap.PrepareParams{ ControllerConfig: testing.FakeControllerConfig(), - BaseConfig: attrs, + ModelConfig: attrs, ControllerName: attrs["name"].(string), - CloudName: "dummy", + Cloud: dummy.SampleCloudSpec(), AdminSecret: AdminSecret, }, ) @@ -98,8 +98,8 @@ bootstrap.PrepareParams{ ControllerConfig: testing.FakeControllerConfig(), ControllerName: cfg.Name(), - BaseConfig: cfg.AllAttrs(), - CloudName: "dummy", + ModelConfig: cfg.AllAttrs(), + Cloud: dummy.SampleCloudSpec(), AdminSecret: AdminSecret, }, ) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/dummy/environs.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/dummy/environs.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/dummy/environs.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/dummy/environs.go 2016-08-16 08:56:25.000000000 +0000 @@ -59,6 +59,7 @@ "github.com/juju/juju/provider/common" "github.com/juju/juju/state" "github.com/juju/juju/state/multiwatcher" + "github.com/juju/juju/state/stateenvirons" "github.com/juju/juju/status" "github.com/juju/juju/storage" "github.com/juju/juju/testing" @@ -73,6 +74,19 @@ var errNotPrepared = errors.New("model is not prepared") +// SampleCloudSpec returns an environs.CloudSpec that can be used to +// open a dummy Environ. +func SampleCloudSpec() environs.CloudSpec { + cred := cloud.NewEmptyCredential() + return environs.CloudSpec{ + Type: "dummy", + Name: "dummy", + Endpoint: "dummy-endpoint", + StorageEndpoint: "dummy-storage-endpoint", + Credential: &cred, + } +} + // SampleConfig() returns an environment configuration with all required // attributes set. func SampleConfig() testing.Attrs { @@ -134,6 +148,7 @@ type OpDestroy struct { Env string + Cloud environs.CloudSpec Error error } @@ -196,7 +211,7 @@ type environProvider struct { mu sync.Mutex ops chan<- Operation - statePolicy state.Policy + newStatePolicy state.NewPolicyFunc supportsSpaces bool supportsSpaceDiscovery bool apiPort int @@ -214,7 +229,7 @@ type environState struct { name string ops chan<- Operation - statePolicy state.Policy + newStatePolicy state.NewPolicyFunc mu sync.Mutex maxId int // maximum instance id allocated so far. maxAddr int // maximum allocated address last byte @@ -231,10 +246,10 @@ // environ represents a client's connection to a given environment's // state. type environ struct { - common.SupportsUnitPlacementPolicy - + storage.ProviderRegistry name string modelUUID string + cloud environs.CloudSpec ecfgMutex sync.Mutex ecfgUnlocked *environConfig spacesMutex sync.RWMutex @@ -256,9 +271,11 @@ // dummy is the dummy environmentProvider singleton. var dummy = environProvider{ - ops: discardOperations, - state: make(map[string]*environState), - statePolicy: environs.NewStatePolicy(), + ops: discardOperations, + state: make(map[string]*environState), + newStatePolicy: stateenvirons.GetNewPolicyFunc( + stateenvirons.GetNewEnvironFunc(environs.New), + ), supportsSpaces: true, supportsSpaceDiscovery: false, } @@ -269,54 +286,79 @@ func Reset(c *gc.C) { logger.Infof("reset model") dummy.mu.Lock() - defer dummy.mu.Unlock() dummy.ops = discardOperations - for _, s := range dummy.state { + oldState := dummy.state + dummy.state = make(map[string]*environState) + dummy.newStatePolicy = stateenvirons.GetNewPolicyFunc( + stateenvirons.GetNewEnvironFunc(environs.New), + ) + dummy.supportsSpaces = true + dummy.supportsSpaceDiscovery = false + dummy.mu.Unlock() + + // NOTE(axw) we must destroy the old states without holding + // the provider lock, or we risk deadlocking. Destroying + // state involves closing the embedded API server, which + // may require waiting on RPC calls that interact with the + // EnvironProvider (e.g. EnvironProvider.Open). + for _, s := range oldState { if s.apiListener != nil { s.apiListener.Close() } s.destroy() } - dummy.state = make(map[string]*environState) if mongoAlive() { err := gitjujutesting.MgoServer.Reset() c.Assert(err, jc.ErrorIsNil) } - dummy.statePolicy = environs.NewStatePolicy() - dummy.supportsSpaces = true - dummy.supportsSpaceDiscovery = false } func (state *environState) destroy() { + state.mu.Lock() + defer state.mu.Unlock() + state.destroyLocked() +} + +func (state *environState) destroyLocked() { if !state.bootstrapped { return } + apiServer := state.apiServer + apiStatePool := state.apiStatePool + apiState := state.apiState + state.apiServer = nil + state.apiStatePool = nil + state.apiState = nil + state.bootstrapped = false + + // Release the lock while we close resources. In particular, + // we must not hold the lock while the API server is being + // closed, as it may need to interact with the Environ while + // shutting down. + state.mu.Unlock() + defer state.mu.Lock() - if state.apiServer != nil { - if err := state.apiServer.Stop(); err != nil && mongoAlive() { + if apiServer != nil { + if err := apiServer.Stop(); err != nil && mongoAlive() { panic(err) } - state.apiServer = nil } - if state.apiStatePool != nil { - if err := state.apiStatePool.Close(); err != nil && mongoAlive() { + if apiStatePool != nil { + if err := apiStatePool.Close(); err != nil && mongoAlive() { panic(err) } - state.apiStatePool = nil } - if state.apiState != nil { - if err := state.apiState.Close(); err != nil && mongoAlive() { + if apiState != nil { + if err := apiState.Close(); err != nil && mongoAlive() { panic(err) } - state.apiState = nil } if mongoAlive() { gitjujutesting.MgoServer.Reset() } - state.bootstrapped = false } // mongoAlive reports whether the mongo server is @@ -350,13 +392,13 @@ } // newState creates the state for a new environment with the given name. -func newState(name string, ops chan<- Operation, policy state.Policy) *environState { +func newState(name string, ops chan<- Operation, newStatePolicy state.NewPolicyFunc) *environState { s := &environState{ - name: name, - ops: ops, - statePolicy: policy, - insts: make(map[instance.Id]*dummyInstance), - globalPorts: make(map[network.PortRange]bool), + name: name, + ops: ops, + newStatePolicy: newStatePolicy, + insts: make(map[instance.Id]*dummyInstance), + globalPorts: make(map[network.PortRange]bool), } return s } @@ -372,14 +414,6 @@ return l.Addr().(*net.TCPAddr).Port } -// SetStatePolicy sets the state.Policy to use when a -// controller is initialised by dummy. -func SetStatePolicy(policy state.Policy) { - dummy.mu.Lock() - dummy.statePolicy = policy - dummy.mu.Unlock() -} - // SetSupportsSpaces allows to enable and disable SupportsSpaces for tests. func SetSupportsSpaces(supports bool) bool { dummy.mu.Lock() @@ -512,17 +546,19 @@ return state, nil } -func (p *environProvider) Open(cfg *config.Config) (environs.Environ, error) { +func (p *environProvider) Open(args environs.OpenParams) (environs.Environ, error) { p.mu.Lock() defer p.mu.Unlock() - ecfg, err := p.newConfig(cfg) + ecfg, err := p.newConfig(args.Config) if err != nil { return nil, err } env := &environ{ - name: ecfg.Name(), - ecfgUnlocked: ecfg, - modelUUID: cfg.UUID(), + ProviderRegistry: StorageProviders(), + name: ecfg.Name(), + modelUUID: args.Config.UUID(), + cloud: args.Cloud, + ecfgUnlocked: ecfg, } if err := env.checkBroken("Open"); err != nil { return nil, err @@ -535,29 +571,8 @@ return nil } -// PrepareForCreateEnvironment is specified in the EnvironProvider interface. -func (p *environProvider) PrepareForCreateEnvironment(controllerUUID string, cfg *config.Config) (*config.Config, error) { - // NOTE: this check might appear redundant, but it's not: some tests - // (apiserver/modelmanager) inject a string value and determine that - // the config is validated later; validating here would render that - // test meaningless. - if cfg.AllAttrs()["controller"] == true { - // NOTE: cfg.Apply *does* validate, but we're only adding a - // valid value so it doesn't matter. - return cfg.Apply(map[string]interface{}{ - "controller": false, - }) - } - return cfg, nil -} - -// PrepareForBootstrap is specified in the EnvironProvider interface. -func (p *environProvider) PrepareForBootstrap(ctx environs.BootstrapContext, cfg *config.Config) (environs.Environ, error) { - return p.Open(cfg) -} - -// BootstrapConfig is specified in the EnvironProvider interface. -func (p *environProvider) BootstrapConfig(args environs.BootstrapConfigParams) (*config.Config, error) { +// PrepareConfig is specified in the EnvironProvider interface. +func (p *environProvider) PrepareConfig(args environs.PrepareConfigParams) (*config.Config, error) { ecfg, err := p.newConfig(args.Config) if err != nil { return nil, err @@ -567,11 +582,23 @@ controllerUUID := args.ControllerUUID if controllerUUID != args.Config.UUID() { - return nil, errors.Errorf("invalid bootstrap config, model UUID %v doesn't equal controller UUID %v", controllerUUID, args.Config.UUID()) + // NOTE: this check might appear redundant, but it's not: some tests + // (apiserver/modelmanager) inject a string value and determine that + // the config is validated later; validating here would render that + // test meaningless. + if args.Config.AllAttrs()["controller"] == true { + // NOTE: cfg.Apply *does* validate, but we're only adding a + // valid value so it doesn't matter. + return args.Config.Apply(map[string]interface{}{ + "controller": false, + }) + } + return args.Config, nil } + envState, ok := p.state[controllerUUID] if ok { - // BootstrapConfig is expected to return the same result given + // PrepareConfig is expected to return the same result given // the same input. We assume that the args are the same for a // previously prepared/bootstrapped controller. return envState.bootstrapConfig, nil @@ -586,8 +613,8 @@ // The environment has not been prepared, so create it and record it. // We don't start listening for State or API connections until - // PrepareForBootstrapConfig has been called. - envState = newState(name, p.ops, p.statePolicy) + // PrepareForBootstrap has been called. + envState = newState(name, p.ops, p.newStatePolicy) cfg := args.Config if ecfg.controller() { p.apiPort = envState.listenAPI() @@ -627,11 +654,6 @@ return nil } -// SupportedArchitectures is specified on the EnvironCapability interface. -func (*environ) SupportedArchitectures() ([]string, error) { - return []string{arch.AMD64, arch.I386, arch.PPC64EL, arch.ARM64}, nil -} - // PrecheckInstance is specified in the state.Prechecker interface. func (*environ) PrecheckInstance(series string, cons constraints.Value, placement string) error { if placement != "" && placement != "valid" { @@ -640,6 +662,16 @@ return nil } +// Create is part of the Environ interface. +func (e *environ) Create(args environs.CreateParams) error { + return nil +} + +// PrepareForBootstrap is part of the Environ interface. +func (e *environ) PrepareForBootstrap(ctx environs.BootstrapContext) error { + return nil +} + func (e *environ) Bootstrap(ctx environs.BootstrapContext, args environs.BootstrapParams) (*environs.BootstrapResult, error) { series := config.PreferredSeries(e.Config()) availableTools, err := args.AvailableTools.Match(coretools.Filter{Series: series}) @@ -705,19 +737,20 @@ st, err := state.Initialize(state.InitializeParams{ ControllerConfig: icfg.Controller.Config, ControllerModelArgs: state.ModelArgs{ - Owner: names.NewUserTag("admin@local"), - Config: icfg.Bootstrap.ControllerModelConfig, - Constraints: icfg.Bootstrap.BootstrapMachineConstraints, - CloudName: icfg.Bootstrap.ControllerCloudName, - CloudRegion: icfg.Bootstrap.ControllerCloudRegion, - CloudCredential: icfg.Bootstrap.ControllerCloudCredentialName, + Owner: names.NewUserTag("admin@local"), + Config: icfg.Bootstrap.ControllerModelConfig, + Constraints: icfg.Bootstrap.BootstrapMachineConstraints, + CloudName: icfg.Bootstrap.ControllerCloudName, + CloudRegion: icfg.Bootstrap.ControllerCloudRegion, + CloudCredential: icfg.Bootstrap.ControllerCloudCredentialName, + StorageProviderRegistry: e, }, Cloud: icfg.Bootstrap.ControllerCloud, CloudName: icfg.Bootstrap.ControllerCloudName, CloudCredentials: cloudCredentials, MongoInfo: info, MongoDialOpts: mongotest.DialOpts(), - Policy: estate.statePolicy, + NewPolicy: estate.newStatePolicy, }) if err != nil { return err @@ -828,8 +861,14 @@ // under the covers. What we need to do is use the state mutex to add a memory // barrier such that the ops channel we see here is the latest. estate.mu.Lock() - defer estate.mu.Unlock() - estate.ops <- OpDestroy{Env: estate.name, Error: res} + ops := estate.ops + name := estate.name + estate.mu.Unlock() + ops <- OpDestroy{ + Env: name, + Cloud: e.cloud, + Error: res, + } }() if err := e.checkBroken("Destroy"); err != nil { return err @@ -837,8 +876,6 @@ if !e.ecfg().controller() { return nil } - estate.mu.Lock() - defer estate.mu.Unlock() estate.destroy() return nil } @@ -858,6 +895,9 @@ validator := constraints.NewValidator() validator.RegisterUnsupported([]string{constraints.CpuPower, constraints.VirtType}) validator.RegisterConflicts([]string{constraints.InstanceType}, []string{constraints.Mem}) + validator.RegisterVocabulary(constraints.Arch, []string{ + arch.AMD64, arch.I386, arch.PPC64EL, arch.ARM64, + }) return validator, nil } @@ -1527,3 +1567,7 @@ func (e *environ) AllocateContainerAddresses(hostInstanceID instance.Id, containerTag names.MachineTag, preparedInfo []network.InterfaceInfo) ([]network.InterfaceInfo, error) { return nil, errors.NotSupportedf("container address allocation") } + +func (e *environ) ReleaseContainerAddresses(interfaces []network.InterfaceInfo) error { + return errors.NotSupportedf("container address allocation") +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/dummy/environs_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/dummy/environs_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/dummy/environs_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/dummy/environs_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -119,9 +119,9 @@ s.ControllerStore, bootstrap.PrepareParams{ ControllerConfig: testing.FakeControllerConfig(), - BaseConfig: s.TestConfig, + ModelConfig: s.TestConfig, ControllerName: s.TestConfig["name"].(string), - CloudName: "dummy", + Cloud: dummy.SampleCloudSpec(), AdminSecret: AdminSecret, }, ) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/dummy/init.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/dummy/init.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/dummy/init.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/dummy/init.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,14 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package dummy - -import ( - dummystorage "github.com/juju/juju/storage/provider/dummy" - "github.com/juju/juju/storage/provider/registry" -) - -func init() { - registry.RegisterEnvironStorageProviders("dummy", "dummy") - registry.RegisterProvider("dummy", &dummystorage.StorageProvider{}) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/dummy/storage.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/dummy/storage.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/dummy/storage.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/dummy/storage.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,13 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package dummy + +import ( + "github.com/juju/juju/state/testing" + "github.com/juju/juju/storage" +) + +func StorageProviders() storage.ProviderRegistry { + return testing.StorageProviders() +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/config.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/config.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/config.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/config.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,32 +7,12 @@ "fmt" "github.com/juju/schema" - "gopkg.in/amz.v3/aws" "gopkg.in/juju/environschema.v1" "github.com/juju/juju/environs/config" ) var configSchema = environschema.Fields{ - "access-key": { - Description: "The EC2 access key", - EnvVar: "AWS_ACCESS_KEY_ID", - Type: environschema.Tstring, - Mandatory: true, - Group: environschema.AccountGroup, - }, - "secret-key": { - Description: "The EC2 secret key", - EnvVar: "AWS_SECRET_ACCESS_KEY", - Type: environschema.Tstring, - Mandatory: true, - Secret: true, - Group: environschema.AccountGroup, - }, - "region": { - Description: "The EC2 region to use", - Type: environschema.Tstring, - }, "vpc-id": { Description: "Use a specific AWS VPC ID (optional). When not specified, Juju requires a default VPC or EC2-Classic features to be available for the account/region.", Example: "vpc-a1b2c3d4", @@ -57,8 +37,6 @@ }() var configDefaults = schema.Defaults{ - "access-key": "", - "secret-key": "", "vpc-id": "", "vpc-id-force": false, } @@ -68,18 +46,6 @@ attrs map[string]interface{} } -func (c *environConfig) region() string { - return c.attrs["region"].(string) -} - -func (c *environConfig) accessKey() string { - return c.attrs["access-key"].(string) -} - -func (c *environConfig) secretKey() string { - return c.attrs["secret-key"].(string) -} - func (c *environConfig) vpcID() string { return c.attrs["vpc-id"].(string) } @@ -116,19 +82,6 @@ } ecfg := &environConfig{cfg, validated} - if ecfg.accessKey() == "" || ecfg.secretKey() == "" { - auth, err := aws.EnvAuth() - if err != nil || ecfg.accessKey() != "" || ecfg.secretKey() != "" { - return nil, fmt.Errorf("model has no access-key or secret-key") - } - ecfg.attrs["access-key"] = auth.AccessKey - ecfg.attrs["secret-key"] = auth.SecretKey - } - - if _, ok := aws.Regions[ecfg.region()]; !ok { - return nil, fmt.Errorf("invalid region name %q", ecfg.region()) - } - if vpcID := ecfg.vpcID(); isVPCIDSetButInvalid(vpcID) { return nil, fmt.Errorf("vpc-id: %q is not a valid AWS VPC ID", vpcID) } else if !isVPCIDSet(vpcID) && ecfg.forceVPCID() { @@ -137,9 +90,6 @@ if old != nil { attrs := old.UnknownAttrs() - if region, _ := attrs["region"].(string); ecfg.region() != region { - return nil, fmt.Errorf("cannot change region from %q to %q", region, ecfg.region()) - } if vpcID, _ := attrs["vpc-id"].(string); vpcID != ecfg.vpcID() { return nil, fmt.Errorf("cannot change vpc-id from %q to %q", vpcID, ecfg.vpcID()) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/config_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/config_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/config_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/config_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -49,11 +49,8 @@ config map[string]interface{} change map[string]interface{} expect map[string]interface{} - region string vpcID string forceVPCID bool - accessKey string - secretKey string firewallMode string blockStorageSource string err string @@ -62,13 +59,28 @@ type attrs map[string]interface{} func (t configTest) check(c *gc.C) { + credential := cloud.NewCredential( + cloud.AccessKeyAuthType, + map[string]string{ + "access-key": "x", + "secret-key": "y", + }, + ) + cloudSpec := environs.CloudSpec{ + Type: "ec2", + Name: "ec2test", + Region: "us-east-1", + Credential: &credential, + } attrs := testing.FakeConfig().Merge(testing.Attrs{ - "type": "ec2", - "region": "us-east-1", + "type": "ec2", }).Merge(t.config) cfg, err := config.New(config.NoDefaults, attrs) c.Assert(err, jc.ErrorIsNil) - e, err := environs.New(cfg) + e, err := environs.New(environs.OpenParams{ + Cloud: cloudSpec, + Config: cfg, + }) if t.change != nil { c.Assert(err, jc.ErrorIsNil) @@ -93,28 +105,9 @@ ecfg := e.(*environ).ecfg() c.Assert(ecfg.Name(), gc.Equals, "testenv") - if t.region != "" { - c.Assert(ecfg.region(), gc.Equals, t.region) - } - c.Assert(ecfg.vpcID(), gc.Equals, t.vpcID) c.Assert(ecfg.forceVPCID(), gc.Equals, t.forceVPCID) - if t.accessKey != "" { - c.Assert(ecfg.accessKey(), gc.Equals, t.accessKey) - c.Assert(ecfg.secretKey(), gc.Equals, t.secretKey) - expected := map[string]string{ - "access-key": t.accessKey, - "secret-key": t.secretKey, - } - c.Assert(err, jc.ErrorIsNil) - actual, err := e.Provider().SecretAttrs(ecfg.Config) - c.Assert(err, jc.ErrorIsNil) - c.Assert(expected, gc.DeepEquals, actual) - } else { - c.Assert(ecfg.accessKey(), gc.DeepEquals, testAuth.AccessKey) - c.Assert(ecfg.secretKey(), gc.DeepEquals, testAuth.SecretKey) - } if t.firewallMode != "" { c.Assert(ecfg.FirewallMode(), gc.Equals, t.firewallMode) } @@ -129,34 +122,6 @@ { config: attrs{}, }, { - config: attrs{ - "region": "eu-west-1", - }, - region: "eu-west-1", - }, { - config: attrs{ - "region": "unknown", - }, - err: ".*invalid region name.*", - }, { - config: attrs{ - "region": "configtest", - }, - region: "configtest", - }, { - config: attrs{ - "region": "configtest", - }, - change: attrs{ - "region": "us-east-1", - }, - err: `.*cannot change region from "configtest" to "us-east-1"`, - }, { - config: attrs{ - "region": 666, - }, - err: `.*expected string, got int\(666\)`, - }, { config: attrs{}, vpcID: "", forceVPCID: false, @@ -284,37 +249,6 @@ vpcID: "vpc-foo", forceVPCID: true, }, { - config: attrs{ - "access-key": 666, - }, - err: `.*expected string, got int\(666\)`, - }, { - config: attrs{ - "secret-key": 666, - }, - err: `.*expected string, got int\(666\)`, - }, { - config: attrs{ - "access-key": "jujuer", - "secret-key": "open sesame", - }, - accessKey: "jujuer", - secretKey: "open sesame", - }, { - config: attrs{ - "access-key": "jujuer", - }, - err: ".*model has no access-key or secret-key", - }, { - config: attrs{ - "secret-key": "badness", - }, - err: ".*model has no access-key or secret-key", - }, { - config: attrs{ - "admin-secret": "Futumpsh", - }, - }, { config: attrs{}, firewallMode: config.FwInstance, }, { @@ -374,8 +308,6 @@ func (s *ConfigSuite) SetUpTest(c *gc.C) { s.BaseSuite.SetUpTest(c) s.savedHome = utils.Home() - s.savedAccessKey = os.Getenv("AWS_ACCESS_KEY_ID") - s.savedSecretKey = os.Getenv("AWS_SECRET_ACCESS_KEY") home := c.MkDir() sshDir := filepath.Join(home, ".ssh") @@ -386,16 +318,12 @@ err = utils.SetHome(home) c.Assert(err, jc.ErrorIsNil) - os.Setenv("AWS_ACCESS_KEY_ID", testAuth.AccessKey) - os.Setenv("AWS_SECRET_ACCESS_KEY", testAuth.SecretKey) aws.Regions["configtest"] = configTestRegion } func (s *ConfigSuite) TearDownTest(c *gc.C) { err := utils.SetHome(s.savedHome) c.Assert(err, jc.ErrorIsNil) - os.Setenv("AWS_ACCESS_KEY_ID", s.savedAccessKey) - os.Setenv("AWS_SECRET_ACCESS_KEY", s.savedSecretKey) delete(aws.Regions, "configtest") s.BaseSuite.TearDownTest(c) } @@ -407,23 +335,7 @@ } } -func (s *ConfigSuite) TestMissingAuth(c *gc.C) { - os.Setenv("AWS_ACCESS_KEY_ID", "") - os.Setenv("AWS_SECRET_ACCESS_KEY", "") - - // Since PR #52 amz.v3 uses these AWS_ vars as fallbacks, if set. - os.Setenv("AWS_ACCESS_KEY", "") - os.Setenv("AWS_SECRET_KEY", "") - - // Since LP r37 goamz uses also these EC2_ as fallbacks, so unset them too. - os.Setenv("EC2_ACCESS_KEY", "") - os.Setenv("EC2_SECRET_KEY", "") - test := configTests[0] - test.err = ".*model has no access-key or secret-key" - test.check(c) -} - -func (s *ConfigSuite) TestBootstrapConfigSetsDefaultBlockSource(c *gc.C) { +func (s *ConfigSuite) TestPrepareConfigSetsDefaultBlockSource(c *gc.C) { s.PatchValue(&verifyCredentials, func(*environ) error { return nil }) attrs := testing.FakeConfig().Merge(testing.Attrs{ "type": "ec2", @@ -431,16 +343,21 @@ cfg, err := config.New(config.NoDefaults, attrs) c.Assert(err, jc.ErrorIsNil) - cfg, err = providerInstance.BootstrapConfig(environs.BootstrapConfigParams{ + credential := cloud.NewCredential( + cloud.AccessKeyAuthType, + map[string]string{ + "access-key": "x", + "secret-key": "y", + }, + ) + cfg, err = providerInstance.PrepareConfig(environs.PrepareConfigParams{ Config: cfg, - Credentials: cloud.NewCredential( - cloud.AccessKeyAuthType, - map[string]string{ - "access-key": "x", - "secret-key": "y", - }, - ), - CloudRegion: "test", + Cloud: environs.CloudSpec{ + Type: "ec2", + Name: "aws", + Region: "test", + Credential: &credential, + }, }) c.Assert(err, jc.ErrorIsNil) source, ok := cfg.StorageDefaultBlockSource() @@ -456,16 +373,21 @@ config, err := config.New(config.NoDefaults, attrs) c.Assert(err, jc.ErrorIsNil) - cfg, err := providerInstance.BootstrapConfig(environs.BootstrapConfigParams{ - Config: config, - CloudRegion: "test", - Credentials: cloud.NewCredential( - cloud.AccessKeyAuthType, - map[string]string{ - "access-key": "x", - "secret-key": "y", - }, - ), + credential := cloud.NewCredential( + cloud.AccessKeyAuthType, + map[string]string{ + "access-key": "x", + "secret-key": "y", + }, + ) + cfg, err := providerInstance.PrepareConfig(environs.PrepareConfigParams{ + Config: config, + Cloud: environs.CloudSpec{ + Type: "ec2", + Name: "aws", + Region: "test", + Credential: &credential, + }, }) c.Assert(err, jc.ErrorIsNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/ebs.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/ebs.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/ebs.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/ebs.go 2016-08-16 08:56:25.000000000 +0000 @@ -15,12 +15,10 @@ "gopkg.in/amz.v3/ec2" "github.com/juju/juju/constraints" - "github.com/juju/juju/environs/config" "github.com/juju/juju/environs/tags" "github.com/juju/juju/instance" "github.com/juju/juju/provider/common" "github.com/juju/juju/storage" - "github.com/juju/juju/storage/poolmanager" ) const ( @@ -116,18 +114,23 @@ var deviceInUseRegexp = regexp.MustCompile(".*Attachment point .* is already in use") -func init() { - ebsssdPool, _ := storage.NewConfig("ebs-ssd", EBS_ProviderType, map[string]interface{}{ - EBS_VolumeType: volumeTypeSsd, - }) - defaultPools := []*storage.Config{ - ebsssdPool, +// StorageProviderTypes implements storage.ProviderRegistry. +func (env *environ) StorageProviderTypes() []storage.ProviderType { + return []storage.ProviderType{EBS_ProviderType} +} + +// StorageProvider implements storage.ProviderRegistry. +func (env *environ) StorageProvider(t storage.ProviderType) (storage.Provider, error) { + if t == EBS_ProviderType { + return &ebsProvider{env}, nil } - poolmanager.RegisterDefaultStoragePools(defaultPools) + return nil, errors.NotFoundf("storage provider %q", t) } // ebsProvider creates volume sources which use AWS EBS volumes. -type ebsProvider struct{} +type ebsProvider struct { + env *environ +} var _ storage.Provider = (*ebsProvider)(nil) @@ -209,14 +212,19 @@ return true } +// DefaultPools is defined on the Provider interface. +func (e *ebsProvider) DefaultPools() []*storage.Config { + ssdPool, _ := storage.NewConfig("ebs-ssd", EBS_ProviderType, map[string]interface{}{ + EBS_VolumeType: volumeTypeSsd, + }) + return []*storage.Config{ssdPool} +} + // VolumeSource is defined on the Provider interface. -func (e *ebsProvider) VolumeSource(environConfig *config.Config, cfg *storage.Config) (storage.VolumeSource, error) { - ec2, _, _, err := awsClients(environConfig) - if err != nil { - return nil, errors.Annotate(err, "creating AWS clients") - } +func (e *ebsProvider) VolumeSource(cfg *storage.Config) (storage.VolumeSource, error) { + environConfig := e.env.Config() source := &ebsVolumeSource{ - ec2: ec2, + env: e.env, envName: environConfig.Name(), modelUUID: environConfig.UUID(), } @@ -224,12 +232,12 @@ } // FilesystemSource is defined on the Provider interface. -func (e *ebsProvider) FilesystemSource(environConfig *config.Config, providerConfig *storage.Config) (storage.FilesystemSource, error) { +func (e *ebsProvider) FilesystemSource(providerConfig *storage.Config) (storage.FilesystemSource, error) { return nil, errors.NotSupportedf("filesystems") } type ebsVolumeSource struct { - ec2 *ec2.EC2 + env *environ envName string // non-unique, informational only modelUUID string } @@ -280,7 +288,7 @@ instances := make(instanceCache) if instanceIds.Size() > 1 { - if err := instances.update(v.ec2, instanceIds.Values()...); err != nil { + if err := instances.update(v.env.ec2, instanceIds.Values()...); err != nil { logger.Debugf("querying running instances: %v", err) // We ignore the error, because we don't want an invalid // InstanceId reference from one VolumeParams to prevent @@ -309,7 +317,7 @@ if err == nil || volumeId == "" { return } - if _, err := v.ec2.DeleteVolume(volumeId); err != nil { + if _, err := v.env.ec2.DeleteVolume(volumeId); err != nil { logger.Errorf("error cleaning up volume %v: %v", volumeId, err) } }() @@ -321,7 +329,7 @@ // Create. instId := string(p.Attachment.InstanceId) - if err := instances.update(v.ec2, instId); err != nil { + if err := instances.update(v.env.ec2, instId); err != nil { return nil, nil, errors.Trace(err) } inst, err := instances.get(instId) @@ -332,7 +340,7 @@ } vol, _ := parseVolumeOptions(p.Size, p.Attributes) vol.AvailZone = inst.AvailZone - resp, err := v.ec2.CreateVolume(vol) + resp, err := v.env.ec2.CreateVolume(vol) if err != nil { return nil, nil, errors.Trace(err) } @@ -344,7 +352,7 @@ resourceTags[k] = v } resourceTags[tagName] = resourceName(p.Tag, v.envName) - if err := tagResources(v.ec2, resourceTags, volumeId); err != nil { + if err := tagResources(v.env.ec2, resourceTags, volumeId); err != nil { return nil, nil, errors.Annotate(err, "tagging volume") } @@ -363,7 +371,7 @@ func (v *ebsVolumeSource) ListVolumes() ([]string, error) { filter := ec2.NewFilter() filter.Add("tag:"+tags.JujuModel, v.modelUUID) - return listVolumes(v.ec2, filter) + return listVolumes(v.env.ec2, filter) } func listVolumes(client *ec2.EC2, filter *ec2.Filter) ([]string, error) { @@ -398,7 +406,7 @@ // operation to fail. If we get an invalid volume ID response, // fall back to querying each volume individually. That should // be rare. - resp, err := v.ec2.Volumes(volIds, nil) + resp, err := v.env.ec2.Volumes(volIds, nil) if err != nil { return nil, err } @@ -430,7 +438,7 @@ // DestroyVolumes is specified on the storage.VolumeSource interface. func (v *ebsVolumeSource) DestroyVolumes(volIds []string) ([]error, error) { - return destroyVolumes(v.ec2, volIds), nil + return destroyVolumes(v.env.ec2, volIds), nil } func destroyVolumes(client *ec2.EC2, volIds []string) []error { @@ -603,7 +611,7 @@ } instances := make(instanceCache) if instIds.Size() > 1 { - if err := instances.update(v.ec2, instIds.Values()...); err != nil { + if err := instances.update(v.env.ec2, instIds.Values()...); err != nil { logger.Debugf("querying running instances: %v", err) // We ignore the error, because we don't want an invalid // InstanceId reference from one VolumeParams to prevent @@ -681,7 +689,7 @@ // Can't attach any more volumes. return "", "", err } - _, err = v.ec2.AttachVolume(volumeId, instId, requestDeviceName) + _, err = v.env.ec2.AttachVolume(volumeId, instId, requestDeviceName) if ec2Err, ok := err.(*ec2.Error); ok { switch ec2Err.Code { case invalidParameterValue: @@ -712,7 +720,7 @@ Delay: 200 * time.Millisecond, } var lastStatus string - volume, err := waitVolume(v.ec2, volumeId, attempt, func(volume *ec2.Volume) (bool, error) { + volume, err := waitVolume(v.env.ec2, volumeId, attempt, func(volume *ec2.Volume) (bool, error) { lastStatus = volume.Status return volume.Status != volumeStatusCreating, nil }) @@ -797,7 +805,7 @@ // DetachVolumes is specified on the storage.VolumeSource interface. func (v *ebsVolumeSource) DetachVolumes(attachParams []storage.VolumeAttachmentParams) ([]error, error) { - return detachVolumes(v.ec2, attachParams) + return detachVolumes(v.env.ec2, attachParams) } func detachVolumes(client *ec2.EC2, attachParams []storage.VolumeAttachmentParams) ([]error, error) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/ebs_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/ebs_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/ebs_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/ebs_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -21,7 +21,6 @@ "github.com/juju/juju/cloud" "github.com/juju/juju/constraints" - "github.com/juju/juju/environs/config" "github.com/juju/juju/environs/imagemetadata" imagetesting "github.com/juju/juju/environs/imagemetadata/testing" "github.com/juju/juju/environs/jujutest" @@ -35,31 +34,7 @@ jujuversion "github.com/juju/juju/version" ) -type storageSuite struct { - testing.BaseSuite -} - -var _ = gc.Suite(&storageSuite{}) - -func (*storageSuite) TestValidateConfigUnknownConfig(c *gc.C) { - p := ec2.EBSProvider() - cfg, err := storage.NewConfig("foo", ec2.EBS_ProviderType, map[string]interface{}{ - "unknown": "config", - }) - c.Assert(err, jc.ErrorIsNil) - err = p.ValidateConfig(cfg) - c.Assert(err, jc.ErrorIsNil) // unknown attrs ignored -} - -func (s *storageSuite) TestSupports(c *gc.C) { - p := ec2.EBSProvider() - c.Assert(p.Supports(storage.StorageKindBlock), jc.IsTrue) - c.Assert(p.Supports(storage.StorageKindFilesystem), jc.IsFalse) -} - -var _ = gc.Suite(&ebsVolumeSuite{}) - -type ebsVolumeSuite struct { +type ebsSuite struct { testing.BaseSuite // TODO(axw) the EBS tests should not be embedding jujutest.Tests. jujutest.Tests @@ -69,7 +44,9 @@ instanceId string } -func (s *ebsVolumeSuite) SetUpSuite(c *gc.C) { +var _ = gc.Suite(&ebsSuite{}) + +func (s *ebsSuite) SetUpSuite(c *gc.C) { s.BaseSuite.SetUpSuite(c) s.Tests.SetUpSuite(c) s.Credential = cloud.NewCredential( @@ -96,13 +73,13 @@ s.BaseSuite.PatchValue(ec2.DeleteSecurityGroupInsistently, deleteSecurityGroupForTestFunc) } -func (s *ebsVolumeSuite) TearDownSuite(c *gc.C) { +func (s *ebsSuite) TearDownSuite(c *gc.C) { s.restoreEC2Patching() s.Tests.TearDownSuite(c) s.BaseSuite.TearDownSuite(c) } -func (s *ebsVolumeSuite) SetUpTest(c *gc.C) { +func (s *ebsSuite) SetUpTest(c *gc.C) { s.BaseSuite.SetUpTest(c) s.BaseSuite.PatchValue(&jujuversion.Current, testing.FakeVersionNumber) s.BaseSuite.PatchValue(&arch.HostArch, func() string { return arch.AMD64 }) @@ -112,22 +89,43 @@ s.PatchValue(&ec2.DestroyVolumeAttempt.Delay, time.Duration(0)) } -func (s *ebsVolumeSuite) TearDownTest(c *gc.C) { +func (s *ebsSuite) TearDownTest(c *gc.C) { s.Tests.TearDownTest(c) s.srv.stopServer(c) s.BaseSuite.TearDownTest(c) } -func (s *ebsVolumeSuite) volumeSource(c *gc.C, cfg *storage.Config) storage.VolumeSource { - envCfg, err := config.New(config.NoDefaults, s.TestConfig) +func (s *ebsSuite) ebsProvider(c *gc.C) storage.Provider { + env := s.Prepare(c) + p, err := env.StorageProvider(ec2.EBS_ProviderType) + c.Assert(err, jc.ErrorIsNil) + return p +} + +func (s *ebsSuite) TestValidateConfigUnknownConfig(c *gc.C) { + p := s.ebsProvider(c) + cfg, err := storage.NewConfig("foo", ec2.EBS_ProviderType, map[string]interface{}{ + "unknown": "config", + }) c.Assert(err, jc.ErrorIsNil) - p := ec2.EBSProvider() - vs, err := p.VolumeSource(envCfg, cfg) + err = p.ValidateConfig(cfg) + c.Assert(err, jc.ErrorIsNil) // unknown attrs ignored +} + +func (s *ebsSuite) TestSupports(c *gc.C) { + p := s.ebsProvider(c) + c.Assert(p.Supports(storage.StorageKindBlock), jc.IsTrue) + c.Assert(p.Supports(storage.StorageKindFilesystem), jc.IsFalse) +} + +func (s *ebsSuite) volumeSource(c *gc.C, cfg *storage.Config) storage.VolumeSource { + p := s.ebsProvider(c) + vs, err := p.VolumeSource(cfg) c.Assert(err, jc.ErrorIsNil) return vs } -func (s *ebsVolumeSuite) createVolumes(vs storage.VolumeSource, instanceId string) ([]storage.CreateVolumesResult, error) { +func (s *ebsSuite) createVolumes(vs storage.VolumeSource, instanceId string) ([]storage.CreateVolumesResult, error) { if instanceId == "" { instanceId = s.srv.ec2srv.NewInstances(1, "m1.medium", imageId, ec2test.Running, nil)[0] } @@ -178,7 +176,7 @@ return vs.CreateVolumes(params) } -func (s *ebsVolumeSuite) assertCreateVolumes(c *gc.C, vs storage.VolumeSource, instanceId string) { +func (s *ebsSuite) assertCreateVolumes(c *gc.C, vs storage.VolumeSource, instanceId string) { results, err := s.createVolumes(vs, instanceId) c.Assert(err, jc.ErrorIsNil) c.Assert(results, gc.HasLen, 3) @@ -247,12 +245,12 @@ return s.less(s.vols[i], s.vols[j]) } -func (s *ebsVolumeSuite) TestCreateVolumes(c *gc.C) { +func (s *ebsSuite) TestCreateVolumes(c *gc.C) { vs := s.volumeSource(c, nil) s.assertCreateVolumes(c, vs, "") } -func (s *ebsVolumeSuite) TestVolumeTags(c *gc.C) { +func (s *ebsSuite) TestVolumeTags(c *gc.C) { vs := s.volumeSource(c, nil) results, err := s.createVolumes(vs, "") c.Assert(err, jc.ErrorIsNil) @@ -303,7 +301,7 @@ }) } -func (s *ebsVolumeSuite) TestVolumeTypeAliases(c *gc.C) { +func (s *ebsSuite) TestVolumeTypeAliases(c *gc.C) { instanceIdRunning := s.srv.ec2srv.NewInstances(1, "m1.medium", imageId, ec2test.Running, nil)[0] vs := s.volumeSource(c, nil) ec2Client := ec2.StorageEC2(vs) @@ -346,7 +344,7 @@ } } -func (s *ebsVolumeSuite) TestDestroyVolumesNotFoundReturnsNil(c *gc.C) { +func (s *ebsSuite) TestDestroyVolumesNotFoundReturnsNil(c *gc.C) { vs := s.volumeSource(c, nil) results, err := vs.DestroyVolumes([]string{"vol-42"}) c.Assert(err, jc.ErrorIsNil) @@ -354,7 +352,7 @@ c.Assert(results[0], jc.ErrorIsNil) } -func (s *ebsVolumeSuite) TestDestroyVolumes(c *gc.C) { +func (s *ebsSuite) TestDestroyVolumes(c *gc.C) { vs := s.volumeSource(c, nil) params := s.setupAttachVolumesTest(c, vs, ec2test.Running) errs, err := vs.DetachVolumes(params) @@ -372,7 +370,7 @@ c.Assert(ec2Vols.Volumes[0].Size, gc.Equals, 20) } -func (s *ebsVolumeSuite) TestDestroyVolumesStillAttached(c *gc.C) { +func (s *ebsSuite) TestDestroyVolumesStillAttached(c *gc.C) { vs := s.volumeSource(c, nil) s.setupAttachVolumesTest(c, vs, ec2test.Running) errs, err := vs.DestroyVolumes([]string{"vol-0"}) @@ -387,7 +385,7 @@ c.Assert(ec2Vols.Volumes[0].Size, gc.Equals, 20) } -func (s *ebsVolumeSuite) TestDescribeVolumes(c *gc.C) { +func (s *ebsSuite) TestDescribeVolumes(c *gc.C) { vs := s.volumeSource(c, nil) s.assertCreateVolumes(c, vs, "") @@ -408,7 +406,7 @@ }}) } -func (s *ebsVolumeSuite) TestDescribeVolumesNotFound(c *gc.C) { +func (s *ebsSuite) TestDescribeVolumesNotFound(c *gc.C) { vs := s.volumeSource(c, nil) vols, err := vs.DescribeVolumes([]string{"vol-42"}) c.Assert(err, jc.ErrorIsNil) @@ -416,7 +414,7 @@ c.Assert(vols[0].Error, gc.ErrorMatches, "vol-42 not found") } -func (s *ebsVolumeSuite) TestListVolumes(c *gc.C) { +func (s *ebsSuite) TestListVolumes(c *gc.C) { vs := s.volumeSource(c, nil) s.assertCreateVolumes(c, vs, "") @@ -427,7 +425,7 @@ c.Assert(volIds, jc.SameContents, []string{"vol-0"}) } -func (s *ebsVolumeSuite) TestListVolumesIgnoresRootDisks(c *gc.C) { +func (s *ebsSuite) TestListVolumesIgnoresRootDisks(c *gc.C) { s.srv.ec2srv.SetCreateRootDisks(true) s.srv.ec2srv.NewInstances(1, "m1.medium", imageId, ec2test.Pending, nil) @@ -443,7 +441,7 @@ c.Assert(volIds, gc.HasLen, 0) } -func (s *ebsVolumeSuite) TestCreateVolumesErrors(c *gc.C) { +func (s *ebsSuite) TestCreateVolumesErrors(c *gc.C) { vs := s.volumeSource(c, nil) volume0 := names.NewVolumeTag("0") @@ -566,7 +564,7 @@ var imageId = "ami-ccf405a5" // Ubuntu Maverick, i386, EBS store -func (s *ebsVolumeSuite) setupAttachVolumesTest( +func (s *ebsSuite) setupAttachVolumesTest( c *gc.C, vs storage.VolumeSource, state awsec2.InstanceState, ) []storage.VolumeAttachmentParams { @@ -583,7 +581,7 @@ }} } -func (s *ebsVolumeSuite) TestAttachVolumesNotRunning(c *gc.C) { +func (s *ebsSuite) TestAttachVolumesNotRunning(c *gc.C) { vs := s.volumeSource(c, nil) instanceId := s.srv.ec2srv.NewInstances(1, "m1.medium", imageId, ec2test.Pending, nil)[0] results, err := s.createVolumes(vs, instanceId) @@ -594,7 +592,7 @@ } } -func (s *ebsVolumeSuite) TestAttachVolumes(c *gc.C) { +func (s *ebsSuite) TestAttachVolumes(c *gc.C) { vs := s.volumeSource(c, nil) params := s.setupAttachVolumesTest(c, vs, ec2test.Running) result, err := vs.AttachVolumes(params) @@ -640,7 +638,7 @@ // TODO(axw) add tests for attempting to attach while // a volume is still in the "creating" state. -func (s *ebsVolumeSuite) TestDetachVolumes(c *gc.C) { +func (s *ebsSuite) TestDetachVolumes(c *gc.C) { vs := s.volumeSource(c, nil) params := s.setupAttachVolumesTest(c, vs, ec2test.Running) _, err := vs.AttachVolumes(params) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/environ.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/environ.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/environ.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/environ.go 2016-08-16 08:56:25.000000000 +0000 @@ -16,7 +16,6 @@ "github.com/juju/utils" "github.com/juju/utils/arch" "github.com/juju/utils/clock" - "gopkg.in/amz.v3/aws" "gopkg.in/amz.v3/ec2" "gopkg.in/amz.v3/s3" "gopkg.in/juju/names.v2" @@ -46,6 +45,7 @@ var ( // Use shortAttempt to poll for short-term events or for retrying API calls. + // TODO(katco): 2016-08-09: lp:1611427 shortAttempt = utils.AttemptStrategy{ Total: 5 * time.Second, Delay: 200 * time.Millisecond, @@ -57,9 +57,10 @@ ) type environ struct { - common.SupportsUnitPlacementPolicy - - name string + name string + cloud environs.CloudSpec + ec2 *ec2.EC2 + s3 *s3.S3 // archMutex gates access to supportedArchitectures archMutex sync.Mutex @@ -70,8 +71,6 @@ // ecfgMutex protects the *Unlocked fields below. ecfgMutex sync.Mutex ecfgUnlocked *environConfig - ec2Unlocked *ec2.EC2 - s3Unlocked *s3.S3 availabilityZonesMutex sync.Mutex availabilityZones []common.AvailabilityZone @@ -81,33 +80,14 @@ return e.ecfg().Config } -func awsClients(cfg *config.Config) (*ec2.EC2, *s3.S3, *environConfig, error) { - ecfg, err := providerInstance.newConfig(cfg) - if err != nil { - return nil, nil, nil, err - } - - auth := aws.Auth{ - AccessKey: ecfg.accessKey(), - SecretKey: ecfg.secretKey(), - } - region := aws.Regions[ecfg.region()] - signer := aws.SignV4Factory(region.Name, "ec2") - return ec2.New(auth, region, signer), s3.New(auth, region), ecfg, nil -} - func (e *environ) SetConfig(cfg *config.Config) error { - ec2Client, s3Client, ecfg, err := awsClients(cfg) + ecfg, err := providerInstance.newConfig(cfg) if err != nil { - return err + return errors.Trace(err) } - e.ecfgMutex.Lock() - defer e.ecfgMutex.Unlock() e.ecfgUnlocked = ecfg - e.ec2Unlocked = ec2Client - e.s3Unlocked = s3Client - + e.ecfgMutex.Unlock() return nil } @@ -118,23 +98,49 @@ return ecfg } -func (e *environ) ec2() *ec2.EC2 { - e.ecfgMutex.Lock() - ec2 := e.ec2Unlocked - e.ecfgMutex.Unlock() - return ec2 -} - func (e *environ) Name() string { return e.name } +// PrepareForBootstrap is part of the Environ interface. +func (env *environ) PrepareForBootstrap(ctx environs.BootstrapContext) error { + if ctx.ShouldVerifyCredentials() { + if err := verifyCredentials(env); err != nil { + return err + } + } + ecfg := env.ecfg() + vpcID, forceVPCID := ecfg.vpcID(), ecfg.forceVPCID() + if err := validateBootstrapVPC(env.ec2, env.cloud.Region, vpcID, forceVPCID, ctx); err != nil { + return errors.Trace(err) + } + return nil +} + +// Create is part of the Environ interface. +func (env *environ) Create(args environs.CreateParams) error { + if err := verifyCredentials(env); err != nil { + return err + } + vpcID := env.ecfg().vpcID() + if err := validateModelVPC(env.ec2, env.name, vpcID); err != nil { + return errors.Trace(err) + } + // TODO(axw) 2016-08-04 #1609643 + // Create global security group(s) here. + return nil +} + +func (env *environ) validateVPC(logInfof func(string, ...interface{}), badge string) error { + return nil +} + +// Bootstrap is part of the Environ interface. func (e *environ) Bootstrap(ctx environs.BootstrapContext, args environs.BootstrapParams) (*environs.BootstrapResult, error) { return common.Bootstrap(ctx, e, args) } -// SupportedArchitectures is specified on the EnvironCapability interface. -func (e *environ) SupportedArchitectures() ([]string, error) { +func (e *environ) getSupportedArchitectures() ([]string, error) { e.archMutex.Lock() defer e.archMutex.Unlock() if e.supportedArchitectures != nil { @@ -177,7 +183,7 @@ []string{constraints.InstanceType}, []string{constraints.Mem, constraints.CpuCores, constraints.CpuPower}) validator.RegisterUnsupported(unsupportedConstraints) - supportedArches, err := e.SupportedArchitectures() + supportedArches, err := e.getSupportedArchitectures() if err != nil { return nil, err } @@ -223,8 +229,8 @@ defer e.availabilityZonesMutex.Unlock() if e.availabilityZones == nil { filter := ec2.NewFilter() - filter.Add("region-name", e.ecfg().region()) - resp, err := ec2AvailabilityZones(e.ec2(), filter) + filter.Add("region-name", e.cloud.Region) + resp, err := ec2AvailabilityZones(e.ec2, filter) if err != nil { return nil, err } @@ -310,7 +316,7 @@ // MetadataLookupParams returns parameters which are used to query simplestreams metadata. func (e *environ) MetadataLookupParams(region string) (*simplestreams.MetadataLookupParams, error) { if region == "" { - region = e.ecfg().region() + region = e.cloud.Region } cloudSpec, err := e.cloudSpec(region) if err != nil { @@ -326,7 +332,7 @@ // Region is specified in the HasRegion interface. func (e *environ) Region() (simplestreams.CloudSpec, error) { - return e.cloudSpec(e.ecfg().region()) + return e.cloudSpec(e.cloud.Region) } func (e *environ) cloudSpec(region string) (simplestreams.CloudSpec, error) { @@ -418,7 +424,7 @@ arches := args.Tools.Arches() spec, err := findInstanceSpec(args.ImageMetadata, &instances.InstanceConstraint{ - Region: e.ecfg().region(), + Region: e.cloud.Region, Series: args.InstanceConfig.Series, Arches: arches, Constraints: args.Constraints, @@ -494,9 +500,13 @@ var subnetIDsForZone []string var subnetErr error - if haveVPCID && !args.Constraints.HaveSpaces() { - subnetIDsForZone, subnetErr = getVPCSubnetIDsForAvailabilityZone(e.ec2(), e.ecfg().vpcID(), zone) - } else if !haveVPCID && args.Constraints.HaveSpaces() { + if haveVPCID { + var allowedSubnetIDs []string + for subnetID, _ := range args.SubnetsToZones { + allowedSubnetIDs = append(allowedSubnetIDs, string(subnetID)) + } + subnetIDsForZone, subnetErr = getVPCSubnetIDsForAvailabilityZone(e.ec2, e.ecfg().vpcID(), zone, allowedSubnetIDs) + } else if args.Constraints.HaveSpaces() { subnetIDsForZone, subnetErr = findSubnetIDsForAvailabilityZone(zone, args.SubnetsToZones) } @@ -522,7 +532,7 @@ logger.Infof("selected subnet %q in zone %q", runArgs.SubnetId, zone) } - instResp, err = runInstances(e.ec2(), runArgs) + instResp, err = runInstances(e.ec2, runArgs) if err == nil || !isZoneOrSubnetConstrainedError(err) { break } @@ -555,7 +565,7 @@ names.NewMachineTag(args.InstanceConfig.MachineId), e.Config().Name(), ) args.InstanceConfig.Tags[tagName] = instanceName - if err := tagResources(e.ec2(), args.InstanceConfig.Tags, string(inst.Id())); err != nil { + if err := tagResources(e.ec2, args.InstanceConfig.Tags, string(inst.Id())); err != nil { return nil, errors.Annotate(err, "tagging instance") } @@ -568,7 +578,7 @@ cfg, ) tags[tagName] = instanceName + "-root" - if err := tagRootDisk(e.ec2(), tags, inst.Instance); err != nil { + if err := tagRootDisk(e.ec2, tags, inst.Instance); err != nil { return nil, errors.Annotate(err, "tagging root disk") } } @@ -625,14 +635,20 @@ // Wait until the instance has an associated EBS volume in the // block-device-mapping. volumeId := findVolumeId(inst) + // TODO(katco): 2016-08-09: lp:1611427 waitRootDiskAttempt := utils.AttemptStrategy{ Total: 5 * time.Minute, Delay: 5 * time.Second, } for a := waitRootDiskAttempt.Start(); volumeId == "" && a.Next(); { resp, err := e.Instances([]string{inst.InstanceId}, nil) - if err != nil { - return err + if err = errors.Annotate(err, "cannot fetch instance information"); err != nil { + logger.Warningf("%v", err) + if a.HasNext() == false { + return err + } + logger.Infof("retrying fetch of instances") + continue } if len(resp.Reservations) > 0 && len(resp.Reservations[0].Instances) > 0 { inst = &resp.Reservations[0].Instances[0] @@ -744,7 +760,7 @@ insts []instance.Instance, filter *ec2.Filter, ) error { - resp, err := e.ec2().Instances(nil, filter) + resp, err := e.ec2.Instances(nil, filter) if err != nil { return err } @@ -777,14 +793,13 @@ // NetworkInterfaces implements NetworkingEnviron.NetworkInterfaces. func (e *environ) NetworkInterfaces(instId instance.Id) ([]network.InterfaceInfo, error) { - ec2Client := e.ec2() var err error var networkInterfacesResp *ec2.NetworkInterfacesResp for a := shortAttempt.Start(); a.Next(); { logger.Tracef("retrieving NICs for instance %q", instId) filter := ec2.NewFilter() filter.Add("attachment.instance-id", string(instId)) - networkInterfacesResp, err = ec2Client.NetworkInterfaces(nil, filter) + networkInterfacesResp, err = e.ec2.NetworkInterfaces(nil, filter) logger.Tracef("instance %q NICs: %#v (err: %v)", instId, networkInterfacesResp, err) if err != nil { logger.Errorf("failed to get instance %q interfaces: %v (retrying)", instId, err) @@ -805,7 +820,7 @@ ec2Interfaces := networkInterfacesResp.Interfaces result := make([]network.InterfaceInfo, len(ec2Interfaces)) for i, iface := range ec2Interfaces { - resp, err := ec2Client.Subnets([]string{iface.SubnetId}, nil) + resp, err := e.ec2.Subnets([]string{iface.SubnetId}, nil) if err != nil { return nil, errors.Annotatef(err, "failed to retrieve subnet %q info", iface.SubnetId) } @@ -894,8 +909,7 @@ results = append(results, info) } } else { - ec2Inst := e.ec2() - resp, err := ec2Inst.Subnets(nil, nil) + resp, err := e.ec2.Subnets(nil, nil) if err != nil { return nil, errors.Annotatef(err, "failed to retrieve subnets") } @@ -1019,7 +1033,7 @@ } func (e *environ) allInstances(filter *ec2.Filter) ([]instance.Instance, error) { - resp, err := e.ec2().Instances(nil, filter) + resp, err := e.ec2.Instances(nil, filter) if err != nil { return nil, errors.Annotate(err, "listing instances") } @@ -1074,7 +1088,7 @@ if err != nil { return errors.Annotate(err, "listing volumes") } - errs := destroyVolumes(e.ec2(), volIds) + errs := destroyVolumes(e.ec2, volIds) for i, err := range errs { if err == nil { continue @@ -1088,7 +1102,7 @@ return errors.Trace(err) } for _, g := range groups { - if err := deleteSecurityGroupInsistently(e.ec2(), g, clock.WallClock); err != nil { + if err := deleteSecurityGroupInsistently(e.ec2, g, clock.WallClock); err != nil { return errors.Annotatef( err, "cannot delete security group %q (%q)", g.Name, g.Id, @@ -1101,7 +1115,7 @@ func (e *environ) allControllerManagedVolumes(controllerUUID string) ([]string, error) { filter := ec2.NewFilter() e.addControllerFilter(filter, controllerUUID) - return listVolumes(e.ec2(), filter) + return listVolumes(e.ec2, filter) } func portsToIPPerms(ports []network.PortRange) []ec2.IPPerm { @@ -1127,7 +1141,7 @@ return err } ipPerms := portsToIPPerms(ports) - _, err = e.ec2().AuthorizeSecurityGroup(g, ipPerms) + _, err = e.ec2.AuthorizeSecurityGroup(g, ipPerms) if err != nil && ec2ErrCode(err) == "InvalidPermission.Duplicate" { if len(ports) == 1 { return nil @@ -1137,7 +1151,7 @@ // otherwise the ports that were *not* duplicates will have // been ignored for i := range ipPerms { - _, err := e.ec2().AuthorizeSecurityGroup(g, ipPerms[i:i+1]) + _, err := e.ec2.AuthorizeSecurityGroup(g, ipPerms[i:i+1]) if err != nil && ec2ErrCode(err) != "InvalidPermission.Duplicate" { return fmt.Errorf("cannot open port %v: %v", ipPerms[i], err) } @@ -1161,7 +1175,7 @@ if err != nil { return err } - _, err = e.ec2().RevokeSecurityGroup(g, portsToIPPerms(ports)) + _, err = e.ec2.RevokeSecurityGroup(g, portsToIPPerms(ports)) if err != nil { return fmt.Errorf("cannot close ports: %v", err) } @@ -1222,7 +1236,6 @@ } func (e *environ) instanceSecurityGroups(instIDs []instance.Id, states ...string) ([]ec2.SecurityGroup, error) { - ec2inst := e.ec2() strInstID := make([]string, len(instIDs)) for i := range instIDs { strInstID[i] = string(instIDs[i]) @@ -1233,7 +1246,7 @@ filter.Add("instance-state-name", states...) } - resp, err := ec2inst.Instances(strInstID, filter) + resp, err := e.ec2.Instances(strInstID, filter) if err != nil { return nil, errors.Annotatef(err, "cannot retrieve instance information from aws to delete security groups") } @@ -1253,7 +1266,7 @@ func (e *environ) controllerSecurityGroups(controllerUUID string) ([]ec2.SecurityGroup, error) { filter := ec2.NewFilter() e.addControllerFilter(filter, controllerUUID) - resp, err := e.ec2().SecurityGroups(nil, filter) + resp, err := e.ec2.SecurityGroups(nil, filter) if err != nil { return nil, errors.Annotate(err, "listing security groups") } @@ -1275,7 +1288,7 @@ if err != nil { return errors.Annotatef(err, "cannot retrieve default security group: %q", jujuGroup) } - if err := deleteSecurityGroupInsistently(e.ec2(), g, clock.WallClock); err != nil { + if err := deleteSecurityGroupInsistently(e.ec2, g, clock.WallClock); err != nil { return errors.Annotate(err, "cannot delete default security group") } return nil @@ -1285,7 +1298,6 @@ if len(ids) == 0 { return nil } - ec2inst := e.ec2() // TODO (anastasiamac 2016-04-11) Err if instances still have resources hanging around. // LP#1568654 @@ -1298,7 +1310,7 @@ // in defer. Bug#1567179. var err error for a := shortAttempt.Start(); a.Next(); { - _, err = terminateInstancesById(ec2inst, ids...) + _, err = terminateInstancesById(e.ec2, ids...) if err == nil || ec2ErrCode(err) != "InvalidInstanceID.NotFound" { // This will return either success at terminating all instances (1st condition) or // encountered error as long as it's not NotFound (2nd condition). @@ -1317,7 +1329,7 @@ // So try each instance individually, ignoring a NotFound error this time. deletedIDs := []instance.Id{} for _, id := range ids { - _, err = terminateInstancesById(ec2inst, id) + _, err = terminateInstancesById(e.ec2, id) if err == nil { deletedIDs = append(deletedIDs, id) } @@ -1360,12 +1372,11 @@ // https://bugs.launchpad.net/juju-core/+bug/1534289 jujuGroup := e.jujuGroupName() - ec2inst := e.ec2() for _, deletable := range securityGroups { if deletable.Name == jujuGroup { continue } - if err := deleteSecurityGroupInsistently(ec2inst, deletable, clock.WallClock); err != nil { + if err := deleteSecurityGroupInsistently(e.ec2, deletable, clock.WallClock); err != nil { // In ideal world, we would err out here. // However: // 1. We do not know if all instances have been terminated. @@ -1501,13 +1512,13 @@ filter := ec2.NewFilter() filter.Add("vpc-id", chosenVPCID) filter.Add("group-name", groupName) - return e.ec2().SecurityGroups(nil, filter) + return e.ec2.SecurityGroups(nil, filter) } // EC2-Classic or EC2-VPC with implicit default VPC need to use the // GroupName.X arguments instead of the filters. groups := ec2.SecurityGroupNames(groupName) - return e.ec2().SecurityGroups(groups, nil) + return e.ec2.SecurityGroups(groups, nil) } // ensureGroup returns the security group with name and perms. @@ -1524,8 +1535,7 @@ inVPCLogSuffix = "" } - ec2inst := e.ec2() - resp, err := ec2inst.CreateSecurityGroup(chosenVPCID, name, "juju group") + resp, err := e.ec2.CreateSecurityGroup(chosenVPCID, name, "juju group") if err != nil && ec2ErrCode(err) != "InvalidGroup.Duplicate" { err = errors.Annotatef(err, "creating security group %q%s", name, inVPCLogSuffix) return zeroGroup, err @@ -1541,7 +1551,7 @@ names.NewModelTag(controllerUUID), cfg, ) - if err := tagResources(ec2inst, tags, g.Id); err != nil { + if err := tagResources(e.ec2, tags, g.Id); err != nil { return g, errors.Annotate(err, "tagging security group") } logger.Debugf("created security group %q with ID %q%s", name, g.Id, inVPCLogSuffix) @@ -1571,7 +1581,7 @@ } } if len(revoke) > 0 { - _, err := ec2inst.RevokeSecurityGroup(g, revoke.ipPerms()) + _, err := e.ec2.RevokeSecurityGroup(g, revoke.ipPerms()) if err != nil { err = errors.Annotatef(err, "revoking security group %q%s", g.Id, inVPCLogSuffix) return zeroGroup, err @@ -1585,7 +1595,7 @@ } } if len(add) > 0 { - _, err := ec2inst.AuthorizeSecurityGroup(g, add.ipPerms()) + _, err := e.ec2.AuthorizeSecurityGroup(g, add.ipPerms()) if err != nil { err = errors.Annotatef(err, "authorizing security group %q%s", g.Id, inVPCLogSuffix) return zeroGroup, err @@ -1715,3 +1725,7 @@ func (e *environ) AllocateContainerAddresses(hostInstanceID instance.Id, containerTag names.MachineTag, preparedInfo []network.InterfaceInfo) ([]network.InterfaceInfo, error) { return nil, errors.NotSupportedf("container address allocation") } + +func (e *environ) ReleaseContainerAddresses(interfaces []network.InterfaceInfo) error { + return errors.NotSupportedf("container address allocation") +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/environ_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/environ_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/environ_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/environ_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,6 +10,7 @@ "github.com/juju/juju/constraints" "github.com/juju/juju/environs" "github.com/juju/juju/environs/simplestreams" + "github.com/juju/juju/instance" "github.com/juju/juju/network" "github.com/juju/juju/state" ) @@ -19,7 +20,7 @@ _ environs.NetworkingEnviron = (*environ)(nil) _ simplestreams.HasRegion = (*environ)(nil) _ state.Prechecker = (*environ)(nil) - _ state.InstanceDistributor = (*environ)(nil) + _ instance.Distributor = (*environ)(nil) ) type Suite struct{} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/environ_vpc.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/environ_vpc.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/environ_vpc.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/environ_vpc.go 2016-08-16 08:56:25.000000000 +0000 @@ -436,10 +436,16 @@ return firstAttributeValue, nil } -// getVPCSubnetIDsForAvailabilityZone returns a list of subnet IDs, which are -// both in the given vpcID and the given zoneName. Returns an error satisfying -// errors.IsNotFound() otherwise. -func getVPCSubnetIDsForAvailabilityZone(apiClient vpcAPIClient, vpcID, zoneName string) ([]string, error) { +// getVPCSubnetIDsForAvailabilityZone returns a sorted list of subnet IDs, which +// are both in the given vpcID and the given zoneName. If allowedSubnetIDs is +// not empty, the returned list will only contain IDs present there. Returns an +// error satisfying errors.IsNotFound() when no results match. +func getVPCSubnetIDsForAvailabilityZone( + apiClient vpcAPIClient, + vpcID, zoneName string, + allowedSubnetIDs []string, +) ([]string, error) { + allowedSubnets := set.NewStrings(allowedSubnetIDs...) vpc := &ec2.VPC{Id: vpcID} subnets, err := getVPCSubnets(apiClient, vpc) if err != nil && !isVPCNotUsableError(err) { @@ -454,9 +460,15 @@ matchingSubnetIDs := set.NewStrings() for _, subnet := range subnets { - if subnet.AvailZone == zoneName { - matchingSubnetIDs.Add(subnet.Id) + if subnet.AvailZone != zoneName { + logger.Infof("skipping subnet %q (in VPC %q): not in the chosen AZ %q", subnet.Id, vpcID, zoneName) + continue } + if !allowedSubnets.IsEmpty() && !allowedSubnets.Contains(subnet.Id) { + logger.Infof("skipping subnet %q (in VPC %q, AZ %q): not matching spaces constraints", subnet.Id, vpcID, zoneName) + continue + } + matchingSubnetIDs.Add(subnet.Id) } if matchingSubnetIDs.IsEmpty() { @@ -464,7 +476,9 @@ return nil, errors.NewNotFound(nil, message) } - return matchingSubnetIDs.SortedValues(), nil + sortedIDs := matchingSubnetIDs.SortedValues() + logger.Infof("found %d subnets in VPC %q matching AZ %q and constraints: %v", len(sortedIDs), vpcID, zoneName, sortedIDs) + return sortedIDs, nil } func findSubnetIDsForAvailabilityZone(zoneName string, subnetsToZones map[network.Id][]string) ([]string, error) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/environ_vpc_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/environ_vpc_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/environ_vpc_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/environ_vpc_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -585,7 +585,7 @@ s.stubAPI.SetErrors(errors.New("too cloudy")) anyVPC := makeEC2VPC(anyVPCID, anyState) - subnetIDs, err := getVPCSubnetIDsForAvailabilityZone(s.stubAPI, anyVPC.Id, anyZone) + subnetIDs, err := getVPCSubnetIDsForAvailabilityZone(s.stubAPI, anyVPC.Id, anyZone, nil) c.Assert(err, gc.ErrorMatches, `cannot get VPC "vpc-anything" subnets: unexpected AWS .*: too cloudy`) c.Check(subnetIDs, gc.IsNil) @@ -596,7 +596,7 @@ s.stubAPI.SetSubnetsResponse(noResults, anyZone, noPublicIPOnLaunch) anyVPC := makeEC2VPC(anyVPCID, anyState) - subnetIDs, err := getVPCSubnetIDsForAvailabilityZone(s.stubAPI, anyVPC.Id, anyZone) + subnetIDs, err := getVPCSubnetIDsForAvailabilityZone(s.stubAPI, anyVPC.Id, anyZone, nil) c.Assert(err, gc.ErrorMatches, `VPC "vpc-anything" has no subnets in AZ "any-zone": no subnets found for VPC.*`) c.Check(err, jc.Satisfies, errors.IsNotFound) c.Check(subnetIDs, gc.IsNil) @@ -608,7 +608,7 @@ s.stubAPI.SetSubnetsResponse(3, "other-zone", noPublicIPOnLaunch) anyVPC := makeEC2VPC(anyVPCID, anyState) - subnetIDs, err := getVPCSubnetIDsForAvailabilityZone(s.stubAPI, anyVPC.Id, "given-zone") + subnetIDs, err := getVPCSubnetIDsForAvailabilityZone(s.stubAPI, anyVPC.Id, "given-zone", nil) c.Assert(err, gc.ErrorMatches, `VPC "vpc-anything" has no subnets in AZ "given-zone"`) c.Check(err, jc.Satisfies, errors.IsNotFound) c.Check(subnetIDs, gc.IsNil) @@ -616,11 +616,26 @@ s.stubAPI.CheckSingleSubnetsCall(c, anyVPC) } +func (s *vpcSuite) TestGetVPCSubnetIDsForAvailabilityZoneWithSubnetsToZones(c *gc.C) { + s.stubAPI.SetSubnetsResponse(4, "my-zone", noPublicIPOnLaunch) + // Simulate we used --constraints spaces=foo, which contains subnet-1 and + // subnet-3 out of the 4 subnets in AZ my-zone (see the related bug + // http://pad.lv/1609343). + allowedSubnetIDs := []string{"subnet-1", "subnet-3"} + + anyVPC := makeEC2VPC(anyVPCID, anyState) + subnetIDs, err := getVPCSubnetIDsForAvailabilityZone(s.stubAPI, anyVPC.Id, "my-zone", allowedSubnetIDs) + c.Assert(err, jc.ErrorIsNil) + c.Check(subnetIDs, jc.DeepEquals, []string{"subnet-1", "subnet-3"}) + + s.stubAPI.CheckSingleSubnetsCall(c, anyVPC) +} + func (s *vpcSuite) TestGetVPCSubnetIDsForAvailabilityZoneSuccess(c *gc.C) { s.stubAPI.SetSubnetsResponse(2, "my-zone", noPublicIPOnLaunch) anyVPC := makeEC2VPC(anyVPCID, anyState) - subnetIDs, err := getVPCSubnetIDsForAvailabilityZone(s.stubAPI, anyVPC.Id, "my-zone") + subnetIDs, err := getVPCSubnetIDsForAvailabilityZone(s.stubAPI, anyVPC.Id, "my-zone", nil) c.Assert(err, jc.ErrorIsNil) // Result slice of IDs is always sorted. c.Check(subnetIDs, jc.DeepEquals, []string{"subnet-0", "subnet-1"}) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/export_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -19,12 +19,8 @@ jujustorage "github.com/juju/juju/storage" ) -func EBSProvider() jujustorage.Provider { - return &ebsProvider{} -} - func StorageEC2(vs jujustorage.VolumeSource) *ec2.EC2 { - return vs.(*ebsVolumeSource).ec2 + return vs.(*ebsVolumeSource).env.ec2 } func JujuGroupName(e environs.Environ) string { @@ -36,7 +32,7 @@ } func EnvironEC2(e environs.Environ) *ec2.EC2 { - return e.(*environ).ec2() + return e.(*environ).ec2 } func InstanceEC2(inst instance.Instance) *ec2.Instance { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/init.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/init.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/init.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/init.go 2016-08-16 08:56:25.000000000 +0000 @@ -3,10 +3,7 @@ package ec2 -import ( - "github.com/juju/juju/environs" - "github.com/juju/juju/storage/provider/registry" -) +import "github.com/juju/juju/environs" const ( providerType = "ec2" @@ -14,10 +11,4 @@ func init() { environs.RegisterProvider(providerType, environProvider{}) - - //Register the AWS specific providers. - registry.RegisterProvider(EBS_ProviderType, &ebsProvider{}) - - // Inform the storage provider registry about the AWS providers. - registry.RegisterEnvironStorageProviders(providerType, EBS_ProviderType) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/init_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/init_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/init_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/init_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,35 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package ec2_test - -import ( - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/provider/ec2" - "github.com/juju/juju/storage" - "github.com/juju/juju/storage/provider/registry" - "github.com/juju/juju/testing" -) - -type providerSuite struct { - testing.BaseSuite -} - -var _ = gc.Suite(&providerSuite{}) - -func (*providerSuite) TestEBSProviderRegistered(c *gc.C) { - p, err := registry.StorageProvider(ec2.EBS_ProviderType) - c.Assert(err, jc.ErrorIsNil) - _, ok := p.(storage.Provider) - c.Assert(ok, jc.IsTrue) -} - -func (*providerSuite) TestSupportedProviders(c *gc.C) { - supported := []storage.ProviderType{ec2.EBS_ProviderType} - for _, providerType := range supported { - ok := registry.IsProviderSupported("ec2", providerType) - c.Assert(ok, jc.IsTrue) - } -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/local_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/local_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/local_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/local_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -50,12 +50,6 @@ jujuversion "github.com/juju/juju/version" ) -type ProviderSuite struct { - coretesting.BaseSuite -} - -var _ = gc.Suite(&ProviderSuite{}) - var localConfigAttrs = coretesting.FakeConfig().Merge(coretesting.Attrs{ "name": "sample", "type": "ec2", @@ -301,8 +295,8 @@ func (t *localServerSuite) TestPrepareForBootstrapWithNotRecommendedButForcedVPCID(c *gc.C) { t.makeTestingDefaultVPCUnavailable(c) params := t.PrepareParams(c) - params.BaseConfig["vpc-id"] = t.srv.defaultVPC.Id - params.BaseConfig["vpc-id-force"] = true + params.ModelConfig["vpc-id"] = t.srv.defaultVPC.Id + params.ModelConfig["vpc-id-force"] = true t.prepareWithParamsAndBootstrapWithVPCID(c, params, t.srv.defaultVPC.Id) } @@ -311,7 +305,7 @@ const emptyVPCID = "" params := t.PrepareParams(c) - params.BaseConfig["vpc-id"] = emptyVPCID + params.ModelConfig["vpc-id"] = emptyVPCID t.prepareWithParamsAndBootstrapWithVPCID(c, params, emptyVPCID) } @@ -333,14 +327,14 @@ func (t *localServerSuite) TestPrepareForBootstrapWithVPCIDNone(c *gc.C) { params := t.PrepareParams(c) - params.BaseConfig["vpc-id"] = "none" + params.ModelConfig["vpc-id"] = "none" t.prepareWithParamsAndBootstrapWithVPCID(c, params, ec2.VPCIDNone) } func (t *localServerSuite) TestPrepareForBootstrapWithDefaultVPCID(c *gc.C) { params := t.PrepareParams(c) - params.BaseConfig["vpc-id"] = t.srv.defaultVPC.Id + params.ModelConfig["vpc-id"] = t.srv.defaultVPC.Id t.prepareWithParamsAndBootstrapWithVPCID(c, params, t.srv.defaultVPC.Id) } @@ -617,7 +611,10 @@ cfg, err := env.Config().Apply(map[string]interface{}{"controller-uuid": "7e386e08-cba7-44a4-a76e-7c1633584210"}) c.Assert(err, jc.ErrorIsNil) - env, err = environs.New(cfg) + env, err = environs.New(environs.OpenParams{ + Cloud: t.CloudSpec(), + Config: cfg, + }) c.Assert(err, jc.ErrorIsNil) msg := "destroy security group error" @@ -640,12 +637,13 @@ "firewall-mode": "global", }) c.Assert(err, jc.ErrorIsNil) - env, err := environs.New(cfg) + env, err := environs.New(environs.OpenParams{t.CloudSpec(), cfg}) c.Assert(err, jc.ErrorIsNil) inst, _ := testing.AssertStartInstance(c, env, t.ControllerUUID, "0") c.Assert(err, jc.ErrorIsNil) - ebsProvider := ec2.EBSProvider() - vs, err := ebsProvider.VolumeSource(env.Config(), nil) + ebsProvider, err := env.StorageProvider(ec2.EBS_ProviderType) + c.Assert(err, jc.ErrorIsNil) + vs, err := ebsProvider.VolumeSource(nil) c.Assert(err, jc.ErrorIsNil) volumeResults, err := vs.CreateVolumes([]storage.VolumeParams{{ Tag: names.NewVolumeTag("0"), @@ -1163,7 +1161,7 @@ c.Assert(err, jc.ErrorIsNil) cons := constraints.MustParse("arch=ppc64el") _, err = validator.Validate(cons) - c.Assert(err, gc.ErrorMatches, "invalid constraint value: arch=ppc64el\nvalid values are:.*") + c.Assert(err, gc.ErrorMatches, "invalid constraint value: arch=ppc64el\nvalid values are: \\[amd64 i386\\]") cons = constraints.MustParse("instance-type=foo") _, err = validator.Validate(cons) c.Assert(err, gc.ErrorMatches, "invalid constraint value: instance-type=foo\nvalid values are:.*") @@ -1248,13 +1246,6 @@ c.Assert(sources, gc.HasLen, 0) } -func (t *localServerSuite) TestSupportedArchitectures(c *gc.C) { - env := t.Prepare(c) - a, err := env.SupportedArchitectures() - c.Assert(err, jc.ErrorIsNil) - c.Assert(a, jc.SameContents, []string{"amd64", "i386"}) -} - func (t *localServerSuite) TestSupportsNetworking(c *gc.C) { env := t.Prepare(c) _, supported := environs.SupportsNetworking(env) @@ -1484,23 +1475,27 @@ } t.srv.startServer(c) + credential := cloud.NewCredential( + cloud.AccessKeyAuthType, + map[string]string{ + "access-key": "x", + "secret-key": "x", + }, + ) + env, err := bootstrap.Prepare( envtesting.BootstrapContext(c), jujuclienttesting.NewMemStore(), bootstrap.PrepareParams{ ControllerConfig: coretesting.FakeControllerConfig(), - BaseConfig: localConfigAttrs, - Credential: cloud.NewCredential( - cloud.AccessKeyAuthType, - map[string]string{ - "access-key": "x", - "secret-key": "x", - }, - ), - ControllerName: localConfigAttrs["name"].(string), - CloudName: "ec2", - CloudRegion: "test", - AdminSecret: testing.AdminSecret, + ModelConfig: localConfigAttrs, + ControllerName: localConfigAttrs["name"].(string), + Cloud: environs.CloudSpec{ + Type: "ec2", + Region: "test", + Credential: &credential, + }, + AdminSecret: testing.AdminSecret, }, ) c.Assert(err, jc.ErrorIsNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/provider.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/provider.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/provider.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/provider.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,7 +9,9 @@ "github.com/juju/errors" "github.com/juju/loggo" "github.com/juju/utils/arch" + "gopkg.in/amz.v3/aws" "gopkg.in/amz.v3/ec2" + "gopkg.in/amz.v3/s3" "github.com/juju/juju/cloud" "github.com/juju/juju/environs" @@ -29,93 +31,78 @@ func (p environProvider) RestrictedConfigAttributes() []string { // TODO(dimitern): Both of these shouldn't be restricted for hosted models. // See bug http://pad.lv/1580417 for more information. - return []string{"region", "vpc-id-force"} + return []string{"vpc-id-force"} } -// PrepareForCreateEnvironment is specified in the EnvironProvider interface. -func (p environProvider) PrepareForCreateEnvironment(controllerUUID string, cfg *config.Config) (*config.Config, error) { - e, err := p.Open(cfg) +// Open is specified in the EnvironProvider interface. +func (p environProvider) Open(args environs.OpenParams) (environs.Environ, error) { + logger.Infof("opening model %q", args.Config.Name()) + + e := new(environ) + e.cloud = args.Cloud + e.name = args.Config.Name() + + var err error + e.ec2, e.s3, err = awsClients(args.Cloud) if err != nil { - return nil, err + return nil, errors.Trace(err) } - env := e.(*environ) - apiClient, modelName, vpcID := env.ec2(), env.Name(), env.ecfg().vpcID() - if err := validateModelVPC(apiClient, modelName, vpcID); err != nil { + if err := e.SetConfig(args.Config); err != nil { return nil, errors.Trace(err) } - - return cfg, nil + return e, nil } -// Open is specified in the EnvironProvider interface. -func (p environProvider) Open(cfg *config.Config) (environs.Environ, error) { - logger.Infof("opening model %q", cfg.Name()) - e := new(environ) - e.name = cfg.Name() - err := e.SetConfig(cfg) - if err != nil { - return nil, err +func awsClients(cloud environs.CloudSpec) (*ec2.EC2, *s3.S3, error) { + if err := validateCloudSpec(cloud); err != nil { + return nil, nil, errors.Annotate(err, "validating cloud spec") } - return e, nil -} -// BootstrapConfig is specified in the EnvironProvider interface. -func (p environProvider) BootstrapConfig(args environs.BootstrapConfigParams) (*config.Config, error) { - // Add credentials to the configuration. - attrs := map[string]interface{}{ - "region": args.CloudRegion, - // TODO(axw) stop relying on hard-coded - // region endpoint information - // in the provider, and use - // args.CloudEndpoint here. - } - switch authType := args.Credentials.AuthType(); authType { - case cloud.AccessKeyAuthType: - credentialAttrs := args.Credentials.Attributes() - attrs["access-key"] = credentialAttrs["access-key"] - attrs["secret-key"] = credentialAttrs["secret-key"] - default: - return nil, errors.NotSupportedf("%q auth-type", authType) + credentialAttrs := cloud.Credential.Attributes() + accessKey := credentialAttrs["access-key"] + secretKey := credentialAttrs["secret-key"] + auth := aws.Auth{ + AccessKey: accessKey, + SecretKey: secretKey, } + // TODO(axw) define region in terms of EC2 and S3 endpoints. + region := aws.Regions[cloud.Region] + signer := aws.SignV4Factory(region.Name, "ec2") + return ec2.New(auth, region, signer), s3.New(auth, region), nil +} + +// PrepareConfig is specified in the EnvironProvider interface. +func (p environProvider) PrepareConfig(args environs.PrepareConfigParams) (*config.Config, error) { + if err := validateCloudSpec(args.Cloud); err != nil { + return nil, errors.Annotate(err, "validating cloud spec") + } // Set the default block-storage source. + attrs := make(map[string]interface{}) if _, ok := args.Config.StorageDefaultBlockSource(); !ok { attrs[config.StorageDefaultBlockSourceKey] = EBS_ProviderType } - - cfg, err := args.Config.Apply(attrs) - if err != nil { - return nil, errors.Trace(err) + if len(attrs) == 0 { + return args.Config, nil } - - return cfg, nil + return args.Config.Apply(attrs) } -// PrepareForBootstrap is specified in the EnvironProvider interface. -func (p environProvider) PrepareForBootstrap( - ctx environs.BootstrapContext, - cfg *config.Config, -) (environs.Environ, error) { - e, err := p.Open(cfg) - if err != nil { - return nil, err +func validateCloudSpec(c environs.CloudSpec) error { + if err := c.Validate(); err != nil { + return errors.Trace(err) } - - env := e.(*environ) - if ctx.ShouldVerifyCredentials() { - if err := verifyCredentials(env); err != nil { - return nil, err - } + if _, ok := aws.Regions[c.Region]; !ok { + return errors.NotValidf("region name %q", c.Region) } - - apiClient, ecfg := env.ec2(), env.ecfg() - region, vpcID, forceVPCID := ecfg.region(), ecfg.vpcID(), ecfg.forceVPCID() - if err := validateBootstrapVPC(apiClient, region, vpcID, forceVPCID, ctx); err != nil { - return nil, errors.Trace(err) + if c.Credential == nil { + return errors.NotValidf("missing credential") } - - return e, nil + if authType := c.Credential.AuthType(); authType != cloud.AccessKeyAuthType { + return errors.NotSupportedf("%q auth-type", authType) + } + return nil } // Validate is specified in the EnvironProvider interface. @@ -146,14 +133,7 @@ // SecretAttrs is specified in the EnvironProvider interface. func (environProvider) SecretAttrs(cfg *config.Config) (map[string]string, error) { - m := make(map[string]string) - ecfg, err := providerInstance.newConfig(cfg) - if err != nil { - return nil, err - } - m["access-key"] = ecfg.accessKey() - m["secret-key"] = ecfg.secretKey() - return m, nil + return make(map[string]string), nil } const badAccessKey = ` @@ -171,7 +151,7 @@ // error will be returned, and the original error will be logged at debug // level. var verifyCredentials = func(e *environ) error { - _, err := e.ec2().AccountAttributes() + _, err := e.ec2.AccountAttributes() if err != nil { logger.Debugf("ec2 request failed: %v", err) if err, ok := err.(*ec2.Error); ok { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/provider_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/provider_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/provider_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/provider_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,77 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package ec2_test + +import ( + "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cloud" + "github.com/juju/juju/environs" + coretesting "github.com/juju/juju/testing" +) + +type ProviderSuite struct { + testing.IsolationSuite + spec environs.CloudSpec + provider environs.EnvironProvider +} + +var _ = gc.Suite(&ProviderSuite{}) + +func (s *ProviderSuite) SetUpTest(c *gc.C) { + s.IsolationSuite.SetUpTest(c) + + credential := cloud.NewCredential( + cloud.AccessKeyAuthType, + map[string]string{ + "access-key": "foo", + "secret-key": "bar", + }, + ) + s.spec = environs.CloudSpec{ + Type: "ec2", + Name: "aws", + Region: "us-east-1", + Credential: &credential, + } + + provider, err := environs.Provider("ec2") + c.Assert(err, jc.ErrorIsNil) + s.provider = provider +} + +func (s *ProviderSuite) TestOpen(c *gc.C) { + env, err := s.provider.Open(environs.OpenParams{ + Cloud: s.spec, + Config: coretesting.ModelConfig(c), + }) + c.Assert(err, jc.ErrorIsNil) + c.Assert(env, gc.NotNil) +} + +func (s *ProviderSuite) TestOpenInvalidRegion(c *gc.C) { + s.spec.Region = "foobar" + s.testOpenError(c, s.spec, `validating cloud spec: region name "foobar" not valid`) +} + +func (s *ProviderSuite) TestOpenMissingCredential(c *gc.C) { + s.spec.Credential = nil + s.testOpenError(c, s.spec, `validating cloud spec: missing credential not valid`) +} + +func (s *ProviderSuite) TestOpenUnsupportedCredential(c *gc.C) { + credential := cloud.NewCredential(cloud.UserPassAuthType, map[string]string{}) + s.spec.Credential = &credential + s.testOpenError(c, s.spec, `validating cloud spec: "userpass" auth-type not supported`) +} + +func (s *ProviderSuite) testOpenError(c *gc.C, spec environs.CloudSpec, expect string) { + _, err := s.provider.Open(environs.OpenParams{ + Cloud: spec, + Config: coretesting.ModelConfig(c), + }) + c.Assert(err, gc.ErrorMatches, expect) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/storage.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/storage.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/ec2/storage.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/ec2/storage.go 2016-08-16 08:56:25.000000000 +0000 @@ -79,12 +79,15 @@ return s.bucket.SignedURL(name, maxExpiratoryPeriod) } +// TODO(katco): 2016-08-09: lp:1611427 var storageAttempt = utils.AttemptStrategy{ Total: 5 * time.Second, Delay: 200 * time.Millisecond, } // DefaultConsistencyStrategy is specified in the StorageReader interface. +// +// TODO(katco): 2016-08-09: lp:1611427 func (s *ec2storage) DefaultConsistencyStrategy() utils.AttemptStrategy { return storageAttempt } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/config.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/config.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/config.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/config.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,7 +9,6 @@ "gopkg.in/juju/environschema.v1" "github.com/juju/juju/environs/config" - "github.com/juju/juju/provider/gce/google" ) // TODO(ericsnow) While not strictly config-related, we could use some @@ -22,68 +21,7 @@ // that we can use to validate this provider's potentially out-of-date // data. -// The GCE-specific config keys. -const ( - cfgPrivateKey = "private-key" - cfgClientID = "client-id" - cfgClientEmail = "client-email" - cfgRegion = "region" - cfgProjectID = "project-id" - cfgImageEndpoint = "image-endpoint" -) - -var configSchema = environschema.Fields{ - cfgPrivateKey: { - Type: environschema.Tstring, - Description: cfgPrivateKey + ` is the private key that matches the public key -associated with the GCE account.`, - EnvVar: google.OSEnvPrivateKey, - Group: environschema.AccountGroup, - Secret: true, - Mandatory: true, - }, - cfgClientID: { - Type: environschema.Tstring, - Description: cfgClientID + ` is the GCE account's OAuth ID.`, - EnvVar: google.OSEnvClientID, - Group: environschema.AccountGroup, - Mandatory: true, - }, - cfgClientEmail: { - Type: environschema.Tstring, - Description: cfgClientEmail + ` is the email address associated with the GCE account.`, - EnvVar: google.OSEnvClientEmail, - Group: environschema.AccountGroup, - Mandatory: true, - }, - cfgProjectID: { - Type: environschema.Tstring, - Description: cfgProjectID + ` is the project ID to use in all GCE API requests`, - EnvVar: google.OSEnvProjectID, - Group: environschema.AccountGroup, - Mandatory: true, - }, - cfgRegion: { - Type: environschema.Tstring, - Description: cfgRegion + ` is the GCE region in which to operate`, - EnvVar: google.OSEnvRegion, - }, - cfgImageEndpoint: { - Type: environschema.Tstring, - Description: cfgImageEndpoint + ` identifies where the provider should look for cloud images (i.e. for simplestreams)`, - EnvVar: google.OSEnvImageEndpoint, - }, -} - -var osEnvFields = func() map[string]string { - m := make(map[string]string) - for name, f := range configSchema { - if f.EnvVar != "" { - m[f.EnvVar] = name - } - } - return m -}() +var configSchema = environschema.Fields{} // configFields is the spec for each GCE config value's type. var configFields = func() schema.Fields { @@ -94,28 +32,13 @@ return fs }() -// TODO(ericsnow) Do we need custom defaults for "image-metadata-url" or -// "agent-metadata-url"? The defaults are the official ones (e.g. -// cloud-images). -var configDefaults = schema.Defaults{ - // See http://cloud-images.ubuntu.com/releases/streams/v1/com.ubuntu.cloud:released:gce.json - cfgImageEndpoint: "https://www.googleapis.com", -} +var configImmutableFields = []string{} -var configImmutableFields = []string{ - cfgPrivateKey, - cfgClientID, - cfgClientEmail, - cfgRegion, - cfgProjectID, - cfgImageEndpoint, -} +var configDefaults = schema.Defaults{} type environConfig struct { - config *config.Config - attrs map[string]interface{} - credentials google.Credentials - conn google.ConnectionConfig + config *config.Config + attrs map[string]interface{} } // newConfig builds a new environConfig from the provided Config @@ -130,101 +53,29 @@ if err != nil { return nil, errors.Trace(err) } - // We don't allow an empty string for any attribute. - for attr, value := range attrs { - if value == "" { - return nil, errors.Errorf("%s: must not be empty", attr) - } - } - newCfg, err := cfg.Apply(attrs) - if err != nil { - return nil, errors.Trace(err) - } - credentials, err := parseCredentials(attrs) - if err != nil { - return nil, errors.Trace(err) - } - ecfg := &environConfig{ - config: newCfg, - attrs: attrs, - credentials: *credentials, - conn: google.ConnectionConfig{ - Region: attrs[cfgRegion].(string), - ProjectID: attrs[cfgProjectID].(string), - }, - } - // Verify that the connection object is valid. - if err := ecfg.conn.Validate(); err != nil { - return nil, errors.Trace(handleInvalidFieldError(err)) - } - if old == nil { - return ecfg, nil - } - // There's an old configuration. Validate it so that any - // default values are correctly coerced for when we - // check the old values later. - oldEcfg, err := newConfig(old, nil) - if err != nil { - return nil, errors.Annotatef(err, "invalid base config") - } - for _, attr := range configImmutableFields { - oldv, newv := oldEcfg.attrs[attr], ecfg.attrs[attr] - if oldv != newv { - return nil, errors.Errorf("%s: cannot change from %v to %v", attr, oldv, newv) - } - } - return ecfg, nil -} - -func (c *environConfig) region() string { - return c.conn.Region -} - -// imageEndpoint identifies where the provider should look for -// cloud images (i.e. for simplestreams). -func (c *environConfig) imageEndpoint() string { - return c.attrs[cfgImageEndpoint].(string) -} -// secret returns the secret configuration values. -func (c *environConfig) secret() map[string]string { - secretAttrs := make(map[string]string) - for attr, val := range c.attrs { - if configSchema[attr].Secret { - secretAttrs[attr] = val.(string) + if old != nil { + // There's an old configuration. Validate it so that any + // default values are correctly coerced for when we check + // the old values later. + oldEcfg, err := newConfig(old, nil) + if err != nil { + return nil, errors.Annotatef(err, "invalid base config") } - } - return secretAttrs -} - -// parseCredentials extracts the OAuth2 info from the config from the -// individual fields (falling back on the JSON file). -func parseCredentials(attrs map[string]interface{}) (*google.Credentials, error) { - values := make(map[string]string) - for attr, val := range attrs { - f := configSchema[attr] - if f.Group == environschema.AccountGroup && f.EnvVar != "" { - values[f.EnvVar] = val.(string) + for _, attr := range configImmutableFields { + oldv, newv := oldEcfg.attrs[attr], attrs[attr] + if oldv != newv { + return nil, errors.Errorf( + "%s: cannot change from %v to %v", + attr, oldv, newv, + ) + } } } - creds, err := google.NewCredentials(values) - if err != nil { - return nil, handleInvalidFieldError(err) - } - return creds, nil -} -// handleInvalidFieldError converts a google.InvalidConfigValue into a new -// error, translating a {provider/gce/google}.OSEnvVar* value into a -// GCE config key in the new error. -func handleInvalidFieldError(err error) error { - vErr, ok := errors.Cause(err).(*google.InvalidConfigValueError) - if !ok { - return err - } - vErr.Key = osEnvFields[vErr.Key] - if vErr.Value == "" { - return errors.Errorf("%s: must not be empty", vErr.Key) + ecfg := &environConfig{ + config: cfg, + attrs: attrs, } - return err + return ecfg, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/config_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/config_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/config_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/config_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -106,50 +106,6 @@ } var newConfigTests = []configTestSpec{{ - info: "client-id is required", - remove: []string{"client-id"}, - err: "client-id: expected string, got nothing", -}, { - info: "client-id cannot be empty", - insert: testing.Attrs{"client-id": ""}, - err: "client-id: must not be empty", -}, { - info: "private-key is required", - remove: []string{"private-key"}, - err: "private-key: expected string, got nothing", -}, { - info: "private-key cannot be empty", - insert: testing.Attrs{"private-key": ""}, - err: "private-key: must not be empty", -}, { - info: "client-email is required", - remove: []string{"client-email"}, - err: "client-email: expected string, got nothing", -}, { - info: "client-email cannot be empty", - insert: testing.Attrs{"client-email": ""}, - err: "client-email: must not be empty", -}, { - info: "region cannot be empty", - insert: testing.Attrs{"region": ""}, - err: "region: must not be empty", -}, { - info: "project-id is required", - remove: []string{"project-id"}, - err: "project-id: expected string, got nothing", -}, { - info: "project-id cannot be empty", - insert: testing.Attrs{"project-id": ""}, - err: "project-id: must not be empty", -}, { - info: "image-endpoint is inserted if missing", - remove: []string{"image-endpoint"}, - expect: testing.Attrs{"image-endpoint": "https://www.googleapis.com"}, -}, { - info: "image-endpoint cannot be empty", - insert: testing.Attrs{"image-endpoint": ""}, - err: "image-endpoint: must not be empty", -}, { info: "unknown field is not touched", insert: testing.Attrs{"unknown-field": 12345}, expect: testing.Attrs{"unknown-field": 12345}, @@ -160,7 +116,10 @@ c.Logf("test %d: %s", i, test.info) testConfig := test.newConfig(c) - environ, err := environs.New(testConfig) + environ, err := environs.New(environs.OpenParams{ + Cloud: gce.MakeTestCloudSpec(), + Config: testConfig, + }) // Check the result if test.err != "" { @@ -209,26 +168,6 @@ info: "no change, no error", expect: gce.ConfigAttrs, }, { - info: "cannot change private-key", - insert: testing.Attrs{"private-key": "okkult"}, - err: "private-key: cannot change from " + gce.PrivateKey + " to okkult", -}, { - info: "cannot change client-id", - insert: testing.Attrs{"client-id": "mutant"}, - err: "client-id: cannot change from " + gce.ClientID + " to mutant", -}, { - info: "cannot change client-email", - insert: testing.Attrs{"client-email": "spam@eggs.com"}, - err: "client-email: cannot change from " + gce.ClientEmail + " to spam@eggs.com", -}, { - info: "cannot change region", - insert: testing.Attrs{"region": "not home"}, - err: "region: cannot change from home to not home", -}, { - info: "cannot change project-id", - insert: testing.Attrs{"project-id": "your-juju"}, - err: "project-id: cannot change from my-juju to your-juju", -}, { info: "can insert unknown field", insert: testing.Attrs{"unknown": "ignoti"}, expect: testing.Attrs{"unknown": "ignoti"}, @@ -251,36 +190,14 @@ } } -func (s *ConfigSuite) TestValidateChangeFromOldConfigWithMissingAttributeWithDefault(c *gc.C) { - // If the provider implementation is changed to add a new configuration attribute - // that has a default value, and the controller is upgraded to the new implementation, - // the validated configuration in the state will have no value for that attribute, - // even though newly validated configurations will. - // - // This test ensures that we can deal with that scenario by removing - // an attribute which currently has a default, and checking that we can - // still validate OK. - - oldCfg := configTestSpec{ - remove: []string{"image-endpoint"}, - }.newConfig(c) - - newCfg, err := testing.ModelConfig(c).Apply(gce.ConfigAttrs.Merge(testing.Attrs{ - "image-endpoint": "https://www.googleapis.com", - })) - c.Assert(err, jc.ErrorIsNil) - - validatedConfig, err := gce.Provider.Validate(newCfg, oldCfg) - - c.Assert(err, jc.ErrorIsNil) - c.Assert(validatedConfig.AllAttrs()["image-endpoint"], gc.Equals, "https://www.googleapis.com") -} - func (s *ConfigSuite) TestSetConfig(c *gc.C) { for i, test := range changeConfigTests { c.Logf("test %d: %s", i, test.info) - environ, err := environs.New(s.config) + environ, err := environs.New(environs.OpenParams{ + Cloud: gce.MakeTestCloudSpec(), + Config: s.config, + }) c.Assert(err, jc.ErrorIsNil) testConfig := test.newConfig(c) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/credentials.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/credentials.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/credentials.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/credentials.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,6 +5,7 @@ import ( "fmt" + "io" "os" "path/filepath" "runtime" @@ -16,29 +17,39 @@ "github.com/juju/juju/provider/gce/google" ) +const ( + credAttrPrivateKey = "private-key" + credAttrClientID = "client-id" + credAttrClientEmail = "client-email" + credAttrProjectID = "project-id" + + // The contents of the file for "jsonfile" auth-type. + credAttrFile = "file" +) + type environProviderCredentials struct{} // CredentialSchemas is part of the environs.ProviderCredentials interface. func (environProviderCredentials) CredentialSchemas() map[cloud.AuthType]cloud.CredentialSchema { return map[cloud.AuthType]cloud.CredentialSchema{ cloud.OAuth2AuthType: {{ - Name: cfgClientID, + Name: credAttrClientID, CredentialAttr: cloud.CredentialAttr{Description: "client ID"}, }, { - Name: cfgClientEmail, + Name: credAttrClientEmail, CredentialAttr: cloud.CredentialAttr{Description: "client e-mail address"}, }, { - Name: cfgPrivateKey, + Name: credAttrPrivateKey, CredentialAttr: cloud.CredentialAttr{ Description: "client secret", Hidden: true, }, }, { - Name: cfgProjectID, + Name: credAttrProjectID, CredentialAttr: cloud.CredentialAttr{Description: "project ID"}, }}, cloud.JSONFileAuthType: {{ - Name: "file", + Name: credAttrFile, CredentialAttr: cloud.CredentialAttr{ Description: "path to the .json file containing your Google Compute Engine project credentials", FilePath: true, @@ -73,7 +84,14 @@ if possibleFilePath == "" { return nil, errors.NotFoundf("gce credentials") } - parsedCred, err := ParseJSONAuthFile(possibleFilePath) + + authFile, err := os.Open(possibleFilePath) + if err != nil { + return nil, errors.Trace(err) + } + defer authFile.Close() + + parsedCred, err := parseJSONAuthFile(authFile) if err != nil { return nil, errors.Annotatef(err, "invalid json credential file %s", possibleFilePath) } @@ -85,9 +103,9 @@ cred := cloud.NewCredential(cloud.JSONFileAuthType, map[string]string{ "file": possibleFilePath, }) - credName := parsedCred.Attributes()[cfgClientEmail] + credName := parsedCred.Attributes()[credAttrClientEmail] if credName == "" { - credName = parsedCred.Attributes()[cfgClientID] + credName = parsedCred.Attributes()[credAttrClientID] } cred.Label = fmt.Sprintf("google credential %q", credName) return &cloud.CloudCredential{ @@ -105,24 +123,16 @@ return filepath.Join(utils.Home(), ".config", "gcloud", f) } -// ParseJSONAuthFile parses the file with the given path, and extracts -// the OAuth2 credentials within. -// -// TODO(axw) unexport this after 2.0-beta10 is out. -func ParseJSONAuthFile(filename string) (cloud.Credential, error) { - authFile, err := os.Open(filename) - if err != nil { - return cloud.Credential{}, errors.Trace(err) - } - defer authFile.Close() - creds, err := google.ParseJSONKey(authFile) +// parseJSONAuthFile parses a file, and extracts the OAuth2 credentials within. +func parseJSONAuthFile(r io.Reader) (cloud.Credential, error) { + creds, err := google.ParseJSONKey(r) if err != nil { return cloud.Credential{}, errors.Trace(err) } return cloud.NewCredential(cloud.OAuth2AuthType, map[string]string{ - cfgProjectID: creds.ProjectID, - cfgClientID: creds.ClientID, - cfgClientEmail: creds.ClientEmail, - cfgPrivateKey: string(creds.PrivateKey), + credAttrProjectID: creds.ProjectID, + credAttrClientID: creds.ClientID, + credAttrClientEmail: creds.ClientEmail, + credAttrPrivateKey: string(creds.PrivateKey), }), nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/credentials_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/credentials_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/credentials_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/credentials_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -62,7 +62,7 @@ func (s *credentialsSuite) TestJSONFileCredentialsValid(c *gc.C) { dir := c.MkDir() filename := filepath.Join(dir, "somefile") - err := ioutil.WriteFile(filename, []byte{}, 0600) + err := ioutil.WriteFile(filename, []byte("contents"), 0600) c.Assert(err, jc.ErrorIsNil) envtesting.AssertProviderCredentialsValid(c, s.provider, "jsonfile", map[string]string{ // For now at least, the contents of the file are not validated diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/disks.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/disks.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/disks.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/disks.go 2016-08-16 08:56:25.000000000 +0000 @@ -12,7 +12,6 @@ "github.com/juju/utils" "github.com/juju/utils/set" - "github.com/juju/juju/environs/config" "github.com/juju/juju/provider/gce/google" "github.com/juju/juju/storage" ) @@ -21,11 +20,22 @@ storageProviderType = storage.ProviderType("gce") ) -func init() { - //TODO(perrito666) Add explicit pools. +// StorageProviderTypes implements storage.ProviderRegistry. +func (env *environ) StorageProviderTypes() []storage.ProviderType { + return []storage.ProviderType{storageProviderType} } -type storageProvider struct{} +// StorageProvider implements storage.ProviderRegistry. +func (env *environ) StorageProvider(t storage.ProviderType) (storage.Provider, error) { + if t == storageProviderType { + return &storageProvider{env}, nil + } + return nil, errors.NotFoundf("storage provider %q", t) +} + +type storageProvider struct { + env *environ +} var _ storage.Provider = (*storageProvider)(nil) @@ -45,7 +55,12 @@ return true } -func (g *storageProvider) FilesystemSource(environConfig *config.Config, providerConfig *storage.Config) (storage.FilesystemSource, error) { +func (g *storageProvider) DefaultPools() []*storage.Config { + // TODO(perrito666) Add explicit pools. + return nil +} + +func (g *storageProvider) FilesystemSource(providerConfig *storage.Config) (storage.FilesystemSource, error) { return nil, errors.NotSupportedf("filesystems") } @@ -55,15 +70,10 @@ modelUUID string } -func (g *storageProvider) VolumeSource(environConfig *config.Config, cfg *storage.Config) (storage.VolumeSource, error) { - // Connect and authenticate. - env, err := newEnviron(environConfig) - if err != nil { - return nil, errors.Annotate(err, "cannot create an environ with this config") - } - +func (g *storageProvider) VolumeSource(cfg *storage.Config) (storage.VolumeSource, error) { + environConfig := g.env.Config() source := &volumeSource{ - gce: env.gce, + gce: g.env.gce, envName: environConfig.Name(), modelUUID: environConfig.UUID(), } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/disks_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/disks_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/disks_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/disks_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,7 +8,6 @@ gc "gopkg.in/check.v1" "gopkg.in/juju/names.v2" - "github.com/juju/juju/environs/config" "github.com/juju/juju/instance" "github.com/juju/juju/provider/gce" "github.com/juju/juju/provider/gce/google" @@ -24,7 +23,10 @@ func (s *storageProviderSuite) SetUpTest(c *gc.C) { s.BaseSuite.SetUpTest(c) - s.provider = gce.GCEStorageProvider() + + var err error + s.provider, err = s.Env.StorageProvider("gce") + c.Assert(err, jc.ErrorIsNil) } func (s *storageProviderSuite) TestValidateConfig(c *gc.C) { @@ -47,22 +49,19 @@ } func (s *storageProviderSuite) TestFSSource(c *gc.C) { - eConfig := &config.Config{} sConfig := &storage.Config{} - _, err := s.provider.FilesystemSource(eConfig, sConfig) + _, err := s.provider.FilesystemSource(sConfig) c.Check(err, gc.ErrorMatches, "filesystems not supported") } func (s *storageProviderSuite) TestVolumeSource(c *gc.C) { - connCfg := s.BaseSuite.Config storageCfg := &storage.Config{} - _, err := s.provider.VolumeSource(connCfg, storageCfg) + _, err := s.provider.VolumeSource(storageCfg) c.Check(err, jc.ErrorIsNil) } type volumeSourceSuite struct { gce.BaseSuite - provider storage.Provider source storage.VolumeSource params []storage.VolumeParams instId instance.Id @@ -73,9 +72,10 @@ func (s *volumeSourceSuite) SetUpTest(c *gc.C) { s.BaseSuite.SetUpTest(c) - s.provider = gce.GCEStorageProvider() - var err error - s.source, err = s.provider.VolumeSource(s.BaseSuite.Config, &storage.Config{}) + + provider, err := s.Env.StorageProvider("gce") + c.Assert(err, jc.ErrorIsNil) + s.source, err = provider.VolumeSource(&storage.Config{}) c.Check(err, jc.ErrorIsNil) inst := gce.NewInstance(s.BaseInstance, s.Env) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/environ_availzones.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/environ_availzones.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/environ_availzones.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/environ_availzones.go 2016-08-16 08:56:25.000000000 +0000 @@ -14,7 +14,7 @@ // AvailabilityZones returns all availability zones in the environment. func (env *environ) AvailabilityZones() ([]common.AvailabilityZone, error) { - zones, err := env.gce.AvailabilityZones(env.ecfg.region()) + zones, err := env.gce.AvailabilityZones(env.cloud.Region) if err != nil { return nil, errors.Trace(err) } @@ -54,7 +54,7 @@ } func (env *environ) availZone(name string) (*google.AvailabilityZone, error) { - zones, err := env.gce.AvailabilityZones(env.ecfg.region()) + zones, err := env.gce.AvailabilityZones(env.cloud.Region) if err != nil { return nil, errors.Trace(err) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/environ_availzones_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/environ_availzones_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/environ_availzones_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/environ_availzones_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -51,7 +51,7 @@ c.Check(s.FakeConn.Calls, gc.HasLen, 1) c.Check(s.FakeConn.Calls[0].FuncName, gc.Equals, "AvailabilityZones") - c.Check(s.FakeConn.Calls[0].Region, gc.Equals, "home") + c.Check(s.FakeConn.Calls[0].Region, gc.Equals, "us-east1") } func (s *environAZSuite) TestInstanceAvailabilityZoneNames(c *gc.C) { @@ -137,7 +137,7 @@ s.FakeCommon.CheckCalls(c, []gce.FakeCall{}) c.Check(s.FakeConn.Calls, gc.HasLen, 1) c.Check(s.FakeConn.Calls[0].FuncName, gc.Equals, "AvailabilityZones") - c.Check(s.FakeConn.Calls[0].Region, gc.Equals, "home") + c.Check(s.FakeConn.Calls[0].Region, gc.Equals, "us-east1") } func (s *environAZSuite) TestParseAvailabilityZonesPlacementUnavailable(c *gc.C) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/environ_broker.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/environ_broker.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/environ_broker.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/environ_broker.go 2016-08-16 08:56:25.000000000 +0000 @@ -96,7 +96,7 @@ series := args.Tools.OneSeries() spec, err := findInstanceSpec( env, &instances.InstanceConstraint{ - Region: env.ecfg.region(), + Region: env.cloud.Region, Series: series, Arches: arches, Constraints: args.Constraints, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/environ.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/environ.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/environ.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/environ.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,10 +4,12 @@ package gce import ( + "strings" "sync" "github.com/juju/errors" + jujucloud "github.com/juju/juju/cloud" "github.com/juju/juju/environs" "github.com/juju/juju/environs/config" "github.com/juju/juju/environs/simplestreams" @@ -56,11 +58,10 @@ } type environ struct { - common.SupportsUnitPlacementPolicy - - name string - uuid string - gce gceConnection + name string + uuid string + cloud environs.CloudSpec + gce gceConnection lock sync.Mutex // lock protects access to ecfg ecfg *environConfig @@ -75,20 +76,42 @@ // Function entry points defined as variables so they can be overridden // for testing purposes. var ( - newConnection = func(ecfg *environConfig) (gceConnection, error) { - return google.Connect(ecfg.conn, &ecfg.credentials) + newConnection = func(conn google.ConnectionConfig, creds *google.Credentials) (gceConnection, error) { + return google.Connect(conn, creds) } destroyEnv = common.Destroy bootstrap = common.Bootstrap ) -func newEnviron(cfg *config.Config) (*environ, error) { +func newEnviron(cloud environs.CloudSpec, cfg *config.Config) (*environ, error) { ecfg, err := newConfig(cfg, nil) if err != nil { return nil, errors.Annotate(err, "invalid config") } + + credAttrs := cloud.Credential.Attributes() + if cloud.Credential.AuthType() == jujucloud.JSONFileAuthType { + contents := credAttrs[credAttrFile] + credential, err := parseJSONAuthFile(strings.NewReader(contents)) + if err != nil { + return nil, errors.Trace(err) + } + credAttrs = credential.Attributes() + } + + credential := &google.Credentials{ + ClientID: credAttrs[credAttrClientID], + ProjectID: credAttrs[credAttrProjectID], + ClientEmail: credAttrs[credAttrClientEmail], + PrivateKey: []byte(credAttrs[credAttrPrivateKey]), + } + connectionConfig := google.ConnectionConfig{ + Region: cloud.Region, + ProjectID: credential.ProjectID, + } + // Connect and authenticate. - conn, err := newConnection(ecfg) + conn, err := newConnection(connectionConfig, credential) if err != nil { return nil, errors.Trace(err) } @@ -100,6 +123,7 @@ return &environ{ name: ecfg.config.Name(), uuid: ecfg.config.UUID(), + cloud: cloud, ecfg: ecfg, gce: conn, namespace: namespace, @@ -119,8 +143,8 @@ // Region returns the CloudSpec to use for the provider, as configured. func (env *environ) Region() (simplestreams.CloudSpec, error) { return simplestreams.CloudSpec{ - Region: env.ecfg.region(), - Endpoint: env.ecfg.imageEndpoint(), + Region: env.cloud.Region, + Endpoint: env.cloud.Endpoint, }, nil } @@ -144,6 +168,24 @@ return env.ecfg.config } +// PrepareForBootstrap implements environs.Environ. +func (env *environ) PrepareForBootstrap(ctx environs.BootstrapContext) error { + if ctx.ShouldVerifyCredentials() { + if err := env.gce.VerifyCredentials(); err != nil { + return errors.Trace(err) + } + } + return nil +} + +// Create implements environs.Environ. +func (env *environ) Create(environs.CreateParams) error { + if err := env.gce.VerifyCredentials(); err != nil { + return errors.Trace(err) + } + return nil +} + // Bootstrap creates a new instance, chosing the series and arch out of // available tools. The series and arch are returned along with a func // that must be called to finalize the bootstrap process by transferring diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/environ_policy.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/environ_policy.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/environ_policy.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/environ_policy.go 2016-08-16 08:56:25.000000000 +0000 @@ -28,9 +28,7 @@ return nil } -// SupportedArchitectures returns the image architectures which can -// be hosted by this environment. -func (env *environ) SupportedArchitectures() ([]string, error) { +func (env *environ) getSupportedArchitectures() ([]string, error) { env.archLock.Lock() defer env.archLock.Unlock() @@ -97,7 +95,7 @@ // vocab - supportedArches, err := env.SupportedArchitectures() + supportedArches, err := env.getSupportedArchitectures() if err != nil { return nil, errors.Trace(err) } @@ -114,10 +112,6 @@ return validator, nil } -// environ provides SupportsUnitPlacement (a method of the -// state.EnvironCapatability interface) by embedding -// common.SupportsUnitPlacementPolicy. - // SupportNetworks returns whether the environment has support to // specify networks for applications and machines. func (env *environ) SupportNetworks() bool { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/environ_policy_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/environ_policy_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/environ_policy_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/environ_policy_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -54,7 +54,7 @@ c.Check(s.FakeConn.Calls, gc.HasLen, 1) c.Check(s.FakeConn.Calls[0].FuncName, gc.Equals, "AvailabilityZones") - c.Check(s.FakeConn.Calls[0].Region, gc.Equals, "home") + c.Check(s.FakeConn.Calls[0].Region, gc.Equals, "us-east1") } func (s *environPolSuite) TestPrecheckInstanceValidInstanceType(c *gc.C) { @@ -125,15 +125,6 @@ c.Check(err, jc.Satisfies, errors.IsNotFound) } -func (s *environPolSuite) TestSupportedArchitectures(c *gc.C) { - s.FakeCommon.Arches = []string{arch.AMD64} - - archList, err := s.Env.SupportedArchitectures() - c.Assert(err, jc.ErrorIsNil) - - c.Check(archList, jc.SameContents, []string{arch.AMD64}) -} - func (s *environPolSuite) TestConstraintsValidator(c *gc.C) { s.FakeCommon.Arches = []string{arch.AMD64} @@ -143,8 +134,11 @@ cons := constraints.MustParse("arch=amd64") unsupported, err := validator.Validate(cons) c.Assert(err, jc.ErrorIsNil) - c.Check(unsupported, gc.HasLen, 0) + + arm64 := arch.ARM64 + _, err = validator.Validate(constraints.Value{Arch: &arm64}) + c.Assert(err, gc.ErrorMatches, "invalid constraint value: arch=arm64\nvalid values are: \\[amd64\\]") } func (s *environPolSuite) TestConstraintsValidatorEmpty(c *gc.C) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/environ_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/environ_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/environ_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/environ_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -38,7 +38,7 @@ cloudSpec, err := s.Env.Region() c.Assert(err, jc.ErrorIsNil) - c.Check(cloudSpec.Region, gc.Equals, "home") + c.Check(cloudSpec.Region, gc.Equals, "us-east1") c.Check(cloudSpec.Endpoint, gc.Equals, "https://www.googleapis.com") } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/export_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,7 +9,6 @@ "github.com/juju/juju/environs/instances" "github.com/juju/juju/instance" "github.com/juju/juju/provider/gce/google" - "github.com/juju/juju/storage" ) var ( @@ -78,8 +77,3 @@ func GetInstances(env *environ) ([]instance.Instance, error) { return env.instances() } - -// Storage -func GCEStorageProvider() storage.Provider { - return &storageProvider{} -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/google/raw.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/google/raw.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/google/raw.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/google/raw.go 2016-08-16 08:56:25.000000000 +0000 @@ -21,7 +21,7 @@ // These are attempt strategies used in waitOperation. var ( // TODO(ericsnow) Tune the timeouts and delays. - + // TODO(katco): 2016-08-09: lp:1611427 attemptsLong = utils.AttemptStrategy{ Total: 5 * time.Minute, Delay: 2 * time.Second, @@ -325,6 +325,8 @@ // waitOperation waits for the provided operation to reach the "done" // status. It follows the given attempt strategy (e.g. wait time between // attempts) and may time out. +// +// TODO(katco): 2016-08-09: lp:1611427 func (rc *rawConn) waitOperation(projectID string, op *compute.Operation, attempts utils.AttemptStrategy) error { // TODO(perrito666) 2016-05-02 lp:1558657 started := time.Now() diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/google/raw_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/google/raw_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/google/raw_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/google/raw_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -14,8 +14,9 @@ type rawConnSuite struct { BaseSuite - op *compute.Operation - rawConn *rawConn + op *compute.Operation + rawConn *rawConn + // TODO(katco): 2016-08-09: lp:1611427 strategy utils.AttemptStrategy callCount int diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/init.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/init.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/init.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/init.go 2016-08-16 08:56:25.000000000 +0000 @@ -3,10 +3,7 @@ package gce -import ( - "github.com/juju/juju/environs" - "github.com/juju/juju/storage/provider/registry" -) +import "github.com/juju/juju/environs" const ( providerType = "gce" @@ -14,10 +11,4 @@ func init() { environs.RegisterProvider(providerType, providerInstance) - - // Register the GCE specific providers. - registry.RegisterProvider(storageProviderType, &storageProvider{}) - - // Inform the storage provider registry about the GCE providers. - registry.RegisterEnvironStorageProviders(providerType, storageProviderType) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/provider.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/provider.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/provider.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/provider.go 2016-08-16 08:56:25.000000000 +0000 @@ -19,65 +19,35 @@ var providerInstance environProvider // Open implements environs.EnvironProvider. -func (environProvider) Open(cfg *config.Config) (environs.Environ, error) { - env, err := newEnviron(cfg) +func (environProvider) Open(args environs.OpenParams) (environs.Environ, error) { + if err := validateCloudSpec(args.Cloud); err != nil { + return nil, errors.Annotate(err, "validating cloud spec") + } + env, err := newEnviron(args.Cloud, args.Config) return env, errors.Trace(err) } -// BootstrapConfig implements environs.EnvironProvider. -func (p environProvider) BootstrapConfig(args environs.BootstrapConfigParams) (*config.Config, error) { - // Add credentials to the configuration. - cfg := args.Config - switch authType := args.Credentials.AuthType(); authType { - case cloud.JSONFileAuthType: - var err error - filename := args.Credentials.Attributes()["file"] - args.Credentials, err = ParseJSONAuthFile(filename) - if err != nil { - return nil, errors.Trace(err) - } - fallthrough - case cloud.OAuth2AuthType: - credentialAttrs := args.Credentials.Attributes() - var err error - cfg, err = args.Config.Apply(map[string]interface{}{ - cfgProjectID: credentialAttrs[cfgProjectID], - cfgClientID: credentialAttrs[cfgClientID], - cfgClientEmail: credentialAttrs[cfgClientEmail], - cfgPrivateKey: credentialAttrs[cfgPrivateKey], - }) - if err != nil { - return nil, errors.Trace(err) - } - default: - return nil, errors.NotSupportedf("%q auth-type", authType) - } - // Ensure cloud info is in config. - var err error - cfg, err = cfg.Apply(map[string]interface{}{ - cfgRegion: args.CloudRegion, - // TODO (anastasiamac 2016-06-09) at some stage will need to - // also add endpoint and storage endpoint. - }) - if err != nil { - return nil, errors.Trace(err) +// PrepareConfig implements environs.EnvironProvider. +func (p environProvider) PrepareConfig(args environs.PrepareConfigParams) (*config.Config, error) { + if err := validateCloudSpec(args.Cloud); err != nil { + return nil, errors.Annotate(err, "validating cloud spec") } - - return p.PrepareForCreateEnvironment(args.ControllerUUID, cfg) + return configWithDefaults(args.Config) } -// PrepareForBootstrap implements environs.EnvironProvider. -func (p environProvider) PrepareForBootstrap(ctx environs.BootstrapContext, cfg *config.Config) (environs.Environ, error) { - env, err := newEnviron(cfg) - if err != nil { - return nil, errors.Trace(err) +func validateCloudSpec(spec environs.CloudSpec) error { + if err := spec.Validate(); err != nil { + return errors.Trace(err) + } + if spec.Credential == nil { + return errors.NotValidf("missing credential") } - if ctx.ShouldVerifyCredentials() { - if err := env.gce.VerifyCredentials(); err != nil { - return nil, errors.Trace(err) - } + switch authType := spec.Credential.AuthType(); authType { + case cloud.OAuth2AuthType, cloud.JSONFileAuthType: + default: + return errors.NotSupportedf("%q auth-type", authType) } - return env, nil + return nil } // Schema returns the configuration schema for an environment. @@ -89,11 +59,6 @@ return fields } -// PrepareForCreateEnvironment is specified in the EnvironProvider interface. -func (p environProvider) PrepareForCreateEnvironment(controllerUUID string, cfg *config.Config) (*config.Config, error) { - return configWithDefaults(cfg) -} - // UpgradeModelConfig is specified in the ModelConfigUpgrader interface. func (environProvider) UpgradeConfig(cfg *config.Config) (*config.Config, error) { return configWithDefaults(cfg) @@ -113,10 +78,7 @@ // RestrictedConfigAttributes is specified in the EnvironProvider interface. func (environProvider) RestrictedConfigAttributes() []string { - return []string{ - cfgRegion, - cfgImageEndpoint, - } + return []string{} } // Validate implements environs.EnvironProvider.Validate. @@ -130,10 +92,5 @@ // SecretAttrs implements environs.EnvironProvider.SecretAttrs. func (environProvider) SecretAttrs(cfg *config.Config) (map[string]string, error) { - // The defaults should be set already, so we pass nil. - ecfg, err := newConfig(cfg, nil) - if err != nil { - return nil, errors.Trace(err) - } - return ecfg.secret(), nil + return map[string]string{}, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/provider_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/provider_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/provider_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/provider_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -16,6 +16,7 @@ gce.BaseSuite provider environs.EnvironProvider + spec environs.CloudSpec } var _ = gc.Suite(&providerSuite{}) @@ -26,6 +27,8 @@ var err error s.provider, err = environs.Provider("gce") c.Check(err, jc.ErrorIsNil) + + s.spec = gce.MakeTestCloudSpec() } func (s *providerSuite) TestRegistered(c *gc.C) { @@ -33,25 +36,44 @@ } func (s *providerSuite) TestOpen(c *gc.C) { - env, err := s.provider.Open(s.Config) + env, err := s.provider.Open(environs.OpenParams{ + Cloud: s.spec, + Config: s.Config, + }) c.Check(err, jc.ErrorIsNil) envConfig := env.Config() c.Assert(envConfig.Name(), gc.Equals, "testenv") } -func (s *providerSuite) TestBootstrapConfig(c *gc.C) { - cfg, err := s.provider.BootstrapConfig(environs.BootstrapConfigParams{ +func (s *providerSuite) TestOpenInvalidCloudSpec(c *gc.C) { + s.spec.Name = "" + s.testOpenError(c, s.spec, `validating cloud spec: cloud name "" not valid`) +} + +func (s *providerSuite) TestOpenMissingCredential(c *gc.C) { + s.spec.Credential = nil + s.testOpenError(c, s.spec, `validating cloud spec: missing credential not valid`) +} + +func (s *providerSuite) TestOpenUnsupportedCredential(c *gc.C) { + credential := cloud.NewCredential(cloud.UserPassAuthType, map[string]string{}) + s.spec.Credential = &credential + s.testOpenError(c, s.spec, `validating cloud spec: "userpass" auth-type not supported`) +} + +func (s *providerSuite) testOpenError(c *gc.C, spec environs.CloudSpec, expect string) { + _, err := s.provider.Open(environs.OpenParams{ + Cloud: spec, Config: s.Config, - Credentials: cloud.NewCredential( - cloud.OAuth2AuthType, - map[string]string{ - "project-id": "x", - "client-id": "y", - "client-email": "zz@example.com", - "private-key": "why", - }, - ), + }) + c.Assert(err, gc.ErrorMatches, expect) +} + +func (s *providerSuite) TestPrepareConfig(c *gc.C) { + cfg, err := s.provider.PrepareConfig(environs.PrepareConfigParams{ + Config: s.Config, + Cloud: gce.MakeTestCloudSpec(), }) c.Check(err, jc.ErrorIsNil) c.Check(cfg, gc.NotNil) @@ -65,15 +87,6 @@ c.Assert(s.Config.AllAttrs(), gc.DeepEquals, validAttrs) } -func (s *providerSuite) TestSecretAttrs(c *gc.C) { - obtainedAttrs, err := s.provider.SecretAttrs(s.Config) - c.Check(err, jc.ErrorIsNil) - - expectedAttrs := map[string]string{"private-key": gce.PrivateKey} - c.Assert(obtainedAttrs, gc.DeepEquals, expectedAttrs) - -} - func (s *providerSuite) TestUpgradeConfig(c *gc.C) { c.Assert(s.provider, gc.Implements, new(environs.ModelConfigUpgrader)) upgrader := s.provider.(environs.ModelConfigUpgrader) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/testing_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/testing_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/gce/testing_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/gce/testing_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -13,6 +13,7 @@ "github.com/juju/version" gc "gopkg.in/check.v1" + "github.com/juju/juju/cloud" "github.com/juju/juju/cloudconfig/instancecfg" "github.com/juju/juju/cloudconfig/providerinit" "github.com/juju/juju/constraints" @@ -35,6 +36,7 @@ ClientName = "ba9876543210-0123456789abcdefghijklmnopqrstuv" ClientID = ClientName + ".apps.googleusercontent.com" ClientEmail = ClientName + "@developer.gserviceaccount.com" + ProjectID = "my-juju" PrivateKey = `-----BEGIN PRIVATE KEY----- ... ... @@ -66,17 +68,34 @@ ConfigAttrs = testing.FakeConfig().Merge(testing.Attrs{ "type": "gce", - "private-key": PrivateKey, - "client-id": ClientID, - "client-email": ClientEmail, - "region": "home", - "project-id": "my-juju", - "image-endpoint": "https://www.googleapis.com", "uuid": "2d02eeac-9dbb-11e4-89d3-123b93f75cba", "controller-uuid": "bfef02f1-932a-425a-a102-62175dcabd1d", }) ) +func MakeTestCloudSpec() environs.CloudSpec { + cred := MakeTestCredential() + return environs.CloudSpec{ + Type: "gce", + Name: "google", + Region: "us-east1", + Endpoint: "https://www.googleapis.com", + Credential: &cred, + } +} + +func MakeTestCredential() cloud.Credential { + return cloud.NewCredential( + cloud.OAuth2AuthType, + map[string]string{ + "project-id": ProjectID, + "client-id": ClientID, + "client-email": ClientEmail, + "private-key": PrivateKey, + }, + ) +} + type BaseSuiteUnpatched struct { gitjujutesting.IsolationSuite @@ -117,7 +136,8 @@ func (s *BaseSuiteUnpatched) initEnv(c *gc.C) { s.Env = &environ{ - name: "google", + name: "google", + cloud: MakeTestCloudSpec(), } cfg := s.NewConfig(c, nil) s.setConfig(c, cfg) @@ -280,7 +300,7 @@ // Patch out all expensive external deps. s.Env.gce = s.FakeConn - s.PatchValue(&newConnection, func(*environConfig) (gceConnection, error) { + s.PatchValue(&newConnection, func(google.ConnectionConfig, *google.Credentials) (gceConnection, error) { return s.FakeConn, nil }) s.PatchValue(&supportedArchitectures, s.FakeCommon.SupportedArchitectures) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/config.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/config.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/config.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/config.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,71 +5,19 @@ import ( "fmt" - "net/url" - "os" - "strings" - "github.com/juju/errors" "github.com/juju/schema" "github.com/juju/juju/environs/config" ) -const ( - SdcAccount = "SDC_ACCOUNT" - SdcKeyId = "SDC_KEY_ID" - SdcUrl = "SDC_URL" - - sdcUser = "sdc-user" - sdcKeyId = "sdc-key-id" - sdcUrl = "sdc-url" - privateKeyPath = "private-key-path" - algorithm = "algorithm" - privateKey = "private-key" +var ( + configFields = schema.Fields{} + configDefaults = schema.Defaults{} + requiredFields = []string{} + configImmutableFields = []string{} ) -var environmentVariables = map[string]string{ - sdcUser: SdcAccount, - sdcKeyId: SdcKeyId, - sdcUrl: SdcUrl, -} - -var configFields = schema.Fields{ - sdcUser: schema.String(), - sdcKeyId: schema.String(), - sdcUrl: schema.String(), - algorithm: schema.String(), - privateKey: schema.String(), -} - -var configDefaults = schema.Defaults{ - sdcUrl: "https://us-west-1.api.joyentcloud.com", - algorithm: "rsa-sha256", - sdcUser: schema.Omit, - sdcKeyId: schema.Omit, - privateKey: schema.Omit, -} - -var requiredFields = []string{ - sdcUrl, - algorithm, - sdcUser, - sdcKeyId, - // privatekey and privatekeypath are handled separately -} - -var configSecretFields = []string{ - sdcUser, - sdcKeyId, - privateKey, -} - -var configImmutableFields = []string{ - sdcUrl, - privateKey, - algorithm, -} - func validateConfig(cfg, old *config.Config) (*environConfig, error) { // Check for valid changes for the base config values. if err := config.Validate(cfg, old); err != nil { @@ -97,23 +45,6 @@ } } - // Read env variables to fill in any missing fields. - for field, envVar := range environmentVariables { - // If field is not set, get it from env variables - if nilOrEmptyString(envConfig.attrs[field]) { - localEnvVariable := os.Getenv(envVar) - if localEnvVariable != "" { - envConfig.attrs[field] = localEnvVariable - } else { - return nil, fmt.Errorf("cannot get %s value from environment variable %s", field, envVar) - } - } - } - - if err := ensurePrivateKey(envConfig); err != nil { - return nil, err - } - // Check for missing fields. for _, field := range requiredFields { if nilOrEmptyString(envConfig.attrs[field]) { @@ -123,14 +54,6 @@ return envConfig, nil } -// Ensure private-key is set. -func ensurePrivateKey(envConfig *environConfig) error { - if !nilOrEmptyString(envConfig.attrs[privateKey]) { - return nil - } - return errors.New("no ssh private key specified in joyent configuration") -} - type environConfig struct { *config.Config attrs map[string]interface{} @@ -140,54 +63,6 @@ return ecfg.attrs } -func (ecfg *environConfig) sdcUrl() string { - return ecfg.attrs[sdcUrl].(string) -} - -func (ecfg *environConfig) sdcUser() string { - return ecfg.attrs[sdcUser].(string) -} - -func (ecfg *environConfig) sdcKeyId() string { - return ecfg.attrs[sdcKeyId].(string) -} - -func (ecfg *environConfig) privateKey() string { - if v, ok := ecfg.attrs[privateKey]; ok { - return v.(string) - } - return "" -} - -func (ecfg *environConfig) algorithm() string { - return ecfg.attrs[algorithm].(string) -} - -func (ecfg *environConfig) SdcUrl() string { - return ecfg.sdcUrl() -} - -func (ecfg *environConfig) Region() string { - sdcUrl := ecfg.sdcUrl() - // Check if running against local services - if isLocalhost(sdcUrl) { - return "some-region" - } - return sdcUrl[strings.LastIndex(sdcUrl, "/")+1 : strings.Index(sdcUrl, ".")] -} - -func isLocalhost(u string) bool { - parsedUrl, err := url.Parse(u) - if err != nil { - return false - } - if strings.HasPrefix(parsedUrl.Host, "localhost") || strings.HasPrefix(parsedUrl.Host, "127.0.0.") { - return true - } - - return false -} - func nilOrEmptyString(i interface{}) bool { return i == nil || i == "" } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/config_internal_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/config_internal_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/config_internal_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/config_internal_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,43 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package joyent - -import ( - gc "gopkg.in/check.v1" - - coretesting "github.com/juju/juju/testing" -) - -type InternalSuite struct { - coretesting.FakeJujuXDGDataHomeSuite -} - -var _ = gc.Suite(&InternalSuite{}) - -func (s *InternalSuite) TestEnsurePrivateKey(c *gc.C) { - m := map[string]interface{}{ - "private-key": "foo", - } - - e := &environConfig{attrs: copymap(m)} - - err := ensurePrivateKey(e) - c.Assert(err, gc.IsNil) - c.Assert(e.attrs, gc.DeepEquals, m) -} - -func (s *InternalSuite) TestEnsurePrivateKeyMissing(c *gc.C) { - e := &environConfig{attrs: map[string]interface{}{}} - - err := ensurePrivateKey(e) - c.Assert(err, gc.ErrorMatches, "no ssh private key specified in joyent configuration") -} - -func copymap(m map[string]interface{}) map[string]interface{} { - m1 := make(map[string]interface{}, len(m)) - for k, v := range m { - m1[k] = v - } - return m1 -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/config_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/config_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/config_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/config_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,12 +4,9 @@ package joyent_test import ( - "fmt" "os" - "github.com/juju/testing" jc "github.com/juju/testing/checkers" - "github.com/juju/utils/ssh" gc "gopkg.in/check.v1" "github.com/juju/juju/cloud" @@ -28,10 +25,25 @@ func validAttrs() coretesting.Attrs { return coretesting.FakeConfig().Merge(coretesting.Attrs{ - "type": "joyent", + "type": "joyent", + }) +} + +func fakeCloudSpec() environs.CloudSpec { + cred := fakeCredential() + return environs.CloudSpec{ + Type: "joyent", + Name: "joyent", + Region: "whatever", + Endpoint: "test://test.api.joyentcloud.com", + Credential: &cred, + } +} + +func fakeCredential() cloud.Credential { + return cloud.NewCredential(cloud.UserPassAuthType, map[string]string{ "sdc-user": "test", "sdc-key-id": "00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff", - "sdc-url": "test://test.api.joyentcloud.com", "private-key": testPrivateKey, "algorithm": "rsa-sha256", }) @@ -39,41 +51,16 @@ type ConfigSuite struct { coretesting.FakeJujuXDGDataHomeSuite - originalValues map[string]testing.Restorer - privateKeyData string } var _ = gc.Suite(&ConfigSuite{}) func (s *ConfigSuite) SetUpSuite(c *gc.C) { s.FakeJujuXDGDataHomeSuite.SetUpSuite(c) - restoreSdcAccount := testing.PatchEnvironment(jp.SdcAccount, "tester") - s.AddCleanup(func(*gc.C) { restoreSdcAccount() }) - restoreSdcKeyId := testing.PatchEnvironment(jp.SdcKeyId, "ff:ee:dd:cc:bb:aa:99:88:77:66:55:44:33:22:11:00") - s.AddCleanup(func(*gc.C) { restoreSdcKeyId() }) - s.privateKeyData = generatePrivateKey(c) jp.RegisterMachinesEndpoint() s.AddCleanup(func(*gc.C) { jp.UnregisterMachinesEndpoint() }) } -func generatePrivateKey(c *gc.C) string { - oldBits := ssh.KeyBits - defer func() { - ssh.KeyBits = oldBits - }() - ssh.KeyBits = 32 - private, _, err := ssh.GenerateKey("test-client") - c.Assert(err, jc.ErrorIsNil) - return private -} - -func (s *ConfigSuite) SetUpTest(c *gc.C) { - s.FakeJujuXDGDataHomeSuite.SetUpTest(c) - for _, envVar := range jp.EnvironmentVariables { - s.PatchEnvironment(envVar, "") - } -} - type configtest struct { info string insert coretesting.Attrs @@ -84,68 +71,6 @@ } var newConfigTests = []configtest{{ - info: "sdc-user is required", - remove: []string{"sdc-user"}, - err: ".* cannot get sdc-user value from environment variable .*", -}, { - info: "sdc-user cannot be empty", - insert: coretesting.Attrs{"sdc-user": ""}, - err: ".* cannot get sdc-user value from environment variable .*", -}, { - info: "can get sdc-user from environment variable", - insert: coretesting.Attrs{"sdc-user": ""}, - expect: coretesting.Attrs{"sdc-user": "tester"}, - envVars: map[string]string{ - "SDC_ACCOUNT": "tester", - }, -}, { - info: "can get sdc-user from environment variable, missing from config", - remove: []string{"sdc-user"}, - expect: coretesting.Attrs{"sdc-user": "tester"}, - envVars: map[string]string{ - "SDC_ACCOUNT": "tester", - }, -}, { - info: "sdc-key-id is required", - remove: []string{"sdc-key-id"}, - err: ".* cannot get sdc-key-id value from environment variable .*", -}, { - info: "sdc-key-id cannot be empty", - insert: coretesting.Attrs{"sdc-key-id": ""}, - err: ".* cannot get sdc-key-id value from environment variable .*", -}, { - info: "can get sdc-key-id from environment variable", - insert: coretesting.Attrs{"sdc-key-id": ""}, - expect: coretesting.Attrs{"sdc-key-id": "key"}, - envVars: map[string]string{ - "SDC_KEY_ID": "key", - }, -}, { - info: "can get sdc-key-id from environment variable, missing from config", - remove: []string{"sdc-key-id"}, - expect: coretesting.Attrs{"sdc-key-id": "key"}, - envVars: map[string]string{ - "SDC_KEY_ID": "key", - }, -}, { - info: "sdc-url is inserted if missing", - expect: coretesting.Attrs{"sdc-url": "test://test.api.joyentcloud.com"}, -}, { - info: "sdc-url cannot be empty", - insert: coretesting.Attrs{"sdc-url": ""}, - err: ".* cannot get sdc-url value from environment variable .*", -}, { - info: "sdc-url is untouched if present", - insert: coretesting.Attrs{"sdc-url": "test://test.api.joyentcloud.com"}, - expect: coretesting.Attrs{"sdc-url": "test://test.api.joyentcloud.com"}, -}, { - info: "algorithm is inserted if missing", - expect: coretesting.Attrs{"algorithm": "rsa-sha256"}, -}, { - info: "algorithm cannot be empty", - insert: coretesting.Attrs{"algorithm": ""}, - err: ".* algorithm: must not be empty", -}, { info: "unknown field is not touched", insert: coretesting.Attrs{"unknown-field": 12345}, expect: coretesting.Attrs{"unknown-field": 12345}, @@ -164,9 +89,11 @@ defer os.Setenv(k, "") } attrs := validAttrs().Merge(test.insert).Delete(test.remove...) - attrs["private-key"] = s.privateKeyData testConfig := newConfig(c, attrs) - environ, err := environs.New(testConfig) + environ, err := environs.New(environs.OpenParams{ + Cloud: fakeCloudSpec(), + Config: testConfig, + }) if test.err == "" { c.Check(err, jc.ErrorIsNil) if err != nil { @@ -192,18 +119,6 @@ info: "no change, no error", expect: validAttrs(), }, { - info: "can change sdc-user", - insert: coretesting.Attrs{"sdc-user": "joyent_user"}, - expect: coretesting.Attrs{"sdc-user": "joyent_user"}, -}, { - info: "can change sdc-key-id", - insert: coretesting.Attrs{"sdc-key-id": "ff:ee:dd:cc:bb:aa:99:88:77:66:55:44:33:22:11:00"}, - expect: coretesting.Attrs{"sdc-key-id": "ff:ee:dd:cc:bb:aa:99:88:77:66:55:44:33:22:11:00"}, -}, { - info: "can change sdc-url", - insert: coretesting.Attrs{"sdc-url": "test://test.api.joyentcloud.com"}, - expect: coretesting.Attrs{"sdc-url": "test://test.api.joyentcloud.com"}, -}, { info: "can insert unknown field", insert: coretesting.Attrs{"unknown": "ignoti"}, expect: coretesting.Attrs{"unknown": "ignoti"}, @@ -237,7 +152,10 @@ baseConfig := newConfig(c, validAttrs()) for i, test := range changeConfigTests { c.Logf("test %d: %s", i, test.info) - environ, err := environs.New(baseConfig) + environ, err := environs.New(environs.OpenParams{ + Cloud: fakeCloudSpec(), + Config: baseConfig, + }) c.Assert(err, jc.ErrorIsNil) attrs := validAttrs().Merge(test.insert).Delete(test.remove...) testConfig := newConfig(c, attrs) @@ -269,21 +187,14 @@ expect: validAttrs(), }} -func (s *ConfigSuite) TestBootstrapConfig(c *gc.C) { +func (s *ConfigSuite) TestPrepareConfig(c *gc.C) { for i, test := range bootstrapConfigTests { c.Logf("test %d: %s", i, test.info) attrs := validAttrs().Merge(test.insert).Delete(test.remove...) - credentialAttrs := make(map[string]string, len(attrs)) - for k, v := range attrs.Delete("type") { - credentialAttrs[k] = fmt.Sprintf("%v", v) - } testConfig := newConfig(c, attrs) - preparedConfig, err := jp.Provider.BootstrapConfig(environs.BootstrapConfigParams{ + preparedConfig, err := jp.Provider.PrepareConfig(environs.PrepareConfigParams{ Config: testConfig, - Credentials: cloud.NewCredential( - cloud.UserPassAuthType, - credentialAttrs, - ), + Cloud: fakeCloudSpec(), }) if test.err == "" { c.Check(err, jc.ErrorIsNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/credentials.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/credentials.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/credentials.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/credentials.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,6 +9,15 @@ "github.com/juju/juju/cloud" ) +const ( + credAttrSDCUser = "sdc-user" + credAttrSDCKeyID = "sdc-key-id" + credAttrPrivateKey = "private-key" + credAttrAlgorithm = "algorithm" + + algorithmDefault = "rsa-sha256" +) + type environProviderCredentials struct{} // CredentialSchemas is part of the environs.ProviderCredentials interface. @@ -16,25 +25,23 @@ return map[cloud.AuthType]cloud.CredentialSchema{ // TODO(axw) we need a more appropriate name for this authentication // type. ssh? - cloud.UserPassAuthType: { - { - sdcUser, cloud.CredentialAttr{Description: "SmartDataCenter user ID"}, - }, { - sdcKeyId, cloud.CredentialAttr{Description: "SmartDataCenter key ID"}, - }, { - privateKey, cloud.CredentialAttr{ - Description: "Private key used to sign requests", - Hidden: true, - FileAttr: privateKeyPath, - }, - }, { - algorithm, cloud.CredentialAttr{ - Description: "Algorithm used to generate the private key (default rsa-sha256)", - Optional: true, - Options: []interface{}{"rsa-sha256", "rsa-sha1", "rsa-sha224", "rsa-sha384", "rsa-sha512"}, - }, + cloud.UserPassAuthType: {{ + credAttrSDCUser, cloud.CredentialAttr{Description: "SmartDataCenter user ID"}, + }, { + credAttrSDCKeyID, cloud.CredentialAttr{Description: "SmartDataCenter key ID"}, + }, { + credAttrPrivateKey, cloud.CredentialAttr{ + Description: "Private key used to sign requests", + Hidden: true, + FileAttr: "private-key-path", + }, + }, { + credAttrAlgorithm, cloud.CredentialAttr{ + Description: "Algorithm used to generate the private key (default rsa-sha256)", + Optional: true, + Options: []interface{}{"rsa-sha256", "rsa-sha1", "rsa-sha224", "rsa-sha384", "rsa-sha512"}, }, - }, + }}, } } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/environ.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/environ.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/environ.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/environ.go 2016-08-16 08:56:25.000000000 +0000 @@ -24,10 +24,8 @@ // This file contains the core of the Joyent Environ implementation. type joyentEnviron struct { - common.SupportsUnitPlacementPolicy - - name string - + name string + cloud environs.CloudSpec compute *joyentCompute // supportedArchitectures caches the architectures @@ -40,14 +38,16 @@ } // newEnviron create a new Joyent environ instance from config. -func newEnviron(cfg *config.Config) (*joyentEnviron, error) { - env := new(joyentEnviron) +func newEnviron(cloud environs.CloudSpec, cfg *config.Config) (*joyentEnviron, error) { + env := &joyentEnviron{ + name: cfg.Name(), + cloud: cloud, + } if err := env.SetConfig(cfg); err != nil { return nil, err } - env.name = cfg.Name() var err error - env.compute, err = newCompute(env.ecfg) + env.compute, err = newCompute(cloud) if err != nil { return nil, err } @@ -83,8 +83,7 @@ return fmt.Errorf("invalid Joyent instance %q specified", *cons.InstanceType) } -// SupportedArchitectures is specified on the EnvironCapability interface. -func (env *joyentEnviron) SupportedArchitectures() ([]string, error) { +func (env *joyentEnviron) getSupportedArchitectures() ([]string, error) { env.archLock.Lock() defer env.archLock.Unlock() if env.supportedArchitectures != nil { @@ -93,8 +92,8 @@ cfg := env.Ecfg() // Create a filter to get all images from our region and for the correct stream. cloudSpec := simplestreams.CloudSpec{ - Region: cfg.Region(), - Endpoint: cfg.SdcUrl(), + Region: env.cloud.Region, + Endpoint: env.cloud.Endpoint, } imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{ CloudSpec: cloudSpec, @@ -120,6 +119,23 @@ return env.Ecfg().Config } +// Create is part of the Environ interface. +func (env *joyentEnviron) Create(environs.CreateParams) error { + if err := verifyCredentials(env); err != nil { + return errors.Trace(err) + } + return nil +} + +func (env *joyentEnviron) PrepareForBootstrap(ctx environs.BootstrapContext) error { + if ctx.ShouldVerifyCredentials() { + if err := verifyCredentials(env); err != nil { + return errors.Trace(err) + } + } + return nil +} + func (env *joyentEnviron) Bootstrap(ctx environs.BootstrapContext, args environs.BootstrapParams) (*environs.BootstrapResult, error) { return common.Bootstrap(ctx, env, args) } @@ -167,12 +183,12 @@ // MetadataLookupParams returns parameters which are used to query simplestreams metadata. func (env *joyentEnviron) MetadataLookupParams(region string) (*simplestreams.MetadataLookupParams, error) { if region == "" { - region = env.Ecfg().Region() + region = env.cloud.Region } return &simplestreams.MetadataLookupParams{ Series: config.PreferredSeries(env.Ecfg()), Region: region, - Endpoint: env.Ecfg().sdcUrl(), + Endpoint: env.cloud.Endpoint, Architectures: []string{"amd64", "armhf"}, }, nil } @@ -180,7 +196,7 @@ // Region is specified in the HasRegion interface. func (env *joyentEnviron) Region() (simplestreams.CloudSpec, error) { return simplestreams.CloudSpec{ - Region: env.Ecfg().Region(), - Endpoint: env.Ecfg().sdcUrl(), + Region: env.cloud.Region, + Endpoint: env.cloud.Endpoint, }, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/environ_instance.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/environ_instance.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/environ_instance.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/environ_instance.go 2016-08-16 08:56:25.000000000 +0000 @@ -34,25 +34,20 @@ ) type joyentCompute struct { - sync.Mutex - ecfg *environConfig cloudapi *cloudapi.Client } -func newCompute(cfg *environConfig) (*joyentCompute, error) { - creds, err := credentials(cfg) +func newCompute(cloud environs.CloudSpec) (*joyentCompute, error) { + creds, err := credentials(cloud) if err != nil { return nil, err } - client := client.NewClient(cfg.sdcUrl(), cloudapi.DefaultAPIVersion, creds, newGoLogger()) - - return &joyentCompute{ - ecfg: cfg, - cloudapi: cloudapi.New(client)}, nil + client := client.NewClient(cloud.Endpoint, cloudapi.DefaultAPIVersion, creds, newGoLogger()) + return &joyentCompute{cloudapi: cloudapi.New(client)}, nil } func (env *joyentEnviron) machineFullName(machineId string) string { - return fmt.Sprintf("juju-%s-%s", env.Config().Name(), names.NewMachineTag(machineId)) + return fmt.Sprintf("juju-%s-%s", env.name, names.NewMachineTag(machineId)) } var unsupportedConstraints = []string{ @@ -65,7 +60,7 @@ func (env *joyentEnviron) ConstraintsValidator() (constraints.Validator, error) { validator := constraints.NewValidator() validator.RegisterUnsupported(unsupportedConstraints) - supportedArches, err := env.SupportedArchitectures() + supportedArches, err := env.getSupportedArchitectures() if err != nil { return nil, err } @@ -91,7 +86,7 @@ series := args.Tools.OneSeries() arches := args.Tools.Arches() spec, err := env.FindInstanceSpec(&instances.InstanceConstraint{ - Region: env.Ecfg().Region(), + Region: env.cloud.Region, Series: series, Arches: arches, Constraints: args.Constraints, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/export_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -17,23 +17,20 @@ "github.com/juju/juju/cloud" "github.com/juju/juju/constraints" "github.com/juju/juju/environs" - "github.com/juju/juju/environs/bootstrap" "github.com/juju/juju/environs/imagemetadata" "github.com/juju/juju/environs/instances" sstesting "github.com/juju/juju/environs/simplestreams/testing" - envtesting "github.com/juju/juju/environs/testing" - "github.com/juju/juju/jujuclient/jujuclienttesting" - "github.com/juju/juju/testing" ) // Use ShortAttempt to poll for short-term events. +// +// TODO(katco): 2016-08-09: lp:1611427 var ShortAttempt = utils.AttemptStrategy{ Total: 5 * time.Second, Delay: 200 * time.Millisecond, } var Provider environs.EnvironProvider = GetProviderInstance() -var EnvironmentVariables = environmentVariables var indexData = ` { @@ -193,46 +190,18 @@ spec, err = env.FindInstanceSpec(&instances.InstanceConstraint{ Series: series, Arches: []string{arch}, - Region: env.Ecfg().Region(), + Region: env.cloud.Region, Constraints: constraints.MustParse(cons), }, imageMetadata) return } -func CredentialsAttributes(attrs testing.Attrs) map[string]string { - credentialAttrs := make(map[string]string) - for _, attr := range []string{"sdc-user", "sdc-key-id", "private-key"} { - if v, ok := attrs[attr]; ok && v != "" { - credentialAttrs[attr] = fmt.Sprintf("%v", v) - } - } - return credentialAttrs -} - -// MakeConfig creates a functional environConfig for a test. -func MakeConfig(c *gc.C, attrs testing.Attrs) *environConfig { - env, err := bootstrap.Prepare( - envtesting.BootstrapContext(c), - jujuclienttesting.NewMemStore(), - bootstrap.PrepareParams{ - ControllerConfig: testing.FakeControllerConfig(), - BaseConfig: attrs, - ControllerName: attrs["name"].(string), - CloudName: "joyent", - Credential: cloud.NewCredential( - cloud.UserPassAuthType, - CredentialsAttributes(attrs), - ), - AdminSecret: "sekrit", - }, - ) - c.Assert(err, jc.ErrorIsNil) - return env.(*joyentEnviron).Ecfg() -} - // MakeCredentials creates credentials for a test. -func MakeCredentials(c *gc.C, attrs testing.Attrs) *auth.Credentials { - creds, err := credentials(MakeConfig(c, attrs)) +func MakeCredentials(c *gc.C, endpoint string, cloudCredential cloud.Credential) *auth.Credentials { + creds, err := credentials(environs.CloudSpec{ + Endpoint: endpoint, + Credential: &cloudCredential, + }) c.Assert(err, jc.ErrorIsNil) return creds } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/init.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/init.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/init.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/init.go 2016-08-16 08:56:25.000000000 +0000 @@ -3,10 +3,7 @@ package joyent -import ( - "github.com/juju/juju/environs" - "github.com/juju/juju/storage/provider/registry" -) +import "github.com/juju/juju/environs" const ( providerType = "joyent" @@ -14,6 +11,4 @@ func init() { environs.RegisterProvider(providerType, providerInstance) - - registry.RegisterEnvironStorageProviders(providerType) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/joyent_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/joyent_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/joyent_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/joyent_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -42,43 +42,38 @@ testKeyFingerprint = "66:ca:1c:09:75:99:35:69:be:91:08:25:03:c0:17:c0" ) -type providerSuite struct { +type baseSuite struct { coretesting.FakeJujuXDGDataHomeSuite envtesting.ToolsFixture restoreTimeouts func() } -var _ = gc.Suite(&providerSuite{}) +var _ = gc.Suite(&baseSuite{}) -func (s *providerSuite) SetUpSuite(c *gc.C) { +func (s *baseSuite) SetUpSuite(c *gc.C) { s.FakeJujuXDGDataHomeSuite.SetUpSuite(c) s.restoreTimeouts = envtesting.PatchAttemptStrategies() } -func (s *providerSuite) TearDownSuite(c *gc.C) { +func (s *baseSuite) TearDownSuite(c *gc.C) { s.restoreTimeouts() s.FakeJujuXDGDataHomeSuite.TearDownSuite(c) } -func (s *providerSuite) SetUpTest(c *gc.C) { +func (s *baseSuite) SetUpTest(c *gc.C) { s.FakeJujuXDGDataHomeSuite.SetUpTest(c) s.ToolsFixture.SetUpTest(c) } -func (s *providerSuite) TearDownTest(c *gc.C) { +func (s *baseSuite) TearDownTest(c *gc.C) { s.ToolsFixture.TearDownTest(c) s.FakeJujuXDGDataHomeSuite.TearDownTest(c) } -func GetFakeConfig(sdcUrl string) coretesting.Attrs { +func GetFakeConfig() coretesting.Attrs { return coretesting.FakeConfig().Merge(coretesting.Attrs{ "name": "joyent-test-model", "type": "joyent", - "sdc-user": testUser, - "sdc-key-id": testKeyFingerprint, - "sdc-url": sdcUrl, - "private-key": testPrivateKey, - "algorithm": "rsa-sha256", "agent-version": coretesting.FakeVersionNumber.String(), }) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/local_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/local_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/local_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/local_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -57,19 +57,27 @@ } type localLiveSuite struct { - providerSuite + baseSuite jujutest.LiveTests cSrv localCloudAPIServer } func (s *localLiveSuite) SetUpSuite(c *gc.C) { - s.providerSuite.SetUpSuite(c) + s.baseSuite.SetUpSuite(c) s.LiveTests.SetUpSuite(c) s.cSrv.setupServer(c) s.AddCleanup(s.cSrv.destroyServer) - s.TestConfig = GetFakeConfig(s.cSrv.Server.URL) - s.TestConfig = s.TestConfig.Merge(coretesting.Attrs{ + s.Credential = cloud.NewCredential(cloud.UserPassAuthType, map[string]string{ + "sdc-user": testUser, + "sdc-key-id": testKeyFingerprint, + "private-key": testPrivateKey, + "algorithm": "rsa-sha256", + }) + s.CloudEndpoint = s.cSrv.Server.URL + s.CloudRegion = "some-region" + + s.TestConfig = GetFakeConfig().Merge(coretesting.Attrs{ "image-metadata-url": "test://host", }) s.LiveTests.UploadArches = []string{arch.AMD64} @@ -79,18 +87,13 @@ func (s *localLiveSuite) TearDownSuite(c *gc.C) { joyent.UnregisterExternalTestImageMetadata() s.LiveTests.TearDownSuite(c) - s.providerSuite.TearDownSuite(c) + s.baseSuite.TearDownSuite(c) } func (s *localLiveSuite) SetUpTest(c *gc.C) { - s.providerSuite.SetUpTest(c) + s.baseSuite.SetUpTest(c) s.LiveTests.SetUpTest(c) - credentialsAttrs := joyent.CredentialsAttributes(s.TestConfig) - s.Credential = cloud.NewCredential( - cloud.UserPassAuthType, - credentialsAttrs, - ) - creds := joyent.MakeCredentials(c, s.TestConfig) + creds := joyent.MakeCredentials(c, s.CloudEndpoint, s.Credential) joyent.UseExternalTestImageMetadata(c, creds) imagetesting.PatchOfficialDataSources(&s.CleanupSuite, "test://host") restoreFinishBootstrap := envtesting.DisableFinishBootstrap() @@ -100,7 +103,7 @@ func (s *localLiveSuite) TearDownTest(c *gc.C) { s.LiveTests.TearDownTest(c) - s.providerSuite.TearDownTest(c) + s.baseSuite.TearDownTest(c) } // localServerSuite contains tests that run against an Joyent service double. @@ -108,13 +111,13 @@ // to test on a live Joyent server. The service double is started and stopped for // each test. type localServerSuite struct { - providerSuite + baseSuite jujutest.Tests cSrv localCloudAPIServer } func (s *localServerSuite) SetUpSuite(c *gc.C) { - s.providerSuite.SetUpSuite(c) + s.baseSuite.SetUpSuite(c) s.PatchValue(&imagemetadata.SimplestreamsImagesPublicKey, sstesting.SignedMetadataPublicKey) s.PatchValue(&keys.JujuPublicKey, sstesting.SignedMetadataPublicKey) @@ -123,7 +126,7 @@ } func (s *localServerSuite) SetUpTest(c *gc.C) { - s.providerSuite.SetUpTest(c) + s.baseSuite.SetUpTest(c) s.PatchValue(&jujuversion.Current, coretesting.FakeVersionNumber) s.cSrv.setupServer(c) @@ -131,14 +134,19 @@ s.Tests.ToolsFixture.UploadArches = []string{arch.AMD64} s.Tests.SetUpTest(c) - s.TestConfig = GetFakeConfig(s.cSrv.Server.URL) - credentialsAttrs := joyent.CredentialsAttributes(s.TestConfig) - s.Credential = cloud.NewCredential( - cloud.UserPassAuthType, - credentialsAttrs, - ) + + s.Credential = cloud.NewCredential(cloud.UserPassAuthType, map[string]string{ + "sdc-user": testUser, + "sdc-key-id": testKeyFingerprint, + "private-key": testPrivateKey, + "algorithm": "rsa-sha256", + }) + s.CloudEndpoint = s.cSrv.Server.URL + s.CloudRegion = "some-region" + s.TestConfig = GetFakeConfig() + // Put some fake image metadata in place. - creds := joyent.MakeCredentials(c, s.TestConfig) + creds := joyent.MakeCredentials(c, s.CloudEndpoint, s.Credential) joyent.UseExternalTestImageMetadata(c, creds) imagetesting.PatchOfficialDataSources(&s.CleanupSuite, "test://host") } @@ -146,7 +154,7 @@ func (s *localServerSuite) TearDownTest(c *gc.C) { joyent.UnregisterExternalTestImageMetadata() s.Tests.TearDownTest(c) - s.providerSuite.TearDownTest(c) + s.baseSuite.TearDownTest(c) } func bootstrapContext(c *gc.C) environs.BootstrapContext { @@ -262,8 +270,11 @@ id1 := inst1.Id() c.Logf("id0: %s, id1: %s", id0, id1) defer func() { - err := env.StopInstances(inst0.Id(), inst1.Id()) - c.Assert(err, jc.ErrorIsNil) + // StopInstances deletes machines in parallel but the Joyent + // API test double isn't goroutine-safe so stop them one at a + // time. See https://pad.lv/1604514 + c.Check(env.StopInstances(inst0.Id()), jc.ErrorIsNil) + c.Check(env.StopInstances(inst1.Id()), jc.ErrorIsNil) }() for i, test := range instanceGathering { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/provider.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/provider.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/provider.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/provider.go 2016-08-16 08:56:25.000000000 +0000 @@ -46,51 +46,17 @@ var _ simplestreams.HasRegion = (*joyentEnviron)(nil) -// RestrictedConfigAttributes is specified in the EnvironProvider interface. +// RestrictedConfigAttributes is part of the EnvironProvider interface. func (joyentProvider) RestrictedConfigAttributes() []string { - return []string{sdcUrl} + return []string{} } -// PrepareForCreateEnvironment is specified in the EnvironProvider interface. -func (joyentProvider) PrepareForCreateEnvironment(controllerUUID string, cfg *config.Config) (*config.Config, error) { - return cfg, nil -} - -// BootstrapConfig is specified in the EnvironProvider interface. -func (p joyentProvider) BootstrapConfig(args environs.BootstrapConfigParams) (*config.Config, error) { - attrs := map[string]interface{}{} - // Add the credential attributes to config. - switch authType := args.Credentials.AuthType(); authType { - case cloud.UserPassAuthType: - credentialAttrs := args.Credentials.Attributes() - for k, v := range credentialAttrs { - attrs[k] = v - } - default: - return nil, errors.NotSupportedf("%q auth-type", authType) - } - if args.CloudEndpoint != "" { - attrs[sdcUrl] = args.CloudEndpoint - } - cfg, err := args.Config.Apply(attrs) - if err != nil { - return nil, errors.Trace(err) - } - return p.PrepareForCreateEnvironment(args.ControllerUUID, cfg) -} - -// PrepareForBootstrap is specified in the EnvironProvider interface. -func (p joyentProvider) PrepareForBootstrap(ctx environs.BootstrapContext, cfg *config.Config) (environs.Environ, error) { - e, err := p.Open(cfg) - if err != nil { - return nil, errors.Trace(err) - } - if ctx.ShouldVerifyCredentials() { - if err := verifyCredentials(e.(*joyentEnviron)); err != nil { - return nil, errors.Trace(err) - } +// PrepareConfig is part of the EnvironProvider interface. +func (p joyentProvider) PrepareConfig(args environs.PrepareConfigParams) (*config.Config, error) { + if err := validateCloudSpec(args.Cloud); err != nil { + return nil, errors.Annotate(err, "validating cloud spec") } - return e, nil + return args.Config, nil } const unauthorisedMessage = ` @@ -103,11 +69,11 @@ // error will be returned, and the original error will be logged at debug // level. var verifyCredentials = func(e *joyentEnviron) error { - creds, err := credentials(e.Ecfg()) + creds, err := credentials(e.cloud) if err != nil { return err } - httpClient := client.NewClient(e.Ecfg().sdcUrl(), cloudapi.DefaultAPIVersion, creds, nil) + httpClient := client.NewClient(e.cloud.Endpoint, cloudapi.DefaultAPIVersion, creds, nil) apiClient := cloudapi.New(httpClient) _, err = apiClient.CountMachines() if err != nil { @@ -120,20 +86,32 @@ return nil } -func credentials(cfg *environConfig) (*auth.Credentials, error) { - authentication, err := auth.NewAuth(cfg.sdcUser(), cfg.privateKey(), cfg.algorithm()) +func credentials(cloud environs.CloudSpec) (*auth.Credentials, error) { + credAttrs := cloud.Credential.Attributes() + sdcUser := credAttrs[credAttrSDCUser] + sdcKeyID := credAttrs[credAttrSDCKeyID] + privateKey := credAttrs[credAttrPrivateKey] + algorithm := credAttrs[credAttrAlgorithm] + if algorithm == "" { + algorithm = algorithmDefault + } + + authentication, err := auth.NewAuth(sdcUser, privateKey, algorithm) if err != nil { return nil, errors.Errorf("cannot create credentials: %v", err) } return &auth.Credentials{ UserAuthentication: authentication, - SdcKeyId: cfg.sdcKeyId(), - SdcEndpoint: auth.Endpoint{URL: cfg.sdcUrl()}, + SdcKeyId: sdcKeyID, + SdcEndpoint: auth.Endpoint{URL: cloud.Endpoint}, }, nil } -func (joyentProvider) Open(cfg *config.Config) (environs.Environ, error) { - env, err := newEnviron(cfg) +func (joyentProvider) Open(args environs.OpenParams) (environs.Environ, error) { + if err := validateCloudSpec(args.Cloud); err != nil { + return nil, errors.Annotate(err, "validating cloud spec") + } + env, err := newEnviron(args.Cloud, args.Config) if err != nil { return nil, err } @@ -149,28 +127,7 @@ } func (joyentProvider) SecretAttrs(cfg *config.Config) (map[string]string, error) { - // If you keep configSecretFields up to date, this method should Just Work. - ecfg, err := validateConfig(cfg, nil) - if err != nil { - return nil, err - } - secretAttrs := map[string]string{} - for _, field := range configSecretFields { - if value, ok := ecfg.attrs[field]; ok { - if stringValue, ok := value.(string); ok { - secretAttrs[field] = stringValue - } else { - // All your secret attributes must be strings at the moment. Sorry. - // It's an expedient and hopefully temporary measure that helps us - // plug a security hole in the API. - return nil, errors.Errorf( - "secret %q field must have a string value; got %v", - field, value, - ) - } - } - } - return secretAttrs, nil + return map[string]string{}, nil } func GetProviderInstance() environs.EnvironProvider { @@ -196,3 +153,16 @@ } return &environConfig{valid, valid.UnknownAttrs()}, nil } + +func validateCloudSpec(spec environs.CloudSpec) error { + if err := spec.Validate(); err != nil { + return errors.Trace(err) + } + if spec.Credential == nil { + return errors.NotValidf("missing credential") + } + if authType := spec.Credential.AuthType(); authType != cloud.UserPassAuthType { + return errors.NotSupportedf("%q auth-type", authType) + } + return nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/provider_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/provider_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/provider_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/provider_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,64 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package joyent_test + +import ( + "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cloud" + "github.com/juju/juju/environs" +) + +type providerSuite struct { + testing.IsolationSuite + + provider environs.EnvironProvider + spec environs.CloudSpec +} + +var _ = gc.Suite(&providerSuite{}) + +func (s *providerSuite) SetUpTest(c *gc.C) { + s.IsolationSuite.SetUpTest(c) + + provider, err := environs.Provider("joyent") + c.Assert(err, jc.ErrorIsNil) + s.provider = provider + s.spec = fakeCloudSpec() +} + +func (s *providerSuite) TestOpen(c *gc.C) { + env, err := s.provider.Open(environs.OpenParams{ + Cloud: s.spec, + Config: newConfig(c, nil), + }) + c.Assert(err, jc.ErrorIsNil) + c.Assert(env, gc.NotNil) +} + +func (s *providerSuite) TestOpenInvalidCloudSpec(c *gc.C) { + s.spec.Name = "" + s.testOpenError(c, s.spec, `validating cloud spec: cloud name "" not valid`) +} + +func (s *providerSuite) TestOpenMissingCredential(c *gc.C) { + s.spec.Credential = nil + s.testOpenError(c, s.spec, `validating cloud spec: missing credential not valid`) +} + +func (s *providerSuite) TestOpenUnsupportedCredential(c *gc.C) { + credential := cloud.NewCredential(cloud.OAuth1AuthType, map[string]string{}) + s.spec.Credential = &credential + s.testOpenError(c, s.spec, `validating cloud spec: "oauth1" auth-type not supported`) +} + +func (s *providerSuite) testOpenError(c *gc.C, spec environs.CloudSpec, expect string) { + _, err := s.provider.Open(environs.OpenParams{ + Cloud: spec, + Config: newConfig(c, nil), + }) + c.Assert(err, gc.ErrorMatches, expect) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/storage.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/storage.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/joyent/storage.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/joyent/storage.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,20 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package joyent + +import ( + "github.com/juju/errors" + + "github.com/juju/juju/storage" +) + +// StorageProviderTypes implements storage.ProviderRegistry. +func (*joyentEnviron) StorageProviderTypes() []storage.ProviderType { + return nil +} + +// StorageProvider implements storage.ProviderRegistry. +func (*joyentEnviron) StorageProvider(t storage.ProviderType) (storage.Provider, error) { + return nil, errors.NotFoundf("storage provider %q", t) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/config_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/config_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/config_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/config_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -321,7 +321,7 @@ c.Logf("test %d: %s", i, test.info) testConfig := test.newConfig(c) - environ, err := environs.New(testConfig) + environ, err := environs.New(environs.OpenParams{lxdCloudSpec(), testConfig}) // Check the result if test.err != "" { @@ -421,7 +421,7 @@ for i, test := range changeConfigTests { c.Logf("test %d: %s", i, test.info) - environ, err := environs.New(s.config) + environ, err := environs.New(environs.OpenParams{lxdCloudSpec(), s.config}) c.Assert(err, jc.ErrorIsNil) testConfig := test.newConfig(c) @@ -451,3 +451,10 @@ c.Check(fields[name], jc.DeepEquals, field) } } + +func lxdCloudSpec() environs.CloudSpec { + return environs.CloudSpec{ + Type: "lxd", + Name: "localhost", + } +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/credentials.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/credentials.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/credentials.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/credentials.go 2016-08-16 08:56:25.000000000 +0000 @@ -16,7 +16,19 @@ // TODO (anastasiamac 2016-04-14) When/If this value changes, // verify that juju/juju/cloud/clouds.go#BuiltInClouds // with lxd type are up to-date. - return map[cloud.AuthType]cloud.CredentialSchema{cloud.EmptyAuthType: {}} + // TODO(wallyworld) update BuiltInClouds to match when we actually take notice of TLSAuthType + return map[cloud.AuthType]cloud.CredentialSchema{ + cloud.EmptyAuthType: {}, + cloud.CertificateAuthType: { + { + cfgClientCert, cloud.CredentialAttr{Description: "The client cert used for connecting to a LXD host machine."}, + }, { + cfgClientKey, cloud.CredentialAttr{Description: "The client key used for connecting to a LXD host machine."}, + }, { + cfgServerPEMCert, cloud.CredentialAttr{Description: "The certificate of the LXD server on the host machine."}, + }, + }, + } } // DetectCredentials is part of the environs.ProviderCredentials interface. diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/credentials_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/credentials_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/credentials_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/credentials_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -29,7 +29,7 @@ } func (s *credentialsSuite) TestCredentialSchemas(c *gc.C) { - envtesting.AssertProviderAuthTypes(c, s.provider, "empty") + envtesting.AssertProviderAuthTypes(c, s.provider, "certificate", "empty") } func (s *credentialsSuite) TestDetectCredentials(c *gc.C) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/environ.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/environ.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/environ.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/environ.go 2016-08-16 08:56:25.000000000 +0000 @@ -26,8 +26,6 @@ } type environ struct { - common.SupportsUnitPlacementPolicy - name string uuid string raw *rawProvider @@ -138,6 +136,24 @@ return cfg } +// PrepareForBootstrap implements environs.Environ. +func (env *environ) PrepareForBootstrap(ctx environs.BootstrapContext) error { + if ctx.ShouldVerifyCredentials() { + if err := env.verifyCredentials(); err != nil { + return errors.Trace(err) + } + } + return nil +} + +// Create implements environs.Environ. +func (env *environ) Create(environs.CreateParams) error { + if err := env.verifyCredentials(); err != nil { + return errors.Trace(err) + } + return nil +} + // Bootstrap creates a new instance, chosing the series and arch out of // available tools. The series and arch are returned along with a func // that must be called to finalize the bootstrap process by transferring diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/environ_policy.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/environ_policy.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/environ_policy.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/environ_policy.go 2016-08-16 08:56:25.000000000 +0000 @@ -12,26 +12,6 @@ "github.com/juju/juju/constraints" ) -type policyProvider interface { - // SupportedArchitectures returns the list of image architectures - // supported by this environment. - SupportedArchitectures() ([]string, error) -} - -type lxdPolicyProvider struct{} - -// SupportedArchitectures returns the image architectures which can -// be hosted by this environment. -func (pp *lxdPolicyProvider) SupportedArchitectures() ([]string, error) { - // TODO(natefinch): This is only correct so long as the lxd is running on - // the local machine. If/when we support a remote lxd environment, we'll - // need to change this to match the arch of the remote machine. - - // TODO(ericsnow) Use common.SupportedArchitectures? - localArch := arch.HostArch() - return []string{localArch}, nil -} - // PrecheckInstance verifies that the provided series and constraints // are valid for use in creating an instance in this environment. func (env *environ) PrecheckInstance(series string, cons constraints.Value, placement string) error { @@ -46,18 +26,6 @@ return nil } -// SupportedArchitectures returns the image architectures which can -// be hosted by this environment. -func (env *environ) SupportedArchitectures() ([]string, error) { - // TODO(ericsnow) The supported arch depends on the targetted - // remote. Thus we may need to support the remote as a constraint. - arches, err := env.raw.SupportedArchitectures() - if err != nil { - return nil, errors.Trace(err) - } - return arches, nil -} - var unsupportedConstraints = []string{ constraints.CpuCores, constraints.CpuPower, @@ -82,12 +50,10 @@ // Register the constraints vocab. - // TODO(ericsnow) This depends on the targetted remote host. - supportedArches, err := env.SupportedArchitectures() - if err != nil { - return nil, errors.Trace(err) - } - validator.RegisterVocabulary(constraints.Arch, supportedArches) + // TODO(natefinch): This is only correct so long as the lxd is running on + // the local machine. If/when we support a remote lxd environment, we'll + // need to change this to match the arch of the remote machine. + validator.RegisterVocabulary(constraints.Arch, []string{arch.HostArch()}) // TODO(ericsnow) Get this working... //validator.RegisterVocabulary(constraints.Container, supportedContainerTypes) @@ -95,10 +61,6 @@ return validator, nil } -// environ provides SupportsUnitPlacement (a method of the -// state.EnvironCapatability interface) by embedding -// common.SupportsUnitPlacementPolicy. - // SupportNetworks returns whether the environment has support to // specify networks for applications and machines. func (env *environ) SupportNetworks() bool { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/environ_policy_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/environ_policy_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/environ_policy_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/environ_policy_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -57,7 +57,7 @@ } func (s *environPolSuite) TestPrecheckInstanceUnsupportedArch(c *gc.C) { - s.Policy.Arches = []string{arch.AMD64} + s.PatchValue(&arch.HostArch, func() string { return arch.AMD64 }) cons := constraints.MustParse("arch=i386") placement := "" @@ -74,17 +74,8 @@ c.Check(err, gc.ErrorMatches, `unknown placement directive: .*`) } -func (s *environPolSuite) TestSupportedArchitecturesOkay(c *gc.C) { - s.Policy.Arches = []string{arch.AMD64} - - archList, err := s.Env.SupportedArchitectures() - c.Assert(err, jc.ErrorIsNil) - - c.Check(archList, jc.SameContents, []string{arch.AMD64}) -} - func (s *environPolSuite) TestConstraintsValidatorOkay(c *gc.C) { - s.Policy.Arches = []string{arch.AMD64} + s.PatchValue(&arch.HostArch, func() string { return arch.AMD64 }) validator, err := s.Env.ConstraintsValidator() c.Assert(err, jc.ErrorIsNil) @@ -107,7 +98,7 @@ } func (s *environPolSuite) TestConstraintsValidatorUnsupported(c *gc.C) { - s.Policy.Arches = []string{arch.AMD64} + s.PatchValue(&arch.HostArch, func() string { return arch.AMD64 }) validator, err := s.Env.ConstraintsValidator() c.Assert(err, jc.ErrorIsNil) @@ -135,7 +126,7 @@ } func (s *environPolSuite) TestConstraintsValidatorVocabArchKnown(c *gc.C) { - s.Policy.Arches = []string{arch.AMD64} + s.PatchValue(&arch.HostArch, func() string { return arch.AMD64 }) validator, err := s.Env.ConstraintsValidator() c.Assert(err, jc.ErrorIsNil) @@ -147,7 +138,7 @@ } func (s *environPolSuite) TestConstraintsValidatorVocabArchUnknown(c *gc.C) { - s.Policy.Arches = []string{arch.AMD64} + s.PatchValue(&arch.HostArch, func() string { return arch.AMD64 }) validator, err := s.Env.ConstraintsValidator() c.Assert(err, jc.ErrorIsNil) @@ -155,7 +146,7 @@ cons := constraints.MustParse("arch=ppc64el") _, err = validator.Validate(cons) - c.Check(err, gc.ErrorMatches, "invalid constraint value: arch=ppc64el\nvalid values are:.*") + c.Check(err, gc.ErrorMatches, "invalid constraint value: arch=ppc64el\nvalid values are: \\[amd64\\]") } func (s *environPolSuite) TestConstraintsValidatorVocabContainerUnknown(c *gc.C) { @@ -170,7 +161,7 @@ } func (s *environPolSuite) TestConstraintsValidatorConflicts(c *gc.C) { - s.Policy.Arches = []string{arch.AMD64} + s.PatchValue(&arch.HostArch, func() string { return arch.AMD64 }) validator, err := s.Env.ConstraintsValidator() c.Assert(err, jc.ErrorIsNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/environ_raw.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/environ_raw.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/environ_raw.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/environ_raw.go 2016-08-16 08:56:25.000000000 +0000 @@ -18,7 +18,6 @@ lxdProfiles lxdImages common.Firewaller - policyProvider } type lxdInstances interface { @@ -48,14 +47,11 @@ return nil, errors.Trace(err) } - policy := &lxdPolicyProvider{} - raw := &rawProvider{ - lxdInstances: client, - lxdProfiles: client, - lxdImages: client, - Firewaller: firewaller, - policyProvider: policy, + lxdInstances: client, + lxdProfiles: client, + lxdImages: client, + Firewaller: firewaller, } return raw, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/environ_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/environ_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/environ_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/environ_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -161,3 +161,8 @@ {"RemoveInstances", []interface{}{prefix, []string{machine1.Name}}}, }) } + +func (s *environSuite) TestPrepareForBootstrap(c *gc.C) { + err := s.Env.PrepareForBootstrap(envtesting.BootstrapContext(c)) + c.Assert(err, jc.ErrorIsNil) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/init.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/init.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/init.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/init.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,15 +7,9 @@ import ( "github.com/juju/juju/environs" - "github.com/juju/juju/storage/provider/registry" -) - -const ( - providerType = "lxd" + "github.com/juju/juju/provider/lxd/lxdnames" ) func init() { - environs.RegisterProvider(providerType, providerInstance) - - registry.RegisterEnvironStorageProviders(providerType) + environs.RegisterProvider(lxdnames.ProviderType, providerInstance) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/lxdnames/names.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/lxdnames/names.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/lxdnames/names.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/lxdnames/names.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,15 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +// Package lxdnames provides names for the lxd provider. +package lxdnames + +// NOTE: this package exists to get around circular imports from cloud and +// provider/lxd. + +// DefaultRegion is the name of the only "region" we support in lxd currently, +// which corresponds to the local lxd daemon. +const DefaultRegion = "localhost" + +// ProviderType defines the provider/cloud type for lxd. +const ProviderType = "lxd" diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/provider.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/provider.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/provider.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/provider.go 2016-08-16 08:56:25.000000000 +0000 @@ -12,6 +12,7 @@ "github.com/juju/juju/cloud" "github.com/juju/juju/environs" "github.com/juju/juju/environs/config" + "github.com/juju/juju/provider/lxd/lxdnames" ) type environProvider struct { @@ -21,41 +22,18 @@ var providerInstance environProvider // Open implements environs.EnvironProvider. -func (environProvider) Open(cfg *config.Config) (environs.Environ, error) { +func (environProvider) Open(args environs.OpenParams) (environs.Environ, error) { // TODO(ericsnow) verify prerequisites (see provider/local/prereq.go)? // TODO(ericsnow) do something similar to correctLocalhostURLs() // (in provider/local/environprovider.go)? - env, err := newEnviron(cfg, newRawProvider) + env, err := newEnviron(args.Config, newRawProvider) return env, errors.Trace(err) } -// BootstrapConfig implements environs.EnvironProvider. -func (p environProvider) BootstrapConfig(args environs.BootstrapConfigParams) (*config.Config, error) { - return p.PrepareForCreateEnvironment(args.ControllerUUID, args.Config) -} - -// PrepareForBootstrap implements environs.EnvironProvider. -func (p environProvider) PrepareForBootstrap(ctx environs.BootstrapContext, cfg *config.Config) (environs.Environ, error) { - // TODO(ericsnow) Do some of what happens in local provider's - // PrepareForBootstrap()? Only if "remote" is local host? - - env, err := newEnviron(cfg, newRawProvider) - if err != nil { - return nil, errors.Trace(err) - } - - if ctx.ShouldVerifyCredentials() { - if err := env.verifyCredentials(); err != nil { - return nil, errors.Trace(err) - } - } - return env, nil -} - -// PrepareForCreateEnvironment is specified in the EnvironProvider interface. -func (environProvider) PrepareForCreateEnvironment(controllerUUID string, cfg *config.Config) (*config.Config, error) { - return cfg, nil +// PrepareConfig implements environs.EnvironProvider. +func (p environProvider) PrepareConfig(args environs.PrepareConfigParams) (*config.Config, error) { + return args.Config, nil } // RestrictedConfigAttributes is specified in the EnvironProvider interface. @@ -106,10 +84,7 @@ // For now we just return a hard-coded "localhost" region, // i.e. the local LXD daemon. We may later want to detect // locally-configured remotes. - // TODO (anastasiamac 2016-04-14) When/If this value changes, - // verify that juju/juju/cloud/clouds.go#BuiltInClouds - // with lxd type are up to-date. - return []cloud.Region{{Name: "localhost"}}, nil + return []cloud.Region{{Name: lxdnames.DefaultRegion}}, nil } // Schema returns the configuration schema for an environment. diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/provider_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/provider_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/provider_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/provider_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -14,8 +14,8 @@ "github.com/juju/juju/cloud" "github.com/juju/juju/environs" - envtesting "github.com/juju/juju/environs/testing" "github.com/juju/juju/provider/lxd" + "github.com/juju/juju/provider/lxd/lxdnames" "github.com/juju/juju/tools/lxdclient" ) @@ -59,7 +59,7 @@ c.Assert(s.provider, gc.Implements, new(environs.CloudRegionDetector)) regions, err := s.provider.(environs.CloudRegionDetector).DetectRegions() c.Assert(err, jc.ErrorIsNil) - c.Assert(regions, jc.DeepEquals, []cloud.Region{{Name: "localhost"}}) + c.Assert(regions, jc.DeepEquals, []cloud.Region{{Name: lxdnames.DefaultRegion}}) } func (s *providerSuite) TestRegistered(c *gc.C) { @@ -104,23 +104,20 @@ } func (s *ProviderFunctionalSuite) TestOpen(c *gc.C) { - env, err := s.provider.Open(s.Config) + env, err := s.provider.Open(environs.OpenParams{ + Cloud: lxdCloudSpec(), + Config: s.Config, + }) c.Assert(err, jc.ErrorIsNil) envConfig := env.Config() c.Check(envConfig.Name(), gc.Equals, "testenv") } -func (s *ProviderFunctionalSuite) TestBootstrapConfig(c *gc.C) { - cfg, err := s.provider.BootstrapConfig(environs.BootstrapConfigParams{ +func (s *ProviderFunctionalSuite) TestPrepareConfig(c *gc.C) { + cfg, err := s.provider.PrepareConfig(environs.PrepareConfigParams{ Config: s.Config, }) c.Assert(err, jc.ErrorIsNil) c.Check(cfg, gc.NotNil) } - -func (s *ProviderFunctionalSuite) TestPrepareForBootstrap(c *gc.C) { - env, err := s.provider.PrepareForBootstrap(envtesting.BootstrapContext(c), s.Config) - c.Assert(err, jc.ErrorIsNil) - c.Check(env, gc.NotNil) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/storage.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/storage.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/storage.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/storage.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,22 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +// +build go1.3 + +package lxd + +import ( + "github.com/juju/errors" + + "github.com/juju/juju/storage" +) + +// StorageProviderTypes implements storage.ProviderRegistry. +func (*environ) StorageProviderTypes() []storage.ProviderType { + return nil +} + +// StorageProvider implements storage.ProviderRegistry. +func (*environ) StorageProvider(t storage.ProviderType) (storage.Provider, error) { + return nil, errors.NotFoundf("storage provider %q", t) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/testing_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/testing_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/lxd/testing_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/lxd/testing_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -291,7 +291,6 @@ Client *StubClient Firewaller *stubFirewaller Common *stubCommon - Policy *stubPolicy } func (s *BaseSuite) SetUpSuite(c *gc.C) { @@ -307,14 +306,12 @@ s.Client = &StubClient{Stub: s.Stub} s.Firewaller = &stubFirewaller{stub: s.Stub} s.Common = &stubCommon{stub: s.Stub} - s.Policy = &stubPolicy{stub: s.Stub} // Patch out all expensive external deps. s.Env.raw = &rawProvider{ - lxdInstances: s.Client, - lxdImages: s.Client, - Firewaller: s.Firewaller, - policyProvider: s.Policy, + lxdInstances: s.Client, + lxdImages: s.Client, + Firewaller: s.Firewaller, } s.Env.base = s.Common } @@ -471,21 +468,6 @@ return nil } -type stubPolicy struct { - stub *gitjujutesting.Stub - - Arches []string -} - -func (s *stubPolicy) SupportedArchitectures() ([]string, error) { - s.stub.AddCall("SupportedArchitectures") - if err := s.stub.NextErr(); err != nil { - return nil, errors.Trace(err) - } - - return s.Arches, nil -} - type StubClient struct { *gitjujutesting.Stub diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/add-juju-bridge.py juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/add-juju-bridge.py --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/add-juju-bridge.py 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/add-juju-bridge.py 2016-08-16 08:56:25.000000000 +0000 @@ -75,6 +75,7 @@ self.is_active = self.method == "dhcp" or self.method == "static" self.is_bridged = [x for x in self.options if x.startswith("bridge_ports ")] self.has_auto_stanza = None + self.parent = None def __str__(self): return self.name @@ -96,7 +97,12 @@ if not self.is_active or self.is_bridged: return self._bridge_unchanged() elif self.is_alias: - return self._bridge_alias() + if self.parent and self.parent.iface and (not self.parent.iface.is_active or self.parent.iface.is_bridged): + # if we didn't change the parent interface + # then we don't change the aliases neither. + return self._bridge_unchanged() + else: + return self._bridge_alias(bridge_name) elif self.is_vlan: return self._bridge_vlan(bridge_name) elif self.is_bonded: @@ -130,11 +136,11 @@ stanzas.append(IfaceStanza(bridge_name, self.family, self.method, options)) return stanzas - def _bridge_alias(self): + def _bridge_alias(self, bridge_name): stanzas = [] if self.has_auto_stanza: - stanzas.append(AutoStanza(self.name)) - stanzas.append(IfaceStanza(self.name, self.family, self.method, list(self.options))) + stanzas.append(AutoStanza(bridge_name)) + stanzas.append(IfaceStanza(bridge_name, self.family, self.method, list(self.options))) return stanzas def _bridge_bond(self, bridge_name): @@ -201,6 +207,8 @@ continue s.iface.has_auto_stanza = s.iface.name in physical_interfaces + self._connect_aliases() + def _parse_stanza(self, stanza_line, iterable): stanza_options = [] for line in iterable: @@ -216,6 +224,24 @@ def stanzas(self): return [x for x in self._stanzas] + def _connect_aliases(self): + """Set a reference in the alias interfaces to its related interface""" + ifaces = {} + aliases = [] + for stanza in self._stanzas: + if stanza.iface is None: + continue + + if stanza.iface.is_alias: + aliases.append(stanza) + else: + ifaces[stanza.iface.name] = stanza + + for alias in aliases: + parent_name = alias.iface.name.split(':')[0] + if parent_name in ifaces: + alias.iface.parent = ifaces[parent_name] + def _physical_interfaces(self): return {x.phy.name: x.phy for x in [y for y in self._stanzas if y.is_physical_interface]} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/bridgescript.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/bridgescript.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/bridgescript.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/bridgescript.go 2016-08-16 08:56:25.000000000 +0000 @@ -87,6 +87,7 @@ self.is_active = self.method == "dhcp" or self.method == "static" self.is_bridged = [x for x in self.options if x.startswith("bridge_ports ")] self.has_auto_stanza = None + self.parent = None def __str__(self): return self.name @@ -108,7 +109,12 @@ if not self.is_active or self.is_bridged: return self._bridge_unchanged() elif self.is_alias: - return self._bridge_alias() + if self.parent and self.parent.iface and (not self.parent.iface.is_active or self.parent.iface.is_bridged): + # if we didn't change the parent interface + # then we don't change the aliases neither. + return self._bridge_unchanged() + else: + return self._bridge_alias(bridge_name) elif self.is_vlan: return self._bridge_vlan(bridge_name) elif self.is_bonded: @@ -142,11 +148,11 @@ stanzas.append(IfaceStanza(bridge_name, self.family, self.method, options)) return stanzas - def _bridge_alias(self): + def _bridge_alias(self, bridge_name): stanzas = [] if self.has_auto_stanza: - stanzas.append(AutoStanza(self.name)) - stanzas.append(IfaceStanza(self.name, self.family, self.method, list(self.options))) + stanzas.append(AutoStanza(bridge_name)) + stanzas.append(IfaceStanza(bridge_name, self.family, self.method, list(self.options))) return stanzas def _bridge_bond(self, bridge_name): @@ -213,6 +219,8 @@ continue s.iface.has_auto_stanza = s.iface.name in physical_interfaces + self._connect_aliases() + def _parse_stanza(self, stanza_line, iterable): stanza_options = [] for line in iterable: @@ -228,6 +236,24 @@ def stanzas(self): return [x for x in self._stanzas] + def _connect_aliases(self): + """Set a reference in the alias interfaces to its related interface""" + ifaces = {} + aliases = [] + for stanza in self._stanzas: + if stanza.iface is None: + continue + + if stanza.iface.is_alias: + aliases.append(stanza) + else: + ifaces[stanza.iface.name] = stanza + + for alias in aliases: + parent_name = alias.iface.name.split(':')[0] + if parent_name in ifaces: + alias.iface.parent = ifaces[parent_name] + def _physical_interfaces(self): return {x.phy.name: x.phy for x in [y for y in self._stanzas if y.is_physical_interface]} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/bridgescript_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/bridgescript_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/bridgescript_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/bridgescript_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -101,7 +101,7 @@ prefix string }{ {networkDHCPInitial, networkDHCPExpected, "test-br-"}, - {networkDHCPWithAliasInitial, networkDHCPWithAliasExpected, "test-br-"}, + {networkStaticWithAliasInitial, networkStaticWithAliasExpected, "test-br-"}, {networkDHCPWithBondInitial, networkDHCPWithBondExpected, "test-br-"}, {networkDualNICInitial, networkDualNICExpected, "test-br-"}, {networkMultipleAliasesInitial, networkMultipleAliasesExpected, "test-br-"}, @@ -300,11 +300,11 @@ gateway 4.3.2.1 bridge_ports eth0 -auto eth0:1 -iface eth0:1 inet static +auto test-br-eth0:1 +iface test-br-eth0:1 inet static address 1.2.3.5` -const networkDHCPWithAliasInitial = `auto lo +const networkStaticWithAliasInitial = `auto lo iface lo inet loopback auto eth0 @@ -322,7 +322,7 @@ dns-nameserver 192.168.1.142` -const networkDHCPWithAliasExpected = `auto lo +const networkStaticWithAliasExpected = `auto lo iface lo inet loopback auto eth0 @@ -334,12 +334,12 @@ address 10.14.0.102/24 bridge_ports eth0 -auto eth0:1 -iface eth0:1 inet static +auto test-br-eth0:1 +iface test-br-eth0:1 inet static address 10.14.0.103/24 -auto eth0:2 -iface eth0:2 inet static +auto test-br-eth0:2 +iface test-br-eth0:2 inet static address 10.14.0.100/24 dns-nameserver 192.168.1.142` @@ -372,8 +372,8 @@ address 10.17.20.201/24 bridge_ports eth0 -auto eth0:1 -iface eth0:1 inet static +auto test-br-eth0:1 +iface test-br-eth0:1 inet static address 10.17.20.202/24 mtu 1500 @@ -498,13 +498,13 @@ address 10.17.20.201/24 bridge_ports eth10 -auto eth10:1 -iface eth10:1 inet static +auto test-br-eth10:1 +iface test-br-eth10:1 inet static address 10.17.20.202/24 mtu 1500 -auto eth10:2 -iface eth10:2 inet static +auto test-br-eth10:2 +iface test-br-eth10:2 inet static address 10.17.20.203/24 mtu 1500 dns-nameservers 10.17.20.200 @@ -667,23 +667,23 @@ address 10.17.20.203/24 bridge_ports eth6 -auto eth6:1 -iface eth6:1 inet static +auto juju-br-eth6:1 +iface juju-br-eth6:1 inet static address 10.17.20.205/24 mtu 1500 -auto eth6:2 -iface eth6:2 inet static +auto juju-br-eth6:2 +iface juju-br-eth6:2 inet static address 10.17.20.204/24 mtu 1500 -auto eth6:3 -iface eth6:3 inet static +auto juju-br-eth6:3 +iface juju-br-eth6:3 inet static address 10.17.20.206/24 mtu 1500 -auto eth6:4 -iface eth6:4 inet static +auto juju-br-eth6:4 +iface juju-br-eth6:4 inet static address 10.17.20.207/24 mtu 1500 diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/config.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/config.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/config.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/config.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,32 +4,13 @@ package maas import ( - "errors" - "fmt" - "net/url" - "strings" - "github.com/juju/schema" "gopkg.in/juju/environschema.v1" "github.com/juju/juju/environs/config" ) -var configSchema = environschema.Fields{ - "maas-server": { - Description: "maas-server specifies the location of the MAAS server. It must specify the base path.", - Type: environschema.Tstring, - Example: "http://192.168.1.1/MAAS/", - }, - "maas-oauth": { - Description: "maas-oauth holds the OAuth credentials from MAAS.", - Type: environschema.Tstring, - }, - "maas-agent-name": { - Description: "maas-agent-name is an optional UUID to group the instances acquired from MAAS, to support multiple models per MAAS user.", - Type: environschema.Tstring, - }, -} +var configSchema = environschema.Fields{} var configFields = func() schema.Fields { fs, _, err := configSchema.ValidationSchema() @@ -39,32 +20,13 @@ return fs }() -var configDefaults = schema.Defaults{ - // For backward-compatibility, maas-agent-name is the empty string - // by default. However, new environments should all use a UUID. - "maas-agent-name": "", -} +var configDefaults = schema.Defaults{} type maasModelConfig struct { *config.Config attrs map[string]interface{} } -func (cfg *maasModelConfig) maasServer() string { - return cfg.attrs["maas-server"].(string) -} - -func (cfg *maasModelConfig) maasOAuth() string { - return cfg.attrs["maas-oauth"].(string) -} - -func (cfg *maasModelConfig) maasAgentName() string { - if uuid, ok := cfg.attrs["maas-agent-name"].(string); ok { - return uuid - } - return "" -} - func (prov maasEnvironProvider) newConfig(cfg *config.Config) (*maasModelConfig, error) { validCfg, err := prov.Validate(cfg, nil) if err != nil { @@ -85,61 +47,19 @@ return fields } -var errMalformedMaasOAuth = errors.New("malformed maas-oauth (3 items separated by colons)") - func (prov maasEnvironProvider) Validate(cfg, oldCfg *config.Config) (*config.Config, error) { // Validate base configuration change before validating MAAS specifics. err := config.Validate(cfg, oldCfg) if err != nil { return nil, err } - validated, err := cfg.ValidateUnknownAttrs(configFields, configDefaults) if err != nil { return nil, err } - - // Add MAAS specific defaults. - providerDefaults := make(map[string]interface{}) - - // Storage. - if _, ok := cfg.StorageDefaultBlockSource(); !ok { - providerDefaults[config.StorageDefaultBlockSourceKey] = maasStorageProviderType - } - - if len(providerDefaults) > 0 { - if cfg, err = cfg.Apply(providerDefaults); err != nil { - return nil, err - } - } - - if oldCfg != nil { - oldAttrs := oldCfg.UnknownAttrs() - validMaasAgentName := false - if oldName, ok := oldAttrs["maas-agent-name"]; !ok || oldName == nil { - // If maas-agent-name was nil (because the config was - // generated pre-1.16.2 the only correct value for it is "" - // See bug #1256179 - validMaasAgentName = (validated["maas-agent-name"] == "") - } else { - validMaasAgentName = (validated["maas-agent-name"] == oldName) - } - if !validMaasAgentName { - return nil, fmt.Errorf("cannot change maas-agent-name") - } - } - envCfg := new(maasModelConfig) - envCfg.Config = cfg - envCfg.attrs = validated - server := envCfg.maasServer() - serverURL, err := url.Parse(server) - if err != nil || serverURL.Scheme == "" || serverURL.Host == "" { - return nil, fmt.Errorf("malformed maas-server URL '%v': %s", server, err) - } - oauth := envCfg.maasOAuth() - if strings.Count(oauth, ":") != 2 { - return nil, errMalformedMaasOAuth + envCfg := &maasModelConfig{ + Config: cfg, + attrs: validated, } - return cfg.Apply(envCfg.attrs) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/config_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/config_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/config_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/config_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,11 +6,9 @@ import ( "github.com/juju/gomaasapi" jc "github.com/juju/testing/checkers" - "github.com/juju/utils" "github.com/juju/utils/set" gc "gopkg.in/check.v1" - "github.com/juju/juju/environs" "github.com/juju/juju/environs/config" "github.com/juju/juju/testing" ) @@ -40,11 +38,7 @@ if err != nil { return nil, err } - env, err := environs.New(cfg) - if err != nil { - return nil, err - } - return env.(*maasEnviron).ecfg(), nil + return providerInstance.newConfig(cfg) } func (s *configSuite) SetUpTest(c *gc.C) { @@ -59,72 +53,11 @@ s.PatchValue(&GetMAAS2Controller, mockGetController) } -func (*configSuite) TestParsesMAASSettings(c *gc.C) { - server := "http://maas.testing.invalid/maas/" - oauth := "consumer-key:resource-token:resource-secret" - future := "futurama" - - uuid, err := utils.NewUUID() - c.Assert(err, jc.ErrorIsNil) - ecfg, err := newConfig(map[string]interface{}{ - "maas-server": server, - "maas-oauth": oauth, - "maas-agent-name": uuid.String(), - "future-key": future, - }) - c.Assert(err, jc.ErrorIsNil) - c.Check(ecfg.maasServer(), gc.Equals, server) - c.Check(ecfg.maasOAuth(), gc.DeepEquals, oauth) - c.Check(ecfg.maasAgentName(), gc.Equals, uuid.String()) - c.Check(ecfg.UnknownAttrs()["future-key"], gc.DeepEquals, future) -} - -func (*configSuite) TestMaasAgentNameDefault(c *gc.C) { - ecfg, err := newConfig(map[string]interface{}{ - "maas-server": "http://maas.testing.invalid/maas/", - "maas-oauth": "consumer-key:resource-token:resource-secret", - }) - c.Assert(err, jc.ErrorIsNil) - c.Check(ecfg.maasAgentName(), gc.Equals, "") -} - -func (*configSuite) TestChecksWellFormedMaasServer(c *gc.C) { - _, err := newConfig(map[string]interface{}{ - "maas-server": "This should have been a URL.", - "maas-oauth": "consumer-key:resource-token:resource-secret", - }) - c.Assert(err, gc.NotNil) - c.Check(err, gc.ErrorMatches, ".*malformed maas-server.*") -} - -func (*configSuite) TestChecksWellFormedMaasOAuth(c *gc.C) { - _, err := newConfig(map[string]interface{}{ - "maas-server": "http://maas.testing.invalid/maas/", - "maas-oauth": "This should have been a 3-part token.", - }) - c.Assert(err, gc.NotNil) - c.Check(err, gc.ErrorMatches, ".*malformed maas-oauth.*") -} - -func (*configSuite) TestBlockStorageProviderDefault(c *gc.C) { - ecfg, err := newConfig(map[string]interface{}{ - "maas-server": "http://maas.testing.invalid/maas/", - "maas-oauth": "consumer-key:resource-token:resource-secret", - }) - c.Assert(err, jc.ErrorIsNil) - src, _ := ecfg.StorageDefaultBlockSource() - c.Assert(src, gc.Equals, "maas") -} - func (*configSuite) TestValidateUpcallsEnvironsConfigValidate(c *gc.C) { // The base Validate() function will not allow an environment to // change its name. Trigger that error so as to prove that the // environment provider's Validate() calls the base Validate(). - baseAttrs := map[string]interface{}{ - "maas-server": "http://maas.testing.invalid/maas/", - "maas-oauth": "consumer-key:resource-token:resource-secret", - } - oldCfg, err := newConfig(baseAttrs) + oldCfg, err := newConfig(nil) c.Assert(err, jc.ErrorIsNil) newName := oldCfg.Name() + "-but-different" newCfg, err := oldCfg.Apply(map[string]interface{}{"name": newName}) @@ -136,22 +69,6 @@ c.Check(err, gc.ErrorMatches, ".*cannot change name.*") } -func (*configSuite) TestValidateCannotChangeAgentName(c *gc.C) { - baseAttrs := map[string]interface{}{ - "maas-server": "http://maas.testing.invalid/maas/", - "maas-oauth": "consumer-key:resource-token:resource-secret", - "maas-agent-name": "1234-5678", - } - oldCfg, err := newConfig(baseAttrs) - c.Assert(err, jc.ErrorIsNil) - newCfg, err := oldCfg.Apply(map[string]interface{}{ - "maas-agent-name": "9876-5432", - }) - c.Assert(err, jc.ErrorIsNil) - _, err = maasEnvironProvider{}.Validate(newCfg, oldCfg.Config) - c.Assert(err, gc.ErrorMatches, "cannot change maas-agent-name") -} - func (*configSuite) TestSchema(c *gc.C) { fields := providerInstance.Schema() // Check that all the fields defined in environs/config diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/constraints.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/constraints.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/constraints.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/constraints.go 2016-08-16 08:56:25.000000000 +0000 @@ -26,7 +26,7 @@ func (environ *maasEnviron) ConstraintsValidator() (constraints.Validator, error) { validator := constraints.NewValidator() validator.RegisterUnsupported(unsupportedConstraints) - supportedArches, err := environ.SupportedArchitectures() + supportedArches, err := environ.getSupportedArchitectures() if err != nil { return nil, err } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/constraints_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/constraints_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/constraints_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/constraints_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -350,7 +350,7 @@ requestValues := suite.testMAASObject.TestServer.NodeOperationRequestValues() nodeRequestValues, found := requestValues["node0"] c.Assert(found, jc.IsTrue) - c.Assert(nodeRequestValues[0].Get("agent_name"), gc.Equals, exampleAgentName) + c.Assert(nodeRequestValues[0].Get("agent_name"), gc.Equals, env.Config().UUID()) } func (suite *environSuite) TestAcquireNodePassesPositiveAndNegativeTags(c *gc.C) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/credentials.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/credentials.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/credentials.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/credentials.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,6 +8,7 @@ "fmt" "io/ioutil" "path/filepath" + "strings" "github.com/juju/errors" "github.com/juju/utils" @@ -15,19 +16,21 @@ "github.com/juju/juju/cloud" ) +const ( + credAttrMAASOAuth = "maas-oauth" +) + type environProviderCredentials struct{} // CredentialSchemas is part of the environs.ProviderCredentials interface. func (environProviderCredentials) CredentialSchemas() map[cloud.AuthType]cloud.CredentialSchema { return map[cloud.AuthType]cloud.CredentialSchema{ - cloud.OAuth1AuthType: { - { - "maas-oauth", cloud.CredentialAttr{ - Description: "OAuth/API-key credentials for MAAS", - Hidden: true, - }, + cloud.OAuth1AuthType: {{ + credAttrMAASOAuth, cloud.CredentialAttr{ + Description: "OAuth/API-key credentials for MAAS", + Hidden: true, }, - }, + }}, } } @@ -51,7 +54,7 @@ return nil, errors.New("MAAS credentials require a value for OAuth token") } cred := cloud.NewCredential(cloud.OAuth1AuthType, map[string]string{ - "maas-oauth": fmt.Sprintf("%v", oauthKey), + credAttrMAASOAuth: fmt.Sprintf("%v", oauthKey), }) server, ok := details["Server"] if server == "" || !ok { @@ -62,5 +65,16 @@ return &cloud.CloudCredential{ AuthCredentials: map[string]cloud.Credential{ "default": cred, - }}, nil + }, + }, nil } + +func parseOAuthToken(cred cloud.Credential) (string, error) { + oauth := cred.Attributes()[credAttrMAASOAuth] + if strings.Count(oauth, ":") != 2 { + return "", errMalformedMaasOAuth + } + return oauth, nil +} + +var errMalformedMaasOAuth = errors.New("malformed maas-oauth (3 items separated by colons)") diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/environ.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/environ.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/environ.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/environ.go 2016-08-16 08:56:25.000000000 +0000 @@ -73,9 +73,9 @@ } type maasEnviron struct { - common.SupportsUnitPlacementPolicy - - name string + name string + cloud environs.CloudSpec + uuid string // archMutex gates access to supportedArchitectures archMutex sync.Mutex @@ -105,13 +105,16 @@ var _ environs.Environ = (*maasEnviron)(nil) -func NewEnviron(cfg *config.Config) (*maasEnviron, error) { - env := new(maasEnviron) +func NewEnviron(cloud environs.CloudSpec, cfg *config.Config) (*maasEnviron, error) { + env := &maasEnviron{ + name: cfg.Name(), + uuid: cfg.UUID(), + cloud: cloud, + } err := env.SetConfig(cfg) if err != nil { return nil, err } - env.name = cfg.Name() env.storageUnlocked = NewStorage(env) env.namespace, err = instance.NewNamespace(cfg.UUID()) @@ -125,7 +128,25 @@ return env.apiVersion == apiVersion2 } -// Bootstrap is specified in the Environ interface. +// PrepareForBootstrap is part of the Environ interface. +func (env *maasEnviron) PrepareForBootstrap(ctx environs.BootstrapContext) error { + if ctx.ShouldVerifyCredentials() { + if err := verifyCredentials(env); err != nil { + return err + } + } + return nil +} + +// Create is part of the Environ interface. +func (env *maasEnviron) Create(environs.CreateParams) error { + if err := verifyCredentials(env); err != nil { + return err + } + return nil +} + +// Bootstrap is part of the Environ interface. func (env *maasEnviron) Bootstrap(ctx environs.BootstrapContext, args environs.BootstrapParams) (*environs.BootstrapResult, error) { result, series, finalizer, err := common.BootstrapInstance(ctx, env, args) if err != nil { @@ -204,15 +225,24 @@ env.ecfgUnlocked = ecfg + maasServer, err := parseCloudEndpoint(env.cloud.Endpoint) + if err != nil { + return errors.Trace(err) + } + maasOAuth, err := parseOAuthToken(*env.cloud.Credential) + if err != nil { + return errors.Trace(err) + } + // We need to know the version of the server we're on. We support 1.9 // and 2.0. MAAS 1.9 uses the 1.0 api version and 2.0 uses the 2.0 api // version. apiVersion := apiVersion2 - controller, err := GetMAAS2Controller(ecfg.maasServer(), ecfg.maasOAuth()) + controller, err := GetMAAS2Controller(maasServer, maasOAuth) switch { case gomaasapi.IsUnsupportedVersionError(err): apiVersion = apiVersion1 - authClient, err := gomaasapi.NewAuthenticatedClient(ecfg.maasServer(), ecfg.maasOAuth(), apiVersion1) + authClient, err := gomaasapi.NewAuthenticatedClient(maasServer, maasOAuth, apiVersion1) if err != nil { return errors.Trace(err) } @@ -233,8 +263,7 @@ return nil } -// SupportedArchitectures is specified on the EnvironCapability interface. -func (env *maasEnviron) SupportedArchitectures() ([]string, error) { +func (env *maasEnviron) getSupportedArchitectures() ([]string, error) { env.archMutex.Lock() defer env.archMutex.Unlock() if env.supportedArchitectures != nil { @@ -676,7 +705,7 @@ return nil, errors.Trace(err) } addStorage2(&acquireParams, volumes) - acquireParams.AgentName = environ.ecfg().maasAgentName() + acquireParams.AgentName = environ.uuid if zoneName != "" { acquireParams.Zone = zoneName } @@ -715,7 +744,7 @@ return gomaasapi.MAASObject{}, errors.Trace(err) } addStorage(acquireParams, volumes) - acquireParams.Add("agent_name", environ.ecfg().maasAgentName()) + acquireParams.Add("agent_name", environ.uuid) if zoneName != "" { acquireParams.Add("zone", zoneName) } @@ -1036,6 +1065,7 @@ } func (environ *maasEnviron) waitForNodeDeployment2(id instance.Id, timeout time.Duration) error { + // TODO(katco): 2016-08-09: lp:1611427 longAttempt := utils.AttemptStrategy{ Delay: 10 * time.Second, Total: timeout, @@ -1426,11 +1456,11 @@ func (environ *maasEnviron) acquiredInstances(ids []instance.Id) ([]instance.Instance, error) { if !environ.usingMAAS2() { filter := getSystemIdValues("id", ids) - filter.Add("agent_name", environ.ecfg().maasAgentName()) + filter.Add("agent_name", environ.uuid) return environ.instances1(filter) } args := gomaasapi.MachinesArgs{ - AgentName: environ.ecfg().maasAgentName(), + AgentName: environ.uuid, SystemIDs: instanceIdsToSystemIDs(ids), } return environ.instances2(args) @@ -1873,7 +1903,7 @@ func (environ *maasEnviron) filteredSubnets2(instId instance.Id) ([]network.SubnetInfo, error) { args := gomaasapi.MachinesArgs{ - AgentName: environ.ecfg().maasAgentName(), + AgentName: environ.uuid, SystemIDs: []string{string(instId)}, } machines, err := environ.maasController.Machines(args) @@ -1937,13 +1967,6 @@ } func (environ *maasEnviron) Destroy() error { - if environ.ecfg().maasAgentName() == "" { - logger.Warningf("No MAAS agent name specified.\n\n" + - "The environment is either not running or from a very early Juju version.\n" + - "It is not safe to release all MAAS instances without an agent name.\n" + - "If the environment is still running, please manually decomission the MAAS machines.") - return errors.New("unsafe destruction") - } if err := common.Destroy(environ); err != nil { return errors.Trace(err) } @@ -2123,7 +2146,7 @@ } primaryMACAddress := primaryNICInfo.MACAddress args := gomaasapi.MachinesArgs{ - AgentName: env.ecfg().maasAgentName(), + AgentName: env.uuid, SystemIDs: []string{string(hostInstanceID)}, } machines, err := env.maasController.Machines(args) @@ -2192,3 +2215,64 @@ logger.Debugf("allocated device interfaces: %+v", finalInterfaces) return finalInterfaces, nil } + +func (env *maasEnviron) ReleaseContainerAddresses(interfaces []network.InterfaceInfo) error { + macAddresses := make([]string, len(interfaces)) + for i, info := range interfaces { + macAddresses[i] = info.MACAddress + } + if !env.usingMAAS2() { + return env.releaseContainerAddresses1(macAddresses) + } + return env.releaseContainerAddresses2(macAddresses) +} + +func (env *maasEnviron) releaseContainerAddresses1(macAddresses []string) error { + devicesAPI := env.getMAASClient().GetSubObject("devices") + values := url.Values{} + for _, address := range macAddresses { + values.Add("mac_address", address) + } + result, err := devicesAPI.CallGet("list", values) + if err != nil { + return errors.Trace(err) + } + devicesArray, err := result.GetArray() + if err != nil { + return errors.Trace(err) + } + deviceIds := make([]string, len(devicesArray)) + for i, deviceItem := range devicesArray { + deviceMap, err := deviceItem.GetMap() + if err != nil { + return errors.Trace(err) + } + id, err := deviceMap["system_id"].GetString() + if err != nil { + return errors.Trace(err) + } + deviceIds[i] = id + } + + for _, id := range deviceIds { + err := devicesAPI.GetSubObject(id).Delete() + if err != nil { + return errors.Annotatef(err, "deleting device %s", id) + } + } + return nil +} + +func (env *maasEnviron) releaseContainerAddresses2(macAddresses []string) error { + devices, err := env.maasController.Devices(gomaasapi.DevicesArgs{MACAddresses: macAddresses}) + if err != nil { + return errors.Trace(err) + } + for _, device := range devices { + err = device.Delete() + if err != nil { + return errors.Annotatef(err, "deleting device %s", device.SystemID()) + } + } + return nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/environprovider.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/environprovider.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/environprovider.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/environprovider.go 2016-08-16 08:56:25.000000000 +0000 @@ -28,9 +28,12 @@ var providerInstance maasEnvironProvider -func (maasEnvironProvider) Open(cfg *config.Config) (environs.Environ, error) { - logger.Debugf("opening model %q.", cfg.Name()) - env, err := NewEnviron(cfg) +func (maasEnvironProvider) Open(args environs.OpenParams) (environs.Environ, error) { + logger.Debugf("opening model %q.", args.Config.Name()) + if err := validateCloudSpec(args.Cloud); err != nil { + return nil, errors.Annotate(err, "validating cloud spec") + } + env, err := NewEnviron(args.Cloud, args.Config) if err != nil { return nil, err } @@ -42,64 +45,24 @@ // RestrictedConfigAttributes is specified in the EnvironProvider interface. func (p maasEnvironProvider) RestrictedConfigAttributes() []string { - return []string{"maas-server"} + return []string{} } -// PrepareForCreateEnvironment is specified in the EnvironProvider interface. -func (p maasEnvironProvider) PrepareForCreateEnvironment(controllerUUID string, cfg *config.Config) (*config.Config, error) { - attrs := cfg.UnknownAttrs() - oldName, found := attrs["maas-agent-name"] - if found && oldName != "" { - return nil, errAgentNameAlreadySet - } - attrs["maas-agent-name"] = cfg.UUID() - return cfg.Apply(attrs) -} - -// BootstrapConfig is specified in the EnvironProvider interface. -func (p maasEnvironProvider) BootstrapConfig(args environs.BootstrapConfigParams) (*config.Config, error) { - // For MAAS, the cloud endpoint may be either a full URL - // for the MAAS server, or just the IP/host. - if args.CloudEndpoint == "" { - return nil, errors.New("MAAS server not specified") - } - server := args.CloudEndpoint - if url, err := url.Parse(server); err != nil || url.Scheme == "" { - server = fmt.Sprintf("http://%s/MAAS", args.CloudEndpoint) - } - - attrs := map[string]interface{}{ - "maas-server": server, - } - // Add the credentials. - switch authType := args.Credentials.AuthType(); authType { - case cloud.OAuth1AuthType: - credentialAttrs := args.Credentials.Attributes() - for k, v := range credentialAttrs { - attrs[k] = v +// PrepareConfig is specified in the EnvironProvider interface. +func (p maasEnvironProvider) PrepareConfig(args environs.PrepareConfigParams) (*config.Config, error) { + if err := validateCloudSpec(args.Cloud); err != nil { + return nil, errors.Annotate(err, "validating cloud spec") + } + var attrs map[string]interface{} + if _, ok := args.Config.StorageDefaultBlockSource(); !ok { + attrs = map[string]interface{}{ + config.StorageDefaultBlockSourceKey: maasStorageProviderType, } - default: - return nil, errors.NotSupportedf("%q auth-type", authType) - } - cfg, err := args.Config.Apply(attrs) - if err != nil { - return nil, errors.Trace(err) - } - return p.PrepareForCreateEnvironment(args.ControllerUUID, cfg) -} - -// PrepareForBootstrap is specified in the EnvironProvider interface. -func (p maasEnvironProvider) PrepareForBootstrap(ctx environs.BootstrapContext, cfg *config.Config) (environs.Environ, error) { - env, err := p.Open(cfg) - if err != nil { - return nil, err } - if ctx.ShouldVerifyCredentials() { - if err := verifyCredentials(env.(*maasEnviron)); err != nil { - return nil, err - } + if len(attrs) == 0 { + return args.Config, nil } - return env, nil + return args.Config.Apply(attrs) } func verifyCredentials(env *maasEnviron) error { @@ -120,16 +83,45 @@ // SecretAttrs is specified in the EnvironProvider interface. func (prov maasEnvironProvider) SecretAttrs(cfg *config.Config) (map[string]string, error) { - secretAttrs := make(map[string]string) - maasCfg, err := prov.newConfig(cfg) - if err != nil { - return nil, err - } - secretAttrs["maas-oauth"] = maasCfg.maasOAuth() - return secretAttrs, nil + return map[string]string{}, nil } // DetectRegions is specified in the environs.CloudRegionDetector interface. func (p maasEnvironProvider) DetectRegions() ([]cloud.Region, error) { return nil, errors.NotFoundf("regions") } + +func validateCloudSpec(spec environs.CloudSpec) error { + if err := spec.Validate(); err != nil { + return errors.Trace(err) + } + if _, err := parseCloudEndpoint(spec.Endpoint); err != nil { + return errors.Annotate(err, "validating endpoint") + } + if spec.Credential == nil { + return errors.NotValidf("missing credential") + } + if authType := spec.Credential.AuthType(); authType != cloud.OAuth1AuthType { + return errors.NotSupportedf("%q auth-type", authType) + } + if _, err := parseOAuthToken(*spec.Credential); err != nil { + return errors.Annotate(err, "validating MAAS OAuth token") + } + return nil +} + +func parseCloudEndpoint(endpoint string) (server string, _ error) { + // For MAAS, the cloud endpoint may be either a full URL + // for the MAAS server, or just the IP/host. + if endpoint == "" { + return "", errors.New("MAAS server not specified") + } + server = endpoint + if url, err := url.Parse(server); err != nil || url.Scheme == "" { + server = fmt.Sprintf("http://%s/MAAS", endpoint) + if _, err := url.Parse(server); err != nil { + return "", errors.NotValidf("endpoint %q", endpoint) + } + } + return server, nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/environprovider_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/environprovider_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/environprovider_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/environprovider_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,9 +5,13 @@ import ( "io/ioutil" + "net/http" + "net/http/httptest" + "net/http/httputil" + "net/url" + "strings" jc "github.com/juju/testing/checkers" - "github.com/juju/utils" gc "gopkg.in/check.v1" "github.com/juju/juju/cloud" @@ -22,152 +26,118 @@ var _ = gc.Suite(&EnvironProviderSuite{}) -func (suite *EnvironProviderSuite) TestSecretAttrsReturnsSensitiveMAASAttributes(c *gc.C) { - const oauth = "aa:bb:cc" - attrs := testing.FakeConfig().Merge(testing.Attrs{ - "type": "maas", - "maas-oauth": oauth, - "maas-server": "http://maas.testing.invalid/maas/", - }) - config, err := config.New(config.NoDefaults, attrs) - c.Assert(err, jc.ErrorIsNil) - - secretAttrs, err := providerInstance.SecretAttrs(config) - c.Assert(err, jc.ErrorIsNil) +func (s *EnvironProviderSuite) cloudSpec() environs.CloudSpec { + credential := oauthCredential("aa:bb:cc") + return environs.CloudSpec{ + Type: "maas", + Name: "maas", + Endpoint: "http://maas.testing.invalid/maas/", + Credential: &credential, + } +} - expectedAttrs := map[string]string{"maas-oauth": oauth} - c.Check(secretAttrs, gc.DeepEquals, expectedAttrs) +func oauthCredential(token string) cloud.Credential { + return cloud.NewCredential( + cloud.OAuth1AuthType, + map[string]string{ + "maas-oauth": token, + }, + ) } -func (suite *EnvironProviderSuite) TestCredentialsSetup(c *gc.C) { - attrs := testing.FakeConfig().Merge(testing.Attrs{ - "type": "maas", - }) +func (suite *EnvironProviderSuite) TestPrepareConfig(c *gc.C) { + attrs := testing.FakeConfig().Merge(testing.Attrs{"type": "maas"}) config, err := config.New(config.NoDefaults, attrs) c.Assert(err, jc.ErrorIsNil) - - cfg, err := providerInstance.BootstrapConfig(environs.BootstrapConfigParams{ - Config: config, - CloudEndpoint: "http://maas.testing.invalid/maas/", - Credentials: cloud.NewCredential( - cloud.OAuth1AuthType, - map[string]string{ - "maas-oauth": "aa:bb:cc", - }, - ), + _, err = providerInstance.PrepareConfig(environs.PrepareConfigParams{ + Config: config, + Cloud: suite.cloudSpec(), }) c.Assert(err, jc.ErrorIsNil) - - attrs = cfg.UnknownAttrs() - server, ok := attrs["maas-server"] - c.Assert(ok, jc.IsTrue) - c.Assert(server, gc.Equals, "http://maas.testing.invalid/maas/") - oauth, ok := attrs["maas-oauth"] - c.Assert(ok, jc.IsTrue) - c.Assert(oauth, gc.Equals, "aa:bb:cc") } -func (suite *EnvironProviderSuite) TestUnknownAttrsContainAgentName(c *gc.C) { - attrs := testing.FakeConfig().Merge(testing.Attrs{ - "type": "maas", - }) +func (suite *EnvironProviderSuite) TestPrepareConfigInvalidOAuth(c *gc.C) { + attrs := testing.FakeConfig().Merge(testing.Attrs{"type": "maas"}) config, err := config.New(config.NoDefaults, attrs) c.Assert(err, jc.ErrorIsNil) - - cfg, err := providerInstance.BootstrapConfig(environs.BootstrapConfigParams{ - Config: config, - CloudEndpoint: "http://maas.testing.invalid/maas/", - Credentials: cloud.NewCredential( - cloud.OAuth1AuthType, - map[string]string{ - "maas-oauth": "aa:bb:cc", - }, - ), + spec := suite.cloudSpec() + cred := oauthCredential("wrongly-formatted-oauth-string") + spec.Credential = &cred + _, err = providerInstance.PrepareConfig(environs.PrepareConfigParams{ + Config: config, + Cloud: spec, }) - c.Assert(err, jc.ErrorIsNil) - - unknownAttrs := cfg.UnknownAttrs() - c.Assert(unknownAttrs["maas-server"], gc.Equals, "http://maas.testing.invalid/maas/") - - uuid, ok := unknownAttrs["maas-agent-name"] - - c.Assert(ok, jc.IsTrue) - c.Assert(uuid, jc.Satisfies, utils.IsValidUUIDString) + c.Assert(err, gc.ErrorMatches, ".*malformed maas-oauth.*") } -func (suite *EnvironProviderSuite) TestMAASServerFromEndpoint(c *gc.C) { - attrs := testing.FakeConfig().Merge(testing.Attrs{ - "type": "maas", - }) +func (suite *EnvironProviderSuite) TestPrepareConfigInvalidEndpoint(c *gc.C) { + attrs := testing.FakeConfig().Merge(testing.Attrs{"type": "maas"}) config, err := config.New(config.NoDefaults, attrs) c.Assert(err, jc.ErrorIsNil) - - cfg, err := providerInstance.BootstrapConfig(environs.BootstrapConfigParams{ - Config: config, - CloudEndpoint: "maas.testing", - Credentials: cloud.NewCredential( - cloud.OAuth1AuthType, - map[string]string{ - "maas-oauth": "aa:bb:cc", - }, - ), + spec := suite.cloudSpec() + spec.Endpoint = "This should have been a URL or host." + _, err = providerInstance.PrepareConfig(environs.PrepareConfigParams{ + Config: config, + Cloud: spec, }) - c.Assert(err, jc.ErrorIsNil) - - unknownAttrs := cfg.UnknownAttrs() - c.Assert(unknownAttrs["maas-server"], gc.Equals, "http://maas.testing/MAAS") + c.Assert(err, gc.ErrorMatches, + `validating cloud spec: validating endpoint: endpoint "This should have been a URL or host." not valid`, + ) } -func (suite *EnvironProviderSuite) TestPrepareSetsAgentName(c *gc.C) { - attrs := testing.FakeConfig().Merge(testing.Attrs{ - "type": "maas", - "maas-oauth": "aa:bb:cc", - "maas-server": "http://maas.testing.invalid/maas/", - }) +func (suite *EnvironProviderSuite) TestPrepareConfigSetsDefaults(c *gc.C) { + attrs := testing.FakeConfig().Merge(testing.Attrs{"type": "maas"}) config, err := config.New(config.NoDefaults, attrs) c.Assert(err, jc.ErrorIsNil) - - config, err = providerInstance.PrepareForCreateEnvironment(suite.controllerUUID, config) + cfg, err := providerInstance.PrepareConfig(environs.PrepareConfigParams{ + Config: config, + Cloud: suite.cloudSpec(), + }) c.Assert(err, jc.ErrorIsNil) + src, _ := cfg.StorageDefaultBlockSource() + c.Assert(src, gc.Equals, "maas") +} - uuid, ok := config.UnknownAttrs()["maas-agent-name"] - c.Assert(ok, jc.IsTrue) - c.Assert(uuid, jc.Satisfies, utils.IsValidUUIDString) +func (suite *EnvironProviderSuite) TestMAASServerFromEndpointURL(c *gc.C) { + suite.testMAASServerFromEndpoint(c, suite.testMAASObject.TestServer.URL) } -func (suite *EnvironProviderSuite) TestPrepareExistingAgentName(c *gc.C) { - attrs := testing.FakeConfig().Merge(testing.Attrs{ - "type": "maas", - "maas-oauth": "aa:bb:cc", - "maas-server": "http://maas.testing.invalid/maas/", - "maas-agent-name": "foobar", - }) - config, err := config.New(config.NoDefaults, attrs) +func (suite *EnvironProviderSuite) TestMAASServerFromEndpointHost(c *gc.C) { + targetURL, err := url.Parse(suite.testMAASObject.TestServer.URL) c.Assert(err, jc.ErrorIsNil) - _, err = providerInstance.PrepareForCreateEnvironment(suite.controllerUUID, config) - c.Assert(err, gc.Equals, errAgentNameAlreadySet) + rp := httputil.NewSingleHostReverseProxy(targetURL) + rp.Director = func(req *http.Request) { + req.URL.Path = strings.TrimPrefix(req.URL.Path, "/MAAS") + req.URL.Scheme = targetURL.Scheme + req.URL.Host = targetURL.Host + } + proxy := httptest.NewServer(rp) + defer proxy.Close() + + // The proxy's host:port will be formatted into a URL, with a + // fixed root path of "/MAAS". + proxyURL, err := url.Parse(proxy.URL) + c.Assert(err, jc.ErrorIsNil) + suite.testMAASServerFromEndpoint(c, proxyURL.Host) } -func (suite *EnvironProviderSuite) TestAgentNameShouldNotBeSetByHand(c *gc.C) { - attrs := testing.FakeConfig().Merge(testing.Attrs{ - "type": "maas", - "maas-agent-name": "foobar", - }) +func (suite *EnvironProviderSuite) testMAASServerFromEndpoint(c *gc.C, endpoint string) { + attrs := testing.FakeConfig().Merge(testing.Attrs{"type": "maas"}) config, err := config.New(config.NoDefaults, attrs) c.Assert(err, jc.ErrorIsNil) - _, err = providerInstance.BootstrapConfig(environs.BootstrapConfigParams{ - Config: config, - CloudEndpoint: "http://maas.testing.invalid/maas/", - Credentials: cloud.NewCredential( - cloud.OAuth1AuthType, - map[string]string{ - "maas-oauth": "aa:bb:cc", - }, - ), + cloudSpec := suite.cloudSpec() + cloudSpec.Endpoint = endpoint + env, err := providerInstance.Open(environs.OpenParams{ + Config: config, + Cloud: cloudSpec, }) - c.Assert(err, gc.Equals, errAgentNameAlreadySet) + c.Assert(err, jc.ErrorIsNil) + + suite.addNode(`{"system_id":"test-allocated"}`) + _, err = env.AllInstances() + c.Assert(err, jc.ErrorIsNil) } // create a temporary file with the given content. The file will be cleaned @@ -182,15 +152,16 @@ } func (suite *EnvironProviderSuite) TestOpenReturnsNilInterfaceUponFailure(c *gc.C) { - const oauth = "wrongly-formatted-oauth-string" - attrs := testing.FakeConfig().Merge(testing.Attrs{ - "type": "maas", - "maas-oauth": oauth, - "maas-server": "http://maas.testing.invalid/maas/", - }) + attrs := testing.FakeConfig().Merge(testing.Attrs{"type": "maas"}) config, err := config.New(config.NoDefaults, attrs) c.Assert(err, jc.ErrorIsNil) - env, err := providerInstance.Open(config) + spec := suite.cloudSpec() + cred := oauthCredential("wrongly-formatted-oauth-string") + spec.Credential = &cred + env, err := providerInstance.Open(environs.OpenParams{ + Cloud: spec, + Config: config, + }) // When Open() fails (i.e. returns a non-nil error), it returns an // environs.Environ interface object with a nil value and a nil // type. diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/environ_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/environ_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/environ_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/environ_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,6 +11,8 @@ "github.com/juju/utils/set" gc "gopkg.in/check.v1" + "github.com/juju/juju/cloud" + "github.com/juju/juju/environs" "github.com/juju/juju/environs/config" envtesting "github.com/juju/juju/environs/testing" "github.com/juju/juju/provider/maas" @@ -69,8 +71,6 @@ func getSimpleTestConfig(c *gc.C, extraAttrs coretesting.Attrs) *config.Config { attrs := coretesting.FakeConfig() attrs["type"] = "maas" - attrs["maas-server"] = "http://maas.testing.invalid" - attrs["maas-oauth"] = "a:b:c" attrs["bootstrap-timeout"] = "1200" for k, v := range extraAttrs { attrs[k] = v @@ -80,12 +80,24 @@ return cfg } +func getSimpleCloudSpec() environs.CloudSpec { + cred := cloud.NewCredential(cloud.OAuth1AuthType, map[string]string{ + "maas-oauth": "a:b:c", + }) + return environs.CloudSpec{ + Type: "maas", + Name: "maas", + Endpoint: "http://maas.testing.invalid", + Credential: &cred, + } +} + func (*environSuite) TestSetConfigValidatesFirst(c *gc.C) { // SetConfig() validates the config change and disallows, for example, // changes in the environment name. oldCfg := getSimpleTestConfig(c, coretesting.Attrs{"name": "old-name"}) newCfg := getSimpleTestConfig(c, coretesting.Attrs{"name": "new-name"}) - env, err := maas.NewEnviron(oldCfg) + env, err := maas.NewEnviron(getSimpleCloudSpec(), oldCfg) c.Assert(err, jc.ErrorIsNil) // SetConfig() fails, even though both the old and the new config are @@ -98,119 +110,29 @@ c.Check(env.Config().Name(), gc.Equals, "old-name") } -func (*environSuite) TestSetConfigRefusesChangingAgentName(c *gc.C) { - oldCfg := getSimpleTestConfig(c, coretesting.Attrs{"maas-agent-name": "agent-one"}) - newCfgTwo := getSimpleTestConfig(c, coretesting.Attrs{"maas-agent-name": "agent-two"}) - env, err := maas.NewEnviron(oldCfg) - c.Assert(err, jc.ErrorIsNil) - - // SetConfig() fails, even though both the old and the new config are - // individually valid. - err = env.SetConfig(newCfgTwo) - c.Assert(err, gc.NotNil) - c.Check(err, gc.ErrorMatches, ".*cannot change maas-agent-name.*") - - // The old config is still in place. The new config never took effect. - c.Check(maas.MAASAgentName(env), gc.Equals, "agent-one") - - // It also refuses to set it to the empty string: - err = env.SetConfig(getSimpleTestConfig(c, coretesting.Attrs{"maas-agent-name": ""})) - c.Check(err, gc.ErrorMatches, ".*cannot change maas-agent-name.*") - - // And to nil - err = env.SetConfig(getSimpleTestConfig(c, nil)) - c.Check(err, gc.ErrorMatches, ".*cannot change maas-agent-name.*") -} - -func (*environSuite) TestSetConfigAllowsEmptyFromNilAgentName(c *gc.C) { - // bug #1256179 is that when using an older version of Juju (<1.16.2) - // we didn't include maas-agent-name in the database, so it was 'nil' - // in the OldConfig. However, when setting an environment, we would set - // it to "" (because maasModelConfig.Validate ensures it is a 'valid' - // string). We can't create that from NewEnviron or newConfig because - // both of them Validate the contents. 'cmd/juju/model - // SetEnvironmentCommand' instead uses conn.State.ModelConfig() which - // just reads the content of the database into a map, so we just create - // the map ourselves. - - // Even though we use 'nil' here, it actually stores it as "" because - // 1.16.2 already validates the value - baseCfg := getSimpleTestConfig(c, coretesting.Attrs{"maas-agent-name": ""}) - c.Check(baseCfg.UnknownAttrs()["maas-agent-name"], gc.Equals, "") - env, err := maas.NewEnviron(baseCfg) - c.Assert(err, jc.ErrorIsNil) - provider := env.Provider() - - attrs := coretesting.FakeConfig() - // These are attrs we need to make it a valid Config, but would usually - // be set by other infrastructure - attrs["type"] = "maas" - attrs["bootstrap-timeout"] = "1200" - nilCfg, err := config.New(config.NoDefaults, attrs) - c.Assert(err, jc.ErrorIsNil) - validatedConfig, err := provider.Validate(baseCfg, nilCfg) - c.Assert(err, jc.ErrorIsNil) - c.Check(validatedConfig.UnknownAttrs()["maas-agent-name"], gc.Equals, "") - // However, you can't set it to an actual value if you haven't been using a value - valueCfg := getSimpleTestConfig(c, coretesting.Attrs{"maas-agent-name": "agent-name"}) - _, err = provider.Validate(valueCfg, nilCfg) - c.Check(err, gc.ErrorMatches, ".*cannot change maas-agent-name.*") -} - -func (*environSuite) TestDestroyWithEmptyAgentName(c *gc.C) { - // Related bug #1256179, comment as above. - baseCfg := getSimpleTestConfig(c, coretesting.Attrs{"maas-agent-name": ""}) - env, err := maas.NewEnviron(baseCfg) - c.Assert(err, jc.ErrorIsNil) - - err = env.Destroy() - c.Assert(err, gc.ErrorMatches, "unsafe destruction") -} - -func (*environSuite) TestSetConfigAllowsChangingNilAgentNameToEmptyString(c *gc.C) { - oldCfg := getSimpleTestConfig(c, nil) - newCfgTwo := getSimpleTestConfig(c, coretesting.Attrs{"maas-agent-name": ""}) - env, err := maas.NewEnviron(oldCfg) - c.Assert(err, jc.ErrorIsNil) - - err = env.SetConfig(newCfgTwo) - c.Assert(err, jc.ErrorIsNil) - c.Check(maas.MAASAgentName(env), gc.Equals, "") -} - func (*environSuite) TestSetConfigUpdatesConfig(c *gc.C) { origAttrs := coretesting.Attrs{ - "server-name": "http://maas2.testing.invalid", - "maas-oauth": "a:b:c", - "admin-secret": "secret", + "apt-mirror": "http://testing1.invalid", } cfg := getSimpleTestConfig(c, origAttrs) - env, err := maas.NewEnviron(cfg) + env, err := maas.NewEnviron(getSimpleCloudSpec(), cfg) c.Check(err, jc.ErrorIsNil) c.Check(env.Config().Name(), gc.Equals, "testenv") - anotherServer := "http://maas.testing.invalid" - anotherOauth := "c:d:e" - anotherSecret := "secret2" newAttrs := coretesting.Attrs{ - "server-name": anotherServer, - "maas-oauth": anotherOauth, - "admin-secret": anotherSecret, + "apt-mirror": "http://testing2.invalid", } cfg2 := getSimpleTestConfig(c, newAttrs) errSetConfig := env.SetConfig(cfg2) c.Check(errSetConfig, gc.IsNil) c.Check(env.Config().Name(), gc.Equals, "testenv") - authClient, _ := gomaasapi.NewAuthenticatedClient(anotherServer, anotherOauth, "1.0") - maasClient := gomaasapi.NewMAAS(*authClient) - MAASServer := maas.GetMAASClient(env) - c.Check(MAASServer, gc.DeepEquals, maasClient) + c.Check(env.Config().AptMirror(), gc.Equals, "http://testing2.invalid") } func (*environSuite) TestNewEnvironSetsConfig(c *gc.C) { cfg := getSimpleTestConfig(c, nil) - env, err := maas.NewEnviron(cfg) + env, err := maas.NewEnviron(getSimpleCloudSpec(), cfg) c.Check(err, jc.ErrorIsNil) c.Check(env.Config().Name(), gc.Equals, "testenv") @@ -223,7 +145,7 @@ func (*environSuite) TestNewCloudinitConfig(c *gc.C) { cfg := getSimpleTestConfig(c, nil) - env, err := maas.NewEnviron(cfg) + env, err := maas.NewEnviron(getSimpleCloudSpec(), cfg) c.Assert(err, jc.ErrorIsNil) modifyNetworkScript := maas.RenderEtcNetworkInterfacesScript() script := expectedCloudinitConfig @@ -239,7 +161,7 @@ "disable-network-management": true, } cfg := getSimpleTestConfig(c, attrs) - env, err := maas.NewEnviron(cfg) + env, err := maas.NewEnviron(getSimpleCloudSpec(), cfg) c.Assert(err, jc.ErrorIsNil) cloudcfg, err := maas.NewCloudinitConfig(env, "testing.invalid", "quantal") c.Assert(err, jc.ErrorIsNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/environ_whitebox_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/environ_whitebox_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/environ_whitebox_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/environ_whitebox_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -483,30 +483,6 @@ c.Assert(sources, gc.HasLen, 0) } -func (suite *environSuite) TestSupportedArchitectures(c *gc.C) { - suite.testMAASObject.TestServer.AddBootImage("uuid-0", `{"architecture": "amd64", "release": "precise"}`) - suite.testMAASObject.TestServer.AddBootImage("uuid-0", `{"architecture": "amd64", "release": "trusty"}`) - suite.testMAASObject.TestServer.AddBootImage("uuid-1", `{"architecture": "amd64", "release": "precise"}`) - suite.testMAASObject.TestServer.AddBootImage("uuid-1", `{"architecture": "ppc64el", "release": "trusty"}`) - env := suite.makeEnviron() - a, err := env.SupportedArchitectures() - c.Assert(err, jc.ErrorIsNil) - c.Assert(a, jc.SameContents, []string{"amd64", "ppc64el"}) -} - -func (suite *environSuite) TestSupportedArchitecturesFallback(c *gc.C) { - // If we cannot query boot-images (e.g. MAAS server version 1.4), - // then Juju will fall over to listing all the available nodes. - suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0", "architecture": "amd64/generic"}`) - suite.testMAASObject.TestServer.NewNode(`{"system_id": "node1", "architecture": "armhf"}`) - suite.addSubnet(c, 9, 9, "node0") - suite.addSubnet(c, 9, 9, "node1") - env := suite.makeEnviron() - a, err := env.SupportedArchitectures() - c.Assert(err, jc.ErrorIsNil) - c.Assert(a, jc.SameContents, []string{"amd64", "armhf"}) -} - func (suite *environSuite) TestConstraintsValidator(c *gc.C) { suite.testMAASObject.TestServer.AddBootImage("uuid-0", `{"architecture": "amd64", "release": "trusty"}`) env := suite.makeEnviron() @@ -1086,16 +1062,16 @@ }) c.Assert(s.testMAASObject.TestServer.NodesOperationRequestValues(), gc.DeepEquals, []url.Values{{ "name": []string{"bootstrap-host"}, - "agent_name": []string{exampleAgentName}, + "agent_name": []string{env.Config().UUID()}, }, { "zone": []string{"zone1"}, - "agent_name": []string{exampleAgentName}, + "agent_name": []string{env.Config().UUID()}, }, { "zone": []string{"zonelord"}, - "agent_name": []string{exampleAgentName}, + "agent_name": []string{env.Config().UUID()}, }, { "zone": []string{"zone2"}, - "agent_name": []string{exampleAgentName}, + "agent_name": []string{env.Config().UUID()}, }}) } @@ -1122,3 +1098,32 @@ "acquire", "acquire", }) } + +func (s *environSuite) TestReleaseContainerAddresses(c *gc.C) { + s.testMAASObject.TestServer.AddDevice(&gomaasapi.TestDevice{ + SystemId: "device1", + MACAddress: "mac1", + }) + s.testMAASObject.TestServer.AddDevice(&gomaasapi.TestDevice{ + SystemId: "device2", + MACAddress: "mac2", + }) + s.testMAASObject.TestServer.AddDevice(&gomaasapi.TestDevice{ + SystemId: "device3", + MACAddress: "mac3", + }) + + env := s.makeEnviron() + err := env.ReleaseContainerAddresses([]network.InterfaceInfo{ + {MACAddress: "mac1"}, + {MACAddress: "mac3"}, + {MACAddress: "mac4"}, + }) + c.Assert(err, jc.ErrorIsNil) + + var systemIds []string + for systemId, _ := range s.testMAASObject.TestServer.Devices() { + systemIds = append(systemIds, systemId) + } + c.Assert(systemIds, gc.DeepEquals, []string{"device2"}) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/export_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -14,10 +14,6 @@ ShortAttempt = &shortAttempt ) -func MAASAgentName(env environs.Environ) string { - return env.(*maasEnviron).ecfg().maasAgentName() -} - func GetMAASClient(env environs.Environ) *gomaasapi.MAASObject { return env.(*maasEnviron).getMAASClient() } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/init.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/init.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/init.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/init.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,7 +5,6 @@ import ( "github.com/juju/juju/environs" - "github.com/juju/juju/storage/provider/registry" ) const ( @@ -14,9 +13,4 @@ func init() { environs.RegisterProvider(providerType, maasEnvironProvider{}) - - //Register the MAAS specific storage providers. - registry.RegisterProvider(maasStorageProviderType, &maasStorageProvider{}) - - registry.RegisterEnvironStorageProviders(providerType, maasStorageProviderType) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/init_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/init_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/init_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/init_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,34 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package maas_test - -import ( - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/storage" - "github.com/juju/juju/storage/provider/registry" - "github.com/juju/juju/testing" -) - -type maasProviderSuite struct { - testing.BaseSuite -} - -var _ = gc.Suite(&maasProviderSuite{}) - -func (*maasProviderSuite) TestMAASProviderRegistered(c *gc.C) { - p, err := registry.StorageProvider(storage.ProviderType("maas")) - c.Assert(err, jc.ErrorIsNil) - _, ok := p.(storage.Provider) - c.Assert(ok, jc.IsTrue) -} - -func (*maasProviderSuite) TestSupportedProviders(c *gc.C) { - supported := []storage.ProviderType{storage.ProviderType("maas")} - for _, providerType := range supported { - ok := registry.IsProviderSupported("maas", providerType) - c.Assert(ok, jc.IsTrue) - } -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/maas2_environ_whitebox_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/maas2_environ_whitebox_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/maas2_environ_whitebox_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/maas2_environ_whitebox_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -18,6 +18,7 @@ "gopkg.in/juju/names.v2" goyaml "gopkg.in/yaml.v2" + "github.com/juju/juju/cloud" "github.com/juju/juju/cloudconfig/cloudinit" "github.com/juju/juju/constraints" "github.com/juju/juju/environs" @@ -48,15 +49,19 @@ testServer.AddGetResponse("/api/1.0/version/", http.StatusOK, "") testServer.Start() suite.AddCleanup(func(*gc.C) { testServer.Close() }) - testAttrs := coretesting.Attrs{} - for k, v := range maasEnvAttrs { - testAttrs[k] = v + cred := cloud.NewCredential(cloud.OAuth1AuthType, map[string]string{ + "maas-oauth": "a:b:c", + }) + cloud := environs.CloudSpec{ + Type: "maas", + Name: "maas", + Endpoint: testServer.Server.URL, + Credential: &cred, } - testAttrs["maas-server"] = testServer.Server.URL - attrs := coretesting.FakeConfig().Merge(testAttrs) + attrs := coretesting.FakeConfig().Merge(maasEnvAttrs) cfg, err := config.New(config.NoDefaults, attrs) c.Assert(err, jc.ErrorIsNil) - return NewEnviron(cfg) + return NewEnviron(cloud, cfg) } func (suite *maas2EnvironSuite) TestNewEnvironWithController(c *gc.C) { @@ -65,30 +70,10 @@ c.Assert(env, gc.NotNil) } -func (suite *maas2EnvironSuite) TestSupportedArchitectures(c *gc.C) { - controller := &fakeController{ - bootResources: []gomaasapi.BootResource{ - &fakeBootResource{name: "wily", architecture: "amd64/blah"}, - &fakeBootResource{name: "wily", architecture: "amd64/something"}, - &fakeBootResource{name: "xenial", architecture: "arm/somethingelse"}, - }, - } - env := suite.makeEnviron(c, controller) - result, err := env.SupportedArchitectures() - c.Assert(err, jc.ErrorIsNil) - c.Assert(result, gc.DeepEquals, []string{"amd64", "arm"}) -} - -func (suite *maas2EnvironSuite) TestSupportedArchitecturesError(c *gc.C) { - env := suite.makeEnviron(c, &fakeController{bootResourcesError: errors.New("Something terrible!")}) - _, err := env.SupportedArchitectures() - c.Assert(err, gc.ErrorMatches, "Something terrible!") -} - func (suite *maas2EnvironSuite) injectControllerWithSpacesAndCheck(c *gc.C, spaces []gomaasapi.Space, expected gomaasapi.AllocateMachineArgs) *maasEnviron { var env *maasEnviron check := func(args gomaasapi.AllocateMachineArgs) { - expected.AgentName = env.ecfg().maasAgentName() + expected.AgentName = env.Config().UUID() c.Assert(args, gc.DeepEquals, expected) } controller := &fakeController{ @@ -109,7 +94,7 @@ var env *maasEnviron checkArgs := func(args gomaasapi.MachinesArgs) { c.Check(args.SystemIDs, gc.DeepEquals, expectedSystemIDs) - c.Check(args.AgentName, gc.Equals, env.ecfg().maasAgentName()) + c.Check(args.AgentName, gc.Equals, env.Config().UUID()) } machines := make([]gomaasapi.Machine, len(returnSystemIDs)) for index, id := range returnSystemIDs { @@ -260,7 +245,7 @@ func (suite *maas2EnvironSuite) TestStopInstancesStopsAndReleasesInstances(c *gc.C) { // Return a cannot complete indicating that test1 is in the wrong state. // The release operation will still release the others and succeed. - controller := newFakeControllerWithFiles(&fakeFile{name: "agent-prefix-provider-state"}) + controller := newFakeControllerWithFiles(&fakeFile{name: coretesting.ModelTag.Id() + "-provider-state"}) err := suite.makeEnviron(c, controller).StopInstances("test1", "test2", "test3") c.Check(err, jc.ErrorIsNil) args := collectReleaseArgs(controller) @@ -271,7 +256,7 @@ func (suite *maas2EnvironSuite) TestStopInstancesIgnoresConflict(c *gc.C) { // Return a cannot complete indicating that test1 is in the wrong state. // The release operation will still release the others and succeed. - controller := newFakeControllerWithFiles(&fakeFile{name: "agent-prefix-provider-state"}) + controller := newFakeControllerWithFiles(&fakeFile{name: coretesting.ModelTag.Id() + "-provider-state"}) controller.SetErrors(gomaasapi.NewCannotCompleteError("test1 not allocated")) err := suite.makeEnviron(c, controller).StopInstances("test1", "test2", "test3") c.Check(err, jc.ErrorIsNil) @@ -282,7 +267,7 @@ } func (suite *maas2EnvironSuite) TestStopInstancesIgnoresMissingNodeAndRecurses(c *gc.C) { - controller := newFakeControllerWithFiles(&fakeFile{name: "agent-prefix-provider-state"}) + controller := newFakeControllerWithFiles(&fakeFile{name: coretesting.ModelTag.Id() + "-provider-state"}) controller.SetErrors( gomaasapi.NewBadRequestError("no such machine: test1"), gomaasapi.NewBadRequestError("no such machine: test1"), @@ -298,7 +283,7 @@ } func (suite *maas2EnvironSuite) checkStopInstancesFails(c *gc.C, withError error) { - controller := newFakeControllerWithFiles(&fakeFile{name: "agent-prefix-provider-state"}) + controller := newFakeControllerWithFiles(&fakeFile{name: coretesting.ModelTag.Id() + "-provider-state"}) controller.SetErrors(withError) err := suite.makeEnviron(c, controller).StopInstances("test1", "test2", "test3") c.Check(err, gc.ErrorMatches, fmt.Sprintf("cannot release nodes: %s", withError)) @@ -337,7 +322,7 @@ suite.injectController(&fakeController{ allocateMachineArgsCheck: func(args gomaasapi.AllocateMachineArgs) { c.Assert(args, gc.DeepEquals, gomaasapi.AllocateMachineArgs{ - AgentName: env.ecfg().maasAgentName(), + AgentName: env.Config().UUID(), Zone: "foo", MinMemory: 8192, }) @@ -365,7 +350,7 @@ suite.injectController(&fakeController{ allocateMachineArgsCheck: func(args gomaasapi.AllocateMachineArgs) { c.Assert(args, gc.DeepEquals, gomaasapi.AllocateMachineArgs{ - AgentName: env.ecfg().maasAgentName()}) + AgentName: env.Config().UUID()}) }, allocateMachine: &fakeMachine{ systemID: "Bruce Sterling", @@ -459,7 +444,7 @@ suite.injectController(&fakeController{ allocateMachineArgsCheck: func(args gomaasapi.AllocateMachineArgs) { c.Assert(args, jc.DeepEquals, gomaasapi.AllocateMachineArgs{ - AgentName: env.ecfg().maasAgentName(), + AgentName: env.Config().UUID(), Storage: getStorage(), }) }, @@ -511,7 +496,7 @@ suite.injectController(&fakeController{ allocateMachineArgsCheck: func(args gomaasapi.AllocateMachineArgs) { c.Assert(args, gc.DeepEquals, gomaasapi.AllocateMachineArgs{ - AgentName: env.ecfg().maasAgentName(), + AgentName: env.Config().UUID(), Interfaces: getPositives(), NotSpace: getNegatives(), }) @@ -1025,6 +1010,7 @@ systemID: "foo", } controller := &fakeController{ + Stub: &testing.Stub{}, machines: []gomaasapi.Machine{&fakeMachine{ Stub: &testing.Stub{}, systemID: "1", @@ -1180,6 +1166,7 @@ Stub: &testing.Stub{}, } controller := &fakeController{ + Stub: &testing.Stub{}, machines: []gomaasapi.Machine{&fakeMachine{ Stub: &testing.Stub{}, systemID: "1", @@ -1286,7 +1273,7 @@ subnet := makeFakeSubnet(3) checkMachinesArgs := func(args gomaasapi.MachinesArgs) { expected := gomaasapi.MachinesArgs{ - AgentName: env.ecfg().maasAgentName(), + AgentName: env.Config().UUID(), SystemIDs: []string{"1"}, } c.Assert(args, jc.DeepEquals, expected) @@ -1499,7 +1486,7 @@ func (suite *maas2EnvironSuite) TestStartInstanceEndToEnd(c *gc.C) { suite.setupFakeTools(c) machine := newFakeMachine("gus", arch.HostArch(), "Deployed") - file := &fakeFile{name: "agent-prefix-provider-state"} + file := &fakeFile{name: coretesting.ModelTag.Id() + "-provider-state"} controller := newFakeControllerWithFiles(file) controller.machines = []gomaasapi.Machine{machine} controller.allocateMachine = machine @@ -1582,7 +1569,7 @@ c.Assert(err, jc.ErrorIsNil) controller.files = []gomaasapi.File{&fakeFile{ - name: "agent-prefix-provider-state", + name: coretesting.ModelTag.Id() + "-provider-state", contents: state, }} controllerInstances, err := env.ControllerInstances(suite.controllerUUID) @@ -1599,8 +1586,8 @@ } func (suite *maas2EnvironSuite) TestDestroy(c *gc.C) { - file1 := &fakeFile{name: "agent-prefix-provider-state"} - file2 := &fakeFile{name: "agent-prefix-horace"} + file1 := &fakeFile{name: coretesting.ModelTag.Id() + "-provider-state"} + file2 := &fakeFile{name: coretesting.ModelTag.Id() + "-horace"} controller := newFakeControllerWithFiles(file1, file2) controller.machines = []gomaasapi.Machine{&fakeMachine{systemID: "pete"}} env := suite.makeEnviron(c, controller) @@ -1655,7 +1642,7 @@ // Add a dummy file to storage so we can use that to check the // obtained source later. env := suite.makeEnviron(c, newFakeControllerWithFiles( - &fakeFile{name: "agent-prefix-tools/filename", contents: makeRandomBytes(10)}, + &fakeFile{name: coretesting.ModelTag.Id() + "-tools/filename", contents: makeRandomBytes(10)}, )) sources, err := envtools.GetMetadataSources(env) c.Assert(err, jc.ErrorIsNil) @@ -1687,3 +1674,62 @@ _, err = validator.Validate(cons) c.Assert(err, gc.ErrorMatches, "invalid constraint value: arch=ppc64el\nvalid values are: \\[amd64 armhf\\]") } + +func (suite *maas2EnvironSuite) TestReleaseContainerAddresses(c *gc.C) { + dev1 := newFakeDeviceWithMAC("eleven") + dev2 := newFakeDeviceWithMAC("will") + controller := newFakeController() + controller.devices = []gomaasapi.Device{dev1, dev2} + + env := suite.makeEnviron(c, controller) + err := env.ReleaseContainerAddresses([]network.InterfaceInfo{ + {MACAddress: "will"}, + {MACAddress: "dustin"}, + {MACAddress: "eleven"}, + }) + c.Assert(err, jc.ErrorIsNil) + + args, ok := getArgs(c, controller.Calls()).(gomaasapi.DevicesArgs) + c.Assert(ok, jc.IsTrue) + expected := gomaasapi.DevicesArgs{MACAddresses: []string{"will", "dustin", "eleven"}} + c.Assert(args, gc.DeepEquals, expected) + + dev1.CheckCallNames(c, "Delete") + dev2.CheckCallNames(c, "Delete") +} + +func (suite *maas2EnvironSuite) TestReleaseContainerAddressesErrorGettingDevices(c *gc.C) { + controller := newFakeControllerWithErrors(errors.New("Everything done broke")) + env := suite.makeEnviron(c, controller) + err := env.ReleaseContainerAddresses([]network.InterfaceInfo{{MACAddress: "anything"}}) + c.Assert(err, gc.ErrorMatches, "Everything done broke") +} + +func (suite *maas2EnvironSuite) TestReleaseContainerAddressesErrorDeletingDevice(c *gc.C) { + dev1 := newFakeDeviceWithMAC("eleven") + dev1.systemID = "hopper" + dev1.SetErrors(errors.New("don't delete me")) + controller := newFakeController() + controller.devices = []gomaasapi.Device{dev1} + + env := suite.makeEnviron(c, controller) + err := env.ReleaseContainerAddresses([]network.InterfaceInfo{ + {MACAddress: "eleven"}, + }) + c.Assert(err, gc.ErrorMatches, "deleting device hopper: don't delete me") + + _, ok := getArgs(c, controller.Calls()).(gomaasapi.DevicesArgs) + c.Assert(ok, jc.IsTrue) + + dev1.CheckCallNames(c, "Delete") +} + +func newFakeDeviceWithMAC(macAddress string) *fakeDevice { + return &fakeDevice{ + Stub: &testing.Stub{}, + interface_: &fakeInterface{ + Stub: &testing.Stub{}, + macAddress: macAddress, + }, + } +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/maas2storage.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/maas2storage.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/maas2storage.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/maas2storage.go 2016-08-16 08:56:25.000000000 +0000 @@ -76,6 +76,8 @@ } // DefaultConsistencyStrategy implements storage.StorageReader +// +// TODO(katco): 2016-08-09: lp:1611427 func (stor *maas2Storage) DefaultConsistencyStrategy() utils.AttemptStrategy { return utils.AttemptStrategy{} } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/maas2storage_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/maas2storage_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/maas2storage_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/maas2storage_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -29,10 +29,10 @@ } func (s *maas2StorageSuite) makeStorage(c *gc.C, controller gomaasapi.Controller) *maas2Storage { - storage, ok := NewStorage(s.makeEnviron(c, controller)).(*maas2Storage) + env := s.makeEnviron(c, controller) + env.uuid = "prefix" + storage, ok := NewStorage(env).(*maas2Storage) c.Assert(ok, jc.IsTrue) - ecfg := storage.environ.ecfg() - ecfg.attrs["maas-agent-name"] = "prefix" return storage } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/maas2_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/maas2_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/maas2_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/maas2_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,6 +11,8 @@ "github.com/juju/utils/set" gc "gopkg.in/check.v1" + "github.com/juju/juju/cloud" + "github.com/juju/juju/environs" "github.com/juju/juju/environs/config" coretesting "github.com/juju/juju/testing" "github.com/juju/juju/version" @@ -35,15 +37,23 @@ for k, v := range maasEnvAttrs { testAttrs[k] = v } - testAttrs["maas-server"] = "http://any-old-junk.invalid/" testAttrs["agent-version"] = version.Current.String() - testAttrs["maas-agent-name"] = "agent-prefix" + + cred := cloud.NewCredential(cloud.OAuth1AuthType, map[string]string{ + "maas-oauth": "a:b:c", + }) + cloud := environs.CloudSpec{ + Type: "maas", + Name: "maas", + Endpoint: "http://any-old-junk.invalid/", + Credential: &cred, + } attrs := coretesting.FakeConfig().Merge(testAttrs) suite.controllerUUID = coretesting.FakeControllerConfig().ControllerUUID() cfg, err := config.New(config.NoDefaults, attrs) c.Assert(err, jc.ErrorIsNil) - env, err := NewEnviron(cfg) + env, err := NewEnviron(cloud, cfg) c.Assert(err, jc.ErrorIsNil) c.Assert(env, gc.NotNil) return env @@ -87,8 +97,9 @@ return &fakeController{Stub: &testing.Stub{}, files: files} } -func (c *fakeController) Devices(gomaasapi.DevicesArgs) ([]gomaasapi.Device, error) { - return c.devices, nil +func (c *fakeController) Devices(args gomaasapi.DevicesArgs) ([]gomaasapi.Device, error) { + c.MethodCall(c, "Devices", args) + return c.devices, c.NextErr() } func (c *fakeController) Machines(args gomaasapi.MachinesArgs) ([]gomaasapi.Machine, error) { @@ -503,3 +514,8 @@ d.interfaceSet = append(d.interfaceSet, d.interface_) return d.interface_, d.NextErr() } + +func (d *fakeDevice) Delete() error { + d.MethodCall(d, "Delete") + return d.NextErr() +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/maas_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/maas_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/maas_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/maas_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -18,6 +18,8 @@ "github.com/juju/utils/set" gc "gopkg.in/check.v1" + "github.com/juju/juju/cloud" + "github.com/juju/juju/environs" "github.com/juju/juju/environs/config" sstesting "github.com/juju/juju/environs/simplestreams/testing" envtesting "github.com/juju/juju/environs/testing" @@ -86,8 +88,6 @@ return &out } -const exampleAgentName = "dfb69555-0bc4-4d1f-85f2-4ee390974984" - func (s *providerSuite) SetUpSuite(c *gc.C) { s.baseProviderSuite.SetUpSuite(c) s.testMAASObject = gomaasapi.NewTestMAAS("1.0") @@ -118,26 +118,28 @@ } var maasEnvAttrs = coretesting.Attrs{ - "name": "test-env", - "type": "maas", - "maas-oauth": "a:b:c", - "maas-agent-name": exampleAgentName, + "name": "test-env", + "type": "maas", } // makeEnviron creates a functional maasEnviron for a test. func (suite *providerSuite) makeEnviron() *maasEnviron { - testAttrs := coretesting.Attrs{} - for k, v := range maasEnvAttrs { - testAttrs[k] = v + cred := cloud.NewCredential(cloud.OAuth1AuthType, map[string]string{ + "maas-oauth": "a:b:c", + }) + cloud := environs.CloudSpec{ + Type: "maas", + Name: "maas", + Endpoint: suite.testMAASObject.TestServer.URL, + Credential: &cred, } - testAttrs["maas-server"] = suite.testMAASObject.TestServer.URL - attrs := coretesting.FakeConfig().Merge(testAttrs) + attrs := coretesting.FakeConfig().Merge(maasEnvAttrs) suite.controllerUUID = coretesting.FakeControllerConfig().ControllerUUID() cfg, err := config.New(config.NoDefaults, attrs) if err != nil { panic(err) } - env, err := NewEnviron(cfg) + env, err := NewEnviron(cloud, cfg) if err != nil { panic(err) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/storage.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/storage.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/storage.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/storage.go 2016-08-16 08:56:25.000000000 +0000 @@ -55,11 +55,7 @@ // This prevents different environments from interfering with each other. // We're using the agent name UUID here. func prefixWithPrivateNamespace(env *maasEnviron, name string) string { - prefix := env.ecfg().maasAgentName() - if prefix != "" { - return prefix + "-" + name - } - return name + return env.uuid + "-" + name } func (stor *maas1Storage) prefixWithPrivateNamespace(name string) string { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/storage_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/storage_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/storage_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/storage_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -388,22 +388,11 @@ } func (s *storageSuite) TestprefixWithPrivateNamespacePrefixesWithAgentName(c *gc.C) { - sstor := NewStorage(s.makeEnviron()) + env := s.makeEnviron() + sstor := NewStorage(env) stor := sstor.(*maas1Storage) - agentName := stor.environ.ecfg().maasAgentName() - c.Assert(agentName, gc.Not(gc.Equals), "") - expectedPrefix := agentName + "-" + expectedPrefix := env.Config().UUID() + "-" const name = "myname" expectedResult := expectedPrefix + name c.Assert(stor.prefixWithPrivateNamespace(name), gc.Equals, expectedResult) } - -func (s *storageSuite) TesttprefixWithPrivateNamespaceIgnoresAgentName(c *gc.C) { - sstor := NewStorage(s.makeEnviron()) - stor := sstor.(*maas1Storage) - ecfg := stor.environ.ecfg() - ecfg.attrs["maas-agent-name"] = "" - - const name = "myname" - c.Assert(stor.prefixWithPrivateNamespace(name), gc.Equals, name) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/volumes.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/volumes.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/maas/volumes.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/maas/volumes.go 2016-08-16 08:56:25.000000000 +0000 @@ -15,7 +15,6 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/constraints" - "github.com/juju/juju/environs/config" "github.com/juju/juju/provider/common" "github.com/juju/juju/storage" ) @@ -34,6 +33,19 @@ tagsAttribute = "tags" ) +// StorageProviderTypes implements storage.ProviderRegistry. +func (*maasEnviron) StorageProviderTypes() []storage.ProviderType { + return []storage.ProviderType{maasStorageProviderType} +} + +// StorageProvider implements storage.ProviderRegistry. +func (*maasEnviron) StorageProvider(t storage.ProviderType) (storage.Provider, error) { + if t == maasStorageProviderType { + return maasStorageProvider{}, nil + } + return nil, errors.NotFoundf("storage provider %q", t) +} + // maasStorageProvider allows volumes to be specified when a node is acquired. type maasStorageProvider struct{} @@ -82,34 +94,39 @@ } // ValidateConfig is defined on the Provider interface. -func (e *maasStorageProvider) ValidateConfig(cfg *storage.Config) error { +func (maasStorageProvider) ValidateConfig(cfg *storage.Config) error { _, err := newStorageConfig(cfg.Attrs()) return errors.Trace(err) } // Supports is defined on the Provider interface. -func (e *maasStorageProvider) Supports(k storage.StorageKind) bool { +func (maasStorageProvider) Supports(k storage.StorageKind) bool { return k == storage.StorageKindBlock } // Scope is defined on the Provider interface. -func (e *maasStorageProvider) Scope() storage.Scope { +func (maasStorageProvider) Scope() storage.Scope { return storage.ScopeEnviron } // Dynamic is defined on the Provider interface. -func (e *maasStorageProvider) Dynamic() bool { +func (maasStorageProvider) Dynamic() bool { return false } +// DefaultPools is defined on the Provider interface. +func (maasStorageProvider) DefaultPools() []*storage.Config { + return nil +} + // VolumeSource is defined on the Provider interface. -func (e *maasStorageProvider) VolumeSource(environConfig *config.Config, providerConfig *storage.Config) (storage.VolumeSource, error) { +func (maasStorageProvider) VolumeSource(providerConfig *storage.Config) (storage.VolumeSource, error) { // Dynamic volumes not supported. return nil, errors.NotSupportedf("volumes") } // FilesystemSource is defined on the Provider interface. -func (e *maasStorageProvider) FilesystemSource(environConfig *config.Config, providerConfig *storage.Config) (storage.FilesystemSource, error) { +func (maasStorageProvider) FilesystemSource(providerConfig *storage.Config) (storage.FilesystemSource, error) { return nil, errors.NotSupportedf("filesystems") } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/manual/config.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/manual/config.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/manual/config.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/manual/config.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,7 +11,6 @@ var ( configFields = schema.Fields{ - "bootstrap-host": schema.String(), "bootstrap-user": schema.String(), } configDefaults = schema.Defaults{ @@ -28,10 +27,6 @@ return &environConfig{Config: config, attrs: attrs} } -func (c *environConfig) bootstrapHost() string { - return c.attrs["bootstrap-host"].(string) -} - func (c *environConfig) bootstrapUser() string { return c.attrs["bootstrap-user"].(string) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/manual/config_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/manual/config_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/manual/config_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/manual/config_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,6 +10,7 @@ jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" + "github.com/juju/juju/environs" "github.com/juju/juju/environs/config" coretesting "github.com/juju/juju/testing" ) @@ -20,13 +21,21 @@ var _ = gc.Suite(&configSuite{}) +func CloudSpec() environs.CloudSpec { + return environs.CloudSpec{ + Name: "manual", + Type: "manual", + Endpoint: "hostname", + } +} + func MinimalConfigValues() map[string]interface{} { return map[string]interface{}{ "name": "test", "type": "manual", "uuid": coretesting.ModelTag.Id(), "controller-uuid": coretesting.ModelTag.Id(), - "bootstrap-host": "hostname", + "firewall-mode": "instance", "bootstrap-user": "", // While the ca-cert bits aren't entirely minimal, they avoid the need // to set up a fake home. @@ -50,25 +59,6 @@ return envConfig } -func (s *configSuite) TestValidateConfig(c *gc.C) { - testConfig := MinimalConfig(c) - testConfig, err := testConfig.Apply(map[string]interface{}{"bootstrap-host": ""}) - c.Assert(err, jc.ErrorIsNil) - _, err = manualProvider{}.Validate(testConfig, nil) - c.Assert(err, gc.ErrorMatches, "bootstrap-host must be specified") - - values := MinimalConfigValues() - delete(values, "bootstrap-user") - testConfig, err = config.New(config.UseDefaults, values) - c.Assert(err, jc.ErrorIsNil) - - valid, err := manualProvider{}.Validate(testConfig, nil) - c.Assert(err, jc.ErrorIsNil) - unknownAttrs := valid.UnknownAttrs() - c.Assert(unknownAttrs["bootstrap-host"], gc.Equals, "hostname") - c.Assert(unknownAttrs["bootstrap-user"], gc.Equals, "") -} - func (s *configSuite) TestConfigMutability(c *gc.C) { testConfig := MinimalConfig(c) valid, err := manualProvider{}.Validate(testConfig, nil) @@ -80,7 +70,6 @@ // machine agent's config/upstart config. oldConfig := testConfig for k, v := range map[string]interface{}{ - "bootstrap-host": "new-hostname", "bootstrap-user": "new-username", } { testConfig = MinimalConfig(c) @@ -93,14 +82,11 @@ } } -func (s *configSuite) TestBootstrapHostUser(c *gc.C) { +func (s *configSuite) TestBootstrapUser(c *gc.C) { values := MinimalConfigValues() testConfig := getModelConfig(c, values) - c.Assert(testConfig.bootstrapHost(), gc.Equals, "hostname") c.Assert(testConfig.bootstrapUser(), gc.Equals, "") - values["bootstrap-host"] = "127.0.0.1" values["bootstrap-user"] = "ubuntu" testConfig = getModelConfig(c, values) - c.Assert(testConfig.bootstrapHost(), gc.Equals, "127.0.0.1") c.Assert(testConfig.bootstrapUser(), gc.Equals, "ubuntu") } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/manual/environ.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/manual/environ.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/manual/environ.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/manual/environ.go 2016-08-16 08:56:25.000000000 +0000 @@ -15,7 +15,6 @@ "github.com/juju/errors" "github.com/juju/loggo" "github.com/juju/utils" - "github.com/juju/utils/arch" "github.com/juju/utils/ssh" "github.com/juju/juju/agent" @@ -45,12 +44,9 @@ ) type manualEnviron struct { - common.SupportsUnitPlacementPolicy - - cfg *environConfig - cfgmutex sync.Mutex - ubuntuUserInited bool - ubuntuUserInitMutex sync.Mutex + host string + cfgmutex sync.Mutex + cfg *environConfig } var errNoStartInstance = errors.New("manual provider cannot start instances") @@ -84,23 +80,29 @@ return e.envConfig().Config } -// SupportedArchitectures is specified on the EnvironCapability interface. -func (e *manualEnviron) SupportedArchitectures() ([]string, error) { - return arch.AllSupportedArches, nil +// PrepareForBootstrap is part of the Environ interface. +func (e *manualEnviron) PrepareForBootstrap(ctx environs.BootstrapContext) error { + if err := ensureBootstrapUbuntuUser(ctx, e.host, e.envConfig()); err != nil { + return err + } + return nil +} + +// Create is part of the Environ interface. +func (e *manualEnviron) Create(environs.CreateParams) error { + return nil } -// Bootstrap is specified on the Environ interface. +// Bootstrap is part of the Environ interface. func (e *manualEnviron) Bootstrap(ctx environs.BootstrapContext, args environs.BootstrapParams) (*environs.BootstrapResult, error) { - envConfig := e.envConfig() - host := envConfig.bootstrapHost() - provisioned, err := manualCheckProvisioned(host) + provisioned, err := manualCheckProvisioned(e.host) if err != nil { return nil, errors.Annotate(err, "failed to check provisioned status") } if provisioned { return nil, manual.ErrProvisioned } - hc, series, err := manualDetectSeriesAndHardwareCharacteristics(host) + hc, series, err := manualDetectSeriesAndHardwareCharacteristics(e.host) if err != nil { return nil, err } @@ -110,7 +112,7 @@ if err := instancecfg.FinishInstanceConfig(icfg, e.Config()); err != nil { return err } - return common.ConfigureMachine(ctx, ssh.DefaultClient, host, icfg) + return common.ConfigureMachine(ctx, ssh.DefaultClient, e.host, icfg) } result := &environs.BootstrapResult{ @@ -147,7 +149,7 @@ noAgentDir, ) out, err := runSSHCommand( - "ubuntu@"+e.cfg.bootstrapHost(), + "ubuntu@"+e.host, []string{"/bin/bash"}, stdin, ) @@ -186,7 +188,7 @@ var found bool for i, id := range ids { if id == BootstrapInstanceId { - instances[i] = manualBootstrapInstance{e.envConfig().bootstrapHost()} + instances[i] = manualBootstrapInstance{e.host} found = true } else { err = environs.ErrPartialInstances @@ -238,7 +240,7 @@ utils.ShQuote(agent.DefaultPaths.LogDir), ) _, err := runSSHCommand( - "ubuntu@"+e.envConfig().bootstrapHost(), + "ubuntu@"+e.host, []string{"sudo", "/bin/bash"}, script, ) return err diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/manual/environ_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/manual/environ_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/manual/environ_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/manual/environ_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,7 +9,6 @@ "github.com/juju/errors" "github.com/juju/testing" jc "github.com/juju/testing/checkers" - "github.com/juju/utils/arch" gc "gopkg.in/check.v1" "github.com/juju/juju/constraints" @@ -18,31 +17,27 @@ coretesting "github.com/juju/juju/testing" ) -type environSuite struct { +type baseEnvironSuite struct { coretesting.FakeJujuXDGDataHomeSuite env *manualEnviron } -var _ = gc.Suite(&environSuite{}) - -func (s *environSuite) SetUpTest(c *gc.C) { +func (s *baseEnvironSuite) SetUpTest(c *gc.C) { s.FakeJujuXDGDataHomeSuite.SetUpTest(c) - env, err := manualProvider{}.Open(MinimalConfig(c)) + env, err := manualProvider{}.Open(environs.OpenParams{ + Cloud: CloudSpec(), + Config: MinimalConfig(c), + }) c.Assert(err, jc.ErrorIsNil) s.env = env.(*manualEnviron) } -func (s *environSuite) TestSetConfig(c *gc.C) { - err := s.env.SetConfig(MinimalConfig(c)) - c.Assert(err, jc.ErrorIsNil) - - testConfig := MinimalConfig(c) - testConfig, err = testConfig.Apply(map[string]interface{}{"bootstrap-host": ""}) - c.Assert(err, jc.ErrorIsNil) - err = s.env.SetConfig(testConfig) - c.Assert(err, gc.ErrorMatches, "bootstrap-host must be specified") +type environSuite struct { + baseEnvironSuite } +var _ = gc.Suite(&environSuite{}) + func (s *environSuite) TestInstances(c *gc.C) { var ids []instance.Id @@ -118,12 +113,6 @@ } } -func (s *environSuite) TestSupportedArchitectures(c *gc.C) { - arches, err := s.env.SupportedArchitectures() - c.Assert(err, jc.ErrorIsNil) - c.Assert(arches, gc.DeepEquals, arch.AllSupportedArches) -} - func (s *environSuite) TestSupportsNetworking(c *gc.C) { _, ok := environs.SupportsNetworking(s.env) c.Assert(ok, jc.IsFalse) @@ -138,34 +127,12 @@ c.Assert(unsupported, jc.SameContents, []string{"cpu-power", "instance-type", "tags", "virt-type"}) } -type bootstrapSuite struct { - coretesting.FakeJujuXDGDataHomeSuite - env *manualEnviron -} - -var _ = gc.Suite(&bootstrapSuite{}) - -func (s *bootstrapSuite) SetUpTest(c *gc.C) { - s.FakeJujuXDGDataHomeSuite.SetUpTest(c) - env, err := manualProvider{}.Open(MinimalConfig(c)) - c.Assert(err, jc.ErrorIsNil) - s.env = env.(*manualEnviron) -} - type controllerInstancesSuite struct { - coretesting.FakeJujuXDGDataHomeSuite - env *manualEnviron + baseEnvironSuite } var _ = gc.Suite(&controllerInstancesSuite{}) -func (s *controllerInstancesSuite) SetUpTest(c *gc.C) { - s.FakeJujuXDGDataHomeSuite.SetUpTest(c) - env, err := manualProvider{}.Open(MinimalConfig(c)) - c.Assert(err, jc.ErrorIsNil) - s.env = env.(*manualEnviron) -} - func (s *controllerInstancesSuite) TestControllerInstances(c *gc.C) { var outputResult string var errResult error diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/manual/init.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/manual/init.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/manual/init.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/manual/init.go 2016-08-16 08:56:25.000000000 +0000 @@ -3,10 +3,7 @@ package manual -import ( - "github.com/juju/juju/environs" - "github.com/juju/juju/storage/provider/registry" -) +import "github.com/juju/juju/environs" const ( providerType = "manual" @@ -15,6 +12,4 @@ func init() { p := manualProvider{} environs.RegisterProvider(providerType, p, "null") - - registry.RegisterEnvironStorageProviders(providerType) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/manual/provider.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/manual/provider.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/manual/provider.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/manual/provider.go 2016-08-16 08:56:25.000000000 +0000 @@ -21,12 +21,10 @@ // Verify that we conform to the interface. var _ environs.EnvironProvider = (*manualProvider)(nil) -var errNoBootstrapHost = errors.New("bootstrap-host must be specified") - var initUbuntuUser = manual.InitUbuntuUser -func ensureBootstrapUbuntuUser(ctx environs.BootstrapContext, cfg *environConfig) error { - err := initUbuntuUser(cfg.bootstrapHost(), cfg.bootstrapUser(), cfg.AuthorizedKeys(), ctx.GetStdin(), ctx.GetStdout()) +func ensureBootstrapUbuntuUser(ctx environs.BootstrapContext, host string, cfg *environConfig) error { + err := initUbuntuUser(host, cfg.bootstrapUser(), cfg.AuthorizedKeys(), ctx.GetStdin(), ctx.GetStdout()) if err != nil { logger.Errorf("initializing ubuntu user: %v", err) return err @@ -37,7 +35,7 @@ // RestrictedConfigAttributes is specified in the EnvironProvider interface. func (p manualProvider) RestrictedConfigAttributes() []string { - return []string{"bootstrap-host", "bootstrap-user"} + return []string{"bootstrap-user"} } // DetectRegions is specified in the environs.CloudRegionDetector interface. @@ -45,58 +43,48 @@ return nil, errors.NotFoundf("regions") } -// PrepareForCreateEnvironment is specified in the EnvironProvider interface. -func (p manualProvider) PrepareForCreateEnvironment(controllerUUID string, cfg *config.Config) (*config.Config, error) { - return cfg, nil -} - -// BootstrapConfig is specified in the EnvironProvider interface. -func (p manualProvider) BootstrapConfig(args environs.BootstrapConfigParams) (*config.Config, error) { - if args.CloudEndpoint == "" { +// PrepareConfig is specified in the EnvironProvider interface. +func (p manualProvider) PrepareConfig(args environs.PrepareConfigParams) (*config.Config, error) { + if args.Cloud.Endpoint == "" { return nil, errors.Errorf( "missing address of host to bootstrap: " + `please specify "juju bootstrap manual/"`, ) } - cfg, err := args.Config.Apply(map[string]interface{}{ - "bootstrap-host": args.CloudEndpoint, - }) - if err != nil { - return nil, errors.Trace(err) - } - envConfig, err := p.validate(cfg, nil) + envConfig, err := p.validate(args.Config, nil) if err != nil { return nil, err } - return cfg.Apply(envConfig.attrs) + return args.Config.Apply(envConfig.attrs) } -// PrepareForBootstrap is specified in the EnvironProvider interface. -func (p manualProvider) PrepareForBootstrap(ctx environs.BootstrapContext, cfg *config.Config) (environs.Environ, error) { - if _, err := p.validate(cfg, nil); err != nil { - return nil, err - } - envConfig := newModelConfig(cfg, cfg.UnknownAttrs()) - if err := ensureBootstrapUbuntuUser(ctx, envConfig); err != nil { - return nil, err +func (p manualProvider) Open(args environs.OpenParams) (environs.Environ, error) { + if err := validateCloudSpec(args.Cloud); err != nil { + return nil, errors.Trace(err) } - return p.open(envConfig) -} - -func (p manualProvider) Open(cfg *config.Config) (environs.Environ, error) { - _, err := p.validate(cfg, nil) + _, err := p.validate(args.Config, nil) if err != nil { return nil, err } // validate adds missing manual-specific config attributes // with their defaults in the result; we don't wnat that in // Open. - envConfig := newModelConfig(cfg, cfg.UnknownAttrs()) - return p.open(envConfig) + envConfig := newModelConfig(args.Config, args.Config.UnknownAttrs()) + return p.open(args.Cloud.Endpoint, envConfig) +} + +func validateCloudSpec(spec environs.CloudSpec) error { + if spec.Endpoint == "" { + return errors.Errorf( + "missing address of host to bootstrap: " + + `please specify "juju bootstrap manual/"`, + ) + } + return nil } -func (p manualProvider) open(cfg *environConfig) (environs.Environ, error) { - env := &manualEnviron{cfg: cfg} +func (p manualProvider) open(host string, cfg *environConfig) (environs.Environ, error) { + env := &manualEnviron{host: host, cfg: cfg} // Need to call SetConfig to initialise storage. if err := env.SetConfig(cfg.Config); err != nil { return nil, err @@ -121,9 +109,6 @@ return nil, err } envConfig := newModelConfig(cfg, validated) - if envConfig.bootstrapHost() == "" { - return nil, errNoBootstrapHost - } // Check various immutable attributes. if old != nil { oldEnvConfig, err := p.validate(old, nil) @@ -132,7 +117,6 @@ } for _, key := range [...]string{ "bootstrap-user", - "bootstrap-host", } { if err = checkImmutableString(envConfig, oldEnvConfig, key); err != nil { return nil, err diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/manual/provider_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/manual/provider_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/manual/provider_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/manual/provider_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -33,14 +33,6 @@ }) } -func (s *providerSuite) TestPrepareForCreateEnvironment(c *gc.C) { - testConfig, err := config.New(config.UseDefaults, manual.MinimalConfigValues()) - c.Assert(err, jc.ErrorIsNil) - cfg, err := manual.ProviderInstance.PrepareForCreateEnvironment(coretesting.ModelTag.Id(), testConfig) - c.Assert(err, jc.ErrorIsNil) - c.Assert(cfg, gc.Equals, testConfig) -} - func (s *providerSuite) TestPrepareForBootstrapCloudEndpointAndRegion(c *gc.C) { ctx, err := s.testPrepareForBootstrap(c, "endpoint", "region") c.Assert(err, jc.ErrorIsNil) @@ -57,17 +49,26 @@ minimal := manual.MinimalConfigValues() testConfig, err := config.New(config.UseDefaults, minimal) c.Assert(err, jc.ErrorIsNil) - testConfig, err = manual.ProviderInstance.BootstrapConfig(environs.BootstrapConfigParams{ - Config: testConfig, - CloudEndpoint: endpoint, - CloudRegion: region, + cloudSpec := environs.CloudSpec{ + Endpoint: endpoint, + Region: region, + } + testConfig, err = manual.ProviderInstance.PrepareConfig(environs.PrepareConfigParams{ + Config: testConfig, + Cloud: cloudSpec, + }) + if err != nil { + return nil, err + } + env, err := manual.ProviderInstance.Open(environs.OpenParams{ + Cloud: cloudSpec, + Config: testConfig, }) if err != nil { return nil, err } ctx := envtesting.BootstrapContext(c) - _, err = manual.ProviderInstance.PrepareForBootstrap(ctx, testConfig) - return ctx, err + return ctx, env.PrepareForBootstrap(ctx) } func (s *providerSuite) TestNullAlias(c *gc.C) { @@ -84,7 +85,7 @@ c.Assert(err, jc.ErrorIsNil) attrs := manual.MinimalConfigValues() - testConfig, err := config.New(config.UseDefaults, attrs) + testConfig, err := config.New(config.NoDefaults, attrs) c.Assert(err, jc.ErrorIsNil) c.Check(testConfig.EnableOSRefreshUpdate(), jc.IsTrue) c.Check(testConfig.EnableOSUpgrade(), jc.IsTrue) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/manual/storage.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/manual/storage.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/manual/storage.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/manual/storage.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,20 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package manual + +import ( + "github.com/juju/errors" + + "github.com/juju/juju/storage" +) + +// StorageProviderTypes implements storage.ProviderRegistry. +func (*manualEnviron) StorageProviderTypes() []storage.ProviderType { + return nil +} + +// StorageProvider implements storage.ProviderRegistry. +func (*manualEnviron) StorageProvider(t storage.ProviderType) (storage.Provider, error) { + return nil, errors.NotFoundf("storage provider %q", t) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/openstack/cinder.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/openstack/cinder.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/openstack/cinder.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/openstack/cinder.go 2016-08-16 08:56:25.000000000 +0000 @@ -15,7 +15,6 @@ "gopkg.in/goose.v1/identity" "gopkg.in/goose.v1/nova" - "github.com/juju/juju/environs/config" "github.com/juju/juju/environs/tags" "github.com/juju/juju/instance" "github.com/juju/juju/storage" @@ -33,8 +32,56 @@ volumeStatusInUse = "in-use" ) +// StorageProviderTypes implements storage.ProviderRegistry. +func (*Environ) StorageProviderTypes() []storage.ProviderType { + return []storage.ProviderType{CinderProviderType} +} + +// StorageProvider implements storage.ProviderRegistry. +func (env *Environ) StorageProvider(t storage.ProviderType) (storage.Provider, error) { + if t != CinderProviderType { + return nil, errors.NotFoundf("storage provider %q", t) + } + return env.cinderProvider() +} + +func (env *Environ) cinderProvider() (*cinderProvider, error) { + env.ecfgMutex.Lock() + envName := env.ecfgUnlocked.Config.Name() + modelUUID := env.ecfgUnlocked.Config.UUID() + env.ecfgMutex.Unlock() + + storageAdapter, err := newOpenstackStorage(env) + if err != nil { + return nil, errors.Trace(err) + } + return &cinderProvider{storageAdapter, envName, modelUUID}, nil +} + +var newOpenstackStorage = func(env *Environ) (OpenstackStorage, error) { + env.ecfgMutex.Lock() + authClient := env.client + envNovaClient := env.novaUnlocked + env.ecfgMutex.Unlock() + + endpointUrl, err := getVolumeEndpointURL(authClient, env.cloud.Region) + if err != nil { + if errors.IsNotFound(err) { + return nil, errors.NewNotSupported(err, "volumes not supported") + } + return nil, errors.Annotate(err, "getting volume endpoint") + } + + return &openstackStorageAdapter{ + cinderClient{cinder.Basic(endpointUrl, authClient.TenantId(), authClient.Token)}, + novaClient{envNovaClient}, + }, nil +} + type cinderProvider struct { - newStorageAdapter func(*config.Config) (openstackStorage, error) + storageAdapter OpenstackStorage + envName string + modelUUID string } var _ storage.Provider = (*cinderProvider)(nil) @@ -45,24 +92,20 @@ } // VolumeSource implements storage.Provider. -func (p *cinderProvider) VolumeSource(environConfig *config.Config, providerConfig *storage.Config) (storage.VolumeSource, error) { +func (p *cinderProvider) VolumeSource(providerConfig *storage.Config) (storage.VolumeSource, error) { if err := p.ValidateConfig(providerConfig); err != nil { return nil, err } - storageAdapter, err := p.newStorageAdapter(environConfig) - if err != nil { - return nil, err - } source := &cinderVolumeSource{ - storageAdapter: storageAdapter, - envName: environConfig.Name(), - modelUUID: environConfig.UUID(), + storageAdapter: p.storageAdapter, + envName: p.envName, + modelUUID: p.modelUUID, } return source, nil } // FilesystemSource implements storage.Provider. -func (p *cinderProvider) FilesystemSource(environConfig *config.Config, providerConfig *storage.Config) (storage.FilesystemSource, error) { +func (p *cinderProvider) FilesystemSource(providerConfig *storage.Config) (storage.FilesystemSource, error) { return nil, errors.NotSupportedf("filesystems") } @@ -92,8 +135,13 @@ return true } +// DefaultPools implements storage.Provider. +func (p *cinderProvider) DefaultPools() []*storage.Config { + return nil +} + type cinderVolumeSource struct { - storageAdapter openstackStorage + storageAdapter OpenstackStorage envName string // non unique, informational only modelUUID string } @@ -169,7 +217,7 @@ } // listVolumes returns all of the volumes matching the given function. -func listVolumes(storageAdapter openstackStorage, match func(*cinder.Volume) bool) ([]storage.VolumeInfo, error) { +func listVolumes(storageAdapter OpenstackStorage, match func(*cinder.Volume) bool) ([]storage.VolumeInfo, error) { cinderVolumes, err := storageAdapter.GetVolumesDetail() if err != nil { return nil, err @@ -214,7 +262,7 @@ return destroyVolumes(s.storageAdapter, volumeIds), nil } -func destroyVolumes(storageAdapter openstackStorage, volumeIds []string) []error { +func destroyVolumes(storageAdapter OpenstackStorage, volumeIds []string) []error { var wg sync.WaitGroup wg.Add(len(volumeIds)) results := make([]error, len(volumeIds)) @@ -228,7 +276,7 @@ return results } -func destroyVolume(storageAdapter openstackStorage, volumeId string) error { +func destroyVolume(storageAdapter OpenstackStorage, volumeId string) error { logger.Debugf("destroying volume %q", volumeId) // Volumes must not be in-use when destroying. A volume may // still be in-use when the instance it is attached to is @@ -332,7 +380,7 @@ } func waitVolume( - storageAdapter openstackStorage, + storageAdapter OpenstackStorage, volumeId string, pred func(*cinder.Volume) (bool, error), ) (*cinder.Volume, error) { @@ -357,7 +405,7 @@ return detachVolumes(s.storageAdapter, args) } -func detachVolumes(storageAdapter openstackStorage, args []storage.VolumeAttachmentParams) ([]error, error) { +func detachVolumes(storageAdapter OpenstackStorage, args []storage.VolumeAttachmentParams) ([]error, error) { results := make([]error, len(args)) for i, arg := range args { // Check to see if the volume is already detached. @@ -390,7 +438,7 @@ } } -func detachVolume(instanceId, volumeId string, attachments []nova.VolumeAttachment, storageAdapter openstackStorage) error { +func detachVolume(instanceId, volumeId string, attachments []nova.VolumeAttachment, storageAdapter OpenstackStorage) error { // TODO(axw) verify whether we need to do this find step. From looking at the example // responses in the OpenStack docs, the "attachment ID" is always the same as the // volume ID. So we should just be able to issue a blind detach request, and then @@ -410,7 +458,7 @@ return nil } -type openstackStorage interface { +type OpenstackStorage interface { GetVolume(volumeId string) (*cinder.Volume, error) GetVolumesDetail() ([]cinder.Volume, error) DeleteVolume(volumeId string) error @@ -438,32 +486,6 @@ return url.Parse(endpoint) } -func newOpenstackStorageAdapter(environConfig *config.Config) (openstackStorage, error) { - ecfg, err := providerInstance.newConfig(environConfig) - if err != nil { - return nil, errors.Trace(err) - } - client, err := authClient(ecfg) - if err != nil { - return nil, errors.Trace(err) - } else if err := client.Authenticate(); err != nil { - return nil, errors.Trace(err) - } - - endpointUrl, err := getVolumeEndpointURL(client, ecfg.region()) - if err != nil { - if errors.IsNotFound(err) { - return nil, errors.NewNotSupported(err, "volumes not supported") - } - return nil, errors.Annotate(err, "getting volume endpoint") - } - - return &openstackStorageAdapter{ - cinderClient{cinder.Basic(endpointUrl, client.TenantId(), client.Token)}, - novaClient{nova.New(client)}, - }, nil -} - type openstackStorageAdapter struct { cinderClient novaClient @@ -477,7 +499,7 @@ *nova.Client } -// CreateVolume is part of the openstackStorage interface. +// CreateVolume is part of the OpenstackStorage interface. func (ga *openstackStorageAdapter) CreateVolume(args cinder.CreateVolumeVolumeParams) (*cinder.Volume, error) { resp, err := ga.cinderClient.CreateVolume(args) if err != nil { @@ -486,7 +508,7 @@ return &resp.Volume, nil } -// GetVolumesDetail is part of the openstackStorage interface. +// GetVolumesDetail is part of the OpenstackStorage interface. func (ga *openstackStorageAdapter) GetVolumesDetail() ([]cinder.Volume, error) { resp, err := ga.cinderClient.GetVolumesDetail() if err != nil { @@ -495,7 +517,7 @@ return resp.Volumes, nil } -// GetVolume is part of the openstackStorage interface. +// GetVolume is part of the OpenstackStorage interface. func (ga *openstackStorageAdapter) GetVolume(volumeId string) (*cinder.Volume, error) { resp, err := ga.cinderClient.GetVolume(volumeId) if err != nil { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/openstack/config.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/openstack/config.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/openstack/config.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/openstack/config.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,68 +5,14 @@ import ( "fmt" - "net/url" - "github.com/juju/errors" "github.com/juju/schema" - "gopkg.in/goose.v1/identity" "gopkg.in/juju/environschema.v1" "github.com/juju/juju/environs/config" ) var configSchema = environschema.Fields{ - "username": { - Description: "The user name to use when auth-mode is userpass.", - Type: environschema.Tstring, - EnvVars: identity.CredEnvUser, - Group: environschema.AccountGroup, - }, - "password": { - Description: "The password to use when auth-mode is userpass.", - Type: environschema.Tstring, - EnvVars: identity.CredEnvSecrets, - Group: environschema.AccountGroup, - Secret: true, - }, - "tenant-name": { - Description: "The openstack tenant name.", - Type: environschema.Tstring, - EnvVars: identity.CredEnvTenantName, - Group: environschema.AccountGroup, - }, - "auth-url": { - Description: "The keystone URL for authentication.", - Type: environschema.Tstring, - EnvVars: identity.CredEnvAuthURL, - Example: "https://yourkeystoneurl:443/v2.0/", - Group: environschema.AccountGroup, - }, - "auth-mode": { - Description: "The authentication mode to use. When set to keypair, the access-key and secret-key parameters should be set; when set to userpass or legacy, the username and password parameters should be set.", - Type: environschema.Tstring, - Values: []interface{}{AuthKeyPair, AuthLegacy, AuthUserPass}, - Group: environschema.AccountGroup, - }, - "access-key": { - Description: "The access key to use when auth-mode is set to keypair.", - Type: environschema.Tstring, - EnvVars: identity.CredEnvUser, - Group: environschema.AccountGroup, - Secret: true, - }, - "secret-key": { - Description: "The secret key to use when auth-mode is set to keypair.", - EnvVars: identity.CredEnvSecrets, - Group: environschema.AccountGroup, - Type: environschema.Tstring, - Secret: true, - }, - "region": { - Description: "The openstack region.", - Type: environschema.Tstring, - EnvVars: identity.CredEnvRegion, - }, "use-floating-ip": { Description: "Whether a floating IP address is required to give the nodes a public IP address. Some installations assign public IP addresses by default without requiring a floating IP address.", Type: environschema.Tbool, @@ -94,46 +40,6 @@ attrs map[string]interface{} } -func (c *environConfig) region() string { - return c.attrs["region"].(string) -} - -func (c *environConfig) username() string { - return c.attrs["username"].(string) -} - -func (c *environConfig) password() string { - return c.attrs["password"].(string) -} - -func (c *environConfig) tenantName() string { - return c.attrs["tenant-name"].(string) -} - -func (c *environConfig) domainName() string { - dname, ok := c.attrs["domain-name"] - if ok { - return dname.(string) - } - return "" -} - -func (c *environConfig) authURL() string { - return c.attrs["auth-url"].(string) -} - -func (c *environConfig) authMode() AuthMode { - return AuthMode(c.attrs["auth-mode"].(string)) -} - -func (c *environConfig) accessKey() string { - return c.attrs["access-key"].(string) -} - -func (c *environConfig) secretKey() string { - return c.attrs["secret-key"].(string) -} - func (c *environConfig) useFloatingIP() bool { return c.attrs["use-floating-ip"].(bool) } @@ -175,47 +81,6 @@ } ecfg := &environConfig{cfg, validated} - switch ecfg.authMode() { - case AuthUserPass, AuthLegacy: - if ecfg.username() == "" { - return nil, errors.NotValidf("missing username") - } - if ecfg.password() == "" { - return nil, errors.NotValidf("missing password") - } - case AuthKeyPair: - if ecfg.accessKey() == "" { - return nil, errors.NotValidf("missing access-key") - } - if ecfg.secretKey() == "" { - return nil, errors.NotValidf("missing secret-key") - } - default: - return nil, fmt.Errorf("unexpected authentication mode %q", ecfg.authMode()) - } - - if ecfg.authURL() == "" { - return nil, errors.NotValidf("missing auth-url") - } - if ecfg.tenantName() == "" { - return nil, errors.NotValidf("missing tenant-name") - } - if ecfg.region() == "" { - return nil, errors.NotValidf("missing region") - } - - parts, err := url.Parse(ecfg.authURL()) - if err != nil || parts.Host == "" || parts.Scheme == "" { - return nil, fmt.Errorf("invalid auth-url value %q", ecfg.authURL()) - } - - if old != nil { - attrs := old.UnknownAttrs() - if region, _ := attrs["region"].(string); ecfg.region() != region { - return nil, fmt.Errorf("cannot change region from %q to %q", region, ecfg.region()) - } - } - // Check for deprecated fields and log a warning. We also print to stderr to ensure the user sees the message // even if they are not running with --debug. cfgAttrs := cfg.AllAttrs() diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/openstack/config_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/openstack/config_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/openstack/config_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/openstack/config_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,8 +4,6 @@ package openstack import ( - "os" - jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" @@ -17,25 +15,6 @@ type ConfigSuite struct { testing.BaseSuite - savedVars map[string]string -} - -// Ensure any environment variables a user may have set locally are reset. -var envVars = map[string]string{ - "AWS_SECRET_ACCESS_KEY": "", - "EC2_SECRET_KEYS": "", - "NOVA_API_KEY": "", - "NOVA_PASSWORD": "", - "NOVA_PROJECT_ID": "", - "NOVA_REGION": "", - "NOVA_USERNAME": "", - "OS_ACCESS_KEY": "", - "OS_AUTH_URL": "", - "OS_PASSWORD": "", - "OS_REGION_NAME": "", - "OS_SECRET_KEY": "", - "OS_TENANT_NAME": "", - "OS_USERNAME": "", } var _ = gc.Suite(&ConfigSuite{}) @@ -49,18 +28,10 @@ config testing.Attrs change map[string]interface{} expect map[string]interface{} - envVars map[string]string region string useFloatingIP bool useDefaultSecurityGroup bool network string - username string - password string - tenantName string - authMode AuthMode - authURL string - accessKey string - secretKey string firewallMode string err string sslHostnameVerification bool @@ -68,19 +39,7 @@ blockStorageSource string } -var requiredConfig = testing.Attrs{ - "region": "configtest", - "auth-url": "http://auth", - "username": "user", - "password": "pass", - "tenant-name": "tenant", -} - -func restoreEnvVars(envVars map[string]string) { - for k, v := range envVars { - os.Setenv(k, v) - } -} +var requiredConfig = testing.Attrs{} func (t configTest) check(c *gc.C) { attrs := testing.FakeConfig().Merge(testing.Attrs{ @@ -90,17 +49,23 @@ cfg, err := config.New(config.NoDefaults, attrs) c.Assert(err, jc.ErrorIsNil) - // Set environment variables if any. - savedVars := make(map[string]string) - if t.envVars != nil { - for k, v := range t.envVars { - savedVars[k] = os.Getenv(k) - os.Setenv(k, v) - } + credential := cloud.NewCredential(cloud.UserPassAuthType, map[string]string{ + "username": "user", + "password": "secret", + "tenant-name": "sometenant", + }) + cloudSpec := environs.CloudSpec{ + Type: "openstack", + Name: "openstack", + Endpoint: "http://auth", + Region: "Configtest", + Credential: &credential, } - defer restoreEnvVars(savedVars) - e, err := environs.New(cfg) + e, err := environs.New(environs.OpenParams{ + Cloud: cloudSpec, + Config: cfg, + }) if t.change != nil { c.Assert(err, jc.ErrorIsNil) @@ -125,33 +90,6 @@ ecfg := e.(*Environ).ecfg() c.Assert(ecfg.Name(), gc.Equals, "testenv") - if t.region != "" { - c.Assert(ecfg.region(), gc.Equals, t.region) - } - if t.authMode != "" { - c.Assert(ecfg.authMode(), gc.Equals, t.authMode) - } - if t.accessKey != "" { - c.Assert(ecfg.accessKey(), gc.Equals, t.accessKey) - } - if t.secretKey != "" { - c.Assert(ecfg.secretKey(), gc.Equals, t.secretKey) - } - if t.username != "" { - c.Assert(ecfg.username(), gc.Equals, t.username) - c.Assert(ecfg.password(), gc.Equals, t.password) - c.Assert(ecfg.tenantName(), gc.Equals, t.tenantName) - c.Assert(ecfg.authURL(), gc.Equals, t.authURL) - expected := map[string]string{ - "username": t.username, - "password": t.password, - "tenant-name": t.tenantName, - } - c.Assert(err, jc.ErrorIsNil) - actual, err := e.Provider().SecretAttrs(ecfg.Config) - c.Assert(err, jc.ErrorIsNil) - c.Assert(expected, gc.DeepEquals, actual) - } if t.firewallMode != "" { c.Assert(ecfg.FirewallMode(), gc.Equals, t.firewallMode) } @@ -178,142 +116,11 @@ func (s *ConfigSuite) SetUpTest(c *gc.C) { s.BaseSuite.SetUpTest(c) - s.savedVars = make(map[string]string) - for v, val := range envVars { - s.savedVars[v] = os.Getenv(v) - os.Setenv(v, val) - } s.PatchValue(&authenticateClient, func(*Environ) error { return nil }) } -func (s *ConfigSuite) TearDownTest(c *gc.C) { - for k, v := range s.savedVars { - os.Setenv(k, v) - } - s.BaseSuite.TearDownTest(c) -} - var configTests = []configTest{ { - summary: "setting region", - config: requiredConfig.Merge(testing.Attrs{ - "region": "testreg", - }), - region: "testreg", - }, { - summary: "setting region (2)", - config: requiredConfig.Merge(testing.Attrs{ - "region": "configtest", - }), - region: "configtest", - }, { - summary: "changing region", - config: requiredConfig, - change: testing.Attrs{ - "region": "otherregion", - }, - err: `cannot change region from "configtest" to "otherregion"`, - }, { - summary: "invalid region", - config: requiredConfig.Merge(testing.Attrs{ - "region": 666, - }), - err: `.*expected string, got int\(666\)`, - }, { - summary: "missing region in model", - config: requiredConfig.Delete("region"), - err: "missing region not valid", - }, { - summary: "invalid username", - config: requiredConfig.Merge(testing.Attrs{ - "username": 666, - }), - err: `.*expected string, got int\(666\)`, - }, { - summary: "missing username in model", - config: requiredConfig.Delete("username"), - err: "missing username not valid", - }, { - summary: "invalid password", - config: requiredConfig.Merge(testing.Attrs{ - "password": 666, - }), - err: `.*expected string, got int\(666\)`, - }, { - summary: "missing password in model", - config: requiredConfig.Delete("password"), - err: "missing password not valid", - }, { - summary: "invalid tenant-name", - config: requiredConfig.Merge(testing.Attrs{ - "tenant-name": 666, - }), - err: `.*expected string, got int\(666\)`, - }, { - summary: "missing tenant in model", - config: requiredConfig.Delete("tenant-name"), - err: "missing tenant-name not valid", - }, { - summary: "invalid auth-url type", - config: requiredConfig.Merge(testing.Attrs{ - "auth-url": 666, - }), - err: `.*expected string, got int\(666\)`, - }, { - summary: "missing auth-url in model", - config: requiredConfig.Delete("auth-url"), - err: "missing auth-url not valid", - }, { - summary: "invalid authorization mode", - config: requiredConfig.Merge(testing.Attrs{ - "auth-mode": "invalid-mode", - }), - err: `auth-mode: expected one of \[keypair legacy userpass\], got "invalid-mode"`, - }, { - summary: "keypair authorization mode", - config: requiredConfig.Merge(testing.Attrs{ - "auth-mode": "keypair", - "access-key": "MyAccessKey", - "secret-key": "MySecretKey", - }), - authMode: "keypair", - accessKey: "MyAccessKey", - secretKey: "MySecretKey", - }, { - summary: "keypair authorization mode without access key", - config: requiredConfig.Merge(testing.Attrs{ - "auth-mode": "keypair", - "secret-key": "MySecretKey", - }), - err: "missing access-key not valid", - }, { - summary: "keypair authorization mode without secret key", - config: requiredConfig.Merge(testing.Attrs{ - "auth-mode": "keypair", - "access-key": "MyAccessKey", - }), - err: "missing secret-key not valid", - }, { - summary: "invalid auth-url format", - config: requiredConfig.Merge(testing.Attrs{ - "auth-url": "invalid", - }), - err: `invalid auth-url value "invalid"`, - }, { - summary: "valid auth args", - config: requiredConfig.Merge(testing.Attrs{ - "username": "jujuer", - "password": "open sesame", - "tenant-name": "juju tenant", - "auth-mode": "legacy", - "auth-url": "http://some.url/v2", - }), - username: "jujuer", - password: "open sesame", - tenantName: "juju tenant", - authURL: "http://some.url/v2", - authMode: AuthLegacy, - }, { summary: "default use floating ip", config: requiredConfig, // Do not use floating IP's by default. @@ -422,11 +229,6 @@ "type": "openstack", "default-image-id": "id-1234", "default-instance-type": "big", - "username": "u", - "password": "p", - "tenant-name": "t", - "region": "r", - "auth-url": "http://auth", }) cfg, err := config.New(config.NoDefaults, attrs) @@ -442,7 +244,7 @@ } } -func (s *ConfigSuite) TestBootstrapConfigSetsDefaultBlockSource(c *gc.C) { +func (s *ConfigSuite) TestPrepareConfigSetsDefaultBlockSource(c *gc.C) { attrs := testing.FakeConfig().Merge(testing.Attrs{ "type": "openstack", }) @@ -451,23 +253,28 @@ _, ok := cfg.StorageDefaultBlockSource() c.Assert(ok, jc.IsFalse) - cfg, err = providerInstance.BootstrapConfig(bootstrapConfigParams(cfg)) + cfg, err = providerInstance.PrepareConfig(prepareConfigParams(cfg)) c.Assert(err, jc.ErrorIsNil) source, ok := cfg.StorageDefaultBlockSource() c.Assert(ok, jc.IsTrue) c.Assert(source, gc.Equals, "cinder") } -func bootstrapConfigParams(cfg *config.Config) environs.BootstrapConfigParams { - return environs.BootstrapConfigParams{ +func prepareConfigParams(cfg *config.Config) environs.PrepareConfigParams { + credential := cloud.NewCredential(cloud.UserPassAuthType, map[string]string{ + "username": "user", + "password": "secret", + "tenant-name": "sometenant", + }) + return environs.PrepareConfigParams{ Config: cfg, - Credentials: cloud.NewCredential(cloud.UserPassAuthType, map[string]string{ - "username": "user", - "password": "secret", - "tenant-name": "sometenant", - }), - CloudRegion: "region", - CloudEndpoint: "http://auth", + Cloud: environs.CloudSpec{ + Type: "openstack", + Name: "canonistack", + Region: "region", + Endpoint: "http://auth", + Credential: &credential, + }, } } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/openstack/credentials.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/openstack/credentials.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/openstack/credentials.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/openstack/credentials.go 2016-08-16 08:56:25.000000000 +0000 @@ -17,6 +17,15 @@ "github.com/juju/juju/cloud" ) +const ( + credAttrTenantName = "tenant-name" + credAttrUserName = "username" + credAttrPassword = "password" + credAttrDomainName = "domain-name" + credAttrAccessKey = "access-key" + credAttrSecretKey = "secret-key" +) + type OpenstackCredentials struct{} // CredentialSchemas is part of the environs.ProviderCredentials interface. @@ -24,16 +33,16 @@ return map[cloud.AuthType]cloud.CredentialSchema{ cloud.UserPassAuthType: { { - "username", cloud.CredentialAttr{Description: "The username to authenticate with."}, + credAttrUserName, cloud.CredentialAttr{Description: "The username to authenticate with."}, }, { - "password", cloud.CredentialAttr{ + credAttrPassword, cloud.CredentialAttr{ Description: "The password for the specified username.", Hidden: true, }, }, { - "tenant-name", cloud.CredentialAttr{Description: "The OpenStack tenant name."}, + credAttrTenantName, cloud.CredentialAttr{Description: "The OpenStack tenant name."}, }, { - "domain-name", cloud.CredentialAttr{ + credAttrDomainName, cloud.CredentialAttr{ Description: "The OpenStack domain name.", Optional: true, }, @@ -41,14 +50,14 @@ }, cloud.AccessKeyAuthType: { { - "access-key", cloud.CredentialAttr{Description: "The access key to authenticate with."}, + credAttrAccessKey, cloud.CredentialAttr{Description: "The access key to authenticate with."}, }, { - "secret-key", cloud.CredentialAttr{ + credAttrSecretKey, cloud.CredentialAttr{ Description: "The secret key to authenticate with.", Hidden: true, }, }, { - "tenant-name", cloud.CredentialAttr{Description: "The OpenStack tenant name."}, + credAttrTenantName, cloud.CredentialAttr{Description: "The OpenStack tenant name."}, }, }, } @@ -116,19 +125,19 @@ credential = cloud.NewCredential( cloud.UserPassAuthType, map[string]string{ - "username": creds.User, - "password": creds.Secrets, - "tenant-name": creds.TenantName, - "domain-name": creds.DomainName, + credAttrUserName: creds.User, + credAttrPassword: creds.Secrets, + credAttrTenantName: creds.TenantName, + credAttrDomainName: creds.DomainName, }, ) } else { credential = cloud.NewCredential( cloud.AccessKeyAuthType, map[string]string{ - "access-key": creds.User, - "secret-key": creds.Secrets, - "tenant-name": creds.TenantName, + credAttrAccessKey: creds.User, + credAttrSecretKey: creds.Secrets, + credAttrTenantName: creds.TenantName, }, ) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/openstack/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/openstack/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/openstack/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/openstack/export_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -16,7 +16,6 @@ "github.com/juju/juju/constraints" "github.com/juju/juju/environs" - "github.com/juju/juju/environs/config" "github.com/juju/juju/environs/imagemetadata" "github.com/juju/juju/environs/instances" "github.com/juju/juju/environs/simplestreams" @@ -36,9 +35,10 @@ // MetadataStorage returns a Storage instance which is used to store simplestreams metadata for tests. func MetadataStorage(e environs.Environ) envstorage.Storage { - ecfg := e.(*Environ).ecfg() + env := e.(*Environ) + ecfg := env.ecfg() container := "juju-dist-test" - client, err := authClient(ecfg) + client, err := authClient(env.cloud, ecfg) if err != nil { panic(fmt.Errorf("cannot create %s container: %v", container, err)) } @@ -71,22 +71,13 @@ var ( NovaListAvailabilityZones = &novaListAvailabilityZones AvailabilityZoneAllocations = &availabilityZoneAllocations + NewOpenstackStorage = &newOpenstackStorage ) -type OpenstackStorage openstackStorage - -func NewCinderProvider(s OpenstackStorage) storage.Provider { - return &cinderProvider{ - func(*config.Config) (openstackStorage, error) { - return openstackStorage(s), nil - }, - } -} - func NewCinderVolumeSource(s OpenstackStorage) storage.VolumeSource { const envName = "testenv" modelUUID := testing.ModelTag.Id() - return &cinderVolumeSource{openstackStorage(s), envName, modelUUID} + return &cinderVolumeSource{s, envName, modelUUID} } // Include images for arches currently supported. i386 is no longer @@ -433,7 +424,7 @@ return findInstanceSpec(env, &instances.InstanceConstraint{ Series: series, Arches: []string{arch}, - Region: env.ecfg().region(), + Region: env.cloud.Region, Constraints: constraints.MustParse(cons), }, imageMetadata) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/openstack/init.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/openstack/init.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/openstack/init.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/openstack/init.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,7 +6,6 @@ import ( "github.com/juju/juju/environs" "github.com/juju/juju/environs/tools" - "github.com/juju/juju/storage/provider/registry" ) const ( @@ -18,13 +17,4 @@ environs.RegisterImageDataSourceFunc("keystone catalog", getKeystoneImageSource) tools.RegisterToolsDataSourceFunc("keystone catalog", getKeystoneToolsSource) - - // Register the Openstack specific providers. - registry.RegisterProvider( - CinderProviderType, - &cinderProvider{newOpenstackStorageAdapter}, - ) - - // Register the Cinder provider with the Openstack provider. - registry.RegisterEnvironStorageProviders(providerType, CinderProviderType) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/openstack/live_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/openstack/live_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/openstack/live_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/openstack/live_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -15,9 +15,6 @@ "gopkg.in/goose.v1/identity" "gopkg.in/goose.v1/nova" - "github.com/juju/juju/environs" - "github.com/juju/juju/environs/bootstrap" - "github.com/juju/juju/environs/config" "github.com/juju/juju/environs/jujutest" "github.com/juju/juju/environs/storage" envtesting "github.com/juju/juju/environs/testing" @@ -211,27 +208,15 @@ } func (s *LiveTests) assertStartInstanceDefaultSecurityGroup(c *gc.C, useDefault bool) { - attrs := s.TestConfig.Merge(coretesting.Attrs{ - "name": "sample-" + randomName(), + s.LiveTests.PatchValue(&s.TestConfig, s.TestConfig.Merge(coretesting.Attrs{ "use-default-secgroup": useDefault, - }) - cfg, err := config.New(config.NoDefaults, attrs) - c.Assert(err, jc.ErrorIsNil) - // Set up a test environment. - env, err := environs.New(cfg) - c.Assert(err, jc.ErrorIsNil) - c.Assert(env, gc.NotNil) - defer env.Destroy() - // Bootstrap and start an instance. - err = bootstrap.Bootstrap(envtesting.BootstrapContext(c), env, bootstrap.BootstrapParams{ - ControllerConfig: coretesting.FakeControllerConfig(), - AdminSecret: jujutesting.AdminSecret, - CAPrivateKey: coretesting.CAKey, - }) - c.Assert(err, jc.ErrorIsNil) - inst, _ := jujutesting.AssertStartInstance(c, env, s.ControllerUUID, "100") + })) + s.Destroy(c) + s.BootstrapOnce(c) + + inst, _ := jujutesting.AssertStartInstance(c, s.Env, s.ControllerUUID, "100") // Check whether the instance has the default security group assigned. - novaClient := openstack.GetNovaClient(env) + novaClient := openstack.GetNovaClient(s.Env) groups, err := novaClient.GetServerSecurityGroups(string(inst.Id())) c.Assert(err, jc.ErrorIsNil) defaultGroupFound := false diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/openstack/local_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/openstack/local_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/openstack/local_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/openstack/local_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -56,7 +56,6 @@ "github.com/juju/juju/provider/common" "github.com/juju/juju/provider/openstack" "github.com/juju/juju/status" - "github.com/juju/juju/storage/provider/registry" coretesting "github.com/juju/juju/testing" jujuversion "github.com/juju/juju/version" ) @@ -159,18 +158,8 @@ } func overrideCinderProvider(c *gc.C, s *gitjujutesting.CleanupSuite) { - // Override the cinder storage provider, since there is no test - // double for cinder. - old, err := registry.StorageProvider(openstack.CinderProviderType) - c.Assert(err, jc.ErrorIsNil) - registry.RegisterProvider(openstack.CinderProviderType, nil) - registry.RegisterProvider( - openstack.CinderProviderType, - openstack.NewCinderProvider(&mockAdapter{}), - ) - s.AddCleanup(func(*gc.C) { - registry.RegisterProvider(openstack.CinderProviderType, nil) - registry.RegisterProvider(openstack.CinderProviderType, old) + s.PatchValue(openstack.NewOpenstackStorage, func(*openstack.Environ) (openstack.OpenstackStorage, error) { + return &mockAdapter{}, nil }) } @@ -278,6 +267,17 @@ s.BaseSuite.TearDownTest(c) } +func (s *localServerSuite) openEnviron(c *gc.C, attrs coretesting.Attrs) environs.Environ { + cfg, err := config.New(config.NoDefaults, s.TestConfig.Merge(attrs)) + c.Assert(err, jc.ErrorIsNil) + env, err := environs.New(environs.OpenParams{ + Cloud: s.CloudSpec(), + Config: cfg, + }) + c.Assert(err, jc.ErrorIsNil) + return env +} + func (s *localServerSuite) TestBootstrap(c *gc.C) { // Tests uses Prepare, so destroy first. err := environs.Destroy(s.env.Config().Name(), s.env, s.ControllerStore) @@ -308,18 +308,8 @@ err := environs.Destroy(s.env.Config().Name(), s.env, s.ControllerStore) c.Assert(err, jc.ErrorIsNil) - // Create a config that matches s.TestConfig but with use-floating-ip set to true - cfg, err := config.New(config.NoDefaults, s.TestConfig.Merge(coretesting.Attrs{ - "use-floating-ip": true, - })) - c.Assert(err, jc.ErrorIsNil) - env, err := environs.New(cfg) - c.Assert(err, jc.ErrorIsNil) - err = bootstrap.Bootstrap(envtesting.BootstrapContext(c), env, bootstrap.BootstrapParams{ - ControllerConfig: coretesting.FakeControllerConfig(), - AdminSecret: testing.AdminSecret, - CAPrivateKey: coretesting.CAKey, - }) + env := s.openEnviron(c, coretesting.Attrs{"use-floating-ip": true}) + err = bootstrapEnv(c, env) c.Assert(err, gc.ErrorMatches, "(.|\n)*cannot allocate a public IP as needed(.|\n)*") } @@ -347,18 +337,8 @@ return nil }) - // Create a config that matches s.TestConfig but with use-floating-ip set to true - cfg, err := config.New(config.NoDefaults, s.TestConfig.Merge(coretesting.Attrs{ - "use-floating-ip": true, - })) - c.Assert(err, jc.ErrorIsNil) - env, err := environs.New(cfg) - c.Assert(err, jc.ErrorIsNil) - err = bootstrap.Bootstrap(envtesting.BootstrapContext(c), env, bootstrap.BootstrapParams{ - ControllerConfig: coretesting.FakeControllerConfig(), - AdminSecret: testing.AdminSecret, - CAPrivateKey: coretesting.CAKey, - }) + env := s.openEnviron(c, coretesting.Attrs{"use-floating-ip": true}) + err := bootstrapEnv(c, env) c.Assert(err, jc.ErrorIsNil) c.Assert(bootstrapFinished, jc.IsTrue) } @@ -385,17 +365,8 @@ return nil }) - cfg, err := config.New(config.NoDefaults, s.TestConfig.Merge(coretesting.Attrs{ - "use-floating-ip": false, - })) - c.Assert(err, jc.ErrorIsNil) - env, err := environs.New(cfg) - c.Assert(err, jc.ErrorIsNil) - err = bootstrap.Bootstrap(envtesting.BootstrapContext(c), env, bootstrap.BootstrapParams{ - ControllerConfig: coretesting.FakeControllerConfig(), - AdminSecret: testing.AdminSecret, - CAPrivateKey: coretesting.CAKey, - }) + env := s.openEnviron(c, coretesting.Attrs{"use-floating-ip": false}) + err := bootstrapEnv(c, env) c.Assert(err, jc.ErrorIsNil) c.Assert(bootstrapFinished, jc.IsTrue) } @@ -422,18 +393,9 @@ err := environs.Destroy(s.env.Config().Name(), s.env, s.ControllerStore) c.Assert(err, jc.ErrorIsNil) - attrs := s.TestConfig.Merge(coretesting.Attrs{"use-floating-ip": false}) - env, err := bootstrap.Prepare( - envtesting.BootstrapContext(c), - s.ControllerStore, - prepareParams(attrs, s.cred), - ) - c.Assert(err, jc.ErrorIsNil) - err = bootstrap.Bootstrap(envtesting.BootstrapContext(c), env, bootstrap.BootstrapParams{ - ControllerConfig: coretesting.FakeControllerConfig(), - AdminSecret: testing.AdminSecret, - CAPrivateKey: coretesting.CAKey, - }) + s.TestConfig["use-floating-ip"] = false + env := s.Prepare(c) + err = bootstrapEnv(c, env) c.Assert(err, jc.ErrorIsNil) inst, _ := testing.AssertStartInstance(c, env, s.ControllerUUID, "100") err = env.StopInstances(inst.Id()) @@ -456,11 +418,7 @@ c.Assert(err, jc.ErrorIsNil) env := s.Prepare(c) - err = bootstrap.Bootstrap(envtesting.BootstrapContext(c), env, bootstrap.BootstrapParams{ - ControllerConfig: coretesting.FakeControllerConfig(), - AdminSecret: testing.AdminSecret, - CAPrivateKey: coretesting.CAKey, - }) + err = bootstrapEnv(c, env) c.Assert(err, jc.ErrorIsNil) _, hc := testing.AssertStartInstanceWithConstraints(c, env, s.ControllerUUID, "100", constraints.MustParse("mem=1024")) c.Check(*hc.Arch, gc.Equals, "amd64") @@ -470,40 +428,43 @@ } func (s *localServerSuite) TestStartInstanceNetwork(c *gc.C) { - cfg, err := config.New(config.NoDefaults, s.TestConfig.Merge(coretesting.Attrs{ + cfg, err := s.env.Config().Apply(coretesting.Attrs{ // A label that corresponds to a nova test service network "network": "net", - })) + }) c.Assert(err, jc.ErrorIsNil) - env, err := environs.New(cfg) + err = s.env.SetConfig(cfg) c.Assert(err, jc.ErrorIsNil) - inst, _ := testing.AssertStartInstance(c, env, s.ControllerUUID, "100") - err = env.StopInstances(inst.Id()) + + inst, _ := testing.AssertStartInstance(c, s.env, s.ControllerUUID, "100") + err = s.env.StopInstances(inst.Id()) c.Assert(err, jc.ErrorIsNil) } func (s *localServerSuite) TestStartInstanceNetworkUnknownLabel(c *gc.C) { - cfg, err := config.New(config.NoDefaults, s.TestConfig.Merge(coretesting.Attrs{ + cfg, err := s.env.Config().Apply(coretesting.Attrs{ // A label that has no related network in the nova test service "network": "no-network-with-this-label", - })) + }) c.Assert(err, jc.ErrorIsNil) - env, err := environs.New(cfg) + err = s.env.SetConfig(cfg) c.Assert(err, jc.ErrorIsNil) - inst, _, _, err := testing.StartInstance(env, s.ControllerUUID, "100") + + inst, _, _, err := testing.StartInstance(s.env, s.ControllerUUID, "100") c.Check(inst, gc.IsNil) c.Assert(err, gc.ErrorMatches, "No networks exist with label .*") } func (s *localServerSuite) TestStartInstanceNetworkUnknownId(c *gc.C) { - cfg, err := config.New(config.NoDefaults, s.TestConfig.Merge(coretesting.Attrs{ + cfg, err := s.env.Config().Apply(coretesting.Attrs{ // A valid UUID but no related network in the nova test service "network": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6", - })) + }) c.Assert(err, jc.ErrorIsNil) - env, err := environs.New(cfg) + err = s.env.SetConfig(cfg) c.Assert(err, jc.ErrorIsNil) - inst, _, _, err := testing.StartInstance(env, s.ControllerUUID, "100") + + inst, _, _, err := testing.StartInstance(s.env, s.ControllerUUID, "100") c.Check(inst, gc.IsNil) c.Assert(err, gc.ErrorMatches, "cannot run instance: (\\n|.)*"+ "caused by: "+ @@ -552,11 +513,7 @@ } func (s *localServerSuite) TestStopInstance(c *gc.C) { - cfg, err := config.New(config.NoDefaults, s.TestConfig.Merge(coretesting.Attrs{ - "firewall-mode": config.FwInstance})) - c.Assert(err, jc.ErrorIsNil) - env, err := environs.New(cfg) - c.Assert(err, jc.ErrorIsNil) + env := s.openEnviron(c, coretesting.Attrs{"firewall-mode": config.FwInstance}) instanceName := "100" inst, _ := testing.AssertStartInstance(c, env, s.ControllerUUID, instanceName) // Openstack now has three security groups for the server, the default @@ -568,7 +525,7 @@ fmt.Sprintf("juju-%v-%v-%v", s.ControllerUUID, modelUUID, instanceName), } assertSecurityGroups(c, env, allSecurityGroups) - err = env.StopInstances(inst.Id()) + err := env.StopInstances(inst.Id()) c.Assert(err, jc.ErrorIsNil) // The security group for this instance is now removed. assertSecurityGroups(c, env, []string{ @@ -591,11 +548,7 @@ }, ) defer cleanup() - cfg, err := config.New(config.NoDefaults, s.TestConfig.Merge(coretesting.Attrs{ - "firewall-mode": config.FwInstance})) - c.Assert(err, jc.ErrorIsNil) - env, err := environs.New(cfg) - c.Assert(err, jc.ErrorIsNil) + env := s.openEnviron(c, coretesting.Attrs{"firewall-mode": config.FwInstance}) instanceName := "100" inst, _ := testing.AssertStartInstance(c, env, s.ControllerUUID, instanceName) modelUUID := env.Config().UUID() @@ -604,17 +557,13 @@ fmt.Sprintf("juju-%v-%v-%v", s.ControllerUUID, modelUUID, instanceName), } assertSecurityGroups(c, env, allSecurityGroups) - err = env.StopInstances(inst.Id()) + err := env.StopInstances(inst.Id()) c.Assert(err, jc.ErrorIsNil) assertSecurityGroups(c, env, allSecurityGroups) } func (s *localServerSuite) TestDestroyEnvironmentDeletesSecurityGroupsFWModeInstance(c *gc.C) { - cfg, err := config.New(config.NoDefaults, s.TestConfig.Merge(coretesting.Attrs{ - "firewall-mode": config.FwInstance})) - c.Assert(err, jc.ErrorIsNil) - env, err := environs.New(cfg) - c.Assert(err, jc.ErrorIsNil) + env := s.openEnviron(c, coretesting.Attrs{"firewall-mode": config.FwInstance}) instanceName := "100" testing.AssertStartInstance(c, env, s.ControllerUUID, instanceName) modelUUID := env.Config().UUID() @@ -623,17 +572,13 @@ fmt.Sprintf("juju-%v-%v-%v", s.ControllerUUID, modelUUID, instanceName), } assertSecurityGroups(c, env, allSecurityGroups) - err = env.Destroy() + err := env.Destroy() c.Check(err, jc.ErrorIsNil) assertSecurityGroups(c, env, []string{"default"}) } func (s *localServerSuite) TestDestroyEnvironmentDeletesSecurityGroupsFWModeGlobal(c *gc.C) { - cfg, err := config.New(config.NoDefaults, s.TestConfig.Merge(coretesting.Attrs{ - "firewall-mode": config.FwGlobal})) - c.Assert(err, jc.ErrorIsNil) - env, err := environs.New(cfg) - c.Assert(err, jc.ErrorIsNil) + env := s.openEnviron(c, coretesting.Attrs{"firewall-mode": config.FwGlobal}) instanceName := "100" testing.AssertStartInstance(c, env, s.ControllerUUID, instanceName) modelUUID := env.Config().UUID() @@ -642,22 +587,14 @@ fmt.Sprintf("juju-%v-%v-global", s.ControllerUUID, modelUUID), } assertSecurityGroups(c, env, allSecurityGroups) - err = env.Destroy() + err := env.Destroy() c.Check(err, jc.ErrorIsNil) assertSecurityGroups(c, env, []string{"default"}) } func (s *localServerSuite) TestDestroyController(c *gc.C) { - cfg, err := config.New(config.NoDefaults, s.TestConfig.Merge(coretesting.Attrs{ - "uuid": utils.MustNewUUID().String(), - })) - c.Assert(err, jc.ErrorIsNil) - env, err := environs.New(cfg) - c.Assert(err, jc.ErrorIsNil) - controllerCfg, err := config.New(config.NoDefaults, s.TestConfig) - c.Assert(err, jc.ErrorIsNil) - controllerEnv, err := environs.New(controllerCfg) - c.Assert(err, jc.ErrorIsNil) + env := s.openEnviron(c, coretesting.Attrs{"uuid": utils.MustNewUUID().String()}) + controllerEnv := s.env controllerInstanceName := "100" testing.AssertStartInstance(c, controllerEnv, s.ControllerUUID, controllerInstanceName) @@ -676,7 +613,7 @@ allControllerSecurityGroups, allHostedModelSecurityGroups..., )) - err = controllerEnv.DestroyController(s.ControllerUUID) + err := controllerEnv.DestroyController(s.ControllerUUID) c.Check(err, jc.ErrorIsNil) assertSecurityGroups(c, controllerEnv, []string{"default"}) assertInstanceIds(c, env) @@ -684,16 +621,8 @@ } func (s *localServerSuite) TestDestroyHostedModel(c *gc.C) { - cfg, err := config.New(config.NoDefaults, s.TestConfig.Merge(coretesting.Attrs{ - "uuid": utils.MustNewUUID().String(), - })) - c.Assert(err, jc.ErrorIsNil) - env, err := environs.New(cfg) - c.Assert(err, jc.ErrorIsNil) - controllerCfg, err := config.New(config.NoDefaults, s.TestConfig) - c.Assert(err, jc.ErrorIsNil) - controllerEnv, err := environs.New(controllerCfg) - c.Assert(err, jc.ErrorIsNil) + env := s.openEnviron(c, coretesting.Attrs{"uuid": utils.MustNewUUID().String()}) + controllerEnv := s.env controllerInstanceName := "100" controllerInstance, _ := testing.AssertStartInstance(c, controllerEnv, s.ControllerUUID, controllerInstanceName) @@ -712,7 +641,7 @@ allControllerSecurityGroups, allHostedModelSecurityGroups..., )) - err = env.Destroy() + err := env.Destroy() c.Check(err, jc.ErrorIsNil) assertSecurityGroups(c, controllerEnv, allControllerSecurityGroups) assertInstanceIds(c, env) @@ -775,13 +704,7 @@ } func (s *localServerSuite) TestAllInstancesFloatingIP(c *gc.C) { - // Create a config that matches s.TestConfig but with use-floating-ip - cfg, err := config.New(config.NoDefaults, s.TestConfig.Merge(coretesting.Attrs{ - "use-floating-ip": true, - })) - c.Assert(err, jc.ErrorIsNil) - env, err := environs.New(cfg) - c.Assert(err, jc.ErrorIsNil) + env := s.openEnviron(c, coretesting.Attrs{"use-floating-ip": true}) inst0, _ := testing.AssertStartInstance(c, env, s.ControllerUUID, "100") inst1, _ := testing.AssertStartInstance(c, env, s.ControllerUUID, "101") @@ -798,13 +721,7 @@ } func (s *localServerSuite) assertInstancesGathering(c *gc.C, withFloatingIP bool) { - // Create a config that matches s.TestConfig but with use-floating-ip - cfg, err := config.New(config.NoDefaults, s.TestConfig.Merge(coretesting.Attrs{ - "use-floating-ip": withFloatingIP, - })) - c.Assert(err, jc.ErrorIsNil) - env, err := environs.New(cfg) - c.Assert(err, jc.ErrorIsNil) + env := s.openEnviron(c, coretesting.Attrs{"use-floating-ip": withFloatingIP}) inst0, _ := testing.AssertStartInstance(c, env, s.ControllerUUID, "100") id0 := inst0.Id() @@ -951,11 +868,7 @@ // TODO (wallyworld) - this test was copied from the ec2 provider. // It should be moved to environs.jujutests.Tests. func (s *localServerSuite) TestBootstrapInstanceUserDataAndState(c *gc.C) { - err := bootstrap.Bootstrap(envtesting.BootstrapContext(c), s.env, bootstrap.BootstrapParams{ - ControllerConfig: coretesting.FakeControllerConfig(), - AdminSecret: testing.AdminSecret, - CAPrivateKey: coretesting.CAKey, - }) + err := bootstrapEnv(c, s.env) c.Assert(err, jc.ErrorIsNil) // Check that ControllerInstances returns the ID of the bootstrap machine. @@ -981,14 +894,12 @@ func (s *localServerSuite) assertGetImageMetadataSources(c *gc.C, stream, officialSourcePath string) { // Create a config that matches s.TestConfig but with the specified stream. - envAttrs := s.TestConfig + attrs := coretesting.Attrs{} if stream != "" { - envAttrs = envAttrs.Merge(coretesting.Attrs{"image-stream": stream}) + attrs = coretesting.Attrs{"image-stream": stream} } - cfg, err := config.New(config.NoDefaults, envAttrs) - c.Assert(err, jc.ErrorIsNil) - env, err := environs.New(cfg) - c.Assert(err, jc.ErrorIsNil) + env := s.openEnviron(c, attrs) + sources, err := environs.ImageMetadataSources(env) c.Assert(err, jc.ErrorIsNil) c.Assert(sources, gc.HasLen, 4) @@ -1047,13 +958,6 @@ c.Assert(err, jc.ErrorIsNil) } -func (s *localServerSuite) TestSupportedArchitectures(c *gc.C) { - env := s.Open(c, s.env.Config()) - a, err := env.SupportedArchitectures() - c.Assert(err, jc.ErrorIsNil) - c.Assert(a, jc.SameContents, []string{"amd64", "arm64", "ppc64el", "s390x"}) -} - func (s *localServerSuite) TestSupportsNetworking(c *gc.C) { env := s.Open(c, s.env.Config()) _, ok := environs.SupportsNetworking(env) @@ -1088,7 +992,7 @@ // data was created for it for the test. cons := constraints.MustParse("arch=i386") _, err = validator.Validate(cons) - c.Assert(err, gc.ErrorMatches, "invalid constraint value: arch=i386\nvalid values are:.*") + c.Assert(err, gc.ErrorMatches, "invalid constraint value: arch=i386\nvalid values are: \\[amd64 arm64 ppc64el s390x\\]") cons = constraints.MustParse("instance-type=foo") _, err = validator.Validate(cons) c.Assert(err, gc.ErrorMatches, "invalid constraint value: instance-type=foo\nvalid values are:.*") @@ -1386,7 +1290,10 @@ newattrs["ssl-hostname-verification"] = true cfg, err := config.New(config.NoDefaults, newattrs) c.Assert(err, jc.ErrorIsNil) - env, err := environs.New(cfg) + env, err := environs.New(environs.OpenParams{ + Cloud: makeCloudSpec(s.cred), + Config: cfg, + }) c.Assert(err, jc.ErrorIsNil) _, err = env.AllInstances() c.Assert(err, gc.ErrorMatches, "(.|\n)*x509: certificate signed by unknown authority") @@ -1406,11 +1313,7 @@ openstack.UseTestImageData(metadataStorage, s.cred) defer openstack.RemoveTestImageData(metadataStorage) - err = bootstrap.Bootstrap(envtesting.BootstrapContext(c), s.env, bootstrap.BootstrapParams{ - ControllerConfig: coretesting.FakeControllerConfig(), - AdminSecret: testing.AdminSecret, - CAPrivateKey: coretesting.CAKey, - }) + err = bootstrapEnv(c, s.env) c.Assert(err, jc.ErrorIsNil) } @@ -1537,11 +1440,7 @@ } func (s *localServerSuite) TestAllInstancesIgnoresOtherMachines(c *gc.C) { - err := bootstrap.Bootstrap(envtesting.BootstrapContext(c), s.env, bootstrap.BootstrapParams{ - ControllerConfig: coretesting.FakeControllerConfig(), - AdminSecret: testing.AdminSecret, - CAPrivateKey: coretesting.CAKey, - }) + err := bootstrapEnv(c, s.env) c.Assert(err, jc.ErrorIsNil) // Check that we see 1 instance in the environment @@ -1619,11 +1518,7 @@ } func (t *localServerSuite) testStartInstanceAvailZone(c *gc.C, zone string) (instance.Instance, error) { - err := bootstrap.Bootstrap(envtesting.BootstrapContext(c), t.env, bootstrap.BootstrapParams{ - ControllerConfig: coretesting.FakeControllerConfig(), - AdminSecret: testing.AdminSecret, - CAPrivateKey: coretesting.CAKey, - }) + err := bootstrapEnv(c, t.env) c.Assert(err, jc.ErrorIsNil) params := environs.StartInstanceParams{ @@ -1703,11 +1598,7 @@ } func (t *localServerSuite) TestStartInstanceDistributionParams(c *gc.C) { - err := bootstrap.Bootstrap(envtesting.BootstrapContext(c), t.env, bootstrap.BootstrapParams{ - ControllerConfig: coretesting.FakeControllerConfig(), - AdminSecret: testing.AdminSecret, - CAPrivateKey: coretesting.CAKey, - }) + err := bootstrapEnv(c, t.env) c.Assert(err, jc.ErrorIsNil) var mock mockAvailabilityZoneAllocations @@ -1731,11 +1622,7 @@ } func (t *localServerSuite) TestStartInstanceDistributionErrors(c *gc.C) { - err := bootstrap.Bootstrap(envtesting.BootstrapContext(c), t.env, bootstrap.BootstrapParams{ - ControllerConfig: coretesting.FakeControllerConfig(), - AdminSecret: testing.AdminSecret, - CAPrivateKey: coretesting.CAKey, - }) + err := bootstrapEnv(c, t.env) c.Assert(err, jc.ErrorIsNil) mock := mockAvailabilityZoneAllocations{ @@ -1758,11 +1645,7 @@ } func (t *localServerSuite) TestStartInstanceDistribution(c *gc.C) { - err := bootstrap.Bootstrap(envtesting.BootstrapContext(c), t.env, bootstrap.BootstrapParams{ - ControllerConfig: coretesting.FakeControllerConfig(), - AdminSecret: testing.AdminSecret, - CAPrivateKey: coretesting.CAKey, - }) + err := bootstrapEnv(c, t.env) c.Assert(err, jc.ErrorIsNil) // test-available is the only available AZ, so AvailabilityZoneAllocations @@ -1798,11 +1681,7 @@ }, ) - err := bootstrap.Bootstrap(envtesting.BootstrapContext(c), t.env, bootstrap.BootstrapParams{ - ControllerConfig: coretesting.FakeControllerConfig(), - AdminSecret: testing.AdminSecret, - CAPrivateKey: coretesting.CAKey, - }) + err := bootstrapEnv(c, t.env) c.Assert(err, jc.ErrorIsNil) cleanup := t.srv.Nova.RegisterControlPoint( @@ -1840,11 +1719,7 @@ }, ) - err := bootstrap.Bootstrap(envtesting.BootstrapContext(c), t.env, bootstrap.BootstrapParams{ - ControllerConfig: coretesting.FakeControllerConfig(), - AdminSecret: testing.AdminSecret, - CAPrivateKey: coretesting.CAKey, - }) + err := bootstrapEnv(c, t.env) c.Assert(err, jc.ErrorIsNil) cleanup := t.srv.Nova.RegisterControlPoint( @@ -1863,11 +1738,7 @@ } func (t *localServerSuite) TestStartInstanceDistributionAZNotImplemented(c *gc.C) { - err := bootstrap.Bootstrap(envtesting.BootstrapContext(c), t.env, bootstrap.BootstrapParams{ - ControllerConfig: coretesting.FakeControllerConfig(), - AdminSecret: testing.AdminSecret, - CAPrivateKey: coretesting.CAKey, - }) + err := bootstrapEnv(c, t.env) c.Assert(err, jc.ErrorIsNil) mock := mockAvailabilityZoneAllocations{ @@ -1881,11 +1752,7 @@ } func (t *localServerSuite) TestInstanceTags(c *gc.C) { - err := bootstrap.Bootstrap(envtesting.BootstrapContext(c), t.env, bootstrap.BootstrapParams{ - ControllerConfig: coretesting.FakeControllerConfig(), - AdminSecret: testing.AdminSecret, - CAPrivateKey: coretesting.CAKey, - }) + err := bootstrapEnv(c, t.env) c.Assert(err, jc.ErrorIsNil) instances, err := t.env.AllInstances() @@ -1904,11 +1771,7 @@ } func (t *localServerSuite) TestTagInstance(c *gc.C) { - err := bootstrap.Bootstrap(envtesting.BootstrapContext(c), t.env, bootstrap.BootstrapParams{ - ControllerConfig: coretesting.FakeControllerConfig(), - AdminSecret: testing.AdminSecret, - CAPrivateKey: coretesting.CAKey, - }) + err := bootstrapEnv(c, t.env) c.Assert(err, jc.ErrorIsNil) assertMetadata := func(extraKey, extraValue string) { @@ -1952,16 +1815,24 @@ func prepareParams(attrs map[string]interface{}, cred *identity.Credentials) bootstrap.PrepareParams { return bootstrap.PrepareParams{ ControllerConfig: coretesting.FakeControllerConfig(), - BaseConfig: attrs, + ModelConfig: attrs, ControllerName: attrs["name"].(string), - Credential: makeCredential(cred), - CloudName: "openstack", - CloudEndpoint: cred.URL, - CloudRegion: cred.Region, + Cloud: makeCloudSpec(cred), AdminSecret: testing.AdminSecret, } } +func makeCloudSpec(cred *identity.Credentials) environs.CloudSpec { + credential := makeCredential(cred) + return environs.CloudSpec{ + Type: "openstack", + Name: "openstack", + Endpoint: cred.URL, + Region: cred.Region, + Credential: &credential, + } +} + func makeCredential(cred *identity.Credentials) cloud.Credential { return cloud.NewCredential( cloud.UserPassAuthType, @@ -2072,3 +1943,11 @@ novaService.SetupHTTP(mux) return novaService } + +func bootstrapEnv(c *gc.C, env environs.Environ) error { + return bootstrap.Bootstrap(envtesting.BootstrapContext(c), env, bootstrap.BootstrapParams{ + ControllerConfig: coretesting.FakeControllerConfig(), + AdminSecret: testing.AdminSecret, + CAPrivateKey: coretesting.CAKey, + }) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/openstack/provider_configurator.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/openstack/provider_configurator.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/openstack/provider_configurator.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/openstack/provider_configurator.go 2016-08-16 08:56:25.000000000 +0000 @@ -40,14 +40,6 @@ // GetConfigDefaults implements ProviderConfigurator interface. func (c *defaultConfigurator) GetConfigDefaults() schema.Defaults { return schema.Defaults{ - "username": "", - "password": "", - "tenant-name": "", - "auth-url": "", - "auth-mode": string(AuthUserPass), - "access-key": "", - "secret-key": "", - "region": "", "use-floating-ip": false, "use-default-secgroup": false, "network": "", diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/openstack/provider.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/openstack/provider.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/openstack/provider.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/openstack/provider.go 2016-08-16 08:56:25.000000000 +0000 @@ -71,23 +71,28 @@ Delay: 200 * time.Millisecond, } -func (p EnvironProvider) Open(cfg *config.Config) (environs.Environ, error) { - logger.Infof("opening model %q", cfg.Name()) - e := new(Environ) +func (p EnvironProvider) Open(args environs.OpenParams) (environs.Environ, error) { + logger.Infof("opening model %q", args.Config.Name()) + if err := validateCloudSpec(args.Cloud); err != nil { + return nil, errors.Annotate(err, "validating cloud spec") + } + e := &Environ{ + name: args.Config.Name(), + cloud: args.Cloud, + } e.firewaller = p.FirewallerFactory.GetFirewaller(e) e.configurator = p.Configurator - err := e.SetConfig(cfg) + err := e.SetConfig(args.Config) if err != nil { return nil, err } - e.name = cfg.Name() return e, nil } // RestrictedConfigAttributes is specified in the EnvironProvider interface. func (p EnvironProvider) RestrictedConfigAttributes() []string { - return []string{"region", "auth-url", "auth-mode"} + return []string{} } // DetectRegions implements environs.CloudRegionDetector. @@ -107,37 +112,14 @@ }}, nil } -// PrepareForCreateEnvironment is specified in the EnvironProvider interface. -func (p EnvironProvider) PrepareForCreateEnvironment(controllerUUID string, cfg *config.Config) (*config.Config, error) { - return cfg, nil -} - -// BootstrapConfig is specified in the EnvironProvider interface. -func (p EnvironProvider) BootstrapConfig(args environs.BootstrapConfigParams) (*config.Config, error) { - // Add credentials to the configuration. - attrs := map[string]interface{}{ - "region": args.CloudRegion, - "auth-url": args.CloudEndpoint, - } - credentialAttrs := args.Credentials.Attributes() - switch authType := args.Credentials.AuthType(); authType { - case cloud.UserPassAuthType: - // TODO(axw) we need a way of saying to use legacy auth. - attrs["username"] = credentialAttrs["username"] - attrs["password"] = credentialAttrs["password"] - attrs["tenant-name"] = credentialAttrs["tenant-name"] - attrs["domain-name"] = credentialAttrs["domain-name"] - attrs["auth-mode"] = AuthUserPass - case cloud.AccessKeyAuthType: - attrs["access-key"] = credentialAttrs["access-key"] - attrs["secret-key"] = credentialAttrs["secret-key"] - attrs["tenant-name"] = credentialAttrs["tenant-name"] - attrs["auth-mode"] = AuthKeyPair - default: - return nil, errors.NotSupportedf("%q auth-type", authType) +// PrepareConfig is specified in the EnvironProvider interface. +func (p EnvironProvider) PrepareConfig(args environs.PrepareConfigParams) (*config.Config, error) { + if err := validateCloudSpec(args.Cloud); err != nil { + return nil, errors.Annotate(err, "validating cloud spec") } // Set the default block-storage source. + attrs := make(map[string]interface{}) if _, ok := args.Config.StorageDefaultBlockSource(); !ok { attrs[config.StorageDefaultBlockSourceKey] = CinderProviderType } @@ -146,23 +128,7 @@ if err != nil { return nil, errors.Trace(err) } - return p.PrepareForCreateEnvironment(args.ControllerUUID, cfg) -} - -// PrepareForBootstrap is specified in the EnvironProvider interface. -func (p EnvironProvider) PrepareForBootstrap( - ctx environs.BootstrapContext, - cfg *config.Config, -) (environs.Environ, error) { - e, err := p.Open(cfg) - if err != nil { - return nil, err - } - // Verify credentials. - if err := authenticateClient(e.(*Environ)); err != nil { - return nil, err - } - return e, nil + return cfg, nil } // MetadataLookupParams returns parameters which are used to query image metadata to @@ -178,15 +144,7 @@ } func (p EnvironProvider) SecretAttrs(cfg *config.Config) (map[string]string, error) { - m := make(map[string]string) - ecfg, err := p.newConfig(cfg) - if err != nil { - return nil, err - } - m["username"] = ecfg.username() - m["password"] = ecfg.password() - m["tenant-name"] = ecfg.tenantName() - return m, nil + return make(map[string]string), nil } func (p EnvironProvider) newConfig(cfg *config.Config) (*environConfig, error) { @@ -198,15 +156,14 @@ } type Environ struct { - common.SupportsUnitPlacementPolicy - - name string + name string + cloud environs.CloudSpec - // archMutex gates access to supportedArchitectures + // archMutex gates access to cachedSupportedArchitectures archMutex sync.Mutex - // supportedArchitectures caches the architectures + // cachedSupportedArchitectures caches the architectures // for which images can be instantiated. - supportedArchitectures []string + cachedSupportedArchitectures []string ecfgMutex sync.Mutex ecfgUnlocked *environConfig @@ -230,7 +187,7 @@ var _ environs.Environ = (*Environ)(nil) var _ simplestreams.HasRegion = (*Environ)(nil) var _ state.Prechecker = (*Environ)(nil) -var _ state.InstanceDistributor = (*Environ)(nil) +var _ instance.Distributor = (*Environ)(nil) var _ environs.InstanceTagger = (*Environ)(nil) type openstackInstance struct { @@ -409,26 +366,6 @@ return nova } -// SupportedArchitectures is specified on the EnvironCapability interface. -func (e *Environ) SupportedArchitectures() ([]string, error) { - e.archMutex.Lock() - defer e.archMutex.Unlock() - if e.supportedArchitectures != nil { - return e.supportedArchitectures, nil - } - // Create a filter to get all images from our region and for the correct stream. - cloudSpec, err := e.Region() - if err != nil { - return nil, err - } - imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{ - CloudSpec: cloudSpec, - Stream: e.Config().ImageStream(), - }) - e.supportedArchitectures, err = common.SupportedArchitectures(e, imageConstraint) - return e.supportedArchitectures, err -} - var unsupportedConstraints = []string{ constraints.Tags, constraints.CpuPower, @@ -441,7 +378,7 @@ []string{constraints.InstanceType}, []string{constraints.Mem, constraints.Arch, constraints.RootDisk, constraints.CpuCores}) validator.RegisterUnsupported(unsupportedConstraints) - supportedArches, err := e.SupportedArchitectures() + supportedArches, err := e.supportedArchitectures() if err != nil { return nil, err } @@ -460,6 +397,25 @@ return validator, nil } +func (e *Environ) supportedArchitectures() ([]string, error) { + e.archMutex.Lock() + defer e.archMutex.Unlock() + if e.cachedSupportedArchitectures != nil { + return e.cachedSupportedArchitectures, nil + } + // Create a filter to get all images from our region and for the correct stream. + cloudSpec, err := e.Region() + if err != nil { + return nil, err + } + imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{ + CloudSpec: cloudSpec, + Stream: e.Config().ImageStream(), + }) + e.cachedSupportedArchitectures, err = common.SupportedArchitectures(e, imageConstraint) + return e.cachedSupportedArchitectures, err +} + var novaListAvailabilityZones = (*nova.Client).ListAvailabilityZones type openstackAvailabilityZone struct { @@ -563,6 +519,26 @@ return errors.Errorf("invalid Openstack flavour %q specified", *cons.InstanceType) } +// PrepareForBootstrap is part of the Environ interface. +func (e *Environ) PrepareForBootstrap(ctx environs.BootstrapContext) error { + // Verify credentials. + if err := authenticateClient(e); err != nil { + return err + } + return nil +} + +// Create is part of the Environ interface. +func (e *Environ) Create(environs.CreateParams) error { + // Verify credentials. + if err := authenticateClient(e); err != nil { + return err + } + // TODO(axw) 2016-08-04 #1609643 + // Create global security group(s) here. + return nil +} + func (e *Environ) Bootstrap(ctx environs.BootstrapContext, args environs.BootstrapParams) (*environs.BootstrapResult, error) { // The client's authentication may have been reset when finding tools if the agent-version // attribute was updated so we need to re-authenticate. This will be a no-op if already authenticated. @@ -596,31 +572,32 @@ return e.ecfg().Config } -func newCredentials(ecfg *environConfig) (identity.Credentials, identity.AuthMode) { +func newCredentials(spec environs.CloudSpec) (identity.Credentials, identity.AuthMode) { + credAttrs := spec.Credential.Attributes() cred := identity.Credentials{ - User: ecfg.username(), - Secrets: ecfg.password(), - Region: ecfg.region(), - TenantName: ecfg.tenantName(), - URL: ecfg.authURL(), - DomainName: ecfg.domainName(), + Region: spec.Region, + URL: spec.Endpoint, + TenantName: credAttrs[credAttrTenantName], } - // authModeCfg has already been validated so we know it's one of the values below. + + // AuthType is validated when the environment is opened, so it's known + // to be one of these values. var authMode identity.AuthMode - switch AuthMode(ecfg.authMode()) { - case AuthLegacy: - authMode = identity.AuthLegacy - case AuthUserPass: + switch spec.Credential.AuthType() { + case cloud.UserPassAuthType: + // TODO(axw) we need a way of saying to use legacy auth. + cred.User = credAttrs[credAttrUserName] + cred.Secrets = credAttrs[credAttrPassword] + cred.DomainName = credAttrs[credAttrDomainName] authMode = identity.AuthUserPass if cred.DomainName != "" { authMode = identity.AuthUserPassV3 } - case AuthKeyPair: + case cloud.AccessKeyAuthType: + cred.User = credAttrs[credAttrAccessKey] + cred.Secrets = credAttrs[credAttrSecretKey] authMode = identity.AuthKeyPair - cred.User = ecfg.accessKey() - cred.Secrets = ecfg.secretKey() } - return cred, authMode } @@ -646,13 +623,13 @@ return client } -func authClient(ecfg *environConfig) (client.AuthenticatingClient, error) { +func authClient(spec environs.CloudSpec, ecfg *environConfig) (client.AuthenticatingClient, error) { - identityClientVersion, err := identityClientVersion(ecfg.authURL()) + identityClientVersion, err := identityClientVersion(spec.Endpoint) if err != nil { return nil, errors.Annotate(err, "cannot create a client") } - cred, authMode := newCredentials(ecfg) + cred, authMode := newCredentials(spec) newClient := client.NewClient if ecfg.SSLHostnameVerification() == false { @@ -704,7 +681,7 @@ defer e.ecfgMutex.Unlock() e.ecfgUnlocked = ecfg - client, err := authClient(ecfg) + client, err := authClient(e.cloud, ecfg) if err != nil { return errors.Annotate(err, "cannot set config") } @@ -722,7 +699,7 @@ } // The last part of the path should be the version #. // Example: https://keystone.foo:443/v3/ - logger.Debugf("authURL: %s", authURL) + logger.Tracef("authURL: %s", authURL) versionNumStr := url.Path[2:] if versionNumStr[len(versionNumStr)-1] == '/' { versionNumStr = versionNumStr[:len(versionNumStr)-1] @@ -914,7 +891,7 @@ series := args.Tools.OneSeries() arches := args.Tools.Arches() spec, err := findInstanceSpec(e, &instances.InstanceConstraint{ - Region: e.ecfg().region(), + Region: e.cloud.Region, Series: series, Arches: arches, Constraints: args.Constraints, @@ -1302,14 +1279,13 @@ } // Delete all volumes managed by the controller. - cfg := e.Config() - storageAdapter, err := newOpenstackStorageAdapter(cfg) + cinder, err := e.cinderProvider() if err == nil { - volIds, err := allControllerManagedVolumes(storageAdapter, controllerUUID) + volIds, err := allControllerManagedVolumes(cinder.storageAdapter, controllerUUID) if err != nil { return errors.Annotate(err, "listing volumes") } - errs := destroyVolumes(storageAdapter, volIds) + errs := destroyVolumes(cinder.storageAdapter, volIds) for i, err := range errs { if err == nil { continue @@ -1325,7 +1301,7 @@ return nil } -func allControllerManagedVolumes(storageAdapter openstackStorage, controllerUUID string) ([]string, error) { +func allControllerManagedVolumes(storageAdapter OpenstackStorage, controllerUUID string) ([]string, error) { volumes, err := listVolumes(storageAdapter, func(v *cinder.Volume) bool { return v.Metadata[tags.JujuController] == controllerUUID }) @@ -1424,7 +1400,7 @@ // MetadataLookupParams returns parameters which are used to query simplestreams metadata. func (e *Environ) MetadataLookupParams(region string) (*simplestreams.MetadataLookupParams, error) { if region == "" { - region = e.ecfg().region() + region = e.cloud.Region } cloudSpec, err := e.cloudSpec(region) if err != nil { @@ -1440,13 +1416,13 @@ // Region is specified in the HasRegion interface. func (e *Environ) Region() (simplestreams.CloudSpec, error) { - return e.cloudSpec(e.ecfg().region()) + return e.cloudSpec(e.cloud.Region) } func (e *Environ) cloudSpec(region string) (simplestreams.CloudSpec, error) { return simplestreams.CloudSpec{ Region: region, - Endpoint: e.ecfg().authURL(), + Endpoint: e.cloud.Endpoint, }, nil } @@ -1457,3 +1433,30 @@ } return nil } + +func validateCloudSpec(spec environs.CloudSpec) error { + if err := spec.Validate(); err != nil { + return errors.Trace(err) + } + if err := validateAuthURL(spec.Endpoint); err != nil { + return errors.Annotate(err, "validating auth-url") + } + if spec.Credential == nil { + return errors.NotValidf("missing credential") + } + switch authType := spec.Credential.AuthType(); authType { + case cloud.UserPassAuthType: + case cloud.AccessKeyAuthType: + default: + return errors.NotSupportedf("%q auth-type", authType) + } + return nil +} + +func validateAuthURL(authURL string) error { + parts, err := url.Parse(authURL) + if err != nil || parts.Host == "" || parts.Scheme == "" { + return errors.NotValidf("auth-url %q", authURL) + } + return nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/rackspace/environ_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/rackspace/environ_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/rackspace/environ_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/rackspace/environ_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,6 +9,7 @@ gc "gopkg.in/check.v1" + "github.com/juju/errors" "github.com/juju/juju/cloudconfig/instancecfg" "github.com/juju/juju/constraints" "github.com/juju/juju/environs" @@ -18,6 +19,7 @@ "github.com/juju/juju/provider/common" "github.com/juju/juju/provider/rackspace" "github.com/juju/juju/status" + "github.com/juju/juju/storage" "github.com/juju/juju/testing" "github.com/juju/juju/tools" "github.com/juju/utils/ssh" @@ -107,6 +109,16 @@ return nil, nil } +func (e *fakeEnviron) Create(args environs.CreateParams) error { + e.Push("Create", args) + return nil +} + +func (e *fakeEnviron) PrepareForBootstrap(ctx environs.BootstrapContext) error { + e.Push("PrepareForBootstrap", ctx) + return nil +} + func (e *fakeEnviron) Bootstrap(ctx environs.BootstrapContext, params environs.BootstrapParams) (*environs.BootstrapResult, error) { e.Push("Bootstrap", ctx, params) return nil, nil @@ -138,16 +150,6 @@ return e.config } -func (e *fakeEnviron) SupportedArchitectures() ([]string, error) { - e.Push("SupportedArchitectures") - return nil, nil -} - -func (e *fakeEnviron) SupportsUnitPlacement() error { - e.Push("SupportsUnitPlacement") - return nil -} - func (e *fakeEnviron) ConstraintsValidator() (constraints.Validator, error) { e.Push("ConstraintsValidator") return nil, nil @@ -203,6 +205,16 @@ return nil } +func (e *fakeEnviron) StorageProviderTypes() []storage.ProviderType { + e.Push("StorageProviderTypes") + return nil +} + +func (e *fakeEnviron) StorageProvider(t storage.ProviderType) (storage.Provider, error) { + e.Push("StorageProvider", t) + return nil, errors.NotImplementedf("StorageProvider") +} + type fakeConfigurator struct { methodCalls []methodCall } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/rackspace/init.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/rackspace/init.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/rackspace/init.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/rackspace/init.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,7 +6,6 @@ import ( "github.com/juju/juju/environs" "github.com/juju/juju/provider/openstack" - "github.com/juju/juju/storage/provider/registry" ) const ( @@ -23,6 +22,4 @@ osProvider, } environs.RegisterProvider(providerType, providerInstance) - - registry.RegisterEnvironStorageProviders(providerType, openstack.CinderProviderType) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/rackspace/provider_configurator.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/rackspace/provider_configurator.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/rackspace/provider_configurator.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/rackspace/provider_configurator.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,7 +10,6 @@ "github.com/juju/juju/cloudconfig/cloudinit" "github.com/juju/juju/environs" - "github.com/juju/juju/provider/openstack" ) type rackspaceConfigurator struct { @@ -38,14 +37,6 @@ // GetConfigDefaults implements ProviderConfigurator interface. func (c *rackspaceConfigurator) GetConfigDefaults() schema.Defaults { return schema.Defaults{ - "username": "", - "password": "", - "tenant-name": "", - "auth-url": "https://identity.api.rackspacecloud.com/v2.0", - "auth-mode": string(openstack.AuthUserPass), - "access-key": "", - "secret-key": "", - "region": "", "use-floating-ip": false, "use-default-secgroup": false, "network": "", diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/rackspace/provider.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/rackspace/provider.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/rackspace/provider.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/rackspace/provider.go 2016-08-16 08:56:25.000000000 +0000 @@ -16,11 +16,22 @@ var providerInstance *environProvider -// BootstrapConfig is specified in the EnvironProvider interface. -func (p *environProvider) BootstrapConfig(args environs.BootstrapConfigParams) (*config.Config, error) { +// PrepareConfig is part of the EnvironProvider interface. +func (p *environProvider) PrepareConfig(args environs.PrepareConfigParams) (*config.Config, error) { + args.Cloud = transformCloudSpec(args.Cloud) + return p.EnvironProvider.PrepareConfig(args) +} + +// Open is part of the EnvironProvider interface. +func (p *environProvider) Open(args environs.OpenParams) (environs.Environ, error) { + args.Cloud = transformCloudSpec(args.Cloud) + return p.EnvironProvider.Open(args) +} + +func transformCloudSpec(spec environs.CloudSpec) environs.CloudSpec { // Rackspace regions are expected to be uppercase, but Juju // stores and displays them in lowercase in the CLI. Ensure // they're uppercase when they get to the Rackspace API. - args.CloudRegion = strings.ToUpper(args.CloudRegion) - return p.EnvironProvider.BootstrapConfig(args) + spec.Region = strings.ToUpper(spec.Region) + return spec } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/rackspace/provider_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/rackspace/provider_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/rackspace/provider_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/rackspace/provider_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -41,14 +41,18 @@ s.innerProvider.CheckCallNames(c, "Validate") } -func (s *providerSuite) TestBootstrapConfig(c *gc.C) { - args := environs.BootstrapConfigParams{CloudRegion: "dfw"} - s.provider.BootstrapConfig(args) +func (s *providerSuite) TestPrepareConfig(c *gc.C) { + args := environs.PrepareConfigParams{ + Cloud: environs.CloudSpec{ + Region: "dfw", + }, + } + s.provider.PrepareConfig(args) expect := args - expect.CloudRegion = "DFW" + expect.Cloud.Region = "DFW" s.innerProvider.CheckCalls(c, []testing.StubCall{ - {"BootstrapConfig", []interface{}{expect}}, + {"PrepareConfig", []interface{}{expect}}, }) } @@ -56,8 +60,8 @@ testing.Stub } -func (p *fakeProvider) Open(cfg *config.Config) (environs.Environ, error) { - p.MethodCall(p, "Open", cfg) +func (p *fakeProvider) Open(args environs.OpenParams) (environs.Environ, error) { + p.MethodCall(p, "Open", args) return nil, nil } @@ -71,8 +75,8 @@ return nil, nil } -func (p *fakeProvider) BootstrapConfig(args environs.BootstrapConfigParams) (*config.Config, error) { - p.MethodCall(p, "BootstrapConfig", args) +func (p *fakeProvider) PrepareConfig(args environs.PrepareConfigParams) (*config.Config, error) { + p.MethodCall(p, "PrepareConfig", args) return nil, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/client.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/client.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/client.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/client.go 2016-08-16 08:56:25.000000000 +0000 @@ -19,6 +19,7 @@ "github.com/juju/govmomi/vim25/mo" "golang.org/x/net/context" + "github.com/juju/juju/environs" "github.com/juju/juju/instance" "github.com/juju/juju/network" "github.com/juju/juju/provider/common" @@ -38,17 +39,24 @@ recurser *list.Recurser } -var newClient = func(ecfg *environConfig) (*client, error) { - url, err := ecfg.url() - if err != nil { - return nil, err +var newClient = func(cloud environs.CloudSpec) (*client, error) { + + credAttrs := cloud.Credential.Attributes() + username := credAttrs[credAttrUser] + password := credAttrs[credAttrPassword] + connURL := &url.URL{ + Scheme: "https", + User: url.UserPassword(username, password), + Host: cloud.Endpoint, + Path: "/sdk", } - connection, err := newConnection(url) + + connection, err := newConnection(connURL) if err != nil { return nil, errors.Trace(err) } finder := find.NewFinder(connection.Client, true) - datacenter, err := finder.Datacenter(context.TODO(), ecfg.datacenter()) + datacenter, err := finder.Datacenter(context.TODO(), cloud.Region) if err != nil { return nil, errors.Trace(err) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/config.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/config.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/config.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/config.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,9 +6,6 @@ package vsphere import ( - "fmt" - "net/url" - "github.com/juju/errors" "github.com/juju/schema" @@ -17,41 +14,23 @@ // The vmware-specific config keys. const ( - cfgDatacenter = "datacenter" - cfgHost = "host" - cfgUser = "user" - cfgPassword = "password" cfgExternalNetwork = "external-network" ) // configFields is the spec for each vmware config value's type. -var configFields = schema.Fields{ - cfgHost: schema.String(), - cfgUser: schema.String(), - cfgPassword: schema.String(), - cfgDatacenter: schema.String(), - cfgExternalNetwork: schema.String(), -} - -var requiredFields = []string{ - cfgHost, - cfgUser, - cfgPassword, - cfgDatacenter, -} +var ( + configFields = schema.Fields{ + cfgExternalNetwork: schema.String(), + } -var configDefaults = schema.Defaults{ - cfgExternalNetwork: "", -} + requiredFields = []string{} -var configSecretFields = []string{ - cfgPassword, -} + configDefaults = schema.Defaults{ + cfgExternalNetwork: "", + } -var configImmutableFields = []string{ - cfgHost, - cfgDatacenter, -} + configImmutableFields = []string{} +) type environConfig struct { *config.Config @@ -96,39 +75,10 @@ return ecfg, nil } -func (c *environConfig) datacenter() string { - return c.attrs[cfgDatacenter].(string) -} - -func (c *environConfig) host() string { - return c.attrs[cfgHost].(string) -} - -func (c *environConfig) user() string { - return c.attrs[cfgUser].(string) -} - -func (c *environConfig) password() string { - return c.attrs[cfgPassword].(string) -} - func (c *environConfig) externalNetwork() string { return c.attrs[cfgExternalNetwork].(string) } -func (c *environConfig) url() (*url.URL, error) { - return url.Parse(fmt.Sprintf("https://%s:%s@%s/sdk", c.user(), c.password(), c.host())) -} - -// secret gathers the "secret" config values and returns them. -func (c *environConfig) secret() map[string]string { - secretAttrs := make(map[string]string, len(configSecretFields)) - for _, key := range configSecretFields { - secretAttrs[key] = c.attrs[key].(string) - } - return secretAttrs -} - // validate checks vmware-specific config values. func (c environConfig) validate() error { // All fields must be populated, even with just the default. @@ -137,10 +87,6 @@ return errors.Errorf("%s: must not be empty", field) } } - if _, err := c.url(); err != nil { - return errors.Trace(err) - } - return nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/config_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/config_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/config_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/config_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -26,7 +26,7 @@ func (s *ConfigSuite) SetUpTest(c *gc.C) { s.BaseSuite.SetUpTest(c) - cfg, err := testing.ModelConfig(c).Apply(vsphere.ConfigAttrs) + cfg, err := testing.ModelConfig(c).Apply(vsphere.ConfigAttrs()) c.Assert(err, jc.ErrorIsNil) s.config = cfg } @@ -75,7 +75,7 @@ } func (ts configTestSpec) attrs() testing.Attrs { - return vsphere.ConfigAttrs.Merge(ts.insert).Delete(ts.remove...) + return vsphere.ConfigAttrs().Merge(ts.insert).Delete(ts.remove...) } func (ts configTestSpec) newConfig(c *gc.C) *config.Config { @@ -86,38 +86,6 @@ } var newConfigTests = []configTestSpec{{ - info: "datacenter is required", - remove: []string{"datacenter"}, - err: "datacenter: expected string, got nothing", -}, { - info: "datacenter cannot be empty", - insert: testing.Attrs{"datacenter": ""}, - err: "datacenter: must not be empty", -}, { - info: "host is required", - remove: []string{"host"}, - err: "host: expected string, got nothing", -}, { - info: "host cannot be empty", - insert: testing.Attrs{"host": ""}, - err: "host: must not be empty", -}, { - info: "user is required", - remove: []string{"user"}, - err: "user: expected string, got nothing", -}, { - info: "user cannot be empty", - insert: testing.Attrs{"user": ""}, - err: "user: must not be empty", -}, { - info: "password is required", - remove: []string{"password"}, - err: "password: expected string, got nothing", -}, { - info: "password cannot be empty", - insert: testing.Attrs{"password": ""}, - err: "password: must not be empty", -}, { info: "unknown field is not touched", insert: testing.Attrs{"unknown-field": "12345"}, expect: testing.Attrs{"unknown-field": "12345"}, @@ -128,7 +96,10 @@ c.Logf("test %d: %s", i, test.info) testConfig := test.newConfig(c) - environ, err := environs.New(testConfig) + environ, err := environs.New(environs.OpenParams{ + Cloud: vsphere.FakeCloudSpec(), + Config: testConfig, + }) // Check the result if test.err != "" { @@ -162,7 +133,7 @@ oldcfg := test.newConfig(c) newcfg := s.config - expected := vsphere.ConfigAttrs + expected := vsphere.ConfigAttrs() // Validate the new config (relative to the old one) using the // provider. @@ -189,23 +160,7 @@ var changeConfigTests = []configTestSpec{{ info: "no change, no error", - expect: vsphere.ConfigAttrs, -}, { - info: "cannot change datacenter", - insert: testing.Attrs{"datacenter": "/datacenter2"}, - err: "datacenter: cannot change from /datacenter1 to /datacenter2", -}, { - info: "cannot change host", - insert: testing.Attrs{"host": "host2"}, - err: "host: cannot change from host1 to host2", -}, { - info: "cannot change user", - insert: testing.Attrs{"user": "user2"}, - expect: testing.Attrs{"user": "user2"}, -}, { - info: "cannot change password", - insert: testing.Attrs{"password": "password2"}, - expect: testing.Attrs{"password": "password2"}, + expect: vsphere.ConfigAttrs(), }, { info: "can insert unknown field", insert: testing.Attrs{"unknown": "ignoti"}, @@ -232,7 +187,10 @@ for i, test := range changeConfigTests { c.Logf("test %d: %s", i, test.info) - environ, err := environs.New(s.config) + environ, err := environs.New(environs.OpenParams{ + Cloud: vsphere.FakeCloudSpec(), + Config: s.config, + }) c.Assert(err, jc.ErrorIsNil) testConfig := test.newConfig(c) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/credentials.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/credentials.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/credentials.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/credentials.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,6 +11,11 @@ "github.com/juju/juju/cloud" ) +const ( + credAttrUser = "user" + credAttrPassword = "password" +) + type environProviderCredentials struct{} // CredentialSchemas is part of the environs.ProviderCredentials interface. @@ -18,9 +23,9 @@ return map[cloud.AuthType]cloud.CredentialSchema{ cloud.UserPassAuthType: { { - "user", cloud.CredentialAttr{Description: "The username to authenticate with."}, + credAttrUser, cloud.CredentialAttr{Description: "The username to authenticate with."}, }, { - "password", cloud.CredentialAttr{ + credAttrPassword, cloud.CredentialAttr{ Description: "The password to authenticate with.", Hidden: true, }, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/environ.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/environ.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/environ.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/environ.go 2016-08-16 08:56:25.000000000 +0000 @@ -22,9 +22,8 @@ // Note: This provider/environment does *not* implement storage. type environ struct { - common.SupportsUnitPlacementPolicy - name string + cloud environs.CloudSpec client *client archLock sync.Mutex // archLock protects access to the following fields. @@ -37,13 +36,13 @@ ecfg *environConfig } -func newEnviron(cfg *config.Config) (*environ, error) { +func newEnviron(cloud environs.CloudSpec, cfg *config.Config) (*environ, error) { ecfg, err := newValidConfig(cfg, configDefaults) if err != nil { return nil, errors.Annotate(err, "invalid config") } - client, err := newClient(ecfg) + client, err := newClient(cloud) if err != nil { return nil, errors.Annotatef(err, "failed to create new client") } @@ -55,6 +54,7 @@ env := &environ{ name: ecfg.Name(), + cloud: cloud, ecfg: ecfg, client: client, namespace: namespace, @@ -95,6 +95,16 @@ return cfg } +// PrepareForBootstrap implements environs.Environ. +func (env *environ) PrepareForBootstrap(ctx environs.BootstrapContext) error { + return nil +} + +// Create implements environs.Environ. +func (env *environ) Create(environs.CreateParams) error { + return nil +} + //this variable is exported, because it has to be rewritten in external unit tests var Bootstrap = common.Bootstrap diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/environ_network.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/environ_network.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/environ_network.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/environ_network.go 2016-08-16 08:56:25.000000000 +0000 @@ -49,6 +49,12 @@ return nil, errors.Trace(errors.NotSupportedf("Ports")) } +// AllocateContainerAddresses implements environs.Networking. func (e *environ) AllocateContainerAddresses(hostInstanceID instance.Id, containerTag names.MachineTag, preparedInfo []network.InterfaceInfo) ([]network.InterfaceInfo, error) { return nil, errors.NotSupportedf("container address allocation") } + +// ReleaseContainerAddresses implements environs.Networking. +func (e *environ) ReleaseContainerAddresses(interfaces []network.InterfaceInfo) error { + return errors.NotSupportedf("container address allocation") +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/environ_policy.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/environ_policy.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/environ_policy.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/environ_policy.go 2016-08-16 08:56:25.000000000 +0000 @@ -26,9 +26,7 @@ return nil } -// SupportedArchitectures returns the image architectures which can -// be hosted by this environment. -func (env *environ) SupportedArchitectures() ([]string, error) { +func (env *environ) getSupportedArchitectures() ([]string, error) { env.archLock.Lock() defer env.archLock.Unlock() @@ -83,7 +81,7 @@ // vocab - supportedArches, err := env.SupportedArchitectures() + supportedArches, err := env.getSupportedArchitectures() if err != nil { return nil, errors.Trace(err) } @@ -92,8 +90,6 @@ return validator, nil } -// SupportsUnitPlacement implement via common.SupportsUnitPlacementPolicy - // SupportNetworks returns whether the environment has support to // specify networks for applications and machines. func (env *environ) SupportNetworks() bool { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/environ_policy_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/environ_policy_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/environ_policy_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/environ_policy_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,7 +7,6 @@ import ( jc "github.com/juju/testing/checkers" - "github.com/juju/utils/arch" gc "gopkg.in/check.v1" "github.com/juju/juju/constraints" @@ -20,13 +19,6 @@ var _ = gc.Suite(&environPolSuite{}) -func (s *environPolSuite) TestSupportedArchitectures(c *gc.C) { - archList, err := s.Env.SupportedArchitectures() - c.Assert(err, jc.ErrorIsNil) - - c.Check(archList, jc.SameContents, []string{arch.AMD64}) -} - func (s *environPolSuite) TestConstraintsValidator(c *gc.C) { validator, err := s.Env.ConstraintsValidator() c.Assert(err, jc.ErrorIsNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/environ_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/environ_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/environ_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/environ_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,9 +9,11 @@ "os" "github.com/juju/errors" + jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" "github.com/juju/juju/environs" + envtesting "github.com/juju/juju/environs/testing" "github.com/juju/juju/feature" "github.com/juju/juju/juju/osenv" "github.com/juju/juju/provider/vsphere" @@ -46,3 +48,8 @@ err := s.Env.Destroy() c.Assert(err, gc.ErrorMatches, "Destroy called") } + +func (s *environSuite) TestPrepareForBootstrap(c *gc.C) { + err := s.Env.PrepareForBootstrap(envtesting.BootstrapContext(c)) + c.Check(err, jc.ErrorIsNil) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/init.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/init.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/init.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/init.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,10 +5,7 @@ package vsphere -import ( - "github.com/juju/juju/environs" - "github.com/juju/juju/storage/provider/registry" -) +import "github.com/juju/juju/environs" const ( providerType = "vsphere" @@ -16,5 +13,4 @@ func init() { environs.RegisterProvider(providerType, providerInstance) - registry.RegisterEnvironStorageProviders(providerType) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/provider.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/provider.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/provider.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/provider.go 2016-08-16 08:56:25.000000000 +0000 @@ -24,54 +24,25 @@ var logger = loggo.GetLogger("juju.provider.vmware") // Open implements environs.EnvironProvider. -func (environProvider) Open(cfg *config.Config) (environs.Environ, error) { - env, err := newEnviron(cfg) - return env, errors.Trace(err) -} - -// BootstrapConfig implements environs.EnvironProvider. -func (p environProvider) BootstrapConfig(args environs.BootstrapConfigParams) (*config.Config, error) { - cfg := args.Config - switch authType := args.Credentials.AuthType(); authType { - case cloud.UserPassAuthType: - credentialAttrs := args.Credentials.Attributes() - var err error - cfg, err = cfg.Apply(map[string]interface{}{ - cfgUser: credentialAttrs["user"], - cfgPassword: credentialAttrs["password"], - }) - if err != nil { - return nil, errors.Trace(err) - } - default: - return nil, errors.NotSupportedf("%q auth-type", authType) +func (environProvider) Open(args environs.OpenParams) (environs.Environ, error) { + if err := validateCloudSpec(args.Cloud); err != nil { + return nil, errors.Annotate(err, "validating cloud spec") } - return p.PrepareForCreateEnvironment(args.ControllerUUID, cfg) + env, err := newEnviron(args.Cloud, args.Config) + return env, errors.Trace(err) } -// PrepareForBootstrap implements environs.EnvironProvider. -func (p environProvider) PrepareForBootstrap(ctx environs.BootstrapContext, cfg *config.Config) (environs.Environ, error) { - env, err := newEnviron(cfg) - if err != nil { - return nil, errors.Trace(err) +// PrepareConfig implements environs.EnvironProvider. +func (p environProvider) PrepareConfig(args environs.PrepareConfigParams) (*config.Config, error) { + if err := validateCloudSpec(args.Cloud); err != nil { + return nil, errors.Annotate(err, "validating cloud spec") } - - return env, nil -} - -// PrepareForCreateEnvironment is specified in the EnvironProvider interface. -func (environProvider) PrepareForCreateEnvironment(controllerUUID string, cfg *config.Config) (*config.Config, error) { - return cfg, nil + return args.Config, nil } // RestrictedConfigAttributes is specified in the EnvironProvider interface. func (environProvider) RestrictedConfigAttributes() []string { - return []string{ - cfgDatacenter, - cfgHost, - cfgUser, - cfgPassword, - } + return []string{} } // Validate implements environs.EnvironProvider. @@ -99,10 +70,19 @@ // SecretAttrs implements environs.EnvironProvider. func (environProvider) SecretAttrs(cfg *config.Config) (map[string]string, error) { - // The defaults should be set already, so we pass nil. - ecfg, err := newValidConfig(cfg, nil) - if err != nil { - return nil, errors.Trace(err) + return map[string]string{}, nil +} + +func validateCloudSpec(spec environs.CloudSpec) error { + if err := spec.Validate(); err != nil { + return errors.Trace(err) + } + // TODO(axw) add validation of endpoint/region. + if spec.Credential == nil { + return errors.NotValidf("missing credential") + } + if authType := spec.Credential.AuthType(); authType != cloud.UserPassAuthType { + return errors.NotSupportedf("%q auth-type", authType) } - return ecfg.secret(), nil + return nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/provider_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/provider_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/provider_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/provider_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,7 +11,6 @@ "github.com/juju/juju/cloud" "github.com/juju/juju/environs" - "github.com/juju/juju/environs/testing" "github.com/juju/juju/provider/vsphere" ) @@ -19,6 +18,7 @@ vsphere.BaseSuite provider environs.EnvironProvider + spec environs.CloudSpec } var _ = gc.Suite(&providerSuite{}) @@ -29,6 +29,7 @@ var err error s.provider, err = environs.Provider("vsphere") c.Check(err, jc.ErrorIsNil) + s.spec = vsphere.FakeCloudSpec() } func (s *providerSuite) TestRegistered(c *gc.C) { @@ -36,29 +37,47 @@ } func (s *providerSuite) TestOpen(c *gc.C) { - env, err := s.provider.Open(s.Config) + env, err := s.provider.Open(environs.OpenParams{ + Cloud: s.spec, + Config: s.Config, + }) c.Check(err, jc.ErrorIsNil) envConfig := env.Config() c.Assert(envConfig.Name(), gc.Equals, "testenv") } -func (s *providerSuite) TestBootstrapConfig(c *gc.C) { - cfg, err := s.provider.BootstrapConfig(environs.BootstrapConfigParams{ +func (s *providerSuite) TestOpenInvalidCloudSpec(c *gc.C) { + s.spec.Name = "" + s.testOpenError(c, s.spec, `validating cloud spec: cloud name "" not valid`) +} + +func (s *providerSuite) TestOpenMissingCredential(c *gc.C) { + s.spec.Credential = nil + s.testOpenError(c, s.spec, `validating cloud spec: missing credential not valid`) +} + +func (s *providerSuite) TestOpenUnsupportedCredential(c *gc.C) { + credential := cloud.NewCredential(cloud.OAuth1AuthType, map[string]string{}) + s.spec.Credential = &credential + s.testOpenError(c, s.spec, `validating cloud spec: "oauth1" auth-type not supported`) +} + +func (s *providerSuite) testOpenError(c *gc.C, spec environs.CloudSpec, expect string) { + _, err := s.provider.Open(environs.OpenParams{ + Cloud: spec, Config: s.Config, - Credentials: cloud.NewCredential( - cloud.UserPassAuthType, - map[string]string{"user": "u", "password": "p"}, - ), }) - c.Check(err, jc.ErrorIsNil) - c.Check(cfg, gc.NotNil) + c.Assert(err, gc.ErrorMatches, expect) } -func (s *providerSuite) TestPrepareForBootstrap(c *gc.C) { - env, err := s.provider.PrepareForBootstrap(testing.BootstrapContext(c), s.Config) +func (s *providerSuite) TestPrepareConfig(c *gc.C) { + cfg, err := s.provider.PrepareConfig(environs.PrepareConfigParams{ + Config: s.Config, + Cloud: s.spec, + }) c.Check(err, jc.ErrorIsNil) - c.Check(env, gc.NotNil) + c.Check(cfg, gc.NotNil) } func (s *providerSuite) TestValidate(c *gc.C) { @@ -68,12 +87,3 @@ validAttrs := validCfg.AllAttrs() c.Assert(s.Config.AllAttrs(), gc.DeepEquals, validAttrs) } - -func (s *providerSuite) TestSecretAttrs(c *gc.C) { - obtainedAttrs, err := s.provider.SecretAttrs(s.Config) - c.Check(err, jc.ErrorIsNil) - - expectedAttrs := map[string]string{"password": "password1"} - c.Assert(obtainedAttrs, gc.DeepEquals, expectedAttrs) - -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/storage.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/storage.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/storage.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/storage.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,20 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package vsphere + +import ( + "github.com/juju/errors" + + "github.com/juju/juju/storage" +) + +// StorageProviderTypes implements storage.ProviderRegistry. +func (*environ) StorageProviderTypes() []storage.ProviderType { + return nil +} + +// StorageProvider implements storage.ProviderRegistry. +func (*environ) StorageProvider(t storage.ProviderType) (storage.Provider, error) { + return nil, errors.NotFoundf("storage provider %q", t) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/testing_test.go juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/testing_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/provider/vsphere/testing_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/provider/vsphere/testing_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -23,23 +23,38 @@ "golang.org/x/net/context" gc "gopkg.in/check.v1" + "github.com/juju/juju/cloud" "github.com/juju/juju/environs" "github.com/juju/juju/environs/config" "github.com/juju/juju/juju/osenv" "github.com/juju/juju/testing" ) -var ( - ConfigAttrs = testing.FakeConfig().Merge(testing.Attrs{ +func ConfigAttrs() testing.Attrs { + return testing.FakeConfig().Merge(testing.Attrs{ "type": "vsphere", "uuid": "2d02eeac-9dbb-11e4-89d3-123b93f75cba", - "datacenter": "/datacenter1", - "host": "host1", - "user": "user1", - "password": "password1", "external-network": "", }) -) +} + +func FakeCloudSpec() environs.CloudSpec { + cred := FakeCredential() + return environs.CloudSpec{ + Type: "vsphere", + Name: "vsphere", + Region: "/datacenter1", + Endpoint: "host1", + Credential: &cred, + } +} + +func FakeCredential() cloud.Credential { + return cloud.NewCredential(cloud.UserPassAuthType, map[string]string{ + "user": "user1", + "password": "password1", + }) +} type BaseSuite struct { gitjujutesting.IsolationSuite @@ -63,9 +78,12 @@ } func (s *BaseSuite) initEnv(c *gc.C) { - cfg, err := testing.ModelConfig(c).Apply(ConfigAttrs) + cfg, err := testing.ModelConfig(c).Apply(ConfigAttrs()) c.Assert(err, jc.ErrorIsNil) - env, err := environs.New(cfg) + env, err := environs.New(environs.OpenParams{ + Cloud: FakeCloudSpec(), + Config: cfg, + }) c.Assert(err, jc.ErrorIsNil) s.Env = env.(*environ) s.setConfig(c, cfg) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/resource/resourceadapters/apiserver.go juju-core-2.0~beta15/src/github.com/juju/juju/resource/resourceadapters/apiserver.go --- juju-core-2.0~beta12/src/github.com/juju/juju/resource/resourceadapters/apiserver.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/resource/resourceadapters/apiserver.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,6 +11,7 @@ "github.com/juju/juju/apiserver/common" "github.com/juju/juju/apiserver/common/apihttp" + "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/resource" internalserver "github.com/juju/juju/resource/api/private/server" "github.com/juju/juju/resource/api/server" @@ -19,7 +20,7 @@ // NewPublicFacade provides the public API facade for resources. It is // passed into common.RegisterStandardFacade. -func NewPublicFacade(st *corestate.State, _ *common.Resources, authorizer common.Authorizer) (*server.Facade, error) { +func NewPublicFacade(st *corestate.State, _ facade.Resources, authorizer facade.Authorizer) (*server.Facade, error) { if !authorizer.AuthClient() { return nil, common.ErrPerm } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/scripts/win-installer/setup.iss juju-core-2.0~beta15/src/github.com/juju/juju/scripts/win-installer/setup.iss --- juju-core-2.0~beta12/src/github.com/juju/juju/scripts/win-installer/setup.iss 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/scripts/win-installer/setup.iss 2016-08-16 08:56:25.000000000 +0000 @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "Juju" -#define MyAppVersion "2.0-beta12" +#define MyAppVersion "2.0-beta15" #define MyAppPublisher "Canonical, Ltd" #define MyAppURL "http://juju.ubuntu.com/" #define MyAppExeName "juju.exe" diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/service/service.go juju-core-2.0~beta15/src/github.com/juju/juju/service/service.go --- juju-core-2.0~beta12/src/github.com/juju/juju/service/service.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/service/service.go 2016-08-16 08:56:25.000000000 +0000 @@ -195,6 +195,8 @@ // installStartRetryAttempts defines how much InstallAndStart retries // upon Start failures. +// +// TODO(katco): 2016-08-09: lp:1611427 var installStartRetryAttempts = utils.AttemptStrategy{ Total: 1 * time.Second, Delay: 250 * time.Millisecond, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/service/windows/password_windows.go juju-core-2.0~beta15/src/github.com/juju/juju/service/windows/password_windows.go --- juju-core-2.0~beta12/src/github.com/juju/juju/service/windows/password_windows.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/service/windows/password_windows.go 2016-08-16 08:56:25.000000000 +0000 @@ -67,6 +67,7 @@ return nil } +// TODO(katco): 2016-08-09: lp:1611427 var changeServicePasswordAttempts = utils.AttemptStrategy{ Total: 5 * time.Second, Delay: 6 * time.Second, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/addmachine.go juju-core-2.0~beta15/src/github.com/juju/juju/state/addmachine.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/addmachine.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/addmachine.go 2016-08-16 08:56:25.000000000 +0000 @@ -163,13 +163,6 @@ var ops []txn.Op var mdocs []*machineDoc for _, template := range templates { - // Adding a machine without any principals is - // only permitted if unit placement is supported. - if len(template.principals) == 0 && template.InstanceId == "" { - if err := st.supportsUnitPlacement(); err != nil { - return nil, errors.Trace(err) - } - } mdoc, addOps, err := st.addMachineOps(template) if err != nil { return nil, errors.Trace(err) @@ -342,10 +335,6 @@ if containerType == "" { return nil, nil, errors.New("no container type specified") } - // Adding a machine within a machine implies add-machine or placement. - if err := st.supportsUnitPlacement(); err != nil { - return nil, nil, err - } // If a parent machine is specified, make sure it exists // and can support the requested container type. @@ -406,10 +395,6 @@ return nil, nil, errors.New("no container type specified") } if parentTemplate.InstanceId == "" { - // Adding a machine within a machine implies add-machine or placement. - if err := st.supportsUnitPlacement(); err != nil { - return nil, nil, err - } if err := st.precheckInstance(parentTemplate.Series, parentTemplate.Constraints, parentTemplate.Placement); err != nil { return nil, nil, err } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/allcollections.go juju-core-2.0~beta15/src/github.com/juju/juju/state/allcollections.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/allcollections.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/allcollections.go 2016-08-16 08:56:25.000000000 +0000 @@ -121,15 +121,24 @@ // This collection records the model migrations which // are currently in progress. It is used to ensure that only - // one model migration document exists per environment. + // one model migration document exists per model. migrationsActiveC: {global: true}, + // This collection tracks migration progress reports from the + // migration minions. + migrationsMinionSyncC: {global: true}, + // This collection holds user information that's not specific to any // one model. usersC: { global: true, }, + // This collection holds users that are relative to controllers. + controllerUsersC: { + global: true, + }, + // This collection holds the last time the user connected to the API server. userLastLoginC: { global: true, @@ -194,9 +203,6 @@ rawAccess: true, }, - // This collection holds the source from where model settings came. - modelSettingsSourcesC: {}, - // This collection contains governors that prevent certain kinds of // changes from being accepted. blocksC: {}, @@ -263,6 +269,10 @@ rebootC: {}, sshHostKeysC: {}, + // This collection contains information from removed machines + // that needs to be cleaned up in the provider. + machineRemovalsC: {}, + // ----- // These collections hold information associated with storage. @@ -319,14 +329,18 @@ // ----- - // TODO(ericsnow) Use a component-oriented registration mechanism... - // This collection holds information associated with charm payloads. - // See payload/persistence/mongo.go. - "payloads": {}, + payloadsC: { + indexes: []mgo.Index{{ + Key: []string{"model-uuid", "unitid"}, + }, { + Key: []string{"model-uuid", "name"}, + }}, + }, // This collection holds information associated with charm resources. - // See resource/persistence/mongo.go. + // See resource/persistence/mongo.go, where it should never have + // been put in the first place. "resources": {}, // ----- @@ -350,6 +364,7 @@ storageConstraintsC: {}, statusesC: {}, statusesHistoryC: { + rawAccess: true, indexes: []mgo.Index{{ Key: []string{"model-uuid", "globalkey", "updated"}, }}, @@ -394,6 +409,7 @@ constraintsC = "constraints" containerRefsC = "containerRefs" controllersC = "controllers" + controllerUsersC = "controllerusers" filesystemAttachmentsC = "filesystemAttachments" filesystemsC = "filesystems" globalSettingsC = "globalSettings" @@ -402,19 +418,21 @@ instanceDataC = "instanceData" leasesC = "leases" machinesC = "machines" + machineRemovalsC = "machineremovals" meterStatusC = "meterStatus" metricsC = "metrics" metricsManagerC = "metricsmanager" minUnitsC = "minunits" - migrationsStatusC = "migrations.status" migrationsActiveC = "migrations.active" migrationsC = "migrations" - modelSettingsSourcesC = "modelSettingsSources" + migrationsMinionSyncC = "migrations.minionsync" + migrationsStatusC = "migrations.status" modelUserLastConnectionC = "modelUserLastConnection" modelUsersC = "modelusers" modelsC = "models" modelEntityRefsC = "modelEntityRefs" openedPortsC = "openedPorts" + payloadsC = "payloads" permissionsC = "permissions" providerIDsC = "providerIDs" rebootC = "reboot" @@ -447,6 +465,5 @@ usersC = "users" volumeAttachmentsC = "volumeattachments" volumesC = "volumes" - // "payloads" (see payload/persistence/mongo.go) // "resources" (see resource/persistence/mongo.go) ) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/allwatcher_internal_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/allwatcher_internal_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/allwatcher_internal_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/allwatcher_internal_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -22,6 +22,7 @@ "github.com/juju/juju/state/multiwatcher" "github.com/juju/juju/state/watcher" "github.com/juju/juju/status" + "github.com/juju/juju/storage" "github.com/juju/juju/testing" ) @@ -55,7 +56,10 @@ "name": fmt.Sprintf("testenv%d", s.envCount), "uuid": utils.MustNewUUID().String(), }) - _, st, err := s.state.NewModel(ModelArgs{CloudName: "dummy", Config: cfg, Owner: s.owner}) + _, st, err := s.state.NewModel(ModelArgs{ + CloudName: "dummy", Config: cfg, Owner: s.owner, + StorageProviderRegistry: storage.StaticProviderRegistry{}, + }) c.Assert(err, jc.ErrorIsNil) s.AddCleanup(func(*gc.C) { st.Close() }) return st @@ -292,7 +296,7 @@ allWatcherBaseSuite } -func (s *allWatcherStateSuite) Reset(c *gc.C) { +func (s *allWatcherStateSuite) reset(c *gc.C) { s.TearDownTest(c) s.SetUpTest(c) } @@ -429,7 +433,7 @@ entities := all.All() substNilSinceTimeForEntities(c, entities) assertEntitiesEqual(c, entities, test.expectContents) - s.Reset(c) + s.reset(c) } } @@ -565,7 +569,6 @@ } func (s *allWatcherStateSuite) TestClosingPorts(c *gc.C) { - defer s.Reset(c) // Init the test model. wordpress := AddTestingService(c, s.state, "wordpress", AddTestingCharm(c, s.state, "wordpress")) u, err := wordpress.AddUnit() @@ -660,7 +663,6 @@ } func (s *allWatcherStateSuite) TestSettings(c *gc.C) { - defer s.Reset(c) // Init the test model. svc := AddTestingService(c, s.state, "dummy-application", AddTestingCharm(c, s.state, "dummy")) b := newAllWatcherStateBacking(s.state) @@ -939,71 +941,82 @@ func (s *allWatcherStateSuite) TestStateWatcherTwoModels(c *gc.C) { loggo.GetLogger("juju.state.watcher").SetLogLevel(loggo.TRACE) + // The return values for the setup and trigger functions are the + // number of changes to expect. for i, test := range []struct { about string - setUpState func(*State) - triggerEvent func(*State) + setUpState func(*State) int + triggerEvent func(*State) int }{ { about: "machines", - triggerEvent: func(st *State) { + triggerEvent: func(st *State) int { m0, err := st.AddMachine("trusty", JobHostUnits) c.Assert(err, jc.ErrorIsNil) c.Assert(m0.Id(), gc.Equals, "0") + return 1 }, }, { about: "applications", - triggerEvent: func(st *State) { + triggerEvent: func(st *State) int { AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) + return 1 }, }, { about: "units", - setUpState: func(st *State) { + setUpState: func(st *State) int { AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) + return 1 }, - triggerEvent: func(st *State) { + triggerEvent: func(st *State) int { svc, err := st.Application("wordpress") c.Assert(err, jc.ErrorIsNil) _, err = svc.AddUnit() c.Assert(err, jc.ErrorIsNil) + return 3 }, }, { about: "relations", - setUpState: func(st *State) { + setUpState: func(st *State) int { AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) AddTestingService(c, st, "mysql", AddTestingCharm(c, st, "mysql")) + return 2 }, - triggerEvent: func(st *State) { + triggerEvent: func(st *State) int { eps, err := st.InferEndpoints("mysql", "wordpress") c.Assert(err, jc.ErrorIsNil) _, err = st.AddRelation(eps...) c.Assert(err, jc.ErrorIsNil) + return 3 }, }, { about: "annotations", - setUpState: func(st *State) { + setUpState: func(st *State) int { m, err := st.AddMachine("trusty", JobHostUnits) c.Assert(err, jc.ErrorIsNil) c.Assert(m.Id(), gc.Equals, "0") + return 1 }, - triggerEvent: func(st *State) { + triggerEvent: func(st *State) int { m, err := st.Machine("0") c.Assert(err, jc.ErrorIsNil) err = st.SetAnnotations(m, map[string]string{"foo": "bar"}) c.Assert(err, jc.ErrorIsNil) + return 1 }, }, { about: "statuses", - setUpState: func(st *State) { + setUpState: func(st *State) int { m, err := st.AddMachine("trusty", JobHostUnits) c.Assert(err, jc.ErrorIsNil) c.Assert(m.Id(), gc.Equals, "0") err = m.SetProvisioned("inst-id", "fake_nonce", nil) c.Assert(err, jc.ErrorIsNil) + return 1 }, - triggerEvent: func(st *State) { + triggerEvent: func(st *State) int { m, err := st.Machine("0") c.Assert(err, jc.ErrorIsNil) @@ -1015,35 +1028,40 @@ } err = m.SetStatus(sInfo) c.Assert(err, jc.ErrorIsNil) + return 1 }, }, { about: "constraints", - setUpState: func(st *State) { + setUpState: func(st *State) int { AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) + return 1 }, - triggerEvent: func(st *State) { + triggerEvent: func(st *State) int { svc, err := st.Application("wordpress") c.Assert(err, jc.ErrorIsNil) cpuCores := uint64(99) err = svc.SetConstraints(constraints.Value{CpuCores: &cpuCores}) c.Assert(err, jc.ErrorIsNil) + return 1 }, }, { about: "settings", - setUpState: func(st *State) { + setUpState: func(st *State) int { AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) + return 1 }, - triggerEvent: func(st *State) { + triggerEvent: func(st *State) int { svc, err := st.Application("wordpress") c.Assert(err, jc.ErrorIsNil) err = svc.UpdateConfigSettings(charm.Settings{"blog-title": "boring"}) c.Assert(err, jc.ErrorIsNil) + return 1 }, }, { about: "blocks", - triggerEvent: func(st *State) { + triggerEvent: func(st *State) int { m, found, err := st.GetBlockForType(DestroyBlock) c.Assert(err, jc.ErrorIsNil) c.Assert(found, jc.IsFalse) @@ -1051,6 +1069,7 @@ err = st.SwitchBlockOn(DestroyBlock, "test block") c.Assert(err, jc.ErrorIsNil) + return 1 }, }, } { @@ -1060,17 +1079,15 @@ c.Logf("Making changes to model %s", st.ModelUUID()) if test.setUpState != nil { - test.setUpState(st) + expected := test.setUpState(st) // Consume events from setup. - w.AssertChanges(c) - w.AssertNoChange(c) + w.AssertChanges(c, expected) otherW.AssertNoChange(c) } - test.triggerEvent(st) + expected := test.triggerEvent(st) // Check event was isolated to the correct watcher. - w.AssertChanges(c) - w.AssertNoChange(c) + w.AssertChanges(c, expected) otherW.AssertNoChange(c) } otherState := s.newState(c) @@ -1086,7 +1103,7 @@ checkIsolationForEnv(s.state, w1, w2) checkIsolationForEnv(otherState, w2, w1) }() - s.Reset(c) + s.reset(c) } } @@ -3116,23 +3133,26 @@ } } -func (tw *testWatcher) AssertChanges(c *gc.C) { +func (tw *testWatcher) AssertChanges(c *gc.C, expected int) { var count int tw.st.StartSync() + maxWait := time.After(testing.LongWait) done: for { select { case d := <-tw.deltas: - if len(d) == 0 { + count += len(d) + if count == expected { break done } - count += len(d) - case <-time.After(testing.LongWait): - // no change detected + case <-maxWait: + // insufficient changes seen break done } } - c.Assert(count, jc.GreaterThan, 0) + // ensure there are no more than we expect + tw.AssertNoChange(c) + c.Assert(count, gc.Equals, expected) } type entityInfoSlice []multiwatcher.EntityInfo diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/annotations_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/annotations_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/annotations_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/annotations_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,6 +11,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/state" + "github.com/juju/juju/storage" "github.com/juju/juju/testing" ) @@ -179,7 +180,10 @@ "uuid": uuid.String(), }) owner := names.NewUserTag("test@remote") - env, st, err := s.State.NewModel(state.ModelArgs{CloudName: "dummy", Config: cfg, Owner: owner}) + env, st, err := s.State.NewModel(state.ModelArgs{ + CloudName: "dummy", Config: cfg, Owner: owner, + StorageProviderRegistry: storage.StaticProviderRegistry{}, + }) c.Assert(err, jc.ErrorIsNil) return env, st } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/application.go juju-core-2.0~beta15/src/github.com/juju/juju/state/application.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/application.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/application.go 2016-08-16 08:56:25.000000000 +0000 @@ -433,16 +433,91 @@ return asserts, nil } -func (s *Application) checkStorageUpgrade(newMeta *charm.Meta) (err error) { +func (s *Application) checkStorageUpgrade(newMeta *charm.Meta) (_ []txn.Op, err error) { defer errors.DeferredAnnotatef(&err, "cannot upgrade application %q to charm %q", s, newMeta.Name) ch, _, err := s.Charm() if err != nil { - return errors.Trace(err) + return nil, errors.Trace(err) } oldMeta := ch.Meta() - for name := range oldMeta.Storage { - if _, ok := newMeta.Storage[name]; !ok { - return errors.Errorf("storage %q removed", name) + var ops []txn.Op + var units []*Unit + for name, oldStorageMeta := range oldMeta.Storage { + if _, ok := newMeta.Storage[name]; ok { + continue + } + if oldStorageMeta.CountMin > 0 { + return nil, errors.Errorf("required storage %q removed", name) + } + // Optional storage has been removed. So long as there + // are no instances of the store, it can safely be + // removed. + if oldStorageMeta.Shared { + n, err := s.st.countEntityStorageInstancesForName( + s.Tag(), name, + ) + if err != nil { + return nil, errors.Trace(err) + } + if n > 0 { + return nil, errors.Errorf("in-use storage %q removed", name) + } + // TODO(axw) if/when it is possible to + // add shared storage instance to an + // application post-deployment, we must + // include a txn.Op here that asserts + // that the number of instances is zero. + } else { + if units == nil { + var err error + units, err = s.AllUnits() + if err != nil { + return nil, errors.Trace(err) + } + ops = append(ops, txn.Op{ + C: applicationsC, + Id: s.doc.DocID, + Assert: bson.D{{"unitcount", len(units)}}, + }) + for _, unit := range units { + // Here we check that the storage + // attachment count remains the same. + // To get around the ABA problem, we + // also add ops for the individual + // attachments below. + ops = append(ops, txn.Op{ + C: unitsC, + Id: unit.doc.DocID, + Assert: bson.D{{ + "storageattachmentcount", + unit.doc.StorageAttachmentCount, + }}, + }) + } + } + for _, unit := range units { + attachments, err := s.st.UnitStorageAttachments(unit.UnitTag()) + if err != nil { + return nil, errors.Trace(err) + } + for _, attachment := range attachments { + storageTag := attachment.StorageInstance() + storageName, err := names.StorageName(storageTag.Id()) + if err != nil { + return nil, errors.Trace(err) + } + if storageName == name { + return nil, errors.Errorf("in-use storage %q removed", name) + } + // We assert that other storage attachments still exist to + // avoid the ABA problem. + ops = append(ops, txn.Op{ + C: storageAttachmentsC, + Id: storageAttachmentId(unit.Name(), storageTag.Id()), + Assert: txn.DocExists, + }) + } + } } } less := func(a, b int) bool { @@ -452,7 +527,7 @@ oldStorageMeta, ok := oldMeta.Storage[name] if !ok { if newStorageMeta.CountMin > 0 { - return errors.Errorf("required storage %q added", name) + return nil, errors.Errorf("required storage %q added", name) } // New storage is fine as long as it is not required. // @@ -463,31 +538,31 @@ continue } if newStorageMeta.Type != oldStorageMeta.Type { - return errors.Errorf( + return nil, errors.Errorf( "existing storage %q type changed from %q to %q", name, oldStorageMeta.Type, newStorageMeta.Type, ) } if newStorageMeta.Shared != oldStorageMeta.Shared { - return errors.Errorf( + return nil, errors.Errorf( "existing storage %q shared changed from %v to %v", name, oldStorageMeta.Shared, newStorageMeta.Shared, ) } if newStorageMeta.ReadOnly != oldStorageMeta.ReadOnly { - return errors.Errorf( + return nil, errors.Errorf( "existing storage %q read-only changed from %v to %v", name, oldStorageMeta.ReadOnly, newStorageMeta.ReadOnly, ) } if newStorageMeta.Location != oldStorageMeta.Location { - return errors.Errorf( + return nil, errors.Errorf( "existing storage %q location changed from %q to %q", name, oldStorageMeta.Location, newStorageMeta.Location, ) } if newStorageMeta.CountMin > oldStorageMeta.CountMin { - return errors.Errorf( + return nil, errors.Errorf( "existing storage %q range contracted: min increased from %d to %d", name, oldStorageMeta.CountMin, newStorageMeta.CountMin, ) @@ -497,7 +572,7 @@ if oldStorageMeta.CountMax == -1 { oldCountMax = "" } - return errors.Errorf( + return nil, errors.Errorf( "existing storage %q range contracted: max decreased from %v to %d", name, oldCountMax, newStorageMeta.CountMax, ) @@ -506,13 +581,13 @@ // If a location is specified, the store may not go // from being a singleton to multiple, since then the // location has a different meaning. - return errors.Errorf( + return nil, errors.Errorf( "existing storage %q with location changed from singleton to multiple", name, ) } } - return nil + return ops, nil } // changeCharmOps returns the operations necessary to set a service's @@ -643,9 +718,11 @@ // Check storage to ensure no storage is removed, and no required // storage is added for which there are no constraints. - if err := s.checkStorageUpgrade(ch.Meta()); err != nil { + storageOps, err := s.checkStorageUpgrade(ch.Meta()) + if err != nil { return nil, errors.Trace(err) } + ops = append(ops, storageOps...) // And finally, decrement the old settings. return append(ops, decOps...), nil @@ -1075,7 +1152,6 @@ if err != nil { return nil, err } - // TODO(ericsnow) Use a generic registry instead. resOps, err := removeUnitResourcesOps(s.st, u.doc.Application, u.doc.Name) if err != nil { return nil, errors.Trace(err) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/application_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/application_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/application_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/application_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -20,12 +20,10 @@ "gopkg.in/mgo.v2/txn" "github.com/juju/juju/constraints" - "github.com/juju/juju/environs/config" "github.com/juju/juju/state" "github.com/juju/juju/state/testing" "github.com/juju/juju/status" - "github.com/juju/juju/storage/provider" - "github.com/juju/juju/storage/provider/registry" + "github.com/juju/juju/testing/factory" ) type ServiceSuite struct { @@ -38,7 +36,7 @@ func (s *ServiceSuite) SetUpTest(c *gc.C) { s.ConnSuite.SetUpTest(c) - s.policy.GetConstraintsValidator = func(*config.Config, state.SupportedArchitecturesQuerier) (constraints.Validator, error) { + s.policy.GetConstraintsValidator = func() (constraints.Validator, error) { validator := constraints.NewValidator() validator.RegisterConflicts([]string{constraints.InstanceType}, []string{constraints.Mem}) validator.RegisterUnsupported([]string{constraints.CpuPower}) @@ -1703,11 +1701,9 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(dirty, jc.IsTrue) - // Run the cleanup and check the charm. + // Run the cleanup err = s.State.Cleanup() c.Assert(err, jc.ErrorIsNil) - _, err = s.State.Charm(s.charm.URL()) - c.Assert(err, jc.Satisfies, errors.IsNotFound) // Check we're now clean. dirty, err = s.State.NeedsCleanup() @@ -1789,7 +1785,7 @@ logger := loggo.GetLogger("test") logger.SetLogLevel(loggo.DEBUG) var tw loggo.TestWriter - c.Assert(loggo.RegisterWriter("constraints-tester", &tw, loggo.DEBUG), gc.IsNil) + c.Assert(loggo.RegisterWriter("constraints-tester", &tw), gc.IsNil) cons := constraints.MustParse("mem=4G cpu-power=10") err := s.mysql.SetConstraints(cons) @@ -2161,6 +2157,16 @@ range: 0- ` +const oneRequiredOneOptionalStorageMeta = ` +storage: + data0: + type: block + data1: + type: block + multiple: + range: 0- +` + const twoRequiredStorageMeta = ` storage: data0: @@ -2232,7 +2238,6 @@ } func (s *ServiceSuite) setCharmFromMeta(c *gc.C, oldMeta, newMeta string) error { - registry.RegisterEnvironStorageProviders("someprovider", provider.LoopProviderType) oldCh := s.AddMetaCharm(c, "mysql", oldMeta, 2) newCh := s.AddMetaCharm(c, "mysql", newMeta, 3) svc := s.AddTestingService(c, "test", oldCh) @@ -2241,12 +2246,45 @@ return svc.SetCharm(cfg) } -func (s *ServiceSuite) TestSetCharmStorageRemoved(c *gc.C) { +func (s *ServiceSuite) TestSetCharmOptionalUnusedStorageRemoved(c *gc.C) { + err := s.setCharmFromMeta(c, + mysqlBaseMeta+oneRequiredOneOptionalStorageMeta, + mysqlBaseMeta+oneRequiredStorageMeta, + ) + c.Assert(err, jc.ErrorIsNil) + // It's valid to remove optional storage so long + // as it is not in use. +} + +func (s *ServiceSuite) TestSetCharmOptionalUsedStorageRemoved(c *gc.C) { + oldMeta := mysqlBaseMeta + oneRequiredOneOptionalStorageMeta + newMeta := mysqlBaseMeta + oneRequiredStorageMeta + oldCh := s.AddMetaCharm(c, "mysql", oldMeta, 2) + newCh := s.AddMetaCharm(c, "mysql", newMeta, 3) + svc := s.Factory.MakeApplication(c, &factory.ApplicationParams{ + Name: "test", + Charm: oldCh, + Storage: map[string]state.StorageConstraints{ + "data0": {Count: 1}, + "data1": {Count: 1}, + }, + }) + defer state.SetBeforeHooks(c, s.State, func() { + // Adding a unit will cause the storage to be in-use. + _, err := svc.AddUnit() + c.Assert(err, jc.ErrorIsNil) + }).Check() + cfg := state.SetCharmConfig{Charm: newCh} + err := svc.SetCharm(cfg) + c.Assert(err, gc.ErrorMatches, `cannot upgrade application "test" to charm "mysql": in-use storage "data1" removed`) +} + +func (s *ServiceSuite) TestSetCharmRequiredStorageRemoved(c *gc.C) { err := s.setCharmFromMeta(c, - mysqlBaseMeta+twoOptionalStorageMeta, - mysqlBaseMeta+oneOptionalStorageMeta, + mysqlBaseMeta+oneRequiredStorageMeta, + mysqlBaseMeta, ) - c.Assert(err, gc.ErrorMatches, `cannot upgrade application "test" to charm "mysql": storage "data1" removed`) + c.Assert(err, gc.ErrorMatches, `cannot upgrade application "test" to charm "mysql": required storage "data0" removed`) } func (s *ServiceSuite) TestSetCharmRequiredStorageAdded(c *gc.C) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/assign_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/assign_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/assign_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/assign_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -19,8 +19,6 @@ "github.com/juju/juju/state" "github.com/juju/juju/storage/poolmanager" "github.com/juju/juju/storage/provider" - "github.com/juju/juju/storage/provider/dummy" - "github.com/juju/juju/storage/provider/registry" ) type AssignSuite struct { @@ -686,10 +684,9 @@ s.ConnSuite.SetUpTest(c) wordpress := s.AddTestingService(c, "wordpress", s.AddTestingCharm(c, "wordpress")) s.wordpress = wordpress - pm := poolmanager.New(state.NewStateSettings(s.State)) + pm := poolmanager.New(state.NewStateSettings(s.State), provider.CommonStorageProviders()) _, err := pm.Create("loop-pool", provider.LoopProviderType, map[string]interface{}{}) c.Assert(err, jc.ErrorIsNil) - registry.RegisterEnvironStorageProviders("someprovider", provider.LoopProviderType) } func (s *assignCleanSuite) errorMessage(msg string) string { @@ -1030,12 +1027,6 @@ } func (s *assignCleanSuite) TestAssignToMachineErrors(c *gc.C) { - registry.RegisterProvider("static", &dummy.StorageProvider{ - IsDynamic: false, - }) - registry.RegisterEnvironStorageProviders("someprovider", "static") - defer registry.RegisterProvider("static", nil) - _, unit, _ := s.setupSingleStorage(c, "filesystem", "static") machine, err := s.State.AddMachine("quantal", state.JobHostUnits) c.Assert(err, jc.ErrorIsNil) @@ -1055,11 +1046,6 @@ } func (s *assignCleanSuite) TestAssignUnitWithNonDynamicStorageCleanAvailable(c *gc.C) { - registry.RegisterProvider("static", &dummy.StorageProvider{ - IsDynamic: false, - }) - registry.RegisterEnvironStorageProviders("someprovider", "static") - defer registry.RegisterProvider("static", nil) _, unit, _ := s.setupSingleStorage(c, "filesystem", "static") storageAttachments, err := s.State.UnitStorageAttachments(unit.UnitTag()) c.Assert(err, jc.ErrorIsNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/backups/db.go juju-core-2.0~beta15/src/github.com/juju/juju/state/backups/db.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/backups/db.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/backups/db.go 2016-08-16 08:56:25.000000000 +0000 @@ -47,9 +47,10 @@ // ignoredDatabases is the list of databases that should not be // backed up. var ignoredDatabases = set.NewStrings( + "admin", storageDBName, - "presence", - imagestorage.ImagesDB, + "presence", // note: this is still backed up anyway + imagestorage.ImagesDB, // note: this is still backed up anyway ) type DBSession interface { @@ -188,18 +189,23 @@ // stripIgnored removes the ignored DBs from the mongo dump files. // This involves deleting DB-specific directories. +// +// NOTE(fwereade): the only directories we actually delete are "admin" +// and "backups"; and those only if they're in the `ignored` set. I have +// no idea why the code was structured this way; but I am, as requested +// as usual by management, *not* fixing anything about backup beyond the +// bug du jour. +// +// Basically, the ignored set is a filthy lie, and all the work we do to +// generate it is pure obfuscation. func stripIgnored(ignored set.Strings, dumpDir string) error { for _, dbName := range ignored.Values() { - if dbName != "backups" { - // We allow all ignored databases except "backups" to be - // included in the archive file. Restore will be - // responsible for deleting those databases after - // restoring them. - continue - } - dirname := filepath.Join(dumpDir, dbName) - if err := os.RemoveAll(dirname); err != nil { - return errors.Trace(err) + switch dbName { + case storageDBName, "admin": + dirname := filepath.Join(dumpDir, dbName) + if err := os.RemoveAll(dirname); err != nil { + return errors.Trace(err) + } } } @@ -355,6 +361,13 @@ } func (md *mongoRestorer32) options(dumpDir string) []string { + // note the batchSize, which is known to mitigate EOF errors + // seen when using mongorestore; as seen and reported in + // https://jira.mongodb.org/browse/TOOLS-939 -- not guaranteed + // to *help* with lp:1605653, but observed not to hurt. + // + // The value of 100 was chosen because it's more pessimistic + // than the "1000" that many report success using in the bug. options := []string{ "--ssl", "--authenticationDatabase", "admin", @@ -363,6 +376,7 @@ "--password", md.Password, "--drop", "--oplogReplay", + "--batchSize", "100", dumpDir, } return options diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/backups/db_restore_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/backups/db_restore_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/backups/db_restore_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/backups/db_restore_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -115,7 +115,7 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(ranCommand, gc.Equals, "/a/fake/mongorestore") - c.Assert(ranWithArgs, gc.DeepEquals, []string{"--ssl", "--authenticationDatabase", "admin", "--host", "127.0.0.1", "--username", "fakeUsername", "--password", "fakePassword", "--drop", "--oplogReplay", "fakePath"}) + c.Assert(ranWithArgs, gc.DeepEquals, []string{"--ssl", "--authenticationDatabase", "admin", "--host", "127.0.0.1", "--username", "fakeUsername", "--password", "fakePassword", "--drop", "--oplogReplay", "--batchSize", "100", "fakePath"}) user := &mgo.User{Username: "machine-0", Password: "fakePassword"} c.Assert(mgoDb.user, gc.DeepEquals, user) c.Assert(mgoSession.closed, jc.IsTrue) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/backups/restore.go juju-core-2.0~beta15/src/github.com/juju/juju/state/backups/restore.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/backups/restore.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/backups/restore.go 2016-08-16 08:56:25.000000000 +0000 @@ -27,6 +27,7 @@ "github.com/juju/juju/mongo" "github.com/juju/juju/network" "github.com/juju/juju/state" + "github.com/juju/juju/state/stateenvirons" "github.com/juju/juju/worker/peergrouper" ) @@ -137,7 +138,7 @@ // assign to variables for testing purposes. var mongoDefaultDialOpts = mongo.DefaultDialOpts -var environsNewStatePolicy = environs.NewStatePolicy +var environsGetNewPolicyFunc = stateenvirons.GetNewPolicyFunc // newStateConnection tries to connect to the newly restored controller. func newStateConnection(modelTag names.ModelTag, info *mongo.MongoInfo) (*state.State, error) { @@ -152,9 +153,11 @@ newStateConnDelay = 15 * time.Second newStateConnMinAttempts = 8 ) + // TODO(katco): 2016-08-09: lp:1611427 attempt := utils.AttemptStrategy{Delay: newStateConnDelay, Min: newStateConnMinAttempts} + getEnviron := stateenvirons.GetNewEnvironFunc(environs.New) for a := attempt.Start(); a.Next(); { - st, err = state.Open(modelTag, info, mongoDefaultDialOpts(), environsNewStatePolicy()) + st, err = state.Open(modelTag, info, mongoDefaultDialOpts(), environsGetNewPolicyFunc(getEnviron)) if err == nil { return st, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/backups/restore_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/backups/restore_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/backups/restore_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/backups/restore_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -24,6 +24,7 @@ "github.com/juju/juju/agent" "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/environs" "github.com/juju/juju/mongo/mongotest" "github.com/juju/juju/state" statetesting "github.com/juju/juju/state/testing" @@ -265,7 +266,11 @@ c.Assert(st.Close(), jc.ErrorIsNil) r.PatchValue(&mongoDefaultDialOpts, mongotest.DialOpts) - r.PatchValue(&environsNewStatePolicy, func() state.Policy { return nil }) + r.PatchValue(&environsGetNewPolicyFunc, func( + func(*state.State) (environs.Environ, error), + ) state.NewPolicyFunc { + return nil + }) st, err = newStateConnection(st.ModelTag(), statetesting.NewMongoInfo()) c.Assert(err, jc.ErrorIsNil) c.Assert(st.Close(), jc.ErrorIsNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/binarystorage_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/binarystorage_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/binarystorage_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/binarystorage_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -21,6 +21,7 @@ "github.com/juju/juju/mongo" "github.com/juju/juju/state" "github.com/juju/juju/state/binarystorage" + "github.com/juju/juju/storage" "github.com/juju/juju/testing" "github.com/juju/juju/tools" ) @@ -86,6 +87,7 @@ CloudName: "dummy", Config: cfg, Owner: names.NewLocalUserTag("test-admin"), + StorageProviderRegistry: storage.StaticProviderRegistry{}, }) c.Assert(err, jc.ErrorIsNil) s.AddCleanup(func(*gc.C) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/block_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/block_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/block_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/block_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -13,6 +13,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/state" + "github.com/juju/juju/storage" "github.com/juju/juju/testing" ) @@ -194,7 +195,10 @@ "uuid": uuid.String(), }) owner := names.NewUserTag("test@remote") - env, st, err := s.State.NewModel(state.ModelArgs{CloudName: "dummy", Config: cfg, Owner: owner}) + env, st, err := s.State.NewModel(state.ModelArgs{ + CloudName: "dummy", Config: cfg, Owner: owner, + StorageProviderRegistry: storage.StaticProviderRegistry{}, + }) c.Assert(err, jc.ErrorIsNil) return env, st } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/charm.go juju-core-2.0~beta15/src/github.com/juju/juju/state/charm.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/charm.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/charm.go 2016-08-16 08:56:25.000000000 +0000 @@ -392,12 +392,8 @@ return nil } -// deleteCharmArchive deletes a charm archive from blob storage -// and removes the corresponding charm record from state. +// deleteCharmArchive deletes a charm archive from blob storage. func (st *State) deleteCharmArchive(curl *charm.URL, storagePath string) error { - if err := st.deleteCharm(curl); err != nil { - return errors.Annotate(err, "cannot delete charm record from state") - } stor := storage.NewStorage(st.ModelUUID(), st.MongoSession()) if err := stor.Remove(storagePath); err != nil { return errors.Annotate(err, "cannot delete charm from storage") @@ -437,20 +433,6 @@ return nil, errors.Trace(err) } -// deleteCharm removes the charm record with curl from state. -func (st *State) deleteCharm(curl *charm.URL) error { - op := []txn.Op{{ - C: charmsC, - Id: curl.String(), - Remove: true, - }} - err := st.runTransaction(op) - if err == mgo.ErrNotFound { - return nil - } - return errors.Trace(err) -} - type hasMeta interface { Meta() *charm.Meta } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/charm_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/charm_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/charm_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/charm_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -499,22 +499,6 @@ c.Assert(charms[2].URL(), gc.DeepEquals, curl2) } -func (s *CharmSuite) TestDeleteCharm(c *gc.C) { - info := s.dummyCharm(c, "cs:quantal/dummy-1") - sch, err := s.State.AddCharm(info) - c.Assert(err, jc.ErrorIsNil) - - err = state.DeleteCharm(s.State, sch.URL()) - c.Assert(err, jc.ErrorIsNil) - - _, err = s.State.Charm(sch.URL()) - c.Assert(err, jc.Satisfies, errors.IsNotFound) - - // Deleting again is a no-op. - err = state.DeleteCharm(s.State, sch.URL()) - c.Assert(err, jc.ErrorIsNil) -} - type CharmTestHelperSuite struct { ConnSuite } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/cleanup.go juju-core-2.0~beta15/src/github.com/juju/juju/state/cleanup.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/cleanup.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/cleanup.go 2016-08-16 08:56:25.000000000 +0000 @@ -308,14 +308,26 @@ } // Mark storage attachments as dying, so that they are detached // and removed from state, allowing the unit to terminate. - storageAttachments, err := st.UnitStorageAttachments(unit.UnitTag()) + return st.cleanupUnitStorageAttachments(unit.UnitTag(), false) +} + +func (st *State) cleanupUnitStorageAttachments(unitTag names.UnitTag, remove bool) error { + storageAttachments, err := st.UnitStorageAttachments(unitTag) if err != nil { return err } for _, storageAttachment := range storageAttachments { - err := st.DestroyStorageAttachment( - storageAttachment.StorageInstance(), unit.UnitTag(), - ) + storageTag := storageAttachment.StorageInstance() + err := st.DestroyStorageAttachment(storageTag, unitTag) + if errors.IsNotFound(err) { + continue + } else if err != nil { + return err + } + if !remove { + continue + } + err = st.RemoveStorageAttachment(storageTag, unitTag) if errors.IsNotFound(err) { continue } else if err != nil { @@ -330,15 +342,24 @@ func (st *State) cleanupRemovedUnit(unitId string) error { actions, err := st.matchingActionsByReceiverId(unitId) if err != nil { - return err + return errors.Trace(err) + } + cancelled := ActionResults{ + Status: ActionCancelled, + Message: "unit removed", } - - cancelled := ActionResults{Status: ActionCancelled, Message: "unit removed"} for _, action := range actions { if _, err = action.Finish(cancelled); err != nil { - return err + return errors.Trace(err) } } + + change := payloadCleanupChange{ + Unit: unitId, + } + if err := Apply(st.database, change); err != nil { + return errors.Trace(err) + } return nil } @@ -364,9 +385,6 @@ } else if err != nil { return err } - if err := cleanupDyingMachineResources(machine); err != nil { - return err - } // In an ideal world, we'd call machine.Destroy() here, and thus prevent // new dependencies being added while we clean up the ones we know about. // But machine destruction is unsophisticated, and doesn't allow for @@ -380,6 +398,9 @@ return err } } + if err := cleanupDyingMachineResources(machine); err != nil { + return err + } // We need to refresh the machine at this point, because the local copy // of the document will not reflect changes caused by the unit cleanups // above, and may thus fail immediately. @@ -484,6 +505,10 @@ } else if err != nil { return err } + // Destroy and remove all storage attachments for the unit. + if err := st.cleanupUnitStorageAttachments(unit.UnitTag(), true); err != nil { + return errors.Annotatef(err, "cannot destroy storage for unit %q", unitName) + } for _, subName := range unit.SubordinateNames() { if err := st.obliterateUnit(subName); err != nil { return err diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/cleanup_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/cleanup_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/cleanup_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/cleanup_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,7 +5,6 @@ import ( "bytes" - "fmt" "github.com/juju/errors" jc "github.com/juju/testing/checkers" @@ -16,8 +15,6 @@ "github.com/juju/juju/instance" "github.com/juju/juju/state" "github.com/juju/juju/state/storage" - "github.com/juju/juju/storage/provider" - "github.com/juju/juju/storage/provider/registry" "github.com/juju/juju/testing/factory" ) @@ -27,11 +24,6 @@ var _ = gc.Suite(&CleanupSuite{}) -func (s *CleanupSuite) SetUpSuite(c *gc.C) { - s.ConnSuite.SetUpSuite(c) - registry.RegisterEnvironStorageProviders("someprovider", provider.LoopProviderType) -} - func (s *CleanupSuite) SetUpTest(c *gc.C) { s.ConnSuite.SetUpTest(c) s.assertDoesNotNeedCleanup(c) @@ -94,8 +86,6 @@ s.assertCleanupRuns(c) _, _, err = stor.Get(storagePath) c.Assert(err, jc.Satisfies, errors.IsNotFound) - _, err = s.State.Charm(ch.URL()) - c.Assert(err, jc.Satisfies, errors.IsNotFound) } func (s *CleanupSuite) TestCleanupControllerModels(c *gc.C) { @@ -244,8 +234,7 @@ c.Assert(err, jc.ErrorIsNil) s.assertDoesNotNeedCleanup(c) err = manager.ForceDestroy() - expect := fmt.Sprintf("machine is required by the model") - c.Assert(err, gc.ErrorMatches, expect) + c.Assert(err, gc.ErrorMatches, "machine is required by the model") s.assertDoesNotNeedCleanup(c) assertLife(c, manager, state.Alive) } @@ -280,6 +269,50 @@ assertLife(c, machine, state.Dead) } +func (s *CleanupSuite) TestCleanupForceDestroyMachineCleansStorageAttachments(c *gc.C) { + machine, err := s.State.AddMachine("quantal", state.JobHostUnits) + c.Assert(err, jc.ErrorIsNil) + s.assertDoesNotNeedCleanup(c) + + ch := s.AddTestingCharm(c, "storage-block") + storage := map[string]state.StorageConstraints{ + "data": makeStorageCons("loop", 1024, 1), + } + service := s.AddTestingServiceWithStorage(c, "storage-block", ch, storage) + u, err := service.AddUnit() + c.Assert(err, jc.ErrorIsNil) + err = u.AssignToMachine(machine) + c.Assert(err, jc.ErrorIsNil) + + // check no cleanups + s.assertDoesNotNeedCleanup(c) + + // this tag matches the storage instance created for the unit above. + storageTag := names.NewStorageTag("data/0") + + sa, err := s.State.StorageAttachment(storageTag, u.UnitTag()) + c.Assert(err, jc.ErrorIsNil) + c.Assert(sa.Life(), gc.Equals, state.Alive) + + // destroy machine and run cleanups + err = machine.ForceDestroy() + c.Assert(err, jc.ErrorIsNil) + s.assertCleanupCount(c, 2) + + // After running the cleanup, the storage attachment and instance + // should both be removed. + _, err = s.State.StorageAttachment(storageTag, u.UnitTag()) + c.Assert(err, jc.Satisfies, errors.IsNotFound) + _, err = s.State.StorageInstance(storageTag) + c.Assert(err, jc.Satisfies, errors.IsNotFound) + + // Check that the unit has been removed. + assertRemoved(c, u) + + // check no cleanups + s.assertDoesNotNeedCleanup(c) +} + func (s *CleanupSuite) TestCleanupForceDestroyedMachineWithContainer(c *gc.C) { // Create a machine with a container. machine, err := s.State.AddMachine("quantal", state.JobHostUnits) @@ -481,7 +514,6 @@ func (s *CleanupSuite) TestCleanupStorageInstances(c *gc.C) { ch := s.AddTestingCharm(c, "storage-block") - registry.RegisterEnvironStorageProviders("someprovider", provider.LoopProviderType) storage := map[string]state.StorageConstraints{ "data": makeStorageCons("loop", 1024, 1), } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/cloudcredentials.go juju-core-2.0~beta15/src/github.com/juju/juju/state/cloudcredentials.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/cloudcredentials.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/cloudcredentials.go 2016-08-16 08:56:25.000000000 +0000 @@ -40,7 +40,7 @@ for iter.Next(&doc) { credentials[doc.Name] = doc.toCredential() } - if err := iter.Close(); err != nil { + if err := iter.Err(); err != nil { return nil, errors.Annotatef( err, "cannot get cloud credentials for user %q, cloud %q", user.Canonical(), cloudName, @@ -62,7 +62,17 @@ if err != nil { return nil, errors.Annotate(err, "validating cloud credentials") } - ops = append(ops, updateCloudCredentialsOps(user, cloudName, credentials)...) + existingCreds, err := st.CloudCredentials(user, cloudName) + if err != nil { + return nil, errors.Maskf(err, "fetching cloud credentials") + } + for credName, cred := range credentials { + if _, ok := existingCreds[credName]; ok { + ops = append(ops, updateCloudCredentialOp(user, cloudName, credName, cred)) + } else { + ops = append(ops, createCloudCredentialOp(user, cloudName, credName, cred)) + } + } return ops, nil } if err := st.run(buildTxn); err != nil { @@ -74,26 +84,35 @@ return nil } -// updateCloudCredentialsOps returns a list of txn.Ops that will create -// or update a set of cloud credentials for a user. -func updateCloudCredentialsOps(user names.UserTag, cloudName string, credentials map[string]cloud.Credential) []txn.Op { - owner := user.Canonical() - ops := make([]txn.Op, 0, len(credentials)) - for name, credential := range credentials { - ops = append(ops, txn.Op{ - C: cloudCredentialsC, - Id: cloudCredentialDocID(user, cloudName, name), - Assert: txn.DocMissing, - Insert: &cloudCredentialDoc{ - Owner: owner, - Cloud: cloudName, - Name: name, - AuthType: string(credential.AuthType()), - Attributes: credential.Attributes(), - }, - }) +// createCloudCredentialOp returns a txn.Op that will create +// a cloud credential. +func createCloudCredentialOp(user names.UserTag, cloudName, credName string, cred cloud.Credential) txn.Op { + return txn.Op{ + C: cloudCredentialsC, + Id: cloudCredentialDocID(user, cloudName, credName), + Assert: txn.DocMissing, + Insert: &cloudCredentialDoc{ + Owner: user.Canonical(), + Cloud: cloudName, + Name: credName, + AuthType: string(cred.AuthType()), + Attributes: cred.Attributes(), + }, + } +} + +// updateCloudCredentialOp returns a txn.Op that will update +// a cloud credential. +func updateCloudCredentialOp(user names.UserTag, cloudName, credName string, cred cloud.Credential) txn.Op { + return txn.Op{ + C: cloudCredentialsC, + Id: cloudCredentialDocID(user, cloudName, credName), + Assert: txn.DocExists, + Update: bson.D{{"$set", bson.D{ + {"auth-type", string(cred.AuthType())}, + {"attributes", cred.Attributes()}, + }}}, } - return ops } func cloudCredentialDocID(user names.UserTag, cloudName, credentialName string) string { @@ -109,6 +128,14 @@ // validateCloudCredentials checks that the supplied cloud credentials are // valid for use with the controller's cloud, and returns a set of txn.Ops // to assert the same in a transaction. +// +// TODO(rogpeppe) We're going to a lot of effort here to assert that a +// cloud's auth types haven't changed since we looked at them a moment +// ago, but we don't support changing a cloud's definition currently and +// it's not clear that doing so would be a good idea, as changing a +// cloud's auth type would invalidate all existing credentials and would +// usually involve a new provider version and juju binary too, so +// perhaps all this code is unnecessary. func validateCloudCredentials(cloud cloud.Cloud, cloudName string, credentials map[string]cloud.Credential) ([]txn.Op, error) { requiredAuthTypes := make(set.Strings) for name, credential := range credentials { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/cloudcredentials_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/cloudcredentials_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/cloudcredentials_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/cloudcredentials_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,139 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package state_test + +import ( + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/juju/names.v2" + + "github.com/juju/juju/cloud" +) + +type CloudCredentialsSuite struct { + ConnSuite +} + +var _ = gc.Suite(&CloudCredentialsSuite{}) + +func (s *CloudCredentialsSuite) TestUpdateCloudCredentialsNew(c *gc.C) { + err := s.State.AddCloud("stratus", cloud.Cloud{ + Type: "low", + AuthTypes: cloud.AuthTypes{cloud.AccessKeyAuthType, cloud.UserPassAuthType}, + }) + c.Assert(err, jc.ErrorIsNil) + + creds := map[string]cloud.Credential{ + "cred1": cloud.NewCredential(cloud.AccessKeyAuthType, map[string]string{ + "foo": "foo val", + "bar": "bar val", + }), + "cred2": cloud.NewCredential(cloud.AccessKeyAuthType, map[string]string{ + "a": "a val", + "b": "b val", + }), + "cred3": cloud.NewCredential(cloud.UserPassAuthType, map[string]string{ + "user": "bob", + "password": "bob's password", + }), + } + addCredLabels(creds) + + err = s.State.UpdateCloudCredentials(names.NewUserTag("bob"), "stratus", creds) + c.Assert(err, jc.ErrorIsNil) + // The retrieved credentials have labels although cloud.NewCredential + // doesn't have them, so add them. + for name, cred := range creds { + cred.Label = name + creds[name] = cred + } + creds1, err := s.State.CloudCredentials(names.NewUserTag("bob"), "stratus") + c.Assert(err, jc.ErrorIsNil) + c.Assert(creds1, jc.DeepEquals, creds) +} + +func (s *CloudCredentialsSuite) TestCloudCredentialsEmpty(c *gc.C) { + creds, err := s.State.CloudCredentials(names.NewUserTag("bob"), "dummy") + c.Assert(err, jc.ErrorIsNil) + c.Assert(creds, gc.HasLen, 0) +} + +func (s *CloudCredentialsSuite) TestUpdateCloudCredentialsExisting(c *gc.C) { + err := s.State.AddCloud("stratus", cloud.Cloud{ + Type: "low", + AuthTypes: cloud.AuthTypes{cloud.AccessKeyAuthType, cloud.UserPassAuthType}, + }) + c.Assert(err, jc.ErrorIsNil) + err = s.State.UpdateCloudCredentials(names.NewUserTag("bob"), "stratus", map[string]cloud.Credential{ + "cred1": cloud.NewCredential(cloud.AccessKeyAuthType, map[string]string{ + "foo": "foo val", + "bar": "bar val", + }), + "cred2": cloud.NewCredential(cloud.AccessKeyAuthType, map[string]string{ + "a": "a val", + "b": "b val", + }), + "cred3": cloud.NewCredential(cloud.UserPassAuthType, map[string]string{ + "user": "bob", + "password": "bob's password", + }), + }) + c.Assert(err, jc.ErrorIsNil) + err = s.State.UpdateCloudCredentials(names.NewUserTag("bob"), "stratus", map[string]cloud.Credential{ + "cred1": cloud.NewCredential(cloud.UserPassAuthType, map[string]string{ + "user": "bob's nephew", + "password": "simple", + }), + "cred2": cloud.NewCredential(cloud.AccessKeyAuthType, map[string]string{ + "b": "new b val", + }), + "cred4": cloud.NewCredential(cloud.AccessKeyAuthType, map[string]string{ + "d": "d val", + }), + }) + c.Assert(err, jc.ErrorIsNil) + + expect := map[string]cloud.Credential{ + "cred1": cloud.NewCredential(cloud.UserPassAuthType, map[string]string{ + "user": "bob's nephew", + "password": "simple", + }), + "cred2": cloud.NewCredential(cloud.AccessKeyAuthType, map[string]string{ + "b": "new b val", + }), + "cred3": cloud.NewCredential(cloud.UserPassAuthType, map[string]string{ + "user": "bob", + "password": "bob's password", + }), + "cred4": cloud.NewCredential(cloud.AccessKeyAuthType, map[string]string{ + "d": "d val", + }), + } + addCredLabels(expect) + + creds1, err := s.State.CloudCredentials(names.NewUserTag("bob"), "stratus") + c.Assert(err, jc.ErrorIsNil) + c.Assert(creds1, jc.DeepEquals, expect) +} + +func (s *CloudCredentialsSuite) TestUpdateCloudCredentialsInvalidAuthType(c *gc.C) { + err := s.State.AddCloud("stratus", cloud.Cloud{ + Type: "low", + AuthTypes: cloud.AuthTypes{cloud.AccessKeyAuthType}, + }) + err = s.State.UpdateCloudCredentials(names.NewUserTag("bob"), "stratus", map[string]cloud.Credential{ + "cred1": cloud.NewCredential(cloud.UserPassAuthType, nil), + }) + c.Assert(err, gc.ErrorMatches, `updating cloud credentials for user "user-bob", cloud "stratus": validating cloud credentials: credential "cred1" with auth-type "userpass" is not supported \(expected one of \["access-key"\]\)`) +} + +// addCredLabels adds labels to all the given credentials, because +// the labels are present when the credentials are returned from the +// state but not when created with NewCredential. +func addCredLabels(creds map[string]cloud.Credential) { + for name, cred := range creds { + cred.Label = name + creds[name] = cred + } +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/cloud.go juju-core-2.0~beta15/src/github.com/juju/juju/state/cloud.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/cloud.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/cloud.go 2016-08-16 08:56:25.000000000 +0000 @@ -107,6 +107,23 @@ return doc.toCloud(), nil } +// AddCloud creates a cloud with the given name and details. +// Note that the Config is deliberately ignored - it's only +// relevant when bootstrapping. +func (st *State) AddCloud(name string, c cloud.Cloud) error { + if err := validateCloud(c); err != nil { + return errors.Annotate(err, "invalid cloud") + } + ops := []txn.Op{createCloudOp(c, name)} + if err := st.runTransaction(ops); err != nil { + if err == txn.ErrAborted { + err = errors.AlreadyExistsf("cloud %q", name) + } + return err + } + return nil +} + // validateCloud checks that the supplied cloud is valid. func validateCloud(cloud cloud.Cloud) error { if cloud.Type == "" { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/cloudimagemetadata/image.go juju-core-2.0~beta15/src/github.com/juju/juju/state/cloudimagemetadata/image.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/cloudimagemetadata/image.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/cloudimagemetadata/image.go 2016-08-16 08:56:25.000000000 +0000 @@ -372,16 +372,3 @@ } return arches, nil } - -// MetadataArchitectureQuerier isolates querying supported architectures. -type MetadataArchitectureQuerier struct { - Storage Storage -} - -// SupportedArchitectures implements state policy SupportedArchitecturesQuerier.SupportedArchitectures. -func (q *MetadataArchitectureQuerier) SupportedArchitectures(stream, region string) ([]string, error) { - return q.Storage.SupportedArchitectures(MetadataFilter{ - Stream: stream, - Region: region, - }) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/cloud_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/cloud_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/cloud_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/cloud_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,76 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package state_test + +import ( + "github.com/juju/errors" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cloud" +) + +type CloudSuite struct { + ConnSuite +} + +var _ = gc.Suite(&CloudSuite{}) + +func (s *CloudSuite) TestCloudNotFound(c *gc.C) { + cld, err := s.State.Cloud("unknown") + c.Assert(err, gc.ErrorMatches, `cloud "unknown" not found`) + c.Assert(cld, jc.DeepEquals, cloud.Cloud{}) + c.Assert(err, jc.Satisfies, errors.IsNotFound) +} + +func (s *CloudSuite) TestAddCloud(c *gc.C) { + cld := cloud.Cloud{ + Type: "low", + AuthTypes: cloud.AuthTypes{cloud.AccessKeyAuthType, cloud.UserPassAuthType}, + Endpoint: "global-endpoint", + StorageEndpoint: "global-storage", + Regions: []cloud.Region{{ + Name: "region1", + Endpoint: "region1-endpoint", + StorageEndpoint: "region1-storage", + }, { + Name: "region2", + Endpoint: "region2-endpoint", + StorageEndpoint: "region2-storage", + }}, + } + err := s.State.AddCloud("stratus", cld) + c.Assert(err, jc.ErrorIsNil) + cld1, err := s.State.Cloud("stratus") + c.Assert(err, jc.ErrorIsNil) + c.Assert(cld1, jc.DeepEquals, cld) +} + +func (s *CloudSuite) TestAddCloudDuplicate(c *gc.C) { + err := s.State.AddCloud("stratus", cloud.Cloud{ + Type: "low", + AuthTypes: cloud.AuthTypes{cloud.AccessKeyAuthType, cloud.UserPassAuthType}, + }) + c.Assert(err, jc.ErrorIsNil) + err = s.State.AddCloud("stratus", cloud.Cloud{ + Type: "low", + AuthTypes: cloud.AuthTypes{cloud.AccessKeyAuthType, cloud.UserPassAuthType}, + }) + c.Assert(err, gc.ErrorMatches, `cloud "stratus" already exists`) + c.Assert(err, jc.Satisfies, errors.IsAlreadyExists) +} + +func (s *CloudSuite) TestAddCloudNoType(c *gc.C) { + err := s.State.AddCloud("stratus", cloud.Cloud{ + AuthTypes: cloud.AuthTypes{cloud.AccessKeyAuthType, cloud.UserPassAuthType}, + }) + c.Assert(err, gc.ErrorMatches, `invalid cloud: empty Type not valid`) +} + +func (s *CloudSuite) TestAddCloudNoAuthTypes(c *gc.C) { + err := s.State.AddCloud("stratus", cloud.Cloud{ + Type: "foo", + }) + c.Assert(err, gc.ErrorMatches, `invalid cloud: empty auth-types not valid`) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/collection.go juju-core-2.0~beta15/src/github.com/juju/juju/state/collection.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/collection.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/collection.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,10 +4,10 @@ package state import ( + "github.com/juju/errors" "gopkg.in/mgo.v2" "gopkg.in/mgo.v2/bson" - "github.com/juju/errors" "github.com/juju/juju/mongo" ) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/configvalidator_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/configvalidator_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/configvalidator_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/configvalidator_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -53,7 +53,7 @@ func (s *ConfigValidatorSuite) SetUpTest(c *gc.C) { s.ConnSuite.SetUpTest(c) s.configValidator = mockConfigValidator{} - s.policy.GetConfigValidator = func(string) (state.ConfigValidator, error) { + s.policy.GetConfigValidator = func() (config.Validator, error) { return &s.configValidator, nil } } @@ -73,7 +73,7 @@ func (s *ConfigValidatorSuite) TestUpdateModelConfigFailsOnConfigValidateError(c *gc.C) { var configValidatorErr error - s.policy.GetConfigValidator = func(string) (state.ConfigValidator, error) { + s.policy.GetConfigValidator = func() (config.Validator, error) { configValidatorErr = errors.NotFoundf("") return &s.configValidator, configValidatorErr } @@ -93,7 +93,7 @@ func (s *ConfigValidatorSuite) TestConfigValidateUnimplemented(c *gc.C) { var configValidatorErr error - s.policy.GetConfigValidator = func(string) (state.ConfigValidator, error) { + s.policy.GetConfigValidator = func() (config.Validator, error) { return nil, configValidatorErr } @@ -105,7 +105,7 @@ } func (s *ConfigValidatorSuite) TestConfigValidateNoPolicy(c *gc.C) { - s.policy.GetConfigValidator = func(providerType string) (state.ConfigValidator, error) { + s.policy.GetConfigValidator = func() (config.Validator, error) { c.Errorf("should not have been invoked") return nil, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/conn_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/conn_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/conn_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/conn_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,8 +10,10 @@ "gopkg.in/juju/names.v2" "gopkg.in/mgo.v2" + "github.com/juju/juju/provider/dummy" "github.com/juju/juju/state" statetesting "github.com/juju/juju/state/testing" + "github.com/juju/juju/storage" "github.com/juju/juju/testing" ) @@ -34,8 +36,14 @@ func (cs *ConnSuite) SetUpTest(c *gc.C) { c.Log("SetUpTest") - cs.policy = statetesting.MockPolicy{} - cs.StateSuite.Policy = &cs.policy + cs.policy = statetesting.MockPolicy{ + GetStorageProviderRegistry: func() (storage.ProviderRegistry, error) { + return dummy.StorageProviders(), nil + }, + } + cs.StateSuite.NewPolicy = func(*state.State) state.Policy { + return &cs.policy + } cs.StateSuite.SetUpTest(c) @@ -107,7 +115,10 @@ "uuid": utils.MustNewUUID().String(), }) otherOwner := names.NewLocalUserTag("test-admin") - _, otherState, err := s.State.NewModel(state.ModelArgs{CloudName: "dummy", Config: cfg, Owner: otherOwner}) + _, otherState, err := s.State.NewModel(state.ModelArgs{ + CloudName: "dummy", Config: cfg, Owner: otherOwner, + StorageProviderRegistry: storage.StaticProviderRegistry{}, + }) c.Assert(err, jc.ErrorIsNil) s.AddCleanup(func(*gc.C) { otherState.Close() }) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/constraints.go juju-core-2.0~beta15/src/github.com/juju/juju/state/constraints.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/constraints.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/constraints.go 2016-08-16 08:56:25.000000000 +0000 @@ -27,10 +27,11 @@ Container *instance.ContainerType Tags *[]string Spaces *[]string + VirtType *string } func (doc constraintsDoc) value() constraints.Value { - return constraints.Value{ + result := constraints.Value{ Arch: doc.Arch, CpuCores: doc.CpuCores, CpuPower: doc.CpuPower, @@ -40,11 +41,13 @@ Container: doc.Container, Tags: doc.Tags, Spaces: doc.Spaces, + VirtType: doc.VirtType, } + return result } func newConstraintsDoc(st *State, cons constraints.Value) constraintsDoc { - return constraintsDoc{ + result := constraintsDoc{ Arch: cons.Arch, CpuCores: cons.CpuCores, CpuPower: cons.CpuPower, @@ -54,7 +57,9 @@ Container: cons.Container, Tags: cons.Tags, Spaces: cons.Spaces, + VirtType: cons.VirtType, } + return result } func createConstraintsOp(st *State, id string, cons constraints.Value) txn.Op { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/constraintsvalidation_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/constraintsvalidation_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/constraintsvalidation_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/constraintsvalidation_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,14 +4,25 @@ package state_test import ( + "regexp" + + "github.com/juju/errors" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" "github.com/juju/juju/constraints" - "github.com/juju/juju/environs/config" "github.com/juju/juju/state" ) +type applicationConstraintsSuite struct { + ConnSuite + + applicationName string + testCharm *state.Charm +} + +var _ = gc.Suite(&applicationConstraintsSuite{}) + type constraintsValidationSuite struct { ConnSuite } @@ -20,7 +31,7 @@ func (s *constraintsValidationSuite) SetUpTest(c *gc.C) { s.ConnSuite.SetUpTest(c) - s.policy.GetConstraintsValidator = func(*config.Config, state.SupportedArchitecturesQuerier) (constraints.Validator, error) { + s.policy.GetConstraintsValidator = func() (constraints.Validator, error) { validator := constraints.NewValidator() validator.RegisterConflicts( []string{constraints.InstanceType}, @@ -174,6 +185,30 @@ effectiveServiceCons: "container=kvm arch=amd64", effectiveUnitCons: "container=kvm mem=8G arch=amd64", effectiveMachineCons: "mem=8G arch=amd64", +}, { + about: "specify image virt-type when deploying applications on multi-hypervisor aware openstack", + consToSet: "virt-type=kvm", + consFallback: "", + + // application deployment constraints are transformed into machine + // provisioning constraints. Unit constraints must also have virt-type set + // to ensure consistency in scalability. + effectiveModelCons: "", + effectiveServiceCons: "virt-type=kvm", + effectiveUnitCons: "virt-type=kvm", + effectiveMachineCons: "virt-type=kvm", +}, { + about: "ensure model and application constraints are separate", + consToSet: "virt-type=kvm", + consFallback: "mem=2G", + + // application deployment constraints are transformed into machine + // provisioning constraints. Unit constraints must also have virt-type set + // to ensure consistency in scalability. + effectiveModelCons: "mem=2G", + effectiveServiceCons: "virt-type=kvm", + effectiveUnitCons: "mem=2G virt-type=kvm", + effectiveMachineCons: "mem=2G virt-type=kvm", }} func (s *constraintsValidationSuite) TestMachineConstraints(c *gc.C) { @@ -231,3 +266,37 @@ c.Check(scons, jc.DeepEquals, constraints.MustParse(t.effectiveServiceCons)) } } + +func (s *applicationConstraintsSuite) SetUpTest(c *gc.C) { + s.ConnSuite.SetUpTest(c) + s.policy.GetConstraintsValidator = func() (constraints.Validator, error) { + validator := constraints.NewValidator() + validator.RegisterVocabulary(constraints.VirtType, []string{"kvm"}) + return validator, nil + } + s.applicationName = "wordpress" + s.testCharm = s.AddTestingCharm(c, s.applicationName) +} + +func (s *applicationConstraintsSuite) TestAddApplicationInvalidConstraints(c *gc.C) { + cons := constraints.MustParse("virt-type=blah") + _, err := s.State.AddApplication(state.AddApplicationArgs{ + Name: s.applicationName, + Series: "", + Charm: s.testCharm, + Constraints: cons, + }) + c.Assert(errors.Cause(err), gc.ErrorMatches, regexp.QuoteMeta("invalid constraint value: virt-type=blah\nvalid values are: [kvm]")) +} + +func (s *applicationConstraintsSuite) TestAddApplicationValidConstraints(c *gc.C) { + cons := constraints.MustParse("virt-type=kvm") + service, err := s.State.AddApplication(state.AddApplicationArgs{ + Name: s.applicationName, + Series: "", + Charm: s.testCharm, + Constraints: cons, + }) + c.Assert(err, jc.ErrorIsNil) + c.Assert(service, gc.NotNil) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/controller.go juju-core-2.0~beta15/src/github.com/juju/juju/state/controller.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/controller.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/controller.go 2016-08-16 08:56:25.000000000 +0000 @@ -13,8 +13,8 @@ // controllerSettingsGlobalKey is the key for the controller and its settings. controllerSettingsGlobalKey = "controllerSettings" - // controllerInheritedSettingsGlobalKey is the key for default settings shared across models. - controllerInheritedSettingsGlobalKey = "controllerInheritedSettings" + // controllerGlobalKey is the key for controller. + controllerGlobalKey = "c" ) // ControllerConfig returns the config values for the controller. diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/controlleruser.go juju-core-2.0~beta15/src/github.com/juju/juju/state/controlleruser.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/controlleruser.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/controlleruser.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,91 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package state + +import ( + "fmt" + "strings" + "time" + + "github.com/juju/errors" + "github.com/juju/juju/core/description" + "gopkg.in/juju/names.v2" + "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/txn" +) + +const defaultControllerPermission = description.LoginAccess + +// setAccess changes the user's access permissions on the controller. +func (st *State) setControllerAccess(access description.Access, userGlobalKey string) error { + if err := access.Validate(); err != nil { + return errors.Trace(err) + } + op := updatePermissionOp(controllerGlobalKey, userGlobalKey, access) + err := st.runTransaction([]txn.Op{op}) + if err == txn.ErrAborted { + return errors.NotFoundf("existing permissions") + } + return errors.Trace(err) +} + +// controllerUser a model userAccessDoc. +func (st *State) controllerUser(user names.UserTag) (userAccessDoc, error) { + controllerUser := userAccessDoc{} + controllerUsers, closer := st.getCollection(controllerUsersC) + defer closer() + + username := strings.ToLower(user.Canonical()) + err := controllerUsers.FindId(username).One(&controllerUser) + if err == mgo.ErrNotFound { + return userAccessDoc{}, errors.NotFoundf("controller user %q", user.Canonical()) + } + // DateCreated is inserted as UTC, but read out as local time. So we + // convert it back to UTC here. + controllerUser.DateCreated = controllerUser.DateCreated.UTC() + return controllerUser, nil +} + +func createControllerUserOps(controllerUUID string, user, createdBy names.UserTag, displayName string, dateCreated time.Time, access description.Access) []txn.Op { + creatorname := createdBy.Canonical() + doc := &userAccessDoc{ + ID: userAccessID(user), + ObjectUUID: controllerUUID, + UserName: user.Canonical(), + DisplayName: displayName, + CreatedBy: creatorname, + DateCreated: dateCreated, + } + ops := []txn.Op{ + createPermissionOp(controllerGlobalKey, userGlobalKey(userAccessID(user)), access), + { + C: controllerUsersC, + Id: userAccessID(user), + Assert: txn.DocMissing, + Insert: doc, + }, + } + return ops +} + +// RemoveControllerUser removes a user from the database. +func (st *State) removeControllerUser(user names.UserTag) error { + ops := []txn.Op{ + removePermissionOp(controllerGlobalKey, userGlobalKey(userAccessID(user))), + { + C: controllerUsersC, + Id: userAccessID(user), + Assert: txn.DocExists, + Remove: true, + }} + + err := st.runTransaction(ops) + if err == txn.ErrAborted { + err = errors.NewNotFound(nil, fmt.Sprintf("controller user %q does not exist", user.Canonical())) + } + if err != nil { + return errors.Trace(err) + } + return nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/controlleruser_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/controlleruser_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/controlleruser_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/controlleruser_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,86 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package state_test + +import ( + "github.com/juju/errors" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/juju/names.v2" + + "github.com/juju/juju/core/description" + "github.com/juju/juju/testing/factory" +) + +type ControllerUserSuite struct { + ConnSuite +} + +var _ = gc.Suite(&ControllerUserSuite{}) + +type accessAwareUser interface { + Access() description.Access +} + +func (s *ControllerUserSuite) TestDefaultAccessControllerUser(c *gc.C) { + user := s.Factory.MakeUser(c, + &factory.UserParams{ + Name: "validusername", + }) + _ = s.Factory.MakeUser(c, &factory.UserParams{Name: "createdby"}) + t := user.Tag() + userTag := t.(names.UserTag) + ctag := names.NewControllerTag(s.State.ControllerUUID()) + controllerUser, err := s.State.UserAccess(userTag, ctag) + c.Assert(err, jc.ErrorIsNil) + c.Assert(controllerUser.Access, gc.Equals, description.LoginAccess) +} + +func (s *ControllerUserSuite) TestSetAccessControllerUser(c *gc.C) { + user := s.Factory.MakeUser(c, + &factory.UserParams{ + Name: "validusername", + }) + _ = s.Factory.MakeUser(c, &factory.UserParams{Name: "createdby"}) + t := user.Tag() + userTag := t.(names.UserTag) + ctag := names.NewControllerTag(s.State.ControllerUUID()) + controllerUser, err := s.State.UserAccess(userTag, ctag) + c.Assert(err, jc.ErrorIsNil) + c.Assert(controllerUser.Access, gc.Equals, description.LoginAccess) + + s.State.SetUserAccess(userTag, ctag, description.AddModelAccess) + + controllerUser, err = s.State.UserAccess(user.UserTag(), ctag) + c.Assert(controllerUser.Access, gc.Equals, description.AddModelAccess) +} + +func (s *ControllerUserSuite) TestRemoveControllerUser(c *gc.C) { + user := s.Factory.MakeUser(c, &factory.UserParams{Name: "validUsername"}) + ctag := names.NewControllerTag(s.State.ControllerUUID()) + _, err := s.State.UserAccess(user.UserTag(), ctag) + c.Assert(err, jc.ErrorIsNil) + + err = s.State.RemoveUserAccess(user.UserTag(), ctag) + c.Assert(err, jc.ErrorIsNil) + + _, err = s.State.UserAccess(user.UserTag(), ctag) + c.Assert(err, jc.Satisfies, errors.IsNotFound) +} + +func (s *ControllerUserSuite) TestRemoveControllerUserSucceeds(c *gc.C) { + user := s.Factory.MakeUser(c, &factory.UserParams{}) + ctag := names.NewControllerTag(s.State.ControllerUUID()) + err := s.State.RemoveUserAccess(user.UserTag(), ctag) + c.Assert(err, jc.ErrorIsNil) +} + +func (s *ControllerUserSuite) TestRemoveControllerUserFails(c *gc.C) { + user := s.Factory.MakeUser(c, &factory.UserParams{}) + ctag := names.NewControllerTag(s.State.ControllerUUID()) + err := s.State.RemoveUserAccess(user.UserTag(), ctag) + c.Assert(err, jc.ErrorIsNil) + err = s.State.RemoveUserAccess(user.UserTag(), ctag) + c.Assert(err, jc.Satisfies, errors.IsNotFound) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/database.go juju-core-2.0~beta15/src/github.com/juju/juju/state/database.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/database.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/database.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,6 +10,7 @@ jujutxn "github.com/juju/txn" "gopkg.in/juju/names.v2" "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/txn" "github.com/juju/juju/mongo" ) @@ -55,6 +56,45 @@ Schema() collectionSchema } +// Change represents any mgo/txn-representable change to a Database. +type Change interface { + + // Prepare ensures that db is in a valid base state for applying + // the change, and returns mgo/txn operations that will fail any + // enclosing transaction if the state has materially changed; or + // returns an error. + Prepare(db Database) ([]txn.Op, error) +} + +// ErrChangeComplete can be returned from Prepare to finish an Apply +// attempt and report success without taking any further action. +var ErrChangeComplete = errors.New("change complete") + +// Apply runs the supplied Change against the supplied Database. If it +// returns no error, the change succeeded. +func Apply(db Database, change Change) error { + db, closer := db.CopySession() + defer closer() + + buildTxn := func(int) ([]txn.Op, error) { + ops, err := change.Prepare(db) + if errors.Cause(err) == ErrChangeComplete { + return nil, jujutxn.ErrNoOperations + } + if err != nil { + return nil, errors.Trace(err) + } + return ops, nil + } + + runner, closer := db.TransactionRunner() + defer closer() + if err := runner.Run(buildTxn); err != nil { + return errors.Trace(err) + } + return nil +} + // collectionInfo describes important features of a collection. type collectionInfo struct { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/distribution.go juju-core-2.0~beta15/src/github.com/juju/juju/state/distribution.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/distribution.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/distribution.go 2016-08-16 08:56:25.000000000 +0000 @@ -22,11 +22,7 @@ if u.st.policy == nil { return candidates, nil } - cfg, err := u.st.ModelConfig() - if err != nil { - return nil, err - } - distributor, err := u.st.policy.InstanceDistributor(cfg) + distributor, err := u.st.policy.InstanceDistributor() if errors.IsNotImplemented(err) { return candidates, nil } else if err != nil { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/distribution_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/distribution_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/distribution_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/distribution_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,7 +10,6 @@ jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" - "github.com/juju/juju/environs/config" "github.com/juju/juju/instance" "github.com/juju/juju/state" ) @@ -44,7 +43,7 @@ func (s *InstanceDistributorSuite) SetUpTest(c *gc.C) { s.ConnSuite.SetUpTest(c) s.distributor = mockInstanceDistributor{} - s.policy.GetInstanceDistributor = func(*config.Config) (state.InstanceDistributor, error) { + s.policy.GetInstanceDistributor = func() (instance.Distributor, error) { return &s.distributor, nil } s.wordpress = s.AddTestingService( @@ -131,7 +130,7 @@ _, err = unit.AssignToCleanEmptyMachine() c.Assert(err, gc.ErrorMatches, ".*no assignment for you") // If the policy's InstanceDistributor method fails, that will be returned first. - s.policy.GetInstanceDistributor = func(*config.Config) (state.InstanceDistributor, error) { + s.policy.GetInstanceDistributor = func() (instance.Distributor, error) { return nil, fmt.Errorf("incapable of InstanceDistributor") } _, err = unit.AssignToCleanMachine() @@ -158,7 +157,7 @@ func (s *InstanceDistributorSuite) TestInstanceDistributorUnimplemented(c *gc.C) { s.setupScenario(c) var distributorErr error - s.policy.GetInstanceDistributor = func(*config.Config) (state.InstanceDistributor, error) { + s.policy.GetInstanceDistributor = func() (instance.Distributor, error) { return nil, distributorErr } unit, err := s.wordpress.AddUnit() @@ -171,7 +170,7 @@ } func (s *InstanceDistributorSuite) TestDistributeInstancesNoPolicy(c *gc.C) { - s.policy.GetInstanceDistributor = func(*config.Config) (state.InstanceDistributor, error) { + s.policy.GetInstanceDistributor = func() (instance.Distributor, error) { c.Errorf("should not have been invoked") return nil, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/environcapability_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/environcapability_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/environcapability_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/environcapability_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,129 +0,0 @@ -// Copyright 2014 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package state_test - -import ( - "fmt" - - "github.com/juju/errors" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/environs/config" - "github.com/juju/juju/instance" - "github.com/juju/juju/state" -) - -type EnvironCapabilitySuite struct { - ConnSuite - capability mockEnvironCapability -} - -var _ = gc.Suite(&EnvironCapabilitySuite{}) - -type mockEnvironCapability struct { - supportsUnitPlacementError error -} - -func (p *mockEnvironCapability) SupportedArchitectures() ([]string, error) { - panic("unused") -} - -func (p *mockEnvironCapability) SupportsUnitPlacement() error { - return p.supportsUnitPlacementError -} - -func (s *EnvironCapabilitySuite) SetUpTest(c *gc.C) { - s.ConnSuite.SetUpTest(c) - s.capability = mockEnvironCapability{} - s.policy.GetEnvironCapability = func(*config.Config) (state.EnvironCapability, error) { - return &s.capability, nil - } -} - -func (s *EnvironCapabilitySuite) addOneMachine(c *gc.C) (*state.Machine, error) { - return s.State.AddOneMachine(state.MachineTemplate{ - Series: "quantal", - Jobs: []state.MachineJob{state.JobHostUnits}, - }) -} - -func (s *EnvironCapabilitySuite) addOneMachineWithInstanceId(c *gc.C) (*state.Machine, error) { - return s.State.AddOneMachine(state.MachineTemplate{ - Series: "quantal", - Jobs: []state.MachineJob{state.JobHostUnits}, - InstanceId: "i-rate", - Nonce: "ya", - }) -} - -func (s *EnvironCapabilitySuite) addMachineInsideNewMachine(c *gc.C) error { - template := state.MachineTemplate{ - Series: "quantal", - Jobs: []state.MachineJob{state.JobHostUnits}, - } - _, err := s.State.AddMachineInsideNewMachine(template, template, instance.LXD) - return err -} - -func (s *EnvironCapabilitySuite) TestSupportsUnitPlacementAddMachine(c *gc.C) { - // Ensure that AddOneMachine fails when SupportsUnitPlacement returns an error. - s.capability.supportsUnitPlacementError = fmt.Errorf("no add-machine for you") - _, err := s.addOneMachine(c) - c.Assert(err, gc.ErrorMatches, ".*no add-machine for you") - err = s.addMachineInsideNewMachine(c) - c.Assert(err, gc.ErrorMatches, ".*no add-machine for you") - // If the policy's EnvironCapability method fails, that will be returned first. - s.policy.GetEnvironCapability = func(*config.Config) (state.EnvironCapability, error) { - return nil, fmt.Errorf("incapable of EnvironCapability") - } - _, err = s.addOneMachine(c) - c.Assert(err, gc.ErrorMatches, ".*incapable of EnvironCapability") -} - -func (s *EnvironCapabilitySuite) TestSupportsUnitPlacementAddMachineInstanceId(c *gc.C) { - // Ensure that AddOneMachine with a non-empty InstanceId does not fail. - s.capability.supportsUnitPlacementError = fmt.Errorf("no add-machine for you") - _, err := s.addOneMachineWithInstanceId(c) - c.Assert(err, jc.ErrorIsNil) -} - -func (s *EnvironCapabilitySuite) TestSupportsUnitPlacementUnitAssignment(c *gc.C) { - m, err := s.addOneMachine(c) - c.Assert(err, jc.ErrorIsNil) - - charm := s.AddTestingCharm(c, "wordpress") - service := s.AddTestingService(c, "wordpress", charm) - unit, err := service.AddUnit() - c.Assert(err, jc.ErrorIsNil) - - s.capability.supportsUnitPlacementError = fmt.Errorf("no unit placement for you") - err = unit.AssignToMachine(m) - c.Assert(err, gc.ErrorMatches, ".*no unit placement for you") - - err = unit.AssignToNewMachine() - c.Assert(err, jc.ErrorIsNil) -} - -func (s *EnvironCapabilitySuite) TestEnvironCapabilityUnimplemented(c *gc.C) { - var capabilityErr error - s.policy.GetEnvironCapability = func(*config.Config) (state.EnvironCapability, error) { - return nil, capabilityErr - } - _, err := s.addOneMachine(c) - c.Assert(err, gc.ErrorMatches, "cannot add a new machine: policy returned nil EnvironCapability without an error") - capabilityErr = errors.NotImplementedf("EnvironCapability") - _, err = s.addOneMachine(c) - c.Assert(err, jc.ErrorIsNil) -} - -func (s *EnvironCapabilitySuite) TestSupportsUnitPlacementNoPolicy(c *gc.C) { - s.policy.GetEnvironCapability = func(*config.Config) (state.EnvironCapability, error) { - c.Errorf("should not have been invoked") - return nil, nil - } - state.SetPolicy(s.State, nil) - _, err := s.addOneMachine(c) - c.Assert(err, jc.ErrorIsNil) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/export_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -21,6 +21,7 @@ "gopkg.in/mgo.v2/bson" "gopkg.in/mgo.v2/txn" + "github.com/juju/juju/core/description" "github.com/juju/juju/core/lease" "github.com/juju/juju/mongo" "github.com/juju/juju/network" @@ -38,6 +39,7 @@ StorageInstancesC = storageInstancesC GUISettingsC = guisettingsC GlobalSettingsC = globalSettingsC + SettingsC = settingsC ) var ( @@ -53,6 +55,7 @@ ApplicationGlobalKey = applicationGlobalKey ReadSettings = readSettings ControllerInheritedSettingsGlobalKey = controllerInheritedSettingsGlobalKey + ModelGlobalKey = modelGlobalKey MergeBindings = mergeBindings UpgradeInProgressError = errUpgradeInProgress ) @@ -457,8 +460,8 @@ var ActionNotificationIdToActionId = actionNotificationIdToActionId -func UpdateModelUserLastConnection(e *ModelUser, when time.Time) error { - return e.updateLastConnection(when) +func UpdateModelUserLastConnection(st *State, e description.UserAccess, when time.Time) error { + return st.updateLastModelConnection(e.UserTag, when) } func RemoveEndpointBindingsForService(c *gc.C, service *Application) { @@ -489,7 +492,3 @@ } return client.Leases(), nil } - -func DeleteCharm(st *State, curl *charm.URL) error { - return st.deleteCharm(curl) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/filesystem.go juju-core-2.0~beta15/src/github.com/juju/juju/state/filesystem.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/filesystem.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/filesystem.go 2016-08-16 08:56:25.000000000 +0000 @@ -753,29 +753,34 @@ ops = append(ops, volumeOps...) } - filesystemOps := []txn.Op{ - createStatusOp(st, filesystemGlobalKey(filesystemId), statusDoc{ - Status: status.StatusPending, - // TODO(fwereade): 2016-03-17 lp:1558657 - Updated: time.Now().UnixNano(), - }), + status := statusDoc{ + Status: status.StatusPending, + // TODO(fwereade): 2016-03-17 lp:1558657 + Updated: time.Now().UnixNano(), + } + doc := filesystemDoc{ + FilesystemId: filesystemId, + VolumeId: volumeId, + StorageId: params.storage.Id(), + Binding: params.binding.String(), + Params: ¶ms, + // Every filesystem is created with one attachment. + AttachmentCount: 1, + } + ops = append(ops, st.newFilesystemOps(doc, status)...) + return ops, filesystemTag, volumeTag, nil +} + +func (st *State) newFilesystemOps(doc filesystemDoc, status statusDoc) []txn.Op { + return []txn.Op{ + createStatusOp(st, filesystemGlobalKey(doc.FilesystemId), status), { C: filesystemsC, - Id: filesystemId, + Id: doc.FilesystemId, Assert: txn.DocMissing, - Insert: &filesystemDoc{ - FilesystemId: filesystemId, - VolumeId: volumeId, - StorageId: params.storage.Id(), - Binding: params.binding.String(), - Params: ¶ms, - // Every filesystem is created with one attachment. - AttachmentCount: 1, - }, + Insert: &doc, }, } - ops = append(ops, filesystemOps...) - return ops, filesystemTag, volumeTag, nil } func (st *State) filesystemParamsWithDefaults(params FilesystemParams) (FilesystemParams, error) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/initialize_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/initialize_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/initialize_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/initialize_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -16,6 +16,7 @@ "github.com/juju/juju/mongo/mongotest" "github.com/juju/juju/state" statetesting "github.com/juju/juju/state/testing" + "github.com/juju/juju/storage" "github.com/juju/juju/testing" ) @@ -47,7 +48,7 @@ modelTag, statetesting.NewMongoInfo(), mongotest.DialOpts(), - state.Policy(nil), + state.NewPolicyFunc(nil), ) c.Assert(err, jc.ErrorIsNil) s.State = st @@ -65,7 +66,6 @@ func (s *InitializeSuite) TestInitialize(c *gc.C) { cfg := testing.ModelConfig(c) uuid := cfg.UUID() - initial := cfg.AllAttrs() owner := names.NewLocalUserTag("initialize-admin") userpassCredential := cloud.NewCredential( @@ -88,11 +88,12 @@ st, err := state.Initialize(state.InitializeParams{ ControllerConfig: controllerCfg, ControllerModelArgs: state.ModelArgs{ - Owner: owner, - Config: cfg, - CloudName: "dummy", - CloudRegion: "some-region", - CloudCredential: "some-credential", + Owner: owner, + Config: cfg, + CloudName: "dummy", + CloudRegion: "some-region", + CloudCredential: "some-credential", + StorageProviderRegistry: storage.StaticProviderRegistry{}, }, CloudName: "dummy", Cloud: cloud.Cloud{ @@ -117,7 +118,13 @@ cfg, err = s.State.ModelConfig() c.Assert(err, jc.ErrorIsNil) - c.Assert(cfg.AllAttrs(), jc.DeepEquals, initial) + expected := cfg.AllAttrs() + for k, v := range config.ConfigDefaults() { + if _, ok := expected[k]; !ok { + expected[k] = v + } + } + c.Assert(cfg.AllAttrs(), jc.DeepEquals, expected) // Check that the model has been created. model, err := s.State.Model() c.Assert(err, jc.ErrorIsNil) @@ -130,10 +137,10 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(entity.Tag(), gc.Equals, owner) // Check that the owner has an ModelUser created for the bootstrapped model. - modelUser, err := s.State.ModelUser(model.Owner()) + modelUser, err := s.State.UserAccess(model.Owner(), model.Tag()) c.Assert(err, jc.ErrorIsNil) - c.Assert(modelUser.UserTag(), gc.Equals, owner) - c.Assert(modelUser.ModelTag(), gc.Equals, model.Tag()) + c.Assert(modelUser.UserTag, gc.Equals, owner) + c.Assert(modelUser.Object, gc.Equals, model.Tag()) // Check that the model can be found through the tag. entity, err = s.State.FindEntity(modelTag) @@ -167,9 +174,10 @@ _, err := state.Initialize(state.InitializeParams{ ControllerConfig: controllerCfg, ControllerModelArgs: state.ModelArgs{ - CloudName: "dummy", - Owner: owner, - Config: modelCfg, + CloudName: "dummy", + Owner: owner, + Config: modelCfg, + StorageProviderRegistry: storage.StaticProviderRegistry{}, }, CloudName: "dummy", Cloud: cloud.Cloud{ @@ -189,7 +197,7 @@ ) } -func (s *InitializeSuite) TestInitializeWithControllerinheritedconfig(c *gc.C) { +func (s *InitializeSuite) TestInitializeWithControllerInheritedConfig(c *gc.C) { cfg := testing.ModelConfig(c) uuid := cfg.UUID() initial := cfg.AllAttrs() @@ -203,9 +211,10 @@ st, err := state.Initialize(state.InitializeParams{ ControllerConfig: controllerCfg, ControllerModelArgs: state.ModelArgs{ - CloudName: "dummy", - Owner: owner, - Config: cfg, + CloudName: "dummy", + Owner: owner, + Config: cfg, + StorageProviderRegistry: storage.StaticProviderRegistry{}, }, CloudName: "dummy", Cloud: cloud.Cloud{ @@ -225,13 +234,21 @@ s.openState(c, modelTag) - ControllerInheritedConfig, err := state.ReadSettings(s.State, state.GlobalSettingsC, state.ControllerInheritedSettingsGlobalKey) + controllerInheritedConfig, err := state.ReadSettings(s.State, state.GlobalSettingsC, state.ControllerInheritedSettingsGlobalKey) c.Assert(err, jc.ErrorIsNil) - c.Assert(ControllerInheritedConfig.Map(), jc.DeepEquals, controllerInheritedConfigIn) + c.Assert(controllerInheritedConfig.Map(), jc.DeepEquals, controllerInheritedConfigIn) + expected := cfg.AllAttrs() + for k, v := range config.ConfigDefaults() { + if _, ok := expected[k]; !ok { + expected[k] = v + } + } + // Config as read from state has resources tags coerced to a map. + expected["resource-tags"] = map[string]string{} cfg, err = s.State.ModelConfig() c.Assert(err, jc.ErrorIsNil) - c.Assert(cfg.AllAttrs(), jc.DeepEquals, initial) + c.Assert(cfg.AllAttrs(), jc.DeepEquals, expected) } func (s *InitializeSuite) TestDoubleInitializeConfig(c *gc.C) { @@ -246,9 +263,10 @@ args := state.InitializeParams{ ControllerConfig: controllerCfg, ControllerModelArgs: state.ModelArgs{ - CloudName: "dummy", - Owner: owner, - Config: cfg, + CloudName: "dummy", + Owner: owner, + Config: cfg, + StorageProviderRegistry: storage.StaticProviderRegistry{}, }, CloudName: "dummy", Cloud: cloud.Cloud{ @@ -290,7 +308,7 @@ } func (s *InitializeSuite) testBadModelConfig(c *gc.C, update map[string]interface{}, remove []string, expect string) { - good := testing.ModelConfig(c) + good := testing.CustomModelConfig(c, testing.Attrs{"uuid": testing.ModelTag.Id()}) bad, err := good.Apply(update) c.Assert(err, jc.ErrorIsNil) bad, err = bad.Remove(remove) @@ -303,9 +321,10 @@ args := state.InitializeParams{ ControllerConfig: controllerCfg, ControllerModelArgs: state.ModelArgs{ - CloudName: "dummy", - Owner: owner, - Config: bad, + CloudName: "dummy", + Owner: owner, + Config: bad, + StorageProviderRegistry: storage.StaticProviderRegistry{}, }, CloudName: "dummy", Cloud: cloud.Cloud{ @@ -330,9 +349,9 @@ // ModelConfig remains inviolate. cfg, err := s.State.ModelConfig() c.Assert(err, jc.ErrorIsNil) - goodAttrs := good.AllAttrs() + goodWithDefaults, err := config.New(config.UseDefaults, good.AllAttrs()) c.Assert(err, jc.ErrorIsNil) - c.Assert(cfg.AllAttrs(), jc.DeepEquals, goodAttrs) + c.Assert(cfg.AllAttrs(), jc.DeepEquals, goodWithDefaults.AllAttrs()) } func (s *InitializeSuite) TestCloudConfigWithForbiddenValues(c *gc.C) { @@ -351,9 +370,10 @@ args := state.InitializeParams{ ControllerConfig: controllerCfg, ControllerModelArgs: state.ModelArgs{ - CloudName: "dummy", - Owner: names.NewLocalUserTag("initialize-admin"), - Config: modelCfg, + CloudName: "dummy", + Owner: names.NewLocalUserTag("initialize-admin"), + Config: modelCfg, + StorageProviderRegistry: storage.StaticProviderRegistry{}, }, CloudName: "dummy", Cloud: cloud.Cloud{ diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/interface.go juju-core-2.0~beta15/src/github.com/juju/juju/state/interface.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/interface.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/interface.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,6 +9,7 @@ "github.com/juju/version" "gopkg.in/juju/names.v2" + "github.com/juju/juju/cloud" "github.com/juju/juju/controller" "github.com/juju/juju/environs/config" "github.com/juju/juju/instance" @@ -98,6 +99,13 @@ NotifyWatcherFactory } +// CloudAccessor defines the methods needed to obtain information +// about clouds and credentials. +type CloudAccessor interface { + Cloud(cloud string) (cloud.Cloud, error) + CloudCredentials(user names.UserTag, cloud string) (map[string]cloud.Credential, error) +} + // ModelAccessor defines the methods needed to watch for model // config changes, and read the model config. type ModelAccessor interface { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/internal_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/internal_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/internal_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/internal_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,14 +4,20 @@ package state import ( + "github.com/juju/errors" jujutesting "github.com/juju/testing" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" "gopkg.in/juju/names.v2" "github.com/juju/juju/cloud" + "github.com/juju/juju/constraints" + "github.com/juju/juju/environs/config" + "github.com/juju/juju/instance" "github.com/juju/juju/mongo" "github.com/juju/juju/mongo/mongotest" + "github.com/juju/juju/storage" + "github.com/juju/juju/storage/provider" "github.com/juju/juju/testing" ) @@ -55,9 +61,10 @@ st, err := Initialize(InitializeParams{ ControllerConfig: controllerCfg, ControllerModelArgs: ModelArgs{ - CloudName: "dummy", - Owner: s.owner, - Config: modelCfg, + CloudName: "dummy", + Owner: s.owner, + Config: modelCfg, + StorageProviderRegistry: provider.CommonStorageProviders(), }, CloudName: "dummy", Cloud: cloud.Cloud{ @@ -66,6 +73,9 @@ }, MongoInfo: info, MongoDialOpts: mongotest.DialOpts(), + NewPolicy: func(*State) Policy { + return internalStatePolicy{} + }, }) c.Assert(err, jc.ErrorIsNil) s.state = st @@ -76,3 +86,25 @@ s.BaseSuite.TearDownTest(c) s.MgoSuite.TearDownTest(c) } + +type internalStatePolicy struct{} + +func (internalStatePolicy) Prechecker() (Prechecker, error) { + return nil, errors.NotImplementedf("Prechecker") +} + +func (internalStatePolicy) ConfigValidator() (config.Validator, error) { + return nil, errors.NotImplementedf("ConfigValidator") +} + +func (internalStatePolicy) ConstraintsValidator() (constraints.Validator, error) { + return nil, errors.NotImplementedf("ConstraintsValidator") +} + +func (internalStatePolicy) InstanceDistributor() (instance.Distributor, error) { + return nil, errors.NotImplementedf("InstanceDistributor") +} + +func (internalStatePolicy) StorageProviderRegistry() (storage.ProviderRegistry, error) { + return provider.CommonStorageProviders(), nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/life.go juju-core-2.0~beta15/src/github.com/juju/juju/state/life.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/life.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/life.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,6 +4,7 @@ package state import ( + "github.com/juju/errors" "gopkg.in/mgo.v2/bson" "github.com/juju/juju/mongo" @@ -32,10 +33,16 @@ } } -var isAliveDoc = bson.D{{"life", Alive}} -var isDyingDoc = bson.D{{"life", Dying}} -var isDeadDoc = bson.D{{"life", Dead}} -var notDeadDoc = bson.D{{"life", bson.D{{"$ne", Dead}}}} +var ( + isAliveDoc = bson.D{{"life", Alive}} + isDyingDoc = bson.D{{"life", Dying}} + isDeadDoc = bson.D{{"life", Dead}} + notDeadDoc = bson.D{{"life", bson.D{{"$ne", Dead}}}} + + errDeadOrGone = errors.New("neither alive nor dying") + errAlreadyDying = errors.New("already dying") + errAlreadyRemoved = errors.New("already removed") +) // Living describes state entities with a lifecycle. type Living interface { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/life_ns.go juju-core-2.0~beta15/src/github.com/juju/juju/state/life_ns.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/life_ns.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/life_ns.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,46 @@ +package state + +import ( + "github.com/juju/errors" + "gopkg.in/mgo.v2/bson" + "gopkg.in/mgo.v2/txn" + + "github.com/juju/juju/mongo" +) + +// nsLife_ backs nsLife. +type nsLife_ struct{} + +// nsLife namespaces low-level entity-life functionality. See the +// discussion in nsPayloads: this exists not to be the one place for +// life functionality (that would be a huge change), but to at least +// represent the parts we need for payloads in a consistent fashion. +// +// Both the namespacing and the explicit Collection->op approach seem +// to be good ideas, and should ideally be extended as we continue. +var nsLife = nsLife_{} + +// notDeadOp returns errDeadOrGone if the identified entity is not Alive +// or Dying, or a txn.Op that will fail if the condition no longer +// holds. +func (nsLife_) notDeadOp(entities mongo.Collection, docID string) (txn.Op, error) { + notDead := nsLife.notDead() + sel := append(bson.D{{"_id", docID}}, notDead...) + count, err := entities.Find(sel).Count() + if err != nil { + return txn.Op{}, errors.Trace(err) + } else if count == 0 { + return txn.Op{}, errDeadOrGone + } + return txn.Op{ + C: entities.Name(), + Id: docID, + Assert: notDead, + }, nil +} + +// notDead returns a selector that matches only documents whose life +// field is not set to Dead. +func (nsLife_) notDead() bson.D { + return bson.D{{"life", bson.D{{"$ne", Dead}}}} +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/linklayerdevices.go juju-core-2.0~beta15/src/github.com/juju/juju/state/linklayerdevices.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/linklayerdevices.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/linklayerdevices.go 2016-08-16 08:56:25.000000000 +0000 @@ -100,6 +100,22 @@ return &LinkLayerDevice{st: st, doc: doc} } +// AllLinkLayerDevices returns all link layer devices in the model. +func (st *State) AllLinkLayerDevices() (devices []*LinkLayerDevice, err error) { + devicesCollection, closer := st.getCollection(linkLayerDevicesC) + defer closer() + + sdocs := []linkLayerDeviceDoc{} + err = devicesCollection.Find(nil).All(&sdocs) + if err != nil { + return nil, errors.Errorf("cannot get all link layer devices") + } + for _, d := range sdocs { + devices = append(devices, newLinkLayerDevice(st, d)) + } + return devices, nil +} + // DocID returns the globally unique ID of the link-layer device, including the // model UUID as prefix. func (dev *LinkLayerDevice) DocID() string { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/linklayerdevices_ipaddresses.go juju-core-2.0~beta15/src/github.com/juju/juju/state/linklayerdevices_ipaddresses.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/linklayerdevices_ipaddresses.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/linklayerdevices_ipaddresses.go 2016-08-16 08:56:25.000000000 +0000 @@ -325,3 +325,19 @@ return errors.Trace(iter.Close()) } + +// AllIPAddresses returns all ip addresses in the model. +func (st *State) AllIPAddresses() (addresses []*Address, err error) { + addressesCollection, closer := st.getCollection(ipAddressesC) + defer closer() + + sdocs := []ipAddressDoc{} + err = addressesCollection.Find(bson.D{}).All(&sdocs) + if err != nil { + return nil, errors.Errorf("cannot get all ip addresses") + } + for _, a := range sdocs { + addresses = append(addresses, newIPAddress(st, a)) + } + return addresses, nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/machine.go juju-core-2.0~beta15/src/github.com/juju/juju/state/machine.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/machine.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/machine.go 2016-08-16 08:56:25.000000000 +0000 @@ -828,12 +828,9 @@ return ops, nil } -// Remove removes the machine from state. It will fail if the machine -// is not Dead. -func (m *Machine) Remove() (err error) { - defer errors.DeferredAnnotatef(&err, "cannot remove machine %s", m.doc.Id) +func (m *Machine) removeOps() ([]txn.Op, error) { if m.doc.Life != Dead { - return fmt.Errorf("machine is not dead") + return nil, fmt.Errorf("machine is not dead") } ops := []txn.Op{ { @@ -858,23 +855,23 @@ } linkLayerDevicesOps, err := m.removeAllLinkLayerDevicesOps() if err != nil { - return err + return nil, errors.Trace(err) } devicesAddressesOps, err := m.removeAllAddressesOps() if err != nil { - return err + return nil, errors.Trace(err) } portsOps, err := m.removePortsOps() if err != nil { - return err + return nil, errors.Trace(err) } filesystemOps, err := m.st.removeMachineFilesystemsOps(m.MachineTag()) if err != nil { - return err + return nil, errors.Trace(err) } volumeOps, err := m.st.removeMachineVolumesOps(m.MachineTag()) if err != nil { - return err + return nil, errors.Trace(err) } ops = append(ops, linkLayerDevicesOps...) ops = append(ops, devicesAddressesOps...) @@ -882,10 +879,35 @@ ops = append(ops, removeContainerRefOps(m.st, m.Id())...) ops = append(ops, filesystemOps...) ops = append(ops, volumeOps...) + return ops, nil +} + +// Remove removes the machine from state. It will fail if the machine +// is not Dead. +func (m *Machine) Remove() (err error) { + defer errors.DeferredAnnotatef(&err, "cannot remove machine %s", m.doc.Id) logger.Tracef("removing machine %q", m.Id()) - // The only abort conditions in play indicate that the machine has already - // been removed. - return onAbort(m.st.runTransaction(ops), nil) + // Local variable so we can re-get the machine without disrupting + // the caller. + machine := m + buildTxn := func(attempt int) ([]txn.Op, error) { + if attempt != 0 { + machine, err = machine.st.Machine(machine.Id()) + if errors.IsNotFound(err) { + // The machine's gone away, that's fine. + return nil, jujutxn.ErrNoOperations + } + if err != nil { + return nil, errors.Trace(err) + } + } + ops, err := machine.removeOps() + if err != nil { + return nil, errors.Trace(err) + } + return ops, nil + } + return m.st.run(buildTxn) } // Refresh refreshes the contents of the machine from the underlying diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/machineremovals.go juju-core-2.0~beta15/src/github.com/juju/juju/state/machineremovals.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/machineremovals.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/machineremovals.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,207 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package state + +import ( + "sort" + "strings" + + "github.com/juju/errors" + "github.com/juju/utils/set" + "gopkg.in/juju/names.v2" + "gopkg.in/mgo.v2/bson" + "gopkg.in/mgo.v2/txn" +) + +// machineRemovalDoc indicates that this machine needs to be removed +// and any necessary provider-level cleanup should now be done. +type machineRemovalDoc struct { + DocID string `bson:"_id"` + MachineID string `bson:"machine-id"` +} + +func (m *Machine) markForRemovalOps() ([]txn.Op, error) { + if m.doc.Life != Dead { + return nil, errors.Errorf("machine is not dead") + } + ops := []txn.Op{{ + C: machinesC, + Id: m.doc.DocID, + // Check that the machine is still dead (and implicitly that + // it still exists). + Assert: isDeadDoc, + }, { + C: machineRemovalsC, + Id: m.globalKey(), + Insert: &machineRemovalDoc{MachineID: m.Id()}, + // No assert here - it's ok if the machine has already been + // marked. The id will prevent duplicates. + }} + return ops, nil +} + +// MarkForRemoval requests that this machine be removed after any +// needed provider-level cleanup is done. +func (m *Machine) MarkForRemoval() (err error) { + defer errors.DeferredAnnotatef(&err, "cannot remove machine %s", m.doc.Id) + // Local variable so we can refresh the machine if needed. + machine := m + buildTxn := func(attempt int) ([]txn.Op, error) { + if attempt != 0 { + if machine, err = machine.st.Machine(machine.Id()); err != nil { + return nil, errors.Trace(err) + } + } + ops, err := machine.markForRemovalOps() + if err != nil { + return nil, errors.Trace(err) + } + return ops, nil + } + return m.st.run(buildTxn) +} + +// AllMachineRemovals returns (the ids of) all of the machines that +// need to be removed but need provider-level cleanup. +func (st *State) AllMachineRemovals() ([]string, error) { + removals, close := st.getCollection(machineRemovalsC) + defer close() + + var docs []machineRemovalDoc + err := removals.Find(nil).All(&docs) + if err != nil { + return nil, errors.Trace(err) + } + results := make([]string, len(docs)) + for i := range docs { + results[i] = docs[i].MachineID + } + return results, nil +} + +func (st *State) allMachinesMatching(query bson.D) ([]*Machine, error) { + machines, close := st.getCollection(machinesC) + defer close() + + var docs []machineDoc + err := machines.Find(query).All(&docs) + if err != nil { + return nil, errors.Trace(err) + } + results := make([]*Machine, len(docs)) + for i, doc := range docs { + results[i] = newMachine(st, &doc) + } + return results, nil +} + +func plural(count int) string { + if count == 1 { + return "" + } + return "s" +} + +func collectMissingMachineIds(expectedIds []string, machines []*Machine) []string { + expectedSet := set.NewStrings(expectedIds...) + actualSet := set.NewStrings() + for _, machine := range machines { + actualSet.Add(machine.Id()) + } + return expectedSet.Difference(actualSet).SortedValues() +} + +func checkValidMachineIds(machineIds []string) error { + var invalidIds []string + for _, id := range machineIds { + if !names.IsValidMachine(id) { + invalidIds = append(invalidIds, id) + } + } + if len(invalidIds) == 0 { + return nil + } + return errors.Errorf("Invalid machine id%s: %s", + plural(len(invalidIds)), + strings.Join(invalidIds, ", "), + ) +} + +func (st *State) completeMachineRemovalsOps(ids []string) ([]txn.Op, error) { + removals, err := st.AllMachineRemovals() + if err != nil { + return nil, errors.Trace(err) + } + removalSet := set.NewStrings(removals...) + query := bson.D{{"machineid", bson.D{{"$in", ids}}}} + machinesToRemove, err := st.allMachinesMatching(query) + if err != nil { + return nil, errors.Trace(err) + } + + var ops []txn.Op + var missingRemovals []string + for _, machine := range machinesToRemove { + if !removalSet.Contains(machine.Id()) { + missingRemovals = append(missingRemovals, machine.Id()) + continue + } + + ops = append(ops, txn.Op{ + C: machineRemovalsC, + Id: machine.globalKey(), + Assert: txn.DocExists, + Remove: true, + }) + removeMachineOps, err := machine.removeOps() + if err != nil { + return nil, errors.Trace(err) + } + ops = append(ops, removeMachineOps...) + } + // We should complain about machines that still exist but haven't + // been marked for removal. + if len(missingRemovals) > 0 { + sort.Strings(missingRemovals) + return nil, errors.Errorf( + "cannot remove machine%s %s: not marked for removal", + plural(len(missingRemovals)), + strings.Join(missingRemovals, ", "), + ) + } + + // Log last to reduce the likelihood of repeating the message on + // retries. + if len(machinesToRemove) < len(ids) { + missingMachines := collectMissingMachineIds(ids, machinesToRemove) + logger.Debugf("skipping nonexistent machine%s: %s", + plural(len(missingMachines)), + strings.Join(missingMachines, ", "), + ) + } + + return ops, nil +} + +// CompleteMachineRemovals finishes the removal of the specified +// machines. The machines must have been marked for removal +// previously. Valid-looking-but-unknown machine ids are ignored so +// that this is idempotent. +func (st *State) CompleteMachineRemovals(ids ...string) error { + if err := checkValidMachineIds(ids); err != nil { + return errors.Trace(err) + } + + buildTxn := func(int) ([]txn.Op, error) { + // We don't need to reget state for subsequent attempts since + // completeMachineRemovalsOps gets the removals and the + // machines each time anyway. + ops, err := st.completeMachineRemovalsOps(ids) + if err != nil { + return nil, errors.Trace(err) + } + return ops, nil + } + return st.run(buildTxn) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/machineremovals_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/machineremovals_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/machineremovals_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/machineremovals_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,137 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package state_test + +import ( + "github.com/juju/errors" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/state" + "github.com/juju/juju/state/testing" + "github.com/juju/juju/worker/workertest" +) + +type MachineRemovalSuite struct { + ConnSuite +} + +var _ = gc.Suite(&MachineRemovalSuite{}) + +func (s *MachineRemovalSuite) TestMarkingAndCompletingMachineRemoval(c *gc.C) { + m1 := s.makeMachine(c, true) + m2 := s.makeMachine(c, true) + + err := m1.MarkForRemoval() + c.Assert(err, jc.ErrorIsNil) + err = m2.MarkForRemoval() + c.Assert(err, jc.ErrorIsNil) + + // Check marking a machine multiple times is ok. + err = m1.MarkForRemoval() + c.Assert(err, jc.ErrorIsNil) + + // Check machines haven't been removed. + _, err = s.State.Machine(m1.Id()) + c.Assert(err, jc.ErrorIsNil) + _, err = s.State.Machine(m2.Id()) + c.Assert(err, jc.ErrorIsNil) + + removals, err := s.State.AllMachineRemovals() + c.Assert(err, jc.ErrorIsNil) + c.Check(removals, jc.SameContents, []string{m1.Id(), m2.Id()}) + + err = s.State.CompleteMachineRemovals(m1.Id(), "100") + c.Assert(err, jc.ErrorIsNil) + removals2, err := s.State.AllMachineRemovals() + c.Check(removals2, jc.SameContents, []string{m2.Id()}) + + _, err = s.State.Machine(m1.Id()) + c.Assert(err, gc.ErrorMatches, "machine 0 not found") + c.Assert(err, jc.Satisfies, errors.IsNotFound) + // But m2 is still there. + _, err = s.State.Machine(m2.Id()) + c.Assert(err, jc.ErrorIsNil) +} + +func (s *MachineRemovalSuite) TestMarkForRemovalRequiresDeadness(c *gc.C) { + m := s.makeMachine(c, false) + err := m.MarkForRemoval() + c.Assert(err, gc.ErrorMatches, "cannot remove machine 0: machine is not dead") +} + +func (s *MachineRemovalSuite) TestMarkForRemovalAssertsMachineStillExists(c *gc.C) { + m := s.makeMachine(c, true) + defer state.SetBeforeHooks(c, s.State, func() { + c.Assert(m.Remove(), gc.IsNil) + }).Check() + err := m.MarkForRemoval() + c.Assert(err, gc.ErrorMatches, "cannot remove machine 0: machine 0 not found") +} + +func (s *MachineRemovalSuite) TestCompleteMachineRemovalsRequiresMark(c *gc.C) { + m1 := s.makeMachine(c, true) + m2 := s.makeMachine(c, true) + err := s.State.CompleteMachineRemovals(m1.Id(), m2.Id()) + c.Assert(err, gc.ErrorMatches, "cannot remove machines 0, 1: not marked for removal") +} + +func (s *MachineRemovalSuite) TestCompleteMachineRemovalsRequiresMarkSingular(c *gc.C) { + m1 := s.makeMachine(c, true) + err := s.State.CompleteMachineRemovals(m1.Id()) + c.Assert(err, gc.ErrorMatches, "cannot remove machine 0: not marked for removal") +} + +func (s *MachineRemovalSuite) TestCompleteMachineRemovalsIgnoresNonexistent(c *gc.C) { + err := s.State.CompleteMachineRemovals("0", "1") + c.Assert(err, jc.ErrorIsNil) +} + +func (s *MachineRemovalSuite) TestCompleteMachineRemovalsInvalid(c *gc.C) { + err := s.State.CompleteMachineRemovals("A", "0/lxd/1", "B") + c.Assert(err, gc.ErrorMatches, "Invalid machine ids: A, B") +} + +func (s *MachineRemovalSuite) TestWatchMachineRemovals(c *gc.C) { + w, wc := s.createRemovalWatcher(c, s.State) + wc.AssertOneChange() // Initial event. + + m1 := s.makeMachine(c, true) + m2 := s.makeMachine(c, true) + + err := m1.MarkForRemoval() + c.Assert(err, jc.ErrorIsNil) + wc.AssertOneChange() + + err = m2.MarkForRemoval() + c.Assert(err, jc.ErrorIsNil) + wc.AssertOneChange() + + s.State.CompleteMachineRemovals(m1.Id(), m2.Id()) + wc.AssertOneChange() + + testing.AssertStop(c, w) + wc.AssertClosed() +} + +func (s *MachineRemovalSuite) createRemovalWatcher(c *gc.C, st *state.State) ( + state.NotifyWatcher, testing.NotifyWatcherC, +) { + w := st.WatchMachineRemovals() + s.AddCleanup(func(c *gc.C) { workertest.CleanKill(c, w) }) + return w, testing.NewNotifyWatcherC(c, st, w) +} + +func (s *MachineRemovalSuite) makeMachine(c *gc.C, deadAlready bool) *state.Machine { + m, err := s.State.AddMachine("xenial", state.JobHostUnits) + c.Assert(err, jc.ErrorIsNil) + if deadAlready { + deadenMachine(c, m) + } + return m +} + +func deadenMachine(c *gc.C, m *state.Machine) { + c.Assert(m.EnsureDead(), jc.ErrorIsNil) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/machine_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/machine_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/machine_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/machine_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -18,16 +18,15 @@ "gopkg.in/mgo.v2/bson" "github.com/juju/juju/constraints" - "github.com/juju/juju/environs/config" "github.com/juju/juju/instance" "github.com/juju/juju/mongo/mongotest" "github.com/juju/juju/network" + "github.com/juju/juju/provider/dummy" "github.com/juju/juju/state" "github.com/juju/juju/state/testing" "github.com/juju/juju/status" "github.com/juju/juju/storage/poolmanager" "github.com/juju/juju/storage/provider" - "github.com/juju/juju/storage/provider/registry" coretesting "github.com/juju/juju/testing" "github.com/juju/juju/worker" ) @@ -42,7 +41,7 @@ func (s *MachineSuite) SetUpTest(c *gc.C) { s.ConnSuite.SetUpTest(c) - s.policy.GetConstraintsValidator = func(*config.Config, state.SupportedArchitecturesQuerier) (constraints.Validator, error) { + s.policy.GetConstraintsValidator = func() (constraints.Validator, error) { validator := constraints.NewValidator() validator.RegisterConflicts([]string{constraints.InstanceType}, []string{constraints.Mem}) validator.RegisterUnsupported([]string{constraints.CpuPower}) @@ -605,7 +604,7 @@ func (s *MachineSuite) TestSetMongoPassword(c *gc.C) { info := testing.NewMongoInfo() - st, err := state.Open(s.modelTag, info, mongotest.DialOpts(), state.Policy(nil)) + st, err := state.Open(s.modelTag, info, mongotest.DialOpts(), state.NewPolicyFunc(nil)) c.Assert(err, jc.ErrorIsNil) defer func() { // Remove the admin password so that the test harness can reset the state. @@ -636,7 +635,7 @@ // Check that we can log in with the correct password. info.Password = "foo" - st1, err := state.Open(s.modelTag, info, mongotest.DialOpts(), state.Policy(nil)) + st1, err := state.Open(s.modelTag, info, mongotest.DialOpts(), state.NewPolicyFunc(nil)) c.Assert(err, jc.ErrorIsNil) defer st1.Close() @@ -865,10 +864,9 @@ } func (s *MachineSuite) TestMachineSetInstanceInfoSuccess(c *gc.C) { - pm := poolmanager.New(state.NewStateSettings(s.State)) + pm := poolmanager.New(state.NewStateSettings(s.State), dummy.StorageProviders()) _, err := pm.Create("loop-pool", provider.LoopProviderType, map[string]interface{}{}) c.Assert(err, jc.ErrorIsNil) - registry.RegisterEnvironStorageProviders("someprovider", provider.LoopProviderType) // Must create the requested block device prior to SetInstanceInfo. volumeTag := s.addVolume(c, state.VolumeParams{Size: 1000, Pool: "loop-pool"}, "123") @@ -1427,7 +1425,7 @@ logger := loggo.GetLogger("test") logger.SetLogLevel(loggo.DEBUG) var tw loggo.TestWriter - c.Assert(loggo.RegisterWriter("constraints-tester", &tw, loggo.DEBUG), gc.IsNil) + c.Assert(loggo.RegisterWriter("constraints-tester", &tw), gc.IsNil) machine, err := s.State.AddMachine("quantal", state.JobHostUnits) c.Assert(err, jc.ErrorIsNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/migration_export.go juju-core-2.0~beta15/src/github.com/juju/juju/state/migration_export.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/migration_export.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/migration_export.go 2016-08-16 08:56:25.000000000 +0000 @@ -86,6 +86,27 @@ if err := export.relations(); err != nil { return nil, errors.Trace(err) } + if err := export.spaces(); err != nil { + return nil, errors.Trace(err) + } + if err := export.subnets(); err != nil { + return nil, errors.Trace(err) + } + + if err := export.ipaddresses(); err != nil { + return nil, errors.Trace(err) + } + + if err := export.linklayerdevices(); err != nil { + return nil, errors.Trace(err) + } + + if err := export.sshHostKeys(); err != nil { + return nil, errors.Trace(err) + } + if err := export.storage(); err != nil { + return nil, errors.Trace(err) + } if err := export.model.Validate(); err != nil { return nil, errors.Trace(err) @@ -156,25 +177,14 @@ return errors.Trace(err) } for _, user := range users { - var access string - switch { - case user.IsAdmin(): - access = string(AdminAccess) - case user.IsReadWrite(): - access = string(WriteAccess) - default: - access = string(ReadAccess) - - } - - lastConn := lastConnections[strings.ToLower(user.UserName())] + lastConn := lastConnections[strings.ToLower(user.UserName)] arg := description.UserArgs{ - Name: user.UserTag(), - DisplayName: user.DisplayName(), - CreatedBy: names.NewUserTag(user.CreatedBy()), - DateCreated: user.DateCreated(), + Name: user.UserTag, + DisplayName: user.DisplayName, + CreatedBy: user.CreatedBy, + DateCreated: user.DateCreated, LastConnection: lastConn, - Access: access, + Access: user.Access, } e.model.AddUser(arg) } @@ -188,17 +198,13 @@ } e.logger.Debugf("found %d machines", len(machines)) - instanceDataCollection, closer := e.st.getCollection(instanceDataC) - defer closer() - - var instData []instanceData - instances := make(map[string]instanceData) - if err := instanceDataCollection.Find(nil).All(&instData); err != nil { - return errors.Annotate(err, "instance data") + instances, err := e.loadMachineInstanceData() + if err != nil { + return errors.Trace(err) } - e.logger.Debugf("found %d instanceData", len(instData)) - for _, data := range instData { - instances[data.MachineId] = data + blockDevices, err := e.loadMachineBlockDevices() + if err != nil { + return errors.Trace(err) } // Read all the open ports documents. @@ -228,7 +234,7 @@ } } - exMachine, err := e.newMachine(exParent, machine, instances, portsData) + exMachine, err := e.newMachine(exParent, machine, instances, portsData, blockDevices) if err != nil { return errors.Trace(err) } @@ -238,7 +244,39 @@ return nil } -func (e *exporter) newMachine(exParent description.Machine, machine *Machine, instances map[string]instanceData, portsData []portsDoc) (description.Machine, error) { +func (e *exporter) loadMachineInstanceData() (map[string]instanceData, error) { + instanceDataCollection, closer := e.st.getCollection(instanceDataC) + defer closer() + + var instData []instanceData + instances := make(map[string]instanceData) + if err := instanceDataCollection.Find(nil).All(&instData); err != nil { + return nil, errors.Annotate(err, "instance data") + } + e.logger.Debugf("found %d instanceData", len(instData)) + for _, data := range instData { + instances[data.MachineId] = data + } + return instances, nil +} + +func (e *exporter) loadMachineBlockDevices() (map[string][]BlockDeviceInfo, error) { + coll, closer := e.st.getCollection(blockDevicesC) + defer closer() + + var deviceData []blockDevicesDoc + result := make(map[string][]BlockDeviceInfo) + if err := coll.Find(nil).All(&deviceData); err != nil { + return nil, errors.Annotate(err, "block devices") + } + e.logger.Debugf("found %d block device records", len(deviceData)) + for _, data := range deviceData { + result[data.Machine] = data.BlockDevices + } + return result, nil +} + +func (e *exporter) newMachine(exParent description.Machine, machine *Machine, instances map[string]instanceData, portsData []portsDoc, blockDevices map[string][]BlockDeviceInfo) (description.Machine, error) { args := description.MachineArgs{ Id: machine.MachineTag(), Nonce: machine.doc.Nonce, @@ -283,6 +321,23 @@ } exMachine.SetInstance(e.newCloudInstanceArgs(instData)) + // We don't rely on devices being there. If they aren't, we get an empty slice, + // which is fine to iterate over with range. + for _, device := range blockDevices[machine.doc.Id] { + exMachine.AddBlockDevice(description.BlockDeviceArgs{ + Name: device.DeviceName, + Links: device.DeviceLinks, + Label: device.Label, + UUID: device.UUID, + HardwareID: device.HardwareId, + BusAddress: device.BusAddress, + Size: device.Size, + FilesystemType: device.FilesystemType, + InUse: device.InUse, + MountPoint: device.MountPoint, + }) + } + // Find the current machine status. globalKey := machine.globalKey() statusArgs, err := e.statusArgs(globalKey) @@ -607,6 +662,109 @@ return nil } +func (e *exporter) spaces() error { + spaces, err := e.st.AllSpaces() + if err != nil { + return errors.Trace(err) + } + e.logger.Debugf("read %d spaces", len(spaces)) + + for _, space := range spaces { + e.model.AddSpace(description.SpaceArgs{ + Name: space.Name(), + Public: space.IsPublic(), + ProviderID: string(space.ProviderId()), + }) + } + return nil +} + +func (e *exporter) linklayerdevices() error { + linklayerdevices, err := e.st.AllLinkLayerDevices() + if err != nil { + return errors.Trace(err) + } + e.logger.Debugf("read %d ip devices", len(linklayerdevices)) + for _, device := range linklayerdevices { + e.model.AddLinkLayerDevice(description.LinkLayerDeviceArgs{ + ProviderID: string(device.ProviderID()), + MachineID: device.MachineID(), + Name: device.Name(), + MTU: device.MTU(), + Type: string(device.Type()), + MACAddress: device.MACAddress(), + IsAutoStart: device.IsAutoStart(), + IsUp: device.IsUp(), + ParentName: device.ParentName(), + }) + } + return nil +} + +func (e *exporter) subnets() error { + subnets, err := e.st.AllSubnets() + if err != nil { + return errors.Trace(err) + } + e.logger.Debugf("read %d subnets", len(subnets)) + + for _, subnet := range subnets { + e.model.AddSubnet(description.SubnetArgs{ + CIDR: subnet.CIDR(), + ProviderId: string(subnet.ProviderId()), + VLANTag: subnet.VLANTag(), + AvailabilityZone: subnet.AvailabilityZone(), + SpaceName: subnet.SpaceName(), + }) + } + return nil +} + +func (e *exporter) ipaddresses() error { + ipaddresses, err := e.st.AllIPAddresses() + if err != nil { + return errors.Trace(err) + } + e.logger.Debugf("read %d ip addresses", len(ipaddresses)) + for _, addr := range ipaddresses { + e.model.AddIPAddress(description.IPAddressArgs{ + ProviderID: string(addr.ProviderID()), + DeviceName: addr.DeviceName(), + MachineID: addr.MachineID(), + SubnetCIDR: addr.SubnetCIDR(), + ConfigMethod: string(addr.ConfigMethod()), + Value: addr.Value(), + DNSServers: addr.DNSServers(), + DNSSearchDomains: addr.DNSSearchDomains(), + GatewayAddress: addr.GatewayAddress(), + }) + } + return nil +} + +func (e *exporter) sshHostKeys() error { + machines, err := e.st.AllMachines() + if err != nil { + return errors.Trace(err) + } + for _, machine := range machines { + keys, err := e.st.GetSSHHostKeys(machine.MachineTag()) + if errors.IsNotFound(err) { + continue + } else if err != nil { + return errors.Trace(err) + } + if len(keys) == 0 { + continue + } + e.model.AddSSHHostKey(description.SSHHostKeyArgs{ + MachineID: machine.Id(), + Keys: keys, + }) + } + return nil +} + func (e *exporter) readAllRelationScopes() (set.Strings, error) { relationScopes, closer := e.st.getCollection(relationScopesC) defer closer() @@ -902,6 +1060,7 @@ RootDisk: optionalInt("rootdisk"), Spaces: optionalStringSlice("spaces"), Tags: optionalStringSlice("tags"), + VirtType: optionalString("virttype"), } if optionalErr != nil { return description.ConstraintsArgs{}, errors.Trace(optionalErr) @@ -947,3 +1106,219 @@ e.logger.Warningf("unexported annotation for %s, %s", doc.Tag, key) } } + +func (e *exporter) storage() error { + if err := e.volumes(); err != nil { + return errors.Trace(err) + } + if err := e.filesystems(); err != nil { + return errors.Trace(err) + } + return nil +} + +func (e *exporter) volumes() error { + coll, closer := e.st.getCollection(volumesC) + defer closer() + + attachments, err := e.readVolumeAttachments() + if err != nil { + return errors.Trace(err) + } + + var doc volumeDoc + iter := coll.Find(nil).Sort("_id").Iter() + defer iter.Close() + for iter.Next(&doc) { + vol := &volume{e.st, doc} + if err := e.addVolume(vol, attachments[doc.Name]); err != nil { + return errors.Trace(err) + } + } + if err := iter.Err(); err != nil { + return errors.Annotate(err, "failed to read volumes") + } + return nil +} + +func (e *exporter) addVolume(vol *volume, volAttachments []volumeAttachmentDoc) error { + args := description.VolumeArgs{ + Tag: vol.VolumeTag(), + Binding: vol.LifeBinding(), + // TODO: add storage link + } + logger.Debugf("addVolume: %#v", vol.doc) + if info, err := vol.Info(); err == nil { + logger.Debugf(" info %#v", info) + args.Provisioned = true + args.Size = info.Size + args.Pool = info.Pool + args.HardwareID = info.HardwareId + args.VolumeID = info.VolumeId + args.Persistent = info.Persistent + } else { + params, _ := vol.Params() + logger.Debugf(" params %#v", params) + args.Size = params.Size + args.Pool = params.Pool + } + + globalKey := vol.globalKey() + statusArgs, err := e.statusArgs(globalKey) + if err != nil { + return errors.Annotatef(err, "status for volume %s", vol.doc.Name) + } + + exVolume := e.model.AddVolume(args) + exVolume.SetStatus(statusArgs) + exVolume.SetStatusHistory(e.statusHistoryArgs(globalKey)) + if count := len(volAttachments); count != vol.doc.AttachmentCount { + return errors.Errorf("volume attachment count mismatch, have %d, expected %d", + count, vol.doc.AttachmentCount) + } + for _, doc := range volAttachments { + va := volumeAttachment{doc} + logger.Debugf(" attachment %#v", doc) + args := description.VolumeAttachmentArgs{ + Machine: va.Machine(), + } + if info, err := va.Info(); err == nil { + logger.Debugf(" info %#v", info) + args.Provisioned = true + args.ReadOnly = info.ReadOnly + args.DeviceName = info.DeviceName + args.DeviceLink = info.DeviceLink + args.BusAddress = info.BusAddress + } else { + params, _ := va.Params() + logger.Debugf(" params %#v", params) + args.ReadOnly = params.ReadOnly + } + exVolume.AddAttachment(args) + } + return nil +} + +func (e *exporter) readVolumeAttachments() (map[string][]volumeAttachmentDoc, error) { + coll, closer := e.st.getCollection(volumeAttachmentsC) + defer closer() + + result := make(map[string][]volumeAttachmentDoc) + var doc volumeAttachmentDoc + var count int + iter := coll.Find(nil).Iter() + defer iter.Close() + for iter.Next(&doc) { + result[doc.Volume] = append(result[doc.Volume], doc) + count++ + } + if err := iter.Err(); err != nil { + return nil, errors.Annotate(err, "failed to read volumes attachments") + } + e.logger.Debugf("read %d volume attachment documents", count) + return result, nil +} + +func (e *exporter) filesystems() error { + coll, closer := e.st.getCollection(filesystemsC) + defer closer() + + attachments, err := e.readFilesystemAttachments() + if err != nil { + return errors.Trace(err) + } + + var doc filesystemDoc + iter := coll.Find(nil).Sort("_id").Iter() + defer iter.Close() + for iter.Next(&doc) { + fs := &filesystem{e.st, doc} + if err := e.addFilesystem(fs, attachments[doc.FilesystemId]); err != nil { + return errors.Trace(err) + } + } + if err := iter.Err(); err != nil { + return errors.Annotate(err, "failed to read filesystems") + } + return nil +} + +func (e *exporter) addFilesystem(fs *filesystem, fsAttachments []filesystemAttachmentDoc) error { + // Here we don't care about the cases where the filesystem is not assigned to storage instances + // nor no backing volues. In both those situations we have empty tags. + storage, _ := fs.Storage() + volume, _ := fs.Volume() + args := description.FilesystemArgs{ + Tag: fs.FilesystemTag(), + Storage: storage, + Volume: volume, + Binding: fs.LifeBinding(), + } + logger.Debugf("addFilesystem: %#v", fs.doc) + if info, err := fs.Info(); err == nil { + logger.Debugf(" info %#v", info) + args.Provisioned = true + args.Size = info.Size + args.Pool = info.Pool + args.FilesystemID = info.FilesystemId + } else { + params, _ := fs.Params() + logger.Debugf(" params %#v", params) + args.Size = params.Size + args.Pool = params.Pool + } + + globalKey := fs.globalKey() + statusArgs, err := e.statusArgs(globalKey) + if err != nil { + return errors.Annotatef(err, "status for filesystem %s", fs.doc.FilesystemId) + } + + exFilesystem := e.model.AddFilesystem(args) + exFilesystem.SetStatus(statusArgs) + exFilesystem.SetStatusHistory(e.statusHistoryArgs(globalKey)) + if count := len(fsAttachments); count != fs.doc.AttachmentCount { + return errors.Errorf("filesystem attachment count mismatch, have %d, expected %d", + count, fs.doc.AttachmentCount) + } + for _, doc := range fsAttachments { + va := filesystemAttachment{doc} + logger.Debugf(" attachment %#v", doc) + args := description.FilesystemAttachmentArgs{ + Machine: va.Machine(), + } + if info, err := va.Info(); err == nil { + logger.Debugf(" info %#v", info) + args.Provisioned = true + args.ReadOnly = info.ReadOnly + args.MountPoint = info.MountPoint + } else { + params, _ := va.Params() + logger.Debugf(" params %#v", params) + args.ReadOnly = params.ReadOnly + args.MountPoint = params.Location + } + exFilesystem.AddAttachment(args) + } + return nil +} + +func (e *exporter) readFilesystemAttachments() (map[string][]filesystemAttachmentDoc, error) { + coll, closer := e.st.getCollection(filesystemAttachmentsC) + defer closer() + + result := make(map[string][]filesystemAttachmentDoc) + var doc filesystemAttachmentDoc + var count int + iter := coll.Find(nil).Iter() + defer iter.Close() + for iter.Next(&doc) { + result[doc.Filesystem] = append(result[doc.Filesystem], doc) + count++ + } + if err := iter.Err(); err != nil { + return nil, errors.Annotate(err, "failed to read filesystem attachments") + } + e.logger.Debugf("read %d filesystem attachment documents", count) + return result, nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/migration_export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/migration_export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/migration_export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/migration_export_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -14,6 +14,7 @@ "github.com/juju/juju/constraints" "github.com/juju/juju/core/description" + "github.com/juju/juju/network" "github.com/juju/juju/state" "github.com/juju/juju/status" "github.com/juju/juju/testing/factory" @@ -121,7 +122,10 @@ dbModelCfg, err := dbModel.Config() c.Assert(err, jc.ErrorIsNil) modelAttrs := dbModelCfg.AllAttrs() - c.Assert(model.Config(), jc.DeepEquals, modelAttrs) + modelCfg := model.Config() + // Config as read from state has resources tags coerced to a map. + modelCfg["resource-tags"] = map[string]string{} + c.Assert(modelCfg, jc.DeepEquals, modelAttrs) c.Assert(model.LatestToolsVersion(), gc.Equals, latestTools) c.Assert(model.Annotations(), jc.DeepEquals, testAnnotations) constraints := model.Constraints() @@ -143,19 +147,19 @@ // Make sure we have some last connection times for the admin user, // and create a few other users. lastConnection := state.NowToTheSecond() - owner, err := s.State.ModelUser(s.Owner) + owner, err := s.State.UserAccess(s.Owner, s.State.ModelTag()) c.Assert(err, jc.ErrorIsNil) - err = state.UpdateModelUserLastConnection(owner, lastConnection) + err = state.UpdateModelUserLastConnection(s.State, owner, lastConnection) c.Assert(err, jc.ErrorIsNil) bobTag := names.NewUserTag("bob@external") - bob, err := s.State.AddModelUser(state.ModelUserSpec{ + bob, err := s.State.AddModelUser(state.UserAccessSpec{ User: bobTag, CreatedBy: s.Owner, - Access: state.ReadAccess, + Access: description.ReadAccess, }) c.Assert(err, jc.ErrorIsNil) - err = state.UpdateModelUserLastConnection(bob, lastConnection) + err = state.UpdateModelUserLastConnection(s.State, bob, lastConnection) c.Assert(err, jc.ErrorIsNil) model, err := s.State.Export() @@ -169,24 +173,32 @@ exportedAdmin := users[1] c.Assert(exportedAdmin.Name(), gc.Equals, s.Owner) - c.Assert(exportedAdmin.DisplayName(), gc.Equals, owner.DisplayName()) + c.Assert(exportedAdmin.DisplayName(), gc.Equals, owner.DisplayName) c.Assert(exportedAdmin.CreatedBy(), gc.Equals, s.Owner) - c.Assert(exportedAdmin.DateCreated(), gc.Equals, owner.DateCreated()) + c.Assert(exportedAdmin.DateCreated(), gc.Equals, owner.DateCreated) c.Assert(exportedAdmin.LastConnection(), gc.Equals, lastConnection) - c.Assert(exportedAdmin.IsReadOnly(), jc.IsFalse) + c.Assert(exportedAdmin.Access(), gc.Equals, description.AdminAccess) c.Assert(exportedBob.Name(), gc.Equals, bobTag) c.Assert(exportedBob.DisplayName(), gc.Equals, "") c.Assert(exportedBob.CreatedBy(), gc.Equals, s.Owner) - c.Assert(exportedBob.DateCreated(), gc.Equals, bob.DateCreated()) + c.Assert(exportedBob.DateCreated(), gc.Equals, bob.DateCreated) c.Assert(exportedBob.LastConnection(), gc.Equals, lastConnection) - c.Assert(exportedBob.IsReadOnly(), jc.IsTrue) + c.Assert(exportedBob.Access(), gc.Equals, description.ReadAccess) } func (s *MigrationExportSuite) TestMachines(c *gc.C) { + s.assertMachinesMigrated(c, constraints.MustParse("arch=amd64 mem=8G")) +} + +func (s *MigrationExportSuite) TestMachinesWithVirtConstraint(c *gc.C) { + s.assertMachinesMigrated(c, constraints.MustParse("arch=amd64 mem=8G virt-type=kvm")) +} + +func (s *MigrationExportSuite) assertMachinesMigrated(c *gc.C, cons constraints.Value) { // Add a machine with an LXC container. machine1 := s.Factory.MakeMachine(c, &factory.MachineParams{ - Constraints: constraints.MustParse("arch=amd64 mem=8G"), + Constraints: cons, }) nested := s.Factory.MakeMachineNested(c, machine1.Id(), nil) err := s.State.SetAnnotations(machine1, testAnnotations) @@ -205,8 +217,11 @@ c.Assert(exported.Annotations(), jc.DeepEquals, testAnnotations) constraints := exported.Constraints() c.Assert(constraints, gc.NotNil) - c.Assert(constraints.Architecture(), gc.Equals, "amd64") - c.Assert(constraints.Memory(), gc.Equals, 8*gig) + c.Assert(constraints.Architecture(), gc.Equals, *cons.Arch) + c.Assert(constraints.Memory(), gc.Equals, *cons.Mem) + if cons.HasVirtType() { + c.Assert(constraints.VirtType(), gc.Equals, *cons.VirtType) + } tools, err := machine1.AgentTools() c.Assert(err, jc.ErrorIsNil) @@ -224,12 +239,65 @@ c.Assert(container.Tag(), gc.Equals, nested.MachineTag()) } +func (s *MigrationExportSuite) TestMachineDevices(c *gc.C) { + machine := s.Factory.MakeMachine(c, nil) + // Create two devices, first with all fields set, second just to show that + // we do both. + sda := state.BlockDeviceInfo{ + DeviceName: "sda", + DeviceLinks: []string{"some", "data"}, + Label: "sda-label", + UUID: "some-uuid", + HardwareId: "magic", + BusAddress: "bus stop", + Size: 16 * 1024 * 1024 * 1024, + FilesystemType: "ext4", + InUse: true, + MountPoint: "/", + } + sdb := state.BlockDeviceInfo{DeviceName: "sdb", MountPoint: "/var/lib/lxd"} + err := machine.SetMachineBlockDevices(sda, sdb) + c.Assert(err, jc.ErrorIsNil) + + model, err := s.State.Export() + c.Assert(err, jc.ErrorIsNil) + machines := model.Machines() + c.Assert(machines, gc.HasLen, 1) + exported := machines[0] + + devices := exported.BlockDevices() + c.Assert(devices, gc.HasLen, 2) + ex1, ex2 := devices[0], devices[1] + + c.Check(ex1.Name(), gc.Equals, "sda") + c.Check(ex1.Links(), jc.DeepEquals, []string{"some", "data"}) + c.Check(ex1.Label(), gc.Equals, "sda-label") + c.Check(ex1.UUID(), gc.Equals, "some-uuid") + c.Check(ex1.HardwareID(), gc.Equals, "magic") + c.Check(ex1.BusAddress(), gc.Equals, "bus stop") + c.Check(ex1.Size(), gc.Equals, uint64(16*1024*1024*1024)) + c.Check(ex1.FilesystemType(), gc.Equals, "ext4") + c.Check(ex1.InUse(), jc.IsTrue) + c.Check(ex1.MountPoint(), gc.Equals, "/") + + c.Check(ex2.Name(), gc.Equals, "sdb") + c.Check(ex2.MountPoint(), gc.Equals, "/var/lib/lxd") +} + func (s *MigrationExportSuite) TestApplications(c *gc.C) { + s.assertMigrateApplications(c, constraints.MustParse("arch=amd64 mem=8G")) +} + +func (s *MigrationExportSuite) TestApplicationsWithVirtConstraint(c *gc.C) { + s.assertMigrateApplications(c, constraints.MustParse("arch=amd64 mem=8G virt-type=kvm")) +} + +func (s *MigrationExportSuite) assertMigrateApplications(c *gc.C, cons constraints.Value) { application := s.Factory.MakeApplication(c, &factory.ApplicationParams{ Settings: map[string]interface{}{ "foo": "bar", }, - Constraints: constraints.MustParse("arch=amd64 mem=8G"), + Constraints: cons, }) err := application.UpdateLeaderSettings(&goodToken{}, map[string]string{ "leader": "true", @@ -264,8 +332,11 @@ constraints := exported.Constraints() c.Assert(constraints, gc.NotNil) - c.Assert(constraints.Architecture(), gc.Equals, "amd64") - c.Assert(constraints.Memory(), gc.Equals, 8*gig) + c.Assert(constraints.Architecture(), gc.Equals, *cons.Arch) + c.Assert(constraints.Memory(), gc.Equals, *cons.Mem) + if cons.HasVirtType() { + c.Assert(constraints.VirtType(), gc.Equals, *cons.VirtType) + } history := exported.StatusHistory() c.Assert(history, gc.HasLen, expectedHistoryCount) @@ -442,9 +513,294 @@ checkEndpoint(exEps[1], wordpress_0.Name(), wpEp, wordpressSettings) } +func (s *MigrationExportSuite) TestSpaces(c *gc.C) { + s.Factory.MakeSpace(c, &factory.SpaceParams{ + Name: "one", ProviderID: network.Id("provider"), IsPublic: true}) + + model, err := s.State.Export() + c.Assert(err, jc.ErrorIsNil) + + spaces := model.Spaces() + c.Assert(spaces, gc.HasLen, 1) + space := spaces[0] + c.Assert(space.Name(), gc.Equals, "one") + c.Assert(space.ProviderID(), gc.Equals, "provider") + c.Assert(space.Public(), jc.IsTrue) +} + +func (s *MigrationExportSuite) TestMultipleSpaces(c *gc.C) { + s.Factory.MakeSpace(c, &factory.SpaceParams{Name: "one"}) + s.Factory.MakeSpace(c, &factory.SpaceParams{Name: "two"}) + s.Factory.MakeSpace(c, &factory.SpaceParams{Name: "three"}) + + model, err := s.State.Export() + c.Assert(err, jc.ErrorIsNil) + c.Assert(model.Spaces(), gc.HasLen, 3) +} + +func (s *MigrationExportSuite) TestLinkLayerDevices(c *gc.C) { + machine := s.Factory.MakeMachine(c, &factory.MachineParams{ + Constraints: constraints.MustParse("arch=amd64 mem=8G"), + }) + deviceArgs := state.LinkLayerDeviceArgs{ + Name: "foo", + Type: state.EthernetDevice, + } + err := machine.SetLinkLayerDevices(deviceArgs) + c.Assert(err, jc.ErrorIsNil) + + model, err := s.State.Export() + c.Assert(err, jc.ErrorIsNil) + + devices := model.LinkLayerDevices() + c.Assert(devices, gc.HasLen, 1) + device := devices[0] + c.Assert(device.Name(), gc.Equals, "foo") + c.Assert(device.Type(), gc.Equals, string(state.EthernetDevice)) +} + +func (s *MigrationExportSuite) TestSubnets(c *gc.C) { + _, err := s.State.AddSubnet(state.SubnetInfo{ + CIDR: "10.0.0.0/24", + ProviderId: network.Id("foo"), + VLANTag: 64, + AvailabilityZone: "bar", + SpaceName: "bam", + }) + c.Assert(err, jc.ErrorIsNil) + _, err = s.State.AddSpace("bam", "", nil, true) + c.Assert(err, jc.ErrorIsNil) + + model, err := s.State.Export() + c.Assert(err, jc.ErrorIsNil) + + subnets := model.Subnets() + c.Assert(subnets, gc.HasLen, 1) + subnet := subnets[0] + c.Assert(subnet.CIDR(), gc.Equals, "10.0.0.0/24") + c.Assert(subnet.ProviderId(), gc.Equals, "foo") + c.Assert(subnet.VLANTag(), gc.Equals, 64) + c.Assert(subnet.AvailabilityZone(), gc.Equals, "bar") + c.Assert(subnet.SpaceName(), gc.Equals, "bam") +} + +func (s *MigrationExportSuite) TestIPAddresses(c *gc.C) { + machine := s.Factory.MakeMachine(c, &factory.MachineParams{ + Constraints: constraints.MustParse("arch=amd64 mem=8G"), + }) + _, err := s.State.AddSubnet(state.SubnetInfo{CIDR: "0.1.2.0/24"}) + c.Assert(err, jc.ErrorIsNil) + deviceArgs := state.LinkLayerDeviceArgs{ + Name: "foo", + Type: state.EthernetDevice, + } + err = machine.SetLinkLayerDevices(deviceArgs) + c.Assert(err, jc.ErrorIsNil) + args := state.LinkLayerDeviceAddress{ + DeviceName: "foo", + ConfigMethod: state.StaticAddress, + CIDRAddress: "0.1.2.3/24", + ProviderID: "bar", + DNSServers: []string{"bam", "mam"}, + DNSSearchDomains: []string{"weeee"}, + GatewayAddress: "0.1.2.1", + } + err = machine.SetDevicesAddresses(args) + c.Assert(err, jc.ErrorIsNil) + + model, err := s.State.Export() + c.Assert(err, jc.ErrorIsNil) + + addresses := model.IPAddresses() + c.Assert(addresses, gc.HasLen, 1) + addr := addresses[0] + c.Assert(addr.Value(), gc.Equals, "0.1.2.3") + c.Assert(addr.MachineID(), gc.Equals, machine.Id()) + c.Assert(addr.DeviceName(), gc.Equals, "foo") + c.Assert(addr.ConfigMethod(), gc.Equals, string(state.StaticAddress)) + c.Assert(addr.SubnetCIDR(), gc.Equals, "0.1.2.0/24") + c.Assert(addr.ProviderID(), gc.Equals, "bar") + c.Assert(addr.DNSServers(), jc.DeepEquals, []string{"bam", "mam"}) + c.Assert(addr.DNSSearchDomains(), jc.DeepEquals, []string{"weeee"}) + c.Assert(addr.GatewayAddress(), gc.Equals, "0.1.2.1") +} + +func (s *MigrationExportSuite) TestSSHHostKeys(c *gc.C) { + machine := s.Factory.MakeMachine(c, &factory.MachineParams{ + Constraints: constraints.MustParse("arch=amd64 mem=8G"), + }) + err := s.State.SetSSHHostKeys(machine.MachineTag(), []string{"bam", "mam"}) + c.Assert(err, jc.ErrorIsNil) + + model, err := s.State.Export() + c.Assert(err, jc.ErrorIsNil) + + keys := model.SSHHostKeys() + c.Assert(keys, gc.HasLen, 1) + key := keys[0] + c.Assert(key.MachineID(), gc.Equals, machine.Id()) + c.Assert(key.Keys(), jc.DeepEquals, []string{"bam", "mam"}) +} + type goodToken struct{} // Check implements leadership.Token func (*goodToken) Check(interface{}) error { return nil } + +func (s *MigrationExportSuite) TestVolumes(c *gc.C) { + machine := s.Factory.MakeMachine(c, &factory.MachineParams{ + Volumes: []state.MachineVolumeParams{{ + Volume: state.VolumeParams{Size: 1234}, + Attachment: state.VolumeAttachmentParams{ReadOnly: true}, + }, { + Volume: state.VolumeParams{Size: 4000}, + }}, + }) + machineTag := machine.MachineTag() + + // We know that the first volume is called "0/0" as it is the first volume + // (volumes use sequences), and it is bound to machine 0. + volTag := names.NewVolumeTag("0/0") + err := s.State.SetVolumeInfo(volTag, state.VolumeInfo{ + HardwareId: "magic", + Size: 1500, + VolumeId: "volume id", + Persistent: true, + }) + c.Assert(err, jc.ErrorIsNil) + err = s.State.SetVolumeAttachmentInfo(machineTag, volTag, state.VolumeAttachmentInfo{ + DeviceName: "device name", + DeviceLink: "device link", + BusAddress: "bus address", + ReadOnly: true, + }) + c.Assert(err, jc.ErrorIsNil) + + model, err := s.State.Export() + c.Assert(err, jc.ErrorIsNil) + + volumes := model.Volumes() + c.Assert(volumes, gc.HasLen, 2) + provisioned, notProvisioned := volumes[0], volumes[1] + + c.Check(provisioned.Tag(), gc.Equals, volTag) + binding, err := provisioned.Binding() + c.Check(err, jc.ErrorIsNil) + c.Check(binding, gc.Equals, machineTag) + c.Check(provisioned.Provisioned(), jc.IsTrue) + c.Check(provisioned.Size(), gc.Equals, uint64(1500)) + c.Check(provisioned.Pool(), gc.Equals, "loop") + c.Check(provisioned.HardwareID(), gc.Equals, "magic") + c.Check(provisioned.VolumeID(), gc.Equals, "volume id") + c.Check(provisioned.Persistent(), jc.IsTrue) + attachments := provisioned.Attachments() + c.Assert(attachments, gc.HasLen, 1) + attachment := attachments[0] + c.Check(attachment.Machine(), gc.Equals, machineTag) + c.Check(attachment.Provisioned(), jc.IsTrue) + c.Check(attachment.ReadOnly(), jc.IsTrue) + c.Check(attachment.DeviceName(), gc.Equals, "device name") + c.Check(attachment.DeviceLink(), gc.Equals, "device link") + c.Check(attachment.BusAddress(), gc.Equals, "bus address") + + c.Check(notProvisioned.Tag(), gc.Equals, names.NewVolumeTag("0/1")) + binding, err = notProvisioned.Binding() + c.Check(err, jc.ErrorIsNil) + c.Check(binding, gc.Equals, machineTag) + c.Check(notProvisioned.Provisioned(), jc.IsFalse) + c.Check(notProvisioned.Size(), gc.Equals, uint64(4000)) + c.Check(notProvisioned.Pool(), gc.Equals, "loop") + c.Check(notProvisioned.HardwareID(), gc.Equals, "") + c.Check(notProvisioned.VolumeID(), gc.Equals, "") + c.Check(notProvisioned.Persistent(), jc.IsFalse) + attachments = notProvisioned.Attachments() + c.Assert(attachments, gc.HasLen, 1) + attachment = attachments[0] + c.Check(attachment.Machine(), gc.Equals, machineTag) + c.Check(attachment.Provisioned(), jc.IsFalse) + c.Check(attachment.ReadOnly(), jc.IsFalse) + c.Check(attachment.DeviceName(), gc.Equals, "") + c.Check(attachment.DeviceLink(), gc.Equals, "") + c.Check(attachment.BusAddress(), gc.Equals, "") + + // Make sure there is a status. + status := provisioned.Status() + c.Check(status.Value(), gc.Equals, "pending") +} + +func (s *MigrationExportSuite) TestFilesystems(c *gc.C) { + machine := s.Factory.MakeMachine(c, &factory.MachineParams{ + Filesystems: []state.MachineFilesystemParams{{ + Filesystem: state.FilesystemParams{Size: 1234}, + Attachment: state.FilesystemAttachmentParams{ + Location: "location", + ReadOnly: true}, + }, { + Filesystem: state.FilesystemParams{Size: 4000}, + }}, + }) + machineTag := machine.MachineTag() + + // We know that the first filesystem is called "0/0" as it is the first + // filesystem (filesystems use sequences), and it is bound to machine 0. + fsTag := names.NewFilesystemTag("0/0") + err := s.State.SetFilesystemInfo(fsTag, state.FilesystemInfo{ + Size: 1500, + FilesystemId: "filesystem id", + }) + c.Assert(err, jc.ErrorIsNil) + err = s.State.SetFilesystemAttachmentInfo(machineTag, fsTag, state.FilesystemAttachmentInfo{ + MountPoint: "/mnt/foo", + ReadOnly: true, + }) + c.Assert(err, jc.ErrorIsNil) + + model, err := s.State.Export() + c.Assert(err, jc.ErrorIsNil) + + filesystems := model.Filesystems() + c.Assert(filesystems, gc.HasLen, 2) + provisioned, notProvisioned := filesystems[0], filesystems[1] + + c.Check(provisioned.Tag(), gc.Equals, fsTag) + c.Check(provisioned.Volume(), gc.Equals, names.VolumeTag{}) + c.Check(provisioned.Storage(), gc.Equals, names.StorageTag{}) + binding, err := provisioned.Binding() + c.Check(err, jc.ErrorIsNil) + c.Check(binding, gc.Equals, machineTag) + c.Check(provisioned.Provisioned(), jc.IsTrue) + c.Check(provisioned.Size(), gc.Equals, uint64(1500)) + c.Check(provisioned.Pool(), gc.Equals, "rootfs") + c.Check(provisioned.FilesystemID(), gc.Equals, "filesystem id") + attachments := provisioned.Attachments() + c.Assert(attachments, gc.HasLen, 1) + attachment := attachments[0] + c.Check(attachment.Machine(), gc.Equals, machineTag) + c.Check(attachment.Provisioned(), jc.IsTrue) + c.Check(attachment.ReadOnly(), jc.IsTrue) + c.Check(attachment.MountPoint(), gc.Equals, "/mnt/foo") + + c.Check(notProvisioned.Tag(), gc.Equals, names.NewFilesystemTag("0/1")) + c.Check(notProvisioned.Volume(), gc.Equals, names.VolumeTag{}) + c.Check(notProvisioned.Storage(), gc.Equals, names.StorageTag{}) + binding, err = notProvisioned.Binding() + c.Check(err, jc.ErrorIsNil) + c.Check(binding, gc.Equals, machineTag) + c.Check(notProvisioned.Provisioned(), jc.IsFalse) + c.Check(notProvisioned.Size(), gc.Equals, uint64(4000)) + c.Check(notProvisioned.Pool(), gc.Equals, "rootfs") + c.Check(notProvisioned.FilesystemID(), gc.Equals, "") + attachments = notProvisioned.Attachments() + c.Assert(attachments, gc.HasLen, 1) + attachment = attachments[0] + c.Check(attachment.Machine(), gc.Equals, machineTag) + c.Check(attachment.Provisioned(), jc.IsFalse) + c.Check(attachment.ReadOnly(), jc.IsFalse) + c.Check(attachment.MountPoint(), gc.Equals, "") + + // Make sure there is a status. + status := provisioned.Status() + c.Check(status.Value(), gc.Equals, "pending") +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/migration_import.go juju-core-2.0~beta15/src/github.com/juju/juju/state/migration_import.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/migration_import.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/migration_import.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,6 +10,7 @@ "github.com/juju/loggo" "github.com/juju/version" "gopkg.in/juju/charm.v6-unstable" + "gopkg.in/juju/names.v2" "gopkg.in/mgo.v2/bson" "gopkg.in/mgo.v2/txn" @@ -17,7 +18,9 @@ "github.com/juju/juju/core/description" "github.com/juju/juju/environs/config" "github.com/juju/juju/instance" + "github.com/juju/juju/network" "github.com/juju/juju/status" + "github.com/juju/juju/storage" "github.com/juju/juju/tools" ) @@ -54,6 +57,12 @@ Config: cfg, Owner: model.Owner(), MigrationMode: MigrationModeImporting, + + // NOTE(axw) we create the model without any storage + // pools. We'll need to import the storage pools from + // the model description before adding any volumes, + // filesystems or storage instances. + StorageProviderRegistry: storage.StaticProviderRegistry{}, }) if err != nil { return nil, nil, errors.Trace(err) @@ -83,6 +92,9 @@ if err := newSt.SetModelConstraints(restore.constraints(model.Constraints())); err != nil { return nil, nil, errors.Annotate(err, "model constraints") } + if err := restore.sshHostKeys(); err != nil { + return nil, nil, errors.Annotate(err, "sshHostKeys") + } if err := restore.modelUsers(); err != nil { return nil, nil, errors.Annotate(err, "modelUsers") @@ -96,6 +108,22 @@ if err := restore.relations(); err != nil { return nil, nil, errors.Annotate(err, "relations") } + if err := restore.spaces(); err != nil { + return nil, nil, errors.Annotate(err, "spaces") + } + if err := restore.linklayerdevices(); err != nil { + return nil, nil, errors.Annotate(err, "linklayerdevices") + } + if err := restore.subnets(); err != nil { + return nil, nil, errors.Annotate(err, "subnets") + } + if err := restore.ipaddresses(); err != nil { + return nil, nil, errors.Annotate(err, "ipaddresses") + } + + if err := restore.storage(); err != nil { + return nil, nil, errors.Annotate(err, "storage") + } // NOTE: at the end of the import make sure that the mode of the model // is set to "imported" not "active" (or whatever we call it). This way @@ -180,7 +208,7 @@ // the wrong DateCreated, so we remove it, and add in all the users we // know about. It is also possible that the owner of the model no // longer has access to the model due to changes over time. - if err := i.st.RemoveModelUser(i.dbModel.Owner()); err != nil { + if err := i.st.RemoveUserAccess(i.dbModel.Owner(), i.dbModel.ModelTag()); err != nil { return errors.Trace(err) } @@ -188,22 +216,13 @@ modelUUID := i.dbModel.UUID() var ops []txn.Op for _, user := range users { - var access Access - switch { - case user.IsReadOnly(): - access = ReadAccess - case user.IsReadWrite(): - access = WriteAccess - default: - access = AdminAccess - } ops = append(ops, createModelUserOps( modelUUID, user.Name(), user.CreatedBy(), user.DisplayName(), user.DateCreated(), - access)..., + user.Access())..., ) } if err := i.st.runTransaction(ops); err != nil { @@ -216,11 +235,7 @@ if lastConnection.IsZero() { continue } - envUser, err := i.st.ModelUser(user.Name()) - if err != nil { - return errors.Trace(err) - } - err = envUser.updateLastConnection(lastConnection) + err := i.st.updateLastModelConnection(user.Name(), lastConnection) if err != nil { return errors.Trace(err) } @@ -315,6 +330,9 @@ if err := i.importStatusHistory(machine.globalKey(), m.StatusHistory()); err != nil { return errors.Trace(err) } + if err := i.importMachineBlockDevices(machine, m); err != nil { + return errors.Trace(err) + } // Now that this machine exists in the database, process each of the // containers in this machine. @@ -326,6 +344,29 @@ return nil } +func (i *importer) importMachineBlockDevices(machine *Machine, m description.Machine) error { + var devices []BlockDeviceInfo + for _, device := range m.BlockDevices() { + devices = append(devices, BlockDeviceInfo{ + DeviceName: device.Name(), + DeviceLinks: device.Links(), + Label: device.Label(), + UUID: device.UUID(), + HardwareId: device.HardwareID(), + BusAddress: device.BusAddress(), + Size: device.Size(), + FilesystemType: device.FilesystemType(), + InUse: device.InUse(), + MountPoint: device.MountPoint(), + }) + } + + if err := machine.SetMachineBlockDevices(devices...); err != nil { + return errors.Trace(err) + } + return nil +} + func (i *importer) machinePortsOps(m description.Machine) []txn.Op { var result []txn.Op machineID := m.Id() @@ -820,6 +861,212 @@ return doc } +func (i *importer) spaces() error { + i.logger.Debugf("importing spaces") + for _, s := range i.model.Spaces() { + // The subnets are added after the spaces. + _, err := i.st.AddSpace(s.Name(), network.Id(s.ProviderID()), nil, s.Public()) + if err != nil { + i.logger.Errorf("error importing space %s: %s", s.Name(), err) + return errors.Annotate(err, s.Name()) + } + } + + i.logger.Debugf("importing spaces succeeded") + return nil +} + +func (i *importer) linklayerdevices() error { + i.logger.Debugf("importing linklayerdevices") + for _, device := range i.model.LinkLayerDevices() { + err := i.addLinkLayerDevice(device) + if err != nil { + i.logger.Errorf("error importing ip device %v: %s", device, err) + return errors.Trace(err) + } + } + // Loop a second time so we can ensure that all devices have had their + // parent created. + ops := []txn.Op{} + for _, device := range i.model.LinkLayerDevices() { + if device.ParentName() == "" { + continue + } + parentDocID, err := i.parentDocIDFromDevice(device) + if err != nil { + return errors.Trace(err) + } + ops = append(ops, incrementDeviceNumChildrenOp(parentDocID)) + + } + if err := i.st.runTransaction(ops); err != nil { + return errors.Trace(err) + } + i.logger.Debugf("importing linklayerdevices succeeded") + return nil +} + +func (i *importer) parentDocIDFromDevice(device description.LinkLayerDevice) (string, error) { + hostMachineID, parentName, err := parseLinkLayerDeviceParentNameAsGlobalKey(device.ParentName()) + if err != nil { + return "", errors.Trace(err) + } + if hostMachineID == "" { + // ParentName is not a global key, but on the same machine. + hostMachineID = device.MachineID() + parentName = device.ParentName() + } + return i.st.docID(linkLayerDeviceGlobalKey(hostMachineID, parentName)), nil +} + +func (i *importer) addLinkLayerDevice(device description.LinkLayerDevice) error { + providerID := device.ProviderID() + modelUUID := i.st.ModelUUID() + localID := linkLayerDeviceGlobalKey(device.MachineID(), device.Name()) + linkLayerDeviceDocID := i.st.docID(localID) + newDoc := &linkLayerDeviceDoc{ + ModelUUID: modelUUID, + DocID: linkLayerDeviceDocID, + MachineID: device.MachineID(), + ProviderID: providerID, + Name: device.Name(), + MTU: device.MTU(), + Type: LinkLayerDeviceType(device.Type()), + MACAddress: device.MACAddress(), + IsAutoStart: device.IsAutoStart(), + IsUp: device.IsUp(), + ParentName: device.ParentName(), + } + + ops := []txn.Op{{ + C: linkLayerDevicesC, + Id: newDoc.DocID, + Insert: newDoc, + }, + insertLinkLayerDevicesRefsOp(modelUUID, linkLayerDeviceDocID), + } + if providerID != "" { + id := network.Id(providerID) + ops = append(ops, i.st.networkEntityGlobalKeyOp("linklayerdevice", id)) + } + if err := i.st.runTransaction(ops); err != nil { + return errors.Trace(err) + } + return nil +} + +func (i *importer) subnets() error { + i.logger.Debugf("importing subnets") + for _, subnet := range i.model.Subnets() { + err := i.addSubnet(SubnetInfo{ + CIDR: subnet.CIDR(), + ProviderId: network.Id(subnet.ProviderId()), + VLANTag: subnet.VLANTag(), + AvailabilityZone: subnet.AvailabilityZone(), + SpaceName: subnet.SpaceName(), + }) + if err != nil { + return errors.Trace(err) + } + } + i.logger.Debugf("importing subnets succeeded") + return nil +} + +func (i *importer) addSubnet(args SubnetInfo) error { + buildTxn := func(attempt int) ([]txn.Op, error) { + subnet, err := i.st.newSubnetFromArgs(args) + if err != nil { + return nil, errors.Trace(err) + } + ops := i.st.addSubnetOps(args) + if attempt != 0 { + if _, err = i.st.Subnet(args.CIDR); err == nil { + return nil, errors.AlreadyExistsf("subnet %q", args.CIDR) + } + if err := subnet.Refresh(); err != nil { + if errors.IsNotFound(err) { + return nil, errors.Errorf("ProviderId %q not unique", args.ProviderId) + } + return nil, errors.Trace(err) + } + } + return ops, nil + } + err := i.st.run(buildTxn) + if err != nil { + return errors.Trace(err) + } + return nil +} + +func (i *importer) ipaddresses() error { + i.logger.Debugf("importing ip addresses") + for _, addr := range i.model.IPAddresses() { + err := i.addIPAddress(addr) + if err != nil { + i.logger.Errorf("error importing ip address %v: %s", addr, err) + return errors.Trace(err) + } + } + i.logger.Debugf("importing ip addresses succeeded") + return nil +} + +func (i *importer) addIPAddress(addr description.IPAddress) error { + addressValue := addr.Value() + subnetCIDR := addr.SubnetCIDR() + + globalKey := ipAddressGlobalKey(addr.MachineID(), addr.DeviceName(), addressValue) + ipAddressDocID := i.st.docID(globalKey) + providerID := addr.ProviderID() + + modelUUID := i.st.ModelUUID() + + newDoc := &ipAddressDoc{ + DocID: ipAddressDocID, + ModelUUID: modelUUID, + ProviderID: providerID, + DeviceName: addr.DeviceName(), + MachineID: addr.MachineID(), + SubnetCIDR: subnetCIDR, + ConfigMethod: AddressConfigMethod(addr.ConfigMethod()), + Value: addressValue, + DNSServers: addr.DNSServers(), + DNSSearchDomains: addr.DNSSearchDomains(), + GatewayAddress: addr.GatewayAddress(), + } + + ops := []txn.Op{{ + C: ipAddressesC, + Id: newDoc.DocID, + Insert: newDoc, + }} + + if providerID != "" { + id := network.Id(providerID) + ops = append(ops, i.st.networkEntityGlobalKeyOp("address", id)) + } + if err := i.st.runTransaction(ops); err != nil { + return errors.Trace(err) + } + return nil +} + +func (i *importer) sshHostKeys() error { + i.logger.Debugf("importing ssh host keys") + for _, key := range i.model.SSHHostKeys() { + name := names.NewMachineTag(key.MachineID()) + err := i.st.SetSSHHostKeys(name, key.Keys()) + if err != nil { + i.logger.Errorf("error importing ssh host keys %v: %s", key, err) + return errors.Trace(err) + } + } + i.logger.Debugf("importing ssh host keys succeeded") + return nil +} + func (i *importer) importStatusHistory(globalKey string, history []description.Status) error { docs := make([]interface{}, len(history)) for i, statusVal := range history { @@ -831,6 +1078,9 @@ Updated: statusVal.Updated().UnixNano(), } } + if len(docs) == 0 { + return nil + } statusHistory, closer := i.st.getCollection(statusesHistoryC) defer closer() @@ -874,5 +1124,212 @@ if tags := cons.Tags(); len(tags) > 0 { result.Tags = &tags } + if virt := cons.VirtType(); virt != "" { + result.VirtType = &virt + } return result } + +func (i *importer) storage() error { + if err := i.volumes(); err != nil { + return errors.Annotate(err, "volumes") + } + if err := i.filesystems(); err != nil { + return errors.Annotate(err, "filesystems") + } + return nil +} + +func (i *importer) volumes() error { + i.logger.Debugf("importing volumes") + for _, volume := range i.model.Volumes() { + err := i.addVolume(volume) + if err != nil { + i.logger.Errorf("error importing volume %s: %s", volume.Tag(), err) + return errors.Trace(err) + } + } + i.logger.Debugf("importing volumes succeeded") + return nil +} + +func (i *importer) addVolume(volume description.Volume) error { + + attachments := volume.Attachments() + tag := volume.Tag() + var binding string + bindingTag, err := volume.Binding() + if err != nil { + return errors.Trace(err) + } + if bindingTag != nil { + binding = bindingTag.String() + } + var params *VolumeParams + var info *VolumeInfo + if volume.Provisioned() { + info = &VolumeInfo{ + HardwareId: volume.HardwareID(), + Size: volume.Size(), + Pool: volume.Pool(), + VolumeId: volume.VolumeID(), + Persistent: volume.Persistent(), + } + } else { + params = &VolumeParams{ + Size: volume.Size(), + Pool: volume.Pool(), + } + } + doc := volumeDoc{ + Name: tag.Id(), + // TODO: add storage ID + // StorageId: ..., + // Life: ..., // TODO: import life, default is Alive + Binding: binding, + Params: params, + Info: info, + AttachmentCount: len(attachments), + } + status := i.makeStatusDoc(volume.Status()) + ops := i.st.newVolumeOps(doc, status) + + for _, attachment := range attachments { + ops = append(ops, i.addVolumeAttachmentOp(tag.Id(), attachment)) + } + + if err := i.st.runTransaction(ops); err != nil { + return errors.Trace(err) + } + + if err := i.importStatusHistory(volumeGlobalKey(tag.Id()), volume.StatusHistory()); err != nil { + return errors.Annotate(err, "status history") + } + return nil +} + +func (i *importer) addVolumeAttachmentOp(volID string, attachment description.VolumeAttachment) txn.Op { + var info *VolumeAttachmentInfo + var params *VolumeAttachmentParams + if attachment.Provisioned() { + info = &VolumeAttachmentInfo{ + DeviceName: attachment.DeviceName(), + DeviceLink: attachment.DeviceLink(), + BusAddress: attachment.BusAddress(), + ReadOnly: attachment.ReadOnly(), + } + } else { + params = &VolumeAttachmentParams{ + ReadOnly: attachment.ReadOnly(), + } + } + + machineId := attachment.Machine().Id() + return txn.Op{ + C: volumeAttachmentsC, + Id: volumeAttachmentId(machineId, volID), + Assert: txn.DocMissing, + Insert: &volumeAttachmentDoc{ + Volume: volID, + Machine: machineId, + Params: params, + Info: info, + }, + } +} + +func (i *importer) filesystems() error { + i.logger.Debugf("importing filesystems") + for _, fs := range i.model.Filesystems() { + err := i.addFilesystem(fs) + if err != nil { + i.logger.Errorf("error importing filesystem %s: %s", fs.Tag(), err) + return errors.Trace(err) + } + } + i.logger.Debugf("importing filesystems succeeded") + return nil +} + +func (i *importer) addFilesystem(filesystem description.Filesystem) error { + + attachments := filesystem.Attachments() + tag := filesystem.Tag() + var binding string + bindingTag, err := filesystem.Binding() + if err != nil { + return errors.Trace(err) + } + if bindingTag != nil { + binding = bindingTag.String() + } + var params *FilesystemParams + var info *FilesystemInfo + if filesystem.Provisioned() { + info = &FilesystemInfo{ + Size: filesystem.Size(), + Pool: filesystem.Pool(), + FilesystemId: filesystem.FilesystemID(), + } + } else { + params = &FilesystemParams{ + Size: filesystem.Size(), + Pool: filesystem.Pool(), + } + } + doc := filesystemDoc{ + FilesystemId: tag.Id(), + StorageId: filesystem.Storage().Id(), + VolumeId: filesystem.Volume().Id(), + // Life: ..., // TODO: import life, default is Alive + Binding: binding, + Params: params, + Info: info, + AttachmentCount: len(attachments), + } + status := i.makeStatusDoc(filesystem.Status()) + ops := i.st.newFilesystemOps(doc, status) + + for _, attachment := range attachments { + ops = append(ops, i.addFilesystemAttachmentOp(tag.Id(), attachment)) + } + + if err := i.st.runTransaction(ops); err != nil { + return errors.Trace(err) + } + + if err := i.importStatusHistory(filesystemGlobalKey(tag.Id()), filesystem.StatusHistory()); err != nil { + return errors.Annotate(err, "status history") + } + return nil +} + +func (i *importer) addFilesystemAttachmentOp(fsID string, attachment description.FilesystemAttachment) txn.Op { + var info *FilesystemAttachmentInfo + var params *FilesystemAttachmentParams + if attachment.Provisioned() { + info = &FilesystemAttachmentInfo{ + MountPoint: attachment.MountPoint(), + ReadOnly: attachment.ReadOnly(), + } + } else { + params = &FilesystemAttachmentParams{ + Location: attachment.MountPoint(), + ReadOnly: attachment.ReadOnly(), + } + } + + machineId := attachment.Machine().Id() + return txn.Op{ + C: filesystemAttachmentsC, + Id: filesystemAttachmentId(machineId, fsID), + Assert: txn.DocMissing, + Insert: &filesystemAttachmentDoc{ + Filesystem: fsID, + Machine: machineId, + // Life: ..., // TODO: import life, default is Alive + Params: params, + Info: info, + }, + } +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/migration_import_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/migration_import_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/migration_import_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/migration_import_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,6 +4,7 @@ package state_test import ( + "fmt" "time" "github.com/juju/errors" @@ -57,6 +58,10 @@ newModel, newSt, err := s.State.Import(in) c.Assert(err, jc.ErrorIsNil) + // add the cleanup here to close the model. + s.AddCleanup(func(c *gc.C) { + c.Check(newSt.Close(), jc.ErrorIsNil) + }) return newModel, newSt } @@ -133,39 +138,39 @@ c.Assert(blocks[0].Message(), gc.Equals, "locked down") } -func (s *MigrationImportSuite) newModelUser(c *gc.C, name string, readOnly bool, lastConnection time.Time) *state.ModelUser { - access := state.AdminAccess +func (s *MigrationImportSuite) newModelUser(c *gc.C, name string, readOnly bool, lastConnection time.Time) description.UserAccess { + access := description.AdminAccess if readOnly { - access = state.ReadAccess + access = description.ReadAccess } - user, err := s.State.AddModelUser(state.ModelUserSpec{ + user, err := s.State.AddModelUser(state.UserAccessSpec{ User: names.NewUserTag(name), CreatedBy: s.Owner, Access: access, }) c.Assert(err, jc.ErrorIsNil) if !lastConnection.IsZero() { - err = state.UpdateModelUserLastConnection(user, lastConnection) + err = state.UpdateModelUserLastConnection(s.State, user, lastConnection) c.Assert(err, jc.ErrorIsNil) } return user } -func (s *MigrationImportSuite) AssertUserEqual(c *gc.C, newUser, oldUser *state.ModelUser) { - c.Assert(newUser.UserName(), gc.Equals, oldUser.UserName()) - c.Assert(newUser.DisplayName(), gc.Equals, oldUser.DisplayName()) - c.Assert(newUser.CreatedBy(), gc.Equals, oldUser.CreatedBy()) - c.Assert(newUser.DateCreated(), gc.Equals, oldUser.DateCreated()) - c.Assert(newUser.IsReadOnly(), gc.Equals, newUser.IsReadOnly()) +func (s *MigrationImportSuite) AssertUserEqual(c *gc.C, newUser, oldUser description.UserAccess) { + c.Assert(newUser.UserName, gc.Equals, oldUser.UserName) + c.Assert(newUser.DisplayName, gc.Equals, oldUser.DisplayName) + c.Assert(newUser.CreatedBy, gc.Equals, oldUser.CreatedBy) + c.Assert(newUser.DateCreated, gc.Equals, oldUser.DateCreated) + c.Assert(newUser.Access, gc.Equals, newUser.Access) - connTime, err := oldUser.LastConnection() + connTime, err := s.State.LastModelConnection(oldUser.UserTag) if state.IsNeverConnectedError(err) { - _, err := newUser.LastConnection() + _, err := s.State.LastModelConnection(newUser.UserTag) // The new user should also return an error for last connection. c.Assert(err, jc.Satisfies, state.IsNeverConnectedError) } else { c.Assert(err, jc.ErrorIsNil) - newTime, err := newUser.LastConnection() + newTime, err := s.State.LastModelConnection(newUser.UserTag) c.Assert(err, jc.ErrorIsNil) c.Assert(newTime, gc.Equals, connTime) } @@ -174,7 +179,7 @@ func (s *MigrationImportSuite) TestModelUsers(c *gc.C) { // To be sure with this test, we create three env users, and remove // the owner. - err := s.State.RemoveModelUser(s.Owner) + err := s.State.RemoveUserAccess(s.Owner, s.modelTag) c.Assert(err, jc.ErrorIsNil) lastConnection := state.NowToTheSecond() @@ -184,11 +189,10 @@ delta := s.newModelUser(c, "delta@external", true, time.Time{}) newModel, newSt := s.importModel(c) - defer newSt.Close() // Check the import values of the users. - for _, user := range []*state.ModelUser{bravo, charlie, delta} { - newUser, err := newSt.ModelUser(user.UserTag()) + for _, user := range []description.UserAccess{bravo, charlie, delta} { + newUser, err := newSt.UserAccess(user.UserTag, newModel.Tag()) c.Assert(err, jc.ErrorIsNil) s.AssertUserEqual(c, newUser, user) } @@ -240,7 +244,6 @@ c.Assert(allMachines, gc.HasLen, 2) _, newSt := s.importModel(c) - defer newSt.Close() importedMachines, err := newSt.AllMachines() c.Assert(err, jc.ErrorIsNil) @@ -270,40 +273,70 @@ c.Assert(newCons.String(), gc.Equals, cons.String()) } +func (s *MigrationImportSuite) TestMachineDevices(c *gc.C) { + machine := s.Factory.MakeMachine(c, nil) + // Create two devices, first with all fields set, second just to show that + // we do both. + sda := state.BlockDeviceInfo{ + DeviceName: "sda", + DeviceLinks: []string{"some", "data"}, + Label: "sda-label", + UUID: "some-uuid", + HardwareId: "magic", + BusAddress: "bus stop", + Size: 16 * 1024 * 1024 * 1024, + FilesystemType: "ext4", + InUse: true, + MountPoint: "/", + } + sdb := state.BlockDeviceInfo{DeviceName: "sdb", MountPoint: "/var/lib/lxd"} + err := machine.SetMachineBlockDevices(sda, sdb) + c.Assert(err, jc.ErrorIsNil) + + _, newSt := s.importModel(c) + + imported, err := newSt.Machine(machine.Id()) + c.Assert(err, jc.ErrorIsNil) + + devices, err := newSt.BlockDevices(imported.MachineTag()) + c.Assert(err, jc.ErrorIsNil) + + c.Check(devices, jc.DeepEquals, []state.BlockDeviceInfo{sda, sdb}) +} + func (s *MigrationImportSuite) TestApplications(c *gc.C) { - // Add a service with both settings and leadership settings. + // Add a application with both settings and leadership settings. cons := constraints.MustParse("arch=amd64 mem=8G") - service := s.Factory.MakeApplication(c, &factory.ApplicationParams{ + application := s.Factory.MakeApplication(c, &factory.ApplicationParams{ Settings: map[string]interface{}{ "foo": "bar", }, Constraints: cons, }) - err := service.UpdateLeaderSettings(&goodToken{}, map[string]string{ + err := application.UpdateLeaderSettings(&goodToken{}, map[string]string{ "leader": "true", }) c.Assert(err, jc.ErrorIsNil) - err = service.SetMetricCredentials([]byte("sekrit")) + err = application.SetMetricCredentials([]byte("sekrit")) c.Assert(err, jc.ErrorIsNil) - // Expose the service. - c.Assert(service.SetExposed(), jc.ErrorIsNil) - err = s.State.SetAnnotations(service, testAnnotations) + // Expose the application. + c.Assert(application.SetExposed(), jc.ErrorIsNil) + err = s.State.SetAnnotations(application, testAnnotations) c.Assert(err, jc.ErrorIsNil) - s.primeStatusHistory(c, service, status.StatusActive, 5) + s.primeStatusHistory(c, application, status.StatusActive, 5) - allServices, err := s.State.AllApplications() + allApplications, err := s.State.AllApplications() c.Assert(err, jc.ErrorIsNil) - c.Assert(allServices, gc.HasLen, 1) + c.Assert(allApplications, gc.HasLen, 1) _, newSt := s.importModel(c) - defer newSt.Close() - importedServices, err := newSt.AllApplications() + importedApplications, err := newSt.AllApplications() c.Assert(err, jc.ErrorIsNil) - c.Assert(importedServices, gc.HasLen, 1) + c.Assert(importedApplications, gc.HasLen, 1) - exported := allServices[0] - imported := importedServices[0] + exported := allApplications[0] + imported := importedApplications[0] c.Assert(imported.ApplicationTag(), gc.Equals, exported.ApplicationTag()) c.Assert(imported.Series(), gc.Equals, exported.Series()) @@ -323,7 +356,7 @@ c.Assert(importedLeaderSettings, jc.DeepEquals, exportedLeaderSettings) s.assertAnnotations(c, newSt, imported) - s.checkStatusHistory(c, service, imported, 5) + s.checkStatusHistory(c, application, imported, 5) newCons, err := imported.Constraints() c.Assert(err, jc.ErrorIsNil) @@ -336,7 +369,6 @@ s.makeApplicationWithLeader(c, "wordpress", 4, 2) _, newSt := s.importModel(c) - defer newSt.Close() leaders := make(map[string]string) leases, err := state.LeadershipLeases(newSt) @@ -351,7 +383,14 @@ } func (s *MigrationImportSuite) TestUnits(c *gc.C) { - cons := constraints.MustParse("arch=amd64 mem=8G") + s.assertUnitsMigrated(c, constraints.MustParse("arch=amd64 mem=8G")) +} + +func (s *MigrationImportSuite) TestUnitsWithVirtConstraint(c *gc.C) { + s.assertUnitsMigrated(c, constraints.MustParse("arch=amd64 mem=8G virt-type=kvm")) +} + +func (s *MigrationImportSuite) assertUnitsMigrated(c *gc.C, cons constraints.Value) { exported, pwd := s.Factory.MakeUnitReturningPassword(c, &factory.UnitParams{ Constraints: cons, }) @@ -365,13 +404,12 @@ s.primeStatusHistory(c, exported.Agent(), status.StatusIdle, 5) _, newSt := s.importModel(c) - defer newSt.Close() - importedServices, err := newSt.AllApplications() + importedApplications, err := newSt.AllApplications() c.Assert(err, jc.ErrorIsNil) - c.Assert(importedServices, gc.HasLen, 1) + c.Assert(importedApplications, gc.HasLen, 1) - importedUnits, err := importedServices[0].AllUnits() + importedUnits, err := importedApplications[0].AllUnits() c.Assert(err, jc.ErrorIsNil) c.Assert(importedUnits, gc.HasLen, 1) imported := importedUnits[0] @@ -427,7 +465,6 @@ c.Assert(err, jc.ErrorIsNil) _, newSt := s.importModel(c) - defer newSt.Close() newWordpress, err := newSt.Application("wordpress") c.Assert(err, jc.ErrorIsNil) @@ -453,7 +490,6 @@ c.Assert(err, jc.ErrorIsNil) _, newSt := s.importModel(c) - defer newSt.Close() // Even though the opened ports document is stored with the // machine, the only way to easily access it is through the units. @@ -470,23 +506,34 @@ }) } +func (s *MigrationImportSuite) TestSpaces(c *gc.C) { + space := s.Factory.MakeSpace(c, &factory.SpaceParams{ + Name: "one", ProviderID: network.Id("provider"), IsPublic: true}) + + _, newSt := s.importModel(c) + + imported, err := newSt.Space(space.Name()) + c.Assert(err, jc.ErrorIsNil) + + c.Assert(imported.Name(), gc.Equals, space.Name()) + c.Assert(imported.ProviderId(), gc.Equals, space.ProviderId()) + c.Assert(imported.IsPublic(), gc.Equals, space.IsPublic()) +} + func (s *MigrationImportSuite) TestDestroyEmptyModel(c *gc.C) { - newModel, newSt := s.importModel(c) - defer newSt.Close() + newModel, _ := s.importModel(c) s.assertDestroyModelAdvancesLife(c, newModel, state.Dead) } func (s *MigrationImportSuite) TestDestroyModelWithMachine(c *gc.C) { s.Factory.MakeMachine(c, nil) - newModel, newSt := s.importModel(c) - defer newSt.Close() + newModel, _ := s.importModel(c) s.assertDestroyModelAdvancesLife(c, newModel, state.Dying) } -func (s *MigrationImportSuite) TestDestroyModelWithService(c *gc.C) { +func (s *MigrationImportSuite) TestDestroyModelWithApplication(c *gc.C) { s.Factory.MakeApplication(c, nil) - newModel, newSt := s.importModel(c) - defer newSt.Close() + newModel, _ := s.importModel(c) s.assertDestroyModelAdvancesLife(c, newModel, state.Dying) } @@ -498,6 +545,291 @@ c.Assert(m.Life(), gc.Equals, life) } +func (s *MigrationImportSuite) TestLinkLayerDevice(c *gc.C) { + machine := s.Factory.MakeMachine(c, &factory.MachineParams{ + Constraints: constraints.MustParse("arch=amd64 mem=8G"), + }) + deviceArgs := state.LinkLayerDeviceArgs{ + Name: "foo", + Type: state.EthernetDevice, + } + err := machine.SetLinkLayerDevices(deviceArgs) + c.Assert(err, jc.ErrorIsNil) + _, newSt := s.importModel(c) + + devices, err := newSt.AllLinkLayerDevices() + c.Assert(err, jc.ErrorIsNil) + c.Assert(devices, gc.HasLen, 1) + device := devices[0] + c.Assert(device.Name(), gc.Equals, "foo") + c.Assert(device.Type(), gc.Equals, state.EthernetDevice) +} + +func (s *MigrationImportSuite) TestLinkLayerDeviceMigratesReferences(c *gc.C) { + machine := s.Factory.MakeMachine(c, &factory.MachineParams{ + Constraints: constraints.MustParse("arch=amd64 mem=8G"), + }) + machine2 := s.Factory.MakeMachineNested(c, machine.Id(), &factory.MachineParams{ + Constraints: constraints.MustParse("arch=amd64 mem=8G"), + }) + deviceArgs := []state.LinkLayerDeviceArgs{{ + Name: "foo", + Type: state.BridgeDevice, + }, { + Name: "bar", + ParentName: "foo", + Type: state.EthernetDevice, + }} + for _, args := range deviceArgs { + err := machine.SetLinkLayerDevices(args) + c.Assert(err, jc.ErrorIsNil) + } + machine2DeviceArgs := state.LinkLayerDeviceArgs{ + Name: "baz", + ParentName: fmt.Sprintf("m#%v#d#foo", machine.Id()), + Type: state.EthernetDevice, + } + err := machine2.SetLinkLayerDevices(machine2DeviceArgs) + c.Assert(err, jc.ErrorIsNil) + _, newSt := s.importModel(c) + + devices, err := newSt.AllLinkLayerDevices() + c.Assert(err, jc.ErrorIsNil) + c.Assert(devices, gc.HasLen, 3) + var parent *state.LinkLayerDevice + others := []*state.LinkLayerDevice{} + for _, device := range devices { + if device.Name() == "foo" { + parent = device + } else { + others = append(others, device) + } + } + // Assert we found the parent. + c.Assert(others, gc.HasLen, 2) + err = parent.Remove() + c.Assert(err, gc.ErrorMatches, `.*parent device "foo" has 2 children.*`) + err = others[0].Remove() + c.Assert(err, jc.ErrorIsNil) + err = parent.Remove() + c.Assert(err, gc.ErrorMatches, `.*parent device "foo" has 1 children.*`) + err = others[1].Remove() + c.Assert(err, jc.ErrorIsNil) + err = parent.Remove() + c.Assert(err, jc.ErrorIsNil) +} + +func (s *MigrationImportSuite) TestSubnets(c *gc.C) { + original, err := s.State.AddSubnet(state.SubnetInfo{ + CIDR: "10.0.0.0/24", + ProviderId: network.Id("foo"), + VLANTag: 64, + AvailabilityZone: "bar", + SpaceName: "bam", + }) + c.Assert(err, jc.ErrorIsNil) + _, err = s.State.AddSpace("bam", "", nil, true) + c.Assert(err, jc.ErrorIsNil) + + _, newSt := s.importModel(c) + + subnet, err := newSt.Subnet(original.CIDR()) + c.Assert(err, jc.ErrorIsNil) + + c.Assert(subnet.CIDR(), gc.Equals, "10.0.0.0/24") + c.Assert(subnet.ProviderId(), gc.Equals, network.Id("foo")) + c.Assert(subnet.VLANTag(), gc.Equals, 64) + c.Assert(subnet.AvailabilityZone(), gc.Equals, "bar") + c.Assert(subnet.SpaceName(), gc.Equals, "bam") +} + +func (s *MigrationImportSuite) TestIPAddress(c *gc.C) { + machine := s.Factory.MakeMachine(c, &factory.MachineParams{ + Constraints: constraints.MustParse("arch=amd64 mem=8G"), + }) + _, err := s.State.AddSubnet(state.SubnetInfo{CIDR: "0.1.2.0/24"}) + c.Assert(err, jc.ErrorIsNil) + deviceArgs := state.LinkLayerDeviceArgs{ + Name: "foo", + Type: state.EthernetDevice, + } + err = machine.SetLinkLayerDevices(deviceArgs) + c.Assert(err, jc.ErrorIsNil) + args := state.LinkLayerDeviceAddress{ + DeviceName: "foo", + ConfigMethod: state.StaticAddress, + CIDRAddress: "0.1.2.3/24", + ProviderID: "bar", + DNSServers: []string{"bam", "mam"}, + DNSSearchDomains: []string{"weeee"}, + GatewayAddress: "0.1.2.1", + } + err = machine.SetDevicesAddresses(args) + c.Assert(err, jc.ErrorIsNil) + + _, newSt := s.importModel(c) + + addresses, _ := newSt.AllIPAddresses() + c.Assert(addresses, gc.HasLen, 1) + c.Assert(err, jc.ErrorIsNil) + addr := addresses[0] + c.Assert(addr.Value(), gc.Equals, "0.1.2.3") + c.Assert(addr.MachineID(), gc.Equals, machine.Id()) + c.Assert(addr.DeviceName(), gc.Equals, "foo") + c.Assert(addr.ConfigMethod(), gc.Equals, state.StaticAddress) + c.Assert(addr.SubnetCIDR(), gc.Equals, "0.1.2.0/24") + c.Assert(addr.ProviderID(), gc.Equals, network.Id("bar")) + c.Assert(addr.DNSServers(), jc.DeepEquals, []string{"bam", "mam"}) + c.Assert(addr.DNSSearchDomains(), jc.DeepEquals, []string{"weeee"}) + c.Assert(addr.GatewayAddress(), gc.Equals, "0.1.2.1") +} + +func (s *MigrationImportSuite) TestSSHHostKey(c *gc.C) { + machine := s.Factory.MakeMachine(c, &factory.MachineParams{ + Constraints: constraints.MustParse("arch=amd64 mem=8G"), + }) + err := s.State.SetSSHHostKeys(machine.MachineTag(), []string{"bam", "mam"}) + c.Assert(err, jc.ErrorIsNil) + + _, newSt := s.importModel(c) + + machine2, err := newSt.Machine(machine.Id()) + c.Assert(err, jc.ErrorIsNil) + keys, err := newSt.GetSSHHostKeys(machine2.MachineTag()) + c.Assert(err, jc.ErrorIsNil) + c.Assert(keys, jc.DeepEquals, state.SSHHostKeys{"bam", "mam"}) +} + +func (s *MigrationImportSuite) TestVolumes(c *gc.C) { + machine := s.Factory.MakeMachine(c, &factory.MachineParams{ + Volumes: []state.MachineVolumeParams{{ + Volume: state.VolumeParams{Size: 1234}, + Attachment: state.VolumeAttachmentParams{ReadOnly: true}, + }, { + Volume: state.VolumeParams{Size: 4000}, + Attachment: state.VolumeAttachmentParams{ReadOnly: true}, + }}, + }) + machineTag := machine.MachineTag() + + // We know that the first volume is called "0/0" - although I don't know why. + volTag := names.NewVolumeTag("0/0") + volInfo := state.VolumeInfo{ + HardwareId: "magic", + Size: 1500, + Pool: "loop", + VolumeId: "volume id", + Persistent: true, + } + err := s.State.SetVolumeInfo(volTag, volInfo) + c.Assert(err, jc.ErrorIsNil) + volAttachmentInfo := state.VolumeAttachmentInfo{ + DeviceName: "device name", + DeviceLink: "device link", + BusAddress: "bus address", + ReadOnly: true, + } + err = s.State.SetVolumeAttachmentInfo(machineTag, volTag, volAttachmentInfo) + c.Assert(err, jc.ErrorIsNil) + + _, newSt := s.importModel(c) + + volume, err := newSt.Volume(volTag) + c.Assert(err, jc.ErrorIsNil) + + // TODO: check status + // TODO: check storage instance + info, err := volume.Info() + c.Assert(err, jc.ErrorIsNil) + c.Check(info, jc.DeepEquals, volInfo) + + attachment, err := newSt.VolumeAttachment(machineTag, volTag) + c.Assert(err, jc.ErrorIsNil) + attInfo, err := attachment.Info() + c.Assert(err, jc.ErrorIsNil) + c.Check(attInfo, jc.DeepEquals, volAttachmentInfo) + + volTag = names.NewVolumeTag("0/1") + volume, err = newSt.Volume(volTag) + c.Assert(err, jc.ErrorIsNil) + + params, needsProvisioning := volume.Params() + c.Check(needsProvisioning, jc.IsTrue) + c.Check(params.Pool, gc.Equals, "loop") + c.Check(params.Size, gc.Equals, uint64(4000)) + + attachment, err = newSt.VolumeAttachment(machineTag, volTag) + c.Assert(err, jc.ErrorIsNil) + attParams, needsProvisioning := attachment.Params() + c.Check(needsProvisioning, jc.IsTrue) + c.Check(attParams.ReadOnly, jc.IsTrue) +} + +func (s *MigrationImportSuite) TestFilesystems(c *gc.C) { + machine := s.Factory.MakeMachine(c, &factory.MachineParams{ + Filesystems: []state.MachineFilesystemParams{{ + Filesystem: state.FilesystemParams{Size: 1234}, + Attachment: state.FilesystemAttachmentParams{ + Location: "location", + ReadOnly: true}, + }, { + Filesystem: state.FilesystemParams{Size: 4000}, + Attachment: state.FilesystemAttachmentParams{ + ReadOnly: true}, + }}, + }) + machineTag := machine.MachineTag() + + // We know that the first filesystem is called "0/0" as it is the first + // filesystem (filesystems use sequences), and it is bound to machine 0. + fsTag := names.NewFilesystemTag("0/0") + fsInfo := state.FilesystemInfo{ + Size: 1500, + Pool: "rootfs", + FilesystemId: "filesystem id", + } + err := s.State.SetFilesystemInfo(fsTag, fsInfo) + c.Assert(err, jc.ErrorIsNil) + fsAttachmentInfo := state.FilesystemAttachmentInfo{ + MountPoint: "/mnt/foo", + ReadOnly: true, + } + err = s.State.SetFilesystemAttachmentInfo(machineTag, fsTag, fsAttachmentInfo) + c.Assert(err, jc.ErrorIsNil) + + _, newSt := s.importModel(c) + + filesystem, err := newSt.Filesystem(fsTag) + c.Assert(err, jc.ErrorIsNil) + + // TODO: check status + // TODO: check storage instance + info, err := filesystem.Info() + c.Assert(err, jc.ErrorIsNil) + c.Check(info, jc.DeepEquals, fsInfo) + + attachment, err := newSt.FilesystemAttachment(machineTag, fsTag) + c.Assert(err, jc.ErrorIsNil) + attInfo, err := attachment.Info() + c.Assert(err, jc.ErrorIsNil) + c.Check(attInfo, jc.DeepEquals, fsAttachmentInfo) + + fsTag = names.NewFilesystemTag("0/1") + filesystem, err = newSt.Filesystem(fsTag) + c.Assert(err, jc.ErrorIsNil) + + params, needsProvisioning := filesystem.Params() + c.Check(needsProvisioning, jc.IsTrue) + c.Check(params.Pool, gc.Equals, "rootfs") + c.Check(params.Size, gc.Equals, uint64(4000)) + + attachment, err = newSt.FilesystemAttachment(machineTag, fsTag) + c.Assert(err, jc.ErrorIsNil) + attParams, needsProvisioning := attachment.Params() + c.Check(needsProvisioning, jc.IsTrue) + c.Check(attParams.ReadOnly, jc.IsTrue) +} + // newModel replaces the uuid and name of the config attributes so we // can use all the other data to validate imports. An owner and name of the // model are unique together in a controller. diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/migration_internal_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/migration_internal_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/migration_internal_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/migration_internal_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -26,6 +26,7 @@ permissionsC, settingsC, sequenceC, + sshHostKeysC, statusesC, statusesHistoryC, @@ -46,11 +47,26 @@ // relation relationsC, relationScopesC, + + // networking + ipAddressesC, + spacesC, + linkLayerDevicesC, + subnetsC, + + // storage + blockDevicesC, + filesystemsC, + filesystemAttachmentsC, + volumesC, + volumeAttachmentsC, ) ignoredCollections := set.NewStrings( - // Precheck ensures that there are no cleanup docs. + // Precheck ensures that there are no cleanup docs or pending + // machine removals. cleanupsC, + machineRemovalsC, // We don't export the controller model at this stage. controllersC, // Clouds aren't migrated. They must exist in the @@ -67,6 +83,9 @@ // Users aren't migrated. usersC, userLastLoginC, + // Controller users contain extra data about users therefore + // are not migrated either. + controllerUsersC, // userenvnameC is just to provide a unique key constraint. usermodelnameC, // Metrics aren't migrated. @@ -91,6 +110,7 @@ migrationsC, migrationsStatusC, migrationsActiveC, + migrationsMinionSyncC, // The container ref document is primarily there to keep track // of a particular machine's containers. The migration format @@ -110,15 +130,17 @@ // separately. modelEntityRefsC, - // The SSH host keys for each machine will be reported as each - // machine agent starts up. - sshHostKeysC, + // This is marked as deprecated, and should probably be removed. + actionresultsC, + + // These are recreated whilst migrating other network entities. + providerIDsC, + linkLayerDevicesRefsC, ) // THIS SET WILL BE REMOVED WHEN MIGRATIONS ARE COMPLETE todoCollections := set.NewStrings( // model configuration - modelSettingsSourcesC, globalSettingsC, // model @@ -134,27 +156,13 @@ endpointBindingsC, // storage - blockDevicesC, - filesystemsC, - filesystemAttachmentsC, storageInstancesC, storageAttachmentsC, storageConstraintsC, - volumesC, - volumeAttachmentsC, - - // network - ipAddressesC, - providerIDsC, - linkLayerDevicesC, - linkLayerDevicesRefsC, - subnetsC, - spacesC, // actions actionsC, actionNotificationsC, - actionresultsC, // uncategorised metricsManagerC, // should really be copied across @@ -173,6 +181,9 @@ // If this test fails, it means that a new collection has been added // but migrations for it has not been done. This is a Bad Thingâ„¢. + // Beware, if your collection is something controller-related it might + // not need migration (such as Users or ControllerUsers) in that + // case they only need to be accounted for among the ignored collections. c.Assert(remainder, gc.HasLen, 0) } @@ -201,16 +212,16 @@ fields := set.NewStrings( // ID is the same as UserName (but lowercased) "ID", - // ModelUUID shouldn't be exported, and is inherited + // ObjectUUID shouldn't be exported, and is inherited // from the model definition. - "ModelUUID", + "ObjectUUID", // Tracked fields: "UserName", "DisplayName", "CreatedBy", "DateCreated", ) - s.AssertExportedFields(c, modelUserDoc{}, fields) + s.AssertExportedFields(c, userAccessDoc{}, fields) } func (s *MigrationSuite) TestPermissionDocFields(c *gc.C) { @@ -508,6 +519,7 @@ "Container", "Tags", "Spaces", + "VirtType", ) s.AssertExportedFields(c, constraintsDoc{}, fields) } @@ -526,6 +538,200 @@ s.AssertExportedFields(c, historicalStatusDoc{}, fields) } +func (s *MigrationSuite) TestSpaceDocFields(c *gc.C) { + ignored := set.NewStrings( + // Always alive, not explicitly exported. + "Life", + ) + migrated := set.NewStrings( + "Name", + "IsPublic", + "ProviderId", + ) + s.AssertExportedFields(c, spaceDoc{}, migrated.Union(ignored)) +} + +func (s *MigrationSuite) TestBlockDeviceFields(c *gc.C) { + ignored := set.NewStrings( + "DocID", + "ModelUUID", + // We manage machine through containment. + "Machine", + ) + migrated := set.NewStrings( + "BlockDevices", + ) + s.AssertExportedFields(c, blockDevicesDoc{}, migrated.Union(ignored)) + // The meat is in the type stored in "BlockDevices". + migrated = set.NewStrings( + "DeviceName", + "DeviceLinks", + "Label", + "UUID", + "HardwareId", + "BusAddress", + "Size", + "FilesystemType", + "InUse", + "MountPoint", + ) + s.AssertExportedFields(c, BlockDeviceInfo{}, migrated) +} + +func (s *MigrationSuite) TestSubnetDocFields(c *gc.C) { + ignored := set.NewStrings( + // DocID is the env + name + "DocID", + // ModelUUID shouldn't be exported, and is inherited + // from the model definition. + "ModelUUID", + // Always alive, not explicitly exported. + "Life", + + // Currently unused (never set or exposed). + "IsPublic", + ) + migrated := set.NewStrings( + "CIDR", + "VLANTag", + "SpaceName", + "ProviderId", + "AvailabilityZone", + ) + s.AssertExportedFields(c, subnetDoc{}, migrated.Union(ignored)) +} + +func (s *MigrationSuite) TestIPAddressDocFields(c *gc.C) { + ignored := set.NewStrings( + "DocID", + "ModelUUID", + ) + migrated := set.NewStrings( + "DeviceName", + "MachineID", + "DNSSearchDomains", + "GatewayAddress", + "ProviderID", + "DNSServers", + "SubnetCIDR", + "ConfigMethod", + "Value", + ) + s.AssertExportedFields(c, ipAddressDoc{}, migrated.Union(ignored)) +} + +func (s *MigrationSuite) TestLinkLayerDeviceDocFields(c *gc.C) { + ignored := set.NewStrings( + "ModelUUID", + "DocID", + ) + migrated := set.NewStrings( + "MachineID", + "ProviderID", + "Name", + "MTU", + "Type", + "MACAddress", + "IsAutoStart", + "IsUp", + "ParentName", + ) + s.AssertExportedFields(c, linkLayerDeviceDoc{}, migrated.Union(ignored)) +} + +func (s *MigrationSuite) TestSSHHostKeyDocFields(c *gc.C) { + ignored := set.NewStrings() + migrated := set.NewStrings( + "Keys", + ) + s.AssertExportedFields(c, sshHostKeysDoc{}, migrated.Union(ignored)) +} + +func (s *MigrationSuite) TestVolumeDocFields(c *gc.C) { + ignored := set.NewStrings( + "ModelUUID", + "DocID", + "Life", + ) + migrated := set.NewStrings( + "Name", + "AttachmentCount", // through count of attachment instances + "Binding", + "Info", + "Params", + ) + todo := set.NewStrings("StorageId") + s.AssertExportedFields(c, volumeDoc{}, migrated.Union(ignored).Union(todo)) + // The info and params fields ar structs. + s.AssertExportedFields(c, VolumeInfo{}, set.NewStrings( + "HardwareId", "Size", "Pool", "VolumeId", "Persistent")) + s.AssertExportedFields(c, VolumeParams{}, set.NewStrings( + "Size", "Pool")) +} + +func (s *MigrationSuite) TestVolumeAttachmentDocFields(c *gc.C) { + ignored := set.NewStrings( + "ModelUUID", + "DocID", + "Life", + ) + migrated := set.NewStrings( + "Volume", + "Machine", + "Info", + "Params", + ) + s.AssertExportedFields(c, volumeAttachmentDoc{}, migrated.Union(ignored)) + // The info and params fields ar structs. + s.AssertExportedFields(c, VolumeAttachmentInfo{}, set.NewStrings( + "DeviceName", "DeviceLink", "BusAddress", "ReadOnly")) + s.AssertExportedFields(c, VolumeAttachmentParams{}, set.NewStrings( + "ReadOnly")) +} + +func (s *MigrationSuite) TestFilesystemDocFields(c *gc.C) { + ignored := set.NewStrings( + "ModelUUID", + "DocID", + "Life", + ) + migrated := set.NewStrings( + "FilesystemId", + "StorageId", + "VolumeId", + "AttachmentCount", // through count of attachment instances + "Binding", + "Info", + "Params", + ) + s.AssertExportedFields(c, filesystemDoc{}, migrated.Union(ignored)) + // The info and params fields ar structs. + s.AssertExportedFields(c, FilesystemInfo{}, set.NewStrings( + "Size", "Pool", "FilesystemId")) + s.AssertExportedFields(c, FilesystemParams{}, set.NewStrings( + "Size", "Pool")) +} + +func (s *MigrationSuite) TestFilesystemAttachmentDocFields(c *gc.C) { + ignored := set.NewStrings( + "ModelUUID", + "DocID", + "Life", + ) + migrated := set.NewStrings( + "Filesystem", + "Machine", + "Info", + "Params", + ) + s.AssertExportedFields(c, filesystemAttachmentDoc{}, migrated.Union(ignored)) + // The info and params fields ar structs. + s.AssertExportedFields(c, FilesystemAttachmentInfo{}, set.NewStrings( + "MountPoint", "ReadOnly")) + s.AssertExportedFields(c, FilesystemAttachmentParams{}, set.NewStrings( + "Location", "ReadOnly")) +} + func (s *MigrationSuite) AssertExportedFields(c *gc.C, doc interface{}, fields set.Strings) { expected := getExportedFields(doc) unknown := expected.Difference(fields) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/modelconfig.go juju-core-2.0~beta15/src/github.com/juju/juju/state/modelconfig.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/modelconfig.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/modelconfig.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,14 +5,13 @@ import ( "github.com/juju/errors" - "gopkg.in/mgo.v2" - "gopkg.in/mgo.v2/bson" - "gopkg.in/mgo.v2/txn" "github.com/juju/juju/controller" "github.com/juju/juju/environs/config" ) +type attrValues map[string]interface{} + var disallowedModelConfigAttrs = [...]string{ "admin-secret", "ca-private-key", @@ -47,30 +46,74 @@ return nil } -// ModelConfigValues returns the config values for the model represented -// by this state. -func (st *State) ModelConfigValues() (config.ConfigValues, error) { - modelSettings, err := readSettings(st, settingsC, modelGlobalKey) - if err != nil { - return nil, errors.Trace(err) +// inheritedConfigAttributes returns the merged collection of inherited config +// values used as model defaults when adding models or unsetting values. +func (st *State) inheritedConfigAttributes() (map[string]interface{}, error) { + configSources := modelConfigSources(st) + values := make(attrValues) + for _, src := range configSources { + cfg, err := src.sourceFunc() + if errors.IsNotFound(err) { + continue + } + if err != nil { + return nil, errors.Annotatef(err, "reading %s settings", src.name) + } + for attrName, value := range cfg { + values[attrName] = value + } } - // TODO(wallyworld) - this data should not be stored separately - sources, closer := st.getCollection(modelSettingsSourcesC) - defer closer() - - var settingsSources settingsSourcesDoc - err = sources.FindId(modelGlobalKey).One(&settingsSources) - if err == mgo.ErrNotFound { - err = errors.NotFoundf("settings sources") + return values, nil +} + +// modelConfigValues returns the values and source for the supplied model config +// when combined with controller and Juju defaults. +func (st *State) modelConfigValues(modelCfg attrValues) (config.ConfigValues, error) { + resultValues := make(attrValues) + for k, v := range modelCfg { + resultValues[k] = v } - if err != nil { - return nil, err + + // Read all of the current inherited config values so + // we can dynamically reflect the origin of the model config. + configSources := modelConfigSources(st) + sourceNames := make([]string, 0, len(configSources)) + sourceAttrs := make([]attrValues, 0, len(configSources)) + for _, src := range configSources { + sourceNames = append(sourceNames, src.name) + cfg, err := src.sourceFunc() + if errors.IsNotFound(err) { + continue + } + if err != nil { + return nil, errors.Annotatef(err, "reading %s settings", src.name) + } + sourceAttrs = append(sourceAttrs, cfg) + + // If no modelCfg was passed in, we'll accumulate data + // for the inherited values instead. + if len(modelCfg) == 0 { + for k, v := range cfg { + resultValues[k] = v + } + } } + + // Figure out the source of each config attribute based + // on the current model values and the inherited values. result := make(config.ConfigValues) - for attr, val := range modelSettings.Map() { - source, ok := settingsSources.Sources[attr] - if !ok { - source = config.JujuModelConfigSource + for attr, val := range resultValues { + // Find the source of config for which the model + // value matches. If there's a match, the last match + // in the search order will be the source of config. + // If there's no match, the source is the model. + source := config.JujuModelConfigSource + n := len(sourceAttrs) + for i := range sourceAttrs { + if sourceAttrs[n-i-1][attr] == val { + source = sourceNames[n-i-1] + break + } } result[attr] = config.ConfigValue{ Value: val, @@ -80,8 +123,45 @@ return result, nil } +// UpdateModelConfigDefaultValues updates the inherited settings used when creating a new model. +func (st *State) UpdateModelConfigDefaultValues(attrs map[string]interface{}, removed []string) error { + settings, err := readSettings(st, globalSettingsC, controllerInheritedSettingsGlobalKey) + if err != nil { + return errors.Trace(err) + } + + // TODO(axw) 2013-12-6 #1167616 + // Ensure that the settings on disk have not changed + // underneath us. The settings changes are actually + // applied as a delta to what's on disk; if there has + // been a concurrent update, the change may not be what + // the user asked for. + settings.Update(attrs) + for _, r := range removed { + settings.Delete(r) + } + _, err = settings.Write() + return err +} + +// ModelConfigValues returns the config values for the model represented +// by this state. +func (st *State) ModelConfigValues() (config.ConfigValues, error) { + cfg, err := st.ModelConfig() + if err != nil { + return nil, errors.Trace(err) + } + return st.modelConfigValues(cfg.AllAttrs()) +} + +// ModelConfigDefaultValues returns the default config values to be used +// when creating a new model, and the origin of those values. +func (st *State) ModelConfigDefaultValues() (config.ConfigValues, error) { + return st.modelConfigValues(nil) +} + // checkControllerInheritedConfig returns an error if the shared local cloud config is definitely invalid. -func checkControllerInheritedConfig(attrs map[string]interface{}) error { +func checkControllerInheritedConfig(attrs attrValues) error { disallowedCloudConfigAttrs := append(disallowedModelConfigAttrs[:], config.AgentVersionKey) for _, attr := range disallowedCloudConfigAttrs { if _, ok := attrs[attr]; ok { @@ -96,7 +176,7 @@ return nil } -func (st *State) buildAndValidateModelConfig(updateAttrs map[string]interface{}, removeAttrs []string, oldConfig *config.Config) (validCfg *config.Config, err error) { +func (st *State) buildAndValidateModelConfig(updateAttrs attrValues, removeAttrs []string, oldConfig *config.Config) (*config.Config, error) { newConfig, err := oldConfig.Apply(updateAttrs) if err != nil { return nil, errors.Trace(err) @@ -123,6 +203,31 @@ return nil } + if len(removeAttrs) > 0 { + var removed []string + if updateAttrs == nil { + updateAttrs = make(map[string]interface{}) + } + // For each removed attribute, pick up any inherited value + // and if there's one, use that. + inherited, err := st.inheritedConfigAttributes() + if err != nil { + return errors.Trace(err) + } + for _, attr := range removeAttrs { + // We we are updating an attribute, that takes + // precedence over removing. + if _, ok := updateAttrs[attr]; ok { + continue + } + if val, ok := inherited[attr]; ok { + updateAttrs[attr] = val + } else { + removed = append(removed, attr) + } + } + removeAttrs = removed + } // TODO(axw) 2013-12-6 #1167616 // Ensure that the settings on disk have not changed // underneath us. The settings changes are actually @@ -157,49 +262,15 @@ modelSettings.Delete(k) } } + // Some values require marshalling before storage. + validAttrs = config.CoerceForStorage(validAttrs) modelSettings.Update(validAttrs) - changes, ops := modelSettings.settingsUpdateOps() - ops = append(ops, updateModelSourcesOps(changes)...) + _, ops := modelSettings.settingsUpdateOps() return modelSettings.write(ops) } -func updateModelSourcesOps(changes []ItemChange) []txn.Op { - var update bson.D - var set = make(bson.M) - for _, c := range changes { - set["sources."+c.Key] = "model" - } - update = append(update, bson.DocElem{"$set", set}) - - ops := []txn.Op{{ - C: modelSettingsSourcesC, - Id: modelGlobalKey, - Assert: txn.DocExists, - Update: update, - }} - return ops -} - -// settingsSourcesDoc stores for each model attribute, -// the source of the attribute. -type settingsSourcesDoc struct { - // Sources defines the named source for each settings attribute. - Sources map[string]string `bson:"sources,omitempty"` -} - -func createSettingsSourceOp(values map[string]string) txn.Op { - return txn.Op{ - C: modelSettingsSourcesC, - Id: modelGlobalKey, - Assert: txn.DocMissing, - Insert: &settingsSourcesDoc{ - Sources: values, - }, - } -} - -type modelConfigSourceFunc func() (map[string]interface{}, error) +type modelConfigSourceFunc func() (attrValues, error) type modelConfigSource struct { name string @@ -212,14 +283,20 @@ // overall config values, later values override earlier ones. func modelConfigSources(st *State) []modelConfigSource { return []modelConfigSource{ - {config.JujuControllerSource, st.ControllerInheritedConfig}, + {config.JujuDefaultSource, func() (attrValues, error) { return config.ConfigDefaults(), nil }}, + {config.JujuControllerSource, st.controllerInheritedConfig}, // We will also support local cloud region, tenant, user etc } } -// ControllerInheritedConfig returns the inherited config values +const ( + // controllerInheritedSettingsGlobalKey is the key for default settings shared across models. + controllerInheritedSettingsGlobalKey = "controller" +) + +// controllerInheritedConfig returns the inherited config values // sourced from the local cloud config. -func (st *State) ControllerInheritedConfig() (map[string]interface{}, error) { +func (st *State) controllerInheritedConfig() (attrValues, error) { settings, err := readSettings(st, globalSettingsC, controllerInheritedSettingsGlobalKey) if err != nil { return nil, errors.Trace(err) @@ -229,13 +306,10 @@ // composeModelConfigAttributes returns a set of model config settings composed from known // sources of default values overridden by model specific attributes. -// Also returned is a map containing the source location for each model attribute. -// The source location is the name of the config values from which an attribute came. func composeModelConfigAttributes( - modelAttr map[string]interface{}, configSources ...modelConfigSource, -) (map[string]interface{}, map[string]string, error) { - resultAttrs := make(map[string]interface{}) - settingsSources := make(map[string]string) + modelAttr attrValues, configSources ...modelConfigSource, +) (attrValues, error) { + resultAttrs := make(attrValues) // Compose default settings from all known sources. for _, source := range configSources { @@ -244,19 +318,24 @@ continue } if err != nil { - return nil, nil, errors.Annotatef(err, "reading %s settings", source.name) + return nil, errors.Annotatef(err, "reading %s settings", source.name) } for name, val := range newSettings { resultAttrs[name] = val - settingsSources[name] = source.name } } // Merge in model specific settings. for attr, val := range modelAttr { resultAttrs[attr] = val - settingsSources[attr] = config.JujuModelConfigSource } - return resultAttrs, settingsSources, nil + return resultAttrs, nil +} + +// ComposeNewModelConfig returns a complete map of config attributes suitable for +// creating a new model, by combining user specified values with system defaults. +func (st *State) ComposeNewModelConfig(modelAttr map[string]interface{}) (map[string]interface{}, error) { + configSources := modelConfigSources(st) + return composeModelConfigAttributes(modelAttr, configSources...) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/modelconfig_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/modelconfig_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/modelconfig_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/modelconfig_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,16 +4,21 @@ package state_test import ( - "fmt" + "strings" + "github.com/juju/errors" jc "github.com/juju/testing/checkers" "github.com/juju/utils" + "github.com/juju/utils/set" gc "gopkg.in/check.v1" "gopkg.in/juju/names.v2" "github.com/juju/juju/constraints" "github.com/juju/juju/environs/config" + "github.com/juju/juju/mongo/mongotest" "github.com/juju/juju/state" + statetesting "github.com/juju/juju/state/testing" + "github.com/juju/juju/storage" "github.com/juju/juju/testing" ) @@ -24,8 +29,11 @@ var _ = gc.Suite(&ModelConfigSuite{}) func (s *ModelConfigSuite) SetUpTest(c *gc.C) { + s.ControllerInheritedConfig = map[string]interface{}{ + "apt-mirror": "http://cloud-mirror", + } s.ConnSuite.SetUpTest(c) - s.policy.GetConstraintsValidator = func(*config.Config, state.SupportedArchitecturesQuerier) (constraints.Validator, error) { + s.policy.GetConstraintsValidator = func() (constraints.Validator, error) { validator := constraints.NewValidator() validator.RegisterConflicts([]string{constraints.InstanceType}, []string{constraints.Mem}) validator.RegisterUnsupported([]string{constraints.CpuPower}) @@ -36,18 +44,18 @@ func (s *ModelConfigSuite) TestAdditionalValidation(c *gc.C) { updateAttrs := map[string]interface{}{"logging-config": "juju=ERROR"} configValidator1 := func(updateAttrs map[string]interface{}, removeAttrs []string, oldConfig *config.Config) error { - c.Assert(updateAttrs, gc.DeepEquals, map[string]interface{}{"logging-config": "juju=ERROR"}) - if _, found := updateAttrs["logging-config"]; found { - return fmt.Errorf("cannot change logging-config") + c.Assert(updateAttrs, jc.DeepEquals, map[string]interface{}{"logging-config": "juju=ERROR"}) + if lc, found := updateAttrs["logging-config"]; found && lc != "" { + return errors.New("cannot change logging-config") } return nil } - removeAttrs := []string{"logging-config"} + removeAttrs := []string{"some-attr"} configValidator2 := func(updateAttrs map[string]interface{}, removeAttrs []string, oldConfig *config.Config) error { - c.Assert(removeAttrs, gc.DeepEquals, []string{"logging-config"}) + c.Assert(removeAttrs, jc.DeepEquals, []string{"some-attr"}) for _, i := range removeAttrs { - if i == "logging-config" { - return fmt.Errorf("cannot remove logging-config") + if i == "some-attr" { + return errors.New("cannot remove some-attr") } } return nil @@ -59,7 +67,7 @@ err := s.State.UpdateModelConfig(updateAttrs, nil, configValidator1) c.Assert(err, gc.ErrorMatches, "cannot change logging-config") err = s.State.UpdateModelConfig(nil, removeAttrs, configValidator2) - c.Assert(err, gc.ErrorMatches, "cannot remove logging-config") + c.Assert(err, gc.ErrorMatches, "cannot remove some-attr") err = s.State.UpdateModelConfig(updateAttrs, nil, configValidator3) c.Assert(err, jc.ErrorIsNil) } @@ -78,7 +86,27 @@ oldCfg, err := s.State.ModelConfig() c.Assert(err, jc.ErrorIsNil) - c.Assert(oldCfg, gc.DeepEquals, cfg) + c.Assert(oldCfg, jc.DeepEquals, cfg) +} + +func (s *ModelConfigSuite) TestComposeNewModelConfig(c *gc.C) { + attrs := map[string]interface{}{ + "authorized-keys": "different-keys", + "arbitrary-key": "shazam!", + "uuid": testing.ModelTag.Id(), + "type": "dummy", + "name": "test", + "resource-tags": map[string]string{"a": "b", "c": "d"}, + } + cfgAttrs, err := s.State.ComposeNewModelConfig(attrs) + c.Assert(err, jc.ErrorIsNil) + expectedCfg, err := config.New(config.UseDefaults, attrs) + c.Assert(err, jc.ErrorIsNil) + expected := expectedCfg.AllAttrs() + expected["apt-mirror"] = "http://cloud-mirror" + // config.New() adds logging-config so remove it. + expected["logging-config"] = "" + c.Assert(cfgAttrs, jc.DeepEquals, expected) } func (s *ModelConfigSuite) TestUpdateModelConfigRejectsControllerConfig(c *gc.C) { @@ -87,6 +115,68 @@ c.Assert(err, gc.ErrorMatches, `cannot set controller attribute "api-port" on a model`) } +func (s *ModelConfigSuite) TestUpdateModelConfigRemoveInherited(c *gc.C) { + attrs := map[string]interface{}{ + "apt-mirror": "http://different-mirror", + "arbitrary-key": "shazam!", + } + err := s.State.UpdateModelConfig(attrs, nil, nil) + c.Assert(err, jc.ErrorIsNil) + + err = s.State.UpdateModelConfig(nil, []string{"apt-mirror", "arbitrary-key"}, nil) + c.Assert(err, jc.ErrorIsNil) + cfg, err := s.State.ModelConfig() + c.Assert(err, jc.ErrorIsNil) + allAttrs := cfg.AllAttrs() + c.Assert(allAttrs["apt-mirror"], gc.Equals, "http://cloud-mirror") + _, ok := allAttrs["arbitrary-key"] + c.Assert(ok, jc.IsFalse) +} + +func (s *ModelConfigSuite) TestUpdateModelConfigCoerce(c *gc.C) { + attrs := map[string]interface{}{ + "resource-tags": map[string]string{"a": "b", "c": "d"}, + } + err := s.State.UpdateModelConfig(attrs, nil, nil) + c.Assert(err, jc.ErrorIsNil) + + modelSettings, err := s.State.ReadSettings(state.SettingsC, state.ModelGlobalKey) + c.Assert(err, jc.ErrorIsNil) + expectedTags := map[string]string{"a": "b", "c": "d"} + tagsStr := config.CoerceForStorage(modelSettings.Map())["resource-tags"].(string) + tagItems := strings.Split(tagsStr, " ") + tagsMap := make(map[string]string) + for _, kv := range tagItems { + parts := strings.Split(kv, "=") + tagsMap[parts[0]] = parts[1] + } + c.Assert(tagsMap, gc.DeepEquals, expectedTags) + + cfg, err := s.State.ModelConfig() + c.Assert(err, jc.ErrorIsNil) + c.Assert(cfg.AllAttrs()["resource-tags"], gc.DeepEquals, expectedTags) +} + +func (s *ModelConfigSuite) TestUpdateModelConfigPreferredOverRemove(c *gc.C) { + attrs := map[string]interface{}{ + "apt-mirror": "http://different-mirror", + "arbitrary-key": "shazam!", + } + err := s.State.UpdateModelConfig(attrs, nil, nil) + c.Assert(err, jc.ErrorIsNil) + + err = s.State.UpdateModelConfig(map[string]interface{}{ + "apt-mirror": "http://another-mirror", + }, []string{"apt-mirror", "arbitrary-key"}, nil) + c.Assert(err, jc.ErrorIsNil) + cfg, err := s.State.ModelConfig() + c.Assert(err, jc.ErrorIsNil) + allAttrs := cfg.AllAttrs() + c.Assert(allAttrs["apt-mirror"], gc.Equals, "http://another-mirror") + _, ok := allAttrs["arbitrary-key"] + c.Assert(ok, jc.IsFalse) +} + type ModelConfigSourceSuite struct { ConnSuite } @@ -107,7 +197,7 @@ c.Assert(err, jc.ErrorIsNil) } -func (s *ModelConfigSourceSuite) TestModelConfigWhenSetOverridesCloudValue(c *gc.C) { +func (s *ModelConfigSourceSuite) TestModelConfigWhenSetOverridesControllerValue(c *gc.C) { attrs := map[string]interface{}{ "authorized-keys": "different-keys", "apt-mirror": "http://anothermirror", @@ -147,6 +237,7 @@ owner := names.NewUserTag("test@remote") _, st, err := s.State.NewModel(state.ModelArgs{ Config: cfg, Owner: owner, CloudName: "dummy", + StorageProviderRegistry: storage.StaticProviderRegistry{}, }) c.Assert(err, jc.ErrorIsNil) defer st.Close() @@ -167,13 +258,21 @@ c.Assert(modelCfg.AllAttrs()["apt-mirror"], gc.Equals, "http://mirror") } -func (s *ModelConfigSourceSuite) TestModelConfigValues(c *gc.C) { - modelCfg, err := s.State.ModelConfig() - c.Assert(err, jc.ErrorIsNil) +func (s *ModelConfigSourceSuite) assertModelConfigValues(c *gc.C, modelCfg *config.Config, modelAttributes, controllerAttributes set.Strings) { expectedValues := make(config.ConfigValues) + defaultAttributes := set.NewStrings() + for defaultAttr := range config.ConfigDefaults() { + defaultAttributes.Add(defaultAttr) + } for attr, val := range modelCfg.AllAttrs() { source := "model" - if attr == "apt-mirror" || attr == "http-proxy" { + if defaultAttributes.Contains(attr) { + source = "default" + } + if modelAttributes.Contains(attr) { + source = "model" + } + if controllerAttributes.Contains(attr) { source = "controller" } expectedValues[attr] = config.ConfigValue{ @@ -186,49 +285,82 @@ c.Assert(sources, jc.DeepEquals, expectedValues) } -func (s *ModelConfigSourceSuite) TestModelConfigUpdateSetsSource(c *gc.C) { +func (s *ModelConfigSourceSuite) TestModelConfigValues(c *gc.C) { + modelCfg, err := s.State.ModelConfig() + c.Assert(err, jc.ErrorIsNil) + modelAttributes := set.NewStrings("name", "apt-mirror", "logging-config", "authorized-keys", "resource-tags") + s.assertModelConfigValues(c, modelCfg, modelAttributes, set.NewStrings("http-proxy")) +} + +func (s *ModelConfigSourceSuite) TestModelConfigUpdateSource(c *gc.C) { attrs := map[string]interface{}{ "http-proxy": "http://anotherproxy", + "apt-mirror": "http://mirror", } err := s.State.UpdateModelConfig(attrs, nil, nil) c.Assert(err, jc.ErrorIsNil) - modelCfg, err := s.State.ModelConfig() c.Assert(err, jc.ErrorIsNil) + modelAttributes := set.NewStrings("name", "http-proxy", "logging-config", "authorized-keys", "resource-tags") + s.assertModelConfigValues(c, modelCfg, modelAttributes, set.NewStrings("apt-mirror")) +} + +func (s *ModelConfigSourceSuite) TestModelConfigDefaults(c *gc.C) { expectedValues := make(config.ConfigValues) - for attr, val := range modelCfg.AllAttrs() { - source := "model" - if attr == "apt-mirror" { - source = "controller" - } + for attr, val := range config.ConfigDefaults() { + source := "default" expectedValues[attr] = config.ConfigValue{ Value: val, Source: source, } } - sources, err := s.State.ModelConfigValues() + expectedValues["http-proxy"] = config.ConfigValue{ + Value: "http://proxy", + Source: "controller", + } + expectedValues["apt-mirror"] = config.ConfigValue{ + Value: "http://mirror", + Source: "controller", + } + sources, err := s.State.ModelConfigDefaultValues() c.Assert(err, jc.ErrorIsNil) c.Assert(sources, jc.DeepEquals, expectedValues) } -func (s *ModelConfigSourceSuite) TestModelConfigDeleteSetsSource(c *gc.C) { - err := s.State.UpdateModelConfig(nil, []string{"apt-mirror"}, nil) +func (s *ModelConfigSourceSuite) TestUpdateModelConfigDefaults(c *gc.C) { + // Set up values that will be removed. + attrs := map[string]interface{}{ + "http-proxy": "http://http-proxy", + "https-proxy": "https://https-proxy", + } + err := s.State.UpdateModelConfigDefaultValues(attrs, nil) c.Assert(err, jc.ErrorIsNil) - modelCfg, err := s.State.ModelConfig() + attrs = map[string]interface{}{ + "apt-mirror": "http://different-mirror", + } + err = s.State.UpdateModelConfigDefaultValues(attrs, []string{"http-proxy", "https-proxy"}) + c.Assert(err, jc.ErrorIsNil) + + info := statetesting.NewMongoInfo() + anotherState, err := state.Open(s.modelTag, info, mongotest.DialOpts(), state.NewPolicyFunc(nil)) + c.Assert(err, jc.ErrorIsNil) + defer anotherState.Close() + + cfg, err := anotherState.ModelConfigDefaultValues() c.Assert(err, jc.ErrorIsNil) expectedValues := make(config.ConfigValues) - for attr, val := range modelCfg.AllAttrs() { - source := "model" - if attr == "http-proxy" { - source = "controller" - } + for attr, val := range config.ConfigDefaults() { expectedValues[attr] = config.ConfigValue{ Value: val, - Source: source, + Source: "default", } } - sources, err := s.State.ModelConfigValues() - c.Assert(err, jc.ErrorIsNil) - c.Assert(sources, jc.DeepEquals, expectedValues) + delete(expectedValues, "http-mirror") + delete(expectedValues, "https-mirror") + expectedValues["apt-mirror"] = config.ConfigValue{ + Value: "http://different-mirror", + Source: "controller", + } + c.Assert(cfg, jc.DeepEquals, expectedValues) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/model.go juju-core-2.0~beta15/src/github.com/juju/juju/state/model.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/model.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/model.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,6 +9,7 @@ "github.com/juju/errors" jujutxn "github.com/juju/txn" + "github.com/juju/version" "gopkg.in/juju/names.v2" "gopkg.in/mgo.v2" "gopkg.in/mgo.v2/bson" @@ -16,10 +17,11 @@ jujucloud "github.com/juju/juju/cloud" "github.com/juju/juju/constraints" + "github.com/juju/juju/core/description" "github.com/juju/juju/environs/config" "github.com/juju/juju/mongo" "github.com/juju/juju/status" - "github.com/juju/version" + "github.com/juju/juju/storage" ) // modelGlobalKey is the key for the model, its @@ -169,6 +171,10 @@ // Constraints contains the initial constraints for the model. Constraints constraints.Value + // StorageProviderRegistry is used to determine and store the + // details of the default storage pools. + StorageProviderRegistry storage.ProviderRegistry + // Owner is the user that owns the model. Owner names.UserTag @@ -187,6 +193,9 @@ if m.Owner == (names.UserTag{}) { return errors.NotValidf("empty Owner") } + if m.StorageProviderRegistry == nil { + return errors.NotValidf("nil StorageProviderRegistry") + } switch m.MigrationMode { case MigrationModeActive, MigrationModeImporting: default: @@ -252,7 +261,7 @@ uuid := args.Config.UUID() session := st.session.Copy() - newSt, err := newState(names.NewModelTag(uuid), session, st.mongoInfo, st.policy) + newSt, err := newState(names.NewModelTag(uuid), session, st.mongoInfo, st.newPolicy) if err != nil { return nil, nil, errors.Annotate(err, "could not create state for new model") } @@ -577,22 +586,22 @@ } // Users returns a slice of all users for this model. -func (m *Model) Users() ([]*ModelUser, error) { +func (m *Model) Users() ([]description.UserAccess, error) { if m.st.ModelUUID() != m.UUID() { return nil, errors.New("cannot lookup model users outside the current model") } coll, closer := m.st.getCollection(modelUsersC) defer closer() - var userDocs []modelUserDoc + var userDocs []userAccessDoc err := coll.Find(nil).All(&userDocs) if err != nil { return nil, errors.Trace(err) } - var modelUsers []*ModelUser + var modelUsers []description.UserAccess for _, doc := range userDocs { - mu, err := NewModelUser(m.st, doc) + mu, err := NewModelUserAccess(m.st, doc) if err != nil { return nil, errors.Trace(err) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/modelmigration.go juju-core-2.0~beta15/src/github.com/juju/juju/state/modelmigration.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/modelmigration.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/modelmigration.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,15 +6,18 @@ import ( "fmt" "strconv" + "strings" "time" "github.com/juju/errors" + "github.com/juju/utils/set" "gopkg.in/juju/names.v2" "gopkg.in/mgo.v2" "gopkg.in/mgo.v2/bson" "gopkg.in/mgo.v2/txn" "github.com/juju/juju/core/migration" + "github.com/juju/juju/mongo" ) // This file contains functionality for managing the state documents @@ -71,11 +74,36 @@ // current progress of the migration. SetStatusMessage(text string) error + // MinionReport records a report from a migration minion worker + // about the success or failure to complete its actions for a + // given migration phase. + MinionReport(tag names.Tag, phase migration.Phase, success bool) error + + // GetMinionReports returns details of the minions that have + // reported success or failure for the current migration phase, as + // well as those which are yet to report. + GetMinionReports() (*MinionReports, error) + + // WatchMinionReports returns a notify watcher which triggers when + // a migration minion has reported back about the success or failure + // of its actions for the current migration phase. + WatchMinionReports() (NotifyWatcher, error) + // Refresh updates the contents of the ModelMigration from the // underlying state. Refresh() error } +// MinionReports indicates the sets of agents whose migration minion +// workers have completed the current migration phase, have failed to +// complete the current migration phase, or are yet to report +// regarding the current migration phase. +type MinionReports struct { + Succeeded []names.Tag + Failed []names.Tag + Unknown []names.Tag +} + // modelMigration is an implementation of ModelMigration. type modelMigration struct { st *State @@ -155,6 +183,15 @@ StatusMessage string `bson:"status-message"` } +type modelMigMinionSyncDoc struct { + Id string `bson:"_id"` + MigrationId string `bson:"migration-id"` + Phase string `bson:"phase"` + EntityKey string `bson:"entity-key"` + Time int64 `bson:"time"` + Success bool `bson:"success"` +} + // Id implements ModelMigration. func (mig *modelMigration) Id() string { return mig.doc.Id @@ -257,6 +294,21 @@ update["success-time"] = now } var ops []txn.Op + + // If the migration aborted, make the model active again. + if nextPhase == migration.ABORTDONE { + ops = append(ops, txn.Op{ + C: modelsC, + Id: mig.doc.ModelUUID, + Assert: txn.DocExists, + Update: bson.M{ + "$set": bson.M{"migration-mode": MigrationModeActive}, + }, + }) + } + + // Set end timestamps and mark migration as no longer active if a + // terminal phase is hit. if nextPhase.IsTerminal() { nextDoc.EndTime = now update["end-time"] = now @@ -301,6 +353,168 @@ return nil } +// MinionReport implements ModelMigration. +func (mig *modelMigration) MinionReport(tag names.Tag, phase migration.Phase, success bool) error { + globalKey, err := agentTagToGlobalKey(tag) + if err != nil { + return errors.Trace(err) + } + docID := mig.minionReportId(phase, globalKey) + doc := modelMigMinionSyncDoc{ + Id: docID, + MigrationId: mig.Id(), + Phase: phase.String(), + EntityKey: globalKey, + Time: GetClock().Now().UnixNano(), + Success: success, + } + ops := []txn.Op{{ + C: migrationsMinionSyncC, + Id: docID, + Insert: doc, + Assert: txn.DocMissing, + }} + err = mig.st.runTransaction(ops) + if errors.Cause(err) == txn.ErrAborted { + coll, closer := mig.st.getCollection(migrationsMinionSyncC) + defer closer() + var existingDoc modelMigMinionSyncDoc + err := coll.FindId(docID).Select(bson.M{"success": 1}).One(&existingDoc) + if err != nil { + return errors.Annotate(err, "checking existing report") + } + if existingDoc.Success != success { + return errors.Errorf("conflicting reports received for %s/%s/%s", + mig.Id(), phase.String(), tag) + } + return nil + } else if err != nil { + return errors.Trace(err) + } + return nil +} + +// GetMinionReports implements ModelMigration. +func (mig *modelMigration) GetMinionReports() (*MinionReports, error) { + all, err := mig.getAllAgents() + if err != nil { + return nil, errors.Trace(err) + } + + phase, err := mig.Phase() + if err != nil { + return nil, errors.Annotate(err, "retrieving phase") + } + + coll, closer := mig.st.getCollection(migrationsMinionSyncC) + defer closer() + query := coll.Find(bson.M{"_id": bson.M{ + "$regex": "^" + mig.minionReportId(phase, ".+"), + }}) + query = query.Select(bson.M{ + "entity-key": 1, + "success": 1, + }) + var docs []bson.M + if err := query.All(&docs); err != nil { + return nil, errors.Annotate(err, "retrieving minion reports") + } + + succeeded := set.NewTags() + failed := set.NewTags() + for _, doc := range docs { + entityKey, ok := doc["entity-key"].(string) + if !ok { + return nil, errors.Errorf("unexpected entity-key %v", doc["entity-key"]) + } + tag, err := globalKeyToAgentTag(entityKey) + if err != nil { + return nil, errors.Trace(err) + } + success, ok := doc["success"].(bool) + if !ok { + return nil, errors.Errorf("unexpected success value: %v", doc["success"]) + } + if success { + succeeded.Add(tag) + } else { + failed.Add(tag) + } + } + + unknown := all.Difference(succeeded).Difference(failed) + + return &MinionReports{ + Succeeded: succeeded.Values(), + Failed: failed.Values(), + Unknown: unknown.Values(), + }, nil +} + +// WatchMinionReports implements ModelMigration. +func (mig *modelMigration) WatchMinionReports() (NotifyWatcher, error) { + phase, err := mig.Phase() + if err != nil { + return nil, errors.Annotate(err, "retrieving phase") + } + prefix := mig.minionReportId(phase, "") + filter := func(rawId interface{}) bool { + id, ok := rawId.(string) + if !ok { + return false + } + return strings.HasPrefix(id, prefix) + } + return newNotifyCollWatcher(mig.st, migrationsMinionSyncC, filter), nil +} + +func (mig *modelMigration) minionReportId(phase migration.Phase, globalKey string) string { + return fmt.Sprintf("%s:%s:%s", mig.Id(), phase.String(), globalKey) +} + +func (mig *modelMigration) getAllAgents() (set.Tags, error) { + machineTags, err := mig.loadAgentTags(machinesC, "machineid", + func(id string) names.Tag { return names.NewMachineTag(id) }, + ) + if err != nil { + return nil, errors.Annotate(err, "loading machine tags") + } + + unitTags, err := mig.loadAgentTags(unitsC, "name", + func(name string) names.Tag { return names.NewUnitTag(name) }, + ) + if err != nil { + return nil, errors.Annotate(err, "loading unit names") + } + + return machineTags.Union(unitTags), nil +} + +func (mig *modelMigration) loadAgentTags(collName, fieldName string, convert func(string) names.Tag) ( + set.Tags, error, +) { + // During migrations we know that nothing there are no machines or + // units being provisioned or destroyed so a simple query of the + // collections will do. + coll, closer := mig.st.getCollection(collName) + defer closer() + var docs []bson.M + err := coll.Find(nil).Select(bson.M{fieldName: 1}).All(&docs) + if err != nil { + return nil, errors.Trace(err) + } + + out := set.NewTags() + for _, doc := range docs { + v, ok := doc[fieldName].(string) + if !ok { + return nil, errors.Errorf("invalid %s value: %v", fieldName, doc[fieldName]) + } + out.Add(convert(v)) + } + return out, nil +} + // Refresh implements ModelMigration. func (mig *modelMigration) Refresh() error { // Only the status document is updated. The modelMigDoc is static @@ -389,6 +603,7 @@ StartTime: now, Phase: migration.QUIESCE.String(), PhaseChangedTime: now, + StatusMessage: "starting", } return []txn.Op{{ C: migrationsC, @@ -405,6 +620,13 @@ Id: modelUUID, Assert: txn.DocMissing, Insert: bson.M{"id": doc.Id}, + }, { + C: modelsC, + Id: modelUUID, + Assert: txn.DocExists, + Update: bson.M{"$set": bson.M{ + "migration-mode": MigrationModeExporting, + }}, }, model.assertActiveOp(), }, nil } @@ -430,14 +652,33 @@ return nil } -// GetModelMigration returns the most recent ModelMigration for a +// LatestModelMigration returns the most recent ModelMigration for a // model (if any). -func (st *State) GetModelMigration() (ModelMigration, error) { +func (st *State) LatestModelMigration() (ModelMigration, error) { migColl, closer := st.getCollection(migrationsC) defer closer() - query := migColl.Find(bson.M{"model-uuid": st.ModelUUID()}) query = query.Sort("-_id").Limit(1) + mig, err := st.modelMigrationFromQuery(query) + if err != nil { + return nil, errors.Trace(err) + } + return mig, nil +} + +// ModelMigration retrieves a specific ModelMigration by its +// id. See also LatestModelMigration. +func (st *State) ModelMigration(id string) (ModelMigration, error) { + migColl, closer := st.getCollection(migrationsC) + defer closer() + mig, err := st.modelMigrationFromQuery(migColl.FindId(id)) + if err != nil { + return nil, errors.Trace(err) + } + return mig, nil +} + +func (st *State) modelMigrationFromQuery(query mongo.Query) (ModelMigration, error) { var doc modelMigDoc err := query.One(&doc) if err == mgo.ErrNotFound { @@ -450,8 +691,10 @@ defer closer() var statusDoc modelMigStatusDoc err = statusColl.FindId(doc.Id).One(&statusDoc) - if err != nil { - return nil, errors.Annotate(err, "failed to find status document") + if err == mgo.ErrNotFound { + return nil, errors.NotFoundf("migration status") + } else if err != nil { + return nil, errors.Annotate(err, "migration status lookup failed") } return &modelMigration{ @@ -479,3 +722,30 @@ } return time.Unix(0, i) } + +func agentTagToGlobalKey(tag names.Tag) (string, error) { + switch t := tag.(type) { + case names.MachineTag: + return machineGlobalKey(t.Id()), nil + case names.UnitTag: + return unitAgentGlobalKey(t.Id()), nil + default: + return "", errors.Errorf("%s is not an agent tag", tag) + } +} + +func globalKeyToAgentTag(key string) (names.Tag, error) { + parts := strings.SplitN(key, "#", 2) + if len(parts) != 2 { + return nil, errors.NotValidf("global key %q", key) + } + keyType, keyId := parts[0], parts[1] + switch keyType { + case "m": + return names.NewMachineTag(keyId), nil + case "u": + return names.NewUnitTag(keyId), nil + default: + return nil, errors.NotValidf("global key type %q", keyType) + } +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/modelmigration_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/modelmigration_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/modelmigration_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/modelmigration_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -18,6 +18,7 @@ "github.com/juju/juju/state" statetesting "github.com/juju/juju/state/testing" coretesting "github.com/juju/juju/testing" + "github.com/juju/juju/testing/factory" ) type ModelMigrationSuite struct { @@ -56,6 +57,10 @@ } func (s *ModelMigrationSuite) TestCreate(c *gc.C) { + model, err := s.State2.Model() + c.Assert(err, jc.ErrorIsNil) + c.Assert(model.MigrationMode(), gc.Equals, state.MigrationModeActive) + mig, err := s.State2.CreateModelMigration(s.stdSpec) c.Assert(err, jc.ErrorIsNil) @@ -63,10 +68,9 @@ checkIdAndAttempt(c, mig, 0) c.Check(mig.StartTime(), gc.Equals, s.clock.Now()) - c.Check(mig.SuccessTime().IsZero(), jc.IsTrue) c.Check(mig.EndTime().IsZero(), jc.IsTrue) - c.Check(mig.StatusMessage(), gc.Equals, "") + c.Check(mig.StatusMessage(), gc.Equals, "starting") c.Check(mig.InitiatedBy(), gc.Equals, "admin") info, err := mig.TargetInfo() @@ -77,6 +81,9 @@ c.Check(mig.PhaseChangedTime(), gc.Equals, mig.StartTime()) assertMigrationActive(c, s.State2) + + c.Assert(model.Refresh(), jc.ErrorIsNil) + c.Check(model.MigrationMode(), gc.Equals, state.MigrationModeExporting) } func (s *ModelMigrationSuite) TestIdSequencesAreIndependent(c *gc.C) { @@ -213,18 +220,18 @@ c.Check(err, gc.ErrorMatches, "model already attached to target controller") } -func (s *ModelMigrationSuite) TestGet(c *gc.C) { +func (s *ModelMigrationSuite) TestLatestModelMigration(c *gc.C) { mig1, err := s.State2.CreateModelMigration(s.stdSpec) c.Assert(err, jc.ErrorIsNil) - mig2, err := s.State2.GetModelMigration() + mig2, err := s.State2.LatestModelMigration() c.Assert(err, jc.ErrorIsNil) c.Assert(mig1.Id(), gc.Equals, mig2.Id()) } -func (s *ModelMigrationSuite) TestGetNotExist(c *gc.C) { - mig, err := s.State.GetModelMigration() +func (s *ModelMigrationSuite) TestLatestModelMigrationNotExist(c *gc.C) { + mig, err := s.State.LatestModelMigration() c.Check(mig, gc.IsNil) c.Check(errors.IsNotFound(err), jc.IsTrue) } @@ -237,7 +244,7 @@ _, err := s.State2.CreateModelMigration(s.stdSpec) c.Assert(err, jc.ErrorIsNil) - mig, err := s.State2.GetModelMigration() + mig, err := s.State2.LatestModelMigration() c.Check(mig.Id(), gc.Equals, fmt.Sprintf("%s:%d", modelUUID, i)) c.Assert(mig.SetPhase(migration.ABORT), jc.ErrorIsNil) @@ -245,20 +252,36 @@ } } +func (s *ModelMigrationSuite) TestModelMigration(c *gc.C) { + mig1, err := s.State2.CreateModelMigration(s.stdSpec) + c.Assert(err, jc.ErrorIsNil) + + mig2, err := s.State2.ModelMigration(mig1.Id()) + c.Check(err, jc.ErrorIsNil) + c.Check(mig1.Id(), gc.Equals, mig2.Id()) + c.Check(mig2.StartTime(), gc.Equals, s.clock.Now()) +} + +func (s *ModelMigrationSuite) TestModelMigrationNotFound(c *gc.C) { + _, err := s.State2.ModelMigration("does not exist") + c.Check(err, jc.Satisfies, errors.IsNotFound) + c.Check(err, gc.ErrorMatches, "migration not found") +} + func (s *ModelMigrationSuite) TestRefresh(c *gc.C) { mig1, err := s.State2.CreateModelMigration(s.stdSpec) c.Assert(err, jc.ErrorIsNil) - mig2, err := s.State2.GetModelMigration() + mig2, err := s.State2.LatestModelMigration() c.Assert(err, jc.ErrorIsNil) - err = mig1.SetPhase(migration.READONLY) + err = mig1.SetPhase(migration.PRECHECK) c.Assert(err, jc.ErrorIsNil) assertPhase(c, mig2, migration.QUIESCE) err = mig2.Refresh() c.Assert(err, jc.ErrorIsNil) - assertPhase(c, mig2, migration.READONLY) + assertPhase(c, mig2, migration.PRECHECK) } func (s *ModelMigrationSuite) TestSuccessfulPhaseTransitions(c *gc.C) { @@ -268,11 +291,10 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(mig, gc.NotNil) - mig2, err := st.GetModelMigration() + mig2, err := st.LatestModelMigration() c.Assert(err, jc.ErrorIsNil) phases := []migration.Phase{ - migration.READONLY, migration.PRECHECK, migration.IMPORT, migration.VALIDATION, @@ -330,6 +352,11 @@ c.Assert(mig.SetPhase(migration.ABORTDONE), jc.ErrorIsNil) s.assertMigrationCleanedUp(c, mig) + + // Model should be set back to active. + model, err := s.State2.Model() + c.Assert(err, jc.ErrorIsNil) + c.Assert(model.MigrationMode(), gc.Equals, state.MigrationModeActive) } func (s *ModelMigrationSuite) TestREAPFAILEDCleanup(c *gc.C) { @@ -338,7 +365,6 @@ // Advance the migration to REAPFAILED. phases := []migration.Phase{ - migration.READONLY, migration.PRECHECK, migration.IMPORT, migration.VALIDATION, @@ -374,31 +400,31 @@ c.Assert(mig, gc.Not(gc.IsNil)) defer state.SetBeforeHooks(c, s.State2, func() { - mig, err := s.State2.GetModelMigration() + mig, err := s.State2.LatestModelMigration() c.Assert(err, jc.ErrorIsNil) - c.Assert(mig.SetPhase(migration.READONLY), jc.ErrorIsNil) + c.Assert(mig.SetPhase(migration.PRECHECK), jc.ErrorIsNil) }).Check() - err = mig.SetPhase(migration.READONLY) + err = mig.SetPhase(migration.PRECHECK) c.Assert(err, gc.ErrorMatches, "phase already changed") assertPhase(c, mig, migration.QUIESCE) // After a refresh it the phase change should be ok. c.Assert(mig.Refresh(), jc.ErrorIsNil) - err = mig.SetPhase(migration.READONLY) + err = mig.SetPhase(migration.PRECHECK) c.Assert(err, jc.ErrorIsNil) - assertPhase(c, mig, migration.READONLY) + assertPhase(c, mig, migration.PRECHECK) } func (s *ModelMigrationSuite) TestStatusMessage(c *gc.C) { mig, err := s.State2.CreateModelMigration(s.stdSpec) c.Assert(mig, gc.Not(gc.IsNil)) - mig2, err := s.State2.GetModelMigration() + mig2, err := s.State2.LatestModelMigration() c.Assert(err, jc.ErrorIsNil) - c.Check(mig.StatusMessage(), gc.Equals, "") - c.Check(mig2.StatusMessage(), gc.Equals, "") + c.Check(mig.StatusMessage(), gc.Equals, "starting") + c.Check(mig2.StatusMessage(), gc.Equals, "starting") err = mig.SetStatusMessage("foo bar") c.Assert(err, jc.ErrorIsNil) @@ -411,7 +437,7 @@ func (s *ModelMigrationSuite) TestWatchForModelMigration(c *gc.C) { // Start watching for migration. - w, wc := s.createWatcher(c, s.State2) + w, wc := s.createMigrationWatcher(c, s.State2) wc.AssertOneChange() // Create the migration - should be reported. @@ -437,19 +463,19 @@ c.Assert(err, jc.ErrorIsNil) // Start watching for a migration - the in progress one should be reported. - _, wc := s.createWatcher(c, s.State2) + _, wc := s.createMigrationWatcher(c, s.State2) wc.AssertOneChange() } func (s *ModelMigrationSuite) TestWatchForModelMigrationMultiModel(c *gc.C) { - _, wc2 := s.createWatcher(c, s.State2) + _, wc2 := s.createMigrationWatcher(c, s.State2) wc2.AssertOneChange() // Create another hosted model to migrate and watch for // migrations. State3 := s.Factory.MakeModel(c, nil) s.AddCleanup(func(*gc.C) { State3.Close() }) - _, wc3 := s.createWatcher(c, State3) + _, wc3 := s.createMigrationWatcher(c, State3) wc3.AssertOneChange() // Create a migration for 2. @@ -465,7 +491,7 @@ wc3.AssertOneChange() } -func (s *ModelMigrationSuite) createWatcher(c *gc.C, st *state.State) ( +func (s *ModelMigrationSuite) createMigrationWatcher(c *gc.C, st *state.State) ( state.NotifyWatcher, statetesting.NotifyWatcherC, ) { w := st.WatchForModelMigration() @@ -494,7 +520,7 @@ wc.AssertOneChange() // Change phase. - c.Assert(mig2.SetPhase(migration.READONLY), jc.ErrorIsNil) + c.Assert(mig2.SetPhase(migration.PRECHECK), jc.ErrorIsNil) wc.AssertOneChange() // End it. @@ -517,14 +543,14 @@ func (s *ModelMigrationSuite) TestWatchMigrationStatusMultiModel(c *gc.C) { _, wc2 := s.createStatusWatcher(c, s.State2) - wc2.AssertOneChange() + wc2.AssertOneChange() // initial event // Create another hosted model to migrate and watch for // migrations. State3 := s.Factory.MakeModel(c, nil) s.AddCleanup(func(*gc.C) { State3.Close() }) _, wc3 := s.createStatusWatcher(c, State3) - wc3.AssertOneChange() + wc3.AssertOneChange() // initial event // Create a migration for 2. mig, err := s.State2.CreateModelMigration(s.stdSpec) @@ -545,15 +571,163 @@ wc3.AssertNoChange() } +func (s *ModelMigrationSuite) TestMinionReports(c *gc.C) { + // Create some machines and units to report with. + factory2 := factory.NewFactory(s.State2) + m0 := factory2.MakeMachine(c, nil) + u0 := factory2.MakeUnit(c, &factory.UnitParams{Machine: m0}) + m1 := factory2.MakeMachine(c, nil) + m2 := factory2.MakeMachine(c, nil) + + mig, err := s.State2.CreateModelMigration(s.stdSpec) + c.Assert(err, jc.ErrorIsNil) + + const phase = migration.QUIESCE + c.Assert(mig.MinionReport(m0.Tag(), phase, true), jc.ErrorIsNil) + c.Assert(mig.MinionReport(m1.Tag(), phase, false), jc.ErrorIsNil) + c.Assert(mig.MinionReport(u0.Tag(), phase, true), jc.ErrorIsNil) + + reports, err := mig.GetMinionReports() + c.Assert(err, jc.ErrorIsNil) + c.Check(reports.Succeeded, jc.SameContents, []names.Tag{m0.Tag(), u0.Tag()}) + c.Check(reports.Failed, jc.SameContents, []names.Tag{m1.Tag()}) + c.Check(reports.Unknown, jc.SameContents, []names.Tag{m2.Tag()}) +} + +func (s *ModelMigrationSuite) TestDuplicateMinionReportsSameSuccess(c *gc.C) { + // It should be OK for a minion report to arrive more than once + // for the same migration, agent and phase as long as the value of + // "success" is the same. + mig, err := s.State2.CreateModelMigration(s.stdSpec) + c.Assert(err, jc.ErrorIsNil) + tag := names.NewMachineTag("42") + c.Check(mig.MinionReport(tag, migration.QUIESCE, true), jc.ErrorIsNil) + c.Check(mig.MinionReport(tag, migration.QUIESCE, true), jc.ErrorIsNil) +} + +func (s *ModelMigrationSuite) TestDuplicateMinionReportsDifferingSuccess(c *gc.C) { + // It is not OK for a minion report to arrive more than once for + // the same migration, agent and phase when the "success" value + // changes. + mig, err := s.State2.CreateModelMigration(s.stdSpec) + c.Assert(err, jc.ErrorIsNil) + tag := names.NewMachineTag("42") + c.Check(mig.MinionReport(tag, migration.QUIESCE, true), jc.ErrorIsNil) + err = mig.MinionReport(tag, migration.QUIESCE, false) + c.Check(err, gc.ErrorMatches, + fmt.Sprintf("conflicting reports received for %s/QUIESCE/machine-42", mig.Id())) +} + +func (s *ModelMigrationSuite) TestMinionReportWithOldPhase(c *gc.C) { + // It is OK for a report to arrive for even a migration has moved + // on. + mig, err := s.State2.CreateModelMigration(s.stdSpec) + c.Assert(err, jc.ErrorIsNil) + + // Get another reference to the same migration. + migalt, err := s.State2.LatestModelMigration() + c.Assert(err, jc.ErrorIsNil) + + // Confirm that there's no reports when starting. + reports, err := mig.GetMinionReports() + c.Assert(err, jc.ErrorIsNil) + c.Check(reports.Succeeded, gc.HasLen, 0) + + // Advance the migration + c.Assert(mig.SetPhase(migration.PRECHECK), jc.ErrorIsNil) + + // Submit minion report for the old phase. + tag := names.NewMachineTag("42") + c.Assert(mig.MinionReport(tag, migration.QUIESCE, true), jc.ErrorIsNil) + + // The report should still have been recorded. + reports, err = migalt.GetMinionReports() + c.Assert(err, jc.ErrorIsNil) + c.Check(reports.Succeeded, jc.SameContents, []names.Tag{tag}) +} + +func (s *ModelMigrationSuite) TestMinionReportWithInactiveMigration(c *gc.C) { + // Create a migration. + mig, err := s.State2.CreateModelMigration(s.stdSpec) + c.Assert(err, jc.ErrorIsNil) + + // Get another reference to the same migration. + migalt, err := s.State2.LatestModelMigration() + c.Assert(err, jc.ErrorIsNil) + + // Abort the migration. + c.Assert(mig.SetPhase(migration.ABORT), jc.ErrorIsNil) + c.Assert(mig.SetPhase(migration.ABORTDONE), jc.ErrorIsNil) + + // Confirm that there's no reports when starting. + reports, err := mig.GetMinionReports() + c.Assert(err, jc.ErrorIsNil) + c.Check(reports.Succeeded, gc.HasLen, 0) + + // Submit a minion report for it. + tag := names.NewMachineTag("42") + c.Assert(mig.MinionReport(tag, migration.QUIESCE, true), jc.ErrorIsNil) + + // The report should still have been recorded. + reports, err = migalt.GetMinionReports() + c.Assert(err, jc.ErrorIsNil) + c.Check(reports.Succeeded, jc.SameContents, []names.Tag{tag}) +} + +func (s *ModelMigrationSuite) TestWatchMinionReports(c *gc.C) { + mig, wc := s.createMigAndWatchReports(c, s.State2) + wc.AssertOneChange() // initial event + + // A report should trigger the watcher. + c.Assert(mig.MinionReport(names.NewMachineTag("0"), migration.QUIESCE, true), jc.ErrorIsNil) + wc.AssertOneChange() + + // A report for a different phase shouldn't trigger the watcher. + c.Assert(mig.MinionReport(names.NewMachineTag("1"), migration.IMPORT, true), jc.ErrorIsNil) + wc.AssertNoChange() +} + +func (s *ModelMigrationSuite) TestWatchMinionReportsMultiModel(c *gc.C) { + mig, wc := s.createMigAndWatchReports(c, s.State2) + wc.AssertOneChange() // initial event + + State3 := s.Factory.MakeModel(c, nil) + s.AddCleanup(func(*gc.C) { State3.Close() }) + mig3, wc3 := s.createMigAndWatchReports(c, State3) + wc3.AssertOneChange() // initial event + + // Ensure the correct watchers are triggered. + c.Assert(mig.MinionReport(names.NewMachineTag("0"), migration.QUIESCE, true), jc.ErrorIsNil) + wc.AssertOneChange() + wc3.AssertNoChange() + + c.Assert(mig3.MinionReport(names.NewMachineTag("0"), migration.QUIESCE, true), jc.ErrorIsNil) + wc.AssertNoChange() + wc3.AssertOneChange() +} + func (s *ModelMigrationSuite) createStatusWatcher(c *gc.C, st *state.State) ( state.NotifyWatcher, statetesting.NotifyWatcherC, ) { - w, err := st.WatchMigrationStatus() - c.Assert(err, jc.ErrorIsNil) + w := st.WatchMigrationStatus() s.AddCleanup(func(c *gc.C) { statetesting.AssertStop(c, w) }) return w, statetesting.NewNotifyWatcherC(c, st, w) } +func (s *ModelMigrationSuite) createMigAndWatchReports(c *gc.C, st *state.State) ( + state.ModelMigration, statetesting.NotifyWatcherC, +) { + mig, err := st.CreateModelMigration(s.stdSpec) + c.Assert(err, jc.ErrorIsNil) + + w, err := mig.WatchMinionReports() + c.Assert(err, jc.ErrorIsNil) + s.AddCleanup(func(*gc.C) { statetesting.AssertStop(c, w) }) + wc := statetesting.NewNotifyWatcherC(c, st, w) + + return mig, wc +} + func assertPhase(c *gc.C, mig state.ModelMigration, phase migration.Phase) { actualPhase, err := mig.Phase() c.Assert(err, jc.ErrorIsNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/model_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/model_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/model_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/model_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -15,10 +15,12 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/cloud" + "github.com/juju/juju/core/description" "github.com/juju/juju/environs/config" "github.com/juju/juju/mongo/mongotest" "github.com/juju/juju/state" statetesting "github.com/juju/juju/state/testing" + "github.com/juju/juju/storage" "github.com/juju/juju/testing" "github.com/juju/juju/testing/factory" ) @@ -62,7 +64,12 @@ cfg, _ := s.createTestModelConfig(c) owner := names.NewUserTag("non-existent@local") - _, _, err := s.State.NewModel(state.ModelArgs{CloudName: "dummy", Config: cfg, Owner: owner}) + _, _, err := s.State.NewModel(state.ModelArgs{ + CloudName: "dummy", + Config: cfg, + Owner: owner, + StorageProviderRegistry: storage.StaticProviderRegistry{}, + }) c.Assert(err, gc.ErrorMatches, `cannot create model: user "non-existent" not found`) } @@ -71,7 +78,12 @@ owner := s.Factory.MakeUser(c, nil).UserTag() // Create the first model. - _, st1, err := s.State.NewModel(state.ModelArgs{CloudName: "dummy", Config: cfg, Owner: owner}) + _, st1, err := s.State.NewModel(state.ModelArgs{ + CloudName: "dummy", + Config: cfg, + Owner: owner, + StorageProviderRegistry: storage.StaticProviderRegistry{}, + }) c.Assert(err, jc.ErrorIsNil) defer st1.Close() @@ -83,7 +95,12 @@ "name": cfg.Name(), "uuid": newUUID.String(), }) - _, _, err = s.State.NewModel(state.ModelArgs{CloudName: "dummy", Config: cfg2, Owner: owner}) + _, _, err = s.State.NewModel(state.ModelArgs{ + CloudName: "dummy", + Config: cfg2, + Owner: owner, + StorageProviderRegistry: storage.StaticProviderRegistry{}, + }) errMsg := fmt.Sprintf("model %q for %s already exists", cfg2.Name(), owner.Canonical()) c.Assert(err, gc.ErrorMatches, errMsg) c.Assert(errors.IsAlreadyExists(err), jc.IsTrue) @@ -102,7 +119,12 @@ c.Assert(err, jc.ErrorIsNil) // We should now be able to create the other model. - env2, st2, err := s.State.NewModel(state.ModelArgs{CloudName: "dummy", Config: cfg2, Owner: owner}) + env2, st2, err := s.State.NewModel(state.ModelArgs{ + CloudName: "dummy", + Config: cfg2, + Owner: owner, + StorageProviderRegistry: storage.StaticProviderRegistry{}, + }) c.Assert(err, jc.ErrorIsNil) defer st2.Close() c.Assert(env2, gc.NotNil) @@ -113,7 +135,12 @@ cfg, uuid := s.createTestModelConfig(c) owner := names.NewUserTag("test@remote") - model, st, err := s.State.NewModel(state.ModelArgs{CloudName: "dummy", Config: cfg, Owner: owner}) + model, st, err := s.State.NewModel(state.ModelArgs{ + CloudName: "dummy", + Config: cfg, + Owner: owner, + StorageProviderRegistry: storage.StaticProviderRegistry{}, + }) c.Assert(err, jc.ErrorIsNil) defer st.Close() @@ -155,10 +182,11 @@ owner := names.NewUserTag("test@remote") env, st, err := s.State.NewModel(state.ModelArgs{ - CloudName: "dummy", - Config: cfg, - Owner: owner, - MigrationMode: state.MigrationModeImporting, + CloudName: "dummy", + Config: cfg, + Owner: owner, + MigrationMode: state.MigrationModeImporting, + StorageProviderRegistry: storage.StaticProviderRegistry{}, }) c.Assert(err, jc.ErrorIsNil) defer st.Close() @@ -170,7 +198,12 @@ cfg, _ := s.createTestModelConfig(c) owner := names.NewUserTag("test@remote") - env, st, err := s.State.NewModel(state.ModelArgs{CloudName: "dummy", Config: cfg, Owner: owner}) + env, st, err := s.State.NewModel(state.ModelArgs{ + CloudName: "dummy", + Config: cfg, + Owner: owner, + StorageProviderRegistry: storage.StaticProviderRegistry{}, + }) c.Assert(err, jc.ErrorIsNil) defer st.Close() @@ -197,6 +230,7 @@ CloudName: "dummy", Config: cfg, Owner: names.NewUserTag("test@remote"), + StorageProviderRegistry: storage.StaticProviderRegistry{}, }) c.Assert(err, jc.ErrorIsNil) defer st.Close() @@ -693,14 +727,14 @@ assertObtainedUsersMatchExpectedUsers(c, obtainedUsersOtherEnv, expectedUsersOtherEnv) } -func addModelUsers(c *gc.C, st *state.State) (expected []*state.ModelUser) { +func addModelUsers(c *gc.C, st *state.State) (expected []description.UserAccess) { // get the model owner testAdmin := names.NewUserTag("test-admin") - owner, err := st.ModelUser(testAdmin) + owner, err := st.UserAccess(testAdmin, st.ModelTag()) c.Assert(err, jc.ErrorIsNil) f := factory.NewFactory(st) - return []*state.ModelUser{ + return []description.UserAccess{ // we expect the owner to be an existing model user owner, // add new users to the model @@ -710,13 +744,13 @@ } } -func assertObtainedUsersMatchExpectedUsers(c *gc.C, obtainedUsers, expectedUsers []*state.ModelUser) { +func assertObtainedUsersMatchExpectedUsers(c *gc.C, obtainedUsers, expectedUsers []description.UserAccess) { c.Assert(len(obtainedUsers), gc.Equals, len(expectedUsers)) for i, obtained := range obtainedUsers { - c.Assert(obtained.ModelTag().Id(), gc.Equals, expectedUsers[i].ModelTag().Id()) - c.Assert(obtained.UserName(), gc.Equals, expectedUsers[i].UserName()) - c.Assert(obtained.DisplayName(), gc.Equals, expectedUsers[i].DisplayName()) - c.Assert(obtained.CreatedBy(), gc.Equals, expectedUsers[i].CreatedBy()) + c.Assert(obtained.Object.Id(), gc.Equals, expectedUsers[i].Object.Id()) + c.Assert(obtained.UserTag, gc.Equals, expectedUsers[i].UserTag) + c.Assert(obtained.DisplayName, gc.Equals, expectedUsers[i].DisplayName) + c.Assert(obtained.CreatedBy, gc.Equals, expectedUsers[i].CreatedBy) } } @@ -777,7 +811,12 @@ st, owner := s.initializeState(c, []cloud.Region{{Name: "some-region"}}, []cloud.AuthType{cloud.EmptyAuthType}, nil) defer st.Close() cfg, _ := createTestModelConfig(c, st.ModelUUID()) - _, _, err := st.NewModel(state.ModelArgs{CloudName: "another", Config: cfg, Owner: owner}) + _, _, err := st.NewModel(state.ModelArgs{ + CloudName: "another", + Config: cfg, + Owner: owner, + StorageProviderRegistry: storage.StaticProviderRegistry{}, + }) c.Assert(err, gc.ErrorMatches, "controller cloud dummy does not match model cloud another") } @@ -786,8 +825,11 @@ defer st.Close() cfg, _ := createTestModelConfig(c, st.ModelUUID()) _, _, err := st.NewModel(state.ModelArgs{ - CloudName: "dummy", - Config: cfg, Owner: owner, CloudRegion: "missing-region", + CloudName: "dummy", + Config: cfg, + Owner: owner, + CloudRegion: "missing-region", + StorageProviderRegistry: storage.StaticProviderRegistry{}, }) c.Assert(err, gc.ErrorMatches, `region "missing-region" not found \(expected one of \["some-region"\]\)`) } @@ -796,7 +838,12 @@ st, owner := s.initializeState(c, []cloud.Region{{Name: "some-region"}}, []cloud.AuthType{cloud.EmptyAuthType}, nil) defer st.Close() cfg, _ := createTestModelConfig(c, st.ModelUUID()) - _, _, err := st.NewModel(state.ModelArgs{CloudName: "dummy", Config: cfg, Owner: owner}) + _, _, err := st.NewModel(state.ModelArgs{ + CloudName: "dummy", + Config: cfg, + Owner: owner, + StorageProviderRegistry: storage.StaticProviderRegistry{}, + }) c.Assert(err, gc.ErrorMatches, "missing CloudRegion not valid") } @@ -809,8 +856,11 @@ defer st.Close() cfg, _ := createTestModelConfig(c, st.ModelUUID()) _, _, err := st.NewModel(state.ModelArgs{ - CloudName: "dummy", - Config: cfg, Owner: owner, CloudCredential: "unknown-credential", + CloudName: "dummy", + Config: cfg, + Owner: owner, + CloudCredential: "unknown-credential", + StorageProviderRegistry: storage.StaticProviderRegistry{}, }) c.Assert(err, gc.ErrorMatches, `credential "unknown-credential" not found`) } @@ -825,7 +875,9 @@ cfg, _ := createTestModelConfig(c, st.ModelUUID()) _, _, err := st.NewModel(state.ModelArgs{ CloudName: "dummy", - Config: cfg, Owner: owner, + Config: cfg, + Owner: owner, + StorageProviderRegistry: storage.StaticProviderRegistry{}, }) c.Assert(err, gc.ErrorMatches, "missing CloudCredential not valid") } @@ -836,7 +888,10 @@ cfg, _ := createTestModelConfig(c, st.ModelUUID()) cfg, err := cfg.Apply(map[string]interface{}{"name": "whatever"}) c.Assert(err, jc.ErrorIsNil) - _, newSt, err := st.NewModel(state.ModelArgs{CloudName: "dummy", Config: cfg, Owner: owner}) + _, newSt, err := st.NewModel(state.ModelArgs{ + CloudName: "dummy", Config: cfg, Owner: owner, + StorageProviderRegistry: storage.StaticProviderRegistry{}, + }) c.Assert(err, jc.ErrorIsNil) newSt.Close() } @@ -863,11 +918,12 @@ st, err := state.Initialize(state.InitializeParams{ ControllerConfig: controllerCfg, ControllerModelArgs: state.ModelArgs{ - Owner: owner, - Config: cfg, - CloudName: "dummy", - CloudRegion: controllerRegion, - CloudCredential: controllerCredential, + Owner: owner, + Config: cfg, + CloudName: "dummy", + CloudRegion: controllerRegion, + CloudCredential: controllerCredential, + StorageProviderRegistry: storage.StaticProviderRegistry{}, }, CloudName: "dummy", Cloud: cloud.Cloud{ diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/modeluser.go juju-core-2.0~beta15/src/github.com/juju/juju/state/modeluser.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/modeluser.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/modeluser.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,43 +9,13 @@ "time" "github.com/juju/errors" - jujutxn "github.com/juju/txn" "gopkg.in/juju/names.v2" "gopkg.in/mgo.v2" "gopkg.in/mgo.v2/bson" "gopkg.in/mgo.v2/txn" -) -// ModelUser represents a user access to an model whereas the user -// could represent a remote user or a user across multiple models the -// model user always represents a single user for a single model. -// There should be no more than one ModelUser per model. -type ModelUser struct { - st *State - doc modelUserDoc - modelPermission *permission -} - -// NewModelUser returns a ModelUser with permissions. -func NewModelUser(st *State, doc modelUserDoc) (*ModelUser, error) { - mu := &ModelUser{ - st: st, - doc: doc, - } - if err := mu.refreshPermission(); err != nil { - return nil, errors.Trace(err) - } - return mu, nil -} - -type modelUserDoc struct { - ID string `bson:"_id"` - ModelUUID string `bson:"model-uuid"` - UserName string `bson:"user"` - DisplayName string `bson:"displayname"` - CreatedBy string `bson:"createdby"` - DateCreated time.Time `bson:"datecreated"` -} + "github.com/juju/juju/core/description" +) // modelUserLastConnectionDoc is updated by the apiserver whenever the user // connects over the API. This update is not done using mgo.txn so the values @@ -59,102 +29,28 @@ LastConnection time.Time `bson:"last-connection"` } -// ID returns the ID of the model user. -func (e *ModelUser) ID() string { - return e.doc.ID -} - -// ModelTag returns the model tag of the model user. -func (e *ModelUser) ModelTag() names.ModelTag { - return names.NewModelTag(e.doc.ModelUUID) -} - -// UserTag returns the tag for the model user. -func (e *ModelUser) UserTag() names.UserTag { - return names.NewUserTag(e.doc.UserName) -} - -// UserName returns the user name of the model user. -func (e *ModelUser) UserName() string { - return e.doc.UserName -} - -// DisplayName returns the display name of the model user. -func (e *ModelUser) DisplayName() string { - return e.doc.DisplayName -} - -// CreatedBy returns the user who created the model user. -func (e *ModelUser) CreatedBy() string { - return e.doc.CreatedBy -} - -// DateCreated returns the date the model user was created in UTC. -func (e *ModelUser) DateCreated() time.Time { - return e.doc.DateCreated.UTC() -} - -// refreshPermission reloads the permission for this model user from persistence. -func (e *ModelUser) refreshPermission() error { - perm, err := e.st.userPermission(modelGlobalKey, e.globalKey()) - if err != nil { - return errors.Annotate(err, "updating permission") - } - e.modelPermission = perm - return nil -} - -// IsReadOnly returns whether or not the user has write access or only -// read access to the model. -func (e *ModelUser) IsReadOnly() bool { - return e.modelPermission.isReadOnly() -} - -// IsAdmin is a convenience method that -// returns whether or not the user has AdminAccess. -func (e *ModelUser) IsAdmin() bool { - return e.modelPermission.isAdmin() -} - -// IsReadWrite is a convenience method that -// returns whether or not the user has WriteAccess. -func (e *ModelUser) IsReadWrite() bool { - return e.modelPermission.isReadWrite() -} - -// IsGreaterAccess returns true if provided access is higher than -// the current one. -func (e *ModelUser) IsGreaterAccess(a Access) bool { - return e.modelPermission.isGreaterAccess(a) -} - -// SetAccess changes the user's access permissions on the model. -func (e *ModelUser) SetAccess(access Access) error { - switch access { - case ReadAccess, AdminAccess, WriteAccess: - default: - return errors.Errorf("invalid model access %q", access) - } - op := updatePermissionOp(modelGlobalKey, e.globalKey(), access) - err := e.st.runTransaction([]txn.Op{op}) - if err != nil { - return errors.Trace(err) +// setModelAccess changes the user's access permissions on the model. +func (st *State) setModelAccess(access description.Access, userGlobalKey string) error { + op := updatePermissionOp(modelGlobalKey, userGlobalKey, access) + err := st.runTransaction([]txn.Op{op}) + if err == txn.ErrAborted { + return errors.NotFoundf("existing permissions") } - return e.refreshPermission() + return errors.Trace(err) } -// LastConnection returns when this ModelUser last connected through the API +// LastModelConnection returns when this User last connected through the API // in UTC. The resulting time will be nil if the user has never logged in. -func (e *ModelUser) LastConnection() (time.Time, error) { - lastConnections, closer := e.st.getRawCollection(modelUserLastConnectionC) +func (st *State) LastModelConnection(user names.UserTag) (time.Time, error) { + lastConnections, closer := st.getRawCollection(modelUserLastConnectionC) defer closer() - username := strings.ToLower(e.UserName()) + username := user.Canonical() var lastConn modelUserLastConnectionDoc - err := lastConnections.FindId(e.st.docID(username)).Select(bson.D{{"last-connection", 1}}).One(&lastConn) + err := lastConnections.FindId(st.docID(username)).Select(bson.D{{"last-connection", 1}}).One(&lastConn) if err != nil { if err == mgo.ErrNotFound { - err = errors.Wrap(err, NeverConnectedError(e.UserName())) + err = errors.Wrap(err, NeverConnectedError(username)) } return time.Time{}, errors.Trace(err) } @@ -178,13 +74,13 @@ return ok } -// UpdateLastConnection updates the last connection time of the model user. -func (e *ModelUser) UpdateLastConnection() error { - return e.updateLastConnection(nowToTheSecond()) +// UpdateLastModelConnection updates the last connection time of the model user. +func (st *State) UpdateLastModelConnection(user names.UserTag) error { + return st.updateLastModelConnection(user, nowToTheSecond()) } -func (e *ModelUser) updateLastConnection(when time.Time) error { - lastConnections, closer := e.st.getCollection(modelUserLastConnectionC) +func (st *State) updateLastModelConnection(user names.UserTag, when time.Time) error { + lastConnections, closer := st.getCollection(modelUserLastConnectionC) defer closer() lastConnectionsW := lastConnections.Writeable() @@ -195,129 +91,62 @@ session.SetSafe(&mgo.Safe{}) lastConn := modelUserLastConnectionDoc{ - ID: e.st.docID(strings.ToLower(e.UserName())), - ModelUUID: e.ModelTag().Id(), - UserName: e.UserName(), + ID: st.docID(strings.ToLower(user.Canonical())), + ModelUUID: st.ModelUUID(), + UserName: user.Canonical(), LastConnection: when, } _, err := lastConnectionsW.UpsertId(lastConn.ID, lastConn) return errors.Trace(err) } -func modelUserGlobalKey(userID string) string { - // mu stands for model user. - return fmt.Sprintf("mu#%s", userID) -} - -func (e *ModelUser) globalKey() string { - // TODO(perrito666) this asumes out of band knowledge of how modelUserID is crafted - username := strings.ToLower(e.UserName()) - return modelUserGlobalKey(username) -} - -// ModelUser returns the model user. -func (st *State) ModelUser(user names.UserTag) (*ModelUser, error) { - modelUser := &ModelUser{st: st} +// ModelUser a model userAccessDoc. +func (st *State) modelUser(user names.UserTag) (userAccessDoc, error) { + modelUser := userAccessDoc{} modelUsers, closer := st.getCollection(modelUsersC) defer closer() username := strings.ToLower(user.Canonical()) - err := modelUsers.FindId(username).One(&modelUser.doc) + err := modelUsers.FindId(username).One(&modelUser) if err == mgo.ErrNotFound { - return nil, errors.NotFoundf("model user %q", user.Canonical()) + return userAccessDoc{}, errors.NotFoundf("model user %q", username) } // DateCreated is inserted as UTC, but read out as local time. So we // convert it back to UTC here. - modelUser.doc.DateCreated = modelUser.doc.DateCreated.UTC() - if err := modelUser.refreshPermission(); err != nil { - return nil, errors.Trace(err) - } + modelUser.DateCreated = modelUser.DateCreated.UTC() return modelUser, nil } -// ModelUserSpec defines the attributes that can be set when adding a new -// model user. -type ModelUserSpec struct { - User names.UserTag - CreatedBy names.UserTag - DisplayName string - Access Access -} - -// AddModelUser adds a new user to the database. -func (st *State) AddModelUser(spec ModelUserSpec) (*ModelUser, error) { - // Ensure local user exists in state before adding them as an model user. - if spec.User.IsLocal() { - localUser, err := st.User(spec.User) - if err != nil { - return nil, errors.Annotate(err, fmt.Sprintf("user %q does not exist locally", spec.User.Name())) - } - if spec.DisplayName == "" { - spec.DisplayName = localUser.DisplayName() - } - } - - // Ensure local createdBy user exists. - if spec.CreatedBy.IsLocal() { - if _, err := st.User(spec.CreatedBy); err != nil { - return nil, errors.Annotatef(err, "createdBy user %q does not exist locally", spec.CreatedBy.Name()) - } - } - - // Default to read access if not otherwise specified. - if spec.Access == UndefinedAccess { - spec.Access = ReadAccess - } - - modelUUID := st.ModelUUID() - buildTxn := func(attempt int) ([]txn.Op, error) { - return createModelUserOps(modelUUID, spec.User, spec.CreatedBy, spec.DisplayName, nowToTheSecond(), spec.Access), nil - } - err := st.run(buildTxn) - if err == jujutxn.ErrExcessiveContention { - err = errors.AlreadyExistsf("model user %q", spec.User.Canonical()) - } - if err != nil { - return nil, errors.Trace(err) - } - return st.ModelUser(spec.User) -} - -// modelUserID returns the document id of the model user -func modelUserID(user names.UserTag) string { - username := user.Canonical() - return strings.ToLower(username) -} - -func createModelUserOps(modelUUID string, user, createdBy names.UserTag, displayName string, dateCreated time.Time, access Access) []txn.Op { +func createModelUserOps(modelUUID string, user, createdBy names.UserTag, displayName string, dateCreated time.Time, access description.Access) []txn.Op { creatorname := createdBy.Canonical() - doc := &modelUserDoc{ - ID: modelUserID(user), - ModelUUID: modelUUID, + doc := &userAccessDoc{ + ID: userAccessID(user), + ObjectUUID: modelUUID, UserName: user.Canonical(), DisplayName: displayName, CreatedBy: creatorname, DateCreated: dateCreated, } ops := []txn.Op{ - createPermissionOp(modelGlobalKey, modelUserGlobalKey(modelUserID(user)), access), + createPermissionOp(modelGlobalKey, userGlobalKey(userAccessID(user)), access), { C: modelUsersC, - Id: modelUserID(user), + Id: userAccessID(user), Assert: txn.DocMissing, Insert: doc, }, } + return ops } -// RemoveModelUser removes a user from the database. -func (st *State) RemoveModelUser(user names.UserTag) error { +// removeModelUser removes a user from the database. +func (st *State) removeModelUser(user names.UserTag) error { ops := []txn.Op{ - removePermissionOp(modelGlobalKey, modelUserGlobalKey(modelUserID(user))), + removePermissionOp(modelGlobalKey, userGlobalKey(userAccessID(user))), { C: modelUsersC, - Id: modelUserID(user), + Id: userAccessID(user), Assert: txn.DocExists, Remove: true, }} @@ -365,15 +194,15 @@ modelUsers, userCloser := st.getRawCollection(modelUsersC) defer userCloser() - var userSlice []modelUserDoc - err := modelUsers.Find(bson.D{{"user", user.Canonical()}}).Select(bson.D{{"model-uuid", 1}, {"_id", 1}}).All(&userSlice) + var userSlice []userAccessDoc + err := modelUsers.Find(bson.D{{"user", user.Canonical()}}).Select(bson.D{{"object-uuid", 1}, {"_id", 1}}).All(&userSlice) if err != nil { return nil, err } var result []*UserModel for _, doc := range userSlice { - modelTag := names.NewModelTag(doc.ModelUUID) + modelTag := names.NewModelTag(doc.ObjectUUID) env, err := st.GetModel(modelTag) if err != nil { return nil, errors.Trace(err) @@ -399,7 +228,7 @@ defer closer() username := strings.ToLower(user.Canonical()) - subjectGlobalKey := modelUserGlobalKey(username) + subjectGlobalKey := userGlobalKey(username) // TODO(perrito666) 20160606 this is prone to errors, it will just // yield ErrPerm and be hard to trace, use ModelUser and Permission. @@ -410,7 +239,7 @@ {"_id", fmt.Sprintf("%s:%s", serverUUID, permissionID(modelGlobalKey, subjectGlobalKey))}, {"object-global-key", modelGlobalKey}, {"subject-global-key", subjectGlobalKey}, - {"access", AdminAccess}, + {"access", description.AdminAccess}, }).Count() if err != nil { return false, errors.Trace(err) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/modeluser_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/modeluser_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/modeluser_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/modeluser_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -13,7 +13,9 @@ gc "gopkg.in/check.v1" "gopkg.in/juju/names.v2" + "github.com/juju/juju/core/description" "github.com/juju/juju/state" + "github.com/juju/juju/storage" "github.com/juju/juju/testing" "github.com/juju/juju/testing/factory" ) @@ -24,151 +26,176 @@ var _ = gc.Suite(&ModelUserSuite{}) -type userWithPermissions interface { - IsReadOnly() bool - IsReadWrite() bool - IsAdmin() bool -} - -func checkModelUserHasRightAccess(c *gc.C, expected state.Access, user userWithPermissions) { - switch expected { - case state.ReadAccess: - c.Assert(user.IsReadOnly(), jc.IsTrue) - c.Assert(user.IsReadWrite(), jc.IsFalse) - c.Assert(user.IsAdmin(), jc.IsFalse) - case state.WriteAccess: - c.Assert(user.IsReadOnly(), jc.IsFalse) - c.Assert(user.IsReadWrite(), jc.IsTrue) - c.Assert(user.IsAdmin(), jc.IsFalse) - case state.AdminAccess: - c.Assert(user.IsReadOnly(), jc.IsFalse) - c.Assert(user.IsReadWrite(), jc.IsFalse) - c.Assert(user.IsAdmin(), jc.IsTrue) - default: - c.FailNow() - } -} - func (s *ModelUserSuite) TestAddModelUser(c *gc.C) { now := state.NowToTheSecond() - user := s.Factory.MakeUser(c, &factory.UserParams{Name: "validusername", NoModelUser: true}) + user := s.Factory.MakeUser(c, + &factory.UserParams{ + Name: "validusername", + NoModelUser: true, + }) createdBy := s.Factory.MakeUser(c, &factory.UserParams{Name: "createdby"}) - modelUser, err := s.State.AddModelUser(state.ModelUserSpec{ - User: user.UserTag(), CreatedBy: createdBy.UserTag(), Access: state.WriteAccess}) - c.Assert(err, jc.ErrorIsNil) - - c.Assert(modelUser.ID(), gc.Equals, fmt.Sprintf("%s:validusername@local", s.modelTag.Id())) - c.Assert(modelUser.ModelTag(), gc.Equals, s.modelTag) - c.Assert(modelUser.UserName(), gc.Equals, "validusername@local") - c.Assert(modelUser.DisplayName(), gc.Equals, user.DisplayName()) - checkModelUserHasRightAccess(c, state.WriteAccess, modelUser) - c.Assert(modelUser.CreatedBy(), gc.Equals, "createdby@local") - c.Assert(modelUser.DateCreated().Equal(now) || modelUser.DateCreated().After(now), jc.IsTrue) - when, err := modelUser.LastConnection() + modelUser, err := s.State.AddModelUser( + state.UserAccessSpec{ + User: user.UserTag(), + CreatedBy: createdBy.UserTag(), + Access: description.WriteAccess, + }) + c.Assert(err, jc.ErrorIsNil) + + c.Assert(modelUser.UserID, gc.Equals, fmt.Sprintf("%s:validusername@local", s.modelTag.Id())) + c.Assert(modelUser.Object, gc.Equals, s.modelTag) + c.Assert(modelUser.UserName, gc.Equals, "validusername@local") + c.Assert(modelUser.DisplayName, gc.Equals, user.DisplayName()) + c.Assert(modelUser.Access, gc.Equals, description.WriteAccess) + c.Assert(modelUser.CreatedBy.Id(), gc.Equals, "createdby@local") + c.Assert(modelUser.DateCreated.Equal(now) || modelUser.DateCreated.After(now), jc.IsTrue) + when, err := s.State.LastModelConnection(modelUser.UserTag) c.Assert(err, jc.Satisfies, state.IsNeverConnectedError) c.Assert(when.IsZero(), jc.IsTrue) - modelUser, err = s.State.ModelUser(user.UserTag()) + modelUser, err = s.State.UserAccess(user.UserTag(), s.State.ModelTag()) c.Assert(err, jc.ErrorIsNil) - c.Assert(modelUser.ID(), gc.Equals, fmt.Sprintf("%s:validusername@local", s.modelTag.Id())) - c.Assert(modelUser.ModelTag(), gc.Equals, s.modelTag) - c.Assert(modelUser.UserName(), gc.Equals, "validusername@local") - c.Assert(modelUser.DisplayName(), gc.Equals, user.DisplayName()) - checkModelUserHasRightAccess(c, state.WriteAccess, modelUser) - c.Assert(modelUser.CreatedBy(), gc.Equals, "createdby@local") - c.Assert(modelUser.DateCreated().Equal(now) || modelUser.DateCreated().After(now), jc.IsTrue) - when, err = modelUser.LastConnection() + c.Assert(modelUser.UserID, gc.Equals, fmt.Sprintf("%s:validusername@local", s.modelTag.Id())) + c.Assert(modelUser.Object, gc.Equals, s.modelTag) + c.Assert(modelUser.UserName, gc.Equals, "validusername@local") + c.Assert(modelUser.DisplayName, gc.Equals, user.DisplayName()) + c.Assert(modelUser.Access, gc.Equals, description.WriteAccess) + c.Assert(modelUser.CreatedBy.Id(), gc.Equals, "createdby@local") + c.Assert(modelUser.DateCreated.Equal(now) || modelUser.DateCreated.After(now), jc.IsTrue) + when, err = s.State.LastModelConnection(modelUser.UserTag) c.Assert(err, jc.Satisfies, state.IsNeverConnectedError) c.Assert(when.IsZero(), jc.IsTrue) } func (s *ModelUserSuite) TestAddReadOnlyModelUser(c *gc.C) { - user := s.Factory.MakeUser(c, &factory.UserParams{Name: "validusername", NoModelUser: true}) + user := s.Factory.MakeUser(c, + &factory.UserParams{ + Name: "validusername", + NoModelUser: true, + }) createdBy := s.Factory.MakeUser(c, &factory.UserParams{Name: "createdby"}) - modelUser, err := s.State.AddModelUser(state.ModelUserSpec{ - User: user.UserTag(), CreatedBy: createdBy.UserTag(), Access: state.ReadAccess}) + modelUser, err := s.State.AddModelUser( + state.UserAccessSpec{ + User: user.UserTag(), + CreatedBy: createdBy.UserTag(), + Access: description.ReadAccess, + }) c.Assert(err, jc.ErrorIsNil) - c.Assert(modelUser.UserName(), gc.Equals, "validusername@local") - c.Assert(modelUser.DisplayName(), gc.Equals, user.DisplayName()) - checkModelUserHasRightAccess(c, state.ReadAccess, modelUser) + c.Assert(modelUser.UserName, gc.Equals, "validusername@local") + c.Assert(modelUser.DisplayName, gc.Equals, user.DisplayName()) + c.Assert(modelUser.Access, gc.Equals, description.ReadAccess) // Make sure that it is set when we read the user out. - modelUser, err = s.State.ModelUser(user.UserTag()) + modelUser, err = s.State.UserAccess(user.UserTag(), s.State.ModelTag()) c.Assert(err, jc.ErrorIsNil) - c.Assert(modelUser.UserName(), gc.Equals, "validusername@local") - checkModelUserHasRightAccess(c, state.ReadAccess, modelUser) + c.Assert(modelUser.UserName, gc.Equals, "validusername@local") + c.Assert(modelUser.Access, gc.Equals, description.ReadAccess) } func (s *ModelUserSuite) TestAddReadWriteModelUser(c *gc.C) { - user := s.Factory.MakeUser(c, &factory.UserParams{Name: "validusername", NoModelUser: true}) + user := s.Factory.MakeUser(c, + &factory.UserParams{ + Name: "validusername", + NoModelUser: true, + }) createdBy := s.Factory.MakeUser(c, &factory.UserParams{Name: "createdby"}) - modelUser, err := s.State.AddModelUser(state.ModelUserSpec{ - User: user.UserTag(), CreatedBy: createdBy.UserTag(), Access: state.WriteAccess}) + modelUser, err := s.State.AddModelUser( + state.UserAccessSpec{ + User: user.UserTag(), + CreatedBy: createdBy.UserTag(), + Access: description.WriteAccess, + }) c.Assert(err, jc.ErrorIsNil) - c.Assert(modelUser.UserName(), gc.Equals, "validusername@local") - c.Assert(modelUser.DisplayName(), gc.Equals, user.DisplayName()) - checkModelUserHasRightAccess(c, state.WriteAccess, modelUser) + c.Assert(modelUser.UserName, gc.Equals, "validusername@local") + c.Assert(modelUser.DisplayName, gc.Equals, user.DisplayName()) + c.Assert(modelUser.Access, gc.Equals, description.WriteAccess) // Make sure that it is set when we read the user out. - modelUser, err = s.State.ModelUser(user.UserTag()) + modelUser, err = s.State.UserAccess(user.UserTag(), s.State.ModelTag()) c.Assert(err, jc.ErrorIsNil) - c.Assert(modelUser.UserName(), gc.Equals, "validusername@local") - checkModelUserHasRightAccess(c, state.WriteAccess, modelUser) + c.Assert(modelUser.UserName, gc.Equals, "validusername@local") + c.Assert(modelUser.Access, gc.Equals, description.WriteAccess) } func (s *ModelUserSuite) TestAddAdminModelUser(c *gc.C) { - user := s.Factory.MakeUser(c, &factory.UserParams{Name: "validusername", NoModelUser: true}) + user := s.Factory.MakeUser(c, + &factory.UserParams{ + Name: "validusername", + NoModelUser: true, + }) createdBy := s.Factory.MakeUser(c, &factory.UserParams{Name: "createdby"}) - modelUser, err := s.State.AddModelUser(state.ModelUserSpec{ - User: user.UserTag(), CreatedBy: createdBy.UserTag(), Access: state.AdminAccess}) + modelUser, err := s.State.AddModelUser( + state.UserAccessSpec{ + User: user.UserTag(), + CreatedBy: createdBy.UserTag(), + Access: description.AdminAccess, + }) c.Assert(err, jc.ErrorIsNil) - c.Assert(modelUser.UserName(), gc.Equals, "validusername@local") - c.Assert(modelUser.DisplayName(), gc.Equals, user.DisplayName()) - checkModelUserHasRightAccess(c, state.AdminAccess, modelUser) + c.Assert(modelUser.UserName, gc.Equals, "validusername@local") + c.Assert(modelUser.DisplayName, gc.Equals, user.DisplayName()) + c.Assert(modelUser.Access, gc.Equals, description.AdminAccess) // Make sure that it is set when we read the user out. - modelUser, err = s.State.ModelUser(user.UserTag()) + modelUser, err = s.State.UserAccess(user.UserTag(), s.State.ModelTag()) c.Assert(err, jc.ErrorIsNil) - c.Assert(modelUser.UserName(), gc.Equals, "validusername@local") - checkModelUserHasRightAccess(c, state.AdminAccess, modelUser) + c.Assert(modelUser.UserName, gc.Equals, "validusername@local") + c.Assert(modelUser.Access, gc.Equals, description.AdminAccess) } func (s *ModelUserSuite) TestDefaultAccessModelUser(c *gc.C) { - user := s.Factory.MakeUser(c, &factory.UserParams{Name: "validusername", NoModelUser: true}) + user := s.Factory.MakeUser(c, + &factory.UserParams{ + Name: "validusername", + NoModelUser: true, + }) createdBy := s.Factory.MakeUser(c, &factory.UserParams{Name: "createdby"}) - modelUser, err := s.State.AddModelUser(state.ModelUserSpec{ - User: user.UserTag(), CreatedBy: createdBy.UserTag()}) + modelUser, err := s.State.AddModelUser( + state.UserAccessSpec{ + User: user.UserTag(), + CreatedBy: createdBy.UserTag(), + Access: description.ReadAccess, + }) c.Assert(err, jc.ErrorIsNil) - checkModelUserHasRightAccess(c, state.ReadAccess, modelUser) + c.Assert(modelUser.Access, gc.Equals, description.ReadAccess) } func (s *ModelUserSuite) TestSetAccessModelUser(c *gc.C) { - user := s.Factory.MakeUser(c, &factory.UserParams{Name: "validusername", NoModelUser: true}) + user := s.Factory.MakeUser(c, + &factory.UserParams{ + Name: "validusername", + NoModelUser: true, + }) createdBy := s.Factory.MakeUser(c, &factory.UserParams{Name: "createdby"}) - modelUser, err := s.State.AddModelUser(state.ModelUserSpec{ - User: user.UserTag(), CreatedBy: createdBy.UserTag(), Access: state.AdminAccess}) + modelUser, err := s.State.AddModelUser( + state.UserAccessSpec{ + User: user.UserTag(), + CreatedBy: createdBy.UserTag(), + Access: description.AdminAccess, + }) c.Assert(err, jc.ErrorIsNil) - checkModelUserHasRightAccess(c, state.AdminAccess, modelUser) + c.Assert(modelUser.Access, gc.Equals, description.AdminAccess) - modelUser.SetAccess(state.ReadAccess) + s.State.SetUserAccess(modelUser.UserTag, s.State.ModelTag(), description.ReadAccess) - modelUser, err = s.State.ModelUser(user.UserTag()) - checkModelUserHasRightAccess(c, state.ReadAccess, modelUser) + modelUser, err = s.State.UserAccess(user.UserTag(), s.State.ModelTag()) + c.Assert(modelUser.Access, gc.Equals, description.ReadAccess) } func (s *ModelUserSuite) TestCaseUserNameVsId(c *gc.C) { model, err := s.State.Model() c.Assert(err, jc.ErrorIsNil) - user, err := s.State.AddModelUser(state.ModelUserSpec{ + user, err := s.State.AddModelUser(state.UserAccessSpec{ User: names.NewUserTag("Bob@RandomProvider"), - CreatedBy: model.Owner()}) + CreatedBy: model.Owner(), + Access: description.ReadAccess, + }) c.Assert(err, gc.IsNil) - c.Assert(user.UserName(), gc.Equals, "Bob@RandomProvider") - c.Assert(user.ID(), gc.Equals, state.DocID(s.State, "bob@randomprovider")) + c.Assert(user.UserName, gc.Equals, "Bob@RandomProvider") + c.Assert(user.UserID, gc.Equals, state.DocID(s.State, "bob@randomprovider")) } func (s *ModelUserSuite) TestCaseSensitiveModelUserErrors(c *gc.C) { @@ -176,10 +203,12 @@ c.Assert(err, jc.ErrorIsNil) s.Factory.MakeModelUser(c, &factory.ModelUserParams{User: "Bob@ubuntuone"}) - _, err = s.State.AddModelUser(state.ModelUserSpec{ + _, err = s.State.AddModelUser(state.UserAccessSpec{ User: names.NewUserTag("boB@ubuntuone"), - CreatedBy: model.Owner()}) - c.Assert(err, gc.ErrorMatches, `model user "boB@ubuntuone" already exists`) + CreatedBy: model.Owner(), + Access: description.ReadAccess, + }) + c.Assert(err, gc.ErrorMatches, `user access "boB@ubuntuone" already exists`) c.Assert(errors.IsAlreadyExists(err), jc.IsTrue) } @@ -191,11 +220,11 @@ // assert case insensitive lookup for each username for _, username := range usernames { userTag := names.NewUserTag(username) - obtainedUser, err := st1.ModelUser(userTag) + obtainedUser, err := st1.UserAccess(userTag, st1.ModelTag()) c.Assert(err, jc.ErrorIsNil) c.Assert(obtainedUser, gc.DeepEquals, expectedUser) - _, err = st2.ModelUser(userTag) + _, err = st2.UserAccess(userTag, st2.ModelTag()) c.Assert(errors.IsNotFound(err), jc.IsTrue) } } @@ -216,43 +245,45 @@ func (s *ModelUserSuite) TestAddModelDisplayName(c *gc.C) { modelUserDefault := s.Factory.MakeModelUser(c, nil) - c.Assert(modelUserDefault.DisplayName(), gc.Matches, "display name-[0-9]*") + c.Assert(modelUserDefault.DisplayName, gc.Matches, "display name-[0-9]*") modelUser := s.Factory.MakeModelUser(c, &factory.ModelUserParams{DisplayName: "Override user display name"}) - c.Assert(modelUser.DisplayName(), gc.Equals, "Override user display name") + c.Assert(modelUser.DisplayName, gc.Equals, "Override user display name") } func (s *ModelUserSuite) TestAddModelNoUserFails(c *gc.C) { createdBy := s.Factory.MakeUser(c, &factory.UserParams{Name: "createdby"}) - _, err := s.State.AddModelUser(state.ModelUserSpec{ + _, err := s.State.AddModelUser(state.UserAccessSpec{ User: names.NewLocalUserTag("validusername"), - CreatedBy: createdBy.UserTag()}) + CreatedBy: createdBy.UserTag(), + Access: description.ReadAccess}) c.Assert(err, gc.ErrorMatches, `user "validusername" does not exist locally: user "validusername" not found`) } func (s *ModelUserSuite) TestAddModelNoCreatedByUserFails(c *gc.C) { user := s.Factory.MakeUser(c, &factory.UserParams{Name: "validusername"}) - _, err := s.State.AddModelUser(state.ModelUserSpec{ + _, err := s.State.AddModelUser(state.UserAccessSpec{ User: user.UserTag(), - CreatedBy: names.NewLocalUserTag("createdby")}) + CreatedBy: names.NewLocalUserTag("createdby"), + Access: description.ReadAccess}) c.Assert(err, gc.ErrorMatches, `createdBy user "createdby" does not exist locally: user "createdby" not found`) } func (s *ModelUserSuite) TestRemoveModelUser(c *gc.C) { user := s.Factory.MakeUser(c, &factory.UserParams{Name: "validUsername"}) - _, err := s.State.ModelUser(user.UserTag()) + _, err := s.State.UserAccess(user.UserTag(), s.State.ModelTag()) c.Assert(err, jc.ErrorIsNil) - err = s.State.RemoveModelUser(user.UserTag()) + err = s.State.RemoveUserAccess(user.UserTag(), s.State.ModelTag()) c.Assert(err, jc.ErrorIsNil) - _, err = s.State.ModelUser(user.UserTag()) + _, err = s.State.UserAccess(user.UserTag(), s.State.ModelTag()) c.Assert(err, jc.Satisfies, errors.IsNotFound) } func (s *ModelUserSuite) TestRemoveModelUserFails(c *gc.C) { user := s.Factory.MakeUser(c, &factory.UserParams{NoModelUser: true}) - err := s.State.RemoveModelUser(user.UserTag()) + err := s.State.RemoveUserAccess(user.UserTag(), s.State.ModelTag()) c.Assert(err, jc.Satisfies, errors.IsNotFound) } @@ -260,11 +291,11 @@ now := state.NowToTheSecond() createdBy := s.Factory.MakeUser(c, &factory.UserParams{Name: "createdby"}) user := s.Factory.MakeUser(c, &factory.UserParams{Name: "validusername", Creator: createdBy.Tag()}) - modelUser, err := s.State.ModelUser(user.UserTag()) + modelUser, err := s.State.UserAccess(user.UserTag(), s.State.ModelTag()) c.Assert(err, jc.ErrorIsNil) - err = modelUser.UpdateLastConnection() + err = s.State.UpdateLastModelConnection(user.UserTag()) c.Assert(err, jc.ErrorIsNil) - when, err := modelUser.LastConnection() + when, err := s.State.LastModelConnection(modelUser.UserTag) c.Assert(err, jc.ErrorIsNil) // It is possible that the update is done over a second boundary, so we need // to check for after now as well as equal. @@ -277,36 +308,37 @@ // Create a user and add them to the inital model. createdBy := s.Factory.MakeUser(c, &factory.UserParams{Name: "createdby"}) user := s.Factory.MakeUser(c, &factory.UserParams{Name: "validusername", Creator: createdBy.Tag()}) - modelUser, err := s.State.ModelUser(user.UserTag()) + modelUser, err := s.State.UserAccess(user.UserTag(), s.State.ModelTag()) c.Assert(err, jc.ErrorIsNil) // Create a second model and add the same user to this. st2 := s.Factory.MakeModel(c, nil) defer st2.Close() - modelUser2, err := st2.AddModelUser(state.ModelUserSpec{ + modelUser2, err := st2.AddModelUser(state.UserAccessSpec{ User: user.UserTag(), - CreatedBy: createdBy.UserTag()}) + CreatedBy: createdBy.UserTag(), + Access: description.ReadAccess}) c.Assert(err, jc.ErrorIsNil) // Now we have two model users with the same username. Ensure we get // separate last connections. // Connect modelUser and get last connection. - err = modelUser.UpdateLastConnection() + err = s.State.UpdateLastModelConnection(user.UserTag()) c.Assert(err, jc.ErrorIsNil) - when, err := modelUser.LastConnection() + when, err := s.State.LastModelConnection(modelUser.UserTag) c.Assert(err, jc.ErrorIsNil) c.Assert(when.After(now) || when.Equal(now), jc.IsTrue) // Try to get last connection for modelUser2. As they have never connected, // we expect to get an error. - _, err = modelUser2.LastConnection() + _, err = st2.LastModelConnection(modelUser2.UserTag) c.Assert(err, gc.ErrorMatches, `never connected: "validusername@local"`) // Connect modelUser2 and get last connection. - err = modelUser2.UpdateLastConnection() + err = s.State.UpdateLastModelConnection(modelUser2.UserTag) c.Assert(err, jc.ErrorIsNil) - when, err = modelUser2.LastConnection() + when, err = s.State.LastModelConnection(modelUser2.UserTag) c.Assert(err, jc.ErrorIsNil) c.Assert(when.After(now) || when.Equal(now), jc.IsTrue) } @@ -331,9 +363,13 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(models, gc.HasLen, 1) c.Assert(models[0].UUID(), gc.Equals, s.State.ModelUUID()) - when, err := models[0].LastConnection() + st, err := s.State.ForModel(models[0].ModelTag()) + c.Assert(err, jc.ErrorIsNil) + modelUser, err := s.State.UserAccess(user.UserTag(), models[0].ModelTag()) + when, err := st.LastModelConnection(modelUser.UserTag) c.Assert(err, jc.Satisfies, state.IsNeverConnectedError) c.Assert(when.IsZero(), jc.IsTrue) + c.Assert(st.Close(), jc.ErrorIsNil) } func (s *ModelUserSuite) newEnvWithOwner(c *gc.C, name string, owner names.UserTag) *state.Model { @@ -347,7 +383,10 @@ "name": name, "uuid": uuid.String(), }) - model, st, err := s.State.NewModel(state.ModelArgs{CloudName: "dummy", Config: cfg, Owner: owner}) + model, st, err := s.State.NewModel(state.ModelArgs{ + CloudName: "dummy", Config: cfg, Owner: owner, + StorageProviderRegistry: storage.StaticProviderRegistry{}, + }) c.Assert(err, jc.ErrorIsNil) defer st.Close() return model @@ -374,8 +413,9 @@ newEnv, err := envState.Model() c.Assert(err, jc.ErrorIsNil) - _, err = envState.AddModelUser(state.ModelUserSpec{ - User: user, CreatedBy: newEnv.Owner()}) + _, err = envState.AddModelUser(state.UserAccessSpec{ + User: user, CreatedBy: newEnv.Owner(), + Access: description.ReadAccess}) c.Assert(err, jc.ErrorIsNil) return newEnv } @@ -425,8 +465,8 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(isAdmin, jc.IsTrue) - readonly := s.Factory.MakeModelUser(c, &factory.ModelUserParams{Access: state.ReadAccess}) - isAdmin, err = s.State.IsControllerAdministrator(readonly.UserTag()) + readonly := s.Factory.MakeModelUser(c, &factory.ModelUserParams{Access: description.ReadAccess}) + isAdmin, err = s.State.IsControllerAdministrator(readonly.UserTag) c.Assert(err, jc.ErrorIsNil) c.Assert(isAdmin, jc.IsFalse) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/open.go juju-core-2.0~beta15/src/github.com/juju/juju/state/open.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/open.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/open.go 2016-08-16 08:56:25.000000000 +0000 @@ -16,9 +16,12 @@ "github.com/juju/juju/cloud" "github.com/juju/juju/controller" + "github.com/juju/juju/core/description" "github.com/juju/juju/environs/config" "github.com/juju/juju/mongo" "github.com/juju/juju/status" + "github.com/juju/juju/storage" + "github.com/juju/juju/storage/poolmanager" "github.com/juju/juju/worker" ) @@ -31,8 +34,8 @@ // may be provided. // // Open returns unauthorizedError if access is unauthorized. -func Open(tag names.ModelTag, info *mongo.MongoInfo, opts mongo.DialOpts, policy Policy) (*State, error) { - st, err := open(tag, info, opts, policy) +func Open(tag names.ModelTag, info *mongo.MongoInfo, opts mongo.DialOpts, newPolicy NewPolicyFunc) (*State, error) { + st, err := open(tag, info, opts, newPolicy) if err != nil { return nil, errors.Trace(err) } @@ -51,7 +54,7 @@ return st, nil } -func open(tag names.ModelTag, info *mongo.MongoInfo, opts mongo.DialOpts, policy Policy) (*State, error) { +func open(tag names.ModelTag, info *mongo.MongoInfo, opts mongo.DialOpts, newPolicy NewPolicyFunc) (*State, error) { logger.Infof("opening state, mongo addresses: %q; entity %v", info.Addrs, info.Tag) logger.Debugf("dialing mongo") session, err := mongo.DialWithInfo(info.Info, opts) @@ -81,7 +84,7 @@ tag = ssInfo.ModelTag } - st, err := newState(tag, session, info, policy) + st, err := newState(tag, session, info, newPolicy) if err != nil { return nil, errors.Trace(err) } @@ -129,8 +132,9 @@ // models on the specified cloud. ControllerInheritedConfig map[string]interface{} - // Policy is the set of state policies to apply. - Policy Policy + // NewPolicy is a function that returns the set of state policies + // to apply. + NewPolicy NewPolicyFunc // MongoInfo contains the information required to address and // authenticate with Mongo. @@ -195,7 +199,7 @@ // When creating the controller model, the new model // UUID is also used as the controller UUID. modelTag := names.NewModelTag(args.ControllerModelArgs.Config.UUID()) - st, err := open(modelTag, args.MongoInfo, args.MongoDialOpts, args.Policy) + st, err := open(modelTag, args.MongoInfo, args.MongoDialOpts, args.NewPolicy) if err != nil { return nil, errors.Trace(err) } @@ -228,9 +232,10 @@ return nil, err } - ops := []txn.Op{ - createInitialUserOp(st, args.ControllerModelArgs.Owner, args.MongoInfo.Password, salt), - { + ops := createInitialUserOps(st.ControllerUUID(), args.ControllerModelArgs.Owner, args.MongoInfo.Password, salt) + ops = append(ops, + + txn.Op{ C: controllersC, Id: modelGlobalKey, Assert: txn.DocMissing, @@ -240,19 +245,19 @@ }, }, createCloudOp(args.Cloud, args.CloudName), - { + txn.Op{ C: controllersC, Id: apiHostPortsKey, Assert: txn.DocMissing, Insert: &apiHostPortsDoc{}, }, - { + txn.Op{ C: controllersC, Id: stateServingInfoKey, Assert: txn.DocMissing, Insert: &StateServingInfo{}, }, - { + txn.Op{ C: controllersC, Id: hostedModelCountKey, Assert: txn.DocMissing, @@ -260,14 +265,14 @@ }, createSettingsOp(controllersC, controllerSettingsGlobalKey, args.ControllerConfig), createSettingsOp(globalSettingsC, controllerInheritedSettingsGlobalKey, args.ControllerInheritedConfig), - } - if len(args.CloudCredentials) > 0 { - credentialsOps := updateCloudCredentialsOps( + ) + for credName, cred := range args.CloudCredentials { + ops = append(ops, createCloudCredentialOp( args.ControllerModelArgs.Owner, args.CloudName, - args.CloudCredentials, - ) - ops = append(ops, credentialsOps...) + credName, + cred, + )) } ops = append(ops, modelOps...) @@ -281,8 +286,8 @@ } // modelSetupOps returns the transactions necessary to set up a model. -func (st *State) modelSetupOps(args ModelArgs, ControllerInheritedConfig map[string]interface{}) ([]txn.Op, error) { - if err := checkControllerInheritedConfig(ControllerInheritedConfig); err != nil { +func (st *State) modelSetupOps(args ModelArgs, controllerInheritedConfig map[string]interface{}) ([]txn.Op, error) { + if err := checkControllerInheritedConfig(controllerInheritedConfig); err != nil { return nil, errors.Trace(err) } if err := checkModelConfig(args.Config); err != nil { @@ -306,7 +311,7 @@ isHostedModel := controllerUUID != modelUUID modelUserOps := createModelUserOps( - modelUUID, args.Owner, args.Owner, args.Owner.Name(), nowToTheSecond(), AdminAccess, + modelUUID, args.Owner, args.Owner, args.Owner.Name(), nowToTheSecond(), description.AdminAccess, ) ops := []txn.Op{ createStatusOp(st, modelGlobalKey, modelStatusDoc), @@ -316,27 +321,41 @@ ops = append(ops, incHostedModelCountOp()) } + // Create the default storage pools for the model. + defaultStoragePoolsOps, err := st.createDefaultStoragePoolsOps(args.StorageProviderRegistry) + if err != nil { + return nil, errors.Trace(err) + } + ops = append(ops, defaultStoragePoolsOps...) + // Create the final map of config attributes for the model. // If we have ControllerInheritedConfig passed in, that means state // is being initialised and there won't be any config sources // in state. var configSources []modelConfigSource - if len(ControllerInheritedConfig) > 0 { - configSources = []modelConfigSource{{ - name: config.JujuControllerSource, - sourceFunc: modelConfigSourceFunc(func() (map[string]interface{}, error) { - return ControllerInheritedConfig, nil - })}} + if len(controllerInheritedConfig) > 0 { + configSources = []modelConfigSource{ + { + name: config.JujuDefaultSource, + sourceFunc: modelConfigSourceFunc(func() (attrValues, error) { + return config.ConfigDefaults(), nil + })}, + { + name: config.JujuControllerSource, + sourceFunc: modelConfigSourceFunc(func() (attrValues, error) { + return controllerInheritedConfig, nil + })}} } else { configSources = modelConfigSources(st) } - modelCfg, cfgSource, err := composeModelConfigAttributes(args.Config.AllAttrs(), configSources...) + modelCfg, err := composeModelConfigAttributes(args.Config.AllAttrs(), configSources...) if err != nil { return nil, errors.Trace(err) } + // Some values require marshalling before storage. + modelCfg = config.CoerceForStorage(modelCfg) ops = append(ops, createSettingsOp(settingsC, modelGlobalKey, modelCfg), - createSettingsSourceOp(cfgSource), createModelEntityRefsOp(modelUUID), createModelOp( args.Owner, @@ -351,6 +370,28 @@ return ops, nil } +func (st *State) createDefaultStoragePoolsOps(registry storage.ProviderRegistry) ([]txn.Op, error) { + m := poolmanager.MemSettings{make(map[string]map[string]interface{})} + pm := poolmanager.New(m, registry) + for _, providerType := range registry.StorageProviderTypes() { + p, err := registry.StorageProvider(providerType) + if err != nil { + return nil, errors.Trace(err) + } + if err := poolmanager.AddDefaultStoragePools(p, pm); err != nil { + return nil, errors.Annotatef( + err, "adding default storage pools for %q", providerType, + ) + } + } + + var ops []txn.Op + for key, settings := range m.Settings { + ops = append(ops, createSettingsOp(settingsC, key, settings)) + } + return ops, nil +} + func maybeUnauthorized(err error, msg string) error { if err == nil { return nil @@ -387,7 +428,7 @@ // // newState takes responsibility for the supplied *mgo.Session, and will // close it if it cannot be returned under the aegis of a *State. -func newState(modelTag names.ModelTag, session *mgo.Session, mongoInfo *mongo.MongoInfo, policy Policy) (_ *State, err error) { +func newState(modelTag names.ModelTag, session *mgo.Session, mongoInfo *mongo.MongoInfo, newPolicy NewPolicyFunc) (_ *State, err error) { defer func() { if err != nil { @@ -406,13 +447,17 @@ } // Create State. - return &State{ + st := &State{ modelTag: modelTag, mongoInfo: mongoInfo, session: session, database: database, - policy: policy, - }, nil + newPolicy: newPolicy, + } + if newPolicy != nil { + st.policy = newPolicy(st) + } + return st, nil } // MongoConnectionInfo returns information for connecting to mongo diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/package_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/package_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/package_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/package_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,8 +6,9 @@ import ( "testing" - coretesting "github.com/juju/juju/testing" "github.com/juju/utils/os" + + coretesting "github.com/juju/juju/testing" ) func TestPackage(t *testing.T) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/payloads.go juju-core-2.0~beta15/src/github.com/juju/juju/state/payloads.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/payloads.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/payloads.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,95 +5,263 @@ import ( "github.com/juju/errors" + "gopkg.in/mgo.v2/bson" + "gopkg.in/mgo.v2/txn" "github.com/juju/juju/payload" ) -// TODO(ericsnow) Track juju-level status in the status collection. - -// EnvPayloads exposes high-level interaction with all payloads -// in an model. -type EnvPayloads interface { - // ListAll builds the list of registered payloads in the env and returns it. - ListAll() ([]payload.FullPayloadInfo, error) -} - -// UnitPayloads exposes high-level interaction with payloads for a unit. -type UnitPayloads interface { - // Track tracks a payload in state. If a payload is - // already being tracked for the same (unit, payload name, plugin ID) - // then the request will fail. The unit must also be "alive". - Track(payload payload.Payload) error - // SetStatus sets the status of a payload. Only some fields - // must be set on the provided info: Name, Status, Details.ID, and - // Details.Status. If the payload is not in state then the request - // will fail. - SetStatus(docID, status string) error - // List builds the list of payloads registered for - // the given unit and IDs. If no IDs are provided then all - // registered payloads for the unit are returned. In the case that - // IDs are provided, any that are not in state are ignored and only - // the found ones are returned. It is up to the caller to - // extrapolate the list of missing IDs. - List(ids ...string) ([]payload.Result, error) - // LookUpReturns the Juju ID for the corresponding payload. - LookUp(name, rawID string) (string, error) - // Untrack removes the identified payload from state. If the - // given ID is not in state then the request will fail. - Untrack(id string) error -} - -// TODO(ericsnow) Use a more generic component registration mechanism? - -type newEnvPayloadsFunc func(Persistence) (EnvPayloads, error) -type newUnitPayloadsFunc func(persist Persistence, unit, machine string) (UnitPayloads, error) - -// TODO(ericsnow) Merge the 2 vars -var ( - newEnvPayloads newEnvPayloadsFunc - newUnitPayloads newUnitPayloadsFunc -) +// ModelPayloads returns a ModelPayloads for the state's model. +func (st *State) ModelPayloads() (ModelPayloads, error) { + return ModelPayloads{ + db: st.database, + }, nil +} -// SetPayloadComponent registers the functions that provide the state -// functionality related to payloads. -func SetPayloadsComponent(epFunc newEnvPayloadsFunc, upFunc newUnitPayloadsFunc) { - newEnvPayloads = epFunc - newUnitPayloads = upFunc +// ModelPayloads lets you read all unit payloads in a model. +type ModelPayloads struct { + db Database } -// EnvPayloads exposes interaction with payloads in state. -func (st *State) EnvPayloads() (EnvPayloads, error) { - if newEnvPayloads == nil { - return nil, errors.Errorf("payloads not supported") +// ListAll builds the list of payload information that is registered in state. +func (mp ModelPayloads) ListAll() ([]payload.FullPayloadInfo, error) { + coll, closer := mp.db.GetCollection(payloadsC) + defer closer() + + var docs []payloadDoc + if err := coll.Find(nil).All(&docs); err != nil { + return nil, errors.Trace(err) } + return nsPayloads.asPayloads(docs), nil +} - db := st.newPersistence() - envPayloads, err := newEnvPayloads(db) +// UnitPayloads returns a UnitPayloads for the supplied unit. +func (st *State) UnitPayloads(unit *Unit) (UnitPayloads, error) { + machineID, err := unit.AssignedMachineId() if err != nil { + return UnitPayloads{}, errors.Trace(err) + } + return UnitPayloads{ + db: st.database, + unit: unit.Name(), + machine: machineID, + }, nil +} + +// UnitPayloads lets you CRUD payloads for a single unit. +type UnitPayloads struct { + db Database + unit string + machine string +} + +// List has two different modes of operation, because that's never a bad +// idea. If you pass no args, it returns information about all payloads +// tracked by the unit; if you pass names, it returns a slice of results +// corresponding to names, in which any names not tracked have both the +// NotFound field *and* an Error set. +func (up UnitPayloads) List(names ...string) ([]payload.Result, error) { + + var sel bson.D + var out func([]payloadDoc) []payload.Result + if len(names) == 0 { + sel = nsPayloads.forUnit(up.unit) + out = nsPayloads.asResults + } else { + sel = nsPayloads.forUnitWithNames(up.unit, names) + out = func(docs []payloadDoc) []payload.Result { + return nsPayloads.orderedResults(docs, names) + } + } + + coll, closer := up.db.GetCollection(payloadsC) + defer closer() + var docs []payloadDoc + if err := coll.Find(sel).All(&docs); err != nil { return nil, errors.Trace(err) } + return out(docs), nil +} - return envPayloads, nil +// LookUp returns its first argument and no error. +func (UnitPayloads) LookUp(name, rawID string) (string, error) { + // This method *is* used in the apiserver layer, both to extract + // the name from the payload and to implement the LookUp facade + // method which allows clients to ask the server what the first + // of two strings might be. + // + // The previous implementation would hit the db to as well, to + // exactly the same effect as implemented here. Would drop the + // whole useless slice, but don't want to bloat the diff. + return name, nil } -// UnitPayloads exposes interaction with payloads in state -// for a the given unit. -func (st *State) UnitPayloads(unit *Unit) (UnitPayloads, error) { - if newUnitPayloads == nil { - return nil, errors.Errorf("payloads not supported") +// Track inserts the provided payload info in state. If the payload +// is already in the DB then it is replaced. +func (up UnitPayloads) Track(pl payload.Payload) error { + + // XXX OMFG payload/context/register.go:83 launches bad data + // which flies on a majestic unvalidated arc right through the + // system until it lands here. This code should be: + // + // if pl.Unit != up.unit { + // return errors.NotValidf("unexpected Unit %q", pl.Unit) + // } + // + // ...but is instead: + pl.Unit = up.unit + + if err := pl.Validate(); err != nil { + return errors.Trace(err) } - machineID, err := unit.AssignedMachineId() + doc := nsPayloads.asDoc(payload.FullPayloadInfo{ + Payload: pl, + Machine: up.machine, + }) + change := payloadTrackChange{doc} + if err := Apply(up.db, change); err != nil { + return errors.Trace(err) + } + return nil +} + +// SetStatus updates the raw status for the identified payload to the +// provided value. If the payload is missing then payload.ErrNotFound +// is returned. +func (up UnitPayloads) SetStatus(name, status string) error { + if err := payload.ValidateState(status); err != nil { + return errors.Trace(err) + } + + change := payloadSetStatusChange{ + Unit: up.unit, + Name: name, + Status: status, + } + if err := Apply(up.db, change); err != nil { + return errors.Trace(err) + } + return nil +} + +// Untrack removes the identified payload from state. It does not +// trigger the actual destruction of the payload. If the payload is +// missing then this is a noop. +func (up UnitPayloads) Untrack(name string) error { + logger.Tracef("untracking %q", name) + change := payloadUntrackChange{ + Unit: up.unit, + Name: name, + } + if err := Apply(up.db, change); err != nil { + return errors.Trace(err) + } + return nil +} + +// payloadTrackChange records a single unit payload. +type payloadTrackChange struct { + Doc payloadDoc +} + +// Prepare is part of the Change interface. +func (change payloadTrackChange) Prepare(db Database) ([]txn.Op, error) { + + unit := change.Doc.UnitID + units, closer := db.GetCollection(unitsC) + defer closer() + unitOp, err := nsLife.notDeadOp(units, unit) + if errors.Cause(err) == errDeadOrGone { + return nil, errors.Errorf("unit %q no longer available", unit) + } else if err != nil { + return nil, errors.Trace(err) + } + + payloads, closer := db.GetCollection(payloadsC) + defer closer() + payloadOp, err := nsPayloads.trackOp(payloads, change.Doc) if err != nil { return nil, errors.Trace(err) } - unitID := unit.UnitTag().Id() - persist := st.newPersistence() - unitPayloads, err := newUnitPayloads(persist, unitID, machineID) + return []txn.Op{unitOp, payloadOp}, nil +} + +// payloadSetStatusChange updates a single payload status. +type payloadSetStatusChange struct { + Unit string + Name string + Status string +} + +// Prepare is part of the Change interface. +func (change payloadSetStatusChange) Prepare(db Database) ([]txn.Op, error) { + docID := nsPayloads.docID(change.Unit, change.Name) + payloads, closer := db.GetCollection(payloadsC) + defer closer() + + op, err := nsPayloads.setStatusOp(payloads, docID, change.Status) + if errors.Cause(err) == errAlreadyRemoved { + return nil, payload.ErrNotFound + } else if err != nil { + return nil, errors.Trace(err) + } + return []txn.Op{op}, nil +} + +// payloadUntrackChange removes a single unit payload. +type payloadUntrackChange struct { + Unit string + Name string +} + +// Prepare is part of the Change interface. +func (change payloadUntrackChange) Prepare(db Database) ([]txn.Op, error) { + docID := nsPayloads.docID(change.Unit, change.Name) + payloads, closer := db.GetCollection(payloadsC) + defer closer() + + op, err := nsPayloads.untrackOp(payloads, docID) + if errors.Cause(err) == errAlreadyRemoved { + return nil, ErrChangeComplete + } else if err != nil { + return nil, errors.Trace(err) + } + return []txn.Op{op}, nil +} + +// payloadCleanupChange removes all unit payloads. +type payloadCleanupChange struct { + Unit string +} + +// Prepare is part of the Change interface. +func (change payloadCleanupChange) Prepare(db Database) ([]txn.Op, error) { + payloads, closer := db.GetCollection(payloadsC) + defer closer() + + sel := nsPayloads.forUnit(change.Unit) + fields := bson.D{{"_id", 1}} + var docs []struct { + DocID string `bson:"_id"` + } + err := payloads.Find(sel).Select(fields).All(&docs) if err != nil { return nil, errors.Trace(err) + } else if len(docs) == 0 { + return nil, ErrChangeComplete } - return unitPayloads, nil + ops := make([]txn.Op, 0, len(docs)) + for _, doc := range docs { + op, err := nsPayloads.untrackOp(payloads, doc.DocID) + if errors.Cause(err) == errAlreadyRemoved { + continue + } else if err != nil { + return nil, errors.Trace(err) + } + ops = append(ops, op) + } + return ops, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/payloads_ns.go juju-core-2.0~beta15/src/github.com/juju/juju/state/payloads_ns.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/payloads_ns.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/payloads_ns.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,218 @@ +package state + +import ( + "fmt" + + "github.com/juju/errors" + "gopkg.in/juju/charm.v6-unstable" + "gopkg.in/mgo.v2/bson" + "gopkg.in/mgo.v2/txn" + + "github.com/juju/juju/mongo" + "github.com/juju/juju/payload" +) + +// payloadDoc is the top-level document for payloads. +type payloadDoc struct { + // _id encodes UnitID and Name (which should theoretically + // match the name of a payload-class defined in the charm -- + // for example "my-payload" -- but nothing really checks). + UnitID string `bson:"unitid"` + Name string `bson:"name"` + + // MachineID doesn't belong here. + MachineID string `bson:"machine-id"` + + // Type is again a freeform field that might match that of a + // payload-class defined in the charm -- for example, "docker". + Type string `bson:"type"` + + // RawID records the substrate-specific payload id -- for + // example, "9cd6338abdf09beb", the actual docker container + // we're tracking. + RawID string `bson:"rawid"` + + // State is sort of like status, valid values are defined in + // package payloads. + State string `bson:"state"` + + // Labels contain whatever additional arbitrary strings were + // left over after we hoovered up from the + // command line. + Labels []string `bson:"labels"` +} + +// nsPayloads_ backs nsPayloads. +type nsPayloads_ struct{} + +// nsPayloads namespaces low-level unit-payload functionality: it's +// meant to be the one place in the code where we wrangle queries, +// serialization, and updates to payload data. (The UnitPayloads and +// ModelPayloads types may run queries directly, because it's silly +// to build *another* mongo-aping layer with its own idiosyncratic +// implementations of One and All and so on; but they should be getting +// all their queries from here, and using these methods to convert +// types, and generally making a point of *not* knowing anything about +// how the actual data is represented.) +var nsPayloads = nsPayloads_{} + +// docID is globalKey as written by someone who thought it would be +// helpful to reinvent the 'u##' prefix (which *would* indicate +// that payloads are things-that-exist-per-unit, and do so in a way +// consistent with the rest of the DB. /sigh.) +func (nsPayloads_) docID(unit, name string) string { + return fmt.Sprintf("payload#%s#%s", unit, name) +} + +// forUnit returns a selector that matches all payloads for the unit. +func (nsPayloads_) forUnit(unit string) bson.D { + return bson.D{{"unitid", unit}} +} + +// forUnitWithNames returns a selector that matches any payloads for the +// supplied unit that have the supplied names. +func (nsPayloads_) forUnitWithNames(unit string, names []string) bson.D { + ids := make([]string, 0, len(names)) + for _, name := range names { + ids = append(ids, nsPayloads.docID(unit, name)) + } + return bson.D{{"_id", bson.D{{"$in", ids}}}} +} + +// asDoc converts a FullPayloadInfo into an independent payloadDoc. +func (nsPayloads_) asDoc(p payload.FullPayloadInfo) payloadDoc { + labels := make([]string, len(p.Labels)) + copy(labels, p.Labels) + return payloadDoc{ + UnitID: p.Unit, + Name: p.PayloadClass.Name, + MachineID: p.Machine, + Type: p.PayloadClass.Type, + RawID: p.ID, + State: p.Status, + Labels: labels, + } +} + +// asPayload converts a payloadDoc into an independent FullPayloadInfo. +func (nsPayloads_) asPayload(doc payloadDoc) payload.FullPayloadInfo { + labels := make([]string, len(doc.Labels)) + copy(labels, doc.Labels) + p := payload.FullPayloadInfo{ + Payload: payload.Payload{ + PayloadClass: charm.PayloadClass{ + Name: doc.Name, + Type: doc.Type, + }, + ID: doc.RawID, + Status: doc.State, + Labels: labels, + Unit: doc.UnitID, + }, + Machine: doc.MachineID, + } + return p +} + +// asPayloads converts a slice of payloadDocs into a corresponding slice +// of independent FullPayloadInfos. +func (nsPayloads_) asPayloads(docs []payloadDoc) []payload.FullPayloadInfo { + payloads := make([]payload.FullPayloadInfo, 0, len(docs)) + for _, doc := range docs { + payloads = append(payloads, nsPayloads.asPayload(doc)) + } + return payloads +} + +// asResults converts a slice of payloadDocs into a corresponding slice +// of independent payload.Results. +func (nsPayloads_) asResults(docs []payloadDoc) []payload.Result { + results := make([]payload.Result, 0, len(docs)) + for _, doc := range docs { + full := nsPayloads.asPayload(doc) + results = append(results, payload.Result{ + ID: doc.Name, + Payload: &full, + }) + } + return results +} + +// orderedResults converts payloadDocs into payload.Results, in the +// order defined by names, and represents missing names in the highly +// baroque fashion apparently designed into Results. +func (nsPayloads_) orderedResults(docs []payloadDoc, names []string) []payload.Result { + found := make(map[string]payloadDoc) + for _, doc := range docs { + found[doc.Name] = doc + } + results := make([]payload.Result, len(names)) + for i, name := range names { + results[i].ID = name + if doc, ok := found[name]; ok { + full := nsPayloads.asPayload(doc) + results[i].Payload = &full + } else { + results[i].NotFound = true + results[i].Error = errors.NotFoundf(name) + } + } + return results +} + +// trackOp returns a txn.Op that will either insert or update the +// supplied payload, and fail if the observed precondition changes. +func (nsPayloads_) trackOp(payloads mongo.Collection, doc payloadDoc) (txn.Op, error) { + docID := nsPayloads.docID(doc.UnitID, doc.Name) + payloadOp := txn.Op{ + C: payloads.Name(), + Id: docID, + } + count, err := payloads.FindId(docID).Count() + if err != nil { + return txn.Op{}, errors.Trace(err) + } else if count == 0 { + payloadOp.Assert = txn.DocMissing + payloadOp.Insert = doc + } else { + payloadOp.Assert = txn.DocExists + payloadOp.Update = bson.D{{"$set", doc}} + } + return payloadOp, nil +} + +// untrackOp returns a txn.Op that will unconditionally remove the +// identified document. If the payload doesn't exist, it returns +// errAlreadyRemoved. +func (nsPayloads_) untrackOp(payloads mongo.Collection, docID string) (txn.Op, error) { + count, err := payloads.FindId(docID).Count() + if err != nil { + return txn.Op{}, errors.Trace(err) + } else if count == 0 { + return txn.Op{}, errAlreadyRemoved + } + return txn.Op{ + C: payloads.Name(), + Id: docID, + Assert: txn.DocExists, + Remove: true, + }, nil +} + +// setStatusOp returns a txn.Op that updates the status of the +// identified payload. If the payload doesn't exist, it returns +// errAlreadyRemoved. +func (nsPayloads_) setStatusOp(payloads mongo.Collection, docID string, status string) (txn.Op, error) { + count, err := payloads.FindId(docID).Count() + if err != nil { + return txn.Op{}, errors.Trace(err) + } else if count == 0 { + return txn.Op{}, errAlreadyRemoved + } + return txn.Op{ + C: payloads.Name(), + Id: docID, + Assert: txn.DocExists, + Update: bson.D{{"$set", bson.D{{"state", status}}}}, + }, nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/payloads_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/payloads_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/payloads_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/payloads_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,31 +4,495 @@ package state_test import ( + "fmt" + "sort" + + "github.com/juju/errors" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" "gopkg.in/juju/charm.v6-unstable" - "github.com/juju/juju/component/all" "github.com/juju/juju/payload" "github.com/juju/juju/state" + "github.com/juju/juju/testing/factory" ) -func init() { - if err := all.RegisterForServer(); err != nil { - panic(err) +type PayloadsSuite struct { + ConnSuite +} + +var _ = gc.Suite(&PayloadsSuite{}) + +func (s *PayloadsSuite) TestLookUp(c *gc.C) { + fix := s.newFixture(c) + + result, err := fix.UnitPayloads.LookUp("returned", "ignored") + c.Check(result, gc.Equals, "returned") + c.Check(err, jc.ErrorIsNil) +} + +func (s *PayloadsSuite) TestListPartial(c *gc.C) { + // Note: List and ListAll are extensively tested via the Check + // methods on payloadFixture, used throughout the suite. But + // they don't cover this feature... + fix, initial := s.newPayloadFixture(c) + results, err := fix.UnitPayloads.List("whatever", initial.Name) + c.Assert(err, jc.ErrorIsNil) + c.Assert(results, gc.HasLen, 2) + + missing := results[0] + c.Check(missing.ID, gc.Equals, "whatever") + c.Check(missing.Payload, gc.IsNil) + c.Check(missing.NotFound, jc.IsTrue) + c.Check(missing.Error, jc.Satisfies, errors.IsNotFound) + c.Check(missing.Error, gc.ErrorMatches, "whatever not found") + + found := results[1] + c.Check(found.ID, gc.Equals, initial.Name) + c.Assert(found.Payload, gc.NotNil) + c.Assert(*found.Payload, jc.DeepEquals, fix.FullPayload(initial)) + c.Check(found.NotFound, jc.IsFalse) + c.Check(found.Error, jc.ErrorIsNil) +} + +func (s *PayloadsSuite) TestNoPayloads(c *gc.C) { + fix := s.newFixture(c) + + fix.CheckNoPayload(c) +} + +func (s *PayloadsSuite) TestTrackInvalidPayload(c *gc.C) { + // not an exhaustive test, just an indication we do Validate() + fix := s.newFixture(c) + pl := fix.SamplePayload("") + + err := fix.UnitPayloads.Track(pl) + c.Check(err, jc.Satisfies, errors.IsNotValid) + c.Check(err, gc.ErrorMatches, `missing ID not valid`) + fix.CheckNoPayload(c) +} + +func (s *PayloadsSuite) TestTrackInvalidUnit(c *gc.C) { + + // Note: this is STUPID, but none of the unit-specific contexts + // between `api/context/register.go` and here ever check that + // the track request is correctly targeted. So we overwrite it + // unconditionally... because register is unconditionally + // sending a garbage unit name for some reason. + + fix := s.newFixture(c) + expect := fix.SamplePayload("some-docker-id") + track := expect + track.Unit = "different/0" + + err := fix.UnitPayloads.Track(track) + // In a sensible implementation, this would be: + // + // c.Check(err, jc.Satisfies, errors.IsNotValid) + // c.Check(err, gc.ErrorMatches, `unexpected Unit "different/0" not valid`) + // + // fix.CheckUnitPayloads(c) + // fix.CheckModelPayloads(c) + // + // ...but instead we have: + c.Assert(err, jc.ErrorIsNil) + fix.CheckOnePayload(c, expect) +} + +func (s *PayloadsSuite) TestTrackInsertPayload(c *gc.C) { + fix := s.newFixture(c) + desired := fix.SamplePayload("some-docker-id") + + err := fix.UnitPayloads.Track(desired) + c.Assert(err, jc.ErrorIsNil) + fix.CheckOnePayload(c, desired) +} + +func (s *PayloadsSuite) TestTrackUpdatePayload(c *gc.C) { + fix, initial := s.newPayloadFixture(c) + replacement := initial + replacement.ID = "new-exciting-different" + + err := fix.UnitPayloads.Track(replacement) + c.Assert(err, jc.ErrorIsNil) + fix.CheckOnePayload(c, replacement) +} + +func (s *PayloadsSuite) TestTrackMultiplePayloads(c *gc.C) { + fix, initial := s.newPayloadFixture(c) + additional := fix.SamplePayload("another-docker-id") + additional.Name = "app" + + err := fix.UnitPayloads.Track(additional) + c.Assert(err, jc.ErrorIsNil) + + full1 := fix.FullPayload(initial) + full2 := fix.FullPayload(additional) + fix.CheckUnitPayloads(c, full1, full2) + fix.CheckModelPayloads(c, full1, full2) +} + +func (s *PayloadsSuite) TestTrackMultipleUnits(c *gc.C) { + fix, initial := s.newPayloadFixture(c) + + // Create a new unit to add another payload to. + applicationName := fix.Unit.ApplicationName() + application, err := s.State.Application(applicationName) + c.Assert(err, jc.ErrorIsNil) + machine2 := s.Factory.MakeMachine(c, nil) + unit2 := s.Factory.MakeUnit(c, &factory.UnitParams{ + Application: application, + Machine: machine2, + }) + + // Add a payload which should be independent of the + // UnitPayloads in the fixture. + unit2Payloads, err := s.State.UnitPayloads(unit2) + c.Assert(err, jc.ErrorIsNil) + additional := initial + additional.Unit = unit2.Name() + err = unit2Payloads.Track(additional) + c.Assert(err, jc.ErrorIsNil) + + // Check the independent payload only shows up in + // the fixture's ModelPayloads, not its UnitPayloads. + full1 := fix.FullPayload(initial) + full2 := payload.FullPayloadInfo{ + Payload: additional, + Machine: machine2.Id(), } + fix.CheckUnitPayloads(c, full1) + fix.CheckModelPayloads(c, full1, full2) } -var ( - _ = gc.Suite(&envPayloadsSuite{}) - _ = gc.Suite(&unitPayloadsSuite{}) -) +func (s *PayloadsSuite) TestSetStatusInvalid(c *gc.C) { + fix, initial := s.newPayloadFixture(c) + + err := fix.UnitPayloads.SetStatus(initial.Name, "twirling") + c.Check(err, jc.Satisfies, errors.IsNotValid) + c.Check(err.Error(), gc.Equals, `status "twirling" not supported; expected one of ["running", "starting", "stopped", "stopping"]`) + + fix.CheckOnePayload(c, initial) +} + +func (s *PayloadsSuite) TestSetStatus(c *gc.C) { + fix, initial := s.newPayloadFixture(c) + expect := initial + expect.Status = "stopping" + + err := fix.UnitPayloads.SetStatus(initial.Name, "stopping") + c.Assert(err, jc.ErrorIsNil) + + fix.CheckOnePayload(c, expect) +} + +func (s *PayloadsSuite) TestUntrackMissing(c *gc.C) { + fix := s.newFixture(c) + + err := fix.UnitPayloads.Untrack("whatever") + c.Assert(err, jc.ErrorIsNil) + fix.CheckNoPayload(c) +} + +func (s *PayloadsSuite) TestUntrack(c *gc.C) { + fix, initial := s.newPayloadFixture(c) + + err := fix.UnitPayloads.Untrack(initial.Name) + c.Assert(err, jc.ErrorIsNil) + fix.CheckNoPayload(c) +} + +func (s *PayloadsSuite) TestRemoveUnitUntracksPayloads(c *gc.C) { + fix, _ := s.newPayloadFixture(c) + additional := fix.SamplePayload("another-docker-id") + additional.Name = "app" + err := fix.UnitPayloads.Track(additional) + c.Assert(err, jc.ErrorIsNil) + + err = fix.Unit.Destroy() + c.Assert(err, jc.ErrorIsNil) + err = s.State.Cleanup() + c.Assert(err, jc.ErrorIsNil) + fix.CheckNoPayload(c) +} + +func (s *PayloadsSuite) TestTrackRaceDyingUnit(c *gc.C) { + fix := s.newFixture(c) + preventUnitDestroyRemove(c, fix.Unit) + + defer state.SetBeforeHooks(c, s.State, func() { + err := fix.Unit.Destroy() + c.Assert(err, jc.ErrorIsNil) + }).Check() + + desired := fix.SamplePayload("this-is-fine") + err := fix.UnitPayloads.Track(desired) + c.Assert(err, jc.ErrorIsNil) + fix.CheckOnePayload(c, desired) +} + +func (s *PayloadsSuite) TestTrackRaceDeadUnit(c *gc.C) { + fix := s.newFixture(c) + preventUnitDestroyRemove(c, fix.Unit) + + defer state.SetBeforeHooks(c, s.State, func() { + err := fix.Unit.Destroy() + c.Assert(err, jc.ErrorIsNil) + err = fix.Unit.EnsureDead() + c.Assert(err, jc.ErrorIsNil) + }).Check() + + desired := fix.SamplePayload("sorry-too-late") + err := fix.UnitPayloads.Track(desired) + c.Check(err, gc.ErrorMatches, fix.DeadUnitMessage()) + fix.CheckNoPayload(c) +} + +func (s *PayloadsSuite) TestTrackRaceRemovedUnit(c *gc.C) { + fix := s.newFixture(c) + + defer state.SetBeforeHooks(c, s.State, func() { + err := fix.Unit.Destroy() + c.Assert(err, jc.ErrorIsNil) + }).Check() + + desired := fix.SamplePayload("sorry-too-late") + err := fix.UnitPayloads.Track(desired) + c.Check(err, gc.ErrorMatches, fix.DeadUnitMessage()) + fix.CheckNoPayload(c) +} + +func (s *PayloadsSuite) TestTrackRaceTrack(c *gc.C) { + fix := s.newFixture(c) + desired := fix.SamplePayload("wanted") + interloper := fix.SamplePayload("not-wanted") + + defer state.SetBeforeHooks(c, s.State, func() { + err := fix.UnitPayloads.Track(interloper) + c.Assert(err, jc.ErrorIsNil) + }).Check() + + err := fix.UnitPayloads.Track(desired) + c.Assert(err, jc.ErrorIsNil) + fix.CheckOnePayload(c, desired) +} + +func (s *PayloadsSuite) TestTrackRaceSetStatus(c *gc.C) { + fix, initial := s.newPayloadFixture(c) + desired := initial + desired.Status = "starting" + + defer state.SetBeforeHooks(c, s.State, func() { + err := fix.UnitPayloads.SetStatus(initial.Name, "stopping") + c.Assert(err, jc.ErrorIsNil) + }).Check() + + err := fix.UnitPayloads.Track(desired) + c.Assert(err, jc.ErrorIsNil) + fix.CheckOnePayload(c, desired) +} + +func (s *PayloadsSuite) TestTrackRaceUntrack(c *gc.C) { + fix, initial := s.newPayloadFixture(c) + + defer state.SetBeforeHooks(c, s.State, func() { + err := fix.UnitPayloads.Untrack(initial.Name) + c.Assert(err, jc.ErrorIsNil) + }).Check() + + err := fix.UnitPayloads.Track(initial) + c.Assert(err, jc.ErrorIsNil) + fix.CheckOnePayload(c, initial) +} + +func (s *PayloadsSuite) TestSetStatusRaceTrack(c *gc.C) { + fix, initial := s.newPayloadFixture(c) + expect := initial + expect.Status = "stopped" + + defer state.SetBeforeHooks(c, s.State, func() { + err := fix.UnitPayloads.Track(initial) + c.Assert(err, jc.ErrorIsNil) + }).Check() + + err := fix.UnitPayloads.SetStatus(initial.Name, "stopped") + c.Assert(err, jc.ErrorIsNil) + fix.CheckOnePayload(c, expect) +} + +func (s *PayloadsSuite) TestSetStatusRaceUntrack(c *gc.C) { + fix, initial := s.newPayloadFixture(c) + + defer state.SetBeforeHooks(c, s.State, func() { + err := fix.UnitPayloads.Untrack(initial.Name) + c.Assert(err, jc.ErrorIsNil) + }).Check() + + err := fix.UnitPayloads.SetStatus(initial.Name, "stopped") + c.Check(errors.Cause(err), gc.Equals, payload.ErrNotFound) + c.Check(err, gc.ErrorMatches, "payload not found") + fix.CheckNoPayload(c) +} + +func (s *PayloadsSuite) TestUntrackRaceTrack(c *gc.C) { + fix, initial := s.newPayloadFixture(c) + + defer state.SetBeforeHooks(c, s.State, func() { + err := fix.UnitPayloads.Track(initial) + c.Assert(err, jc.ErrorIsNil) + }).Check() + + err := fix.UnitPayloads.Untrack(initial.Name) + c.Assert(err, jc.ErrorIsNil) + fix.CheckNoPayload(c) +} + +func (s *PayloadsSuite) TestUntrackRaceSetStatus(c *gc.C) { + fix, initial := s.newPayloadFixture(c) + + defer state.SetBeforeHooks(c, s.State, func() { + err := fix.UnitPayloads.SetStatus(initial.Name, "stopping") + c.Assert(err, jc.ErrorIsNil) + }).Check() + + err := fix.UnitPayloads.Untrack(initial.Name) + c.Assert(err, jc.ErrorIsNil) + fix.CheckNoPayload(c) +} + +func (s *PayloadsSuite) TestUntrackRaceUntrack(c *gc.C) { + fix, initial := s.newPayloadFixture(c) + + defer state.SetBeforeHooks(c, s.State, func() { + err := fix.UnitPayloads.Untrack(initial.Name) + c.Assert(err, jc.ErrorIsNil) + }).Check() + + err := fix.UnitPayloads.Untrack(initial.Name) + c.Assert(err, jc.ErrorIsNil) + fix.CheckNoPayload(c) +} + +// ------------------------- +// test helpers + +type payloadsFixture struct { + ModelPayloads state.ModelPayloads + UnitPayloads state.UnitPayloads + Machine *state.Machine + Unit *state.Unit +} + +func (s *PayloadsSuite) newFixture(c *gc.C) payloadsFixture { + machine := s.Factory.MakeMachine(c, nil) + unit := s.Factory.MakeUnit(c, &factory.UnitParams{Machine: machine}) + modelPayloads, err := s.State.ModelPayloads() + c.Assert(err, jc.ErrorIsNil) + unitPayloads, err := s.State.UnitPayloads(unit) + c.Assert(err, jc.ErrorIsNil) + return payloadsFixture{ + ModelPayloads: modelPayloads, + UnitPayloads: unitPayloads, + Machine: machine, + Unit: unit, + } +} + +func (s *PayloadsSuite) newPayloadFixture(c *gc.C) (payloadsFixture, payload.Payload) { + fix := s.newFixture(c) + initial := fix.SamplePayload("some-docker-id") + err := fix.UnitPayloads.Track(initial) + c.Assert(err, jc.ErrorIsNil) + return fix, initial +} + +func (fix payloadsFixture) SamplePayload(id string) payload.Payload { + return payload.Payload{ + PayloadClass: charm.PayloadClass{ + Name: "database", + Type: "docker", + }, + Status: payload.StateRunning, + ID: id, + Unit: fix.Unit.Name(), + } +} + +func (fix payloadsFixture) DeadUnitMessage() string { + return fmt.Sprintf("unit %q no longer available", fix.Unit.Name()) +} + +func (fix payloadsFixture) FullPayload(pl payload.Payload) payload.FullPayloadInfo { + return payload.FullPayloadInfo{ + Payload: pl, + Machine: fix.Machine.Id(), + } +} + +func (fix payloadsFixture) CheckNoPayload(c *gc.C) { + fix.CheckModelPayloads(c) + fix.CheckUnitPayloads(c) +} -type envPayloadsSuite struct { +func (fix payloadsFixture) CheckOnePayload(c *gc.C, expect payload.Payload) { + full := fix.FullPayload(expect) + fix.CheckModelPayloads(c, full) + fix.CheckUnitPayloads(c, full) +} + +func (fix payloadsFixture) CheckModelPayloads(c *gc.C, expect ...payload.FullPayloadInfo) { + actual, err := fix.ModelPayloads.ListAll() + c.Check(err, jc.ErrorIsNil) + sort.Sort(byPayloadInfo(actual)) + sort.Sort(byPayloadInfo(expect)) + c.Check(actual, jc.DeepEquals, expect) +} + +func (fix payloadsFixture) CheckUnitPayloads(c *gc.C, expect ...payload.FullPayloadInfo) { + actual, err := fix.UnitPayloads.List() + c.Check(err, jc.ErrorIsNil) + extracted := fix.extractInfos(c, actual) + sort.Sort(byPayloadInfo(extracted)) + sort.Sort(byPayloadInfo(expect)) + c.Check(extracted, jc.DeepEquals, expect) +} + +func (payloadsFixture) extractInfos(c *gc.C, results []payload.Result) []payload.FullPayloadInfo { + fulls := make([]payload.FullPayloadInfo, 0, len(results)) + for _, result := range results { + c.Assert(result.ID, gc.Equals, result.Payload.Name) + c.Assert(result.Payload, gc.NotNil) + c.Assert(result.NotFound, jc.IsFalse) + c.Assert(result.Error, jc.ErrorIsNil) + fulls = append(fulls, *result.Payload) + } + return fulls +} + +type byPayloadInfo []payload.FullPayloadInfo + +func (s byPayloadInfo) Len() int { return len(s) } +func (s byPayloadInfo) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s byPayloadInfo) Less(i, j int) bool { + if s[i].Machine != s[j].Machine { + return s[i].Machine < s[j].Machine + } + if s[i].Payload.Unit != s[j].Payload.Unit { + return s[i].Payload.Unit < s[j].Payload.Unit + } + return s[i].Payload.Name < s[j].Payload.Name +} + +// ---------------------------------------------------------- +// original functional tests + +type PayloadsFunctionalSuite struct { ConnSuite } -func (s *envPayloadsSuite) TestFunctional(c *gc.C) { +var _ = gc.Suite(&PayloadsFunctionalSuite{}) + +func (s *PayloadsFunctionalSuite) TestModelPayloads(c *gc.C) { machine := "0" unit := addUnit(c, s.ConnSuite, unitArgs{ charm: "dummy", @@ -40,7 +504,7 @@ ust, err := s.State.UnitPayloads(unit) c.Assert(err, jc.ErrorIsNil) - st, err := s.State.EnvPayloads() + st, err := s.State.ModelPayloads() c.Assert(err, jc.ErrorIsNil) payloads, err := st.ListAll() @@ -89,11 +553,7 @@ c.Check(payloads, gc.HasLen, 0) } -type unitPayloadsSuite struct { - ConnSuite -} - -func (s *unitPayloadsSuite) TestFunctional(c *gc.C) { +func (s *PayloadsFunctionalSuite) TestUnitPayloads(c *gc.C) { machine := "0" unit := addUnit(c, s.ConnSuite, unitArgs{ charm: "dummy", diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/persistence.go juju-core-2.0~beta15/src/github.com/juju/juju/state/persistence.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/persistence.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/persistence.go 2016-08-16 08:56:25.000000000 +0000 @@ -100,10 +100,6 @@ return []txn.Op{{ C: applicationsC, Id: applicationID, - Assert: txn.DocExists, - }, { - C: applicationsC, - Id: applicationID, Assert: isAliveDoc, }} } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/policy.go juju-core-2.0~beta15/src/github.com/juju/juju/state/policy.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/policy.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/policy.go 2016-08-16 08:56:25.000000000 +0000 @@ -12,8 +12,13 @@ "github.com/juju/juju/environs/config" "github.com/juju/juju/instance" "github.com/juju/juju/state/cloudimagemetadata" + "github.com/juju/juju/storage" ) +// NewPolicyFunc is the type of a function that, +// given a *State, returns a Policy for that State. +type NewPolicyFunc func(*State) Policy + // Policy is an interface provided to State that may // be consulted by State to validate or modify the // behaviour of certain operations. @@ -24,24 +29,20 @@ // be ignored. Any other error will cause an error // in the use of the policy. type Policy interface { - // Prechecker takes a *config.Config and returns a Prechecker or an error. - Prechecker(*config.Config) (Prechecker, error) + // Prechecker returns a Prechecker or an error. + Prechecker() (Prechecker, error) + + // ConfigValidator returns a config.Validator or an error. + ConfigValidator() (config.Validator, error) + + // ConstraintsValidator returns a constraints.Validator or an error. + ConstraintsValidator() (constraints.Validator, error) + + // InstanceDistributor returns an instance.Distributor or an error. + InstanceDistributor() (instance.Distributor, error) - // ConfigValidator takes a provider type name and returns a ConfigValidator - // or an error. - ConfigValidator(providerType string) (ConfigValidator, error) - - // EnvironCapability takes a *config.Config and returns an EnvironCapability - // or an error. - EnvironCapability(*config.Config) (EnvironCapability, error) - - // ConstraintsValidator takes a *config.Config and SupportedArchitecturesQuerier - // to return a constraints.Validator or an error. - ConstraintsValidator(*config.Config, SupportedArchitecturesQuerier) (constraints.Validator, error) - - // InstanceDistributor takes a *config.Config and returns an - // InstanceDistributor or an error. - InstanceDistributor(*config.Config) (InstanceDistributor, error) + // StorageProviderRegistry returns a storage.ProviderRegistry or an error. + StorageProviderRegistry() (storage.ProviderRegistry, error) } // Prechecker is a policy interface that is provided to State @@ -58,37 +59,13 @@ PrecheckInstance(series string, cons constraints.Value, placement string) error } -// ConfigValidator is a policy interface that is provided to State -// to check validity of new configuration attributes before applying them to state. -type ConfigValidator interface { - Validate(cfg, old *config.Config) (valid *config.Config, err error) -} - -// EnvironCapability implements access to metadata about the capabilities -// of an model. -type EnvironCapability interface { - // SupportedArchitectures returns the image architectures which can - // be hosted by this model. - SupportedArchitectures() ([]string, error) - - // SupportsUnitAssignment returns an error which, if non-nil, indicates - // that the model does not support unit placement. If the model - // does not support unit placement, then machines may not be created - // without units, and units cannot be placed explcitly. - SupportsUnitPlacement() error -} - // precheckInstance calls the state's assigned policy, if non-nil, to obtain // a Prechecker, and calls PrecheckInstance if a non-nil Prechecker is returned. func (st *State) precheckInstance(series string, cons constraints.Value, placement string) error { if st.policy == nil { return nil } - cfg, err := st.ModelConfig() - if err != nil { - return err - } - prechecker, err := st.policy.Prechecker(cfg) + prechecker, err := st.policy.Prechecker() if errors.IsNotImplemented(err) { return nil } else if err != nil { @@ -103,25 +80,44 @@ func (st *State) constraintsValidator() (constraints.Validator, error) { // Default behaviour is to simply use a standard validator with // no model specific behaviour built in. - defaultValidator := constraints.NewValidator() - if st.policy == nil { - return defaultValidator, nil - } - cfg, err := st.ModelConfig() - if err != nil { - return nil, err - } - validator, err := st.policy.ConstraintsValidator( - cfg, - &cloudimagemetadata.MetadataArchitectureQuerier{st.CloudImageMetadataStorage}, - ) - if errors.IsNotImplemented(err) { - return defaultValidator, nil - } else if err != nil { - return nil, err - } - if validator == nil { - return nil, fmt.Errorf("policy returned nil constraints validator without an error") + var validator constraints.Validator + if st.policy != nil { + var err error + validator, err = st.policy.ConstraintsValidator() + if errors.IsNotImplemented(err) { + validator = constraints.NewValidator() + } else if err != nil { + return nil, err + } else if validator == nil { + return nil, fmt.Errorf("policy returned nil constraints validator without an error") + } + } else { + validator = constraints.NewValidator() + } + + // Add supported architectures gleaned from cloud image + // metadata to the validator's vocabulary. + model, err := st.Model() + if err != nil { + return nil, errors.Annotate(err, "getting model") + } + if region := model.CloudRegion(); region != "" { + cfg, err := st.ModelConfig() + if err != nil { + return nil, errors.Trace(err) + } + arches, err := st.CloudImageMetadataStorage.SupportedArchitectures( + cloudimagemetadata.MetadataFilter{ + Stream: cfg.AgentStream(), + Region: region, + }, + ) + if err != nil { + return nil, errors.Annotate(err, "querying supported architectures") + } + if len(arches) != 0 { + validator.UpdateVocabulary(constraints.Arch, arches) + } } return validator, nil } @@ -151,13 +147,13 @@ } // validate calls the state's assigned policy, if non-nil, to obtain -// a ConfigValidator, and calls Validate if a non-nil ConfigValidator is +// a config.Validator, and calls Validate if a non-nil config.Validator is // returned. func (st *State) validate(cfg, old *config.Config) (valid *config.Config, err error) { if st.policy == nil { return cfg, nil } - configValidator, err := st.policy.ConfigValidator(cfg.Type()) + configValidator, err := st.policy.ConfigValidator() if errors.IsNotImplemented(err) { return cfg, nil } else if err != nil { @@ -169,51 +165,9 @@ return configValidator.Validate(cfg, old) } -// supportsUnitPlacement calls the state's assigned policy, if non-nil, -// to obtain an EnvironCapability, and calls SupportsUnitPlacement if a -// non-nil EnvironCapability is returned. -func (st *State) supportsUnitPlacement() error { +func (st *State) storageProviderRegistry() (storage.ProviderRegistry, error) { if st.policy == nil { - return nil - } - cfg, err := st.ModelConfig() - if err != nil { - return errors.Trace(err) + return storage.StaticProviderRegistry{}, nil } - capability, err := st.policy.EnvironCapability(cfg) - if errors.IsNotImplemented(err) { - return nil - } else if err != nil { - return errors.Trace(err) - } - if capability == nil { - return fmt.Errorf("policy returned nil EnvironCapability without an error") - } - return capability.SupportsUnitPlacement() -} - -// InstanceDistributor is a policy interface that is provided -// to State to perform distribution of units across instances -// for high availability. -type InstanceDistributor interface { - // DistributeInstance takes a set of clean, empty - // instances, and a distribution group, and returns - // the subset of candidates which the policy will - // allow entry into the distribution group. - // - // The AssignClean and AssignCleanEmpty unit - // assignment policies will attempt to assign a - // unit to each of the resulting instances until - // one is successful. If no instances can be assigned - // to (e.g. because of concurrent deployments), then - // a new machine will be allocated. - DistributeInstances(candidates, distributionGroup []instance.Id) ([]instance.Id, error) -} - -// SupportedArchitecturesQuerier implements access to stored cloud image metadata -// to retrieve a collection of supported architectures. -type SupportedArchitecturesQuerier interface { - // SupportedArchitectures returns a collection of unique architectures - // from cloud image metadata that satisfy passed in filtering parameters. - SupportedArchitectures(stream, region string) ([]string, error) + return st.policy.StorageProviderRegistry() } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/prechecker_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/prechecker_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/prechecker_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/prechecker_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -12,7 +12,6 @@ "github.com/juju/juju/agent" "github.com/juju/juju/constraints" - "github.com/juju/juju/environs/config" "github.com/juju/juju/instance" "github.com/juju/juju/state" ) @@ -41,7 +40,7 @@ func (s *PrecheckerSuite) SetUpTest(c *gc.C) { s.ConnSuite.SetUpTest(c) s.prechecker = mockPrechecker{} - s.policy.GetPrechecker = func(*config.Config) (state.Prechecker, error) { + s.policy.GetPrechecker = func() (state.Prechecker, error) { return &s.prechecker, nil } } @@ -70,7 +69,7 @@ c.Assert(err, gc.ErrorMatches, ".*no instance for you") // If the policy's Prechecker method fails, that will be returned first. - s.policy.GetPrechecker = func(*config.Config) (state.Prechecker, error) { + s.policy.GetPrechecker = func() (state.Prechecker, error) { return nil, fmt.Errorf("no prechecker for you") } _, err = s.addOneMachine(c, constraints.Value{}, "placement") @@ -79,7 +78,7 @@ func (s *PrecheckerSuite) TestPrecheckPrecheckerUnimplemented(c *gc.C) { var precheckerErr error - s.policy.GetPrechecker = func(*config.Config) (state.Prechecker, error) { + s.policy.GetPrechecker = func() (state.Prechecker, error) { return nil, precheckerErr } _, err := s.addOneMachine(c, constraints.Value{}, "placement") @@ -90,7 +89,7 @@ } func (s *PrecheckerSuite) TestPrecheckNoPolicy(c *gc.C) { - s.policy.GetPrechecker = func(*config.Config) (state.Prechecker, error) { + s.policy.GetPrechecker = func() (state.Prechecker, error) { c.Errorf("should not have been invoked") return nil, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/reboot_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/reboot_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/reboot_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/reboot_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -61,30 +61,26 @@ }, s.machine.Id(), instance.LXD) c.Assert(err, jc.ErrorIsNil) - s.w, err = s.machine.WatchForRebootEvent() - c.Assert(err, jc.ErrorIsNil) + s.w = s.machine.WatchForRebootEvent() s.wc = statetesting.NewNotifyWatcherC(c, s.State, s.w) s.wc.AssertOneChange() - s.wC1, err = s.c1.WatchForRebootEvent() - c.Assert(err, jc.ErrorIsNil) + s.wC1 = s.c1.WatchForRebootEvent() // Initial event on container 1. s.wcC1 = statetesting.NewNotifyWatcherC(c, s.State, s.wC1) s.wcC1.AssertOneChange() // Get reboot watcher on container 2 - s.wC2, err = s.c2.WatchForRebootEvent() - c.Assert(err, jc.ErrorIsNil) + s.wC2 = s.c2.WatchForRebootEvent() // Initial event on container 2. s.wcC2 = statetesting.NewNotifyWatcherC(c, s.State, s.wC2) s.wcC2.AssertOneChange() // Get reboot watcher on container 3 - s.wC3, err = s.c3.WatchForRebootEvent() - c.Assert(err, jc.ErrorIsNil) + s.wC3 = s.c3.WatchForRebootEvent() // Initial event on container 3. s.wcC3 = statetesting.NewNotifyWatcherC(c, s.State, s.wC3) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/relation.go juju-core-2.0~beta15/src/github.com/juju/juju/state/relation.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/relation.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/relation.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,7 +4,6 @@ package state import ( - stderrors "errors" "fmt" "sort" "strconv" @@ -135,8 +134,6 @@ return rel.st.run(buildTxn) } -var errAlreadyDying = stderrors.New("entity is already dying and cannot be destroyed") - // destroyOps returns the operations necessary to destroy the relation, and // whether those operations will lead to the relation's removal. These // operations may include changes to the relation's services; however, if diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/settings.go juju-core-2.0~beta15/src/github.com/juju/juju/state/settings.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/settings.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/settings.go 2016-08-16 08:56:25.000000000 +0000 @@ -226,7 +226,6 @@ if err != nil { return nil, err } - s.disk = copyMap(s.core, nil) return changes, nil } @@ -317,7 +316,7 @@ return s, nil } -var errSettingsExist = fmt.Errorf("cannot overwrite existing settings") +var errSettingsExist = errors.New("cannot overwrite existing settings") func createSettingsOp(collection, key string, values map[string]interface{}) txn.Op { newValues := copyMap(values, escapeReplacer.Replace) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/spaces.go juju-core-2.0~beta15/src/github.com/juju/juju/state/spaces.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/spaces.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/spaces.go 2016-08-16 08:56:25.000000000 +0000 @@ -20,8 +20,6 @@ } type spaceDoc struct { - DocID string `bson:"_id"` - ModelUUID string `bson:"model-uuid"` Life Life `bson:"life"` Name string `bson:"name"` IsPublic bool `bson:"is-public"` @@ -33,11 +31,6 @@ return s.doc.Life } -// ID returns the unique id for the space, for other entities to reference it -func (s *Space) ID() string { - return s.doc.DocID -} - // String implements fmt.Stringer. func (s *Space) String() string { return s.doc.Name @@ -48,6 +41,11 @@ return s.doc.Name } +// IsPublic returns whether the space is public or not. +func (s *Space) IsPublic() bool { + return s.doc.IsPublic +} + // ProviderId returns the provider id of the space. This will be the empty // string except on substrates that directly support spaces. func (s *Space) ProviderId() network.Id { @@ -82,10 +80,7 @@ return nil, errors.NewNotValid(nil, "invalid space name") } - spaceID := st.docID(name) spaceDoc := spaceDoc{ - DocID: spaceID, - ModelUUID: st.ModelUUID(), Life: Alive, Name: name, IsPublic: isPublic, @@ -95,7 +90,7 @@ ops := []txn.Op{{ C: spacesC, - Id: spaceID, + Id: name, Assert: txn.DocMissing, Insert: spaceDoc, }} @@ -110,7 +105,7 @@ // subnet in use is not permitted. ops = append(ops, txn.Op{ C: subnetsC, - Id: st.docID(subnetId), + Id: subnetId, Assert: txn.DocExists, Update: bson.D{{"$set", bson.D{{"space-name", name}}}}, }) @@ -185,7 +180,7 @@ ops := []txn.Op{{ C: spacesC, - Id: s.doc.DocID, + Id: s.doc.Name, Update: bson.D{{"$set", bson.D{{"life", Dead}}}}, Assert: isAliveDoc, }} @@ -209,7 +204,7 @@ ops := []txn.Op{{ C: spacesC, - Id: s.doc.DocID, + Id: s.doc.Name, Remove: true, Assert: isDeadDoc, }} @@ -232,7 +227,7 @@ defer closer() var doc spaceDoc - err := spaces.FindId(s.doc.DocID).One(&doc) + err := spaces.FindId(s.doc.Name).One(&doc) if err == mgo.ErrNotFound { return errors.NotFoundf("space %q", s) } else if err != nil { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/spaces_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/spaces_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/spaces_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/spaces_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,7 +6,6 @@ import ( "fmt" "net" - "strings" "github.com/juju/errors" jc "github.com/juju/testing/checkers" @@ -97,7 +96,6 @@ c.Assert(state.SpaceDoc(space).IsPublic, gc.Equals, args.IsPublic) c.Assert(space.Life(), gc.Equals, state.Alive) - c.Assert(strings.HasSuffix(space.ID(), args.Name), jc.IsTrue) c.Assert(space.String(), gc.Equals, args.Name) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/stateenvirons/config.go juju-core-2.0~beta15/src/github.com/juju/juju/state/stateenvirons/config.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/stateenvirons/config.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/stateenvirons/config.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,74 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package stateenvirons + +import ( + "github.com/juju/errors" + "gopkg.in/juju/names.v2" + + "github.com/juju/juju/cloud" + "github.com/juju/juju/environs" + "github.com/juju/juju/state" +) + +// EnvironConfigGetter implements environs.EnvironConfigGetter +// in terms of a *state.State. +type EnvironConfigGetter struct { + *state.State +} + +// CloudSpec implements environs.EnvironConfigGetter. +func (g EnvironConfigGetter) CloudSpec(tag names.ModelTag) (environs.CloudSpec, error) { + model, err := g.GetModel(tag) + if err != nil { + return environs.CloudSpec{}, errors.Trace(err) + } + cloudName := model.Cloud() + regionName := model.CloudRegion() + credentialName := model.CloudCredential() + modelOwner := model.Owner() + return CloudSpec(g.State, cloudName, regionName, credentialName, modelOwner) +} + +// CloudSpec returns an environs.CloudSpec from a *state.State, +// given the cloud, region and credential names. +func CloudSpec( + accessor state.CloudAccessor, + cloudName, regionName, credentialName string, + credentialOwner names.UserTag, +) (environs.CloudSpec, error) { + modelCloud, err := accessor.Cloud(cloudName) + if err != nil { + return environs.CloudSpec{}, errors.Trace(err) + } + + var credential *cloud.Credential + if credentialName != "" { + credentials, err := accessor.CloudCredentials(credentialOwner, cloudName) + if err != nil { + return environs.CloudSpec{}, errors.Trace(err) + } + var ok bool + credentialValue, ok := credentials[credentialName] + if !ok { + return environs.CloudSpec{}, errors.NotFoundf("credential %q", credentialName) + } + credential = &credentialValue + } + + return environs.MakeCloudSpec(modelCloud, cloudName, regionName, credential) +} + +// NewEnvironFunc defines the type of a function that, given a state.State, +// returns a new Environ. +type NewEnvironFunc func(*state.State) (environs.Environ, error) + +// GetNewEnvironFunc returns a NewEnvironFunc, that constructs Environs +// using the given environs.NewEnvironFunc. +func GetNewEnvironFunc(newEnviron environs.NewEnvironFunc) NewEnvironFunc { + return func(st *state.State) (environs.Environ, error) { + g := EnvironConfigGetter{st} + return environs.GetEnviron(g, newEnviron) + } +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/stateenvirons/config_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/stateenvirons/config_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/stateenvirons/config_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/stateenvirons/config_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,63 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package stateenvirons_test + +import ( + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cloud" + "github.com/juju/juju/environs" + "github.com/juju/juju/state/stateenvirons" + statetesting "github.com/juju/juju/state/testing" + "github.com/juju/juju/testing/factory" +) + +type environSuite struct { + statetesting.StateSuite +} + +var _ = gc.Suite(&environSuite{}) + +func (s *environSuite) TestGetNewEnvironFunc(c *gc.C) { + var calls int + var callArgs environs.OpenParams + newEnviron := func(args environs.OpenParams) (environs.Environ, error) { + calls++ + callArgs = args + return nil, nil + } + stateenvirons.GetNewEnvironFunc(newEnviron)(s.State) + c.Assert(calls, gc.Equals, 1) + + cfg, err := s.State.ModelConfig() + c.Assert(err, jc.ErrorIsNil) + c.Assert(callArgs.Config, jc.DeepEquals, cfg) +} + +func (s *environSuite) TestCloudSpec(c *gc.C) { + owner := s.Factory.MakeUser(c, nil).UserTag() + emptyCredential := cloud.NewEmptyCredential() + err := s.State.UpdateCloudCredentials(owner, "dummy", map[string]cloud.Credential{ + "empty-credential": emptyCredential, + }) + c.Assert(err, jc.ErrorIsNil) + + st := s.Factory.MakeModel(c, &factory.ModelParams{ + Name: "foo", + CloudName: "dummy", + CloudCredential: "empty-credential", + Owner: owner, + }) + defer st.Close() + + emptyCredential.Label = "empty-credential" + cloudSpec, err := stateenvirons.EnvironConfigGetter{st}.CloudSpec(st.ModelTag()) + c.Assert(err, jc.ErrorIsNil) + c.Assert(cloudSpec, jc.DeepEquals, environs.CloudSpec{ + Type: "dummy", + Name: "dummy", + Credential: &emptyCredential, + }) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/stateenvirons/doc.go juju-core-2.0~beta15/src/github.com/juju/juju/state/stateenvirons/doc.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/stateenvirons/doc.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/stateenvirons/doc.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,6 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +// Package stateenvirons provides types and functions that interface +// the state and environs packages. +package stateenvirons diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/stateenvirons/package_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/stateenvirons/package_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/stateenvirons/package_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/stateenvirons/package_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,22 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package stateenvirons_test + +import ( + "testing" + + "github.com/juju/utils/os" + + coretesting "github.com/juju/juju/testing" +) + +func TestPackage(t *testing.T) { + // At this stage, Juju only supports running the apiservers and database + // on Ubuntu. If we end up officially supporting CentOS, then we should + // make sure we run the tests there. + if os.HostOS() != os.Ubuntu { + t.Skipf("skipping tests on %v", os.HostOS()) + } + coretesting.MgoTestPackage(t) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/stateenvirons/policy.go juju-core-2.0~beta15/src/github.com/juju/juju/state/stateenvirons/policy.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/stateenvirons/policy.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/stateenvirons/policy.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,89 @@ +// Copyright 2014, 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package stateenvirons + +import ( + "github.com/juju/errors" + + "github.com/juju/juju/constraints" + "github.com/juju/juju/environs" + "github.com/juju/juju/environs/config" + "github.com/juju/juju/instance" + "github.com/juju/juju/state" + "github.com/juju/juju/storage" + "github.com/juju/juju/storage/provider" +) + +// environStatePolicy implements state.Policy in +// terms of environs.Environ and related types. +type environStatePolicy struct { + st *state.State + getEnviron func(*state.State) (environs.Environ, error) +} + +// GetNewPolicyFunc returns a state.NewPolicyFunc that will return +// a state.Policy implemented in terms of environs.Environ and +// related types. The provided function will be used to construct +// environs.Environs given a state.State. +func GetNewPolicyFunc(getEnviron func(*state.State) (environs.Environ, error)) state.NewPolicyFunc { + return func(st *state.State) state.Policy { + return environStatePolicy{st, getEnviron} + } +} + +// Prechecker implements state.Policy. +func (p environStatePolicy) Prechecker() (state.Prechecker, error) { + // Environ implements state.Prechecker. + return p.getEnviron(p.st) +} + +// ConfigValidator implements state.Policy. +func (p environStatePolicy) ConfigValidator() (config.Validator, error) { + model, err := p.st.Model() + if err != nil { + return nil, errors.Annotate(err, "getting model") + } + cloud, err := p.st.Cloud(model.Cloud()) + if err != nil { + return nil, errors.Annotate(err, "getting cloud") + } + // EnvironProvider implements state.ConfigValidator. + return environs.Provider(cloud.Type) +} + +// ConstraintsValidator implements state.Policy. +func (p environStatePolicy) ConstraintsValidator() (constraints.Validator, error) { + env, err := p.getEnviron(p.st) + if err != nil { + return nil, err + } + return env.ConstraintsValidator() +} + +// InstanceDistributor implements state.Policy. +func (p environStatePolicy) InstanceDistributor() (instance.Distributor, error) { + env, err := p.getEnviron(p.st) + if err != nil { + return nil, err + } + if p, ok := env.(instance.Distributor); ok { + return p, nil + } + return nil, errors.NotImplementedf("InstanceDistributor") +} + +// StorageProviderRegistry implements state.Policy. +func (p environStatePolicy) StorageProviderRegistry() (storage.ProviderRegistry, error) { + env, err := p.getEnviron(p.st) + if err != nil { + return nil, errors.Trace(err) + } + return NewStorageProviderRegistry(env), nil +} + +// NewStorageProviderRegistry returns a storage.ProviderRegistry that chains +// the provided Environ with the common storage providers. +func NewStorageProviderRegistry(env environs.Environ) storage.ProviderRegistry { + return storage.ChainedProviderRegistry{env, provider.CommonStorageProviders()} +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/state.go juju-core-2.0~beta15/src/github.com/juju/juju/state/state.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/state.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/state.go 2016-08-16 08:56:25.000000000 +0000 @@ -80,6 +80,7 @@ session *mgo.Session database Database policy Policy + newPolicy NewPolicyFunc // cloudName is the name of the cloud on which the model // represented by this state runs. @@ -146,14 +147,33 @@ // this method. Otherwise, there is a race condition in which collections // could be added to during or after the running of this method. func (st *State) RemoveAllModelDocs() error { - return st.removeAllModelDocs(bson.D{{"life", Dead}}) + err := st.removeAllModelDocs(bson.D{{"life", Dead}}) + if errors.Cause(err) == txn.ErrAborted { + return errors.New("can't remove model: model not dead") + } + return errors.Trace(err) } // RemoveImportingModelDocs removes all documents from multi-model collections // for the current model. This method asserts that the model's migration mode // is "importing". func (st *State) RemoveImportingModelDocs() error { - return st.removeAllModelDocs(bson.D{{"migration-mode", MigrationModeImporting}}) + err := st.removeAllModelDocs(bson.D{{"migration-mode", MigrationModeImporting}}) + if errors.Cause(err) == txn.ErrAborted { + return errors.New("can't remove model: model not being imported for migration") + } + return errors.Trace(err) +} + +// RemoveExportingModelDocs removes all documents from multi-model collections +// for the current model. This method asserts that the model's migration mode +// is "exporting". +func (st *State) RemoveExportingModelDocs() error { + err := st.removeAllModelDocs(bson.D{{"migration-mode", MigrationModeExporting}}) + if errors.Cause(err) == txn.ErrAborted { + return errors.New("can't remove model: model not being exported for migration") + } + return errors.Trace(err) } func (st *State) removeAllModelDocs(modelAssertion bson.D) error { @@ -181,27 +201,40 @@ ops = append(ops, decHostedModelCountOp()) } - // Add all per-model docs to the txn. + var rawCollections []string + // Remove each collection in its own transaction. for name, info := range st.database.Schema() { if info.global { continue } if info.rawAccess { - if err := st.removeAllInCollectionRaw(name); err != nil { - return errors.Trace(err) - } + rawCollections = append(rawCollections, name) continue } - // TODO(rog): 2016-05-06 lp:1579010 - // We can end up with an enormous transaction here, - // because we'll have one operation for each document in the - // whole model. - var err error - ops, err = st.appendRemoveAllInCollectionOps(ops, name) + + ops, err := st.removeAllInCollectionOps(name) + if err != nil { + return errors.Trace(err) + } + // Make sure we gate everything on the model assertion. + ops = append([]txn.Op{{ + C: modelsC, + Id: st.ModelUUID(), + Assert: modelAssertion, + }}, ops...) + err = st.runTransaction(ops) if err != nil { return errors.Trace(err) } } + // Now remove raw collections + for _, name := range rawCollections { + if err := st.removeAllInCollectionRaw(name); err != nil { + return errors.Trace(err) + } + } + + // Run the remaining ops to remove the model. return st.runTransaction(ops) } @@ -214,9 +247,9 @@ return errors.Trace(err) } -// appendRemoveAllInCollectionOps appends to ops operations to +// removeAllInCollectionOps appends to ops operations to // remove all the documents in the given named collection. -func (st *State) appendRemoveAllInCollectionOps(ops []txn.Op, name string) ([]txn.Op, error) { +func (st *State) removeAllInCollectionOps(name string) ([]txn.Op, error) { coll, closer := st.getCollection(name) defer closer() @@ -225,6 +258,7 @@ if err != nil { return nil, errors.Trace(err) } + var ops []txn.Op for _, id := range ids { ops = append(ops, txn.Op{ C: name, @@ -240,7 +274,7 @@ func (st *State) ForModel(model names.ModelTag) (*State, error) { session := st.session.Copy() newSt, err := newState( - model, session, st.mongoInfo, st.policy, + model, session, st.mongoInfo, st.newPolicy, ) if err != nil { return nil, errors.Trace(err) @@ -535,8 +569,8 @@ var errUpgradeInProgress = errors.New(params.CodeUpgradeInProgress) -// IsUpgradeInProgressError returns true if the error is cause by an -// upgrade in progress +// IsUpgradeInProgressError returns true if the error is caused by an +// in-progress upgrade. func IsUpgradeInProgressError(err error) bool { return errors.Cause(err) == errUpgradeInProgress } @@ -947,6 +981,14 @@ } } + // Ignore constraints that result from this call as + // these would be accumulation of model and application constraints + // but we only want application constraints to be persisted here. + _, err = st.resolveConstraints(args.Constraints) + if err != nil { + return nil, errors.Trace(err) + } + for _, placement := range args.Placement { data, err := st.parsePlacement(placement) if err != nil { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/state_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/state_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/state_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/state_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -37,9 +37,9 @@ "github.com/juju/juju/state/multiwatcher" statetesting "github.com/juju/juju/state/testing" "github.com/juju/juju/status" + "github.com/juju/juju/storage" "github.com/juju/juju/storage/poolmanager" "github.com/juju/juju/storage/provider" - "github.com/juju/juju/storage/provider/registry" "github.com/juju/juju/testing" "github.com/juju/juju/testing/factory" jujuversion "github.com/juju/juju/version" @@ -72,7 +72,7 @@ func (s *StateSuite) SetUpTest(c *gc.C) { s.ConnSuite.SetUpTest(c) - s.policy.GetConstraintsValidator = func(*config.Config, state.SupportedArchitecturesQuerier) (constraints.Validator, error) { + s.policy.GetConstraintsValidator = func() (constraints.Validator, error) { validator := constraints.NewValidator() validator.RegisterConflicts([]string{constraints.InstanceType}, []string{constraints.Mem}) validator.RegisterUnsupported([]string{constraints.CpuPower}) @@ -137,14 +137,14 @@ func (s *StateSuite) TestDialAgain(c *gc.C) { // Ensure idempotent operations on Dial are working fine. for i := 0; i < 2; i++ { - st, err := state.Open(s.modelTag, statetesting.NewMongoInfo(), mongotest.DialOpts(), state.Policy(nil)) + st, err := state.Open(s.modelTag, statetesting.NewMongoInfo(), mongotest.DialOpts(), nil) c.Assert(err, jc.ErrorIsNil) c.Assert(st.Close(), gc.IsNil) } } func (s *StateSuite) TestOpenAcceptsMissingModelTag(c *gc.C) { - st, err := state.Open(names.ModelTag{}, statetesting.NewMongoInfo(), mongotest.DialOpts(), state.Policy(nil)) + st, err := state.Open(names.ModelTag{}, statetesting.NewMongoInfo(), mongotest.DialOpts(), nil) c.Assert(err, jc.ErrorIsNil) c.Check(st.ModelTag(), gc.Equals, s.modelTag) @@ -154,7 +154,7 @@ func (s *StateSuite) TestOpenRequiresExtantModelTag(c *gc.C) { uuid := utils.MustNewUUID() tag := names.NewModelTag(uuid.String()) - st, err := state.Open(tag, statetesting.NewMongoInfo(), mongotest.DialOpts(), state.Policy(nil)) + st, err := state.Open(tag, statetesting.NewMongoInfo(), mongotest.DialOpts(), nil) if !c.Check(st, gc.IsNil) { c.Check(st.Close(), jc.ErrorIsNil) } @@ -163,7 +163,7 @@ } func (s *StateSuite) TestOpenSetsModelTag(c *gc.C) { - st, err := state.Open(s.modelTag, statetesting.NewMongoInfo(), mongotest.DialOpts(), state.Policy(nil)) + st, err := state.Open(s.modelTag, statetesting.NewMongoInfo(), mongotest.DialOpts(), nil) c.Assert(err, jc.ErrorIsNil) defer st.Close() @@ -176,7 +176,7 @@ func (s *StateSuite) TestNoModelDocs(c *gc.C) { c.Assert(s.State.EnsureModelRemoved(), gc.ErrorMatches, - fmt.Sprintf("found documents for model with uuid %s: 1 constraints doc, 2 leases doc, 1 modelSettingsSources doc, 1 modelusers doc, 1 permissions doc, 1 settings doc, 1 statuses doc", s.State.ModelUUID())) + fmt.Sprintf("found documents for model with uuid %s: 1 constraints doc, 2 leases doc, 1 modelusers doc, 2 permissions doc, 1 settings doc, 1 statuses doc", s.State.ModelUUID())) } func (s *StateSuite) TestMongoSession(c *gc.C) { @@ -274,7 +274,7 @@ func (s *MultiEnvStateSuite) SetUpTest(c *gc.C) { s.ConnSuite.SetUpTest(c) - s.policy.GetConstraintsValidator = func(*config.Config, state.SupportedArchitecturesQuerier) (constraints.Validator, error) { + s.policy.GetConstraintsValidator = func() (constraints.Validator, error) { validator := constraints.NewValidator() validator.RegisterConflicts([]string{constraints.InstanceType}, []string{constraints.Mem}) validator.RegisterUnsupported([]string{constraints.CpuPower}) @@ -447,8 +447,7 @@ f := factory.NewFactory(st) m := f.MakeMachine(c, &factory.MachineParams{}) c.Assert(m.Id(), gc.Equals, "0") - w, err := m.WatchForRebootEvent() - c.Assert(err, jc.ErrorIsNil) + w := m.WatchForRebootEvent() return w }, triggerEvent: func(st *state.State) { @@ -886,10 +885,9 @@ } func (s *StateSuite) TestAddMachineWithVolumes(c *gc.C) { - pm := poolmanager.New(state.NewStateSettings(s.State)) + pm := poolmanager.New(state.NewStateSettings(s.State), provider.CommonStorageProviders()) _, err := pm.Create("loop-pool", provider.LoopProviderType, map[string]interface{}{}) c.Assert(err, jc.ErrorIsNil) - registry.RegisterEnvironStorageProviders("someprovider", provider.LoopProviderType) oneJob := []state.MachineJob{state.JobHostUnits} cons := constraints.MustParse("mem=4G") @@ -1859,7 +1857,7 @@ logger := loggo.GetLogger("test") logger.SetLogLevel(loggo.DEBUG) tw := &loggo.TestWriter{} - c.Assert(loggo.RegisterWriter("constraints-tester", tw, loggo.DEBUG), gc.IsNil) + c.Assert(loggo.RegisterWriter("constraints-tester", tw), gc.IsNil) cons := constraints.MustParse("mem=4G cpu-power=10") err := s.State.SetModelConstraints(cons) @@ -2450,7 +2448,7 @@ defer st.Close() err := st.RemoveAllModelDocs() - c.Assert(err, gc.ErrorMatches, "transaction aborted") + c.Assert(err, gc.ErrorMatches, "can't remove model: model not dead") } func (s *StateSuite) TestRemoveImportingModelDocsFailsActive(c *gc.C) { @@ -2458,7 +2456,7 @@ defer st.Close() err := st.RemoveImportingModelDocs() - c.Assert(err, gc.ErrorMatches, "transaction aborted") + c.Assert(err, gc.ErrorMatches, "can't remove model: model not being imported for migration") } func (s *StateSuite) TestRemoveImportingModelDocsFailsExporting(c *gc.C) { @@ -2470,7 +2468,7 @@ c.Assert(err, jc.ErrorIsNil) err = st.RemoveImportingModelDocs() - c.Assert(err, gc.ErrorMatches, "transaction aborted") + c.Assert(err, gc.ErrorMatches, "can't remove model: model not being imported for migration") } func (s *StateSuite) TestRemoveImportingModelDocsImporting(c *gc.C) { @@ -2493,6 +2491,46 @@ c.Assert(state.HostedModelCount(c, s.State), gc.Equals, 0) } +func (s *StateSuite) TestRemoveExportingModelDocsFailsActive(c *gc.C) { + st := s.Factory.MakeModel(c, nil) + defer st.Close() + + err := st.RemoveExportingModelDocs() + c.Assert(err, gc.ErrorMatches, "can't remove model: model not being exported for migration") +} + +func (s *StateSuite) TestRemoveExportingModelDocsFailsImporting(c *gc.C) { + st := s.Factory.MakeModel(c, nil) + defer st.Close() + model, err := st.Model() + c.Assert(err, jc.ErrorIsNil) + err = model.SetMigrationMode(state.MigrationModeImporting) + c.Assert(err, jc.ErrorIsNil) + + err = st.RemoveExportingModelDocs() + c.Assert(err, gc.ErrorMatches, "can't remove model: model not being exported for migration") +} + +func (s *StateSuite) TestRemoveExportingModelDocsExporting(c *gc.C) { + st := s.Factory.MakeModel(c, nil) + defer st.Close() + userModelKey := s.insertFakeModelDocs(c, st) + c.Assert(state.HostedModelCount(c, s.State), gc.Equals, 1) + + model, err := st.Model() + c.Assert(err, jc.ErrorIsNil) + err = model.SetMigrationMode(state.MigrationModeExporting) + c.Assert(err, jc.ErrorIsNil) + + err = st.RemoveExportingModelDocs() + c.Assert(err, jc.ErrorIsNil) + + // test that we can not find the user:envName unique index + s.checkUserModelNameExists(c, checkUserModelNameArgs{st: st, id: userModelKey, exists: false}) + s.AssertModelDeleted(c, st) + c.Assert(state.HostedModelCount(c, s.State), gc.Equals, 0) +} + type attrs map[string]interface{} func (s *StateSuite) TestWatchForModelConfigChanges(c *gc.C) { @@ -2576,7 +2614,7 @@ } func tryOpenState(modelTag names.ModelTag, info *mongo.MongoInfo) error { - st, err := state.Open(modelTag, info, mongotest.DialOpts(), state.Policy(nil)) + st, err := state.Open(modelTag, info, mongotest.DialOpts(), nil) if err == nil { err = st.Close() } @@ -2605,7 +2643,7 @@ info.Addrs = []string{"0.1.2.3:1234"} st, err := state.Open(testing.ModelTag, info, mongo.DialOpts{ Timeout: 1 * time.Millisecond, - }, state.Policy(nil)) + }, nil) if err == nil { st.Close() } @@ -2621,7 +2659,7 @@ t0 := time.Now() st, err := state.Open(testing.ModelTag, info, mongo.DialOpts{ Timeout: 1 * time.Millisecond, - }, state.Policy(nil)) + }, nil) if err == nil { st.Close() } @@ -3290,7 +3328,7 @@ // interact with the closed state, causing it to return an // unexpected error (often "Closed explictly"). func testWatcherDiesWhenStateCloses(c *gc.C, modelTag names.ModelTag, startWatcher func(c *gc.C, st *state.State) waiter) { - st, err := state.Open(modelTag, statetesting.NewMongoInfo(), mongotest.DialOpts(), state.Policy(nil)) + st, err := state.Open(modelTag, statetesting.NewMongoInfo(), mongotest.DialOpts(), nil) c.Assert(err, jc.ErrorIsNil) watcher := startWatcher(c, st) err = st.Close() @@ -3328,7 +3366,7 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(info, jc.DeepEquals, expected) - st, err := state.Open(s.modelTag, statetesting.NewMongoInfo(), mongotest.DialOpts(), state.Policy(nil)) + st, err := state.Open(s.modelTag, statetesting.NewMongoInfo(), mongotest.DialOpts(), nil) c.Assert(err, jc.ErrorIsNil) defer st.Close() @@ -4094,9 +4132,10 @@ st, err := state.Initialize(state.InitializeParams{ ControllerConfig: controllerCfg, ControllerModelArgs: state.ModelArgs{ - CloudName: "dummy", - Owner: owner, - Config: cfg, + CloudName: "dummy", + Owner: owner, + Config: cfg, + StorageProviderRegistry: storage.StaticProviderRegistry{}, }, CloudName: "dummy", Cloud: cloud.Cloud{ diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/storage.go juju-core-2.0~beta15/src/github.com/juju/juju/state/storage.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/storage.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/storage.go 2016-08-16 08:56:25.000000000 +0000 @@ -20,7 +20,6 @@ "github.com/juju/juju/storage" "github.com/juju/juju/storage/poolmanager" "github.com/juju/juju/storage/provider" - "github.com/juju/juju/storage/provider/registry" ) // StorageInstance represents the state of a unit or application-wide storage @@ -911,25 +910,15 @@ } } - // Ensure the pool type is supported by the model. - conf, err := st.ModelConfig() - if err != nil { - return errors.Trace(err) - } - envType := conf.Type() - if !registry.IsProviderSupported(envType, providerType) { - return errors.Errorf( - "pool %q uses storage provider %q which is not supported for models of type %q", - poolName, - providerType, - envType, - ) - } return nil } func poolStorageProvider(st *State, poolName string) (storage.ProviderType, storage.Provider, error) { - poolManager := poolmanager.New(NewStateSettings(st)) + registry, err := st.storageProviderRegistry() + if err != nil { + return "", nil, errors.Annotate(err, "getting storage provider registry") + } + poolManager := poolmanager.New(NewStateSettings(st), registry) pool, err := poolManager.Get(poolName) if errors.IsNotFound(err) { // If there's no pool called poolName, maybe a provider type diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/storage_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/storage_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/storage_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/storage_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,6 +5,7 @@ import ( "fmt" + "sort" "github.com/juju/errors" jc "github.com/juju/testing/checkers" @@ -14,13 +15,14 @@ "gopkg.in/juju/names.v2" "gopkg.in/mgo.v2" + "github.com/juju/juju/provider/dummy" "github.com/juju/juju/state" "github.com/juju/juju/state/testing" "github.com/juju/juju/storage" "github.com/juju/juju/storage/poolmanager" "github.com/juju/juju/storage/provider" - "github.com/juju/juju/storage/provider/dummy" - "github.com/juju/juju/storage/provider/registry" + dummystorage "github.com/juju/juju/storage/provider/dummy" + "github.com/juju/juju/testing/factory" ) type StorageStateSuite struct { @@ -33,47 +35,13 @@ ConnSuite } -func (s *StorageStateSuiteBase) SetUpSuite(c *gc.C) { - s.ConnSuite.SetUpSuite(c) - - registry.RegisterProvider("environscoped", &dummy.StorageProvider{ - StorageScope: storage.ScopeEnviron, - IsDynamic: true, - }) - registry.RegisterProvider("machinescoped", &dummy.StorageProvider{ - StorageScope: storage.ScopeMachine, - IsDynamic: true, - }) - registry.RegisterProvider("environscoped-block", &dummy.StorageProvider{ - StorageScope: storage.ScopeEnviron, - SupportsFunc: func(k storage.StorageKind) bool { - return k == storage.StorageKindBlock - }, - IsDynamic: true, - }) - registry.RegisterProvider("static", &dummy.StorageProvider{ - IsDynamic: false, - }) - registry.RegisterEnvironStorageProviders( - "someprovider", "environscoped", "machinescoped", - "environscoped-block", "static", - ) - s.AddCleanup(func(c *gc.C) { - registry.RegisterProvider("environscoped", nil) - registry.RegisterProvider("machinescoped", nil) - registry.RegisterProvider("environscoped-block", nil) - registry.RegisterProvider("static", nil) - }) -} - func (s *StorageStateSuiteBase) SetUpTest(c *gc.C) { s.ConnSuite.SetUpTest(c) // Create a default pool for block devices. - pm := poolmanager.New(state.NewStateSettings(s.State)) + pm := poolmanager.New(state.NewStateSettings(s.State), dummy.StorageProviders()) _, err := pm.Create("loop-pool", provider.LoopProviderType, map[string]interface{}{}) c.Assert(err, jc.ErrorIsNil) - registry.RegisterEnvironStorageProviders("someprovider", provider.LoopProviderType) // Create a pool that creates persistent block devices. _, err = pm.Create("persistent-block", "environscoped-block", map[string]interface{}{ @@ -918,6 +886,58 @@ } } +func mustStorageConfig(name string, provider storage.ProviderType, attrs map[string]interface{}) *storage.Config { + cfg, err := storage.NewConfig(name, provider, attrs) + if err != nil { + panic(err) + } + return cfg +} + +var testingStorageProviders = storage.StaticProviderRegistry{ + map[storage.ProviderType]storage.Provider{ + "dummy": &dummystorage.StorageProvider{ + DefaultPools_: []*storage.Config{radiancePool}, + }, + "lancashire": &dummystorage.StorageProvider{ + DefaultPools_: []*storage.Config{blackPool}, + }, + }, +} + +var radiancePool = mustStorageConfig("radiance", "dummy", map[string]interface{}{"k": "v"}) +var blackPool = mustStorageConfig("black", "lancashire", map[string]interface{}{}) + +func (s *StorageStateSuite) TestNewModelDefaultPools(c *gc.C) { + st := s.Factory.MakeModel(c, &factory.ModelParams{ + StorageProviderRegistry: testingStorageProviders, + }) + s.AddCleanup(func(*gc.C) { st.Close() }) + + // When a model is created, it is populated with the default + // pools of each storage provider supported by the model's + // cloud provider. + pm := poolmanager.New(state.NewStateSettings(st), testingStorageProviders) + listed, err := pm.List() + c.Assert(err, jc.ErrorIsNil) + sort.Sort(byStorageConfigName(listed)) + c.Assert(listed, jc.DeepEquals, []*storage.Config{blackPool, radiancePool}) +} + +type byStorageConfigName []*storage.Config + +func (c byStorageConfigName) Len() int { + return len(c) +} + +func (c byStorageConfigName) Less(a, b int) bool { + return c[a].Name() < c[b].Name() +} + +func (c byStorageConfigName) Swap(a, b int) { + c[a], c[b] = c[b], c[a] +} + // TODO(axw) the following require shared storage support to test: // - StorageAttachments can't be added to Dying StorageInstance // - StorageInstance without attachments is removed by Destroy diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/subnets.go juju-core-2.0~beta15/src/github.com/juju/juju/state/subnets.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/subnets.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/subnets.go 2016-08-16 08:56:25.000000000 +0000 @@ -48,8 +48,10 @@ CIDR string `bson:"cidr"` VLANTag int `bson:"vlantag,omitempty"` AvailabilityZone string `bson:"availabilityzone,omitempty"` - IsPublic bool `bson:"is-public,omitempty"` - SpaceName string `bson:"space-name,omitempty"` + // TODO: add IsPublic to SubnetArgs, add an IsPublic method and add + // IsPublic to migration import/export. + IsPublic bool `bson:"is-public,omitempty"` + SpaceName string `bson:"space-name,omitempty"` } // Life returns whether the subnet is Alive, Dying or Dead. @@ -57,7 +59,7 @@ return s.doc.Life } -// ID returns the unique id for the subnet, for other entities to reference it +// ID returns the unique id for the subnet, for other entities to reference it. func (s *Subnet) ID() string { return s.doc.DocID } @@ -192,36 +194,13 @@ func (st *State) AddSubnet(args SubnetInfo) (subnet *Subnet, err error) { defer errors.DeferredAnnotatef(&err, "adding subnet %q", args.CIDR) - subnetID := st.docID(args.CIDR) - subDoc := subnetDoc{ - DocID: subnetID, - ModelUUID: st.ModelUUID(), - Life: Alive, - CIDR: args.CIDR, - VLANTag: args.VLANTag, - ProviderId: string(args.ProviderId), - AvailabilityZone: args.AvailabilityZone, - SpaceName: args.SpaceName, - } - subnet = &Subnet{doc: subDoc, st: st} - err = subnet.Validate() + subnet, err = st.newSubnetFromArgs(args) if err != nil { - return nil, err + return nil, errors.Trace(err) } - + ops := st.addSubnetOps(args) + ops = append(ops, assertModelActiveOp(st.ModelUUID())) buildTxn := func(attempt int) ([]txn.Op, error) { - ops := []txn.Op{ - assertModelActiveOp(st.ModelUUID()), - { - C: subnetsC, - Id: subnetID, - Assert: txn.DocMissing, - Insert: subDoc, - }, - } - if args.ProviderId != "" { - ops = append(ops, st.networkEntityGlobalKeyOp("subnet", args.ProviderId)) - } if attempt != 0 { if err := checkModelActive(st); err != nil { @@ -246,6 +225,52 @@ return subnet, nil } +func (st *State) newSubnetFromArgs(args SubnetInfo) (*Subnet, error) { + subnetID := st.docID(args.CIDR) + subDoc := subnetDoc{ + DocID: subnetID, + ModelUUID: st.ModelUUID(), + Life: Alive, + CIDR: args.CIDR, + VLANTag: args.VLANTag, + ProviderId: string(args.ProviderId), + AvailabilityZone: args.AvailabilityZone, + SpaceName: args.SpaceName, + } + subnet := &Subnet{doc: subDoc, st: st} + err := subnet.Validate() + if err != nil { + return nil, errors.Trace(err) + } + return subnet, nil +} + +func (st *State) addSubnetOps(args SubnetInfo) []txn.Op { + subnetID := st.docID(args.CIDR) + subDoc := subnetDoc{ + DocID: subnetID, + ModelUUID: st.ModelUUID(), + Life: Alive, + CIDR: args.CIDR, + VLANTag: args.VLANTag, + ProviderId: string(args.ProviderId), + AvailabilityZone: args.AvailabilityZone, + SpaceName: args.SpaceName, + } + ops := []txn.Op{ + { + C: subnetsC, + Id: subnetID, + Assert: txn.DocMissing, + Insert: subDoc, + }, + } + if args.ProviderId != "" { + ops = append(ops, st.networkEntityGlobalKeyOp("subnet", args.ProviderId)) + } + return ops +} + // Subnet returns the subnet specified by the cidr. func (st *State) Subnet(cidr string) (*Subnet, error) { subnets, closer := st.getCollection(subnetsC) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/testing/conn.go juju-core-2.0~beta15/src/github.com/juju/juju/state/testing/conn.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/testing/conn.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/testing/conn.go 2016-08-16 08:56:25.000000000 +0000 @@ -14,13 +14,16 @@ "github.com/juju/juju/mongo" "github.com/juju/juju/mongo/mongotest" "github.com/juju/juju/state" + "github.com/juju/juju/storage" + "github.com/juju/juju/storage/provider" + dummystorage "github.com/juju/juju/storage/provider/dummy" "github.com/juju/juju/testing" ) // Initialize initializes the state and returns it. If state was not // already initialized, and cfg is nil, the minimal default model // configuration will be used. -func Initialize(c *gc.C, owner names.UserTag, cfg *config.Config, controllerInheritedConfig map[string]interface{}, policy state.Policy) *state.State { +func Initialize(c *gc.C, owner names.UserTag, cfg *config.Config, controllerInheritedConfig map[string]interface{}, newPolicy state.NewPolicyFunc) *state.State { if cfg == nil { cfg = testing.ModelConfig(c) } @@ -35,6 +38,7 @@ CloudName: "dummy", Config: cfg, Owner: owner, + StorageProviderRegistry: StorageProviders(), }, ControllerInheritedConfig: controllerInheritedConfig, CloudName: "dummy", @@ -44,12 +48,38 @@ }, MongoInfo: mgoInfo, MongoDialOpts: dialOpts, - Policy: policy, + NewPolicy: newPolicy, }) c.Assert(err, jc.ErrorIsNil) return st } +func StorageProviders() storage.ProviderRegistry { + return storage.ChainedProviderRegistry{ + storage.StaticProviderRegistry{ + map[storage.ProviderType]storage.Provider{ + "static": &dummystorage.StorageProvider{IsDynamic: false}, + "environscoped": &dummystorage.StorageProvider{ + StorageScope: storage.ScopeEnviron, + IsDynamic: true, + }, + "environscoped-block": &dummystorage.StorageProvider{ + StorageScope: storage.ScopeEnviron, + IsDynamic: true, + SupportsFunc: func(k storage.StorageKind) bool { + return k == storage.StorageKindBlock + }, + }, + "machinescoped": &dummystorage.StorageProvider{ + StorageScope: storage.ScopeMachine, + IsDynamic: true, + }, + }, + }, + provider.CommonStorageProviders(), + } +} + // NewMongoInfo returns information suitable for // connecting to the testing controller's mongo database. func NewMongoInfo() *mongo.MongoInfo { @@ -66,6 +96,6 @@ func NewState(c *gc.C) *state.State { owner := names.NewLocalUserTag("test-admin") cfg := testing.ModelConfig(c) - policy := MockPolicy{} - return Initialize(c, owner, cfg, nil, &policy) + newPolicy := func(*state.State) state.Policy { return &MockPolicy{} } + return Initialize(c, owner, cfg, nil, newPolicy) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/testing/policy.go juju-core-2.0~beta15/src/github.com/juju/juju/state/testing/policy.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/testing/policy.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/testing/policy.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,48 +8,50 @@ "github.com/juju/juju/constraints" "github.com/juju/juju/environs/config" + "github.com/juju/juju/instance" "github.com/juju/juju/state" + "github.com/juju/juju/storage" ) type MockPolicy struct { - GetPrechecker func(*config.Config) (state.Prechecker, error) - GetConfigValidator func(string) (state.ConfigValidator, error) - GetEnvironCapability func(*config.Config) (state.EnvironCapability, error) - GetConstraintsValidator func(*config.Config, state.SupportedArchitecturesQuerier) (constraints.Validator, error) - GetInstanceDistributor func(*config.Config) (state.InstanceDistributor, error) + GetPrechecker func() (state.Prechecker, error) + GetConfigValidator func() (config.Validator, error) + GetConstraintsValidator func() (constraints.Validator, error) + GetInstanceDistributor func() (instance.Distributor, error) + GetStorageProviderRegistry func() (storage.ProviderRegistry, error) } -func (p *MockPolicy) Prechecker(cfg *config.Config) (state.Prechecker, error) { +func (p *MockPolicy) Prechecker() (state.Prechecker, error) { if p.GetPrechecker != nil { - return p.GetPrechecker(cfg) + return p.GetPrechecker() } return nil, errors.NotImplementedf("Prechecker") } -func (p *MockPolicy) ConfigValidator(providerType string) (state.ConfigValidator, error) { +func (p *MockPolicy) ConfigValidator() (config.Validator, error) { if p.GetConfigValidator != nil { - return p.GetConfigValidator(providerType) + return p.GetConfigValidator() } return nil, errors.NotImplementedf("ConfigValidator") } -func (p *MockPolicy) EnvironCapability(cfg *config.Config) (state.EnvironCapability, error) { - if p.GetEnvironCapability != nil { - return p.GetEnvironCapability(cfg) +func (p *MockPolicy) ConstraintsValidator() (constraints.Validator, error) { + if p.GetConstraintsValidator != nil { + return p.GetConstraintsValidator() } - return nil, errors.NotImplementedf("EnvironCapability") + return nil, errors.NotImplementedf("ConstraintsValidator") } -func (p *MockPolicy) ConstraintsValidator(cfg *config.Config, querier state.SupportedArchitecturesQuerier) (constraints.Validator, error) { - if p.GetConstraintsValidator != nil { - return p.GetConstraintsValidator(cfg, querier) +func (p *MockPolicy) InstanceDistributor() (instance.Distributor, error) { + if p.GetInstanceDistributor != nil { + return p.GetInstanceDistributor() } - return nil, errors.NewNotImplemented(nil, "ConstraintsValidator") + return nil, errors.NotImplementedf("InstanceDistributor") } -func (p *MockPolicy) InstanceDistributor(cfg *config.Config) (state.InstanceDistributor, error) { - if p.GetInstanceDistributor != nil { - return p.GetInstanceDistributor(cfg) +func (p *MockPolicy) StorageProviderRegistry() (storage.ProviderRegistry, error) { + if p.GetStorageProviderRegistry != nil { + return p.GetStorageProviderRegistry() } - return nil, errors.NewNotImplemented(nil, "InstanceDistributor") + return nil, errors.NotImplementedf("StorageProviderRegistry") } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/testing/suite.go juju-core-2.0~beta15/src/github.com/juju/juju/state/testing/suite.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/testing/suite.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/testing/suite.go 2016-08-16 08:56:25.000000000 +0000 @@ -21,7 +21,7 @@ type StateSuite struct { jujutesting.MgoSuite testing.BaseSuite - Policy state.Policy + NewPolicy state.NewPolicyFunc State *state.State Owner names.UserTag Factory *factory.Factory @@ -44,7 +44,7 @@ s.BaseSuite.SetUpTest(c) s.Owner = names.NewLocalUserTag("test-admin") - s.State = Initialize(c, s.Owner, s.InitialConfig, s.ControllerInheritedConfig, s.Policy) + s.State = Initialize(c, s.Owner, s.InitialConfig, s.ControllerInheritedConfig, s.NewPolicy) s.AddCleanup(func(*gc.C) { s.State.Close() }) s.Factory = factory.NewFactory(s.State) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/undertaker.go juju-core-2.0~beta15/src/github.com/juju/juju/state/undertaker.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/undertaker.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/undertaker.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,10 +4,8 @@ package state import ( - "gopkg.in/mgo.v2/bson" - "github.com/juju/errors" - + "gopkg.in/mgo.v2/bson" "gopkg.in/mgo.v2/txn" ) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/unitagent.go juju-core-2.0~beta15/src/github.com/juju/juju/state/unitagent.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/unitagent.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/unitagent.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,8 +5,9 @@ import ( "github.com/juju/errors" - "github.com/juju/juju/status" "gopkg.in/juju/names.v2" + + "github.com/juju/juju/status" ) // UnitAgent represents the state of a service's unit agent. diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/unit.go juju-core-2.0~beta15/src/github.com/juju/juju/state/unit.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/unit.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/unit.go 2016-08-16 08:56:25.000000000 +0000 @@ -349,9 +349,6 @@ func (u *Unit) eraseHistory() error { history, closer := u.st.getCollection(statusesHistoryC) defer closer() - // XXX(fwereade): 2015-06-19 this is anything but safe: we must not mix - // txn and non-txn operations in the same collection without clear and - // detailed reasoning for so doing. historyW := history.Writeable() if _, err := historyW.RemoveAll(bson.D{{"statusid", u.globalKey()}}); err != nil { @@ -531,8 +528,6 @@ return ops, nil } -var errAlreadyRemoved = errors.New("entity has already been removed") - // removeOps returns the operations necessary to remove the unit, assuming // the supplied asserts apply to the unit document. func (u *Unit) removeOps(asserts bson.D) ([]txn.Op, error) { @@ -1349,9 +1344,6 @@ if !canHost { return fmt.Errorf("machine %q cannot host units", m) } - if err := m.st.supportsUnitPlacement(); err != nil { - return errors.Trace(err) - } if err := validateDynamicMachineStoragePools(m, storagePools); err != nil { return errors.Trace(err) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/upgrades.go juju-core-2.0~beta15/src/github.com/juju/juju/state/upgrades.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/upgrades.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/upgrades.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,11 +7,12 @@ "time" "github.com/juju/errors" - "github.com/juju/juju/status" "github.com/juju/loggo" "gopkg.in/juju/names.v2" "gopkg.in/mgo.v2/bson" "gopkg.in/mgo.v2/txn" + + "github.com/juju/juju/status" ) var upgradesLogger = loggo.GetLogger("juju.state.upgrade") diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/upgrades_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/upgrades_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/upgrades_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/upgrades_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -13,10 +13,9 @@ "gopkg.in/mgo.v2/bson" "gopkg.in/mgo.v2/txn" + "github.com/juju/juju/core/description" "github.com/juju/juju/network" "github.com/juju/juju/status" - "github.com/juju/juju/storage/provider" - "github.com/juju/juju/storage/provider/registry" ) type upgradesSuite struct { @@ -228,7 +227,6 @@ } func setupMachineBoundStorageTests(c *gc.C, st *State) (*Machine, Volume, Filesystem, func() error) { - registry.RegisterEnvironStorageProviders("someprovider", provider.LoopProviderType, provider.RootfsProviderType) // Make an unprovisioned machine with storage for tests to use. // TODO(axw) extend testing/factory to allow creating unprovisioned // machines. @@ -460,10 +458,11 @@ stateOwner, err := s.state.AddUser("bob", "notused", "notused", "bob") c.Assert(err, jc.ErrorIsNil) ownerTag := stateOwner.UserTag() - _, err = s.state.AddModelUser(ModelUserSpec{ + _, err = s.state.AddModelUser(UserAccessSpec{ User: ownerTag, CreatedBy: ownerTag, DisplayName: "", + Access: description.ReadAccess, }) c.Assert(err, jc.ErrorIsNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/useraccess.go juju-core-2.0~beta15/src/github.com/juju/juju/state/useraccess.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/useraccess.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/useraccess.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,198 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package state + +import ( + "fmt" + "strings" + "time" + + "gopkg.in/juju/names.v2" + "gopkg.in/mgo.v2/txn" + + "github.com/juju/errors" + "github.com/juju/juju/core/description" +) + +type userAccessDoc struct { + ID string `bson:"_id"` + ObjectUUID string `bson:"object-uuid"` + UserName string `bson:"user"` + DisplayName string `bson:"displayname"` + CreatedBy string `bson:"createdby"` + DateCreated time.Time `bson:"datecreated"` +} + +// UserAccessSpec defines the attributes that can be set when adding a new +// user access. +type UserAccessSpec struct { + User names.UserTag + CreatedBy names.UserTag + DisplayName string + Access description.Access +} + +// AddModelUser adds a new user for the current model to the database. +func (st *State) AddModelUser(spec UserAccessSpec) (description.UserAccess, error) { + if err := description.ValidateModelAccess(spec.Access); err != nil { + return description.UserAccess{}, errors.Annotate(err, "adding model user") + } + return st.addUserAccess(spec, modelGlobalKey) +} + +// AddControllerUser adds a new user for the curent controller to the database. +func (st *State) AddControllerUser(spec UserAccessSpec) (description.UserAccess, error) { + if err := description.ValidateControllerAccess(spec.Access); err != nil { + return description.UserAccess{}, errors.Annotate(err, "adding controller user") + } + return st.addUserAccess(spec, controllerGlobalKey) +} + +func (st *State) addUserAccess(spec UserAccessSpec, targetGlobalKey string) (description.UserAccess, error) { + // Ensure local user exists in state before adding them as an model user. + if spec.User.IsLocal() { + localUser, err := st.User(spec.User) + if err != nil { + return description.UserAccess{}, errors.Annotate(err, fmt.Sprintf("user %q does not exist locally", spec.User.Name())) + } + if spec.DisplayName == "" { + spec.DisplayName = localUser.DisplayName() + } + } + + // Ensure local createdBy user exists. + if spec.CreatedBy.IsLocal() { + if _, err := st.User(spec.CreatedBy); err != nil { + return description.UserAccess{}, errors.Annotatef(err, "createdBy user %q does not exist locally", spec.CreatedBy.Name()) + } + } + var ( + ops []txn.Op + err error + targetTag names.Tag + ) + switch targetGlobalKey { + case modelGlobalKey: + ops = createModelUserOps( + st.ModelUUID(), + spec.User, + spec.CreatedBy, + spec.DisplayName, + nowToTheSecond(), + spec.Access) + targetTag = st.ModelTag() + case controllerGlobalKey: + ops = createControllerUserOps( + st.ControllerUUID(), + spec.User, + spec.CreatedBy, + spec.DisplayName, + nowToTheSecond(), + spec.Access) + targetTag = names.NewControllerTag(st.ControllerUUID()) + default: + return description.UserAccess{}, errors.NotSupportedf("user access global key %q", targetGlobalKey) + } + err = st.runTransaction(ops) + if err == txn.ErrAborted { + err = errors.AlreadyExistsf("user access %q", spec.User.Canonical()) + } + if err != nil { + return description.UserAccess{}, errors.Trace(err) + } + return st.UserAccess(spec.User, targetTag) +} + +// userAccessID returns the document id of the user access. +func userAccessID(user names.UserTag) string { + username := user.Canonical() + return strings.ToLower(username) +} + +// NewModelUserAccess returns a new description.UserAccess for the given userDoc and +// current Model. +func NewModelUserAccess(st *State, userDoc userAccessDoc) (description.UserAccess, error) { + perm, err := st.userPermission(modelGlobalKey, userGlobalKey(strings.ToLower(userDoc.UserName))) + if err != nil { + return description.UserAccess{}, errors.Annotate(err, "obtaining model permission") + } + return newUserAccess(perm, userDoc, names.NewModelTag(userDoc.ObjectUUID)), nil +} + +// NewControllerUserAccess returns a new description.UserAccess for the given userDoc and +// current Controller. +func NewControllerUserAccess(st *State, userDoc userAccessDoc) (description.UserAccess, error) { + perm, err := st.userPermission(controllerGlobalKey, userGlobalKey(strings.ToLower(userDoc.UserName))) + if err != nil { + return description.UserAccess{}, errors.Annotate(err, "obtaining controller permission") + } + return newUserAccess(perm, userDoc, names.NewControllerTag(userDoc.ObjectUUID)), nil +} + +func newUserAccess(perm *permission, userDoc userAccessDoc, object names.Tag) description.UserAccess { + return description.UserAccess{ + UserID: userDoc.ID, + UserTag: names.NewUserTag(userDoc.UserName), + Object: object, + Access: perm.access(), + CreatedBy: names.NewUserTag(userDoc.CreatedBy), + DateCreated: userDoc.DateCreated.UTC(), + DisplayName: userDoc.DisplayName, + UserName: userDoc.UserName, + } +} + +// UserAccess returns a new description.UserAccess for the passed subject and target. +func (st *State) UserAccess(subject names.UserTag, target names.Tag) (description.UserAccess, error) { + var ( + userDoc userAccessDoc + err error + ) + switch target.Kind() { + case names.ModelTagKind: + userDoc, err = st.modelUser(subject) + if err == nil { + return NewModelUserAccess(st, userDoc) + } + case names.ControllerTagKind: + userDoc, err = st.controllerUser(subject) + if err == nil { + return NewControllerUserAccess(st, userDoc) + } + default: + return description.UserAccess{}, errors.NotValidf("%q as a target", target.Kind()) + } + return description.UserAccess{}, errors.Trace(err) +} + +// SetUserAccess sets level on to . +func (st *State) SetUserAccess(subject names.UserTag, target names.Tag, access description.Access) (description.UserAccess, error) { + err := access.Validate() + if err != nil { + return description.UserAccess{}, errors.Trace(err) + } + switch target.Kind() { + case names.ModelTagKind: + err = st.setModelAccess(access, userGlobalKey(userAccessID(subject))) + case names.ControllerTagKind: + err = st.setControllerAccess(access, userGlobalKey(userAccessID(subject))) + default: + return description.UserAccess{}, errors.NotValidf("%q as a target", target.Kind()) + } + if err != nil { + return description.UserAccess{}, errors.Trace(err) + } + return st.UserAccess(subject, target) +} + +// RemoveUserAccess removes access for subject to the passed tag. +func (st *State) RemoveUserAccess(subject names.UserTag, target names.Tag) error { + switch target.Kind() { + case names.ModelTagKind: + return errors.Trace(st.removeModelUser(subject)) + case names.ControllerTagKind: + return errors.Trace(st.removeControllerUser(subject)) + } + return errors.NotValidf("%q as a target", target.Kind()) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/user.go juju-core-2.0~beta15/src/github.com/juju/juju/state/user.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/user.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/user.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,7 +4,7 @@ // NOTE: the users that are being stored in the database here are only // the local users, like "admin" or "bob" (@local). In the world // where we have external user providers hooked up, there are no records -// in the databse for users that are authenticated elsewhere. +// in the database for users that are authenticated elsewhere. package state @@ -23,6 +23,12 @@ "gopkg.in/mgo.v2/txn" ) +const userGlobalKeyPrefix = "us" + +func userGlobalKey(userID string) string { + return fmt.Sprintf("%s#%s", userGlobalKeyPrefix, userID) +} + func (st *State) checkUserExists(name string) (bool, error) { users, closer := st.getCollection(usersC) defer closer() @@ -64,6 +70,7 @@ } nameToLower := strings.ToLower(name) + dateCreated := nowToTheSecond() user := &User{ st: st, doc: userDoc{ @@ -72,7 +79,7 @@ DisplayName: displayName, SecretKey: secretKey, CreatedBy: creator, - DateCreated: nowToTheSecond(), + DateCreated: dateCreated, }, } @@ -91,6 +98,14 @@ Assert: txn.DocMissing, Insert: &user.doc, }} + controllerUserOps := createControllerUserOps(st.ControllerUUID(), + names.NewUserTag(name), + names.NewUserTag(creator), + displayName, + dateCreated, + defaultControllerPermission) + ops = append(ops, controllerUserOps...) + err := st.runTransaction(ops) if err == txn.ErrAborted { err = errors.AlreadyExistsf("user") @@ -101,8 +116,41 @@ return user, nil } -func createInitialUserOp(st *State, user names.UserTag, password, salt string) txn.Op { +// RemoveUser marks the user as deleted. This obviates the ability of a user +// to function, but keeps the userDoc retaining provenance, i.e. auditing. +func (st *State) RemoveUser(tag names.UserTag) error { + name := strings.ToLower(tag.Name()) + + u, err := st.User(tag) + if err != nil { + return errors.Trace(err) + } + if u.IsDeleted() { + return nil + } + + buildTxn := func(attempt int) ([]txn.Op, error) { + if attempt > 0 { + // If it is not our first attempt, refresh the user. + if err := u.Refresh(); err != nil { + return nil, errors.Trace(err) + } + } + ops := []txn.Op{{ + Id: name, + C: usersC, + Assert: txn.DocExists, + Update: bson.M{"$set": bson.M{"deleted": true}}, + }} + return ops, nil + } + + return st.run(buildTxn) +} + +func createInitialUserOps(controllerUUID string, user names.UserTag, password, salt string) []txn.Op { nameToLower := strings.ToLower(user.Name()) + dateCreated := nowToTheSecond() doc := userDoc{ DocID: nameToLower, Name: user.Name(), @@ -110,14 +158,24 @@ PasswordHash: utils.UserPasswordHash(password, salt), PasswordSalt: salt, CreatedBy: user.Name(), - DateCreated: nowToTheSecond(), + DateCreated: dateCreated, } - return txn.Op{ + ops := []txn.Op{{ C: usersC, Id: nameToLower, Assert: txn.DocMissing, Insert: &doc, - } + }} + controllerUserOps := createControllerUserOps(controllerUUID, + names.NewUserTag(user.Name()), + names.NewUserTag(user.Name()), + user.Name(), + dateCreated, + defaultControllerPermission) + + ops = append(ops, controllerUserOps...) + return ops + } // getUser fetches information about the user with the @@ -146,10 +204,19 @@ if err := st.getUser(tag.Name(), &user.doc); err != nil { return nil, errors.Trace(err) } + if user.doc.Deleted { + // This error is returned to the apiserver and from there to the api + // client. So we don't annotate with information regarding deletion. + // TODO(redir): We'll return a deletedUserError in the future so we can + // return more appropriate errors, e.g. username not available. + return nil, errors.UserNotFoundf("%q", user.Name()) + } return user, nil } -// User returns the state User for the given name, +// AllUsers returns a slice of state.User. This includes all active users. If +// includeDeactivated is true it also returns inactive users. At this point it +// never returns deleted users. func (st *State) AllUsers(includeDeactivated bool) ([]*User, error) { var result []*User @@ -157,8 +224,24 @@ defer closer() var query bson.D + // TODO(redir): Provide option to retrieve deleted users in future PR. + // e.g. if !includeDelted. + // Ensure the query checks for users without the deleted attribute, and + // also that if it does that the value is not true. fwereade wanted to be + // sure we cannot miss users that previously existed without the deleted + // attr. Since this will only be in 2.0 that should never happen, but... + // belt and suspenders. + query = append(query, bson.DocElem{ + "deleted", bson.D{{"$ne", true}}, + }) + + // As above, in the case that a user previously existed and doesn't have a + // deactivated attribute, we make sure the query checks for the existence + // of the attribute, and if it exists that it is not true. if !includeDeactivated { - query = append(query, bson.DocElem{"deactivated", false}) + query = append(query, bson.DocElem{ + "deactivated", bson.D{{"$ne", true}}, + }) } iter := users.Find(query).Iter() defer iter.Close() @@ -183,11 +266,11 @@ } type userDoc struct { - DocID string `bson:"_id"` - Name string `bson:"name"` - DisplayName string `bson:"displayname"` - // Removing users means they still exist, but are marked deactivated - Deactivated bool `bson:"deactivated"` + DocID string `bson:"_id"` + Name string `bson:"name"` + DisplayName string `bson:"displayname"` + Deactivated bool `bson:"deactivated,omitempty"` + Deleted bool `bson:"deleted,omitempty"` // Deleted users are marked deleted but not removed. SecretKey []byte `bson:"secretkey,omitempty"` PasswordHash string `bson:"passwordhash"` PasswordSalt string `bson:"passwordsalt"` @@ -289,6 +372,9 @@ // UpdateLastLogin sets the LastLogin time of the user to be now (to the // nearest second). func (u *User) UpdateLastLogin() (err error) { + if err := u.ensureNotDeleted(); err != nil { + return errors.Annotate(err, "cannot update last login") + } lastLogins, closer := u.st.getCollection(userLastLoginC) defer closer() @@ -316,6 +402,9 @@ // SetPassword sets the password associated with the User. func (u *User) SetPassword(password string) error { + if err := u.ensureNotDeleted(); err != nil { + return errors.Annotate(err, "cannot set password") + } salt, err := utils.RandomSalt() if err != nil { return err @@ -327,6 +416,11 @@ // password. If the User has a secret key set then it // will be cleared. func (u *User) SetPasswordHash(pwHash string, pwSalt string) error { + if err := u.ensureNotDeleted(); err != nil { + // If we do get a late set of the password this is fine b/c we have an + // explicit check before login. + return errors.Annotate(err, "cannot set password hash") + } update := bson.D{{"$set", bson.D{ {"passwordhash", pwHash}, {"passwordsalt", pwSalt}, @@ -351,14 +445,15 @@ return nil } -// PasswordValid returns whether the given password is valid for the User. +// PasswordValid returns whether the given password is valid for the User. The +// caller should call user.Refresh before calling this. func (u *User) PasswordValid(password string) bool { - // If the User is deactivated, no point in carrying on. Since any - // authentication checks are done very soon after the user is read - // from the database, there is a very small timeframe where an user - // could be disabled after it has been read but prior to being checked, - // but in practice, this isn't a problem. - if u.IsDisabled() { + // If the User is deactivated or deleted, there is no point in carrying on. + // Since any authentication checks are done very soon after the user is + // read from the database, there is a very small timeframe where an user + // could be disabled after it has been read but prior to being checked, but + // in practice, this isn't a problem. + if u.IsDisabled() || u.IsDeleted() { return false } if u.doc.PasswordSalt != "" { @@ -379,6 +474,9 @@ // Disable deactivates the user. Disabled identities cannot log in. func (u *User) Disable() error { + if err := u.ensureNotDeleted(); err != nil { + return errors.Annotate(err, "cannot disable") + } environment, err := u.st.ControllerModel() if err != nil { return errors.Trace(err) @@ -391,6 +489,9 @@ // Enable reactivates the user, setting disabled to false. func (u *User) Enable() error { + if err := u.ensureNotDeleted(); err != nil { + return errors.Annotate(err, "cannot enable") + } return errors.Annotatef(u.setDeactivated(false), "cannot enable user %q", u.Name()) } @@ -418,6 +519,34 @@ return u.doc.Deactivated } +// IsDeleted returns whether the user is currently deleted. +func (u *User) IsDeleted() bool { + return u.doc.Deleted +} + +// DeletedUserError is used to indicate when an attempt to mutate a deleted +// user is attempted. +type DeletedUserError struct { + UserName string +} + +// Error implements the error interface. +func (e DeletedUserError) Error() string { + return fmt.Sprintf("user %q deleted", e.UserName) +} + +// ensureNotDeleted refreshes the user to ensure it wasn't deleted since we +// acquired it. +func (u *User) ensureNotDeleted() error { + if err := u.Refresh(); err != nil { + return errors.Trace(err) + } + if u.doc.Deleted { + return DeletedUserError{u.Name()} + } + return nil +} + // userList type is used to provide the methods for sorting. type userList []*User diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/user_internal_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/user_internal_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/user_internal_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/user_internal_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,8 +4,12 @@ package state import ( + "strings" + gc "gopkg.in/check.v1" "gopkg.in/juju/names.v2" + + "github.com/juju/juju/core/description" ) type internalUserSuite struct { @@ -14,15 +18,34 @@ var _ = gc.Suite(&internalUserSuite{}) -func (s *internalUserSuite) TestCreateInitialUserOp(c *gc.C) { +func (s *internalUserSuite) TestCreateInitialUserOps(c *gc.C) { tag := names.NewUserTag("AdMiN") - op := createInitialUserOp(nil, tag, "abc", "salt") + ops := createInitialUserOps(s.state.ControllerUUID(), tag, "abc", "salt") + c.Assert(ops, gc.HasLen, 3) + op := ops[0] c.Assert(op.Id, gc.Equals, "admin") doc := op.Insert.(*userDoc) c.Assert(doc.DocID, gc.Equals, "admin") c.Assert(doc.Name, gc.Equals, "AdMiN") c.Assert(doc.PasswordSalt, gc.Equals, "salt") + + // controller user permissions + op = ops[1] + permdoc := op.Insert.(*permissionDoc) + c.Assert(permdoc.Access, gc.Equals, string(description.LoginAccess)) + c.Assert(permdoc.ID, gc.Equals, permissionID(controllerGlobalKey, userGlobalKey(strings.ToLower(tag.Canonical())))) + c.Assert(permdoc.SubjectGlobalKey, gc.Equals, userGlobalKey(strings.ToLower(tag.Canonical()))) + c.Assert(permdoc.ObjectGlobalKey, gc.Equals, controllerGlobalKey) + + // controller user + op = ops[2] + cudoc := op.Insert.(*userAccessDoc) + c.Assert(cudoc.ID, gc.Equals, "admin@local") + c.Assert(cudoc.ObjectUUID, gc.Equals, s.state.ControllerUUID()) + c.Assert(cudoc.UserName, gc.Equals, "AdMiN@local") + c.Assert(cudoc.DisplayName, gc.Equals, "AdMiN") + c.Assert(cudoc.CreatedBy, gc.Equals, "AdMiN@local") } func (s *internalUserSuite) TestCaseNameVsId(c *gc.C) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/userpermission.go juju-core-2.0~beta15/src/github.com/juju/juju/state/userpermission.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/userpermission.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/userpermission.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,6 +10,8 @@ "gopkg.in/mgo.v2" "gopkg.in/mgo.v2/bson" "gopkg.in/mgo.v2/txn" + + "github.com/juju/juju/core/description" ) // permission represents the permission a user has @@ -26,7 +28,15 @@ // SubjectGlobalKey holds the id for the user/group that is given permission. SubjectGlobalKey string `bson:"subject-global-key"` // Access is the permission level. - Access Access `bson:"access"` + Access string `bson:"access"` +} + +func stringToAccess(a string) description.Access { + return description.Access(a) +} + +func accessToString(a description.Access) string { + return string(a) } // userPermission returns a Permission for the given Subject and User. @@ -35,9 +45,25 @@ permissions, closer := st.getCollection(permissionsC) defer closer() - err := permissions.FindId(permissionID(objectKey, subjectKey)).One(&userPermission.doc) + id := permissionID(objectKey, subjectKey) + err := permissions.FindId(st.docID(id)).One(&userPermission.doc) + if err == mgo.ErrNotFound { + return nil, errors.NotFoundf("user permissions for user %q", id) + } + return userPermission, nil + +} + +// globalUserPermission returns a Permission for the given Subject and User. +func (st *State) globalUserPermission(objectKey, subjectKey string) (*permission, error) { + userPermission := &permission{} + permissions, closer := st.getRawCollection(permissionsC) + defer closer() + + id := permissionID(objectKey, subjectKey) + err := permissions.FindId(st.docID(id)).One(&userPermission.doc) if err == mgo.ErrNotFound { - return nil, errors.NotFoundf("user permissions for user %q", subjectKey) + return nil, errors.NotFoundf("user permissions for user %q", id) } return userPermission, nil @@ -46,51 +72,39 @@ // isReadOnly returns whether or not the user has write access or only // read access to the model. func (p *permission) isReadOnly() bool { - return p.doc.Access == UndefinedAccess || p.doc.Access == ReadAccess + return stringToAccess(p.doc.Access) == description.UndefinedAccess || stringToAccess(p.doc.Access) == description.ReadAccess } // isAdmin is a convenience method that -// returns whether or not the user has AdminAccess. +// returns whether or not the user has description.AdminAccess. func (p *permission) isAdmin() bool { - return p.doc.Access == AdminAccess + return stringToAccess(p.doc.Access) == description.AdminAccess } // isReadWrite is a convenience method that -// returns whether or not the user has WriteAccess. +// returns whether or not the user has description.WriteAccess. func (p *permission) isReadWrite() bool { - return p.doc.Access == WriteAccess + return stringToAccess(p.doc.Access) == description.WriteAccess } -func (p *permission) access() Access { - return p.doc.Access -} - -func (p *permission) isGreaterAccess(a Access) bool { - switch p.doc.Access { - case UndefinedAccess: - return a == ReadAccess || a == WriteAccess || a == AdminAccess - case ReadAccess: - return a == WriteAccess || a == AdminAccess - case WriteAccess: - return a == AdminAccess - } - return false +func (p *permission) access() description.Access { + return stringToAccess(p.doc.Access) } func permissionID(objectKey, subjectKey string) string { - // example: e#mo#jim + // example: e#us#jim // e: model global key (its always e). - // mo: model user key prefix. + // us: user key prefix. // jim: an arbitrary username. return fmt.Sprintf("%s#%s", objectKey, subjectKey) } -func updatePermissionOp(objectGlobalKey, subjectGlobalKey string, access Access) txn.Op { +func updatePermissionOp(objectGlobalKey, subjectGlobalKey string, access description.Access) txn.Op { return txn.Op{ C: permissionsC, Id: permissionID(objectGlobalKey, subjectGlobalKey), Assert: txn.DocExists, - Update: bson.D{{"$set", bson.D{{"access", access}}}}, + Update: bson.D{{"$set", bson.D{{"access", accessToString(access)}}}}, } } @@ -103,12 +117,12 @@ } } -func createPermissionOp(objectGlobalKey, subjectGlobalKey string, access Access) txn.Op { +func createPermissionOp(objectGlobalKey, subjectGlobalKey string, access description.Access) txn.Op { doc := &permissionDoc{ ID: permissionID(objectGlobalKey, subjectGlobalKey), SubjectGlobalKey: subjectGlobalKey, ObjectGlobalKey: objectGlobalKey, - Access: access, + Access: accessToString(access), } return txn.Op{ C: permissionsC, @@ -117,22 +131,3 @@ Insert: doc, } } - -// Access represents the level of access granted to a user on a model. -type Access string - -const ( - // UndefinedAccess is not a valid access type. It is the value - // unmarshaled when access is not defined by the document at all. - UndefinedAccess Access = "" - - // ReadAccess allows a user to read information about a model, without - // being able to make any changes. - ReadAccess Access = "read" - - // WriteAccess allows a user to make changes to a model. - WriteAccess Access = "write" - - // AdminAccess allows a user full control over the model. - AdminAccess Access = "admin" -) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/user_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/user_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/user_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/user_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,6 +4,7 @@ package state_test import ( + "fmt" "regexp" "time" @@ -129,19 +130,128 @@ c.Assert(user.PasswordValid("a-password"), jc.IsTrue) } +func (s *UserSuite) TestRemoveUserNonExistent(c *gc.C) { + err := s.State.RemoveUser(names.NewUserTag("harvey")) + c.Assert(errors.IsNotFound(err), jc.IsTrue) +} + +func isDeletedUserError(err error) bool { + _, ok := errors.Cause(err).(state.DeletedUserError) + return ok +} + +func (s *UserSuite) TestAllUsersSkipsDeletedUsers(c *gc.C) { + user := s.Factory.MakeUser(c, &factory.UserParams{Name: "one"}) + _ = s.Factory.MakeUser(c, &factory.UserParams{Name: "two"}) + _ = s.Factory.MakeUser(c, &factory.UserParams{Name: "three"}) + + all, err := s.State.AllUsers(true) + c.Check(err, jc.ErrorIsNil) + c.Check(len(all), jc.DeepEquals, 4) + + var got []string + for _, u := range all { + got = append(got, u.Name()) + } + c.Check(got, jc.SameContents, []string{"test-admin", "one", "two", "three"}) + + s.State.RemoveUser(user.UserTag()) + + all, err = s.State.AllUsers(true) + got = nil + for _, u := range all { + got = append(got, u.Name()) + } + c.Check(err, jc.ErrorIsNil) + c.Check(len(all), jc.DeepEquals, 3) + c.Check(got, jc.SameContents, []string{"test-admin", "two", "three"}) + +} + +func (s *UserSuite) TestRemoveUser(c *gc.C) { + user := s.Factory.MakeUser(c, &factory.UserParams{Password: "so sekrit"}) + + // Assert user exists and can authenticate. + c.Assert(user.PasswordValid("so sekrit"), jc.IsTrue) + + // Look for the user. + u, err := s.State.User(user.UserTag()) + c.Check(err, jc.ErrorIsNil) + c.Assert(u, jc.DeepEquals, user) + + // Remove the user. + err = s.State.RemoveUser(user.UserTag()) + c.Check(err, jc.ErrorIsNil) + + // Check that we cannot update last login. + err = u.UpdateLastLogin() + c.Check(err, gc.NotNil) + c.Check(isDeletedUserError(err), jc.IsTrue) + c.Assert(err.Error(), jc.DeepEquals, + fmt.Sprintf("cannot update last login: user %q deleted", user.Name())) + + // Check that we cannot set a password. + err = u.SetPassword("should fail too") + c.Check(err, gc.NotNil) + c.Check(isDeletedUserError(err), jc.IsTrue) + c.Assert(err.Error(), jc.DeepEquals, + fmt.Sprintf("cannot set password: user %q deleted", user.Name())) + + // Check that we cannot set the password hash. + err = u.SetPasswordHash("also", "fail") + c.Check(err, gc.NotNil) + c.Check(isDeletedUserError(err), jc.IsTrue) + c.Assert(err.Error(), jc.DeepEquals, + fmt.Sprintf("cannot set password hash: user %q deleted", user.Name())) + + // Check that we cannot validate a password. + c.Assert(u.PasswordValid("should fail"), jc.IsFalse) + + // Check that we cannot enable the user. + err = u.Enable() + c.Check(err, gc.NotNil) + c.Check(isDeletedUserError(err), jc.IsTrue) + c.Assert(err.Error(), jc.DeepEquals, + fmt.Sprintf("cannot enable: user %q deleted", user.Name())) + + // Check that we cannot disable the user. + err = u.Disable() + c.Check(err, gc.NotNil) + c.Check(isDeletedUserError(err), jc.IsTrue) + c.Assert(err.Error(), jc.DeepEquals, + fmt.Sprintf("cannot disable: user %q deleted", user.Name())) + + // Check again to verify the user cannot be retrieved. + u, err = s.State.User(user.UserTag()) + c.Check(err, jc.Satisfies, errors.IsUserNotFound) +} + func (s *UserSuite) TestDisable(c *gc.C) { user := s.Factory.MakeUser(c, &factory.UserParams{Password: "a-password"}) c.Assert(user.IsDisabled(), jc.IsFalse) + c.Assert(s.activeUsers(c), jc.DeepEquals, []string{"test-admin", user.Name()}) err := user.Disable() c.Assert(err, jc.ErrorIsNil) c.Assert(user.IsDisabled(), jc.IsTrue) c.Assert(user.PasswordValid("a-password"), jc.IsFalse) + c.Assert(s.activeUsers(c), jc.DeepEquals, []string{"test-admin"}) err = user.Enable() c.Assert(err, jc.ErrorIsNil) c.Assert(user.IsDisabled(), jc.IsFalse) c.Assert(user.PasswordValid("a-password"), jc.IsTrue) + c.Assert(s.activeUsers(c), jc.DeepEquals, []string{"test-admin", user.Name()}) +} + +func (s *UserSuite) activeUsers(c *gc.C) []string { + users, err := s.State.AllUsers(false) + c.Assert(err, jc.ErrorIsNil) + names := make([]string, len(users)) + for i, u := range users { + names[i] = u.Name() + } + return names } func (s *UserSuite) TestSetPasswordHash(c *gc.C) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/utils/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/utils/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/utils/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/utils/export_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,7 +4,5 @@ package utils var ( - PatchedNewEnvironment = &newEnviron - TestingCharmMetadata = charmMetadata ) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/utils/instance.go juju-core-2.0~beta15/src/github.com/juju/juju/state/utils/instance.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/utils/instance.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/utils/instance.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,40 +0,0 @@ -// Copyright 2014 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package utils - -import ( - "github.com/juju/errors" - - "github.com/juju/juju/environs" - "github.com/juju/juju/instance" - "github.com/juju/juju/provider/common" - "github.com/juju/juju/state" -) - -var newEnviron = environs.New - -// AvailabilityZone returns the availability zone associated with -// an instance ID. -func AvailabilityZone(st *state.State, instID instance.Id) (string, error) { - // Get the provider. - env, err := environs.GetEnviron(st, newEnviron) - if err != nil { - return "", errors.Trace(err) - } - zenv, ok := env.(common.ZonedEnviron) - if !ok { - return "", errors.NotSupportedf(`zones for provider "%T"`, env) - } - - // Request the zone. - zones, err := zenv.InstanceAvailabilityZoneNames([]instance.Id{instID}) - if err != nil { - return "", errors.Trace(err) - } - if len(zones) != 1 { - return "", errors.Errorf("received invalid zones: expected 1, got %d", len(zones)) - } - - return zones[0], nil -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/utils/instance_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/utils/instance_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/utils/instance_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/utils/instance_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,42 +0,0 @@ -// Copyright 2014 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package utils_test - -import ( - "github.com/juju/errors" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/environs" - "github.com/juju/juju/environs/config" - "github.com/juju/juju/juju/testing" - "github.com/juju/juju/state/utils" -) - -type instanceSuite struct { - testing.JujuConnSuite -} - -var _ = gc.Suite(&instanceSuite{}) - -func (s *instanceSuite) TestAvailabilityZone(c *gc.C) { - env := fakeZonedEnv{Environ: s.Environ} - env.instZones = []string{"a_zone"} - s.PatchValue(utils.PatchedNewEnvironment, func(config *config.Config) (environs.Environ, error) { - return &env, nil - }) - - zone, err := utils.AvailabilityZone(s.State, "id-1") - c.Assert(err, jc.ErrorIsNil) - - c.Check(zone, gc.Equals, "a_zone") -} - -func (s *instanceSuite) TestAvailabilityZoneUnsupported(c *gc.C) { - // Trigger a not supported error. - s.AssertConfigParameterUpdated(c, "broken", "InstanceAvailabilityZoneNames") - - _, err := utils.AvailabilityZone(s.State, "id-1") - c.Check(err, jc.Satisfies, errors.IsNotSupported) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/volume.go juju-core-2.0~beta15/src/github.com/juju/juju/state/volume.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/volume.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/volume.go 2016-08-16 08:56:25.000000000 +0000 @@ -459,16 +459,16 @@ if err != nil { return false, errors.Trace(err) } + if provider.Scope() == storage.ScopeMachine { + // Any storage created by a machine must be destroyed + // along with the machine. + return true, nil + } if provider.Dynamic() { - // Even machine-scoped storage could be provisioned - // while the machine is Dying, and we don't know at - // this layer whether or not it will be Persistent. - // - // TODO(axw) extend storage provider interface to - // determine up-front whether or not a volume will - // be persistent. This will have to depend on the - // machine type, since, e.g., loop devices will - // outlive LXC containers. + // We don't know ahead of time whether the storage + // will be Persistent, so we assume it will be, and + // rely on the environment-level storage provisioner + // to clean up. return false, nil } // Volume is static, so even if it is provisioned, it will @@ -746,27 +746,32 @@ if err != nil { return nil, names.VolumeTag{}, errors.Annotate(err, "cannot generate volume name") } - ops := []txn.Op{ - createStatusOp(st, volumeGlobalKey(name), statusDoc{ - Status: status.StatusPending, - // TODO(fwereade): 2016-03-17 lp:1558657 - Updated: time.Now().UnixNano(), - }), + status := statusDoc{ + Status: status.StatusPending, + // TODO(fwereade): 2016-03-17 lp:1558657 + Updated: time.Now().UnixNano(), + } + doc := volumeDoc{ + Name: name, + StorageId: params.storage.Id(), + Binding: params.binding.String(), + Params: ¶ms, + // Every volume is created with one attachment. + AttachmentCount: 1, + } + return st.newVolumeOps(doc, status), names.NewVolumeTag(name), nil +} + +func (st *State) newVolumeOps(doc volumeDoc, status statusDoc) []txn.Op { + return []txn.Op{ + createStatusOp(st, volumeGlobalKey(doc.Name), status), { C: volumesC, - Id: name, + Id: doc.Name, Assert: txn.DocMissing, - Insert: &volumeDoc{ - Name: name, - StorageId: params.storage.Id(), - Binding: params.binding.String(), - Params: ¶ms, - // Every volume is created with one attachment. - AttachmentCount: 1, - }, + Insert: &doc, }, } - return ops, names.NewVolumeTag(name), nil } func (st *State) volumeParamsWithDefaults(params VolumeParams) (VolumeParams, error) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/volume_test.go juju-core-2.0~beta15/src/github.com/juju/juju/state/volume_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/volume_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/volume_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -12,6 +12,7 @@ "github.com/juju/juju/constraints" "github.com/juju/juju/instance" + "github.com/juju/juju/provider/dummy" "github.com/juju/juju/state" "github.com/juju/juju/state/testing" "github.com/juju/juju/storage/poolmanager" @@ -112,7 +113,7 @@ func (s *VolumeStateSuite) TestAddServiceDefaultPool(c *gc.C) { // Register a default pool. - pm := poolmanager.New(state.NewStateSettings(s.State)) + pm := poolmanager.New(state.NewStateSettings(s.State), dummy.StorageProviders()) _, err := pm.Create("default-block", provider.LoopProviderType, map[string]interface{}{}) c.Assert(err, jc.ErrorIsNil) err = s.State.UpdateModelConfig(map[string]interface{}{ @@ -739,13 +740,12 @@ c.Assert(machine.Destroy(), jc.ErrorIsNil) // Cannot advance to Dead while there are persistent, or - // unprovisioned dynamic volumes (regardless of scope). + // unprovisioned environ-scoped dynamic volumes. err = machine.EnsureDead() c.Assert(err, jc.Satisfies, state.IsHasAttachmentsError) - c.Assert(err, gc.ErrorMatches, "machine 0 has attachments \\[volume-0 volume-0-1 volume-0-2\\]") + c.Assert(err, gc.ErrorMatches, "machine 0 has attachments \\[volume-0 volume-0-1\\]") s.obliterateVolumeAttachment(c, machine.MachineTag(), names.NewVolumeTag("0")) s.obliterateVolumeAttachment(c, machine.MachineTag(), names.NewVolumeTag("0/1")) - s.obliterateVolumeAttachment(c, machine.MachineTag(), names.NewVolumeTag("0/2")) c.Assert(machine.EnsureDead(), jc.ErrorIsNil) c.Assert(machine.Remove(), jc.ErrorIsNil) @@ -758,9 +758,7 @@ for _, v := range allVolumes { remaining.Add(v.Tag().String()) } - c.Assert(remaining.SortedValues(), jc.DeepEquals, []string{ - "volume-0", "volume-0-1", "volume-0-2", - }) + c.Assert(remaining.SortedValues(), jc.DeepEquals, []string{"volume-0", "volume-0-1"}) attachments, err := s.State.MachineVolumeAttachments(machine.MachineTag()) c.Assert(err, jc.ErrorIsNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/state/watcher.go juju-core-2.0~beta15/src/github.com/juju/juju/state/watcher.go --- juju-core-2.0~beta12/src/github.com/juju/juju/state/watcher.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/state/watcher.go 2016-08-16 08:56:25.000000000 +0000 @@ -1685,58 +1685,9 @@ } } -// cleanupWatcher notifies of changes in the cleanups collection. -type cleanupWatcher struct { - commonWatcher - out chan struct{} -} - -var _ Watcher = (*cleanupWatcher)(nil) - // WatchCleanups starts and returns a CleanupWatcher. func (st *State) WatchCleanups() NotifyWatcher { - return newCleanupWatcher(st) -} - -func newCleanupWatcher(st *State) NotifyWatcher { - w := &cleanupWatcher{ - commonWatcher: newCommonWatcher(st), - out: make(chan struct{}), - } - go func() { - defer w.tomb.Done() - defer close(w.out) - w.tomb.Kill(w.loop()) - }() - return w -} - -// Changes returns the event channel for w. -func (w *cleanupWatcher) Changes() <-chan struct{} { - return w.out -} - -func (w *cleanupWatcher) loop() (err error) { - in := make(chan watcher.Change) - w.watcher.WatchCollectionWithFilter(cleanupsC, in, isLocalID(w.st)) - defer w.watcher.UnwatchCollection(cleanupsC, in) - - out := w.out - for { - select { - case <-w.tomb.Dying(): - return tomb.ErrDying - case <-w.watcher.Dead(): - return stateWatcherDeadError(w.watcher.Err()) - case ch := <-in: - if _, ok := collect(ch, in, w.tomb.Dying()); !ok { - return tomb.ErrDying - } - out = w.out - case out <- struct{}{}: - out = nil - } - } + return newNotifyCollWatcher(st, cleanupsC, isLocalID(st)) } // actionStatusWatcher is a StringsWatcher that filters notifications @@ -1944,10 +1895,6 @@ sink chan []string } -// ensure collectionWatcher is a StringsWatcher -// TODO(dfc) this needs to move to a test -var _ StringsWatcher = (*collectionWatcher)(nil) - // colWCfg contains the parameters for watching a collection. type colWCfg struct { col string @@ -2369,69 +2316,21 @@ // WatchForRebootEvent returns a notify watcher that will trigger an event // when the reboot flag is set on our machine agent, our parent machine agent // or grandparent machine agent -func (m *Machine) WatchForRebootEvent() (NotifyWatcher, error) { +func (m *Machine) WatchForRebootEvent() NotifyWatcher { machineIds := m.machinesToCareAboutRebootsFor() machines := set.NewStrings(machineIds...) - return newRebootWatcher(m.st, machines), nil -} - -type rebootWatcher struct { - commonWatcher - machines set.Strings - out chan struct{} -} -func newRebootWatcher(st *State, machines set.Strings) NotifyWatcher { - w := &rebootWatcher{ - commonWatcher: newCommonWatcher(st), - machines: machines, - out: make(chan struct{}), - } - go func() { - defer w.tomb.Done() - defer close(w.out) - w.tomb.Kill(w.loop()) - }() - return w -} - -// Changes returns the event channel for the rebootWatcher. -func (w *rebootWatcher) Changes() <-chan struct{} { - return w.out -} - -func (w *rebootWatcher) loop() error { - in := make(chan watcher.Change) filter := func(key interface{}) bool { if id, ok := key.(string); ok { - if id, err := w.st.strictLocalID(id); err == nil { - return w.machines.Contains(id) + if id, err := m.st.strictLocalID(id); err == nil { + return machines.Contains(id) } else { return false } } - w.tomb.Kill(fmt.Errorf("expected string, got %T: %v", key, key)) return false } - w.watcher.WatchCollectionWithFilter(rebootC, in, filter) - defer w.watcher.UnwatchCollection(rebootC, in) - out := w.out - for { - select { - case <-w.tomb.Dying(): - return tomb.ErrDying - case <-w.watcher.Dead(): - return stateWatcherDeadError(w.watcher.Err()) - case ch := <-in: - if _, ok := collect(ch, in, w.tomb.Dying()); !ok { - return tomb.ErrDying - } - out = w.out - case out <- struct{}{}: - out = nil - - } - } + return newNotifyCollWatcher(m.st, rebootC, filter) } // blockDevicesWatcher notifies about changes to all block devices @@ -2572,20 +2471,41 @@ // // Note that this watcher does not produce an initial event if there's // never been a migration attempt for the model. -func (st *State) WatchMigrationStatus() (NotifyWatcher, error) { - return newMigrationStatusWatcher(st), nil +func (st *State) WatchMigrationStatus() NotifyWatcher { + // Watch the entire migrationsStatusC collection for migration + // status updates related to the State's model. This is more + // efficient and simpler than tracking the current active + // migration (and changing watchers when one migration finishes + // and another starts. + // + // This approach is safe because there are strong guarantees that + // there will only be one active migration per model. The watcher + // will only see changes for one migration status document at a + // time for the model. + return newNotifyCollWatcher(st, migrationsStatusC, isLocalID(st)) +} + +// WatchMachineRemovals returns a NotifyWatcher which triggers +// whenever machine removal records are added or removed. +func (st *State) WatchMachineRemovals() NotifyWatcher { + return newNotifyCollWatcher(st, machineRemovalsC, isLocalID(st)) } -type migrationStatusWatcher struct { +// notifyCollWatcher implements NotifyWatcher, triggering when a +// change is seen in a specific collection matching the provided +// filter function. +type notifyCollWatcher struct { commonWatcher collName string + filter func(interface{}) bool sink chan struct{} } -func newMigrationStatusWatcher(st *State) NotifyWatcher { - w := &migrationStatusWatcher{ +func newNotifyCollWatcher(st *State, collName string, filter func(interface{}) bool) NotifyWatcher { + w := ¬ifyCollWatcher{ commonWatcher: newCommonWatcher(st), - collName: migrationsStatusC, + collName: collName, + filter: filter, sink: make(chan struct{}), } go func() { @@ -2597,24 +2517,14 @@ } // Changes returns the event channel for this watcher. -func (w *migrationStatusWatcher) Changes() <-chan struct{} { +func (w *notifyCollWatcher) Changes() <-chan struct{} { return w.sink } -func (w *migrationStatusWatcher) loop() error { +func (w *notifyCollWatcher) loop() error { in := make(chan watcher.Change) - // Watch the entire migrationsStatusC collection for migration - // status updates related to the State's model. This is more - // efficient and simpler than tracking the current active - // migration (and changing watchers when one migration finishes - // and another starts. - // - // This approach is safe because there are strong guarantees that - // there will only be one active migration per model. The watcher - // will only see changes for one migration status document at a - // time for the model. - w.watcher.WatchCollectionWithFilter(w.collName, in, isLocalID(w.st)) + w.watcher.WatchCollectionWithFilter(w.collName, in, w.filter) defer w.watcher.UnwatchCollection(w.collName, in) out := w.sink // out set so that initial event is sent. @@ -2625,9 +2535,6 @@ case <-w.watcher.Dead(): return stateWatcherDeadError(w.watcher.Err()) case change := <-in: - if change.Revno == -1 { - return errors.New("model migration status disappeared (shouldn't happen)") - } if _, ok := collect(change, in, w.tomb.Dying()); !ok { return tomb.ErrDying } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/storage/interface.go juju-core-2.0~beta15/src/github.com/juju/juju/storage/interface.go --- juju-core-2.0~beta12/src/github.com/juju/juju/storage/interface.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/storage/interface.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,7 +6,6 @@ import ( "gopkg.in/juju/names.v2" - "github.com/juju/juju/environs/config" "github.com/juju/juju/instance" ) @@ -24,22 +23,34 @@ ScopeMachine ) +// ProviderRegistry is an interface for obtaining storage providers. +type ProviderRegistry interface { + // StorageProviderTypes returns the storage provider types + // contained within this registry. + StorageProviderTypes() []ProviderType + + // StorageProvider returns the storage provider with the given + // provider type. StorageProvider must return an errors satisfying + // errors.IsNotFound if the registry does not contain said the + // specified provider type. + StorageProvider(ProviderType) (Provider, error) +} + // Provider is an interface for obtaining storage sources. type Provider interface { - // VolumeSource returns a VolumeSource given the specified cloud - // and storage provider configurations, or an error if the provider - // does not support creating volumes or the configuration is invalid. + // VolumeSource returns a VolumeSource given the specified storage + // provider configurations, or an error if the provider does not + // support creating volumes or the configuration is invalid. // // If the storage provider does not support creating volumes as a // first-class primitive, then VolumeSource must return an error // satisfying errors.IsNotSupported. - VolumeSource(environConfig *config.Config, providerConfig *Config) (VolumeSource, error) + VolumeSource(*Config) (VolumeSource, error) // FilesystemSource returns a FilesystemSource given the specified - // cloud and storage provider configurations, or an error if the - // provider does not support creating filesystems or the configuration - // is invalid. - FilesystemSource(environConfig *config.Config, providerConfig *Config) (FilesystemSource, error) + // storage provider configurations, or an error if the provider does + // not support creating filesystems or the configuration is invalid. + FilesystemSource(*Config) (FilesystemSource, error) // Supports reports whether or not the storage provider supports // the specified storage kind. @@ -57,6 +68,10 @@ // created at the time a machine is provisioned. Dynamic() bool + // DefaultPools returns the default storage pools for this provider, + // to register in each new model. + DefaultPools() []*Config + // ValidateConfig validates the provided storage provider config, // returning an error if it is invalid. ValidateConfig(*Config) error diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/storage/path_test.go juju-core-2.0~beta15/src/github.com/juju/juju/storage/path_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/storage/path_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/storage/path_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -37,24 +37,3 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(path, jc.SamePath, expect) } - -func (s *BlockDevicePathSuite) TestSortBlockDevices(c *gc.C) { - devices := []storage.BlockDevice{{ - DeviceName: "sdb", - DeviceLinks: []string{"by-b", "by-a"}, - }, { - DeviceName: "sda", - DeviceLinks: []string{"by-c", "by-d"}, - }} - storage.SortBlockDevices(devices) - - expected := []storage.BlockDevice{{ - DeviceName: "sda", - DeviceLinks: []string{"by-c", "by-d"}, - }, { - DeviceName: "sdb", - DeviceLinks: []string{"by-a", "by-b"}, - }} - - c.Assert(devices, jc.DeepEquals, expected) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/storage/poolmanager/defaultpool.go juju-core-2.0~beta15/src/github.com/juju/juju/storage/poolmanager/defaultpool.go --- juju-core-2.0~beta12/src/github.com/juju/juju/storage/poolmanager/defaultpool.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/storage/poolmanager/defaultpool.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,19 +9,11 @@ "github.com/juju/juju/storage" ) -var defaultPools []*storage.Config - -// RegisterDefaultStoragePools registers pool information to be saved to -// state when AddDefaultStoragePools is called. -func RegisterDefaultStoragePools(pools []*storage.Config) { - defaultPools = append(defaultPools, pools...) -} - -// AddDefaultStoragePools is run at bootstrap and on upgrade to ensure that -// out of the box storage pools are created. -func AddDefaultStoragePools(settings SettingsManager) error { - pm := New(settings) - for _, pool := range defaultPools { +// AddDefaultStoragePools adds the default storage pools for the given +// provider to the given pool manager. This is called whenever a new +// model is created. +func AddDefaultStoragePools(p storage.Provider, pm PoolManager) error { + for _, pool := range p.DefaultPools() { if err := addDefaultPool(pm, pool); err != nil { return err } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/storage/poolmanager/defaultpool_test.go juju-core-2.0~beta15/src/github.com/juju/juju/storage/poolmanager/defaultpool_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/storage/poolmanager/defaultpool_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/storage/poolmanager/defaultpool_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,36 +4,40 @@ package poolmanager_test import ( + "github.com/juju/testing" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" - jujutesting "github.com/juju/juju/juju/testing" - "github.com/juju/juju/state" "github.com/juju/juju/storage" "github.com/juju/juju/storage/poolmanager" + dummystorage "github.com/juju/juju/storage/provider/dummy" ) type defaultStoragePoolsSuite struct { - jujutesting.JujuConnSuite + testing.IsolationSuite } var _ = gc.Suite(&defaultStoragePoolsSuite{}) func (s *defaultStoragePoolsSuite) TestDefaultStoragePools(c *gc.C) { - p1, err := storage.NewConfig("pool1", storage.ProviderType("loop"), map[string]interface{}{"1": "2"}) - p2, err := storage.NewConfig("pool2", storage.ProviderType("tmpfs"), map[string]interface{}{"3": "4"}) + p1, err := storage.NewConfig("pool1", storage.ProviderType("whatever"), map[string]interface{}{"1": "2"}) c.Assert(err, jc.ErrorIsNil) - defaultPools := []*storage.Config{p1, p2} - poolmanager.RegisterDefaultStoragePools(defaultPools) - - settings := state.NewStateSettings(s.State) - err = poolmanager.AddDefaultStoragePools(settings) + p2, err := storage.NewConfig("pool2", storage.ProviderType("whatever"), map[string]interface{}{"3": "4"}) c.Assert(err, jc.ErrorIsNil) - pm := poolmanager.New(settings) - for _, pool := range defaultPools { - p, err := pm.Get(pool.Name()) - c.Assert(err, jc.ErrorIsNil) - c.Assert(p.Provider(), gc.Equals, pool.Provider()) - c.Assert(p.Attrs(), gc.DeepEquals, pool.Attrs()) + provider := &dummystorage.StorageProvider{ + DefaultPools_: []*storage.Config{p1, p2}, } + + settings := poolmanager.MemSettings{make(map[string]map[string]interface{})} + pm := poolmanager.New(settings, storage.StaticProviderRegistry{ + map[storage.ProviderType]storage.Provider{"whatever": provider}, + }) + + err = poolmanager.AddDefaultStoragePools(provider, pm) + c.Assert(err, jc.ErrorIsNil) + + c.Assert(settings.Settings, jc.DeepEquals, map[string]map[string]interface{}{ + "pool#pool1": map[string]interface{}{"1": "2", "name": "pool1", "type": "whatever"}, + "pool#pool2": map[string]interface{}{"3": "4", "name": "pool2", "type": "whatever"}, + }) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/storage/poolmanager/interface.go juju-core-2.0~beta15/src/github.com/juju/juju/storage/poolmanager/interface.go --- juju-core-2.0~beta12/src/github.com/juju/juju/storage/poolmanager/interface.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/storage/poolmanager/interface.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,6 +4,9 @@ package poolmanager import ( + "strings" + + "github.com/juju/errors" "github.com/juju/juju/storage" ) @@ -28,3 +31,48 @@ RemoveSettings(key string) error ListSettings(keyPrefix string) (map[string]map[string]interface{}, error) } + +// MemSettings is an in-memory implementation of SettingsManager. +// This type does not provide any goroutine-safety. +type MemSettings struct { + Settings map[string]map[string]interface{} +} + +// CreateSettings is part of the SettingsManager interface. +func (m MemSettings) CreateSettings(key string, settings map[string]interface{}) error { + if _, ok := m.Settings[key]; ok { + return errors.AlreadyExistsf("settings with key %q", key) + } + m.Settings[key] = settings + return nil +} + +// ReadSettings is part of the SettingsManager interface. +func (m MemSettings) ReadSettings(key string) (map[string]interface{}, error) { + settings, ok := m.Settings[key] + if !ok { + return nil, errors.NotFoundf("settings with key %q", key) + } + return settings, nil +} + +// RemoveSettings is part of the SettingsManager interface. +func (m MemSettings) RemoveSettings(key string) error { + if _, ok := m.Settings[key]; !ok { + return errors.NotFoundf("settings with key %q", key) + } + delete(m.Settings, key) + return nil +} + +// ListSettings is part of the SettingsManager interface. +func (m MemSettings) ListSettings(keyPrefix string) (map[string]map[string]interface{}, error) { + result := make(map[string]map[string]interface{}) + for key, settings := range m.Settings { + if !strings.HasPrefix(key, keyPrefix) { + continue + } + result[key] = settings + } + return result, nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/storage/poolmanager/poolmanager.go juju-core-2.0~beta15/src/github.com/juju/juju/storage/poolmanager/poolmanager.go --- juju-core-2.0~beta12/src/github.com/juju/juju/storage/poolmanager/poolmanager.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/storage/poolmanager/poolmanager.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,7 +8,6 @@ "github.com/juju/juju/storage" "github.com/juju/juju/storage/provider" - "github.com/juju/juju/storage/provider/registry" ) const ( @@ -23,14 +22,15 @@ ) // New returns a PoolManager implementation using the specified state. -func New(settings SettingsManager) PoolManager { - return &poolManager{settings} +func New(settings SettingsManager, registry storage.ProviderRegistry) PoolManager { + return &poolManager{settings, registry} } var _ PoolManager = (*poolManager)(nil) type poolManager struct { settings SettingsManager + registry storage.ProviderRegistry } const globalKeyPrefix = "pool#" @@ -52,7 +52,7 @@ if err != nil { return nil, errors.Trace(err) } - p, err := registry.StorageProvider(providerType) + p, err := pm.registry.StorageProvider(providerType) if err != nil { return nil, errors.Trace(err) } @@ -88,7 +88,7 @@ return nil, errors.Annotatef(err, "reading pool %q", name) } } - return configFromSettings(settings) + return pm.configFromSettings(settings) } // List is defined on PoolManager interface. @@ -99,7 +99,7 @@ } var result []*storage.Config for _, attrs := range settings { - cfg, err := configFromSettings(attrs) + cfg, err := pm.configFromSettings(attrs) if err != nil { return nil, errors.Trace(err) } @@ -108,7 +108,7 @@ return result, nil } -func configFromSettings(settings map[string]interface{}) (*storage.Config, error) { +func (pm *poolManager) configFromSettings(settings map[string]interface{}) (*storage.Config, error) { providerType := storage.ProviderType(settings[Type].(string)) name := settings[Name].(string) // Ensure returned attributes are stripped of name and type, @@ -119,7 +119,7 @@ if err != nil { return nil, errors.Trace(err) } - p, err := registry.StorageProvider(providerType) + p, err := pm.registry.StorageProvider(providerType) if err != nil { return nil, errors.Trace(err) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/storage/poolmanager/poolmanager_test.go juju-core-2.0~beta15/src/github.com/juju/juju/storage/poolmanager/poolmanager_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/storage/poolmanager/poolmanager_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/storage/poolmanager/poolmanager_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -12,13 +12,13 @@ statetesting "github.com/juju/juju/state/testing" "github.com/juju/juju/storage" "github.com/juju/juju/storage/poolmanager" - "github.com/juju/juju/storage/provider/dummy" - "github.com/juju/juju/storage/provider/registry" + dummystorage "github.com/juju/juju/storage/provider/dummy" ) type poolSuite struct { // TODO - don't use state directly, mock it out and add feature tests. statetesting.StateSuite + registry storage.StaticProviderRegistry poolManager poolmanager.PoolManager settings poolmanager.SettingsManager } @@ -32,7 +32,12 @@ func (s *poolSuite) SetUpTest(c *gc.C) { s.StateSuite.SetUpTest(c) s.settings = state.NewStateSettings(s.State) - s.poolManager = poolmanager.New(s.settings) + s.registry = storage.StaticProviderRegistry{ + map[storage.ProviderType]storage.Provider{ + "loop": &dummystorage.StorageProvider{}, + }, + } + s.poolManager = poolmanager.New(s.settings, s.registry) } func (s *poolSuite) createSettings(c *gc.C) { @@ -116,12 +121,11 @@ } func (s *poolSuite) TestCreateInvalidConfig(c *gc.C) { - registry.RegisterProvider("invalid", &dummy.StorageProvider{ + s.registry.Providers["invalid"] = &dummystorage.StorageProvider{ ValidateConfigFunc: func(*storage.Config) error { return errors.New("no good") }, - }) - defer registry.RegisterProvider("invalid", nil) + } _, err := s.poolManager.Create("testpool", "invalid", nil) c.Assert(err, gc.ErrorMatches, "validating storage provider config: no good") } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/common.go juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/common.go --- juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/common.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/common.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,15 +9,20 @@ "github.com/juju/juju/storage" ) -var errNoMountPoint = errors.New("filesystem mount point not specified") +var ( + errNoMountPoint = errors.New("filesystem mount point not specified") -// CommonProviders returns the storage providers used by all environments. -func CommonProviders() map[storage.ProviderType]storage.Provider { - return map[storage.ProviderType]storage.Provider{ + commonStorageProviders = map[storage.ProviderType]storage.Provider{ LoopProviderType: &loopProvider{logAndExec}, RootfsProviderType: &rootfsProvider{logAndExec}, TmpfsProviderType: &tmpfsProvider{logAndExec}, } +) + +// CommonStorageProviders returns a storage.ProviderRegistry that contains +// the common storage providers. +func CommonStorageProviders() storage.ProviderRegistry { + return storage.StaticProviderRegistry{commonStorageProviders} } // ValidateConfig performs storage provider config validation, including diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/common_test.go juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/common_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/common_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/common_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -19,11 +19,13 @@ var _ = gc.Suite(&providerCommonSuite{}) func (s *providerCommonSuite) TestCommonProvidersExported(c *gc.C) { + registry := provider.CommonStorageProviders() var common []storage.ProviderType - for pType, p := range provider.CommonProviders() { + for _, pType := range registry.StorageProviderTypes() { common = append(common, pType) - _, ok := p.(storage.Provider) - c.Check(ok, jc.IsTrue) + p, err := registry.StorageProvider(pType) + c.Assert(err, jc.ErrorIsNil) + c.Assert(p, gc.NotNil) } c.Assert(common, jc.SameContents, []storage.ProviderType{ provider.LoopProviderType, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/dummy/provider.go juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/dummy/provider.go --- juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/dummy/provider.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/dummy/provider.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,7 +7,6 @@ "github.com/juju/errors" "github.com/juju/testing" - "github.com/juju/juju/environs/config" "github.com/juju/juju/storage" ) @@ -26,13 +25,16 @@ // dynamic provisioning. IsDynamic bool + // DefaultPools_ will be returned by DefaultPools. + DefaultPools_ []*storage.Config + // VolumeSourceFunc will be called by VolumeSource, if non-nil; // otherwise VolumeSource will return a NotSupported error. - VolumeSourceFunc func(*config.Config, *storage.Config) (storage.VolumeSource, error) + VolumeSourceFunc func(*storage.Config) (storage.VolumeSource, error) // FilesystemSourceFunc will be called by FilesystemSource, if non-nil; // otherwise FilesystemSource will return a NotSupported error. - FilesystemSourceFunc func(*config.Config, *storage.Config) (storage.FilesystemSource, error) + FilesystemSourceFunc func(*storage.Config) (storage.FilesystemSource, error) // ValidateConfigFunc will be called by ValidateConfig, if non-nil; // otherwise ValidateConfig returns nil. @@ -44,19 +46,19 @@ } // VolumeSource is defined on storage.Provider. -func (p *StorageProvider) VolumeSource(environConfig *config.Config, providerConfig *storage.Config) (storage.VolumeSource, error) { - p.MethodCall(p, "VolumeSource", environConfig, providerConfig) +func (p *StorageProvider) VolumeSource(providerConfig *storage.Config) (storage.VolumeSource, error) { + p.MethodCall(p, "VolumeSource", providerConfig) if p.VolumeSourceFunc != nil { - return p.VolumeSourceFunc(environConfig, providerConfig) + return p.VolumeSourceFunc(providerConfig) } return nil, errors.NotSupportedf("volumes") } // FilesystemSource is defined on storage.Provider. -func (p *StorageProvider) FilesystemSource(environConfig *config.Config, providerConfig *storage.Config) (storage.FilesystemSource, error) { - p.MethodCall(p, "FilesystemSource", environConfig, providerConfig) +func (p *StorageProvider) FilesystemSource(providerConfig *storage.Config) (storage.FilesystemSource, error) { + p.MethodCall(p, "FilesystemSource", providerConfig) if p.FilesystemSourceFunc != nil { - return p.FilesystemSourceFunc(environConfig, providerConfig) + return p.FilesystemSourceFunc(providerConfig) } return nil, errors.NotSupportedf("filesystems") } @@ -90,3 +92,9 @@ p.MethodCall(p, "Dynamic") return p.IsDynamic } + +// DefaultPool is defined on storage.Provider. +func (p *StorageProvider) DefaultPools() []*storage.Config { + p.MethodCall(p, "DefaultPools") + return p.DefaultPools_ +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/loop.go juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/loop.go --- juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/loop.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/loop.go 2016-08-16 08:56:25.000000000 +0000 @@ -13,7 +13,6 @@ "github.com/juju/errors" "gopkg.in/juju/names.v2" - "github.com/juju/juju/environs/config" "github.com/juju/juju/storage" ) @@ -52,10 +51,7 @@ } // VolumeSource is defined on the Provider interface. -func (lp *loopProvider) VolumeSource( - environConfig *config.Config, - sourceConfig *storage.Config, -) (storage.VolumeSource, error) { +func (lp *loopProvider) VolumeSource(sourceConfig *storage.Config) (storage.VolumeSource, error) { if err := lp.validateFullConfig(sourceConfig); err != nil { return nil, err } @@ -69,10 +65,7 @@ } // FilesystemSource is defined on the Provider interface. -func (lp *loopProvider) FilesystemSource( - environConfig *config.Config, - providerConfig *storage.Config, -) (storage.FilesystemSource, error) { +func (lp *loopProvider) FilesystemSource(providerConfig *storage.Config) (storage.FilesystemSource, error) { return nil, errors.NotSupportedf("filesystems") } @@ -91,6 +84,11 @@ return true } +// DefaultPools is defined on the Provider interface. +func (*loopProvider) DefaultPools() []*storage.Config { + return nil +} + // loopVolumeSource provides common functionality to handle // loop devices for rootfs and host loop volume sources. type loopVolumeSource struct { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/loop_test.go juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/loop_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/loop_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/loop_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -47,13 +47,13 @@ p := s.loopProvider(c) cfg, err := storage.NewConfig("name", provider.LoopProviderType, map[string]interface{}{}) c.Assert(err, jc.ErrorIsNil) - _, err = p.VolumeSource(nil, cfg) + _, err = p.VolumeSource(cfg) c.Assert(err, gc.ErrorMatches, "storage directory not specified") cfg, err = storage.NewConfig("name", provider.LoopProviderType, map[string]interface{}{ "storage-dir": c.MkDir(), }) c.Assert(err, jc.ErrorIsNil) - _, err = p.VolumeSource(nil, cfg) + _, err = p.VolumeSource(cfg) c.Assert(err, jc.ErrorIsNil) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/registry/init.go juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/registry/init.go --- juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/registry/init.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/registry/init.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,15 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package registry - -import ( - "github.com/juju/juju/storage/provider" -) - -func init() { - // Register the providers common to all environments, eg loop, tmpfs etc - for providerType, p := range provider.CommonProviders() { - RegisterProvider(providerType, p) - } -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/registry/package_test.go juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/registry/package_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/registry/package_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/registry/package_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,14 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package registry_test - -import ( - stdtesting "testing" - - gc "gopkg.in/check.v1" -) - -func TestPackage(t *stdtesting.T) { - gc.TestingT(t) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/registry/providerregistry.go juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/registry/providerregistry.go --- juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/registry/providerregistry.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/registry/providerregistry.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,106 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package registry - -import ( - "github.com/juju/errors" - - "github.com/juju/juju/storage" - "github.com/juju/juju/storage/provider" -) - -// -// A registry of storage providers. -// - -// providers maps from provider type to storage.Provider for -// each registered provider type. -var providers = make(map[storage.ProviderType]storage.Provider) - -// RegisterProvider registers a new storage provider of the given type. -// -// If the provider is nil, then any previously registered provider with -// the same type will be unregistered; this is purely available for -// testing. -func RegisterProvider(providerType storage.ProviderType, p storage.Provider) { - if p == nil { - delete(providers, providerType) - return - } - if providers[providerType] != nil { - panic(errors.Errorf("juju: duplicate storage provider type %q", providerType)) - } - providers[providerType] = p -} - -// StorageProvider returns the previously registered provider with the given type. -func StorageProvider(providerType storage.ProviderType) (storage.Provider, error) { - p, ok := providers[providerType] - if !ok { - return nil, errors.NotFoundf("storage provider %q", providerType) - } - return p, nil -} - -// -// A registry of storage provider types which are -// valid for a Juju Environ. -// - -// supportedEnvironProviders maps from environment type to a slice of -// supported ProviderType(s). -var supportedEnvironProviders = make(map[string][]storage.ProviderType) - -// ResetEnvironStorageProviders clears out the supported storage providers for -// the specified environment type. This is provided for testing purposes. -func ResetEnvironStorageProviders(envType string) { - delete(supportedEnvironProviders, envType) -} - -// RegisterEnvironStorageProviders records which storage provider types -// are valid for an environment. -// This is to be called from the environ provider's init(). -// Also registered will be provider types common to all environments. -func RegisterEnvironStorageProviders(envType string, providers ...storage.ProviderType) { - existing := supportedEnvironProviders[envType] - for _, p := range providers { - if IsProviderSupported(envType, p) { - continue - } - existing = append(existing, p) - } - - // Add the common providers. - for p := range provider.CommonProviders() { - if IsProviderSupported(envType, p) { - continue - } - existing = append(existing, p) - } - supportedEnvironProviders[envType] = existing -} - -// Returns true is provider is supported for the environment. -func IsProviderSupported(envType string, providerType storage.ProviderType) bool { - providerTypes, ok := EnvironStorageProviders(envType) - if !ok { - return false - } - for _, p := range providerTypes { - if p == providerType { - return true - } - } - return false -} - -// EnvironStorageProviders returns storage provider types -// for the specified environment. -func EnvironStorageProviders(envType string) ([]storage.ProviderType, bool) { - providerTypes, ok := supportedEnvironProviders[envType] - if !ok { - return nil, false - } - return providerTypes, true -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/registry/providerregistry_test.go juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/registry/providerregistry_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/registry/providerregistry_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/registry/providerregistry_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,117 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package registry_test - -import ( - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/environs" - // Ensure environ providers are registered. - _ "github.com/juju/juju/provider/all" - "github.com/juju/juju/storage" - "github.com/juju/juju/storage/provider" - "github.com/juju/juju/storage/provider/registry" -) - -type providerRegistrySuite struct{} - -var _ = gc.Suite(&providerRegistrySuite{}) - -type mockProvider struct { - storage.Provider -} - -func (s *providerRegistrySuite) TestRegisterProvider(c *gc.C) { - p1 := &mockProvider{} - ptype := storage.ProviderType("foo") - registry.RegisterProvider(ptype, p1) - p, err := registry.StorageProvider(ptype) - c.Assert(err, jc.ErrorIsNil) - c.Assert(p, gc.Equals, p1) -} - -func (s *providerRegistrySuite) TestUnregisterProvider(c *gc.C) { - ptype := storage.ProviderType("foo") - - // No-op, since there's nothing registered yet. - registry.RegisterProvider(ptype, nil) - - // Register and then unregister, ensure that the provider cannot - // be accessed. - registry.RegisterProvider(ptype, &mockProvider{}) - registry.RegisterProvider(ptype, nil) - _, err := registry.StorageProvider(storage.ProviderType("foo")) - c.Assert(err, gc.ErrorMatches, `storage provider "foo" not found`) -} - -func (s *providerRegistrySuite) TestNoSuchProvider(c *gc.C) { - _, err := registry.StorageProvider(storage.ProviderType("foo")) - c.Assert(err, gc.ErrorMatches, `storage provider "foo" not found`) -} - -func (s *providerRegistrySuite) TestRegisterProviderDuplicate(c *gc.C) { - defer func() { - if v := recover(); v != nil { - c.Assert(v, gc.ErrorMatches, `.*duplicate storage provider type "foo"`) - } - }() - p1 := &mockProvider{} - p2 := &mockProvider{} - registry.RegisterProvider(storage.ProviderType("foo"), p1) - registry.RegisterProvider(storage.ProviderType("foo"), p2) - c.Errorf("panic expected") -} - -func (s *providerRegistrySuite) TestSupportedEnvironProviders(c *gc.C) { - ptypeFoo := storage.ProviderType("foo") - ptypeBar := storage.ProviderType("bar") - registry.RegisterEnvironStorageProviders("ec2", ptypeFoo, ptypeBar) - c.Assert(registry.IsProviderSupported("ec2", ptypeFoo), jc.IsTrue) - c.Assert(registry.IsProviderSupported("ec2", ptypeBar), jc.IsTrue) - c.Assert(registry.IsProviderSupported("ec2", storage.ProviderType("foobar")), jc.IsFalse) - c.Assert(registry.IsProviderSupported("openstack", ptypeBar), jc.IsFalse) -} - -func (s *providerRegistrySuite) TestSupportedEnvironCommonProviders(c *gc.C) { - for _, envProvider := range environs.RegisteredProviders() { - for storageProvider := range provider.CommonProviders() { - c.Logf("Checking storage provider %v is registered for env provider %v", storageProvider, envProvider) - c.Assert(registry.IsProviderSupported(envProvider, storageProvider), jc.IsTrue) - } - } -} - -func (s *providerRegistrySuite) TestRegisterEnvironProvidersMultipleCalls(c *gc.C) { - ptypeFoo := storage.ProviderType("foo") - ptypeBar := storage.ProviderType("bar") - registry.RegisterEnvironStorageProviders("ec2", ptypeFoo) - registry.RegisterEnvironStorageProviders("ec2", ptypeBar) - registry.RegisterEnvironStorageProviders("ec2", ptypeBar) - c.Assert(registry.IsProviderSupported("ec2", ptypeFoo), jc.IsTrue) - c.Assert(registry.IsProviderSupported("ec2", ptypeBar), jc.IsTrue) -} - -func (s *providerRegistrySuite) TestListEnvProviderUnknownEnv(c *gc.C) { - all, exists := registry.EnvironStorageProviders("fluffy") - c.Assert(exists, jc.IsFalse) - c.Assert(all, gc.IsNil) -} - -func (s *providerRegistrySuite) TestListEnvProviderKnownEnv(c *gc.C) { - ptypeFoo := storage.ProviderType("foo") - registry.RegisterEnvironStorageProviders("ec2", ptypeFoo) - all, exists := registry.EnvironStorageProviders("ec2") - c.Assert(exists, jc.IsTrue) - c.Assert(len(all) > 0, jc.IsTrue) - - found := false - for _, one := range all { - if one == ptypeFoo { - found = true - break - } - } - c.Assert(found, jc.IsTrue) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/rootfs.go juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/rootfs.go --- juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/rootfs.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/rootfs.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,7 +10,6 @@ "github.com/juju/errors" "gopkg.in/juju/names.v2" - "github.com/juju/juju/environs/config" "github.com/juju/juju/storage" ) @@ -49,12 +48,12 @@ } // VolumeSource is defined on the Provider interface. -func (p *rootfsProvider) VolumeSource(environConfig *config.Config, providerConfig *storage.Config) (storage.VolumeSource, error) { +func (p *rootfsProvider) VolumeSource(providerConfig *storage.Config) (storage.VolumeSource, error) { return nil, errors.NotSupportedf("volumes") } // FilesystemSource is defined on the Provider interface. -func (p *rootfsProvider) FilesystemSource(environConfig *config.Config, sourceConfig *storage.Config) (storage.FilesystemSource, error) { +func (p *rootfsProvider) FilesystemSource(sourceConfig *storage.Config) (storage.FilesystemSource, error) { if err := p.validateFullConfig(sourceConfig); err != nil { return nil, err } @@ -82,6 +81,11 @@ return true } +// DefaultPools is defined on the Provider interface. +func (*rootfsProvider) DefaultPools() []*storage.Config { + return nil +} + type rootfsFilesystemSource struct { dirFuncs dirFuncs run runCommandFunc diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/rootfs_test.go juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/rootfs_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/rootfs_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/rootfs_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -50,13 +50,13 @@ p := s.rootfsProvider(c) cfg, err := storage.NewConfig("name", provider.RootfsProviderType, map[string]interface{}{}) c.Assert(err, jc.ErrorIsNil) - _, err = p.FilesystemSource(nil, cfg) + _, err = p.FilesystemSource(cfg) c.Assert(err, gc.ErrorMatches, "storage directory not specified") cfg, err = storage.NewConfig("name", provider.RootfsProviderType, map[string]interface{}{ "storage-dir": c.MkDir(), }) c.Assert(err, jc.ErrorIsNil) - _, err = p.FilesystemSource(nil, cfg) + _, err = p.FilesystemSource(cfg) c.Assert(err, jc.ErrorIsNil) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/tmpfs.go juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/tmpfs.go --- juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/tmpfs.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/tmpfs.go 2016-08-16 08:56:25.000000000 +0000 @@ -12,7 +12,6 @@ "github.com/juju/utils" "gopkg.in/juju/names.v2" - "github.com/juju/juju/environs/config" "github.com/juju/juju/storage" ) @@ -51,12 +50,12 @@ } // VolumeSource is defined on the Provider interface. -func (p *tmpfsProvider) VolumeSource(environConfig *config.Config, providerConfig *storage.Config) (storage.VolumeSource, error) { +func (p *tmpfsProvider) VolumeSource(providerConfig *storage.Config) (storage.VolumeSource, error) { return nil, errors.NotSupportedf("volumes") } // FilesystemSource is defined on the Provider interface. -func (p *tmpfsProvider) FilesystemSource(environConfig *config.Config, sourceConfig *storage.Config) (storage.FilesystemSource, error) { +func (p *tmpfsProvider) FilesystemSource(sourceConfig *storage.Config) (storage.FilesystemSource, error) { if err := p.validateFullConfig(sourceConfig); err != nil { return nil, err } @@ -84,6 +83,11 @@ return true } +// DefaultPools is defined on the Provider interface. +func (*tmpfsProvider) DefaultPools() []*storage.Config { + return nil +} + type tmpfsFilesystemSource struct { dirFuncs dirFuncs run runCommandFunc diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/tmpfs_test.go juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/tmpfs_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/storage/provider/tmpfs_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/storage/provider/tmpfs_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -48,13 +48,13 @@ p := s.tmpfsProvider(c) cfg, err := storage.NewConfig("name", provider.TmpfsProviderType, map[string]interface{}{}) c.Assert(err, jc.ErrorIsNil) - _, err = p.FilesystemSource(nil, cfg) + _, err = p.FilesystemSource(cfg) c.Assert(err, gc.ErrorMatches, "storage directory not specified") cfg, err = storage.NewConfig("name", provider.TmpfsProviderType, map[string]interface{}{ "storage-dir": c.MkDir(), }) c.Assert(err, jc.ErrorIsNil) - _, err = p.FilesystemSource(nil, cfg) + _, err = p.FilesystemSource(cfg) c.Assert(err, jc.ErrorIsNil) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/storage/registries.go juju-core-2.0~beta15/src/github.com/juju/juju/storage/registries.go --- juju-core-2.0~beta12/src/github.com/juju/juju/storage/registries.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/storage/registries.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,69 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package storage + +import ( + "sort" + + "github.com/juju/errors" +) + +// ChainedProviderRegistry is storage provider registry that combines +// multiple storage provider registries, chaining their results. Registries +// earlier in the chain take precedence. +type ChainedProviderRegistry []ProviderRegistry + +// StorageProviderTypes implements ProviderRegistry. +func (r ChainedProviderRegistry) StorageProviderTypes() []ProviderType { + var result []ProviderType + for _, r := range r { + result = append(result, r.StorageProviderTypes()...) + } + return result +} + +// StorageProvider implements ProviderRegistry. +func (r ChainedProviderRegistry) StorageProvider(t ProviderType) (Provider, error) { + for _, r := range r { + p, err := r.StorageProvider(t) + if err == nil { + return p, nil + } + if errors.IsNotFound(err) { + continue + } + return nil, errors.Annotatef(err, "getting storage provider %q", t) + } + return nil, errors.NotFoundf("storage provider %q", t) +} + +// StaticProviderRegistry is a storage provider registry with a statically +// defined set of providers. +type StaticProviderRegistry struct { + // Providers contains the storage providers for this registry. + Providers map[ProviderType]Provider +} + +// StorageProviderTypes implements ProviderRegistry. +func (r StaticProviderRegistry) StorageProviderTypes() []ProviderType { + typeStrings := make([]string, 0, len(r.Providers)) + for t := range r.Providers { + typeStrings = append(typeStrings, string(t)) + } + sort.Strings(typeStrings) + types := make([]ProviderType, len(typeStrings)) + for i, s := range typeStrings { + types[i] = ProviderType(s) + } + return types +} + +// StorageProvider implements ProviderRegistry. +func (r StaticProviderRegistry) StorageProvider(t ProviderType) (Provider, error) { + p, ok := r.Providers[t] + if ok { + return p, nil + } + return nil, errors.NotFoundf("storage provider %q", t) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/storage/sort.go juju-core-2.0~beta15/src/github.com/juju/juju/storage/sort.go --- juju-core-2.0~beta12/src/github.com/juju/juju/storage/sort.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/storage/sort.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,10 +8,6 @@ // SortBlockDevices sorts block devices by device name. func SortBlockDevices(devices []BlockDevice) { sort.Sort(byDeviceName(devices)) - - for i := range devices { - sort.Strings(devices[i].DeviceLinks) - } } type byDeviceName []BlockDevice diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/testing/constants.go juju-core-2.0~beta15/src/github.com/juju/juju/testing/constants.go --- juju-core-2.0~beta12/src/github.com/juju/juju/testing/constants.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/testing/constants.go 2016-08-16 08:56:25.000000000 +0000 @@ -21,6 +21,7 @@ // test suite const LongWait = 10 * time.Second +// TODO(katco): 2016-08-09: lp:1611427 var LongAttempt = &utils.AttemptStrategy{ Total: LongWait, Delay: ShortWait, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/testing/environ.go juju-core-2.0~beta15/src/github.com/juju/juju/testing/environ.go --- juju-core-2.0~beta12/src/github.com/juju/juju/testing/environ.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/testing/environ.go 2016-08-16 08:56:25.000000000 +0000 @@ -92,10 +92,6 @@ return cfg } -const ( - SampleModelName = "erewhemos" -) - const DefaultMongoPassword = "conn-from-name-secret" // FakeJujuXDGDataHomeSuite isolates the user's home directory and diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/testing/factory/factory.go juju-core-2.0~beta15/src/github.com/juju/juju/testing/factory/factory.go --- juju-core-2.0~beta12/src/github.com/juju/juju/testing/factory/factory.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/testing/factory/factory.go 2016-08-16 08:56:25.000000000 +0000 @@ -19,10 +19,13 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/constraints" + "github.com/juju/juju/core/description" "github.com/juju/juju/instance" "github.com/juju/juju/network" "github.com/juju/juju/state" "github.com/juju/juju/status" + "github.com/juju/juju/storage" + "github.com/juju/juju/storage/provider" "github.com/juju/juju/testcharms" "github.com/juju/juju/testing" jujuversion "github.com/juju/juju/version" @@ -51,7 +54,7 @@ Creator names.Tag NoModelUser bool Disabled bool - Access state.Access + Access description.Access } // ModelUserParams defines the parameters for creating an environment user. @@ -59,7 +62,7 @@ User string DisplayName string CreatedBy names.Tag - Access state.Access + Access description.Access } // CharmParams defines the parameters for creating a charm. @@ -90,6 +93,7 @@ Charm *state.Charm Status *status.StatusInfo Settings map[string]interface{} + Storage map[string]state.StorageConstraints Constraints constraints.Value } @@ -117,12 +121,20 @@ } type ModelParams struct { - Name string - Owner names.Tag - ConfigAttrs testing.Attrs - CloudName string - CloudRegion string - CloudCredential string + Name string + Owner names.Tag + ConfigAttrs testing.Attrs + CloudName string + CloudRegion string + CloudCredential string + StorageProviderRegistry storage.ProviderRegistry +} + +type SpaceParams struct { + Name string + ProviderID network.Id + Subnets []string + IsPublic bool } // RandomSuffix adds a random 5 character suffix to the presented string. @@ -169,15 +181,15 @@ c.Assert(err, jc.ErrorIsNil) params.Creator = env.Owner() } - if params.Access == state.UndefinedAccess { - params.Access = state.AdminAccess + if params.Access == description.UndefinedAccess { + params.Access = description.AdminAccess } creatorUserTag := params.Creator.(names.UserTag) user, err := factory.st.AddUser( params.Name, params.DisplayName, params.Password, creatorUserTag.Name()) c.Assert(err, jc.ErrorIsNil) if !params.NoModelUser { - _, err := factory.st.AddModelUser(state.ModelUserSpec{ + _, err := factory.st.AddModelUser(state.UserAccessSpec{ User: user.UserTag(), CreatedBy: names.NewUserTag(user.CreatedBy()), DisplayName: params.DisplayName, @@ -196,7 +208,7 @@ // attributes of ModelUserParams that are the default empty values, some // meaningful valid values are used instead. If params is not specified, // defaults are used. -func (factory *Factory) MakeModelUser(c *gc.C, params *ModelUserParams) *state.ModelUser { +func (factory *Factory) MakeModelUser(c *gc.C, params *ModelUserParams) description.UserAccess { if params == nil { params = &ModelUserParams{} } @@ -207,8 +219,8 @@ if params.DisplayName == "" { params.DisplayName = uniqueString("display name") } - if params.Access == state.UndefinedAccess { - params.Access = state.AdminAccess + if params.Access == description.UndefinedAccess { + params.Access = description.AdminAccess } if params.CreatedBy == nil { env, err := factory.st.Model() @@ -216,7 +228,7 @@ params.CreatedBy = env.Owner() } createdByUserTag := params.CreatedBy.(names.UserTag) - modelUser, err := factory.st.AddModelUser(state.ModelUserSpec{ + modelUser, err := factory.st.AddModelUser(state.UserAccessSpec{ User: names.NewUserTag(params.User), CreatedBy: createdByUserTag, DisplayName: params.DisplayName, @@ -388,6 +400,7 @@ Name: params.Name, Charm: params.Charm, Settings: charm.Settings(params.Settings), + Storage: params.Storage, Constraints: params.Constraints, }) c.Assert(err, jc.ErrorIsNil) @@ -567,6 +580,9 @@ c.Assert(err, jc.ErrorIsNil) params.Owner = origEnv.Owner() } + if params.StorageProviderRegistry == nil { + params.StorageProviderRegistry = provider.CommonStorageProviders() + } // It only makes sense to make an model with the same provider // as the initial model, or things will break elsewhere. currentCfg, err := factory.st.ModelConfig() @@ -585,7 +601,22 @@ CloudCredential: params.CloudCredential, Config: cfg, Owner: params.Owner.(names.UserTag), + StorageProviderRegistry: params.StorageProviderRegistry, }) c.Assert(err, jc.ErrorIsNil) return st } + +// MakeSpace will create a new space with the specified params. If the space +// name is not set, a unique space name is created. +func (factory *Factory) MakeSpace(c *gc.C, params *SpaceParams) *state.Space { + if params == nil { + params = new(SpaceParams) + } + if params.Name == "" { + params.Name = uniqueString("space-") + } + space, err := factory.st.AddSpace(params.Name, params.ProviderID, params.Subnets, params.IsPublic) + c.Assert(err, jc.ErrorIsNil) + return space +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/testing/factory/factory_test.go juju-core-2.0~beta15/src/github.com/juju/juju/testing/factory/factory_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/testing/factory/factory_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/testing/factory/factory_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -15,11 +15,12 @@ "gopkg.in/juju/charm.v6-unstable" "gopkg.in/juju/names.v2" + "github.com/juju/juju/core/description" "github.com/juju/juju/instance" "github.com/juju/juju/state" statetesting "github.com/juju/juju/state/testing" + "github.com/juju/juju/storage" "github.com/juju/juju/storage/provider" - "github.com/juju/juju/storage/provider/registry" "github.com/juju/juju/testing" "github.com/juju/juju/testing/factory" ) @@ -32,7 +33,13 @@ var _ = gc.Suite(&factorySuite{}) func (s *factorySuite) SetUpTest(c *gc.C) { - s.Policy = new(statetesting.MockPolicy) + s.NewPolicy = func(*state.State) state.Policy { + return &statetesting.MockPolicy{ + GetStorageProviderRegistry: func() (storage.ProviderRegistry, error) { + return provider.CommonStorageProviders(), nil + }, + } + } s.StateSuite.SetUpTest(c) s.Factory = factory.NewFactory(s.State) } @@ -89,7 +96,7 @@ c.Assert(err, jc.Satisfies, state.IsNeverLoggedInError) c.Assert(savedLastLogin, gc.Equals, lastLogin) - _, err = s.State.ModelUser(user.UserTag()) + _, err = s.State.UserAccess(user.UserTag(), s.State.ModelTag()) c.Assert(err, jc.ErrorIsNil) } @@ -124,18 +131,18 @@ _, err := s.State.User(user.UserTag()) c.Assert(err, jc.ErrorIsNil) - _, err = s.State.ModelUser(user.UserTag()) + _, err = s.State.UserAccess(user.UserTag(), s.State.ModelTag()) c.Assert(err, jc.Satisfies, errors.IsNotFound) } func (s *factorySuite) TestMakeModelUserNil(c *gc.C) { modelUser := s.Factory.MakeModelUser(c, nil) - saved, err := s.State.ModelUser(modelUser.UserTag()) + saved, err := s.State.UserAccess(modelUser.UserTag, modelUser.Object) c.Assert(err, jc.ErrorIsNil) - c.Assert(saved.ModelTag().Id(), gc.Equals, modelUser.ModelTag().Id()) - c.Assert(saved.UserName(), gc.Equals, modelUser.UserName()) - c.Assert(saved.DisplayName(), gc.Equals, modelUser.DisplayName()) - c.Assert(saved.CreatedBy(), gc.Equals, modelUser.CreatedBy()) + c.Assert(saved.Object.Id(), gc.Equals, modelUser.Object.Id()) + c.Assert(saved.UserName, gc.Equals, modelUser.UserName) + c.Assert(saved.DisplayName, gc.Equals, modelUser.DisplayName) + c.Assert(saved.CreatedBy, gc.Equals, modelUser.CreatedBy) } func (s *factorySuite) TestMakeModelUserPartialParams(c *gc.C) { @@ -143,12 +150,12 @@ modelUser := s.Factory.MakeModelUser(c, &factory.ModelUserParams{ User: "foobar123"}) - saved, err := s.State.ModelUser(modelUser.UserTag()) + saved, err := s.State.UserAccess(modelUser.UserTag, modelUser.Object) c.Assert(err, jc.ErrorIsNil) - c.Assert(saved.ModelTag().Id(), gc.Equals, modelUser.ModelTag().Id()) - c.Assert(saved.UserName(), gc.Equals, "foobar123@local") - c.Assert(saved.DisplayName(), gc.Equals, modelUser.DisplayName()) - c.Assert(saved.CreatedBy(), gc.Equals, modelUser.CreatedBy()) + c.Assert(saved.Object.Id(), gc.Equals, modelUser.Object.Id()) + c.Assert(saved.UserName, gc.Equals, "foobar123@local") + c.Assert(saved.DisplayName, gc.Equals, modelUser.DisplayName) + c.Assert(saved.CreatedBy, gc.Equals, modelUser.CreatedBy) } func (s *factorySuite) TestMakeModelUserParams(c *gc.C) { @@ -165,12 +172,12 @@ DisplayName: "Foo Bar", }) - saved, err := s.State.ModelUser(modelUser.UserTag()) + saved, err := s.State.UserAccess(modelUser.UserTag, s.State.ModelTag()) c.Assert(err, jc.ErrorIsNil) - c.Assert(saved.ModelTag().Id(), gc.Equals, modelUser.ModelTag().Id()) - c.Assert(saved.UserName(), gc.Equals, "foobar@local") - c.Assert(saved.CreatedBy(), gc.Equals, "createdby@local") - c.Assert(saved.DisplayName(), gc.Equals, "Foo Bar") + c.Assert(saved.Object.Id(), gc.Equals, modelUser.Object.Id()) + c.Assert(saved.UserName, gc.Equals, "foobar@local") + c.Assert(saved.CreatedBy.Id(), gc.Equals, "createdby@local") + c.Assert(saved.DisplayName, gc.Equals, "Foo Bar") } func (s *factorySuite) TestMakeModelUserInvalidCreatedBy(c *gc.C) { @@ -182,9 +189,9 @@ } c.Assert(invalidFunc, gc.PanicMatches, `interface conversion: .*`) - saved, err := s.State.ModelUser(names.NewLocalUserTag("bob")) + saved, err := s.State.UserAccess(names.NewLocalUserTag("bob"), s.State.ModelTag()) c.Assert(err, jc.Satisfies, errors.IsNotFound) - c.Assert(saved, gc.IsNil) + c.Assert(saved, gc.DeepEquals, description.UserAccess{}) } func (s *factorySuite) TestMakeModelUserNonLocalUser(c *gc.C) { @@ -195,12 +202,12 @@ CreatedBy: creator.UserTag(), }) - saved, err := s.State.ModelUser(modelUser.UserTag()) + saved, err := s.State.UserAccess(modelUser.UserTag, s.State.ModelTag()) c.Assert(err, jc.ErrorIsNil) - c.Assert(saved.ModelTag().Id(), gc.Equals, modelUser.ModelTag().Id()) - c.Assert(saved.UserName(), gc.Equals, "foobar@ubuntuone") - c.Assert(saved.DisplayName(), gc.Equals, "Foo Bar") - c.Assert(saved.CreatedBy(), gc.Equals, creator.UserTag().Canonical()) + c.Assert(saved.Object.Id(), gc.Equals, modelUser.Object.Id()) + c.Assert(saved.UserName, gc.Equals, "foobar@ubuntuone") + c.Assert(saved.DisplayName, gc.Equals, "Foo Bar") + c.Assert(saved.CreatedBy.Canonical(), gc.Equals, creator.UserTag().Canonical()) } func (s *factorySuite) TestMakeMachineNil(c *gc.C) { @@ -226,7 +233,6 @@ } func (s *factorySuite) TestMakeMachine(c *gc.C) { - registry.RegisterEnvironStorageProviders("someprovider", provider.LoopProviderType) series := "quantal" jobs := []state.MachineJob{state.JobManageModel} password, err := utils.RandomPassword() diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/testing/locking.go juju-core-2.0~beta15/src/github.com/juju/juju/testing/locking.go --- juju-core-2.0~beta12/src/github.com/juju/juju/testing/locking.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/testing/locking.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,73 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package testing - -import ( - "errors" - "sync" - "time" -) - -// TestLockingFunction verifies that a function obeys a given lock. -// -// Use this as a building block in your own tests for proper locking. -// Parameters are a lock that you expect your function to block on, and -// the function that you want to test for proper locking on that lock. -// -// This helper attempts to verify that the function both obtains and releases -// the lock. It will panic if the function fails to do either. -// TODO: Support generic sync.Locker instead of just Mutex. -// TODO: This could be a gocheck checker. -// TODO(rog): make this work reliably even for functions that take longer -// than a few µs to execute. -func TestLockingFunction(lock *sync.Mutex, function func()) { - // We record two events that must happen in the right order. - // Buffer the channel so that we don't get hung up during attempts - // to push the events in. - events := make(chan string, 2) - // Synchronization channel, to make sure that the function starts - // trying to run at the point where we're going to make it block. - proceed := make(chan bool, 1) - - goroutine := func() { - proceed <- true - function() - events <- "complete function" - } - - lock.Lock() - go goroutine() - // Make the goroutine start now. It should block inside "function()." - // (It's fine, technically even better, if the goroutine started right - // away, and this channel is buffered specifically so that it can.) - <-proceed - - // Give a misbehaved function plenty of rope to hang itself. We don't - // want it to block for a microsecond, hand control back to here so we - // think it's stuck on the lock, and then later continue on its merry - // lockless journey to finish last, as expected but for the wrong - // reason. - for counter := 0; counter < 10; counter++ { - // TODO: In Go 1.1, use runtime.GoSched instead. - time.Sleep(0) - } - - // Unblock the goroutine. - events <- "release lock" - lock.Unlock() - - // Now that we've released the lock, the function is unblocked. Read - // the 2 events. (This will wait until the function has completed.) - firstEvent := <-events - secondEvent := <-events - if firstEvent != "release lock" || secondEvent != "complete function" { - panic(errors.New("function did not obey lock")) - } - - // Also, the function must have released the lock. - blankLock := sync.Mutex{} - if *lock != blankLock { - panic(errors.New("function did not release lock")) - } -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/testing/locking_test.go juju-core-2.0~beta15/src/github.com/juju/juju/testing/locking_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/testing/locking_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/testing/locking_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,46 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package testing - -import ( - "errors" - "sync" - - gc "gopkg.in/check.v1" -) - -type LockingSuite struct{} - -var _ = gc.Suite(&LockingSuite{}) - -func (LockingSuite) TestTestLockingFunctionPassesCorrectLock(c *gc.C) { - lock := sync.Mutex{} - function := func() { - lock.Lock() - lock.Unlock() - } - // TestLockingFunction succeeds. - TestLockingFunction(&lock, function) -} - -func (LockingSuite) TestTestLockingFunctionDetectsDisobeyedLock(c *gc.C) { - lock := sync.Mutex{} - function := func() {} - c.Check( - func() { TestLockingFunction(&lock, function) }, - gc.Panics, - errors.New("function did not obey lock")) -} - -func (LockingSuite) TestTestLockingFunctionDetectsFailureToReleaseLock(c *gc.C) { - lock := sync.Mutex{} - defer lock.Unlock() - function := func() { - lock.Lock() - } - c.Check( - func() { TestLockingFunction(&lock, function) }, - gc.Panics, - errors.New("function did not release lock")) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/tools/lxdclient/client.go juju-core-2.0~beta15/src/github.com/juju/juju/tools/lxdclient/client.go --- juju-core-2.0~beta12/src/github.com/juju/juju/tools/lxdclient/client.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/tools/lxdclient/client.go 2016-08-16 08:56:25.000000000 +0000 @@ -16,11 +16,12 @@ "strings" "syscall" + "github.com/juju/errors" + "github.com/juju/loggo" "github.com/lxc/lxd" lxdshared "github.com/lxc/lxd/shared" - "github.com/juju/errors" - "github.com/juju/loggo" + "github.com/juju/juju/network" ) var logger = loggo.GetLogger("juju.tools.lxdclient") @@ -96,13 +97,18 @@ *profileClient *instanceClient *imageClient - baseURL string + baseURL string + defaultProfileBridgeName string } func (c Client) String() string { return fmt.Sprintf("Client(%s)", c.baseURL) } +func (c Client) DefaultProfileBridgeName() string { + return c.defaultProfileBridgeName +} + // Connect opens an API connection to LXD and returns a high-level // Client wrapper around that connection. func Connect(cfg Config) (*Client, error) { @@ -110,20 +116,32 @@ return nil, errors.Trace(err) } - remote := cfg.Remote.ID() + remoteID := cfg.Remote.ID() raw, err := newRawClient(cfg.Remote) if err != nil { return nil, errors.Trace(err) } + // If this is the LXD provider on the localhost, let's do an extra check to + // make sure the default profile has a correctly configured bridge, and + // which one is it. + var bridgeName string + if remoteID == remoteIDForLocal { + bridgeName, err = verifyDefaultProfileBridgeConfig(raw) + if err != nil { + return nil, errors.Trace(err) + } + } + conn := &Client{ - serverConfigClient: &serverConfigClient{raw}, - certClient: &certClient{raw}, - profileClient: &profileClient{raw}, - instanceClient: &instanceClient{raw, remote}, - imageClient: &imageClient{raw, connectToRaw}, - baseURL: raw.BaseURL, + serverConfigClient: &serverConfigClient{raw}, + certClient: &certClient{raw}, + profileClient: &profileClient{raw}, + instanceClient: &instanceClient{raw, remoteID}, + imageClient: &imageClient{raw, connectToRaw}, + baseURL: raw.BaseURL, + defaultProfileBridgeName: bridgeName, } return conn, nil } @@ -169,7 +187,6 @@ // newRawClient connects to the LXD host that is defined in Config. func newRawClient(remote Remote) (*lxd.Client, error) { host := remote.Host - logger.Debugf("connecting to LXD remote %q: %q", remote.ID(), host) if remote.ID() == remoteIDForLocal && host == "" { host = "unix://" + lxdshared.VarPath("unix.socket") @@ -180,6 +197,7 @@ host = net.JoinHostPort(host, lxdshared.DefaultPort) } } + logger.Debugf("connecting to LXD remote %q: %q", remote.ID(), host) clientCert := "" if remote.Cert != nil && remote.Cert.CertPEM != nil { @@ -229,39 +247,63 @@ } } - /* If this is the LXD provider on the localhost, let's do an extra - * check to make sure that lxdbr0 is configured. - */ - if remote.ID() == remoteIDForLocal { - profile, err := client.ProfileConfig("default") - if err != nil { - return nil, errors.Trace(err) - } + return client, nil +} - /* If the default profile doesn't have eth0 in it, then the - * user has messed with it, so let's just use whatever they set up. - * - * Otherwise, if it looks like it's pointing at our lxdbr0, - * let's check and make sure that is configured. - */ - eth0, ok := profile.Devices["eth0"] - if ok && eth0["type"] == "nic" && eth0["nictype"] == "bridged" && eth0["parent"] == "lxdbr0" { - conf, err := ioutil.ReadFile(LXDBridgeFile) - if err != nil { - if !os.IsNotExist(err) { - return nil, errors.Trace(err) - } else { - return nil, bridgeConfigError("lxdbr0 configured but no config file found at " + LXDBridgeFile) - } - } +// verifyDefaultProfileBridgeConfig takes a LXD API client and extracts the +// network bridge configured on the "default" profile. Additionally, if the +// default bridge bridge is used, its configuration in LXDBridgeFile is also +// inspected to make sure it has a chance to work. +func verifyDefaultProfileBridgeConfig(client *lxd.Client) (string, error) { + const ( + defaultProfileName = "default" + configTypeKey = "type" + configTypeNic = "nic" + configNicTypeKey = "nictype" + configBridged = "bridged" + configEth0 = "eth0" + configParentKey = "parent" + ) - if err = checkLXDBridgeConfiguration(string(conf)); err != nil { - return nil, errors.Trace(err) - } - } + config, err := client.ProfileConfig(defaultProfileName) + if err != nil { + return "", errors.Trace(err) } - return client, nil + // If the default profile doesn't have eth0 in it, then the user has messed + // with it, so let's just use whatever they set up. + eth0, ok := config.Devices[configEth0] + if !ok { + return "", errors.Errorf("unexpected LXD %q profile config without eth0: %+v", defaultProfileName, config) + } + + // If eth0 is there, but not with the expected attributes, likewise fail + // early. + if eth0[configTypeKey] != configTypeNic || eth0[configNicTypeKey] != configBridged { + return "", errors.Errorf("unexpected LXD %q profile config: %+v", defaultProfileName, config) + } + + bridgeName := eth0[configParentKey] + logger.Infof(`LXD "default" profile uses network bridge %q`, bridgeName) + + if bridgeName != network.DefaultLXDBridge { + // When the user changed which bridge to use, just return its name and + // check no further. + return bridgeName, nil + } + + bridgeConfig, err := ioutil.ReadFile(LXDBridgeFile) + if os.IsNotExist(err) { + return "", bridgeConfigError("lxdbr0 configured but no config file found at " + LXDBridgeFile) + } else if err != nil { + return "", errors.Trace(err) + } + + if err := checkLXDBridgeConfiguration(string(bridgeConfig)); err != nil { + return "", errors.Trace(err) + } + + return bridgeName, nil } func bridgeConfigError(err string) error { @@ -292,7 +334,7 @@ * because the default profile that juju will use says * lxdbr0. So let's fail if it doesn't. */ - if name != "lxdbr0" { + if name != network.DefaultLXDBridge { return bridgeConfigError(fmt.Sprintf(LXDBridgeFile+" has a bridge named %s, not lxdbr0", name)) } } else if strings.HasPrefix(line, "LXD_IPV4_ADDR=") || strings.HasPrefix(line, "LXD_IPV6_ADDR=") { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/tools/lxdclient/client_profile.go juju-core-2.0~beta15/src/github.com/juju/juju/tools/lxdclient/client_profile.go --- juju-core-2.0~beta12/src/github.com/juju/juju/tools/lxdclient/client_profile.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/tools/lxdclient/client_profile.go 2016-08-16 08:56:25.000000000 +0000 @@ -13,7 +13,8 @@ type rawProfileClient interface { ProfileCreate(name string) error ListProfiles() ([]string, error) - SetProfileConfigItem(name, key, value string) error + SetProfileConfigItem(profile, key, value string) error + GetProfileConfig(profile string) (map[string]string, error) ProfileDelete(profile string) error ProfileDeviceAdd(profile, devname, devtype string, props []string) (*lxd.Response, error) } @@ -70,3 +71,21 @@ } return false, nil } + +// SetProfileConfigItem updates the given profile config key to the given value. +func (p profileClient) SetProfileConfigItem(profile, key, value string) error { + if err := p.raw.SetProfileConfigItem(profile, key, value); err != nil { + return errors.Trace(err) + } + return nil +} + +// GetProfileConfig returns a map with all keys and values for the given +// profile. +func (p profileClient) GetProfileConfig(profile string) (map[string]string, error) { + config, err := p.raw.GetProfileConfig(profile) + if err != nil { + return nil, errors.Trace(err) + } + return config, nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/tools/lxdclient/config.go juju-core-2.0~beta15/src/github.com/juju/juju/tools/lxdclient/config.go --- juju-core-2.0~beta12/src/github.com/juju/juju/tools/lxdclient/config.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/tools/lxdclient/config.go 2016-08-16 08:56:25.000000000 +0000 @@ -58,23 +58,19 @@ return cfg, errors.Trace(err) } - /* UsingTCP will try to figure out the network name of the lxdbr0, - * which means that lxdbr0 needs to be up. If this lxd has never had - * anything done to it, it hasn't been socket activated yet, and lxdbr0 - * won't exist. So, we rely on this poke to get lxdbr0 started. - */ - _, err = client.ServerStatus() - if err != nil { + if _, err := client.ServerStatus(); err != nil { return cfg, errors.Trace(err) } - remote, err := cfg.Remote.UsingTCP() + // If the default profile's bridge was never used before, the next call with + // also activate it and get its address. + remote, err := cfg.Remote.UsingTCP(client.defaultProfileBridgeName) if err != nil { return cfg, errors.Trace(err) } // Update the server config and authorized certs. - serverCert, err := prepareRemote(client, *remote.Cert) + serverCert, err := prepareRemote(client, remote.Cert) if err != nil { return cfg, errors.Trace(err) } @@ -88,7 +84,7 @@ return cfg, nil } -func prepareRemote(client *Client, newCert Cert) (string, error) { +func prepareRemote(client *Client, newCert *Cert) (string, error) { // Make sure the LXD service is configured to listen to local https // requests, rather than only via the Unix socket. // TODO: jam 2016-02-25 This tells LXD to listen on all addresses, @@ -99,8 +95,12 @@ return "", errors.Trace(err) } + if newCert == nil { + return "", nil + } + // Make sure the LXD service will allow our certificate to connect - if err := client.AddCert(newCert); err != nil { + if err := client.AddCert(*newCert); err != nil { return "", errors.Trace(err) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/tools/lxdclient/remote.go juju-core-2.0~beta15/src/github.com/juju/juju/tools/lxdclient/remote.go --- juju-core-2.0~beta12/src/github.com/juju/juju/tools/lxdclient/remote.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/tools/lxdclient/remote.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,13 +9,11 @@ "github.com/juju/errors" "github.com/juju/utils" lxdshared "github.com/lxc/lxd/shared" - - "github.com/juju/juju/network" ) const ( // remoteLocalName is a specific remote name in the default LXD config. - // See https://github.com/lxc/lxd/blob/master/config.go:defaultRemote. + // See https://github.com/lxc/lxd/blob/master/config.go:DefaultRemotes. remoteLocalName = "local" remoteIDForLocal = remoteLocalName ) @@ -130,8 +128,6 @@ r.Protocol = LXDProtocol } - // TODO(ericsnow) Set r.Cert to nil? - return r } @@ -176,35 +172,31 @@ return nil } -// UsingTCP converts the remote into a non-local version. For -// non-local remotes this is a no-op. +// UsingTCP converts the remote into a non-local version. For non-local remotes +// this is a no-op. // -// For a "local" remote (see Local), the remote is changed to a one -// with the host set to the IP address of the local lxcbr0 bridge -// interface. The remote is also set up for remote access, setting -// the cert if not already set. -func (r Remote) UsingTCP() (Remote, error) { +// For a "local" remote (see Local), the remote is changed to a one with the +// host set to the first IPv4 address assigned to the given bridgeName. The +// remote is also set up for remote access, setting the cert if not already set. +func (r Remote) UsingTCP(bridgeName string) (Remote, error) { // Note that r is a value receiver, so it is an implicit copy. if !r.isLocal() { return r, nil } - // TODO: jam 2016-02-25 This should be updated for systems that are - // space aware, as we may not be just using the default LXC - // bridge. - addr, err := utils.GetAddressForInterface(network.DefaultLXDBridge) + address, err := utils.GetAddressForInterface(bridgeName) if err != nil { return r, errors.Trace(err) } - r.Host = addr + r.Host = address + + // TODO(ericsnow) Change r.Name if "local"? Prepend "juju-"? r, err = r.WithDefaults() if err != nil { return r, errors.Trace(err) } - // TODO(ericsnow) Change r.Name if "local"? Prepend "juju-"? - return r, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/tools/lxdclient/remote_test.go juju-core-2.0~beta15/src/github.com/juju/juju/tools/lxdclient/remote_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/tools/lxdclient/remote_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/tools/lxdclient/remote_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,15 +6,12 @@ package lxdclient_test import ( - "fmt" "net" "github.com/juju/errors" jc "github.com/juju/testing/checkers" - "github.com/juju/utils" gc "gopkg.in/check.v1" - "github.com/juju/juju/network" "github.com/juju/juju/tools/lxdclient" ) @@ -416,7 +413,7 @@ Protocol: lxdclient.LXDProtocol, Cert: s.Cert, } - nonlocal, err := remote.UsingTCP() + nonlocal, err := remote.UsingTCP("") c.Assert(err, jc.ErrorIsNil) c.Check(nonlocal, jc.DeepEquals, remote) @@ -427,12 +424,6 @@ } func (s *remoteFunctionalSuite) TestUsingTCP(c *gc.C) { - if _, err := net.InterfaceByName(network.DefaultLXDBridge); err != nil { - c.Skip("network bridge interface not found") - } - if _, err := utils.GetAddressForInterface(network.DefaultLXDBridge); err != nil { - c.Skip(fmt.Sprintf("no IPv4 address available for %s", network.DefaultLXDBridge)) - } lxdclient.PatchGenerateCertificate(&s.CleanupSuite, testingCert, testingKey) remote := lxdclient.Remote{ @@ -440,7 +431,7 @@ Host: "", Cert: nil, } - nonlocal, err := remote.UsingTCP() + nonlocal, err := remote.UsingTCP("lo") c.Assert(err, jc.ErrorIsNil) checkValidRemote(c, &nonlocal) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/tools/lxdclient/testing_test.go juju-core-2.0~beta15/src/github.com/juju/juju/tools/lxdclient/testing_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/tools/lxdclient/testing_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/tools/lxdclient/testing_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,6 +7,7 @@ import ( "crypto/x509" + "runtime" "github.com/juju/errors" "github.com/juju/testing" @@ -23,6 +24,13 @@ Cert *Cert } +func (s *BaseSuite) SetUpSuite(c *gc.C) { + s.IsolationSuite.SetUpSuite(c) + if runtime.GOOS == "windows" { + c.Skip("LXD is not supported on Windows") + } +} + func (s *BaseSuite) SetUpTest(c *gc.C) { s.IsolationSuite.SetUpTest(c) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/version/version.go juju-core-2.0~beta15/src/github.com/juju/juju/version/version.go --- juju-core-2.0~beta12/src/github.com/juju/juju/version/version.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/version/version.go 2016-08-16 08:56:25.000000000 +0000 @@ -19,7 +19,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 = "2.0-beta12" +const version = "2.0-beta15" // The version that we switched over from old style numbering to new style. var switchOverVersion = semversion.MustParse("1.19.9") diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/watcher/migrationstatus.go juju-core-2.0~beta15/src/github.com/juju/juju/watcher/migrationstatus.go --- juju-core-2.0~beta12/src/github.com/juju/juju/watcher/migrationstatus.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/watcher/migrationstatus.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,6 +8,7 @@ // MigrationStatus is the client side version of // params.MigrationStatus. type MigrationStatus struct { + MigrationId string Attempt int Phase migration.Phase SourceAPIAddrs []string diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/apicaller/connect.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/apicaller/connect.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/apicaller/connect.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/apicaller/connect.go 2016-08-16 08:56:25.000000000 +0000 @@ -21,6 +21,8 @@ // retry strategy for "handling" ErrNotProvisioned. It exists // in the name of stability; as the code evolves, it would be // great to see its function moved up a level or two. + // + // TODO(katco): 2016-08-09: lp:1611427 checkProvisionedStrategy = utils.AttemptStrategy{ Total: 1 * time.Minute, Delay: 5 * time.Second, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/apicaller/retry_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/apicaller/retry_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/apicaller/retry_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/apicaller/retry_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -39,6 +39,7 @@ errNotProvisioned, // first strategy attempt nil, // success on second strategy attempt ) + // TODO(katco): 2016-08-09: lp:1611427 strategy := utils.AttemptStrategy{Min: 3} conn, err := strategyTest(stub, strategy, func(apiOpen api.OpenFunc) (api.Connection, error) { return apicaller.OnlyConnect(&mockAgent{stub: stub}, apiOpen) @@ -56,6 +57,7 @@ errNotProvisioned, // first strategy attempt nil, // second strategy attempt ) + // TODO(katco): 2016-08-09: lp:1611427 strategy := utils.AttemptStrategy{Min: 3} conn, err := strategyTest(stub, strategy, func(apiOpen api.OpenFunc) (api.Connection, error) { return apicaller.OnlyConnect(&mockAgent{stub: stub}, apiOpen) @@ -85,6 +87,7 @@ errNotProvisioned, // second strategy attempt errors.New("splat pow"), // third strategy attempt ) + // TODO(katco): 2016-08-09: lp:1611427 strategy := utils.AttemptStrategy{Min: 3} conn, err := strategyTest(stub, strategy, func(apiOpen api.OpenFunc) (api.Connection, error) { return connect(&mockAgent{stub: stub}, apiOpen) @@ -113,6 +116,7 @@ errNotProvisioned, // second strategy attempt errNotProvisioned, // third strategy attempt ) + // TODO(katco): 2016-08-09: lp:1611427 strategy := utils.AttemptStrategy{Min: 3} conn, err := strategyTest(stub, strategy, func(apiOpen api.OpenFunc) (api.Connection, error) { return connect(&mockAgent{stub: stub}, apiOpen) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/apicaller/util_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/apicaller/util_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/apicaller/util_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/apicaller/util_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -162,6 +162,7 @@ return test() } +// TODO(katco): 2016-08-09: lp:1611427 func strategyTest(stub *testing.Stub, strategy utils.AttemptStrategy, test func(api.OpenFunc) (api.Connection, error)) (api.Connection, error) { unpatch := testing.PatchValue(apicaller.Strategy, strategy) defer unpatch() diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/catacomb/catacomb.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/catacomb/catacomb.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/catacomb/catacomb.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/catacomb/catacomb.go 2016-08-16 08:56:25.000000000 +0000 @@ -112,7 +112,7 @@ go func() { defer catacomb.tomb.Done() defer catacomb.wg.Wait() - catacomb.Kill(plan.Work()) + catacomb.Kill(runSafely(plan.Work)) }() return nil } @@ -247,3 +247,14 @@ func (err dyingError) Error() string { return fmt.Sprintf("catacomb %p is dying", err.catacomb) } + +// runSafely will ensure that the function is run, and any error is returned. +// If there is a panic, then that will be returned as an error. +func runSafely(f func() error) (err error) { + defer func() { + if panicResult := recover(); panicResult != nil { + err = errors.Errorf("panic resulted in: %v", panicResult) + } + }() + return f() +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/catacomb/catacomb_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/catacomb/catacomb_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/catacomb/catacomb_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/catacomb/catacomb_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -218,6 +218,13 @@ w.assertDead(c) } +func (s *CatacombSuite) TestPanicWorkerStillStops(c *gc.C) { + err := s.fix.run(c, func() { + panic("failed to startup") + }) + c.Check(err, gc.ErrorMatches, "panic resulted in: failed to startup") +} + func (s *CatacombSuite) TestAddWhenDyingStopsWorker(c *gc.C) { err := s.fix.run(c, func() { w := s.fix.startErrorWorker(c, nil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/dependency/context.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/dependency/context.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/dependency/context.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/dependency/context.go 2016-08-16 08:56:25.000000000 +0000 @@ -63,19 +63,19 @@ // rawAccess is a GetResourceFunc that neither checks enpiry nor records access. func (ctx *context) rawAccess(resourceName string, out interface{}) error { - input := ctx.workers[resourceName] - if input == nil { - // No worker running (or not declared). - return ErrMissing + input, found := ctx.workers[resourceName] + if !found { + return errors.Annotatef(ErrMissing, "%q not declared", resourceName) + } else if input == nil { + return errors.Annotatef(ErrMissing, "%q not running", resourceName) } if out == nil { - // No conversion necessary. + // No conversion necessary, just an exist check. return nil } convert := ctx.outputs[resourceName] if convert == nil { - // Conversion required, no func available. - return ErrMissing + return errors.Annotatef(ErrMissing, "%q not exposed", resourceName) } return convert(input, out) } @@ -95,11 +95,14 @@ // report returns a convenient representation of ra. func (ra resourceAccess) report() map[string]interface{} { - return map[string]interface{}{ - KeyName: ra.name, - KeyType: ra.as, - KeyError: ra.err, + report := map[string]interface{}{ + KeyName: ra.name, + KeyType: ra.as, } + if ra.err != nil { + report[KeyError] = ra.err.Error() + } + return report } // resourceLogReport returns a convenient representation of accessLog. diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/dependency/engine.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/dependency/engine.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/dependency/engine.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/dependency/engine.go 2016-08-16 08:56:25.000000000 +0000 @@ -189,11 +189,14 @@ // oneShotDying approach in loop means that it can continue to // process requests until the last possible moment. Only once // loop has exited do we fall back to this report. - return map[string]interface{}{ + report := map[string]interface{}{ KeyState: "stopped", - KeyError: engine.Wait(), KeyManifolds: engine.manifoldsReport(), } + if err := engine.Wait(); err != nil { + report[KeyError] = err.Error() + } + return report } } @@ -210,11 +213,14 @@ reportError = engine.worstError } } - return map[string]interface{}{ + report := map[string]interface{}{ KeyState: state, - KeyError: reportError, KeyManifolds: engine.manifoldsReport(), } + if reportError != nil { + report[KeyError] = reportError.Error() + } + return report } // manifoldsReport collects and returns information about the engine's manifolds @@ -223,13 +229,20 @@ func (engine *Engine) manifoldsReport() map[string]interface{} { manifolds := map[string]interface{}{} for name, info := range engine.current { - manifolds[name] = map[string]interface{}{ + report := map[string]interface{}{ KeyState: info.state(), - KeyError: info.err, KeyInputs: engine.manifolds[name].Inputs, - KeyReport: info.report(), KeyResourceLog: resourceLogReport(info.resourceLog), } + if info.err != nil { + report[KeyError] = info.err.Error() + } + if reporter, ok := info.worker.(Reporter); ok { + if reporter != engine { + report[KeyReport] = reporter.Report() + } + } + manifolds[name] = report } return manifolds } @@ -629,15 +642,6 @@ return "stopped" } -// report returns any available report from the worker. If the worker is not -// a Reporter, or is not present, this method will return nil. -func (info workerInfo) report() map[string]interface{} { - if reporter, ok := info.worker.(Reporter); ok { - return reporter.Report() - } - return nil -} - // installTicket is used by engine to induce installation of a named manifold // and pass on any errors encountered in the process. type installTicket struct { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/dependency/engine_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/dependency/engine_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/dependency/engine_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/dependency/engine_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -165,7 +165,8 @@ err = engine.Install("other-task", dependency.Manifold{ Start: func(context dependency.Context) (worker.Worker, error) { err := context.Get("some-task", nil) - c.Check(err, gc.Equals, dependency.ErrMissing) + c.Check(errors.Cause(err), gc.Equals, dependency.ErrMissing) + c.Check(err, gc.ErrorMatches, `"some-task" not declared: dependency not available`) close(done) // Return a real worker so we don't keep restarting and potentially double-closing. return startMinimalWorker(context) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/dependency/reporter_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/dependency/reporter_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/dependency/reporter_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/dependency/reporter_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -32,7 +32,6 @@ report := engine.Report() c.Check(report, jc.DeepEquals, map[string]interface{}{ "state": "started", - "error": nil, "manifolds": map[string]interface{}{}, }) }) @@ -44,7 +43,6 @@ report := engine.Report() c.Check(report, jc.DeepEquals, map[string]interface{}{ "state": "stopped", - "error": nil, "manifolds": map[string]interface{}{}, }) }) @@ -91,11 +89,9 @@ } c.Check(report, jc.DeepEquals, map[string]interface{}{ "state": "stopping", - "error": nil, "manifolds": map[string]interface{}{ "task": map[string]interface{}{ "state": "stopping", - "error": nil, "inputs": ([]string)(nil), "resource-log": []map[string]interface{}{}, "report": map[string]interface{}{ @@ -122,11 +118,9 @@ report := engine.Report() c.Check(report, jc.DeepEquals, map[string]interface{}{ "state": "started", - "error": nil, "manifolds": map[string]interface{}{ "task": map[string]interface{}{ "state": "started", - "error": nil, "inputs": ([]string)(nil), "resource-log": []map[string]interface{}{}, "report": map[string]interface{}{ @@ -135,12 +129,10 @@ }, "another task": map[string]interface{}{ "state": "started", - "error": nil, "inputs": []string{"task"}, "resource-log": []map[string]interface{}{{ - "name": "task", - "type": "", - "error": nil, + "name": "task", + "type": "", }}, "report": map[string]interface{}{ "key1": "hello there", @@ -163,18 +155,16 @@ report := engine.Report() c.Check(report, jc.DeepEquals, map[string]interface{}{ "state": "stopped", - "error": nil, "manifolds": map[string]interface{}{ "task": map[string]interface{}{ "state": "stopped", - "error": dependency.ErrMissing, + "error": `"missing" not running: dependency not available`, "inputs": []string{"missing"}, "resource-log": []map[string]interface{}{{ "name": "missing", "type": "", - "error": dependency.ErrMissing, + "error": `"missing" not running: dependency not available`, }}, - "report": (map[string]interface{})(nil), }, }, }) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/discoverspaces/worker_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/discoverspaces/worker_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/discoverspaces/worker_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/discoverspaces/worker_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -19,6 +19,7 @@ "github.com/juju/juju/network" "github.com/juju/juju/provider/dummy" "github.com/juju/juju/state" + "github.com/juju/juju/state/stateenvirons" coretesting "github.com/juju/juju/testing" "github.com/juju/juju/worker" "github.com/juju/juju/worker/discoverspaces" @@ -188,9 +189,7 @@ func (s *WorkerSuite) startWorker(c *gc.C) (worker.Worker, gate.Lock) { // create fresh environ to see any injected broken-ness - config, err := s.State.ModelConfig() - c.Assert(err, jc.ErrorIsNil) - environ, err := environs.New(config) + environ, err := stateenvirons.GetNewEnvironFunc(environs.New)(s.State) c.Assert(err, jc.ErrorIsNil) lock := gate.NewLock() diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/diskmanager/diskmanager.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/diskmanager/diskmanager.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/diskmanager/diskmanager.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/diskmanager/diskmanager.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,6 +5,7 @@ import ( "reflect" + "sort" "time" "github.com/juju/loggo" @@ -55,6 +56,9 @@ return err } storage.SortBlockDevices(blockDevices) + for _, blockDevice := range blockDevices { + sort.Strings(blockDevice.DeviceLinks) + } if reflect.DeepEqual(blockDevices, *old) { logger.Tracef("no changes to block devices detected") return nil diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/diskmanager/diskmanager_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/diskmanager/diskmanager_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/diskmanager/diskmanager_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/diskmanager/diskmanager_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -53,32 +53,36 @@ var oldDevices []storage.BlockDevice var devicesSet [][]storage.BlockDevice var setDevices BlockDeviceSetterFunc = func(devices []storage.BlockDevice) error { - devicesSet = append(devicesSet, devices) + devicesSet = append(devicesSet, append([]storage.BlockDevice{}, devices...)) return nil } + device := storage.BlockDevice{DeviceName: "sda", DeviceLinks: []string{"a", "b"}} var listDevices diskmanager.ListBlockDevicesFunc = func() ([]storage.BlockDevice, error) { - return []storage.BlockDevice{{DeviceName: "sda"}}, nil - } - for i := 0; i < 2; i++ { - err := diskmanager.DoWork(listDevices, setDevices, &oldDevices) - c.Assert(err, jc.ErrorIsNil) + return []storage.BlockDevice{device}, nil } - listDevices = func() ([]storage.BlockDevice, error) { - return []storage.BlockDevice{{DeviceName: "sdb"}}, nil - } err := diskmanager.DoWork(listDevices, setDevices, &oldDevices) c.Assert(err, jc.ErrorIsNil) + c.Assert(devicesSet, gc.HasLen, 1) + + // diskmanager only calls the BlockDeviceSetter when it sees a + // change in disks. Order of DeviceLinks should not matter. + device.DeviceLinks = []string{"b", "a"} + err = diskmanager.DoWork(listDevices, setDevices, &oldDevices) + c.Assert(err, jc.ErrorIsNil) + c.Assert(devicesSet, gc.HasLen, 1) - // diskmanager only calls the BlockDeviceSetter when it sees - // a change in disks. + device.DeviceName = "sdb" + err = diskmanager.DoWork(listDevices, setDevices, &oldDevices) + c.Assert(err, jc.ErrorIsNil) c.Assert(devicesSet, gc.HasLen, 2) + c.Assert(devicesSet[0], gc.DeepEquals, []storage.BlockDevice{{ - DeviceName: "sda", + DeviceName: "sda", DeviceLinks: []string{"a", "b"}, }}) c.Assert(devicesSet[1], gc.DeepEquals, []storage.BlockDevice{{ - DeviceName: "sdb", + DeviceName: "sdb", DeviceLinks: []string{"a", "b"}, }}) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/environ/environ_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/environ/environ_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/environ/environ_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/environ/environ_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,7 +11,6 @@ gc "gopkg.in/check.v1" "github.com/juju/juju/environs" - "github.com/juju/juju/environs/config" coretesting "github.com/juju/juju/testing" "github.com/juju/juju/worker/environ" "github.com/juju/juju/worker/workertest" @@ -72,13 +71,13 @@ fix.Run(c, func(context *runContext) { tracker, err := environ.NewTracker(environ.Config{ Observer: context, - NewEnvironFunc: func(*config.Config) (environs.Environ, error) { + NewEnvironFunc: func(environs.OpenParams) (environs.Environ, error) { return nil, errors.NotValidf("config") }, }) c.Check(err, gc.ErrorMatches, `cannot create environ: config not valid`) c.Check(tracker, gc.IsNil) - context.CheckCallNames(c, "ModelConfig") + context.CheckCallNames(c, "ModelConfig", "CloudSpec") }) } @@ -102,10 +101,31 @@ }) } +func (s *TrackerSuite) TestCloudSpec(c *gc.C) { + cloudSpec := environs.CloudSpec{ + Name: "foo", + Type: "bar", + Region: "baz", + } + fix := &fixture{cloud: cloudSpec} + fix.Run(c, func(context *runContext) { + tracker, err := environ.NewTracker(environ.Config{ + Observer: context, + NewEnvironFunc: func(args environs.OpenParams) (environs.Environ, error) { + c.Assert(args.Cloud, jc.DeepEquals, cloudSpec) + return nil, errors.NotValidf("cloud spec") + }, + }) + c.Check(err, gc.ErrorMatches, `cannot create environ: cloud spec not valid`) + c.Check(tracker, gc.IsNil) + context.CheckCallNames(c, "ModelConfig", "CloudSpec") + }) +} + func (s *TrackerSuite) TestWatchFails(c *gc.C) { fix := &fixture{ observerErrs: []error{ - nil, errors.New("grrk splat"), + nil, nil, errors.New("grrk splat"), }, } fix.Run(c, func(context *runContext) { @@ -118,7 +138,7 @@ err = workertest.CheckKilled(c, tracker) c.Check(err, gc.ErrorMatches, "cannot watch environ config: grrk splat") - context.CheckCallNames(c, "ModelConfig", "WatchForModelConfigChanges") + context.CheckCallNames(c, "ModelConfig", "CloudSpec", "WatchForModelConfigChanges") }) } @@ -135,14 +155,14 @@ context.CloseNotify() err = workertest.CheckKilled(c, tracker) c.Check(err, gc.ErrorMatches, "environ config watch closed") - context.CheckCallNames(c, "ModelConfig", "WatchForModelConfigChanges") + context.CheckCallNames(c, "ModelConfig", "CloudSpec", "WatchForModelConfigChanges") }) } func (s *TrackerSuite) TestWatchedModelConfigFails(c *gc.C) { fix := &fixture{ observerErrs: []error{ - nil, nil, errors.New("blam ouch"), + nil, nil, nil, errors.New("blam ouch"), }, } fix.Run(c, func(context *runContext) { @@ -156,7 +176,7 @@ context.SendNotify() err = workertest.CheckKilled(c, tracker) c.Check(err, gc.ErrorMatches, "cannot read environ config: blam ouch") - context.CheckCallNames(c, "ModelConfig", "WatchForModelConfigChanges", "ModelConfig") + context.CheckCallNames(c, "ModelConfig", "CloudSpec", "WatchForModelConfigChanges", "ModelConfig") }) } @@ -165,7 +185,7 @@ fix.Run(c, func(context *runContext) { tracker, err := environ.NewTracker(environ.Config{ Observer: context, - NewEnvironFunc: func(*config.Config) (environs.Environ, error) { + NewEnvironFunc: func(environs.OpenParams) (environs.Environ, error) { env := &mockEnviron{} env.SetErrors(errors.New("SetConfig is broken")) return env, nil @@ -177,7 +197,7 @@ context.SendNotify() err = workertest.CheckKilled(c, tracker) c.Check(err, gc.ErrorMatches, "cannot update environ config: SetConfig is broken") - context.CheckCallNames(c, "ModelConfig", "WatchForModelConfigChanges", "ModelConfig") + context.CheckCallNames(c, "ModelConfig", "CloudSpec", "WatchForModelConfigChanges", "ModelConfig") }) } @@ -218,6 +238,6 @@ } break } - context.CheckCallNames(c, "ModelConfig", "WatchForModelConfigChanges", "ModelConfig") + context.CheckCallNames(c, "ModelConfig", "CloudSpec", "WatchForModelConfigChanges", "ModelConfig") }) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/environ/fixture_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/environ/fixture_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/environ/fixture_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/environ/fixture_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,6 +8,7 @@ "github.com/juju/testing" gc "gopkg.in/check.v1" + names "gopkg.in/juju/names.v2" "github.com/juju/juju/environs" "github.com/juju/juju/environs/config" @@ -20,6 +21,7 @@ type fixture struct { watcherErr error observerErrs []error + cloud environs.CloudSpec initialConfig map[string]interface{} } @@ -27,6 +29,7 @@ watcher := newNotifyWatcher(fix.watcherErr) defer workertest.DirtyKill(c, watcher) context := &runContext{ + cloud: fix.cloud, config: newModelConfig(c, fix.initialConfig), watcher: watcher, } @@ -37,6 +40,7 @@ type runContext struct { mu sync.Mutex stub testing.Stub + cloud environs.CloudSpec config map[string]interface{} watcher *notifyWatcher } @@ -48,6 +52,17 @@ context.config = newModelConfig(c, extraAttrs) } +// CloudSpec is part of the environ.ConfigObserver interface. +func (context *runContext) CloudSpec(tag names.ModelTag) (environs.CloudSpec, error) { + context.mu.Lock() + defer context.mu.Unlock() + context.stub.AddCall("CloudSpec", tag) + if err := context.stub.NextErr(); err != nil { + return environs.CloudSpec{}, err + } + return context.cloud, nil +} + // ModelConfig is part of the environ.ConfigObserver interface. func (context *runContext) ModelConfig() (*config.Config, error) { context.mu.Lock() @@ -144,6 +159,6 @@ return nil } -func newMockEnviron(cfg *config.Config) (environs.Environ, error) { - return &mockEnviron{cfg: cfg}, nil +func newMockEnviron(args environs.OpenParams) (environs.Environ, error) { + return &mockEnviron{cfg: args.Config}, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/environ/wait_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/environ/wait_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/environ/wait_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/environ/wait_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,7 +11,6 @@ gc "gopkg.in/check.v1" "github.com/juju/juju/environs" - "github.com/juju/juju/environs/config" coretesting "github.com/juju/juju/testing" "github.com/juju/juju/worker/environ" "github.com/juju/juju/worker/workertest" @@ -103,11 +102,11 @@ "type": "unknown", }, } - newEnvironFunc := func(cfg *config.Config) (environs.Environ, error) { - if cfg.Type() == "unknown" { + newEnvironFunc := func(args environs.OpenParams) (environs.Environ, error) { + if args.Config.Type() == "unknown" { return nil, errors.NotValidf("config") } - return &mockEnviron{cfg: cfg}, nil + return &mockEnviron{cfg: args.Config}, nil } fix.Run(c, func(context *runContext) { abort := make(chan struct{}) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/firewaller/firewaller_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/firewaller/firewaller_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/firewaller/firewaller_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/firewaller/firewaller_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -60,7 +60,7 @@ c.Assert(s.st, gc.NotNil) // Create the firewaller API facade. - s.firewaller = s.st.Firewaller() + s.firewaller = apifirewaller.NewState(s.st) c.Assert(s.firewaller, gc.NotNil) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/introspection/doc.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/introspection/doc.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/introspection/doc.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/introspection/doc.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,12 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +// Package introspection defines the worker that can report internal agent state +// through the use of a machine local socket. +// +// The most interesting endpoints at this stage are: +// * `/debug/pprof/goroutine?debug=1` +// - prints out all the goroutines in the agent +// * `/debug/pprof/heap?debug=1` +// - prints out the heap profile +package introspection diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/introspection/manifold.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/introspection/manifold.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/introspection/manifold.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/introspection/manifold.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,53 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package introspection + +import ( + "runtime" + + "github.com/juju/errors" + "github.com/juju/loggo" + + "github.com/juju/juju/agent" + "github.com/juju/juju/worker" + "github.com/juju/juju/worker/dependency" +) + +var logger = loggo.GetLogger("juju.worker.introspection") + +// ManifoldConfig describes the resources required to construct the +// introspection worker. +type ManifoldConfig struct { + AgentName string + WorkerFunc func(Config) (worker.Worker, error) +} + +// Manifold returns a Manifold which encapsulates the introspection worker. +func Manifold(config ManifoldConfig) dependency.Manifold { + return dependency.Manifold{ + Inputs: []string{config.AgentName}, + Start: func(context dependency.Context) (worker.Worker, error) { + // Since the worker listens on an abstract domain socket, this + // is only available on linux. + if runtime.GOOS != "linux" { + logger.Debugf("introspection worker not supported on %q", runtime.GOOS) + return nil, dependency.ErrUninstall + } + + var a agent.Agent + if err := context.Get(config.AgentName, &a); err != nil { + return nil, errors.Trace(err) + } + + socketName := "jujud-" + a.CurrentConfig().Tag().String() + w, err := config.WorkerFunc(Config{ + SocketName: socketName, + }) + if err != nil { + return nil, errors.Trace(err) + } + return w, nil + }, + } +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/introspection/manifold_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/introspection/manifold_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/introspection/manifold_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/introspection/manifold_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,132 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package introspection_test + +import ( + "runtime" + + "github.com/juju/errors" + "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/juju/names.v2" + + "github.com/juju/juju/agent" + "github.com/juju/juju/api/base" + "github.com/juju/juju/worker" + "github.com/juju/juju/worker/dependency" + dt "github.com/juju/juju/worker/dependency/testing" + "github.com/juju/juju/worker/introspection" +) + +type ManifoldSuite struct { + testing.IsolationSuite + manifold dependency.Manifold + startErr error +} + +var _ = gc.Suite(&ManifoldSuite{}) + +func (s *ManifoldSuite) SetUpTest(c *gc.C) { + s.IsolationSuite.SetUpTest(c) + s.startErr = nil + s.manifold = introspection.Manifold(introspection.ManifoldConfig{ + AgentName: "agent-name", + WorkerFunc: func(cfg introspection.Config) (worker.Worker, error) { + if s.startErr != nil { + return nil, s.startErr + } + return &dummyWorker{config: cfg}, nil + }, + }) +} + +func (s *ManifoldSuite) TestInputs(c *gc.C) { + c.Check(s.manifold.Inputs, jc.DeepEquals, []string{"agent-name"}) +} + +func (s *ManifoldSuite) TestStartNonLinux(c *gc.C) { + if runtime.GOOS == "linux" { + c.Skip("testing for non-linux") + } + + context := dt.StubContext(nil, map[string]interface{}{ + "agent-name": dependency.ErrMissing, + }) + + worker, err := s.manifold.Start(context) + c.Check(worker, gc.IsNil) + c.Check(err, gc.Equals, dependency.ErrUninstall) +} + +func (s *ManifoldSuite) TestStartAgentMissing(c *gc.C) { + if runtime.GOOS != "linux" { + c.Skip("introspection worker not supported on non-linux") + } + + context := dt.StubContext(nil, map[string]interface{}{ + "agent-name": dependency.ErrMissing, + }) + + worker, err := s.manifold.Start(context) + c.Check(worker, gc.IsNil) + c.Check(errors.Cause(err), gc.Equals, dependency.ErrMissing) +} + +func (s *ManifoldSuite) TestStartError(c *gc.C) { + if runtime.GOOS != "linux" { + c.Skip("introspection worker not supported on non-linux") + } + + s.startErr = errors.New("boom") + context := dt.StubContext(nil, map[string]interface{}{ + "agent-name": &dummyAgent{}, + }) + + worker, err := s.manifold.Start(context) + c.Check(worker, gc.IsNil) + c.Check(err, gc.ErrorMatches, "boom") +} + +func (s *ManifoldSuite) TestStartSuccess(c *gc.C) { + if runtime.GOOS != "linux" { + c.Skip("introspection worker not supported on non-linux") + } + + context := dt.StubContext(nil, map[string]interface{}{ + "agent-name": &dummyAgent{}, + }) + + worker, err := s.manifold.Start(context) + c.Check(err, jc.ErrorIsNil) + dummy, ok := worker.(*dummyWorker) + c.Assert(ok, jc.IsTrue) + c.Assert(dummy.config.SocketName, gc.Equals, "jujud-machine-42") +} + +type dummyAgent struct { + agent.Agent +} + +func (*dummyAgent) CurrentConfig() agent.Config { + return &dummyConfig{} +} + +type dummyConfig struct { + agent.Config +} + +func (*dummyConfig) Tag() names.Tag { + return names.NewMachineTag("42") +} + +type dummyApiCaller struct { + base.APICaller +} + +type dummyWorker struct { + worker.Worker + + config introspection.Config +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/introspection/package_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/introspection/package_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/introspection/package_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/introspection/package_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,14 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package introspection + +import ( + "testing" + + gc "gopkg.in/check.v1" +) + +func TestSuite(t *testing.T) { + gc.TestingT(t) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/introspection/pprof/pprof.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/introspection/pprof/pprof.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/introspection/pprof/pprof.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/introspection/pprof/pprof.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,226 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package pprof is a fork of net/http/pprof modified to communicate +// over a unix socket. +// +// Changes from the original version +// +// - This fork does not automatically register itself with the default +// net/http ServeMux. +// - To start the pprof handler, see the Start method in socket.go. +// - For compatability with Go 1.2.1, support for obtaining trace data +// has been removed. +// +// --------------------------------------------------------------- +// +// Package pprof serves via its HTTP server runtime profiling data +// in the format expected by the pprof visualization tool. +// For more information about pprof, see +// http://code.google.com/p/google-perftools/. +// +// The package is typically only imported for the side effect of +// registering its HTTP handlers. +// The handled paths all begin with /debug/pprof/. +// +// To use pprof, link this package into your program: +// import _ "net/http/pprof" +// +// If your application is not already running an http server, you +// need to start one. Add "net/http" and "log" to your imports and +// the following code to your main function: +// +// go func() { +// log.Println(http.ListenAndServe("localhost:6060", nil)) +// }() +// +// Then use the pprof tool to look at the heap profile: +// +// go tool pprof http://localhost:6060/debug/pprof/heap +// +// Or to look at a 30-second CPU profile: +// +// go tool pprof http://localhost:6060/debug/pprof/profile +// +// Or to look at the goroutine blocking profile: +// +// go tool pprof http://localhost:6060/debug/pprof/block +// +// To view all available profiles, open http://localhost:6060/debug/pprof/ +// in your browser. +// +// For a study of the facility in action, visit +// +// https://blog.golang.org/2011/06/profiling-go-programs.html +// +package pprof + +import ( + "bufio" + "bytes" + "fmt" + "html/template" + "io" + "log" + "net/http" + "os" + "runtime" + "runtime/pprof" + "strconv" + "strings" + "time" +) + +// Cmdline responds with the running program's +// command line, with arguments separated by NUL bytes. +// The package initialization registers it as /debug/pprof/cmdline. +func Cmdline(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + fmt.Fprintf(w, strings.Join(os.Args, "\x00")) +} + +func sleep(w http.ResponseWriter, d time.Duration) { + var clientGone <-chan bool + if cn, ok := w.(http.CloseNotifier); ok { + clientGone = cn.CloseNotify() + } + select { + case <-time.After(d): + case <-clientGone: + } +} + +// Profile responds with the pprof-formatted cpu profile. +// The package initialization registers it as /debug/pprof/profile. +func Profile(w http.ResponseWriter, r *http.Request) { + sec, _ := strconv.ParseInt(r.FormValue("seconds"), 10, 64) + if sec == 0 { + sec = 30 + } + + // Set Content Type assuming StartCPUProfile will work, + // because if it does it starts writing. + w.Header().Set("Content-Type", "application/octet-stream") + if err := pprof.StartCPUProfile(w); err != nil { + // StartCPUProfile failed, so no writes yet. + // Can change header back to text content + // and send error code. + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "Could not enable CPU profiling: %s\n", err) + return + } + sleep(w, time.Duration(sec)*time.Second) + pprof.StopCPUProfile() +} + +// Symbol looks up the program counters listed in the request, +// responding with a table mapping program counters to function names. +// The package initialization registers it as /debug/pprof/symbol. +func Symbol(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + + // We have to read the whole POST body before + // writing any output. Buffer the output here. + var buf bytes.Buffer + + // We don't know how many symbols we have, but we + // do have symbol information. Pprof only cares whether + // this number is 0 (no symbols available) or > 0. + fmt.Fprintf(&buf, "num_symbols: 1\n") + + var b *bufio.Reader + if r.Method == "POST" { + b = bufio.NewReader(r.Body) + } else { + b = bufio.NewReader(strings.NewReader(r.URL.RawQuery)) + } + + for { + word, err := b.ReadSlice('+') + if err == nil { + word = word[0 : len(word)-1] // trim + + } + pc, _ := strconv.ParseUint(string(word), 0, 64) + if pc != 0 { + f := runtime.FuncForPC(uintptr(pc)) + if f != nil { + fmt.Fprintf(&buf, "%#x %s\n", pc, f.Name()) + } + } + + // Wait until here to check for err; the last + // symbol will have an err because it doesn't end in +. + if err != nil { + if err != io.EOF { + fmt.Fprintf(&buf, "reading request: %v\n", err) + } + break + } + } + + w.Write(buf.Bytes()) +} + +// Handler returns an HTTP handler that serves the named profile. +func Handler(name string) http.Handler { + return handler(name) +} + +type handler string + +func (name handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + debug, _ := strconv.Atoi(r.FormValue("debug")) + p := pprof.Lookup(string(name)) + if p == nil { + w.WriteHeader(404) + fmt.Fprintf(w, "Unknown profile: %s\n", name) + return + } + gc, _ := strconv.Atoi(r.FormValue("gc")) + if name == "heap" && gc > 0 { + runtime.GC() + } + p.WriteTo(w, debug) + return +} + +// Index responds with the pprof-formatted profile named by the request. +// For example, "/debug/pprof/heap" serves the "heap" profile. +// Index responds to a request for "/debug/pprof/" with an HTML page +// listing the available profiles. +func Index(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.Path, "/debug/pprof/") { + name := strings.TrimPrefix(r.URL.Path, "/debug/pprof/") + if name != "" { + handler(name).ServeHTTP(w, r) + return + } + } + + profiles := pprof.Profiles() + if err := indexTmpl.Execute(w, profiles); err != nil { + log.Print(err) + } +} + +var indexTmpl = template.Must(template.New("index").Parse(` + +/debug/pprof/ + + +/debug/pprof/
+
+profiles:
+ +{{range .}} +
{{.Count}}{{.Name}} +{{end}} +
+
+full goroutine stack dump
+ + +`)) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/introspection/socket.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/introspection/socket.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/introspection/socket.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/introspection/socket.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,98 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package introspection + +import ( + "net" + "net/http" + "runtime" + + "github.com/juju/errors" + "launchpad.net/tomb" + + "github.com/juju/juju/worker" + "github.com/juju/juju/worker/introspection/pprof" +) + +// Config describes the arguments required to create the introspection worker. +type Config struct { + SocketName string +} + +// Validate checks the config values to assert they are valid to create the worker. +func (c *Config) Validate() error { + if c.SocketName == "" { + return errors.NotValidf("empty SocketName") + } + return nil +} + +// socketListener is a worker and constructed with NewWorker. +type socketListener struct { + tomb tomb.Tomb + listener *net.UnixListener +} + +// NewWorker starts an http server listening on an abstract domain socket +// which will be created with the specified name. +func NewWorker(config Config) (worker.Worker, error) { + if err := config.Validate(); err != nil { + return nil, errors.Trace(err) + } + if runtime.GOOS != "linux" { + return nil, errors.NotSupportedf("os %q", runtime.GOOS) + } + + path := "@" + config.SocketName + addr, err := net.ResolveUnixAddr("unix", path) + if err != nil { + return nil, errors.Annotate(err, "unable to resolve unix socket") + } + + l, err := net.ListenUnix("unix", addr) + if err != nil { + return nil, errors.Annotate(err, "unable to listen on unix socket") + } + logger.Debugf("introspection worker listening on %q", path) + + w := &socketListener{ + listener: l, + } + go w.serve() + go w.run() + return w, nil +} + +func (w *socketListener) serve() { + mux := http.NewServeMux() + mux.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index)) + mux.Handle("/debug/pprof/cmdline", http.HandlerFunc(pprof.Cmdline)) + mux.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile)) + mux.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol)) + + srv := http.Server{ + Handler: mux, + } + + logger.Debugf("stats worker now servering") + srv.Serve(w.listener) + logger.Debugf("stats worker servering finished") +} + +func (w *socketListener) run() { + <-w.tomb.Dying() + logger.Debugf("stats worker closing listener") + w.listener.Close() + w.tomb.Done() +} + +// Kill implements worker.Worker. +func (w *socketListener) Kill() { + w.tomb.Kill(nil) +} + +// Wait implements worker.Worker. +func (w *socketListener) Wait() error { + return w.tomb.Wait() +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/introspection/socket_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/introspection/socket_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/introspection/socket_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/introspection/socket_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,115 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package introspection_test + +import ( + "bufio" + "bytes" + "fmt" + "io/ioutil" + "net" + "regexp" + "runtime" + + "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/worker" + "github.com/juju/juju/worker/introspection" + "github.com/juju/juju/worker/workertest" +) + +type suite struct { + testing.IsolationSuite +} + +var _ = gc.Suite(&suite{}) + +func (s *suite) TestConfigValidation(c *gc.C) { + w, err := introspection.NewWorker(introspection.Config{}) + c.Check(w, gc.IsNil) + c.Assert(err, gc.ErrorMatches, "empty SocketName not valid") +} + +func (s *suite) TestStartStop(c *gc.C) { + if runtime.GOOS != "linux" { + c.Skip("introspection worker not supported on non-linux") + } + + w, err := introspection.NewWorker(introspection.Config{ + SocketName: "introspection-test", + }) + c.Assert(err, jc.ErrorIsNil) + workertest.CheckKill(c, w) +} + +type introspectionSuite struct { + testing.IsolationSuite + + name string + worker worker.Worker +} + +var _ = gc.Suite(&introspectionSuite{}) + +func (s *introspectionSuite) SetUpTest(c *gc.C) { + if runtime.GOOS != "linux" { + c.Skip("introspection worker not supported on non-linux") + } + + s.IsolationSuite.SetUpTest(c) + + s.name = "introspection-test" + w, err := introspection.NewWorker(introspection.Config{ + SocketName: s.name, + }) + c.Assert(err, jc.ErrorIsNil) + s.worker = w + s.AddCleanup(func(c *gc.C) { + workertest.CheckKill(c, w) + }) +} + +func (s *introspectionSuite) call(c *gc.C, url string) []byte { + path := "@" + s.name + conn, err := net.Dial("unix", path) + c.Assert(err, jc.ErrorIsNil) + defer conn.Close() + + _, err = fmt.Fprintf(conn, "GET %s HTTP/1.0\r\n\r\n", url) + c.Assert(err, jc.ErrorIsNil) + + buf, err := ioutil.ReadAll(conn) + c.Assert(err, jc.ErrorIsNil) + return buf +} + +func (s *introspectionSuite) TestCmdLine(c *gc.C) { + buf := s.call(c, "/debug/pprof/cmdline") + c.Assert(buf, gc.NotNil) + matches(c, buf, ".*github.com/juju/juju/worker/introspection/_test/introspection.test") +} + +func (s *introspectionSuite) TestGoroutineProfile(c *gc.C) { + buf := s.call(c, "/debug/pprof/goroutine") + c.Assert(buf, gc.NotNil) + matches(c, buf, `^goroutine profile: total \d+`) +} + +// matches fails if regex is not found in the contents of b. +// b is expected to be the response from the pprof http server, and will +// contain some HTTP preamble that should be ignored. +func matches(c *gc.C, b []byte, regex string) { + re, err := regexp.Compile(regex) + c.Assert(err, jc.ErrorIsNil) + r := bytes.NewReader(b) + sc := bufio.NewScanner(r) + for sc.Scan() { + if re.MatchString(sc.Text()) { + return + } + } + c.Fatalf("%q did not match regex %q", string(b), regex) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/logforwarder/logforwarder_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/logforwarder/logforwarder_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/logforwarder/logforwarder_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/logforwarder/logforwarder_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -205,7 +205,8 @@ s.stream.waitBeforeNext(c) s.stream.waitAfterNext(c) s.sender.waitAfterSend(c) - s.stub.CheckCallNames(c, "Close", "Next", "Send") + // Check that the config change has been picked up and + // that the second record is sent. rec2.Message = "send to 10.0.0.2" s.stub.CheckCall(c, 2, "Send", []logfwd.Record{rec2}) s.stub.ResetCalls() diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/logger/logger.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/logger/logger.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/logger/logger.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/logger/logger.go 2016-08-16 08:56:25.000000000 +0000 @@ -48,7 +48,7 @@ } else { if loggingConfig != logger.lastConfig { log.Debugf("reconfiguring logging from %q to %q", logger.lastConfig, loggingConfig) - loggo.ResetLoggers() + loggo.DefaultContext().ResetLoggerLevels() if err := loggo.ConfigureLoggers(loggingConfig); err != nil { // This shouldn't occur as the loggingConfig should be // validated by the original Config before it gets here. diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/logger/logger_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/logger/logger_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/logger/logger_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/logger/logger_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -94,7 +94,7 @@ initial := "=DEBUG;wibble=ERROR" c.Assert(expected, gc.Not(gc.Equals), initial) - loggo.ResetLoggers() + loggo.DefaultContext().ResetLoggerLevels() err = loggo.ConfigureLoggers(initial) c.Assert(err, jc.ErrorIsNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/logsender/bufferedlogwriter.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/logsender/bufferedlogwriter.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/logsender/bufferedlogwriter.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/logsender/bufferedlogwriter.go 2016-08-16 08:56:25.000000000 +0000 @@ -36,7 +36,7 @@ // it with Loggo and returns its output channel. func InstallBufferedLogWriter(maxLen int) (LogRecordCh, error) { writer := NewBufferedLogWriter(maxLen) - err := loggo.RegisterWriter(writerName, writer, loggo.TRACE) + err := loggo.RegisterWriter(writerName, writer) if err != nil { return nil, errors.Annotate(err, "failed to set up log buffering") } @@ -46,7 +46,7 @@ // UninstallBufferedLogWriter removes the BufferedLogWriter previously // installed by InstallBufferedLogWriter and closes it. func UninstallBufferedLogWriter() error { - writer, _, err := loggo.RemoveWriter(writerName) + writer, err := loggo.RemoveWriter(writerName) if err != nil { return errors.Annotate(err, "failed to uninstall log buffering") } @@ -122,13 +122,13 @@ } // Write sends a new log message to the writer. This implements the loggo.Writer interface. -func (w *BufferedLogWriter) Write(level loggo.Level, module, filename string, line int, ts time.Time, message string) { +func (w *BufferedLogWriter) Write(entry loggo.Entry) { w.in <- &LogRecord{ - Time: ts, - Module: module, - Location: fmt.Sprintf("%s:%d", filepath.Base(filename), line), - Level: level, - Message: message, + Time: entry.Timestamp, + Module: entry.Module, + Location: fmt.Sprintf("%s:%d", filepath.Base(entry.Filename), entry.Line), + Level: entry.Level, + Message: entry.Message, } } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/logsender/bufferedlogwriter_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/logsender/bufferedlogwriter_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/logsender/bufferedlogwriter_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/logsender/bufferedlogwriter_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -55,13 +55,14 @@ now := time.Now() for i := 0; i < numMessages; i++ { s.writer.Write( - loggo.Level(i), - fmt.Sprintf("module%d", i), - fmt.Sprintf("filename%d", i), - i, // line number - now.Add(time.Duration(i)), - fmt.Sprintf("message%d", i), - ) + loggo.Entry{ + Level: loggo.Level(i), + Module: fmt.Sprintf("module%d", i), + Filename: fmt.Sprintf("filename%d", i), + Line: i, + Timestamp: now.Add(time.Duration(i)), + Message: fmt.Sprintf("message%d", i), + }) } for i := 0; i < numMessages; i++ { @@ -77,7 +78,15 @@ func (s *bufferedLogWriterSuite) TestLimiting(c *gc.C) { write := func(msgNum int) { - s.writer.Write(loggo.INFO, "module", "filename", 42, time.Now(), fmt.Sprintf("log%d", msgNum)) + s.writer.Write( + loggo.Entry{ + Level: loggo.INFO, + Module: "module", + Filename: "filename", + Line: 42, + Timestamp: time.Now(), + Message: fmt.Sprintf("log%d", msgNum), + }) } expect := func(msgNum, dropped int) { @@ -164,7 +173,15 @@ func (s *bufferedLogWriterSuite) writeAndReceive(c *gc.C) { now := time.Now() - s.writer.Write(loggo.INFO, "module", "filename", 99, now, "message") + s.writer.Write( + loggo.Entry{ + Level: loggo.INFO, + Module: "module", + Filename: "filename", + Line: 99, + Timestamp: now, + Message: "message", + }) c.Assert(*s.receiveOne(c), gc.DeepEquals, logsender.LogRecord{ Time: now, Module: "module", diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/meterstatus/runner.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/meterstatus/runner.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/meterstatus/runner.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/meterstatus/runner.go 2016-08-16 08:56:25.000000000 +0000 @@ -48,10 +48,12 @@ Delay: 250 * time.Millisecond, Cancel: interrupt, } + logger.Debugf("acquire lock %q for meter status hook execution", w.machineLockName) releaser, err := mutex.Acquire(spec) if err != nil { return nil, errors.Trace(err) } + logger.Debugf("lock %q acquired", w.machineLockName) return releaser, nil } @@ -68,6 +70,8 @@ if err != nil { return errors.Annotate(err, "failed to acquire machine lock") } + // Defer the logging first so it is executed after the Release. LIFO. + defer logger.Debugf("release lock %q for meter status hook execution", w.machineLockName) defer releaser.Release() return r.RunHook(string(hooks.MeterStatusChanged)) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/migrationflag/worker.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/migrationflag/worker.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/migrationflag/worker.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/migrationflag/worker.go 2016-08-16 08:56:25.000000000 +0000 @@ -25,9 +25,10 @@ // Predicate defines a predicate. type Predicate func(migration.Phase) bool -// IsNone is a predicate. -func IsNone(phase migration.Phase) bool { - return phase == migration.NONE +// IsTerminal returns true when the given phase means a migration has +// finished (successfully or otherwise). +func IsTerminal(phase migration.Phase) bool { + return phase.IsTerminal() } // Config holds the dependencies and configuration for a Worker. diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/migrationflag/worker_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/migrationflag/worker_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/migrationflag/worker_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/migrationflag/worker_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -130,3 +130,22 @@ workertest.CleanKill(c, worker) checkCalls(c, stub, "Phase", "Watch", "Phase", "Phase", "Phase") } + +func (*WorkerSuite) TestIsTerminal(c *gc.C) { + tests := []struct { + phase migration.Phase + expected bool + }{ + {migration.QUIESCE, false}, + {migration.SUCCESS, false}, + {migration.ABORT, false}, + {migration.NONE, true}, + {migration.UNKNOWN, true}, + {migration.ABORTDONE, true}, + {migration.DONE, true}, + } + for _, t := range tests { + c.Check(migrationflag.IsTerminal(t.phase), gc.Equals, t.expected, + gc.Commentf("for %s", t.phase)) + } +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/migrationmaster/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/migrationmaster/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/migrationmaster/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/migrationmaster/export_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,7 +0,0 @@ -// Copyright 2016 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package migrationmaster - -var ApiOpen = &apiOpen -var TempSuccessSleep = &tempSuccessSleep diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/migrationmaster/manifoldconfig_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/migrationmaster/manifoldconfig_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/migrationmaster/manifoldconfig_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/migrationmaster/manifoldconfig_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,79 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package migrationmaster_test + +import ( + "github.com/juju/errors" + "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + "github.com/juju/utils/clock" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/api/base" + "github.com/juju/juju/worker" + "github.com/juju/juju/worker/migrationmaster" +) + +type ManifoldConfigSuite struct { + testing.IsolationSuite + config migrationmaster.ManifoldConfig +} + +var _ = gc.Suite(&ManifoldConfigSuite{}) + +func (s *ManifoldConfigSuite) SetUpTest(c *gc.C) { + s.IsolationSuite.SetUpTest(c) + s.config = s.validConfig() +} + +func (s *ManifoldConfigSuite) validConfig() migrationmaster.ManifoldConfig { + return migrationmaster.ManifoldConfig{ + AgentName: "agent", + APICallerName: "api-caller", + FortressName: "fortress", + Clock: struct{ clock.Clock }{}, + NewFacade: func(base.APICaller) (migrationmaster.Facade, error) { return nil, nil }, + NewWorker: func(migrationmaster.Config) (worker.Worker, error) { return nil, nil }, + } +} + +func (s *ManifoldConfigSuite) TestValid(c *gc.C) { + c.Check(s.config.Validate(), jc.ErrorIsNil) +} + +func (s *ManifoldConfigSuite) TestMissingAgentName(c *gc.C) { + s.config.AgentName = "" + s.checkNotValid(c, "empty AgentName not valid") +} + +func (s *ManifoldConfigSuite) TestMissingAPICallerName(c *gc.C) { + s.config.APICallerName = "" + s.checkNotValid(c, "empty APICallerName not valid") +} + +func (s *ManifoldConfigSuite) TestMissingFortressName(c *gc.C) { + s.config.FortressName = "" + s.checkNotValid(c, "empty FortressName not valid") +} + +func (s *ManifoldConfigSuite) TestMissingClock(c *gc.C) { + s.config.Clock = nil + s.checkNotValid(c, "nil Clock not valid") +} + +func (s *ManifoldConfigSuite) TestMissingNewFacade(c *gc.C) { + s.config.NewFacade = nil + s.checkNotValid(c, "nil NewFacade not valid") +} + +func (s *ManifoldConfigSuite) TestMissingNewWorker(c *gc.C) { + s.config.NewWorker = nil + s.checkNotValid(c, "nil NewWorker not valid") +} + +func (s *ManifoldConfigSuite) checkNotValid(c *gc.C, expect string) { + err := s.config.Validate() + c.Check(err, gc.ErrorMatches, expect) + c.Check(err, jc.Satisfies, errors.IsNotValid) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/migrationmaster/manifold.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/migrationmaster/manifold.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/migrationmaster/manifold.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/migrationmaster/manifold.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,7 +5,12 @@ import ( "github.com/juju/errors" + "github.com/juju/utils/clock" + + "github.com/juju/juju/agent" + "github.com/juju/juju/api" "github.com/juju/juju/api/base" + "github.com/juju/juju/migration" "github.com/juju/juju/worker" "github.com/juju/juju/worker/dependency" "github.com/juju/juju/worker/fortress" @@ -14,15 +19,20 @@ // ManifoldConfig defines the names of the manifolds on which a // Worker manifold will depend. type ManifoldConfig struct { + AgentName string APICallerName string FortressName string + Clock clock.Clock NewFacade func(base.APICaller) (Facade, error) NewWorker func(Config) (worker.Worker, error) } -// validate is called by start to check for bad configuration. -func (config ManifoldConfig) validate() error { +// Validate is called by start to check for bad configuration. +func (config ManifoldConfig) Validate() error { + if config.AgentName == "" { + return errors.NotValidf("empty AgentName") + } if config.APICallerName == "" { return errors.NotValidf("empty APICallerName") } @@ -35,29 +45,44 @@ if config.NewWorker == nil { return errors.NotValidf("nil NewWorker") } + if config.Clock == nil { + return errors.NotValidf("nil Clock") + } return nil } // start is a StartFunc for a Worker manifold. func (config ManifoldConfig) start(context dependency.Context) (worker.Worker, error) { - if err := config.validate(); err != nil { + if err := config.Validate(); err != nil { return nil, errors.Trace(err) } - var apiCaller base.APICaller - if err := context.Get(config.APICallerName, &apiCaller); err != nil { + + var agent agent.Agent + if err := context.Get(config.AgentName, &agent); err != nil { + return nil, errors.Trace(err) + } + var apiConn api.Connection + if err := context.Get(config.APICallerName, &apiConn); err != nil { return nil, errors.Trace(err) } var guard fortress.Guard if err := context.Get(config.FortressName, &guard); err != nil { return nil, errors.Trace(err) } - facade, err := config.NewFacade(apiCaller) + facade, err := config.NewFacade(apiConn) if err != nil { return nil, errors.Trace(err) } + apiClient := apiConn.Client() worker, err := config.NewWorker(Config{ - Facade: facade, - Guard: guard, + ModelUUID: agent.CurrentConfig().Model().Id(), + Facade: facade, + Guard: guard, + APIOpen: api.Open, + UploadBinaries: migration.UploadBinaries, + CharmDownloader: apiClient, + ToolsDownloader: apiClient, + Clock: config.Clock, }) if err != nil { return nil, errors.Trace(err) @@ -65,10 +90,31 @@ return worker, nil } +func errorFilter(err error) error { + switch err { + case ErrMigrated: + // If the model has migrated, the migrationmaster should no + // longer be running. + return dependency.ErrUninstall + case ErrInactive: + // If the migration is no longer active, restart the + // migrationmaster immediately so it can wait for the next + // attempt. + return dependency.ErrBounce + default: + return err + } +} + // Manifold packages a Worker for use in a dependency.Engine. func Manifold(config ManifoldConfig) dependency.Manifold { return dependency.Manifold{ - Inputs: []string{config.APICallerName, config.FortressName}, + Inputs: []string{ + config.AgentName, + config.APICallerName, + config.FortressName, + }, Start: config.start, + Filter: errorFilter, } } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/migrationmaster/shim.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/migrationmaster/shim.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/migrationmaster/shim.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/migrationmaster/shim.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,11 +7,12 @@ "github.com/juju/errors" "github.com/juju/juju/api/base" "github.com/juju/juju/api/migrationmaster" + "github.com/juju/juju/api/watcher" "github.com/juju/juju/worker" ) func NewFacade(apiCaller base.APICaller) (Facade, error) { - facade := migrationmaster.NewClient(apiCaller) + facade := migrationmaster.NewClient(apiCaller, watcher.NewNotifyWatcher) return facade, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/migrationmaster/validate_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/migrationmaster/validate_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/migrationmaster/validate_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/migrationmaster/validate_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,11 +5,15 @@ import ( "github.com/juju/errors" - "github.com/juju/juju/worker/fortress" - "github.com/juju/juju/worker/migrationmaster" "github.com/juju/testing" jc "github.com/juju/testing/checkers" + "github.com/juju/utils/clock" gc "gopkg.in/check.v1" + + "github.com/juju/juju/api" + "github.com/juju/juju/migration" + "github.com/juju/juju/worker/fortress" + "github.com/juju/juju/worker/migrationmaster" ) type ValidateSuite struct { @@ -23,6 +27,12 @@ c.Check(err, jc.ErrorIsNil) } +func (*ValidateSuite) TestMissingModelUUID(c *gc.C) { + config := validConfig() + config.ModelUUID = "" + checkNotValid(c, config, "empty ModelUUID not valid") +} + func (*ValidateSuite) TestMissingGuard(c *gc.C) { config := validConfig() config.Guard = nil @@ -35,10 +45,46 @@ checkNotValid(c, config, "nil Facade not valid") } +func (*ValidateSuite) TestMissingAPIOpen(c *gc.C) { + config := validConfig() + config.APIOpen = nil + checkNotValid(c, config, "nil APIOpen not valid") +} + +func (*ValidateSuite) TestMissingUploadBinaries(c *gc.C) { + config := validConfig() + config.UploadBinaries = nil + checkNotValid(c, config, "nil UploadBinaries not valid") +} + +func (*ValidateSuite) TestMissingCharmDownloader(c *gc.C) { + config := validConfig() + config.CharmDownloader = nil + checkNotValid(c, config, "nil CharmDownloader not valid") +} + +func (*ValidateSuite) TestMissingToolsDownloader(c *gc.C) { + config := validConfig() + config.ToolsDownloader = nil + checkNotValid(c, config, "nil ToolsDownloader not valid") +} + +func (*ValidateSuite) TestMissingClock(c *gc.C) { + config := validConfig() + config.Clock = nil + checkNotValid(c, config, "nil Clock not valid") +} + func validConfig() migrationmaster.Config { return migrationmaster.Config{ - Guard: struct{ fortress.Guard }{}, - Facade: struct{ migrationmaster.Facade }{}, + ModelUUID: "uuid", + Guard: struct{ fortress.Guard }{}, + Facade: struct{ migrationmaster.Facade }{}, + APIOpen: func(*api.Info, api.DialOpts) (api.Connection, error) { return nil, nil }, + UploadBinaries: func(migration.UploadBinariesConfig) error { return nil }, + CharmDownloader: struct{ migration.CharmDownloader }{}, + ToolsDownloader: struct{ migration.ToolsDownloader }{}, + Clock: struct{ clock.Clock }{}, } } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/migrationmaster/worker.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/migrationmaster/worker.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/migrationmaster/worker.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/migrationmaster/worker.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,66 +4,122 @@ package migrationmaster import ( + "fmt" + "strings" "time" "github.com/juju/errors" "github.com/juju/loggo" + "github.com/juju/utils/clock" + "gopkg.in/juju/names.v2" "github.com/juju/juju/api" - "github.com/juju/juju/api/migrationmaster" "github.com/juju/juju/api/migrationtarget" "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/core/migration" + coremigration "github.com/juju/juju/core/migration" + "github.com/juju/juju/migration" "github.com/juju/juju/watcher" "github.com/juju/juju/worker/catacomb" - "github.com/juju/juju/worker/dependency" "github.com/juju/juju/worker/fortress" ) var ( - logger = loggo.GetLogger("juju.worker.migrationmaster") - apiOpen = api.Open - tempSuccessSleep = 10 * time.Second - - // ErrDoneForNow indicates a temporary issue was encountered and - // that the worker should restart and retry. - ErrDoneForNow = errors.New("done for now") + // ErrInactive is returned when the migration is no longer active + // (probably aborted). In this case the migrationmaster should be + // restarted so that it can wait for the next migration attempt. + ErrInactive = errors.New("migration is no longer active") + + // ErrMigrated is returned when the model has migrated to another + // server. The migrationmaster should not be restarted again in + // this case. + ErrMigrated = errors.New("model has migrated") +) + +const ( + // maxMinionWait is the maximum time that the migrationmaster will + // wait for minions to report back regarding a given migration + // phase. + maxMinionWait = 15 * time.Minute + + // minionWaitLogInterval is the time between progress update + // messages, while the migrationmaster is waiting for reports from + // minions. + minionWaitLogInterval = 30 * time.Second ) // Facade exposes controller functionality to a Worker. type Facade interface { - // Watch returns a watcher which reports when a migration is // active for the model associated with the API connection. Watch() (watcher.NotifyWatcher, error) // GetMigrationStatus returns the details and progress of the // latest model migration. - GetMigrationStatus() (migrationmaster.MigrationStatus, error) + GetMigrationStatus() (coremigration.MigrationStatus, error) // SetPhase updates the phase of the currently active model // migration. - SetPhase(migration.Phase) error + SetPhase(coremigration.Phase) error + + // SetStatusMessage sets a human readable message regarding the + // progress of a migration. + SetStatusMessage(string) error // Export returns a serialized representation of the model // associated with the API connection. - Export() ([]byte, error) + Export() (coremigration.SerializedModel, error) + + // Reap removes all documents of the model associated with the API + // connection. + Reap() error + + // WatchMinionReports returns a watcher which reports when a migration + // minion has made a report for the current migration phase. + WatchMinionReports() (watcher.NotifyWatcher, error) + + // GetMinionReports returns details of the reports made by migration + // minions to the controller for the current migration phase. + GetMinionReports() (coremigration.MinionReports, error) } // Config defines the operation of a Worker. type Config struct { - Facade Facade - Guard fortress.Guard + ModelUUID string + Facade Facade + Guard fortress.Guard + APIOpen func(*api.Info, api.DialOpts) (api.Connection, error) + UploadBinaries func(migration.UploadBinariesConfig) error + CharmDownloader migration.CharmDownloader + ToolsDownloader migration.ToolsDownloader + Clock clock.Clock } // Validate returns an error if config cannot drive a Worker. func (config Config) Validate() error { + if config.ModelUUID == "" { + return errors.NotValidf("empty ModelUUID") + } if config.Facade == nil { return errors.NotValidf("nil Facade") } if config.Guard == nil { return errors.NotValidf("nil Guard") } + if config.APIOpen == nil { + return errors.NotValidf("nil APIOpen") + } + if config.UploadBinaries == nil { + return errors.NotValidf("nil UploadBinaries") + } + if config.CharmDownloader == nil { + return errors.NotValidf("nil CharmDownloader") + } + if config.ToolsDownloader == nil { + return errors.NotValidf("nil ToolsDownloader") + } + if config.Clock == nil { + return errors.NotValidf("nil Clock") + } return nil } @@ -72,8 +128,17 @@ if err := config.Validate(); err != nil { return nil, errors.Trace(err) } + + // Soon we will get model specific logs generated in the + // controller logged against the model. Until then, distinguish + // the logs from different migrationmaster insteads using the + // model UUID suffix. + loggerName := "juju.worker.migrationmaster:" + config.ModelUUID[len(config.ModelUUID)-6:] + logger := loggo.GetLogger(loggerName) + w := &Worker{ config: config, + logger: logger, } err := catacomb.Invoke(catacomb.Plan{ Site: &w.catacomb, @@ -90,6 +155,7 @@ type Worker struct { catacomb catacomb.Catacomb config Config + logger loggo.Logger } // Kill implements worker.Worker. @@ -115,30 +181,25 @@ return errors.Trace(err) } - // TODO(mjs) - log messages should indicate the model name and - // UUID. Independent logger per migration master instance? - phase := status.Phase for { var err error switch phase { - case migration.QUIESCE: + case coremigration.QUIESCE: phase, err = w.doQUIESCE() - case migration.READONLY: - phase, err = w.doREADONLY() - case migration.PRECHECK: + case coremigration.PRECHECK: phase, err = w.doPRECHECK() - case migration.IMPORT: - phase, err = w.doIMPORT(status.TargetInfo) - case migration.VALIDATION: + case coremigration.IMPORT: + phase, err = w.doIMPORT(status.TargetInfo, status.ModelUUID) + case coremigration.VALIDATION: phase, err = w.doVALIDATION(status.TargetInfo, status.ModelUUID) - case migration.SUCCESS: - phase, err = w.doSUCCESS() - case migration.LOGTRANSFER: + case coremigration.SUCCESS: + phase, err = w.doSUCCESS(status) + case coremigration.LOGTRANSFER: phase, err = w.doLOGTRANSFER() - case migration.REAP: + case coremigration.REAP: phase, err = w.doREAP() - case migration.ABORT: + case coremigration.ABORT: phase, err = w.doABORT(status.TargetInfo, status.ModelUUID) default: return errors.Errorf("unknown phase: %v [%d]", phase.String(), phase) @@ -148,7 +209,7 @@ // A phase handler should only return an error if the // migration master should exit. In the face of other // errors the handler should log the problem and then - // return the appropriate error phases to transition to - + // return the appropriate error phase to transition to - // i.e. ABORT or REAPFAILED) return errors.Trace(err) } @@ -157,18 +218,18 @@ return w.catacomb.ErrDying() } - logger.Infof("setting migration phase to %s", phase) + w.logger.Infof("setting migration phase to %s", phase) if err := w.config.Facade.SetPhase(phase); err != nil { return errors.Annotate(err, "failed to set phase") } + status.Phase = phase if modelHasMigrated(phase) { - // TODO(mjs) - use manifold Filter so that the dep engine - // error types aren't required here. - return dependency.ErrUninstall + return ErrMigrated } else if phase.IsTerminal() { - // Some other terminal phase, exit and try again. - return ErrDoneForNow + // Some other terminal phase (aborted), exit and try + // again. + return ErrInactive } } } @@ -182,61 +243,103 @@ } } -func (w *Worker) doQUIESCE() (migration.Phase, error) { +func (w *Worker) setInfoStatus(s string, a ...interface{}) { + w.setStatusAndLog(w.logger.Infof, s, a...) +} + +func (w *Worker) setErrorStatus(s string, a ...interface{}) { + w.setStatusAndLog(w.logger.Errorf, s, a...) +} + +func (w *Worker) setStatusAndLog(log func(string, ...interface{}), s string, a ...interface{}) { + message := fmt.Sprintf(s, a...) + log(message) + if err := w.setStatus(message); err != nil { + // Setting status isn't critical. If it fails, just logging + // the problem here and not passing it upstream makes things a + // lot clearer in the caller. + w.logger.Errorf("%s", err) + } +} + +func (w *Worker) setStatus(message string) error { + err := w.config.Facade.SetStatusMessage(message) + return errors.Annotate(err, "failed to set status message") +} + +func (w *Worker) doQUIESCE() (coremigration.Phase, error) { // TODO(mjs) - Wait for all agents to report back. - return migration.READONLY, nil + // w.setInfoStatus("model quiescing to readonly mode") + return coremigration.PRECHECK, nil } -func (w *Worker) doREADONLY() (migration.Phase, error) { +func (w *Worker) doPRECHECK() (coremigration.Phase, error) { // TODO(mjs) - To be implemented. - return migration.PRECHECK, nil + // w.setInfoStatus("performing prechecks") + return coremigration.IMPORT, nil } -func (w *Worker) doPRECHECK() (migration.Phase, error) { - // TODO(mjs) - To be implemented. - return migration.IMPORT, nil +func (w *Worker) doIMPORT(targetInfo coremigration.TargetInfo, modelUUID string) (coremigration.Phase, error) { + err := w.transferModel(targetInfo, modelUUID) + if err != nil { + w.setErrorStatus("model export failed: %v", err) + return coremigration.ABORT, nil + } + return coremigration.VALIDATION, nil } -func (w *Worker) doIMPORT(targetInfo migration.TargetInfo) (migration.Phase, error) { - logger.Infof("exporting model") - bytes, err := w.config.Facade.Export() +func (w *Worker) transferModel(targetInfo coremigration.TargetInfo, modelUUID string) error { + w.setInfoStatus("exporting model") + serialized, err := w.config.Facade.Export() if err != nil { - logger.Errorf("model export failed: %v", err) - return migration.ABORT, nil + return errors.Annotate(err, "model export failed") } - logger.Infof("opening API connection to target controller") - conn, err := openAPIConn(targetInfo) + w.setInfoStatus("importing model into target controller") + conn, err := w.openAPIConn(targetInfo) if err != nil { - logger.Errorf("failed to connect to target controller: %v", err) - return migration.ABORT, nil + return errors.Annotate(err, "failed to connect to target controller") } defer conn.Close() - - logger.Infof("importing model into target controller") targetClient := migrationtarget.NewClient(conn) - err = targetClient.Import(bytes) + err = targetClient.Import(serialized.Bytes) if err != nil { - logger.Errorf("failed to import model into target controller: %v", err) - return migration.ABORT, nil + return errors.Annotate(err, "failed to import model into target controller") } - return migration.VALIDATION, nil + w.setInfoStatus("uploading model binaries into target controller") + targetModelConn, err := w.openAPIConnForModel(targetInfo, modelUUID) + if err != nil { + return errors.Annotate(err, "failed to open connection to target model") + } + defer targetModelConn.Close() + targetModelClient := targetModelConn.Client() + err = w.config.UploadBinaries(migration.UploadBinariesConfig{ + Charms: serialized.Charms, + CharmDownloader: w.config.CharmDownloader, + CharmUploader: targetModelClient, + Tools: serialized.Tools, + ToolsDownloader: w.config.ToolsDownloader, + ToolsUploader: targetModelClient, + }) + return errors.Annotate(err, "failed migration binaries") } -func (w *Worker) doVALIDATION(targetInfo migration.TargetInfo, modelUUID string) (migration.Phase, error) { +func (w *Worker) doVALIDATION(targetInfo coremigration.TargetInfo, modelUUID string) (coremigration.Phase, error) { // TODO(mjs) - Wait for all agents to report back. // Once all agents have validated, activate the model. - err := activateModel(targetInfo, modelUUID) + err := w.activateModel(targetInfo, modelUUID) if err != nil { - return migration.ABORT, nil + w.setErrorStatus("model activation failed %v", err) + return coremigration.ABORT, nil } - return migration.SUCCESS, nil + return coremigration.SUCCESS, nil } -func activateModel(targetInfo migration.TargetInfo, modelUUID string) error { - conn, err := openAPIConn(targetInfo) +func (w *Worker) activateModel(targetInfo coremigration.TargetInfo, modelUUID string) error { + w.setInfoStatus("activating model in target controller") + conn, err := w.openAPIConn(targetInfo) if err != nil { return errors.Trace(err) } @@ -247,35 +350,47 @@ return errors.Trace(err) } -func (w *Worker) doSUCCESS() (migration.Phase, error) { - // XXX(mjs) - this is a horrible hack, which helps to ensure that - // minions will see the SUCCESS state (due to watcher event - // coalescing). It will go away soon. - time.Sleep(tempSuccessSleep) - return migration.LOGTRANSFER, nil +func (w *Worker) doSUCCESS(status coremigration.MigrationStatus) (coremigration.Phase, error) { + err := w.waitForMinions(status, waitForAll, "successful") + switch errors.Cause(err) { + case nil, errMinionReportFailed, errMinionReportTimeout: + // There's no turning back from SUCCESS - any problems should + // have been picked up in VALIDATION. After the minion wait in + // the SUCCESS phase, the migration can only proceed to + // LOGTRANSFER. + return coremigration.LOGTRANSFER, nil + default: + return coremigration.SUCCESS, errors.Trace(err) + } } -func (w *Worker) doLOGTRANSFER() (migration.Phase, error) { +func (w *Worker) doLOGTRANSFER() (coremigration.Phase, error) { // TODO(mjs) - To be implemented. - return migration.REAP, nil + // w.setInfoStatus("successful: transferring logs to target controller") + return coremigration.REAP, nil } -func (w *Worker) doREAP() (migration.Phase, error) { - // TODO(mjs) - To be implemented. - return migration.DONE, nil +func (w *Worker) doREAP() (coremigration.Phase, error) { + w.setInfoStatus("successful: removing model from source controller") + err := w.config.Facade.Reap() + if err != nil { + return coremigration.REAPFAILED, errors.Trace(err) + } + return coremigration.DONE, nil } -func (w *Worker) doABORT(targetInfo migration.TargetInfo, modelUUID string) (migration.Phase, error) { - if err := removeImportedModel(targetInfo, modelUUID); err != nil { +func (w *Worker) doABORT(targetInfo coremigration.TargetInfo, modelUUID string) (coremigration.Phase, error) { + w.setInfoStatus("aborted: removing model from target controller") + if err := w.removeImportedModel(targetInfo, modelUUID); err != nil { // This isn't fatal. Removing the imported model is a best - // efforts attempt. - logger.Errorf("failed to reverse model import: %v", err) + // efforts attempt so just report the error and proceed. + w.setErrorStatus("failed to remove model from target controller: %v", err) } - return migration.ABORTDONE, nil + return coremigration.ABORTDONE, nil } -func removeImportedModel(targetInfo migration.TargetInfo, modelUUID string) error { - conn, err := openAPIConn(targetInfo) +func (w *Worker) removeImportedModel(targetInfo coremigration.TargetInfo, modelUUID string) error { + conn, err := w.openAPIConn(targetInfo) if err != nil { return errors.Trace(err) } @@ -286,8 +401,8 @@ return errors.Trace(err) } -func (w *Worker) waitForActiveMigration() (migrationmaster.MigrationStatus, error) { - var empty migrationmaster.MigrationStatus +func (w *Worker) waitForActiveMigration() (coremigration.MigrationStatus, error) { + var empty coremigration.MigrationStatus watcher, err := w.config.Facade.Watch() if err != nil { @@ -304,38 +419,176 @@ return empty, w.catacomb.ErrDying() case <-watcher.Changes(): } + status, err := w.config.Facade.GetMigrationStatus() switch { case params.IsCodeNotFound(err): - if err := w.config.Guard.Unlock(); err != nil { - return empty, errors.Trace(err) + // There's never been a migration. + case err == nil && status.Phase.IsTerminal(): + // No migration in progress. + if modelHasMigrated(status.Phase) { + return empty, ErrMigrated } - continue case err != nil: return empty, errors.Annotate(err, "retrieving migration status") + default: + // Migration is in progress. + return status, nil } - if modelHasMigrated(status.Phase) { - return empty, dependency.ErrUninstall + + // While waiting for a migration, ensure the fortress is open. + if err := w.config.Guard.Unlock(); err != nil { + return empty, errors.Trace(err) } - if !status.Phase.IsTerminal() { - return status, nil + } +} + +// Possible values for waitForMinion's waitPolicy argument. +const failFast = false // Stop waiting at first minion failure report +const waitForAll = true // Wait for all minion reports to arrive (or timeout) + +var errMinionReportTimeout = errors.New("timed out waiting for all agents to report") +var errMinionReportFailed = errors.New("one or more agents failed a migration phase") + +func (w *Worker) waitForMinions(status coremigration.MigrationStatus, waitPolicy bool, infoPrefix string) error { + clk := w.config.Clock + maxWait := maxMinionWait - clk.Now().Sub(status.PhaseChangedTime) + timeout := clk.After(maxWait) + + w.setInfoStatus("%s: waiting for agents to report back", infoPrefix) + w.logger.Infof("waiting for agents to report back for migration phase %s (will wait up to %s)", + status.Phase, truncDuration(maxWait)) + + watch, err := w.config.Facade.WatchMinionReports() + if err != nil { + return errors.Trace(err) + } + if err := w.catacomb.Add(watch); err != nil { + return errors.Trace(err) + } + + logProgress := clk.After(minionWaitLogInterval) + + var reports coremigration.MinionReports + for { + select { + case <-w.catacomb.Dying(): + return w.catacomb.ErrDying() + + case <-timeout: + w.logger.Errorf(formatMinionTimeout(reports, status)) + return errors.Trace(errMinionReportTimeout) + + case <-watch.Changes(): + var err error + reports, err = w.config.Facade.GetMinionReports() + if err != nil { + return errors.Trace(err) + } + if err := validateMinionReports(reports, status); err != nil { + return errors.Trace(err) + } + failures := len(reports.FailedMachines) + len(reports.FailedUnits) + if failures > 0 { + w.logger.Errorf(formatMinionFailure(reports)) + if waitPolicy == failFast { + return errors.Trace(errMinionReportFailed) + } + } + if reports.UnknownCount == 0 { + w.logger.Infof(formatMinionWaitDone(reports)) + if failures > 0 { + return errors.Trace(errMinionReportFailed) + } + return nil + } + + case <-logProgress: + w.setInfoStatus("%s: ", infoPrefix, formatMinionWaitUpdate(reports)) + logProgress = clk.After(minionWaitLogInterval) } } } -func openAPIConn(targetInfo migration.TargetInfo) (api.Connection, error) { +func truncDuration(d time.Duration) time.Duration { + return (d / time.Second) * time.Second +} + +func validateMinionReports(reports coremigration.MinionReports, status coremigration.MigrationStatus) error { + if reports.MigrationId != status.MigrationId { + return errors.Errorf("unexpected migration id in minion reports, got %v, expected %v", + reports.MigrationId, status.MigrationId) + } + if reports.Phase != status.Phase { + return errors.Errorf("minion reports phase (%s) does not match migration phase (%s)", + reports.Phase, status.Phase) + } + return nil +} + +func formatMinionTimeout(reports coremigration.MinionReports, status coremigration.MigrationStatus) string { + if reports.IsZero() { + return fmt.Sprintf("no agents reported in time") + } + + msg := "%s agents failed to report in time for migration phase %s including:" + if len(reports.SomeUnknownMachines) > 0 { + msg += fmt.Sprintf("machines: %s;", strings.Join(reports.SomeUnknownMachines, ", ")) + } + if len(reports.SomeUnknownUnits) > 0 { + msg += fmt.Sprintf(" units: %s", strings.Join(reports.SomeUnknownUnits, ", ")) + } + return msg +} + +func formatMinionFailure(reports coremigration.MinionReports) string { + msg := fmt.Sprintf("some agents failed %s: ", reports.Phase) + if len(reports.FailedMachines) > 0 { + msg += fmt.Sprintf("failed machines: %s; ", strings.Join(reports.FailedMachines, ", ")) + } + if len(reports.FailedUnits) > 0 { + msg += fmt.Sprintf("failed units: %s", strings.Join(reports.FailedUnits, ", ")) + } + return msg +} + +func formatMinionWaitUpdate(reports coremigration.MinionReports) string { + if reports.IsZero() { + return fmt.Sprintf("no reports from agents yet") + } + + msg := fmt.Sprintf("waiting for agents to report back: %d succeeded, %d still to report", + reports.SuccessCount, reports.UnknownCount) + failed := len(reports.FailedMachines) + len(reports.FailedUnits) + if failed > 0 { + msg += fmt.Sprintf(", %d failed", failed) + } + return msg +} + +func formatMinionWaitDone(reports coremigration.MinionReports) string { + return fmt.Sprintf("completed waiting for agents to report for %s: %d succeeded, %d failed", + reports.Phase, reports.SuccessCount, len(reports.FailedMachines)+len(reports.FailedUnits)) +} + +func (w *Worker) openAPIConn(targetInfo coremigration.TargetInfo) (api.Connection, error) { + return w.openAPIConnForModel(targetInfo, "") +} + +func (w *Worker) openAPIConnForModel(targetInfo coremigration.TargetInfo, modelUUID string) (api.Connection, error) { apiInfo := &api.Info{ Addrs: targetInfo.Addrs, CACert: targetInfo.CACert, Tag: targetInfo.AuthTag, Password: targetInfo.Password, + ModelTag: names.NewModelTag(modelUUID), } // Use zero DialOpts (no retries) because the worker must stay // responsive to Kill requests. We don't want it to be blocked by // a long set of retry attempts. - return apiOpen(apiInfo, api.DialOpts{}) + return w.config.APIOpen(apiInfo, api.DialOpts{}) } -func modelHasMigrated(phase migration.Phase) bool { - return phase == migration.DONE || phase == migration.REAPFAILED +func modelHasMigrated(phase coremigration.Phase) bool { + return phase == coremigration.DONE || phase == coremigration.REAPFAILED } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/migrationmaster/worker_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/migrationmaster/worker_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/migrationmaster/worker_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/migrationmaster/worker_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,22 +4,24 @@ package migrationmaster_test import ( + "reflect" "time" "github.com/juju/errors" jujutesting "github.com/juju/testing" jc "github.com/juju/testing/checkers" + "github.com/juju/utils" + "github.com/juju/version" gc "gopkg.in/check.v1" "gopkg.in/juju/names.v2" "github.com/juju/juju/api" - masterapi "github.com/juju/juju/api/migrationmaster" "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/core/migration" + coremigration "github.com/juju/juju/core/migration" + "github.com/juju/juju/migration" coretesting "github.com/juju/juju/testing" "github.com/juju/juju/watcher" "github.com/juju/juju/worker" - "github.com/juju/juju/worker/dependency" "github.com/juju/juju/worker/fortress" "github.com/juju/juju/worker/migrationmaster" "github.com/juju/juju/worker/workertest" @@ -27,19 +29,23 @@ type Suite struct { coretesting.BaseSuite + clock *coretesting.Clock stub *jujutesting.Stub connection *stubConnection connectionErr error + masterFacade *stubMasterFacade + config migrationmaster.Config } var _ = gc.Suite(&Suite{}) var ( - fakeSerializedModel = []byte("model") - modelTagString = names.NewModelTag("model-uuid").String() + fakeModelBytes = []byte("model") + modelTag = names.NewModelTag("model-uuid") + modelTagString = modelTag.String() // Define stub calls that commonly appear in tests here to allow reuse. - apiOpenCall = jujutesting.StubCall{ + apiOpenCallController = jujutesting.StubCall{ "apiOpen", []interface{}{ &api.Info{ @@ -51,10 +57,23 @@ api.DialOpts{}, }, } + apiOpenCallModel = jujutesting.StubCall{ + "apiOpen", + []interface{}{ + &api.Info{ + Addrs: []string{"1.2.3.4:5"}, + CACert: "cert", + Tag: names.NewUserTag("admin"), + Password: "secret", + ModelTag: modelTag, + }, + api.DialOpts{}, + }, + } importCall = jujutesting.StubCall{ "APICall:MigrationTarget.Import", []interface{}{ - params.SerializedModel{Bytes: fakeSerializedModel}, + params.SerializedModel{Bytes: fakeModelBytes}, }, } activateCall = jujutesting.StubCall{ @@ -75,11 +94,25 @@ func (s *Suite) SetUpTest(c *gc.C) { s.BaseSuite.SetUpTest(c) + s.clock = coretesting.NewClock(time.Now()) s.stub = new(jujutesting.Stub) s.connection = &stubConnection{stub: s.stub} s.connectionErr = nil - s.PatchValue(migrationmaster.ApiOpen, s.apiOpen) - s.PatchValue(migrationmaster.TempSuccessSleep, time.Millisecond) + + s.masterFacade = newStubMasterFacade(s.stub, s.clock.Now()) + + // The default worker Config used by most of the tests. Tests may + // tweak parts of this as needed. + s.config = migrationmaster.Config{ + ModelUUID: utils.MustNewUUID().String(), + Facade: s.masterFacade, + Guard: newStubGuard(s.stub), + APIOpen: s.apiOpen, + UploadBinaries: nullUploadBinaries, + CharmDownloader: fakeCharmDownloader, + ToolsDownloader: fakeToolsDownloader, + Clock: s.clock, + } } func (s *Suite) apiOpen(info *api.Info, dialOpts api.DialOpts) (api.Connection, error) { @@ -90,288 +123,451 @@ return s.connection, nil } -func (s *Suite) triggerMigration(masterClient *stubMasterClient) { - masterClient.watcherChanges <- struct{}{} +func (s *Suite) triggerMigration() { + select { + case s.masterFacade.watcherChanges <- struct{}{}: + default: + panic("migration watcher channel unexpectedly closed") + } + +} + +func (s *Suite) triggerMinionReports() { + select { + case s.masterFacade.minionReportsChanges <- struct{}{}: + default: + panic("minion reports watcher channel unexpectedly closed") + } } func (s *Suite) TestSuccessfulMigration(c *gc.C) { - masterClient := newStubMasterClient(s.stub) - worker, err := migrationmaster.New(migrationmaster.Config{ - Facade: masterClient, - Guard: newStubGuard(s.stub), - }) + s.config.UploadBinaries = makeStubUploadBinaries(s.stub) + worker, err := migrationmaster.New(s.config) c.Assert(err, jc.ErrorIsNil) - s.triggerMigration(masterClient) + defer workertest.DirtyKill(c, worker) + s.triggerMigration() + s.triggerMinionReports() err = workertest.CheckKilled(c, worker) - c.Assert(errors.Cause(err), gc.Equals, dependency.ErrUninstall) + c.Assert(errors.Cause(err), gc.Equals, migrationmaster.ErrMigrated) // Observe that the migration was seen, the model exported, an API // connection to the target controller was made, the model was // imported and then the migration completed. s.stub.CheckCalls(c, []jujutesting.StubCall{ - {"masterClient.Watch", nil}, - {"masterClient.GetMigrationStatus", nil}, + {"masterFacade.Watch", nil}, + {"masterFacade.GetMigrationStatus", nil}, {"guard.Lockdown", nil}, - {"masterClient.SetPhase", []interface{}{migration.READONLY}}, - {"masterClient.SetPhase", []interface{}{migration.PRECHECK}}, - {"masterClient.SetPhase", []interface{}{migration.IMPORT}}, - {"masterClient.Export", nil}, - apiOpenCall, + {"masterFacade.SetPhase", []interface{}{coremigration.PRECHECK}}, + {"masterFacade.SetPhase", []interface{}{coremigration.IMPORT}}, + {"masterFacade.Export", nil}, + apiOpenCallController, importCall, - connCloseCall, - {"masterClient.SetPhase", []interface{}{migration.VALIDATION}}, - apiOpenCall, + apiOpenCallModel, + {"UploadBinaries", []interface{}{ + []string{"charm0", "charm1"}, + fakeCharmDownloader, + map[version.Binary]string{ + version.MustParseBinary("2.1.0-trusty-amd64"): "/tools/0", + }, + fakeToolsDownloader, + }}, + connCloseCall, // for target model + connCloseCall, // for target controller + {"masterFacade.SetPhase", []interface{}{coremigration.VALIDATION}}, + apiOpenCallController, activateCall, connCloseCall, - {"masterClient.SetPhase", []interface{}{migration.SUCCESS}}, - {"masterClient.SetPhase", []interface{}{migration.LOGTRANSFER}}, - {"masterClient.SetPhase", []interface{}{migration.REAP}}, - {"masterClient.SetPhase", []interface{}{migration.DONE}}, + {"masterFacade.SetPhase", []interface{}{coremigration.SUCCESS}}, + {"masterFacade.WatchMinionReports", nil}, + {"masterFacade.GetMinionReports", nil}, + {"masterFacade.SetPhase", []interface{}{coremigration.LOGTRANSFER}}, + {"masterFacade.SetPhase", []interface{}{coremigration.REAP}}, + {"masterFacade.Reap", nil}, + {"masterFacade.SetPhase", []interface{}{coremigration.DONE}}, }) } func (s *Suite) TestMigrationResume(c *gc.C) { // Test that a partially complete migration can be resumed. - - masterClient := newStubMasterClient(s.stub) - worker, err := migrationmaster.New(migrationmaster.Config{ - Facade: masterClient, - Guard: newStubGuard(s.stub), - }) + worker, err := migrationmaster.New(s.config) c.Assert(err, jc.ErrorIsNil) - masterClient.status.Phase = migration.SUCCESS - s.triggerMigration(masterClient) + defer workertest.DirtyKill(c, worker) + s.masterFacade.status.Phase = coremigration.SUCCESS + s.triggerMigration() + s.triggerMinionReports() err = workertest.CheckKilled(c, worker) - c.Assert(errors.Cause(err), gc.Equals, dependency.ErrUninstall) + c.Assert(errors.Cause(err), gc.Equals, migrationmaster.ErrMigrated) s.stub.CheckCalls(c, []jujutesting.StubCall{ - {"masterClient.Watch", nil}, - {"masterClient.GetMigrationStatus", nil}, + {"masterFacade.Watch", nil}, + {"masterFacade.GetMigrationStatus", nil}, {"guard.Lockdown", nil}, - {"masterClient.SetPhase", []interface{}{migration.LOGTRANSFER}}, - {"masterClient.SetPhase", []interface{}{migration.REAP}}, - {"masterClient.SetPhase", []interface{}{migration.DONE}}, + {"masterFacade.WatchMinionReports", nil}, + {"masterFacade.GetMinionReports", nil}, + {"masterFacade.SetPhase", []interface{}{coremigration.LOGTRANSFER}}, + {"masterFacade.SetPhase", []interface{}{coremigration.REAP}}, + {"masterFacade.Reap", nil}, + {"masterFacade.SetPhase", []interface{}{coremigration.DONE}}, }) } func (s *Suite) TestPreviouslyAbortedMigration(c *gc.C) { - masterClient := newStubMasterClient(s.stub) - masterClient.status.Phase = migration.ABORTDONE - s.triggerMigration(masterClient) - worker, err := migrationmaster.New(migrationmaster.Config{ - Facade: masterClient, - Guard: newStubGuard(s.stub), - }) + s.masterFacade.status.Phase = coremigration.ABORTDONE + s.triggerMigration() + + worker, err := migrationmaster.New(s.config) c.Assert(err, jc.ErrorIsNil) - workertest.CheckAlive(c, worker) - workertest.CleanKill(c, worker) + defer workertest.CleanKill(c, worker) - // No reliable way to test stub calls in this case unfortunately. + s.waitForStubCalls(c, []string{ + "masterFacade.Watch", + "masterFacade.GetMigrationStatus", + "guard.Unlock", + }) } func (s *Suite) TestPreviouslyCompletedMigration(c *gc.C) { - masterClient := newStubMasterClient(s.stub) - masterClient.status.Phase = migration.DONE - s.triggerMigration(masterClient) - worker, err := migrationmaster.New(migrationmaster.Config{ - Facade: masterClient, - Guard: newStubGuard(s.stub), - }) + s.masterFacade.status.Phase = coremigration.DONE + s.triggerMigration() + worker, err := migrationmaster.New(s.config) c.Assert(err, jc.ErrorIsNil) + defer workertest.DirtyKill(c, worker) err = workertest.CheckKilled(c, worker) - c.Assert(errors.Cause(err), gc.Equals, dependency.ErrUninstall) + c.Assert(errors.Cause(err), gc.Equals, migrationmaster.ErrMigrated) s.stub.CheckCalls(c, []jujutesting.StubCall{ - {"masterClient.Watch", nil}, - {"masterClient.GetMigrationStatus", nil}, + {"masterFacade.Watch", nil}, + {"masterFacade.GetMigrationStatus", nil}, }) } func (s *Suite) TestWatchFailure(c *gc.C) { - masterClient := newStubMasterClient(s.stub) - masterClient.watchErr = errors.New("boom") - worker, err := migrationmaster.New(migrationmaster.Config{ - Facade: masterClient, - Guard: newStubGuard(s.stub), - }) + s.masterFacade.watchErr = errors.New("boom") + worker, err := migrationmaster.New(s.config) c.Assert(err, jc.ErrorIsNil) + defer workertest.DirtyKill(c, worker) err = workertest.CheckKilled(c, worker) c.Assert(err, gc.ErrorMatches, "watching for migration: boom") } func (s *Suite) TestStatusError(c *gc.C) { - masterClient := newStubMasterClient(s.stub) - masterClient.statusErr = errors.New("splat") - worker, err := migrationmaster.New(migrationmaster.Config{ - Facade: masterClient, - Guard: newStubGuard(s.stub), - }) + s.masterFacade.statusErr = errors.New("splat") + worker, err := migrationmaster.New(s.config) c.Assert(err, jc.ErrorIsNil) - s.triggerMigration(masterClient) + defer workertest.DirtyKill(c, worker) + s.triggerMigration() err = workertest.CheckKilled(c, worker) s.stub.CheckCalls(c, []jujutesting.StubCall{ - {"masterClient.Watch", nil}, - {"masterClient.GetMigrationStatus", nil}, + {"masterFacade.Watch", nil}, + {"masterFacade.GetMigrationStatus", nil}, }) } func (s *Suite) TestStatusNotFound(c *gc.C) { - masterClient := newStubMasterClient(s.stub) - masterClient.statusErr = ¶ms.Error{Code: params.CodeNotFound} - worker, err := migrationmaster.New(migrationmaster.Config{ - Facade: masterClient, - Guard: newStubGuard(s.stub), - }) - c.Assert(err, jc.ErrorIsNil) - s.triggerMigration(masterClient) + s.masterFacade.statusErr = ¶ms.Error{Code: params.CodeNotFound} + s.triggerMigration() - workertest.CheckAlive(c, worker) - workertest.CleanKill(c, worker) + worker, err := migrationmaster.New(s.config) + c.Assert(err, jc.ErrorIsNil) + defer workertest.CleanKill(c, worker) - s.stub.CheckCalls(c, []jujutesting.StubCall{ - {"masterClient.Watch", nil}, - {"masterClient.GetMigrationStatus", nil}, - {"guard.Unlock", nil}, + s.waitForStubCalls(c, []string{ + "masterFacade.Watch", + "masterFacade.GetMigrationStatus", + "guard.Unlock", }) } func (s *Suite) TestUnlockError(c *gc.C) { - masterClient := newStubMasterClient(s.stub) - masterClient.statusErr = ¶ms.Error{Code: params.CodeNotFound} + s.masterFacade.statusErr = ¶ms.Error{Code: params.CodeNotFound} guard := newStubGuard(s.stub) guard.unlockErr = errors.New("pow") - worker, err := migrationmaster.New(migrationmaster.Config{ - Facade: masterClient, - Guard: guard, - }) + s.config.Guard = guard + worker, err := migrationmaster.New(s.config) c.Assert(err, jc.ErrorIsNil) - s.triggerMigration(masterClient) + defer workertest.DirtyKill(c, worker) + s.triggerMigration() err = workertest.CheckKilled(c, worker) c.Check(err, gc.ErrorMatches, "pow") s.stub.CheckCalls(c, []jujutesting.StubCall{ - {"masterClient.Watch", nil}, - {"masterClient.GetMigrationStatus", nil}, + {"masterFacade.Watch", nil}, + {"masterFacade.GetMigrationStatus", nil}, {"guard.Unlock", nil}, }) } func (s *Suite) TestLockdownError(c *gc.C) { - masterClient := newStubMasterClient(s.stub) guard := newStubGuard(s.stub) guard.lockdownErr = errors.New("biff") - worker, err := migrationmaster.New(migrationmaster.Config{ - Facade: masterClient, - Guard: guard, - }) + s.config.Guard = guard + worker, err := migrationmaster.New(s.config) c.Assert(err, jc.ErrorIsNil) - s.triggerMigration(masterClient) + defer workertest.DirtyKill(c, worker) + s.triggerMigration() err = workertest.CheckKilled(c, worker) c.Check(err, gc.ErrorMatches, "biff") s.stub.CheckCalls(c, []jujutesting.StubCall{ - {"masterClient.Watch", nil}, - {"masterClient.GetMigrationStatus", nil}, + {"masterFacade.Watch", nil}, + {"masterFacade.GetMigrationStatus", nil}, {"guard.Lockdown", nil}, }) } func (s *Suite) TestExportFailure(c *gc.C) { - masterClient := newStubMasterClient(s.stub) - masterClient.exportErr = errors.New("boom") - worker, err := migrationmaster.New(migrationmaster.Config{ - Facade: masterClient, - Guard: newStubGuard(s.stub), - }) + s.masterFacade.exportErr = errors.New("boom") + worker, err := migrationmaster.New(s.config) c.Assert(err, jc.ErrorIsNil) - s.triggerMigration(masterClient) + defer workertest.DirtyKill(c, worker) + s.triggerMigration() err = workertest.CheckKilled(c, worker) - c.Assert(err, gc.Equals, migrationmaster.ErrDoneForNow) + c.Assert(err, gc.Equals, migrationmaster.ErrInactive) s.stub.CheckCalls(c, []jujutesting.StubCall{ - {"masterClient.Watch", nil}, - {"masterClient.GetMigrationStatus", nil}, + {"masterFacade.Watch", nil}, + {"masterFacade.GetMigrationStatus", nil}, {"guard.Lockdown", nil}, - {"masterClient.SetPhase", []interface{}{migration.READONLY}}, - {"masterClient.SetPhase", []interface{}{migration.PRECHECK}}, - {"masterClient.SetPhase", []interface{}{migration.IMPORT}}, - {"masterClient.Export", nil}, - {"masterClient.SetPhase", []interface{}{migration.ABORT}}, - apiOpenCall, + {"masterFacade.SetPhase", []interface{}{coremigration.PRECHECK}}, + {"masterFacade.SetPhase", []interface{}{coremigration.IMPORT}}, + {"masterFacade.Export", nil}, + {"masterFacade.SetPhase", []interface{}{coremigration.ABORT}}, + apiOpenCallController, abortCall, connCloseCall, - {"masterClient.SetPhase", []interface{}{migration.ABORTDONE}}, + {"masterFacade.SetPhase", []interface{}{coremigration.ABORTDONE}}, }) } func (s *Suite) TestAPIOpenFailure(c *gc.C) { - masterClient := newStubMasterClient(s.stub) - worker, err := migrationmaster.New(migrationmaster.Config{ - Facade: masterClient, - Guard: newStubGuard(s.stub), - }) + worker, err := migrationmaster.New(s.config) c.Assert(err, jc.ErrorIsNil) + defer workertest.DirtyKill(c, worker) s.connectionErr = errors.New("boom") - s.triggerMigration(masterClient) + s.triggerMigration() err = workertest.CheckKilled(c, worker) - c.Assert(err, gc.Equals, migrationmaster.ErrDoneForNow) + c.Assert(err, gc.Equals, migrationmaster.ErrInactive) s.stub.CheckCalls(c, []jujutesting.StubCall{ - {"masterClient.Watch", nil}, - {"masterClient.GetMigrationStatus", nil}, + {"masterFacade.Watch", nil}, + {"masterFacade.GetMigrationStatus", nil}, {"guard.Lockdown", nil}, - {"masterClient.SetPhase", []interface{}{migration.READONLY}}, - {"masterClient.SetPhase", []interface{}{migration.PRECHECK}}, - {"masterClient.SetPhase", []interface{}{migration.IMPORT}}, - {"masterClient.Export", nil}, - apiOpenCall, - {"masterClient.SetPhase", []interface{}{migration.ABORT}}, - apiOpenCall, - {"masterClient.SetPhase", []interface{}{migration.ABORTDONE}}, + {"masterFacade.SetPhase", []interface{}{coremigration.PRECHECK}}, + {"masterFacade.SetPhase", []interface{}{coremigration.IMPORT}}, + {"masterFacade.Export", nil}, + apiOpenCallController, + {"masterFacade.SetPhase", []interface{}{coremigration.ABORT}}, + apiOpenCallController, + {"masterFacade.SetPhase", []interface{}{coremigration.ABORTDONE}}, }) } func (s *Suite) TestImportFailure(c *gc.C) { - masterClient := newStubMasterClient(s.stub) - worker, err := migrationmaster.New(migrationmaster.Config{ - Facade: masterClient, - Guard: newStubGuard(s.stub), - }) + worker, err := migrationmaster.New(s.config) c.Assert(err, jc.ErrorIsNil) + defer workertest.DirtyKill(c, worker) s.connection.importErr = errors.New("boom") - s.triggerMigration(masterClient) + s.triggerMigration() err = workertest.CheckKilled(c, worker) - c.Assert(err, gc.Equals, migrationmaster.ErrDoneForNow) + c.Assert(err, gc.Equals, migrationmaster.ErrInactive) s.stub.CheckCalls(c, []jujutesting.StubCall{ - {"masterClient.Watch", nil}, - {"masterClient.GetMigrationStatus", nil}, + {"masterFacade.Watch", nil}, + {"masterFacade.GetMigrationStatus", nil}, {"guard.Lockdown", nil}, - {"masterClient.SetPhase", []interface{}{migration.READONLY}}, - {"masterClient.SetPhase", []interface{}{migration.PRECHECK}}, - {"masterClient.SetPhase", []interface{}{migration.IMPORT}}, - {"masterClient.Export", nil}, - apiOpenCall, + {"masterFacade.SetPhase", []interface{}{coremigration.PRECHECK}}, + {"masterFacade.SetPhase", []interface{}{coremigration.IMPORT}}, + {"masterFacade.Export", nil}, + apiOpenCallController, importCall, connCloseCall, - {"masterClient.SetPhase", []interface{}{migration.ABORT}}, - apiOpenCall, + {"masterFacade.SetPhase", []interface{}{coremigration.ABORT}}, + apiOpenCallController, abortCall, connCloseCall, - {"masterClient.SetPhase", []interface{}{migration.ABORTDONE}}, + {"masterFacade.SetPhase", []interface{}{coremigration.ABORTDONE}}, + }) +} + +func (s *Suite) TestMinionWaitWatchError(c *gc.C) { + worker, err := migrationmaster.New(s.config) + c.Assert(err, jc.ErrorIsNil) + defer workertest.DirtyKill(c, worker) + s.masterFacade.minionReportsWatchErr = errors.New("boom") + s.masterFacade.status.Phase = coremigration.SUCCESS + s.triggerMigration() + + err = workertest.CheckKilled(c, worker) + c.Assert(err, gc.ErrorMatches, "boom") +} + +func (s *Suite) TestMinionWaitGetError(c *gc.C) { + worker, err := migrationmaster.New(s.config) + c.Assert(err, jc.ErrorIsNil) + defer workertest.DirtyKill(c, worker) + s.masterFacade.minionReportsErr = errors.New("boom") + s.masterFacade.status.Phase = coremigration.SUCCESS + s.triggerMigration() + s.triggerMinionReports() + + err = workertest.CheckKilled(c, worker) + c.Assert(err, gc.ErrorMatches, "boom") +} + +func (s *Suite) TestMinionWaitSUCCESSFailedMachine(c *gc.C) { + // With the SUCCESS phase the master should wait for all reports, + // continuing even if some minions report failure. + + s.masterFacade.minionReports.FailedMachines = []string{"42"} + worker, err := migrationmaster.New(s.config) + c.Assert(err, jc.ErrorIsNil) + defer workertest.DirtyKill(c, worker) + s.masterFacade.status.Phase = coremigration.SUCCESS + s.triggerMigration() + s.triggerMinionReports() + + err = workertest.CheckKilled(c, worker) + c.Assert(err, gc.Equals, migrationmaster.ErrMigrated) + + s.stub.CheckCalls(c, []jujutesting.StubCall{ + {"masterFacade.Watch", nil}, + {"masterFacade.GetMigrationStatus", nil}, + {"guard.Lockdown", nil}, + {"masterFacade.WatchMinionReports", nil}, + {"masterFacade.GetMinionReports", nil}, + {"masterFacade.SetPhase", []interface{}{coremigration.LOGTRANSFER}}, + {"masterFacade.SetPhase", []interface{}{coremigration.REAP}}, + {"masterFacade.Reap", nil}, + {"masterFacade.SetPhase", []interface{}{coremigration.DONE}}, }) } +func (s *Suite) TestMinionWaitSUCCESSFailedUnit(c *gc.C) { + // See note for TestMinionWaitSUCCESSFailedMachine above. + + s.masterFacade.minionReports.FailedUnits = []string{"foo/2"} + worker, err := migrationmaster.New(s.config) + c.Assert(err, jc.ErrorIsNil) + defer workertest.DirtyKill(c, worker) + s.masterFacade.status.Phase = coremigration.SUCCESS + s.triggerMigration() + s.triggerMinionReports() + + err = workertest.CheckKilled(c, worker) + c.Assert(err, gc.Equals, migrationmaster.ErrMigrated) + + s.stub.CheckCalls(c, []jujutesting.StubCall{ + {"masterFacade.Watch", nil}, + {"masterFacade.GetMigrationStatus", nil}, + {"guard.Lockdown", nil}, + {"masterFacade.WatchMinionReports", nil}, + {"masterFacade.GetMinionReports", nil}, + {"masterFacade.SetPhase", []interface{}{coremigration.LOGTRANSFER}}, + {"masterFacade.SetPhase", []interface{}{coremigration.REAP}}, + {"masterFacade.Reap", nil}, + {"masterFacade.SetPhase", []interface{}{coremigration.DONE}}, + }) +} + +func (s *Suite) TestMinionWaitSUCCESSTimeout(c *gc.C) { + // The SUCCESS phase is special in that even if some minions fail + // to report the migration should continue. There's no turning + // back from SUCCESS. + worker, err := migrationmaster.New(s.config) + c.Assert(err, jc.ErrorIsNil) + defer workertest.DirtyKill(c, worker) + + s.masterFacade.status.Phase = coremigration.SUCCESS + s.triggerMigration() + + select { + case <-s.clock.Alarms(): + case <-time.After(coretesting.LongWait): + c.Fatal("timed out waiting for clock.After call") + } + + // Move time ahead in order to trigger timeout. + s.clock.Advance(15 * time.Minute) + + err = workertest.CheckKilled(c, worker) + c.Assert(err, gc.Equals, migrationmaster.ErrMigrated) + + s.stub.CheckCalls(c, []jujutesting.StubCall{ + {"masterFacade.Watch", nil}, + {"masterFacade.GetMigrationStatus", nil}, + {"guard.Lockdown", nil}, + {"masterFacade.WatchMinionReports", nil}, + {"masterFacade.SetPhase", []interface{}{coremigration.LOGTRANSFER}}, + {"masterFacade.SetPhase", []interface{}{coremigration.REAP}}, + {"masterFacade.Reap", nil}, + {"masterFacade.SetPhase", []interface{}{coremigration.DONE}}, + }) +} + +func (s *Suite) TestMinionWaitWrongPhase(c *gc.C) { + worker, err := migrationmaster.New(s.config) + c.Assert(err, jc.ErrorIsNil) + defer workertest.DirtyKill(c, worker) + s.masterFacade.status.Phase = coremigration.SUCCESS + s.triggerMigration() + + // Have the phase in the minion reports be different from the + // migration status. This shouldn't happen but the migrationmaster + // should handle it. + s.masterFacade.minionReports.Phase = coremigration.PRECHECK + s.triggerMinionReports() + + err = workertest.CheckKilled(c, worker) + c.Assert(err, gc.ErrorMatches, `minion reports phase \(PRECHECK\) does not match migration phase \(SUCCESS\)`) +} + +func (s *Suite) TestMinionWaitMigrationIdChanged(c *gc.C) { + worker, err := migrationmaster.New(s.config) + c.Assert(err, jc.ErrorIsNil) + defer workertest.DirtyKill(c, worker) + s.masterFacade.status.Phase = coremigration.SUCCESS + s.triggerMigration() + + // Have the migration id in the minion reports be different from + // the migration status. This shouldn't happen but the + // migrationmaster should handle it. + s.masterFacade.minionReports.MigrationId = "blah" + s.triggerMinionReports() + + err = workertest.CheckKilled(c, worker) + c.Assert(err, gc.ErrorMatches, + "unexpected migration id in minion reports, got blah, expected model-uuid:2") +} + +func (s *Suite) waitForStubCalls(c *gc.C, expectedCallNames []string) { + var callNames []string + for a := coretesting.LongAttempt.Start(); a.Next(); { + callNames = stubCallNames(s.stub) + if reflect.DeepEqual(callNames, expectedCallNames) { + return + } + } + c.Fatalf("failed to see expected calls. saw: %v", callNames) +} + +func stubCallNames(stub *jujutesting.Stub) []string { + var out []string + for _, call := range stub.Calls() { + out = append(out, call.FuncName) + } + return out +} + func newStubGuard(stub *jujutesting.Stub) *stubGuard { return &stubGuard{stub: stub} } @@ -392,15 +588,16 @@ return g.unlockErr } -func newStubMasterClient(stub *jujutesting.Stub) *stubMasterClient { - return &stubMasterClient{ +func newStubMasterFacade(stub *jujutesting.Stub, now time.Time) *stubMasterFacade { + return &stubMasterFacade{ stub: stub, - watcherChanges: make(chan struct{}, 1), - status: masterapi.MigrationStatus{ - ModelUUID: "model-uuid", - Attempt: 2, - Phase: migration.QUIESCE, - TargetInfo: migration.TargetInfo{ + watcherChanges: make(chan struct{}, 999), + status: coremigration.MigrationStatus{ + MigrationId: "model-uuid:2", + ModelUUID: "model-uuid", + Phase: coremigration.QUIESCE, + PhaseChangedTime: now, + TargetInfo: coremigration.TargetInfo{ ControllerTag: names.NewModelTag("controller-uuid"), Addrs: []string{"1.2.3.4:5"}, CACert: "cert", @@ -408,46 +605,96 @@ Password: "secret", }, }, + + // Give minionReportsChanges a larger-than-required buffer to + // support waits at a number of phases. + minionReportsChanges: make(chan struct{}, 999), + + // Default to happy state. Test may wish to tweak. + minionReports: coremigration.MinionReports{ + MigrationId: "model-uuid:2", + Phase: coremigration.SUCCESS, + SuccessCount: 5, + UnknownCount: 0, + }, } } -type stubMasterClient struct { - masterapi.Client - stub *jujutesting.Stub +type stubMasterFacade struct { + migrationmaster.Facade + + stub *jujutesting.Stub + watcherChanges chan struct{} watchErr error - status masterapi.MigrationStatus + status coremigration.MigrationStatus statusErr error - exportErr error + + exportErr error + + minionReportsChanges chan struct{} + minionReportsWatchErr error + minionReports coremigration.MinionReports + minionReportsErr error } -func (c *stubMasterClient) Watch() (watcher.NotifyWatcher, error) { - c.stub.AddCall("masterClient.Watch") +func (c *stubMasterFacade) Watch() (watcher.NotifyWatcher, error) { + c.stub.AddCall("masterFacade.Watch") if c.watchErr != nil { return nil, c.watchErr } - return newMockWatcher(c.watcherChanges), nil } -func (c *stubMasterClient) GetMigrationStatus() (masterapi.MigrationStatus, error) { - c.stub.AddCall("masterClient.GetMigrationStatus") +func (c *stubMasterFacade) GetMigrationStatus() (coremigration.MigrationStatus, error) { + c.stub.AddCall("masterFacade.GetMigrationStatus") if c.statusErr != nil { - return masterapi.MigrationStatus{}, c.statusErr + return coremigration.MigrationStatus{}, c.statusErr } return c.status, nil } -func (c *stubMasterClient) Export() ([]byte, error) { - c.stub.AddCall("masterClient.Export") +func (c *stubMasterFacade) WatchMinionReports() (watcher.NotifyWatcher, error) { + c.stub.AddCall("masterFacade.WatchMinionReports") + if c.minionReportsWatchErr != nil { + return nil, c.minionReportsWatchErr + } + return newMockWatcher(c.minionReportsChanges), nil +} + +func (c *stubMasterFacade) GetMinionReports() (coremigration.MinionReports, error) { + c.stub.AddCall("masterFacade.GetMinionReports") + if c.minionReportsErr != nil { + return coremigration.MinionReports{}, c.minionReportsErr + } + return c.minionReports, nil +} + +func (c *stubMasterFacade) Export() (coremigration.SerializedModel, error) { + c.stub.AddCall("masterFacade.Export") if c.exportErr != nil { - return nil, c.exportErr + return coremigration.SerializedModel{}, c.exportErr } - return fakeSerializedModel, nil + return coremigration.SerializedModel{ + Bytes: fakeModelBytes, + Charms: []string{"charm0", "charm1"}, + Tools: map[version.Binary]string{ + version.MustParseBinary("2.1.0-trusty-amd64"): "/tools/0", + }, + }, nil +} + +func (c *stubMasterFacade) SetPhase(phase coremigration.Phase) error { + c.stub.AddCall("masterFacade.SetPhase", phase) + return nil } -func (c *stubMasterClient) SetPhase(phase migration.Phase) error { - c.stub.AddCall("masterClient.SetPhase", phase) +func (c *stubMasterFacade) SetStatusMessage(message string) error { + return nil +} + +func (c *stubMasterFacade) Reap() error { + c.stub.AddCall("masterFacade.Reap") return nil } @@ -491,7 +738,36 @@ return errors.New("unexpected API call") } +func (c *stubConnection) Client() *api.Client { + // This is kinda crappy but the *Client doesn't have to be + // functional... + return new(api.Client) +} + func (c *stubConnection) Close() error { c.stub.AddCall("Connection.Close") return nil } + +func makeStubUploadBinaries(stub *jujutesting.Stub) func(migration.UploadBinariesConfig) error { + return func(config migration.UploadBinariesConfig) error { + stub.AddCall( + "UploadBinaries", + config.Charms, + config.CharmDownloader, + config.Tools, + config.ToolsDownloader, + ) + return nil + } +} + +// nullUploadBinaries is a UploadBinaries variant which is intended to +// not get called. +func nullUploadBinaries(migration.UploadBinariesConfig) error { + panic("should not get called") +} + +var fakeCharmDownloader = struct{ migration.CharmDownloader }{} + +var fakeToolsDownloader = struct{ migration.ToolsDownloader }{} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/migrationminion/worker.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/migrationminion/worker.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/migrationminion/worker.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/migrationminion/worker.go 2016-08-16 08:56:25.000000000 +0000 @@ -20,11 +20,8 @@ // Facade exposes controller functionality to a Worker. type Facade interface { - - // Watch returns a watcher which reports when the status changes - // for the migration for the model associated with the API - // connection. Watch() (watcher.MigrationStatusWatcher, error) + Report(migrationId string, phase migration.Phase, success bool) error } // Config defines the operation of a Worker. @@ -108,7 +105,7 @@ func (w *Worker) handle(status watcher.MigrationStatus) error { logger.Infof("migration phase is now: %s", status.Phase) - if status.Phase == migration.NONE { + if !status.Phase.IsRunning() { return w.config.Guard.Unlock() } @@ -120,44 +117,41 @@ } switch status.Phase { - case migration.QUIESCE: - // TODO(mjs) - once Will's stable mode work comes - // together this worker will only start up when a - // migration is active. Here the minion should report - // to the controller that it is running so that the - // migration can progress to READONLY. - case migration.VALIDATION: - // TODO(mjs) - check connection to the target - // controller here and report success/failure. case migration.SUCCESS: - err := w.doSUCCESS(status.TargetAPIAddrs, status.TargetCACert) - if err != nil { + // Report first because the config update in doSUCCESS will + // cause the API connection to drop. The SUCCESS phase is the + // point of no return anyway. + if err := w.report(status, true); err != nil { + return errors.Trace(err) + } + if err = w.doSUCCESS(status); err != nil { return errors.Trace(err) } - case migration.ABORT: - // TODO(mjs) - exit here once Will's stable mode work - // comes together. The minion is done if these phases - // are reached. default: // The minion doesn't need to do anything for other // migration phases. } - return nil + return errors.Trace(err) } -func (w *Worker) doSUCCESS(targetAddrs []string, caCert string) error { - hps, err := apiAddrsToHostPorts(targetAddrs) +func (w *Worker) doSUCCESS(status watcher.MigrationStatus) error { + hps, err := apiAddrsToHostPorts(status.TargetAPIAddrs) if err != nil { return errors.Annotate(err, "converting API addresses") } err = w.config.Agent.ChangeConfig(func(conf agent.ConfigSetter) error { conf.SetAPIHostPorts(hps) - conf.SetCACert(caCert) + conf.SetCACert(status.TargetCACert) return nil }) return errors.Annotate(err, "setting agent config") } +func (w *Worker) report(status watcher.MigrationStatus, success bool) error { + err := w.config.Facade.Report(status.MigrationId, status.Phase, success) + return errors.Annotate(err, "failed to report phase progress") +} + func apiAddrsToHostPorts(addrs []string) ([][]network.HostPort, error) { hps, err := network.ParseHostPorts(addrs...) if err != nil { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/migrationminion/worker_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/migrationminion/worker_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/migrationminion/worker_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/migrationminion/worker_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -110,17 +110,32 @@ s.stub.CheckCallNames(c, "Watch", "Lockdown") } -func (s *Suite) TestNONE(c *gc.C) { - s.client.watcher.changes <- watcher.MigrationStatus{ - Phase: migration.NONE, +func (s *Suite) TestNonRunningPhases(c *gc.C) { + phases := []migration.Phase{ + migration.UNKNOWN, + migration.NONE, + migration.LOGTRANSFER, + migration.REAP, + migration.REAPFAILED, + migration.DONE, + migration.ABORT, + migration.ABORTDONE, + } + for _, phase := range phases { + s.checkNonRunningPhase(c, phase) } +} + +func (s *Suite) checkNonRunningPhase(c *gc.C, phase migration.Phase) { + c.Logf("checking %s", phase) + s.stub.ResetCalls() + s.client.watcher.changes <- watcher.MigrationStatus{Phase: phase} w, err := migrationminion.New(migrationminion.Config{ Facade: s.client, Guard: s.guard, Agent: s.agent, }) c.Assert(err, jc.ErrorIsNil) - workertest.CheckAlive(c, w) workertest.CleanKill(c, w) s.stub.CheckCallNames(c, "Watch", "Unlock") @@ -129,6 +144,7 @@ func (s *Suite) TestSUCCESS(c *gc.C) { addrs := []string{"1.1.1.1:1", "9.9.9.9:9"} s.client.watcher.changes <- watcher.MigrationStatus{ + MigrationId: "id", Phase: migration.SUCCESS, TargetAPIAddrs: addrs, TargetCACert: "top secret", @@ -143,12 +159,13 @@ select { case <-s.agent.configChanged: case <-time.After(coretesting.LongWait): - c.Fatal("timed out waiting for config to be changed") + c.Fatal("timed out") } workertest.CleanKill(c, w) c.Assert(s.agent.conf.addrs, gc.DeepEquals, addrs) c.Assert(s.agent.conf.caCert, gc.DeepEquals, "top secret") - s.stub.CheckCallNames(c, "Watch", "Lockdown") + s.stub.CheckCallNames(c, "Watch", "Lockdown", "Report") + s.stub.CheckCall(c, 2, "Report", "id", migration.SUCCESS, true) } func newStubGuard(stub *jujutesting.Stub) *stubGuard { @@ -192,6 +209,11 @@ return c.watcher, nil } +func (c *stubMinionClient) Report(id string, phase migration.Phase, success bool) error { + c.stub.MethodCall(c, "Report", id, phase, success) + return nil +} + func newStubWatcher() *stubWatcher { return &stubWatcher{ Worker: workertest.NewErrorWorker(nil), diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/modelworkermanager/modelworkermanager.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/modelworkermanager/modelworkermanager.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/modelworkermanager/modelworkermanager.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/modelworkermanager/modelworkermanager.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,13 +7,15 @@ "time" "github.com/juju/errors" - "github.com/juju/utils/set" + "github.com/juju/loggo" "github.com/juju/juju/state" "github.com/juju/juju/worker" "github.com/juju/juju/worker/catacomb" ) +var logger = loggo.GetLogger("juju.workers.modelworkermanager") + // Backend defines the State functionality used by the manager worker. type Backend interface { WatchModels() state.StringsWatcher @@ -52,8 +54,7 @@ return nil, errors.Trace(err) } m := &modelWorkerManager{ - config: config, - started: set.NewStrings(), + config: config, } err := catacomb.Invoke(catacomb.Plan{ @@ -70,7 +71,6 @@ catacomb catacomb.Catacomb config Config runner worker.Runner - started set.Strings } // Kill satisfies the Worker interface. @@ -113,25 +113,16 @@ } func (m *modelWorkerManager) ensure(uuid string) error { - if m.started.Contains(uuid) { - // A second StartWorker for a given ID is mostly - // harmless -- it will probably be ignored, but - // might work if the previous worker has already - // stopped without error. Neither situation will - // hurt us directly, but we prefer to eliminate - // the messy uncertainty. - return nil - } starter := m.starter(uuid) if err := m.runner.StartWorker(uuid, starter); err != nil { return errors.Trace(err) } - m.started.Add(uuid) return nil } func (m *modelWorkerManager) starter(uuid string) func() (worker.Worker, error) { return func() (worker.Worker, error) { + logger.Debugf("starting workers for %s", uuid) worker, err := m.config.NewWorker(uuid) if err != nil { return nil, errors.Annotatef(err, "cannot manage model %q", uuid) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/modelworkermanager/modelworkermanager_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/modelworkermanager/modelworkermanager_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/modelworkermanager/modelworkermanager_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/modelworkermanager/modelworkermanager_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -87,16 +87,20 @@ }) } -func (s *suite) TestNeverRestartsFinishedWorker(c *gc.C) { +func (s *suite) TestRestartsFinishedWorker(c *gc.C) { + // It must be possible to restart the workers for a model due to + // model migrations: a model can be migrated away from a + // controller and then migrated back later. s.runTest(c, func(w worker.Worker, backend *mockBackend) { backend.sendModelChange("uuid") workers := s.waitWorkers(c, 1) - workers[0].tomb.Kill(nil) + workertest.CleanKill(c, workers[0]) + + s.assertNoWorkers(c) - // even when we get a change for it backend.sendModelChange("uuid") workertest.CheckAlive(c, w) - s.assertNoWorkers(c) + s.waitWorkers(c, 1) }) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/peergrouper/initiate.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/peergrouper/initiate.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/peergrouper/initiate.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/peergrouper/initiate.go 2016-08-16 08:56:25.000000000 +0000 @@ -15,6 +15,7 @@ "github.com/juju/juju/mongo" ) +// TODO(katco): 2016-08-09: lp:1611427 var initiateAttemptStrategy = utils.AttemptStrategy{ Total: 60 * time.Second, Delay: 5 * time.Second, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/peergrouper/worker.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/peergrouper/worker.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/peergrouper/worker.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/peergrouper/worker.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,8 +11,6 @@ "github.com/juju/loggo" "github.com/juju/replicaset" - "github.com/juju/juju/apiserver/common/networkingcommon" - "github.com/juju/juju/environs/config" "github.com/juju/juju/instance" "github.com/juju/juju/network" "github.com/juju/juju/state" @@ -32,7 +30,6 @@ Space(id string) (SpaceReader, error) SetOrGetMongoSpaceName(spaceName network.SpaceName) (network.SpaceName, error) SetMongoSpaceState(mongoSpaceState state.MongoSpaceStates) error - ModelConfig() (*config.Config, error) } type stateMachine interface { @@ -108,7 +105,7 @@ // New returns a new worker that maintains the mongo replica set // with respect to the given state. -func New(st *state.State) (worker.Worker, error) { +func New(st *state.State, supportsSpaces bool) (worker.Worker, error) { cfg, err := st.ControllerConfig() if err != nil { return nil, err @@ -118,7 +115,6 @@ mongoPort: cfg.StatePort(), apiPort: cfg.APIPort(), } - supportsSpaces := networkingcommon.SupportsSpaces(shim) == nil return newWorker(shim, newPublisher(st), supportsSpaces) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/provisioner/container_initialisation.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/provisioner/container_initialisation.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/provisioner/container_initialisation.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/provisioner/container_initialisation.go 2016-08-16 08:56:25.000000000 +0000 @@ -31,7 +31,6 @@ type ContainerSetup struct { runner worker.Runner supportedContainers []instance.ContainerType - imageURLGetter container.ImageURLGetter provisioner *apiprovisioner.State machine *apiprovisioner.Machine config agent.Config @@ -52,7 +51,6 @@ Runner worker.Runner WorkerName string SupportedContainers []instance.ContainerType - ImageURLGetter container.ImageURLGetter Machine *apiprovisioner.Machine Provisioner *apiprovisioner.State Config agent.Config @@ -64,7 +62,6 @@ func NewContainerSetupHandler(params ContainerSetupParams) watcher.StringsHandler { return &ContainerSetup{ runner: params.Runner, - imageURLGetter: params.ImageURLGetter, machine: params.Machine, supportedContainers: params.SupportedContainers, provisioner: params.Provisioner, @@ -156,14 +153,17 @@ Name: cs.initLockName, Clock: clock.WallClock, // If we don't get the lock straigh away, there is no point trying multiple - // times per second for an operation that is likelty to ake multiple seconds. + // times per second for an operation that is likelty to take multiple seconds. Delay: time.Second, Cancel: abort, } + logger.Debugf("acquire lock %q for container initialisation", cs.initLockName) releaser, err := mutex.Acquire(spec) if err != nil { return errors.Annotate(err, "failed to acquire initialization lock") } + logger.Debugf("lock %q acquired", cs.initLockName) + defer logger.Debugf("release lock %q for container initialisation", cs.initLockName) defer releaser.Release() if err := initialiser.Initialise(); err != nil { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/provisioner/container_initialisation_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/provisioner/container_initialisation_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/provisioner/container_initialisation_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/provisioner/container_initialisation_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,14 +5,12 @@ import ( "fmt" - "io/ioutil" "os/exec" "runtime" "sync/atomic" "time" "github.com/juju/mutex" - "github.com/juju/testing" jc "github.com/juju/testing/checkers" "github.com/juju/utils/arch" "github.com/juju/utils/clock" @@ -26,7 +24,6 @@ "github.com/juju/juju/agent" apiprovisioner "github.com/juju/juju/api/provisioner" "github.com/juju/juju/container" - containertesting "github.com/juju/juju/container/testing" "github.com/juju/juju/environs" "github.com/juju/juju/instance" "github.com/juju/juju/state" @@ -77,7 +74,7 @@ // Set up provisioner for the state machine. s.agentConfig = s.AgentConfigForTag(c, names.NewMachineTag("0")) var err error - s.p, err = provisioner.NewEnvironProvisioner(s.provisioner, s.agentConfig) + s.p, err = provisioner.NewEnvironProvisioner(s.provisioner, s.agentConfig, s.Environ) c.Assert(err, jc.ErrorIsNil) s.lockName = "provisioner-test" } @@ -90,9 +87,8 @@ } func (s *ContainerSetupSuite) setupContainerWorker(c *gc.C, tag names.MachineTag) (watcher.StringsHandler, worker.Runner) { - testing.PatchExecutable(c, s, "ubuntu-cloudimg-query", containertesting.FakeLxcURLScript) runner := worker.NewRunner(allFatal, noImportance, worker.RestartDelay) - pr := s.st.Provisioner() + pr := apiprovisioner.NewState(s.st) machine, err := pr.Machine(tag) c.Assert(err, jc.ErrorIsNil) err = machine.SetSupportedContainers(instance.ContainerTypes...) @@ -104,7 +100,6 @@ Runner: runner, WorkerName: watcherName, SupportedContainers: instance.ContainerTypes, - ImageURLGetter: &containertesting.MockURLGetter{}, Machine: machine, Provisioner: pr, Config: cfg, @@ -120,7 +115,7 @@ } func (s *ContainerSetupSuite) createContainer(c *gc.C, host *state.Machine, ctype instance.ContainerType) { - inst := s.checkStartInstanceNoSecureConnection(c, host) + inst := s.checkStartInstance(c, host) s.setupContainerWorker(c, host.Tag().(names.MachineTag)) // make a container on the host machine @@ -248,7 +243,7 @@ } func (s *ContainerSetupSuite) TestContainerManagerConfigName(c *gc.C) { - pr := s.st.Provisioner() + pr := apiprovisioner.NewState(s.st) cfg, err := provisioner.ContainerManagerConfig(instance.KVM, pr, s.agentConfig) c.Assert(err, jc.ErrorIsNil) c.Assert(cfg[container.ConfigModelUUID], gc.Equals, coretesting.ModelTag.Id()) @@ -363,26 +358,6 @@ } -func AssertFileContains(c *gc.C, filename string, expectedContent ...string) { - // TODO(dimitern): We should put this in juju/testing repo and - // replace all similar checks with it. - data, err := ioutil.ReadFile(filename) - c.Assert(err, jc.ErrorIsNil) - for _, s := range expectedContent { - c.Assert(string(data), jc.Contains, s) - } -} - -func AssertFileContents(c *gc.C, checker gc.Checker, filename string, expectedContent ...string) { - // TODO(dimitern): We should put this in juju/testing repo and - // replace all similar checks with it. - data, err := ioutil.ReadFile(filename) - c.Assert(err, jc.ErrorIsNil) - for _, s := range expectedContent { - c.Assert(string(data), checker, s) - } -} - type toolsFinderFunc func(v version.Number, series string, arch string) (tools.List, error) func (t toolsFinderFunc) FindTools(v version.Number, series string, arch string) (tools.List, error) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/provisioner/manifold.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/provisioner/manifold.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/provisioner/manifold.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/provisioner/manifold.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,6 +9,7 @@ "github.com/juju/juju/agent" "github.com/juju/juju/api/base" apiprovisioner "github.com/juju/juju/api/provisioner" + "github.com/juju/juju/environs" "github.com/juju/juju/worker" "github.com/juju/juju/worker/dependency" ) @@ -21,25 +22,39 @@ type ManifoldConfig struct { AgentName string APICallerName string + EnvironName string + + NewProvisionerFunc func(*apiprovisioner.State, agent.Config, environs.Environ) (Provisioner, error) } // Manifold creates a manifold that runs an environemnt provisioner. See the // ManifoldConfig type for discussion about how this can/should evolve. func Manifold(config ManifoldConfig) dependency.Manifold { return dependency.Manifold{ - Inputs: []string{config.AgentName, config.APICallerName}, + Inputs: []string{ + config.AgentName, + config.APICallerName, + config.EnvironName, + }, Start: func(context dependency.Context) (worker.Worker, error) { var agent agent.Agent if err := context.Get(config.AgentName, &agent); err != nil { return nil, errors.Trace(err) } + var apiCaller base.APICaller if err := context.Get(config.APICallerName, &apiCaller); err != nil { return nil, errors.Trace(err) } + + var environ environs.Environ + if err := context.Get(config.EnvironName, &environ); err != nil { + return nil, errors.Trace(err) + } + api := apiprovisioner.NewState(apiCaller) - config := agent.CurrentConfig() - w, err := NewEnvironProvisioner(api, config) + agentConfig := agent.CurrentConfig() + w, err := config.NewProvisionerFunc(api, agentConfig, environ) if err != nil { return nil, errors.Trace(err) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/provisioner/manifold_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/provisioner/manifold_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/provisioner/manifold_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/provisioner/manifold_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,6 +11,9 @@ "github.com/juju/juju/agent" "github.com/juju/juju/api/base" + apitesting "github.com/juju/juju/api/base/testing" + apiprovisioner "github.com/juju/juju/api/provisioner" + "github.com/juju/juju/environs" "github.com/juju/juju/worker/dependency" dt "github.com/juju/juju/worker/dependency/testing" "github.com/juju/juju/worker/provisioner" @@ -18,44 +21,84 @@ type ManifoldSuite struct { testing.IsolationSuite + stub testing.Stub } var _ = gc.Suite(&ManifoldSuite{}) -func (s *ManifoldSuite) TestManifold(c *gc.C) { - manifold := provisioner.Manifold(provisioner.ManifoldConfig{ - AgentName: "jeff", - APICallerName: "barry", +func (s *ManifoldSuite) makeManifold() dependency.Manifold { + fakeNewProvFunc := func( + apiSt *apiprovisioner.State, + agentConf agent.Config, + environ environs.Environ, + ) (provisioner.Provisioner, error) { + s.stub.AddCall("NewProvisionerFunc") + return struct{ provisioner.Provisioner }{}, nil + } + return provisioner.Manifold(provisioner.ManifoldConfig{ + AgentName: "agent", + APICallerName: "api-caller", + EnvironName: "environ", + NewProvisionerFunc: fakeNewProvFunc, }) +} - c.Check(manifold.Inputs, jc.DeepEquals, []string{"jeff", "barry"}) +func (s *ManifoldSuite) TestManifold(c *gc.C) { + manifold := s.makeManifold() + c.Check(manifold.Inputs, jc.SameContents, []string{"agent", "api-caller", "environ"}) c.Check(manifold.Output, gc.IsNil) c.Check(manifold.Start, gc.NotNil) - // manifold.Start is tested extensively via direct use in provisioner_test } func (s *ManifoldSuite) TestMissingAgent(c *gc.C) { - manifold := provisioner.Manifold(provisioner.ManifoldConfig{ - AgentName: "agent", - APICallerName: "api-caller", - }) + manifold := s.makeManifold() w, err := manifold.Start(dt.StubContext(nil, map[string]interface{}{ "agent": dependency.ErrMissing, "api-caller": struct{ base.APICaller }{}, + "environ": struct{ environs.Environ }{}, })) c.Check(w, gc.IsNil) c.Check(errors.Cause(err), gc.Equals, dependency.ErrMissing) } func (s *ManifoldSuite) TestMissingAPICaller(c *gc.C) { - manifold := provisioner.Manifold(provisioner.ManifoldConfig{ - AgentName: "agent", - APICallerName: "api-caller", - }) + manifold := s.makeManifold() w, err := manifold.Start(dt.StubContext(nil, map[string]interface{}{ "agent": struct{ agent.Agent }{}, "api-caller": dependency.ErrMissing, + "environ": struct{ environs.Environ }{}, })) c.Check(w, gc.IsNil) c.Check(errors.Cause(err), gc.Equals, dependency.ErrMissing) } + +func (s *ManifoldSuite) TestMissingEnviron(c *gc.C) { + manifold := s.makeManifold() + w, err := manifold.Start(dt.StubContext(nil, map[string]interface{}{ + "agent": struct{ agent.Agent }{}, + "api-caller": struct{ base.APICaller }{}, + "environ": dependency.ErrMissing, + })) + c.Check(w, gc.IsNil) + c.Check(errors.Cause(err), gc.Equals, dependency.ErrMissing) +} + +func (s *ManifoldSuite) TestStarts(c *gc.C) { + manifold := s.makeManifold() + w, err := manifold.Start(dt.StubContext(nil, map[string]interface{}{ + "agent": new(fakeAgent), + "api-caller": apitesting.APICallerFunc(nil), + "environ": struct{ environs.Environ }{}, + })) + c.Check(w, gc.NotNil) + c.Check(err, jc.ErrorIsNil) + s.stub.CheckCallNames(c, "NewProvisionerFunc") +} + +type fakeAgent struct { + agent.Agent +} + +func (a *fakeAgent) CurrentConfig() agent.Config { + return nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/provisioner/provisioner.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/provisioner/provisioner.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/provisioner/provisioner.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/provisioner/provisioner.go 2016-08-16 08:56:25.000000000 +0000 @@ -20,7 +20,6 @@ "github.com/juju/juju/watcher" "github.com/juju/juju/worker" "github.com/juju/juju/worker/catacomb" - "github.com/juju/juju/worker/environ" ) var logger = loggo.GetLogger("juju.provisioner") @@ -70,6 +69,8 @@ // RetryStrategy defines the retry behavior when encountering a retryable // error during provisioning. +// +// TODO(katco): 2016-08-09: lp:1611427 type RetryStrategy struct { retryDelay time.Duration retryCount int @@ -175,15 +176,17 @@ // NewEnvironProvisioner returns a new Provisioner for an environment. // When new machines are added to the state, it allocates instances // from the environment and allocates them to the new machines. -func NewEnvironProvisioner(st *apiprovisioner.State, agentConfig agent.Config) (Provisioner, error) { +func NewEnvironProvisioner(st *apiprovisioner.State, agentConfig agent.Config, environ environs.Environ) (Provisioner, error) { p := &environProvisioner{ provisioner: provisioner{ st: st, agentConfig: agentConfig, toolsFinder: getToolsFinder(st), }, + environ: environ, } p.Provisioner = p + p.broker = environ logger.Tracef("Starting environ provisioner for %q", p.agentConfig.Tag()) err := catacomb.Invoke(catacomb.Plan{ @@ -197,6 +200,10 @@ } func (p *environProvisioner) loop() error { + // TODO(mjs channeling axw) - It would be better if there were + // APIs to watch and fetch provisioner specific config instead of + // watcher for all changes to model config. This would avoid the + // need for a full model config. var modelConfigChanges <-chan struct{} modelWatcher, err := p.st.WatchForModelConfigChanges() if err != nil { @@ -207,15 +214,6 @@ } modelConfigChanges = modelWatcher.Changes() - p.environ, err = environ.WaitForEnviron(modelWatcher, p.st, environs.New, p.catacomb.Dying()) - if err != nil { - if err == environ.ErrWaitAborted { - return p.catacomb.ErrDying() - } - return loggedErrorStack(errors.Trace(err)) - } - p.broker = p.environ - modelConfig := p.environ.Config() p.configObserver.notify(modelConfig) harvestMode := modelConfig.ProvisionerHarvestMode() @@ -328,7 +326,7 @@ return p.catacomb.ErrDying() case _, ok := <-modelWatcher.Changes(): if !ok { - return errors.New("model configuratioon watch closed") + return errors.New("model configuration watch closed") } modelConfig, err := p.st.ModelConfig() if err != nil { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/provisioner/provisioner_task.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/provisioner/provisioner_task.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/provisioner/provisioner_task.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/provisioner/provisioner_task.go 2016-08-16 08:56:25.000000000 +0000 @@ -58,9 +58,6 @@ FindTools(version version.Number, series string, arch string) (coretools.List, error) } -var _ MachineGetter = (*apiprovisioner.State)(nil) -var _ ToolsFinder = (*apiprovisioner.State)(nil) - func NewProvisionerTask( controllerUUID string, machineTag names.MachineTag, @@ -697,61 +694,65 @@ provisioningInfo *params.ProvisioningInfo, startInstanceParams environs.StartInstanceParams, ) error { - - result, err := task.broker.StartInstance(startInstanceParams) - if err != nil { - if !instance.IsRetryableCreationError(errors.Cause(err)) { - // Set the state to error, so the machine will be skipped next - // time until the error is resolved, but don't return an - // error; just keep going with the other machines. + var result *environs.StartInstanceResult + for attemptsLeft := task.retryStartInstanceStrategy.retryCount; attemptsLeft >= 0; attemptsLeft-- { + attemptResult, err := task.broker.StartInstance(startInstanceParams) + if err == nil { + result = attemptResult + break + } else if attemptsLeft <= 0 { + // Set the state to error, so the machine will be skipped + // next time until the error is resolved, but don't return + // an error; just keep going with the other machines. return task.setErrorStatus("cannot start instance for machine %q: %v", machine, err) } - logger.Infof("retryable error received on start instance: %v", err) - for count := task.retryStartInstanceStrategy.retryCount; count > 0; count-- { - if task.retryStartInstanceStrategy.retryDelay > 0 { - select { - case <-task.catacomb.Dying(): - return task.catacomb.ErrDying() - case <-time.After(task.retryStartInstanceStrategy.retryDelay): - } - } - result, err = task.broker.StartInstance(startInstanceParams) - if err == nil { - break - } - // If this was the last attempt and an error was received, set the error - // status on the machine. - if count == 1 { - return task.setErrorStatus("cannot start instance for machine %q: %v", machine, err) - } + logger.Warningf("%v", errors.Annotate(err, "starting instance")) + retryMsg := fmt.Sprintf("will retry to start instance in %v", task.retryStartInstanceStrategy.retryDelay) + if err2 := machine.SetStatus(status.StatusPending, retryMsg, nil); err2 != nil { + logger.Errorf("%v", err2) + } + logger.Infof(retryMsg) + + select { + case <-task.catacomb.Dying(): + return task.catacomb.ErrDying() + case <-time.After(task.retryStartInstanceStrategy.retryDelay): } } - inst := result.Instance - hardware := result.Hardware - nonce := startInstanceParams.InstanceConfig.MachineNonce networkConfig := networkingcommon.NetworkConfigFromInterfaceInfo(result.NetworkInfo) volumes := volumesToApiserver(result.Volumes) - volumeAttachments := volumeAttachmentsToApiserver(result.VolumeAttachments) + volumeNameToAttachmentInfo := volumeAttachmentsToApiserver(result.VolumeAttachments) - err = machine.SetInstanceInfo(inst.Id(), nonce, hardware, networkConfig, volumes, volumeAttachments) - if err == nil { - logger.Infof( - "started machine %s as instance %s with hardware %q, network config %+v, volumes %v, volume attachments %v, subnets to zones %v", - machine, inst.Id(), hardware, - networkConfig, - volumes, volumeAttachments, - startInstanceParams.SubnetsToZones, - ) - return nil - } - // We need to stop the instance right away here, set error status and go on. - task.setErrorStatus("cannot register instance for machine %v: %v", machine, err) - if err := task.broker.StopInstances(inst.Id()); err != nil { - // We cannot even stop the instance, log the error and quit. - return errors.Annotatef(err, "cannot stop instance %q for machine %v", inst.Id(), machine) - } + if err := machine.SetInstanceInfo( + result.Instance.Id(), + startInstanceParams.InstanceConfig.MachineNonce, + result.Hardware, + networkConfig, + volumes, + volumeNameToAttachmentInfo, + ); err != nil { + // We need to stop the instance right away here, set error status and go on. + if err2 := task.setErrorStatus("cannot register instance for machine %v: %v", machine, err); err2 != nil { + logger.Errorf("%v", errors.Annotate(err2, "cannot set machine's status")) + } + if err2 := task.broker.StopInstances(result.Instance.Id()); err2 != nil { + logger.Errorf("%v", errors.Annotate(err2, "after failing to set instance info")) + } + return errors.Annotate(err, "cannot set instance info") + } + + logger.Infof( + "started machine %s as instance %s with hardware %q, network config %+v, volumes %v, volume attachments %v, subnets to zones %v", + machine, + result.Instance.Id(), + result.Hardware, + networkConfig, + volumes, + volumeNameToAttachmentInfo, + startInstanceParams.SubnetsToZones, + ) return nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/provisioner/provisioner_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/provisioner/provisioner_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/provisioner/provisioner_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/provisioner/provisioner_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,7 +5,6 @@ import ( "fmt" - "strconv" "strings" "time" @@ -43,13 +42,10 @@ "github.com/juju/juju/status" "github.com/juju/juju/storage" "github.com/juju/juju/storage/poolmanager" - dummystorage "github.com/juju/juju/storage/provider/dummy" - "github.com/juju/juju/storage/provider/registry" coretesting "github.com/juju/juju/testing" coretools "github.com/juju/juju/tools" jujuversion "github.com/juju/juju/version" "github.com/juju/juju/worker" - dt "github.com/juju/juju/worker/dependency/testing" "github.com/juju/juju/worker/provisioner" ) @@ -115,12 +111,6 @@ } func (s *CommonProvisionerSuite) SetUpTest(c *gc.C) { - // Disable the default state policy, because the - // provisioner needs to be able to test pathological - // scenarios where a machine exists in state with - // invalid environment config. - dummy.SetStatePolicy(nil) - s.JujuConnSuite.SetUpTest(c) // We do not want to pull published image metadata for tests... @@ -182,7 +172,7 @@ s.st = s.OpenAPIAsMachine(c, machine.Tag(), password, agent.BootstrapNonce) c.Assert(s.st, gc.NotNil) c.Logf("API: login as %q successful", machine.Tag()) - s.provisioner = s.st.Provisioner() + s.provisioner = apiprovisioner.NewState(s.st) c.Assert(s.provisioner, gc.NotNil) } @@ -207,11 +197,7 @@ } func (s *CommonProvisionerSuite) checkStartInstance(c *gc.C, m *state.Machine) instance.Instance { - return s.checkStartInstanceCustom(c, m, "pork", s.defaultConstraints, nil, nil, nil, true, nil, true) -} - -func (s *CommonProvisionerSuite) checkStartInstanceNoSecureConnection(c *gc.C, m *state.Machine) instance.Instance { - return s.checkStartInstanceCustom(c, m, "pork", s.defaultConstraints, nil, nil, nil, false, nil, true) + return s.checkStartInstanceCustom(c, m, "pork", s.defaultConstraints, nil, nil, nil, nil, true) } func (s *CommonProvisionerSuite) checkStartInstanceCustom( @@ -220,7 +206,6 @@ networkInfo []network.InterfaceInfo, subnetsToZones map[network.Id][]string, volumes []storage.Volume, - secureServerConnection bool, checkPossibleTools coretools.List, waitInstanceId bool, ) ( @@ -247,7 +232,6 @@ c.Assert(o.SubnetsToZones, jc.DeepEquals, subnetsToZones) c.Assert(o.NetworkInfo, jc.DeepEquals, networkInfo) c.Assert(o.Volumes, jc.DeepEquals, volumes) - c.Assert(o.AgentEnvironment["SECURE_CONTROLLER_CONNECTION"], gc.Equals, strconv.FormatBool(secureServerConnection)) var jobs []multiwatcher.MachineJob for _, job := range m.Jobs() { @@ -426,19 +410,10 @@ func (s *CommonProvisionerSuite) newEnvironProvisioner(c *gc.C) provisioner.Provisioner { machineTag := names.NewMachineTag("0") agentConfig := s.AgentConfigForTag(c, machineTag) - context := dt.StubContext(nil, map[string]interface{}{ - "agent": mockAgent{config: agentConfig}, - "api-caller": s.st, - }) - manifold := provisioner.Manifold(provisioner.ManifoldConfig{ - AgentName: "agent", - APICallerName: "api-caller", - }) - untyped, err := manifold.Start(context) + apiState := apiprovisioner.NewState(s.st) + w, err := provisioner.NewEnvironProvisioner(apiState, agentConfig, s.Environ) c.Assert(err, jc.ErrorIsNil) - typed, ok := untyped.(provisioner.Provisioner) - c.Assert(ok, jc.IsTrue) - return typed + return w } func (s *CommonProvisionerSuite) addMachine() (*state.Machine, error) { @@ -477,7 +452,7 @@ // Check that an instance is provisioned when the machine is created... m, err := s.addMachine() c.Assert(err, jc.ErrorIsNil) - instance := s.checkStartInstanceNoSecureConnection(c, m) + instance := s.checkStartInstance(c, m) // ...and removed, along with the machine, when the machine is Dead. c.Assert(m.EnsureDead(), gc.IsNil) @@ -496,7 +471,7 @@ // Start a provisioner and check those constraints are used. p := s.newEnvironProvisioner(c) defer stop(c, p) - s.checkStartInstanceCustom(c, m, "pork", cons, nil, nil, nil, false, nil, true) + s.checkStartInstanceCustom(c, m, "pork", cons, nil, nil, nil, nil, true) } func (s *ProvisionerSuite) TestPossibleTools(c *gc.C) { @@ -540,7 +515,7 @@ defer stop(c, provisioner) s.checkStartInstanceCustom( c, machine, "pork", constraints.Value{}, - nil, nil, nil, false, expectedList, true, + nil, nil, nil, expectedList, true, ) } @@ -594,7 +569,7 @@ cleanup := dummy.PatchTransientErrorInjectionChannel(errorInjectionChannel) defer cleanup() - retryableError := instance.NewRetryableCreationError("container failed to start and was destroyed") + retryableError := errors.New("container failed to start and was destroyed") destroyError := errors.New("container failed to start and failed to destroy: manual cleanup of containers needed") // send the error message three times, because the provisioner will retry twice as patched above. errorInjectionChannel <- retryableError @@ -640,75 +615,12 @@ // send the error message once // - instance creation should succeed - retryableError := instance.NewRetryableCreationError("container failed to start and was destroyed") - errorInjectionChannel <- retryableError - - m, err := s.addMachine() - c.Assert(err, jc.ErrorIsNil) - s.checkStartInstanceNoSecureConnection(c, m) -} - -func (s *ProvisionerSuite) TestProvisionerSucceedStartInstanceWithInjectedWrappedRetryableCreationError(c *gc.C) { - // Set the retry delay to 0, and retry count to 1 to keep tests short - s.PatchValue(provisioner.RetryStrategyDelay, 0*time.Second) - s.PatchValue(provisioner.RetryStrategyCount, 1) - - // create the error injection channel - errorInjectionChannel := make(chan error, 1) - c.Assert(errorInjectionChannel, gc.NotNil) - - p := s.newEnvironProvisioner(c) - defer stop(c, p) - - // patch the dummy provider error injection channel - cleanup := dummy.PatchTransientErrorInjectionChannel(errorInjectionChannel) - defer cleanup() - - // send the error message once - // - instance creation should succeed - retryableError := errors.Wrap(errors.New(""), instance.NewRetryableCreationError("container failed to start and was destroyed")) + retryableError := errors.New("container failed to start and was destroyed") errorInjectionChannel <- retryableError m, err := s.addMachine() c.Assert(err, jc.ErrorIsNil) - s.checkStartInstanceNoSecureConnection(c, m) -} - -func (s *ProvisionerSuite) TestProvisionerFailStartInstanceWithInjectedNonRetryableCreationError(c *gc.C) { - // create the error injection channel - errorInjectionChannel := make(chan error, 1) - c.Assert(errorInjectionChannel, gc.NotNil) - - p := s.newEnvironProvisioner(c) - defer stop(c, p) - - // patch the dummy provider error injection channel - cleanup := dummy.PatchTransientErrorInjectionChannel(errorInjectionChannel) - defer cleanup() - - // send the error message once - nonRetryableError := errors.New("some nonretryable error") - errorInjectionChannel <- nonRetryableError - - m, err := s.addMachine() - c.Assert(err, jc.ErrorIsNil) - s.checkNoOperations(c) - - t0 := time.Now() - for time.Since(t0) < coretesting.LongWait { - // And check the machine status is set to error. - statusInfo, err := m.Status() - c.Assert(err, jc.ErrorIsNil) - if statusInfo.Status == status.StatusPending { - time.Sleep(coretesting.ShortWait) - continue - } - c.Assert(statusInfo.Status, gc.Equals, status.StatusError) - // check that the status matches the error message - c.Assert(statusInfo.Message, gc.Equals, nonRetryableError.Error()) - return - } - c.Fatal("Test took too long to complete") + s.checkStartInstance(c, m) } func (s *ProvisionerSuite) TestProvisionerStopRetryingIfDying(c *gc.C) { @@ -723,7 +635,7 @@ cleanup := dummy.PatchTransientErrorInjectionChannel(errorInjectionChannel) defer cleanup() - retryableError := instance.NewRetryableCreationError("container failed to start and was destroyed") + retryableError := errors.New("container failed to start and was destroyed") errorInjectionChannel <- retryableError m, err := s.addMachine() @@ -745,7 +657,7 @@ // create a machine to host the container. m, err := s.addMachine() c.Assert(err, jc.ErrorIsNil) - inst := s.checkStartInstanceNoSecureConnection(c, m) + inst := s.checkStartInstance(c, m) // make a container on the machine we just created template := state.MachineTemplate{ @@ -773,7 +685,7 @@ // create a machine to host the container. m, err := s.addMachine() c.Assert(err, jc.ErrorIsNil) - inst := s.checkStartInstanceNoSecureConnection(c, m) + inst := s.checkStartInstance(c, m) // make a container on the machine we just created template := state.MachineTemplate{ @@ -981,7 +893,7 @@ c, m, "pork", cons, nil, expectedSubnetsToZones, - nil, false, nil, true, + nil, nil, true, ) // Cleanup. @@ -1068,10 +980,7 @@ func (s *ProvisionerSuite) TestProvisioningMachinesWithRequestedVolumes(c *gc.C) { // Set up a persistent pool. - registry.RegisterProvider("static", &dummystorage.StorageProvider{IsDynamic: false}) - registry.RegisterEnvironStorageProviders("dummy", "static") - defer registry.RegisterProvider("static", nil) - poolManager := poolmanager.New(state.NewStateSettings(s.State)) + poolManager := poolmanager.New(state.NewStateSettings(s.State), s.Environ) _, err := poolManager.Create("persistent-pool", "static", map[string]interface{}{"persistent": true}) c.Assert(err, jc.ErrorIsNil) @@ -1103,7 +1012,7 @@ inst := s.checkStartInstanceCustom( c, m, "pork", s.defaultConstraints, nil, nil, - expectVolumeInfo, false, + expectVolumeInfo, nil, true, ) @@ -1120,7 +1029,7 @@ // create a machine m, err := s.addMachine() c.Assert(err, jc.ErrorIsNil) - s.checkStartInstanceNoSecureConnection(c, m) + s.checkStartInstance(c, m) // restart the PA stop(c, p) @@ -1145,7 +1054,7 @@ // provision a machine m0, err := s.addMachine() c.Assert(err, jc.ErrorIsNil) - s.checkStartInstanceNoSecureConnection(c, m0) + s.checkStartInstance(c, m0) // stop the provisioner and make the machine dying stop(c, p) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/proxyupdater/manifold.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/proxyupdater/manifold.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/proxyupdater/manifold.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/proxyupdater/manifold.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,44 +5,60 @@ import ( "github.com/juju/errors" - "gopkg.in/juju/names.v2" + "github.com/juju/utils/proxy" "github.com/juju/juju/agent" "github.com/juju/juju/api/base" "github.com/juju/juju/api/proxyupdater" - "github.com/juju/juju/cmd/jujud/agent/engine" "github.com/juju/juju/worker" "github.com/juju/juju/worker/dependency" ) // ManifoldConfig defines the names of the manifolds on which a Manifold will depend. -type ManifoldConfig engine.AgentApiManifoldConfig +type ManifoldConfig struct { + AgentName string + APICallerName string + WorkerFunc func(Config) (worker.Worker, error) + ExternalUpdate func(proxy.Settings) error +} // Manifold returns a dependency manifold that runs a proxy updater worker, // using the api connection resource named in the supplied config. func Manifold(config ManifoldConfig) dependency.Manifold { - typedConfig := engine.AgentApiManifoldConfig(config) - return engine.AgentApiManifold(typedConfig, newWorker) -} - -// newWorker is not currently tested; it should eventually replace New as the -// package's exposed factory func, and then all tests should pass through it. -func newWorker(a agent.Agent, apiCaller base.APICaller) (worker.Worker, error) { - agentConfig := a.CurrentConfig() - switch tag := agentConfig.Tag().(type) { - case names.MachineTag, names.UnitTag: - default: - return nil, errors.Errorf("unknown agent type: %T", tag) - } - - proxyAPI, err := proxyupdater.NewAPI(apiCaller, agentConfig.Tag()) - if err != nil { - return nil, err + return dependency.Manifold{ + Inputs: []string{ + config.AgentName, + config.APICallerName, + }, + Start: func(context dependency.Context) (worker.Worker, error) { + if config.WorkerFunc == nil { + return nil, errors.NotValidf("missing WorkerFunc") + } + var agent agent.Agent + if err := context.Get(config.AgentName, &agent); err != nil { + return nil, err + } + var apiCaller base.APICaller + if err := context.Get(config.APICallerName, &apiCaller); err != nil { + return nil, err + } + + agentConfig := agent.CurrentConfig() + proxyAPI, err := proxyupdater.NewAPI(apiCaller, agentConfig.Tag()) + if err != nil { + return nil, err + } + w, err := config.WorkerFunc(Config{ + Directory: "/home/ubuntu", + RegistryPath: `HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings`, + Filename: ".juju-proxy", + API: proxyAPI, + ExternalUpdate: config.ExternalUpdate, + }) + if err != nil { + return nil, errors.Trace(err) + } + return w, nil + }, } - return NewWorker(Config{ - Directory: "/home/ubuntu", - RegistryPath: `HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings`, - Filename: ".juju-proxy", - API: proxyAPI, - }) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/proxyupdater/manifold_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/proxyupdater/manifold_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/proxyupdater/manifold_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/proxyupdater/manifold_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,89 +4,142 @@ package proxyupdater_test import ( + "github.com/juju/errors" "github.com/juju/testing" jc "github.com/juju/testing/checkers" + "github.com/juju/utils/proxy" gc "gopkg.in/check.v1" "gopkg.in/juju/names.v2" "github.com/juju/juju/agent" - "github.com/juju/juju/cmd/jujud/agent/engine/enginetest" + "github.com/juju/juju/api/base" "github.com/juju/juju/worker" + "github.com/juju/juju/worker/dependency" + dt "github.com/juju/juju/worker/dependency/testing" "github.com/juju/juju/worker/proxyupdater" - proxyup "github.com/juju/juju/worker/proxyupdater" ) type ManifoldSuite struct { testing.IsolationSuite - newCalled bool + config proxyupdater.ManifoldConfig + startErr error } var _ = gc.Suite(&ManifoldSuite{}) +func OtherUpdate(proxy.Settings) error { + return nil +} + func (s *ManifoldSuite) SetUpTest(c *gc.C) { - s.newCalled = false - s.PatchValue(&proxyupdater.NewWorker, - func(_ proxyupdater.Config) (worker.Worker, error) { - s.newCalled = true - return nil, nil + s.IsolationSuite.SetUpTest(c) + s.startErr = nil + s.config = proxyupdater.ManifoldConfig{ + AgentName: "agent-name", + APICallerName: "api-caller-name", + WorkerFunc: func(cfg proxyupdater.Config) (worker.Worker, error) { + if s.startErr != nil { + return nil, s.startErr + } + return &dummyWorker{config: cfg}, nil }, - ) + ExternalUpdate: OtherUpdate, + } +} + +func (s *ManifoldSuite) manifold() dependency.Manifold { + return proxyupdater.Manifold(s.config) } -func (s *ManifoldSuite) TestMachineShouldWrite(c *gc.C) { - config := proxyup.ManifoldConfig(enginetest.AgentApiManifoldTestConfig()) - _, err := enginetest.RunAgentApiManifold( - proxyup.Manifold(config), - &fakeAgent{tag: names.NewMachineTag("42")}, - nil) - c.Assert(err, jc.ErrorIsNil) - c.Assert(s.newCalled, jc.IsTrue) -} - -func (s *ManifoldSuite) TestMachineShouldntWrite(c *gc.C) { - config := proxyup.ManifoldConfig(enginetest.AgentApiManifoldTestConfig()) - _, err := enginetest.RunAgentApiManifold( - proxyup.Manifold(config), - &fakeAgent{tag: names.NewMachineTag("42")}, - nil) - c.Assert(err, jc.ErrorIsNil) - c.Assert(s.newCalled, jc.IsTrue) -} - -func (s *ManifoldSuite) TestUnit(c *gc.C) { - config := proxyup.ManifoldConfig(enginetest.AgentApiManifoldTestConfig()) - _, err := enginetest.RunAgentApiManifold( - proxyup.Manifold(config), - &fakeAgent{tag: names.NewUnitTag("foo/0")}, - nil) - c.Assert(err, jc.ErrorIsNil) - c.Assert(s.newCalled, jc.IsTrue) -} - -func (s *ManifoldSuite) TestNonAgent(c *gc.C) { - config := proxyup.ManifoldConfig(enginetest.AgentApiManifoldTestConfig()) - _, err := enginetest.RunAgentApiManifold( - proxyup.Manifold(config), - &fakeAgent{tag: names.NewUserTag("foo")}, - nil) - c.Assert(err, gc.ErrorMatches, "unknown agent type:.+") - c.Assert(s.newCalled, jc.IsFalse) +func (s *ManifoldSuite) TestInputs(c *gc.C) { + c.Check(s.manifold().Inputs, jc.DeepEquals, []string{"agent-name", "api-caller-name"}) } -type fakeAgent struct { +func (s *ManifoldSuite) TestWorkerFuncMissing(c *gc.C) { + s.config.WorkerFunc = nil + context := dt.StubContext(nil, nil) + worker, err := s.manifold().Start(context) + c.Check(worker, gc.IsNil) + c.Check(err, gc.ErrorMatches, "missing WorkerFunc not valid") +} + +func (s *ManifoldSuite) TestStartAgentMissing(c *gc.C) { + context := dt.StubContext(nil, map[string]interface{}{ + "agent-name": dependency.ErrMissing, + }) + + worker, err := s.manifold().Start(context) + c.Check(worker, gc.IsNil) + c.Check(errors.Cause(err), gc.Equals, dependency.ErrMissing) +} + +func (s *ManifoldSuite) TestStartAPICallerMissing(c *gc.C) { + context := dt.StubContext(nil, map[string]interface{}{ + "agent-name": &dummyAgent{}, + "api-caller-name": dependency.ErrMissing, + }) + + worker, err := s.manifold().Start(context) + c.Check(worker, gc.IsNil) + c.Check(errors.Cause(err), gc.Equals, dependency.ErrMissing) +} + +func (s *ManifoldSuite) TestStartError(c *gc.C) { + s.startErr = errors.New("boom") + context := dt.StubContext(nil, map[string]interface{}{ + "agent-name": &dummyAgent{}, + "api-caller-name": &dummyApiCaller{}, + }) + + worker, err := s.manifold().Start(context) + c.Check(worker, gc.IsNil) + c.Check(err, gc.ErrorMatches, "boom") +} + +func (s *ManifoldSuite) TestStartSuccess(c *gc.C) { + context := dt.StubContext(nil, map[string]interface{}{ + "agent-name": &dummyAgent{}, + "api-caller-name": &dummyApiCaller{}, + }) + + worker, err := s.manifold().Start(context) + c.Check(err, jc.ErrorIsNil) + dummy, ok := worker.(*dummyWorker) + c.Assert(ok, jc.IsTrue) + c.Check(dummy.config.Directory, gc.Equals, "/home/ubuntu") + c.Check(dummy.config.RegistryPath, gc.Equals, `HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings`) + c.Check(dummy.config.Filename, gc.Equals, ".juju-proxy") + c.Check(dummy.config.API, gc.NotNil) + // Checking function equality is problematic. + c.Check(dummy.config.ExternalUpdate, gc.NotNil) +} + +type dummyAgent struct { agent.Agent - tag names.Tag } -func (a *fakeAgent) CurrentConfig() agent.Config { - return &fakeConfig{tag: a.tag} +func (*dummyAgent) CurrentConfig() agent.Config { + return &dummyConfig{} } -type fakeConfig struct { +type dummyConfig struct { agent.Config - tag names.Tag } -func (c *fakeConfig) Tag() names.Tag { - return c.tag +func (*dummyConfig) Tag() names.Tag { + return names.NewMachineTag("42") +} + +type dummyApiCaller struct { + base.APICaller +} + +func (*dummyApiCaller) BestFacadeVersion(_ string) int { + return 42 +} + +type dummyWorker struct { + worker.Worker + + config proxyupdater.Config } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/proxyupdater/proxyupdater.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/proxyupdater/proxyupdater.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/proxyupdater/proxyupdater.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/proxyupdater/proxyupdater.go 2016-08-16 08:56:25.000000000 +0000 @@ -27,10 +27,11 @@ ) type Config struct { - Directory string - RegistryPath string - Filename string - API API + Directory string + RegistryPath string + Filename string + API API + ExternalUpdate func(proxyutils.Settings) error } // API is an interface that is provided to New @@ -136,12 +137,7 @@ } func (w *proxyWorker) writeEnvironment() error { - // TODO(dfc) this should be replaced with a switch on os.HostOS() - osystem, err := series.GetOSFromSeries(series.HostSeries()) - if err != nil { - return err - } - switch osystem { + switch os.HostOS() { case os.Windows: return w.writeEnvironmentToRegistry() default: @@ -158,6 +154,12 @@ // It isn't really fatal, but we should record it. logger.Errorf("error writing proxy environment file: %v", err) } + if externalFunc := w.config.ExternalUpdate; externalFunc != nil { + if err := externalFunc(proxySettings); err != nil { + // It isn't really fatal, but we should record it. + logger.Errorf("%v", err) + } + } } } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/proxyupdater/proxyupdater_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/proxyupdater/proxyupdater_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/proxyupdater/proxyupdater_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/proxyupdater/proxyupdater_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -78,9 +78,11 @@ s.BaseSuite.SetUpTest(c) s.api = NewFakeAPI() - s.config.Directory = c.MkDir() - s.config.Filename = "juju-proxy-settings" - s.config.API = s.api + s.config = proxyupdater.Config{ + Directory: c.MkDir(), + Filename: "juju-proxy-settings", + API: s.api, + } s.PatchValue(&pacconfig.AptProxyConfigFile, path.Join(s.config.Directory, "juju-apt-proxy")) s.proxyFile = path.Join(s.config.Directory, s.config.Filename) } @@ -93,9 +95,10 @@ } func (s *ProxyUpdaterSuite) waitProxySettings(c *gc.C, expected proxy.Settings) { + maxWait := time.After(coretesting.LongWait) for { select { - case <-time.After(time.Second): + case <-maxWait: c.Fatalf("timeout while waiting for proxy settings to change") return case <-time.After(10 * time.Millisecond): @@ -117,9 +120,10 @@ if runtime.GOOS == "windows" { c.Skip("Proxy settings are written to the registry on windows") } + maxWait := time.After(coretesting.LongWait) for { select { - case <-time.After(time.Second): + case <-maxWait: c.Fatalf("timeout while waiting for proxy settings to change") return case <-time.After(10 * time.Millisecond): @@ -215,3 +219,26 @@ assertEnv("ftp_proxy", proxySettings.Ftp) assertEnv("no_proxy", proxySettings.NoProxy) } + +func (s *ProxyUpdaterSuite) TestExternalFuncCalled(c *gc.C) { + proxySettings, _ := s.updateConfig(c) + + var externalSettings proxy.Settings + updated := make(chan struct{}) + s.config.ExternalUpdate = func(values proxy.Settings) error { + externalSettings = values + close(updated) + return nil + } + updater, err := proxyupdater.NewWorker(s.config) + c.Assert(err, jc.ErrorIsNil) + defer worker.Stop(updater) + + select { + case <-time.After(time.Second): + c.Fatal("function not called") + case <-updated: + } + + c.Assert(externalSettings, jc.DeepEquals, proxySettings) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/reboot/package_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/reboot/package_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/reboot/package_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/reboot/package_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,14 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package reboot_test + +import ( + "testing" + + coretesting "github.com/juju/juju/testing" +) + +func TestPackage(t *testing.T) { + coretesting.MgoTestPackage(t) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/reboot/reboot.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/reboot/reboot.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/reboot/reboot.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/reboot/reboot.go 2016-08-16 08:56:25.000000000 +0000 @@ -74,14 +74,18 @@ switch rAction { case params.ShouldReboot: + logger.Debugf("acquiring mutex %q for reboot", r.machineLockName) if _, err := mutex.Acquire(spec); err != nil { return errors.Trace(err) } + logger.Debugf("mutex %q acquired, won't release", r.machineLockName) return worker.ErrRebootMachine case params.ShouldShutdown: + logger.Debugf("acquiring mutex %q for shutdown", r.machineLockName) if _, err := mutex.Acquire(spec); err != nil { return errors.Trace(err) } + logger.Debugf("mutex %q acquired, won't release", r.machineLockName) return worker.ErrShutdownMachine default: return nil diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/reboot/reboot_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/reboot/reboot_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/reboot/reboot_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/reboot/reboot_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,7 +4,6 @@ package reboot_test import ( - stdtesting "testing" "time" jc "github.com/juju/testing/checkers" @@ -18,21 +17,10 @@ "github.com/juju/juju/instance" jujutesting "github.com/juju/juju/juju/testing" "github.com/juju/juju/state" - coretesting "github.com/juju/juju/testing" "github.com/juju/juju/worker" "github.com/juju/juju/worker/reboot" ) -func TestPackage(t *stdtesting.T) { - coretesting.MgoTestPackage(t) -} - -type machines struct { - machine *state.Machine - stateAPI api.Connection - rebootState apireboot.State -} - type rebootSuite struct { jujutesting.JujuConnSuite @@ -43,8 +31,7 @@ ct *state.Machine ctRebootState apireboot.State - lockName string - clock clock.Clock + clock clock.Clock } var _ = gc.Suite(&rebootSuite{}) @@ -78,7 +65,6 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(s.ctRebootState, gc.NotNil) - s.lockName = "reboot-test" s.clock = &fakeClock{delay: time.Millisecond} } @@ -86,15 +72,26 @@ s.JujuConnSuite.TearDownTest(c) } +// NOTE: the various reboot tests use a different lock name for each test. +// This is due to the behaviour of the reboot worker. What it does is acquires +// the named process lock and never releases it. This is fine(ish) on linux as the +// garbage collector will eventually clean up the old lock which will release the +// domain socket, but on windows, the actual lock is a system level semaphore wich +// isn't cleaned up by the golang garbage collector, but instead relies on the process +// dying to release the semaphore handle. +// +// If more tests are added here, they each need their own lock name to avoid blocking +// forever on windows. + func (s *rebootSuite) TestStartStop(c *gc.C) { - worker, err := reboot.NewReboot(s.rebootState, s.AgentConfigForTag(c, s.machine.Tag()), s.lockName, s.clock) + worker, err := reboot.NewReboot(s.rebootState, s.AgentConfigForTag(c, s.machine.Tag()), "test-reboot-start-stop", s.clock) c.Assert(err, jc.ErrorIsNil) worker.Kill() c.Assert(worker.Wait(), gc.IsNil) } func (s *rebootSuite) TestWorkerCatchesRebootEvent(c *gc.C) { - wrk, err := reboot.NewReboot(s.rebootState, s.AgentConfigForTag(c, s.machine.Tag()), s.lockName, s.clock) + wrk, err := reboot.NewReboot(s.rebootState, s.AgentConfigForTag(c, s.machine.Tag()), "test-reboot-event", s.clock) c.Assert(err, jc.ErrorIsNil) err = s.rebootState.RequestReboot() c.Assert(err, jc.ErrorIsNil) @@ -102,7 +99,7 @@ } func (s *rebootSuite) TestContainerCatchesParentFlag(c *gc.C) { - wrk, err := reboot.NewReboot(s.ctRebootState, s.AgentConfigForTag(c, s.ct.Tag()), s.lockName, s.clock) + wrk, err := reboot.NewReboot(s.ctRebootState, s.AgentConfigForTag(c, s.ct.Tag()), "test-reboot-container", s.clock) c.Assert(err, jc.ErrorIsNil) err = s.rebootState.RequestReboot() c.Assert(err, jc.ErrorIsNil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/resumer/config_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/resumer/config_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/resumer/config_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/resumer/config_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,72 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package resumer_test + +import ( + "time" + + "github.com/juju/errors" + "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/worker/resumer" + "github.com/juju/juju/worker/workertest" +) + +type ConfigSuite struct { + testing.IsolationSuite +} + +var _ = gc.Suite(&ConfigSuite{}) + +func (*ConfigSuite) TestValid(c *gc.C) { + config := validConfig() + err := config.Validate() + c.Check(err, jc.ErrorIsNil) +} + +func (*ConfigSuite) TestNilFacade(c *gc.C) { + config := validConfig() + config.Facade = nil + checkInvalid(c, config, "nil Facade not valid") +} + +func (*ConfigSuite) TestNilClock(c *gc.C) { + config := validConfig() + config.Clock = nil + checkInvalid(c, config, "nil Clock not valid") +} + +func (*ConfigSuite) TestZeroInterval(c *gc.C) { + config := validConfig() + config.Interval = 0 + checkInvalid(c, config, "non-positive Interval not valid") +} + +func (*ConfigSuite) TestNegativeInterval(c *gc.C) { + config := validConfig() + config.Interval = -time.Minute + checkInvalid(c, config, "non-positive Interval not valid") +} + +func validConfig() resumer.Config { + return resumer.Config{ + Facade: &fakeFacade{}, + Clock: &fakeClock{}, + Interval: time.Minute, + } +} + +func checkInvalid(c *gc.C, config resumer.Config, match string) { + check := func(err error) { + c.Check(err, jc.Satisfies, errors.IsNotValid) + c.Check(err, gc.ErrorMatches, match) + } + check(config.Validate()) + + worker, err := resumer.NewResumer(config) + workertest.CheckNilOrKill(c, worker) + check(err) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/resumer/export_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/resumer/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/resumer/export_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/resumer/export_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,25 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package resumer - -import ( - "sync" - "time" -) - -var mu sync.Mutex - -func SetInterval(i time.Duration) { - mu.Lock() - defer mu.Unlock() - - interval = i -} - -func RestoreInterval() { - mu.Lock() - defer mu.Unlock() - - interval = defaultInterval -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/resumer/manifold.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/resumer/manifold.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/resumer/manifold.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/resumer/manifold.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,56 +4,95 @@ package resumer import ( + "time" + "github.com/juju/errors" - "gopkg.in/juju/names.v2" + "github.com/juju/utils/clock" "github.com/juju/juju/agent" apiagent "github.com/juju/juju/api/agent" "github.com/juju/juju/api/base" - apiresumer "github.com/juju/juju/api/resumer" "github.com/juju/juju/cmd/jujud/agent/engine" "github.com/juju/juju/state/multiwatcher" "github.com/juju/juju/worker" "github.com/juju/juju/worker/dependency" ) -// ManifoldConfig defines the names of the manifolds on which a Manifold will depend. -type ManifoldConfig engine.AgentApiManifoldConfig +// ManifoldConfig defines the names of the manifolds on which a Manifold +// will depend. +type ManifoldConfig struct { + AgentName string + APICallerName string + Clock clock.Clock + Interval time.Duration + NewFacade func(base.APICaller) (Facade, error) + NewWorker func(Config) (worker.Worker, error) +} -// Manifold returns a dependency manifold that runs a resumer worker, -// using the api connection resource named in the supplied config. -func Manifold(config ManifoldConfig) dependency.Manifold { - typedConfig := engine.AgentApiManifoldConfig(config) - return engine.AgentApiManifold(typedConfig, newWorker) +// newWorker is an engine.AgentApiStartFunc that draws context from the +// ManifoldConfig on which it is defined. +func (config ManifoldConfig) newWorker(a agent.Agent, apiCaller base.APICaller) (worker.Worker, error) { + + // This bit should be encapsulated in another manifold + // satisfying jujud/agent/engine.Flag, as described in + // the implementation below. Shouldn't be a concern here. + if ok, err := isModelManager(a, apiCaller); err != nil { + return nil, errors.Trace(err) + } else if !ok { + // This depends on a job change triggering an agent + // bounce, which does happen today, but is not ideal; + // another reason to use a flag. + return nil, dependency.ErrMissing + } + + // Get the API facade. + if config.NewFacade == nil { + logger.Errorf("nil NewFacade not valid, uninstalling") + return nil, dependency.ErrUninstall + } + facade, err := config.NewFacade(apiCaller) + if err != nil { + return nil, errors.Trace(err) + } + + // Start the worker. + if config.NewWorker == nil { + logger.Errorf("nil NewWorker not valid, uninstalling") + return nil, dependency.ErrUninstall + } + worker, err := config.NewWorker(Config{ + Facade: facade, + Clock: config.Clock, + Interval: config.Interval, + }) + if err != nil { + return nil, errors.Trace(err) + } + return worker, nil } -func newWorker(a agent.Agent, apiCaller base.APICaller) (worker.Worker, error) { - cfg := a.CurrentConfig() - // Grab the tag and ensure that it's for a machine. - tag, ok := cfg.Tag().(names.MachineTag) - if !ok { - return nil, errors.New("this manifold may only be used inside a machine agent") +// Manifold returns a dependency manifold that runs a resumer worker, +// using the resources named or defined in the supplied config. +func Manifold(config ManifoldConfig) dependency.Manifold { + aaConfig := engine.AgentApiManifoldConfig{ + AgentName: config.AgentName, + APICallerName: config.APICallerName, } + return engine.AgentApiManifold(aaConfig, config.newWorker) +} - // Get the machine agent's jobs. - // TODO(fwereade): this functionality should be on the - // deployer facade instead. +// isModelManager returns whether the agent has JobManageModel, +// or an error. +func isModelManager(a agent.Agent, apiCaller base.APICaller) (bool, error) { agentFacade := apiagent.NewState(apiCaller) - entity, err := agentFacade.Entity(tag) + entity, err := agentFacade.Entity(a.CurrentConfig().Tag()) if err != nil { - return nil, err + return false, errors.Trace(err) } - - var isModelManager bool for _, job := range entity.Jobs() { if job == multiwatcher.JobManageModel { - isModelManager = true - break + return true, nil } } - if !isModelManager { - return nil, dependency.ErrMissing - } - - return NewResumer(apiresumer.NewAPI(apiCaller)), nil + return false, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/resumer/manifold_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/resumer/manifold_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/resumer/manifold_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/resumer/manifold_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,116 +4,302 @@ package resumer_test import ( + "errors" + "time" + "github.com/juju/testing" jc "github.com/juju/testing/checkers" + "github.com/juju/utils/clock" gc "gopkg.in/check.v1" "gopkg.in/juju/names.v2" "github.com/juju/juju/agent" - "github.com/juju/juju/api" - apiagent "github.com/juju/juju/api/agent" + "github.com/juju/juju/api/base" "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/cmd/jujud/agent/engine/enginetest" "github.com/juju/juju/state/multiwatcher" "github.com/juju/juju/worker" "github.com/juju/juju/worker/dependency" + dt "github.com/juju/juju/worker/dependency/testing" resumer "github.com/juju/juju/worker/resumer" + "github.com/juju/juju/worker/workertest" ) type ManifoldSuite struct { testing.IsolationSuite - newCalled bool } var _ = gc.Suite(&ManifoldSuite{}) -func (s *ManifoldSuite) SetUpTest(c *gc.C) { - s.newCalled = false - s.PatchValue(&resumer.NewResumer, - func(tr resumer.TransactionResumer) worker.Worker { - s.newCalled = true - return nil - }, - ) -} - -func (s *ManifoldSuite) TestMachine(c *gc.C) { - config := resumer.ManifoldConfig(enginetest.AgentApiManifoldTestConfig()) - _, err := enginetest.RunAgentApiManifold( - resumer.Manifold(config), - &fakeAgent{tag: names.NewMachineTag("42")}, - &fakeAPIConn{machineJob: multiwatcher.JobManageModel}) - c.Assert(err, jc.ErrorIsNil) - c.Assert(s.newCalled, jc.IsTrue) -} - -func (s *ManifoldSuite) TestMachineNonManagerErrors(c *gc.C) { - config := resumer.ManifoldConfig(enginetest.AgentApiManifoldTestConfig()) - _, err := enginetest.RunAgentApiManifold( - resumer.Manifold(config), - &fakeAgent{tag: names.NewMachineTag("42")}, - &fakeAPIConn{machineJob: multiwatcher.JobHostUnits}) - c.Assert(err, gc.Equals, dependency.ErrMissing) - c.Assert(s.newCalled, jc.IsFalse) -} - -func (s *ManifoldSuite) TestUnitErrors(c *gc.C) { - config := resumer.ManifoldConfig(enginetest.AgentApiManifoldTestConfig()) - _, err := enginetest.RunAgentApiManifold( - resumer.Manifold(config), - &fakeAgent{tag: names.NewUnitTag("foo/0")}, - &fakeAPIConn{}) - c.Assert(err, gc.ErrorMatches, "this manifold may only be used inside a machine agent") - c.Assert(s.newCalled, jc.IsFalse) -} - -func (s *ManifoldSuite) TestNonAgentErrors(c *gc.C) { - config := resumer.ManifoldConfig(enginetest.AgentApiManifoldTestConfig()) - _, err := enginetest.RunAgentApiManifold( - resumer.Manifold(config), - &fakeAgent{tag: names.NewUserTag("foo")}, - &fakeAPIConn{}) - c.Assert(err, gc.ErrorMatches, "this manifold may only be used inside a machine agent") - c.Assert(s.newCalled, jc.IsFalse) +func (*ManifoldSuite) TestInputs(c *gc.C) { + manifold := resumer.Manifold(resumer.ManifoldConfig{ + AgentName: "bill", + APICallerName: "ben", + }) + expect := []string{"bill", "ben"} + c.Check(manifold.Inputs, jc.DeepEquals, expect) +} + +func (*ManifoldSuite) TestOutput(c *gc.C) { + manifold := resumer.Manifold(resumer.ManifoldConfig{}) + c.Check(manifold.Output, gc.IsNil) +} + +func (*ManifoldSuite) TestMissingAgent(c *gc.C) { + manifold := resumer.Manifold(resumer.ManifoldConfig{ + AgentName: "agent", + APICallerName: "api-caller", + }) + + worker, err := manifold.Start(dt.StubContext(nil, map[string]interface{}{ + "agent": dependency.ErrMissing, + "api-caller": &fakeAPICaller{}, + })) + workertest.CheckNilOrKill(c, worker) + c.Check(err, gc.Equals, dependency.ErrMissing) +} + +func (*ManifoldSuite) TestMissingAPICaller(c *gc.C) { + manifold := resumer.Manifold(resumer.ManifoldConfig{ + AgentName: "agent", + APICallerName: "api-caller", + }) + + worker, err := manifold.Start(dt.StubContext(nil, map[string]interface{}{ + "agent": &fakeAgent{}, + "api-caller": dependency.ErrMissing, + })) + workertest.CheckNilOrKill(c, worker) + c.Check(err, gc.Equals, dependency.ErrMissing) +} + +func (*ManifoldSuite) TestAgentEntity_Error(c *gc.C) { + manifold := resumer.Manifold(resumer.ManifoldConfig{ + AgentName: "agent", + APICallerName: "api-caller", + }) + + stub := &testing.Stub{} + stub.SetErrors(errors.New("zap")) + apiCaller := &fakeAPICaller{stub: stub} + worker, err := manifold.Start(dt.StubContext(nil, map[string]interface{}{ + "agent": &fakeAgent{}, + "api-caller": apiCaller, + })) + workertest.CheckNilOrKill(c, worker) + c.Check(err, gc.ErrorMatches, "zap") + + stub.CheckCalls(c, []testing.StubCall{{ + FuncName: "Agent.GetEntities", + Args: []interface{}{params.Entities{ + Entities: []params.Entity{{ + Tag: "machine-123", + }}, + }}, + }}) +} + +func (s *ManifoldSuite) TestAgentEntity_NoJob(c *gc.C) { + manifold := resumer.Manifold(resumer.ManifoldConfig{ + AgentName: "agent", + APICallerName: "api-caller", + }) + + worker, err := manifold.Start(dt.StubContext(nil, map[string]interface{}{ + "agent": &fakeAgent{}, + "api-caller": &fakeAPICaller{}, + })) + workertest.CheckNilOrKill(c, worker) + c.Check(err, gc.Equals, dependency.ErrMissing) +} + +func (s *ManifoldSuite) TestAgentEntity_NotModelManager(c *gc.C) { + manifold := resumer.Manifold(resumer.ManifoldConfig{ + AgentName: "agent", + APICallerName: "api-caller", + }) + + worker, err := manifold.Start(dt.StubContext(nil, map[string]interface{}{ + "agent": &fakeAgent{}, + "api-caller": newFakeAPICaller(multiwatcher.JobHostUnits), + })) + workertest.CheckNilOrKill(c, worker) + c.Check(err, gc.Equals, dependency.ErrMissing) +} + +func (s *ManifoldSuite) TestNewFacade_Missing(c *gc.C) { + manifold := resumer.Manifold(resumer.ManifoldConfig{ + AgentName: "agent", + APICallerName: "api-caller", + }) + + worker, err := manifold.Start(dt.StubContext(nil, map[string]interface{}{ + "agent": &fakeAgent{}, + "api-caller": newFakeAPICaller(multiwatcher.JobManageModel), + })) + workertest.CheckNilOrKill(c, worker) + c.Check(err, gc.Equals, dependency.ErrUninstall) +} + +func (s *ManifoldSuite) TestNewFacade_Error(c *gc.C) { + apiCaller := newFakeAPICaller(multiwatcher.JobManageModel) + manifold := resumer.Manifold(resumer.ManifoldConfig{ + AgentName: "agent", + APICallerName: "api-caller", + NewFacade: func(actual base.APICaller) (resumer.Facade, error) { + c.Check(actual, gc.Equals, apiCaller) + return nil, errors.New("pow") + }, + }) + + worker, err := manifold.Start(dt.StubContext(nil, map[string]interface{}{ + "agent": &fakeAgent{}, + "api-caller": apiCaller, + })) + workertest.CheckNilOrKill(c, worker) + c.Check(err, gc.ErrorMatches, "pow") +} + +func (s *ManifoldSuite) TestNewWorker_Missing(c *gc.C) { + manifold := resumer.Manifold(resumer.ManifoldConfig{ + AgentName: "agent", + APICallerName: "api-caller", + NewFacade: func(base.APICaller) (resumer.Facade, error) { + return &fakeFacade{}, nil + }, + }) + + worker, err := manifold.Start(dt.StubContext(nil, map[string]interface{}{ + "agent": &fakeAgent{}, + "api-caller": newFakeAPICaller(multiwatcher.JobManageModel), + })) + workertest.CheckNilOrKill(c, worker) + c.Check(err, gc.Equals, dependency.ErrUninstall) +} + +func (s *ManifoldSuite) TestNewWorker_Error(c *gc.C) { + clock := &fakeClock{} + facade := &fakeFacade{} + manifold := resumer.Manifold(resumer.ManifoldConfig{ + AgentName: "agent", + APICallerName: "api-caller", + Clock: clock, + Interval: time.Hour, + NewFacade: func(base.APICaller) (resumer.Facade, error) { + return facade, nil + }, + NewWorker: func(actual resumer.Config) (worker.Worker, error) { + c.Check(actual, jc.DeepEquals, resumer.Config{ + Facade: facade, + Clock: clock, + Interval: time.Hour, + }) + return nil, errors.New("blam") + }, + }) + + worker, err := manifold.Start(dt.StubContext(nil, map[string]interface{}{ + "agent": &fakeAgent{}, + "api-caller": newFakeAPICaller(multiwatcher.JobManageModel), + })) + workertest.CheckNilOrKill(c, worker) + c.Check(err, gc.ErrorMatches, "blam") +} + +func (s *ManifoldSuite) TestNewWorker_Success(c *gc.C) { + expect := &fakeWorker{} + manifold := resumer.Manifold(resumer.ManifoldConfig{ + AgentName: "agent", + APICallerName: "api-caller", + NewFacade: func(base.APICaller) (resumer.Facade, error) { + return &fakeFacade{}, nil + }, + NewWorker: func(actual resumer.Config) (worker.Worker, error) { + return expect, nil + }, + }) + + actual, err := manifold.Start(dt.StubContext(nil, map[string]interface{}{ + "agent": &fakeAgent{}, + "api-caller": newFakeAPICaller(multiwatcher.JobManageModel), + })) + c.Check(err, jc.ErrorIsNil) + c.Check(actual, gc.Equals, expect) } +// fakeFacade should not be called. +type fakeFacade struct { + resumer.Facade +} + +// fakeClock should not be called. +type fakeClock struct { + clock.Clock +} + +// fakeWorker should not be called. +type fakeWorker struct { + worker.Worker +} + +// fakeAgent exists to expose a tag via CurrentConfig().Tag(). type fakeAgent struct { agent.Agent - tag names.Tag } +// CurrentConfig returns an agent.Config with a working Tag() method. func (a *fakeAgent) CurrentConfig() agent.Config { - return &fakeConfig{tag: a.tag} + return &fakeConfig{} } +// fakeConfig exists to expose Tag. type fakeConfig struct { agent.Config - tag names.Tag } +// Tag returns a Tag. func (c *fakeConfig) Tag() names.Tag { - return c.tag + return names.NewMachineTag("123") } -type fakeAPIConn struct { - api.Connection - machineJob multiwatcher.MachineJob +func newFakeAPICaller(jobs ...multiwatcher.MachineJob) *fakeAPICaller { + return &fakeAPICaller{jobs: jobs} } -func (f *fakeAPIConn) APICall(objType string, version int, id, request string, args interface{}, response interface{}) error { +// fakeAPICaller exists to handle the hackish checkModelManager's api +// call directly, because it shouldn't happen in this context at all +// and we don't want it leaking into the config. +type fakeAPICaller struct { + base.APICaller + stub *testing.Stub + jobs []multiwatcher.MachineJob +} + +// APICall is part of the base.APICaller interface. +func (f *fakeAPICaller) APICall(objType string, version int, id, request string, args interface{}, response interface{}) error { + if f.stub != nil { + // We don't usually set the stub here, most of the time + // the APICall hack is just an unwanted distraction from + // the NewFacade/NewWorker bits that *should* exist long- + // term. This makes it easier to just delete the broken + // tests, and most of this type, including all of the + // methods, when we drop the job check. + f.stub.AddCall(objType+"."+request, args) + if err := f.stub.NextErr(); err != nil { + return err + } + } + if res, ok := response.(*params.AgentGetEntitiesResults); ok { + jobs := make([]multiwatcher.MachineJob, 0, len(f.jobs)) + jobs = append(jobs, f.jobs...) res.Entities = []params.AgentGetEntitiesResult{ - {Jobs: []multiwatcher.MachineJob{f.machineJob}}, + {Jobs: jobs}, } } - return nil } -func (*fakeAPIConn) BestFacadeVersion(facade string) int { +// BestFacadeVersion is part of the base.APICaller interface. +func (*fakeAPICaller) BestFacadeVersion(facade string) int { return 42 } - -func (f *fakeAPIConn) Agent() *apiagent.State { - return apiagent.NewState(f) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/resumer/resumer.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/resumer/resumer.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/resumer/resumer.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/resumer/resumer.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,67 +4,94 @@ package resumer import ( - "fmt" "time" - "github.com/juju/juju/worker" + "github.com/juju/errors" + "github.com/juju/juju/worker/catacomb" "github.com/juju/loggo" - "launchpad.net/tomb" + "github.com/juju/utils/clock" ) var logger = loggo.GetLogger("juju.worker.resumer") -// defaultInterval is the standard value for the interval setting. -const defaultInterval = time.Minute +// Facade defines the interface for types capable of resuming +// transactions. +type Facade interface { -// interval sets how often the resuming is called. -var interval = defaultInterval - -// TransactionResumer defines the interface for types capable to -// resume transactions. -type TransactionResumer interface { // ResumeTransactions resumes all pending transactions. ResumeTransactions() error } -// Resumer is responsible for a periodical resuming of pending transactions. -type Resumer struct { - tomb tomb.Tomb - tr TransactionResumer +// Config holds the dependencies and configuration necessary to +// drive a Resumer. +type Config struct { + Facade Facade + Clock clock.Clock + Interval time.Duration +} + +// Validate returns an error if config cannot be expected to drive +// a Resumer. +func (config Config) Validate() error { + if config.Facade == nil { + return errors.NotValidf("nil Facade") + } + if config.Clock == nil { + return errors.NotValidf("nil Clock") + } + if config.Interval <= 0 { + return errors.NotValidf("non-positive Interval") + } + return nil } -// NewResumer periodically resumes pending transactions. -var NewResumer = func(tr TransactionResumer) worker.Worker { - rr := &Resumer{tr: tr} - go func() { - defer rr.tomb.Done() - rr.tomb.Kill(rr.loop()) - }() - return rr +// Resumer is responsible for periodically resuming all pending +// transactions. +type Resumer struct { + catacomb catacomb.Catacomb + config Config } -func (rr *Resumer) String() string { - return fmt.Sprintf("resumer") +// NewResumer returns a new Resumer or an error. If the Resumer is +// not nil, the caller is responsible for stopping it via `Kill()` +// and handling any error returned from `Wait()`. +var NewResumer = func(config Config) (*Resumer, error) { + if err := config.Validate(); err != nil { + return nil, errors.Trace(err) + } + rr := &Resumer{config: config} + err := catacomb.Invoke(catacomb.Plan{ + Site: &rr.catacomb, + Work: rr.loop, + }) + if err != nil { + return nil, errors.Trace(err) + } + return rr, nil } +// Kill is part of the worker.Worker interface. func (rr *Resumer) Kill() { - rr.tomb.Kill(nil) + rr.catacomb.Kill(nil) } +// Wait is part of the worker.Worker interface. func (rr *Resumer) Wait() error { - return rr.tomb.Wait() + return rr.catacomb.Wait() } func (rr *Resumer) loop() error { + var interval time.Duration for { select { - case <-rr.tomb.Dying(): - return tomb.ErrDying - case <-time.After(interval): - // TODO(fwereade): 2016-03-17 lp:1558657 - if err := rr.tr.ResumeTransactions(); err != nil { - logger.Errorf("cannot resume transactions: %v", err) + case <-rr.catacomb.Dying(): + return rr.catacomb.ErrDying() + case <-rr.config.Clock.After(interval): + err := rr.config.Facade.ResumeTransactions() + if err != nil { + return errors.Annotate(err, "cannot resume transactions") } } + interval = rr.config.Interval } } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/resumer/resumer_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/resumer/resumer_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/resumer/resumer_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/resumer/resumer_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,134 +5,116 @@ import ( "errors" - "sync" "time" - "github.com/juju/juju/worker" - "github.com/juju/testing" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" - "github.com/juju/juju/state" coretesting "github.com/juju/juju/testing" "github.com/juju/juju/worker/resumer" + "github.com/juju/juju/worker/workertest" ) type ResumerSuite struct { - coretesting.BaseSuite - - mockState *transactionResumerMock + testing.IsolationSuite } var _ = gc.Suite(&ResumerSuite{}) -// Ensure *state.State implements TransactionResumer -var _ resumer.TransactionResumer = (*state.State)(nil) - -func (s *ResumerSuite) SetUpTest(c *gc.C) { - s.BaseSuite.SetUpTest(c) - - s.mockState = &transactionResumerMock{ - Stub: &testing.Stub{}, - } +func (*ResumerSuite) TestImmediateFailure(c *gc.C) { + fix := newFixture(errors.New("zap")) + stub := fix.Run(c, func(_ *coretesting.Clock, worker *resumer.Resumer) { + err := workertest.CheckKilled(c, worker) + c.Check(err, gc.ErrorMatches, "cannot resume transactions: zap") + }) + stub.CheckCallNames(c, "ResumeTransactions") } -func (s *ResumerSuite) TestRunStopWithMockState(c *gc.C) { - w := resumer.NewResumer(s.mockState) - c.Assert(worker.Stop(w), gc.IsNil) +func (*ResumerSuite) TestWaitsToResume(c *gc.C) { + fix := newFixture(nil, errors.New("unexpected")) + stub := fix.Run(c, func(clock *coretesting.Clock, worker *resumer.Resumer) { + waitAlarms(c, clock, 2) + clock.Advance(time.Hour - time.Nanosecond) + workertest.CheckAlive(c, worker) + workertest.CleanKill(c, worker) + }) + stub.CheckCallNames(c, "ResumeTransactions") } -func (s *ResumerSuite) TestResumerCalls(c *gc.C) { - // Shorter interval and mock help to count - // the resumer calls in a given timespan. - testInterval := coretesting.ShortWait - resumer.SetInterval(testInterval) - defer resumer.RestoreInterval() - - w := resumer.NewResumer(s.mockState) - defer func() { - c.Assert(worker.Stop(w), gc.IsNil) - }() - - time.Sleep(10 * testInterval) +func (*ResumerSuite) TestResumesAfterWait(c *gc.C) { + fix := newFixture(nil, nil, errors.New("unexpected")) + stub := fix.Run(c, func(clock *coretesting.Clock, worker *resumer.Resumer) { + waitAlarms(c, clock, 2) + clock.Advance(time.Hour) + waitAlarms(c, clock, 1) + workertest.CleanKill(c, worker) + }) + stub.CheckCallNames(c, "ResumeTransactions", "ResumeTransactions") +} - s.mockState.CheckTimestamps(c, testInterval) +func (*ResumerSuite) TestSeveralResumes(c *gc.C) { + fix := newFixture(nil, nil, nil, errors.New("unexpected")) + stub := fix.Run(c, func(clock *coretesting.Clock, worker *resumer.Resumer) { + waitAlarms(c, clock, 2) + clock.Advance(time.Hour) + waitAlarms(c, clock, 1) + clock.Advance(time.Hour) + waitAlarms(c, clock, 1) + workertest.CleanKill(c, worker) + }) + stub.CheckCallNames(c, "ResumeTransactions", "ResumeTransactions", "ResumeTransactions") } -func (s *ResumerSuite) TestResumeTransactionsFailure(c *gc.C) { - // Force the first call to ResumeTransactions() to fail, the - // remaining returning no error. - s.mockState.SetErrors(errors.New("boom!")) +func newFixture(errs ...error) *fixture { + return &fixture{errors: errs} +} - // Shorter interval and mock help to count - // the resumer calls in a given timespan. - testInterval := coretesting.ShortWait - resumer.SetInterval(testInterval) - defer resumer.RestoreInterval() +type fixture struct { + errors []error +} - w := resumer.NewResumer(s.mockState) - defer func() { - c.Assert(worker.Stop(w), gc.IsNil) - }() +type TestFunc func(*coretesting.Clock, *resumer.Resumer) - // For 4 intervals between 2 and 3 calls should be made. - time.Sleep(4 * testInterval) - s.mockState.CheckNumCallsBetween(c, 2, 3) -} +func (fix fixture) Run(c *gc.C, test TestFunc) *testing.Stub { -// TODO(waigani) This could be a simpler and more robust if the resumer took a -// Clock. + stub := &testing.Stub{} + stub.SetErrors(fix.errors...) + clock := coretesting.NewClock(time.Now()) + facade := newMockFacade(stub) -// transactionResumerMock is used to check the -// calls of ResumeTransactions(). -type transactionResumerMock struct { - *testing.Stub + worker, err := resumer.NewResumer(resumer.Config{ + Facade: facade, + Interval: time.Hour, + Clock: clock, + }) + c.Assert(err, jc.ErrorIsNil) + defer workertest.DirtyKill(c, worker) - mu sync.Mutex - timestamps []time.Time + test(clock, worker) + return stub } -func (tr *transactionResumerMock) ResumeTransactions() error { - tr.mu.Lock() - defer tr.mu.Unlock() - - tr.timestamps = append(tr.timestamps, time.Now()) - tr.MethodCall(tr, "ResumeTransactions") - return tr.NextErr() +func newMockFacade(stub *testing.Stub) *mockFacade { + return &mockFacade{stub: stub} } -func (tr *transactionResumerMock) CheckNumCallsBetween(c *gc.C, minCalls, maxCalls int) { - tr.mu.Lock() - defer tr.mu.Unlock() +type mockFacade struct { + stub *testing.Stub +} - // To combat test flakyness (see bug #1462412) we're expecting up - // to maxCalls, but at least minCalls. - calls := tr.Stub.Calls() - c.Assert(len(calls), jc.GreaterThan, minCalls-1) - c.Assert(len(calls), jc.LessThan, maxCalls+1) - for _, call := range calls { - c.Check(call.FuncName, gc.Equals, "ResumeTransactions") - } +func (mock *mockFacade) ResumeTransactions() error { + mock.stub.AddCall("ResumeTransactions") + return mock.stub.NextErr() } -func (tr *transactionResumerMock) CheckTimestamps(c *gc.C, testInterval time.Duration) { - // Check that a number of calls has happened with a time - // difference somewhere between the interval and twice the - // interval. A more precise time behavior cannot be - // specified due to the load during the test. - tr.mu.Lock() - defer tr.mu.Unlock() - - longestInterval := 4 * testInterval - c.Assert(len(tr.timestamps) > 0, jc.IsTrue) - for i := 1; i < len(tr.timestamps); i++ { - diff := tr.timestamps[i].Sub(tr.timestamps[i-1]) - - c.Assert(diff >= testInterval, jc.IsTrue) - c.Assert(diff <= longestInterval, jc.IsTrue) - tr.Stub.CheckCall(c, i-1, "ResumeTransactions") +func waitAlarms(c *gc.C, clock *coretesting.Clock, count int) { + timeout := time.After(coretesting.LongWait) + for i := 0; i < count; i++ { + select { + case <-clock.Alarms(): + case <-timeout: + c.Fatalf("timed out waiting for alarm %d", i) + } } } - -var _ resumer.TransactionResumer = (*transactionResumerMock)(nil) diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/resumer/shim.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/resumer/shim.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/resumer/shim.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/resumer/shim.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,28 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package resumer + +import ( + "github.com/juju/errors" + + "github.com/juju/juju/api/base" + "github.com/juju/juju/api/resumer" + "github.com/juju/juju/worker" +) + +// NewFacade returns a useful live implementation for +// ManifoldConfig.NewFacade. +func NewFacade(apiCaller base.APICaller) (Facade, error) { + return resumer.NewAPI(apiCaller), nil +} + +// NewWorker returns a useful live implementation for +// ManifoldConfig.NewWorker. +func NewWorker(config Config) (worker.Worker, error) { + worker, err := NewResumer(config) + if err != nil { + return nil, errors.Trace(err) + } + return worker, nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/runner_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/runner_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/runner_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/runner_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -131,20 +131,30 @@ starter := newTestWorkerStarter() starter.stopWait = make(chan struct{}) + // Start a worker, and wait for it. err := runner.StartWorker("id", testWorkerStart(starter)) c.Assert(err, jc.ErrorIsNil) starter.assertStarted(c, true) + + // XXX the above does not imply the *runner* knows it's started. + // voodoo sleep ahoy! + time.Sleep(testing.ShortWait) + + // Stop the worker, which will block... err = runner.StopWorker("id") c.Assert(err, jc.ErrorIsNil) + + // While it's still blocked, try to start another. err = runner.StartWorker("id", testWorkerStart(starter)) c.Assert(err, jc.ErrorIsNil) - close(starter.stopWait) - starter.assertStarted(c, false) - // Check that the task is restarted immediately without - // the usual restart timeout delay. + // Unblock the stopping worker, and check that the task is + // restarted immediately without the usual restart timeout + // delay. t0 := time.Now() - starter.assertStarted(c, true) + close(starter.stopWait) + starter.assertStarted(c, false) // stop notification + starter.assertStarted(c, true) // start notification restartDuration := time.Since(t0) if restartDuration > 1*time.Second { c.Fatalf("task did not restart immediately") diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/singular/mongo_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/singular/mongo_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/singular/mongo_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/singular/mongo_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -524,6 +524,7 @@ Id: i + 1, }) } + // TODO(katco): 2016-08-09: lp:1611427 attempt := utils.AttemptStrategy{ Total: 60 * time.Second, Delay: 1 * time.Second, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/storageprovisioner/common.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/storageprovisioner/common.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/storageprovisioner/common.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/storageprovisioner/common.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,9 +10,7 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/environs/config" "github.com/juju/juju/storage" - "github.com/juju/juju/storage/provider/registry" "github.com/juju/juju/watcher" ) @@ -129,19 +127,19 @@ // a map in-between calls to the volume/filesystem/attachment // event handlers. func volumeSource( - environConfig *config.Config, baseStorageDir string, sourceName string, providerType storage.ProviderType, + registry storage.ProviderRegistry, ) (storage.VolumeSource, error) { - provider, sourceConfig, err := sourceParams(providerType, sourceName, baseStorageDir) + provider, sourceConfig, err := sourceParams(baseStorageDir, sourceName, providerType, registry) if err != nil { return nil, errors.Annotatef(err, "getting storage source %q params", sourceName) } if !provider.Dynamic() { return nil, errNonDynamic } - source, err := provider.VolumeSource(environConfig, sourceConfig) + source, err := provider.VolumeSource(sourceConfig) if err != nil { return nil, errors.Annotatef(err, "getting storage source %q", sourceName) } @@ -156,23 +154,28 @@ // a map in-between calls to the volume/filesystem/attachment // event handlers. func filesystemSource( - environConfig *config.Config, baseStorageDir string, sourceName string, providerType storage.ProviderType, + registry storage.ProviderRegistry, ) (storage.FilesystemSource, error) { - provider, sourceConfig, err := sourceParams(providerType, sourceName, baseStorageDir) + provider, sourceConfig, err := sourceParams(baseStorageDir, sourceName, providerType, registry) if err != nil { return nil, errors.Annotatef(err, "getting storage source %q params", sourceName) } - source, err := provider.FilesystemSource(environConfig, sourceConfig) + source, err := provider.FilesystemSource(sourceConfig) if err != nil { return nil, errors.Annotatef(err, "getting storage source %q", sourceName) } return source, nil } -func sourceParams(providerType storage.ProviderType, sourceName, baseStorageDir string) (storage.Provider, *storage.Config, error) { +func sourceParams( + baseStorageDir string, + sourceName string, + providerType storage.ProviderType, + registry storage.ProviderRegistry, +) (storage.Provider, *storage.Config, error) { provider, err := registry.StorageProvider(providerType) if err != nil { return nil, nil, errors.Annotate(err, "getting provider") diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/storageprovisioner/config.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/storageprovisioner/config.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/storageprovisioner/config.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/storageprovisioner/config.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,6 +5,7 @@ import ( "github.com/juju/errors" + "github.com/juju/juju/storage" "github.com/juju/utils/clock" "gopkg.in/juju/names.v2" ) @@ -16,7 +17,7 @@ Volumes VolumeAccessor Filesystems FilesystemAccessor Life LifecycleManager - Environ ModelAccessor + Registry storage.ProviderRegistry Machines MachineAccessor Status StatusSetter Clock clock.Clock @@ -47,8 +48,8 @@ if config.Life == nil { return errors.NotValidf("nil Life") } - if config.Environ == nil { - return errors.NotValidf("nil Environ") + if config.Registry == nil { + return errors.NotValidf("nil Registry") } if config.Machines == nil { return errors.NotValidf("nil Machines") diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/storageprovisioner/config_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/storageprovisioner/config_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/storageprovisioner/config_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/storageprovisioner/config_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,6 +11,7 @@ gc "gopkg.in/check.v1" "gopkg.in/juju/names.v2" + "github.com/juju/juju/storage" coretesting "github.com/juju/juju/testing" "github.com/juju/juju/worker/storageprovisioner" ) @@ -66,9 +67,9 @@ s.checkNotValid(c, "nil Life not valid") } -func (s *ConfigSuite) TestNilEnviron(c *gc.C) { - s.config.Environ = nil - s.checkNotValid(c, "nil Environ not valid") +func (s *ConfigSuite) TestNilRegistry(c *gc.C) { + s.config.Registry = nil + s.checkNotValid(c, "nil Registry not valid") } func (s *ConfigSuite) TestNilMachines(c *gc.C) { @@ -93,9 +94,9 @@ } func validEnvironConfig() storageprovisioner.Config { - config := almostValidConfig() - config.Scope = coretesting.ModelTag - return config + cfg := almostValidConfig() + cfg.Scope = coretesting.ModelTag + return cfg } func validMachineConfig() storageprovisioner.Config { @@ -118,8 +119,8 @@ Life: struct { storageprovisioner.LifecycleManager }{}, - Environ: struct { - storageprovisioner.ModelAccessor + Registry: struct { + storage.ProviderRegistry }{}, Machines: struct { storageprovisioner.MachineAccessor diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/storageprovisioner/filesystem_ops.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/storageprovisioner/filesystem_ops.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/storageprovisioner/filesystem_ops.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/storageprovisioner/filesystem_ops.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,7 +10,6 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/environs/config" "github.com/juju/juju/status" "github.com/juju/juju/storage" ) @@ -22,8 +21,10 @@ filesystemParams = append(filesystemParams, op.args) } paramsBySource, filesystemSources, err := filesystemParamsBySource( - ctx.modelConfig, ctx.config.StorageDir, - filesystemParams, ctx.managedFilesystemSource, + ctx.config.StorageDir, + filesystemParams, + ctx.managedFilesystemSource, + ctx.config.Registry, ) if err != nil { return errors.Trace(err) @@ -124,11 +125,11 @@ filesystemAttachmentParams = append(filesystemAttachmentParams, args) } paramsBySource, filesystemSources, err := filesystemAttachmentParamsBySource( - ctx.modelConfig, ctx.config.StorageDir, filesystemAttachmentParams, ctx.filesystems, ctx.managedFilesystemSource, + ctx.config.Registry, ) if err != nil { return errors.Trace(err) @@ -194,8 +195,10 @@ return errors.Trace(err) } paramsBySource, filesystemSources, err := filesystemParamsBySource( - ctx.modelConfig, ctx.config.StorageDir, - filesystemParams, ctx.managedFilesystemSource, + ctx.config.StorageDir, + filesystemParams, + ctx.managedFilesystemSource, + ctx.config.Registry, ) if err != nil { return errors.Trace(err) @@ -267,10 +270,11 @@ filesystemAttachmentParams = append(filesystemAttachmentParams, op.args) } paramsBySource, filesystemSources, err := filesystemAttachmentParamsBySource( - ctx.modelConfig, ctx.config.StorageDir, + ctx.config.StorageDir, filesystemAttachmentParams, ctx.filesystems, ctx.managedFilesystemSource, + ctx.config.Registry, ) if err != nil { return errors.Trace(err) @@ -328,10 +332,10 @@ // filesystemParamsBySource separates the filesystem parameters by filesystem source. func filesystemParamsBySource( - environConfig *config.Config, baseStorageDir string, params []storage.FilesystemParams, managedFilesystemSource storage.FilesystemSource, + registry storage.ProviderRegistry, ) (map[string][]storage.FilesystemParams, map[string]storage.FilesystemSource, error) { // TODO(axw) later we may have multiple instantiations (sources) // for a storage provider, e.g. multiple Ceph installations. For @@ -348,7 +352,7 @@ continue } filesystemSource, err := filesystemSource( - environConfig, baseStorageDir, sourceName, params.Provider, + baseStorageDir, sourceName, params.Provider, registry, ) if errors.Cause(err) == errNonDynamic { filesystemSource = nil @@ -390,11 +394,11 @@ // filesystemAttachmentParamsBySource separates the filesystem attachment parameters by filesystem source. func filesystemAttachmentParamsBySource( - environConfig *config.Config, baseStorageDir string, params []storage.FilesystemAttachmentParams, filesystems map[names.FilesystemTag]storage.Filesystem, managedFilesystemSource storage.FilesystemSource, + registry storage.ProviderRegistry, ) (map[string][]storage.FilesystemAttachmentParams, map[string]storage.FilesystemSource, error) { // TODO(axw) later we may have multiple instantiations (sources) // for a storage provider, e.g. multiple Ceph installations. For @@ -414,7 +418,7 @@ continue } filesystemSource, err := filesystemSource( - environConfig, baseStorageDir, sourceName, params.Provider, + baseStorageDir, sourceName, params.Provider, registry, ) if err != nil { return nil, nil, errors.Annotate(err, "getting filesystem source") diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/storageprovisioner/manifold_machine.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/storageprovisioner/manifold_machine.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/storageprovisioner/manifold_machine.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/storageprovisioner/manifold_machine.go 2016-08-16 08:56:25.000000000 +0000 @@ -14,6 +14,7 @@ "github.com/juju/juju/api/base" "github.com/juju/juju/api/storageprovisioner" "github.com/juju/juju/cmd/jujud/agent/engine" + "github.com/juju/juju/storage/provider" "github.com/juju/juju/worker" "github.com/juju/juju/worker/dependency" ) @@ -48,7 +49,7 @@ Volumes: api, Filesystems: api, Life: api, - Environ: api, + Registry: provider.CommonStorageProviders(), Machines: api, Status: api, Clock: config.Clock, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/storageprovisioner/manifold_model.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/storageprovisioner/manifold_model.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/storageprovisioner/manifold_model.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/storageprovisioner/manifold_model.go 2016-08-16 08:56:25.000000000 +0000 @@ -10,6 +10,7 @@ "github.com/juju/juju/api/base" "github.com/juju/juju/api/storageprovisioner" + "github.com/juju/juju/environs" "github.com/juju/juju/worker" "github.com/juju/juju/worker/dependency" ) @@ -18,6 +19,7 @@ type ModelManifoldConfig struct { APICallerName string ClockName string + EnvironName string Scope names.Tag StorageDir string @@ -26,7 +28,7 @@ // ModelManifold returns a dependency.Manifold that runs a storage provisioner. func ModelManifold(config ModelManifoldConfig) dependency.Manifold { return dependency.Manifold{ - Inputs: []string{config.APICallerName, config.ClockName}, + Inputs: []string{config.APICallerName, config.ClockName, config.EnvironName}, Start: func(context dependency.Context) (worker.Worker, error) { var clock clock.Clock @@ -37,6 +39,10 @@ if err := context.Get(config.APICallerName, &apiCaller); err != nil { return nil, errors.Trace(err) } + var environ environs.Environ + if err := context.Get(config.EnvironName, &environ); err != nil { + return nil, errors.Trace(err) + } api, err := storageprovisioner.NewState(apiCaller, config.Scope) if err != nil { @@ -48,7 +54,7 @@ Volumes: api, Filesystems: api, Life: api, - Environ: api, + Registry: environ, Machines: api, Status: api, Clock: clock, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/storageprovisioner/manifold_model_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/storageprovisioner/manifold_model_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/storageprovisioner/manifold_model_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/storageprovisioner/manifold_model_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,6 +11,7 @@ gc "gopkg.in/check.v1" "github.com/juju/juju/api/base" + "github.com/juju/juju/environs" "github.com/juju/juju/worker/dependency" dt "github.com/juju/juju/worker/dependency/testing" "github.com/juju/juju/worker/storageprovisioner" @@ -26,8 +27,9 @@ manifold := storageprovisioner.ModelManifold(storageprovisioner.ModelManifoldConfig{ APICallerName: "grenouille", ClockName: "bustopher", + EnvironName: "environ", }) - c.Check(manifold.Inputs, jc.DeepEquals, []string{"grenouille", "bustopher"}) + c.Check(manifold.Inputs, jc.DeepEquals, []string{"grenouille", "bustopher", "environ"}) c.Check(manifold.Output, gc.IsNil) c.Check(manifold.Start, gc.NotNil) // ...Start is *not* well-tested, in common with many manifold configs. @@ -37,10 +39,12 @@ manifold := storageprovisioner.ModelManifold(storageprovisioner.ModelManifoldConfig{ APICallerName: "api-caller", ClockName: "clock", + EnvironName: "environ", }) _, err := manifold.Start(dt.StubContext(nil, map[string]interface{}{ "api-caller": struct{ base.APICaller }{}, "clock": dependency.ErrMissing, + "environ": struct{ environs.Environ }{}, })) c.Check(errors.Cause(err), gc.Equals, dependency.ErrMissing) } @@ -49,10 +53,26 @@ manifold := storageprovisioner.ModelManifold(storageprovisioner.ModelManifoldConfig{ APICallerName: "api-caller", ClockName: "clock", + EnvironName: "environ", }) _, err := manifold.Start(dt.StubContext(nil, map[string]interface{}{ "api-caller": dependency.ErrMissing, "clock": struct{ clock.Clock }{}, + "environ": struct{ environs.Environ }{}, + })) + c.Check(errors.Cause(err), gc.Equals, dependency.ErrMissing) +} + +func (s *ManifoldSuite) TestMissingEnviron(c *gc.C) { + manifold := storageprovisioner.ModelManifold(storageprovisioner.ModelManifoldConfig{ + APICallerName: "api-caller", + ClockName: "clock", + EnvironName: "environ", + }) + _, err := manifold.Start(dt.StubContext(nil, map[string]interface{}{ + "api-caller": struct{ base.APICaller }{}, + "clock": struct{ clock.Clock }{}, + "environ": dependency.ErrMissing, })) c.Check(errors.Cause(err), gc.Equals, dependency.ErrMissing) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/storageprovisioner/mock_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/storageprovisioner/mock_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/storageprovisioner/mock_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/storageprovisioner/mock_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -5,7 +5,6 @@ import ( "strconv" - "sync" "time" "github.com/juju/errors" @@ -16,10 +15,8 @@ "github.com/juju/juju/apiserver/common" "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/environs/config" "github.com/juju/juju/instance" "github.com/juju/juju/storage" - "github.com/juju/juju/testing" "github.com/juju/juju/watcher" ) @@ -91,36 +88,6 @@ return w.changes } -type mockModelAccessor struct { - watcher *mockNotifyWatcher - mu sync.Mutex - cfg *config.Config -} - -func (e *mockModelAccessor) WatchForModelConfigChanges() (watcher.NotifyWatcher, error) { - return e.watcher, nil -} - -func (e *mockModelAccessor) ModelConfig() (*config.Config, error) { - e.mu.Lock() - cfg := e.cfg - e.mu.Unlock() - return cfg, nil -} - -func (e *mockModelAccessor) setConfig(cfg *config.Config) { - e.mu.Lock() - e.cfg = cfg - e.mu.Unlock() -} - -func newMockModelAccessor(c *gc.C) *mockModelAccessor { - return &mockModelAccessor{ - watcher: newMockNotifyWatcher(), - cfg: testing.ModelConfig(c), - } -} - type mockVolumeAccessor struct { volumesWatcher *mockStringsWatcher attachmentsWatcher *mockAttachmentsWatcher @@ -454,8 +421,8 @@ storage.Provider dynamic bool - volumeSourceFunc func(*config.Config, *storage.Config) (storage.VolumeSource, error) - filesystemSourceFunc func(*config.Config, *storage.Config) (storage.FilesystemSource, error) + volumeSourceFunc func(*storage.Config) (storage.VolumeSource, error) + filesystemSourceFunc func(*storage.Config) (storage.FilesystemSource, error) createVolumesFunc func([]storage.VolumeParams) ([]storage.CreateVolumesResult, error) createFilesystemsFunc func([]storage.FilesystemParams) ([]storage.CreateFilesystemsResult, error) attachVolumesFunc func([]storage.VolumeAttachmentParams) ([]storage.AttachVolumesResult, error) @@ -480,16 +447,16 @@ createFilesystemsArgs [][]storage.FilesystemParams } -func (p *dummyProvider) VolumeSource(environConfig *config.Config, providerConfig *storage.Config) (storage.VolumeSource, error) { +func (p *dummyProvider) VolumeSource(providerConfig *storage.Config) (storage.VolumeSource, error) { if p.volumeSourceFunc != nil { - return p.volumeSourceFunc(environConfig, providerConfig) + return p.volumeSourceFunc(providerConfig) } return &dummyVolumeSource{provider: p}, nil } -func (p *dummyProvider) FilesystemSource(environConfig *config.Config, providerConfig *storage.Config) (storage.FilesystemSource, error) { +func (p *dummyProvider) FilesystemSource(providerConfig *storage.Config) (storage.FilesystemSource, error) { if p.filesystemSourceFunc != nil { - return p.filesystemSourceFunc(environConfig, providerConfig) + return p.filesystemSourceFunc(providerConfig) } return &dummyFilesystemSource{provider: p}, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/storageprovisioner/storageprovisioner.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/storageprovisioner/storageprovisioner.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/storageprovisioner/storageprovisioner.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/storageprovisioner/storageprovisioner.go 2016-08-16 08:56:25.000000000 +0000 @@ -33,7 +33,6 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/environs/config" "github.com/juju/juju/storage" "github.com/juju/juju/storage/provider" "github.com/juju/juju/watcher" @@ -156,18 +155,6 @@ SetStatus([]params.EntityStatusArgs) error } -// ModelAccessor defines an interface used to enable a storage provisioner -// worker to watch changes to and read model config, to use when -// provisioning storage. -type ModelAccessor interface { - // WatchForModelConfigChanges returns a watcher that will be notified - // whenever the model config changes in state. - WatchForModelConfigChanges() (watcher.NotifyWatcher, error) - - // ModelConfig returns the current model config. - ModelConfig() (*config.Config, error) -} - // NewStorageProvisioner returns a Worker which manages // provisioning (deprovisioning), and attachment (detachment) // of first-class volumes and filesystems. @@ -218,14 +205,6 @@ ) machineChanges := make(chan names.MachineTag) - modelConfigWatcher, err := w.config.Environ.WatchForModelConfigChanges() - if err != nil { - return errors.Annotate(err, "watching model config") - } - if err := w.catacomb.Add(modelConfigWatcher); err != nil { - return errors.Trace(err) - } - // Machine-scoped provisioners need to watch block devices, to create // volume-backed filesystems. if machineTag, ok := w.config.Scope.(names.MachineTag); ok { @@ -239,44 +218,41 @@ machineBlockDevicesChanges = machineBlockDevicesWatcher.Changes() } - startWatchers := func() error { - volumesWatcher, err := w.config.Volumes.WatchVolumes() - if err != nil { - return errors.Annotate(err, "watching volumes") - } - if err := w.catacomb.Add(volumesWatcher); err != nil { - return errors.Trace(err) - } - volumesChanges = volumesWatcher.Changes() + volumesWatcher, err := w.config.Volumes.WatchVolumes() + if err != nil { + return errors.Annotate(err, "watching volumes") + } + if err := w.catacomb.Add(volumesWatcher); err != nil { + return errors.Trace(err) + } + volumesChanges = volumesWatcher.Changes() - filesystemsWatcher, err := w.config.Filesystems.WatchFilesystems() - if err != nil { - return errors.Annotate(err, "watching filesystems") - } - if err := w.catacomb.Add(filesystemsWatcher); err != nil { - return errors.Trace(err) - } - filesystemsChanges = filesystemsWatcher.Changes() + filesystemsWatcher, err := w.config.Filesystems.WatchFilesystems() + if err != nil { + return errors.Annotate(err, "watching filesystems") + } + if err := w.catacomb.Add(filesystemsWatcher); err != nil { + return errors.Trace(err) + } + filesystemsChanges = filesystemsWatcher.Changes() - volumeAttachmentsWatcher, err := w.config.Volumes.WatchVolumeAttachments() - if err != nil { - return errors.Annotate(err, "watching volume attachments") - } - if err := w.catacomb.Add(volumeAttachmentsWatcher); err != nil { - return errors.Trace(err) - } - volumeAttachmentsChanges = volumeAttachmentsWatcher.Changes() + volumeAttachmentsWatcher, err := w.config.Volumes.WatchVolumeAttachments() + if err != nil { + return errors.Annotate(err, "watching volume attachments") + } + if err := w.catacomb.Add(volumeAttachmentsWatcher); err != nil { + return errors.Trace(err) + } + volumeAttachmentsChanges = volumeAttachmentsWatcher.Changes() - filesystemAttachmentsWatcher, err := w.config.Filesystems.WatchFilesystemAttachments() - if err != nil { - return errors.Annotate(err, "watching filesystem attachments") - } - if err := w.catacomb.Add(filesystemAttachmentsWatcher); err != nil { - return errors.Trace(err) - } - filesystemAttachmentsChanges = filesystemAttachmentsWatcher.Changes() - return nil + filesystemAttachmentsWatcher, err := w.config.Filesystems.WatchFilesystemAttachments() + if err != nil { + return errors.Annotate(err, "watching filesystem attachments") } + if err := w.catacomb.Add(filesystemAttachmentsWatcher); err != nil { + return errors.Trace(err) + } + filesystemAttachmentsChanges = filesystemAttachmentsWatcher.Changes() ctx := context{ kill: w.catacomb.Kill, @@ -309,22 +285,6 @@ select { case <-w.catacomb.Dying(): return w.catacomb.ErrDying() - case _, ok := <-modelConfigWatcher.Changes(): - if !ok { - return errors.New("environ config watcher closed") - } - modelConfig, err := w.config.Environ.ModelConfig() - if err != nil { - return errors.Annotate(err, "getting model config") - } - if ctx.modelConfig == nil { - // We've received the initial model config, - // so we can begin provisioning storage. - if err := startWatchers(); err != nil { - return err - } - } - ctx.modelConfig = modelConfig case changes, ok := <-volumesChanges: if !ok { return errors.New("volumes watcher closed") @@ -450,10 +410,9 @@ } type context struct { - kill func(error) - addWorker func(worker.Worker) error - config Config - modelConfig *config.Config + kill func(error) + addWorker func(worker.Worker) error + config Config // volumes contains information about provisioned volumes. volumes map[names.VolumeTag]storage.Volume diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/storageprovisioner/storageprovisioner_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/storageprovisioner/storageprovisioner_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/storageprovisioner/storageprovisioner_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/storageprovisioner/storageprovisioner_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -13,10 +13,8 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/environs/config" "github.com/juju/juju/instance" "github.com/juju/juju/storage" - "github.com/juju/juju/storage/provider/registry" coretesting "github.com/juju/juju/testing" "github.com/juju/juju/watcher" "github.com/juju/juju/worker" @@ -26,6 +24,7 @@ type storageProvisionerSuite struct { coretesting.BaseSuite provider *dummyProvider + registry storage.ProviderRegistry managedFilesystemSource *mockManagedFilesystemSource } @@ -34,10 +33,12 @@ func (s *storageProvisionerSuite) SetUpTest(c *gc.C) { s.BaseSuite.SetUpTest(c) s.provider = &dummyProvider{dynamic: true} - registry.RegisterProvider("dummy", s.provider) - s.AddCleanup(func(*gc.C) { - registry.RegisterProvider("dummy", nil) - }) + s.registry = storage.StaticProviderRegistry{ + map[storage.ProviderType]storage.Provider{ + "dummy": s.provider, + }, + } + s.managedFilesystemSource = nil s.PatchValue( storageprovisioner.NewManagedFilesystemSource, @@ -60,7 +61,7 @@ Volumes: newMockVolumeAccessor(), Filesystems: newMockFilesystemAccessor(), Life: &mockLifecycleManager{}, - Environ: newMockModelAccessor(c), + Registry: s.registry, Machines: newMockMachineAccessor(c), Status: &mockStatusSetter{}, Clock: &mockClock{}, @@ -124,7 +125,7 @@ return nil, nil } - args := &workerArgs{volumes: volumeAccessor} + args := &workerArgs{volumes: volumeAccessor, registry: s.registry} worker := newStorageProvisioner(c, args) defer func() { c.Assert(worker.Wait(), gc.IsNil) }() defer worker.Kill() @@ -138,10 +139,6 @@ // The worker should create volumes according to ids "1" and "2". volumeAccessor.volumesWatcher.changes <- []string{"1", "2"} - // ... but not until the environment config is available. - assertNoEvent(c, volumeInfoSet, "volume info set") - assertNoEvent(c, volumeAttachmentInfoSet, "volume attachment info set") - args.environ.watcher.changes <- struct{}{} waitChannel(c, volumeInfoSet, "waiting for volume info to be set") waitChannel(c, volumeAttachmentInfoSet, "waiting for volume attachments to be set") } @@ -184,7 +181,7 @@ return nil, errors.New("should not be called") } - args := &workerArgs{volumes: volumeAccessor} + args := &workerArgs{volumes: volumeAccessor, registry: s.registry} worker := newStorageProvisioner(c, args) defer func() { c.Assert(worker.Wait(), gc.IsNil) }() defer worker.Kill() @@ -196,7 +193,6 @@ // The worker should create volumes according to ids "1". volumeAccessor.volumesWatcher.changes <- []string{"1"} - args.environ.watcher.changes <- struct{}{} waitChannel(c, volumeAttachmentInfoSet, "waiting for volume attachments to be set") assertNoEvent(c, attachVolumesCalled, "AttachVolumes called") } @@ -225,7 +221,7 @@ }}, nil } - args := &workerArgs{volumes: volumeAccessor, clock: clock} + args := &workerArgs{volumes: volumeAccessor, clock: clock, registry: s.registry} worker := newStorageProvisioner(c, args) defer func() { c.Assert(worker.Wait(), gc.IsNil) }() defer worker.Kill() @@ -234,7 +230,6 @@ MachineTag: "machine-1", AttachmentTag: "volume-1", }} volumeAccessor.volumesWatcher.changes <- []string{"1"} - args.environ.watcher.changes <- struct{}{} waitChannel(c, volumeInfoSet, "waiting for volume info to be set") c.Assert(createVolumeTimes, gc.HasLen, 10) @@ -295,7 +290,7 @@ }}, nil } - args := &workerArgs{filesystems: filesystemAccessor, clock: clock} + args := &workerArgs{filesystems: filesystemAccessor, clock: clock, registry: s.registry} worker := newStorageProvisioner(c, args) defer func() { c.Assert(worker.Wait(), gc.IsNil) }() defer worker.Kill() @@ -304,7 +299,6 @@ MachineTag: "machine-1", AttachmentTag: "filesystem-1", }} filesystemAccessor.filesystemsWatcher.changes <- []string{"1"} - args.environ.watcher.changes <- struct{}{} waitChannel(c, filesystemInfoSet, "waiting for filesystem info to be set") c.Assert(createFilesystemTimes, gc.HasLen, 10) @@ -376,7 +370,7 @@ }}, nil } - args := &workerArgs{volumes: volumeAccessor, clock: clock} + args := &workerArgs{volumes: volumeAccessor, clock: clock, registry: s.registry} worker := newStorageProvisioner(c, args) defer func() { c.Assert(worker.Wait(), gc.IsNil) }() defer worker.Kill() @@ -385,7 +379,6 @@ MachineTag: "machine-1", AttachmentTag: "volume-1", }} volumeAccessor.volumesWatcher.changes <- []string{"1"} - args.environ.watcher.changes <- struct{}{} waitChannel(c, volumeInfoSet, "waiting for volume info to be set") waitChannel(c, volumeAttachmentInfoSet, "waiting for volume attachments to be set") c.Assert(attachVolumeTimes, gc.HasLen, 10) @@ -459,7 +452,7 @@ }}, nil } - args := &workerArgs{filesystems: filesystemAccessor, clock: clock} + args := &workerArgs{filesystems: filesystemAccessor, clock: clock, registry: s.registry} worker := newStorageProvisioner(c, args) defer func() { c.Assert(worker.Wait(), gc.IsNil) }() defer worker.Kill() @@ -468,7 +461,6 @@ MachineTag: "machine-1", AttachmentTag: "filesystem-1", }} filesystemAccessor.filesystemsWatcher.changes <- []string{"1"} - args.environ.watcher.changes <- struct{}{} waitChannel(c, filesystemInfoSet, "waiting for filesystem info to be set") waitChannel(c, filesystemAttachmentInfoSet, "waiting for filesystem attachments to be set") c.Assert(attachFilesystemTimes, gc.HasLen, 10) @@ -563,6 +555,7 @@ life: &mockLifecycleManager{ life: life, }, + registry: s.registry, } worker := newStorageProvisioner(c, args) defer func() { c.Assert(worker.Wait(), gc.IsNil) }() @@ -574,7 +567,6 @@ MachineTag: "machine-1", AttachmentTag: "volume-2", }} volumeAccessor.volumesWatcher.changes <- []string{"1"} - args.environ.watcher.changes <- struct{}{} waitChannel(c, validated, "waiting for volume parameter validation") assertNoEvent(c, createdVolumes, "volume created") c.Assert(validateCalls, gc.Equals, 1) @@ -664,6 +656,7 @@ life: &mockLifecycleManager{ life: life, }, + registry: s.registry, } worker := newStorageProvisioner(c, args) defer func() { c.Assert(worker.Wait(), gc.IsNil) }() @@ -675,7 +668,6 @@ MachineTag: "machine-1", AttachmentTag: "filesystem-2", }} filesystemAccessor.filesystemsWatcher.changes <- []string{"1"} - args.environ.watcher.changes <- struct{}{} waitChannel(c, validated, "waiting for filesystem parameter validation") assertNoEvent(c, createdFilesystems, "filesystem created") c.Assert(validateCalls, gc.Equals, 1) @@ -732,16 +724,13 @@ return nil, nil } - args := &workerArgs{filesystems: filesystemAccessor} + args := &workerArgs{filesystems: filesystemAccessor, registry: s.registry} worker := newStorageProvisioner(c, args) defer func() { c.Assert(worker.Wait(), gc.IsNil) }() defer worker.Kill() // The worker should create filesystems according to ids "1" and "2". filesystemAccessor.filesystemsWatcher.changes <- []string{"1", "2"} - // ... but not until the environment config is available. - assertNoEvent(c, filesystemInfoSet, "filesystem info set") - args.environ.watcher.changes <- struct{}{} waitChannel(c, filesystemInfoSet, "waiting for filesystem info to be set") } @@ -756,13 +745,12 @@ return nil, nil } - args := &workerArgs{volumes: volumeAccessor} + args := &workerArgs{volumes: volumeAccessor, registry: s.registry} worker := newStorageProvisioner(c, args) defer worker.Wait() defer worker.Kill() volumeAccessor.volumesWatcher.changes <- []string{needsInstanceVolumeId} - args.environ.watcher.changes <- struct{}{} assertNoEvent(c, volumeInfoSet, "volume info set") args.machines.instanceIds[names.NewMachineTag("1")] = "inst-id" args.machines.watcher.changes <- struct{}{} @@ -777,14 +765,13 @@ return nil, nil } - args := &workerArgs{volumes: volumeAccessor} + args := &workerArgs{volumes: volumeAccessor, registry: s.registry} worker := newStorageProvisioner(c, args) defer worker.Wait() defer worker.Kill() // Volumes for non-dynamic providers should not be created. s.provider.dynamic = false - args.environ.watcher.changes <- struct{}{} volumeAccessor.volumesWatcher.changes <- []string{"1"} assertNoEvent(c, volumeInfoSet, "volume info set") } @@ -843,7 +830,7 @@ VolumeTag: "volume-1", } - args := &workerArgs{volumes: volumeAccessor} + args := &workerArgs{volumes: volumeAccessor, registry: s.registry} worker := newStorageProvisioner(c, args) defer func() { c.Assert(worker.Wait(), gc.IsNil) }() defer worker.Kill() @@ -859,7 +846,6 @@ }} assertNoEvent(c, volumeAttachmentInfoSet, "volume attachment info set") volumeAccessor.volumesWatcher.changes <- []string{"1"} - args.environ.watcher.changes <- struct{}{} waitChannel(c, volumeAttachmentInfoSet, "waiting for volume attachments to be set") c.Assert(allVolumeAttachments, jc.SameContents, expectedVolumeAttachments) @@ -925,7 +911,7 @@ FilesystemTag: "filesystem-1", } - args := &workerArgs{filesystems: filesystemAccessor} + args := &workerArgs{filesystems: filesystemAccessor, registry: s.registry} worker := newStorageProvisioner(c, args) defer func() { c.Assert(worker.Wait(), gc.IsNil) }() defer worker.Kill() @@ -939,10 +925,8 @@ }, { MachineTag: "machine-0", AttachmentTag: "filesystem-1", }} - // ... but not until the environment config is available. assertNoEvent(c, filesystemAttachmentInfoSet, "filesystem attachment info set") filesystemAccessor.filesystemsWatcher.changes <- []string{"1"} - args.environ.watcher.changes <- struct{}{} waitChannel(c, filesystemAttachmentInfoSet, "waiting for filesystem attachments to be set") c.Assert(allFilesystemAttachments, jc.SameContents, expectedFilesystemAttachments) @@ -965,6 +949,7 @@ args := &workerArgs{ scope: names.NewMachineTag("0"), filesystems: filesystemAccessor, + registry: s.registry, } worker := newStorageProvisioner(c, args) defer func() { c.Assert(worker.Wait(), gc.IsNil) }() @@ -978,8 +963,6 @@ Size: 123, } filesystemAccessor.filesystemsWatcher.changes <- []string{"0/0", "0/1"} - assertNoEvent(c, filesystemInfoSet, "filesystem info set") - args.environ.watcher.changes <- struct{}{} // Only the block device for volume 0/0 is attached at the moment, // so only the corresponding filesystem will be created. @@ -1030,6 +1013,7 @@ args := &workerArgs{ scope: names.NewMachineTag("0"), filesystems: filesystemAccessor, + registry: s.registry, } worker := newStorageProvisioner(c, args) defer func() { c.Assert(worker.Wait(), gc.IsNil) }() @@ -1056,8 +1040,6 @@ MachineTag: "machine-0", AttachmentTag: "filesystem-0-0", }} - assertNoEvent(c, infoSet, "filesystem attachment info set") - args.environ.watcher.changes <- struct{}{} filesystemAccessor.filesystemsWatcher.changes <- []string{"0/0"} info := waitChannel( @@ -1073,33 +1055,6 @@ }}) } -func (s *storageProvisionerSuite) TestUpdateModelConfig(c *gc.C) { - volumeAccessor := newMockVolumeAccessor() - volumeAccessor.provisionedMachines["machine-1"] = instance.Id("already-provisioned-1") - s.provider.volumeSourceFunc = func(envConfig *config.Config, sourceConfig *storage.Config) (storage.VolumeSource, error) { - c.Assert(envConfig, gc.NotNil) - c.Assert(sourceConfig, gc.NotNil) - c.Assert(envConfig.AllAttrs()["foo"], gc.Equals, "bar") - return nil, errors.New("zinga") - } - - args := &workerArgs{volumes: volumeAccessor} - worker := newStorageProvisioner(c, args) - defer worker.Wait() - defer worker.Kill() - - newConfig, err := args.environ.cfg.Apply(map[string]interface{}{"foo": "bar"}) - c.Assert(err, jc.ErrorIsNil) - - args.environ.watcher.changes <- struct{}{} - args.environ.setConfig(newConfig) - args.environ.watcher.changes <- struct{}{} - args.volumes.volumesWatcher.changes <- []string{"1", "2"} - - err = worker.Wait() - c.Assert(err, gc.ErrorMatches, `creating volumes: getting volume source: getting storage source "dummy": zinga`) -} - func (s *storageProvisionerSuite) TestResourceTags(c *gc.C) { volumeInfoSet := make(chan interface{}) volumeAccessor := newMockVolumeAccessor() @@ -1118,18 +1073,19 @@ } var volumeSource dummyVolumeSource - s.provider.volumeSourceFunc = func(envConfig *config.Config, sourceConfig *storage.Config) (storage.VolumeSource, error) { + s.provider.volumeSourceFunc = func(sourceConfig *storage.Config) (storage.VolumeSource, error) { return &volumeSource, nil } var filesystemSource dummyFilesystemSource - s.provider.filesystemSourceFunc = func(envConfig *config.Config, sourceConfig *storage.Config) (storage.FilesystemSource, error) { + s.provider.filesystemSourceFunc = func(sourceConfig *storage.Config) (storage.FilesystemSource, error) { return &filesystemSource, nil } args := &workerArgs{ volumes: volumeAccessor, filesystems: filesystemAccessor, + registry: s.registry, } worker := newStorageProvisioner(c, args) defer func() { c.Assert(worker.Wait(), gc.IsNil) }() @@ -1137,7 +1093,6 @@ volumeAccessor.volumesWatcher.changes <- []string{"1"} filesystemAccessor.filesystemsWatcher.changes <- []string{"1"} - args.environ.watcher.changes <- struct{}{} waitChannel(c, volumeInfoSet, "waiting for volume info to be set") waitChannel(c, filesystemInfoSet, "waiting for filesystem info to be set") c.Assert(volumeSource.createVolumesArgs, jc.DeepEquals, [][]storage.VolumeParams{{{ @@ -1171,7 +1126,7 @@ return nil, errors.New("belly up") } - args := &workerArgs{volumes: volumeAccessor} + args := &workerArgs{volumes: volumeAccessor, registry: s.registry} worker := newStorageProvisioner(c, args) defer worker.Wait() defer worker.Kill() @@ -1184,7 +1139,6 @@ }() args.volumes.volumesWatcher.changes <- []string{"1"} - args.environ.watcher.changes <- struct{}{} waitChannel(c, done, "waiting for worker to exit") } @@ -1195,7 +1149,7 @@ return []params.ErrorResult{{Error: ¶ms.Error{Message: "message", Code: "code"}}}, nil } - args := &workerArgs{volumes: volumeAccessor} + args := &workerArgs{volumes: volumeAccessor, registry: s.registry} worker := newStorageProvisioner(c, args) defer func() { err := worker.Wait() @@ -1210,7 +1164,6 @@ }() args.volumes.volumesWatcher.changes <- []string{"1"} - args.environ.watcher.changes <- struct{}{} assertNoEvent(c, done, "worker exited") } @@ -1226,7 +1179,8 @@ } args := &workerArgs{ - life: &mockLifecycleManager{removeAttachments: removeAttachments}, + life: &mockLifecycleManager{removeAttachments: removeAttachments}, + registry: s.registry, } worker := newStorageProvisioner(c, args) defer worker.Wait() @@ -1235,7 +1189,6 @@ args.volumes.attachmentsWatcher.changes <- []watcher.MachineStorageId{{ MachineTag: "machine-0", AttachmentTag: "volume-0", }} - args.environ.watcher.changes <- struct{}{} waitChannel(c, removed, "waiting for attachment to be removed") } @@ -1300,6 +1253,7 @@ attachmentLife: attachmentLife, removeAttachments: removeAttachments, }, + registry: s.registry, } worker := newStorageProvisioner(c, args) defer func() { c.Assert(worker.Wait(), gc.IsNil) }() @@ -1309,7 +1263,6 @@ MachineTag: "machine-1", AttachmentTag: "volume-1", }} volumeAccessor.volumesWatcher.changes <- []string{"1"} - args.environ.watcher.changes <- struct{}{} waitChannel(c, volumeAttachmentInfoSet, "waiting for volume attachments to be set") volumeAccessor.attachmentsWatcher.changes <- []watcher.MachineStorageId{{ MachineTag: "machine-1", AttachmentTag: "volume-1", @@ -1368,13 +1321,13 @@ attachmentLife: attachmentLife, removeAttachments: removeAttachments, }, + registry: s.registry, } worker := newStorageProvisioner(c, args) defer func() { c.Assert(worker.Wait(), gc.IsNil) }() defer worker.Kill() volumeAccessor.volumesWatcher.changes <- []string{volume.Id()} - args.environ.watcher.changes <- struct{}{} volumeAccessor.attachmentsWatcher.changes <- []watcher.MachineStorageId{{ MachineTag: machine.String(), AttachmentTag: volume.String(), @@ -1427,7 +1380,8 @@ } args := &workerArgs{ - life: &mockLifecycleManager{removeAttachments: removeAttachments}, + life: &mockLifecycleManager{removeAttachments: removeAttachments}, + registry: s.registry, } worker := newStorageProvisioner(c, args) defer worker.Wait() @@ -1436,7 +1390,6 @@ args.filesystems.attachmentsWatcher.changes <- []watcher.MachineStorageId{{ MachineTag: "machine-0", AttachmentTag: "filesystem-0", }} - args.environ.watcher.changes <- struct{}{} waitChannel(c, removed, "waiting for attachment to be removed") } @@ -1501,6 +1454,7 @@ attachmentLife: attachmentLife, removeAttachments: removeAttachments, }, + registry: s.registry, } worker := newStorageProvisioner(c, args) defer func() { c.Assert(worker.Wait(), gc.IsNil) }() @@ -1510,7 +1464,6 @@ MachineTag: "machine-1", AttachmentTag: "filesystem-1", }} filesystemAccessor.filesystemsWatcher.changes <- []string{"1"} - args.environ.watcher.changes <- struct{}{} waitChannel(c, filesystemAttachmentInfoSet, "waiting for filesystem attachments to be set") filesystemAccessor.attachmentsWatcher.changes <- []watcher.MachineStorageId{{ MachineTag: "machine-1", AttachmentTag: "filesystem-1", @@ -1552,6 +1505,7 @@ life: life, remove: remove, }, + registry: s.registry, } worker := newStorageProvisioner(c, args) defer func() { c.Assert(worker.Wait(), gc.IsNil) }() @@ -1561,7 +1515,6 @@ provisionedVolume.Id(), unprovisionedVolume.Id(), } - args.environ.watcher.changes <- struct{}{} // Both volumes should be removed; the provisioned one // should be deprovisioned first. @@ -1614,13 +1567,13 @@ life: life, remove: remove, }, + registry: s.registry, } worker := newStorageProvisioner(c, args) defer func() { c.Assert(worker.Wait(), gc.IsNil) }() defer worker.Kill() volumeAccessor.volumesWatcher.changes <- []string{volume.Id()} - args.environ.watcher.changes <- struct{}{} waitChannel(c, removedChan, "waiting for volume to be removed") c.Assert(destroyVolumeTimes, gc.HasLen, 10) @@ -1683,6 +1636,7 @@ life: life, remove: remove, }, + registry: s.registry, } worker := newStorageProvisioner(c, args) defer func() { c.Assert(worker.Wait(), gc.IsNil) }() @@ -1692,7 +1646,6 @@ provisionedFilesystem.Id(), unprovisionedFilesystem.Id(), } - args.environ.watcher.changes <- struct{}{} // Both filesystems should be removed; the provisioned one // *should* be deprovisioned first, but we don't currently @@ -1728,9 +1681,6 @@ if args.life == nil { args.life = &mockLifecycleManager{} } - if args.environ == nil { - args.environ = newMockModelAccessor(c) - } if args.machines == nil { args.machines = newMockMachineAccessor(c) } @@ -1746,7 +1696,7 @@ Volumes: args.volumes, Filesystems: args.filesystems, Life: args.life, - Environ: args.environ, + Registry: args.registry, Machines: args.machines, Status: args.statusSetter, Clock: args.clock, @@ -1760,7 +1710,7 @@ volumes *mockVolumeAccessor filesystems *mockFilesystemAccessor life *mockLifecycleManager - environ *mockModelAccessor + registry storage.ProviderRegistry machines *mockMachineAccessor clock clock.Clock statusSetter *mockStatusSetter diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/storageprovisioner/volume_ops.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/storageprovisioner/volume_ops.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/storageprovisioner/volume_ops.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/storageprovisioner/volume_ops.go 2016-08-16 08:56:25.000000000 +0000 @@ -8,7 +8,6 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/environs/config" "github.com/juju/juju/status" "github.com/juju/juju/storage" ) @@ -20,7 +19,7 @@ volumeParams = append(volumeParams, op.args) } paramsBySource, volumeSources, err := volumeParamsBySource( - ctx.modelConfig, ctx.config.StorageDir, volumeParams, + ctx.config.StorageDir, volumeParams, ctx.config.Registry, ) if err != nil { return errors.Trace(err) @@ -129,7 +128,7 @@ volumeAttachmentParams = append(volumeAttachmentParams, op.args) } paramsBySource, volumeSources, err := volumeAttachmentParamsBySource( - ctx.modelConfig, ctx.config.StorageDir, volumeAttachmentParams, + ctx.config.StorageDir, volumeAttachmentParams, ctx.config.Registry, ) if err != nil { return errors.Trace(err) @@ -195,7 +194,7 @@ return errors.Trace(err) } paramsBySource, volumeSources, err := volumeParamsBySource( - ctx.modelConfig, ctx.config.StorageDir, volumeParams, + ctx.config.StorageDir, volumeParams, ctx.config.Registry, ) if err != nil { return errors.Trace(err) @@ -267,7 +266,7 @@ volumeAttachmentParams = append(volumeAttachmentParams, op.args) } paramsBySource, volumeSources, err := volumeAttachmentParamsBySource( - ctx.modelConfig, ctx.config.StorageDir, volumeAttachmentParams, + ctx.config.StorageDir, volumeAttachmentParams, ctx.config.Registry, ) if err != nil { return errors.Trace(err) @@ -325,9 +324,9 @@ // volumeParamsBySource separates the volume parameters by volume source. func volumeParamsBySource( - environConfig *config.Config, baseStorageDir string, params []storage.VolumeParams, + registry storage.ProviderRegistry, ) (map[string][]storage.VolumeParams, map[string]storage.VolumeSource, error) { // TODO(axw) later we may have multiple instantiations (sources) // for a storage provider, e.g. multiple Ceph installations. For @@ -340,7 +339,7 @@ continue } volumeSource, err := volumeSource( - environConfig, baseStorageDir, sourceName, params.Provider, + baseStorageDir, sourceName, params.Provider, registry, ) if errors.Cause(err) == errNonDynamic { volumeSource = nil @@ -381,9 +380,9 @@ // volumeAttachmentParamsBySource separates the volume attachment parameters by volume source. func volumeAttachmentParamsBySource( - environConfig *config.Config, baseStorageDir string, params []storage.VolumeAttachmentParams, + registry storage.ProviderRegistry, ) (map[string][]storage.VolumeAttachmentParams, map[string]storage.VolumeSource, error) { // TODO(axw) later we may have multiple instantiations (sources) // for a storage provider, e.g. multiple Ceph installations. For @@ -398,7 +397,7 @@ continue } volumeSource, err := volumeSource( - environConfig, baseStorageDir, sourceName, params.Provider, + baseStorageDir, sourceName, params.Provider, registry, ) if err != nil { return nil, nil, errors.Annotate(err, "getting volume source") diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/uniter/charm/bundles_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/uniter/charm/bundles_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/uniter/charm/bundles_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/uniter/charm/bundles_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,9 +7,7 @@ "os" "path/filepath" "regexp" - "time" - gitjujutesting "github.com/juju/testing" jc "github.com/juju/testing/checkers" "github.com/juju/utils" gc "gopkg.in/check.v1" @@ -20,12 +18,10 @@ "github.com/juju/juju/juju/testing" "github.com/juju/juju/state" "github.com/juju/juju/testcharms" - coretesting "github.com/juju/juju/testing" "github.com/juju/juju/worker/uniter/charm" ) type BundlesDirSuite struct { - gitjujutesting.HTTPSuite testing.JujuConnSuite st api.Connection @@ -35,17 +31,14 @@ 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. @@ -69,7 +62,6 @@ err := s.st.Close() c.Assert(err, jc.ErrorIsNil) s.JujuConnSuite.TearDownTest(c) - s.HTTPSuite.TearDownTest(c) } func (s *BundlesDirSuite) AddCharm(c *gc.C) (charm.BundleInfo, *state.Charm) { @@ -136,24 +128,15 @@ c.Assert(err, jc.ErrorIsNil) assertCharm(c, ch, sch) - // Abort a download. + // Check the abort chan is honoured. err = os.RemoveAll(bunsdir) c.Assert(err, jc.ErrorIsNil) 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, regexp.QuoteMeta(`failed to download charm "cs:quantal/dummy-1" from API server: aborted`)) - close(done) - }() close(abort) - gitjujutesting.Server.Response(500, nil, nil) - select { - case <-done: - case <-time.After(coretesting.LongWait): - c.Fatalf("timed out waiting for abort") - } + + ch, err = d.Read(apiCharm, abort) + c.Assert(ch, gc.IsNil) + c.Assert(err, gc.ErrorMatches, regexp.QuoteMeta(`failed to download charm "cs:quantal/dummy-1" from API server: aborted`)) } func assertCharm(c *gc.C, bun charm.Bundle, sch *state.Charm) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/uniter/operation/executor.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/uniter/operation/executor.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/uniter/operation/executor.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/uniter/operation/executor.go 2016-08-16 08:56:25.000000000 +0000 @@ -73,6 +73,7 @@ if err != nil { return errors.Annotate(err, "could not acquire lock") } + defer logger.Debugf("lock released") defer releaser.Release() } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/uniter/remotestate/watcher_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/uniter/remotestate/watcher_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/uniter/remotestate/watcher_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/uniter/remotestate/watcher_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -530,11 +530,13 @@ assertNotifyEvent(c, s.watcher.RemoteStateChanged(), "waiting for remote state change") // Advance the clock past the trigger time. + s.waitAlarmsStable(c) s.clock.Advance(11 * time.Second) assertNotifyEvent(c, s.watcher.RemoteStateChanged(), "waiting for remote state change") c.Assert(s.watcher.Snapshot().UpdateStatusVersion, gc.Equals, initial.UpdateStatusVersion+1) // Advance again but not past the trigger time. + s.waitAlarmsStable(c) s.clock.Advance(6 * time.Second) assertNoNotifyEvent(c, s.watcher.RemoteStateChanged(), "unexpected remote state change") c.Assert(s.watcher.Snapshot().UpdateStatusVersion, gc.Equals, initial.UpdateStatusVersion+1) @@ -544,3 +546,24 @@ assertNotifyEvent(c, s.watcher.RemoteStateChanged(), "waiting for remote state change") c.Assert(s.watcher.Snapshot().UpdateStatusVersion, gc.Equals, initial.UpdateStatusVersion+2) } + +// waitAlarmsStable is used to wait until the remote watcher's loop has +// stopped churning (at least for testing.ShortWait), so that we can +// then Advance the clock with some confidence that the SUT really is +// waiting for it. This seems likely to be more stable than waiting for +// a specific number of loop iterations; it's currently 9, but waiting +// for a specific number is very likely to start failing intermittently +// again, as in lp:1604955, if the SUT undergoes even subtle changes. +func (s *WatcherSuite) waitAlarmsStable(c *gc.C) { + timeout := time.After(testing.LongWait) + for i := 0; ; i++ { + c.Logf("waiting for alarm %d", i) + select { + case <-s.clock.Alarms(): + case <-time.After(testing.ShortWait): + return + case <-timeout: + c.Fatalf("never stopped setting alarms") + } + } +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/uniter/runner/context/unitStorage_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/uniter/runner/context/unitStorage_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/uniter/runner/context/unitStorage_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/uniter/runner/context/unitStorage_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -11,11 +11,10 @@ "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/provider/ec2" + "github.com/juju/juju/provider/dummy" "github.com/juju/juju/state" "github.com/juju/juju/storage/poolmanager" "github.com/juju/juju/storage/provider" - "github.com/juju/juju/storage/provider/registry" "github.com/juju/juju/worker/uniter/runner/context" ) @@ -136,14 +135,11 @@ func setupTestStorageSupport(c *gc.C, s *state.State) { stsetts := state.NewStateSettings(s) - poolManager := poolmanager.New(stsetts) + poolManager := poolmanager.New(stsetts, dummy.StorageProviders()) _, err := poolManager.Create(testPool, provider.LoopProviderType, map[string]interface{}{"it": "works"}) c.Assert(err, jc.ErrorIsNil) - _, err = poolManager.Create(testPersistentPool, ec2.EBS_ProviderType, map[string]interface{}{"persistent": true}) + _, err = poolManager.Create(testPersistentPool, "environscoped", map[string]interface{}{"persistent": true}) c.Assert(err, jc.ErrorIsNil) - - registry.RegisterEnvironStorageProviders("dummy", ec2.EBS_ProviderType) - registry.RegisterEnvironStorageProviders("admin", ec2.EBS_ProviderType) } func (s *unitStorageSuite) createStorageEnabledUnit(c *gc.C) { diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/uniter/runner/jujuc/add-metric.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/uniter/runner/jujuc/add-metric.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/uniter/runner/jujuc/add-metric.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/uniter/runner/jujuc/add-metric.go 2016-08-16 08:56:25.000000000 +0000 @@ -37,7 +37,7 @@ return &cmd.Info{ Name: "add-metric", Args: "key1=value1 [key2=value2 ...]", - Purpose: "send metrics", + Purpose: "add metrics", } } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/uniter/runner/jujuc/add-metric_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/uniter/runner/jujuc/add-metric_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/uniter/runner/jujuc/add-metric_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/uniter/runner/jujuc/add-metric_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -32,7 +32,7 @@ Usage: add-metric key1=value1 [key2=value2 ...] Summary: -send metrics +add metrics `[1:]) c.Assert(bufferString(ctx.Stderr), gc.Equals, "") } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/uniter/uniter.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/uniter/uniter.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/uniter/uniter.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/uniter/uniter.go 2016-08-16 08:56:25.000000000 +0000 @@ -198,6 +198,7 @@ logger.Infof("hooks are retried %v", u.hookRetryStrategy.ShouldRetry) retryHookChan := make(chan struct{}, 1) + // TODO(katco): 2016-08-09: This type is deprecated: lp:1611427 retryHookTimer := utils.NewBackoffTimer(utils.BackoffTimerConfig{ Min: u.hookRetryStrategy.MinRetryTime, Max: u.hookRetryStrategy.MaxRetryTime, @@ -521,10 +522,12 @@ Delay: 250 * time.Millisecond, Cancel: u.catacomb.Dying(), } + logger.Debugf("acquire lock %q for uniter hook execution", u.hookLockName) releaser, err := mutex.Acquire(spec) if err != nil { return nil, errors.Trace(err) } + logger.Debugf("lock %q acquired", u.hookLockName) return releaser, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/upgradesteps/worker.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/upgradesteps/worker.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/upgradesteps/worker.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/upgradesteps/worker.go 2016-08-16 08:56:25.000000000 +0000 @@ -439,6 +439,7 @@ return isMaster, nil } +// TODO(katco): 2016-08-09: lp:1611427 var getUpgradeRetryStrategy = func() utils.AttemptStrategy { return utils.AttemptStrategy{ Delay: 2 * time.Minute, diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/upgradesteps/worker_test.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/upgradesteps/worker_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/upgradesteps/worker_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/upgradesteps/worker_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -24,6 +24,7 @@ "github.com/juju/juju/mongo/mongotest" "github.com/juju/juju/state" "github.com/juju/juju/state/multiwatcher" + "github.com/juju/juju/state/stateenvirons" statetesting "github.com/juju/juju/state/testing" "github.com/juju/juju/status" coretesting "github.com/juju/juju/testing" @@ -89,7 +90,7 @@ } func (s *UpgradeSuite) captureLogs(c *gc.C) { - c.Assert(loggo.RegisterWriter("upgrade-tests", &s.logWriter, loggo.INFO), gc.IsNil) + c.Assert(loggo.RegisterWriter("upgrade-tests", &s.logWriter), gc.IsNil) s.AddCleanup(func(*gc.C) { loggo.RemoveWriter("upgrade-tests") s.logWriter.Clear() @@ -410,7 +411,10 @@ func (s *UpgradeSuite) openStateForUpgrade() (*state.State, error) { mongoInfo := s.State.MongoConnectionInfo() - st, err := state.Open(s.State.ModelTag(), mongoInfo, mongotest.DialOpts(), environs.NewStatePolicy()) + newPolicy := stateenvirons.GetNewPolicyFunc( + stateenvirons.GetNewEnvironFunc(environs.New), + ) + st, err := state.Open(s.State.ModelTag(), mongoInfo, mongotest.DialOpts(), newPolicy) if err != nil { return nil, err } @@ -486,6 +490,7 @@ const maxUpgradeRetries = 3 func (s *UpgradeSuite) setInstantRetryStrategy(c *gc.C) { + // TODO(katco): 2016-08-09: lp:1611427 s.PatchValue(&getUpgradeRetryStrategy, func() utils.AttemptStrategy { c.Logf("setting instant retry strategy for upgrade: retries=%d", maxUpgradeRetries) return utils.AttemptStrategy{ diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/worker/workertest/check.go juju-core-2.0~beta15/src/github.com/juju/juju/worker/workertest/check.go --- juju-core-2.0~beta12/src/github.com/juju/juju/worker/workertest/check.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/worker/workertest/check.go 2016-08-16 08:56:25.000000000 +0000 @@ -97,3 +97,21 @@ err := CheckKill(c, w) c.Logf("ignoring error: %v", err) } + +// CheckNilOrKill has no effect if w is nil; otherwise, it fails the test +// and tries to stop the (non-nil) worker via CleanKill(). It's suitable +// for testing constructor failure: +// +// someWorker, err := some.NewWorker(badConfig) +// workertest.CheckNilOrKill(c, someWorker) +// c.Check(err, ... +// +// ...because it will do the right thing if your constructor succeeds +// unexpectedly, and make every effort to prevent a rogue worker living +// beyond its test. +func CheckNilOrKill(c *gc.C, w worker.Worker) { + if !c.Check(w, gc.IsNil) { + c.Logf("stopping rogue worker...") + CleanKill(c, w) + } +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/juju/wrench/wrench_test.go juju-core-2.0~beta15/src/github.com/juju/juju/wrench/wrench_test.go --- juju-core-2.0~beta12/src/github.com/juju/juju/wrench/wrench_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/juju/wrench/wrench_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -34,7 +34,7 @@ s.BaseSuite.SetUpTest(c) // BaseSuite turns off wrench so restore the non-testing default. wrench.SetEnabled(true) - c.Assert(loggo.RegisterWriter("wrench-tests", &s.logWriter, loggo.TRACE), gc.IsNil) + c.Assert(loggo.RegisterWriter("wrench-tests", &s.logWriter), gc.IsNil) s.AddCleanup(func(*gc.C) { s.logWriter.Clear() loggo.RemoveWriter("wrench-tests") diff -Nru juju-core-2.0~beta12/src/github.com/juju/loggo/benchmarks_test.go juju-core-2.0~beta15/src/github.com/juju/loggo/benchmarks_test.go --- juju-core-2.0~beta12/src/github.com/juju/loggo/benchmarks_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/loggo/benchmarks_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,105 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package loggo_test + +import ( + "io/ioutil" + "os" + + "github.com/juju/loggo" + gc "gopkg.in/check.v1" +) + +type BenchmarksSuite struct { + logger loggo.Logger + writer *writer +} + +var _ = gc.Suite(&BenchmarksSuite{}) + +func (s *BenchmarksSuite) SetUpTest(c *gc.C) { + loggo.ResetLogging() + s.logger = loggo.GetLogger("test.writer") + s.writer = &writer{} + err := loggo.RegisterWriter("test", s.writer) + c.Assert(err, gc.IsNil) +} + +func (s *BenchmarksSuite) BenchmarkLoggingNoWriters(c *gc.C) { + // No writers + loggo.RemoveWriter("test") + for i := 0; i < c.N; i++ { + s.logger.Warningf("just a simple warning for %d", i) + } +} + +func (s *BenchmarksSuite) BenchmarkLoggingNoWritersNoFormat(c *gc.C) { + // No writers + loggo.RemoveWriter("test") + for i := 0; i < c.N; i++ { + s.logger.Warningf("just a simple warning") + } +} + +func (s *BenchmarksSuite) BenchmarkLoggingTestWriters(c *gc.C) { + for i := 0; i < c.N; i++ { + s.logger.Warningf("just a simple warning for %d", i) + } + c.Assert(s.writer.Log(), gc.HasLen, c.N) +} + +func (s *BenchmarksSuite) BenchmarkLoggingDiskWriter(c *gc.C) { + logFile := s.setupTempFileWriter(c) + defer logFile.Close() + msg := "just a simple warning for %d" + for i := 0; i < c.N; i++ { + s.logger.Warningf(msg, i) + } + offset, err := logFile.Seek(0, os.SEEK_CUR) + c.Assert(err, gc.IsNil) + c.Assert((offset > int64(len(msg))*int64(c.N)), gc.Equals, true, + gc.Commentf("Not enough data was written to the log file.")) +} + +func (s *BenchmarksSuite) BenchmarkLoggingDiskWriterNoMessages(c *gc.C) { + logFile := s.setupTempFileWriter(c) + defer logFile.Close() + // Change the log level + writer, err := loggo.RemoveWriter("testfile") + c.Assert(err, gc.IsNil) + loggo.RegisterWriter("testfile", loggo.NewMinimumLevelWriter(writer, loggo.WARNING)) + msg := "just a simple warning for %d" + for i := 0; i < c.N; i++ { + s.logger.Debugf(msg, i) + } + offset, err := logFile.Seek(0, os.SEEK_CUR) + c.Assert(err, gc.IsNil) + c.Assert(offset, gc.Equals, int64(0), + gc.Commentf("Data was written to the log file.")) +} + +func (s *BenchmarksSuite) BenchmarkLoggingDiskWriterNoMessagesLogLevel(c *gc.C) { + logFile := s.setupTempFileWriter(c) + defer logFile.Close() + // Change the log level + s.logger.SetLogLevel(loggo.WARNING) + msg := "just a simple warning for %d" + for i := 0; i < c.N; i++ { + s.logger.Debugf(msg, i) + } + offset, err := logFile.Seek(0, os.SEEK_CUR) + c.Assert(err, gc.IsNil) + c.Assert(offset, gc.Equals, int64(0), + gc.Commentf("Data was written to the log file.")) +} + +func (s *BenchmarksSuite) setupTempFileWriter(c *gc.C) *os.File { + loggo.RemoveWriter("test") + logFile, err := ioutil.TempFile(c.MkDir(), "loggo-test") + c.Assert(err, gc.IsNil) + writer := loggo.NewSimpleWriter(logFile, loggo.DefaultFormatter) + err = loggo.RegisterWriter("testfile", writer) + c.Assert(err, gc.IsNil) + return logFile +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/loggo/checkers_test.go juju-core-2.0~beta15/src/github.com/juju/loggo/checkers_test.go --- juju-core-2.0~beta12/src/github.com/juju/loggo/checkers_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/loggo/checkers_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,44 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package loggo_test + +import ( + "fmt" + "time" + + gc "gopkg.in/check.v1" +) + +func Between(start, end time.Time) gc.Checker { + if end.Before(start) { + return &betweenChecker{end, start} + } + return &betweenChecker{start, end} +} + +type betweenChecker struct { + start, end time.Time +} + +func (checker *betweenChecker) Info() *gc.CheckerInfo { + info := gc.CheckerInfo{ + Name: "Between", + Params: []string{"obtained"}, + } + return &info +} + +func (checker *betweenChecker) Check(params []interface{}, names []string) (result bool, error string) { + when, ok := params[0].(time.Time) + if !ok { + return false, "obtained value type must be time.Time" + } + if when.Before(checker.start) { + return false, fmt.Sprintf("obtained time %q is before start time %q", when, checker.start) + } + if when.After(checker.end) { + return false, fmt.Sprintf("obtained time %q is after end time %q", when, checker.end) + } + return true, "" +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/loggo/config.go juju-core-2.0~beta15/src/github.com/juju/loggo/config.go --- juju-core-2.0~beta12/src/github.com/juju/loggo/config.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/loggo/config.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,96 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package loggo + +import ( + "fmt" + "sort" + "strings" +) + +// Config is a mapping of logger module names to logging severity levels. +type Config map[string]Level + +// String returns a logger configuration string that may be parsed +// using ParseConfigurationString. +func (c Config) String() string { + if c == nil { + return "" + } + // output in alphabetical order. + names := []string{} + for name := range c { + names = append(names, name) + } + sort.Strings(names) + + var entries []string + for _, name := range names { + level := c[name] + if name == "" { + name = rootString + } + entry := fmt.Sprintf("%s=%s", name, level) + entries = append(entries, entry) + } + return strings.Join(entries, ";") +} + +func parseConfigValue(value string) (string, Level, error) { + pair := strings.SplitN(value, "=", 2) + if len(pair) < 2 { + return "", UNSPECIFIED, fmt.Errorf("config value expected '=', found %q", value) + } + name := strings.TrimSpace(pair[0]) + if name == "" { + return "", UNSPECIFIED, fmt.Errorf("config value %q has missing module name", value) + } + + levelStr := strings.TrimSpace(pair[1]) + level, ok := ParseLevel(levelStr) + if !ok { + return "", UNSPECIFIED, fmt.Errorf("unknown severity level %q", levelStr) + } + if name == rootString { + name = "" + } + return name, level, nil +} + +// ParseConfigString parses a logger configuration string into a map of logger +// names and their associated log level. This method is provided to allow +// other programs to pre-validate a configuration string rather than just +// calling ConfigureLoggers. +// +// Logging modules are colon- or semicolon-separated; each module is specified +// as =. White space outside of module names and levels is +// ignored. The root module is specified with the name "". +// +// As a special case, a log level may be specified on its own. +// This is equivalent to specifying the level of the root module, +// so "DEBUG" is equivalent to `=DEBUG` +// +// An example specification: +// `=ERROR; foo.bar=WARNING` +func ParseConfigString(specification string) (Config, error) { + specification = strings.TrimSpace(specification) + if specification == "" { + return nil, nil + } + cfg := make(Config) + if level, ok := ParseLevel(specification); ok { + cfg[""] = level + return cfg, nil + } + + values := strings.FieldsFunc(specification, func(r rune) bool { return r == ';' || r == ':' }) + for _, value := range values { + name, level, err := parseConfigValue(value) + if err != nil { + return nil, err + } + cfg[name] = level + } + return cfg, nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/loggo/config_test.go juju-core-2.0~beta15/src/github.com/juju/loggo/config_test.go --- juju-core-2.0~beta12/src/github.com/juju/loggo/config_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/loggo/config_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,152 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package loggo + +import gc "gopkg.in/check.v1" + +type ConfigSuite struct{} + +var _ = gc.Suite(&ConfigSuite{}) + +func (*ConfigSuite) TestParseConfigValue(c *gc.C) { + for i, test := range []struct { + value string + module string + level Level + err string + }{{ + err: `config value expected '=', found ""`, + }, { + value: "WARNING", + err: `config value expected '=', found "WARNING"`, + }, { + value: "=WARNING", + err: `config value "=WARNING" has missing module name`, + }, { + value: " name = WARNING ", + module: "name", + level: WARNING, + }, { + value: "name = foo", + err: `unknown severity level "foo"`, + }, { + value: "name=DEBUG=INFO", + err: `unknown severity level "DEBUG=INFO"`, + }, { + value: " = info", + module: "", + level: INFO, + }} { + c.Logf("%d: %s", i, test.value) + module, level, err := parseConfigValue(test.value) + if test.err == "" { + c.Check(err, gc.IsNil) + c.Check(module, gc.Equals, test.module) + c.Check(level, gc.Equals, test.level) + } else { + c.Check(module, gc.Equals, "") + c.Check(level, gc.Equals, UNSPECIFIED) + c.Check(err.Error(), gc.Equals, test.err) + } + } +} + +func (*ConfigSuite) TestPaarseConfigurationString(c *gc.C) { + for i, test := range []struct { + configuration string + expected Config + err string + }{{ + configuration: "", + // nil Config, no error + }, { + configuration: "INFO", + expected: Config{"": INFO}, + }, { + configuration: "=INFO", + err: `config value "=INFO" has missing module name`, + }, { + configuration: "=UNSPECIFIED", + expected: Config{"": UNSPECIFIED}, + }, { + configuration: "=DEBUG", + expected: Config{"": DEBUG}, + }, { + configuration: "test.module=debug", + expected: Config{"test.module": DEBUG}, + }, { + configuration: "module=info; sub.module=debug; other.module=warning", + expected: Config{ + "module": INFO, + "sub.module": DEBUG, + "other.module": WARNING, + }, + }, { + // colons not semicolons + configuration: "module=info: sub.module=debug: other.module=warning", + expected: Config{ + "module": INFO, + "sub.module": DEBUG, + "other.module": WARNING, + }, + }, { + configuration: " foo.bar \t\r\n= \t\r\nCRITICAL \t\r\n; \t\r\nfoo \r\t\n = DEBUG", + expected: Config{ + "foo": DEBUG, + "foo.bar": CRITICAL, + }, + }, { + configuration: "foo;bar", + err: `config value expected '=', found "foo"`, + }, { + configuration: "foo=", + err: `unknown severity level ""`, + }, { + configuration: "foo=unknown", + err: `unknown severity level "unknown"`, + }} { + c.Logf("%d: %q", i, test.configuration) + config, err := ParseConfigString(test.configuration) + if test.err == "" { + c.Check(err, gc.IsNil) + c.Check(config, gc.DeepEquals, test.expected) + } else { + c.Check(config, gc.IsNil) + c.Check(err.Error(), gc.Equals, test.err) + } + } +} + +func (*ConfigSuite) TestConfigString(c *gc.C) { + for i, test := range []struct { + config Config + expected string + }{{ + config: nil, + expected: "", + }, { + config: Config{"": INFO}, + expected: "=INFO", + }, { + config: Config{"": UNSPECIFIED}, + expected: "=UNSPECIFIED", + }, { + config: Config{"": DEBUG}, + expected: "=DEBUG", + }, { + config: Config{"test.module": DEBUG}, + expected: "test.module=DEBUG", + }, { + config: Config{ + "": WARNING, + "module": INFO, + "sub.module": DEBUG, + "other.module": WARNING, + }, + expected: "=WARNING;module=INFO;other.module=WARNING;sub.module=DEBUG", + }} { + c.Logf("%d: %q", i, test.expected) + c.Check(test.config.String(), gc.Equals, test.expected) + } +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/loggo/context.go juju-core-2.0~beta15/src/github.com/juju/loggo/context.go --- juju-core-2.0~beta12/src/github.com/juju/loggo/context.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/loggo/context.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,198 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package loggo + +import ( + "fmt" + "strings" + "sync" +) + +// Context produces loggers for a hierarchy of modules. The context holds +// a collection of hierarchical loggers and their writers. +type Context struct { + root *module + + // Perhaps have one mutex? + modulesMutex sync.Mutex + modules map[string]*module + + writersMutex sync.Mutex + writers map[string]Writer + + // writeMuxtex is used to serialise write operations. + writeMutex sync.Mutex +} + +// NewLoggers returns a new Context with no writers set. +// If the root level is UNSPECIFIED, WARNING is used. +func NewContext(rootLevel Level) *Context { + if rootLevel < TRACE || rootLevel > CRITICAL { + rootLevel = WARNING + } + context := &Context{ + modules: make(map[string]*module), + writers: make(map[string]Writer), + } + context.root = &module{ + level: rootLevel, + context: context, + } + context.modules[""] = context.root + return context +} + +// GetLogger returns a Logger for the given module name, creating it and +// its parents if necessary. +func (c *Context) GetLogger(name string) Logger { + name = strings.TrimSpace(strings.ToLower(name)) + c.modulesMutex.Lock() + defer c.modulesMutex.Unlock() + return Logger{c.getLoggerModule(name)} +} + +func (c *Context) getLoggerModule(name string) *module { + if name == rootString { + name = "" + } + impl, found := c.modules[name] + if found { + return impl + } + parentName := "" + if i := strings.LastIndex(name, "."); i >= 0 { + parentName = name[0:i] + } + parent := c.getLoggerModule(parentName) + impl = &module{name, UNSPECIFIED, parent, c} + c.modules[name] = impl + return impl +} + +// Config returns the current configuration of the Loggers. Loggers +// with UNSPECIFIED level will not be included. +func (c *Context) Config() Config { + result := make(Config) + c.modulesMutex.Lock() + defer c.modulesMutex.Unlock() + + for name, module := range c.modules { + if module.level != UNSPECIFIED { + result[name] = module.level + } + } + return result +} + +// CompleteConfig returns all the loggers and their defined levels, +// even if that level is UNSPECIFIED. +func (c *Context) CompleteConfig() Config { + result := make(Config) + c.modulesMutex.Lock() + defer c.modulesMutex.Unlock() + + for name, module := range c.modules { + result[name] = module.level + } + return result +} + +// ApplyConfig configures the logging modules according to the provided config. +func (c *Context) ApplyConfig(config Config) { + c.modulesMutex.Lock() + defer c.modulesMutex.Unlock() + for name, level := range config { + module := c.getLoggerModule(name) + module.setLevel(level) + } +} + +// ResetLoggerLevels iterates through the known logging modules and sets the +// levels of all to UNSPECIFIED, except for which is set to WARNING. +func (c *Context) ResetLoggerLevels() { + c.modulesMutex.Lock() + defer c.modulesMutex.Unlock() + // Setting the root module to UNSPECIFIED will set it to WARNING. + for _, module := range c.modules { + module.setLevel(UNSPECIFIED) + } +} + +func (c *Context) write(entry Entry) { + c.writeMutex.Lock() + defer c.writeMutex.Unlock() + for _, writer := range c.getWriters() { + writer.Write(entry) + } +} + +func (c *Context) getWriters() []Writer { + c.writersMutex.Lock() + defer c.writersMutex.Unlock() + var result []Writer + for _, writer := range c.writers { + result = append(result, writer) + } + return result +} + +// AddWriter adds a writer to the list to be called for each logging call. +// The name cannot be empty, and the writer cannot be nil. If an existing +// writer exists with the specified name, an error is returned. +func (c *Context) AddWriter(name string, writer Writer) error { + if name == "" { + return fmt.Errorf("name cannot be empty") + } + if writer == nil { + return fmt.Errorf("writer cannot be nil") + } + c.writersMutex.Lock() + defer c.writersMutex.Unlock() + if _, found := c.writers[name]; found { + return fmt.Errorf("context already has a writer named %q", name) + } + c.writers[name] = writer + return nil +} + +// RemoveWriter remotes the specified writer. If a writer is not found with +// the specified name an error is returned. The writer that was removed is also +// returned. +func (c *Context) RemoveWriter(name string) (Writer, error) { + c.writersMutex.Lock() + defer c.writersMutex.Unlock() + reg, found := c.writers[name] + if !found { + return nil, fmt.Errorf("context has no writer named %q", name) + } + delete(c.writers, name) + return reg, nil +} + +// ReplaceWriter is a convenience method that does the equivalent of RemoveWriter +// followed by AddWriter with the same name. The replaced writer is returned. +func (c *Context) ReplaceWriter(name string, writer Writer) (Writer, error) { + if name == "" { + return nil, fmt.Errorf("name cannot be empty") + } + if writer == nil { + return nil, fmt.Errorf("writer cannot be nil") + } + c.writersMutex.Lock() + defer c.writersMutex.Unlock() + reg, found := c.writers[name] + if !found { + return nil, fmt.Errorf("context has no writer named %q", name) + } + oldWriter := reg + c.writers[name] = writer + return oldWriter, nil +} + +// ResetWriters is generally only used in testing and removes all the writers. +func (c *Context) ResetWriters() { + c.writersMutex.Lock() + defer c.writersMutex.Unlock() + c.writers = make(map[string]Writer) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/loggo/context_test.go juju-core-2.0~beta15/src/github.com/juju/loggo/context_test.go --- juju-core-2.0~beta12/src/github.com/juju/loggo/context_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/loggo/context_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,328 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package loggo_test + +import ( + "github.com/juju/loggo" + gc "gopkg.in/check.v1" +) + +type ContextSuite struct{} + +var _ = gc.Suite(&ContextSuite{}) + +func (*ContextSuite) TestNewContextRootLevel(c *gc.C) { + for i, test := range []struct { + level loggo.Level + expected loggo.Level + }{{ + level: loggo.UNSPECIFIED, + expected: loggo.WARNING, + }, { + level: loggo.DEBUG, + expected: loggo.DEBUG, + }, { + level: loggo.INFO, + expected: loggo.INFO, + }, { + level: loggo.WARNING, + expected: loggo.WARNING, + }, { + level: loggo.ERROR, + expected: loggo.ERROR, + }, { + level: loggo.CRITICAL, + expected: loggo.CRITICAL, + }, { + level: loggo.Level(42), + expected: loggo.WARNING, + }} { + c.Log("%d: %s", i, test.level) + context := loggo.NewContext(test.level) + cfg := context.Config() + c.Check(cfg, gc.HasLen, 1) + value, found := cfg[""] + c.Check(found, gc.Equals, true) + c.Check(value, gc.Equals, test.expected) + } +} + +func logAllSeverities(logger loggo.Logger) { + logger.Criticalf("something critical") + logger.Errorf("an error") + logger.Warningf("a warning message") + logger.Infof("an info message") + logger.Debugf("a debug message") + logger.Tracef("a trace message") +} + +func checkLogEntry(c *gc.C, entry, expected loggo.Entry) { + c.Check(entry.Level, gc.Equals, expected.Level) + c.Check(entry.Module, gc.Equals, expected.Module) + c.Check(entry.Message, gc.Equals, expected.Message) +} + +func checkLogEntries(c *gc.C, obtained, expected []loggo.Entry) { + if c.Check(len(obtained), gc.Equals, len(expected)) { + for i := range obtained { + checkLogEntry(c, obtained[i], expected[i]) + } + } +} + +func (*ContextSuite) TestGetLoggerRoot(c *gc.C) { + context := loggo.NewContext(loggo.DEBUG) + blank := context.GetLogger("") + root := context.GetLogger("") + c.Assert(blank, gc.Equals, root) +} + +func (*ContextSuite) TestGetLoggerCase(c *gc.C) { + context := loggo.NewContext(loggo.DEBUG) + upper := context.GetLogger("TEST") + lower := context.GetLogger("test") + c.Assert(upper, gc.Equals, lower) + c.Assert(upper.Name(), gc.Equals, "test") +} + +func (*ContextSuite) TestGetLoggerSpace(c *gc.C) { + context := loggo.NewContext(loggo.DEBUG) + space := context.GetLogger(" test ") + lower := context.GetLogger("test") + c.Assert(space, gc.Equals, lower) + c.Assert(space.Name(), gc.Equals, "test") +} + +func (*ContextSuite) TestNewContextNoWriter(c *gc.C) { + // Should be no output. + context := loggo.NewContext(loggo.DEBUG) + logger := context.GetLogger("test") + logAllSeverities(logger) +} + +func (*ContextSuite) newContextWithTestWriter(c *gc.C, level loggo.Level) (*loggo.Context, *loggo.TestWriter) { + writer := &loggo.TestWriter{} + context := loggo.NewContext(level) + context.AddWriter("test", writer) + return context, writer +} + +func (s *ContextSuite) TestNewContextRootSeverityWarning(c *gc.C) { + context, writer := s.newContextWithTestWriter(c, loggo.WARNING) + logger := context.GetLogger("test") + logAllSeverities(logger) + checkLogEntries(c, writer.Log(), []loggo.Entry{ + {Level: loggo.CRITICAL, Module: "test", Message: "something critical"}, + {Level: loggo.ERROR, Module: "test", Message: "an error"}, + {Level: loggo.WARNING, Module: "test", Message: "a warning message"}, + }) +} + +func (s *ContextSuite) TestNewContextRootSeverityTrace(c *gc.C) { + context, writer := s.newContextWithTestWriter(c, loggo.TRACE) + logger := context.GetLogger("test") + logAllSeverities(logger) + checkLogEntries(c, writer.Log(), []loggo.Entry{ + {Level: loggo.CRITICAL, Module: "test", Message: "something critical"}, + {Level: loggo.ERROR, Module: "test", Message: "an error"}, + {Level: loggo.WARNING, Module: "test", Message: "a warning message"}, + {Level: loggo.INFO, Module: "test", Message: "an info message"}, + {Level: loggo.DEBUG, Module: "test", Message: "a debug message"}, + {Level: loggo.TRACE, Module: "test", Message: "a trace message"}, + }) +} + +func (*ContextSuite) TestNewContextConfig(c *gc.C) { + context := loggo.NewContext(loggo.DEBUG) + config := context.Config() + c.Assert(config, gc.DeepEquals, loggo.Config{"": loggo.DEBUG}) +} + +func (*ContextSuite) TestNewLoggerAddsConfig(c *gc.C) { + context := loggo.NewContext(loggo.DEBUG) + _ = context.GetLogger("test.module") + c.Assert(context.Config(), gc.DeepEquals, loggo.Config{ + "": loggo.DEBUG, + }) + c.Assert(context.CompleteConfig(), gc.DeepEquals, loggo.Config{ + "": loggo.DEBUG, + "test": loggo.UNSPECIFIED, + "test.module": loggo.UNSPECIFIED, + }) +} + +func (*ContextSuite) TestApplyNilConfig(c *gc.C) { + context := loggo.NewContext(loggo.DEBUG) + context.ApplyConfig(nil) + c.Assert(context.Config(), gc.DeepEquals, loggo.Config{"": loggo.DEBUG}) +} + +func (*ContextSuite) TestApplyConfigRootUnspecified(c *gc.C) { + context := loggo.NewContext(loggo.DEBUG) + context.ApplyConfig(loggo.Config{"": loggo.UNSPECIFIED}) + c.Assert(context.Config(), gc.DeepEquals, loggo.Config{"": loggo.WARNING}) +} + +func (*ContextSuite) TestApplyConfigRootTrace(c *gc.C) { + context := loggo.NewContext(loggo.WARNING) + context.ApplyConfig(loggo.Config{"": loggo.TRACE}) + c.Assert(context.Config(), gc.DeepEquals, loggo.Config{"": loggo.TRACE}) +} + +func (*ContextSuite) TestApplyConfigCreatesModules(c *gc.C) { + context := loggo.NewContext(loggo.WARNING) + context.ApplyConfig(loggo.Config{"first.second": loggo.TRACE}) + c.Assert(context.Config(), gc.DeepEquals, + loggo.Config{ + "": loggo.WARNING, + "first.second": loggo.TRACE, + }) + c.Assert(context.CompleteConfig(), gc.DeepEquals, + loggo.Config{ + "": loggo.WARNING, + "first": loggo.UNSPECIFIED, + "first.second": loggo.TRACE, + }) +} + +func (*ContextSuite) TestApplyConfigAdditive(c *gc.C) { + context := loggo.NewContext(loggo.WARNING) + context.ApplyConfig(loggo.Config{"first.second": loggo.TRACE}) + context.ApplyConfig(loggo.Config{"other.module": loggo.DEBUG}) + c.Assert(context.Config(), gc.DeepEquals, + loggo.Config{ + "": loggo.WARNING, + "first.second": loggo.TRACE, + "other.module": loggo.DEBUG, + }) + c.Assert(context.CompleteConfig(), gc.DeepEquals, + loggo.Config{ + "": loggo.WARNING, + "first": loggo.UNSPECIFIED, + "first.second": loggo.TRACE, + "other": loggo.UNSPECIFIED, + "other.module": loggo.DEBUG, + }) +} + +func (*ContextSuite) TestResetLoggerLevels(c *gc.C) { + context := loggo.NewContext(loggo.DEBUG) + context.ApplyConfig(loggo.Config{"first.second": loggo.TRACE}) + context.ResetLoggerLevels() + c.Assert(context.Config(), gc.DeepEquals, + loggo.Config{ + "": loggo.WARNING, + }) + c.Assert(context.CompleteConfig(), gc.DeepEquals, + loggo.Config{ + "": loggo.WARNING, + "first": loggo.UNSPECIFIED, + "first.second": loggo.UNSPECIFIED, + }) +} + +func (*ContextSuite) TestWriterNamesNone(c *gc.C) { + context := loggo.NewContext(loggo.DEBUG) + writers := context.WriterNames() + c.Assert(writers, gc.HasLen, 0) +} + +func (*ContextSuite) TestAddWriterNoName(c *gc.C) { + context := loggo.NewContext(loggo.DEBUG) + err := context.AddWriter("", nil) + c.Assert(err.Error(), gc.Equals, "name cannot be empty") +} + +func (*ContextSuite) TestAddWriterNil(c *gc.C) { + context := loggo.NewContext(loggo.DEBUG) + err := context.AddWriter("foo", nil) + c.Assert(err.Error(), gc.Equals, "writer cannot be nil") +} + +func (*ContextSuite) TestNamedAddWriter(c *gc.C) { + context := loggo.NewContext(loggo.DEBUG) + err := context.AddWriter("foo", &writer{name: "foo"}) + c.Assert(err, gc.IsNil) + err = context.AddWriter("foo", &writer{name: "foo"}) + c.Assert(err.Error(), gc.Equals, `context already has a writer named "foo"`) + + writers := context.WriterNames() + c.Assert(writers, gc.DeepEquals, []string{"foo"}) +} + +func (*ContextSuite) TestRemoveWriter(c *gc.C) { + context := loggo.NewContext(loggo.DEBUG) + w, err := context.RemoveWriter("unknown") + c.Assert(err.Error(), gc.Equals, `context has no writer named "unknown"`) + c.Assert(w, gc.IsNil) +} + +func (*ContextSuite) TestRemoveWriterFound(c *gc.C) { + context := loggo.NewContext(loggo.DEBUG) + original := &writer{name: "foo"} + err := context.AddWriter("foo", original) + c.Assert(err, gc.IsNil) + existing, err := context.RemoveWriter("foo") + c.Assert(err, gc.IsNil) + c.Assert(existing, gc.Equals, original) + + writers := context.WriterNames() + c.Assert(writers, gc.HasLen, 0) +} + +func (*ContextSuite) TestReplaceWriterNoName(c *gc.C) { + context := loggo.NewContext(loggo.DEBUG) + existing, err := context.ReplaceWriter("", nil) + c.Assert(err.Error(), gc.Equals, "name cannot be empty") + c.Assert(existing, gc.IsNil) +} + +func (*ContextSuite) TestReplaceWriterNil(c *gc.C) { + context := loggo.NewContext(loggo.DEBUG) + existing, err := context.ReplaceWriter("foo", nil) + c.Assert(err.Error(), gc.Equals, "writer cannot be nil") + c.Assert(existing, gc.IsNil) +} + +func (*ContextSuite) TestReplaceWriterNotFound(c *gc.C) { + context := loggo.NewContext(loggo.DEBUG) + existing, err := context.ReplaceWriter("foo", &writer{}) + c.Assert(err.Error(), gc.Equals, `context has no writer named "foo"`) + c.Assert(existing, gc.IsNil) +} + +func (*ContextSuite) TestMultipleWriters(c *gc.C) { + first := &writer{} + second := &writer{} + third := &writer{} + context := loggo.NewContext(loggo.TRACE) + err := context.AddWriter("first", first) + c.Assert(err, gc.IsNil) + err = context.AddWriter("second", second) + c.Assert(err, gc.IsNil) + err = context.AddWriter("third", third) + c.Assert(err, gc.IsNil) + + logger := context.GetLogger("test") + logAllSeverities(logger) + + expected := []loggo.Entry{ + {Level: loggo.CRITICAL, Module: "test", Message: "something critical"}, + {Level: loggo.ERROR, Module: "test", Message: "an error"}, + {Level: loggo.WARNING, Module: "test", Message: "a warning message"}, + {Level: loggo.INFO, Module: "test", Message: "an info message"}, + {Level: loggo.DEBUG, Module: "test", Message: "a debug message"}, + {Level: loggo.TRACE, Module: "test", Message: "a trace message"}, + } + + checkLogEntries(c, first.Log(), expected) + checkLogEntries(c, second.Log(), expected) + checkLogEntries(c, third.Log(), expected) +} + +type writer struct { + loggo.TestWriter + // The name exists to discriminate writer equality. + name string +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/loggo/entry.go juju-core-2.0~beta15/src/github.com/juju/loggo/entry.go --- juju-core-2.0~beta12/src/github.com/juju/loggo/entry.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/loggo/entry.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,22 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package loggo + +import "time" + +// Entry represents a single log message. +type Entry struct { + // Level is the severity of the log message. + Level Level + // Module is the dotted module name from the logger. + Module string + // Filename is the full path the file that logged the message. + Filename string + // Line is the line number of the Filename. + Line int + // Timestamp is when the log message was created + Timestamp time.Time + // Message is the formatted string from teh log call. + Message string +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/loggo/export_test.go juju-core-2.0~beta15/src/github.com/juju/loggo/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/loggo/export_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/loggo/export_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,20 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package loggo + +// WriterNames returns the names of the context's writers for testing purposes. +func (c *Context) WriterNames() []string { + c.writersMutex.Lock() + defer c.writersMutex.Unlock() + var result []string + for name := range c.writers { + result = append(result, name) + } + return result +} + +func ResetDefaultContext() { + ResetLogging() + DefaultContext().AddWriter(DefaultWriterName, defaultWriter()) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/loggo/formatter.go juju-core-2.0~beta15/src/github.com/juju/loggo/formatter.go --- juju-core-2.0~beta12/src/github.com/juju/loggo/formatter.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/loggo/formatter.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,21 +9,13 @@ "time" ) -// Formatter defines the single method Format, which takes the logging -// information, and converts it to a string. -type Formatter interface { - Format(level Level, module, filename string, line int, timestamp time.Time, message string) string -} - -// DefaultFormatter provides a simple concatenation of all the components. -type DefaultFormatter struct{} - -// Format returns the parameters separated by spaces except for filename and -// line which are separated by a colon. The timestamp is shown to second -// resolution in UTC. -func (*DefaultFormatter) Format(level Level, module, filename string, line int, timestamp time.Time, message string) string { - ts := timestamp.In(time.UTC).Format("2006-01-02 15:04:05") +// DefaultFormatter returns the parameters separated by spaces except for +// filename and line which are separated by a colon. The timestamp is shown +// to second resolution in UTC. For example: +// 2016-07-02 15:04:05 +func DefaultFormatter(entry Entry) string { + ts := entry.Timestamp.In(time.UTC).Format("2006-01-02 15:04:05") // Just get the basename from the filename - filename = filepath.Base(filename) - return fmt.Sprintf("%s %s %s %s:%d %s", ts, level, module, filename, line, message) + filename := filepath.Base(entry.Filename) + return fmt.Sprintf("%s %s %s %s:%d %s", ts, entry.Level, entry.Module, filename, entry.Line, entry.Message) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/loggo/formatter_test.go juju-core-2.0~beta15/src/github.com/juju/loggo/formatter_test.go --- juju-core-2.0~beta12/src/github.com/juju/loggo/formatter_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/loggo/formatter_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -17,9 +17,16 @@ func (*formatterSuite) TestDefaultFormat(c *gc.C) { location, err := time.LoadLocation("UTC") - c.Assert(err, gc.IsNil) testTime := time.Date(2013, 5, 3, 10, 53, 24, 123456, location) - formatter := &loggo.DefaultFormatter{} - formatted := formatter.Format(loggo.WARNING, "test.module", "some/deep/filename", 42, testTime, "hello world!") + c.Assert(err, gc.IsNil) + entry := loggo.Entry{ + Level: loggo.WARNING, + Module: "test.module", + Filename: "some/deep/filename", + Line: 42, + Timestamp: testTime, + Message: "hello world!", + } + formatted := loggo.DefaultFormatter(entry) c.Assert(formatted, gc.Equals, "2013-05-03 10:53:24 WARNING test.module filename:42 hello world!") } diff -Nru juju-core-2.0~beta12/src/github.com/juju/loggo/global.go juju-core-2.0~beta15/src/github.com/juju/loggo/global.go --- juju-core-2.0~beta12/src/github.com/juju/loggo/global.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/loggo/global.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,85 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package loggo + +var ( + defaultContext = newDefaultContxt() +) + +func newDefaultContxt() *Context { + ctx := NewContext(WARNING) + ctx.AddWriter(DefaultWriterName, defaultWriter()) + return ctx +} + +// DefaultContext returns the global default logging context. +func DefaultContext() *Context { + return defaultContext +} + +// LoggerInfo returns information about the configured loggers and their +// logging levels. The information is returned in the format expected by +// ConfigureLoggers. Loggers with UNSPECIFIED level will not +// be included. +func LoggerInfo() string { + return defaultContext.Config().String() +} + +// GetLogger returns a Logger for the given module name, +// creating it and its parents if necessary. +func GetLogger(name string) Logger { + return defaultContext.GetLogger(name) +} + +// ResetLogging iterates through the known modules and sets the levels of all +// to UNSPECIFIED, except for which is set to WARNING. The call also +// removes all writers in the DefaultContext and puts the original default +// writer back as the only writer. +func ResetLogging() { + defaultContext.ResetLoggerLevels() + defaultContext.ResetWriters() +} + +// ResetWriters puts the list of writers back into the initial state. +func ResetWriters() { + defaultContext.ResetWriters() +} + +// ReplaceDefaultWriter is a convenience method that does the equivalent of +// RemoveWriter and then RegisterWriter with the name "default". The previous +// default writer, if any is returned. +func ReplaceDefaultWriter(writer Writer) (Writer, error) { + return defaultContext.ReplaceWriter(DefaultWriterName, writer) +} + +// RegisterWriter adds the writer to the list of writers in the DefaultContext +// that get notified when logging. If there is already a registered writer +// with that name, an error is returned. +func RegisterWriter(name string, writer Writer) error { + return defaultContext.AddWriter(name, writer) +} + +// RemoveWriter removes the Writer identified by 'name' and returns it. +// If the Writer is not found, an error is returned. +func RemoveWriter(name string) (Writer, error) { + return defaultContext.RemoveWriter(name) +} + +// ConfigureLoggers configures loggers according to the given string +// specification, which specifies a set of modules and their associated +// logging levels. Loggers are colon- or semicolon-separated; each +// module is specified as =. White space outside of +// module names and levels is ignored. The root module is specified +// with the name "". +// +// An example specification: +// `=ERROR; foo.bar=WARNING` +func ConfigureLoggers(specification string) error { + config, err := ParseConfigString(specification) + if err != nil { + return err + } + defaultContext.ApplyConfig(config) + return nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/loggo/global_test.go juju-core-2.0~beta15/src/github.com/juju/loggo/global_test.go --- juju-core-2.0~beta12/src/github.com/juju/loggo/global_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/loggo/global_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,87 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package loggo_test + +import ( + "github.com/juju/loggo" + gc "gopkg.in/check.v1" +) + +type GlobalSuite struct{} + +var _ = gc.Suite(&GlobalSuite{}) + +func (*GlobalSuite) SetUpTest(c *gc.C) { + loggo.ResetDefaultContext() +} + +func (*GlobalSuite) TestRootLogger(c *gc.C) { + var root loggo.Logger + + got := loggo.GetLogger("") + + c.Check(got.Name(), gc.Equals, root.Name()) + c.Check(got.LogLevel(), gc.Equals, root.LogLevel()) +} + +func (*GlobalSuite) TestModuleName(c *gc.C) { + logger := loggo.GetLogger("loggo.testing") + c.Check(logger.Name(), gc.Equals, "loggo.testing") +} + +func (*GlobalSuite) TestLevel(c *gc.C) { + logger := loggo.GetLogger("testing") + level := logger.LogLevel() + c.Check(level, gc.Equals, loggo.UNSPECIFIED) +} + +func (*GlobalSuite) TestEffectiveLevel(c *gc.C) { + logger := loggo.GetLogger("testing") + level := logger.EffectiveLogLevel() + c.Check(level, gc.Equals, loggo.WARNING) +} + +func (*GlobalSuite) TestLevelsSharedForSameModule(c *gc.C) { + logger1 := loggo.GetLogger("testing.module") + logger2 := loggo.GetLogger("testing.module") + + logger1.SetLogLevel(loggo.INFO) + c.Assert(logger1.IsInfoEnabled(), gc.Equals, true) + c.Assert(logger2.IsInfoEnabled(), gc.Equals, true) +} + +func (*GlobalSuite) TestModuleLowered(c *gc.C) { + logger1 := loggo.GetLogger("TESTING.MODULE") + logger2 := loggo.GetLogger("Testing") + + c.Assert(logger1.Name(), gc.Equals, "testing.module") + c.Assert(logger2.Name(), gc.Equals, "testing") +} + +func (s *GlobalSuite) TestConfigureLoggers(c *gc.C) { + err := loggo.ConfigureLoggers("testing.module=debug") + c.Assert(err, gc.IsNil) + expected := "=WARNING;testing.module=DEBUG" + c.Assert(loggo.DefaultContext().Config().String(), gc.Equals, expected) + c.Assert(loggo.LoggerInfo(), gc.Equals, expected) +} + +func (*GlobalSuite) TestRegisterWriterExistingName(c *gc.C) { + err := loggo.RegisterWriter("default", &writer{}) + c.Assert(err, gc.ErrorMatches, `context already has a writer named "default"`) +} + +func (*GlobalSuite) TestReplaceDefaultWriter(c *gc.C) { + oldWriter, err := loggo.ReplaceDefaultWriter(&writer{}) + c.Assert(oldWriter, gc.NotNil) + c.Assert(err, gc.IsNil) + c.Assert(loggo.DefaultContext().WriterNames(), gc.DeepEquals, []string{"default"}) +} + +func (*GlobalSuite) TestRemoveWriter(c *gc.C) { + oldWriter, err := loggo.RemoveWriter("default") + c.Assert(oldWriter, gc.NotNil) + c.Assert(err, gc.IsNil) + c.Assert(loggo.DefaultContext().WriterNames(), gc.HasLen, 0) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/loggo/level.go juju-core-2.0~beta15/src/github.com/juju/loggo/level.go --- juju-core-2.0~beta12/src/github.com/juju/loggo/level.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/loggo/level.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,81 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package loggo + +import ( + "strings" + "sync/atomic" +) + +// The severity levels. Higher values are more considered more +// important. +const ( + UNSPECIFIED Level = iota + TRACE + DEBUG + INFO + WARNING + ERROR + CRITICAL +) + +// Level holds a severity level. +type Level uint32 + +// ParseLevel converts a string representation of a logging level to a +// Level. It returns the level and whether it was valid or not. +func ParseLevel(level string) (Level, bool) { + level = strings.ToUpper(level) + switch level { + case "UNSPECIFIED": + return UNSPECIFIED, true + case "TRACE": + return TRACE, true + case "DEBUG": + return DEBUG, true + case "INFO": + return INFO, true + case "WARN", "WARNING": + return WARNING, true + case "ERROR": + return ERROR, true + case "CRITICAL": + return CRITICAL, true + default: + return UNSPECIFIED, false + } +} + +// String implements Stringer. +func (level Level) String() string { + switch level { + case UNSPECIFIED: + return "UNSPECIFIED" + case TRACE: + return "TRACE" + case DEBUG: + return "DEBUG" + case INFO: + return "INFO" + case WARNING: + return "WARNING" + case ERROR: + return "ERROR" + case CRITICAL: + return "CRITICAL" + default: + return "" + } +} + +// get atomically gets the value of the given level. +func (level *Level) get() Level { + return Level(atomic.LoadUint32((*uint32)(level))) +} + +// set atomically sets the value of the receiver +// to the given level. +func (level *Level) set(newLevel Level) { + atomic.StoreUint32((*uint32)(level), uint32(newLevel)) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/loggo/level_test.go juju-core-2.0~beta15/src/github.com/juju/loggo/level_test.go --- juju-core-2.0~beta12/src/github.com/juju/loggo/level_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/loggo/level_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,96 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package loggo_test + +import ( + gc "gopkg.in/check.v1" + + "github.com/juju/loggo" +) + +type LevelSuite struct{} + +var _ = gc.Suite(&LevelSuite{}) + +var parseLevelTests = []struct { + str string + level loggo.Level + fail bool +}{{ + str: "trace", + level: loggo.TRACE, +}, { + str: "TrAce", + level: loggo.TRACE, +}, { + str: "TRACE", + level: loggo.TRACE, +}, { + str: "debug", + level: loggo.DEBUG, +}, { + str: "DEBUG", + level: loggo.DEBUG, +}, { + str: "info", + level: loggo.INFO, +}, { + str: "INFO", + level: loggo.INFO, +}, { + str: "warn", + level: loggo.WARNING, +}, { + str: "WARN", + level: loggo.WARNING, +}, { + str: "warning", + level: loggo.WARNING, +}, { + str: "WARNING", + level: loggo.WARNING, +}, { + str: "error", + level: loggo.ERROR, +}, { + str: "ERROR", + level: loggo.ERROR, +}, { + str: "critical", + level: loggo.CRITICAL, +}, { + str: "not_specified", + fail: true, +}, { + str: "other", + fail: true, +}, { + str: "", + fail: true, +}} + +func (s *LevelSuite) TestParseLevel(c *gc.C) { + for _, test := range parseLevelTests { + level, ok := loggo.ParseLevel(test.str) + c.Assert(level, gc.Equals, test.level) + c.Assert(ok, gc.Equals, !test.fail) + } +} + +var levelStringValueTests = map[loggo.Level]string{ + loggo.UNSPECIFIED: "UNSPECIFIED", + loggo.DEBUG: "DEBUG", + loggo.TRACE: "TRACE", + loggo.INFO: "INFO", + loggo.WARNING: "WARNING", + loggo.ERROR: "ERROR", + loggo.CRITICAL: "CRITICAL", + loggo.Level(42): "", // other values are unknown +} + +func (s *LevelSuite) TestLevelStringValue(c *gc.C) { + for level, str := range levelStringValueTests { + c.Assert(level.String(), gc.Equals, str) + } +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/loggo/logger.go juju-core-2.0~beta15/src/github.com/juju/loggo/logger.go --- juju-core-2.0~beta12/src/github.com/juju/loggo/logger.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/loggo/logger.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,28 +6,9 @@ import ( "fmt" "runtime" - "sort" - "strings" - "sync" - "sync/atomic" "time" ) -// Level holds a severity level. -type Level uint32 - -// The severity levels. Higher values are more considered more -// important. -const ( - UNSPECIFIED Level = iota - TRACE - DEBUG - INFO - WARNING - ERROR - CRITICAL -) - // A Logger represents a logging module. It has an associated logging // level which can be changed; messages of lesser severity will // be dropped. Loggers have a hierarchical relationship - see @@ -39,210 +20,9 @@ impl *module } -type module struct { - name string - level Level - parent *module -} - -// Initially the modules map only contains the root module. -var ( - root = &module{level: WARNING} - modulesMutex sync.Mutex - modules = map[string]*module{ - "": root, - } -) - -func (level Level) String() string { - switch level { - case UNSPECIFIED: - return "UNSPECIFIED" - case TRACE: - return "TRACE" - case DEBUG: - return "DEBUG" - case INFO: - return "INFO" - case WARNING: - return "WARNING" - case ERROR: - return "ERROR" - case CRITICAL: - return "CRITICAL" - } - return "" -} - -// get atomically gets the value of the given level. -func (level *Level) get() Level { - return Level(atomic.LoadUint32((*uint32)(level))) -} - -// set atomically sets the value of the receiver -// to the given level. -func (level *Level) set(newLevel Level) { - atomic.StoreUint32((*uint32)(level), uint32(newLevel)) -} - -// getLoggerInternal assumes that the modulesMutex is locked. -func getLoggerInternal(name string) Logger { - impl, found := modules[name] - if found { - return Logger{impl} - } - parentName := "" - if i := strings.LastIndex(name, "."); i >= 0 { - parentName = name[0:i] - } - parent := getLoggerInternal(parentName).impl - impl = &module{name, UNSPECIFIED, parent} - modules[name] = impl - return Logger{impl} -} - -// GetLogger returns a Logger for the given module name, -// creating it and its parents if necessary. -func GetLogger(name string) Logger { - // Lowercase the module name, and look for it in the modules map. - name = strings.ToLower(name) - modulesMutex.Lock() - defer modulesMutex.Unlock() - return getLoggerInternal(name) -} - -// LoggerInfo returns information about the configured loggers and their logging -// levels. The information is returned in the format expected by -// ConfigureModules. Loggers with UNSPECIFIED level will not -// be included. -func LoggerInfo() string { - output := []string{} - // output in alphabetical order. - keys := []string{} - modulesMutex.Lock() - defer modulesMutex.Unlock() - for key := range modules { - keys = append(keys, key) - } - sort.Strings(keys) - for _, name := range keys { - mod := modules[name] - severity := mod.level.get() - if severity == UNSPECIFIED { - continue - } - output = append(output, fmt.Sprintf("%s=%s", mod.Name(), severity)) - } - return strings.Join(output, ";") -} - -// ParseConfigurationString parses a logger configuration string into a map of -// logger names and their associated log level. This method is provided to -// allow other programs to pre-validate a configuration string rather than -// just calling ConfigureLoggers. -// -// Loggers are colon- or semicolon-separated; each module is specified as -// =. White space outside of module names and levels is -// ignored. The root module is specified with the name "". -// -// As a special case, a log level may be specified on its own. -// This is equivalent to specifying the level of the root module, -// so "DEBUG" is equivalent to `=DEBUG` -// -// An example specification: -// `=ERROR; foo.bar=WARNING` -func ParseConfigurationString(specification string) (map[string]Level, error) { - levels := make(map[string]Level) - if level, ok := ParseLevel(specification); ok { - levels[""] = level - return levels, nil - } - values := strings.FieldsFunc(specification, func(r rune) bool { return r == ';' || r == ':' }) - for _, value := range values { - s := strings.SplitN(value, "=", 2) - if len(s) < 2 { - return nil, fmt.Errorf("logger specification expected '=', found %q", value) - } - name := strings.TrimSpace(s[0]) - levelStr := strings.TrimSpace(s[1]) - if name == "" || levelStr == "" { - return nil, fmt.Errorf("logger specification %q has blank name or level", value) - } - if name == "" { - name = "" - } - level, ok := ParseLevel(levelStr) - if !ok { - return nil, fmt.Errorf("unknown severity level %q", levelStr) - } - levels[name] = level - } - return levels, nil -} - -// ConfigureLoggers configures loggers according to the given string -// specification, which specifies a set of modules and their associated -// logging levels. Loggers are colon- or semicolon-separated; each -// module is specified as =. White space outside of -// module names and levels is ignored. The root module is specified -// with the name "". -// -// An example specification: -// `=ERROR; foo.bar=WARNING` -func ConfigureLoggers(specification string) error { - if specification == "" { - return nil - } - levels, err := ParseConfigurationString(specification) - if err != nil { - return err - } - for name, level := range levels { - GetLogger(name).SetLogLevel(level) - } - return nil -} - -// ResetLogging iterates through the known modules and sets the levels of all -// to UNSPECIFIED, except for which is set to WARNING. -func ResetLoggers() { - modulesMutex.Lock() - defer modulesMutex.Unlock() - for name, module := range modules { - if name == "" { - module.level.set(WARNING) - } else { - module.level.set(UNSPECIFIED) - } - } -} - -// ParseLevel converts a string representation of a logging level to a -// Level. It returns the level and whether it was valid or not. -func ParseLevel(level string) (Level, bool) { - level = strings.ToUpper(level) - switch level { - case "UNSPECIFIED": - return UNSPECIFIED, true - case "TRACE": - return TRACE, true - case "DEBUG": - return DEBUG, true - case "INFO": - return INFO, true - case "WARN", "WARNING": - return WARNING, true - case "ERROR": - return ERROR, true - case "CRITICAL": - return CRITICAL, true - } - return UNSPECIFIED, false -} - func (logger Logger) getModule() *module { if logger.impl == nil { - return root + return defaultContext.root } return logger.impl } @@ -252,32 +32,12 @@ return logger.getModule().Name() } -// LogLevel returns the configured log level of the logger. +// LogLevel returns the configured min log level of the logger. func (logger Logger) LogLevel() Level { - return logger.getModule().level.get() -} - -func (module *module) getEffectiveLogLevel() Level { - // Note: the root module is guaranteed to have a - // specified logging level, so acts as a suitable sentinel - // for this loop. - for { - if level := module.level.get(); level != UNSPECIFIED { - return level - } - module = module.parent - } - panic("unreachable") + return logger.getModule().level } -func (module *module) Name() string { - if module.name == "" { - return "" - } - return module.name -} - -// EffectiveLogLevel returns the effective log level of +// EffectiveLogLevel returns the effective min log level of // the receiver - that is, messages with a lesser severity // level will be discarded. // @@ -293,12 +53,7 @@ // See EffectiveLogLevel for how this affects the // actual messages logged. func (logger Logger) SetLogLevel(level Level) { - module := logger.getModule() - // The root module can't be unspecified. - if module.name == "" && level == UNSPECIFIED { - level = WARNING - } - module.level.set(level) + logger.getModule().setLevel(level) } // Logf logs a printf-formatted message at the given level. @@ -318,10 +73,8 @@ // Note that the writers may also filter out messages that // are less than their registered minimum severity level. func (logger Logger) LogCallf(calldepth int, level Level, message string, args ...interface{}) { - if logger.getModule().getEffectiveLogLevel() > level || - !WillWrite(level) || - level < TRACE || - level > CRITICAL { + module := logger.getModule() + if !module.willWrite(level) { return } // Gather time, and filename, line number. @@ -347,7 +100,13 @@ if len(args) > 0 { formattedMessage = fmt.Sprintf(message, args...) } - writeToWriters(level, logger.impl.name, file, line, now, formattedMessage) + module.write(Entry{ + Level: level, + Filename: file, + Line: line, + Timestamp: now, + Message: formattedMessage, + }) } // Criticalf logs the printf-formatted message at critical level. @@ -383,7 +142,7 @@ // IsLevelEnabled returns whether debugging is enabled // for the given log level. func (logger Logger) IsLevelEnabled(level Level) bool { - return logger.getModule().getEffectiveLogLevel() <= level + return logger.getModule().willWrite(level) } // IsErrorEnabled returns whether debugging is enabled diff -Nru juju-core-2.0~beta12/src/github.com/juju/loggo/logger_test.go juju-core-2.0~beta15/src/github.com/juju/loggo/logger_test.go --- juju-core-2.0~beta12/src/github.com/juju/loggo/logger_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/loggo/logger_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,38 +4,31 @@ package loggo_test import ( - "io/ioutil" - "os" - gc "gopkg.in/check.v1" "github.com/juju/loggo" ) -type loggerSuite struct{} +type LoggerSuite struct{} -var _ = gc.Suite(&loggerSuite{}) +var _ = gc.Suite(&LoggerSuite{}) -func (*loggerSuite) SetUpTest(c *gc.C) { - loggo.ResetLoggers() +func (*LoggerSuite) SetUpTest(c *gc.C) { + loggo.ResetDefaultContext() } -func (*loggerSuite) TestRootLogger(c *gc.C) { +func (s *LoggerSuite) TestRootLogger(c *gc.C) { root := loggo.Logger{} - c.Assert(root.Name(), gc.Equals, "") - c.Assert(root.IsErrorEnabled(), gc.Equals, true) - c.Assert(root.IsWarningEnabled(), gc.Equals, true) - c.Assert(root.IsInfoEnabled(), gc.Equals, false) - c.Assert(root.IsDebugEnabled(), gc.Equals, false) - c.Assert(root.IsTraceEnabled(), gc.Equals, false) -} - -func (*loggerSuite) TestModuleName(c *gc.C) { - logger := loggo.GetLogger("loggo.testing") - c.Assert(logger.Name(), gc.Equals, "loggo.testing") + c.Check(root.Name(), gc.Equals, "") + c.Check(root.LogLevel(), gc.Equals, loggo.WARNING) + c.Check(root.IsErrorEnabled(), gc.Equals, true) + c.Check(root.IsWarningEnabled(), gc.Equals, true) + c.Check(root.IsInfoEnabled(), gc.Equals, false) + c.Check(root.IsDebugEnabled(), gc.Equals, false) + c.Check(root.IsTraceEnabled(), gc.Equals, false) } -func (*loggerSuite) TestSetLevel(c *gc.C) { +func (s *LoggerSuite) TestSetLevel(c *gc.C) { logger := loggo.GetLogger("testing") c.Assert(logger.LogLevel(), gc.Equals, loggo.UNSPECIFIED) @@ -99,16 +92,7 @@ c.Assert(logger.EffectiveLogLevel(), gc.Equals, loggo.WARNING) } -func (*loggerSuite) TestLevelsSharedForSameModule(c *gc.C) { - logger1 := loggo.GetLogger("testing.module") - logger2 := loggo.GetLogger("testing.module") - - logger1.SetLogLevel(loggo.INFO) - c.Assert(logger1.IsInfoEnabled(), gc.Equals, true) - c.Assert(logger2.IsInfoEnabled(), gc.Equals, true) -} - -func (*loggerSuite) TestModuleLowered(c *gc.C) { +func (s *LoggerSuite) TestModuleLowered(c *gc.C) { logger1 := loggo.GetLogger("TESTING.MODULE") logger2 := loggo.GetLogger("Testing") @@ -116,7 +100,7 @@ c.Assert(logger2.Name(), gc.Equals, "testing") } -func (*loggerSuite) TestLevelsInherited(c *gc.C) { +func (s *LoggerSuite) TestLevelsInherited(c *gc.C) { root := loggo.GetLogger("") first := loggo.GetLogger("first") second := loggo.GetLogger("first.second") @@ -153,354 +137,3 @@ c.Assert(second.LogLevel(), gc.Equals, loggo.INFO) c.Assert(second.EffectiveLogLevel(), gc.Equals, loggo.INFO) } - -var parseLevelTests = []struct { - str string - level loggo.Level - fail bool -}{{ - str: "trace", - level: loggo.TRACE, -}, { - str: "TrAce", - level: loggo.TRACE, -}, { - str: "TRACE", - level: loggo.TRACE, -}, { - str: "debug", - level: loggo.DEBUG, -}, { - str: "DEBUG", - level: loggo.DEBUG, -}, { - str: "info", - level: loggo.INFO, -}, { - str: "INFO", - level: loggo.INFO, -}, { - str: "warn", - level: loggo.WARNING, -}, { - str: "WARN", - level: loggo.WARNING, -}, { - str: "warning", - level: loggo.WARNING, -}, { - str: "WARNING", - level: loggo.WARNING, -}, { - str: "error", - level: loggo.ERROR, -}, { - str: "ERROR", - level: loggo.ERROR, -}, { - str: "critical", - level: loggo.CRITICAL, -}, { - str: "not_specified", - fail: true, -}, { - str: "other", - fail: true, -}, { - str: "", - fail: true, -}} - -func (*loggerSuite) TestParseLevel(c *gc.C) { - for _, test := range parseLevelTests { - level, ok := loggo.ParseLevel(test.str) - c.Assert(level, gc.Equals, test.level) - c.Assert(ok, gc.Equals, !test.fail) - } -} - -var levelStringValueTests = map[loggo.Level]string{ - loggo.UNSPECIFIED: "UNSPECIFIED", - loggo.DEBUG: "DEBUG", - loggo.TRACE: "TRACE", - loggo.INFO: "INFO", - loggo.WARNING: "WARNING", - loggo.ERROR: "ERROR", - loggo.CRITICAL: "CRITICAL", - loggo.Level(42): "", // other values are unknown -} - -func (*loggerSuite) TestLevelStringValue(c *gc.C) { - for level, str := range levelStringValueTests { - c.Assert(level.String(), gc.Equals, str) - } -} - -type stack_error struct { - message string - stack []string -} - -func (s *stack_error) Error() string { - return s.message -} - -func (s *stack_error) StackTrace() []string { - return s.stack -} - -func checkLastMessage(c *gc.C, writer *loggo.TestWriter, expected string) { - log := writer.Log() - writer.Clear() - obtained := log[len(log)-1].Message - c.Check(obtained, gc.Equals, expected) -} - -func (*loggerSuite) TestLoggingStrings(c *gc.C) { - writer := &loggo.TestWriter{} - loggo.ReplaceDefaultWriter(writer) - logger := loggo.GetLogger("test") - logger.SetLogLevel(loggo.TRACE) - - logger.Infof("simple") - checkLastMessage(c, writer, "simple") - - logger.Infof("with args %d", 42) - checkLastMessage(c, writer, "with args 42") - - logger.Infof("working 100%") - checkLastMessage(c, writer, "working 100%") - - logger.Infof("missing %s") - checkLastMessage(c, writer, "missing %s") -} - -func (*loggerSuite) TestLocationCapture(c *gc.C) { - writer := &loggo.TestWriter{} - loggo.ReplaceDefaultWriter(writer) - logger := loggo.GetLogger("test") - logger.SetLogLevel(loggo.TRACE) - - logger.Criticalf("critical message") //tag critical-location - logger.Errorf("error message") //tag error-location - logger.Warningf("warning message") //tag warning-location - logger.Infof("info message") //tag info-location - logger.Debugf("debug message") //tag debug-location - logger.Tracef("trace message") //tag trace-location - - log := writer.Log() - tags := []string{ - "critical-location", - "error-location", - "warning-location", - "info-location", - "debug-location", - "trace-location", - } - c.Assert(log, gc.HasLen, len(tags)) - for x := range tags { - assertLocation(c, log[x], tags[x]) - } - -} - -var configureLoggersTests = []struct { - spec string - info string - err string -}{{ - spec: "", - info: "=WARNING", -}, { - spec: "=UNSPECIFIED", - info: "=WARNING", -}, { - spec: "=DEBUG", - info: "=DEBUG", -}, { - spec: "TRACE", - info: "=TRACE", -}, { - spec: "test.module=debug", - info: "=WARNING;test.module=DEBUG", -}, { - spec: "module=info; sub.module=debug; other.module=warning", - info: "=WARNING;module=INFO;other.module=WARNING;sub.module=DEBUG", -}, { - spec: " foo.bar \t\r\n= \t\r\nCRITICAL \t\r\n; \t\r\nfoo \r\t\n = DEBUG", - info: "=WARNING;foo=DEBUG;foo.bar=CRITICAL", -}, { - spec: "foo;bar", - info: "=WARNING", - err: `logger specification expected '=', found "foo"`, -}, { - spec: "=foo", - info: "=WARNING", - err: `logger specification "=foo" has blank name or level`, -}, { - spec: "foo=", - info: "=WARNING", - err: `logger specification "foo=" has blank name or level`, -}, { - spec: "=", - info: "=WARNING", - err: `logger specification "=" has blank name or level`, -}, { - spec: "foo=unknown", - info: "=WARNING", - err: `unknown severity level "unknown"`, -}, { - // Test that nothing is changed even when the - // first part of the specification parses ok. - spec: "module=info; foo=unknown", - info: "=WARNING", - err: `unknown severity level "unknown"`, -}} - -func (*loggerSuite) TestConfigureLoggers(c *gc.C) { - for i, test := range configureLoggersTests { - c.Logf("test %d: %q", i, test.spec) - loggo.ResetLoggers() - err := loggo.ConfigureLoggers(test.spec) - c.Check(loggo.LoggerInfo(), gc.Equals, test.info) - if test.err != "" { - c.Assert(err, gc.ErrorMatches, test.err) - continue - } - c.Assert(err, gc.IsNil) - - // Test that it's idempotent. - err = loggo.ConfigureLoggers(test.spec) - c.Assert(err, gc.IsNil) - c.Assert(loggo.LoggerInfo(), gc.Equals, test.info) - - // Test that calling ConfigureLoggers with the - // output of LoggerInfo works too. - err = loggo.ConfigureLoggers(test.info) - c.Assert(err, gc.IsNil) - c.Assert(loggo.LoggerInfo(), gc.Equals, test.info) - } -} - -type logwriterSuite struct { - logger loggo.Logger - writer *loggo.TestWriter -} - -var _ = gc.Suite(&logwriterSuite{}) - -func (s *logwriterSuite) SetUpTest(c *gc.C) { - loggo.ResetLoggers() - loggo.RemoveWriter("default") - s.writer = &loggo.TestWriter{} - err := loggo.RegisterWriter("test", s.writer, loggo.TRACE) - c.Assert(err, gc.IsNil) - s.logger = loggo.GetLogger("test.writer") - // Make it so the logger itself writes all messages. - s.logger.SetLogLevel(loggo.TRACE) -} - -func (s *logwriterSuite) TearDownTest(c *gc.C) { - loggo.ResetWriters() -} - -func (s *logwriterSuite) TestLogDoesntLogWeirdLevels(c *gc.C) { - s.logger.Logf(loggo.UNSPECIFIED, "message") - c.Assert(s.writer.Log(), gc.HasLen, 0) - - s.logger.Logf(loggo.Level(42), "message") - c.Assert(s.writer.Log(), gc.HasLen, 0) - - s.logger.Logf(loggo.CRITICAL+loggo.Level(1), "message") - c.Assert(s.writer.Log(), gc.HasLen, 0) -} - -func (s *logwriterSuite) TestMessageFormatting(c *gc.C) { - s.logger.Logf(loggo.INFO, "some %s included", "formatting") - log := s.writer.Log() - c.Assert(log, gc.HasLen, 1) - c.Assert(log[0].Message, gc.Equals, "some formatting included") - c.Assert(log[0].Level, gc.Equals, loggo.INFO) -} - -func (s *logwriterSuite) BenchmarkLoggingNoWriters(c *gc.C) { - // No writers - loggo.RemoveWriter("test") - for i := 0; i < c.N; i++ { - s.logger.Warningf("just a simple warning for %d", i) - } -} - -func (s *logwriterSuite) BenchmarkLoggingNoWritersNoFormat(c *gc.C) { - // No writers - loggo.RemoveWriter("test") - for i := 0; i < c.N; i++ { - s.logger.Warningf("just a simple warning") - } -} - -func (s *logwriterSuite) BenchmarkLoggingTestWriters(c *gc.C) { - for i := 0; i < c.N; i++ { - s.logger.Warningf("just a simple warning for %d", i) - } - c.Assert(s.writer.Log, gc.HasLen, c.N) -} - -func setupTempFileWriter(c *gc.C) (logFile *os.File, cleanup func()) { - loggo.RemoveWriter("test") - logFile, err := ioutil.TempFile("", "loggo-test") - c.Assert(err, gc.IsNil) - cleanup = func() { - logFile.Close() - os.Remove(logFile.Name()) - } - writer := loggo.NewSimpleWriter(logFile, &loggo.DefaultFormatter{}) - err = loggo.RegisterWriter("testfile", writer, loggo.TRACE) - c.Assert(err, gc.IsNil) - return -} - -func (s *logwriterSuite) BenchmarkLoggingDiskWriter(c *gc.C) { - logFile, cleanup := setupTempFileWriter(c) - defer cleanup() - msg := "just a simple warning for %d" - for i := 0; i < c.N; i++ { - s.logger.Warningf(msg, i) - } - offset, err := logFile.Seek(0, os.SEEK_CUR) - c.Assert(err, gc.IsNil) - c.Assert((offset > int64(len(msg))*int64(c.N)), gc.Equals, true, - gc.Commentf("Not enough data was written to the log file.")) -} - -func (s *logwriterSuite) BenchmarkLoggingDiskWriterNoMessages(c *gc.C) { - logFile, cleanup := setupTempFileWriter(c) - defer cleanup() - // Change the log level - writer, _, err := loggo.RemoveWriter("testfile") - c.Assert(err, gc.IsNil) - loggo.RegisterWriter("testfile", writer, loggo.WARNING) - msg := "just a simple warning for %d" - for i := 0; i < c.N; i++ { - s.logger.Debugf(msg, i) - } - offset, err := logFile.Seek(0, os.SEEK_CUR) - c.Assert(err, gc.IsNil) - c.Assert(offset, gc.Equals, int64(0), - gc.Commentf("Data was written to the log file.")) -} - -func (s *logwriterSuite) BenchmarkLoggingDiskWriterNoMessagesLogLevel(c *gc.C) { - logFile, cleanup := setupTempFileWriter(c) - defer cleanup() - // Change the log level - s.logger.SetLogLevel(loggo.WARNING) - msg := "just a simple warning for %d" - for i := 0; i < c.N; i++ { - s.logger.Debugf(msg, i) - } - offset, err := logFile.Seek(0, os.SEEK_CUR) - c.Assert(err, gc.IsNil) - c.Assert(offset, gc.Equals, int64(0), - gc.Commentf("Data was written to the log file.")) -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/loggo/logging_test.go juju-core-2.0~beta15/src/github.com/juju/loggo/logging_test.go --- juju-core-2.0~beta12/src/github.com/juju/loggo/logging_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/loggo/logging_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,92 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package loggo_test + +import ( + "time" + + gc "gopkg.in/check.v1" + + "github.com/juju/loggo" +) + +type LoggingSuite struct { + context *loggo.Context + writer *writer + logger loggo.Logger +} + +var _ = gc.Suite(&LoggingSuite{}) + +func (s *LoggingSuite) SetUpTest(c *gc.C) { + s.writer = &writer{} + s.context = loggo.NewContext(loggo.TRACE) + s.context.AddWriter("test", s.writer) + s.logger = s.context.GetLogger("test") +} + +func (s *LoggingSuite) TestLoggingStrings(c *gc.C) { + s.logger.Infof("simple") + s.logger.Infof("with args %d", 42) + s.logger.Infof("working 100%") + s.logger.Infof("missing %s") + + checkLogEntries(c, s.writer.Log(), []loggo.Entry{ + {Level: loggo.INFO, Module: "test", Message: "simple"}, + {Level: loggo.INFO, Module: "test", Message: "with args 42"}, + {Level: loggo.INFO, Module: "test", Message: "working 100%"}, + {Level: loggo.INFO, Module: "test", Message: "missing %s"}, + }) +} + +func (s *LoggingSuite) TestLoggingLimitWarning(c *gc.C) { + s.logger.SetLogLevel(loggo.WARNING) + start := time.Now() + logAllSeverities(s.logger) + end := time.Now() + entries := s.writer.Log() + checkLogEntries(c, entries, []loggo.Entry{ + {Level: loggo.CRITICAL, Module: "test", Message: "something critical"}, + {Level: loggo.ERROR, Module: "test", Message: "an error"}, + {Level: loggo.WARNING, Module: "test", Message: "a warning message"}, + }) + + for _, entry := range entries { + c.Check(entry.Timestamp, Between(start, end)) + } +} + +func (s *LoggingSuite) TestLocationCapture(c *gc.C) { + s.logger.Criticalf("critical message") //tag critical-location + s.logger.Errorf("error message") //tag error-location + s.logger.Warningf("warning message") //tag warning-location + s.logger.Infof("info message") //tag info-location + s.logger.Debugf("debug message") //tag debug-location + s.logger.Tracef("trace message") //tag trace-location + + log := s.writer.Log() + tags := []string{ + "critical-location", + "error-location", + "warning-location", + "info-location", + "debug-location", + "trace-location", + } + c.Assert(log, gc.HasLen, len(tags)) + for x := range tags { + assertLocation(c, log[x], tags[x]) + } +} + +func (s *LoggingSuite) TestLogDoesntLogWeirdLevels(c *gc.C) { + s.logger.Logf(loggo.UNSPECIFIED, "message") + c.Assert(s.writer.Log(), gc.HasLen, 0) + + s.logger.Logf(loggo.Level(42), "message") + c.Assert(s.writer.Log(), gc.HasLen, 0) + + s.logger.Logf(loggo.CRITICAL+loggo.Level(1), "message") + c.Assert(s.writer.Log(), gc.HasLen, 0) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/loggo/Makefile juju-core-2.0~beta15/src/github.com/juju/loggo/Makefile --- juju-core-2.0~beta12/src/github.com/juju/loggo/Makefile 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/loggo/Makefile 2016-08-16 08:56:25.000000000 +0000 @@ -1,7 +1,7 @@ default: check check: - go test && go test -compiler gccgo + go test docs: godoc2md github.com/juju/loggo > README.md diff -Nru juju-core-2.0~beta12/src/github.com/juju/loggo/module.go juju-core-2.0~beta15/src/github.com/juju/loggo/module.go --- juju-core-2.0~beta12/src/github.com/juju/loggo/module.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/loggo/module.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,61 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package loggo + +// Do not change rootName: modules.resolve() will misbehave if it isn't "". +const ( + rootString = "" + defaultRootLevel = WARNING + defaultLevel = UNSPECIFIED +) + +type module struct { + name string + level Level + parent *module + context *Context +} + +// Name returns the module's name. +func (module *module) Name() string { + if module.name == "" { + return rootString + } + return module.name +} + +func (m *module) willWrite(level Level) bool { + if level < TRACE || level > CRITICAL { + return false + } + return level >= m.getEffectiveLogLevel() +} + +func (module *module) getEffectiveLogLevel() Level { + // Note: the root module is guaranteed to have a + // specified logging level, so acts as a suitable sentinel + // for this loop. + for { + if level := module.level.get(); level != UNSPECIFIED { + return level + } + module = module.parent + } + panic("unreachable") +} + +// setLevel sets the severity level of the given module. +// The root module cannot be set to UNSPECIFIED level. +func (module *module) setLevel(level Level) { + // The root module can't be unspecified. + if module.name == "" && level == UNSPECIFIED { + level = WARNING + } + module.level.set(level) +} + +func (m *module) write(entry Entry) { + entry.Module = m.name + m.context.write(entry) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/loggo/package_test.go juju-core-2.0~beta15/src/github.com/juju/loggo/package_test.go --- juju-core-2.0~beta12/src/github.com/juju/loggo/package_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/loggo/package_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,70 +4,11 @@ package loggo_test import ( - "fmt" - "io/ioutil" - "strings" "testing" gc "gopkg.in/check.v1" - - "github.com/juju/loggo" ) func Test(t *testing.T) { gc.TestingT(t) } - -func assertLocation(c *gc.C, msg loggo.TestLogValues, tag string) { - loc := location(tag) - c.Assert(msg.Filename, gc.Equals, loc.file) - c.Assert(msg.Line, gc.Equals, loc.line) -} - -// All this location stuff is to avoid having hard coded line numbers -// in the tests. Any line where as a test writer you want to capture the -// file and line number, add a comment that has `//tag name` as the end of -// the line. The name must be unique across all the tests, and the test -// will panic if it is not. This name is then used to read the actual -// file and line numbers. - -func location(tag string) Location { - loc, ok := tagToLocation[tag] - if !ok { - panic(fmt.Errorf("tag %q not found", tag)) - } - return loc -} - -type Location struct { - file string - line int -} - -func (loc Location) String() string { - return fmt.Sprintf("%s:%d", loc.file, loc.line) -} - -var tagToLocation = make(map[string]Location) - -func setLocationsForTags(filename string) { - data, err := ioutil.ReadFile(filename) - if err != nil { - panic(err) - } - lines := strings.Split(string(data), "\n") - for i, line := range lines { - if j := strings.Index(line, "//tag "); j >= 0 { - tag := line[j+len("//tag "):] - if _, found := tagToLocation[tag]; found { - panic(fmt.Errorf("tag %q already processed previously")) - } - tagToLocation[tag] = Location{file: filename, line: i + 1} - } - } -} - -func init() { - setLocationsForTags("logger_test.go") - setLocationsForTags("writer_test.go") -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/loggo/README.md juju-core-2.0~beta15/src/github.com/juju/loggo/README.md --- juju-core-2.0~beta12/src/github.com/juju/loggo/README.md 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/loggo/README.md 2016-08-16 08:56:25.000000000 +0000 @@ -45,6 +45,13 @@ +## Constants +``` go +const DefaultWriterName = "default" +``` +DefaultWriterName is the name of the writer default writer for +a Context. + ## func ConfigureLoggers @@ -64,19 +71,67 @@ `=ERROR; foo.bar=WARNING` +## func DefaultFormatter +``` go +func DefaultFormatter(entry Entry) string +``` +DefaultFormatter returns the parameters separated by spaces except for +filename and line which are separated by a colon. The timestamp is shown +to second resolution in UTC. + + ## func LoggerInfo ``` go func LoggerInfo() string ``` -LoggerInfo returns information about the configured loggers and their logging -levels. The information is returned in the format expected by -ConfigureModules. Loggers with UNSPECIFIED level will not +LoggerInfo returns information about the configured loggers and their +logging levels. The information is returned in the format expected by +ConfigureLoggers. Loggers with UNSPECIFIED level will not be included. -## func ParseConfigurationString +## func RegisterWriter +``` go +func RegisterWriter(name string, writer Writer) error +``` +RegisterWriter adds the writer to the list of writers to the DefaultContext +that get notified when logging. If there is already a registered writer +with that name, an error is returned. + + +## func ResetLoggers +``` go +func ResetLoggers() +``` +ResetLogging iterates through the known modules and sets the levels of all +to UNSPECIFIED, except for which is set to WARNING. + + +## func ResetWriters +``` go +func ResetWriters() +``` +ResetWriters puts the list of writers back into the initial state. + + + +## type Config +``` go +type Config map[string]Level +``` +Config is a mapping of logger module names to logging severity levels. + + + + + + + + + +### func ParseConfigurationString ``` go -func ParseConfigurationString(specification string) (map[string]Level, error) +func ParseConfigurationString(specification string) (Config, error) ``` ParseConfigurationString parses a logger configuration string into a map of logger names and their associated log level. This method is provided to @@ -97,75 +152,151 @@ `=ERROR; foo.bar=WARNING` -## func RegisterWriter + + +### func (Config) String ``` go -func RegisterWriter(name string, writer Writer, minLevel Level) error +func (c Config) String() string ``` -RegisterWriter adds the writer to the list of writers that get notified -when logging. When registering, the caller specifies the minimum logging -level that will be written, and a name for the writer. If there is already -a registered writer with that name, an error is returned. +String returns a logger configuration string that may be parsed +using ParseLoggersConfig. -## func ResetLoggers + +## type Context ``` go -func ResetLoggers() +type Context struct { + // contains filtered or unexported fields +} ``` -ResetLogging iterates through the known modules and sets the levels of all -to UNSPECIFIED, except for which is set to WARNING. +Context produces loggers for a hierarchy of modules. The context holds +a collection of hierarchical loggers and their writers. -## func ResetWriters + + + + + + + +### func DefaultContext ``` go -func ResetWriters() +func DefaultContext() *Context ``` -ResetWriters puts the list of writers back into the initial state. +DefaultContext returns the global default logging context. + + +### func NewContext +``` go +func NewContext(rootLevel Level, defaultWriter Writer) *Context +``` +NewLoggers returns a new Context with a possible default writer set. +If the root level is UNSPECIFIED, WARNING is used. + + -## func WillWrite +### func (\*Context) AddWriter ``` go -func WillWrite(level Level) bool +func (c *Context) AddWriter(name string, writer Writer) error ``` -WillWrite returns whether there are any writers registered -at or above the given severity level. If it returns -false, a log message at the given level will be discarded. +AddWriter adds an writer to the list to be called for each logging call. +The name cannot be empty, and the writer cannot be nil. If an existing +writer exists with the specified name, an error is returned. -## type DefaultFormatter +### func (\*Context) ApplyConfig ``` go -type DefaultFormatter struct{} +func (c *Context) ApplyConfig(config Config) ``` -DefaultFormatter provides a simple concatenation of all the components. +ApplyConfig configures the logging modules according to the provided config. + + + +### func (\*Context) CompleteConfig +``` go +func (c *Context) CompleteConfig() Config +``` +CompleteConfig returns all the loggers and their defined levels, +even if that level is UNSPECIFIED. + + + +### func (\*Context) Config +``` go +func (c *Context) Config() Config +``` +Config returns the current configuration of the Loggers. Loggers +with UNSPECIFIED level will not be included. + + + +### func (\*Context) GetLogger +``` go +func (c *Context) GetLogger(name string) Logger +``` +GetLogger returns a Logger for the given module name, creating it and +its parents if necessary. + +### func (\*Context) RemoveWriter +``` go +func (c *Context) RemoveWriter(name string) (Writer, error) +``` +RemoveWriter remotes the specified writer. If a writer is not found with +the specified name an error is returned. The writer that was removed is also +returned. +### func (\*Context) ReplaceWriter +``` go +func (c *Context) ReplaceWriter(name string, writer Writer) (Writer, error) +``` +ReplaceWriter is a convenience function that does the equivalent of RemoveWriter +followed by AddWriter with the same name. The replaced writer is returned. +### func (\*Context) ResetLoggerLevels +``` go +func (c *Context) ResetLoggerLevels() +``` +ResetLoggerLevels iterates through the known logging modules and sets the +levels of all to UNSPECIFIED, except for which is set to WARNING. -### func (\*DefaultFormatter) Format +### func (\*Context) ResetWriters ``` go -func (*DefaultFormatter) Format(level Level, module, filename string, line int, timestamp time.Time, message string) string +func (c *Context) ResetWriters() ``` -Format returns the parameters separated by spaces except for filename and -line which are separated by a colon. The timestamp is shown to second -resolution in UTC. +ResetWriters is generally only used in testing and removes all the writers, and +adds back in the default writer if one was specified when the Context was created. -## type Formatter +## type Entry ``` go -type Formatter interface { - Format(level Level, module, filename string, line int, timestamp time.Time, message string) string +type Entry struct { + // Level is the severity of the log message. + Level Level + // Module is the dotted module name from the logger. + Module string + // Filename is the full path the file that logged the message. + Filename string + // Line is the line number of the Filename. + Line int + // Timestamp is when the log message was created + Timestamp time.Time + // Message is the formatted string from teh log call. + Message string } ``` -Formatter defines the single method Format, which takes the logging -information, and converts it to a string. +Entry represents a single log message. @@ -219,6 +350,8 @@ ``` go func (level Level) String() string ``` +String implements Stringer. + ## type Logger @@ -273,7 +406,7 @@ ``` go func (logger Logger) EffectiveLogLevel() Level ``` -EffectiveLogLevel returns the effective log level of +EffectiveLogLevel returns the effective min log level of the receiver - that is, messages with a lesser severity level will be discarded. @@ -371,7 +504,7 @@ ``` go func (logger Logger) LogLevel() Level ``` -LogLevel returns the configured log level of the logger. +LogLevel returns the configured min log level of the logger. @@ -422,29 +555,6 @@ -## type TestLogValues -``` go -type TestLogValues struct { - Level Level - Module string - Filename string - Line int - Timestamp time.Time - Message string -} -``` -TestLogValues represents a single logging call. - - - - - - - - - - - ## type TestWriter ``` go type TestWriter struct { @@ -474,7 +584,7 @@ ### func (\*TestWriter) Log ``` go -func (writer *TestWriter) Log() []TestLogValues +func (writer *TestWriter) Log() []Entry ``` Log returns a copy of the current logged values. @@ -482,7 +592,7 @@ ### func (\*TestWriter) Write ``` go -func (writer *TestWriter) Write(level Level, module, filename string, line int, timestamp time.Time, message string) +func (writer *TestWriter) Write(entry Entry) ``` Write saves the params as members in the TestLogValues struct appended to the Log array. @@ -497,7 +607,7 @@ // generating the log message; the time stamp holds // the time the log message was generated, and // message holds the log message itself. - Write(level Level, name, filename string, line int, timestamp time.Time, message string) + Write(entry Entry) } ``` Writer is implemented by any recipient of log messages. @@ -510,9 +620,17 @@ +### func NewMinimumLevelWriter +``` go +func NewMinimumLevelWriter(writer Writer, minLevel Level) Writer +``` +NewMinLevelWriter returns a Writer that will only pass on the Write calls +to the provided writer if the log level is at or above the specified minimul level. + + ### func NewSimpleWriter ``` go -func NewSimpleWriter(writer io.Writer, formatter Formatter) Writer +func NewSimpleWriter(writer io.Writer, formatter func(entry Entry) string) Writer ``` NewSimpleWriter returns a new writer that writes log messages to the given io.Writer formatting the @@ -521,7 +639,7 @@ ### func RemoveWriter ``` go -func RemoveWriter(name string) (Writer, Level, error) +func RemoveWriter(name string) (Writer, error) ``` RemoveWriter removes the Writer identified by 'name' and returns it. If the Writer is not found, an error is returned. @@ -545,4 +663,4 @@ - - - -Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md) +Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md) \ No newline at end of file diff -Nru juju-core-2.0~beta12/src/github.com/juju/loggo/testwriter.go juju-core-2.0~beta15/src/github.com/juju/loggo/testwriter.go --- juju-core-2.0~beta12/src/github.com/juju/loggo/testwriter.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/loggo/testwriter.go 2016-08-16 08:56:25.000000000 +0000 @@ -6,32 +6,21 @@ import ( "path" "sync" - "time" ) -// TestLogValues represents a single logging call. -type TestLogValues struct { - Level Level - Module string - Filename string - Line int - Timestamp time.Time - Message string -} - // TestWriter is a useful Writer for testing purposes. Each component of the // logging message is stored in the Log array. type TestWriter struct { mu sync.Mutex - log []TestLogValues + log []Entry } // Write saves the params as members in the TestLogValues struct appended to the Log array. -func (writer *TestWriter) Write(level Level, module, filename string, line int, timestamp time.Time, message string) { +func (writer *TestWriter) Write(entry Entry) { writer.mu.Lock() defer writer.mu.Unlock() - writer.log = append(writer.log, - TestLogValues{level, module, path.Base(filename), line, timestamp, message}) + entry.Filename = path.Base(entry.Filename) + writer.log = append(writer.log, entry) } // Clear removes any saved log messages. @@ -42,10 +31,10 @@ } // Log returns a copy of the current logged values. -func (writer *TestWriter) Log() []TestLogValues { +func (writer *TestWriter) Log() []Entry { writer.mu.Lock() defer writer.mu.Unlock() - v := make([]TestLogValues, len(writer.log)) + v := make([]Entry, len(writer.log)) copy(v, writer.log) return v } diff -Nru juju-core-2.0~beta12/src/github.com/juju/loggo/util_test.go juju-core-2.0~beta15/src/github.com/juju/loggo/util_test.go --- juju-core-2.0~beta12/src/github.com/juju/loggo/util_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/loggo/util_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,68 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package loggo_test + +import ( + "fmt" + "io/ioutil" + "strings" + + "github.com/juju/loggo" + + gc "gopkg.in/check.v1" +) + +func init() { + setLocationsForTags("logging_test.go") + setLocationsForTags("writer_test.go") +} + +func assertLocation(c *gc.C, msg loggo.Entry, tag string) { + loc := location(tag) + c.Assert(msg.Filename, gc.Equals, loc.file) + c.Assert(msg.Line, gc.Equals, loc.line) +} + +// All this location stuff is to avoid having hard coded line numbers +// in the tests. Any line where as a test writer you want to capture the +// file and line number, add a comment that has `//tag name` as the end of +// the line. The name must be unique across all the tests, and the test +// will panic if it is not. This name is then used to read the actual +// file and line numbers. + +func location(tag string) Location { + loc, ok := tagToLocation[tag] + if !ok { + panic(fmt.Errorf("tag %q not found", tag)) + } + return loc +} + +type Location struct { + file string + line int +} + +func (loc Location) String() string { + return fmt.Sprintf("%s:%d", loc.file, loc.line) +} + +var tagToLocation = make(map[string]Location) + +func setLocationsForTags(filename string) { + data, err := ioutil.ReadFile(filename) + if err != nil { + panic(err) + } + lines := strings.Split(string(data), "\n") + for i, line := range lines { + if j := strings.Index(line, "//tag "); j >= 0 { + tag := line[j+len("//tag "):] + if _, found := tagToLocation[tag]; found { + panic(fmt.Errorf("tag %q already processed previously")) + } + tagToLocation[tag] = Location{file: filename, line: i + 1} + } + } +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/loggo/writer.go juju-core-2.0~beta15/src/github.com/juju/loggo/writer.go --- juju-core-2.0~beta12/src/github.com/juju/loggo/writer.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/loggo/writer.go 2016-08-16 08:56:25.000000000 +0000 @@ -7,146 +7,64 @@ "fmt" "io" "os" - "sync" - "time" ) +// DefaultWriterName is the name of the default writer for +// a Context. +const DefaultWriterName = "default" + // Writer is implemented by any recipient of log messages. type Writer interface { - // Write writes a message to the Writer with the given - // level and module name. The filename and line hold - // the file name and line number of the code that is - // generating the log message; the time stamp holds - // the time the log message was generated, and - // message holds the log message itself. - Write(level Level, name, filename string, line int, timestamp time.Time, message string) -} - -type registeredWriter struct { - writer Writer - level Level -} - -// defaultName is the name of a writer that is registered -// by default that writes to stderr. -const defaultName = "default" - -var ( - writerMutex sync.Mutex - writers = map[string]*registeredWriter{ - defaultName: ®isteredWriter{ - writer: NewSimpleWriter(os.Stderr, &DefaultFormatter{}), - level: TRACE, - }, - } - globalMinLevel = TRACE -) - -// ResetWriters puts the list of writers back into the initial state. -func ResetWriters() { - writerMutex.Lock() - defer writerMutex.Unlock() - writers = map[string]*registeredWriter{ - "default": ®isteredWriter{ - writer: NewSimpleWriter(os.Stderr, &DefaultFormatter{}), - level: TRACE, - }, - } - findMinLevel() -} - -// ReplaceDefaultWriter is a convenience method that does the equivalent of -// RemoveWriter and then RegisterWriter with the name "default". The previous -// default writer, if any is returned. -func ReplaceDefaultWriter(writer Writer) (Writer, error) { - if writer == nil { - return nil, fmt.Errorf("Writer cannot be nil") - } - writerMutex.Lock() - defer writerMutex.Unlock() - reg, found := writers[defaultName] - if !found { - return nil, fmt.Errorf("there is no %q writer", defaultName) - } - oldWriter := reg.writer - reg.writer = writer - return oldWriter, nil - -} - -// RegisterWriter adds the writer to the list of writers that get notified -// when logging. When registering, the caller specifies the minimum logging -// level that will be written, and a name for the writer. If there is already -// a registered writer with that name, an error is returned. -func RegisterWriter(name string, writer Writer, minLevel Level) error { - if writer == nil { - return fmt.Errorf("Writer cannot be nil") - } - writerMutex.Lock() - defer writerMutex.Unlock() - if _, found := writers[name]; found { - return fmt.Errorf("there is already a Writer registered with the name %q", name) - } - writers[name] = ®isteredWriter{writer: writer, level: minLevel} - findMinLevel() - return nil + // Write writes a message to the Writer with the given level and module + // name. The filename and line hold the file name and line number of the + // code that is generating the log message; the time stamp holds the time + // the log message was generated, and message holds the log message + // itself. + Write(entry Entry) } -// RemoveWriter removes the Writer identified by 'name' and returns it. -// If the Writer is not found, an error is returned. -func RemoveWriter(name string) (Writer, Level, error) { - writerMutex.Lock() - defer writerMutex.Unlock() - registered, found := writers[name] - if !found { - return nil, UNSPECIFIED, fmt.Errorf("Writer %q is not registered", name) +// NewMinLevelWriter returns a Writer that will only pass on the Write calls +// to the provided writer if the log level is at or above the specified +// minimum level. +func NewMinimumLevelWriter(writer Writer, minLevel Level) Writer { + return &minLevelWriter{ + writer: writer, + level: minLevel, } - delete(writers, name) - findMinLevel() - return registered.writer, registered.level, nil } -func findMinLevel() { - // We assume the lock is already held - minLevel := CRITICAL - for _, registered := range writers { - if registered.level < minLevel { - minLevel = registered.level - } - } - globalMinLevel.set(minLevel) +type minLevelWriter struct { + writer Writer + level Level } -// WillWrite returns whether there are any writers registered -// at or above the given severity level. If it returns -// false, a log message at the given level will be discarded. -func WillWrite(level Level) bool { - return level >= globalMinLevel.get() -} - -func writeToWriters(level Level, module, filename string, line int, timestamp time.Time, message string) { - writerMutex.Lock() - defer writerMutex.Unlock() - for _, registered := range writers { - if level >= registered.level { - registered.writer.Write(level, module, filename, line, timestamp, message) - } +// Write writes the log record. +func (w minLevelWriter) Write(entry Entry) { + if entry.Level < w.level { + return } + w.writer.Write(entry) } type simpleWriter struct { writer io.Writer - formatter Formatter + formatter func(entry Entry) string } -// NewSimpleWriter returns a new writer that writes -// log messages to the given io.Writer formatting the -// messages with the given formatter. -func NewSimpleWriter(writer io.Writer, formatter Formatter) Writer { +// NewSimpleWriter returns a new writer that writes log messages to the given +// io.Writer formatting the messages with the given formatter. +func NewSimpleWriter(writer io.Writer, formatter func(entry Entry) string) Writer { + if formatter == nil { + formatter = DefaultFormatter + } return &simpleWriter{writer, formatter} } -func (simple *simpleWriter) Write(level Level, module, filename string, line int, timestamp time.Time, message string) { - logLine := simple.formatter.Format(level, module, filename, line, timestamp, message) +func (simple *simpleWriter) Write(entry Entry) { + logLine := simple.formatter(entry) fmt.Fprintln(simple.writer, logLine) } + +func defaultWriter() Writer { + return NewSimpleWriter(os.Stderr, DefaultFormatter) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/loggo/writer_test.go juju-core-2.0~beta15/src/github.com/juju/loggo/writer_test.go --- juju-core-2.0~beta12/src/github.com/juju/loggo/writer_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/loggo/writer_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,7 +4,7 @@ package loggo_test import ( - "fmt" + "bytes" "time" gc "gopkg.in/check.v1" @@ -12,233 +12,26 @@ "github.com/juju/loggo" ) -type writerBasicsSuite struct{} +type SimpleWriterSuite struct{} -var _ = gc.Suite(&writerBasicsSuite{}) +var _ = gc.Suite(&SimpleWriterSuite{}) -func (s *writerBasicsSuite) TearDownTest(c *gc.C) { - loggo.ResetWriters() -} - -func (*writerBasicsSuite) TestRemoveDefaultWriter(c *gc.C) { - defaultWriter, level, err := loggo.RemoveWriter("default") - c.Assert(err, gc.IsNil) - c.Assert(level, gc.Equals, loggo.TRACE) - c.Assert(defaultWriter, gc.NotNil) - - // Trying again fails. - defaultWriter, level, err = loggo.RemoveWriter("default") - c.Assert(err, gc.ErrorMatches, `Writer "default" is not registered`) - c.Assert(level, gc.Equals, loggo.UNSPECIFIED) - c.Assert(defaultWriter, gc.IsNil) -} - -func (*writerBasicsSuite) TestRegisterWriterExistingName(c *gc.C) { - err := loggo.RegisterWriter("default", &loggo.TestWriter{}, loggo.INFO) - c.Assert(err, gc.ErrorMatches, `there is already a Writer registered with the name "default"`) -} - -func (*writerBasicsSuite) TestRegisterNilWriter(c *gc.C) { - err := loggo.RegisterWriter("nil", nil, loggo.INFO) - c.Assert(err, gc.ErrorMatches, `Writer cannot be nil`) -} - -func (*writerBasicsSuite) TestRegisterWriterTypedNil(c *gc.C) { - // If the interface is a typed nil, we have to trust the user. - var writer *loggo.TestWriter - err := loggo.RegisterWriter("nil", writer, loggo.INFO) - c.Assert(err, gc.IsNil) -} - -func (*writerBasicsSuite) TestReplaceDefaultWriter(c *gc.C) { - oldWriter, err := loggo.ReplaceDefaultWriter(&loggo.TestWriter{}) - c.Assert(oldWriter, gc.NotNil) - c.Assert(err, gc.IsNil) -} - -func (*writerBasicsSuite) TestReplaceDefaultWriterWithNil(c *gc.C) { - oldWriter, err := loggo.ReplaceDefaultWriter(nil) - c.Assert(oldWriter, gc.IsNil) - c.Assert(err, gc.ErrorMatches, "Writer cannot be nil") -} - -func (*writerBasicsSuite) TestReplaceDefaultWriterNoDefault(c *gc.C) { - loggo.RemoveWriter("default") - oldWriter, err := loggo.ReplaceDefaultWriter(&loggo.TestWriter{}) - c.Assert(oldWriter, gc.IsNil) - c.Assert(err, gc.ErrorMatches, `there is no "default" writer`) -} - -func (s *writerBasicsSuite) TestWillWrite(c *gc.C) { - // By default, the root logger watches TRACE messages - c.Assert(loggo.WillWrite(loggo.TRACE), gc.Equals, true) - // Note: ReplaceDefaultWriter doesn't let us change the default log - // level :( - writer, _, err := loggo.RemoveWriter("default") - c.Assert(err, gc.IsNil) - c.Assert(writer, gc.NotNil) - err = loggo.RegisterWriter("default", writer, loggo.CRITICAL) - c.Assert(err, gc.IsNil) - c.Assert(loggo.WillWrite(loggo.TRACE), gc.Equals, false) - c.Assert(loggo.WillWrite(loggo.DEBUG), gc.Equals, false) - c.Assert(loggo.WillWrite(loggo.INFO), gc.Equals, false) - c.Assert(loggo.WillWrite(loggo.WARNING), gc.Equals, false) - c.Assert(loggo.WillWrite(loggo.CRITICAL), gc.Equals, true) -} - -type writerSuite struct { - logger loggo.Logger -} - -var _ = gc.Suite(&writerSuite{}) - -func (s *writerSuite) SetUpTest(c *gc.C) { - loggo.ResetLoggers() - loggo.RemoveWriter("default") - s.logger = loggo.GetLogger("test.writer") - // Make it so the logger itself writes all messages. - s.logger.SetLogLevel(loggo.TRACE) -} - -func (s *writerSuite) TearDownTest(c *gc.C) { - loggo.ResetWriters() -} - -func (s *writerSuite) TearDownSuite(c *gc.C) { - loggo.ResetLoggers() -} - -func (s *writerSuite) TestWritingCapturesFileAndLineAndModule(c *gc.C) { - writer := &loggo.TestWriter{} - err := loggo.RegisterWriter("test", writer, loggo.INFO) - c.Assert(err, gc.IsNil) - - s.logger.Infof("Info message") //tag capture - - log := writer.Log() - c.Assert(log, gc.HasLen, 1) - assertLocation(c, log[0], "capture") - c.Assert(log[0].Module, gc.Equals, "test.writer") -} - -func (s *writerSuite) TestWritingLimitWarning(c *gc.C) { - writer := &loggo.TestWriter{} - err := loggo.RegisterWriter("test", writer, loggo.WARNING) - c.Assert(err, gc.IsNil) - - start := time.Now() - s.logger.Criticalf("Something critical.") - s.logger.Errorf("An error.") - s.logger.Warningf("A warning message") - s.logger.Infof("Info message") - s.logger.Tracef("Trace the function") - end := time.Now() - - log := writer.Log() - c.Assert(log, gc.HasLen, 3) - c.Assert(log[0].Level, gc.Equals, loggo.CRITICAL) - c.Assert(log[0].Message, gc.Equals, "Something critical.") - c.Assert(log[0].Timestamp, Between(start, end)) - - c.Assert(log[1].Level, gc.Equals, loggo.ERROR) - c.Assert(log[1].Message, gc.Equals, "An error.") - c.Assert(log[1].Timestamp, Between(start, end)) - - c.Assert(log[2].Level, gc.Equals, loggo.WARNING) - c.Assert(log[2].Message, gc.Equals, "A warning message") - c.Assert(log[2].Timestamp, Between(start, end)) -} - -func (s *writerSuite) TestWritingLimitTrace(c *gc.C) { - writer := &loggo.TestWriter{} - err := loggo.RegisterWriter("test", writer, loggo.TRACE) - c.Assert(err, gc.IsNil) - - start := time.Now() - s.logger.Criticalf("Something critical.") - s.logger.Errorf("An error.") - s.logger.Warningf("A warning message") - s.logger.Infof("Info message") - s.logger.Tracef("Trace the function") - end := time.Now() - - log := writer.Log() - c.Assert(log, gc.HasLen, 5) - c.Assert(log[0].Level, gc.Equals, loggo.CRITICAL) - c.Assert(log[0].Message, gc.Equals, "Something critical.") - c.Assert(log[0].Timestamp, Between(start, end)) - - c.Assert(log[1].Level, gc.Equals, loggo.ERROR) - c.Assert(log[1].Message, gc.Equals, "An error.") - c.Assert(log[1].Timestamp, Between(start, end)) - - c.Assert(log[2].Level, gc.Equals, loggo.WARNING) - c.Assert(log[2].Message, gc.Equals, "A warning message") - c.Assert(log[2].Timestamp, Between(start, end)) - - c.Assert(log[3].Level, gc.Equals, loggo.INFO) - c.Assert(log[3].Message, gc.Equals, "Info message") - c.Assert(log[3].Timestamp, Between(start, end)) - - c.Assert(log[4].Level, gc.Equals, loggo.TRACE) - c.Assert(log[4].Message, gc.Equals, "Trace the function") - c.Assert(log[4].Timestamp, Between(start, end)) -} - -func (s *writerSuite) TestMultipleWriters(c *gc.C) { - errorWriter := &loggo.TestWriter{} - err := loggo.RegisterWriter("error", errorWriter, loggo.ERROR) - c.Assert(err, gc.IsNil) - warningWriter := &loggo.TestWriter{} - err = loggo.RegisterWriter("warning", warningWriter, loggo.WARNING) - c.Assert(err, gc.IsNil) - infoWriter := &loggo.TestWriter{} - err = loggo.RegisterWriter("info", infoWriter, loggo.INFO) - c.Assert(err, gc.IsNil) - traceWriter := &loggo.TestWriter{} - err = loggo.RegisterWriter("trace", traceWriter, loggo.TRACE) - c.Assert(err, gc.IsNil) - - s.logger.Errorf("An error.") - s.logger.Warningf("A warning message") - s.logger.Infof("Info message") - s.logger.Tracef("Trace the function") - - c.Assert(errorWriter.Log(), gc.HasLen, 1) - c.Assert(warningWriter.Log(), gc.HasLen, 2) - c.Assert(infoWriter.Log(), gc.HasLen, 3) - c.Assert(traceWriter.Log(), gc.HasLen, 4) -} - -func Between(start, end time.Time) gc.Checker { - if end.Before(start) { - return &betweenChecker{end, start} +func (s *SimpleWriterSuite) TestNewSimpleWriter(c *gc.C) { + now := time.Now() + formatter := func(entry loggo.Entry) string { + return "<< " + entry.Message + " >>" } - return &betweenChecker{start, end} -} - -type betweenChecker struct { - start, end time.Time -} + buf := &bytes.Buffer{} -func (checker *betweenChecker) Info() *gc.CheckerInfo { - info := gc.CheckerInfo{ - Name: "Between", - Params: []string{"obtained"}, - } - return &info -} + writer := loggo.NewSimpleWriter(buf, formatter) + writer.Write(loggo.Entry{ + Level: loggo.INFO, + Module: "test", + Filename: "somefile.go", + Line: 12, + Timestamp: now, + Message: "a message", + }) -func (checker *betweenChecker) Check(params []interface{}, names []string) (result bool, error string) { - when, ok := params[0].(time.Time) - if !ok { - return false, "obtained value type must be time.Time" - } - if when.Before(checker.start) { - return false, fmt.Sprintf("obtained time %q is before start time %q", when, checker.start) - } - if when.After(checker.end) { - return false, fmt.Sprintf("obtained time %q is after end time %q", when, checker.end) - } - return true, "" + c.Check(buf.String(), gc.Equals, "<< a message >>\n") } diff -Nru juju-core-2.0~beta12/src/github.com/juju/romulus/api/terms/api.go juju-core-2.0~beta15/src/github.com/juju/romulus/api/terms/api.go --- juju-core-2.0~beta12/src/github.com/juju/romulus/api/terms/api.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/romulus/api/terms/api.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,227 +0,0 @@ -// Copyright 2016 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -// Package terms contains the terms service API client. -package terms - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "net/http" - "net/url" - "os" - "time" - - "github.com/juju/errors" - "gopkg.in/macaroon-bakery.v1/httpbakery" -) - -var BaseURL = "https://api.jujucharms.com/terms/v1" - -// CheckAgreementsRequest holds a slice of terms and the /v1/agreement -// endpoint will check if the user has agreed to the specified terms -// and return a slice of terms the user has not agreed to yet. -type CheckAgreementsRequest struct { - Terms []string -} - -// GetTermsResponse holds the response of the GetTerms call. -type GetTermsResponse struct { - Name string `json:"name"` - Title string `json:"title"` - Revision int `json:"revision"` - CreatedOn time.Time `json:"created-on"` - Content string `json:"content"` -} - -// SaveAgreementResponses holds the response of the SaveAgreement -// call. -type SaveAgreementResponses struct { - Agreements []AgreementResponse `json:"agreements"` -} - -// AgreementResponse holds the a single agreement made by -// the user to a specific revision of terms and conditions -// document. -type AgreementResponse struct { - User string `json:"user"` - Term string `json:"term"` - Revision int `json:"revision"` - CreatedOn time.Time `json:"created-on"` -} - -// SaveAgreements holds the parameters for creating new -// user agreements to one or more specific revisions of terms. -type SaveAgreements struct { - Agreements []SaveAgreement `json:"agreements"` -} - -// SaveAgreement holds the parameters for creating a new -// user agreement to a specific revision of terms. -type SaveAgreement struct { - TermName string `json:"termname"` - TermRevision int `json:"termrevision"` -} - -// Client defines method needed for the Terms Service CLI -// commands. -type Client interface { - GetUnsignedTerms(p *CheckAgreementsRequest) ([]GetTermsResponse, error) - SaveAgreement(p *SaveAgreements) (*SaveAgreementResponses, error) - GetUsersAgreements() ([]AgreementResponse, error) -} - -var _ Client = (*client)(nil) - -type httpClient interface { - Do(*http.Request) (*http.Response, error) - DoWithBody(req *http.Request, body io.ReadSeeker) (*http.Response, error) -} - -// client is the implementation of the Client interface. -type client struct { - client httpClient -} - -// ClientOption defines a function which configures a Client. -type ClientOption func(h *client) error - -// HTTPClient returns a function that sets the http client used by the API -// (e.g. if we want to use TLS). -func HTTPClient(c httpClient) func(h *client) error { - return func(h *client) error { - h.client = c - return nil - } -} - -// NewClient returns a new client for plan management. -func NewClient(options ...ClientOption) (Client, error) { - c := &client{ - client: httpbakery.NewClient(), - } - - for _, option := range options { - err := option(c) - if err != nil { - return nil, errors.Trace(err) - } - } - - return c, nil -} - -func getBaseURL() string { - baseURL := BaseURL - if termsURL := os.Getenv("JUJU_TERMS"); termsURL != "" { - baseURL = termsURL - } - return baseURL -} - -// GetUnsignedTerms returns the default plan for the specified charm. -func (c *client) GetUnsignedTerms(p *CheckAgreementsRequest) ([]GetTermsResponse, error) { - values := url.Values{} - for _, t := range p.Terms { - values.Add("Terms", t) - } - u := fmt.Sprintf("%s/agreement?%s", getBaseURL(), values.Encode()) - req, err := http.NewRequest("GET", u, nil) - if err != nil { - return nil, errors.Trace(err) - } - req.Header.Set("Content-Type", "application/json") - response, err := c.client.Do(req) - if err != nil { - return nil, errors.Trace(err) - } - if response.StatusCode != http.StatusOK { - b, err := ioutil.ReadAll(response.Body) - if err != nil { - return nil, errors.Errorf("failed to get unsigned terms: %v", response.Status) - } - return nil, errors.Errorf("failed to get unsigned terms: %v: %s", response.Status, string(b)) - } - defer discardClose(response) - var results []GetTermsResponse - dec := json.NewDecoder(response.Body) - err = dec.Decode(&results) - if err != nil { - return nil, errors.Trace(err) - } - return results, nil -} - -// SaveAgreements saves a user agreement to the specificed terms document. -func (c *client) SaveAgreement(p *SaveAgreements) (*SaveAgreementResponses, error) { - u := fmt.Sprintf("%s/agreement", getBaseURL()) - req, err := http.NewRequest("POST", u, nil) - if err != nil { - return nil, errors.Trace(err) - } - req.Header.Set("Content-Type", "application/json") - data, err := json.Marshal(p.Agreements) - if err != nil { - return nil, errors.Trace(err) - } - response, err := c.client.DoWithBody(req, bytes.NewReader(data)) - if err != nil { - return nil, errors.Trace(err) - } - if response.StatusCode != http.StatusOK { - b, err := ioutil.ReadAll(response.Body) - if err != nil { - return nil, errors.Errorf("failed to save agreement: %v", response.Status) - } - return nil, errors.Errorf("failed to save agreement: %v: %s", response.Status, string(b)) - } - defer discardClose(response) - var results SaveAgreementResponses - dec := json.NewDecoder(response.Body) - err = dec.Decode(&results) - if err != nil { - return nil, errors.Trace(err) - } - return &results, nil -} - -// GetUsersAgreements returns all agreements the user has made. -func (c *client) GetUsersAgreements() ([]AgreementResponse, error) { - u := fmt.Sprintf("%s/agreements", getBaseURL()) - req, err := http.NewRequest("GET", u, nil) - if err != nil { - return nil, errors.Trace(err) - } - response, err := c.client.Do(req) - if err != nil { - return nil, errors.Trace(err) - } - if response.StatusCode != http.StatusOK { - b, err := ioutil.ReadAll(response.Body) - if err != nil { - return nil, errors.Errorf("failed to get signed agreements: %v", response.Status) - } - return nil, errors.Errorf("failed to get signed agreements: %v: %s", response.Status, string(b)) - } - defer discardClose(response) - - var results []AgreementResponse - dec := json.NewDecoder(response.Body) - err = dec.Decode(&results) - if err != nil { - return nil, errors.Trace(err) - } - return results, nil -} - -// discardClose reads any remaining data from the response body and closes it. -func discardClose(response *http.Response) { - if response == nil || response.Body == nil { - return - } - io.Copy(ioutil.Discard, response.Body) - response.Body.Close() -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/romulus/api/terms/api_test.go juju-core-2.0~beta15/src/github.com/juju/romulus/api/terms/api_test.go --- juju-core-2.0~beta12/src/github.com/juju/romulus/api/terms/api_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/romulus/api/terms/api_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,237 +0,0 @@ -// Copyright 2016 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -// Package terms defines the terms service API. -package terms_test - -import ( - "bytes" - "encoding/json" - "io" - "io/ioutil" - "net/http" - stdtesting "testing" - "time" - - "github.com/juju/testing" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/romulus/api/terms" -) - -type apiSuite struct { - client terms.Client - httpClient *mockHttpClient -} - -func Test(t *stdtesting.T) { - gc.TestingT(t) -} - -var _ = gc.Suite(&apiSuite{}) - -func (s *apiSuite) SetUpTest(c *gc.C) { - s.httpClient = &mockHttpClient{} - var err error - s.client, err = terms.NewClient(terms.HTTPClient(s.httpClient)) - c.Assert(err, jc.ErrorIsNil) -} - -func (s *apiSuite) TestSignedAgreements(c *gc.C) { - t := time.Now().UTC() - s.httpClient.status = http.StatusOK - s.httpClient.SetBody(c, []terms.AgreementResponse{ - { - User: "test-user", - Term: "hello-world-terms", - Revision: 1, - CreatedOn: t, - }, - { - User: "test-user", - Term: "hello-universe-terms", - Revision: 42, - CreatedOn: t, - }, - }) - signedAgreements, err := s.client.GetUsersAgreements() - c.Assert(err, jc.ErrorIsNil) - c.Assert(signedAgreements, gc.HasLen, 2) - c.Assert(signedAgreements[0].User, gc.Equals, "test-user") - c.Assert(signedAgreements[0].Term, gc.Equals, "hello-world-terms") - c.Assert(signedAgreements[0].Revision, gc.Equals, 1) - c.Assert(signedAgreements[0].CreatedOn, gc.DeepEquals, t) - c.Assert(signedAgreements[1].User, gc.Equals, "test-user") - c.Assert(signedAgreements[1].Term, gc.Equals, "hello-universe-terms") - c.Assert(signedAgreements[1].Revision, gc.Equals, 42) - c.Assert(signedAgreements[1].CreatedOn, gc.DeepEquals, t) -} - -func (s *apiSuite) TestUnsignedTerms(c *gc.C) { - s.httpClient.status = http.StatusOK - s.httpClient.SetBody(c, []terms.GetTermsResponse{ - { - Name: "hello-world-terms", - Revision: 1, - Content: "terms doc content", - }, - { - Name: "hello-universe-terms", - Revision: 1, - Content: "universal terms doc content", - }, - }) - missingAgreements, err := s.client.GetUnsignedTerms(&terms.CheckAgreementsRequest{ - Terms: []string{ - "hello-world-terms/1", - "hello-universe-terms/1", - }, - }) - c.Assert(err, jc.ErrorIsNil) - c.Assert(missingAgreements, gc.HasLen, 2) - c.Assert(missingAgreements[0].Name, gc.Equals, "hello-world-terms") - c.Assert(missingAgreements[0].Revision, gc.Equals, 1) - c.Assert(missingAgreements[0].Content, gc.Equals, "terms doc content") - c.Assert(missingAgreements[1].Name, gc.Equals, "hello-universe-terms") - c.Assert(missingAgreements[1].Revision, gc.Equals, 1) - c.Assert(missingAgreements[1].Content, gc.Equals, "universal terms doc content") - s.httpClient.SetBody(c, terms.SaveAgreementResponses{ - Agreements: []terms.AgreementResponse{{ - User: "test-user", - Term: "hello-world-terms", - Revision: 1, - }}}) - - p1 := &terms.SaveAgreements{ - Agreements: []terms.SaveAgreement{{ - TermName: "hello-world-terms", - TermRevision: 1, - }}} - response, err := s.client.SaveAgreement(p1) - c.Assert(err, jc.ErrorIsNil) - c.Assert(response.Agreements, gc.HasLen, 1) - c.Assert(response.Agreements[0].User, gc.Equals, "test-user") - c.Assert(response.Agreements[0].Term, gc.Equals, "hello-world-terms") - c.Assert(response.Agreements[0].Revision, gc.Equals, 1) -} - -func (s *apiSuite) TestNoFoundReturnsError(c *gc.C) { - s.httpClient.status = http.StatusNotFound - s.httpClient.body = []byte("something failed") - _, err := s.client.GetUnsignedTerms(&terms.CheckAgreementsRequest{ - Terms: []string{ - "hello-world-terms/1", - "hello-universe-terms/1", - }, - }) - c.Assert(err, gc.ErrorMatches, "failed to get unsigned terms: Not Found: something failed") -} - -func (s *apiSuite) TestSignedAgreementsEnvTermsURL(c *gc.C) { - cleanup := testing.PatchEnvironment("JUJU_TERMS", "http://example.com") - defer cleanup() - - t := time.Now().UTC() - s.httpClient.status = http.StatusOK - s.httpClient.SetBody(c, []terms.AgreementResponse{ - { - User: "test-user", - Term: "hello-world-terms", - Revision: 1, - CreatedOn: t, - }, - { - User: "test-user", - Term: "hello-universe-terms", - Revision: 42, - CreatedOn: t, - }, - }) - _, err := s.client.GetUsersAgreements() - c.Assert(err, jc.ErrorIsNil) - c.Assert(s.httpClient.Calls(), gc.HasLen, 1) - s.httpClient.CheckCall(c, 0, "Do", "http://example.com/agreements") -} - -func (s *apiSuite) TestUnsignedTermsEnvTermsURL(c *gc.C) { - cleanup := testing.PatchEnvironment("JUJU_TERMS", "http://example.com") - defer cleanup() - - s.httpClient.status = http.StatusOK - s.httpClient.SetBody(c, []terms.GetTermsResponse{ - { - Name: "hello-world-terms", - Revision: 1, - Content: "terms doc content", - }, - { - Name: "hello-universe-terms", - Revision: 1, - Content: "universal terms doc content", - }, - }) - _, err := s.client.GetUnsignedTerms(&terms.CheckAgreementsRequest{ - Terms: []string{ - "hello-world-terms/1", - "hello-universe-terms/1", - }, - }) - c.Assert(err, jc.ErrorIsNil) - c.Assert(s.httpClient.Calls(), gc.HasLen, 1) - s.httpClient.CheckCall(c, 0, "Do", "http://example.com/agreement?Terms=hello-world-terms%2F1&Terms=hello-universe-terms%2F1") - s.httpClient.ResetCalls() -} - -func (s *apiSuite) TestSaveAgreementEnvTermsURL(c *gc.C) { - cleanup := testing.PatchEnvironment("JUJU_TERMS", "http://example.com") - defer cleanup() - - s.httpClient.status = http.StatusOK - s.httpClient.SetBody(c, terms.SaveAgreementResponses{}) - p1 := &terms.SaveAgreements{ - Agreements: []terms.SaveAgreement{{ - TermName: "hello-world-terms", - TermRevision: 1, - }}} - _, err := s.client.SaveAgreement(p1) - c.Assert(err, jc.ErrorIsNil) - c.Assert(s.httpClient.Calls(), gc.HasLen, 1) - s.httpClient.CheckCall(c, 0, "DoWithBody", "http://example.com/agreement") -} - -type mockHttpClient struct { - testing.Stub - status int - body []byte -} - -func (m *mockHttpClient) Do(req *http.Request) (*http.Response, error) { - m.AddCall("Do", req.URL.String()) - return &http.Response{ - Status: http.StatusText(m.status), - StatusCode: m.status, - Proto: "HTTP/1.0", - ProtoMajor: 1, - ProtoMinor: 1, - Body: ioutil.NopCloser(bytes.NewReader(m.body)), - }, nil -} - -func (m *mockHttpClient) DoWithBody(req *http.Request, body io.ReadSeeker) (*http.Response, error) { - m.AddCall("DoWithBody", req.URL.String()) - return &http.Response{ - Status: http.StatusText(m.status), - StatusCode: m.status, - Proto: "HTTP/1.0", - ProtoMajor: 1, - ProtoMinor: 1, - Body: ioutil.NopCloser(bytes.NewReader(m.body)), - }, nil -} - -func (m *mockHttpClient) SetBody(c *gc.C, v interface{}) { - b, err := json.Marshal(&v) - c.Assert(err, jc.ErrorIsNil) - m.body = b -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/romulus/cmd/agree/agree.go juju-core-2.0~beta15/src/github.com/juju/romulus/cmd/agree/agree.go --- juju-core-2.0~beta12/src/github.com/juju/romulus/cmd/agree/agree.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/romulus/cmd/agree/agree.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,19 +9,19 @@ "fmt" "os" "os/exec" - "strconv" "strings" "github.com/juju/cmd" "github.com/juju/errors" "github.com/juju/juju/cmd/modelcmd" + "github.com/juju/terms-client/api" + "github.com/juju/terms-client/api/wireformat" + "gopkg.in/juju/charm.v6-unstable" "launchpad.net/gnuflag" - - "github.com/juju/romulus/api/terms" ) var ( - clientNew = terms.NewClient + clientNew = api.NewClient ) const agreeDoc = ` @@ -51,6 +51,7 @@ } type term struct { + owner string name string revision int } @@ -88,14 +89,14 @@ } for _, t := range args { - name, rev, err := parseTermRevision(t) + termId, err := charm.ParseTerm(t) if err != nil { return errors.Annotate(err, "invalid term format") } - if rev == 0 { + if termId.Revision == 0 { return errors.Errorf("must specify a valid term revision %q", t) } - c.terms = append(c.terms, term{name, rev}) + c.terms = append(c.terms, term{owner: termId.Owner, name: termId.Name, revision: termId.Revision}) c.termIds = append(c.termIds, t) } if len(c.terms) == 0 { @@ -111,7 +112,7 @@ return errors.Trace(err) } - termsClient, err := clientNew(terms.HTTPClient(client)) + termsClient, err := clientNew(api.HTTPClient(client)) if err != nil { return err } @@ -124,8 +125,8 @@ return nil } - needAgreement := []terms.GetTermsResponse{} - terms, err := termsClient.GetUnsignedTerms(&terms.CheckAgreementsRequest{ + needAgreement := []wireformat.GetTermsResponse{} + terms, err := termsClient.GetUnsignedTerms(&wireformat.CheckAgreementsRequest{ Terms: c.termIds, }) if err != nil { @@ -150,7 +151,7 @@ agreedTerms := make([]term, len(needAgreement)) for i, t := range needAgreement { - agreedTerms[i] = term{name: t.Name, revision: t.Revision} + agreedTerms[i] = term{owner: t.Owner, name: t.Name, revision: t.Revision} } answer = strings.TrimSpace(answer) @@ -167,15 +168,16 @@ return nil } -func saveAgreements(ctx *cmd.Context, termsClient terms.Client, ts []term) error { - agreements := make([]terms.SaveAgreement, len(ts)) +func saveAgreements(ctx *cmd.Context, termsClient api.Client, ts []term) error { + agreements := make([]wireformat.SaveAgreement, len(ts)) for i, t := range ts { - agreements[i] = terms.SaveAgreement{ + agreements[i] = wireformat.SaveAgreement{ + TermOwner: t.owner, TermName: t.name, TermRevision: t.revision, } } - response, err := termsClient.SaveAgreement(&terms.SaveAgreements{Agreements: agreements}) + response, err := termsClient.SaveAgreement(&wireformat.SaveAgreements{Agreements: agreements}) if err != nil { return errors.Annotate(err, "failed to save user agreement") } @@ -192,27 +194,7 @@ return bufio.NewReader(os.Stdin).ReadString('\n') } -func parseTermRevision(s string) (string, int, error) { - fail := func(err error) (string, int, error) { - return "", -1, err - } - tokens := strings.Split(s, "/") - if len(tokens) == 1 { - return tokens[0], 0, nil - } else if len(tokens) > 2 { - return fail(errors.New("unknown term revision format")) - } - - termName := tokens[0] - termRevisionString := tokens[1] - termRevision, err := strconv.Atoi(termRevisionString) - if err != nil { - return fail(errors.Trace(err)) - } - return termName, termRevision, nil -} - -func printTerms(ctx *cmd.Context, terms []terms.GetTermsResponse) error { +func printTerms(ctx *cmd.Context, terms []wireformat.GetTermsResponse) error { output := "" for _, t := range terms { output += fmt.Sprintf(` diff -Nru juju-core-2.0~beta12/src/github.com/juju/romulus/cmd/agree/agree_test.go juju-core-2.0~beta15/src/github.com/juju/romulus/cmd/agree/agree_test.go --- juju-core-2.0~beta12/src/github.com/juju/romulus/cmd/agree/agree_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/romulus/cmd/agree/agree_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,11 +9,12 @@ "github.com/juju/cmd/cmdtesting" coretesting "github.com/juju/juju/testing" + "github.com/juju/terms-client/api" + "github.com/juju/terms-client/api/wireformat" jujutesting "github.com/juju/testing" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" - "github.com/juju/romulus/api/terms" "github.com/juju/romulus/cmd/agree" ) @@ -34,7 +35,7 @@ s.FakeJujuXDGDataHomeSuite.SetUpTest(c) s.client = &mockClient{} - jujutesting.PatchValue(agree.ClientNew, func(...terms.ClientOption) (terms.Client, error) { + jujutesting.PatchValue(agree.ClientNew, func(...api.ClientOption) (api.Client, error) { return s.client, nil }) } @@ -45,7 +46,7 @@ }) s.client.user = "test-user" - s.client.setUnsignedTerms([]terms.GetTermsResponse{}) + s.client.setUnsignedTerms([]wireformat.GetTermsResponse{}) ctx, err := cmdtesting.RunCommand(c, agree.NewAgreeCommand(), "test-term/1") c.Assert(err, jc.ErrorIsNil) @@ -59,7 +60,7 @@ }) s.client.user = "test-user" - s.client.setUnsignedTerms([]terms.GetTermsResponse{{ + s.client.setUnsignedTerms([]wireformat.GetTermsResponse{{ Name: "test-term", Revision: 1, Content: testTerms, @@ -75,11 +76,16 @@ about: "everything works", args: []string{"test-term/1", "--yes"}, stdout: "Agreed to revision 1 of test-term for Juju users\n", - apiCalls: []jujutesting.StubCall{{FuncName: "SaveAgreement", Args: []interface{}{&terms.SaveAgreements{Agreements: []terms.SaveAgreement{{TermName: "test-term", TermRevision: 1}}}}}}, + apiCalls: []jujutesting.StubCall{{FuncName: "SaveAgreement", Args: []interface{}{&wireformat.SaveAgreements{Agreements: []wireformat.SaveAgreement{{TermName: "test-term", TermRevision: 1}}}}}}, + }, { + about: "everything works with owner term", + args: []string{"owner/test-term/1", "--yes"}, + stdout: "Agreed to revision 1 of test-term for Juju users\n", + apiCalls: []jujutesting.StubCall{{FuncName: "SaveAgreement", Args: []interface{}{&wireformat.SaveAgreements{Agreements: []wireformat.SaveAgreement{{TermOwner: "owner", TermName: "test-term", TermRevision: 1}}}}}}, }, { about: "cannot parse revision number", args: []string{"test-term/abc"}, - err: "invalid term format: strconv.ParseInt: parsing \"abc\": invalid syntax", + err: `must specify a valid term revision "test-term/abc"`, }, { about: "missing arguments", args: []string{}, @@ -96,11 +102,11 @@ `, apiCalls: []jujutesting.StubCall{{ FuncName: "GetUnunsignedTerms", Args: []interface{}{ - &terms.CheckAgreementsRequest{Terms: []string{"test-term/1"}}, + &wireformat.CheckAgreementsRequest{Terms: []string{"test-term/1"}}, }, }, { FuncName: "SaveAgreement", Args: []interface{}{ - &terms.SaveAgreements{Agreements: []terms.SaveAgreement{{TermName: "test-term", TermRevision: 1}}}, + &wireformat.SaveAgreements{Agreements: []wireformat.SaveAgreement{{TermName: "test-term", TermRevision: 1}}}, }, }}, }, { @@ -115,7 +121,7 @@ `, apiCalls: []jujutesting.StubCall{{ FuncName: "GetUnunsignedTerms", Args: []interface{}{ - &terms.CheckAgreementsRequest{Terms: []string{"test-term/1"}}, + &wireformat.CheckAgreementsRequest{Terms: []string{"test-term/1"}}, }, }}, }, { @@ -135,11 +141,11 @@ apiCalls: []jujutesting.StubCall{ { FuncName: "GetUnunsignedTerms", Args: []interface{}{ - &terms.CheckAgreementsRequest{Terms: []string{"test-term/1", "test-term/2"}}, + &wireformat.CheckAgreementsRequest{Terms: []string{"test-term/1", "test-term/2"}}, }, }, { FuncName: "SaveAgreement", Args: []interface{}{ - &terms.SaveAgreements{Agreements: []terms.SaveAgreement{ + &wireformat.SaveAgreements{Agreements: []wireformat.SaveAgreement{ {TermName: "test-term", TermRevision: 1}, }}, }, @@ -155,8 +161,8 @@ Agreed to revision 2 of test-term for Juju users `, apiCalls: []jujutesting.StubCall{ - {FuncName: "SaveAgreement", Args: []interface{}{&terms.SaveAgreements{ - Agreements: []terms.SaveAgreement{ + {FuncName: "SaveAgreement", Args: []interface{}{&wireformat.SaveAgreements{ + Agreements: []wireformat.SaveAgreement{ {TermName: "test-term", TermRevision: 1}, {TermName: "test-term", TermRevision: 2}, }}}}}, @@ -184,15 +190,16 @@ } type mockClient struct { + api.Client jujutesting.Stub lock sync.Mutex user string - terms []terms.GetTermsResponse - unsignedTerms []terms.GetTermsResponse + terms []wireformat.GetTermsResponse + unsignedTerms []wireformat.GetTermsResponse } -func (c *mockClient) setUnsignedTerms(t []terms.GetTermsResponse) { +func (c *mockClient) setUnsignedTerms(t []wireformat.GetTermsResponse) { c.lock.Lock() defer c.lock.Unlock() c.unsignedTerms = t @@ -200,27 +207,27 @@ // SaveAgreement saves user's agreement to the specified // revision of the terms documents -func (c *mockClient) SaveAgreement(p *terms.SaveAgreements) (*terms.SaveAgreementResponses, error) { +func (c *mockClient) SaveAgreement(p *wireformat.SaveAgreements) (*wireformat.SaveAgreementResponses, error) { c.AddCall("SaveAgreement", p) - responses := make([]terms.AgreementResponse, len(p.Agreements)) + responses := make([]wireformat.AgreementResponse, len(p.Agreements)) for i, agreement := range p.Agreements { - responses[i] = terms.AgreementResponse{ + responses[i] = wireformat.AgreementResponse{ User: c.user, Term: agreement.TermName, Revision: agreement.TermRevision, } } - return &terms.SaveAgreementResponses{responses}, nil + return &wireformat.SaveAgreementResponses{responses}, nil } -func (c *mockClient) GetUnsignedTerms(p *terms.CheckAgreementsRequest) ([]terms.GetTermsResponse, error) { +func (c *mockClient) GetUnsignedTerms(p *wireformat.CheckAgreementsRequest) ([]wireformat.GetTermsResponse, error) { c.MethodCall(c, "GetUnunsignedTerms", p) - r := make([]terms.GetTermsResponse, len(c.unsignedTerms)) + r := make([]wireformat.GetTermsResponse, len(c.unsignedTerms)) copy(r, c.unsignedTerms) return r, nil } -func (c *mockClient) GetUsersAgreements() ([]terms.AgreementResponse, error) { +func (c *mockClient) GetUsersAgreements() ([]wireformat.AgreementResponse, error) { c.MethodCall(c, "GetUsersAgreements") - return []terms.AgreementResponse{}, nil + return []wireformat.AgreementResponse{}, nil } diff -Nru juju-core-2.0~beta12/src/github.com/juju/romulus/cmd/listagreements/listagreements.go juju-core-2.0~beta15/src/github.com/juju/romulus/cmd/listagreements/listagreements.go --- juju-core-2.0~beta12/src/github.com/juju/romulus/cmd/listagreements/listagreements.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/romulus/cmd/listagreements/listagreements.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,22 +9,22 @@ "github.com/juju/cmd" "github.com/juju/errors" "github.com/juju/juju/cmd/modelcmd" + "github.com/juju/terms-client/api" + "github.com/juju/terms-client/api/wireformat" "gopkg.in/macaroon-bakery.v1/httpbakery" "launchpad.net/gnuflag" - - "github.com/juju/romulus/api/terms" ) var ( newClient = func(client *httpbakery.Client) (TermsServiceClient, error) { - return terms.NewClient(terms.HTTPClient(client)) + return api.NewClient(api.HTTPClient(client)) } ) // TermsServiceClient defines methods needed for the Terms Service CLI // commands. type TermsServiceClient interface { - GetUsersAgreements() ([]terms.AgreementResponse, error) + GetUsersAgreements() ([]wireformat.AgreementResponse, error) } const listAgreementsDoc = ` @@ -86,7 +86,7 @@ return errors.Annotate(err, "failed to list user agreements") } if agreements == nil { - agreements = []terms.AgreementResponse{} + agreements = []wireformat.AgreementResponse{} } err = c.out.Write(ctx, agreements) if err != nil { diff -Nru juju-core-2.0~beta12/src/github.com/juju/romulus/cmd/listagreements/listagreements_test.go juju-core-2.0~beta15/src/github.com/juju/romulus/cmd/listagreements/listagreements_test.go --- juju-core-2.0~beta12/src/github.com/juju/romulus/cmd/listagreements/listagreements_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/romulus/cmd/listagreements/listagreements_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -9,12 +9,12 @@ "github.com/juju/cmd/cmdtesting" coretesting "github.com/juju/juju/testing" + "github.com/juju/terms-client/api/wireformat" jujutesting "github.com/juju/testing" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" "gopkg.in/macaroon-bakery.v1/httpbakery" - "github.com/juju/romulus/api/terms" "github.com/juju/romulus/cmd/listagreements" ) @@ -46,6 +46,16 @@ } ] ` + expectedListAgreementsJSONOutputWithOwner = `[ + { + "user": "test-user", + "owner": "owner", + "term": "test-term", + "revision": 1, + "created-on": "2015-12-25T00:00:00Z" + } +] +` ) func (s *listAgreementsSuite) TestGetUsersAgreements(c *gc.C) { @@ -60,7 +70,7 @@ c.Assert(err, gc.ErrorMatches, "failed to list user agreements: well, this is embarassing") c.Assert(s.client.called, jc.IsTrue) - agreements := []terms.AgreementResponse{{ + agreements := []wireformat.AgreementResponse{{ User: "test-user", Term: "test-term", Revision: 1, @@ -81,14 +91,48 @@ c.Assert(s.client.called, jc.IsTrue) } +func (s *listAgreementsSuite) TestGetUsersAgreementsWithTermOwner(c *gc.C) { + ctx, err := cmdtesting.RunCommand(c, listagreements.NewListAgreementsCommand()) + c.Assert(err, jc.ErrorIsNil) + c.Assert(cmdtesting.Stdout(ctx), gc.Equals, `[] +`) + c.Assert(s.client.called, jc.IsTrue) + + s.client.setError("well, this is embarassing") + ctx, err = cmdtesting.RunCommand(c, listagreements.NewListAgreementsCommand()) + c.Assert(err, gc.ErrorMatches, "failed to list user agreements: well, this is embarassing") + c.Assert(s.client.called, jc.IsTrue) + + agreements := []wireformat.AgreementResponse{{ + User: "test-user", + Owner: "owner", + Term: "test-term", + Revision: 1, + CreatedOn: time.Date(2015, 12, 25, 0, 0, 0, 0, time.UTC), + }} + s.client.setAgreements(agreements) + + ctx, err = cmdtesting.RunCommand(c, listagreements.NewListAgreementsCommand()) + c.Assert(err, jc.ErrorIsNil) + c.Assert(ctx, gc.NotNil) + c.Assert(cmdtesting.Stdout(ctx), gc.Equals, expectedListAgreementsJSONOutputWithOwner) + c.Assert(s.client.called, jc.IsTrue) + + ctx, err = cmdtesting.RunCommand(c, listagreements.NewListAgreementsCommand(), "--format", "yaml") + c.Assert(err, jc.ErrorIsNil) + c.Assert(ctx, gc.NotNil) + c.Assert(cmdtesting.Stdout(ctx), gc.Equals, "- user: test-user\n owner: owner\n term: test-term\n revision: 1\n createdon: 2015-12-25T00:00:00Z\n") + c.Assert(s.client.called, jc.IsTrue) +} + type mockClient struct { called bool - agreements []terms.AgreementResponse + agreements []wireformat.AgreementResponse err string } -func (c *mockClient) setAgreements(agreements []terms.AgreementResponse) { +func (c *mockClient) setAgreements(agreements []wireformat.AgreementResponse) { c.agreements = agreements c.called = false c.err = "" @@ -100,7 +144,7 @@ c.agreements = nil } -func (c *mockClient) GetUsersAgreements() ([]terms.AgreementResponse, error) { +func (c *mockClient) GetUsersAgreements() ([]wireformat.AgreementResponse, error) { c.called = true if c.err != "" { return nil, errors.New(c.err) diff -Nru juju-core-2.0~beta12/src/github.com/juju/romulus/dependencies.tsv juju-core-2.0~beta15/src/github.com/juju/romulus/dependencies.tsv --- juju-core-2.0~beta12/src/github.com/juju/romulus/dependencies.tsv 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/romulus/dependencies.tsv 2016-08-16 08:56:25.000000000 +0000 @@ -22,6 +22,7 @@ github.com/juju/replicaset git fb7294cf57a1e2f08a57691f1246d129a87ab7e8 2015-05-08T02:21:43Z github.com/juju/rfc git ebdbbdb950cd039a531d15cdc2ac2cbd94f068ee 2016-07-11T02:42:13Z github.com/juju/schema git 075de04f9b7d7580d60a1e12a0b3f50bb18e6998 2016-04-20T04:42:03Z +github.com/juju/terms-client git 8b4b1f20960150c2529db2003824014490291299 2016-08-08T09:34:01Z github.com/juju/testing git ccf839b5a07a7a05009f8fa3ec41cd05fb2e0b08 2016-06-24T20:35:24Z github.com/juju/txn git 99ec629d0066a4d73c54d8e021a7fc1dc07df614 2015-06-09T16:58:27Z github.com/juju/usso git 68a59c96c178fbbad65926e7f93db50a2cd14f33 2016-04-01T10:44:24Z @@ -36,7 +37,7 @@ gopkg.in/check.v1 git 4f90aeace3a26ad7021961c297b22c42160c7b25 2016-01-05T16:49:36Z gopkg.in/errgo.v1 git 66cb46252b94c1f3d65646f54ee8043ab38d766c 2015-10-07T15:31:57Z gopkg.in/juju/blobstore.v2 git 51fa6e26128d74e445c72d3a91af555151cc3654 2016-01-25T02:37:03Z -gopkg.in/juju/charm.v6-unstable git 8796be6021c9ecb20630950498ec515f7dd24575 2016-06-09T14:28:26Z +gopkg.in/juju/charm.v6-unstable git a3bb92d047b0892452b6a39ece59b4d3a2ac35b9 2016-07-22T08:34:31Z gopkg.in/juju/charmrepo.v2-unstable git 6e6733987fb03100f30e494cc1134351fe4a593b 2016-05-30T23:07:41Z gopkg.in/juju/environschema.v1 git 7359fc7857abe2b11b5b3e23811a9c64cb6b01e0 2015-11-04T11:58:10Z gopkg.in/juju/names.v2 git 5426d66579afd36fc63d809dd58806806c2f161f 2016-06-23T03:33:52Z diff -Nru juju-core-2.0~beta12/src/github.com/juju/terms-client/api/api.go juju-core-2.0~beta15/src/github.com/juju/terms-client/api/api.go --- juju-core-2.0~beta12/src/github.com/juju/terms-client/api/api.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/terms-client/api/api.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,376 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the GPLv3, see LICENCE file for details. + +// The api package contains the interface and implementation of the +// terms service client. +package api + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "strconv" + "strings" + + "github.com/juju/errors" + "gopkg.in/macaroon-bakery.v1/httpbakery" + + "github.com/juju/terms-client/api/wireformat" +) + +var BaseURL = "https://api.jujucharms.com/terms" + +// Client represents the interface of the terms service client ap client apii. +type Client interface { + // Saves a Terms and Conditions document under the specified owner/name + // and returns a term document with the new revision number + // (only term owner, name and revision are returned). + SaveTerm(owner, name, content string) (string, error) + + // GetTerm returns the term that matches the specified criteria. + // If revision is 0, it will return the latest revision of the term. + GetTerm(owner, name string, revision int) (*wireformat.Term, error) + + // GetUnsignedTerms checks for agreements to the specified terms + // and returns all terms that the user has not agreed to. + GetUnsignedTerms(*wireformat.CheckAgreementsRequest) ([]wireformat.GetTermsResponse, error) + + // SaveAgreement saves the users agreement to the specified terms (revision must always be specified). + SaveAgreement(*wireformat.SaveAgreements) (*wireformat.SaveAgreementResponses, error) + + // GetUsersAgreements returns all agreements the user (the user making the request) has made. + GetUsersAgreements() ([]wireformat.AgreementResponse, error) + + // Publish publishes the owned term identified by input parameters + // and returns the published term id. + // Only owned terms require publishing. + Publish(owner, name string, revision int) (string, error) +} + +type httpClient interface { + Do(*http.Request) (*http.Response, error) + DoWithBody(req *http.Request, body io.ReadSeeker) (*http.Response, error) +} + +// ClientOption defines a function which configures a Client. +type ClientOption func(h *client) + +// HTTPClient returns a function that sets the http client used by the API +// (e.g. if we want to use TLS). +func HTTPClient(c httpClient) ClientOption { + return func(h *client) { + h.bclient = c + } +} + +// ServiceURL returns a function that sets the terms service URL used +// by the API. +func ServiceURL(serviceURL string) ClientOption { + return func(h *client) { + h.serviceURL = serviceURL + } +} + +// NewClient returns a terms service api client. +func NewClient(options ...ClientOption) (Client, error) { + bakeryClient := httpbakery.NewClient() + c := &client{ + serviceURL: getBaseURL(), + bclient: bakeryClient, + } + for _, option := range options { + option(c) + } + return c, nil +} + +type client struct { + serviceURL string + bclient httpClient +} + +func unmarshalError(data []byte) (string, error) { + var e struct { + Error string `json:"error"` + Message string `json:"message"` + } + err := json.Unmarshal(data, &e) + if err != nil { + return "", errors.Trace(err) + } + if e.Error != "" { + return e.Error, nil + } + return e.Message, nil +} + +// Publish publishes the owned term identified by input parameters +// and returns the published term id. +func (c *client) Publish(owner, name string, revision int) (string, error) { + fail := func(err error) (string, error) { + return "", err + } + if owner == "" { + return fmt.Sprintf("%s/%d", name, revision), nil + } + termURL := fmt.Sprintf("%s/v1/terms/%s/%s/%d/publish", c.serviceURL, owner, name, revision) + + req, err := http.NewRequest("POST", termURL, nil) + if err != nil { + return fail(errors.Trace(err)) + } + + response, err := c.bclient.DoWithBody(req, nil) + if err != nil { + return fail(errors.Trace(err)) + } + defer discardClose(response) + data, err := ioutil.ReadAll(response.Body) + if err != nil { + return fail(errors.Trace(err)) + } + if response.StatusCode != http.StatusOK { + message, uerr := unmarshalError(data) + if uerr != nil { + return fail(errors.New(string(data))) + } + return fail(errors.New(message)) + } + var id struct { + TermID string `json:"term-id"` + } + err = json.Unmarshal(data, &id) + if err != nil { + return fail(errors.Trace(err)) + } + return id.TermID, nil +} + +// GetTerm implements the Client interface. It returns the term that +// matches the specified criteria. If revision is 0, it will return the +// latest revision of the term. +func (c *client) GetTerm(owner, name string, revision int) (*wireformat.Term, error) { + termURL, err := appendTermURL(c.serviceURL, owner, name, revision) + if err != nil { + return nil, errors.Trace(err) + } + + req, err := http.NewRequest("GET", termURL.String(), nil) + if err != nil { + return nil, errors.Trace(err) + } + response, err := c.bclient.Do(req) + if err != nil { + return nil, errors.Trace(err) + } + defer discardClose(response) + data, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, errors.Trace(err) + } + if response.StatusCode != http.StatusOK { + message, uerr := unmarshalError(data) + if uerr != nil { + return nil, errors.New(string(data)) + } + return nil, errors.New(message) + } + var terms []wireformat.Term + err = json.Unmarshal(data, &terms) + if err != nil { + return nil, errors.Trace(err) + } + if len(terms) == 0 { + return nil, errors.NotFoundf("term") + } + return &terms[0], nil +} + +// SaveTerm implements the Client interface. It saves a Terms and Conditions document +// under the specified owner/name and returns a term document with the new revision number +// (only term owner, name and revision are returned). +func (c *client) SaveTerm(owner, name, content string) (string, error) { + termURL, err := appendTermURL(c.serviceURL, owner, name, 0) + if err != nil { + return "", errors.Trace(err) + } + + term := wireformat.SaveTerm{ + Content: content, + } + data, err := json.Marshal(term) + if err != nil { + return "", errors.Trace(err) + } + + req, err := http.NewRequest("POST", termURL.String(), nil) + if err != nil { + return "", errors.Trace(err) + } + req.Header.Set("Content-Type", "application/json") + + response, err := c.bclient.DoWithBody(req, bytes.NewReader(data)) + if err != nil { + return "", errors.Trace(err) + } + defer discardClose(response) + data, err = ioutil.ReadAll(response.Body) + if err != nil { + return "", errors.Trace(err) + } + if response.StatusCode != http.StatusOK { + message, uerr := unmarshalError(data) + if uerr != nil { + return "", errors.New(string(data)) + } + return "", errors.New(message) + } + var savedTerm wireformat.TermIDResponse + err = json.Unmarshal(data, &savedTerm) + if err != nil { + return "", errors.Trace(err) + } + return savedTerm.TermID, nil +} + +// GetUsersAgreements implements the Client interface. It returns all +// agreements the user (the user making the request) has made. +func (c *client) GetUsersAgreements() ([]wireformat.AgreementResponse, error) { + u := fmt.Sprintf("%s/v1/agreements", c.serviceURL) + req, err := http.NewRequest("GET", u, nil) + if err != nil { + return nil, errors.Trace(err) + } + response, err := c.bclient.Do(req) + if err != nil { + return nil, errors.Trace(err) + } + if response.StatusCode != http.StatusOK { + b, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, errors.Errorf("failed to get signed agreements: %v", response.Status) + } + return nil, errors.Errorf("failed to get signed agreements: %v: %s", response.Status, string(b)) + } + defer discardClose(response) + + var results []wireformat.AgreementResponse + dec := json.NewDecoder(response.Body) + err = dec.Decode(&results) + if err != nil { + return nil, errors.Trace(err) + } + return results, nil +} + +// SaveAgreement implements the Client interface. It saves the users +// agreement to the specified term (revision must always be specified). +func (c *client) SaveAgreement(request *wireformat.SaveAgreements) (*wireformat.SaveAgreementResponses, error) { + u := fmt.Sprintf("%s/v1/agreement", c.serviceURL) + req, err := http.NewRequest("POST", u, nil) + if err != nil { + return nil, errors.Trace(err) + } + req.Header.Set("Content-Type", "application/json") + data, err := json.Marshal(request.Agreements) + if err != nil { + return nil, errors.Trace(err) + } + response, err := c.bclient.DoWithBody(req, bytes.NewReader(data)) + if err != nil { + return nil, errors.Trace(err) + } + if response.StatusCode != http.StatusOK { + b, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, errors.Errorf("failed to save agreement: %v", response.Status) + } + return nil, errors.Errorf("failed to save agreement: %v: %s", response.Status, string(b)) + } + defer discardClose(response) + var results wireformat.SaveAgreementResponses + dec := json.NewDecoder(response.Body) + err = dec.Decode(&results) + if err != nil { + return nil, errors.Trace(err) + } + return &results, nil +} + +// GetUnsignedTerms implements the Client interface. It checks for agreements +// to the specified terms and returns all terms that the user has not agreed +// to. +func (c *client) GetUnsignedTerms(terms *wireformat.CheckAgreementsRequest) ([]wireformat.GetTermsResponse, error) { + values := url.Values{} + for _, t := range terms.Terms { + values.Add("Terms", t) + } + u := fmt.Sprintf("%s/v1/agreement?%s", c.serviceURL, values.Encode()) + req, err := http.NewRequest("GET", u, nil) + if err != nil { + return nil, errors.Trace(err) + } + req.Header.Set("Content-Type", "application/json") + response, err := c.bclient.Do(req) + if err != nil { + return nil, errors.Trace(err) + } + if response.StatusCode != http.StatusOK { + b, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, errors.Errorf("failed to get unsigned terms: %v", response.Status) + } + return nil, errors.Errorf("failed to get unsigned terms: %v: %s", response.Status, string(b)) + } + defer discardClose(response) + var results []wireformat.GetTermsResponse + dec := json.NewDecoder(response.Body) + err = dec.Decode(&results) + if err != nil { + return nil, errors.Trace(err) + } + return results, nil +} + +func getBaseURL() string { + baseURL := BaseURL + if termsURL := os.Getenv("JUJU_TERMS"); termsURL != "" { + baseURL = termsURL + } + return baseURL +} + +func appendTermURL(baseURLStr, owner, term string, revision int) (*url.URL, error) { + b, err := url.Parse(baseURLStr) + if err != nil { + return nil, errors.Annotatef(err, "cannot parse %q", baseURLStr) + } + b.Path = strings.TrimSuffix(b.Path, "/") + "/v1/terms" + if owner != "" { + b.Path = b.Path + "/" + strings.TrimPrefix(owner, "/") + } + if term == "" { + return nil, errors.New("empty term name") + } + b.Path = strings.TrimSuffix(b.Path, "/") + "/" + strings.TrimPrefix(term, "/") + if revision != 0 { + values := b.Query() + values.Set("revision", strconv.FormatInt(int64(revision), 10)) + b.RawQuery = values.Encode() + } + return b, nil +} + +// discardClose reads any remaining data from the response body and closes it. +func discardClose(response *http.Response) { + if response == nil || response.Body == nil { + return + } + io.Copy(ioutil.Discard, response.Body) + response.Body.Close() +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/terms-client/api/api_test.go juju-core-2.0~beta15/src/github.com/juju/terms-client/api/api_test.go --- juju-core-2.0~beta12/src/github.com/juju/terms-client/api/api_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/terms-client/api/api_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,366 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package api_test + +import ( + "bytes" + "encoding/json" + "io" + "io/ioutil" + "net/http" + stdtesting "testing" + "time" + + "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/terms-client/api" + "github.com/juju/terms-client/api/wireformat" +) + +type apiSuite struct { + client api.Client + httpClient *mockHttpClient +} + +func Test(t *stdtesting.T) { + gc.TestingT(t) +} + +var _ = gc.Suite(&apiSuite{}) + +func (s *apiSuite) SetUpTest(c *gc.C) { + s.newClient(c) +} + +func (s *apiSuite) newClient(c *gc.C) { + s.httpClient = &mockHttpClient{} + var err error + s.client, err = api.NewClient(api.HTTPClient(s.httpClient)) + c.Assert(err, jc.ErrorIsNil) +} + +func (s *apiSuite) TestPublish(c *gc.C) { + termID := struct { + TermID string `json:"term-id"` + }{ + TermID: "test-owner/test-term/17", + } + s.httpClient.status = http.StatusOK + s.httpClient.SetBody(c, termID) + + id, err := s.client.Publish("test-owner", "test-term", 17) + c.Assert(err, jc.ErrorIsNil) + c.Assert(id, gc.Equals, termID.TermID) + s.httpClient.CheckCall(c, 0, "DoWithBody", "https://api.jujucharms.com/terms/v1/terms/test-owner/test-term/17/publish") +} + +func (s *apiSuite) TestSaveOwnedTerm(c *gc.C) { + term := wireformat.TermIDResponse{ + TermID: "test-term/1", + } + s.httpClient.status = http.StatusOK + s.httpClient.SetBody(c, term) + savedTerm, err := s.client.SaveTerm("owner", "test-term", "You hereby agree to run this test.") + c.Assert(err, jc.ErrorIsNil) + c.Assert(savedTerm, jc.DeepEquals, term.TermID) + s.httpClient.CheckCall(c, 0, "DoWithBody", "https://api.jujucharms.com/terms/v1/terms/owner/test-term") +} + +func (s *apiSuite) TestSaveOwnerlessTerm(c *gc.C) { + term := wireformat.TermIDResponse{ + TermID: "test-term/1", + } + s.httpClient.status = http.StatusOK + s.httpClient.SetBody(c, term) + savedTerm, err := s.client.SaveTerm("", "test-term", "You hereby agree to run this test.") + c.Assert(err, jc.ErrorIsNil) + c.Assert(savedTerm, jc.DeepEquals, term.TermID) + s.httpClient.CheckCall(c, 0, "DoWithBody", "https://api.jujucharms.com/terms/v1/terms/test-term") +} + +func (s *apiSuite) TestSaveTermError(c *gc.C) { + s.httpClient.status = http.StatusInternalServerError + s.httpClient.SetBody(c, struct { + Error string `json:"error"` + }{"silly internal error"}) + _, err := s.client.SaveTerm("", "test-term", "You hereby agree to run this test.") + c.Assert(err, gc.ErrorMatches, "silly internal error") +} + +func (s *apiSuite) TestGetOwnedTermWithRevision(c *gc.C) { + t := time.Now().Round(time.Second).UTC() + term := []wireformat.Term{{ + Owner: "owner", + Name: "test-term", + Revision: 17, + CreatedOn: wireformat.TimeRFC3339(t), + Content: "You hereby agree to run this test.", + }} + s.httpClient.status = http.StatusOK + s.httpClient.SetBody(c, term) + savedTerm, err := s.client.GetTerm("owner", "test-term", 17) + c.Assert(err, jc.ErrorIsNil) + c.Assert(savedTerm, jc.DeepEquals, &term[0]) + s.httpClient.CheckCall(c, 0, "Do", "https://api.jujucharms.com/terms/v1/terms/owner/test-term?revision=17") +} + +func (s *apiSuite) TestGetOwnedTermWithoutRevision(c *gc.C) { + t := time.Now().Round(time.Second).UTC() + term := []wireformat.Term{{ + Owner: "owner", + Name: "test-term", + Revision: 17, + CreatedOn: wireformat.TimeRFC3339(t), + Content: "You hereby agree to run this test.", + }} + s.httpClient.status = http.StatusOK + s.httpClient.SetBody(c, term) + savedTerm, err := s.client.GetTerm("owner", "test-term", 0) + c.Assert(err, jc.ErrorIsNil) + c.Assert(savedTerm, jc.DeepEquals, &term[0]) + s.httpClient.CheckCall(c, 0, "Do", "https://api.jujucharms.com/terms/v1/terms/owner/test-term") +} + +func (s *apiSuite) TestGetOwnerlessTermWithRevision(c *gc.C) { + t := time.Now().Round(time.Second).UTC() + term := []wireformat.Term{{ + Name: "test-term", + Revision: 17, + CreatedOn: wireformat.TimeRFC3339(t), + Content: "You hereby agree to run this test.", + }} + s.httpClient.status = http.StatusOK + s.httpClient.SetBody(c, term) + savedTerm, err := s.client.GetTerm("", "test-term", 17) + c.Assert(err, jc.ErrorIsNil) + c.Assert(savedTerm, jc.DeepEquals, &term[0]) + s.httpClient.CheckCall(c, 0, "Do", "https://api.jujucharms.com/terms/v1/terms/test-term?revision=17") +} + +func (s *apiSuite) TestGetOwnerlessTermWithoutRevision(c *gc.C) { + t := time.Now().Round(time.Second).UTC() + term := []wireformat.Term{{ + Name: "test-term", + Revision: 17, + CreatedOn: wireformat.TimeRFC3339(t), + Content: "You hereby agree to run this test.", + }} + s.httpClient.status = http.StatusOK + s.httpClient.SetBody(c, term) + savedTerm, err := s.client.GetTerm("", "test-term", 0) + c.Assert(err, jc.ErrorIsNil) + c.Assert(savedTerm, jc.DeepEquals, &term[0]) + s.httpClient.CheckCall(c, 0, "Do", "https://api.jujucharms.com/terms/v1/terms/test-term") +} + +func (s *apiSuite) TestGetTermError(c *gc.C) { + s.httpClient.status = http.StatusInternalServerError + s.httpClient.SetBody(c, struct { + Error string `json:"error"` + }{"silly internal error"}) + _, err := s.client.GetTerm("", "test-term", 17) + c.Assert(err, gc.ErrorMatches, "silly internal error") +} + +func (s *apiSuite) TestSignedAgreements(c *gc.C) { + t := time.Now().Round(time.Second).UTC() + s.httpClient.status = http.StatusOK + s.httpClient.SetBody(c, []wireformat.Agreement{{ + User: "test-user", + Term: "hello-world-terms", + Revision: 1, + CreatedOn: wireformat.TimeRFC3339(t), + }, { + User: "test-user", + Term: "hello-universe-terms", + Revision: 42, + CreatedOn: wireformat.TimeRFC3339(t), + }, + }) + signedAgreements, err := s.client.GetUsersAgreements() + c.Assert(err, jc.ErrorIsNil) + c.Assert(signedAgreements, gc.HasLen, 2) + c.Assert(signedAgreements[0].User, gc.Equals, "test-user") + c.Assert(signedAgreements[0].Term, gc.Equals, "hello-world-terms") + c.Assert(signedAgreements[0].Revision, gc.Equals, 1) + c.Assert(signedAgreements[0].CreatedOn, gc.DeepEquals, t) + c.Assert(signedAgreements[1].User, gc.Equals, "test-user") + c.Assert(signedAgreements[1].Term, gc.Equals, "hello-universe-terms") + c.Assert(signedAgreements[1].Revision, gc.Equals, 42) + c.Assert(signedAgreements[1].CreatedOn, gc.DeepEquals, t) +} + +func (s *apiSuite) TestUnsignedTerms(c *gc.C) { + s.httpClient.status = http.StatusOK + s.httpClient.SetBody(c, []wireformat.Term{{ + Name: "hello-world-terms", + Revision: 1, + Content: "terms doc content", + }, { + Name: "hello-universe-terms", + Revision: 1, + Content: "universal terms doc content", + }, + }) + missingAgreements, err := s.client.GetUnsignedTerms(&wireformat.CheckAgreementsRequest{ + Terms: []string{ + "hello-world-terms/1", + "hello-universe-terms/1", + }, + }) + c.Assert(err, jc.ErrorIsNil) + c.Assert(missingAgreements, gc.HasLen, 2) + c.Assert(missingAgreements[0].Name, gc.Equals, "hello-world-terms") + c.Assert(missingAgreements[0].Revision, gc.Equals, 1) + c.Assert(missingAgreements[0].Content, gc.Equals, "terms doc content") + c.Assert(missingAgreements[1].Name, gc.Equals, "hello-universe-terms") + c.Assert(missingAgreements[1].Revision, gc.Equals, 1) + c.Assert(missingAgreements[1].Content, gc.Equals, "universal terms doc content") + s.httpClient.SetBody(c, wireformat.SaveAgreementResponses{ + Agreements: []wireformat.AgreementResponse{{ + User: "test-user", + Term: "hello-world-terms", + Revision: 1, + }}}, + ) + + p1 := wireformat.SaveAgreements{ + Agreements: []wireformat.SaveAgreement{{ + TermName: "hello-world-terms", + TermRevision: 1, + }}, + } + response, err := s.client.SaveAgreement(&p1) + c.Assert(err, jc.ErrorIsNil) + c.Assert(response.Agreements, gc.HasLen, 1) + c.Assert(response.Agreements[0].User, gc.Equals, "test-user") + c.Assert(response.Agreements[0].Term, gc.Equals, "hello-world-terms") + c.Assert(response.Agreements[0].Revision, gc.Equals, 1) +} + +func (s *apiSuite) TestNotFoundError(c *gc.C) { + s.httpClient.status = http.StatusNotFound + s.httpClient.body = []byte("something failed") + _, err := s.client.GetUnsignedTerms(&wireformat.CheckAgreementsRequest{ + Terms: []string{ + "hello-world-terms/1", + "hello-universe-terms/1", + }, + }) + c.Assert(err, gc.ErrorMatches, "failed to get unsigned terms: Not Found: something failed") +} + +func (s *apiSuite) TestSignedAgreementsEnvTermsURL(c *gc.C) { + cleanup := testing.PatchEnvironment("JUJU_TERMS", "http://example.com") + defer cleanup() + s.newClient(c) + + t := time.Now().UTC() + s.httpClient.status = http.StatusOK + s.httpClient.SetBody(c, []wireformat.AgreementResponse{ + { + User: "test-user", + Term: "hello-world-terms", + Revision: 1, + CreatedOn: t, + }, + { + User: "test-user", + Term: "hello-universe-terms", + Revision: 42, + CreatedOn: t, + }, + }) + _, err := s.client.GetUsersAgreements() + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.httpClient.Calls(), gc.HasLen, 1) + s.httpClient.CheckCall(c, 0, "Do", "http://example.com/v1/agreements") +} + +func (s *apiSuite) TestUnsignedTermsEnvTermsURL(c *gc.C) { + cleanup := testing.PatchEnvironment("JUJU_TERMS", "http://example.com") + defer cleanup() + s.newClient(c) + + s.httpClient.status = http.StatusOK + s.httpClient.SetBody(c, []wireformat.GetTermsResponse{ + { + Name: "hello-world-terms", + Revision: 1, + Content: "terms doc content", + }, + { + Name: "hello-universe-terms", + Revision: 1, + Content: "universal terms doc content", + }, + }) + _, err := s.client.GetUnsignedTerms(&wireformat.CheckAgreementsRequest{ + Terms: []string{ + "hello-world-terms/1", + "hello-universe-terms/1", + }, + }) + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.httpClient.Calls(), gc.HasLen, 1) + s.httpClient.CheckCall(c, 0, "Do", "http://example.com/v1/agreement?Terms=hello-world-terms%2F1&Terms=hello-universe-terms%2F1") + s.httpClient.ResetCalls() +} + +func (s *apiSuite) TestSaveAgreementEnvTermsURL(c *gc.C) { + cleanup := testing.PatchEnvironment("JUJU_TERMS", "http://example.com") + defer cleanup() + s.newClient(c) + + s.httpClient.status = http.StatusOK + s.httpClient.SetBody(c, wireformat.SaveAgreementResponses{}) + p1 := wireformat.SaveAgreements{ + Agreements: []wireformat.SaveAgreement{{ + TermName: "hello-world-terms", + TermRevision: 1, + }}, + } + _, err := s.client.SaveAgreement(&p1) + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.httpClient.Calls(), gc.HasLen, 1) + s.httpClient.CheckCall(c, 0, "DoWithBody", "http://example.com/v1/agreement") +} + +type mockHttpClient struct { + testing.Stub + status int + body []byte +} + +func (m *mockHttpClient) Do(req *http.Request) (*http.Response, error) { + m.AddCall("Do", req.URL.String()) + return &http.Response{ + Status: http.StatusText(m.status), + StatusCode: m.status, + Proto: "HTTP/1.0", + ProtoMajor: 1, + ProtoMinor: 1, + Body: ioutil.NopCloser(bytes.NewReader(m.body)), + }, nil +} + +func (m *mockHttpClient) DoWithBody(req *http.Request, body io.ReadSeeker) (*http.Response, error) { + m.AddCall("DoWithBody", req.URL.String()) + return &http.Response{ + Status: http.StatusText(m.status), + StatusCode: m.status, + Proto: "HTTP/1.0", + ProtoMajor: 1, + ProtoMinor: 1, + Body: ioutil.NopCloser(bytes.NewReader(m.body)), + }, nil +} + +func (m *mockHttpClient) SetBody(c *gc.C, v interface{}) { + b, err := json.Marshal(&v) + c.Assert(err, jc.ErrorIsNil) + m.body = b +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/terms-client/api/wireformat/entities.go juju-core-2.0~beta15/src/github.com/juju/terms-client/api/wireformat/entities.go --- juju-core-2.0~beta12/src/github.com/juju/terms-client/api/wireformat/entities.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/terms-client/api/wireformat/entities.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,203 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the GPLv3, see LICENCE file for details. + +// The wireformat package contains definitions of wireformat used by the terms +// service. +package wireformat + +import ( + "fmt" + "time" + + "github.com/juju/errors" +) + +// SaveTerm structure contains the content of the terms document +// to be saved. +type SaveTerm struct { + Content string `json:"content"` +} + +// Validate validates the save term request. +func (t *SaveTerm) Validate() error { + if t.Content == "" { + return errors.BadRequestf("empty term content") + } + return nil +} + +// Term contains the terms and conditions document structure. +type Term struct { + Owner string `json:"owner,omitempty" yaml:"owner,omitempty"` + Name string `json:"name" yaml:"name"` + Revision int `json:"revision" yaml:"revision"` + Title string `json:"title,omitempty" yaml:"title,omitempty"` + CreatedOn TimeRFC3339 `json:"created-on,omitempty" yaml:"createdon"` + Published bool `json:"published", yaml:"published"` + Content string `json:"content,omitempty" yaml:"content,omitempty"` +} + +// TermIDResponse contains just the termID +type TermIDResponse struct { + TermID string `json:"term-id"` +} + +func (t *Term) termID() string { + return fmt.Sprintf("%s/%s/%d", t.Owner, t.Name, t.Revision) +} + +// Terms stores a sortable slice of terms. +type Terms []Term + +// Len implements sort.Interface +func (terms Terms) Len() int { + return len([]Term(terms)) +} + +// Less implements sort.Interface +func (terms Terms) Less(i, j int) bool { + return terms[i].termID() < terms[j].termID() +} + +// Swap implements sort.Interface +func (terms Terms) Swap(i, j int) { + terms[i], terms[j] = terms[j], terms[i] +} + +// AgreementRequest holds the parameters for creating a new +// user agreement to a specific revision of terms. +type AgreementRequest struct { + TermOwner string `json:"termowner"` + TermName string `json:"termname"` + TermRevision int `json:"termrevision"` +} + +// Agreements holds multiple agreements peformed in +// a single request. +type Agreements struct { + Agreements []Agreement `json:"agreements"` +} + +// Agreement holds a single agreement made by +// the user to a specific revision of terms and conditions +// document. +type Agreement struct { + User string `json:"user"` + Owner string `json:"owner"` + Term string `json:"term"` + Revision int `json:"revision"` + CreatedOn TimeRFC3339 `json:"created-on"` +} + +// TimeRFC3339 represents a time, which is marshaled +// and unmarshaled using the RFC3339 format +type TimeRFC3339 time.Time + +// MarshalJSON implements the json.Marshaler interface. +func (t TimeRFC3339) MarshalJSON() ([]byte, error) { + b := make([]byte, 0, len(time.RFC3339)+2) + b = append(b, '"') + b = time.Time(t).AppendFormat(b, time.RFC3339) + b = append(b, '"') + return b, nil +} + +// MarshalYAML implements gopkg.in/juju/yaml.v2 Marshaler interface. +func (t TimeRFC3339) MarshalYAML() (interface{}, error) { + return time.Time(t).Format(time.RFC3339), nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (t *TimeRFC3339) UnmarshalJSON(data []byte) error { + t0, err := time.Parse(`"`+time.RFC3339+`"`, string(data)) + if err != nil { + return errors.Trace(err) + } + *t = TimeRFC3339(t0) + return nil +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (t *TimeRFC3339) UnmarshalYAML(unmarshal func(interface{}) error) error { + var data string + err := unmarshal(&data) + if err != nil { + return errors.Trace(err) + } + t0, err := time.Parse(time.RFC3339, data) + if err != nil { + return errors.Trace(err) + } + *t = TimeRFC3339(t0) + return nil +} + +// DebugStatusResponse contains results of various checks that +// form the status of the terms service. +type DebugStatusResponse struct { + Checks map[string]CheckResult `json:"checks"` +} + +// CheckResult holds the result of a single status check. +type CheckResult struct { + // Name is the human readable name for the check. + Name string `json:"name"` + + // Value is the check result. + Value string `json:"value"` + + // Passed reports whether the check passed. + Passed bool `json:"passed"` + + // Duration holds the duration that the + // status check took to run. + Duration time.Duration `json:"duration"` +} + +// GetTermsResponse holds the response of the GetTerms call. +type GetTermsResponse struct { + Name string `json:"name" yaml:"name"` + Owner string `json:"owner,omitempty" yaml:"owner,omitempty"` + Title string `json:"title" yaml:"title"` + Revision int `json:"revision" yaml:"revision"` + CreatedOn time.Time `json:"created-on" yaml:"createdon"` + Content string `json:"content" yaml:"content"` +} + +// CheckAgreementsRequest holds a slice of terms and the /v1/agreement +// endpoint will check if the user has agreed to the specified terms +// and return a slice of terms the user has not agreed to yet. +type CheckAgreementsRequest struct { + Terms []string +} + +// SaveAgreements holds the parameters for creating new +// user agreements to one or more specific revisions of terms. +type SaveAgreements struct { + Agreements []SaveAgreement `json:"agreements"` +} + +// SaveAgreement holds the parameters for creating a new +// user agreement to a specific revision of terms. +type SaveAgreement struct { + TermOwner string `json:"termowner"` + TermName string `json:"termname"` + TermRevision int `json:"termrevision"` +} + +// SaveAgreementResponses holds the response of the SaveAgreement +// call. +type SaveAgreementResponses struct { + Agreements []AgreementResponse `json:"agreements"` +} + +// AgreementResponse holds the a single agreement made by +// the user to a specific revision of terms and conditions +// document. +type AgreementResponse struct { + User string `json:"user" yaml:"user"` + Owner string `json:"owner,omitempty" yaml:"owner,omitempty"` + Term string `json:"term" yaml:"term"` + Revision int `json:"revision" yaml:"revision"` + CreatedOn time.Time `json:"created-on" yaml:"createdon"` +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/terms-client/api/wireformat/entities_test.go juju-core-2.0~beta15/src/github.com/juju/terms-client/api/wireformat/entities_test.go --- juju-core-2.0~beta12/src/github.com/juju/terms-client/api/wireformat/entities_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/terms-client/api/wireformat/entities_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,73 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package wireformat_test + +import ( + "encoding/json" + stdtesting "testing" + "time" + + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/yaml.v2" + + "github.com/juju/terms-client/api/wireformat" +) + +func Test(t *stdtesting.T) { + gc.TestingT(t) +} + +type wireformatSuite struct{} + +var _ = gc.Suite(&wireformatSuite{}) + +func (s *wireformatSuite) TestYAML(c *gc.C) { + t := time.Date(2016, 1, 2, 4, 8, 16, 32, time.UTC) + agreement := wireformat.Agreement{ + User: "test-user", + Owner: "test-owner", + Term: "test-term", + Revision: 17, + CreatedOn: wireformat.TimeRFC3339(t), + } + data, err := yaml.Marshal(agreement) + c.Assert(err, jc.ErrorIsNil) + c.Assert(string(data), jc.DeepEquals, `user: test-user +owner: test-owner +term: test-term +revision: 17 +createdon: 2016-01-02T04:08:16Z +`) + var agreementOut wireformat.Agreement + err = yaml.Unmarshal(data, &agreementOut) + c.Assert(err, jc.ErrorIsNil) + c.Assert(agreement.User, gc.Equals, agreementOut.User) + c.Assert(agreement.Owner, gc.Equals, agreementOut.Owner) + c.Assert(agreement.Term, gc.Equals, agreementOut.Term) + c.Assert(agreement.Revision, gc.Equals, agreementOut.Revision) + c.Assert(time.Time(agreement.CreatedOn).Truncate(time.Second), gc.Equals, time.Time(agreementOut.CreatedOn)) +} + +func (s *wireformatSuite) TestJSON(c *gc.C) { + t := time.Date(2016, 1, 2, 4, 8, 16, 32, time.UTC) + agreement := wireformat.Agreement{ + User: "test-user", + Owner: "test-owner", + Term: "test-term", + Revision: 17, + CreatedOn: wireformat.TimeRFC3339(t), + } + data, err := json.Marshal(agreement) + c.Assert(err, jc.ErrorIsNil) + c.Assert(string(data), jc.DeepEquals, `{"user":"test-user","owner":"test-owner","term":"test-term","revision":17,"created-on":"2016-01-02T04:08:16Z"}`) + var agreementOut wireformat.Agreement + err = json.Unmarshal(data, &agreementOut) + c.Assert(err, jc.ErrorIsNil) + c.Assert(agreement.User, gc.Equals, agreementOut.User) + c.Assert(agreement.Owner, gc.Equals, agreementOut.Owner) + c.Assert(agreement.Term, gc.Equals, agreementOut.Term) + c.Assert(agreement.Revision, gc.Equals, agreementOut.Revision) + c.Assert(time.Time(agreement.CreatedOn).Truncate(time.Second), gc.Equals, time.Time(agreementOut.CreatedOn)) +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/terms-client/cmd/cmd_test.go juju-core-2.0~beta15/src/github.com/juju/terms-client/cmd/cmd_test.go --- juju-core-2.0~beta12/src/github.com/juju/terms-client/cmd/cmd_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/terms-client/cmd/cmd_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,348 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the GPLv3, see LICENCE file for details. + +package cmd_test + +import ( + "fmt" + "sync" + "testing" + + "github.com/juju/cmd/cmdtesting" + "github.com/juju/errors" + jujutesting "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/terms-client/api" + "github.com/juju/terms-client/api/wireformat" + "github.com/juju/terms-client/cmd" +) + +func TestPackage(t *testing.T) { + gc.TestingT(t) +} + +var _ = gc.Suite(&commandSuite{}) + +var testTermsAndConditions = "Test Terms and Conditions" + +type commandSuite struct { + client *mockClient +} + +func (s *commandSuite) SetUpTest(c *gc.C) { + s.client = &mockClient{} + + jujutesting.PatchValue(cmd.ClientNew, func(...api.ClientOption) (api.Client, error) { + return s.client, nil + }) + + jujutesting.PatchValue(cmd.ReadFile, func(string) ([]byte, error) { + return []byte(testTermsAndConditions), nil + }) +} + +func (s *commandSuite) TestPushTerm(c *gc.C) { + tests := []struct { + about string + args []string + err string + stdout string + apiCall []interface{} + }{{ + about: "everything works", + args: []string{"test.txt", "test-term", "--format", "json"}, + stdout: `"test-term/1" +`, + apiCall: []interface{}{"", "test-term", testTermsAndConditions}, + }, { + about: "everything works - with owner", + args: []string{"test.txt", "test-owner/test-term", "--format", "json"}, + stdout: `"test-owner/test-term/1" +`, + apiCall: []interface{}{"test-owner", "test-term", testTermsAndConditions}, + }, { + about: "invalid termid", + args: []string{"test.txt", "!!!!!!", "--format", "json"}, + err: `invalid term id argument: wrong term name format "!!!!!!"`, + }, { + about: "fail if specifying term with revision", + args: []string{"test.txt", "test-term/1", "--format", "json"}, + err: "can't specify a revision with a new term", + }, { + about: "fail if specifying term with tenant", + args: []string{"test.txt", "cs:test-term", "--format", "json"}, + err: "can't specify a tenant with a new term", + }, { + about: "unknown args", + args: []string{"test.txt", "test-term", "unknown", "args", "--format", "json"}, + err: "unknown arguments: unknown,args", + }, { + about: "missing args", + args: []string{"test-term", "--format", "json"}, + err: "missing arguments", + }, + } + for i, test := range tests { + s.client.ResetCalls() + c.Logf("running test %d: %s", i, test.about) + ctx, err := cmdtesting.RunCommand(c, cmd.NewPushTermCommand(), test.args...) + if test.err != "" { + c.Assert(err, gc.ErrorMatches, test.err) + } else { + c.Assert(err, jc.ErrorIsNil) + } + if ctx != nil { + c.Assert(cmdtesting.Stdout(ctx), gc.Equals, test.stdout) + } + if len(test.apiCall) > 0 { + s.client.CheckCall(c, 0, "SaveTerm", test.apiCall...) + } + } +} + +func (s *commandSuite) TestShowTerm(c *gc.C) { + s.client.setTerms([]wireformat.Term{{ + Name: "test-term", + Revision: 1, + Content: testTermsAndConditions, + }}) + tests := []struct { + about string + args []string + err string + stdout string + apiCall []interface{} + }{{ + about: "everything works", + args: []string{"test-term/1", "--format", "json"}, + stdout: `{"name":"test-term","revision":1,"created-on":"0001-01-01T00:00:00Z","published":false,"content":"Test Terms and Conditions"} +`, + apiCall: []interface{}{"", "test-term", 1}, + }, { + about: "everything works in yaml", + args: []string{"test-term/1", "--format", "yaml"}, + stdout: `name: test-term +revision: 1 +createdon: 0001-01-01T00:00:00Z +published: false +content: Test Terms and Conditions +`, + apiCall: []interface{}{"", "test-term", 1}, + }, { + about: "cannot parse revision number", + args: []string{"owner/test-term/abc", "--format", "json"}, + err: `invalid term format: invalid revision number "abc" strconv.ParseInt: parsing "abc": invalid syntax`, + }, { + about: "get latest version", + args: []string{"test-term", "--format", "json"}, + stdout: `{"name":"test-term","revision":1,"created-on":"0001-01-01T00:00:00Z","published":false,"content":"Test Terms and Conditions"} +`, + apiCall: []interface{}{"", "test-term", 0}, + }, { + about: "unknown arguments", + args: []string{"test-term/1", "unknown", "arguments", "--format", "json"}, + err: "unknown arguments: unknown,arguments", + }, { + about: "unknown arguments", + args: []string{}, + err: "missing arguments", + }, + } + for i, test := range tests { + s.client.ResetCalls() + c.Logf("running test %d: %s", i, test.about) + ctx, err := cmdtesting.RunCommand(c, cmd.NewShowTermCommand(), test.args...) + if test.err != "" { + c.Assert(err, gc.ErrorMatches, test.err) + } else { + c.Assert(err, jc.ErrorIsNil) + } + if ctx != nil { + c.Assert(cmdtesting.Stdout(ctx), gc.Equals, test.stdout) + } + if len(test.apiCall) > 0 { + s.client.CheckCall(c, 0, "GetTerm", test.apiCall...) + } + } +} + +func (s *commandSuite) TestShowTermsWithOwners(c *gc.C) { + s.client.setTerms([]wireformat.Term{{ + Owner: "owner", + Name: "test-term", + Revision: 1, + Content: testTermsAndConditions, + }}) + tests := []struct { + about string + args []string + err string + stdout string + apiCall []interface{} + }{{ + about: "everything works - with owner", + args: []string{"test-owner/test-term/1", "--format", "json"}, + stdout: `{"owner":"owner","name":"test-term","revision":1,"created-on":"0001-01-01T00:00:00Z","published":false,"content":"Test Terms and Conditions"} +`, + apiCall: []interface{}{"test-owner", "test-term", 1}, + }, { + about: "parse owner/term-name", + args: []string{"test-owner/abc", "--format", "json"}, + stdout: `{"owner":"owner","name":"test-term","revision":1,"created-on":"0001-01-01T00:00:00Z","published":false,"content":"Test Terms and Conditions"} +`, + apiCall: []interface{}{"test-owner", "abc", 0}, + }, { + about: "everything works with owners in yaml", + args: []string{"owner/test-term/1", "--format", "yaml"}, + stdout: `owner: owner +name: test-term +revision: 1 +createdon: 0001-01-01T00:00:00Z +published: false +content: Test Terms and Conditions +`, + apiCall: []interface{}{"owner", "test-term", 1}, + }} + for i, test := range tests { + s.client.ResetCalls() + c.Logf("running test %d: %s", i, test.about) + ctx, err := cmdtesting.RunCommand(c, cmd.NewShowTermCommand(), test.args...) + if test.err != "" { + c.Assert(err, gc.ErrorMatches, test.err) + } else { + c.Assert(err, jc.ErrorIsNil) + } + if ctx != nil { + c.Assert(cmdtesting.Stdout(ctx), gc.Equals, test.stdout) + } + if len(test.apiCall) > 0 { + s.client.CheckCall(c, 0, "GetTerm", test.apiCall...) + } + } +} + +func (s *commandSuite) TestPublishOwnerlessTerm(c *gc.C) { + ctx, err := cmdtesting.RunCommand(c, cmd.NewPublishTermCommand(), "test-term/1") + c.Assert(err, jc.ErrorIsNil) + c.Assert(cmdtesting.Stdout(ctx), gc.Equals, `only terms with owners require publishing +`) + s.client.CheckNoCalls(c) +} + +func (s *commandSuite) TestPublishTerm(c *gc.C) { + tests := []struct { + about string + args []string + err string + stdout string + apiCall []interface{} + }{{ + about: "everything works", + args: []string{"owner/test-term", "--format", "json"}, + stdout: `"owner/name/1" +`, + apiCall: []interface{}{"owner", "test-term", 0}, + }, { + about: "unknown args", + args: []string{"test-term", "unknown", "args"}, + err: "unknown arguments: unknown,args", + }, { + about: "missing args", + args: []string{}, + err: "missing arguments", + }, + } + for i, test := range tests { + s.client.ResetCalls() + c.Logf("running test %d: %s", i, test.about) + ctx, err := cmdtesting.RunCommand(c, cmd.NewPublishTermCommand(), test.args...) + if test.err != "" { + c.Assert(err, gc.ErrorMatches, test.err) + } else { + c.Assert(err, jc.ErrorIsNil) + } + if ctx != nil { + c.Assert(cmdtesting.Stdout(ctx), gc.Equals, test.stdout) + } + if len(test.apiCall) > 0 { + s.client.CheckCall(c, 0, "Publish", test.apiCall...) + } + } +} + +type mockClient struct { + api.Client + jujutesting.Stub + + lock sync.Mutex + user string + terms []wireformat.Term + unsignedTerms []wireformat.Term +} + +func (c *mockClient) setTerms(t []wireformat.Term) { + c.lock.Lock() + defer c.lock.Unlock() + c.terms = t +} + +func (c *mockClient) setUnsignedTerms(t []wireformat.Term) { + c.lock.Lock() + defer c.lock.Unlock() + c.unsignedTerms = t +} + +func (c *mockClient) SaveTerm(owner, name, content string) (string, error) { + c.AddCall("SaveTerm", owner, name, content) + if owner == "" { + return fmt.Sprintf("%s/1", name), nil + } + return fmt.Sprintf("%s/%s/1", owner, name), nil +} + +// GetTerms returns matching Terms and Conditions documents. +func (c *mockClient) GetTerm(owner, name string, revision int) (*wireformat.Term, error) { + c.AddCall("GetTerm", owner, name, revision) + c.lock.Lock() + defer c.lock.Unlock() + + if len(c.terms) == 0 { + return nil, errors.NotFoundf("term") + } + t := c.terms[0] + return &t, nil +} + +// SaveAgreement saves user's agreement to the specified +// revision of the Terms and Conditions document.s +func (c *mockClient) SaveAgreement(agreements *wireformat.SaveAgreements) (*wireformat.SaveAgreementResponses, error) { + c.AddCall("SaveAgreement", agreements) + responses := make([]wireformat.AgreementResponse, len(agreements.Agreements)) + for i, agreement := range agreements.Agreements { + responses[i] = wireformat.AgreementResponse{ + User: c.user, + Owner: agreement.TermOwner, + Term: agreement.TermName, + Revision: agreement.TermRevision, + } + } + return &wireformat.SaveAgreementResponses{Agreements: responses}, nil +} + +func (c *mockClient) GetUnsignedTerms(terms *wireformat.CheckAgreementsRequest) ([]wireformat.GetTermsResponse, error) { + c.MethodCall(c, "GetUnunsignedTerms", terms) + r := make([]wireformat.GetTermsResponse, len(c.unsignedTerms)) + for i, term := range c.terms { + r[i].Owner = term.Owner + r[i].Name = term.Name + r[i].Revision = term.Revision + } + return r, nil +} + +func (c *mockClient) Publish(owner, name string, revision int) (string, error) { + c.MethodCall(c, "Publish", owner, name, revision) + return "owner/name/1", nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/terms-client/cmd/export_test.go juju-core-2.0~beta15/src/github.com/juju/terms-client/cmd/export_test.go --- juju-core-2.0~beta12/src/github.com/juju/terms-client/cmd/export_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/terms-client/cmd/export_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,9 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the GPLv3, see LICENCE file for details. + +package cmd + +var ( + ClientNew = &clientNew + ReadFile = &readFile +) diff -Nru juju-core-2.0~beta12/src/github.com/juju/terms-client/cmd/publish_term.go juju-core-2.0~beta15/src/github.com/juju/terms-client/cmd/publish_term.go --- juju-core-2.0~beta12/src/github.com/juju/terms-client/cmd/publish_term.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/terms-client/cmd/publish_term.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,113 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the GPLv3, see LICENCE file for details. + +package cmd + +import ( + "strings" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/persistent-cookiejar" + "gopkg.in/juju/charm.v6-unstable" + "gopkg.in/macaroon-bakery.v1/httpbakery" + "launchpad.net/gnuflag" + + "github.com/juju/terms-client/api" +) + +const publishTermDoc = ` +publish-term is used to publish a Terms and Conditions document. +Examples +publish-term me/my-terms +` + +// NewPublishTermCommand returns a new command that can be +// used to publish existing owner terms +// Conditions documents. +func NewPublishTermCommand() *publishTermCommand { + return &publishTermCommand{} +} + +type publishTermCommand struct { + cmd.CommandBase + out cmd.Output + + TermID string + TermsServiceLocation string +} + +// SetFlags implements Command.SetFlags. +func (c *publishTermCommand) SetFlags(f *gnuflag.FlagSet) { + // TODO (mattyw) JUJU_TERMS + f.StringVar(&c.TermsServiceLocation, "url", defaultTermServiceLocation, "url of the terms service") + c.out.AddFlags(f, "yaml", cmd.DefaultFormatters) +} + +// Info implements Command.Info. +func (c *publishTermCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "publish-term", + Args: "", + Purpose: "publishes the given terms document", + Doc: publishTermDoc, + } +} + +// Init reads and verifies the arguments. +func (c *publishTermCommand) Init(args []string) error { + if len(args) < 1 { + return errors.New("missing arguments") + } + c.TermID = args[0] + if err := cmd.CheckEmpty(args[1:]); err != nil { + return errors.Errorf("unknown arguments: %v", strings.Join(args[1:], ",")) + } + return nil +} + +// Run implements Command.Run. +func (c *publishTermCommand) Run(ctx *cmd.Context) error { + jar, err := cookiejar.New(&cookiejar.Options{ + Filename: cookieFile(), + }) + if err != nil { + return errors.Trace(err) + } + defer jar.Save() + bakeryClient := httpbakery.NewClient() + bakeryClient.Jar = jar + bakeryClient.VisitWebPage = httpbakery.OpenWebBrowser + + termsClient, err := clientNew( + api.ServiceURL(c.TermsServiceLocation), + api.HTTPClient(bakeryClient), + ) + if err != nil { + return errors.Trace(err) + } + + termsId, err := charm.ParseTerm(c.TermID) + if err != nil { + return errors.Annotate(err, "invalid term format") + } + if termsId.Owner == "" { + c.out.Write(ctx, "only terms with owners require publishing") + return nil + } + + response, err := termsClient.Publish( + termsId.Owner, + termsId.Name, + termsId.Revision, + ) + if err != nil { + return errors.Trace(err) + } + + err = c.out.Write(ctx, response) + if err != nil { + return errors.Trace(err) + } + return nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/terms-client/cmd/push_term.go juju-core-2.0~beta15/src/github.com/juju/terms-client/cmd/push_term.go --- juju-core-2.0~beta12/src/github.com/juju/terms-client/cmd/push_term.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/terms-client/cmd/push_term.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,151 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the GPLv3, see LICENCE file for details. + +package cmd + +import ( + "io/ioutil" + "os" + "path" + "strings" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/persistent-cookiejar" + "github.com/juju/utils" + "gopkg.in/juju/charm.v6-unstable" + "gopkg.in/macaroon-bakery.v1/httpbakery" + "launchpad.net/gnuflag" + + "github.com/juju/terms-client/api" +) + +var ( + defaultTermServiceLocation = "http://localhost:8081" + readFile = ioutil.ReadFile + clientNew = func(options ...api.ClientOption) (api.Client, error) { + return api.NewClient(options...) + } +) + +const pushTermDoc = ` +push-term is used to create a new Terms and Conditions document. +Examples +push-term text.txt user/enterprise-plan + creates a new Terms and Conditions with the content from + file text.txt and the name enterprise-plan and + returns the revision of the created document. +` + +// NewPushTermCommand returns a new command that can be +// used to create new (revisions) of Terms and +// Conditions documents. +func NewPushTermCommand() *pushTermCommand { + return &pushTermCommand{} +} + +// pushTermCommand creates a new Terms and Conditions document. +type pushTermCommand struct { + cmd.CommandBase + out cmd.Output + + TermID string + TermFilename string + TermsServiceLocation string +} + +// SetFlags implements Command.SetFlags. +func (c *pushTermCommand) SetFlags(f *gnuflag.FlagSet) { + // TODO (mattyw) Replace with JUJU_TERMS + f.StringVar(&c.TermsServiceLocation, "url", defaultTermServiceLocation, "url of the terms service") + c.out.AddFlags(f, "yaml", cmd.DefaultFormatters) +} + +// Info implements Command.Info. +func (c *pushTermCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "push-term", + Args: " ", + Purpose: "create new Terms and Conditions document (revision)", + Doc: pushTermDoc, + } +} + +// Init read and verifies the arguments. +func (c *pushTermCommand) Init(args []string) error { + if len(args) < 2 { + return errors.New("missing arguments") + } + + fn, id, args := args[0], args[1], args[2:] + + if err := cmd.CheckEmpty(args); err != nil { + return errors.Errorf("unknown arguments: %v", strings.Join(args, ",")) + } + + c.TermID = id + c.TermFilename = fn + return nil +} + +// Run implements Command.Run. +func (c *pushTermCommand) Run(ctx *cmd.Context) error { + termid, err := charm.ParseTerm(c.TermID) + if err != nil { + return errors.Annotatef(err, "invalid term id argument") + } + if termid.Revision > 0 { + return errors.Errorf("can't specify a revision with a new term") + } + if termid.Tenant != "" { + return errors.Errorf("can't specify a tenant with a new term") + } + data, err := readFile(c.TermFilename) + if err != nil { + return errors.Annotatef(err, "could not read contents of %q", c.TermFilename) + } + + jar, err := cookiejar.New(&cookiejar.Options{ + Filename: cookieFile(), + }) + if err != nil { + return errors.Trace(err) + } + defer jar.Save() + bakeryClient := httpbakery.NewClient() + bakeryClient.Jar = jar + bakeryClient.VisitWebPage = httpbakery.OpenWebBrowser + + termsClient, err := clientNew( + api.ServiceURL(c.TermsServiceLocation), + api.HTTPClient(bakeryClient), + ) + if err != nil { + return errors.Trace(err) + } + + response, err := termsClient.SaveTerm( + termid.Owner, + termid.Name, + string(data), + ) + if err != nil { + return errors.Trace(err) + } + + err = c.out.Write(ctx, response) + if err != nil { + return errors.Trace(err) + } + return nil +} + +// cookieFile returns the path to the cookie used to store authorization +// macaroons. The returned value can be overridden by setting the +// JUJU_COOKIEFILE environment variable. +func cookieFile() string { + if file := os.Getenv("JUJU_COOKIEFILE"); file != "" { + return file + } + return path.Join(utils.Home(), ".go-cookies") +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/terms-client/cmd/show_term.go juju-core-2.0~beta15/src/github.com/juju/terms-client/cmd/show_term.go --- juju-core-2.0~beta12/src/github.com/juju/terms-client/cmd/show_term.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/terms-client/cmd/show_term.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,108 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the GPLv3, see LICENCE file for details. + +package cmd + +import ( + "strings" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/persistent-cookiejar" + "gopkg.in/juju/charm.v6-unstable" + "gopkg.in/macaroon-bakery.v1/httpbakery" + "launchpad.net/gnuflag" + + "github.com/juju/terms-client/api" +) + +const showTermDoc = ` +show-term is used to show a specific Terms and Conditions document. +Examples +show-term enterprise-plan/1 + shows revision 1 of the enterprise-plan Terms and Conditions. +show-term enterprise-plan + shows the latest revision of the enterprise plan Terms and Conditions. +` + +// NewShowTermCommand returns a new command that can be used +// to shows Terms and Conditions document. +func NewShowTermCommand() *showTermCommand { + return &showTermCommand{} +} + +type showTermCommand struct { + cmd.CommandBase + out cmd.Output + + TermID string + TermsServiceLocation string +} + +// SetFlags implements Command.SetFlags. +func (c *showTermCommand) SetFlags(f *gnuflag.FlagSet) { + // TODO (mattyw) Use JUJU_TERMS + f.StringVar(&c.TermsServiceLocation, "url", defaultTermServiceLocation, "url of the terms service") + c.out.AddFlags(f, "yaml", cmd.DefaultFormatters) +} + +// Info implements Command.Info. +func (c *showTermCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "show-term", + Args: "", + Purpose: "shows the specified term", + Doc: showTermDoc, + } +} + +// Init reads and verifies the arguments. +func (c *showTermCommand) Init(args []string) error { + if len(args) < 1 { + return errors.New("missing arguments") + } + + c.TermID = args[0] + if err := cmd.CheckEmpty(args[1:]); err != nil { + return errors.Errorf("unknown arguments: %v", strings.Join(args[1:], ",")) + } + + return nil +} + +// Run implements Command.Run. +func (c *showTermCommand) Run(ctx *cmd.Context) error { + jar, err := cookiejar.New(&cookiejar.Options{ + Filename: cookieFile(), + }) + if err != nil { + return errors.Trace(err) + } + defer jar.Save() + bakeryClient := httpbakery.NewClient() + bakeryClient.Jar = jar + bakeryClient.VisitWebPage = httpbakery.OpenWebBrowser + + termsClient, err := clientNew( + api.ServiceURL(c.TermsServiceLocation), + api.HTTPClient(bakeryClient), + ) + if err != nil { + return errors.Trace(err) + } + termsId, err := charm.ParseTerm(c.TermID) + if err != nil { + return errors.Annotate(err, "invalid term format") + } + + response, err := termsClient.GetTerm(termsId.Owner, termsId.Name, termsId.Revision) + if err != nil { + return errors.Trace(err) + } + + err = c.out.Write(ctx, response) + if err != nil { + return errors.Trace(err) + } + return nil +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/terms-client/dependencies.tsv juju-core-2.0~beta15/src/github.com/juju/terms-client/dependencies.tsv --- juju-core-2.0~beta12/src/github.com/juju/terms-client/dependencies.tsv 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/terms-client/dependencies.tsv 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,27 @@ +github.com/juju/cmd git a11ae7a7436c133e799f025998cbbefd3f6eef7e 2016-06-01T03:55:01Z +github.com/juju/errors git 1b5e39b83d1835fa480e0c2ddefb040ee82d58b3 2015-09-16T12:56:42Z +github.com/juju/go4 git 40d72ab9641a2a8c36a9c46a51e28367115c8e59 2016-02-22T16:32:58Z +github.com/juju/gojsonpointer git afe8b77aa08f272b49e01b82de78510c11f61500 2015-02-04T19:46:29Z +github.com/juju/gojsonreference git f0d24ac5ee330baa21721cdff56d45e4ee42628e 2015-02-04T19:46:33Z +github.com/juju/gojsonschema git e1ad140384f254c82f89450d9a7c8dd38a632838 2015-03-12T17:00:16Z +github.com/juju/httprequest git 796aaafaf712f666df58d31a482c51233038bf9f 2016-05-03T15:03:27Z +github.com/juju/loggo git 8477fc936adf0e382d680310047ca27e128a309a 2015-05-27T03:58:39Z +github.com/juju/persistent-cookiejar git e710b897c13ca52828ca2fc9769465186fd6d15c 2016-03-31T17:12:27Z +github.com/juju/schema git 075de04f9b7d7580d60a1e12a0b3f50bb18e6998 2016-04-20T04:42:03Z +github.com/juju/testing git ccf839b5a07a7a05009f8fa3ec41cd05fb2e0b08 2016-06-24T20:35:24Z +github.com/juju/utils git 6219812829a3542c827c76cc75f416d4e6c94335 2016-07-08T10:00:56Z +github.com/juju/version git 4ae6172c00626779a5a462c3e3d22fc0e889431a 2016-06-03T19:49:58Z +github.com/juju/webbrowser git 54b8c57083b4afb7dc75da7f13e2967b2606a507 2016-03-09T14:36:29Z +github.com/julienschmidt/httprouter git 77a895ad01ebc98a4dc95d8355bc825ce80a56f6 2015-10-13T22:55:20Z +github.com/rogpeppe/fastuuid git 6724a57986aff9bff1a1770e9347036def7c89f6 2015-01-06T09:32:20Z +golang.org/x/crypto git aedad9a179ec1ea11b7064c57cbc6dc30d7724ec 2015-08-30T18:06:42Z +golang.org/x/net git ea47fc708ee3e20177f3ca3716217c4ab75942cb 2015-08-29T23:03:18Z +gopkg.in/check.v1 git 4f90aeace3a26ad7021961c297b22c42160c7b25 2016-01-05T16:49:36Z +gopkg.in/errgo.v1 git 66cb46252b94c1f3d65646f54ee8043ab38d766c 2015-10-07T15:31:57Z +gopkg.in/juju/charm.v6-unstable git a3bb92d047b0892452b6a39ece59b4d3a2ac35b9 2016-07-22T08:34:31Z +gopkg.in/juju/names.v2 git 5426d66579afd36fc63d809dd58806806c2f161f 2016-06-23T03:33:52Z +gopkg.in/macaroon-bakery.v1 git b097c9d99b2537efaf54492e08f7e148f956ba51 2016-05-24T09:38:11Z +gopkg.in/macaroon.v1 git ab3940c6c16510a850e1c2dd628b919f0f3f1464 2015-01-21T11:42:31Z +gopkg.in/mgo.v2 git 29cc868a5ca65f401ff318143f9408d02f4799cc 2016-06-09T18:00:28Z +gopkg.in/yaml.v2 git a83829b6f1293c91addabc89d0571c246397bbf4 2016-03-01T20:40:22Z +launchpad.net/gnuflag bzr roger.peppe@canonical.com-20140716064605-pk32dnmfust02yab 13 diff -Nru juju-core-2.0~beta12/src/github.com/juju/terms-client/LICENSE juju-core-2.0~beta15/src/github.com/juju/terms-client/LICENSE --- juju-core-2.0~beta12/src/github.com/juju/terms-client/LICENSE 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/terms-client/LICENSE 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,680 @@ +All files in this repository are licensed as follows. If you contribute +to this repository, it is assumed that you license your contribution +under the same license unless you state otherwise. + +All files Copyright (C) 2015 Canonical Ltd. unless otherwise specified in the file. + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff -Nru juju-core-2.0~beta12/src/github.com/juju/terms-client/Makefile juju-core-2.0~beta15/src/github.com/juju/terms-client/Makefile --- juju-core-2.0~beta12/src/github.com/juju/terms-client/Makefile 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/terms-client/Makefile 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,41 @@ +# Copyright 2016 Canonical Ltd. +# Licensed under the GPLv3, see LICENCE file for details. +# +PROJECT := github.com/juju/terms-client + +ifndef GOBIN +GOBIN := $(shell mkdir -p $(GOPATH)/bin; realpath $(GOPATH))/bin +else +REAL_GOBIN := $(shell mkdir -p $(GOBIN); realpath $(GOBIN)) +GOBIN := $(REAL_GOBIN) +endif + +godeps: $(GOBIN)/godeps + GOBIN=$(GOBIN) $(GOBIN)/godeps -u dependencies.tsv + +$(GOBIN)/godeps: $(GOBIN) + GOBIN=$(GOBIN) go get github.com/rogpeppe/godeps + +ifeq ($(MAKE_GODEPS),true) +.PHONY: deps +deps: godeps +else +deps: + @echo "Skipping godeps. export MAKE_GODEPS = true to enable." +endif + +build: deps + GOBIN=$(GOBIN) go build -a $(PROJECT)/... + +install: deps + GOBIN=$(GOBIN) go install -v $(PROJECT)/... + +check: build + GOBIN=$(GOBIN) go test -test.timeout=1200s $(PROJECT)/... + +land: check race + +race: build + GOBIN=$(GOBIN) go test -test.timeout=1200s -race $(PROJECT)/... + +.PHONY: build check install clean race land diff -Nru juju-core-2.0~beta12/src/github.com/juju/terms-client/README.md juju-core-2.0~beta15/src/github.com/juju/terms-client/README.md --- juju-core-2.0~beta12/src/github.com/juju/terms-client/README.md 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/terms-client/README.md 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1 @@ +# terms-client diff -Nru juju-core-2.0~beta12/src/github.com/juju/testing/checkers/log.go juju-core-2.0~beta15/src/github.com/juju/testing/checkers/log.go --- juju-core-2.0~beta12/src/github.com/juju/testing/checkers/log.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/testing/checkers/log.go 2016-08-16 08:56:25.000000000 +0000 @@ -31,7 +31,7 @@ return fmt.Sprintf("SimpleMessages{\n%s\n}", strings.Join(out, "\n")) } -func logToSimpleMessages(log []loggo.TestLogValues) SimpleMessages { +func logToSimpleMessages(log []loggo.Entry) SimpleMessages { out := make(SimpleMessages, len(log)) for i, val := range log { out[i].Level = val.Level @@ -47,10 +47,10 @@ func (checker *logMatches) Check(params []interface{}, _ []string) (result bool, error string) { var obtained SimpleMessages switch params[0].(type) { - case []loggo.TestLogValues: - obtained = logToSimpleMessages(params[0].([]loggo.TestLogValues)) + case []loggo.Entry: + obtained = logToSimpleMessages(params[0].([]loggo.Entry)) default: - return false, "Obtained value must be of type []loggo.TestLogValues or SimpleMessage" + return false, "Obtained value must be of type []loggo.Entry or SimpleMessage" } var expected SimpleMessages diff -Nru juju-core-2.0~beta12/src/github.com/juju/testing/checkers/log_test.go juju-core-2.0~beta15/src/github.com/juju/testing/checkers/log_test.go --- juju-core-2.0~beta12/src/github.com/juju/testing/checkers/log_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/testing/checkers/log_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -43,7 +43,7 @@ var _ = gc.Suite(&LogMatchesSuite{}) func (s *LogMatchesSuite) TestMatchSimpleMessage(c *gc.C) { - log := []loggo.TestLogValues{ + log := []loggo.Entry{ {Level: loggo.INFO, Message: "foo bar"}, {Level: loggo.INFO, Message: "12345"}, } @@ -68,7 +68,7 @@ } func (s *LogMatchesSuite) TestMatchSimpleMessages(c *gc.C) { - log := []loggo.TestLogValues{ + log := []loggo.Entry{ {Level: loggo.INFO, Message: "foo bar"}, {Level: loggo.INFO, Message: "12345"}, } @@ -93,7 +93,7 @@ } func (s *LogMatchesSuite) TestMatchStrings(c *gc.C) { - log := []loggo.TestLogValues{ + log := []loggo.Entry{ {Level: loggo.INFO, Message: "foo bar"}, {Level: loggo.INFO, Message: "12345"}, } @@ -103,7 +103,7 @@ } func (s *LogMatchesSuite) TestMatchInexact(c *gc.C) { - log := []loggo.TestLogValues{ + log := []loggo.Entry{ {Level: loggo.INFO, Message: "foo bar"}, {Level: loggo.INFO, Message: "baz"}, {Level: loggo.DEBUG, Message: "12345"}, @@ -169,11 +169,11 @@ expected := jc.SimpleMessages{} result, err := jc.LogMatches.Check([]interface{}{obtained, expected}, nil) c.Assert(result, gc.Equals, false) - c.Assert(err, gc.Equals, "Obtained value must be of type []loggo.TestLogValues or SimpleMessage") + c.Assert(err, gc.Equals, "Obtained value must be of type []loggo.Entry or SimpleMessage") } func (s *LogMatchesSuite) TestLogMatchesOnlyAcceptsStringOrSimpleMessages(c *gc.C) { - obtained := []loggo.TestLogValues{ + obtained := []loggo.Entry{ {Level: loggo.INFO, Message: "foo bar"}, {Level: loggo.INFO, Message: "baz"}, {Level: loggo.DEBUG, Message: "12345"}, @@ -185,7 +185,7 @@ } func (s *LogMatchesSuite) TestLogMatchesFailsOnInvalidRegex(c *gc.C) { - var obtained interface{} = []loggo.TestLogValues{{Level: loggo.INFO, Message: "foo bar"}} + var obtained interface{} = []loggo.Entry{{Level: loggo.INFO, Message: "foo bar"}} var expected interface{} = []string{"[]foo"} result, err := jc.LogMatches.Check([]interface{}{obtained, expected}, nil /* unused */) diff -Nru juju-core-2.0~beta12/src/github.com/juju/testing/clock.go juju-core-2.0~beta15/src/github.com/juju/testing/clock.go --- juju-core-2.0~beta12/src/github.com/juju/testing/clock.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/testing/clock.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,247 @@ +// Copyright 2015-2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package testing + +import ( + "sort" + "sync" + "time" + + "github.com/juju/utils/clock" +) + +// timerClock exposes the underlying Clock's capabilities to a Timer. +type timerClock interface { + reset(id int, d time.Duration) bool + stop(id int) bool +} + +// Timer implements a mock clock.Timer for testing purposes. +type Timer struct { + ID int + clock timerClock +} + +// Reset is part of the clock.Timer interface. +func (t *Timer) Reset(d time.Duration) bool { + return t.clock.reset(t.ID, d) +} + +// Stop is part of the clock.Timer interface. +func (t *Timer) Stop() bool { + return t.clock.stop(t.ID) +} + +// stoppedTimer is a no-op implementation of clock.Timer. +type stoppedTimer struct{} + +// Reset is part of the clock.Timer interface. +func (stoppedTimer) Reset(time.Duration) bool { return false } + +// Stop is part of the clock.Timer interface. +func (stoppedTimer) Stop() bool { return false } + +// Clock implements a mock clock.Clock for testing purposes. +type Clock struct { + mu sync.Mutex + now time.Time + alarms []alarm + currentAlarmID int + notifyAlarms chan struct{} +} + +// NewClock returns a new clock set to the supplied time. If your SUT needs to +// call After, AfterFunc, or Timer.Reset more than 1024 times: (1) you have +// probably written a bad test; and (2) you'll need to read from the Alarms +// chan to keep the buffer clear. +func NewClock(now time.Time) *Clock { + return &Clock{ + now: now, + notifyAlarms: make(chan struct{}, 1024), + } +} + +// Now is part of the clock.Clock interface. +func (clock *Clock) Now() time.Time { + clock.mu.Lock() + defer clock.mu.Unlock() + return clock.now +} + +// After is part of the clock.Clock interface. +func (clock *Clock) After(d time.Duration) <-chan time.Time { + defer clock.notifyAlarm() + clock.mu.Lock() + defer clock.mu.Unlock() + notify := make(chan time.Time, 1) + if d <= 0 { + notify <- clock.now + } else { + clock.setAlarm(clock.now.Add(d), func() { notify <- clock.now }) + } + return notify +} + +// AfterFunc is part of the clock.Clock interface. +func (clock *Clock) AfterFunc(d time.Duration, f func()) clock.Timer { + defer clock.notifyAlarm() + clock.mu.Lock() + defer clock.mu.Unlock() + if d <= 0 { + f() + return &stoppedTimer{} + } + id := clock.setAlarm(clock.now.Add(d), f) + return &Timer{id, clock} +} + +// Advance advances the result of Now by the supplied duration, and sends +// the "current" time on all alarms which are no longer "in the future". +func (clock *Clock) Advance(d time.Duration) { + clock.mu.Lock() + defer clock.mu.Unlock() + clock.now = clock.now.Add(d) + triggered := 0 + for _, alarm := range clock.alarms { + if clock.now.Before(alarm.time) { + break + } + alarm.trigger() + triggered++ + } + clock.alarms = clock.alarms[triggered:] +} + +// Alarms returns a channel on which you can read one value for every call to +// After and AfterFunc; and for every successful Timer.Reset backed by this +// Clock. It might not be elegant but it's necessary when testing time logic +// that runs on a goroutine other than that of the test. +func (clock *Clock) Alarms() <-chan struct{} { + return clock.notifyAlarms +} + +// reset is the underlying implementation of clock.Timer.Reset, which may be +// called by any Timer backed by this Clock. +func (clock *Clock) reset(id int, d time.Duration) bool { + clock.mu.Lock() + defer clock.mu.Unlock() + + for i, alarm := range clock.alarms { + if id == alarm.ID { + defer clock.notifyAlarm() + clock.alarms[i].time = clock.now.Add(d) + sort.Sort(byTime(clock.alarms)) + return true + } + } + return false +} + +// stop is the underlying implementation of clock.Timer.Reset, which may be +// called by any Timer backed by this Clock. +func (clock *Clock) stop(id int) bool { + clock.mu.Lock() + defer clock.mu.Unlock() + + for i, alarm := range clock.alarms { + if id == alarm.ID { + clock.alarms = removeFromSlice(clock.alarms, i) + return true + } + } + return false +} + +// setAlarm adds an alarm at time t. +// It also sorts the alarms and increments the current ID by 1. +func (clock *Clock) setAlarm(t time.Time, trigger func()) int { + alarm := alarm{ + time: t, + trigger: trigger, + ID: clock.currentAlarmID, + } + clock.alarms = append(clock.alarms, alarm) + sort.Sort(byTime(clock.alarms)) + clock.currentAlarmID = clock.currentAlarmID + 1 + return alarm.ID +} + +// notifyAlarm sends a value on the channel exposed by Alarms(). +func (clock *Clock) notifyAlarm() { + select { + case clock.notifyAlarms <- struct{}{}: + default: + panic("alarm notification buffer full") + } +} + +// alarm records the time at which we're expected to execute trigger. +type alarm struct { + ID int + time time.Time + trigger func() +} + +// byTime is used to sort alarms by time. +type byTime []alarm + +func (a byTime) Len() int { return len(a) } +func (a byTime) Less(i, j int) bool { return a[i].time.Before(a[j].time) } +func (a byTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +// removeFromSlice removes item at the specified index from the slice. +func removeFromSlice(sl []alarm, index int) []alarm { + return append(sl[:index], sl[index+1:]...) +} + +type StubClock struct { + *Stub + + ReturnNow time.Time + ReturnAfter <-chan time.Time + ReturnAfterFunc clock.Timer +} + +func NewStubClock(stub *Stub) *StubClock { + return &StubClock{ + Stub: stub, + } +} + +func (s *StubClock) Now() time.Time { + s.AddCall("Now") + s.NextErr() // pop one off + return s.ReturnNow +} + +func (s *StubClock) After(d time.Duration) <-chan time.Time { + s.AddCall("After", d) + s.NextErr() // pop one off + return s.ReturnAfter +} + +func (s *StubClock) AfterFunc(d time.Duration, f func()) clock.Timer { + s.AddCall("AfterFunc", d, f) + s.NextErr() // pop one off + return s.ReturnAfterFunc +} + +// AutoAdvancingClock wraps a clock.Clock, calling the Advance +// function whenever After or AfterFunc are called. +type AutoAdvancingClock struct { + clock.Clock + Advance func(time.Duration) +} + +func (c *AutoAdvancingClock) After(d time.Duration) <-chan time.Time { + ch := c.Clock.After(d) + c.Advance(d) + return ch +} + +func (c *AutoAdvancingClock) AfterFunc(d time.Duration, f func()) clock.Timer { + t := c.Clock.AfterFunc(d, f) + c.Advance(d) + return t +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/testing/log.go juju-core-2.0~beta15/src/github.com/juju/testing/log.go --- juju-core-2.0~beta12/src/github.com/juju/testing/log.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/testing/log.go 2016-08-16 08:56:25.000000000 +0000 @@ -4,14 +4,17 @@ package testing import ( + "flag" "fmt" "os" - "time" + "path/filepath" "github.com/juju/loggo" gc "gopkg.in/check.v1" ) +var logLocation = flag.Bool("loggo.location", false, "Also log the location of the loggo call") + // LoggingSuite redirects the juju logger to the test logger // when embedded in a gocheck suite type. type LoggingSuite struct{} @@ -27,10 +30,19 @@ return "DEBUG" }() -func (w *gocheckWriter) Write(level loggo.Level, module, filename string, line int, timestamp time.Time, message string) { +func (w *gocheckWriter) Write(entry loggo.Entry) { + filename := filepath.Base(entry.Filename) + var message string + if *logLocation { + message = fmt.Sprintf("%s %s %s:%d %s", entry.Level, entry.Module, filename, entry.Line, entry.Message) + } else { + message = fmt.Sprintf("%s %s %s", entry.Level, entry.Module, entry.Message) + } // Magic calldepth value... - // TODO (frankban) Document why we are using this magic value. - w.c.Output(3, fmt.Sprintf("%s %s %s", level, module, message)) + // The value says "how far up the call stack do we go to find the location". + // It is used to match the standard library log function, and isn't actually + // used by gocheck. + w.c.Output(3, message) } func (s *LoggingSuite) SetUpSuite(c *gc.C) { @@ -38,8 +50,7 @@ } func (s *LoggingSuite) TearDownSuite(c *gc.C) { - loggo.ResetLoggers() - loggo.ResetWriters() + loggo.ResetLogging() } func (s *LoggingSuite) SetUpTest(c *gc.C) { @@ -51,17 +62,16 @@ type discardWriter struct{} -func (discardWriter) Write(level loggo.Level, name, filename string, line int, timestamp time.Time, message string) { +func (discardWriter) Write(entry loggo.Entry) { } func (s *LoggingSuite) setUp(c *gc.C) { - loggo.ResetWriters() + loggo.ResetLogging() // Don't use the default writer for the test logging, which // means we can still get logging output from tests that // replace the default writer. - loggo.ReplaceDefaultWriter(discardWriter{}) - loggo.RegisterWriter("loggingsuite", &gocheckWriter{c}, loggo.TRACE) - loggo.ResetLoggers() + loggo.RegisterWriter(loggo.DefaultWriterName, discardWriter{}) + loggo.RegisterWriter("loggingsuite", &gocheckWriter{c}) err := loggo.ConfigureLoggers(logConfig) c.Assert(err, gc.IsNil) } diff -Nru juju-core-2.0~beta12/src/github.com/juju/testing/log_test.go juju-core-2.0~beta15/src/github.com/juju/testing/log_test.go --- juju-core-2.0~beta12/src/github.com/juju/testing/log_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/testing/log_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -40,7 +40,7 @@ c.Assert(c.GetTestLog(), gc.Matches, ".*DEBUG test message 1\n"+ - ".*TRACE juju message 3\n", + ".*TRACE juju message 3\n$", ) c.Assert(logger.EffectiveLogLevel(), gc.Equals, loggo.WARNING) c.Assert(jujuLogger.EffectiveLogLevel(), gc.Equals, loggo.WARNING) diff -Nru juju-core-2.0~beta12/src/github.com/juju/utils/tar/tar.go juju-core-2.0~beta15/src/github.com/juju/utils/tar/tar.go --- juju-core-2.0~beta12/src/github.com/juju/utils/tar/tar.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/utils/tar/tar.go 2016-08-16 08:56:25.000000000 +0000 @@ -50,6 +50,9 @@ // or empty sting and error in case of error. // We use a base64 encoded sha1 hash, because this is the hash // used by RFC 3230 Digest headers in http responses +// It is not safe to mutate files passed during this function, +// however at least the bytes up to the inital size are written +// successfully if no error is returned. func TarFiles(fileList []string, target io.Writer, strip string) (shaSum string, err error) { shahash := sha1.New() if err := tarAndHashFiles(fileList, target, strip, shahash); err != nil { @@ -111,7 +114,9 @@ return nil } if !fInfo.IsDir() { - if _, err := io.Copy(tarw, f); err != nil { + // Limit data copied to inital stat size included in tar header + // or ErrWriteTooLong is raised by archive/tar Writer. + if _, err := io.CopyN(tarw, f, fInfo.Size()); err != nil { return fmt.Errorf("failed to write %q: %v", fileName, err) } return nil diff -Nru juju-core-2.0~beta12/src/github.com/juju/utils/tls.go juju-core-2.0~beta15/src/github.com/juju/utils/tls.go --- juju-core-2.0~beta12/src/github.com/juju/utils/tls.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/utils/tls.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,68 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package utils + +import ( + "crypto/tls" + "net/http" + "time" +) + +// NewHttpTLSTransport returns a new http.Transport constructed with the TLS config +// and the necessary parameters for Juju. +func NewHttpTLSTransport(tlsConfig *tls.Config) *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. + transport := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: tlsConfig, + DisableKeepAlives: true, + Dial: dial, + TLSHandshakeTimeout: 10 * time.Second, + } + registerFileProtocol(transport) + return transport +} + +// knownGoodCipherSuites contains the list of secure cipher suites to use +// with tls.Config. This list matches those that Go 1.6 implements from +// https://wiki.mozilla.org/Security/Server_Side_TLS#Recommended_configurations. +// +// https://tools.ietf.org/html/rfc7525#section-4.2 excludes RSA exchange completely +// so we could be more strict if all our clients will support +// TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256/384. Unfortunately Go's crypto library +// is limited and doesn't support DHE-RSA-AES256-GCM-SHA384 and +// DHE-RSA-AES256-SHA256, which are part of the recommended set. +// +// Unfortunately we can't drop the RSA algorithms because our servers aren't +// generating ECDHE keys. +var knownGoodCipherSuites = []uint16{ + // These are technically useless for Juju, since we use an RSA certificate, + // but they also don't hurt anything, and supporting an ECDSA certificate + // could be useful in the future. + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + + // Windows doesn't support GCM currently, so we need these for RSA support. + tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, +} + +// SecureTLSConfig returns a tls.Config that conforms to Juju's security +// standards, so as to avoid known security vulnerabilities in certain +// configurations. +// +// Currently it excludes RC4 implementations from the available ciphersuites, +// requires ciphersuites that provide forward secrecy, and sets the minimum TLS +// version to 1.2. +func SecureTLSConfig() *tls.Config { + return &tls.Config{ + CipherSuites: knownGoodCipherSuites, + MinVersion: tls.VersionTLS12, + } +} diff -Nru juju-core-2.0~beta12/src/github.com/juju/utils/tls_test.go juju-core-2.0~beta15/src/github.com/juju/utils/tls_test.go --- juju-core-2.0~beta12/src/github.com/juju/utils/tls_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/utils/tls_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,135 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package utils + +import ( + "fmt" + "io/ioutil" + "net/http" + "os/exec" + "path/filepath" + "runtime" + + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" +) + +type TLSSuite struct{} + +var _ = gc.Suite(TLSSuite{}) + +func (TLSSuite) TestWinCipher(c *gc.C) { + if runtime.GOOS != "windows" { + c.Skip("Windows-specific test.") + } + + d := c.MkDir() + go runServer(d, c) + + out := filepath.Join(d, "out.txt") + + // this script enables TLS 1.2, accepts whatever cert the server has (since + // it's self-signed), then tries to connect to the web server. + script := fmt.Sprintf(`[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true} +(New-Object System.Net.WebClient).DownloadFile("https://127.0.0.1:10443", "%s") +`, out) + err := runPS(d, script) + c.Assert(err, jc.ErrorIsNil) + b, err := ioutil.ReadFile(out) + c.Assert(err, jc.ErrorIsNil) + c.Assert(string(b), gc.Equals, "This is an example server.\n") +} + +func runServer(dir string, c *gc.C) { + handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.Write([]byte("This is an example server.\n")) + }) + + s := http.Server{ + Addr: ":10443", + TLSConfig: SecureTLSConfig(), + Handler: handler, + } + + certFile := filepath.Join(dir, "cert.pem") + err := ioutil.WriteFile(certFile, []byte(cert), 0600) + c.Assert(err, jc.ErrorIsNil) + keyFile := filepath.Join(dir, "key.pem") + err = ioutil.WriteFile(keyFile, []byte(key), 0600) + c.Assert(err, jc.ErrorIsNil) + + err = s.ListenAndServeTLS(certFile, keyFile) + c.Assert(err, jc.ErrorIsNil) +} + +func runPS(dir, script string) error { + scriptFile := filepath.Join(dir, "script.ps1") + args := []string{ + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", "RemoteSigned", + "-File", scriptFile, + } + // Exceptions don't result in a non-zero exit code by default + // when using -File. The exit code of an explicit "exit" when + // using -Command is ignored and results in an exit code of 1. + // We use -File and trap exceptions to cover both. + script = "trap {Write-Error $_; exit 1}\n" + script + if err := ioutil.WriteFile(scriptFile, []byte(script), 0600); err != nil { + return err + } + cmd := exec.Command("powershell.exe", args...) + return cmd.Run() +} + +const ( + cert = `-----BEGIN CERTIFICATE----- +MIIC9TCCAd2gAwIBAgIRALhL8rNhi3x29T8g/AwK9bAwDQYJKoZIhvcNAQELBQAw +EjEQMA4GA1UEChMHQWNtZSBDbzAeFw0xNjA3MjYxNjI4MzRaFw0xNzA3MjYxNjI4 +MzRaMBIxEDAOBgNVBAoTB0FjbWUgQ28wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCaVIZUmQdBTXYATbTmMhscCTUSNt+Dn3OP8w2v/2QJyUz3s1eiiuec +ymD+6TC7lNjzIXhFJnHTyuo/p2d2lHNvbQmUh0kMjPxnIDaCqWZXcjR+vnFo4jgl +VtxCqPG2zi62kZxB0Pu9DzJ7AlqF9BTbpu0INDyFzLJtj73RIv00kRDTpFzHQSNN +tzi9ZzKY7ZS6urftqXc4pqoaSyFXqw7uSNcBcr7Cc8oXIz5tQoVU5m0uKBGOQvwC +b+ICd+RIYS09L1E76UGpDcrJ0LQlysQ/ZMmSsA5YHGf5KE+N0WnWdQCADq3voQra +q47HBpH+ByA1F1REMwgMoFNZRNrEHdXFAgMBAAGjRjBEMA4GA1UdDwEB/wQEAwIF +oDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMA8GA1UdEQQIMAaH +BH8AAAEwDQYJKoZIhvcNAQELBQADggEBAJPwxR3fhEpZz2JB2dAUuj0KqFD7uPQp +m30Slu3cihqQkoaGiSMQdGSZ/VnieHbS/XaZo8JqixU8RucYjVT2eM5YRgcGxU91 +L4yJfPm7qPwGIvwpfqlZK5GcpC/qk3joNqL43gGfn6vbtqw+wF33yfcyTlTO1hwN +vZSU4HC3Hz+FoFnmqkW5lXiuggm/jsdWqPIDA0NJHrws/wjqu3T+wQcfTvIwIPMG +WFmUP5hvWD/9HpizJqROhRZwfsJHDpHDu0nKgSDnV1gX2S5XaUsUWu53V/Hczbo0 +fSD4wg+Zd/x3fh+EpOd1qbHmXrDWSs4z/T61yKzrgENd/kSncJC38pg= +-----END CERTIFICATE----- +` + key = `-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAmlSGVJkHQU12AE205jIbHAk1Ejbfg59zj/MNr/9kCclM97NX +oornnMpg/ukwu5TY8yF4RSZx08rqP6dndpRzb20JlIdJDIz8ZyA2gqlmV3I0fr5x +aOI4JVbcQqjxts4utpGcQdD7vQ8yewJahfQU26btCDQ8hcyybY+90SL9NJEQ06Rc +x0EjTbc4vWcymO2Uurq37al3OKaqGkshV6sO7kjXAXK+wnPKFyM+bUKFVOZtLigR +jkL8Am/iAnfkSGEtPS9RO+lBqQ3KydC0JcrEP2TJkrAOWBxn+ShPjdFp1nUAgA6t +76EK2quOxwaR/gcgNRdURDMIDKBTWUTaxB3VxQIDAQABAoIBAQCBktX10UW2HkMk +nhlz7D22nERito+TAx0Tjw2+5r4nOUvV7E13uwgbLA+j9kVkOOStvTwtUsne+E8U +gojrllgVBYc1nSBH2VdRfkpGCdRTNx+8CklNtiFNuE/V5+KJiTLPNhHrcHrrkQbh +IGjAbt3UTaJVcQYfkG1+b2D/ZlERAC0s0lnqidoqeAaiXDmDesIz+gXkpfbt5mHa +f/LRFRvjtBDjCOTkZ3OdFeSyW+z4zs75vvk3amQNixGW74obFUZFBvF81yUZH7kf +bWBMJMIo024oo4Rpi5k279gx2pWNLHQ68AWF/zLbu32xGrSQuTelVU5MNgEDVB9W +3T01iHwBAoGBAMGGslxNYcf2lg0pW6II4EmOSvbdZ5z9kmV92wkN4zTP3Tzr/Kzf +UMALczvCBYplo6Q6nR+TvRukl8Mr1e5m7Ophfv21vZfprs2YXigL9vTZKRsis8Fk +QSK2kO9CVnWjFu11jYCDN9nUD+9lB+ry9grdY0744a8dTsxmZ1m1ZwA1AoGBAMwm +nF0+OnMkILfsnaK6PVsJBUI5N/j05P/mDQcDZdQMVOBSh/kceQ4LWHXdL0lMVLBY +pGPXqwsO8Q/d2R2oI1acgIFcl73FTchrQd1YaHmnyfqInhKt9QOXj1c0ii4BL3ff +iGVf4gqQVH0B2nK7pjkBlwvpjsYFVDHP9/xkXlFRAoGAC9mgoFBItYLe601mBAUB +Ht/srTMffhh012wedm54RCqaRHm6zicafbf1xWn7Bt90ZsEEEAPu53tro5LSlbeN +uEhiC00On/e6MXKsCU26QIHvp263jRcDegmt1Ei+nJNw+vdgw8bFK7x1gVYxZuyb +rkyiIRrSTvO/eHqox3B5LyUCgYAmKZWTTJ2qhndjSmURVVVA3kfQYFfZPxZLy9pl +lDoF0KRRJrxqUetDN9W6erVrM0ylhnx8eYVs1Mc1WxhKFfM9LpZLGF75R5fJvlsa +oHsvOrFkFwPNpB0oJb3S5GxsOyZ/dxbNNIZRyTcyAxWt2uwwvd5ZiLh6xeY+RY0q +7iw/cQKBgQCaWJ8bSNNhQeaBSW5IVHFctYtLPv9aPHagBdJkKmeb06HWQHi+AvkY +nd0dgM/TfgtnuhbVS4ISkT4vZoSn84hOE7BG5rSPE+/q24Wv5gG0PI1sky8tmXzX +juAEWSJVCSE0TK/mvBVdlyKOJoEgtfMcRfDQfA1rI9My0rU+/Y5A0w== +-----END RSA PRIVATE KEY-----` +) diff -Nru juju-core-2.0~beta12/src/github.com/juju/utils/tls_transport_go12.go juju-core-2.0~beta15/src/github.com/juju/utils/tls_transport_go12.go --- juju-core-2.0~beta12/src/github.com/juju/utils/tls_transport_go12.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/utils/tls_transport_go12.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,29 +0,0 @@ -// Copyright 2016 Canonical Ltd. -// Licensed under the LGPLv3, see LICENCE file for details. - -// +build !go1.3 - -package utils - -import ( - "crypto/tls" - "net/http" -) - -// NewHttpTLSTransport returns a new http.Transport constructed with the TLS config -// and the necessary parameters for Juju. -func NewHttpTLSTransport(tlsConfig *tls.Config) *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. - transport := &http.Transport{ - Proxy: http.ProxyFromEnvironment, - TLSClientConfig: tlsConfig, - DisableKeepAlives: true, - Dial: dial, - // Go 1.2 does not support the TLSHandshaketimeout - // TLSHandshakeTimeout: 10 * time.Second, - } - registerFileProtocol(transport) - return transport -} diff -Nru juju-core-2.0~beta12/src/github.com/juju/utils/tls_transport_go13.go juju-core-2.0~beta15/src/github.com/juju/utils/tls_transport_go13.go --- juju-core-2.0~beta12/src/github.com/juju/utils/tls_transport_go13.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/github.com/juju/utils/tls_transport_go13.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,63 +0,0 @@ -// Copyright 2016 Canonical Ltd. -// Licensed under the LGPLv3, see LICENCE file for details. - -// +build go1.3 - -package utils - -import ( - "crypto/tls" - "net/http" - "time" -) - -// NewHttpTLSTransport returns a new http.Transport constructed with the TLS config -// and the necessary parameters for Juju. -func NewHttpTLSTransport(tlsConfig *tls.Config) *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. - transport := &http.Transport{ - Proxy: http.ProxyFromEnvironment, - TLSClientConfig: tlsConfig, - DisableKeepAlives: true, - Dial: dial, - TLSHandshakeTimeout: 10 * time.Second, - } - registerFileProtocol(transport) - return transport -} - -// knownGoodCipherSuites contains the list of secure cipher suites to use -// with tls.Config. This list matches those that Go 1.6 implements from -// https://wiki.mozilla.org/Security/Server_Side_TLS#Recommended_configurations. -// -// https://tools.ietf.org/html/rfc7525#section-4.2 excludes RSA exchange completely -// so we could be more strict if all our clients will support -// TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256/384. Unfortunately Go's crypto library -// is limited and doesn't support DHE-RSA-AES256-GCM-SHA384 and -// DHE-RSA-AES256-SHA256, which are part of the recommended set. -// -// Unfortunately we can't drop the RSA algorithms because our servers aren't -// generating ECDHE keys. -var knownGoodCipherSuites = []uint16{ - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, -} - -// SecureTLSConfig returns a tls.Config that conforms to Juju's security -// standards, so as to avoid known security vulnerabilities in certain -// configurations. -// -// Currently it excludes RC4 implementations from the available ciphersuites, -// requires ciphersuites that provide forward secrecy, and sets the minimum TLS -// version to 1.2. -func SecureTLSConfig() *tls.Config { - return &tls.Config{ - CipherSuites: knownGoodCipherSuites, - MinVersion: tls.VersionTLS12, - } -} diff -Nru juju-core-2.0~beta12/src/gopkg.in/juju/charm.v6-unstable/internal/test-charm-repo/quantal/terms/metadata.yaml juju-core-2.0~beta15/src/gopkg.in/juju/charm.v6-unstable/internal/test-charm-repo/quantal/terms/metadata.yaml --- juju-core-2.0~beta12/src/gopkg.in/juju/charm.v6-unstable/internal/test-charm-repo/quantal/terms/metadata.yaml 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/gopkg.in/juju/charm.v6-unstable/internal/test-charm-repo/quantal/terms/metadata.yaml 2016-08-16 08:56:25.000000000 +0000 @@ -2,4 +2,4 @@ summary: "Sample charm with terms and conditions" description: | That's a boring charm that requires certain terms. -terms: ["term1/1", "term2"] +terms: ["term1/1", "term2", "owner/term3/1"] diff -Nru juju-core-2.0~beta12/src/gopkg.in/juju/charm.v6-unstable/meta.go juju-core-2.0~beta15/src/gopkg.in/juju/charm.v6-unstable/meta.go --- juju-core-2.0~beta12/src/gopkg.in/juju/charm.v6-unstable/meta.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/gopkg.in/juju/charm.v6-unstable/meta.go 2016-08-16 08:56:25.000000000 +0000 @@ -15,6 +15,7 @@ "github.com/juju/schema" "github.com/juju/utils" "github.com/juju/version" + "gopkg.in/juju/names.v2" "gopkg.in/yaml.v2" "gopkg.in/juju/charm.v6-unstable/hooks" @@ -241,16 +242,134 @@ return result } -var termNameRE = regexp.MustCompile("^[a-z]+([a-z0-9-]+)(/[0-9]+)?$") +var validTermName = regexp.MustCompile(`^[a-zA-Z][\w-]+$`) -func checkTerm(s string) error { - match := termNameRE.FindStringSubmatch(s) - if match == nil { - return fmt.Errorf("invalid term name %q: must match %s", s, termNameRE.String()) +// TermsId represents a single term id. The term can either be owned +// or "public" (meaning there is no owner). +// The Revision starts at 1. Therefore a value of 0 means the revision +// is unset. +type TermsId struct { + Tenant string + Owner string + Name string + Revision int +} + +// Validate returns an error if the Term contains invalid data. +func (t *TermsId) Validate() error { + if t.Tenant != "" && t.Tenant != "cs" { + if !validTermName.MatchString(t.Tenant) { + return fmt.Errorf("wrong term tenant format %q", t.Tenant) + } + } + if t.Owner != "" && !names.IsValidUser(t.Owner) { + return fmt.Errorf("wrong owner format %q", t.Owner) + } + if !validTermName.MatchString(t.Name) { + return fmt.Errorf("wrong term name format %q", t.Name) + } + if t.Revision < 0 { + return fmt.Errorf("negative term revision") } return nil } +// String returns the term in canonical form. +// This would be one of: +// tenant:owner/name/revision +// tenant:name +// owner/name/revision +// owner/name +// name/revision +// name +func (t *TermsId) String() string { + id := make([]byte, 0, len(t.Tenant)+1+len(t.Owner)+1+len(t.Name)+4) + if t.Tenant != "" { + id = append(id, t.Tenant...) + id = append(id, ':') + } + if t.Owner != "" { + id = append(id, t.Owner...) + id = append(id, '/') + } + id = append(id, t.Name...) + if t.Revision != 0 { + id = append(id, '/') + id = strconv.AppendInt(id, int64(t.Revision), 10) + } + return string(id) +} + +// ParseTerm takes a termID as a string and parses it into a Term. +// A complete term is in the form: +// tenant:owner/name/revision +// This function accepts partially specified identifiers +// typically in one of the following forms: +// name +// owner/name +// owner/name/27 # Revision 27 +// name/283 # Revision 283 +// cs:owner/name # Tenant cs +func ParseTerm(s string) (*TermsId, error) { + tenant := "" + termid := s + if t := strings.SplitN(s, ":", 2); len(t) == 2 { + tenant = t[0] + termid = t[1] + } + + tokens := strings.Split(termid, "/") + var term TermsId + switch len(tokens) { + case 1: // "name" + term = TermsId{ + Tenant: tenant, + Name: tokens[0], + } + case 2: // owner/name or name/123 + termRevision, err := strconv.Atoi(tokens[1]) + if err != nil { // owner/name + term = TermsId{ + Tenant: tenant, + Owner: tokens[0], + Name: tokens[1], + } + } else { // name/123 + term = TermsId{ + Tenant: tenant, + Name: tokens[0], + Revision: termRevision, + } + } + case 3: // owner/name/123 + termRevision, err := strconv.Atoi(tokens[2]) + if err != nil { + return nil, errors.Errorf("invalid revision number %q %v", tokens[2], err) + } + term = TermsId{ + Tenant: tenant, + Owner: tokens[0], + Name: tokens[1], + Revision: termRevision, + } + default: + return nil, errors.Errorf("unknown term id format %q", s) + } + if err := term.Validate(); err != nil { + return nil, errors.Trace(err) + } + return &term, nil +} + +// MustParseTerm acts like ParseTerm but panics on error. +func MustParseTerm(s string) *TermsId { + term, err := ParseTerm(s) + if err != nil { + panic(err) + } + return term +} + // ReadMeta reads the content of a metadata.yaml file and returns // its representation. func ReadMeta(r io.Reader) (*Meta, error) { @@ -287,10 +406,6 @@ return err } - // TODO(ericsnow) This line should be moved into parseMeta as soon - // as the terms code gets fixed. - meta1.Terms = parseStringList(m["terms"]) - *meta = *meta1 return nil } @@ -327,6 +442,7 @@ } meta.MinJujuVersion = minver } + meta.Terms = parseStringList(m["terms"]) resources, err := parseMetaResources(m["resources"]) if err != nil { @@ -548,7 +664,7 @@ } for _, term := range meta.Terms { - if terr := checkTerm(term); terr != nil { + if _, terr := ParseTerm(term); terr != nil { return errors.Trace(terr) } } diff -Nru juju-core-2.0~beta12/src/gopkg.in/juju/charm.v6-unstable/meta_test.go juju-core-2.0~beta15/src/gopkg.in/juju/charm.v6-unstable/meta_test.go --- juju-core-2.0~beta12/src/gopkg.in/juju/charm.v6-unstable/meta_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/gopkg.in/juju/charm.v6-unstable/meta_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -56,6 +56,71 @@ c.Assert(meta.Terms, gc.HasLen, 0) } +func (s *MetaSuite) TestValidTermFormat(c *gc.C) { + valid := []string{ + "foobar", + "foobar/27", + "foo/003", + "owner/Foo-bar", + "owner/foobar/27", + "owner/foobar", + "owner/foo-bar", + "owner/foo_bar", + "own-er/foobar", + "ibm/j9-jvm/2", + "term_123-23aAf/1", + "cs:foobar/27", + "cs:foobar", + } + + invalid := []string{ + "/", + "/1", + "//", + "//2", + "27", + "owner/foo/foobar", + "@les/term/1", + "own_er/foobar", + } + + for i, s := range valid { + c.Logf("valid test %d: %s", i, s) + meta := charm.Meta{Terms: []string{s}} + err := meta.Check() + c.Check(err, jc.ErrorIsNil) + } + + for i, s := range invalid { + c.Logf("invalid test %d: %s", i, s) + meta := charm.Meta{Terms: []string{s}} + err := meta.Check() + c.Check(err, gc.NotNil) + } +} + +func (s *MetaSuite) TestTermStringRoundTrip(c *gc.C) { + terms := []string{ + "foobar", + "foobar/27", + "owner/Foo-bar", + "owner/foobar/27", + "owner/foobar", + "owner/foo-bar", + "owner/foo_bar", + "own-er/foobar", + "ibm/j9-jvm/2", + "term_123-23aAf/1", + "cs:foobar/27", + } + for i, term := range terms { + c.Logf("test %d: %s", i, term) + id, err := charm.ParseTerm(term) + c.Check(err, gc.IsNil) + c.Check(id.String(), gc.Equals, term) + } +} + func (s *MetaSuite) TestCheckTerms(c *gc.C) { tests := []struct { about string @@ -63,31 +128,35 @@ expectError string }{{ about: "valid terms", - terms: []string{"term/1", "term/2", "term-without-revision"}, + terms: []string{"term/1", "term/2", "term-without-revision", "tt/2"}, }, { about: "revision not a number", terms: []string{"term/1", "term/a"}, - expectError: "invalid term name \"term/a\": must match.*", + expectError: `wrong term name format "a"`, }, { about: "negative revision", terms: []string{"term/-1"}, - expectError: "invalid term name \"term/-1\": must match.*", + expectError: "negative term revision", }, { about: "wrong format", - terms: []string{"term/1", "term/a/1"}, - expectError: "invalid term name \"term/a/1\": must match.*", + terms: []string{"term/1", "foobar/term/abc/1"}, + expectError: `unknown term id format "foobar/term/abc/1"`, + }, { + about: "term with owner", + terms: []string{"term/1", "term/abc/1"}, + }, { + about: "term with owner no rev", + terms: []string{"term/1", "term/abc"}, }, { about: "term may not contain spaces", terms: []string{"term/1", "term about a term"}, - expectError: "invalid term name \"term about a term\": must match.*", + expectError: `wrong term name format "term about a term"`, + }, { + about: "term name must start with lowercase letter", + terms: []string{"Term/1"}, }, { - about: "term name must start with lowercase letter", - terms: []string{"Term/1"}, - expectError: `invalid term name "Term/1": must match.*`, - }, { - about: "term name match the regexp", - terms: []string{"term_123-23aAf/1"}, - expectError: "invalid term name \"term_123-23aAf/1\": must match.*", + about: "term name match the regexp", + terms: []string{"term_123-23aAf/1"}, }, } for i, test := range tests { @@ -95,9 +164,101 @@ meta := charm.Meta{Terms: test.terms} err := meta.Check() if test.expectError == "" { - c.Assert(err, jc.ErrorIsNil) + c.Check(err, jc.ErrorIsNil) + } else { + c.Check(err, gc.ErrorMatches, test.expectError) + } + } +} + +func (s *MetaSuite) TestParseTerms(c *gc.C) { + tests := []struct { + about string + term string + expectError string + expectTerm charm.TermsId + }{{ + about: "valid term", + term: "term/1", + expectTerm: charm.TermsId{"", "", "term", 1}, + }, { + about: "valid term no revision", + term: "term", + expectTerm: charm.TermsId{"", "", "term", 0}, + }, { + about: "revision not a number", + term: "term/a", + expectError: `wrong term name format "a"`, + }, { + about: "negative revision", + term: "term/-1", + expectError: "negative term revision", + }, { + about: "bad revision", + term: "owner/term/12a", + expectError: `invalid revision number "12a" strconv.ParseInt: parsing "12a": invalid syntax`, + }, { + about: "wrong format", + term: "foobar/term/abc/1", + expectError: `unknown term id format "foobar/term/abc/1"`, + }, { + about: "term with owner", + term: "term/abc/1", + expectTerm: charm.TermsId{"", "term", "abc", 1}, + }, { + about: "term with owner no rev", + term: "term/abc", + expectTerm: charm.TermsId{"", "term", "abc", 0}, + }, { + about: "term may not contain spaces", + term: "term about a term", + expectError: `wrong term name format "term about a term"`, + }, { + about: "term name may start with an uppercase letter", + term: "Term/1", + expectTerm: charm.TermsId{"", "", "Term", 1}, + }, { + about: "term name must not start with a number", + term: "1Term/1", + expectError: `wrong term name format "1Term"`, + }, { + about: "term name match the regexp", + term: "term_123-23aAf/1", + expectTerm: charm.TermsId{"", "", "term_123-23aAf", 1}, + }, { + about: "full term with tenant", + term: "tenant:owner/term/1", + expectTerm: charm.TermsId{"tenant", "owner", "term", 1}, + }, { + about: "bad tenant", + term: "tenant::owner/term/1", + expectError: `wrong owner format ":owner"`, + }, { + about: "ownerless term with tenant", + term: "tenant:term/1", + expectTerm: charm.TermsId{"tenant", "", "term", 1}, + }, { + about: "ownerless revisionless term with tenant", + term: "tenant:term", + expectTerm: charm.TermsId{"tenant", "", "term", 0}, + }, { + about: "owner/term with tenant", + term: "tenant:owner/term", + expectTerm: charm.TermsId{"tenant", "owner", "term", 0}, + }, { + about: "term with tenant", + term: "tenant:term", + expectTerm: charm.TermsId{"tenant", "", "term", 0}, + }} + for i, test := range tests { + c.Logf("running test %v: %v", i, test.about) + term, err := charm.ParseTerm(test.term) + if test.expectError == "" { + c.Check(err, jc.ErrorIsNil) + c.Check(term, gc.DeepEquals, &test.expectTerm) } else { - c.Assert(err, gc.ErrorMatches, test.expectError) + c.Check(err, gc.ErrorMatches, test.expectError) + c.Check(term, gc.IsNil) } } } @@ -113,7 +274,21 @@ c.Assert(err, jc.ErrorIsNil) err = meta.Check() c.Assert(err, jc.ErrorIsNil) - c.Assert(meta.Terms, jc.DeepEquals, []string{"term1/1", "term2"}) + c.Assert(meta.Terms, jc.DeepEquals, []string{"term1/1", "term2", "owner/term3/1"}) +} + +var metaDataWithInvalidTermsId = ` +name: terms +summary: "Sample charm with terms and conditions" +description: | + That's a boring charm that requires certain terms. +terms: ["!!!/abc"] +` + +func (s *MetaSuite) TestReadInvalidTerms(c *gc.C) { + reader := strings.NewReader(metaDataWithInvalidTermsId) + _, err := charm.ReadMeta(reader) + c.Assert(err, gc.ErrorMatches, `wrong owner format "!!!"`) } func (s *MetaSuite) TestReadTags(c *gc.C) { @@ -594,7 +769,7 @@ }, Categories: []string{"quxxxx", "quxxxxx"}, Tags: []string{"openstack", "storage"}, - Terms: []string{"test term 1", "test term 2"}, + Terms: []string{"test-term/1", "test-term/2"}, } for i, codec := range codecs { c.Logf("codec %d", i) diff -Nru juju-core-2.0~beta12/src/gopkg.in/juju/names.v2/controller.go juju-core-2.0~beta15/src/gopkg.in/juju/names.v2/controller.go --- juju-core-2.0~beta12/src/gopkg.in/juju/names.v2/controller.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/gopkg.in/juju/names.v2/controller.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,56 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package names + +import ( + "regexp" +) + +// ControllerTagKind indicates that a tag belongs to a controller. +const ControllerTagKind = "controller" + +// ControllerTag represents a tag used to describe a controller. +type ControllerTag struct { + uuid string +} + +// Lowercase letters, digits and (non-leading) hyphens. +var validControllerName = regexp.MustCompile(`^[a-z0-9]+[a-z0-9-]*$`) + +// NewControllerTag returns the tag of an controller with the given controller UUID. +func NewControllerTag(uuid string) ControllerTag { + return ControllerTag{uuid: uuid} +} + +// ParseControllerTag parses an environ tag string. +func ParseControllerTag(controllerTag string) (ControllerTag, error) { + tag, err := ParseTag(controllerTag) + if err != nil { + return ControllerTag{}, err + } + et, ok := tag.(ControllerTag) + if !ok { + return ControllerTag{}, invalidTagError(controllerTag, ControllerTagKind) + } + return et, nil +} + +// String implements Tag. +func (t ControllerTag) String() string { return t.Kind() + "-" + t.Id() } + +// Kind implements Tag. +func (t ControllerTag) Kind() string { return ControllerTagKind } + +// Id implements Tag. +func (t ControllerTag) Id() string { return t.uuid } + +// IsValidController returns whether id is a valid controller UUID. +func IsValidController(id string) bool { + return validUUID.MatchString(id) +} + +// IsValidControllerName returns whether name is a valid string safe for a controller name. +func IsValidControllerName(name string) bool { + return validControllerName.MatchString(name) +} diff -Nru juju-core-2.0~beta12/src/gopkg.in/juju/names.v2/controller_test.go juju-core-2.0~beta15/src/gopkg.in/juju/names.v2/controller_test.go --- juju-core-2.0~beta12/src/gopkg.in/juju/names.v2/controller_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-2.0~beta15/src/gopkg.in/juju/names.v2/controller_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -0,0 +1,92 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package names_test + +import ( + gc "gopkg.in/check.v1" + + "gopkg.in/juju/names.v2" +) + +type controllerSuite struct{} + +var _ = gc.Suite(&controllerSuite{}) + +var parseControllerTagTests = []struct { + title string + tag string + expected names.Tag + err error +}{{ + title: "empty tag fails", + tag: "", + err: names.InvalidTagError("", ""), +}, { + title: "valid controller- tag", + tag: "controller-f47ac10b-58cc-4372-a567-0e02b2c3d479", + expected: names.NewControllerTag("f47ac10b-58cc-4372-a567-0e02b2c3d479"), +}, { + + title: "invalid controller tag one word", + tag: "dave", + err: names.InvalidTagError("dave", ""), +}, { + title: "invalid controller tag prefix only", + tag: "controller-", + err: names.InvalidTagError("controller-", names.ControllerTagKind), +}, { + title: "invalid controller tag hyphen separated words", + tag: "application-dave", + err: names.InvalidTagError("application-dave", names.ControllerTagKind), +}, { + title: "invalid controller tag non hyphen separated prefix", + tag: "controllerf47ac10b-58cc-4372-a567-0e02b2c3d479", + err: names.InvalidTagError("controllerf47ac10b-58cc-4372-a567-0e02b2c3d479", ""), +}, { + title: "invalid controller tag non hyphen separated terms", + tag: "controllerf47ac10b58cc4372a5670e02b2c3d479", + err: names.InvalidTagError("controllerf47ac10b58cc4372a5670e02b2c3d479", ""), +}} + +func (s *controllerSuite) TestParseControllerTag(c *gc.C) { + for i, t := range parseControllerTagTests { + c.Logf("test %d: %q %s", i, t.title, t.tag) + got, err := names.ParseControllerTag(t.tag) + if err != nil || t.err != nil { + c.Check(err, gc.DeepEquals, t.err) + continue + } + c.Check(got, gc.FitsTypeOf, t.expected) + c.Check(got, gc.Equals, t.expected) + } +} + +var controllerNameTest = []struct { + test string + name string + expected bool +}{{ + test: "Hyphenated true", + name: "foo-bar", + expected: true, +}, { + test: "Whitespsce false", + name: "foo bar", + expected: false, +}, { + test: "Capital false", + name: "fooBar", + expected: false, +}, { + test: "At sign false", + name: "foo@bar", + expected: false, +}} + +func (s *controllerSuite) TestControllerName(c *gc.C) { + for i, t := range controllerNameTest { + c.Logf("test %d: %q", i, t.name) + c.Check(names.IsValidControllerName(t.name), gc.Equals, t.expected) + } +} diff -Nru juju-core-2.0~beta12/src/gopkg.in/juju/names.v2/model_test.go juju-core-2.0~beta15/src/gopkg.in/juju/names.v2/model_test.go --- juju-core-2.0~beta12/src/gopkg.in/juju/names.v2/model_test.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/gopkg.in/juju/names.v2/model_test.go 2016-08-16 08:56:25.000000000 +0000 @@ -26,10 +26,9 @@ }, { tag: "dave", err: names.InvalidTagError("dave", ""), - //}, { - // TODO(dfc) passes, but should not - // tag: "model-", - // err: names.InvalidTagError("model", ""), +}, { + tag: "model-", + err: names.InvalidTagError("model-", names.ModelTagKind), }, { tag: "application-dave", err: names.InvalidTagError("application-dave", names.ModelTagKind), diff -Nru juju-core-2.0~beta12/src/gopkg.in/juju/names.v2/tag.go juju-core-2.0~beta15/src/gopkg.in/juju/names.v2/tag.go --- juju-core-2.0~beta12/src/gopkg.in/juju/names.v2/tag.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/gopkg.in/juju/names.v2/tag.go 2016-08-16 08:56:25.000000000 +0000 @@ -63,7 +63,7 @@ case UnitTagKind, MachineTagKind, ApplicationTagKind, EnvironTagKind, UserTagKind, RelationTagKind, ActionTagKind, VolumeTagKind, CharmTagKind, StorageTagKind, FilesystemTagKind, IPAddressTagKind, SpaceTagKind, SubnetTagKind, - PayloadTagKind, ModelTagKind, CloudTagKind: + PayloadTagKind, ModelTagKind, ControllerTagKind, CloudTagKind: return true } return false @@ -116,6 +116,12 @@ return nil, invalidTagError(tag, kind) } return NewModelTag(id), nil + case ControllerTagKind: + if !IsValidController(id) { + return nil, invalidTagError(tag, kind) + } + return NewControllerTag(id), nil + case RelationTagKind: id = relationTagSuffixToKey(id) if !IsValidRelation(id) { diff -Nru juju-core-2.0~beta12/src/gopkg.in/mgo.v2/session.go juju-core-2.0~beta15/src/gopkg.in/mgo.v2/session.go --- juju-core-2.0~beta12/src/gopkg.in/mgo.v2/session.go 2016-07-18 19:45:44.000000000 +0000 +++ juju-core-2.0~beta15/src/gopkg.in/mgo.v2/session.go 2016-08-16 08:56:25.000000000 +0000 @@ -41,6 +41,7 @@ "sync" "time" + "github.com/juju/loggo" "gopkg.in/mgo.v2/bson" ) @@ -144,10 +145,16 @@ var ( ErrNotFound = errors.New("not found") ErrCursor = errors.New("invalid cursor") + + logPatchedOnce sync.Once + logger = loggo.GetLogger("mgo") ) const ( - defaultPrefetch = 0.25 + defaultPrefetch = 0.25 + + // How many times we will retry an upsert if it produces duplicate + // key errors. maxUpsertRetries = 5 ) @@ -413,6 +420,16 @@ // DialWithInfo establishes a new session to the cluster identified by info. func DialWithInfo(info *DialInfo) (*Session, error) { + // This is using loggo because that can be done here in a + // localised patch, while using mgo's logging would need a change + // in Juju to call mgo.SetLogger. It's in this short-lived patch + // as a stop-gap because it's proving difficult to tell if the + // patch is applied in a running system. If you see it in + // committed code then something has gone very awry - please + // complain loudly! (babbageclunk) + logPatchedOnce.Do(func() { + logger.Debugf("duplicate key error patch applied") + }) addrs := make([]string, len(info.Addrs)) for i, addr := range info.Addrs { p := strings.LastIndexAny(addr, "]:") @@ -2482,7 +2499,8 @@ Upsert: true, } var lerr *LastError - for i := 0; i < maxUpsertRetries; i++ { + // <= to allow for the first attempt (not a retry). + for i := 0; i <= maxUpsertRetries; i++ { lerr, err = c.writeOp(&op, true) // Retry duplicate key errors on upserts. // https://docs.mongodb.com/v3.2/reference/method/db.collection.update/#use-unique-indexes @@ -4219,22 +4237,22 @@ session.SetMode(Strong, false) var doc valueResult - for i := 0; i < maxUpsertRetries; i++ { + for retries := 0; ; retries++ { err = session.DB(dbname).Run(&cmd, &doc) - - if err == nil { - break - } - if change.Upsert && IsDup(err) { - // Retry duplicate key errors on upserts. - // https://docs.mongodb.com/v3.2/reference/method/db.collection.update/#use-unique-indexes - continue - } - if qerr, ok := err.(*QueryError); ok && qerr.Message == "No matching object found" { - return nil, ErrNotFound + if err != nil { + if qerr, ok := err.(*QueryError); ok && qerr.Message == "No matching object found" { + return nil, ErrNotFound + } + if change.Upsert && IsDup(err) && retries < maxUpsertRetries { + // Retry duplicate key errors on upserts. + // https://docs.mongodb.com/v3.2/reference/method/db.collection.update/#use-unique-indexes + continue + } + return nil, err } - return nil, err + break // No error, so don't retry. } + if doc.LastError.N == 0 { return nil, ErrNotFound }