=== modified file 'src/github.com/juju/cmd/cmd.go' --- src/github.com/juju/cmd/cmd.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/cmd/cmd.go 2015-10-23 18:29:32 +0000 @@ -17,14 +17,18 @@ "launchpad.net/gnuflag" ) +// RcPassthroughError indicates that a Juju plugin command exited with a +// non-zero exit code. This error is used to exit with the return code. type RcPassthroughError struct { Code int } +// Error implements error. func (e *RcPassthroughError) Error() string { return fmt.Sprintf("subprocess encountered error code %v", e.Code) } +// IsRcPassthroughError returns whether the error is an RcPassthroughError. func IsRcPassthroughError(err error) bool { _, ok := err.(*RcPassthroughError) return ok @@ -41,6 +45,17 @@ // code 1 without producing error output. var ErrSilent = errors.New("cmd: error out silently") +// IsErrSilent returns whether the error should be logged from cmd.Main. +func IsErrSilent(err error) bool { + if err == ErrSilent { + return true + } + if _, ok := err.(*RcPassthroughError); ok { + return true + } + return false +} + // Command is implemented by types that interpret command-line arguments. type Command interface { // IsSuperCommand returns true if the command is a super command. @@ -91,6 +106,7 @@ // output and errors to Stdout and Stderr respectively. type Context struct { Dir string + Env map[string]string Stdin io.Reader Stdout io.Writer Stderr io.Writer @@ -126,6 +142,22 @@ } } +// Getenv looks up an environment variable in the context. It mirrors +// os.Getenv. An empty string is returned if the key is not set. +func (ctx *Context) Getenv(key string) string { + value, _ := ctx.Env[key] + return value +} + +// Setenv sets an environment variable in the context. It mirrors os.Setenv. +func (ctx *Context) Setenv(key, value string) error { + if ctx.Env == nil { + ctx.Env = make(map[string]string) + } + ctx.Env[key] = value + return nil +} + // AbsPath returns an absolute representation of path, with relative paths // interpreted as relative to ctx.Dir. func (ctx *Context) AbsPath(path string) string { === modified file 'src/github.com/juju/cmd/cmd_test.go' --- src/github.com/juju/cmd/cmd_test.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/cmd/cmd_test.go 2015-10-23 18:29:32 +0000 @@ -5,6 +5,7 @@ import ( "bytes" + "fmt" "os" "path/filepath" @@ -24,6 +25,27 @@ c.Assert(ctx.AbsPath("foo/bar"), gc.Equals, filepath.Join(ctx.Dir, "foo/bar")) } +func (s *CmdSuite) TestContextGetenv(c *gc.C) { + ctx := cmdtesting.Context(c) + ctx.Env = make(map[string]string) + before := ctx.Getenv("foo") + ctx.Env["foo"] = "bar" + after := ctx.Getenv("foo") + + c.Check(before, gc.Equals, "") + c.Check(after, gc.Equals, "bar") +} + +func (s *CmdSuite) TestContextSetenv(c *gc.C) { + ctx := cmdtesting.Context(c) + before := ctx.Env["foo"] + ctx.Setenv("foo", "bar") + after := ctx.Env["foo"] + + c.Check(before, gc.Equals, "") + c.Check(after, gc.Equals, "bar") +} + func (s *CmdSuite) TestInfo(c *gc.C) { minimal := &TestCommand{Name: "verb", Minimal: true} help := minimal.Info().Help(cmdtesting.NewFlagSet()) @@ -143,3 +165,9 @@ c.Assert(arg, gc.Equals, "") c.Assert(err, gc.ErrorMatches, `unrecognized args: \["bar"\]`) } + +func (s *CmdSuite) TestIsErrSilent(c *gc.C) { + c.Assert(cmd.IsErrSilent(cmd.ErrSilent), gc.Equals, true) + c.Assert(cmd.IsErrSilent(cmd.NewRcPassthroughError(99)), gc.Equals, true) + c.Assert(cmd.IsErrSilent(fmt.Errorf("noisy")), gc.Equals, false) +} === added file 'src/github.com/juju/cmd/help.go' --- src/github.com/juju/cmd/help.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/cmd/help.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,220 @@ +// Copyright 2012-2015 Canonical Ltd. +// Licensed under the LGPLv3, see LICENSE file for details. + +package cmd + +import ( + "bytes" + "fmt" + "sort" + "strings" + + "launchpad.net/gnuflag" +) + +type helpCommand struct { + CommandBase + super *SuperCommand + topic string + topicArgs []string + topics map[string]topic + + target *commandReference + targetSuper *SuperCommand +} + +func (c *helpCommand) init() { + c.topics = map[string]topic{ + "commands": { + short: "Basic help for all commands", + long: func() string { return c.super.describeCommands(true) }, + }, + "global-options": { + short: "Options common to all commands", + long: func() string { return c.globalOptions() }, + }, + "topics": { + short: "Topic list", + long: func() string { return c.topicList() }, + }, + } +} + +func echo(s string) func() string { + return func() string { return s } +} + +func (c *helpCommand) addTopic(name, short string, long func() string, aliases ...string) { + if _, found := c.topics[name]; found { + panic(fmt.Sprintf("help topic already added: %s", name)) + } + c.topics[name] = topic{short, long, false} + for _, alias := range aliases { + if _, found := c.topics[alias]; found { + panic(fmt.Sprintf("help topic already added: %s", alias)) + } + c.topics[alias] = topic{short, long, true} + } +} + +func (c *helpCommand) globalOptions() string { + buf := &bytes.Buffer{} + fmt.Fprintf(buf, `Global Options + +These options may be used with any command, and may appear in front of any +command. + +`) + + f := gnuflag.NewFlagSet("", gnuflag.ContinueOnError) + c.super.SetCommonFlags(f) + f.SetOutput(buf) + f.PrintDefaults() + return buf.String() +} + +func (c *helpCommand) topicList() string { + var topics []string + longest := 0 + for name, topic := range c.topics { + if topic.alias { + continue + } + if len(name) > longest { + longest = len(name) + } + topics = append(topics, name) + } + sort.Strings(topics) + for i, name := range topics { + shortHelp := c.topics[name].short + topics[i] = fmt.Sprintf("%-*s %s", longest, name, shortHelp) + } + return fmt.Sprintf("%s", strings.Join(topics, "\n")) +} + +func (c *helpCommand) Info() *Info { + return &Info{ + Name: "help", + Args: "[topic]", + Purpose: helpPurpose, + Doc: ` +See also: topics +`, + } +} + +func (c *helpCommand) Init(args []string) error { + logger.Tracef("helpCommand.Init: %#v", args) + if len(args) == 0 { + // If there is no help topic specified, print basic usage if it is + // there. + if _, ok := c.topics["basics"]; ok { + c.topic = "basics" + } + return nil + } + + // Before we start walking down the subcommand list, we want to check + // to see if the first part is there. + if _, ok := c.super.subcmds[args[0]]; !ok { + if c.super.missingCallback == nil && len(args) > 1 { + return fmt.Errorf("extra arguments to command help: %q", args[1:]) + } + logger.Tracef("help not found, setting topic") + c.topic, c.topicArgs = args[0], args[1:] + return nil + } + + c.targetSuper = c.super + for len(args) > 0 { + c.topic, args = args[0], args[1:] + commandRef, ok := c.targetSuper.subcmds[c.topic] + if !ok { + return fmt.Errorf("subcommand %q not found", c.topic) + } + c.target = &commandRef + // If there are more args and the target isn't a super command + // error out. + logger.Tracef("target name: %s", c.target.name) + if super, ok := c.target.command.(*SuperCommand); ok { + c.targetSuper = super + } else if len(args) > 0 { + return fmt.Errorf("extra arguments to command help: %q", args) + } + } + return nil +} + +func (c *helpCommand) getCommandHelp(super *SuperCommand, command Command, alias string) []byte { + info := command.Info() + + if command != super { + logger.Tracef("command not super") + // If the alias is to a subcommand of another super command + // the alias string holds the "super sub" name. + if alias == "" { + info.Name = fmt.Sprintf("%s %s", super.Name, info.Name) + } else { + info.Name = fmt.Sprintf("%s %s", super.Name, alias) + } + } + if super.usagePrefix != "" { + logger.Tracef("adding super prefix") + info.Name = fmt.Sprintf("%s %s", super.usagePrefix, info.Name) + } + f := gnuflag.NewFlagSet(info.Name, gnuflag.ContinueOnError) + command.SetFlags(f) + return info.Help(f) +} + +func (c *helpCommand) Run(ctx *Context) error { + if c.super.showVersion { + v := newVersionCommand(c.super.version) + v.SetFlags(c.super.flags) + v.Init(nil) + return v.Run(ctx) + } + + // If the topic is a registered subcommand, then run the help command with it + if c.target != nil { + ctx.Stdout.Write(c.getCommandHelp(c.targetSuper, c.target.command, c.target.alias)) + return nil + } + + // If there is no help topic specified, print basic usage. + if c.topic == "" { + // At this point, "help" is selected as the SuperCommand's + // current action, but we want the info to be printed + // as if there was nothing selected. + c.super.action.command = nil + ctx.Stdout.Write(c.getCommandHelp(c.super, c.super, "")) + return nil + } + + // Look to see if the topic is a registered topic. + topic, ok := c.topics[c.topic] + if ok { + fmt.Fprintf(ctx.Stdout, "%s\n", strings.TrimSpace(topic.long())) + return nil + } + // If we have a missing callback, call that with --help + if c.super.missingCallback != nil { + helpArgs := []string{"--help"} + if len(c.topicArgs) > 0 { + helpArgs = append(helpArgs, c.topicArgs...) + } + command := &missingCommand{ + callback: c.super.missingCallback, + superName: c.super.Name, + name: c.topic, + args: helpArgs, + } + err := command.Run(ctx) + _, isUnrecognized := err.(*UnrecognizedCommand) + if !isUnrecognized { + return err + } + } + return fmt.Errorf("unknown command or topic for %s", c.topic) +} === added file 'src/github.com/juju/cmd/help_test.go' --- src/github.com/juju/cmd/help_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/cmd/help_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,161 @@ +// Copyright 2012-2015 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package cmd_test + +import ( + "strings" + + "github.com/juju/loggo" + gitjujutesting "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/cmd" + "github.com/juju/cmd/cmdtesting" +) + +type HelpCommandSuite struct { + gitjujutesting.IsolationSuite +} + +var _ = gc.Suite(&HelpCommandSuite{}) + +func (s *HelpCommandSuite) SetUpTest(c *gc.C) { + s.IsolationSuite.SetUpTest(c) + loggo.GetLogger("juju.cmd").SetLogLevel(loggo.DEBUG) +} + +func (s *HelpCommandSuite) assertStdOutMatches(c *gc.C, ctx *cmd.Context, match string) { + stripped := strings.Replace(cmdtesting.Stdout(ctx), "\n", "", -1) + c.Assert(stripped, gc.Matches, match) +} + +func (s *HelpCommandSuite) TestHelpOutput(c *gc.C) { + for i, test := range []struct { + message string + args []string + usagePrefix string + helpMatch string + errMatch string + }{ + { + message: "no args shows help", + helpMatch: "usage: jujutest .*", + }, { + message: "usage prefix with help command", + args: []string{"help"}, + usagePrefix: "juju", + helpMatch: "usage: juju jujutest .*", + }, { + message: "usage prefix with help flag", + args: []string{"--help"}, + usagePrefix: "juju", + helpMatch: "usage: juju jujutest .*", + }, { + message: "help arg usage", + args: []string{"blah", "--help"}, + helpMatch: "usage: jujutest blah.*blah-doc.*", + }, { + message: "usage prefix with help command", + args: []string{"help", "blah"}, + usagePrefix: "juju", + helpMatch: "usage: juju jujutest blah .*", + }, { + message: "usage prefix with help flag", + args: []string{"blah", "--help"}, + usagePrefix: "juju", + helpMatch: "usage: juju jujutest blah .*", + }, { + message: "too many args", + args: []string{"help", "blah", "blah"}, + errMatch: `extra arguments to command help: \["blah"\]`, + }, + } { + supername := "jujutest" + super := cmd.NewSuperCommand(cmd.SuperCommandParams{Name: supername, UsagePrefix: test.usagePrefix}) + super.Register(&TestCommand{Name: "blah"}) + c.Logf("%d: %s, %q", i, test.message, strings.Join(append([]string{supername}, test.args...), " ")) + + ctx, err := cmdtesting.RunCommand(c, super, test.args...) + if test.errMatch == "" { + c.Assert(err, jc.ErrorIsNil) + s.assertStdOutMatches(c, ctx, test.helpMatch) + + } else { + c.Assert(err, gc.ErrorMatches, test.errMatch) + } + } +} + +func (s *HelpCommandSuite) TestHelpBasics(c *gc.C) { + super := cmd.NewSuperCommand(cmd.SuperCommandParams{Name: "jujutest"}) + super.Register(&TestCommand{Name: "blah"}) + super.AddHelpTopic("basics", "short", "long help basics") + + ctx, err := cmdtesting.RunCommand(c, super) + c.Assert(err, jc.ErrorIsNil) + s.assertStdOutMatches(c, ctx, "long help basics") +} + +func (s *HelpCommandSuite) TestMultipleSuperCommands(c *gc.C) { + level1 := cmd.NewSuperCommand(cmd.SuperCommandParams{Name: "level1"}) + level2 := cmd.NewSuperCommand(cmd.SuperCommandParams{Name: "level2", UsagePrefix: "level1"}) + level1.Register(level2) + level3 := cmd.NewSuperCommand(cmd.SuperCommandParams{Name: "level3", UsagePrefix: "level1 level2"}) + level2.Register(level3) + level3.Register(&TestCommand{Name: "blah"}) + + ctx, err := cmdtesting.RunCommand(c, level1, "help", "level2", "level3", "blah") + c.Assert(err, jc.ErrorIsNil) + s.assertStdOutMatches(c, ctx, "usage: level1 level2 level3 blah.*blah-doc.*") + + _, err = cmdtesting.RunCommand(c, level1, "help", "level2", "missing", "blah") + c.Assert(err, gc.ErrorMatches, `subcommand "missing" not found`) +} + +func (s *HelpCommandSuite) TestAlias(c *gc.C) { + super := cmd.NewSuperCommand(cmd.SuperCommandParams{Name: "super"}) + super.Register(&TestCommand{Name: "blah", Aliases: []string{"alias"}}) + ctx := cmdtesting.Context(c) + code := cmd.Main(super, ctx, []string{"help", "alias"}) + c.Assert(code, gc.Equals, 0) + stripped := strings.Replace(bufferString(ctx.Stdout), "\n", "", -1) + c.Assert(stripped, gc.Matches, "usage: super blah .*aliases: alias") +} + +func (s *HelpCommandSuite) TestRegisterSuperAliasHelp(c *gc.C) { + jc := cmd.NewSuperCommand(cmd.SuperCommandParams{ + Name: "jujutest", + }) + sub := cmd.NewSuperCommand(cmd.SuperCommandParams{ + Name: "bar", + UsagePrefix: "jujutest", + Purpose: "bar functions", + }) + jc.Register(sub) + sub.Register(&simple{name: "foo"}) + + jc.RegisterSuperAlias("bar-foo", "bar", "foo", nil) + + for _, test := range []struct { + args []string + }{ + { + args: []string{"bar", "foo", "--help"}, + }, { + args: []string{"bar", "help", "foo"}, + }, { + args: []string{"help", "bar-foo"}, + }, { + args: []string{"bar-foo", "--help"}, + }, + } { + c.Logf("args: %v", test.args) + ctx := cmdtesting.Context(c) + code := cmd.Main(jc, ctx, test.args) + c.Check(code, gc.Equals, 0) + help := "usage: jujutest bar foo\npurpose: to be simple\n" + c.Check(cmdtesting.Stdout(ctx), gc.Equals, help) + } +} === modified file 'src/github.com/juju/cmd/logging.go' --- src/github.com/juju/cmd/logging.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/cmd/logging.go 2015-10-23 18:29:32 +0000 @@ -13,12 +13,6 @@ "launchpad.net/gnuflag" ) -// WriterFactory defines the single method to create a new -// logging writer for a specified output target. -type WriterFactory interface { - NewWriter(target io.Writer) loggo.Writer -} - // Log supplies the necessary functionality for Commands that wish to set up // logging. type Log struct { @@ -31,13 +25,15 @@ Debug bool ShowLog bool Config string - Factory WriterFactory + + // NewWriter creates a new logging writer for a specified target. + NewWriter func(target io.Writer) loggo.Writer } // GetLogWriter returns a logging writer for the specified target. func (l *Log) GetLogWriter(target io.Writer) loggo.Writer { - if l.Factory != nil { - return l.Factory.NewWriter(target) + if l.NewWriter != nil { + return l.NewWriter(target) } return loggo.NewSimpleWriter(target, &loggo.DefaultFormatter{}) } === modified file 'src/github.com/juju/cmd/output.go' --- src/github.com/juju/cmd/output.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/cmd/output.go 2015-10-23 18:29:32 +0000 @@ -10,6 +10,7 @@ "os" "reflect" "sort" + "strconv" "strings" goyaml "gopkg.in/yaml.v1" @@ -69,7 +70,10 @@ return []byte("True"), nil } return []byte("False"), nil - case reflect.Map, reflect.Float32, reflect.Float64: + case reflect.Float32, reflect.Float64: + sv := strconv.FormatFloat(value.(float64), 'f', -1, 64) + return []byte(sv), nil + case reflect.Map: case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: default: @@ -156,9 +160,12 @@ target = ctx.Stdout } else { path := ctx.AbsPath(c.outPath) - if target, err = os.Create(path); err != nil { + var f *os.File + if f, err = os.Create(path); err != nil { return } + defer f.Close() + target = f } bytes, err := c.formatter.format(value) if err != nil { === modified file 'src/github.com/juju/cmd/output_test.go' --- src/github.com/juju/cmd/output_test.go 2015-04-14 14:11:54 +0000 +++ src/github.com/juju/cmd/output_test.go 2015-10-23 18:29:32 +0000 @@ -55,6 +55,7 @@ {1, "1\n"}, {-1, "-1\n"}, {1.1, "1.1\n"}, + {10000000, "10000000\n"}, {true, "True\n"}, {false, "False\n"}, {"hello", "hello\n"}, @@ -69,6 +70,7 @@ {1, "1\n"}, {-1, "-1\n"}, {1.1, "1.1\n"}, + {10000000, "10000000\n"}, {true, "True\n"}, {false, "False\n"}, {"hello", "hello\n"}, @@ -84,6 +86,7 @@ {1, "1\n"}, {-1, "-1\n"}, {1.1, "1.1\n"}, + {10000000, "10000000\n"}, {true, "true\n"}, {false, "false\n"}, {"hello", `"hello"` + "\n"}, @@ -99,6 +102,7 @@ {1, "1\n"}, {-1, "-1\n"}, {1.1, "1.1\n"}, + {10000000, "10000000\n"}, {true, "true\n"}, {false, "false\n"}, {"hello", "hello\n"}, === modified file 'src/github.com/juju/cmd/supercommand.go' --- src/github.com/juju/cmd/supercommand.go 2015-04-14 14:11:54 +0000 +++ src/github.com/juju/cmd/supercommand.go 2015-10-23 18:29:32 +0000 @@ -4,7 +4,6 @@ package cmd import ( - "bytes" "fmt" "io/ioutil" "sort" @@ -356,7 +355,7 @@ } if len(args) == 0 { c.action = c.subcmds["help"] - return nil + return c.action.command.Init(args) } found := false @@ -426,7 +425,7 @@ ctx.Infof("WARNING: %q is deprecated, please use %q", c.action.name, replacement) } err := c.action.command.Run(ctx) - if err != nil && err != ErrSilent { + if err != nil && !IsErrSilent(err) { logger.Errorf("%v", err) // Now that this has been logged, don't log again in cmd.Main. if !IsRcPassthroughError(err) { @@ -461,183 +460,6 @@ return &UnrecognizedCommand{c.superName + " " + c.name} } -type helpCommand struct { - CommandBase - super *SuperCommand - topic string - topicArgs []string - topics map[string]topic -} - -func (c *helpCommand) init() { - c.topics = map[string]topic{ - "commands": { - short: "Basic help for all commands", - long: func() string { return c.super.describeCommands(true) }, - }, - "global-options": { - short: "Options common to all commands", - long: func() string { return c.globalOptions() }, - }, - "topics": { - short: "Topic list", - long: func() string { return c.topicList() }, - }, - } -} - -func echo(s string) func() string { - return func() string { return s } -} - -func (c *helpCommand) addTopic(name, short string, long func() string, aliases ...string) { - if _, found := c.topics[name]; found { - panic(fmt.Sprintf("help topic already added: %s", name)) - } - c.topics[name] = topic{short, long, false} - for _, alias := range aliases { - if _, found := c.topics[alias]; found { - panic(fmt.Sprintf("help topic already added: %s", alias)) - } - c.topics[alias] = topic{short, long, true} - } -} - -func (c *helpCommand) globalOptions() string { - buf := &bytes.Buffer{} - fmt.Fprintf(buf, `Global Options - -These options may be used with any command, and may appear in front of any -command. - -`) - - f := gnuflag.NewFlagSet("", gnuflag.ContinueOnError) - c.super.SetCommonFlags(f) - f.SetOutput(buf) - f.PrintDefaults() - return buf.String() -} - -func (c *helpCommand) topicList() string { - var topics []string - longest := 0 - for name, topic := range c.topics { - if topic.alias { - continue - } - if len(name) > longest { - longest = len(name) - } - topics = append(topics, name) - } - sort.Strings(topics) - for i, name := range topics { - shortHelp := c.topics[name].short - topics[i] = fmt.Sprintf("%-*s %s", longest, name, shortHelp) - } - return fmt.Sprintf("%s", strings.Join(topics, "\n")) -} - -func (c *helpCommand) Info() *Info { - return &Info{ - Name: "help", - Args: "[topic]", - Purpose: helpPurpose, - Doc: ` -See also: topics -`, - } -} - -func (c *helpCommand) Init(args []string) error { - switch len(args) { - case 0: - case 1: - c.topic = args[0] - default: - if c.super.missingCallback == nil { - return fmt.Errorf("extra arguments to command help: %q", args[1:]) - } else { - c.topic = args[0] - c.topicArgs = args[1:] - } - } - return nil -} - -func (c *helpCommand) Run(ctx *Context) error { - if c.super.showVersion { - v := newVersionCommand(c.super.version) - v.SetFlags(c.super.flags) - v.Init(nil) - return v.Run(ctx) - } - - // If there is no help topic specified, print basic usage. - if c.topic == "" { - if _, ok := c.topics["basics"]; ok { - c.topic = "basics" - } else { - // At this point, "help" is selected as the SuperCommand's - // current action, but we want the info to be printed - // as if there was nothing selected. - c.super.action.command = nil - - info := c.super.Info() - if c.super.usagePrefix != "" { - info.Name = fmt.Sprintf("%s %s", c.super.usagePrefix, info.Name) - } - f := gnuflag.NewFlagSet(info.Name, gnuflag.ContinueOnError) - c.SetFlags(f) - ctx.Stdout.Write(info.Help(f)) - return nil - } - } - // If the topic is a registered subcommand, then run the help command with it - if helpAction, ok := c.super.subcmds[c.topic]; ok { - helpcmd := helpAction.command - info := helpcmd.Info() - if helpAction.alias == "" { - info.Name = fmt.Sprintf("%s %s", c.super.Name, info.Name) - } else { - info.Name = fmt.Sprintf("%s %s", c.super.Name, helpAction.alias) - } - if c.super.usagePrefix != "" { - info.Name = fmt.Sprintf("%s %s", c.super.usagePrefix, info.Name) - } - f := gnuflag.NewFlagSet(info.Name, gnuflag.ContinueOnError) - helpcmd.SetFlags(f) - ctx.Stdout.Write(info.Help(f)) - return nil - } - // Look to see if the topic is a registered topic. - topic, ok := c.topics[c.topic] - if ok { - fmt.Fprintf(ctx.Stdout, "%s\n", strings.TrimSpace(topic.long())) - return nil - } - // If we have a missing callback, call that with --help - if c.super.missingCallback != nil { - helpArgs := []string{"--help"} - if len(c.topicArgs) > 0 { - helpArgs = append(helpArgs, c.topicArgs...) - } - command := &missingCommand{ - callback: c.super.missingCallback, - superName: c.super.Name, - name: c.topic, - args: helpArgs, - } - err := command.Run(ctx) - _, isUnrecognized := err.(*UnrecognizedCommand) - if !isUnrecognized { - return err - } - } - return fmt.Errorf("unknown command or topic for %s", c.topic) -} - // Deprecated calls into the check interface if one was specified, // otherwise it says the command isn't deprecated. func (r commandReference) Deprecated() (bool, string) { === modified file 'src/github.com/juju/cmd/supercommand_test.go' --- src/github.com/juju/cmd/supercommand_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/cmd/supercommand_test.go 2015-10-23 18:29:32 +0000 @@ -249,46 +249,6 @@ c.Assert(bufferString(ctx.Stdout), gc.Equals, "blow up the death star\n") } -func (s *SuperCommandSuite) TestHelp(c *gc.C) { - jc := cmd.NewSuperCommand(cmd.SuperCommandParams{Name: "jujutest"}) - jc.Register(&TestCommand{Name: "blah"}) - ctx := cmdtesting.Context(c) - code := cmd.Main(jc, ctx, []string{"blah", "--help"}) - c.Assert(code, gc.Equals, 0) - stripped := strings.Replace(bufferString(ctx.Stdout), "\n", "", -1) - c.Assert(stripped, gc.Matches, "usage: jujutest blah.*blah-doc.*") -} - -func (s *SuperCommandSuite) TestHelpWithPrefix(c *gc.C) { - jc := cmd.NewSuperCommand(cmd.SuperCommandParams{Name: "jujutest", UsagePrefix: "juju"}) - jc.Register(&TestCommand{Name: "blah"}) - ctx := cmdtesting.Context(c) - code := cmd.Main(jc, ctx, []string{"help"}) - c.Assert(code, gc.Equals, 0) - stripped := strings.Replace(bufferString(ctx.Stdout), "\n", "", -1) - c.Assert(stripped, gc.Matches, "usage: juju jujutest ...*") -} - -func (s *SuperCommandSuite) TestHelpWithPrefixFlag(c *gc.C) { - jc := cmd.NewSuperCommand(cmd.SuperCommandParams{Name: "jujutest", UsagePrefix: "juju"}) - jc.Register(&TestCommand{Name: "blah"}) - ctx := cmdtesting.Context(c) - code := cmd.Main(jc, ctx, []string{"blah", "--help"}) - c.Assert(code, gc.Equals, 0) - stripped := strings.Replace(bufferString(ctx.Stdout), "\n", "", -1) - c.Assert(stripped, gc.Matches, "usage: juju jujutest blah.*blah-doc.*") -} - -func (s *SuperCommandSuite) TestHelpWithPrefixCommand(c *gc.C) { - jc := cmd.NewSuperCommand(cmd.SuperCommandParams{Name: "jujutest", UsagePrefix: "juju"}) - jc.Register(&TestCommand{Name: "blah"}) - ctx := cmdtesting.Context(c) - code := cmd.Main(jc, ctx, []string{"help", "blah"}) - c.Assert(code, gc.Equals, 0) - stripped := strings.Replace(bufferString(ctx.Stdout), "\n", "", -1) - c.Assert(stripped, gc.Matches, "usage: juju jujutest blah.*blah-doc.*") -} - func NewSuperWithCallback(callback func(*cmd.Context, string, []string) error) cmd.Command { return cmd.NewSuperCommand(cmd.SuperCommandParams{ Name: "jujutest", @@ -581,39 +541,3 @@ c.Check(cmdtesting.Stdout(ctx), gc.Equals, test.stdout) } } - -func (s *SuperCommandSuite) TestRegisterSuperAliasHelp(c *gc.C) { - jc := cmd.NewSuperCommand(cmd.SuperCommandParams{ - Name: "jujutest", - }) - sub := cmd.NewSuperCommand(cmd.SuperCommandParams{ - Name: "bar", - UsagePrefix: "jujutest", - Purpose: "bar functions", - }) - jc.Register(sub) - sub.Register(&simple{name: "foo"}) - - jc.RegisterSuperAlias("bar-foo", "bar", "foo", nil) - - for _, test := range []struct { - args []string - }{ - { - args: []string{"bar", "foo", "--help"}, - }, { - args: []string{"bar", "help", "foo"}, - }, { - args: []string{"help", "bar-foo"}, - }, { - args: []string{"bar-foo", "--help"}, - }, - } { - c.Logf("args: %v", test.args) - ctx := cmdtesting.Context(c) - code := cmd.Main(jc, ctx, test.args) - c.Check(code, gc.Equals, 0) - help := "usage: jujutest bar foo\npurpose: to be simple\n" - c.Check(cmdtesting.Stdout(ctx), gc.Equals, help) - } -} === added directory 'src/github.com/juju/deputy' === added file 'src/github.com/juju/deputy/.gitignore' --- src/github.com/juju/deputy/.gitignore 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/deputy/.gitignore 2015-10-23 18:29:32 +0000 @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof === added file 'src/github.com/juju/deputy/LICENSE' --- src/github.com/juju/deputy/LICENSE 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/deputy/LICENSE 2015-10-23 18:29:32 +0000 @@ -0,0 +1,191 @@ +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. + +This software is licensed under the LGPLv3, included below. + +As a special exception to the GNU Lesser General Public License version 3 +("LGPL3"), the copyright holders of this Library give you permission to +convey to a third party a Combined Work that links statically or dynamically +to this Library without providing any Minimal Corresponding Source or +Minimal Application Code as set out in 4d or providing the installation +information set out in section 4e, provided that you comply with the other +provisions of LGPL3 and provided that you meet, for the Application the +terms and conditions of the license(s) which apply to the Application. + +Except as stated in this special exception, the provisions of LGPL3 will +continue to comply in full to this Library. If you modify this Library, you +may apply this exception to your version of this Library, but you are not +obliged to do so. If you do not wish to do so, delete this exception +statement from your version. This exception does not (and cannot) modify any +license terms which apply to the Application, with which you must still +comply. + + + GNU LESSER 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. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser 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 +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. === added file 'src/github.com/juju/deputy/README.md' --- src/github.com/juju/deputy/README.md 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/deputy/README.md 2015-10-23 18:29:32 +0000 @@ -0,0 +1,77 @@ +# deputy [![GoDoc](https://godoc.org/github.com/juju/deputy?status.svg)](https://godoc.org/github.com/juju/deputy) [![Build Status](https://drone.io/github.com/juju/deputy/status.png)](https://drone.io/github.com/juju/deputy/latest) [![Coverage Status](https://coveralls.io/repos/juju/deputy/badge.svg?branch=master)](https://coveralls.io/r/juju/deputy?branch=master) +deputy is a go package that adds smarts on top of os/exec + +![deputy-sm](https://cloud.githubusercontent.com/assets/3185864/8237448/6bc30102-15bd-11e5-9e87-6423197a73d6.jpg) + +image: creative commons, © [MatsuRD](http://matsurd.deviantart.com/art/Paper53-Deputy-Stubbs-342123485) + +## Example + +``` go +// Make a new deputy that'll return the data written to stderr as the error +// message, log everything written to stdout to this application's log, and +// timeout after 30 seconds. +d := deputy.Deputy{ + Errors: deputy.FromStderr, + StdoutLog: func(b []byte]) { log.Print(string(b)) }, + Timeout: time.Second * 30, +} +if err := d.Run(exec.Command("foo")); err != nil { + log.Print(err) +} +``` + +## type Deputy +``` go +type Deputy struct { + // Timeout represents the longest time the command will be allowed to run + // before being killed. + Timeout time.Duration + // Errors describes how errors should be handled. + Errors ErrorHandling + // StdoutLog takes a function that will receive lines written to stdout from + // the command (with the newline elided). + StdoutLog func([]byte) + // StdoutLog takes a function that will receive lines written to stderr from + // the command (with the newline elided). + StderrLog func([]byte) + // contains filtered or unexported fields +} +``` +Deputy is a type that runs Commands with advanced options not available from +os/exec. See the comments on field values for details. + + +### func (Deputy) Run +``` go +func (d Deputy) Run(cmd *exec.Cmd) error +``` +Run starts the specified command and waits for it to complete. Its behavior +conforms to the Options passed to it at construction time. + +Note that, like cmd.Run, Deputy.Run should not be used with +StdoutPipe or StderrPipe. + + +## type ErrorHandling +``` go +type ErrorHandling int +``` +ErrorHandling is a flag that tells Deputy how to handle errors running a +command. See the values below for the different modes. + +``` go +const ( + // DefaultErrs represents the default handling of command errors - this + // simply returns the error from Cmd.Run() + DefaultErrs ErrorHandling = iota + + // FromStderr tells Deputy to convert the stderr output of a command into + // the text of an error, if the command exits with an error. + FromStderr + + // FromStdout tells Deputy to convert the stdout output of a command into + // the text of an error, if the command exits with an error. + FromStdout +) +``` \ No newline at end of file === added file 'src/github.com/juju/deputy/deputy.go' --- src/github.com/juju/deputy/deputy.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/deputy/deputy.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,197 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +// Package deputy provides more advanced options for running commands. +package deputy + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os/exec" + "time" +) + +// ErrorHandling is a flag that tells Deputy how to handle errors running a +// command. See the values below for the different modes. +type ErrorHandling int + +const ( + // DefaultErrs represents the default handling of command errors - this + // simply returns the error from Cmd.Run() + DefaultErrs ErrorHandling = iota + + // FromStderr tells Deputy to convert the stderr output of a command into + // the text of an error, if the command exits with an error. + FromStderr + + // FromStdout tells Deputy to convert the stdout output of a command into + // the text of an error, if the command exits with an error. + FromStdout +) + +// Deputy is a type that runs Commands with advanced options not available from +// os/exec. See the comments on field values for details. +type Deputy struct { + // Timeout represents the longest time the command will be allowed to run + // before being killed. + Timeout time.Duration + // Errors describes how errors should be handled. + Errors ErrorHandling + // StdoutLog takes a function that will receive lines written to stdout from + // the command (with the newline elided). + StdoutLog func([]byte) + // StdoutLog takes a function that will receive lines written to stderr from + // the command (with the newline elided). + StderrLog func([]byte) + + stderrPipe io.ReadCloser + stdoutPipe io.ReadCloser +} + +// Run starts the specified command and waits for it to complete. Its behavior +// conforms to the Options passed to it at construction time. +// +// Note that, like cmd.Run, Deputy.Run should not be used with +// StdoutPipe or StderrPipe. +func (d Deputy) Run(cmd *exec.Cmd) error { + if err := d.makePipes(cmd); err != nil { + return err + } + + errsrc := &bytes.Buffer{} + if d.Errors == FromStderr { + cmd.Stderr = dualWriter(cmd.Stderr, errsrc) + } + if d.Errors == FromStdout { + cmd.Stdout = dualWriter(cmd.Stdout, errsrc) + } + + err := d.run(cmd) + + if d.Errors == DefaultErrs { + return err + } + + if err != nil && errsrc.Len() > 0 { + return fmt.Errorf("%s: %s", err, bytes.TrimSpace(errsrc.Bytes())) + } + return err +} + +func (d *Deputy) makePipes(cmd *exec.Cmd) error { + if d.StderrLog != nil { + var err error + d.stderrPipe, err = cmd.StderrPipe() + if err != nil { + return err + } + } + if d.StdoutLog != nil { + var err error + d.stdoutPipe, err = cmd.StdoutPipe() + if err != nil { + return err + } + } + return nil +} + +func dualWriter(w1, w2 io.Writer) io.Writer { + if w1 == nil { + return w2 + } + if w2 == nil { + return w1 + } + return io.MultiWriter(w1, w2) +} + +func (d Deputy) run(cmd *exec.Cmd) error { + errs := make(chan error) + if err := d.start(cmd, errs); err != nil { + return err + } + if d.Timeout == 0 { + return d.wait(cmd, errs) + } + + done := make(chan error) + + var err error + go func() { + err = d.wait(cmd, errs) + close(done) + }() + + select { + case <-time.After(d.Timeout): + // this may fail, but there's not much we can do about it + _ = cmd.Process.Kill() + return timeoutErr{cmd.Path} + case <-done: + return err + } +} + +func (d Deputy) start(cmd *exec.Cmd, errs chan<- error) error { + if err := cmd.Start(); err != nil { + return err + } + + if d.stdoutPipe != nil { + go pipe(d.StdoutLog, d.stdoutPipe, errs) + } + if d.stderrPipe != nil { + go pipe(d.StderrLog, d.stderrPipe, errs) + } + return nil +} + +func (d Deputy) wait(cmd *exec.Cmd, errs <-chan error) error { + // Note that it's important that we wait for the pipes + // to be closed before calling cmd.Wait otherwise + // Wait can close the pipes before we have read + // all their data. + var err1, err2 error + if d.stdoutPipe != nil { + err1 = <-errs + } + if d.stderrPipe != nil { + err2 = <-errs + } + err := cmd.Wait() + return firstErr(err, err1, err2) +} + +func firstErr(errs ...error) error { + for _, err := range errs { + if err != nil { + return err + } + } + return nil +} + +func pipe(log func([]byte), r io.Reader, errs chan<- error) { + scanner := bufio.NewScanner(r) + for scanner.Scan() { + b := scanner.Bytes() + log(b) + } + + errs <- scanner.Err() +} + +type timeoutErr struct { + path string +} + +func (t timeoutErr) IsTimeout() bool { + return true +} + +func (t timeoutErr) Error() string { + return fmt.Sprintf("timed out waiting for command %q to execute", t.path) +} === added file 'src/github.com/juju/deputy/deputy_test.go' --- src/github.com/juju/deputy/deputy_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/deputy/deputy_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,172 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package deputy + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "strconv" + "strings" + "testing" + "time" + + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" +) + +type suite struct{} + +var _ = gc.Suite(&suite{}) + +func Test(t *testing.T) { + gc.TestingT(t) +} + +type hasTimeout interface { + IsTimeout() bool +} + +func (*suite) TestRunTimeout(c *gc.C) { + cmd := maker{ + timeout: time.Second * 2, + }.make() + + err := Deputy{Timeout: time.Millisecond * 100}.Run(cmd) + + c.Assert(err, gc.NotNil) + if e, ok := err.(hasTimeout); !ok { + c.Errorf("Error caused by timeout does not have Timeout function") + } else { + c.Assert(e.IsTimeout(), jc.IsTrue) + } +} + +func (*suite) TestRunNoTimeout(c *gc.C) { + cmd := maker{}.make() + + err := Deputy{Timeout: time.Millisecond * 200}.Run(cmd) + + c.Assert(err, gc.IsNil) +} + +func (*suite) TestStdoutErr(c *gc.C) { + output := "foooo" + cmd := maker{ + stdout: output, + exit: 1, + }.make() + err := Deputy{Errors: FromStdout}.Run(cmd) + c.Assert(err, gc.ErrorMatches, ".*"+output) +} + +func (*suite) TestStdoutOutput(c *gc.C) { + output := "foooo" + out := &bytes.Buffer{} + cmd := maker{ + stdout: output, + exit: 1, + }.make() + cmd.Stdout = out + err := Deputy{Errors: FromStdout}.Run(cmd) + c.Check(err, gc.ErrorMatches, ".*"+output) + c.Check(output, gc.Equals, strings.TrimSpace(out.String())) +} + +func (*suite) TestStderrOutput(c *gc.C) { + output := "foooo" + out := &bytes.Buffer{} + + cmd := maker{ + stderr: output, + exit: 1, + }.make() + cmd.Stderr = out + err := Deputy{Errors: FromStderr}.Run(cmd) + c.Assert(err, gc.ErrorMatches, ".*"+output) + c.Assert(output, gc.Equals, strings.TrimSpace(out.String())) +} + +func (*suite) TestStderrErr(c *gc.C) { + output := "foooo" + + cmd := maker{ + stderr: output, + exit: 1, + }.make() + err := Deputy{Errors: FromStderr}.Run(cmd) + c.Assert(err, gc.ErrorMatches, ".*"+output) +} + +func (*suite) TestLogs(c *gc.C) { + stdout := "foo!" + stderr := "bar!" + cmd := maker{ + stderr: stderr, + stdout: stdout, + }.make() + var logout []byte + var logerr []byte + + err := Deputy{ + StdoutLog: func(b []byte) { logout = b }, + StderrLog: func(b []byte) { logerr = b }, + }.Run(cmd) + c.Assert(err, jc.ErrorIsNil) + c.Check(string(logout), gc.DeepEquals, stdout) + c.Check(string(logerr), gc.DeepEquals, stderr) +} + +type maker struct { + stdout string + stderr string + exit int + timeout time.Duration +} + +const ( + isHelperProc = "GO_HELPER_PROCESS_OK" + helperStdout = "GO_HELPER_PROCESS_STDOUT" + helperStderr = "GO_HELPER_PROCESS_STDERR" + helperExit = "GO_HELPER_PROCESS_EXIT_CODE" + helperTimeout = "GO_HELPER_PROCESS_TIMEOUT" +) + +func (m maker) make() *exec.Cmd { + cmd := exec.Command(os.Args[0], "-test.run=TestHelperProcess") + cmd.Env = []string{ + fmt.Sprintf("%s=%s", isHelperProc, "1"), + fmt.Sprintf("%s=%s", helperStdout, m.stdout), + fmt.Sprintf("%s=%s", helperStderr, m.stderr), + fmt.Sprintf("%s=%d", helperExit, m.exit), + fmt.Sprintf("%s=%d", helperTimeout, m.timeout.Nanoseconds()), + } + return cmd +} + +func TestHelperProcess(*testing.T) { + if os.Getenv(isHelperProc) != "1" { + return + } + exit, err := strconv.Atoi(os.Getenv(helperExit)) + if err != nil { + fmt.Fprintf(os.Stderr, "error converting exit code: %s", err) + os.Exit(2) + } + defer os.Exit(exit) + + nanos, err := strconv.Atoi(os.Getenv(helperTimeout)) + if err != nil { + fmt.Fprintf(os.Stderr, "error converting timeout: %s", err) + os.Exit(2) + } + <-time.After(time.Duration(int64(nanos)) * time.Nanosecond) + if stderr := os.Getenv(helperStderr); stderr != "" { + fmt.Fprint(os.Stderr, stderr) + } + if stdout := os.Getenv(helperStdout); stdout != "" { + fmt.Fprint(os.Stdout, stdout) + } +} === added file 'src/github.com/juju/deputy/example_test.go' --- src/github.com/juju/deputy/example_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/deputy/example_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,26 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package deputy_test + +import ( + "log" + "os/exec" + "time" + + "github.com/juju/deputy" +) + +func Example() { + // Make a new deputy that'll return the data written to stderr as the error + // message, log everything written to stdout to this application's log, and + // timeout after 30 seconds. + d := deputy.Deputy{ + Errors: deputy.FromStderr, + StdoutLog: func(b []byte) { log.Print(string(b)) }, + Timeout: time.Second * 30, + } + if err := d.Run(exec.Command("foo")); err != nil { + log.Print(err) + } +} === modified file 'src/github.com/juju/juju/Makefile' --- src/github.com/juju/juju/Makefile 2014-09-11 18:13:12 +0000 +++ src/github.com/juju/juju/Makefile 2015-10-23 18:29:32 +0000 @@ -101,6 +101,11 @@ @echo Installing bash completion @sudo install -o root -g root -m 644 etc/bash_completion.d/juju-core /etc/bash_completion.d +GOCHECK_COUNT="$(shell go list -f '{{join .Deps "\n"}}' github.com/juju/juju/... | grep -c "gopkg.in/check.v*")" +check-deps: + @echo "$(GOCHECK_COUNT) instances of gocheck not in test code" + .PHONY: build check install .PHONY: clean format simplify .PHONY: install-dependencies +.PHONY: check-deps === modified file 'src/github.com/juju/juju/README.md' --- src/github.com/juju/juju/README.md 2014-08-20 15:00:12 +0000 +++ src/github.com/juju/juju/README.md 2015-10-23 18:29:32 +0000 @@ -49,7 +49,7 @@ Add `$GOPATH/bin` to your `PATH`, so you can run the go programs you install: - PATH="$PATH:$GOPATH/bin" + PATH="$GOPATH/bin:$PATH" Getting juju === modified file 'src/github.com/juju/juju/agent/agent.go' --- src/github.com/juju/juju/agent/agent.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/agent/agent.go 2015-10-23 18:29:32 +0000 @@ -32,6 +32,13 @@ var logger = loggo.GetLogger("juju.agent") +const ( + // UninstallAgentFile is the name of the file inside the data + // dir that, if it exists, will cause a machine agent to uninstall + // when it receives the termination signal. + UninstallAgentFile = "uninstall-agent" +) + // These are base values used for the corresponding defaults. var ( logDir = paths.MustSucceed(paths.LogDir(version.Current.Series)) @@ -39,6 +46,48 @@ confDir = paths.MustSucceed(paths.ConfDir(version.Current.Series)) ) +// Agent exposes the agent's configuration to other components. This +// interface should probably be segregated (agent.ConfigGetter and +// agent.ConfigChanger?) but YAGNI *currently* advises against same. +type Agent interface { + + // CurrentConfig returns a copy of the agent's configuration. No + // guarantees regarding ongoing correctness are made. + CurrentConfig() Config + + // ChangeConfig allows clients to change the agent's configuration + // by supplying a callback that applies the changes. + ChangeConfig(ConfigMutator) error +} + +// APIHostPortsSetter trivially wraps an Agent to implement +// worker/apiaddressupdater/APIAddressSetter. +type APIHostPortsSetter struct { + Agent +} + +// SetAPIHostPorts is the APIAddressSetter interface. +func (s APIHostPortsSetter) SetAPIHostPorts(servers [][]network.HostPort) error { + return s.ChangeConfig(func(c ConfigSetter) error { + c.SetAPIHostPorts(servers) + return nil + }) +} + +// SetStateServingInfo trivially wraps an Agent to implement +// worker/certupdater/SetStateServingInfo. +type StateServingInfoSetter struct { + Agent +} + +// SetStateServingInfo is the SetStateServingInfo interface. +func (s StateServingInfoSetter) SetStateServingInfo(info params.StateServingInfo) error { + return s.ChangeConfig(func(c ConfigSetter) error { + c.SetStateServingInfo(info) + return nil + }) +} + var ( // DefaultLogDir defines the default log directory for juju agents. // It's defined as a variable so it could be overridden in tests. @@ -124,8 +173,9 @@ // are available StateServingInfo() (params.StateServingInfo, bool) - // APIInfo returns details for connecting to the API server. - APIInfo() *api.Info + // APIInfo returns details for connecting to the API server and + // reports whether the details are available. + APIInfo() (*api.Info, bool) // MongoInfo returns details for connecting to the state server's mongo // database and reports whether those details are available @@ -152,7 +202,7 @@ Environment() names.EnvironTag } -type ConfigSetterOnly interface { +type configSetterOnly interface { // Clone returns a copy of the configuration that // is unaffected by subsequent calls to the Set* // methods @@ -212,12 +262,12 @@ type ConfigSetter interface { Config - ConfigSetterOnly + configSetterOnly } type ConfigSetterWriter interface { Config - ConfigSetterOnly + configSetterOnly ConfigWriter } @@ -500,12 +550,10 @@ } var addrs []string for _, serverHostPorts := range servers { - addr := network.SelectInternalHostPort(serverHostPorts, false) - if addr != "" { - addrs = append(addrs, addr) - } + addrs = append(addrs, network.SelectInternalHostPorts(serverHostPorts, false)...) } c.apiDetails.addresses = addrs + logger.Infof("API server address details %q written to agent config as %q", servers, addrs) } func (c *configInternal) SetValue(key, value string) { @@ -662,6 +710,7 @@ return buf.Bytes(), nil } +// WriteCommands is defined on Config interface. func (c *configInternal) WriteCommands(renderer shell.Renderer) ([]string, error) { data, err := c.fileContents() if err != nil { @@ -674,7 +723,11 @@ return commands, nil } -func (c *configInternal) APIInfo() *api.Info { +// APIInfo is defined on Config interface. +func (c *configInternal) APIInfo() (*api.Info, bool) { + if c.apiDetails == nil || c.apiDetails.addresses == nil { + return nil, false + } servingInfo, isStateServer := c.StateServingInfo() addrs := c.apiDetails.addresses if isStateServer { @@ -701,9 +754,10 @@ Tag: c.tag, Nonce: c.nonce, EnvironTag: c.environment, - } + }, true } +// MongoInfo is defined on Config interface. func (c *configInternal) MongoInfo() (info *mongo.MongoInfo, ok bool) { ssi, ok := c.StateServingInfo() if !ok { === modified file 'src/github.com/juju/juju/agent/agent_test.go' --- src/github.com/juju/juju/agent/agent_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/agent/agent_test.go 2015-10-23 18:29:32 +0000 @@ -542,12 +542,19 @@ c.Assert(reread, jc.DeepEquals, conf) } +func (*suite) TestAPIInfoMissingAddress(c *gc.C) { + conf := agent.EmptyConfig() + _, ok := conf.APIInfo() + c.Assert(ok, jc.IsFalse) +} + func (*suite) TestAPIInfoAddsLocalhostWhenServingInfoPresent(c *gc.C) { attrParams := attributeParams servingInfo := stateServingInfo() conf, err := agent.NewStateMachineConfig(attrParams, servingInfo) c.Assert(err, jc.ErrorIsNil) - apiinfo := conf.APIInfo() + apiinfo, ok := conf.APIInfo() + c.Assert(ok, jc.IsTrue) c.Check(apiinfo.Addrs, gc.HasLen, len(attrParams.APIAddresses)+1) localhostAddressFound := false for _, eachApiAddress := range apiinfo.Addrs { @@ -565,7 +572,8 @@ servingInfo := stateServingInfo() conf, err := agent.NewStateMachineConfig(attrParams, servingInfo) c.Assert(err, jc.ErrorIsNil) - apiinfo := conf.APIInfo() + apiinfo, ok := conf.APIInfo() + c.Assert(ok, jc.IsTrue) c.Check(apiinfo.Addrs, gc.HasLen, len(attrParams.APIAddresses)+1) localhostAddressFound := false for _, eachApiAddress := range apiinfo.Addrs { @@ -601,7 +609,8 @@ attrParams.PreferIPv6 = false conf, err := agent.NewAgentConfig(attrParams) c.Assert(err, jc.ErrorIsNil) - apiinfo := conf.APIInfo() + apiinfo, ok := conf.APIInfo() + c.Assert(ok, jc.IsTrue) c.Assert(apiinfo.Addrs, gc.DeepEquals, attrParams.APIAddresses) } @@ -610,7 +619,8 @@ attrParams.PreferIPv6 = true conf, err := agent.NewAgentConfig(attrParams) c.Assert(err, jc.ErrorIsNil) - apiinfo := conf.APIInfo() + apiinfo, ok := conf.APIInfo() + c.Assert(ok, jc.IsTrue) c.Assert(apiinfo.Addrs, gc.DeepEquals, attrParams.APIAddresses) } @@ -629,7 +639,9 @@ Nonce: attrParams.Nonce, EnvironTag: attrParams.Environment, } - c.Assert(conf.APIInfo(), jc.DeepEquals, expectAPIInfo) + apiInfo, ok := conf.APIInfo() + c.Assert(ok, jc.IsTrue) + c.Assert(apiInfo, jc.DeepEquals, expectAPIInfo) addr := fmt.Sprintf("127.0.0.1:%d", servingInfo.StatePort) expectStateInfo := &mongo.MongoInfo{ Info: mongo.Info{ @@ -648,7 +660,9 @@ expectAPIInfo.Password = "newpassword" expectStateInfo.Password = "newpassword" - c.Assert(conf.APIInfo(), jc.DeepEquals, expectAPIInfo) + apiInfo, ok = conf.APIInfo() + c.Assert(ok, jc.IsTrue) + c.Assert(apiInfo, jc.DeepEquals, expectAPIInfo) info, ok = conf.MongoInfo() c.Assert(ok, jc.IsTrue) c.Assert(info, jc.DeepEquals, expectStateInfo) @@ -682,27 +696,42 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(addrs, gc.DeepEquals, attributeParams.APIAddresses) - // The first cloud-local address for each server is used, - // else if there are none then the first public- or unknown- - // scope address. + // All the best candidate addresses for each server are + // used. Cloud-local addresses are preferred. Otherwise, public + // or unknown scope addresses are used. // // If a server has only machine-local addresses, or none // at all, then it will be excluded. - server1 := network.NewAddresses("0.1.2.3", "0.1.2.4", "zeroonetwothree") + server1 := network.NewAddresses("0.1.0.1", "0.1.0.2", "host.com") server1[0].Scope = network.ScopeCloudLocal server1[1].Scope = network.ScopeCloudLocal server1[2].Scope = network.ScopePublic - server2 := network.NewAddresses("127.0.0.1") - server2[0].Scope = network.ScopeMachineLocal - server3 := network.NewAddresses("0.1.2.5", "zeroonetwofive") - server3[0].Scope = network.ScopeUnknown - server3[1].Scope = network.ScopeUnknown + + server2 := network.NewAddresses("0.2.0.1", "0.2.0.2") + server2[0].Scope = network.ScopePublic + server2[1].Scope = network.ScopePublic + + server3 := network.NewAddresses("127.0.0.1") + server3[0].Scope = network.ScopeMachineLocal + + server4 := network.NewAddresses("0.4.0.1", "elsewhere.net") + server4[0].Scope = network.ScopeUnknown + server4[1].Scope = network.ScopeUnknown + conf.SetAPIHostPorts([][]network.HostPort{ - network.AddressesWithPort(server1, 123), - network.AddressesWithPort(server2, 124), - network.AddressesWithPort(server3, 125), + network.AddressesWithPort(server1, 1111), + network.AddressesWithPort(server2, 2222), + network.AddressesWithPort(server3, 3333), + network.AddressesWithPort(server4, 4444), }) addrs, err = conf.APIAddresses() c.Assert(err, jc.ErrorIsNil) - c.Assert(addrs, gc.DeepEquals, []string{"0.1.2.3:123", "0.1.2.5:125"}) + c.Assert(addrs, gc.DeepEquals, []string{ + "0.1.0.1:1111", + "0.1.0.2:1111", + "0.2.0.1:2222", + "0.2.0.2:2222", + "0.4.0.1:4444", + "elsewhere.net:4444", + }) } === modified file 'src/github.com/juju/juju/agent/export_test.go' --- src/github.com/juju/juju/agent/export_test.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/agent/export_test.go 2015-10-23 18:29:32 +0000 @@ -55,3 +55,7 @@ MachineJobFromParams = machineJobFromParams IsLocalEnv = &isLocalEnv ) + +func EmptyConfig() Config { + return &configInternal{} +} === modified file 'src/github.com/juju/juju/agent/format-1.18_whitebox_test.go' --- src/github.com/juju/juju/agent/format-1.18_whitebox_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/agent/format-1.18_whitebox_test.go 2015-10-23 18:29:32 +0000 @@ -52,7 +52,9 @@ c.Assert(configDataDir, gc.Equals, realDataDir) c.Assert(readConfig.PreferIPv6(), jc.IsFalse) // The api info doesn't have the environment tag set. - c.Assert(readConfig.APIInfo().EnvironTag.Id(), gc.Equals, "") + apiInfo, ok := readConfig.APIInfo() + c.Assert(ok, jc.IsTrue) + c.Assert(apiInfo.EnvironTag.Id(), gc.Equals, "") } func (s *format_1_18Suite) TestStatePortNotParsedWithoutSecret(c *gc.C) { === added directory 'src/github.com/juju/juju/api/addresser' === added file 'src/github.com/juju/juju/api/addresser/addresser.go' --- src/github.com/juju/juju/api/addresser/addresser.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/addresser/addresser.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,78 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package addresser + +import ( + "github.com/juju/errors" + "github.com/juju/loggo" + + "github.com/juju/juju/api/base" + "github.com/juju/juju/api/watcher" + "github.com/juju/juju/apiserver/params" +) + +var logger = loggo.GetLogger("juju.api.addresser") + +const addresserFacade = "Addresser" + +// API provides access to the InstancePoller API facade. +type API struct { + facade base.FacadeCaller +} + +// NewAPI creates a new client-side Addresser facade. +func NewAPI(caller base.APICaller) *API { + if caller == nil { + panic("caller is nil") + } + return &API{ + facade: base.NewFacadeCaller(caller, addresserFacade), + } +} + +// CanDeallocateAddresses checks if the current environment can +// deallocate IP addresses. +func (api *API) CanDeallocateAddresses() (bool, error) { + var result params.BoolResult + if err := api.facade.FacadeCall("CanDeallocateAddresses", nil, &result); err != nil { + return false, errors.Trace(err) + } + if result.Error == nil { + return result.Result, nil + } + return false, errors.Trace(result.Error) +} + +// CleanupIPAddresses releases and removes the dead IP addresses. If not +// all IP addresses could be released and removed a params.ErrTryAgain +// is returned. +func (api *API) CleanupIPAddresses() error { + var result params.ErrorResult + if err := api.facade.FacadeCall("CleanupIPAddresses", nil, &result); err != nil { + return errors.Trace(err) + } + if result.Error == nil { + return nil + } + return errors.Trace(result.Error) +} + +var newEntityWatcher = watcher.NewEntityWatcher + +// WatchIPAddresses returns a EntityWatcher for observing the +// tags of IP addresses with changes in life cycle. +// The initial event will contain the tags of any IP addresses +// which are no longer Alive. +func (api *API) WatchIPAddresses() (watcher.EntityWatcher, error) { + var result params.EntityWatchResult + err := api.facade.FacadeCall("WatchIPAddresses", nil, &result) + if err != nil { + return nil, errors.Trace(err) + } + if result.Error == nil { + w := newEntityWatcher(api.facade.RawAPICaller(), result) + return w, nil + } + return nil, errors.Trace(result.Error) +} === added file 'src/github.com/juju/juju/api/addresser/addresser_test.go' --- src/github.com/juju/juju/api/addresser/addresser_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/addresser/addresser_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,172 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package addresser_test + +import ( + "errors" + + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/api/addresser" + "github.com/juju/juju/api/base" + apitesting "github.com/juju/juju/api/base/testing" + "github.com/juju/juju/api/watcher" + "github.com/juju/juju/apiserver/params" + apiservertesting "github.com/juju/juju/apiserver/testing" + coretesting "github.com/juju/juju/testing" +) + +type AddresserSuite struct { + coretesting.BaseSuite +} + +var _ = gc.Suite(&AddresserSuite{}) + +func (s *AddresserSuite) TestNewAPI(c *gc.C) { + var called int + apiCaller := clientErrorAPICaller(c, "CleanupIPAddresses", nil, &called) + api := addresser.NewAPI(apiCaller) + c.Check(api, gc.NotNil) + c.Check(called, gc.Equals, 0) + + // Make a call so that an error will be returned. + err := api.CleanupIPAddresses() + c.Assert(err, gc.ErrorMatches, "client error!") + c.Assert(called, gc.Equals, 1) +} + +func (s *AddresserSuite) TestNewAPIWithNilCaller(c *gc.C) { + panicFunc := func() { addresser.NewAPI(nil) } + c.Assert(panicFunc, gc.PanicMatches, "caller is nil") +} + +func (s *AddresserSuite) TestCanDeallocateAddressesSuccess(c *gc.C) { + var called int + expectedResult := params.BoolResult{ + Result: true, + } + apiCaller := successAPICaller(c, "CanDeallocateAddresses", nil, expectedResult, &called) + api := addresser.NewAPI(apiCaller) + + ok, err := api.CanDeallocateAddresses() + c.Assert(err, jc.ErrorIsNil) + c.Assert(ok, jc.IsTrue) + c.Assert(called, gc.Equals, 1) +} + +func (s *AddresserSuite) TestCanDeallocateAddressesServerError(c *gc.C) { + var called int + expectedResult := params.BoolResult{ + Error: apiservertesting.ServerError("server boom!"), + } + apiCaller := successAPICaller(c, "CanDeallocateAddresses", nil, expectedResult, &called) + api := addresser.NewAPI(apiCaller) + + ok, err := api.CanDeallocateAddresses() + c.Assert(err, gc.ErrorMatches, "server boom!") + c.Assert(ok, jc.IsFalse) + c.Assert(called, gc.Equals, 1) +} + +func (s *AddresserSuite) TestCleanupIPAddressesSuccess(c *gc.C) { + var called int + expectedResult := params.ErrorResult{} + apiCaller := successAPICaller(c, "CleanupIPAddresses", nil, expectedResult, &called) + api := addresser.NewAPI(apiCaller) + + err := api.CleanupIPAddresses() + c.Assert(err, jc.ErrorIsNil) + c.Assert(called, gc.Equals, 1) +} + +func (s *AddresserSuite) TestCleanupIPAddressesServerError(c *gc.C) { + var called int + expectedResult := params.ErrorResult{ + Error: apiservertesting.ServerError("server boom!"), + } + apiCaller := successAPICaller(c, "CleanupIPAddresses", nil, expectedResult, &called) + api := addresser.NewAPI(apiCaller) + + err := api.CleanupIPAddresses() + c.Assert(err, gc.ErrorMatches, "server boom!") + c.Assert(called, gc.Equals, 1) +} + +func (s *AddresserSuite) TestWatchIPAddressesSuccess(c *gc.C) { + var numFacadeCalls int + var numWatcherCalls int + expectedResult := params.EntityWatchResult{ + EntityWatcherId: "42", + Changes: []string{ + "ipaddress-11111111-0000-0000-0000-000000000000", + "ipaddress-22222222-0000-0000-0000-000000000000", + }, + } + watcherFunc := func(caller base.APICaller, result params.EntityWatchResult) watcher.EntityWatcher { + numWatcherCalls++ + c.Check(caller, gc.NotNil) + c.Check(result, jc.DeepEquals, expectedResult) + return nil + } + s.PatchValue(addresser.NewEntityWatcher, watcherFunc) + + apiCaller := successAPICaller(c, "WatchIPAddresses", nil, expectedResult, &numFacadeCalls) + api := addresser.NewAPI(apiCaller) + + w, err := api.WatchIPAddresses() + c.Assert(err, jc.ErrorIsNil) + c.Assert(numFacadeCalls, gc.Equals, 1) + c.Assert(numWatcherCalls, gc.Equals, 1) + c.Assert(w, gc.IsNil) +} + +func (s *AddresserSuite) TestWatchIPAddressesClientError(c *gc.C) { + var called int + apiCaller := clientErrorAPICaller(c, "WatchIPAddresses", nil, &called) + + api := addresser.NewAPI(apiCaller) + w, err := api.WatchIPAddresses() + + c.Assert(w, jc.ErrorIsNil) + c.Assert(err, gc.ErrorMatches, "client error!") + c.Assert(called, gc.Equals, 1) +} + +func (s *AddresserSuite) TestWatchIPAddressesServerError(c *gc.C) { + var called int + expectedResult := params.EntityWatchResult{ + Error: apiservertesting.ServerError("server boom!"), + } + apiCaller := successAPICaller(c, "WatchIPAddresses", nil, expectedResult, &called) + api := addresser.NewAPI(apiCaller) + + w, err := api.WatchIPAddresses() + c.Assert(w, jc.ErrorIsNil) + c.Assert(err, gc.ErrorMatches, "server boom!") + c.Assert(called, gc.Equals, 1) +} + +func successAPICaller(c *gc.C, method string, expectArgs, useResults interface{}, numCalls *int) base.APICaller { + args := &apitesting.CheckArgs{ + Facade: "Addresser", + VersionIsZero: true, + IdIsEmpty: true, + Method: method, + Args: expectArgs, + Results: useResults, + } + return apitesting.CheckingAPICaller(c, args, numCalls, nil) +} + +func clientErrorAPICaller(c *gc.C, method string, expectArgs interface{}, numCalls *int) base.APICaller { + args := &apitesting.CheckArgs{ + Facade: "Addresser", + VersionIsZero: true, + IdIsEmpty: true, + Method: method, + Args: expectArgs, + } + return apitesting.CheckingAPICaller(c, args, numCalls, errors.New("client error!")) +} === added file 'src/github.com/juju/juju/api/addresser/export_test.go' --- src/github.com/juju/juju/api/addresser/export_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/addresser/export_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,6 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package addresser + +var NewEntityWatcher = &newEntityWatcher === added file 'src/github.com/juju/juju/api/addresser/package_test.go' --- src/github.com/juju/juju/api/addresser/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/addresser/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,14 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package addresser_test + +import ( + stdtesting "testing" + + "github.com/juju/juju/testing" +) + +func TestAll(t *stdtesting.T) { + testing.MgoTestPackage(t) +} === modified file 'src/github.com/juju/juju/api/agent/machine_test.go' --- src/github.com/juju/juju/api/agent/machine_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/agent/machine_test.go 2015-10-23 18:29:32 +0000 @@ -90,7 +90,7 @@ type machineSuite struct { testing.JujuConnSuite machine *state.Machine - st *api.State + st api.Connection } var _ = gc.Suite(&machineSuite{}) === modified file 'src/github.com/juju/juju/api/agent/state.go' --- src/github.com/juju/juju/api/agent/state.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/api/agent/state.go 2015-10-23 18:29:32 +0000 @@ -124,7 +124,7 @@ func (m *Entity) ClearReboot() error { var result params.ErrorResults args := params.SetStatus{ - Entities: []params.EntityStatus{ + Entities: []params.EntityStatusArgs{ {Tag: m.tag.String()}, }, } === modified file 'src/github.com/juju/juju/api/agent/unit_test.go' --- src/github.com/juju/juju/api/agent/unit_test.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/api/agent/unit_test.go 2015-10-23 18:29:32 +0000 @@ -22,7 +22,7 @@ type unitSuite struct { testing.JujuConnSuite unit *state.Unit - st *api.State + st api.Connection } func (s *unitSuite) SetUpTest(c *gc.C) { @@ -34,6 +34,7 @@ password, err := utils.RandomPassword() c.Assert(err, jc.ErrorIsNil) err = s.unit.SetPassword(password) + c.Assert(err, jc.ErrorIsNil) s.st = s.OpenAPIAs(c, s.unit.Tag(), password) } === modified file 'src/github.com/juju/juju/api/allwatcher.go' --- src/github.com/juju/juju/api/allwatcher.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/api/allwatcher.go 2015-10-23 18:29:32 +0000 @@ -9,27 +9,65 @@ "github.com/juju/juju/state/multiwatcher" ) -// AllWatcher holds information allowing us to get Deltas describing changes -// to the entire environment. +// AllWatcher holds information allowing us to get Deltas describing +// changes to the entire environment or all environments (depending on +// the watcher type). type AllWatcher struct { - caller base.APICaller - id *string -} - -func newAllWatcher(caller base.APICaller, id *string) *AllWatcher { - return &AllWatcher{caller, id} -} - + objType string + caller base.APICaller + id *string +} + +// NewAllWatcher returns an AllWatcher instance which interacts with a +// watcher created by the WatchAll API call. +// +// There should be no need to call this from outside of the api +// package. It is only used by Client.WatchAll in this package. +func NewAllWatcher(caller base.APICaller, id *string) *AllWatcher { + return newAllWatcher("AllWatcher", caller, id) +} + +// NewAllEnvWatcher returns an AllWatcher instance which interacts +// with a watcher created by the WatchAllEnvs API call. +// +// There should be no need to call this from outside of the api +// package. It is only used by Client.WatchAllEnvs in +// api/systemmanager. +func NewAllEnvWatcher(caller base.APICaller, id *string) *AllWatcher { + return newAllWatcher("AllEnvWatcher", caller, id) +} + +func newAllWatcher(objType string, caller base.APICaller, id *string) *AllWatcher { + return &AllWatcher{ + objType: objType, + caller: caller, + id: id, + } +} + +// Next returns a new set of deltas from a watcher previously created +// by the WatchAll or WatchAllEnvs API calls. It will block until +// there are deltas to return. func (watcher *AllWatcher) Next() ([]multiwatcher.Delta, error) { var info params.AllWatcherNextResults err := watcher.caller.APICall( - "AllWatcher", watcher.caller.BestFacadeVersion("AllWatcher"), - *watcher.id, "Next", nil, &info) + watcher.objType, + watcher.caller.BestFacadeVersion(watcher.objType), + *watcher.id, + "Next", + nil, &info, + ) return info.Deltas, err } +// Stop shutdowns down a watcher previously created by the WatchAll or +// WatchAllEnvs API calls func (watcher *AllWatcher) Stop() error { return watcher.caller.APICall( - "AllWatcher", watcher.caller.BestFacadeVersion("AllWatcher"), - *watcher.id, "Stop", nil, nil) + watcher.objType, + watcher.caller.BestFacadeVersion(watcher.objType), + *watcher.id, + "Stop", + nil, nil, + ) } === modified file 'src/github.com/juju/juju/api/apiclient.go' --- src/github.com/juju/juju/api/apiclient.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/apiclient.go 2015-10-23 18:29:32 +0000 @@ -1,4 +1,4 @@ -// Copyright 2012, 2013 Canonical Ltd. +// Copyright 2012-2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package api @@ -19,7 +19,6 @@ "golang.org/x/net/websocket" "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/cert" "github.com/juju/juju/network" "github.com/juju/juju/rpc" "github.com/juju/juju/rpc/jsoncodec" @@ -86,73 +85,19 @@ certPool *x509.CertPool } -// Info encapsulates information about a server holding juju state and -// can be used to make a connection to it. -type Info struct { - // Addrs holds the addresses of the state servers. - Addrs []string - - // CACert holds the CA certificate that will be used - // to validate the state server's certificate, in PEM format. - CACert string - - // Tag holds the name of the entity that is connecting. - // If this is nil, and the password are empty, no login attempt will be made. - // (this is to allow tests to access the API to check that operations - // fail when not logged in). - Tag names.Tag - - // Password holds the password for the administrator or connecting entity. - Password string - - // Nonce holds the nonce used when provisioning the machine. Used - // only by the machine agent. - Nonce string `yaml:",omitempty"` - - // EnvironTag holds the environ tag for the environment we are - // trying to connect to. - EnvironTag names.EnvironTag -} - -// DialOpts holds configuration parameters that control the -// Dialing behavior when connecting to a state server. -type DialOpts struct { - // DialAddressInterval is the amount of time to wait - // before starting to dial another address. - DialAddressInterval time.Duration - - // Timeout is the amount of time to wait contacting - // a state server. - Timeout time.Duration - - // RetryDelay is the amount of time to wait between - // unsucssful connection attempts. - RetryDelay time.Duration -} - -// DefaultDialOpts returns a DialOpts representing the default -// parameters for contacting a state server. -func DefaultDialOpts() DialOpts { - return DialOpts{ - DialAddressInterval: 50 * time.Millisecond, - Timeout: 10 * time.Minute, - RetryDelay: 2 * time.Second, - } -} - // Open establishes a connection to the API server using the Info // given, returning a State instance which can be used to make API // requests. // // See Connect for details of the connection mechanics. -func Open(info *Info, opts DialOpts) (*State, error) { +func Open(info *Info, opts DialOpts) (Connection, error) { return open(info, opts, (*State).Login) } // This unexported open method is used both directly above in the Open // function, and also the OpenWithVersion function below to explicitly cause // the API server to think that the client is older than it really is. -func open(info *Info, opts DialOpts, loginFunc func(st *State, tag, pwd, nonce string) error) (*State, error) { +func open(info *Info, opts DialOpts, loginFunc func(st *State, tag, pwd, nonce string) error) (Connection, error) { conn, err := Connect(info, "", nil, opts) if err != nil { return nil, errors.Trace(err) @@ -187,7 +132,7 @@ // OpenWithVersion uses an explicit version of the Admin facade to call Login // on. This allows the caller to pretend to be an older client, and is used // only in testing. -func OpenWithVersion(info *Info, opts DialOpts, loginVersion int) (*State, error) { +func OpenWithVersion(info *Info, opts DialOpts, loginVersion int) (Connection, error) { var loginFunc func(st *State, tag, pwd, nonce string) error switch loginVersion { case 0: @@ -217,31 +162,17 @@ return nil, errors.New(`path tail must start with "/"`) } - pool := x509.NewCertPool() - xcert, err := cert.ParseCert(info.CACert) + pool, err := CreateCertPool(info.CACert) if err != nil { return nil, errors.Annotate(err, "cert pool creation failed") } - pool.AddCert(xcert) - - // If they exist, only use localhost addresses. - var addrs []string - for _, addr := range info.Addrs { - if strings.HasPrefix(addr, "localhost:") { - addrs = append(addrs, addr) - break - } - } - if len(addrs) == 0 { - addrs = info.Addrs - } path := makeAPIPath(info.EnvironTag.Id(), pathTail) // Dial all addresses at reasonable intervals. try := parallel.NewTry(0, nil) defer try.Kill() - for _, addr := range addrs { + for _, addr := range info.Addrs { err := dialWebsocket(addr, path, header, opts, pool, try) if err == parallel.ErrStopped { break === modified file 'src/github.com/juju/juju/api/base/testing/apicaller.go' --- src/github.com/juju/juju/api/base/testing/apicaller.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/api/base/testing/apicaller.go 2015-10-23 18:29:32 +0000 @@ -3,7 +3,14 @@ package testing -import "github.com/juju/names" +import ( + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/api/base" + "github.com/juju/names" + "github.com/juju/testing" +) // APICallerFunc is a function type that implements APICaller. type APICallerFunc func(objType string, version int, id, request string, params, response interface{}) error @@ -23,3 +30,88 @@ func (APICallerFunc) Close() error { return nil } + +// CheckArgs holds the possible arguments to CheckingAPICaller(). Any +// fields non empty fields will be checked to match the arguments +// recieved by the APICall() method of the returned APICallerFunc. If +// Id is empty, but IdIsEmpty is true, the id argument is checked to +// be empty. The same applies to Version being empty, but if +// VersionIsZero set to true the version is checked to be 0. +type CheckArgs struct { + Facade string + Version int + Id string + Method string + Args interface{} + Results interface{} + + IdIsEmpty bool + VersionIsZero bool +} + +func checkArgs(c *gc.C, args *CheckArgs, facade string, version int, id, method string, inArgs, outResults interface{}) { + if args == nil { + c.Logf("checkArgs: args is nil!") + return + } else { + if args.Facade != "" { + c.Check(facade, gc.Equals, args.Facade) + } + if args.Version != 0 { + c.Check(version, gc.Equals, args.Version) + } else if args.VersionIsZero { + c.Check(version, gc.Equals, 0) + } + if args.Id != "" { + c.Check(id, gc.Equals, args.Id) + } else if args.IdIsEmpty { + c.Check(id, gc.Equals, "") + } + if args.Method != "" { + c.Check(method, gc.Equals, args.Method) + } + if args.Args != nil { + c.Check(inArgs, jc.DeepEquals, args.Args) + } + if args.Results != nil { + c.Check(outResults, gc.NotNil) + testing.PatchValue(outResults, args.Results) + } + } +} + +// CheckingAPICaller returns an APICallerFunc which can report the +// number of times its APICall() method was called (if numCalls is not +// nil), as well as check if any of the arguments passed to the +// APICall() method match the values given in args (if args itself is +// not nil, otherwise no arguments are checked). The final error +// result of the APICall() will be set to err. +func CheckingAPICaller(c *gc.C, args *CheckArgs, numCalls *int, err error) base.APICallCloser { + return APICallerFunc( + func(facade string, version int, id, method string, inArgs, outResults interface{}) error { + if numCalls != nil { + *numCalls++ + } + if args != nil { + checkArgs(c, args, facade, version, id, method, inArgs, outResults) + } + return err + }, + ) +} + +// NotifyingCheckingAPICaller returns an APICallerFunc which sends a message on the channel "called" every +// time it recives a call, as well as check if any of the arguments passed to the APICall() method match +// the values given in args (if args itself is not nil, otherwise no arguments are checked). The final +// error result of the APICall() will be set to err. +func NotifyingCheckingAPICaller(c *gc.C, args *CheckArgs, called chan struct{}, err error) base.APICaller { + return APICallerFunc( + func(facade string, version int, id, method string, inArgs, outResults interface{}) error { + called <- struct{}{} + if args != nil { + checkArgs(c, args, facade, version, id, method, inArgs, outResults) + } + return err + }, + ) +} === added file 'src/github.com/juju/juju/api/base/types.go' --- src/github.com/juju/juju/api/base/types.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/base/types.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,18 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package base + +import ( + "time" +) + +// UserEnvironment holds information about an environment and the last +// time the environment was accessed for a particular user. This is a client +// side structure that translates the owner tag into a user facing string. +type UserEnvironment struct { + Name string + UUID string + Owner string + LastConnection *time.Time +} === modified file 'src/github.com/juju/juju/api/block/client_test.go' --- src/github.com/juju/juju/api/block/client_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/block/client_test.go 2015-10-23 18:29:32 +0000 @@ -51,6 +51,7 @@ }) blockClient := block.NewClient(apiCaller) err := blockClient.SwitchBlockOn(blockType, msg) + c.Assert(called, jc.IsTrue) c.Assert(err, gc.IsNil) } @@ -72,6 +73,7 @@ }) blockClient := block.NewClient(apiCaller) err := blockClient.SwitchBlockOn("", "") + c.Assert(called, jc.IsTrue) c.Assert(errors.Cause(err), gc.ErrorMatches, errmsg) } @@ -104,6 +106,7 @@ }) blockClient := block.NewClient(apiCaller) err := blockClient.SwitchBlockOff(blockType) + c.Assert(called, jc.IsTrue) c.Assert(err, gc.IsNil) } @@ -125,6 +128,7 @@ }) blockClient := block.NewClient(apiCaller) err := blockClient.SwitchBlockOff("") + c.Assert(called, jc.IsTrue) c.Assert(errors.Cause(err), gc.ErrorMatches, errmsg) } === added file 'src/github.com/juju/juju/api/certpool.go' --- src/github.com/juju/juju/api/certpool.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/certpool.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,82 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package api + +import ( + "crypto/x509" + "io/ioutil" + "os" + "path/filepath" + + "github.com/juju/errors" + + "github.com/juju/juju/cert" + "github.com/juju/juju/juju/paths" + "github.com/juju/juju/version" +) + +var certDir = filepath.FromSlash(paths.MustSucceed(paths.CertDir(version.Current.Series))) + +// CreateCertPool creates a new x509.CertPool and adds in the caCert passed +// in. All certs from the cert directory (/etc/juju/cert.d on ubuntu) are +// also added. +func CreateCertPool(caCert string) (*x509.CertPool, error) { + + pool := x509.NewCertPool() + if caCert != "" { + xcert, err := cert.ParseCert(caCert) + if err != nil { + return nil, errors.Trace(err) + } + pool.AddCert(xcert) + } + + count := processCertDir(pool) + if count >= 0 { + logger.Debugf("added %d certs to the pool from %s", count, certDir) + } + + return pool, nil +} + +// processCertDir iterates through the certDir looking for *.pem files. +// Each pem file is read in turn and added to the pool. A count of the number +// of successful certificates processed is returned. +func processCertDir(pool *x509.CertPool) (count int) { + fileInfo, err := os.Stat(certDir) + if os.IsNotExist(err) { + logger.Tracef("cert dir %q does not exist", certDir) + return -1 + } + if err != nil { + logger.Infof("unexpected error reading cert dir: %s", err) + return -1 + } + if !fileInfo.IsDir() { + logger.Infof("cert dir %q is not a directory", certDir) + return -1 + } + + matches, err := filepath.Glob(filepath.Join(certDir, "*.pem")) + if err != nil { + logger.Infof("globbing files failed: %s", err) + return -1 + } + + for _, match := range matches { + data, err := ioutil.ReadFile(match) + if err != nil { + logger.Infof("error reading %q: %v", match, err) + continue + } + certificate, err := cert.ParseCert(string(data)) + if err != nil { + logger.Infof("error parsing cert %q: %v", match, err) + continue + } + pool.AddCert(certificate) + count++ + } + return count +} === added file 'src/github.com/juju/juju/api/certpool_test.go' --- src/github.com/juju/juju/api/certpool_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/certpool_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,144 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package api_test + +import ( + "fmt" + "io/ioutil" + "path/filepath" + "strings" + "time" + + "github.com/juju/loggo" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/api" + "github.com/juju/juju/cert" + "github.com/juju/juju/testing" +) + +type certPoolSuite struct { + testing.BaseSuite + logs *certLogs +} + +var _ = gc.Suite(&certPoolSuite{}) + +func (s *certPoolSuite) SetUpTest(c *gc.C) { + s.BaseSuite.SetUpTest(c) + s.logs = &certLogs{} + loggo.GetLogger("juju.api").SetLogLevel(loggo.TRACE) + loggo.RegisterWriter("api-certs", s.logs, loggo.TRACE) +} + +func (*certPoolSuite) TestCreateCertPoolNoCert(c *gc.C) { + pool, err := api.CreateCertPool("") + c.Assert(err, jc.ErrorIsNil) + c.Assert(pool.Subjects(), gc.HasLen, 0) +} + +func (*certPoolSuite) TestCreateCertPoolTestCert(c *gc.C) { + pool, err := api.CreateCertPool(testing.CACert) + c.Assert(err, jc.ErrorIsNil) + c.Assert(pool.Subjects(), gc.HasLen, 1) +} + +func (s *certPoolSuite) TestCreateCertPoolNoDir(c *gc.C) { + certDir := filepath.Join(c.MkDir(), "missing") + s.PatchValue(api.CertDir, certDir) + + pool, err := api.CreateCertPool("") + c.Assert(err, jc.ErrorIsNil) + c.Assert(pool.Subjects(), gc.HasLen, 0) + + c.Assert(s.logs.messages, gc.HasLen, 1) + // The directory not existing is likely to happen a lot, so it is only + // logged out at trace to help be explicit in the case where detailed + // debugging is needed. + c.Assert(s.logs.messages[0], gc.Matches, `TRACE cert dir ".*" does not exist`) +} + +func (s *certPoolSuite) TestCreateCertPoolNotADir(c *gc.C) { + certDir := filepath.Join(c.MkDir(), "missing") + s.PatchValue(api.CertDir, certDir) + // Make the certDir a file instead... + c.Assert(ioutil.WriteFile(certDir, []byte("blah"), 0644), jc.ErrorIsNil) + + pool, err := api.CreateCertPool("") + c.Assert(err, jc.ErrorIsNil) + c.Assert(pool.Subjects(), gc.HasLen, 0) + + c.Assert(s.logs.messages, gc.HasLen, 1) + c.Assert(s.logs.messages[0], gc.Matches, `INFO cert dir ".*" is not a directory`) +} + +func (s *certPoolSuite) TestCreateCertPoolEmptyDir(c *gc.C) { + certDir := c.MkDir() + s.PatchValue(api.CertDir, certDir) + + pool, err := api.CreateCertPool("") + c.Assert(err, jc.ErrorIsNil) + c.Assert(pool.Subjects(), gc.HasLen, 0) + c.Assert(s.logs.messages, gc.HasLen, 1) + c.Assert(s.logs.messages[0], gc.Matches, `DEBUG added 0 certs to the pool from .*`) +} + +func (s *certPoolSuite) TestCreateCertPoolLoadsPEMFiles(c *gc.C) { + certDir := c.MkDir() + s.PatchValue(api.CertDir, certDir) + s.addCert(c, filepath.Join(certDir, "first.pem")) + s.addCert(c, filepath.Join(certDir, "second.pem")) + s.addCert(c, filepath.Join(certDir, "third.pem")) + + pool, err := api.CreateCertPool("") + c.Assert(err, jc.ErrorIsNil) + c.Assert(pool.Subjects(), gc.HasLen, 3) + c.Assert(s.logs.messages, gc.HasLen, 1) + c.Assert(s.logs.messages[0], gc.Matches, `DEBUG added 3 certs to the pool from .*`) +} + +func (s *certPoolSuite) TestCreateCertPoolLoadsOnlyPEMFiles(c *gc.C) { + certDir := c.MkDir() + s.PatchValue(api.CertDir, certDir) + s.addCert(c, filepath.Join(certDir, "first.pem")) + c.Assert(ioutil.WriteFile(filepath.Join(certDir, "second.cert"), []byte("blah"), 0644), jc.ErrorIsNil) + + pool, err := api.CreateCertPool("") + c.Assert(err, jc.ErrorIsNil) + c.Assert(pool.Subjects(), gc.HasLen, 1) + c.Assert(s.logs.messages, gc.HasLen, 1) + c.Assert(s.logs.messages[0], gc.Matches, `DEBUG added 1 certs to the pool from .*`) +} + +func (s *certPoolSuite) TestCreateCertPoolLogsBadCerts(c *gc.C) { + certDir := c.MkDir() + s.PatchValue(api.CertDir, certDir) + c.Assert(ioutil.WriteFile(filepath.Join(certDir, "broken.pem"), []byte("blah"), 0644), jc.ErrorIsNil) + + pool, err := api.CreateCertPool("") + c.Assert(err, jc.ErrorIsNil) + c.Assert(pool.Subjects(), gc.HasLen, 0) + c.Assert(s.logs.messages, gc.HasLen, 2) + c.Assert(s.logs.messages[0], gc.Matches, `INFO error parsing cert ".*broken.pem": .*`) + c.Assert(s.logs.messages[1], gc.Matches, `DEBUG added 0 certs to the pool from .*`) +} + +func (s *certPoolSuite) addCert(c *gc.C, filename string) { + expiry := time.Now().UTC().AddDate(10, 0, 0) + pem, _, err := cert.NewCA("random env name", expiry) + c.Assert(err, jc.ErrorIsNil) + err = ioutil.WriteFile(filename, []byte(pem), 0644) + c.Assert(err, jc.ErrorIsNil) +} + +type certLogs struct { + 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)) + } +} === modified file 'src/github.com/juju/juju/api/charms/client.go' --- src/github.com/juju/juju/api/charms/client.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/charms/client.go 2015-10-23 18:29:32 +0000 @@ -53,3 +53,13 @@ } return charms.CharmURLs, nil } + +// IsMetered returns whether or not the charm is metered. +func (c *Client) IsMetered(charmURL string) (bool, error) { + args := params.CharmInfo{CharmURL: charmURL} + metered := ¶ms.IsMeteredResult{} + if err := c.facade.FacadeCall("IsMetered", args, metered); err != nil { + return false, err + } + return metered.Metered, nil +} === modified file 'src/github.com/juju/juju/api/charms/client_test.go' --- src/github.com/juju/juju/api/charms/client_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/charms/client_test.go 2015-10-23 18:29:32 +0000 @@ -18,6 +18,8 @@ charmsClient *charms.Client } +//TODO (mattyw) There are just mock tests in here. We need real tests for each api call. + var _ = gc.Suite(&charmsMockSuite{}) func (s *charmsMockSuite) TestCharmInfo(c *gc.C) { @@ -84,3 +86,31 @@ c.Assert(listResult, gc.HasLen, 1) c.Assert(listResult[0], gc.DeepEquals, curl) } + +func (s *charmsMockSuite) TestIsMeteredFalse(c *gc.C) { + var called bool + curl := "local:quantal/dummy-1" + apiCaller := basetesting.APICallerFunc( + func(objType string, + version int, + id, request string, + a, result interface{}, + ) error { + called = true + c.Check(objType, gc.Equals, "Charms") + c.Check(id, gc.Equals, "") + c.Check(request, gc.Equals, "IsMetered") + + args, ok := a.(params.CharmInfo) + c.Assert(ok, jc.IsTrue) + c.Assert(args.CharmURL, gc.DeepEquals, curl) + if wanted, k := result.(*charms.CharmInfo); k { + wanted.URL = curl + } + return nil + }) + charmsClient := charms.NewClient(apiCaller) + _, err := charmsClient.IsMetered(curl) + c.Assert(err, jc.ErrorIsNil) + c.Assert(called, jc.IsTrue) +} === added directory 'src/github.com/juju/juju/api/cleaner' === added file 'src/github.com/juju/juju/api/cleaner/cleaner.go' --- src/github.com/juju/juju/api/cleaner/cleaner.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/cleaner/cleaner.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,42 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package cleaner + +import ( + "github.com/juju/juju/api/base" + "github.com/juju/juju/api/watcher" + "github.com/juju/juju/apiserver/params" +) + +const cleanerFacade = "Cleaner" + +// API provides access to the Cleaner API facade. +type API struct { + facade base.FacadeCaller +} + +// NewAPI creates a new client-side Cleaner facade. +func NewAPI(caller base.APICaller) *API { + facadeCaller := base.NewFacadeCaller(caller, cleanerFacade) + return &API{facade: facadeCaller} +} + +// Cleanup calls the server-side Cleanup method. +func (api *API) Cleanup() error { + return api.facade.FacadeCall("Cleanup", nil, nil) +} + +// WatchCleanups calls the server-side WatchCleanups method. +func (api *API) WatchCleanups() (watcher.NotifyWatcher, error) { + var result params.NotifyWatchResult + err := api.facade.FacadeCall("WatchCleanups", nil, &result) + if err != nil { + return nil, err + } + if err := result.Error; err != nil { + return nil, result.Error + } + w := watcher.NewNotifyWatcher(api.facade.RawAPICaller(), result) + return w, nil +} === added file 'src/github.com/juju/juju/api/cleaner/cleaner_test.go' --- src/github.com/juju/juju/api/cleaner/cleaner_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/cleaner/cleaner_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,111 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package cleaner_test + +import ( + "errors" + "time" + + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/api/base" + apitesting "github.com/juju/juju/api/base/testing" + "github.com/juju/juju/api/cleaner" + "github.com/juju/juju/apiserver/params" + coretesting "github.com/juju/juju/testing" +) + +type CleanerSuite struct { + coretesting.BaseSuite +} + +var _ = gc.Suite(&CleanerSuite{}) + +type TestCommon struct { + apiCaller base.APICaller + apiArgs apitesting.CheckArgs + called chan struct{} + api *cleaner.API +} + +// Init returns a new, initialised instance of TestCommon. +func Init(c *gc.C, method string, expectArgs, useResults interface{}, err error) (t *TestCommon) { + t = &TestCommon{} + t.apiArgs = apitesting.CheckArgs{ + Facade: "Cleaner", + VersionIsZero: true, + IdIsEmpty: true, + Method: method, + Args: expectArgs, + Results: useResults, + } + + t.called = make(chan struct{}, 100) + t.apiCaller = apitesting.NotifyingCheckingAPICaller(c, &t.apiArgs, t.called, err) + t.api = cleaner.NewAPI(t.apiCaller) + + c.Check(t.api, gc.NotNil) + return +} + +// AssertNumReceives checks that the watched channel receives "expected" messages +// within a LongWait, but returns as soon as possible. +func AssertNumReceives(c *gc.C, watched chan struct{}, expected uint32) { + var receives uint32 + + for receives < expected { + select { + case <-watched: + receives++ + case <-time.After(coretesting.LongWait): + c.Errorf("timeout while waiting for a call") + } + } + + time.Sleep(coretesting.ShortWait) + c.Assert(receives, gc.Equals, expected) +} + +func (s *CleanerSuite) TestNewAPI(c *gc.C) { + Init(c, "", nil, nil, nil) +} + +func (s *CleanerSuite) TestWatchCleanups(c *gc.C) { + t := Init(c, "", nil, nil, nil) + t.apiArgs.Facade = "" // Multiple facades are called, so we can't check this. + m, err := t.api.WatchCleanups() + AssertNumReceives(c, t.called, 2) + c.Assert(err, jc.ErrorIsNil) + c.Assert(m, gc.NotNil) +} + +func (s *CleanerSuite) TestCleanup(c *gc.C) { + t := Init(c, "Cleanup", nil, nil, nil) + err := t.api.Cleanup() + AssertNumReceives(c, t.called, 1) + c.Assert(err, jc.ErrorIsNil) +} + +func (s *CleanerSuite) TestWatchCleanupsFailFacadeCall(c *gc.C) { + t := Init(c, "WatchCleanups", nil, nil, errors.New("client error!")) + m, err := t.api.WatchCleanups() + c.Assert(err, gc.ErrorMatches, "client error!") + AssertNumReceives(c, t.called, 1) + c.Assert(m, gc.IsNil) +} + +func (s *CleanerSuite) TestWatchCleanupsFailFacadeResult(c *gc.C) { + e := params.Error{ + Message: "Server Error", + } + p := params.NotifyWatchResult{ + Error: &e, + } + t := Init(c, "WatchCleanups", nil, p, nil) + m, err := t.api.WatchCleanups() + AssertNumReceives(c, t.called, 1) + c.Assert(err, gc.ErrorMatches, e.Message) + c.Assert(m, gc.IsNil) +} === added file 'src/github.com/juju/juju/api/cleaner/package_test.go' --- src/github.com/juju/juju/api/cleaner/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/cleaner/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,14 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package cleaner_test + +import ( + "testing" + + gc "gopkg.in/check.v1" +) + +func TestPackage(t *testing.T) { + gc.TestingT(t) +} === modified file 'src/github.com/juju/juju/api/client.go' --- src/github.com/juju/juju/api/client.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/client.go 2015-10-23 18:29:32 +0000 @@ -13,6 +13,7 @@ "net/http" "net/url" "os" + "path" "strings" "time" @@ -29,7 +30,6 @@ "github.com/juju/juju/constraints" "github.com/juju/juju/instance" "github.com/juju/juju/network" - "github.com/juju/juju/state/multiwatcher" "github.com/juju/juju/tools" "github.com/juju/juju/version" ) @@ -41,135 +41,9 @@ st *State } -// NetworksSpecification holds the enabled and disabled networks for a -// service. -type NetworksSpecification struct { - Enabled []string - Disabled []string -} - -// AgentStatus holds status info about a machine or unit agent. -type AgentStatus struct { - Status params.Status - Info string - Data map[string]interface{} - Since *time.Time - Kind params.HistoryKind - Version string - Life string - Err error -} - -// MachineStatus holds status info about a machine. -type MachineStatus struct { - Agent AgentStatus - - // The following fields mirror fields in AgentStatus (introduced - // in 1.19.x). The old fields below are being kept for - // compatibility with old clients. - // They can be removed once API versioning lands. - AgentState params.Status - AgentStateInfo string - AgentVersion string - Life string - Err error - - DNSName string - InstanceId instance.Id - InstanceState string - Series string - Id string - Containers map[string]MachineStatus - Hardware string - Jobs []multiwatcher.MachineJob - HasVote bool - WantsVote bool -} - -// ServiceStatus holds status info about a service. -type ServiceStatus struct { - Err error - Charm string - Exposed bool - Life string - Relations map[string][]string - Networks NetworksSpecification - CanUpgradeTo string - SubordinateTo []string - Units map[string]UnitStatus - Status AgentStatus -} - -// UnitStatusHistory holds a slice of statuses. -type UnitStatusHistory struct { - Statuses []AgentStatus -} - -// UnitStatus holds status info about a unit. -type UnitStatus struct { - // UnitAgent holds the status for a unit's agent. - UnitAgent AgentStatus - - // Workload holds the status for a unit's workload - Workload AgentStatus - - // Until Juju 2.0, we need to continue to return legacy agent state values - // as top level struct attributes when the "FullStatus" API is called. - AgentState params.Status - AgentStateInfo string - AgentVersion string - Life string - Err error - - Machine string - OpenedPorts []string - PublicAddress string - Charm string - Subordinates map[string]UnitStatus -} - -// RelationStatus holds status info about a relation. -type RelationStatus struct { - Id int - Key string - Interface string - Scope charm.RelationScope - Endpoints []EndpointStatus -} - -// EndpointStatus holds status info about a single endpoint -type EndpointStatus struct { - ServiceName string - Name string - Role charm.RelationRole - Subordinate bool -} - -func (epStatus *EndpointStatus) String() string { - return epStatus.ServiceName + ":" + epStatus.Name -} - -// NetworkStatus holds status info about a network. -type NetworkStatus struct { - Err error - ProviderId network.Id - CIDR string - VLANTag int -} - -// Status holds information about the status of a juju environment. -type Status struct { - EnvironmentName string - AvailableVersion string - Machines map[string]MachineStatus - Services map[string]ServiceStatus - Networks map[string]NetworkStatus - Relations []RelationStatus -} - // Status returns the status of the juju environment. -func (c *Client) Status(patterns []string) (*Status, error) { - var result Status +func (c *Client) Status(patterns []string) (*params.FullStatus, error) { + var result params.FullStatus p := params.StatusParams{Patterns: patterns} if err := c.facade.FacadeCall("FullStatus", p, &result); err != nil { return nil, err @@ -179,8 +53,8 @@ // UnitStatusHistory retrieves the last results of status // for unit -func (c *Client) UnitStatusHistory(kind params.HistoryKind, unitName string, size int) (*UnitStatusHistory, error) { - var results UnitStatusHistory +func (c *Client) UnitStatusHistory(kind params.HistoryKind, unitName string, size int) (*params.UnitStatusHistory, error) { + var results params.UnitStatusHistory args := params.StatusHistory{ Kind: kind, Size: size, @@ -189,27 +63,17 @@ err := c.facade.FacadeCall("UnitStatusHistory", args, &results) if err != nil { if params.IsCodeNotImplemented(err) { - return &UnitStatusHistory{}, errors.NotImplementedf("UnitStatusHistory") + return ¶ms.UnitStatusHistory{}, errors.NotImplementedf("UnitStatusHistory") } - return &UnitStatusHistory{}, errors.Trace(err) + return ¶ms.UnitStatusHistory{}, errors.Trace(err) } return &results, nil } -// LegacyMachineStatus holds just the instance-id of a machine. -type LegacyMachineStatus struct { - InstanceId string // Not type instance.Id just to match original api. -} - -// LegacyStatus holds minimal information on the status of a juju environment. -type LegacyStatus struct { - Machines map[string]LegacyMachineStatus -} - // LegacyStatus is a stub version of Status that 1.16 introduced. Should be // removed along with structs when api versioning makes it safe to do so. -func (c *Client) LegacyStatus() (*LegacyStatus, error) { - var result LegacyStatus +func (c *Client) LegacyStatus() (*params.LegacyStatus, error) { + var result params.LegacyStatus if err := c.facade.FacadeCall("Status", nil, &result); err != nil { return nil, err } @@ -457,6 +321,19 @@ return results.Units, err } +// AddServiceUnitsWithPlacement adds a given number of units to a service using the specified +// placement directives to assign units to machines. +func (c *Client) AddServiceUnitsWithPlacement(service string, numUnits int, placement []*instance.Placement) ([]string, error) { + args := params.AddServiceUnits{ + ServiceName: service, + NumUnits: numUnits, + Placement: placement, + } + results := new(params.AddServiceUnitsResults) + err := c.facade.FacadeCall("AddServiceUnitsWithPlacement", args, results) + return results.Units, err +} + // DestroyServiceUnits decreases the number of units dedicated to a service. func (c *Client) DestroyServiceUnits(unitNames ...string) error { params := params.DestroyServiceUnits{unitNames} @@ -619,7 +496,7 @@ return result.Combine() } -// WatchAll holds the id of the newly-created AllWatcher. +// WatchAll holds the id of the newly-created AllWatcher/AllEnvWatcher. type WatchAll struct { AllWatcherId string } @@ -631,7 +508,7 @@ if err := c.facade.FacadeCall("WatchAll", nil, info); err != nil { return nil, err } - return newAllWatcher(c.st, &info.AllWatcherId), nil + return NewAllWatcher(c.st, &info.AllWatcherId), nil } // GetAnnotations returns annotations that have been set on the given entity. @@ -768,12 +645,17 @@ return nil, errors.Errorf("unknown charm type %T", ch) } - endPoint, err := c.localCharmUploadEndpoint(curl.Series) + endPoint, err := c.apiEndpoint("charms", "series="+curl.Series) if err != nil { return nil, errors.Trace(err) } - req, err := http.NewRequest("POST", endPoint, archive) + // wrap archive in a noopCloser to prevent the underlying transport closing + // the request body. This is neccessary to prevent a data race on the underlying + // *os.File as the http transport _may_ issue Close once the body is sent, or it + // may not if there is an error. + noop := &noopCloser{archive} + req, err := http.NewRequest("POST", endPoint, noop) if err != nil { return nil, errors.Annotate(err, "cannot create upload request") } @@ -815,30 +697,50 @@ return charm.MustParseURL(jsonResponse.CharmURL), nil } -func (c *Client) localCharmUploadEndpoint(series string) (string, error) { - var apiEndpoint string +// noopCloser implements io.ReadCloser, but does not close the underlying io.ReadCloser. +// This is necessary to ensure the ownership of io.ReadCloser implementations that are +// passed to the net/http Transport which may (under some circumstances), call Close on +// the body passed to a request. +type noopCloser struct { + io.ReadCloser +} + +func (n *noopCloser) Close() error { + + // do not propogate the Close method to the underlying ReadCloser. + return nil +} + +func (c *Client) apiEndpoint(destination, query string) (string, error) { + root, err := c.apiRoot() + if err != nil { + return "", errors.Trace(err) + } + + upURL := url.URL{ + Scheme: c.st.serverScheme, + Host: c.st.Addr(), + Path: path.Join(root, destination), + RawQuery: query, + } + return upURL.String(), nil +} + +func (c *Client) apiRoot() (string, error) { + var apiRoot string if _, err := c.st.ServerTag(); err == nil { envTag, err := c.st.EnvironTag() if err != nil { return "", errors.Annotate(err, "cannot get API endpoint address") } - apiEndpoint = fmt.Sprintf("/environment/%s/charms", envTag.Id()) + apiRoot = fmt.Sprintf("/environment/%s/", envTag.Id()) } else { // If the server tag is not set, then the agent version is < 1.23. We // use the old API endpoint for backwards compatibility. - apiEndpoint = "/charms" - } - - // Prepare the upload request. - upURL := url.URL{ - Scheme: c.st.serverScheme, - Host: c.st.Addr(), - Path: apiEndpoint, - RawQuery: fmt.Sprintf("series=%s", series), - } - - return upURL.String(), nil + apiRoot = "/" + } + return apiRoot, nil } // AddCharm adds the given charm URL (which must include revision) to @@ -894,16 +796,17 @@ // UploadTools uploads tools at the specified location to the API server over HTTPS. func (c *Client) UploadTools(r io.Reader, vers version.Binary, additionalSeries ...string) (*tools.Tools, error) { // Prepare the upload request. - url := fmt.Sprintf( - // TODO(waigani) This is going to be a problem in the future where we - // want to upload tools for a particular environment. The upload root - // will need to be something like: /environment//" +func envEndpoint(c *gc.C, apiState api.Connection, destination string) string { + envTag, err := apiState.EnvironTag() + c.Assert(err, jc.ErrorIsNil) + return path.Join("/environment", envTag.Id(), destination) } func (s *clientSuite) TestClientEnvironmentUUID(c *gc.C) { === added file 'src/github.com/juju/juju/api/common/errors.go' --- src/github.com/juju/juju/api/common/errors.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/common/errors.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,12 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package common + +import ( + "github.com/juju/errors" +) + +var ( + ErrPartialResults = errors.New("API call only returned partial results") +) === modified file 'src/github.com/juju/juju/api/deployer/deployer_test.go' --- src/github.com/juju/juju/api/deployer/deployer_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/deployer/deployer_test.go 2015-10-23 18:29:32 +0000 @@ -29,7 +29,7 @@ testing.JujuConnSuite *apitesting.APIAddresserTests - stateAPI *api.State + stateAPI api.Connection // These are raw State objects. Use them for setup and assertions, but // should never be touched by the API calls themselves === modified file 'src/github.com/juju/juju/api/environmentmanager/environmentmanager.go' --- src/github.com/juju/juju/api/environmentmanager/environmentmanager.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/environmentmanager/environmentmanager.go 2015-10-23 18:29:32 +0000 @@ -4,8 +4,6 @@ package environmentmanager import ( - "fmt" - "github.com/juju/errors" "github.com/juju/loggo" "github.com/juju/names" @@ -53,7 +51,7 @@ func (c *Client) CreateEnvironment(owner string, account, config map[string]interface{}) (params.Environment, error) { var result params.Environment if !names.IsValidUser(owner) { - return result, fmt.Errorf("invalid owner name %q", owner) + return result, errors.Errorf("invalid owner name %q", owner) } createArgs := params.EnvironmentCreateArgs{ OwnerTag: names.NewUserTag(owner).String(), @@ -72,15 +70,28 @@ // has access to in the current server. Only that state server owner // can list environments for any user (at this stage). Other users // can only ask about their own environments. -func (c *Client) ListEnvironments(user string) ([]params.Environment, error) { - var result params.EnvironmentList +func (c *Client) ListEnvironments(user string) ([]base.UserEnvironment, error) { + var environments params.UserEnvironmentList if !names.IsValidUser(user) { - return nil, fmt.Errorf("invalid user name %q", user) + return nil, errors.Errorf("invalid user name %q", user) } entity := params.Entity{names.NewUserTag(user).String()} - err := c.facade.FacadeCall("ListEnvironments", entity, &result) + err := c.facade.FacadeCall("ListEnvironments", entity, &environments) if err != nil { return nil, errors.Trace(err) } - return result.Environments, nil + result := make([]base.UserEnvironment, len(environments.UserEnvironments)) + for i, env := range environments.UserEnvironments { + owner, err := names.ParseUserTag(env.OwnerTag) + if err != nil { + return nil, errors.Annotatef(err, "OwnerTag %q at position %d", env.OwnerTag, i) + } + result[i] = base.UserEnvironment{ + Name: env.Name, + UUID: env.UUID, + Owner: owner.Username(), + LastConnection: env.LastConnection, + } + } + return result, nil } === modified file 'src/github.com/juju/juju/api/environmentmanager/environmentmanager_test.go' --- src/github.com/juju/juju/api/environmentmanager/environmentmanager_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/environmentmanager/environmentmanager_test.go 2015-10-23 18:29:32 +0000 @@ -113,5 +113,7 @@ c.Assert(envs, gc.HasLen, 2) envNames := []string{envs[0].Name, envs[1].Name} - c.Assert(envNames, jc.SameContents, []string{"first", "second"}) + c.Assert(envNames, jc.DeepEquals, []string{"first", "second"}) + ownerNames := []string{envs[0].Owner, envs[1].Owner} + c.Assert(ownerNames, jc.DeepEquals, []string{"user@remote", "user@remote"}) } === modified file 'src/github.com/juju/juju/api/export_test.go' --- src/github.com/juju/juju/api/export_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/export_test.go 2015-10-23 18:29:32 +0000 @@ -9,6 +9,7 @@ ) var ( + CertDir = &certDir NewWebsocketDialer = newWebsocketDialer NewWebsocketDialerPtr = &newWebsocketDialer WebsocketDialConfig = &websocketDialConfig === modified file 'src/github.com/juju/juju/api/facadeversions.go' --- src/github.com/juju/juju/api/facadeversions.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/facadeversions.go 2015-10-23 18:29:32 +0000 @@ -12,22 +12,28 @@ // Facades that existed before versioning start at 0. var facadeVersions = map[string]int{ "Action": 0, + "Addresser": 1, "Agent": 1, "AllWatcher": 0, + "AllEnvWatcher": 1, "Annotations": 1, "Backups": 0, "Block": 1, "Charms": 1, "CharmRevisionUpdater": 0, "Client": 0, + "Cleaner": 1, "Deployer": 0, "DiskManager": 1, + "EntityWatcher": 1, "Environment": 0, "EnvironmentManager": 1, "FilesystemAttachmentsWatcher": 1, "Firewaller": 1, "HighAvailability": 1, "ImageManager": 1, + "ImageMetadata": 1, + "InstancePoller": 1, "KeyManager": 0, "KeyUpdater": 0, "LeadershipService": 1, @@ -41,11 +47,15 @@ "Provisioner": 1, "Reboot": 1, "RelationUnitsWatcher": 0, + "Resumer": 1, "Rsyslog": 0, "Service": 1, "Storage": 1, + "Spaces": 1, + "Subnets": 1, "StorageProvisioner": 1, "StringsWatcher": 0, + "SystemManager": 1, "Upgrader": 0, "Uniter": 2, "UserManager": 0, === modified file 'src/github.com/juju/juju/api/firewaller/firewaller_test.go' --- src/github.com/juju/juju/api/firewaller/firewaller_test.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/api/firewaller/firewaller_test.go 2015-10-23 18:29:32 +0000 @@ -20,7 +20,7 @@ type firewallerSuite struct { testing.JujuConnSuite - st *api.State + st api.Connection machines []*state.Machine service *state.Service charm *state.Charm === modified file 'src/github.com/juju/juju/api/http.go' --- src/github.com/juju/juju/api/http.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/http.go 2015-10-23 18:29:32 +0000 @@ -16,7 +16,7 @@ apiserverhttp "github.com/juju/juju/apiserver/http" ) -var newHTTPClient = func(s *State) apihttp.HTTPClient { +var newHTTPClient = func(s Connection) apihttp.HTTPClient { return s.NewHTTPClient() } === modified file 'src/github.com/juju/juju/api/http_test.go' --- src/github.com/juju/juju/api/http_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/http_test.go 2015-10-23 18:29:32 +0000 @@ -38,7 +38,7 @@ // This determines the client used in SendHTTPRequest(). s.PatchValue(api.NewHTTPClient, - func(*api.State) apihttp.HTTPClient { + func(api.Connection) apihttp.HTTPClient { return s.Fake }, ) === added directory 'src/github.com/juju/juju/api/imagemetadata' === added file 'src/github.com/juju/juju/api/imagemetadata/client.go' --- src/github.com/juju/juju/api/imagemetadata/client.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/imagemetadata/client.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,59 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package imagemetadata + +import ( + "github.com/juju/errors" + "github.com/juju/loggo" + + "github.com/juju/juju/api/base" + "github.com/juju/juju/apiserver/params" +) + +var logger = loggo.GetLogger("juju.api.imagemetadata") + +// Client provides access to cloud image metadata. +// It is used to find, save and update image metadata. +type Client struct { + base.ClientFacade + facade base.FacadeCaller +} + +// NewClient returns a new metadata client. +func NewClient(st base.APICallCloser) *Client { + frontend, backend := base.NewClientFacade(st, "ImageMetadata") + return &Client{ClientFacade: frontend, facade: backend} +} + +// List returns image metadata that matches filter. +// Empty filter will return all image metadata. +func (c *Client) List( + stream, region string, + series, arches []string, + virtualType, rootStorageType string, +) ([]params.CloudImageMetadata, error) { + in := params.ImageMetadataFilter{ + Region: region, + Series: series, + Arches: arches, + Stream: stream, + VirtualType: virtualType, + RootStorageType: rootStorageType, + } + out := params.ListCloudImageMetadataResult{} + err := c.facade.FacadeCall("List", in, &out) + return out.Result, err +} + +// Save saves specified image metadata. +// Supports bulk saves for scenarios like cloud image metadata caching at bootstrap. +func (c *Client) Save(metadata []params.CloudImageMetadata) ([]params.ErrorResult, error) { + in := params.MetadataSaveParams{Metadata: metadata} + out := params.ErrorResults{} + err := c.facade.FacadeCall("Save", in, &out) + if err != nil { + return nil, errors.Trace(err) + } + return out.Results, nil +} === added file 'src/github.com/juju/juju/api/imagemetadata/client_test.go' --- src/github.com/juju/juju/api/imagemetadata/client_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/imagemetadata/client_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,178 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package imagemetadata_test + +import ( + "github.com/juju/errors" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/api/base/testing" + "github.com/juju/juju/api/imagemetadata" + "github.com/juju/juju/apiserver/params" + coretesting "github.com/juju/juju/testing" +) + +type imagemetadataSuite struct { + coretesting.BaseSuite +} + +var _ = gc.Suite(&imagemetadataSuite{}) + +func (s *imagemetadataSuite) TestList(c *gc.C) { + // setup data for test + imageId := "imageid" + stream := "stream" + region := "region" + series := "series" + arch := "arch" + virtualType := "virtual-type" + rootStorageType := "root-storage-type" + rootStorageSize := uint64(1024) + source := "source" + + called := false + apiCaller := testing.APICallerFunc( + func(objType string, + version int, + id, request string, + a, result interface{}, + ) error { + called = true + c.Check(objType, gc.Equals, "ImageMetadata") + c.Check(id, gc.Equals, "") + c.Check(request, gc.Equals, "List") + + args, ok := a.(params.ImageMetadataFilter) + c.Assert(ok, jc.IsTrue) + + if results, k := result.(*params.ListCloudImageMetadataResult); k { + instances := []params.CloudImageMetadata{ + params.CloudImageMetadata{ + ImageId: imageId, + Stream: args.Stream, + Region: args.Region, + Series: args.Series[0], + Arch: args.Arches[0], + VirtualType: args.VirtualType, + RootStorageType: args.RootStorageType, + RootStorageSize: &rootStorageSize, + Source: source, + }, + } + results.Result = instances + } + + return nil + }) + client := imagemetadata.NewClient(apiCaller) + found, err := client.List( + stream, region, + []string{series}, []string{arch}, + virtualType, rootStorageType, + ) + c.Check(err, jc.ErrorIsNil) + + c.Assert(called, jc.IsTrue) + expected := []params.CloudImageMetadata{ + params.CloudImageMetadata{ + ImageId: imageId, + Stream: stream, + Region: region, + Series: series, + Arch: arch, + VirtualType: virtualType, + RootStorageType: rootStorageType, + RootStorageSize: &rootStorageSize, + Source: source, + }, + } + c.Assert(found, jc.DeepEquals, expected) +} + +func (s *imagemetadataSuite) TestListFacadeCallError(c *gc.C) { + msg := "facade failure" + called := false + apiCaller := testing.APICallerFunc( + func(objType string, + version int, + id, request string, + a, result interface{}, + ) error { + called = true + c.Check(objType, gc.Equals, "ImageMetadata") + c.Check(id, gc.Equals, "") + c.Check(request, gc.Equals, "List") + + return errors.New(msg) + }) + client := imagemetadata.NewClient(apiCaller) + found, err := client.List("", "", nil, nil, "", "") + c.Assert(errors.Cause(err), gc.ErrorMatches, msg) + c.Assert(found, gc.HasLen, 0) + c.Assert(called, jc.IsTrue) +} + +func (s *imagemetadataSuite) TestSave(c *gc.C) { + m := params.CloudImageMetadata{} + called := false + + msg := "save failure" + expected := []params.ErrorResult{ + params.ErrorResult{}, + params.ErrorResult{¶ms.Error{Message: msg}}, + } + + apiCaller := testing.APICallerFunc( + func(objType string, + version int, + id, request string, + a, result interface{}, + ) error { + called = true + c.Check(objType, gc.Equals, "ImageMetadata") + c.Check(id, gc.Equals, "") + c.Check(request, gc.Equals, "Save") + + args, ok := a.(params.MetadataSaveParams) + c.Assert(ok, jc.IsTrue) + c.Assert(args.Metadata, gc.HasLen, 2) + c.Assert(args.Metadata, gc.DeepEquals, []params.CloudImageMetadata{m, m}) + + if results, k := result.(*params.ErrorResults); k { + results.Results = expected + } + + return nil + }) + + client := imagemetadata.NewClient(apiCaller) + errs, err := client.Save([]params.CloudImageMetadata{m, m}) + c.Check(err, jc.ErrorIsNil) + c.Assert(called, jc.IsTrue) + c.Assert(errs, jc.DeepEquals, expected) +} + +func (s *imagemetadataSuite) TestSaveFacadeCallError(c *gc.C) { + m := []params.CloudImageMetadata{{}} + called := false + msg := "facade failure" + apiCaller := testing.APICallerFunc( + func(objType string, + version int, + id, request string, + a, result interface{}, + ) error { + called = true + c.Check(objType, gc.Equals, "ImageMetadata") + c.Check(id, gc.Equals, "") + c.Check(request, gc.Equals, "Save") + return errors.New(msg) + }) + client := imagemetadata.NewClient(apiCaller) + found, err := client.Save(m) + c.Assert(errors.Cause(err), gc.ErrorMatches, msg) + c.Assert(found, gc.HasLen, 0) + c.Assert(called, jc.IsTrue) +} === added file 'src/github.com/juju/juju/api/imagemetadata/package_test.go' --- src/github.com/juju/juju/api/imagemetadata/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/imagemetadata/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,14 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package imagemetadata_test + +import ( + "testing" + + gc "gopkg.in/check.v1" +) + +func TestPackage(t *testing.T) { + gc.TestingT(t) +} === added directory 'src/github.com/juju/juju/api/instancepoller' === added file 'src/github.com/juju/juju/api/instancepoller/export_test.go' --- src/github.com/juju/juju/api/instancepoller/export_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/instancepoller/export_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,18 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package instancepoller + +import ( + "github.com/juju/names" + + "github.com/juju/juju/api/base" + "github.com/juju/juju/apiserver/params" +) + +func NewMachine(caller base.APICaller, tag names.MachineTag, life params.Life) *Machine { + facade := base.NewFacadeCaller(caller, instancePollerFacade) + return &Machine{facade, tag, life} +} + +var NewStringsWatcher = &newStringsWatcher === added file 'src/github.com/juju/juju/api/instancepoller/instancepoller.go' --- src/github.com/juju/juju/api/instancepoller/instancepoller.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/instancepoller/instancepoller.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,61 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package instancepoller + +import ( + "github.com/juju/errors" + "github.com/juju/names" + + "github.com/juju/juju/api/base" + "github.com/juju/juju/api/common" + "github.com/juju/juju/api/watcher" + "github.com/juju/juju/apiserver/params" +) + +const instancePollerFacade = "InstancePoller" + +// API provides access to the InstancePoller API facade. +type API struct { + *common.EnvironWatcher + + facade base.FacadeCaller +} + +// NewAPI creates a new client-side InstancePoller facade. +func NewAPI(caller base.APICaller) *API { + if caller == nil { + panic("caller is nil") + } + facadeCaller := base.NewFacadeCaller(caller, instancePollerFacade) + return &API{ + EnvironWatcher: common.NewEnvironWatcher(facadeCaller), + facade: facadeCaller, + } +} + +// Machine provides access to methods of a state.Machine through the +// facade. +func (api *API) Machine(tag names.MachineTag) (*Machine, error) { + life, err := common.Life(api.facade, tag) + if err != nil { + return nil, errors.Trace(err) + } + return &Machine{api.facade, tag, life}, nil +} + +var newStringsWatcher = watcher.NewStringsWatcher + +// WatchEnvironMachines return a StringsWatcher reporting waiting for the +// environment configuration to change. +func (api *API) WatchEnvironMachines() (watcher.StringsWatcher, error) { + var result params.StringsWatchResult + err := api.facade.FacadeCall("WatchEnvironMachines", nil, &result) + if err != nil { + return nil, err + } + if result.Error != nil { + return nil, result.Error + } + return newStringsWatcher(api.facade.RawAPICaller(), result), nil +} === added file 'src/github.com/juju/juju/api/instancepoller/instancepoller_test.go' --- src/github.com/juju/juju/api/instancepoller/instancepoller_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/instancepoller/instancepoller_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,186 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package instancepoller_test + +import ( + "errors" + + "github.com/juju/names" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/api/base" + apitesting "github.com/juju/juju/api/base/testing" + "github.com/juju/juju/api/instancepoller" + "github.com/juju/juju/api/watcher" + "github.com/juju/juju/apiserver/params" + apiservertesting "github.com/juju/juju/apiserver/testing" + coretesting "github.com/juju/juju/testing" +) + +type InstancePollerSuite struct { + coretesting.BaseSuite +} + +var _ = gc.Suite(&InstancePollerSuite{}) + +func (s *InstancePollerSuite) TestNewAPI(c *gc.C) { + var called int + apiCaller := clientErrorAPICaller(c, "Life", nil, &called) + api := instancepoller.NewAPI(apiCaller) + c.Check(api, gc.NotNil) + c.Check(called, gc.Equals, 0) + + // Nothing happens until we actually call something else. + m, err := api.Machine(names.MachineTag{}) + c.Assert(err, gc.ErrorMatches, "client error!") + c.Assert(m, gc.IsNil) + c.Assert(called, gc.Equals, 1) +} + +func (s *InstancePollerSuite) TestNewAPIWithNilCaller(c *gc.C) { + panicFunc := func() { instancepoller.NewAPI(nil) } + c.Assert(panicFunc, gc.PanicMatches, "caller is nil") +} + +func (s *InstancePollerSuite) TestMachineCallsLife(c *gc.C) { + // We have tested separately the Life method, here we just check + // it's called internally. + var called int + expectedResults := params.LifeResults{ + Results: []params.LifeResult{{Life: "working"}}, + } + apiCaller := successAPICaller(c, "Life", entitiesArgs, expectedResults, &called) + api := instancepoller.NewAPI(apiCaller) + m, err := api.Machine(names.NewMachineTag("42")) + c.Assert(err, jc.ErrorIsNil) + c.Assert(called, gc.Equals, 1) + c.Assert(m.Life(), gc.Equals, params.Life("working")) + c.Assert(m.Id(), gc.Equals, "42") +} + +func (s *InstancePollerSuite) TestWatchEnvironMachinesSuccess(c *gc.C) { + // We're not testing the watcher logic here as it's already tested elsewhere. + var numFacadeCalls int + var numWatcherCalls int + expectResult := params.StringsWatchResult{ + StringsWatcherId: "42", + Changes: []string{"foo", "bar"}, + } + watcherFunc := func(caller base.APICaller, result params.StringsWatchResult) watcher.StringsWatcher { + numWatcherCalls++ + c.Check(caller, gc.NotNil) + c.Check(result, jc.DeepEquals, expectResult) + return nil + } + s.PatchValue(instancepoller.NewStringsWatcher, watcherFunc) + + apiCaller := successAPICaller(c, "WatchEnvironMachines", nil, expectResult, &numFacadeCalls) + + api := instancepoller.NewAPI(apiCaller) + w, err := api.WatchEnvironMachines() + c.Assert(err, jc.ErrorIsNil) + c.Assert(numFacadeCalls, gc.Equals, 1) + c.Assert(numWatcherCalls, gc.Equals, 1) + c.Assert(w, gc.IsNil) +} + +func (s *InstancePollerSuite) TestWatchEnvironMachinesClientError(c *gc.C) { + var called int + apiCaller := clientErrorAPICaller(c, "WatchEnvironMachines", nil, &called) + api := instancepoller.NewAPI(apiCaller) + w, err := api.WatchEnvironMachines() + c.Assert(err, gc.ErrorMatches, "client error!") + c.Assert(w, gc.IsNil) + c.Assert(called, gc.Equals, 1) +} + +func (s *InstancePollerSuite) TestWatchEnvironMachinesServerError(c *gc.C) { + var called int + expectedResults := params.StringsWatchResult{ + Error: apiservertesting.ServerError("server boom!"), + } + apiCaller := successAPICaller(c, "WatchEnvironMachines", nil, expectedResults, &called) + + api := instancepoller.NewAPI(apiCaller) + w, err := api.WatchEnvironMachines() + c.Assert(err, gc.ErrorMatches, "server boom!") + c.Assert(called, gc.Equals, 1) + c.Assert(w, gc.IsNil) +} + +func (s *InstancePollerSuite) TestWatchForEnvironConfigChangesClientError(c *gc.C) { + // We're not testing the success case as we're not patching the + // NewNotifyWatcher call the embedded EnvironWatcher is calling. + var called int + apiCaller := clientErrorAPICaller(c, "WatchForEnvironConfigChanges", nil, &called) + + api := instancepoller.NewAPI(apiCaller) + w, err := api.WatchForEnvironConfigChanges() + c.Assert(err, gc.ErrorMatches, "client error!") + c.Assert(called, gc.Equals, 1) + c.Assert(w, gc.IsNil) +} + +func (s *InstancePollerSuite) TestEnvironConfigSuccess(c *gc.C) { + var called int + expectedConfig := coretesting.EnvironConfig(c) + expectedResults := params.EnvironConfigResult{ + Config: params.EnvironConfig(expectedConfig.AllAttrs()), + } + apiCaller := successAPICaller(c, "EnvironConfig", nil, expectedResults, &called) + + api := instancepoller.NewAPI(apiCaller) + cfg, err := api.EnvironConfig() + c.Assert(err, jc.ErrorIsNil) + c.Assert(called, gc.Equals, 1) + c.Assert(cfg, jc.DeepEquals, expectedConfig) +} + +func (s *InstancePollerSuite) TestEnvironConfigClientError(c *gc.C) { + var called int + apiCaller := clientErrorAPICaller(c, "EnvironConfig", nil, &called) + api := instancepoller.NewAPI(apiCaller) + cfg, err := api.EnvironConfig() + c.Assert(err, gc.ErrorMatches, "client error!") + c.Assert(cfg, gc.IsNil) + c.Assert(called, gc.Equals, 1) +} + +func (s *InstancePollerSuite) TestEnvironConfigServerError(c *gc.C) { + var called int + expectResults := params.EnvironConfigResult{ + Config: params.EnvironConfig{"type": "foo"}, + } + apiCaller := successAPICaller(c, "EnvironConfig", nil, expectResults, &called) + + api := instancepoller.NewAPI(apiCaller) + cfg, err := api.EnvironConfig() + c.Assert(err, gc.NotNil) // the actual error doesn't matter + c.Assert(called, gc.Equals, 1) + c.Assert(cfg, gc.IsNil) +} + +func clientErrorAPICaller(c *gc.C, method string, expectArgs interface{}, numCalls *int) base.APICaller { + args := &apitesting.CheckArgs{ + Facade: "InstancePoller", + VersionIsZero: true, + IdIsEmpty: true, + Method: method, + Args: expectArgs, + } + return apitesting.CheckingAPICaller(c, args, numCalls, errors.New("client error!")) +} + +func successAPICaller(c *gc.C, method string, expectArgs, useResults interface{}, numCalls *int) base.APICaller { + args := &apitesting.CheckArgs{ + Facade: "InstancePoller", + VersionIsZero: true, + IdIsEmpty: true, + Method: method, + Args: expectArgs, + Results: useResults, + } + return apitesting.CheckingAPICaller(c, args, numCalls, nil) +} === added file 'src/github.com/juju/juju/api/instancepoller/machine.go' --- src/github.com/juju/juju/api/instancepoller/machine.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/instancepoller/machine.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,189 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package instancepoller + +import ( + "github.com/juju/errors" + "github.com/juju/names" + + "github.com/juju/juju/api/base" + "github.com/juju/juju/api/common" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/instance" + "github.com/juju/juju/network" +) + +// Machine represents a juju machine as seen by an instancepoller +// worker. +type Machine struct { + facade base.FacadeCaller + + tag names.MachineTag + life params.Life +} + +// Id returns the machine's id. +func (m *Machine) Id() string { + return m.tag.Id() +} + +// Tag returns the machine's tag. +func (m *Machine) Tag() names.MachineTag { + return m.tag +} + +// String returns the machine as a string. +func (m *Machine) String() string { + return m.Id() +} + +// Life returns the machine's lifecycle value. +func (m *Machine) Life() params.Life { + return m.life +} + +// Refresh updates the cached local copy of the machine's data. +func (m *Machine) Refresh() error { + life, err := common.Life(m.facade, m.tag) + if err != nil { + return errors.Trace(err) + } + m.life = life + return nil +} + +// Status returns the machine status. +func (m *Machine) Status() (params.StatusResult, error) { + var results params.StatusResults + args := params.Entities{Entities: []params.Entity{ + {Tag: m.tag.String()}, + }} + err := m.facade.FacadeCall("Status", args, &results) + if err != nil { + return params.StatusResult{}, errors.Trace(err) + } + if len(results.Results) != 1 { + err := errors.Errorf("expected 1 result, got %d", len(results.Results)) + return params.StatusResult{}, err + } + result := results.Results[0] + if result.Error != nil { + return params.StatusResult{}, result.Error + } + return result, nil +} + +// IsManual returns whether the machine is manually provisioned. +func (m *Machine) IsManual() (bool, error) { + var results params.BoolResults + args := params.Entities{Entities: []params.Entity{ + {Tag: m.tag.String()}, + }} + err := m.facade.FacadeCall("AreManuallyProvisioned", args, &results) + if err != nil { + return false, errors.Trace(err) + } + if len(results.Results) != 1 { + err := errors.Errorf("expected 1 result, got %d", len(results.Results)) + return false, err + } + result := results.Results[0] + if result.Error != nil { + return false, result.Error + } + return result.Result, nil +} + +// InstanceId returns the machine's instance id. +func (m *Machine) InstanceId() (instance.Id, error) { + var results params.StringResults + args := params.Entities{Entities: []params.Entity{ + {Tag: m.tag.String()}, + }} + err := m.facade.FacadeCall("InstanceId", args, &results) + if err != nil { + return "", errors.Trace(err) + } + if len(results.Results) != 1 { + err := errors.Errorf("expected 1 result, got %d", len(results.Results)) + return "", err + } + result := results.Results[0] + if result.Error != nil { + return "", result.Error + } + return instance.Id(result.Result), nil +} + +// InstanceStatus returns the machine's instance status. +func (m *Machine) InstanceStatus() (string, error) { + var results params.StringResults + args := params.Entities{Entities: []params.Entity{ + {Tag: m.tag.String()}, + }} + err := m.facade.FacadeCall("InstanceStatus", args, &results) + if err != nil { + return "", errors.Trace(err) + } + if len(results.Results) != 1 { + err := errors.Errorf("expected 1 result, got %d", len(results.Results)) + return "", err + } + result := results.Results[0] + if result.Error != nil { + return "", result.Error + } + return result.Result, nil +} + +// SetInstanceStatus sets the instance status of the machine. +func (m *Machine) SetInstanceStatus(status string) error { + var result params.ErrorResults + args := params.SetInstancesStatus{Entities: []params.InstanceStatus{ + {Tag: m.tag.String(), Status: status}, + }} + err := m.facade.FacadeCall("SetInstanceStatus", args, &result) + if err != nil { + return err + } + return result.OneError() +} + +// ProviderAddresses returns all addresses of the machine known to the +// cloud provider. +func (m *Machine) ProviderAddresses() ([]network.Address, error) { + var results params.MachineAddressesResults + args := params.Entities{Entities: []params.Entity{ + {Tag: m.tag.String()}, + }} + err := m.facade.FacadeCall("ProviderAddresses", args, &results) + if err != nil { + return nil, errors.Trace(err) + } + if len(results.Results) != 1 { + err := errors.Errorf("expected 1 result, got %d", len(results.Results)) + return nil, err + } + result := results.Results[0] + if result.Error != nil { + return nil, result.Error + } + return params.NetworkAddresses(result.Addresses), nil +} + +// SetProviderAddresses sets the cached provider addresses for the +// machine. +func (m *Machine) SetProviderAddresses(addrs ...network.Address) error { + var result params.ErrorResults + args := params.SetMachinesAddresses{ + MachineAddresses: []params.MachineAddresses{{ + Tag: m.tag.String(), + Addresses: params.FromNetworkAddresses(addrs), + }}} + err := m.facade.FacadeCall("SetProviderAddresses", args, &result) + if err != nil { + return err + } + return result.OneError() +} === added file 'src/github.com/juju/juju/api/instancepoller/machine_test.go' --- src/github.com/juju/juju/api/instancepoller/machine_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/instancepoller/machine_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,334 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package instancepoller_test + +import ( + "reflect" + "time" + + "github.com/juju/names" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + apitesting "github.com/juju/juju/api/base/testing" + "github.com/juju/juju/api/instancepoller" + "github.com/juju/juju/apiserver/params" + apiservertesting "github.com/juju/juju/apiserver/testing" + "github.com/juju/juju/instance" + "github.com/juju/juju/network" + coretesting "github.com/juju/juju/testing" +) + +type MachineSuite struct { + coretesting.BaseSuite + + tag names.MachineTag +} + +var _ = gc.Suite(&MachineSuite{}) + +func (s *MachineSuite) SetUpTest(c *gc.C) { + s.BaseSuite.SetUpTest(c) + s.tag = names.NewMachineTag("42") +} + +func (s *MachineSuite) TestNonFacadeMethods(c *gc.C) { + nopCaller := apitesting.APICallerFunc( + func(_ string, _ int, _, _ string, _, _ interface{}) error { + c.Fatalf("facade call was not expected") + return nil + }, + ) + machine := instancepoller.NewMachine(nopCaller, s.tag, params.Dying) + + c.Assert(machine.Id(), gc.Equals, "42") + c.Assert(machine.Tag(), jc.DeepEquals, s.tag) + c.Assert(machine.String(), gc.Equals, "42") + c.Assert(machine.Life(), gc.Equals, params.Dying) +} + +// methodWrapper wraps a Machine method call and returns the error, +// ignoring the result (if any). +type methodWrapper func(*instancepoller.Machine) error + +// machineErrorTests contains all the necessary information to test +// how each Machine method handles client- and server-side API errors, +// as well as the case when the server-side API returns more results +// than expected. +var machineErrorTests = []struct { + method string // only for logging + wrapper methodWrapper + resultsRef interface{} // an instance of the server-side method's result type +}{{ + method: "Refresh", + wrapper: (*instancepoller.Machine).Refresh, + resultsRef: params.LifeResults{}, +}, { + method: "IsManual", + wrapper: func(m *instancepoller.Machine) error { + _, err := m.IsManual() + return err + }, + resultsRef: params.BoolResults{}, +}, { + method: "InstanceId", + wrapper: func(m *instancepoller.Machine) error { + _, err := m.InstanceId() + return err + }, + resultsRef: params.StringResults{}, +}, { + method: "Status", + wrapper: func(m *instancepoller.Machine) error { + _, err := m.Status() + return err + }, + resultsRef: params.StatusResults{}, +}, { + method: "InstanceStatus", + wrapper: func(m *instancepoller.Machine) error { + _, err := m.InstanceStatus() + return err + }, + resultsRef: params.StringResults{}, +}, { + method: "SetInstanceStatus", + wrapper: func(m *instancepoller.Machine) error { + return m.SetInstanceStatus("") + }, + resultsRef: params.ErrorResults{}, +}, { + method: "ProviderAddresses", + wrapper: func(m *instancepoller.Machine) error { + _, err := m.ProviderAddresses() + return err + }, + resultsRef: params.MachineAddressesResults{}, +}, { + method: "SetProviderAddresses", + wrapper: func(m *instancepoller.Machine) error { + return m.SetProviderAddresses() + }, + resultsRef: params.ErrorResults{}, +}} + +func (s *MachineSuite) TestClientError(c *gc.C) { + for i, test := range machineErrorTests { + c.Logf("test #%d: %s", i, test.method) + s.CheckClientError(c, test.wrapper) + } +} + +func (s *MachineSuite) TestServerError(c *gc.C) { + err := apiservertesting.ServerError("server error!") + expected := err.Error() + for i, test := range machineErrorTests { + c.Logf("test #%d: %s", i, test.method) + results := MakeResultsWithErrors(test.resultsRef, err, 1) + s.CheckServerError(c, test.wrapper, expected, results) + } +} + +func (s *MachineSuite) TestTooManyResultsServerError(c *gc.C) { + err := apiservertesting.ServerError("some error") + expected := "expected 1 result, got 2" + for i, test := range machineErrorTests { + c.Logf("test #%d: %s", i, test.method) + results := MakeResultsWithErrors(test.resultsRef, err, 2) + s.CheckServerError(c, test.wrapper, expected, results) + } +} + +func (s *MachineSuite) TestRefreshSuccess(c *gc.C) { + var called int + results := params.LifeResults{ + Results: []params.LifeResult{{Life: params.Dying}}, + } + apiCaller := successAPICaller(c, "Life", entitiesArgs, results, &called) + machine := instancepoller.NewMachine(apiCaller, s.tag, params.Alive) + c.Check(machine.Refresh(), jc.ErrorIsNil) + c.Check(machine.Life(), gc.Equals, params.Dying) + c.Check(called, gc.Equals, 1) +} + +func (s *MachineSuite) TestStatusSuccess(c *gc.C) { + var called int + now := time.Now() + expectStatus := params.StatusResult{ + Status: "foo", + Info: "bar", + Data: map[string]interface{}{ + "int": 42, + "bool": true, + "float": 3.14, + "slice": []string{"a", "b"}, + "map": map[int]string{5: "five"}, + "string": "argh", + }, + Since: &now, + } + results := params.StatusResults{Results: []params.StatusResult{expectStatus}} + apiCaller := successAPICaller(c, "Status", entitiesArgs, results, &called) + machine := instancepoller.NewMachine(apiCaller, s.tag, params.Alive) + status, err := machine.Status() + c.Check(err, jc.ErrorIsNil) + c.Check(status, jc.DeepEquals, expectStatus) + c.Check(called, gc.Equals, 1) +} + +func (s *MachineSuite) TestIsManualSuccess(c *gc.C) { + var called int + results := params.BoolResults{ + Results: []params.BoolResult{{Result: true}}, + } + apiCaller := successAPICaller(c, "AreManuallyProvisioned", entitiesArgs, results, &called) + machine := instancepoller.NewMachine(apiCaller, s.tag, params.Alive) + isManual, err := machine.IsManual() + c.Check(err, jc.ErrorIsNil) + c.Check(isManual, jc.IsTrue) + c.Check(called, gc.Equals, 1) +} + +func (s *MachineSuite) TestInstanceIdSuccess(c *gc.C) { + var called int + results := params.StringResults{ + Results: []params.StringResult{{Result: "i-foo"}}, + } + apiCaller := successAPICaller(c, "InstanceId", entitiesArgs, results, &called) + machine := instancepoller.NewMachine(apiCaller, s.tag, params.Alive) + instId, err := machine.InstanceId() + c.Check(err, jc.ErrorIsNil) + c.Check(instId, gc.Equals, instance.Id("i-foo")) + c.Check(called, gc.Equals, 1) +} + +func (s *MachineSuite) TestInstanceStatusSuccess(c *gc.C) { + var called int + results := params.StringResults{ + Results: []params.StringResult{{Result: "A-OK"}}, + } + apiCaller := successAPICaller(c, "InstanceStatus", entitiesArgs, results, &called) + machine := instancepoller.NewMachine(apiCaller, s.tag, params.Alive) + status, err := machine.InstanceStatus() + c.Check(err, jc.ErrorIsNil) + c.Check(status, gc.Equals, "A-OK") + c.Check(called, gc.Equals, 1) +} + +func (s *MachineSuite) TestSetInstanceStatusSuccess(c *gc.C) { + var called int + expectArgs := params.SetInstancesStatus{ + Entities: []params.InstanceStatus{{ + Tag: "machine-42", + Status: "RUNNING", + }}} + results := params.ErrorResults{ + Results: []params.ErrorResult{{Error: nil}}, + } + apiCaller := successAPICaller(c, "SetInstanceStatus", expectArgs, results, &called) + machine := instancepoller.NewMachine(apiCaller, s.tag, params.Alive) + err := machine.SetInstanceStatus("RUNNING") + c.Check(err, jc.ErrorIsNil) + c.Check(called, gc.Equals, 1) +} + +func (s *MachineSuite) TestProviderAddressesSuccess(c *gc.C) { + var called int + addresses := network.NewAddresses("2001:db8::1", "0.1.2.3") + results := params.MachineAddressesResults{ + Results: []params.MachineAddressesResult{{ + Addresses: params.FromNetworkAddresses(addresses), + }}} + apiCaller := successAPICaller(c, "ProviderAddresses", entitiesArgs, results, &called) + machine := instancepoller.NewMachine(apiCaller, s.tag, params.Alive) + addrs, err := machine.ProviderAddresses() + c.Check(err, jc.ErrorIsNil) + c.Check(addrs, jc.DeepEquals, addresses) + c.Check(called, gc.Equals, 1) +} + +func (s *MachineSuite) TestSetProviderAddressesSuccess(c *gc.C) { + var called int + addresses := network.NewAddresses("2001:db8::1", "0.1.2.3") + expectArgs := params.SetMachinesAddresses{ + MachineAddresses: []params.MachineAddresses{{ + Tag: "machine-42", + Addresses: params.FromNetworkAddresses(addresses), + }}} + results := params.ErrorResults{ + Results: []params.ErrorResult{{Error: nil}}, + } + apiCaller := successAPICaller(c, "SetProviderAddresses", expectArgs, results, &called) + machine := instancepoller.NewMachine(apiCaller, s.tag, params.Alive) + err := machine.SetProviderAddresses(addresses...) + c.Check(err, jc.ErrorIsNil) + c.Check(called, gc.Equals, 1) +} + +func (s *MachineSuite) CheckClientError(c *gc.C, wf methodWrapper) { + var called int + apiCaller := clientErrorAPICaller(c, "", nil, &called) + machine := instancepoller.NewMachine(apiCaller, s.tag, params.Alive) + c.Check(wf(machine), gc.ErrorMatches, "client error!") + c.Check(called, gc.Equals, 1) +} + +func (s *MachineSuite) CheckServerError(c *gc.C, wf methodWrapper, expectErr string, serverResults interface{}) { + var called int + apiCaller := successAPICaller(c, "", nil, serverResults, &called) + machine := instancepoller.NewMachine(apiCaller, s.tag, params.Alive) + c.Check(wf(machine), gc.ErrorMatches, expectErr) + c.Check(called, gc.Equals, 1) +} + +var entitiesArgs = params.Entities{ + Entities: []params.Entity{{Tag: "machine-42"}}, +} + +// MakeResultsWithErrors constructs a new instance of the results type +// (from apiserver/params), matching the given resultsRef, finds its +// first field (expected to be a slice, usually "Results") and adds +// howMany elements to it, setting the Error field of each element to +// err. +// +// This helper makes a few assumptions: +// - resultsRef's type is a struct and has a single field (commonly - "Results") +// - that field is a slice of structs, which have an Error field +// - the Error field is of type *params.Error +// +// Example: +// err := apiservertesting.ServerError("foo") +// r := MakeResultsWithErrors(params.LifeResults{}, err, 2) +// is equvalent to: +// r := params.LifeResults{Results: []params.LifeResult{{Error: err}, {Error: err}}} +// +func MakeResultsWithErrors(resultsRef interface{}, err *params.Error, howMany int) interface{} { + // Make a new instance of the same type as resultsRef. + resultsType := reflect.TypeOf(resultsRef) + newResults := reflect.New(resultsType).Elem() + + // Make a new empty slice for the results. + sliceField := newResults.Field(0) + newSlice := reflect.New(sliceField.Type()).Elem() + + // Make a new result of the slice's element type and set it to err. + newResult := reflect.New(newSlice.Type().Elem()).Elem() + newResult.FieldByName("Error").Set(reflect.ValueOf(err)) + + // Append howMany copies of newResult to the slice. + for howMany > 0 { + sliceField.Set(reflect.Append(sliceField, newResult)) + howMany-- + } + + return newResults.Interface() +} + +// TODO(dimitern): Move this and MakeResultsWithErrors in params/testing ? +func (MachineSuite) TestMakeResultsWithErrors(c *gc.C) { + err := apiservertesting.ServerError("foo") + r1 := MakeResultsWithErrors(params.LifeResults{}, err, 2) + r2 := params.LifeResults{Results: []params.LifeResult{{Error: err}, {Error: err}}} + c.Assert(r1, jc.DeepEquals, r2) +} === added file 'src/github.com/juju/juju/api/instancepoller/package_test.go' --- src/github.com/juju/juju/api/instancepoller/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/instancepoller/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,14 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package instancepoller_test + +import ( + "testing" + + gc "gopkg.in/check.v1" +) + +func TestPackage(t *testing.T) { + gc.TestingT(t) +} === added file 'src/github.com/juju/juju/api/interface.go' --- src/github.com/juju/juju/api/interface.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/interface.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,174 @@ +// Copyright 2012-2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package api + +import ( + "io" + "net/http" + "time" + + "github.com/juju/names" + + "github.com/juju/juju/api/addresser" + "github.com/juju/juju/api/agent" + "github.com/juju/juju/api/charmrevisionupdater" + "github.com/juju/juju/api/cleaner" + "github.com/juju/juju/api/deployer" + "github.com/juju/juju/api/diskmanager" + "github.com/juju/juju/api/environment" + "github.com/juju/juju/api/firewaller" + "github.com/juju/juju/api/instancepoller" + "github.com/juju/juju/api/keyupdater" + apilogger "github.com/juju/juju/api/logger" + "github.com/juju/juju/api/machiner" + "github.com/juju/juju/api/networker" + "github.com/juju/juju/api/provisioner" + "github.com/juju/juju/api/reboot" + "github.com/juju/juju/api/resumer" + "github.com/juju/juju/api/rsyslog" + "github.com/juju/juju/api/storageprovisioner" + "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/juju/version" +) + +// Info encapsulates information about a server holding juju state and +// can be used to make a connection to it. +type Info struct { + + // This block of fields is sufficient to connect: + + // Addrs holds the addresses of the state servers. + Addrs []string + + // CACert holds the CA certificate that will be used + // to validate the state server's certificate, in PEM format. + CACert string + + // EnvironTag holds the environ tag for the environment we are + // trying to connect to. + EnvironTag names.EnvironTag + + // ...but this block of fields is all about the authentication mechanism + // to use after connecting -- if any -- and should probably be extracted. + + // Tag holds the name of the entity that is connecting. + // If this is nil, and the password is empty, no login attempt will be made. + // (this is to allow tests to access the API to check that operations + // fail when not logged in). + Tag names.Tag + + // Password holds the password for the administrator or connecting entity. + Password string + + // Nonce holds the nonce used when provisioning the machine. Used + // only by the machine agent. + Nonce string `yaml:",omitempty"` +} + +// DialOpts holds configuration parameters that control the +// Dialing behavior when connecting to a state server. +type DialOpts struct { + // DialAddressInterval is the amount of time to wait + // before starting to dial another address. + DialAddressInterval time.Duration + + // Timeout is the amount of time to wait contacting + // a state server. + Timeout time.Duration + + // RetryDelay is the amount of time to wait between + // unsucssful connection attempts. + RetryDelay time.Duration +} + +// DefaultDialOpts returns a DialOpts representing the default +// parameters for contacting a state server. +func DefaultDialOpts() DialOpts { + return DialOpts{ + DialAddressInterval: 50 * time.Millisecond, + Timeout: 10 * time.Minute, + RetryDelay: 2 * time.Second, + } +} + +// OpenFunc is the usual form of a function that opens an API connection. +type OpenFunc func(*Info, DialOpts) (Connection, error) + +// Connection exists purely to make api-opening funcs mockable. It's just a +// dumb copy of all the methods on api.Connection; we can and should be extracting +// smaller and more relevant interfaces (and dropping some of them too). +type Connection interface { + + // This first block of methods is pretty close to a sane Connection interface. + Close() error + Broken() <-chan struct{} + Addr() string + APIHostPorts() [][]network.HostPort + + // These are a bit off -- ServerVersion is apparently not known until after + // Login()? Maybe evidence of need for a separate AuthenticatedConnection..? + Login(name, password, nonce string) error + ServerVersion() (version.Number, bool) + + // These are either part of base.APICaller or look like they probably should + // be (ServerTag in particular). It's fine and good for Connection to be an + // APICaller. + APICall(facade string, version int, id, method string, args, response interface{}) error + BestFacadeVersion(string) int + EnvironTag() (names.EnvironTag, error) + ServerTag() (names.EnvironTag, error) + + // These HTTP methods should probably be separated out somehow. + NewHTTPClient() *http.Client + NewHTTPRequest(method, path string) (*http.Request, error) + SendHTTPRequest(path string, args interface{}) (*http.Request, *http.Response, error) + SendHTTPRequestReader(path string, attached io.Reader, meta interface{}, name string) (*http.Request, *http.Response, error) + + // All the rest are strange and questionable and deserve extra attention + // and/or discussion. + + // Something-or-other expects Ping to exist, and *maybe* the heartbeat + // *should* be handled outside the State type, but it's also handled + // inside it as well. We should figure this out sometime -- we should + // 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 + + // These methods expose a bunch of worker-specific facades, and basically + // just should not exist; but removing them is too noisy for a single CL. + // Client in particular is intimately coupled with State -- and the others + // will be easy to remove, but until we're using them via manifolds it's + // prohibitively ugly to do so. + Client() *Client + Machiner() *machiner.State + Resumer() *resumer.API + Networker() networker.State + Provisioner() *provisioner.State + Uniter() (*uniter.State, error) + DiskManager() (*diskmanager.State, error) + StorageProvisioner(scope names.Tag) *storageprovisioner.State + Firewaller() *firewaller.State + Agent() *agent.State + Upgrader() *upgrader.State + Reboot() (*reboot.State, error) + Deployer() *deployer.State + Environment() *environment.Facade + Logger() *apilogger.State + KeyUpdater() *keyupdater.State + Addresser() *addresser.API + InstancePoller() *instancepoller.API + CharmRevisionUpdater() *charmrevisionupdater.State + Cleaner() *cleaner.API + Rsyslog() *rsyslog.State +} === modified file 'src/github.com/juju/juju/api/keyupdater/authorisedkeys_test.go' --- src/github.com/juju/juju/api/keyupdater/authorisedkeys_test.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/api/keyupdater/authorisedkeys_test.go 2015-10-23 18:29:32 +0000 @@ -29,7 +29,7 @@ func (s *keyupdaterSuite) SetUpTest(c *gc.C) { s.JujuConnSuite.SetUpTest(c) - var stateAPI *api.State + var stateAPI api.Connection stateAPI, s.rawMachine = s.OpenAPIAsNewMachine(c) c.Assert(stateAPI, gc.NotNil) s.keyupdater = stateAPI.KeyUpdater() === modified file 'src/github.com/juju/juju/api/logger/logger_test.go' --- src/github.com/juju/juju/api/logger/logger_test.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/api/logger/logger_test.go 2015-10-23 18:29:32 +0000 @@ -32,7 +32,7 @@ func (s *loggerSuite) SetUpTest(c *gc.C) { s.JujuConnSuite.SetUpTest(c) - var stateAPI *api.State + var stateAPI api.Connection stateAPI, s.rawMachine = s.OpenAPIAsNewMachine(c) // Create the logger facade. s.logger = stateAPI.Logger() === modified file 'src/github.com/juju/juju/api/machiner/machine.go' --- src/github.com/juju/juju/api/machiner/machine.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/machiner/machine.go 2015-10-23 18:29:32 +0000 @@ -44,7 +44,7 @@ func (m *Machine) SetStatus(status params.Status, info string, data map[string]interface{}) error { var result params.ErrorResults args := params.SetStatus{ - Entities: []params.EntityStatus{ + Entities: []params.EntityStatusArgs{ {Tag: m.tag.String(), Status: status, Info: info, Data: data}, }, } === modified file 'src/github.com/juju/juju/api/machiner/machiner_test.go' --- src/github.com/juju/juju/api/machiner/machiner_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/machiner/machiner_test.go 2015-10-23 18:29:32 +0000 @@ -30,7 +30,7 @@ testing.JujuConnSuite *apitesting.APIAddresserTests - st *api.State + st api.Connection machine *state.Machine machiner *machiner.State === modified file 'src/github.com/juju/juju/api/metricsmanager/client.go' --- src/github.com/juju/juju/api/metricsmanager/client.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/api/metricsmanager/client.go 2015-10-23 18:29:32 +0000 @@ -16,7 +16,7 @@ // Client provides access to the metrics manager api type Client struct { base.ClientFacade - st *api.State + st api.Connection facade base.FacadeCaller } @@ -29,7 +29,7 @@ var _ MetricsManagerClient = (*Client)(nil) // NewClient creates a new client for accessing the metricsmanager api -func NewClient(st *api.State) *Client { +func NewClient(st api.Connection) *Client { frontend, backend := base.NewClientFacade(st, "MetricsManager") return &Client{ClientFacade: frontend, st: st, facade: backend} } === modified file 'src/github.com/juju/juju/api/networker/networker_test.go' --- src/github.com/juju/juju/api/networker/networker_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/networker/networker_test.go 2015-10-23 18:29:32 +0000 @@ -36,7 +36,7 @@ containerIfaces []state.NetworkInterfaceInfo nestedContainerIfaces []state.NetworkInterfaceInfo - st *api.State + st api.Connection networker networker.State } === modified file 'src/github.com/juju/juju/api/provisioner/machine.go' --- src/github.com/juju/juju/api/provisioner/machine.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/provisioner/machine.go 2015-10-23 18:29:32 +0000 @@ -72,7 +72,7 @@ func (m *Machine) SetStatus(status params.Status, info string, data map[string]interface{}) error { var result params.ErrorResults args := params.SetStatus{ - Entities: []params.EntityStatus{ + Entities: []params.EntityStatusArgs{ {Tag: m.tag.String(), Status: status, Info: info, Data: data}, }, } === modified file 'src/github.com/juju/juju/api/provisioner/provisioner_test.go' --- src/github.com/juju/juju/api/provisioner/provisioner_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/provisioner/provisioner_test.go 2015-10-23 18:29:32 +0000 @@ -27,6 +27,7 @@ "github.com/juju/juju/container" "github.com/juju/juju/feature" "github.com/juju/juju/instance" + "github.com/juju/juju/juju/arch" "github.com/juju/juju/juju/testing" "github.com/juju/juju/mongo" "github.com/juju/juju/network" @@ -48,7 +49,7 @@ *apitesting.EnvironWatcherTests *apitesting.APIAddresserTests - st *api.State + st api.Connection machine *state.Machine provisioner *provisioner.State @@ -470,13 +471,29 @@ } func (s *provisionerSuite) TestProvisioningInfo(c *gc.C) { - cons := constraints.MustParse("cpu-cores=12 mem=8G networks=^net3,^net4") + // Add a couple of spaces. + _, err := s.State.AddSpace("space1", nil, true) + c.Assert(err, jc.ErrorIsNil) + _, err = s.State.AddSpace("space2", nil, false) + c.Assert(err, jc.ErrorIsNil) + // Add 2 subnets into each space. + // Only the first subnet of space2 has AllocatableIPLow|High set. + // Each subnet is in a matching zone (e.g "subnet-#" in "zone#"). + testing.AddSubnetsWithTemplate(c, s.State, 4, state.SubnetInfo{ + CIDR: "10.{{.}}.0.0/16", + ProviderId: "subnet-{{.}}", + AllocatableIPLow: "{{if (eq . 2)}}10.{{.}}.0.5{{end}}", + AllocatableIPHigh: "{{if (eq . 2)}}10.{{.}}.254.254{{end}}", + AvailabilityZone: "zone{{.}}", + SpaceName: "{{if (lt . 2)}}space1{{else}}space2{{end}}", + }) + + cons := constraints.MustParse("cpu-cores=12 mem=8G spaces=^space1,space2") template := state.MachineTemplate{ - Series: "quantal", - Jobs: []state.MachineJob{state.JobHostUnits}, - Placement: "valid", - Constraints: cons, - RequestedNetworks: []string{"net1", "net2"}, + Series: "quantal", + Jobs: []state.MachineJob{state.JobHostUnits}, + Placement: "valid", + Constraints: cons, } machine, err := s.State.AddOneMachine(template) c.Assert(err, jc.ErrorIsNil) @@ -486,8 +503,12 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(provisioningInfo.Series, gc.Equals, template.Series) c.Assert(provisioningInfo.Placement, gc.Equals, template.Placement) - c.Assert(provisioningInfo.Constraints, gc.DeepEquals, template.Constraints) - c.Assert(provisioningInfo.Networks, gc.DeepEquals, template.RequestedNetworks) + c.Assert(provisioningInfo.Constraints, jc.DeepEquals, template.Constraints) + c.Assert(provisioningInfo.Networks, gc.HasLen, 0) + c.Assert(provisioningInfo.SubnetsToZones, jc.DeepEquals, map[string][]string{ + "subnet-2": []string{"zone2"}, + "subnet-3": []string{"zone3"}, + }) } func (s *provisionerSuite) TestProvisioningInfoMachineNotFound(c *gc.C) { @@ -796,7 +817,7 @@ MajorVersion: -1, } if matchArch { - expected.Arch = version.Current.Arch + expected.Arch = arch.HostArch() } c.Assert(args, gc.Equals, expected) result := response.(*params.FindToolsResult) @@ -807,11 +828,12 @@ return apiError }) - var arch *string + var a *string if matchArch { - arch = &version.Current.Arch + arch := arch.HostArch() + a = &arch } - apiList, err := s.provisioner.FindTools(version.Current.Number, version.Current.Series, arch) + apiList, err := s.provisioner.FindTools(version.Current.Number, version.Current.Series, a) c.Assert(called, jc.IsTrue) if apiError != nil { c.Assert(err, gc.Equals, apiError) @@ -833,9 +855,12 @@ container, err := s.State.AddMachineInsideMachine(template, s.machine.Id(), instance.LXC) c.Assert(err, jc.ErrorIsNil) + ifaceInfo, err := s.provisioner.PrepareContainerInterfaceInfo(container.MachineTag()) + c.Assert(err, jc.ErrorIsNil) + c.Assert(ifaceInfo, gc.HasLen, 1) + expectInfo := []network.InterfaceInfo{{ DeviceIndex: 0, - MACAddress: "aa:bb:cc:dd:ee:f0", CIDR: "0.10.0.0/24", NetworkName: "juju-private", ProviderId: "dummy-eth0", @@ -852,11 +877,10 @@ GatewayAddress: network.NewAddress("0.10.0.2"), ExtraConfig: nil, }} - ifaceInfo, err := s.provisioner.PrepareContainerInterfaceInfo(container.MachineTag()) - c.Assert(err, jc.ErrorIsNil) - c.Assert(ifaceInfo, gc.HasLen, 1) c.Assert(ifaceInfo[0].Address, gc.Not(gc.DeepEquals), network.Address{}) + c.Assert(ifaceInfo[0].MACAddress, gc.Not(gc.DeepEquals), "") expectInfo[0].Address = ifaceInfo[0].Address + expectInfo[0].MACAddress = ifaceInfo[0].MACAddress c.Assert(ifaceInfo, jc.DeepEquals, expectInfo) } @@ -883,7 +907,7 @@ addr := network.NewAddress(fmt.Sprintf("0.10.0.%d", i)) ipaddr, err := s.State.AddIPAddress(addr, sub.ID()) c.Check(err, jc.ErrorIsNil) - err = ipaddr.AllocateTo(container.Id(), "") + err = ipaddr.AllocateTo(container.Id(), "", "") c.Check(err, jc.ErrorIsNil) } c.Assert(err, jc.ErrorIsNil) === modified file 'src/github.com/juju/juju/api/reboot/reboot_test.go' --- src/github.com/juju/juju/api/reboot/reboot_test.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/api/reboot/reboot_test.go 2015-10-23 18:29:32 +0000 @@ -29,7 +29,7 @@ testing.JujuConnSuite machine *state.Machine - st *api.State + st api.Connection reboot *reboot.State } === added directory 'src/github.com/juju/juju/api/resumer' === added file 'src/github.com/juju/juju/api/resumer/package_test.go' --- src/github.com/juju/juju/api/resumer/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/resumer/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,14 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package resumer_test + +import ( + "testing" + + gc "gopkg.in/check.v1" +) + +func TestPackage(t *testing.T) { + gc.TestingT(t) +} === added file 'src/github.com/juju/juju/api/resumer/resumer.go' --- src/github.com/juju/juju/api/resumer/resumer.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/resumer/resumer.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,27 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package resumer + +import ( + "github.com/juju/juju/api/base" +) + +const resumerFacade = "Resumer" + +// API provides access to the Resumer API facade. +type API struct { + facade base.FacadeCaller +} + +// NewAPI creates a new client-side Resumer facade. +func NewAPI(caller base.APICaller) *API { + facadeCaller := base.NewFacadeCaller(caller, resumerFacade) + return &API{facade: facadeCaller} + +} + +// ResumeTransactions calls the server-side ResumeTransactions method. +func (api *API) ResumeTransactions() error { + return api.facade.FacadeCall("ResumeTransactions", nil, nil) +} === added file 'src/github.com/juju/juju/api/resumer/resumer_test.go' --- src/github.com/juju/juju/api/resumer/resumer_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/resumer/resumer_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,60 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package resumer_test + +import ( + "errors" + + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + apitesting "github.com/juju/juju/api/base/testing" + "github.com/juju/juju/api/resumer" + coretesting "github.com/juju/juju/testing" +) + +type ResumerSuite struct { + coretesting.BaseSuite +} + +var _ = gc.Suite(&ResumerSuite{}) + +func (s *ResumerSuite) TestResumeTransactionsSuccess(c *gc.C) { + var callCount int + apiCaller := apitesting.APICallerFunc( + func(objType string, version int, id, request string, args, results interface{}) error { + c.Check(objType, gc.Equals, "Resumer") + // Since we're not logging in and getting the supported + // facades and their versions, the client will always send + // version 0. + c.Check(version, gc.Equals, 0) + c.Check(id, gc.Equals, "") + c.Check(request, gc.Equals, "ResumeTransactions") + c.Check(args, gc.IsNil) + c.Check(results, gc.IsNil) + callCount++ + return nil + }, + ) + + st := resumer.NewAPI(apiCaller) + err := st.ResumeTransactions() + c.Check(err, jc.ErrorIsNil) + c.Check(callCount, gc.Equals, 1) +} + +func (s *ResumerSuite) TestResumeTransactionsFailure(c *gc.C) { + var callCount int + apiCaller := apitesting.APICallerFunc( + func(_ string, _ int, _, _ string, _, _ interface{}) error { + callCount++ + return errors.New("boom!") + }, + ) + + st := resumer.NewAPI(apiCaller) + err := st.ResumeTransactions() + c.Check(err, gc.ErrorMatches, "boom!") + c.Check(callCount, gc.Equals, 1) +} === modified file 'src/github.com/juju/juju/api/rsyslog/rsyslog_test.go' --- src/github.com/juju/juju/api/rsyslog/rsyslog_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/rsyslog/rsyslog_test.go 2015-10-23 18:29:32 +0000 @@ -19,7 +19,7 @@ type rsyslogSuite struct { testing.JujuConnSuite - st *api.State + st api.Connection machine *state.Machine rsyslog *rsyslog.State } === modified file 'src/github.com/juju/juju/api/service/client.go' --- src/github.com/juju/juju/api/service/client.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/service/client.go 2015-10-23 18:29:32 +0000 @@ -9,23 +9,27 @@ import ( "github.com/juju/errors" + "github.com/juju/loggo" "github.com/juju/juju/api" "github.com/juju/juju/api/base" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/constraints" + "github.com/juju/juju/instance" "github.com/juju/juju/storage" ) +var logger = loggo.GetLogger("juju.api.service") + // Client allows access to the service API end point. type Client struct { base.ClientFacade - st *api.State + st api.Connection facade base.FacadeCaller } // NewClient creates a new client for accessing the service api. -func NewClient(st *api.State) *Client { +func NewClient(st api.Connection) *Client { frontend, backend := base.NewClientFacade(st, "Service") return &Client{ClientFacade: frontend, st: st, facade: backend} } @@ -44,11 +48,22 @@ return errors.Trace(results.OneError()) } +// EnvironmentUUID returns the environment UUID from the client connection. +func (c *Client) EnvironmentUUID() string { + tag, err := c.st.EnvironTag() + if err != nil { + logger.Warningf("environ tag not an environ: %v", err) + return "" + } + return tag.Id() +} + // ServiceDeploy obtains the charm, either locally or from // the charm store, and deploys it. It allows the specification of // requested networks that must be present on the machines where the // service is deployed. Another way to specify networks to include/exclude -// is using constraints. +// is using constraints. Placement directives, if provided, specify the +// machine on which the charm is deployed. func (c *Client) ServiceDeploy( charmURL string, serviceName string, @@ -56,6 +71,7 @@ configYAML string, cons constraints.Value, toMachineSpec string, + placement []*instance.Placement, networks []string, storage map[string]storage.Constraints, ) error { @@ -67,12 +83,24 @@ ConfigYAML: configYAML, Constraints: cons, ToMachineSpec: toMachineSpec, + Placement: placement, Networks: networks, Storage: storage, }}, } var results params.ErrorResults - err := c.facade.FacadeCall("ServicesDeploy", args, &results) + var err error + if len(placement) > 0 { + err = c.facade.FacadeCall("ServicesDeployWithPlacement", args, &results) + if err != nil { + if params.IsCodeNotImplemented(err) { + return errors.Errorf("unsupported --to parameter %q", toMachineSpec) + } + return err + } + } else { + err = c.facade.FacadeCall("ServicesDeploy", args, &results) + } if err != nil { return err } === modified file 'src/github.com/juju/juju/api/service/client_test.go' --- src/github.com/juju/juju/api/service/client_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/service/client_test.go 2015-10-23 18:29:32 +0000 @@ -95,7 +95,7 @@ return nil }) err := s.client.ServiceDeploy("charmURL", "serviceA", 2, "configYAML", constraints.MustParse("mem=4G"), - "machineSpec", []string{"neta"}, map[string]storage.Constraints{"data": storage.Constraints{Pool: "pool"}}) + "machineSpec", nil, []string{"neta"}, map[string]storage.Constraints{"data": storage.Constraints{Pool: "pool"}}) c.Assert(err, jc.ErrorIsNil) c.Assert(called, jc.IsTrue) } === added directory 'src/github.com/juju/juju/api/spaces' === added file 'src/github.com/juju/juju/api/spaces/package_test.go' --- src/github.com/juju/juju/api/spaces/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/spaces/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,14 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package spaces_test + +import ( + stdtesting "testing" + + "github.com/juju/juju/testing" +) + +func TestAll(t *stdtesting.T) { + testing.MgoTestPackage(t) +} === added file 'src/github.com/juju/juju/api/spaces/spaces.go' --- src/github.com/juju/juju/api/spaces/spaces.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/spaces/spaces.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,77 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package spaces + +import ( + "github.com/juju/errors" + "github.com/juju/loggo" + "github.com/juju/names" + + "github.com/juju/juju/api/base" + "github.com/juju/juju/apiserver/params" +) + +var logger = loggo.GetLogger("juju.api.spaces") + +const spacesFacade = "Spaces" + +// API provides access to the InstancePoller API facade. +type API struct { + base.ClientFacade + facade base.FacadeCaller +} + +// NewAPI creates a new client-side Spaces facade. +func NewAPI(caller base.APICallCloser) *API { + if caller == nil { + panic("caller is nil") + } + clientFacade, facadeCaller := base.NewClientFacade(caller, spacesFacade) + return &API{ + ClientFacade: clientFacade, + facade: facadeCaller, + } +} + +func makeCreateSpaceParams(name string, subnetIds []string, public bool) params.CreateSpaceParams { + spaceTag := names.NewSpaceTag(name).String() + subnetTags := make([]string, len(subnetIds)) + + for i, s := range subnetIds { + subnetTags[i] = names.NewSubnetTag(s).String() + } + + return params.CreateSpaceParams{ + SpaceTag: spaceTag, + SubnetTags: subnetTags, + Public: public, + } +} + +// CreateSpace creates a new Juju network space, associating the +// specified subnets with it (optional; can be empty). +func (api *API) CreateSpace(name string, subnetIds []string, public bool) error { + var response params.ErrorResults + createSpacesParams := params.CreateSpacesParams{ + Spaces: []params.CreateSpaceParams{makeCreateSpaceParams(name, subnetIds, public)}, + } + err := api.facade.FacadeCall("CreateSpaces", createSpacesParams, &response) + if err != nil { + if params.IsCodeNotSupported(err) { + return errors.NewNotSupported(nil, err.Error()) + } + return errors.Trace(err) + } + return response.OneError() +} + +// ListSpaces lists all available spaces and their associated subnets. +func (api *API) ListSpaces() ([]params.Space, error) { + var response params.ListSpacesResults + err := api.facade.FacadeCall("ListSpaces", nil, &response) + if params.IsCodeNotSupported(err) { + return response.Results, errors.NewNotSupported(nil, err.Error()) + } + return response.Results, err +} === added file 'src/github.com/juju/juju/api/spaces/spaces_test.go' --- src/github.com/juju/juju/api/spaces/spaces_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/spaces/spaces_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,173 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package spaces_test + +import ( + "errors" + "fmt" + "math/rand" + + "github.com/juju/names" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/api/base" + apitesting "github.com/juju/juju/api/base/testing" + "github.com/juju/juju/api/spaces" + "github.com/juju/juju/apiserver/params" + coretesting "github.com/juju/juju/testing" +) + +type SpacesSuite struct { + coretesting.BaseSuite + + called int + apiCaller base.APICallCloser + api *spaces.API +} + +var _ = gc.Suite(&SpacesSuite{}) + +func (s *SpacesSuite) init(c *gc.C, args *apitesting.CheckArgs, err error) { + s.called = 0 + s.apiCaller = apitesting.CheckingAPICaller(c, args, &s.called, err) + s.api = spaces.NewAPI(s.apiCaller) + c.Check(s.api, gc.NotNil) + c.Check(s.called, gc.Equals, 0) +} + +func (s *SpacesSuite) TestNewAPISuccess(c *gc.C) { + var called int + apiCaller := apitesting.CheckingAPICaller(c, nil, &called, nil) + api := spaces.NewAPI(apiCaller) + c.Check(api, gc.NotNil) + c.Check(called, gc.Equals, 0) +} + +func (s *SpacesSuite) TestNewAPIWithNilCaller(c *gc.C) { + panicFunc := func() { spaces.NewAPI(nil) } + c.Assert(panicFunc, gc.PanicMatches, "caller is nil") +} + +func makeArgs(name string, subnets []string) (string, []string, apitesting.CheckArgs) { + spaceTag := names.NewSpaceTag(name).String() + subnetTags := []string{} + + for _, s := range subnets { + subnetTags = append(subnetTags, names.NewSubnetTag(s).String()) + } + + expectArgs := params.CreateSpacesParams{ + Spaces: []params.CreateSpaceParams{ + params.CreateSpaceParams{ + SpaceTag: spaceTag, + SubnetTags: subnetTags, + Public: true, + }}} + + expectResults := params.ErrorResults{ + Results: []params.ErrorResult{{}}, + } + + args := apitesting.CheckArgs{ + Facade: "Spaces", + Method: "CreateSpaces", + Args: expectArgs, + Results: expectResults, + } + + return name, subnets, args +} + +func (s *SpacesSuite) testCreateSpace(c *gc.C, name string, subnets []string) { + _, _, args := makeArgs(name, subnets) + s.init(c, &args, nil) + err := s.api.CreateSpace(name, subnets, true) + c.Assert(s.called, gc.Equals, 1) + c.Assert(err, jc.ErrorIsNil) +} + +func (s *SpacesSuite) TestCreateSpace(c *gc.C) { + name := "foo" + subnets := []string{} + r := rand.New(rand.NewSource(0xdeadbeef)) + for i := 0; i < 100; i++ { + for j := 0; j < 10; j++ { + n := r.Uint32() + newSubnet := fmt.Sprintf("%d.%d.%d.0/24", uint8(n>>16), uint8(n>>8), uint8(n)) + subnets = append(subnets, newSubnet) + } + s.testCreateSpace(c, name, subnets) + } +} + +func (s *SpacesSuite) TestCreateSpaceEmptyResults(c *gc.C) { + _, _, args := makeArgs("foo", nil) + args.Results = params.ErrorResults{} + s.init(c, &args, nil) + err := s.api.CreateSpace("foo", nil, true) + c.Check(s.called, gc.Equals, 1) + c.Assert(err, gc.ErrorMatches, "expected 1 result, got 0") +} + +func (s *SpacesSuite) TestCreateSpaceFails(c *gc.C) { + name, subnets, args := makeArgs("foo", []string{"1.1.1.0/24"}) + s.init(c, &args, errors.New("bang")) + err := s.api.CreateSpace(name, subnets, true) + c.Check(s.called, gc.Equals, 1) + c.Assert(err, gc.ErrorMatches, "bang") +} + +func (s *SpacesSuite) testListSpaces(c *gc.C, results []params.Space, err error, expectErr string) { + var expectResults params.ListSpacesResults + if results != nil { + expectResults = params.ListSpacesResults{ + Results: results, + } + } + + args := apitesting.CheckArgs{ + Facade: "Spaces", + Method: "ListSpaces", + Results: expectResults, + } + s.init(c, &args, err) + gotResults, gotErr := s.api.ListSpaces() + c.Assert(s.called, gc.Equals, 1) + c.Assert(gotResults, jc.DeepEquals, results) + if expectErr != "" { + c.Assert(gotErr, gc.ErrorMatches, expectErr) + return + } + if err != nil { + c.Assert(gotErr, jc.DeepEquals, err) + } else { + c.Assert(gotErr, jc.ErrorIsNil) + } +} + +func (s *SpacesSuite) TestListSpacesEmptyResults(c *gc.C) { + s.testListSpaces(c, []params.Space{}, nil, "") +} + +func (s *SpacesSuite) TestListSpacesManyResults(c *gc.C) { + spaces := []params.Space{{ + Name: "space1", + Subnets: []params.Subnet{{ + CIDR: "foo", + }, { + CIDR: "bar", + }}, + }, { + Name: "space2", + }, { + Name: "space3", + Subnets: []params.Subnet{}, + }} + s.testListSpaces(c, spaces, nil, "") +} + +func (s *SpacesSuite) TestListSpacesServerError(c *gc.C) { + s.testListSpaces(c, nil, errors.New("boom"), "boom") +} === modified file 'src/github.com/juju/juju/api/state.go' --- src/github.com/juju/juju/api/state.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/state.go 2015-10-23 18:29:32 +0000 @@ -10,19 +10,23 @@ "github.com/juju/errors" "github.com/juju/names" + "github.com/juju/juju/api/addresser" "github.com/juju/juju/api/agent" "github.com/juju/juju/api/base" "github.com/juju/juju/api/charmrevisionupdater" + "github.com/juju/juju/api/cleaner" "github.com/juju/juju/api/deployer" "github.com/juju/juju/api/diskmanager" "github.com/juju/juju/api/environment" "github.com/juju/juju/api/firewaller" + "github.com/juju/juju/api/instancepoller" "github.com/juju/juju/api/keyupdater" apilogger "github.com/juju/juju/api/logger" "github.com/juju/juju/api/machiner" "github.com/juju/juju/api/networker" "github.com/juju/juju/api/provisioner" "github.com/juju/juju/api/reboot" + "github.com/juju/juju/api/resumer" "github.com/juju/juju/api/rsyslog" "github.com/juju/juju/api/storageprovisioner" "github.com/juju/juju/api/uniter" @@ -237,6 +241,12 @@ return machiner.NewState(st) } +// Resumer returns a version of the state that provides functionality +// required by the resumer worker. +func (st *State) Resumer() *resumer.API { + return resumer.NewAPI(st) +} + // Networker returns a version of the state that provides functionality // required by the networker worker. func (st *State) Networker() networker.State { @@ -311,6 +321,11 @@ return deployer.NewState(st) } +// Addresser returns access to the Addresser API. +func (st *State) Addresser() *addresser.API { + return addresser.NewAPI(st) +} + // Environment returns access to the Environment API func (st *State) Environment() *environment.Facade { return environment.NewFacade(st) @@ -326,11 +341,21 @@ return keyupdater.NewState(st) } +// InstancePoller returns access to the InstancePoller API +func (st *State) InstancePoller() *instancepoller.API { + return instancepoller.NewAPI(st) +} + // CharmRevisionUpdater returns access to the CharmRevisionUpdater API func (st *State) CharmRevisionUpdater() *charmrevisionupdater.State { return charmrevisionupdater.NewState(st) } +// Cleaner returns a version of the state that provides access to the cleaner API +func (st *State) Cleaner() *cleaner.API { + return cleaner.NewAPI(st) +} + // Rsyslog returns access to the Rsyslog API func (st *State) Rsyslog() *rsyslog.State { return rsyslog.NewState(st) === modified file 'src/github.com/juju/juju/api/state_test.go' --- src/github.com/juju/juju/api/state_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/state_test.go 2015-10-23 18:29:32 +0000 @@ -41,7 +41,7 @@ // OpenAPIWithoutLogin connects to the API and returns an api.State without // actually calling st.Login already. The returned strings are the "tag" and // "password" that we would have used to login. -func (s *stateSuite) OpenAPIWithoutLogin(c *gc.C) (*api.State, string, string) { +func (s *stateSuite) OpenAPIWithoutLogin(c *gc.C) (api.Connection, string, string) { info := s.APIInfo(c) tag := info.Tag password := info.Password === modified file 'src/github.com/juju/juju/api/storage/client.go' --- src/github.com/juju/juju/api/storage/client.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/storage/client.go 2015-10-23 18:29:32 +0000 @@ -29,7 +29,7 @@ } // Show retrieves information about desired storage instances. -func (c *Client) Show(tags []names.StorageTag) ([]params.StorageDetails, error) { +func (c *Client) Show(tags []names.StorageTag) ([]params.StorageDetailsResult, error) { found := params.StorageDetailsResults{} entities := make([]params.Entity, len(tags)) for i, tag := range tags { @@ -38,25 +38,12 @@ if err := c.facade.FacadeCall("Show", params.Entities{Entities: entities}, &found); err != nil { return nil, errors.Trace(err) } - return c.convert(found.Results) -} - -func (c *Client) convert(found []params.StorageDetailsResult) ([]params.StorageDetails, error) { - var storages []params.StorageDetails - var allErr params.ErrorResults - for _, result := range found { - if result.Error != nil { - allErr.Results = append(allErr.Results, params.ErrorResult{result.Error}) - continue - } - storages = append(storages, result.Result) - } - return storages, allErr.Combine() + return found.Results, nil } // List lists all storage. -func (c *Client) List() ([]params.StorageInfo, error) { - found := params.StorageInfosResult{} +func (c *Client) List() ([]params.StorageDetailsResult, error) { + found := params.StorageDetailsResults{} if err := c.facade.FacadeCall("List", nil, &found); err != nil { return nil, errors.Trace(err) } @@ -89,13 +76,13 @@ // ListVolumes lists volumes for desired machines. // If no machines provided, a list of all volumes is returned. -func (c *Client) ListVolumes(machines []string) ([]params.VolumeItem, error) { +func (c *Client) ListVolumes(machines []string) ([]params.VolumeDetailsResult, error) { tags := make([]string, len(machines)) for i, one := range machines { tags[i] = names.NewMachineTag(one).String() } args := params.VolumeFilter{Machines: tags} - found := params.VolumeItemsResult{} + found := params.VolumeDetailsResults{} if err := c.facade.FacadeCall("ListVolumes", args, &found); err != nil { return nil, errors.Trace(err) } === modified file 'src/github.com/juju/juju/api/storage/client_test.go' --- src/github.com/juju/juju/api/storage/client_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/storage/client_test.go 2015-10-23 18:29:32 +0000 @@ -50,16 +50,20 @@ if results, k := result.(*params.StorageDetailsResults); k { instances := []params.StorageDetailsResult{ params.StorageDetailsResult{ - Result: params.StorageDetails{StorageTag: oneTag.String()}, + Result: ¶ms.StorageDetails{StorageTag: oneTag.String()}, }, params.StorageDetailsResult{ - Result: params.StorageDetails{ + Result: ¶ms.StorageDetails{ StorageTag: twoTag.String(), - Status: "attached", + Status: params.EntityStatus{ + Status: "attached", + }, Persistent: true, }, }, - params.StorageDetailsResult{Error: common.ServerError(errors.New(msg))}, + params.StorageDetailsResult{ + Error: common.ServerError(errors.New(msg)), + }, } results.Results = instances } @@ -69,10 +73,11 @@ storageClient := storage.NewClient(apiCaller) tags := []names.StorageTag{oneTag, twoTag} found, err := storageClient.Show(tags) - c.Check(errors.Cause(err), gc.ErrorMatches, msg) - c.Assert(found, gc.HasLen, 2) - c.Assert(expected.Contains(found[0].StorageTag), jc.IsTrue) - c.Assert(expected.Contains(found[1].StorageTag), jc.IsTrue) + c.Assert(err, jc.ErrorIsNil) + c.Assert(found, gc.HasLen, 3) + c.Assert(expected.Contains(found[0].Result.StorageTag), jc.IsTrue) + c.Assert(expected.Contains(found[1].Result.StorageTag), jc.IsTrue) + c.Assert(found[2].Error, gc.ErrorMatches, msg) } func (s *storageMockSuite) TestShowFacadeCallError(c *gc.C) { @@ -99,10 +104,7 @@ } func (s *storageMockSuite) TestList(c *gc.C) { - one := "shared-fs/0" - oneTag := names.NewStorageTag(one) - two := "db-dir/1000" - twoTag := names.NewStorageTag(two) + storageTag := names.NewStorageTag("db-dir/1000") msg := "call failure" apiCaller := basetesting.APICallerFunc( @@ -116,21 +118,18 @@ c.Check(request, gc.Equals, "List") c.Check(a, gc.IsNil) - if results, k := result.(*params.StorageInfosResult); k { - instances := []params.StorageInfo{ - params.StorageInfo{ - params.StorageDetails{StorageTag: oneTag.String()}, - common.ServerError(errors.New(msg)), - }, - params.StorageInfo{ - params.StorageDetails{ - StorageTag: twoTag.String(), - Status: "attached", - Persistent: true, + if results, k := result.(*params.StorageDetailsResults); k { + instances := []params.StorageDetailsResult{{ + Error: common.ServerError(errors.New(msg)), + }, { + Result: ¶ms.StorageDetails{ + StorageTag: storageTag.String(), + Status: params.EntityStatus{ + Status: "attached", }, - nil, + Persistent: true, }, - } + }} results.Results = instances } @@ -140,19 +139,17 @@ found, err := storageClient.List() c.Check(err, jc.ErrorIsNil) c.Assert(found, gc.HasLen, 2) - expected := []params.StorageInfo{ - params.StorageInfo{ - StorageDetails: params.StorageDetails{ - StorageTag: "storage-shared-fs-0"}, - Error: ¶ms.Error{Message: msg}, + expected := []params.StorageDetailsResult{{ + Error: ¶ms.Error{Message: msg}, + }, { + Result: ¶ms.StorageDetails{ + StorageTag: "storage-db-dir-1000", + Status: params.EntityStatus{ + Status: "attached", + }, + Persistent: true, }, - params.StorageInfo{ - params.StorageDetails{ - StorageTag: "storage-db-dir-1000", - Status: "attached", - Persistent: true}, - nil}, - } + }} c.Assert(found, jc.DeepEquals, expected) } @@ -317,15 +314,15 @@ args := a.(params.VolumeFilter) c.Assert(args.Machines, gc.HasLen, 2) - c.Assert(result, gc.FitsTypeOf, ¶ms.VolumeItemsResult{}) - results := result.(*params.VolumeItemsResult) + c.Assert(result, gc.FitsTypeOf, ¶ms.VolumeDetailsResults{}) + results := result.(*params.VolumeDetailsResults) attachments := make([]params.VolumeAttachment, len(args.Machines)) for i, m := range args.Machines { attachments[i] = params.VolumeAttachment{ MachineTag: m} } - results.Results = []params.VolumeItem{ - params.VolumeItem{Attachments: attachments}, + results.Results = []params.VolumeDetailsResult{ + params.VolumeDetailsResult{LegacyAttachments: attachments}, } return nil }) @@ -334,9 +331,9 @@ c.Assert(called, jc.IsTrue) c.Assert(err, jc.ErrorIsNil) c.Assert(found, gc.HasLen, 1) - c.Assert(found[0].Attachments, gc.HasLen, len(machines)) - c.Assert(machineTags.Contains(found[0].Attachments[0].MachineTag), jc.IsTrue) - c.Assert(machineTags.Contains(found[0].Attachments[1].MachineTag), jc.IsTrue) + c.Assert(found[0].LegacyAttachments, gc.HasLen, len(machines)) + c.Assert(machineTags.Contains(found[0].LegacyAttachments[0].MachineTag), jc.IsTrue) + c.Assert(machineTags.Contains(found[0].LegacyAttachments[1].MachineTag), jc.IsTrue) } func (s *storageMockSuite) TestListVolumesEmptyFilter(c *gc.C) { @@ -357,10 +354,10 @@ args := a.(params.VolumeFilter) c.Assert(args.IsEmpty(), jc.IsTrue) - c.Assert(result, gc.FitsTypeOf, ¶ms.VolumeItemsResult{}) - results := result.(*params.VolumeItemsResult) - results.Results = []params.VolumeItem{ - {Volume: params.VolumeInstance{VolumeTag: tag}}, + c.Assert(result, gc.FitsTypeOf, ¶ms.VolumeDetailsResults{}) + results := result.(*params.VolumeDetailsResults) + results.Results = []params.VolumeDetailsResult{ + {LegacyVolume: ¶ms.LegacyVolumeDetails{VolumeTag: tag}}, } return nil }) @@ -369,7 +366,7 @@ c.Assert(called, jc.IsTrue) c.Assert(err, jc.ErrorIsNil) c.Assert(found, gc.HasLen, 1) - c.Assert(found[0].Volume.VolumeTag, gc.Equals, tag) + c.Assert(found[0].LegacyVolume.VolumeTag, gc.Equals, tag) } func (s *storageMockSuite) TestListVolumesFacadeCallError(c *gc.C) { === modified file 'src/github.com/juju/juju/api/storageprovisioner/provisioner.go' --- src/github.com/juju/juju/api/storageprovisioner/provisioner.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/storageprovisioner/provisioner.go 2015-10-23 18:29:32 +0000 @@ -455,3 +455,13 @@ } return results.Results, nil } + +// SetStatus sets the status of storage entities. +func (st *State) SetStatus(args []params.EntityStatusArgs) error { + var result params.ErrorResults + err := st.facade.FacadeCall("SetStatus", params.SetStatus{args}, &result) + if err != nil { + return err + } + return result.Combine() +} === added directory 'src/github.com/juju/juju/api/subnets' === added file 'src/github.com/juju/juju/api/subnets/package_test.go' --- src/github.com/juju/juju/api/subnets/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/subnets/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,15 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package subnets_test + +import ( + "testing" + + gc "gopkg.in/check.v1" +) + +// TestAll is the main test function for this package +func TestAll(t *testing.T) { + gc.TestingT(t) +} === added file 'src/github.com/juju/juju/api/subnets/subnets.go' --- src/github.com/juju/juju/api/subnets/subnets.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/subnets/subnets.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,96 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package subnets + +import ( + "github.com/juju/errors" + "github.com/juju/loggo" + "github.com/juju/names" + + "github.com/juju/juju/api/base" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/network" +) + +var logger = loggo.GetLogger("juju.api.subnets") + +const subnetsFacade = "Subnets" + +// API provides access to the Subnets API facade. +type API struct { + base.ClientFacade + facade base.FacadeCaller +} + +// NewAPI creates a new client-side Subnets facade. +func NewAPI(caller base.APICallCloser) *API { + if caller == nil { + panic("caller is nil") + } + clientFacade, facadeCaller := base.NewClientFacade(caller, subnetsFacade) + return &API{ + ClientFacade: clientFacade, + facade: facadeCaller, + } +} + +// AddSubnet adds an existing subnet to the environment. +func (api *API) AddSubnet(subnet names.SubnetTag, providerId network.Id, space names.SpaceTag, zones []string) error { + var response params.ErrorResults + // Prefer ProviderId when set over CIDR. + subnetTag := subnet.String() + if providerId != "" { + subnetTag = "" + } + + params := params.AddSubnetsParams{ + Subnets: []params.AddSubnetParams{{ + SubnetTag: subnetTag, + SubnetProviderId: string(providerId), + SpaceTag: space.String(), + Zones: zones, + }}, + } + err := api.facade.FacadeCall("AddSubnets", params, &response) + if err != nil { + return errors.Trace(err) + } + return response.OneError() +} + +// CreateSubnet creates a new subnet with the provider. +func (api *API) CreateSubnet(subnet names.SubnetTag, space names.SpaceTag, zones []string, isPublic bool) error { + var response params.ErrorResults + params := params.CreateSubnetsParams{ + Subnets: []params.CreateSubnetParams{{ + SubnetTag: subnet.String(), + SpaceTag: space.String(), + Zones: zones, + IsPublic: isPublic, + }}, + } + err := api.facade.FacadeCall("CreateSubnets", params, &response) + if err != nil { + return errors.Trace(err) + } + return response.OneError() +} + +// ListSubnets fetches all the subnets known by the environment. +func (api *API) ListSubnets(spaceTag *names.SpaceTag, zone string) ([]params.Subnet, error) { + var response params.ListSubnetsResults + var space string + if spaceTag != nil { + space = spaceTag.String() + } + args := params.SubnetsFilters{ + SpaceTag: space, + Zone: zone, + } + err := api.facade.FacadeCall("ListSubnets", args, &response) + if err != nil { + return nil, errors.Trace(err) + } + return response.Results, nil +} === added file 'src/github.com/juju/juju/api/subnets/subnets_test.go' --- src/github.com/juju/juju/api/subnets/subnets_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/subnets/subnets_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,217 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package subnets_test + +import ( + "errors" + + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/api/base" + apitesting "github.com/juju/juju/api/base/testing" + "github.com/juju/juju/api/subnets" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/network" + coretesting "github.com/juju/juju/testing" + "github.com/juju/names" +) + +// SubnetsSuite tests the client side subnets API +type SubnetsSuite struct { + coretesting.BaseSuite + + called int + apiCaller base.APICallCloser + api *subnets.API +} + +var _ = gc.Suite(&SubnetsSuite{}) + +func (s *SubnetsSuite) prepareAPICall(c *gc.C, args *apitesting.CheckArgs, err error) { + s.called = 0 + s.apiCaller = apitesting.CheckingAPICaller(c, args, &s.called, err) + s.api = subnets.NewAPI(s.apiCaller) + c.Check(s.api, gc.NotNil) + c.Check(s.called, gc.Equals, 0) +} + +// TestNewAPISuccess checks that a new subnets API is created when passed a non-nil caller +func (s *SubnetsSuite) TestNewAPISuccess(c *gc.C) { + var called int + apiCaller := apitesting.CheckingAPICaller(c, nil, &called, nil) + api := subnets.NewAPI(apiCaller) + c.Check(api, gc.NotNil) + c.Check(called, gc.Equals, 0) +} + +// TestNewAPIWithNilCaller checks that a new subnets API is not created when passed a nil caller +func (s *SubnetsSuite) TestNewAPIWithNilCaller(c *gc.C) { + panicFunc := func() { subnets.NewAPI(nil) } + c.Assert(panicFunc, gc.PanicMatches, "caller is nil") +} + +func makeAddSubnetsArgs(cidr, providerId, space string, zones []string) apitesting.CheckArgs { + spaceTag := names.NewSpaceTag(space).String() + subnetTag := names.NewSubnetTag(cidr).String() + if providerId != "" { + subnetTag = "" + } + + expectArgs := params.AddSubnetsParams{ + Subnets: []params.AddSubnetParams{{ + SpaceTag: spaceTag, + SubnetTag: subnetTag, + SubnetProviderId: providerId, + Zones: zones, + }}} + + expectResults := params.ErrorResults{ + Results: []params.ErrorResult{{}}, + } + + args := apitesting.CheckArgs{ + Facade: "Subnets", + Method: "AddSubnets", + Args: expectArgs, + Results: expectResults, + } + + return args +} + +func makeCreateSubnetsArgs(cidr, space string, zones []string, isPublic bool) apitesting.CheckArgs { + spaceTag := names.NewSpaceTag(space).String() + subnetTag := names.NewSubnetTag(cidr).String() + + expectArgs := params.CreateSubnetsParams{ + Subnets: []params.CreateSubnetParams{{ + SpaceTag: spaceTag, + SubnetTag: subnetTag, + Zones: zones, + IsPublic: isPublic, + }}} + + expectResults := params.ErrorResults{ + Results: []params.ErrorResult{{}}, + } + + args := apitesting.CheckArgs{ + Facade: "Subnets", + Method: "CreateSubnets", + Args: expectArgs, + Results: expectResults, + } + + return args +} + +func makeListSubnetsArgs(space *names.SpaceTag, zone string) apitesting.CheckArgs { + expectResults := params.ListSubnetsResults{} + expectArgs := params.SubnetsFilters{ + SpaceTag: space.String(), + Zone: zone, + } + args := apitesting.CheckArgs{ + Facade: "Subnets", + Method: "ListSubnets", + Results: expectResults, + Args: expectArgs, + } + return args +} + +func (s *SubnetsSuite) TestAddSubnet(c *gc.C) { + cidr := "1.1.1.0/24" + providerId := "foo" + space := "bar" + zones := []string{"foo", "bar"} + args := makeAddSubnetsArgs(cidr, providerId, space, zones) + s.prepareAPICall(c, &args, nil) + err := s.api.AddSubnet( + names.NewSubnetTag(cidr), + network.Id(providerId), + names.NewSpaceTag(space), + zones, + ) + c.Assert(s.called, gc.Equals, 1) + c.Assert(err, jc.ErrorIsNil) +} + +func (s *SubnetsSuite) TestAddSubnetFails(c *gc.C) { + cidr := "1.1.1.0/24" + providerId := "foo" + space := "bar" + zones := []string{"foo", "bar"} + args := makeAddSubnetsArgs(cidr, providerId, space, zones) + s.prepareAPICall(c, &args, errors.New("bang")) + err := s.api.AddSubnet( + names.NewSubnetTag(cidr), + network.Id(providerId), + names.NewSpaceTag(space), + zones, + ) + c.Check(s.called, gc.Equals, 1) + c.Assert(err, gc.ErrorMatches, "bang") +} + +func (s *SubnetsSuite) TestCreateSubnet(c *gc.C) { + cidr := "1.1.1.0/24" + space := "bar" + zones := []string{"foo", "bar"} + isPublic := true + args := makeCreateSubnetsArgs(cidr, space, zones, isPublic) + s.prepareAPICall(c, &args, nil) + err := s.api.CreateSubnet( + names.NewSubnetTag(cidr), + names.NewSpaceTag(space), + zones, + isPublic, + ) + c.Assert(s.called, gc.Equals, 1) + c.Assert(err, jc.ErrorIsNil) +} + +func (s *SubnetsSuite) TestCreateSubnetFails(c *gc.C) { + cidr := "1.1.1.0/24" + isPublic := true + space := "bar" + zones := []string{"foo", "bar"} + args := makeCreateSubnetsArgs(cidr, space, zones, isPublic) + s.prepareAPICall(c, &args, errors.New("bang")) + err := s.api.CreateSubnet( + names.NewSubnetTag(cidr), + names.NewSpaceTag(space), + zones, + isPublic, + ) + c.Check(s.called, gc.Equals, 1) + c.Assert(err, gc.ErrorMatches, "bang") +} + +func (s *SubnetsSuite) TestListSubnetsNoResults(c *gc.C) { + space := names.NewSpaceTag("foo") + zone := "bar" + args := makeListSubnetsArgs(&space, zone) + s.prepareAPICall(c, &args, nil) + results, err := s.api.ListSubnets(&space, zone) + c.Assert(s.called, gc.Equals, 1) + c.Assert(err, jc.ErrorIsNil) + + var expectedResults []params.Subnet + c.Assert(results, jc.DeepEquals, expectedResults) +} + +func (s *SubnetsSuite) TestListSubnetsFails(c *gc.C) { + space := names.NewSpaceTag("foo") + zone := "bar" + args := makeListSubnetsArgs(&space, zone) + s.prepareAPICall(c, &args, errors.New("bang")) + results, err := s.api.ListSubnets(&space, zone) + c.Assert(s.called, gc.Equals, 1) + c.Assert(err, gc.ErrorMatches, "bang") + + var expectedResults []params.Subnet + c.Assert(results, jc.DeepEquals, expectedResults) +} === added directory 'src/github.com/juju/juju/api/systemmanager' === added file 'src/github.com/juju/juju/api/systemmanager/package_test.go' --- src/github.com/juju/juju/api/systemmanager/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/systemmanager/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,14 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package systemmanager_test + +import ( + stdtesting "testing" + + "github.com/juju/juju/testing" +) + +func TestAll(t *stdtesting.T) { + testing.MgoTestPackage(t) +} === added file 'src/github.com/juju/juju/api/systemmanager/systemmanager.go' --- src/github.com/juju/juju/api/systemmanager/systemmanager.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/systemmanager/systemmanager.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,99 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package systemmanager + +import ( + "github.com/juju/errors" + "github.com/juju/loggo" + "github.com/juju/names" + + "github.com/juju/juju/api" + "github.com/juju/juju/api/base" + "github.com/juju/juju/apiserver/params" +) + +var logger = loggo.GetLogger("juju.api.systemmanager") + +// Client provides methods that the Juju client command uses to interact +// with systems 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, "SystemManager") + logger.Tracef("%#v", frontend) + return &Client{ClientFacade: frontend, facade: backend} +} + +// AllEnvironments allows system administrators to get the list of all the +// environments in the system. +func (c *Client) AllEnvironments() ([]base.UserEnvironment, error) { + var environments params.UserEnvironmentList + err := c.facade.FacadeCall("AllEnvironments", nil, &environments) + if err != nil { + return nil, errors.Trace(err) + } + result := make([]base.UserEnvironment, len(environments.UserEnvironments)) + for i, env := range environments.UserEnvironments { + owner, err := names.ParseUserTag(env.OwnerTag) + if err != nil { + return nil, errors.Annotatef(err, "OwnerTag %q at position %d", env.OwnerTag, i) + } + result[i] = base.UserEnvironment{ + Name: env.Name, + UUID: env.UUID, + Owner: owner.Username(), + LastConnection: env.LastConnection, + } + } + return result, nil +} + +// EnvironmentConfig returns all environment settings for the +// system environment. +func (c *Client) EnvironmentConfig() (map[string]interface{}, error) { + result := params.EnvironmentConfigResults{} + err := c.facade.FacadeCall("EnvironmentConfig", nil, &result) + return result.Config, err +} + +// DestroySystem puts the system environment into a "dying" state, +// and removes all non-manager machine instances. Underlying DestroyEnvironment +// calls will fail if there are any manually-provisioned non-manager machines +// in state. +func (c *Client) DestroySystem(destroyEnvs bool, ignoreBlocks bool) error { + args := params.DestroySystemArgs{ + DestroyEnvironments: destroyEnvs, + IgnoreBlocks: ignoreBlocks, + } + return c.facade.FacadeCall("DestroySystem", args, nil) +} + +// ListBlockedEnvironments returns a list of all environments within the system +// which have at least one block in place. +func (c *Client) ListBlockedEnvironments() ([]params.EnvironmentBlockInfo, error) { + result := params.EnvironmentBlockInfoList{} + err := c.facade.FacadeCall("ListBlockedEnvironments", nil, &result) + return result.Environments, err +} + +// RemoveBlocks removes all the blocks in the system. +func (c *Client) RemoveBlocks() error { + args := params.RemoveBlocksArgs{All: true} + return c.facade.FacadeCall("RemoveBlocks", args, nil) +} + +// WatchAllEnv returns an AllEnvWatcher, from which you can request +// the Next collection of Deltas (for all environments). +func (c *Client) WatchAllEnvs() (*api.AllWatcher, error) { + info := new(api.WatchAll) + if err := c.facade.FacadeCall("WatchAllEnvs", nil, info); err != nil { + return nil, err + } + return api.NewAllEnvWatcher(c.facade.RawAPICaller(), &info.AllWatcherId), nil +} === added file 'src/github.com/juju/juju/api/systemmanager/systemmanager_test.go' --- src/github.com/juju/juju/api/systemmanager/systemmanager_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/api/systemmanager/systemmanager_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,154 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package systemmanager_test + +import ( + "fmt" + "time" + + "github.com/juju/names" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/api" + "github.com/juju/juju/api/systemmanager" + commontesting "github.com/juju/juju/apiserver/common/testing" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/feature" + "github.com/juju/juju/juju" + jujutesting "github.com/juju/juju/juju/testing" + "github.com/juju/juju/state" + "github.com/juju/juju/state/multiwatcher" + "github.com/juju/juju/testing" + "github.com/juju/juju/testing/factory" +) + +type systemManagerSuite struct { + jujutesting.JujuConnSuite + commontesting.BlockHelper +} + +var _ = gc.Suite(&systemManagerSuite{}) + +func (s *systemManagerSuite) SetUpTest(c *gc.C) { + s.SetInitialFeatureFlags(feature.JES) + s.JujuConnSuite.SetUpTest(c) +} + +func (s *systemManagerSuite) OpenAPI(c *gc.C) *systemmanager.Client { + conn, err := juju.NewAPIState(s.AdminUserTag(c), s.Environ, api.DialOpts{}) + c.Assert(err, jc.ErrorIsNil) + s.AddCleanup(func(*gc.C) { conn.Close() }) + return systemmanager.NewClient(conn) +} + +func (s *systemManagerSuite) TestAllEnvironments(c *gc.C) { + owner := names.NewUserTag("user@remote") + s.Factory.MakeEnvironment(c, &factory.EnvParams{ + Name: "first", Owner: owner}).Close() + s.Factory.MakeEnvironment(c, &factory.EnvParams{ + Name: "second", Owner: owner}).Close() + + sysManager := s.OpenAPI(c) + envs, err := sysManager.AllEnvironments() + c.Assert(err, jc.ErrorIsNil) + c.Assert(envs, gc.HasLen, 3) + + var obtained []string + for _, env := range envs { + obtained = append(obtained, fmt.Sprintf("%s/%s", env.Owner, env.Name)) + } + expected := []string{ + "dummy-admin@local/dummyenv", + "user@remote/first", + "user@remote/second", + } + c.Assert(obtained, jc.SameContents, expected) +} + +func (s *systemManagerSuite) TestEnvironmentConfig(c *gc.C) { + sysManager := s.OpenAPI(c) + env, err := sysManager.EnvironmentConfig() + c.Assert(err, jc.ErrorIsNil) + c.Assert(env["name"], gc.Equals, "dummyenv") +} + +func (s *systemManagerSuite) TestDestroySystem(c *gc.C) { + s.Factory.MakeEnvironment(c, &factory.EnvParams{Name: "foo"}).Close() + + sysManager := s.OpenAPI(c) + err := sysManager.DestroySystem(false, false) + c.Assert(err, gc.ErrorMatches, "state server environment cannot be destroyed before all other environments are destroyed") +} + +func (s *systemManagerSuite) TestListBlockedEnvironments(c *gc.C) { + err := s.State.SwitchBlockOn(state.ChangeBlock, "change block for state server") + err = s.State.SwitchBlockOn(state.DestroyBlock, "destroy block for state server") + c.Assert(err, jc.ErrorIsNil) + + sysManager := s.OpenAPI(c) + results, err := sysManager.ListBlockedEnvironments() + c.Assert(err, jc.ErrorIsNil) + c.Assert(results, jc.DeepEquals, []params.EnvironmentBlockInfo{ + params.EnvironmentBlockInfo{ + Name: "dummyenv", + UUID: s.State.EnvironUUID(), + OwnerTag: s.AdminUserTag(c).String(), + Blocks: []string{ + "BlockChange", + "BlockDestroy", + }, + }, + }) +} + +func (s *systemManagerSuite) TestRemoveBlocks(c *gc.C) { + s.State.SwitchBlockOn(state.DestroyBlock, "TestBlockDestroyEnvironment") + s.State.SwitchBlockOn(state.ChangeBlock, "TestChangeBlock") + + sysManager := s.OpenAPI(c) + err := sysManager.RemoveBlocks() + c.Assert(err, jc.ErrorIsNil) + + blocks, err := s.State.AllBlocksForSystem() + c.Assert(err, jc.ErrorIsNil) + c.Assert(blocks, gc.HasLen, 0) +} + +func (s *systemManagerSuite) TestWatchAllEnvs(c *gc.C) { + // The WatchAllEnvs infrastructure is comprehensively tested + // else. This test just ensure that the API calls work end-to-end. + sysManager := s.OpenAPI(c) + + w, err := sysManager.WatchAllEnvs() + c.Assert(err, jc.ErrorIsNil) + defer func() { + err := w.Stop() + c.Assert(err, jc.ErrorIsNil) + }() + + deltasC := make(chan []multiwatcher.Delta) + go func() { + deltas, err := w.Next() + c.Assert(err, jc.ErrorIsNil) + deltasC <- deltas + }() + + select { + case deltas := <-deltasC: + c.Assert(deltas, gc.HasLen, 1) + envInfo := deltas[0].Entity.(*multiwatcher.EnvironmentInfo) + + env, err := s.State.Environment() + c.Assert(err, jc.ErrorIsNil) + + c.Assert(envInfo.EnvUUID, gc.Equals, env.UUID()) + c.Assert(envInfo.Name, gc.Equals, env.Name()) + c.Assert(envInfo.Life, gc.Equals, multiwatcher.Life("alive")) + c.Assert(envInfo.Owner, gc.Equals, env.Owner().Id()) + c.Assert(envInfo.ServerUUID, gc.Equals, env.ServerUUID()) + case <-time.After(testing.LongWait): + c.Fatal("timed out") + } +} === modified file 'src/github.com/juju/juju/api/uniter/service.go' --- src/github.com/juju/juju/api/uniter/service.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/uniter/service.go 2015-10-23 18:29:32 +0000 @@ -169,7 +169,7 @@ tag := names.NewUnitTag(unitName) var result params.ErrorResults args := params.SetStatus{ - Entities: []params.EntityStatus{ + Entities: []params.EntityStatusArgs{ { Tag: tag.String(), Status: status, === modified file 'src/github.com/juju/juju/api/uniter/unit.go' --- src/github.com/juju/juju/api/uniter/unit.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/uniter/unit.go 2015-10-23 18:29:32 +0000 @@ -59,7 +59,7 @@ } var result params.ErrorResults args := params.SetStatus{ - Entities: []params.EntityStatus{ + Entities: []params.EntityStatusArgs{ {Tag: u.tag.String(), Status: status, Info: info, Data: data}, }, } @@ -99,7 +99,7 @@ func (u *Unit) SetAgentStatus(status params.Status, info string, data map[string]interface{}) error { var result params.ErrorResults args := params.SetStatus{ - Entities: []params.EntityStatus{ + Entities: []params.EntityStatusArgs{ {Tag: u.tag.String(), Status: status, Info: info, Data: data}, }, } === modified file 'src/github.com/juju/juju/api/uniter/unit_test.go' --- src/github.com/juju/juju/api/uniter/unit_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/uniter/unit_test.go 2015-10-23 18:29:32 +0000 @@ -822,7 +822,7 @@ type unitMetricBatchesSuite struct { testing.JujuConnSuite - st *api.State + st api.Connection uniter *uniter.State apiUnit *uniter.Unit charm *state.Charm === modified file 'src/github.com/juju/juju/api/uniter/uniter.go' --- src/github.com/juju/juju/api/uniter/uniter.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/uniter/uniter.go 2015-10-23 18:29:32 +0000 @@ -90,6 +90,11 @@ return st.facade.BestAPIVersion() } +// Facade returns the current facade. +func (st *State) Facade() base.FacadeCaller { + return st.facade +} + // life requests the lifecycle of the given entity from the server. func (st *State) life(tag names.Tag) (params.Life, error) { return common.Life(st.facade, tag) === modified file 'src/github.com/juju/juju/api/uniter/uniter_test.go' --- src/github.com/juju/juju/api/uniter/uniter_test.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/api/uniter/uniter_test.go 2015-10-23 18:29:32 +0000 @@ -22,7 +22,7 @@ type uniterSuite struct { testing.JujuConnSuite - st *api.State + st api.Connection stateServerMachine *state.Machine wordpressMachine *state.Machine wordpressService *state.Service === modified file 'src/github.com/juju/juju/api/upgrader/unitupgrader_test.go' --- src/github.com/juju/juju/api/upgrader/unitupgrader_test.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/api/upgrader/unitupgrader_test.go 2015-10-23 18:29:32 +0000 @@ -22,7 +22,7 @@ type unitUpgraderSuite struct { jujutesting.JujuConnSuite - stateAPI *api.State + stateAPI api.Connection // These are raw State objects. Use them for setup and assertions, but // should never be touched by the API calls themselves === modified file 'src/github.com/juju/juju/api/upgrader/upgrader_test.go' --- src/github.com/juju/juju/api/upgrader/upgrader_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/upgrader/upgrader_test.go 2015-10-23 18:29:32 +0000 @@ -29,7 +29,7 @@ type machineUpgraderSuite struct { testing.JujuConnSuite - stateAPI *api.State + stateAPI api.Connection // These are raw State objects. Use them for setup and assertions, but // should never be touched by the API calls themselves === modified file 'src/github.com/juju/juju/api/watcher/interfaces.go' --- src/github.com/juju/juju/api/watcher/interfaces.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/watcher/interfaces.go 2015-10-23 18:29:32 +0000 @@ -24,6 +24,14 @@ Err() error } +// EntityWatcher will send events when something changes. +// The content for the changes is a list of tag strings. +type EntityWatcher interface { + Changes() <-chan []string + Stop() error + Err() error +} + // RelationUnitsWatcher will send events when something changes. // The content for the changes is a params.RelationUnitsChange struct. type RelationUnitsWatcher interface { === modified file 'src/github.com/juju/juju/api/watcher/watcher.go' --- src/github.com/juju/juju/api/watcher/watcher.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/watcher/watcher.go 2015-10-23 18:29:32 +0000 @@ -360,3 +360,59 @@ func (w *machineAttachmentsWatcher) Changes() <-chan []params.MachineStorageId { return w.out } + +// EntityWatcher will send events when something changes. +// The content for the changes is a list of tag strings. +type entityWatcher struct { + commonWatcher + caller base.APICaller + entityWatcherId string + out chan []string +} + +func NewEntityWatcher(caller base.APICaller, result params.EntityWatchResult) EntityWatcher { + w := &entityWatcher{ + caller: caller, + entityWatcherId: result.EntityWatcherId, + out: make(chan []string), + } + go func() { + defer w.tomb.Done() + defer close(w.out) + w.tomb.Kill(w.loop(result.Changes)) + }() + return w +} + +func (w *entityWatcher) loop(initialChanges []string) error { + changes := initialChanges + w.newResult = func() interface{} { return new(params.EntityWatchResult) } + w.call = makeWatcherAPICaller(w.caller, "EntityWatcher", w.entityWatcherId) + w.commonWatcher.init() + go w.commonLoop() + + for { + select { + // Send the initial event or subsequent change. + case w.out <- changes: + case <-w.tomb.Dying(): + return nil + } + // Read the next change. + data, ok := <-w.in + if !ok { + // The tomb is already killed with the correct error + // at this point, so just return. + return nil + } + // Changes have been transformed at the server side already. + changes = data.(*params.EntityWatchResult).Changes + } +} + +// Changes returns a channel that receives a list of changes +// as tags (converted to strings) of the watched entities +// with changes. +func (w *entityWatcher) Changes() <-chan []string { + return w.out +} === modified file 'src/github.com/juju/juju/api/watcher/watcher_test.go' --- src/github.com/juju/juju/api/watcher/watcher_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/api/watcher/watcher_test.go 2015-10-23 18:29:32 +0000 @@ -30,7 +30,7 @@ type watcherSuite struct { testing.JujuConnSuite - stateAPI *api.State + stateAPI api.Connection // These are raw State objects. Use them for setup and assertions, but // should never be touched by the API calls themselves === added directory 'src/github.com/juju/juju/apiserver/addresser' === added file 'src/github.com/juju/juju/apiserver/addresser/addresser.go' --- src/github.com/juju/juju/apiserver/addresser/addresser.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/addresser/addresser.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,171 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package addresser + +import ( + "github.com/juju/errors" + "github.com/juju/loggo" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/environs" + "github.com/juju/juju/instance" + "github.com/juju/juju/network" + "github.com/juju/juju/state" + "github.com/juju/juju/state/watcher" +) + +func init() { + common.RegisterStandardFacade("Addresser", 1, NewAddresserAPI) +} + +var logger = loggo.GetLogger("juju.apiserver.addresser") + +// AddresserAPI provides access to the Addresser API facade. +type AddresserAPI struct { + st StateInterface + resources *common.Resources + authorizer common.Authorizer +} + +// NewAddresserAPI creates a new server-side Addresser API facade. +func NewAddresserAPI( + st *state.State, + resources *common.Resources, + authorizer common.Authorizer, +) (*AddresserAPI, error) { + isEnvironManager := authorizer.AuthEnvironManager() + if !isEnvironManager { + // Addresser must run as environment manager. + return nil, common.ErrPerm + } + sti := getState(st) + return &AddresserAPI{ + st: sti, + resources: resources, + authorizer: authorizer, + }, nil +} + +// getNetworkingEnviron checks if the environment implements NetworkingEnviron +// and also if it supports IP address allocation. +func (api *AddresserAPI) getNetworkingEnviron() (environs.NetworkingEnviron, bool, error) { + config, err := api.st.EnvironConfig() + if err != nil { + return nil, false, errors.Annotate(err, "getting environment config") + } + env, err := environs.New(config) + if err != nil { + return nil, false, errors.Annotate(err, "validating environment config") + } + netEnv, ok := environs.SupportsNetworking(env) + if !ok { + return nil, false, nil + } + ok, err = netEnv.SupportsAddressAllocation(network.AnySubnet) + if err != nil && !errors.IsNotSupported(err) { + return nil, false, errors.Annotate(err, "checking allocation support") + } + return netEnv, ok, nil +} + +// CanDeallocateAddresses checks if the current environment can +// deallocate IP addresses. +func (api *AddresserAPI) CanDeallocateAddresses() params.BoolResult { + result := params.BoolResult{} + _, ok, err := api.getNetworkingEnviron() + if err != nil { + result.Error = common.ServerError(err) + return result + } + result.Result = ok + return result +} + +// CleanupIPAddresses releases and removes the dead IP addresses. +func (api *AddresserAPI) CleanupIPAddresses() params.ErrorResult { + result := params.ErrorResult{} + netEnv, ok, err := api.getNetworkingEnviron() + if err != nil { + result.Error = common.ServerError(err) + return result + } + if !ok { + result.Error = common.ServerError(errors.NotSupportedf("IP address deallocation")) + return result + } + // Retrieve dead addresses, release and remove them. + logger.Debugf("retrieving dead IP addresses") + ipAddresses, err := api.st.DeadIPAddresses() + if err != nil { + err = errors.Annotate(err, "getting dead addresses") + result.Error = common.ServerError(err) + return result + } + canRetry := false + for _, ipAddress := range ipAddresses { + ipAddressValue := ipAddress.Value() + logger.Debugf("releasing dead IP address %q", ipAddressValue) + err := api.releaseIPAddress(netEnv, ipAddress) + if err != nil { + logger.Warningf("cannot release IP address %q: %v (will retry)", ipAddressValue, err) + canRetry = true + continue + } + logger.Debugf("removing released IP address %q", ipAddressValue) + err = ipAddress.Remove() + if errors.IsNotFound(err) { + continue + } + if err != nil { + logger.Warningf("failed to remove released IP address %q: %v (will retry)", ipAddressValue, err) + canRetry = true + continue + } + } + if canRetry { + result.Error = common.ServerError(common.ErrTryAgain) + } + return result +} + +// netEnvReleaseAddress is used for testability. +var netEnvReleaseAddress = func(env environs.NetworkingEnviron, + instId instance.Id, subnetId network.Id, addr network.Address, macAddress string) error { + return env.ReleaseAddress(instId, subnetId, addr, macAddress) +} + +// releaseIPAddress releases one IP address. +func (api *AddresserAPI) releaseIPAddress(netEnv environs.NetworkingEnviron, ipAddress StateIPAddress) (err error) { + defer errors.DeferredAnnotatef(&err, "failed to release IP address %q", ipAddress.Value()) + logger.Tracef("attempting to release dead IP address %q", ipAddress.Value()) + // Final check if IP address is really dead. + if ipAddress.Life() != state.Dead { + return errors.New("IP address not dead") + } + // Now release the IP address. + subnetId := network.Id(ipAddress.SubnetId()) + err = netEnvReleaseAddress(netEnv, ipAddress.InstanceId(), subnetId, ipAddress.Address(), ipAddress.MACAddress()) + if err != nil { + return errors.Trace(err) + } + return nil +} + +// WatchIPAddresses observes changes to the IP addresses. +func (api *AddresserAPI) WatchIPAddresses() (params.EntityWatchResult, error) { + watch := &ipAddressesWatcher{api.st.WatchIPAddresses(), api.st} + + if changes, ok := <-watch.Changes(); ok { + mappedChanges, err := watch.MapChanges(changes) + if err != nil { + return params.EntityWatchResult{}, errors.Trace(err) + } + return params.EntityWatchResult{ + EntityWatcherId: api.resources.Register(watch), + Changes: mappedChanges, + }, nil + } + return params.EntityWatchResult{}, watcher.EnsureErr(watch) +} === added file 'src/github.com/juju/juju/apiserver/addresser/addresser_test.go' --- src/github.com/juju/juju/apiserver/addresser/addresser_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/addresser/addresser_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,295 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package addresser_test + +import ( + "github.com/juju/errors" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/addresser" + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/params" + apiservertesting "github.com/juju/juju/apiserver/testing" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/environs" + "github.com/juju/juju/environs/config" + "github.com/juju/juju/environs/configstore" + "github.com/juju/juju/feature" + "github.com/juju/juju/instance" + "github.com/juju/juju/network" + "github.com/juju/juju/provider/dummy" + "github.com/juju/juju/state" + statetesting "github.com/juju/juju/state/testing" + coretesting "github.com/juju/juju/testing" +) + +type AddresserSuite struct { + coretesting.BaseSuite + + st *mockState + api *addresser.AddresserAPI + authoriser apiservertesting.FakeAuthorizer + resources *common.Resources +} + +var _ = gc.Suite(&AddresserSuite{}) + +func (s *AddresserSuite) SetUpSuite(c *gc.C) { + s.BaseSuite.SetUpSuite(c) + environs.RegisterProvider("mock", mockEnvironProvider{}) +} + +func (s *AddresserSuite) SetUpTest(c *gc.C) { + s.BaseSuite.SetUpTest(c) + s.SetFeatureFlags(feature.AddressAllocation) + + s.authoriser = apiservertesting.FakeAuthorizer{ + EnvironManager: true, + } + s.resources = common.NewResources() + s.AddCleanup(func(*gc.C) { s.resources.StopAll() }) + + s.st = newMockState() + addresser.PatchState(s, s.st) + + var err error + s.api, err = addresser.NewAddresserAPI(nil, s.resources, s.authoriser) + c.Assert(err, jc.ErrorIsNil) +} + +func (s *AddresserSuite) TearDownTest(c *gc.C) { + dummy.Reset() + s.BaseSuite.TearDownTest(c) +} + +func (s *AddresserSuite) TestCanDeallocateAddressesEnabled(c *gc.C) { + config := testingEnvConfig(c) + s.st.setConfig(c, config) + + result := s.api.CanDeallocateAddresses() + c.Assert(result, jc.DeepEquals, params.BoolResult{ + Error: nil, + Result: true, + }) +} + +func (s *AddresserSuite) TestCanDeallocateAddressesDisabled(c *gc.C) { + config := testingEnvConfig(c) + s.st.setConfig(c, config) + s.SetFeatureFlags() + + result := s.api.CanDeallocateAddresses() + c.Assert(result, jc.DeepEquals, params.BoolResult{ + Error: nil, + Result: false, + }) +} + +func (s *AddresserSuite) TestCanDeallocateAddressesConfigGetFailure(c *gc.C) { + config := testingEnvConfig(c) + s.st.setConfig(c, config) + + s.st.stub.SetErrors(errors.New("ouch")) + + result := s.api.CanDeallocateAddresses() + c.Assert(result.Error, gc.ErrorMatches, "getting environment config: ouch") + c.Assert(result.Result, jc.IsFalse) +} + +func (s *AddresserSuite) TestCanDeallocateAddressesEnvironmentNewFailure(c *gc.C) { + config := nonexTestingEnvConfig(c) + s.st.setConfig(c, config) + + result := s.api.CanDeallocateAddresses() + c.Assert(result.Error, gc.ErrorMatches, `validating environment config: no registered provider for "nonex"`) + c.Assert(result.Result, jc.IsFalse) +} + +func (s *AddresserSuite) TestCanDeallocateAddressesNotSupportedFailure(c *gc.C) { + config := mockTestingEnvConfig(c) + s.st.setConfig(c, config) + + result := s.api.CanDeallocateAddresses() + c.Assert(result, jc.DeepEquals, params.BoolResult{ + Error: nil, + Result: false, + }) +} + +func (s *AddresserSuite) TestCleanupIPAddressesSuccess(c *gc.C) { + config := testingEnvConfig(c) + s.st.setConfig(c, config) + + dead, err := s.st.DeadIPAddresses() + c.Assert(err, jc.ErrorIsNil) + c.Assert(dead, gc.HasLen, 2) + + apiErr := s.api.CleanupIPAddresses() + c.Assert(apiErr, jc.DeepEquals, params.ErrorResult{}) + + dead, err = s.st.DeadIPAddresses() + c.Assert(err, jc.ErrorIsNil) + c.Assert(dead, gc.HasLen, 0) +} + +func (s *AddresserSuite) TestReleaseAddress(c *gc.C) { + config := testingEnvConfig(c) + s.st.setConfig(c, config) + + // Cleanup initial dead IP addresses. + dead, err := s.st.DeadIPAddresses() + c.Assert(err, jc.ErrorIsNil) + c.Assert(dead, gc.HasLen, 2) + + apiErr := s.api.CleanupIPAddresses() + c.Assert(apiErr, jc.DeepEquals, params.ErrorResult{}) + + dead, err = s.st.DeadIPAddresses() + c.Assert(err, jc.ErrorIsNil) + c.Assert(dead, gc.HasLen, 0) + + // Prepare tests. + called := 0 + s.PatchValue(addresser.NetEnvReleaseAddress, func(env environs.NetworkingEnviron, + instId instance.Id, subnetId network.Id, addr network.Address, macAddress string) error { + called++ + c.Assert(instId, gc.Equals, instance.Id("a3")) + c.Assert(subnetId, gc.Equals, network.Id("a")) + c.Assert(addr, gc.Equals, network.NewAddress("0.1.2.3")) + c.Assert(macAddress, gc.Equals, "fff3") + return nil + }) + + // Set address 0.1.2.3 to dead. + s.st.setDead(c, "0.1.2.3") + + dead, err = s.st.DeadIPAddresses() + c.Assert(err, jc.ErrorIsNil) + c.Assert(dead, gc.HasLen, 1) + + apiErr = s.api.CleanupIPAddresses() + c.Assert(apiErr, jc.DeepEquals, params.ErrorResult{}) + c.Assert(called, gc.Equals, 1) + + dead, err = s.st.DeadIPAddresses() + c.Assert(err, jc.ErrorIsNil) + c.Assert(dead, gc.HasLen, 0) +} + +func (s *AddresserSuite) TestCleanupIPAddressesConfigGetFailure(c *gc.C) { + config := testingEnvConfig(c) + s.st.setConfig(c, config) + + dead, err := s.st.DeadIPAddresses() + c.Assert(err, jc.ErrorIsNil) + c.Assert(dead, gc.HasLen, 2) + + s.st.stub.SetErrors(errors.New("ouch")) + + // First action is getting the environment configuration, + // so the injected error is returned here. + apiErr := s.api.CleanupIPAddresses() + c.Assert(apiErr.Error, gc.ErrorMatches, "getting environment config: ouch") + + // Still has two dead addresses. + dead, err = s.st.DeadIPAddresses() + c.Assert(err, jc.ErrorIsNil) + c.Assert(dead, gc.HasLen, 2) +} + +func (s *AddresserSuite) TestCleanupIPAddressesEnvironmentNewFailure(c *gc.C) { + config := nonexTestingEnvConfig(c) + s.st.setConfig(c, config) + + dead, err := s.st.DeadIPAddresses() + c.Assert(err, jc.ErrorIsNil) + c.Assert(dead, gc.HasLen, 2) + + // Validation of configuration fails due to illegal provider. + apiErr := s.api.CleanupIPAddresses() + c.Assert(apiErr.Error, gc.ErrorMatches, `validating environment config: no registered provider for "nonex"`) + + // Still has two dead addresses. + dead, err = s.st.DeadIPAddresses() + c.Assert(err, jc.ErrorIsNil) + c.Assert(dead, gc.HasLen, 2) +} + +func (s *AddresserSuite) TestCleanupIPAddressesNotSupportedFailure(c *gc.C) { + config := mockTestingEnvConfig(c) + s.st.setConfig(c, config) + + dead, err := s.st.DeadIPAddresses() + c.Assert(err, jc.ErrorIsNil) + c.Assert(dead, gc.HasLen, 2) + + // The tideland environment does not support networking. + apiErr := s.api.CleanupIPAddresses() + c.Assert(apiErr.Error, gc.ErrorMatches, "IP address deallocation not supported") + + // Still has two dead addresses. + dead, err = s.st.DeadIPAddresses() + c.Assert(err, jc.ErrorIsNil) + c.Assert(dead, gc.HasLen, 2) +} + +func (s *AddresserSuite) TestWatchIPAddresses(c *gc.C) { + c.Assert(s.resources.Count(), gc.Equals, 0) + + s.st.addIPAddressWatcher("0.1.2.3", "0.1.2.4", "0.1.2.7") + + result, err := s.api.WatchIPAddresses() + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, gc.DeepEquals, params.EntityWatchResult{ + EntityWatcherId: "1", + Changes: []string{ + "ipaddress-00000000-1111-2222-3333-0123456789ab", + "ipaddress-00000000-1111-2222-4444-0123456789ab", + "ipaddress-00000000-1111-2222-7777-0123456789ab", + }, + Error: nil, + }) + + // Verify the resource was registered and stop when done. + c.Assert(s.resources.Count(), gc.Equals, 1) + resource := s.resources.Get("1") + defer statetesting.AssertStop(c, resource) + + // Check that the Watch has consumed the initial event ("returned" in + // the Watch call) + wc := statetesting.NewStringsWatcherC(c, s.st, resource.(state.StringsWatcher)) + wc.AssertNoChange() +} + +// testingEnvConfig prepares an environment configuration using +// the dummy provider. +func testingEnvConfig(c *gc.C) *config.Config { + cfg, err := config.New(config.NoDefaults, dummy.SampleConfig()) + c.Assert(err, jc.ErrorIsNil) + env, err := environs.Prepare(cfg, envcmd.BootstrapContext(coretesting.Context(c)), configstore.NewMem()) + c.Assert(err, jc.ErrorIsNil) + return env.Config() +} + +// nonexTestingEnvConfig prepares an environment configuration using +// a non-existent provider. +func nonexTestingEnvConfig(c *gc.C) *config.Config { + attrs := dummy.SampleConfig().Merge(coretesting.Attrs{ + "type": "nonex", + }) + cfg, err := config.New(config.NoDefaults, attrs) + c.Assert(err, jc.ErrorIsNil) + return cfg +} + +// mockTestingEnvConfig prepares an environment configuration using +// the mock provider which does not support networking. +func mockTestingEnvConfig(c *gc.C) *config.Config { + cfg, err := config.New(config.NoDefaults, mockConfig()) + c.Assert(err, jc.ErrorIsNil) + env, err := environs.Prepare(cfg, envcmd.BootstrapContext(coretesting.Context(c)), configstore.NewMem()) + c.Assert(err, jc.ErrorIsNil) + return env.Config() +} === added file 'src/github.com/juju/juju/apiserver/addresser/export_test.go' --- src/github.com/juju/juju/apiserver/addresser/export_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/addresser/export_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,20 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package addresser + +import ( + "github.com/juju/juju/state" +) + +var NetEnvReleaseAddress = &netEnvReleaseAddress + +type Patcher interface { + PatchValue(ptr, value interface{}) +} + +func PatchState(p Patcher, st StateInterface) { + p.PatchValue(&getState, func(*state.State) StateInterface { + return st + }) +} === added file 'src/github.com/juju/juju/apiserver/addresser/mock_test.go' --- src/github.com/juju/juju/apiserver/addresser/mock_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/addresser/mock_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,400 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package addresser_test + +import ( + "sort" + "sync" + + gc "gopkg.in/check.v1" + + "github.com/juju/errors" + "github.com/juju/names" + "github.com/juju/testing" + jujutxn "github.com/juju/txn" + + "github.com/juju/juju/apiserver/addresser" + "github.com/juju/juju/environs" + "github.com/juju/juju/environs/config" + "github.com/juju/juju/instance" + "github.com/juju/juju/network" + "github.com/juju/juju/provider/dummy" + "github.com/juju/juju/state" + coretesting "github.com/juju/juju/testing" +) + +// mockState implements StateInterface and allows inspection of called +// methods. +type mockState struct { + mu sync.Mutex + stub *testing.Stub + + config *config.Config + ipAddresses map[string]*mockIPAddress + + ipAddressWatchers []*mockIPAddressWatcher +} + +func newMockState() *mockState { + mst := &mockState{ + stub: &testing.Stub{}, + ipAddresses: make(map[string]*mockIPAddress), + } + mst.setUpState() + return mst +} + +func (mst *mockState) setUpState() { + mst.mu.Lock() + defer mst.mu.Unlock() + + ips := []struct { + value string + uuid string + life state.Life + subnetId string + instanceId string + macaddr string + }{ + {"0.1.2.3", "00000000-1111-2222-3333-0123456789ab", state.Alive, "a", "a3", "fff3"}, + {"0.1.2.4", "00000000-1111-2222-4444-0123456789ab", state.Alive, "b", "b4", "fff4"}, + {"0.1.2.5", "00000000-1111-2222-5555-0123456789ab", state.Alive, "b", "b5", "fff5"}, + {"0.1.2.6", "00000000-1111-2222-6666-0123456789ab", state.Dead, "c", "c6", "fff6"}, + {"0.1.2.7", "00000000-1111-2222-7777-0123456789ab", state.Dead, "c", "c7", "fff7"}, + } + for _, ip := range ips { + mst.ipAddresses[ip.value] = &mockIPAddress{ + stub: mst.stub, + st: mst, + value: ip.value, + tag: names.NewIPAddressTag(ip.uuid), + life: ip.life, + subnetId: ip.subnetId, + instanceId: instance.Id(ip.instanceId), + addr: network.NewAddress(ip.value), + macaddr: ip.macaddr, + } + } +} + +var _ addresser.StateInterface = (*mockState)(nil) + +// EnvironConfig implements StateInterface. +func (mst *mockState) EnvironConfig() (*config.Config, error) { + mst.mu.Lock() + defer mst.mu.Unlock() + + mst.stub.MethodCall(mst, "EnvironConfig") + + if err := mst.stub.NextErr(); err != nil { + return nil, err + } + return mst.config, nil +} + +// setConfig updates the environ config stored internally. Triggers a +// change event for all created config watchers. +func (mst *mockState) setConfig(c *gc.C, newConfig *config.Config) { + mst.mu.Lock() + defer mst.mu.Unlock() + + mst.config = newConfig +} + +// IPAddress implements StateInterface. +func (mst *mockState) IPAddress(value string) (addresser.StateIPAddress, error) { + mst.mu.Lock() + defer mst.mu.Unlock() + + mst.stub.MethodCall(mst, "IPAddress", value) + if err := mst.stub.NextErr(); err != nil { + return nil, err + } + ipAddress, found := mst.ipAddresses[value] + if !found { + return nil, errors.NotFoundf("IP address %s", value) + } + return ipAddress, nil +} + +// setDead sets a mock IP address in state to dead. +func (mst *mockState) setDead(c *gc.C, value string) { + mst.mu.Lock() + defer mst.mu.Unlock() + + ipAddress, found := mst.ipAddresses[value] + c.Assert(found, gc.Equals, true) + + ipAddress.life = state.Dead +} + +// DeadIPAddresses implements StateInterface. +func (mst *mockState) DeadIPAddresses() ([]addresser.StateIPAddress, error) { + mst.mu.Lock() + defer mst.mu.Unlock() + + mst.stub.MethodCall(mst, "DeadIPAddresses") + if err := mst.stub.NextErr(); err != nil { + return nil, err + } + var deadIPAddresses []addresser.StateIPAddress + for _, ipAddress := range mst.ipAddresses { + if ipAddress.life == state.Dead { + deadIPAddresses = append(deadIPAddresses, ipAddress) + } + } + return deadIPAddresses, nil +} + +// WatchIPAddresses implements StateInterface. +func (mst *mockState) WatchIPAddresses() state.StringsWatcher { + mst.mu.Lock() + defer mst.mu.Unlock() + + mst.stub.MethodCall(mst, "WatchIPAddresses") + mst.stub.NextErr() + return mst.nextIPAddressWatcher() +} + +// addIPAddressWatcher adds an IP address watcher with the given IDs. +func (mst *mockState) addIPAddressWatcher(ids ...string) *mockIPAddressWatcher { + w := newMockIPAddressWatcher(ids) + mst.ipAddressWatchers = append(mst.ipAddressWatchers, w) + return w +} + +// nextIPAddressWatcher returns an IP address watcher . +func (mst *mockState) nextIPAddressWatcher() *mockIPAddressWatcher { + if len(mst.ipAddressWatchers) == 0 { + panic("ran out of watchers") + } + + w := mst.ipAddressWatchers[0] + mst.ipAddressWatchers = mst.ipAddressWatchers[1:] + if len(w.initial) == 0 { + ids := make([]string, 0, len(mst.ipAddresses)) + // Initial event - all IP address ids, sorted. + for id := range mst.ipAddresses { + ids = append(ids, id) + } + sort.Strings(ids) + w.initial = ids + } + w.start() + return w +} + +// removeIPAddress is used by mockIPAddress.Remove() +func (mst *mockState) removeIPAddress(value string) error { + mst.mu.Lock() + defer mst.mu.Unlock() + + ipAddr, ok := mst.ipAddresses[value] + if !ok { + return jujutxn.ErrNoOperations + } + if ipAddr.life != state.Dead { + return errors.Errorf("cannot remove IP address %q: IP address is not dead", ipAddr.value) + } + delete(mst.ipAddresses, value) + return nil +} + +// StartSync implements statetesting.SyncStarter, so mockState can be +// used with watcher helpers/checkers. +func (mst *mockState) StartSync() {} + +// mockIPAddress implements StateIPAddress for testing. +type mockIPAddress struct { + addresser.StateIPAddress + + stub *testing.Stub + st *mockState + value string + tag names.Tag + life state.Life + subnetId string + instanceId instance.Id + addr network.Address + macaddr string +} + +var _ addresser.StateIPAddress = (*mockIPAddress)(nil) + +// Value implements StateIPAddress. +func (mip *mockIPAddress) Value() string { + mip.stub.MethodCall(mip, "Value") + mip.stub.NextErr() // Consume the unused error. + return mip.value +} + +// Tag implements StateIPAddress. +func (mip *mockIPAddress) Tag() names.Tag { + mip.stub.MethodCall(mip, "Tag") + mip.stub.NextErr() // Consume the unused error. + return mip.tag +} + +// Life implements StateIPAddress. +func (mip *mockIPAddress) Life() state.Life { + mip.stub.MethodCall(mip, "Life") + mip.stub.NextErr() // Consume the unused error. + return mip.life +} + +// Remove implements StateIPAddress. +func (mip *mockIPAddress) Remove() error { + mip.stub.MethodCall(mip, "Remove") + if err := mip.stub.NextErr(); err != nil { + return err + } + return mip.st.removeIPAddress(mip.value) +} + +// SubnetId implements StateIPAddress. +func (mip *mockIPAddress) SubnetId() string { + mip.stub.MethodCall(mip, "SubnetId") + mip.stub.NextErr() // Consume the unused error. + return mip.subnetId +} + +// InstanceId implements StateIPAddress. +func (mip *mockIPAddress) InstanceId() instance.Id { + mip.stub.MethodCall(mip, "InstanceId") + mip.stub.NextErr() // Consume the unused error. + return mip.instanceId +} + +// Address implements StateIPAddress. +func (mip *mockIPAddress) Address() network.Address { + mip.stub.MethodCall(mip, "Address") + mip.stub.NextErr() // Consume the unused error. + return mip.addr +} + +// MACAddress implements StateIPAddress. +func (mip *mockIPAddress) MACAddress() string { + mip.stub.MethodCall(mip, "MACAddress") + mip.stub.NextErr() // Consume the unused error. + return mip.macaddr +} + +// mockIPAddressWatcher notifies about IP address changes. +type mockIPAddressWatcher struct { + err error + initial []string + wontStart bool + incoming chan []string + changes chan []string + done chan struct{} +} + +var _ state.StringsWatcher = (*mockIPAddressWatcher)(nil) + +func newMockIPAddressWatcher(initial []string) *mockIPAddressWatcher { + mipw := &mockIPAddressWatcher{ + initial: initial, + wontStart: false, + incoming: make(chan []string), + changes: make(chan []string), + done: make(chan struct{}), + } + return mipw +} + +// Kill implements state.Watcher. +func (mipw *mockIPAddressWatcher) Kill() {} + +// Stop implements state.Watcher. +func (mipw *mockIPAddressWatcher) Stop() error { + select { + case <-mipw.done: + // Closed. + default: + // Signal the loop we want to stop. + close(mipw.done) + // Signal the clients we've closed. + close(mipw.changes) + } + return mipw.err +} + +// Wait implements state.Watcher. +func (mipw *mockIPAddressWatcher) Wait() error { + return mipw.Stop() +} + +// Err implements state.Watcher. +func (mipw *mockIPAddressWatcher) Err() error { + return mipw.err +} + +// start starts the backend loop depending on a field setting. +func (mipw *mockIPAddressWatcher) start() { + if mipw.wontStart { + // Set manually by tests that need it. + mipw.Stop() + return + } + go mipw.loop() +} + +func (mipw *mockIPAddressWatcher) loop() { + // Prepare initial event. + unsent := mipw.initial + outChanges := mipw.changes + // Forward any incoming changes until stopped. + for { + select { + case <-mipw.done: + return + case outChanges <- unsent: + outChanges = nil + unsent = nil + case ids := <-mipw.incoming: + unsent = append(unsent, ids...) + outChanges = mipw.changes + } + } +} + +// Changes implements state.StringsWatcher. +func (mipw *mockIPAddressWatcher) Changes() <-chan []string { + return mipw.changes +} + +// mockConfig returns a configuration for the usage of the +// mock provider below. +func mockConfig() coretesting.Attrs { + return dummy.SampleConfig().Merge(coretesting.Attrs{ + "type": "mock", + }) +} + +// mockEnviron is an environment without networking support. +type mockEnviron struct { + environs.Environ +} + +func (e mockEnviron) Config() *config.Config { + cfg, err := config.New(config.NoDefaults, mockConfig()) + if err != nil { + panic("invalid configuration for testing") + } + return cfg +} + +// mockEnvironProvider is the smallest possible provider to +// test the addresses without networking support. +type mockEnvironProvider struct { + environs.EnvironProvider +} + +func (p mockEnvironProvider) PrepareForBootstrap(ctx environs.BootstrapContext, cfg *config.Config) (environs.Environ, error) { + return &mockEnviron{}, nil +} + +func (p mockEnvironProvider) Open(*config.Config) (environs.Environ, error) { + return &mockEnviron{}, nil +} === added file 'src/github.com/juju/juju/apiserver/addresser/package_test.go' --- src/github.com/juju/juju/apiserver/addresser/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/addresser/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,14 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package addresser_test + +import ( + "testing" + + gc "gopkg.in/check.v1" +) + +func TestPackage(t *testing.T) { + gc.TestingT(t) +} === added file 'src/github.com/juju/juju/apiserver/addresser/state.go' --- src/github.com/juju/juju/apiserver/addresser/state.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/addresser/state.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,69 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package addresser + +import ( + "github.com/juju/juju/environs/config" + "github.com/juju/juju/instance" + "github.com/juju/juju/network" + "github.com/juju/juju/state" +) + +// StateIPAddress defines the needed methods of state.IPAddress +// for the work of the Addresser API. +type StateIPAddress interface { + state.Entity + state.EnsureDeader + state.Remover + + Value() string + Life() state.Life + SubnetId() string + InstanceId() instance.Id + Address() network.Address + MACAddress() string +} + +// StateInterface defines the needed methods of state.State +// for the work of the Addresser API. +type StateInterface interface { + // EnvironConfig retrieves the environment configuration. + EnvironConfig() (*config.Config, error) + + // DeadIPAddresses retrieves all dead IP addresses. + DeadIPAddresses() ([]StateIPAddress, error) + + // IPAddress retrieves an IP address by its value. + IPAddress(value string) (StateIPAddress, error) + + // WatchIPAddresses notifies about lifecycle changes + // of IP addresses. + WatchIPAddresses() state.StringsWatcher +} + +type stateShim struct { + *state.State +} + +func (s stateShim) DeadIPAddresses() ([]StateIPAddress, error) { + ipAddresses, err := s.State.DeadIPAddresses() + if err != nil { + return nil, err + } + // Convert []*state.IPAddress into []StateIPAddress. Direct + // casts of complete slices are not possible. + stateIPAddresses := make([]StateIPAddress, len(ipAddresses)) + for i, ipAddress := range ipAddresses { + stateIPAddresses[i] = StateIPAddress(ipAddress) + } + return stateIPAddresses, nil +} + +func (s stateShim) IPAddress(value string) (StateIPAddress, error) { + return s.State.IPAddress(value) +} + +var getState = func(st *state.State) StateInterface { + return stateShim{st} +} === added file 'src/github.com/juju/juju/apiserver/addresser/watcher.go' --- src/github.com/juju/juju/apiserver/addresser/watcher.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/addresser/watcher.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,34 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package addresser + +import ( + "github.com/juju/errors" + + "github.com/juju/juju/state" +) + +// ipAddressesWatcher implements an entity watcher with the transformation +// of the received document ids of IP addresses into their according tags. +type ipAddressesWatcher struct { + state.StringsWatcher + + st StateInterface +} + +// MapChanges converts IP address values to tags. +func (w *ipAddressesWatcher) MapChanges(in []string) ([]string, error) { + if len(in) == 0 { + return in, nil + } + mapped := make([]string, len(in)) + for i, v := range in { + ipAddr, err := w.st.IPAddress(v) + if err != nil { + return nil, errors.Annotate(err, "cannot fetch address") + } + mapped[i] = ipAddr.Tag().String() + } + return mapped, nil +} === modified file 'src/github.com/juju/juju/apiserver/admin.go' --- src/github.com/juju/juju/apiserver/admin.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/admin.go 2015-10-23 18:29:32 +0000 @@ -49,7 +49,7 @@ } // authedApi is the API method finder we'll use after getting logged in. - var authedApi rpc.MethodFinder = newApiRoot(a.root.state, a.root.closeState, a.root.resources, a.root) + var authedApi rpc.MethodFinder = newApiRoot(a.root.state, a.root.resources, a.root) // Use the login validation function, if one was specified. if a.srv.validator != nil { @@ -267,7 +267,10 @@ // update the last connection times for the environment users there. var lastLogin *time.Time if user, ok := entity.(*state.User); ok { - lastLogin = user.LastLogin() + userLastLogin, err := user.LastLogin() + if err != nil && !state.IsNeverLoggedInError(err) { + return nil, nil, errors.Trace(err) + } if lookForEnvUser { envUser, err := st.EnvironmentUser(user.UserTag()) if err != nil { @@ -275,7 +278,10 @@ } // The last connection for the environment takes precedence over // the local user last login time. - lastLogin = envUser.LastConnection() + userLastLogin, err = envUser.LastConnection() + if err != nil && !state.IsNeverConnectedError(err) { + return nil, nil, errors.Trace(err) + } envUser.UpdateLastConnection() } // Only update the user's last login time if it is a successful @@ -283,6 +289,7 @@ // sure that there is an environment user in that environment for // this user. user.UpdateLastLogin() + lastLogin = &userLastLogin } return entity, lastLogin, nil @@ -336,11 +343,7 @@ } } pingTimeout := newPingTimeout(action, maxClientPingInterval) - err = root.getResources().RegisterNamed("pingTimeout", pingTimeout) - if err != nil { - return err - } - return nil + return root.getResources().RegisterNamed("pingTimeout", pingTimeout) } // errRoot implements the API that a client first sees === modified file 'src/github.com/juju/juju/apiserver/admin_test.go' --- src/github.com/juju/juju/apiserver/admin_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/admin_test.go 2015-10-23 18:29:32 +0000 @@ -89,11 +89,11 @@ }, }) -func (s *baseLoginSuite) setupServer(c *gc.C) (*api.State, func()) { +func (s *baseLoginSuite) setupServer(c *gc.C) (api.Connection, func()) { return s.setupServerForEnvironment(c, s.State.EnvironTag()) } -func (s *baseLoginSuite) setupServerForEnvironment(c *gc.C, envTag names.EnvironTag) (*api.State, func()) { +func (s *baseLoginSuite) setupServerForEnvironment(c *gc.C, envTag names.EnvironTag) (api.Connection, func()) { info, cleanup := s.setupServerForEnvironmentWithValidator(c, envTag, nil) st, err := api.Open(info, fastDialOpts) c.Assert(err, jc.ErrorIsNil) @@ -262,7 +262,7 @@ } err = s.State.SetAPIHostPorts(stateAPIHostPorts) c.Assert(err, jc.ErrorIsNil) - connectedAddr, hostPorts = s.loginHostPorts(c, info) + _, hostPorts = s.loginHostPorts(c, info) // Now that we connected, we add the other stateAPIHostPorts. However, // the one we connected to comes first. stateAPIHostPorts = append(connectedAddrHostPorts, stateAPIHostPorts...) @@ -568,7 +568,7 @@ validator := func(params.LoginRequest) error { return nil } - checker := func(c *gc.C, loginErr error, st *api.State) { + checker := func(c *gc.C, loginErr error, st api.Connection) { c.Assert(loginErr, gc.IsNil) // Ensure an API call that would be restricted during @@ -583,7 +583,7 @@ validator := func(params.LoginRequest) error { return errors.New("Login not allowed") } - checker := func(c *gc.C, loginErr error, _ *api.State) { + checker := func(c *gc.C, loginErr error, _ api.Connection) { // error is wrapped in API server c.Assert(loginErr, gc.ErrorMatches, "Login not allowed") } @@ -594,10 +594,10 @@ validator := func(params.LoginRequest) error { return apiserver.UpgradeInProgressError } - checker := func(c *gc.C, loginErr error, st *api.State) { + checker := func(c *gc.C, loginErr error, st api.Connection) { c.Assert(loginErr, gc.IsNil) - var statusResult api.Status + var statusResult params.FullStatus err := st.APICall("Client", 0, "", "FullStatus", params.StatusParams{}, &statusResult) c.Assert(err, jc.ErrorIsNil) @@ -624,7 +624,7 @@ checkLogin(names.NewMachineTag("99999")) } -type validationChecker func(c *gc.C, err error, st *api.State) +type validationChecker func(c *gc.C, err error, st api.Connection) func (s *baseLoginSuite) checkLoginWithValidator(c *gc.C, validator apiserver.LoginValidator, checker validationChecker) { info, cleanup := s.setupServerWithValidator(c, validator) @@ -651,7 +651,7 @@ } func (s *baseLoginSuite) setupServerForEnvironmentWithValidator(c *gc.C, envTag names.EnvironTag, validator apiserver.LoginValidator) (*api.Info, func()) { - listener, err := net.Listen("tcp", ":0") + listener, err := net.Listen("tcp", "127.0.0.1:0") c.Assert(err, jc.ErrorIsNil) srv, err := apiserver.NewServer( s.State, @@ -673,7 +673,7 @@ Tag: nil, Password: "", EnvironTag: envTag, - Addrs: []string{srv.Addr()}, + Addrs: []string{srv.Addr().String()}, CACert: coretesting.CACert, } return info, func() { @@ -682,7 +682,7 @@ } } -func (s *baseLoginSuite) openAPIWithoutLogin(c *gc.C, info *api.Info) *api.State { +func (s *baseLoginSuite) openAPIWithoutLogin(c *gc.C, info *api.Info) api.Connection { info.Tag = nil info.Password = "" st, err := api.Open(info, fastDialOpts) @@ -907,7 +907,7 @@ c.Assert(err, gc.ErrorMatches, `invalid entity name or password`) } -func (s *loginSuite) assertRemoteEnvironment(c *gc.C, st *api.State, expected names.EnvironTag) { +func (s *loginSuite) assertRemoteEnvironment(c *gc.C, st api.Connection, expected names.EnvironTag) { // Look at what the api thinks it has. tag, err := st.EnvironTag() c.Assert(err, jc.ErrorIsNil) @@ -947,12 +947,16 @@ // The user now has last login updated. err = user.Refresh() c.Assert(err, jc.ErrorIsNil) - c.Assert(user.LastLogin(), gc.NotNil) - c.Assert(user.LastLogin().After(startTime), jc.IsTrue) + lastLogin, err := user.LastLogin() + c.Assert(err, jc.ErrorIsNil) + c.Assert(lastLogin, gc.NotNil) + c.Assert(lastLogin.After(startTime), jc.IsTrue) // The env user is also updated. envUser, err := s.State.EnvironmentUser(user.UserTag()) c.Assert(err, jc.ErrorIsNil) - c.Assert(envUser.LastConnection(), gc.NotNil) - c.Assert(envUser.LastConnection().After(startTime), jc.IsTrue) + when, err := envUser.LastConnection() + c.Assert(err, jc.ErrorIsNil) + c.Assert(when, gc.NotNil) + c.Assert(when.After(startTime), jc.IsTrue) } === modified file 'src/github.com/juju/juju/apiserver/adminv2_test.go' --- src/github.com/juju/juju/apiserver/adminv2_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/adminv2_test.go 2015-10-23 18:29:32 +0000 @@ -76,7 +76,9 @@ // The user now has last login updated. err = user.Refresh() c.Assert(err, jc.ErrorIsNil) - c.Assert(user.LastLogin(), gc.NotNil) + lastLogin, err := user.LastLogin() + c.Assert(err, jc.ErrorIsNil) + c.Assert(lastLogin, gc.NotNil) } func (s *loginV2Suite) TestClientLoginToRootOldClient(c *gc.C) { === modified file 'src/github.com/juju/juju/apiserver/allfacades.go' --- src/github.com/juju/juju/apiserver/allfacades.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/allfacades.go 2015-10-23 18:29:32 +0000 @@ -8,12 +8,14 @@ // function will get called to register it. import ( _ "github.com/juju/juju/apiserver/action" + _ "github.com/juju/juju/apiserver/addresser" _ "github.com/juju/juju/apiserver/agent" _ "github.com/juju/juju/apiserver/annotations" _ "github.com/juju/juju/apiserver/backups" _ "github.com/juju/juju/apiserver/block" _ "github.com/juju/juju/apiserver/charmrevisionupdater" _ "github.com/juju/juju/apiserver/charms" + _ "github.com/juju/juju/apiserver/cleaner" _ "github.com/juju/juju/apiserver/client" _ "github.com/juju/juju/apiserver/deployer" _ "github.com/juju/juju/apiserver/diskmanager" @@ -21,6 +23,8 @@ _ "github.com/juju/juju/apiserver/environmentmanager" _ "github.com/juju/juju/apiserver/firewaller" _ "github.com/juju/juju/apiserver/imagemanager" + _ "github.com/juju/juju/apiserver/imagemetadata" + _ "github.com/juju/juju/apiserver/instancepoller" _ "github.com/juju/juju/apiserver/keymanager" _ "github.com/juju/juju/apiserver/keyupdater" _ "github.com/juju/juju/apiserver/logger" @@ -30,10 +34,14 @@ _ "github.com/juju/juju/apiserver/networker" _ "github.com/juju/juju/apiserver/provisioner" _ "github.com/juju/juju/apiserver/reboot" + _ "github.com/juju/juju/apiserver/resumer" _ "github.com/juju/juju/apiserver/rsyslog" _ "github.com/juju/juju/apiserver/service" + _ "github.com/juju/juju/apiserver/spaces" _ "github.com/juju/juju/apiserver/storage" _ "github.com/juju/juju/apiserver/storageprovisioner" + _ "github.com/juju/juju/apiserver/subnets" + _ "github.com/juju/juju/apiserver/systemmanager" _ "github.com/juju/juju/apiserver/uniter" _ "github.com/juju/juju/apiserver/upgrader" _ "github.com/juju/juju/apiserver/usermanager" === modified file 'src/github.com/juju/juju/apiserver/apiserver.go' --- src/github.com/juju/juju/apiserver/apiserver.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/apiserver.go 2015-10-23 18:29:32 +0000 @@ -18,7 +18,6 @@ "github.com/juju/loggo" "github.com/juju/names" "github.com/juju/utils" - "github.com/juju/utils/featureflag" "golang.org/x/net/websocket" "launchpad.net/tomb" @@ -41,7 +40,8 @@ tomb tomb.Tomb wg sync.WaitGroup state *state.State - addr string + statePool *state.StatePool + addr *net.TCPAddr tag names.Tag dataDir string logDir string @@ -130,7 +130,6 @@ return tomb.ErrDying } } - return nil } // updateCertificate generates a new TLS certificate and assigns it @@ -158,18 +157,19 @@ // listener, using the given certificate and key (in PEM format) for // authentication. func NewServer(s *state.State, lis net.Listener, cfg ServerConfig) (*Server, error) { + l, ok := lis.(*net.TCPListener) + if !ok { + return nil, errors.Errorf("listener is not of type *net.TCPListener: %T", lis) + } + return newServer(s, l, cfg) +} + +func newServer(s *state.State, lis *net.TCPListener, cfg ServerConfig) (*Server, error) { logger.Infof("listening on %q", lis.Addr()) - tlsCert, err := tls.X509KeyPair(cfg.Cert, cfg.Key) - if err != nil { - return nil, err - } - _, listeningPort, err := net.SplitHostPort(lis.Addr().String()) - if err != nil { - return nil, err - } srv := &Server{ state: s, - addr: net.JoinHostPort("localhost", listeningPort), + statePool: state.NewStatePool(s), + addr: lis.Addr().(*net.TCPAddr), // cannot fail tag: cfg.Tag, dataDir: cfg.DataDir, logDir: cfg.LogDir, @@ -181,6 +181,10 @@ 2: newAdminApiV2, }, } + tlsCert, err := tls.X509KeyPair(cfg.Cert, cfg.Key) + if err != nil { + return nil, err + } // TODO(rog) check that *srvRoot is a valid type for using // as an RPC server. tlsConfig := tls.Config{ @@ -296,35 +300,39 @@ } func (srv *Server) run(lis net.Listener) { - defer srv.tomb.Done() - defer srv.wg.Wait() // wait for any outstanding requests to complete. - defer srv.state.HackLeadership() // Break deadlocks caused by BlockUntil... calls. + defer func() { + srv.state.HackLeadership() // Break deadlocks caused by BlockUntil... calls. + srv.wg.Wait() // wait for any outstanding requests to complete. + srv.tomb.Done() + srv.statePool.Close() + }() + srv.wg.Add(1) go func() { err := srv.mongoPinger() srv.tomb.Kill(err) srv.wg.Done() }() + // for pat based handlers, they are matched in-order of being // registered, first match wins. So more specific ones have to be // registered first. mux := pat.New() - // For backwards compatibility we register all the old paths - handleAll(mux, "/environment/:envuuid/log", - &debugLogHandler{ - httpHandler: httpHandler{ssState: srv.state}, - logDir: srv.logDir}, - ) - if featureflag.Enabled(feature.DbLog) { + + srvDying := srv.tomb.Dying() + + if feature.IsDbLogEnabled() { handleAll(mux, "/environment/:envuuid/logsink", - &logSinkHandler{ - httpHandler: httpHandler{ssState: srv.state}, - }, - ) + newLogSinkHandler(httpHandler{statePool: srv.statePool}, srv.logDir)) + handleAll(mux, "/environment/:envuuid/log", + newDebugLogDBHandler(srv.statePool, srvDying)) + } else { + handleAll(mux, "/environment/:envuuid/log", + newDebugLogFileHandler(srv.statePool, srvDying, srv.logDir)) } handleAll(mux, "/environment/:envuuid/charms", &charmsHandler{ - httpHandler: httpHandler{ssState: srv.state}, + httpHandler: httpHandler{statePool: srv.statePool}, dataDir: srv.dataDir}, ) // TODO: We can switch from handleAll to mux.Post/Get/etc for entries @@ -333,46 +341,51 @@ // pat only does "text/plain" responses. handleAll(mux, "/environment/:envuuid/tools", &toolsUploadHandler{toolsHandler{ - httpHandler{ssState: srv.state}, + httpHandler{statePool: srv.statePool}, }}, ) handleAll(mux, "/environment/:envuuid/tools/:version", &toolsDownloadHandler{toolsHandler{ - httpHandler{ssState: srv.state}, + httpHandler{statePool: srv.statePool}, }}, ) handleAll(mux, "/environment/:envuuid/backups", &backupHandler{httpHandler{ - ssState: srv.state, + statePool: srv.statePool, strictValidation: true, stateServerEnvOnly: true, }}, ) handleAll(mux, "/environment/:envuuid/api", http.HandlerFunc(srv.apiHandler)) + handleAll(mux, "/environment/:envuuid/images/:kind/:series/:arch/:filename", &imagesDownloadHandler{ - httpHandler: httpHandler{ssState: srv.state}, - dataDir: srv.dataDir}, + httpHandler: httpHandler{statePool: srv.statePool}, + dataDir: srv.dataDir, + state: srv.state}, ) // For backwards compatibility we register all the old paths - handleAll(mux, "/log", - &debugLogHandler{ - httpHandler: httpHandler{ssState: srv.state}, - logDir: srv.logDir}, - ) + + if feature.IsDbLogEnabled() { + handleAll(mux, "/log", newDebugLogDBHandler(srv.statePool, srvDying)) + } else { + handleAll(mux, "/log", newDebugLogFileHandler(srv.statePool, srvDying, srv.logDir)) + } + handleAll(mux, "/charms", &charmsHandler{ - httpHandler: httpHandler{ssState: srv.state}, - dataDir: srv.dataDir}, + httpHandler: httpHandler{statePool: srv.statePool}, + dataDir: srv.dataDir, + }, ) handleAll(mux, "/tools", &toolsUploadHandler{toolsHandler{ - httpHandler{ssState: srv.state}, + httpHandler{statePool: srv.statePool}, }}, ) handleAll(mux, "/tools/:version", &toolsDownloadHandler{toolsHandler{ - httpHandler{ssState: srv.state}, + httpHandler{statePool: srv.statePool}, }}, ) handleAll(mux, "/", http.HandlerFunc(srv.apiHandler)) @@ -412,7 +425,7 @@ } // Addr returns the address that the server is listening on. -func (srv *Server) Addr() string { +func (srv *Server) Addr() *net.TCPAddr { return srv.addr } @@ -430,7 +443,7 @@ conn := rpc.NewConn(codec, notifier) var h *apiHandler - st, _, err := validateEnvironUUID(validateArgs{st: srv.state, envUUID: envUUID}) + st, err := validateEnvironUUID(validateArgs{statePool: srv.statePool, envUUID: envUUID}) if err == nil { h, err = newApiHandler(srv, st, conn, reqNotifier, envUUID) } === modified file 'src/github.com/juju/juju/apiserver/backup.go' --- src/github.com/juju/juju/apiserver/backup.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/backup.go 2015-10-23 18:29:32 +0000 @@ -37,7 +37,6 @@ h.sendError(resp, http.StatusNotFound, err.Error()) return } - defer stateWrapper.cleanup() if err := stateWrapper.authenticateUser(req); err != nil { h.authError(resp, h) === modified file 'src/github.com/juju/juju/apiserver/backups/backups.go' --- src/github.com/juju/juju/apiserver/backups/backups.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/apiserver/backups/backups.go 2015-10-23 18:29:32 +0000 @@ -36,6 +36,11 @@ return nil, errors.Trace(common.ErrPerm) } + // For now, backup operations are only permitted on the system environment. + if !st.IsStateServer() { + return nil, errors.New("backups are not supported for hosted environments") + } + // Get the backup paths. dataDir, err := extractResourceValue(resources, "dataDir") if err != nil { === modified file 'src/github.com/juju/juju/apiserver/backups/backups_test.go' --- src/github.com/juju/juju/apiserver/backups/backups_test.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/apiserver/backups/backups_test.go 2015-10-23 18:29:32 +0000 @@ -19,6 +19,7 @@ "github.com/juju/juju/state" "github.com/juju/juju/state/backups" backupstesting "github.com/juju/juju/state/backups/testing" + "github.com/juju/juju/testing/factory" ) type backupsSuite struct { @@ -77,3 +78,10 @@ c.Check(errors.Cause(err), gc.Equals, common.ErrPerm) } + +func (s *backupsSuite) TestNewAPIHostedEnvironmentFails(c *gc.C) { + otherState := factory.NewFactory(s.State).MakeEnvironment(c, nil) + defer otherState.Close() + _, err := backupsAPI.NewAPI(otherState, s.resources, s.authorizer) + c.Check(err, gc.ErrorMatches, "backups are not supported for hosted environments") +} === modified file 'src/github.com/juju/juju/apiserver/backups/restore.go' --- src/github.com/juju/juju/apiserver/backups/restore.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/backups/restore.go 2015-10-23 18:29:32 +0000 @@ -9,7 +9,6 @@ "github.com/juju/errors" "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/network" "github.com/juju/juju/state" "github.com/juju/juju/state/backups" ) @@ -27,14 +26,13 @@ return errors.Trace(err) } - addr := network.SelectInternalAddress(machine.Addresses(), false) - if addr == "" { - return errors.Errorf("machine %q has no internal address", machine) + addr, err := machine.PrivateAddress() + if err != nil { + return errors.Annotatef(err, "error fetching internal address for machine %q", machine) } - - publicAddress := network.SelectPublicAddress(machine.Addresses()) - if publicAddress == "" { - return errors.Errorf("machine %q has no public address", machine) + publicAddress, err := machine.PublicAddress() + if err != nil { + return errors.Annotatef(err, "error fetching public address for machine %q", machine) } info, err := a.st.RestoreInfoSetter() @@ -58,8 +56,8 @@ logger.Infof("beginning server side restore of backup %q", p.BackupId) // Restore restoreArgs := backups.RestoreArgs{ - PrivateAddress: addr, - PublicAddress: publicAddress, + PrivateAddress: addr.Value, + PublicAddress: publicAddress.Value, NewInstId: instanceId, NewInstTag: machine.Tag(), NewInstSeries: machine.Series(), === modified file 'src/github.com/juju/juju/apiserver/charmrevisionupdater/updater_test.go' --- src/github.com/juju/juju/apiserver/charmrevisionupdater/updater_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/charmrevisionupdater/updater_test.go 2015-10-23 18:29:32 +0000 @@ -122,6 +122,31 @@ c.Assert(err, jc.Satisfies, errors.IsNotFound) } +func (s *charmVersionSuite) TestWordpressCharmNoReadAccessIsntVisible(c *gc.C) { + s.AddMachine(c, "0", state.JobManageEnviron) + s.SetupScenario(c) + + // Disallow read access to the wordpress charm in the charm store. + err := s.Server.NewClient().Put("/quantal/wordpress/meta/perm/read", nil) + c.Assert(err, jc.ErrorIsNil) + + // Run the revision updater and check that the public charm updates are + // still properly notified. + result, err := s.charmrevisionupdater.UpdateLatestRevisions() + c.Assert(err, jc.ErrorIsNil) + c.Assert(result.Error, gc.IsNil) + + curl := charm.MustParseURL("cs:quantal/mysql") + pending, err := s.State.LatestPlaceholderCharm(curl) + c.Assert(err, jc.ErrorIsNil) + c.Assert(pending.String(), gc.Equals, "cs:quantal/mysql-23") + + // No pending charm for wordpress. + curl = charm.MustParseURL("cs:quantal/wordpress") + _, err = s.State.LatestPlaceholderCharm(curl) + c.Assert(err, jc.Satisfies, errors.IsNotFound) +} + func (s *charmVersionSuite) TestEnvironmentUUIDUsed(c *gc.C) { s.AddMachine(c, "0", state.JobManageEnviron) s.SetupScenario(c) === modified file 'src/github.com/juju/juju/apiserver/charms.go' --- src/github.com/juju/juju/apiserver/charms.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/charms.go 2015-10-23 18:29:32 +0000 @@ -48,7 +48,6 @@ h.sendError(w, http.StatusNotFound, err.Error()) return } - defer stateWrapper.cleanup() switch r.Method { case "POST": === modified file 'src/github.com/juju/juju/apiserver/charms/client.go' --- src/github.com/juju/juju/apiserver/charms/client.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/charms/client.go 2015-10-23 18:29:32 +0000 @@ -26,6 +26,7 @@ type Charms interface { List(args params.CharmsList) (params.CharmsListResult, error) CharmInfo(args params.CharmInfo) (api.CharmInfo, error) + IsMetered(args params.CharmInfo) (bool, error) } // API implements the charms interface and is the concrete @@ -94,3 +95,19 @@ } return params.CharmsListResult{CharmURLs: charmURLs}, nil } + +// IsMetered returns whether or not the charm is metered. +func (a *API) IsMetered(args params.CharmInfo) (params.IsMeteredResult, error) { + curl, err := charm.ParseURL(args.CharmURL) + if err != nil { + return params.IsMeteredResult{false}, err + } + aCharm, err := a.access.Charm(curl) + if err != nil { + return params.IsMeteredResult{false}, err + } + if aCharm.Metrics() != nil && len(aCharm.Metrics().Metrics) > 0 { + return params.IsMeteredResult{true}, nil + } + return params.IsMeteredResult{false}, nil +} === modified file 'src/github.com/juju/juju/apiserver/charms/client_test.go' --- src/github.com/juju/juju/apiserver/charms/client_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/charms/client_test.go 2015-10-23 18:29:32 +0000 @@ -14,6 +14,7 @@ "github.com/juju/juju/apiserver/params" "github.com/juju/juju/apiserver/testing" jujutesting "github.com/juju/juju/juju/testing" + "github.com/juju/juju/testing/factory" ) type baseCharmsSuite struct { @@ -155,3 +156,21 @@ c.Check(found.CharmURLs, gc.HasLen, len(expected)) c.Check(found.CharmURLs, jc.DeepEquals, expected) } + +func (s *charmsSuite) TestIsMeteredFalse(c *gc.C) { + charm := s.Factory.MakeCharm(c, &factory.CharmParams{Name: "wordpress"}) + metered, err := s.api.IsMetered(params.CharmInfo{ + CharmURL: charm.URL().String(), + }) + c.Assert(err, jc.ErrorIsNil) + c.Assert(metered.Metered, jc.IsFalse) +} + +func (s *charmsSuite) TestIsMeteredTrue(c *gc.C) { + meteredCharm := s.Factory.MakeCharm(c, &factory.CharmParams{Name: "metered", URL: "cs:quantal/metered"}) + metered, err := s.api.IsMetered(params.CharmInfo{ + CharmURL: meteredCharm.URL().String(), + }) + c.Assert(err, jc.ErrorIsNil) + c.Assert(metered.Metered, jc.IsTrue) +} === added directory 'src/github.com/juju/juju/apiserver/cleaner' === added file 'src/github.com/juju/juju/apiserver/cleaner/cleaner.go' --- src/github.com/juju/juju/apiserver/cleaner/cleaner.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/cleaner/cleaner.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,61 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +// The cleaner package implements the API interface +// used by the cleaner worker. + +package cleaner + +import ( + "github.com/juju/loggo" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/state" + "github.com/juju/juju/state/watcher" +) + +func init() { + common.RegisterStandardFacade("Cleaner", 1, NewCleanerAPI) +} + +var logger = loggo.GetLogger("juju.apiserver.cleaner") + +// CleanerAPI implements the API used by the cleaner worker. +type CleanerAPI struct { + st StateInterface + resources *common.Resources +} + +// NewCleanerAPI creates a new instance of the Cleaner API. +func NewCleanerAPI( + st *state.State, + res *common.Resources, + authorizer common.Authorizer, +) (*CleanerAPI, error) { + if !authorizer.AuthEnvironManager() { + return nil, common.ErrPerm + } + return &CleanerAPI{ + st: getState(st), + resources: res, + }, nil +} + +// Cleanup triggers a state cleanup +func (api *CleanerAPI) Cleanup() error { + return api.st.Cleanup() +} + +// WatchChanges watches for cleanups to be perfomed in state +func (api *CleanerAPI) WatchCleanups() (params.NotifyWatchResult, error) { + watch := api.st.WatchCleanups() + if _, ok := <-watch.Changes(); ok { + return params.NotifyWatchResult{ + NotifyWatcherId: api.resources.Register(watch), + }, nil + } + return params.NotifyWatchResult{ + Error: common.ServerError(watcher.EnsureErr(watch)), + }, nil +} === added file 'src/github.com/juju/juju/apiserver/cleaner/cleaner_test.go' --- src/github.com/juju/juju/apiserver/cleaner/cleaner_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/cleaner/cleaner_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,129 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package cleaner_test + +import ( + "github.com/juju/errors" + "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/cleaner" + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/params" + apiservertesting "github.com/juju/juju/apiserver/testing" + "github.com/juju/juju/state" + coretesting "github.com/juju/juju/testing" +) + +type CleanerSuite struct { + coretesting.BaseSuite + + st *mockState + api *cleaner.CleanerAPI + authoriser apiservertesting.FakeAuthorizer +} + +var _ = gc.Suite(&CleanerSuite{}) + +func (s *CleanerSuite) SetUpTest(c *gc.C) { + s.BaseSuite.SetUpTest(c) + + s.authoriser = apiservertesting.FakeAuthorizer{ + EnvironManager: true, + } + s.st = &mockState{&testing.Stub{}, false} + cleaner.PatchState(s, s.st) + var err error + res := common.NewResources() + s.api, err = cleaner.NewCleanerAPI(nil, res, s.authoriser) + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.api, gc.NotNil) +} + +func (s *CleanerSuite) TestNewCleanerAPIRequiresEnvironManager(c *gc.C) { + anAuthoriser := s.authoriser + anAuthoriser.EnvironManager = false + api, err := cleaner.NewCleanerAPI(nil, nil, anAuthoriser) + c.Assert(api, gc.IsNil) + c.Assert(err, gc.ErrorMatches, "permission denied") + c.Assert(common.ServerError(err), jc.Satisfies, params.IsCodeUnauthorized) +} + +func (s *CleanerSuite) TestWatchCleanupsSuccess(c *gc.C) { + _, err := s.api.WatchCleanups() + c.Assert(err, jc.ErrorIsNil) + s.st.CheckCallNames(c, "WatchCleanups") +} + +func (s *CleanerSuite) TestWatchCleanupsFailure(c *gc.C) { + s.st.SetErrors(errors.New("boom!")) + s.st.watchCleanupsFails = true + + result, err := s.api.WatchCleanups() + c.Assert(err, jc.ErrorIsNil) + c.Assert(result.Error.Error(), gc.Equals, "boom!") + s.st.CheckCallNames(c, "WatchCleanups") +} + +func (s *CleanerSuite) TestCleanupSuccess(c *gc.C) { + err := s.api.Cleanup() + c.Assert(err, jc.ErrorIsNil) + s.st.CheckCallNames(c, "Cleanup") +} + +func (s *CleanerSuite) TestCleanupFailure(c *gc.C) { + s.st.SetErrors(errors.New("Boom!")) + err := s.api.Cleanup() + c.Assert(err, gc.ErrorMatches, "Boom!") + s.st.CheckCallNames(c, "Cleanup") +} + +type mockState struct { + *testing.Stub + watchCleanupsFails bool +} + +type cleanupWatcher struct { + out chan struct{} + st *mockState +} + +func (w *cleanupWatcher) Changes() <-chan struct{} { + return w.out +} + +func (w *cleanupWatcher) Stop() error { + return nil +} + +func (w *cleanupWatcher) Kill() { +} + +func (w *cleanupWatcher) Wait() error { + return nil +} + +func (w *cleanupWatcher) Err() error { + return w.st.NextErr() +} + +func (st *mockState) WatchCleanups() state.NotifyWatcher { + w := &cleanupWatcher{ + out: make(chan struct{}, 1), + st: st, + } + if st.watchCleanupsFails { + close(w.out) + } else { + w.out <- struct{}{} + } + st.MethodCall(st, "WatchCleanups") + return w +} + +func (st *mockState) Cleanup() error { + st.MethodCall(st, "Cleanup") + return st.NextErr() +} === added file 'src/github.com/juju/juju/apiserver/cleaner/export_test.go' --- src/github.com/juju/juju/apiserver/cleaner/export_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/cleaner/export_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,18 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package cleaner + +import ( + "github.com/juju/juju/state" +) + +type Patcher interface { + PatchValue(ptr, value interface{}) +} + +func PatchState(p Patcher, st StateInterface) { + p.PatchValue(&getState, func(*state.State) StateInterface { + return st + }) +} === added file 'src/github.com/juju/juju/apiserver/cleaner/package_test.go' --- src/github.com/juju/juju/apiserver/cleaner/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/cleaner/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,14 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package cleaner_test + +import ( + "testing" + + gc "gopkg.in/check.v1" +) + +func TestPackage(t *testing.T) { + gc.TestingT(t) +} === added file 'src/github.com/juju/juju/apiserver/cleaner/state.go' --- src/github.com/juju/juju/apiserver/cleaner/state.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/cleaner/state.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,19 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package cleaner + +import "github.com/juju/juju/state" + +type StateInterface interface { + Cleanup() error + WatchCleanups() state.NotifyWatcher +} + +type stateShim struct { + *state.State +} + +var getState = func(st *state.State) StateInterface { + return stateShim{st} +} === modified file 'src/github.com/juju/juju/apiserver/client/api_test.go' --- src/github.com/juju/juju/apiserver/client/api_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/client/api_test.go 2015-10-23 18:29:32 +0000 @@ -15,6 +15,7 @@ "github.com/juju/juju/api" 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" @@ -134,7 +135,7 @@ // openAs connects to the API state as the given entity // with the default password for that entity. -func (s *baseSuite) openAs(c *gc.C, tag names.Tag) *api.State { +func (s *baseSuite) openAs(c *gc.C, tag names.Tag) api.Connection { info := s.APIInfo(c) info.Tag = tag // Must match defaultPassword() @@ -156,20 +157,20 @@ // to the scenario not calling SetAgentPresence on the respective entities, // but this behavior is already tested in cmd/juju/status_test.go and // also tested live and it works. -var scenarioStatus = &api.Status{ +var scenarioStatus = ¶ms.FullStatus{ EnvironmentName: "dummyenv", - Machines: map[string]api.MachineStatus{ + Machines: map[string]params.MachineStatus{ "0": { Id: "0", InstanceId: instance.Id("i-machine-0"), - Agent: api.AgentStatus{ + Agent: params.AgentStatus{ Status: "started", Data: make(map[string]interface{}), }, AgentState: "down", AgentStateInfo: "(started)", Series: "quantal", - Containers: map[string]api.MachineStatus{}, + Containers: map[string]params.MachineStatus{}, Jobs: []multiwatcher.MachineJob{multiwatcher.JobManageEnviron}, HasVote: false, WantsVote: true, @@ -177,14 +178,14 @@ "1": { Id: "1", InstanceId: instance.Id("i-machine-1"), - Agent: api.AgentStatus{ + Agent: params.AgentStatus{ Status: "started", Data: make(map[string]interface{}), }, AgentState: "down", AgentStateInfo: "(started)", Series: "quantal", - Containers: map[string]api.MachineStatus{}, + Containers: map[string]params.MachineStatus{}, Jobs: []multiwatcher.MachineJob{multiwatcher.JobHostUnits}, HasVote: false, WantsVote: false, @@ -192,20 +193,20 @@ "2": { Id: "2", InstanceId: instance.Id("i-machine-2"), - Agent: api.AgentStatus{ + Agent: params.AgentStatus{ Status: "started", Data: make(map[string]interface{}), }, AgentState: "down", AgentStateInfo: "(started)", Series: "quantal", - Containers: map[string]api.MachineStatus{}, + Containers: map[string]params.MachineStatus{}, Jobs: []multiwatcher.MachineJob{multiwatcher.JobHostUnits}, HasVote: false, WantsVote: false, }, }, - Services: map[string]api.ServiceStatus{ + Services: map[string]params.ServiceStatus{ "logging": { Charm: "local:quantal/logging-1", Relations: map[string][]string{ @@ -218,8 +219,8 @@ Charm: "local:quantal/mysql-1", Relations: map[string][]string{}, SubordinateTo: []string{}, - Units: map[string]api.UnitStatus{}, - Status: api.AgentStatus{ + Units: map[string]params.UnitStatus{}, + Status: params.AgentStatus{ Status: "unknown", Info: "Waiting for agent initialization to finish", Data: map[string]interface{}{}, @@ -231,34 +232,34 @@ "logging-dir": {"logging"}, }, SubordinateTo: []string{}, - Status: api.AgentStatus{ + Status: params.AgentStatus{ Status: "error", Info: "blam", Data: map[string]interface{}{"remote-unit": "logging/0", "foo": "bar", "relation-id": "0"}, }, - Units: map[string]api.UnitStatus{ + Units: map[string]params.UnitStatus{ "wordpress/0": { - Workload: api.AgentStatus{ + Workload: params.AgentStatus{ Status: "error", Info: "blam", Data: map[string]interface{}{"relation-id": "0"}, }, - UnitAgent: api.AgentStatus{ + UnitAgent: params.AgentStatus{ Status: "idle", Data: make(map[string]interface{}), }, AgentState: "error", AgentStateInfo: "blam", Machine: "1", - Subordinates: map[string]api.UnitStatus{ + Subordinates: map[string]params.UnitStatus{ "logging/0": { AgentState: "pending", - Workload: api.AgentStatus{ + Workload: params.AgentStatus{ Status: "unknown", Info: "Waiting for agent initialization to finish", Data: make(map[string]interface{}), }, - UnitAgent: api.AgentStatus{ + UnitAgent: params.AgentStatus{ Status: "allocating", Data: map[string]interface{}{}, }, @@ -267,27 +268,27 @@ }, "wordpress/1": { AgentState: "pending", - Workload: api.AgentStatus{ + Workload: params.AgentStatus{ Status: "unknown", Info: "Waiting for agent initialization to finish", Data: make(map[string]interface{}), }, - UnitAgent: api.AgentStatus{ + UnitAgent: params.AgentStatus{ Status: "allocating", Info: "", Data: make(map[string]interface{}), }, Machine: "2", - Subordinates: map[string]api.UnitStatus{ + Subordinates: map[string]params.UnitStatus{ "logging/1": { AgentState: "pending", - Workload: api.AgentStatus{ + Workload: params.AgentStatus{ Status: "unknown", Info: "Waiting for agent initialization to finish", Data: make(map[string]interface{}), }, - UnitAgent: api.AgentStatus{ + UnitAgent: params.AgentStatus{ Status: "allocating", Info: "", Data: make(map[string]interface{}), @@ -298,11 +299,11 @@ }, }, }, - Relations: []api.RelationStatus{ + Relations: []params.RelationStatus{ { Id: 0, Key: "logging:logging-directory wordpress:logging-dir", - Endpoints: []api.EndpointStatus{ + Endpoints: []params.EndpointStatus{ { ServiceName: "logging", Name: "logging-directory", @@ -320,7 +321,7 @@ Scope: "container", }, }, - Networks: map[string]api.NetworkStatus{}, + Networks: map[string]params.NetworkStatus{}, } // setUpScenario makes an environment scenario suitable for === modified file 'src/github.com/juju/juju/apiserver/client/client.go' --- src/github.com/juju/juju/apiserver/client/client.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/client/client.go 2015-10-23 18:29:32 +0000 @@ -6,6 +6,7 @@ import ( "fmt" "strings" + "time" "github.com/juju/errors" "github.com/juju/loggo" @@ -53,11 +54,7 @@ if !authorizer.AuthClient() { return nil, common.ErrPerm } - env, err := st.Environment() - if err != nil { - return nil, err - } - urlGetter := common.NewToolsURLGetter(env.UUID(), st) + urlGetter := common.NewToolsURLGetter(st.EnvironUUID(), st) return &Client{ api: &API{ state: st, @@ -177,24 +174,24 @@ if err != nil { return results, err } - addr := network.SelectPublicAddress(machine.Addresses()) - if addr == "" { - return results, fmt.Errorf("machine %q has no public address", machine) + addr, err := machine.PublicAddress() + if err != nil { + return results, errors.Annotatef(err, "error fetching address for machine %q", machine) } - return params.PublicAddressResults{PublicAddress: addr}, nil + return params.PublicAddressResults{PublicAddress: addr.Value}, nil case names.IsValidUnit(p.Target): unit, err := c.api.state.Unit(p.Target) if err != nil { return results, err } - addr, ok := unit.PublicAddress() - if !ok { - return results, fmt.Errorf("unit %q has no public address", unit) + addr, err := unit.PublicAddress() + if err != nil { + return results, errors.Annotatef(err, "error fetching address for unit %q", unit) } - return params.PublicAddressResults{PublicAddress: addr}, nil + return params.PublicAddressResults{PublicAddress: addr.Value}, nil } - return results, fmt.Errorf("unknown unit or machine %q", p.Target) + return results, errors.Errorf("unknown unit or machine %q", p.Target) } // PrivateAddress implements the server side of Client.PrivateAddress. @@ -205,22 +202,22 @@ if err != nil { return results, err } - addr := network.SelectInternalAddress(machine.Addresses(), false) - if addr == "" { - return results, fmt.Errorf("machine %q has no internal address", machine) + addr, err := machine.PrivateAddress() + if err != nil { + return results, errors.Annotatef(err, "error fetching address for machine %q", machine) } - return params.PrivateAddressResults{PrivateAddress: addr}, nil + return params.PrivateAddressResults{PrivateAddress: addr.Value}, nil case names.IsValidUnit(p.Target): unit, err := c.api.state.Unit(p.Target) if err != nil { return results, err } - addr, ok := unit.PrivateAddress() - if !ok { - return results, fmt.Errorf("unit %q has no internal address", unit) + addr, err := unit.PrivateAddress() + if err != nil { + return results, errors.Annotatef(err, "error fetching address for unit %q", unit) } - return params.PrivateAddressResults{PrivateAddress: addr}, nil + return params.PrivateAddressResults{PrivateAddress: addr.Value}, nil } return results, fmt.Errorf("unknown unit or machine %q", p.Target) } @@ -268,6 +265,9 @@ // allows specifying networks to include or exclude on the machine // where the charm gets deployed (either with args.Network or with // constraints). +// +// TODO(dimitern): Drop the special handling of networks in favor of +// spaces constraints, once possible. func (c *Client) ServiceDeployWithNetworks(args params.ServiceDeploy) error { return c.ServiceDeploy(args) } @@ -414,6 +414,13 @@ if args.NumUnits < 1 { return nil, fmt.Errorf("must add at least one unit") } + + // New API uses placement directives. + if len(args.Placement) > 0 { + return jjj.AddUnitsWithPlacement(state, service, args.NumUnits, args.Placement) + } + + // Otherwise we use the older machine spec. if args.NumUnits > 1 && args.ToMachineSpec != "" { return nil, fmt.Errorf("cannot use NumUnits with ToMachineSpec") } @@ -429,6 +436,11 @@ // AddServiceUnits adds a given number of units to a service. func (c *Client) AddServiceUnits(args params.AddServiceUnits) (params.AddServiceUnitsResults, error) { + return c.AddServiceUnitsWithPlacement(args) +} + +// AddServiceUnits adds a given number of units to a service. +func (c *Client) AddServiceUnitsWithPlacement(args params.AddServiceUnits) (params.AddServiceUnitsResults, error) { if err := c.check.ChangeAllowed(); err != nil { return params.AddServiceUnitsResults{}, errors.Trace(err) } @@ -818,13 +830,22 @@ } for _, user := range users { + var lastConn *time.Time + userLastConn, err := user.LastConnection() + if err != nil { + if !state.IsNeverConnectedError(err) { + return results, errors.Trace(err) + } + } else { + lastConn = &userLastConn + } results.Results = append(results.Results, params.EnvUserInfoResult{ Result: ¶ms.EnvUserInfo{ UserName: user.UserName(), DisplayName: user.DisplayName(), CreatedBy: user.CreatedBy(), DateCreated: user.DateCreated(), - LastConnection: user.LastConnection(), + LastConnection: lastConn, }, }) } @@ -1004,13 +1025,9 @@ if err := c.check.ChangeAllowed(); err != nil { return params.ErrorResults{}, errors.Trace(err) } - - // TODO(fwereade): ...seriously? we use *status*, which is *output* for the - // user's edification, as *input* for the *provisioner*? This is pretty damn - // close to deliberate sabotage. - entityStatus := make([]params.EntityStatus, len(p.Entities)) + entityStatus := make([]params.EntityStatusArgs, len(p.Entities)) for i, entity := range p.Entities { - entityStatus[i] = params.EntityStatus{Tag: entity.Tag, Data: map[string]interface{}{"transient": true}} + entityStatus[i] = params.EntityStatusArgs{Tag: entity.Tag, Data: map[string]interface{}{"transient": true}} } return c.api.statusSetter.UpdateStatus(params.SetStatus{ Entities: entityStatus, @@ -1042,3 +1059,14 @@ } return results, nil } + +// DestroyEnvironment will try to destroy the current environment. +// If there is a block on destruction, this method will return an error. +func (c *Client) DestroyEnvironment() (err error) { + if err := c.check.DestroyAllowed(); err != nil { + return errors.Trace(err) + } + + environTag := c.api.state.EnvironTag() + return errors.Trace(common.DestroyEnvironment(c.api.state, environTag)) +} === modified file 'src/github.com/juju/juju/apiserver/client/client_test.go' --- src/github.com/juju/juju/apiserver/client/client_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/client/client_test.go 2015-10-23 18:29:32 +0000 @@ -9,6 +9,7 @@ "sort" "strconv" "strings" + "time" "github.com/juju/errors" "github.com/juju/names" @@ -134,49 +135,47 @@ results, err := s.client.EnvUserInfo() c.Assert(err, jc.ErrorIsNil) - expected := params.EnvUserInfoResults{ - Results: []params.EnvUserInfoResult{ - { - Result: ¶ms.EnvUserInfo{ - UserName: owner.UserName(), - DisplayName: owner.DisplayName(), - CreatedBy: owner.UserName(), - DateCreated: owner.DateCreated(), - LastConnection: owner.LastConnection(), - }, - }, { - Result: ¶ms.EnvUserInfo{ - UserName: "ralphdoe@local", - DisplayName: "Ralph Doe", - CreatedBy: owner.UserName(), - DateCreated: localUser1.DateCreated(), - LastConnection: localUser1.LastConnection(), - }, - }, { - Result: ¶ms.EnvUserInfo{ - UserName: "samsmith@local", - DisplayName: "Sam Smith", - CreatedBy: owner.UserName(), - DateCreated: localUser2.DateCreated(), - LastConnection: localUser2.LastConnection(), - }, - }, { - Result: ¶ms.EnvUserInfo{ - UserName: "bobjohns@ubuntuone", - DisplayName: "Bob Johns", - CreatedBy: owner.UserName(), - DateCreated: remoteUser1.DateCreated(), - LastConnection: remoteUser1.LastConnection(), - }, - }, { - Result: ¶ms.EnvUserInfo{ - UserName: "nicshaw@idprovider", - DisplayName: "Nic Shaw", - CreatedBy: owner.UserName(), - DateCreated: remoteUser2.DateCreated(), - LastConnection: remoteUser2.LastConnection(), - }, - }}, + var expected params.EnvUserInfoResults + for _, r := range []struct { + user *state.EnvironmentUser + info *params.EnvUserInfo + }{ + { + owner, + ¶ms.EnvUserInfo{ + UserName: owner.UserName(), + DisplayName: owner.DisplayName(), + }, + }, { + localUser1, + ¶ms.EnvUserInfo{ + UserName: "ralphdoe@local", + DisplayName: "Ralph Doe", + }, + }, { + localUser2, + ¶ms.EnvUserInfo{ + UserName: "samsmith@local", + DisplayName: "Sam Smith", + }, + }, { + remoteUser1, + ¶ms.EnvUserInfo{ + UserName: "bobjohns@ubuntuone", + DisplayName: "Bob Johns", + }, + }, { + remoteUser2, + ¶ms.EnvUserInfo{ + UserName: "nicshaw@idprovider", + DisplayName: "Nic Shaw", + }, + }, + } { + r.info.CreatedBy = owner.UserName() + r.info.DateCreated = r.user.DateCreated() + r.info.LastConnection = lastConnPointer(c, r.user) + expected.Results = append(expected.Results, params.EnvUserInfoResult{Result: r.info}) } sort.Sort(ByUserName(expected.Results)) @@ -184,6 +183,17 @@ c.Assert(results, jc.DeepEquals, expected) } +func lastConnPointer(c *gc.C, envUser *state.EnvironmentUser) *time.Time { + lastConn, err := envUser.LastConnection() + if err != nil { + if state.IsNeverConnectedError(err) { + return nil + } + c.Fatal(err) + } + return &lastConn +} + // ByUserName implements sort.Interface for []params.EnvUserInfoResult based on // the UserName field. type ByUserName []params.EnvUserInfoResult @@ -273,7 +283,9 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(envUser.UserName(), gc.Equals, user.UserTag().Username()) c.Assert(envUser.CreatedBy(), gc.Equals, dummy.AdminUserTag().Username()) - c.Assert(envUser.LastConnection(), gc.IsNil) + lastConn, err := envUser.LastConnection() + c.Assert(err, jc.Satisfies, state.IsNeverConnectedError) + c.Assert(lastConn, gc.Equals, time.Time{}) } func (s *serverSuite) TestShareEnvironmentAddRemoteUser(c *gc.C) { @@ -294,7 +306,9 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(envUser.UserName(), gc.Equals, user.Username()) c.Assert(envUser.CreatedBy(), gc.Equals, dummy.AdminUserTag().Username()) - c.Assert(envUser.LastConnection(), gc.IsNil) + lastConn, err := envUser.LastConnection() + c.Assert(err, jc.Satisfies, state.IsNeverConnectedError) + c.Assert(lastConn.IsZero(), jc.IsTrue) } func (s *serverSuite) TestShareEnvironmentAddUserTwice(c *gc.C) { @@ -554,7 +568,7 @@ // clearSinceTimes zeros out the updated timestamps inside status // so we can easily check the results. -func clearSinceTimes(status *api.Status) { +func clearSinceTimes(status *params.FullStatus) { for serviceId, service := range status.Services { for unitId, unit := range service.Units { unit.Workload.Since = nil @@ -886,6 +900,60 @@ c.Assert(mid, gc.Equals, machine.Id()+"/lxc/0") } +var clientAddServiceUnitsWithPlacementTests = []struct { + about string + service string // if not set, defaults to 'dummy' + expected []string + machineIds []string + placement []*instance.Placement + err string +}{ + { + about: "valid placement directives", + expected: []string{"dummy/0"}, + placement: []*instance.Placement{{"deadbeef-0bad-400d-8000-4b1d0d06f00d", "valid"}}, + machineIds: []string{"1"}, + }, { + about: "direct machine assignment placement directive", + expected: []string{"dummy/1", "dummy/2"}, + placement: []*instance.Placement{{"#", "1"}, {"lxc", "1"}}, + machineIds: []string{"1", "1/lxc/0"}, + }, { + about: "invalid placement directive", + err: ".* invalid placement is invalid", + expected: []string{"dummy/3"}, + placement: []*instance.Placement{{"deadbeef-0bad-400d-8000-4b1d0d06f00d", "invalid"}}, + }, +} + +func (s *clientSuite) TestClientAddServiceUnitsWithPlacement(c *gc.C) { + s.AddTestingService(c, "dummy", s.AddTestingCharm(c, "dummy")) + // Add a machine for the units to be placed on. + _, err := s.State.AddMachine("quantal", state.JobHostUnits) + c.Assert(err, jc.ErrorIsNil) + for i, t := range clientAddServiceUnitsWithPlacementTests { + c.Logf("test %d. %s", i, t.about) + serviceName := t.service + if serviceName == "" { + serviceName = "dummy" + } + units, err := s.APIState.Client().AddServiceUnitsWithPlacement(serviceName, len(t.expected), t.placement) + if t.err != "" { + c.Assert(err, gc.ErrorMatches, t.err) + continue + } + c.Assert(err, jc.ErrorIsNil) + c.Assert(units, gc.DeepEquals, t.expected) + for i, unitName := range units { + u, err := s.BackingState.Unit(unitName) + c.Assert(err, jc.ErrorIsNil) + assignedMachine, err := u.AssignedMachineId() + c.Assert(err, jc.ErrorIsNil) + c.Assert(assignedMachine, gc.Equals, t.machineIds[i]) + } + } +} + func (s *clientSuite) assertAddServiceUnits(c *gc.C) { units, err := s.APIState.Client().AddServiceUnits("dummy", 3, "") c.Assert(err, jc.ErrorIsNil) @@ -1573,7 +1641,7 @@ } func (s *clientRepoSuite) TestClientServiceDeployWithNetworks(c *gc.C) { - curl, ch := s.UploadCharm(c, "precise/dummy-0", "dummy") + curl, _ := s.UploadCharm(c, "precise/dummy-0", "dummy") err := service.AddCharmWithAuthorization(s.State, params.AddCharmWithAuthorization{URL: curl.String()}) c.Assert(err, jc.ErrorIsNil) cons := constraints.MustParse("mem=4G networks=^net3") @@ -1589,15 +1657,7 @@ curl.String(), "service", 3, "", cons, "", []string{"network-net1", "network-net2"}, ) - c.Assert(err, jc.ErrorIsNil) - service := apiservertesting.AssertPrincipalServiceDeployed(c, s.State, "service", curl, false, ch, cons) - - networks, err := service.Networks() - c.Assert(err, jc.ErrorIsNil) - c.Assert(networks, gc.DeepEquals, []string{"net1", "net2"}) - serviceCons, err := service.Constraints() - c.Assert(err, jc.ErrorIsNil) - c.Assert(serviceCons, gc.DeepEquals, cons) + c.Assert(err, gc.ErrorMatches, "use of --networks is deprecated. Please use spaces") } func (s *clientRepoSuite) setupServiceDeploy(c *gc.C, args string) (*charm.URL, charm.Charm, constraints.Value) { @@ -1608,47 +1668,6 @@ return curl, ch, cons } -func (s *clientRepoSuite) assertServiceDeployWithNetworks(c *gc.C, curl *charm.URL, ch charm.Charm, cons constraints.Value) { - err := s.APIState.Client().ServiceDeployWithNetworks( - curl.String(), "service", 3, "", cons, "", - []string{"network-net1", "network-net2"}, - ) - c.Assert(err, jc.ErrorIsNil) - service := apiservertesting.AssertPrincipalServiceDeployed(c, s.State, "service", curl, false, ch, cons) - networks, err := service.Networks() - c.Assert(err, jc.ErrorIsNil) - c.Assert(networks, gc.DeepEquals, []string{"net1", "net2"}) - serviceCons, err := service.Constraints() - c.Assert(err, jc.ErrorIsNil) - c.Assert(serviceCons, gc.DeepEquals, cons) -} - -func (s *clientRepoSuite) assertServiceDeployWithNetworksBlocked(c *gc.C, msg string, curl *charm.URL, cons constraints.Value) { - err := s.APIState.Client().ServiceDeployWithNetworks( - curl.String(), "service", 3, "", cons, "", - []string{"network-net1", "network-net2"}, - ) - s.AssertBlocked(c, err, msg) -} - -func (s *clientRepoSuite) TestBlockDestroyServiceDeployWithNetworks(c *gc.C) { - curl, ch, cons := s.setupServiceDeploy(c, "mem=4G networks=^net3") - s.BlockDestroyEnvironment(c, "TestBlockDestroyServiceDeployWithNetworks") - s.assertServiceDeployWithNetworks(c, curl, ch, cons) -} - -func (s *clientRepoSuite) TestBlockRemoveServiceDeployWithNetworks(c *gc.C) { - curl, ch, cons := s.setupServiceDeploy(c, "mem=4G networks=^net3") - s.BlockRemoveObject(c, "TestBlockRemoveServiceDeployWithNetworks") - s.assertServiceDeployWithNetworks(c, curl, ch, cons) -} - -func (s *clientRepoSuite) TestBlockChangeServiceDeployWithNetworks(c *gc.C) { - curl, _, cons := s.setupServiceDeploy(c, "mem=4G networks=^net3") - s.BlockAllChanges(c, "TestBlockChangeServiceDeployWithNetworks") - s.assertServiceDeployWithNetworksBlocked(c, "TestBlockChangeServiceDeployWithNetworks", curl, cons) -} - func (s *clientRepoSuite) TestClientServiceDeployPrincipal(c *gc.C) { // TODO(fwereade): test ToMachineSpec directly on srvClient, when we // manage to extract it as a package and can thus do it conveniently. @@ -2237,6 +2256,7 @@ mySvc, err := s.State.Service("mysql") c.Assert(err, jc.ErrorIsNil) rels, err = mySvc.Relations() + c.Assert(err, jc.ErrorIsNil) c.Assert(len(rels), gc.Equals, 1) } @@ -2400,9 +2420,11 @@ c.Assert(err, jc.ErrorIsNil) if !c.Check(deltas, gc.DeepEquals, []multiwatcher.Delta{{ Entity: &multiwatcher.MachineInfo{ + EnvUUID: s.State.EnvironUUID(), Id: m.Id(), InstanceId: "i-0", Status: multiwatcher.Status("pending"), + StatusData: map[string]interface{}{}, Life: multiwatcher.Life("alive"), Series: "quantal", Jobs: []multiwatcher.MachineJob{state.JobManageEnviron.ToParams()}, @@ -2567,13 +2589,14 @@ _, err := s.APIState.Client().PublicAddress("wordpress") c.Assert(err, gc.ErrorMatches, `unknown unit or machine "wordpress"`) _, err = s.APIState.Client().PublicAddress("0") - c.Assert(err, gc.ErrorMatches, `machine "0" has no public address`) + c.Assert(err, gc.ErrorMatches, `error fetching address for machine "0": public no address`) _, err = s.APIState.Client().PublicAddress("wordpress/0") - c.Assert(err, gc.ErrorMatches, `unit "wordpress/0" has no public address`) + c.Assert(err, gc.ErrorMatches, `error fetching address for unit "wordpress/0": public no address`) } func (s *clientSuite) TestClientPublicAddressMachine(c *gc.C) { s.setUpScenario(c) + network.ResetGlobalPreferIPv6() // Internally, network.SelectPublicAddress is used; the "most public" // address is returned. @@ -2609,13 +2632,14 @@ _, err := s.APIState.Client().PrivateAddress("wordpress") c.Assert(err, gc.ErrorMatches, `unknown unit or machine "wordpress"`) _, err = s.APIState.Client().PrivateAddress("0") - c.Assert(err, gc.ErrorMatches, `machine "0" has no internal address`) + c.Assert(err, gc.ErrorMatches, `error fetching address for machine "0": private no address`) _, err = s.APIState.Client().PrivateAddress("wordpress/0") - c.Assert(err, gc.ErrorMatches, `unit "wordpress/0" has no internal address`) + c.Assert(err, gc.ErrorMatches, `error fetching address for unit "wordpress/0": private no address`) } func (s *clientSuite) TestClientPrivateAddress(c *gc.C) { s.setUpScenario(c) + network.ResetGlobalPreferIPv6() // Internally, network.SelectInternalAddress is used; the public // address if no cloud-local one is available. @@ -3218,6 +3242,7 @@ // Check that the store's test mode is enabled when calling AddCharm. curl, _ = s.UploadCharm(c, "utopic/riak-42", "riak") err = s.APIState.Client().AddCharm(curl) + c.Assert(err, jc.ErrorIsNil) c.Assert(repo.testMode, jc.IsTrue) } @@ -3676,3 +3701,15 @@ endpoints := []string{"wordpress", "mysql"} s.assertDestroyRelation(c, endpoints) } + +func (s *clientSuite) TestDestroyEnvironment(c *gc.C) { + // The full tests for DestroyEnvironment are in environmentmanager. + // Here we just test that things are hooked up such that we can destroy + // the environment through the client endpoint to support older juju clients. + err := s.APIState.Client().DestroyEnvironment() + c.Assert(err, jc.ErrorIsNil) + + env, err := s.State.Environment() + c.Assert(err, jc.ErrorIsNil) + c.Assert(env.Life(), gc.Equals, state.Dying) +} === removed file 'src/github.com/juju/juju/apiserver/client/destroy.go' --- src/github.com/juju/juju/apiserver/client/destroy.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/client/destroy.go 1970-01-01 00:00:00 +0000 @@ -1,92 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package client - -import ( - "github.com/juju/errors" - - "github.com/juju/juju/environs" - "github.com/juju/juju/instance" - "github.com/juju/juju/state" -) - -// DestroyEnvironment destroys all services and non-manager machine -// instances in the environment. -func (c *Client) DestroyEnvironment() (err error) { - if err = c.check.DestroyAllowed(); err != nil { - return errors.Trace(err) - } - - env, err := c.api.state.Environment() - if err != nil { - return errors.Trace(err) - } - - if err = env.Destroy(); err != nil { - return errors.Trace(err) - } - - machines, err := c.api.state.AllMachines() - if err != nil { - return errors.Trace(err) - } - - // We must destroy instances server-side to support JES (Juju Environment - // Server), as there's no CLI to fall back on. In that case, we only ever - // destroy non-state machines; we leave destroying state servers in non- - // hosted environments to the CLI, as otherwise the API server may get cut - // off. - if err := destroyInstances(c.api.state, machines); err != nil { - return errors.Trace(err) - } - - // If this is not the state server environment, remove all documents from - // state associated with the environment. - if env.UUID() != env.ServerTag().Id() { - return errors.Trace(c.api.state.RemoveAllEnvironDocs()) - } - - // Return to the caller. If it's the CLI, it will finish up - // by calling the provider's Destroy method, which will - // destroy the state servers, any straggler instances, and - // other provider-specific resources. - return nil -} - -// destroyInstances directly destroys all non-manager, -// non-manual machine instances. -func destroyInstances(st *state.State, machines []*state.Machine) error { - var ids []instance.Id - for _, m := range machines { - if m.IsManager() { - continue - } - if _, isContainer := m.ParentId(); isContainer { - continue - } - manual, err := m.IsManual() - if manual { - continue - } else if err != nil { - return err - } - id, err := m.InstanceId() - if err != nil { - continue - } - ids = append(ids, id) - } - if len(ids) == 0 { - return nil - } - envcfg, err := st.EnvironConfig() - if err != nil { - return err - } - env, err := environs.New(envcfg) - if err != nil { - return err - } - return env.StopInstances(ids...) -} === removed file 'src/github.com/juju/juju/apiserver/client/destroy_test.go' --- src/github.com/juju/juju/apiserver/client/destroy_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/client/destroy_test.go 1970-01-01 00:00:00 +0000 @@ -1,238 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package client_test - -import ( - "fmt" - - "github.com/juju/errors" - "github.com/juju/names" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/apiserver/client" - "github.com/juju/juju/apiserver/common" - apiservertesting "github.com/juju/juju/apiserver/testing" - "github.com/juju/juju/environs" - "github.com/juju/juju/instance" - "github.com/juju/juju/juju/testing" - "github.com/juju/juju/provider/dummy" - "github.com/juju/juju/state" - jujutesting "github.com/juju/juju/testing" - "github.com/juju/juju/testing/factory" -) - -type destroyEnvironmentSuite struct { - baseSuite -} - -var _ = gc.Suite(&destroyEnvironmentSuite{}) - -// setUpManual adds "manually provisioned" machines to state: -// one manager machine, and one non-manager. -func (s *destroyEnvironmentSuite) setUpManual(c *gc.C) (m0, m1 *state.Machine) { - m0, err := s.State.AddMachine("precise", state.JobManageEnviron) - c.Assert(err, jc.ErrorIsNil) - err = m0.SetProvisioned(instance.Id("manual:0"), "manual:0:fake_nonce", nil) - c.Assert(err, jc.ErrorIsNil) - m1, err = s.State.AddMachine("precise", state.JobHostUnits) - c.Assert(err, jc.ErrorIsNil) - err = m1.SetProvisioned(instance.Id("manual:1"), "manual:1:fake_nonce", nil) - c.Assert(err, jc.ErrorIsNil) - return m0, m1 -} - -// setUpInstances adds machines to state backed by instances: -// one manager machine, one non-manager, and a container in the -// non-manager. -func (s *destroyEnvironmentSuite) setUpInstances(c *gc.C) (m0, m1, m2 *state.Machine) { - m0, err := s.State.AddMachine("precise", state.JobManageEnviron) - c.Assert(err, jc.ErrorIsNil) - inst, _ := testing.AssertStartInstance(c, s.Environ, m0.Id()) - err = m0.SetProvisioned(inst.Id(), "fake_nonce", nil) - c.Assert(err, jc.ErrorIsNil) - - m1, err = s.State.AddMachine("precise", state.JobHostUnits) - c.Assert(err, jc.ErrorIsNil) - inst, _ = testing.AssertStartInstance(c, s.Environ, m1.Id()) - err = m1.SetProvisioned(inst.Id(), "fake_nonce", nil) - c.Assert(err, jc.ErrorIsNil) - - m2, err = s.State.AddMachineInsideMachine(state.MachineTemplate{ - Series: "precise", - Jobs: []state.MachineJob{state.JobHostUnits}, - }, m1.Id(), instance.LXC) - c.Assert(err, jc.ErrorIsNil) - err = m2.SetProvisioned("container0", "fake_nonce", nil) - c.Assert(err, jc.ErrorIsNil) - - return m0, m1, m2 -} - -func (s *destroyEnvironmentSuite) TestDestroyEnvironmentManual(c *gc.C) { - _, nonManager := s.setUpManual(c) - - // If there are any non-manager manual machines in state, DestroyEnvironment will - // error. It will not set the Dying flag on the environment. - err := s.APIState.Client().DestroyEnvironment() - c.Assert(err, gc.ErrorMatches, fmt.Sprintf("failed to destroy environment: manually provisioned machines must first be destroyed with `juju destroy-machine %s`", nonManager.Id())) - env, err := s.State.Environment() - c.Assert(err, jc.ErrorIsNil) - c.Assert(env.Life(), gc.Equals, state.Alive) - - // If we remove the non-manager machine, it should pass. - // Manager machines will remain. - err = nonManager.EnsureDead() - c.Assert(err, jc.ErrorIsNil) - err = nonManager.Remove() - c.Assert(err, jc.ErrorIsNil) - err = s.APIState.Client().DestroyEnvironment() - c.Assert(err, jc.ErrorIsNil) - err = env.Refresh() - c.Assert(err, jc.ErrorIsNil) - c.Assert(env.Life(), gc.Equals, state.Dying) -} - -func (s *destroyEnvironmentSuite) TestDestroyEnvironment(c *gc.C) { - manager, nonManager, _ := s.setUpInstances(c) - managerId, _ := manager.InstanceId() - nonManagerId, _ := nonManager.InstanceId() - - instances, err := s.Environ.Instances([]instance.Id{managerId, nonManagerId}) - c.Assert(err, jc.ErrorIsNil) - for _, inst := range instances { - c.Assert(inst, gc.NotNil) - } - - services, err := s.State.AllServices() - c.Assert(err, jc.ErrorIsNil) - - err = s.APIState.Client().DestroyEnvironment() - c.Assert(err, jc.ErrorIsNil) - - // After DestroyEnvironment returns, we should have: - // - all non-manager instances stopped - instances, err = s.Environ.Instances([]instance.Id{managerId, nonManagerId}) - c.Assert(err, gc.Equals, environs.ErrPartialInstances) - c.Assert(instances[0], gc.NotNil) - c.Assert(instances[1], jc.ErrorIsNil) - // - all services in state are Dying or Dead (or removed altogether), - // after running the state Cleanups. - needsCleanup, err := s.State.NeedsCleanup() - c.Assert(err, jc.ErrorIsNil) - c.Assert(needsCleanup, jc.IsTrue) - err = s.State.Cleanup() - c.Assert(err, jc.ErrorIsNil) - for _, s := range services { - err = s.Refresh() - if err != nil { - c.Assert(err, jc.Satisfies, errors.IsNotFound) - } else { - c.Assert(s.Life(), gc.Not(gc.Equals), state.Alive) - } - } - // - environment is Dying - env, err := s.State.Environment() - c.Assert(err, jc.ErrorIsNil) - c.Assert(env.Life(), gc.Equals, state.Dying) -} - -func (s *destroyEnvironmentSuite) TestDestroyEnvironmentWithContainers(c *gc.C) { - ops := make(chan dummy.Operation, 500) - dummy.Listen(ops) - - _, nonManager, _ := s.setUpInstances(c) - nonManagerId, _ := nonManager.InstanceId() - - err := s.APIState.Client().DestroyEnvironment() - c.Assert(err, jc.ErrorIsNil) - for op := range ops { - if op, ok := op.(dummy.OpStopInstances); ok { - c.Assert(op.Ids, jc.SameContents, []instance.Id{nonManagerId}) - break - } - } -} - -func (s *destroyEnvironmentSuite) TestBlockDestroyDestroyEnvironment(c *gc.C) { - // Setup environment - s.setUpInstances(c) - s.BlockDestroyEnvironment(c, "TestBlockDestroyDestroyEnvironment") - err := s.APIState.Client().DestroyEnvironment() - s.AssertBlocked(c, err, "TestBlockDestroyDestroyEnvironment") -} - -func (s *destroyEnvironmentSuite) TestBlockRemoveDestroyEnvironment(c *gc.C) { - // Setup environment - s.setUpInstances(c) - s.BlockRemoveObject(c, "TestBlockRemoveDestroyEnvironment") - err := s.APIState.Client().DestroyEnvironment() - s.AssertBlocked(c, err, "TestBlockRemoveDestroyEnvironment") -} - -func (s *destroyEnvironmentSuite) TestBlockChangesDestroyEnvironment(c *gc.C) { - // Setup environment - s.setUpInstances(c) - // lock environment: can't destroy locked environment - s.BlockAllChanges(c, "TestBlockChangesDestroyEnvironment") - err := s.APIState.Client().DestroyEnvironment() - s.AssertBlocked(c, err, "TestBlockChangesDestroyEnvironment") -} - -type destroyTwoEnvironmentsSuite struct { - testing.JujuConnSuite - otherState *state.State - otherEnvOwner names.UserTag - otherEnvClient *client.Client -} - -var _ = gc.Suite(&destroyTwoEnvironmentsSuite{}) - -func (s *destroyTwoEnvironmentsSuite) SetUpTest(c *gc.C) { - s.JujuConnSuite.SetUpTest(c) - s.otherEnvOwner = names.NewUserTag("jess@dummy") - s.otherState = factory.NewFactory(s.State).MakeEnvironment(c, &factory.EnvParams{ - Owner: s.otherEnvOwner, - Prepare: true, - ConfigAttrs: jujutesting.Attrs{ - "state-server": false, - }, - }) - s.AddCleanup(func(*gc.C) { s.otherState.Close() }) - - // get the client for the other environment - auth := apiservertesting.FakeAuthorizer{ - Tag: s.otherEnvOwner, - EnvironManager: false, - } - var err error - s.otherEnvClient, err = client.NewClient(s.otherState, common.NewResources(), auth) - c.Assert(err, jc.ErrorIsNil) -} - -func (s *destroyTwoEnvironmentsSuite) TestCleanupEnvironDocs(c *gc.C) { - otherFactory := factory.NewFactory(s.otherState) - otherFactory.MakeMachine(c, nil) - m := otherFactory.MakeMachine(c, nil) - otherFactory.MakeMachineNested(c, m.Id(), nil) - - err := s.otherEnvClient.DestroyEnvironment() - c.Assert(err, jc.ErrorIsNil) - - _, err = s.otherState.Environment() - c.Assert(errors.IsNotFound(err), jc.IsTrue) - - _, err = s.State.Environment() - c.Assert(err, jc.ErrorIsNil) - c.Assert(s.otherState.EnsureEnvironmentRemoved(), jc.ErrorIsNil) -} - -func (s *destroyTwoEnvironmentsSuite) TestDestroyStateServerAfterNonStateServerIsDestroyed(c *gc.C) { - err := s.APIState.Client().DestroyEnvironment() - c.Assert(err, gc.ErrorMatches, "failed to destroy environment: state server environment cannot be destroyed before all other environments are destroyed") - err = s.otherEnvClient.DestroyEnvironment() - c.Assert(err, jc.ErrorIsNil) - err = s.APIState.Client().DestroyEnvironment() - c.Assert(err, jc.ErrorIsNil) -} === modified file 'src/github.com/juju/juju/apiserver/client/filtering.go' --- src/github.com/juju/juju/apiserver/client/filtering.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/client/filtering.go 2015-10-23 18:29:32 +0000 @@ -166,12 +166,18 @@ } func unitMatchSubnet(u *state.Unit, patterns []string) (bool, bool, error) { - pub, pubOK := u.PublicAddress() - priv, privOK := u.PrivateAddress() - if !pubOK && !privOK { + pub, pubErr := u.PublicAddress() + if pubErr != nil && !network.IsNoAddress(pubErr) { + return true, false, errors.Trace(pubErr) + } + priv, privErr := u.PrivateAddress() + if privErr != nil && !network.IsNoAddress(privErr) { + return true, false, errors.Trace(privErr) + } + if pubErr != nil && privErr != nil { return true, false, nil } - return matchSubnet(patterns, pub, priv) + return matchSubnet(patterns, pub.Value, priv.Value) } func unitMatchPort(u *state.Unit, patterns []string) (bool, bool, error) { @@ -351,7 +357,7 @@ func matchAgentStatus(patterns []string, status state.Status) (bool, bool, error) { oneValidStatus := false for _, p := range patterns { - // If the pattern isn't a valid status, ignore it. + // If the pattern isn't a known status, ignore it. ps := state.Status(p) if !ps.KnownAgentStatus() { continue === modified file 'src/github.com/juju/juju/apiserver/client/perm_test.go' --- src/github.com/juju/juju/apiserver/client/perm_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/client/perm_test.go 2015-10-23 18:29:32 +0000 @@ -64,7 +64,7 @@ // op performs the operation to be tested using the given state // connection. It returns a function that should be used to // undo any changes made by the operation. - op func(c *gc.C, st *api.State, mst *state.State) (reset func(), err error) + op func(c *gc.C, st api.Connection, mst *state.State) (reset func(), err error) allow []names.Tag deny []names.Tag }{{ @@ -189,7 +189,7 @@ } } -func opClientCharmInfo(c *gc.C, st *api.State, mst *state.State) (func(), error) { +func opClientCharmInfo(c *gc.C, st api.Connection, mst *state.State) (func(), error) { info, err := st.Client().CharmInfo("local:quantal/wordpress-3") if err != nil { c.Check(info, gc.IsNil) @@ -214,7 +214,7 @@ return func() {}, nil } -func opClientAddRelation(c *gc.C, st *api.State, mst *state.State) (func(), error) { +func opClientAddRelation(c *gc.C, st api.Connection, mst *state.State) (func(), error) { _, err := st.Client().AddRelation("nosuch1", "nosuch2") if params.IsCodeNotFound(err) { err = nil @@ -222,7 +222,7 @@ return func() {}, err } -func opClientDestroyRelation(c *gc.C, st *api.State, mst *state.State) (func(), error) { +func opClientDestroyRelation(c *gc.C, st api.Connection, mst *state.State) (func(), error) { err := st.Client().DestroyRelation("nosuch1", "nosuch2") if params.IsCodeNotFound(err) { err = nil @@ -230,7 +230,7 @@ return func() {}, err } -func opClientStatus(c *gc.C, st *api.State, mst *state.State) (func(), error) { +func opClientStatus(c *gc.C, st api.Connection, mst *state.State) (func(), error) { status, err := st.Client().Status(nil) if err != nil { c.Check(status, gc.IsNil) @@ -241,7 +241,7 @@ return func() {}, nil } -func resetBlogTitle(c *gc.C, st *api.State) func() { +func resetBlogTitle(c *gc.C, st api.Connection) func() { return func() { err := st.Client().ServiceSet("wordpress", map[string]string{ "blog-title": "", @@ -250,7 +250,7 @@ } } -func opClientServiceSet(c *gc.C, st *api.State, mst *state.State) (func(), error) { +func opClientServiceSet(c *gc.C, st api.Connection, mst *state.State) (func(), error) { err := st.Client().ServiceSet("wordpress", map[string]string{ "blog-title": "foo", }) @@ -260,7 +260,7 @@ return resetBlogTitle(c, st), nil } -func opClientServiceSetYAML(c *gc.C, st *api.State, mst *state.State) (func(), error) { +func opClientServiceSetYAML(c *gc.C, st api.Connection, mst *state.State) (func(), error) { err := st.Client().ServiceSetYAML("wordpress", `"wordpress": {"blog-title": "foo"}`) if err != nil { return func() {}, err @@ -268,7 +268,7 @@ return resetBlogTitle(c, st), nil } -func opClientServiceGet(c *gc.C, st *api.State, mst *state.State) (func(), error) { +func opClientServiceGet(c *gc.C, st api.Connection, mst *state.State) (func(), error) { _, err := st.Client().ServiceGet("wordpress") if err != nil { return func() {}, err @@ -276,7 +276,7 @@ return func() {}, nil } -func opClientServiceExpose(c *gc.C, st *api.State, mst *state.State) (func(), error) { +func opClientServiceExpose(c *gc.C, st api.Connection, mst *state.State) (func(), error) { err := st.Client().ServiceExpose("wordpress") if err != nil { return func() {}, err @@ -288,7 +288,7 @@ }, nil } -func opClientServiceUnexpose(c *gc.C, st *api.State, mst *state.State) (func(), error) { +func opClientServiceUnexpose(c *gc.C, st api.Connection, mst *state.State) (func(), error) { err := st.Client().ServiceUnexpose("wordpress") if err != nil { return func() {}, err @@ -296,7 +296,7 @@ return func() {}, nil } -func opClientResolved(c *gc.C, st *api.State, _ *state.State) (func(), error) { +func opClientResolved(c *gc.C, st api.Connection, _ *state.State) (func(), error) { err := st.Client().Resolved("wordpress/1", false) // There are several scenarios in which this test is called, one is // that the user is not authorized. In that case we want to exit now, @@ -314,7 +314,7 @@ return func() {}, nil } -func opClientGetAnnotations(c *gc.C, st *api.State, mst *state.State) (func(), error) { +func opClientGetAnnotations(c *gc.C, st api.Connection, mst *state.State) (func(), error) { ann, err := st.Client().GetAnnotations("service-wordpress") if err != nil { return func() {}, err @@ -323,7 +323,7 @@ return func() {}, nil } -func opClientSetAnnotations(c *gc.C, st *api.State, mst *state.State) (func(), error) { +func opClientSetAnnotations(c *gc.C, st api.Connection, mst *state.State) (func(), error) { pairs := map[string]string{"key1": "value1", "key2": "value2"} err := st.Client().SetAnnotations("service-wordpress", pairs) if err != nil { @@ -335,7 +335,7 @@ }, nil } -func opClientServiceDeploy(c *gc.C, st *api.State, mst *state.State) (func(), error) { +func opClientServiceDeploy(c *gc.C, st api.Connection, mst *state.State) (func(), error) { err := st.Client().ServiceDeploy("mad:bad/url-1", "x", 1, "", constraints.Value{}, "") if err.Error() == `charm URL has invalid schema: "mad:bad/url-1"` { err = nil @@ -343,7 +343,7 @@ return func() {}, err } -func opClientServiceDeployWithNetworks(c *gc.C, st *api.State, mst *state.State) (func(), error) { +func opClientServiceDeployWithNetworks(c *gc.C, st api.Connection, mst *state.State) (func(), error) { err := st.Client().ServiceDeployWithNetworks("mad:bad/url-1", "x", 1, "", constraints.Value{}, "", nil) if err.Error() == `charm URL has invalid schema: "mad:bad/url-1"` { err = nil @@ -351,7 +351,7 @@ return func() {}, err } -func opClientServiceUpdate(c *gc.C, st *api.State, mst *state.State) (func(), error) { +func opClientServiceUpdate(c *gc.C, st api.Connection, mst *state.State) (func(), error) { args := params.ServiceUpdate{ ServiceName: "no-such-charm", CharmUrl: "cs:quantal/wordpress-42", @@ -366,7 +366,7 @@ return func() {}, err } -func opClientServiceSetCharm(c *gc.C, st *api.State, mst *state.State) (func(), error) { +func opClientServiceSetCharm(c *gc.C, st api.Connection, mst *state.State) (func(), error) { err := st.Client().ServiceSetCharm("nosuch", "local:quantal/wordpress", false) if params.IsCodeNotFound(err) { err = nil @@ -374,7 +374,7 @@ return func() {}, err } -func opClientAddServiceUnits(c *gc.C, st *api.State, mst *state.State) (func(), error) { +func opClientAddServiceUnits(c *gc.C, st api.Connection, mst *state.State) (func(), error) { _, err := st.Client().AddServiceUnits("nosuch", 1, "") if params.IsCodeNotFound(err) { err = nil @@ -382,7 +382,7 @@ return func() {}, err } -func opClientDestroyServiceUnits(c *gc.C, st *api.State, mst *state.State) (func(), error) { +func opClientDestroyServiceUnits(c *gc.C, st api.Connection, mst *state.State) (func(), error) { err := st.Client().DestroyServiceUnits("wordpress/99") if err != nil && strings.HasPrefix(err.Error(), "no units were destroyed") { err = nil @@ -390,7 +390,7 @@ return func() {}, err } -func opClientServiceDestroy(c *gc.C, st *api.State, mst *state.State) (func(), error) { +func opClientServiceDestroy(c *gc.C, st api.Connection, mst *state.State) (func(), error) { err := st.Client().ServiceDestroy("non-existent") if params.IsCodeNotFound(err) { err = nil @@ -398,12 +398,12 @@ return func() {}, err } -func opClientGetServiceConstraints(c *gc.C, st *api.State, mst *state.State) (func(), error) { +func opClientGetServiceConstraints(c *gc.C, st api.Connection, mst *state.State) (func(), error) { _, err := st.Client().GetServiceConstraints("wordpress") return func() {}, err } -func opClientSetServiceConstraints(c *gc.C, st *api.State, mst *state.State) (func(), error) { +func opClientSetServiceConstraints(c *gc.C, st api.Connection, mst *state.State) (func(), error) { nullConstraints := constraints.Value{} err := st.Client().SetServiceConstraints("wordpress", nullConstraints) if err != nil { @@ -412,7 +412,7 @@ return func() {}, nil } -func opClientSetEnvironmentConstraints(c *gc.C, st *api.State, mst *state.State) (func(), error) { +func opClientSetEnvironmentConstraints(c *gc.C, st api.Connection, mst *state.State) (func(), error) { nullConstraints := constraints.Value{} err := st.Client().SetEnvironmentConstraints(nullConstraints) if err != nil { @@ -421,7 +421,7 @@ return func() {}, nil } -func opClientEnvironmentGet(c *gc.C, st *api.State, mst *state.State) (func(), error) { +func opClientEnvironmentGet(c *gc.C, st api.Connection, mst *state.State) (func(), error) { _, err := st.Client().EnvironmentGet() if err != nil { return func() {}, err @@ -429,7 +429,7 @@ return func() {}, nil } -func opClientEnvironmentSet(c *gc.C, st *api.State, mst *state.State) (func(), error) { +func opClientEnvironmentSet(c *gc.C, st api.Connection, mst *state.State) (func(), error) { args := map[string]interface{}{"some-key": "some-value"} err := st.Client().EnvironmentSet(args) if err != nil { @@ -441,7 +441,7 @@ }, nil } -func opClientSetEnvironAgentVersion(c *gc.C, st *api.State, mst *state.State) (func(), error) { +func opClientSetEnvironAgentVersion(c *gc.C, st api.Connection, mst *state.State) (func(), error) { attrs, err := st.Client().EnvironmentGet() if err != nil { return func() {}, err @@ -460,7 +460,7 @@ }, nil } -func opClientWatchAll(c *gc.C, st *api.State, mst *state.State) (func(), error) { +func opClientWatchAll(c *gc.C, st api.Connection, mst *state.State) (func(), error) { watcher, err := st.Client().WatchAll() if err == nil { watcher.Stop() === modified file 'src/github.com/juju/juju/apiserver/client/run.go' --- src/github.com/juju/juju/apiserver/client/run.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/apiserver/client/run.go 2015-10-23 18:29:32 +0000 @@ -28,7 +28,7 @@ // by the function that actually tries to execute the command. func remoteParamsForMachine(machine *state.Machine, command string, timeout time.Duration) *RemoteExec { // magic boolean parameters are bad :-( - address := network.SelectInternalAddress(machine.Addresses(), false) + address, ok := network.SelectInternalAddress(machine.Addresses(), false) execParams := &RemoteExec{ ExecParams: ssh.ExecParams{ Command: command, @@ -36,8 +36,8 @@ }, MachineId: machine.Id(), } - if address != "" { - execParams.Host = fmt.Sprintf("ubuntu@%s", address) + if ok { + execParams.Host = fmt.Sprintf("ubuntu@%s", address.Value) } return execParams } === modified file 'src/github.com/juju/juju/apiserver/client/status.go' --- src/github.com/juju/juju/apiserver/client/status.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/client/status.go 2015-10-23 18:29:32 +0000 @@ -13,7 +13,6 @@ "gopkg.in/juju/charm.v5" "gopkg.in/juju/charm.v5/hooks" - "github.com/juju/juju/api" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/constraints" "github.com/juju/juju/network" @@ -22,10 +21,10 @@ "github.com/juju/juju/worker/uniter/operation" ) -func agentStatusFromStatusInfo(s []state.StatusInfo, kind params.HistoryKind) []api.AgentStatus { - result := []api.AgentStatus{} +func agentStatusFromStatusInfo(s []state.StatusInfo, kind params.HistoryKind) []params.AgentStatus { + result := []params.AgentStatus{} for _, v := range s { - result = append(result, api.AgentStatus{ + result = append(result, params.AgentStatus{ Status: params.Status(v.Status), Info: v.Message, Data: v.Data, @@ -37,7 +36,7 @@ } -type sortableStatuses []api.AgentStatus +type sortableStatuses []params.AgentStatus func (s sortableStatuses) Len() int { return len(s) @@ -51,25 +50,25 @@ // TODO(perrito666) this client method requires more testing, only its parts are unittested. // UnitStatusHistory returns a slice of past statuses for a given unit. -func (c *Client) UnitStatusHistory(args params.StatusHistory) (api.UnitStatusHistory, error) { +func (c *Client) UnitStatusHistory(args params.StatusHistory) (params.UnitStatusHistory, error) { size := args.Size - 1 if size < 1 { - return api.UnitStatusHistory{}, errors.Errorf("invalid history size: %d", args.Size) + return params.UnitStatusHistory{}, errors.Errorf("invalid history size: %d", args.Size) } unit, err := c.api.state.Unit(args.Name) if err != nil { - return api.UnitStatusHistory{}, errors.Trace(err) + return params.UnitStatusHistory{}, errors.Trace(err) } - statuses := api.UnitStatusHistory{} + statuses := params.UnitStatusHistory{} if args.Kind == params.KindCombined || args.Kind == params.KindWorkload { unitStatuses, err := unit.StatusHistory(size) if err != nil { - return api.UnitStatusHistory{}, errors.Trace(err) + return params.UnitStatusHistory{}, errors.Trace(err) } current, err := unit.Status() if err != nil { - return api.UnitStatusHistory{}, errors.Trace(err) + return params.UnitStatusHistory{}, errors.Trace(err) } unitStatuses = append(unitStatuses, current) @@ -79,12 +78,12 @@ agent := unit.Agent() agentStatuses, err := agent.StatusHistory(size) if err != nil { - return api.UnitStatusHistory{}, errors.Trace(err) + return params.UnitStatusHistory{}, errors.Trace(err) } current, err := agent.Status() if err != nil { - return api.UnitStatusHistory{}, errors.Trace(err) + return params.UnitStatusHistory{}, errors.Trace(err) } agentStatuses = append(agentStatuses, current) @@ -103,12 +102,12 @@ } // FullStatus gives the information needed for juju status over the api -func (c *Client) FullStatus(args params.StatusParams) (api.Status, error) { +func (c *Client) FullStatus(args params.StatusParams) (params.FullStatus, error) { cfg, err := c.api.state.EnvironConfig() if err != nil { - return api.Status{}, errors.Annotate(err, "could not get environ config") + return params.FullStatus{}, errors.Annotate(err, "could not get environ config") } - var noStatus api.Status + var noStatus params.FullStatus var context statusContext if context.services, context.units, context.latestCharms, err = fetchAllServicesAndUnits(c.api.state, len(args.Patterns) <= 0); err != nil { @@ -193,12 +192,13 @@ context.machines[status] = filteredList } } + newToolsVersion, err := c.newToolsVersionAvailable() if err != nil { return noStatus, errors.Annotate(err, "cannot determine if there is a new tools version available") } - return api.Status{ + return params.FullStatus{ EnvironmentName: cfg.Name(), AvailableVersion: newToolsVersion, Machines: processMachines(context.machines), @@ -233,16 +233,16 @@ } // Status is a stub version of FullStatus that was introduced in 1.16 -func (c *Client) Status() (api.LegacyStatus, error) { - var legacyStatus api.LegacyStatus +func (c *Client) Status() (params.LegacyStatus, error) { + var legacyStatus params.LegacyStatus status, err := c.FullStatus(params.StatusParams{}) if err != nil { return legacyStatus, err } - legacyStatus.Machines = make(map[string]api.LegacyMachineStatus) + legacyStatus.Machines = make(map[string]params.LegacyMachineStatus) for machineName, machineStatus := range status.Machines { - legacyStatus.Machines[machineName] = api.LegacyMachineStatus{ + legacyStatus.Machines[machineName] = params.LegacyMachineStatus{ InstanceId: string(machineStatus.InstanceId), } } @@ -406,9 +406,9 @@ return m[id][1:] } -func processMachines(idToMachines map[string][]*state.Machine) map[string]api.MachineStatus { - machinesMap := make(map[string]api.MachineStatus) - cache := make(map[string]api.MachineStatus) +func processMachines(idToMachines map[string][]*state.Machine) map[string]params.MachineStatus { + machinesMap := make(map[string]params.MachineStatus) + cache := make(map[string]params.MachineStatus) for id, machines := range idToMachines { if len(machines) <= 0 { @@ -434,7 +434,7 @@ return machinesMap } -func makeMachineStatus(machine *state.Machine) (status api.MachineStatus) { +func makeMachineStatus(machine *state.Machine) (status params.MachineStatus) { status.Id = machine.Id() agentStatus, compatStatus := processMachine(machine) status.Agent = agentStatus @@ -457,7 +457,14 @@ if err != nil { status.InstanceState = "error" } - status.DNSName = network.SelectPublicAddress(machine.Addresses()) + addr, err := machine.PublicAddress() + if err != nil { + // Usually this indicates that no addresses have been set on the + // machine yet. + addr = network.Address{} + logger.Warningf("error fetching public address: %q", err) + } + status.DNSName = addr.Value } else { if errors.IsNotProvisioned(err) { status.InstanceId = "pending" @@ -478,19 +485,19 @@ } else { status.Hardware = hc.String() } - status.Containers = make(map[string]api.MachineStatus) + status.Containers = make(map[string]params.MachineStatus) return } -func (context *statusContext) processRelations() []api.RelationStatus { - var out []api.RelationStatus +func (context *statusContext) processRelations() []params.RelationStatus { + var out []params.RelationStatus relations := context.getAllRelations() for _, relation := range relations { - var eps []api.EndpointStatus + var eps []params.EndpointStatus var scope charm.RelationScope var relationInterface string for _, ep := range relation.Endpoints() { - eps = append(eps, api.EndpointStatus{ + eps = append(eps, params.EndpointStatus{ ServiceName: ep.ServiceName, Name: ep.Name, Role: ep.Role, @@ -500,7 +507,7 @@ relationInterface = ep.Interface scope = ep.Scope } - relStatus := api.RelationStatus{ + relStatus := params.RelationStatus{ Id: relation.Id(), Key: relation.String(), Interface: relationInterface, @@ -528,16 +535,16 @@ return out } -func (context *statusContext) processNetworks() map[string]api.NetworkStatus { - networksMap := make(map[string]api.NetworkStatus) +func (context *statusContext) processNetworks() map[string]params.NetworkStatus { + networksMap := make(map[string]params.NetworkStatus) for name, network := range context.networks { networksMap[name] = context.makeNetworkStatus(network) } return networksMap } -func (context *statusContext) makeNetworkStatus(network *state.Network) api.NetworkStatus { - return api.NetworkStatus{ +func (context *statusContext) makeNetworkStatus(network *state.Network) params.NetworkStatus { + return params.NetworkStatus{ ProviderId: network.ProviderId(), CIDR: network.CIDR(), VLANTag: network.VLANTag(), @@ -565,15 +572,15 @@ return paramsJobs } -func (context *statusContext) processServices() map[string]api.ServiceStatus { - servicesMap := make(map[string]api.ServiceStatus) +func (context *statusContext) processServices() map[string]params.ServiceStatus { + servicesMap := make(map[string]params.ServiceStatus) for _, s := range context.services { servicesMap[s.Name()] = context.processService(s) } return servicesMap } -func (context *statusContext) processService(service *state.Service) (status api.ServiceStatus) { +func (context *statusContext) processService(service *state.Service) (status params.ServiceStatus) { serviceCharmURL, _ := service.CharmURL() status.Charm = serviceCharmURL.String() status.Exposed = service.IsExposed() @@ -603,12 +610,13 @@ return } } + // TODO(dimitern): Drop support for this in a follow-up. if len(networks) > 0 || cons.HaveNetworks() { // Only the explicitly requested networks (using "juju deploy // --networks=...") will be enabled, and altough when // specified, networks constraints will be used for instance // selection, they won't be actually enabled. - status.Networks = api.NetworksSpecification{ + status.Networks = params.NetworksSpecification{ Enabled: networks, Disabled: append(cons.IncludeNetworks(), cons.ExcludeNetworks()...), } @@ -624,21 +632,51 @@ status.Status.Info = serviceStatus.Message status.Status.Data = serviceStatus.Data status.Status.Since = serviceStatus.Since + + status.MeterStatuses = context.processUnitMeterStatuses(context.units[service.Name()]) } return status } -func (context *statusContext) processUnits(units map[string]*state.Unit, serviceCharm string) map[string]api.UnitStatus { - unitsMap := make(map[string]api.UnitStatus) +func isColorStatus(code state.MeterStatusCode) bool { + return code == state.MeterGreen || code == state.MeterAmber || code == state.MeterRed +} + +func (context *statusContext) processUnitMeterStatuses(units map[string]*state.Unit) map[string]params.MeterStatus { + unitsMap := make(map[string]params.MeterStatus) + for _, unit := range units { + status, err := unit.GetMeterStatus() + if err != nil { + continue + } + if isColorStatus(status.Code) { + unitsMap[unit.Name()] = params.MeterStatus{Color: strings.ToLower(status.Code.String()), Message: status.Info} + } + } + if len(unitsMap) > 0 { + return unitsMap + } + return nil +} + +func (context *statusContext) processUnits(units map[string]*state.Unit, serviceCharm string) map[string]params.UnitStatus { + unitsMap := make(map[string]params.UnitStatus) for _, unit := range units { unitsMap[unit.Name()] = context.processUnit(unit, serviceCharm) } return unitsMap } -func (context *statusContext) processUnit(unit *state.Unit, serviceCharm string) api.UnitStatus { - var result api.UnitStatus - result.PublicAddress, _ = unit.PublicAddress() +func (context *statusContext) processUnit(unit *state.Unit, serviceCharm string) params.UnitStatus { + var result params.UnitStatus + addr, err := unit.PublicAddress() + if err != nil { + // Usually this indicates that no addresses have been set on the + // machine yet. + addr = network.Address{} + logger.Warningf("error fetching public address: %v", err) + } + result.PublicAddress = addr.Value unitPorts, _ := unit.OpenedPorts() for _, port := range unitPorts { result.OpenedPorts = append(result.OpenedPorts, port.String()) @@ -653,7 +691,7 @@ processUnitAndAgentStatus(unit, &result) if subUnits := unit.SubordinateNames(); len(subUnits) > 0 { - result.Subordinates = make(map[string]api.UnitStatus) + result.Subordinates = make(map[string]params.UnitStatus) for _, name := range subUnits { subUnit := context.unitByName(name) // subUnit may be nil if subordinate was filtered out. @@ -703,7 +741,7 @@ } // processUnitAndAgentStatus retrieves status information for both unit and unitAgents. -func processUnitAndAgentStatus(unit *state.Unit, status *api.UnitStatus) { +func processUnitAndAgentStatus(unit *state.Unit, status *params.UnitStatus) { status.UnitAgent, status.Workload = processUnitStatus(unit) // Legacy fields required until Juju 2.0. @@ -733,7 +771,7 @@ } // populateStatusFromGetter creates status information for machines, units. -func populateStatusFromGetter(agent *api.AgentStatus, getter state.StatusGetter) { +func populateStatusFromGetter(agent *params.AgentStatus, getter state.StatusGetter) { statusInfo, err := getter.Status() agent.Err = err agent.Status = params.Status(statusInfo.Status) @@ -744,7 +782,7 @@ // processMachine retrieves version and status information for the given machine. // It also returns deprecated legacy status information. -func processMachine(machine *state.Machine) (out api.AgentStatus, compat api.AgentStatus) { +func processMachine(machine *state.Machine) (out params.AgentStatus, compat params.AgentStatus) { out.Life = processLife(machine) if t, err := machine.AgentTools(); err == nil { @@ -795,7 +833,7 @@ } // processUnit retrieves version and status information for the given unit. -func processUnitStatus(unit *state.Unit) (agentStatus, workloadStatus api.AgentStatus) { +func processUnitStatus(unit *state.Unit) (agentStatus, workloadStatus params.AgentStatus) { // First determine the agent status information. unitAgent := unit.Agent() populateStatusFromGetter(&agentStatus, unitAgent) @@ -809,7 +847,7 @@ return } -func canBeLost(status *api.UnitStatus) bool { +func canBeLost(status *params.UnitStatus) bool { // Pending and Installing are deprecated. // Need to still check pending for existing deployments. switch status.UnitAgent.Status { @@ -828,7 +866,7 @@ // processUnitLost determines whether the given unit should be marked as lost. // TODO(fwereade/wallyworld): this is also model-level code and should sit in // between state and this package. -func processUnitLost(unit *state.Unit, status *api.UnitStatus) { +func processUnitLost(unit *state.Unit, status *params.UnitStatus) { if !canBeLost(status) { // The status is allocating or installing - there's no point // in enquiring about the agent liveness. === modified file 'src/github.com/juju/juju/apiserver/client/status_test.go' --- src/github.com/juju/juju/apiserver/client/status_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/client/status_test.go 2015-10-23 18:29:32 +0000 @@ -8,6 +8,7 @@ gc "gopkg.in/check.v1" "github.com/juju/juju/apiserver/client" + "github.com/juju/juju/apiserver/params" "github.com/juju/juju/instance" "github.com/juju/juju/state" "github.com/juju/juju/testing/factory" @@ -89,7 +90,6 @@ } func (s *statusUnitTestSuite) TestProcessMachinesWithEmbeddedContainers(c *gc.C) { - host := s.MakeMachine(c, &factory.MachineParams{InstanceId: instance.Id("1")}) lxcHost := s.MakeMachineNested(c, host.Id(), nil) machines := map[string][]*state.Machine{ @@ -108,3 +108,59 @@ c.Check(hostContainer, gc.HasLen, 2) c.Check(hostContainer[lxcHost.Id()].Containers, gc.HasLen, 1) } + +var testUnits = []struct { + unitName string + setStatus *state.MeterStatus + expectedStatus *params.MeterStatus +}{{ + setStatus: &state.MeterStatus{Code: state.MeterGreen, Info: "test information"}, + expectedStatus: ¶ms.MeterStatus{Color: "green", Message: "test information"}, +}, { + setStatus: &state.MeterStatus{Code: state.MeterAmber, Info: "test information"}, + expectedStatus: ¶ms.MeterStatus{Color: "amber", Message: "test information"}, +}, { + setStatus: &state.MeterStatus{Code: state.MeterRed, Info: "test information"}, + expectedStatus: ¶ms.MeterStatus{Color: "red", Message: "test information"}, +}, { + setStatus: &state.MeterStatus{Code: state.MeterGreen, Info: "test information"}, + expectedStatus: ¶ms.MeterStatus{Color: "green", Message: "test information"}, +}, {}, +} + +func (s *statusUnitTestSuite) TestMeterStatus(c *gc.C) { + service := s.MakeService(c, nil) + + units, err := service.AllUnits() + c.Assert(err, jc.ErrorIsNil) + c.Assert(units, gc.HasLen, 0) + + for i, unit := range testUnits { + u, err := service.AddUnit() + testUnits[i].unitName = u.Name() + c.Assert(err, jc.ErrorIsNil) + if unit.setStatus != nil { + err := u.SetMeterStatus(unit.setStatus.Code.String(), unit.setStatus.Info) + c.Assert(err, jc.ErrorIsNil) + } + } + + client := s.APIState.Client() + status, err := client.Status(nil) + c.Assert(err, jc.ErrorIsNil) + c.Assert(status, gc.NotNil) + serviceStatus, ok := status.Services[service.Name()] + c.Assert(ok, gc.Equals, true) + + c.Assert(serviceStatus.MeterStatuses, gc.HasLen, len(testUnits)-1) + for _, unit := range testUnits { + unitStatus, ok := serviceStatus.MeterStatuses[unit.unitName] + + if unit.expectedStatus != nil { + c.Assert(ok, gc.Equals, true) + c.Assert(&unitStatus, gc.DeepEquals, unit.expectedStatus) + } else { + c.Assert(ok, gc.Equals, false) + } + } +} === modified file 'src/github.com/juju/juju/apiserver/common/block_test.go' --- src/github.com/juju/juju/apiserver/common/block_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/common/block_test.go 2015-10-23 18:29:32 +0000 @@ -16,6 +16,7 @@ ) type mockBlock struct { + state.Block t state.BlockType m string } @@ -28,6 +29,8 @@ func (m mockBlock) Message() string { return m.m } +func (m mockBlock) EnvUUID() string { return "" } + type blockCheckerSuite struct { testing.FakeJujuHomeSuite aBlock state.Block === modified file 'src/github.com/juju/juju/apiserver/common/blockdevices.go' --- src/github.com/juju/juju/apiserver/common/blockdevices.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/common/blockdevices.go 2015-10-23 18:29:32 +0000 @@ -13,6 +13,7 @@ func BlockDeviceFromState(in state.BlockDeviceInfo) storage.BlockDevice { return storage.BlockDevice{ in.DeviceName, + in.DeviceLinks, in.Label, in.UUID, in.HardwareId, @@ -36,11 +37,23 @@ if volumeInfo.HardwareId == dev.HardwareId { return &dev, true } - } else if attachmentInfo.BusAddress != "" { + continue + } + if attachmentInfo.BusAddress != "" { if attachmentInfo.BusAddress == dev.BusAddress { return &dev, true } - } else if attachmentInfo.DeviceName == dev.DeviceName { + continue + } + if attachmentInfo.DeviceLink != "" { + for _, link := range dev.DeviceLinks { + if attachmentInfo.DeviceLink == link { + return &dev, true + } + } + continue + } + if attachmentInfo.DeviceName == dev.DeviceName { return &dev, true } } === modified file 'src/github.com/juju/juju/apiserver/common/common_test.go' --- src/github.com/juju/juju/apiserver/common/common_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/common/common_test.go 2015-10-23 18:29:32 +0000 @@ -9,6 +9,7 @@ "github.com/juju/names" jc "github.com/juju/testing/checkers" + "github.com/juju/utils" gc "gopkg.in/check.v1" "github.com/juju/juju/apiserver/common" @@ -117,3 +118,60 @@ func u(unit string) names.Tag { return names.NewUnitTag(unit) } func serviceTag(service string) names.Tag { return names.NewServiceTag(service) } func m(machine string) names.Tag { return names.NewMachineTag(machine) } + +func (s *commonSuite) TestAuthFuncForTagKind(c *gc.C) { + // TODO(dimitern): This list of all supported tags and kinds needs + // to live in juju/names. + uuid, err := utils.NewUUID() + c.Assert(err, jc.ErrorIsNil) + + allTags := []names.Tag{ + nil, // invalid tag + names.NewActionTag(uuid.String()), + names.NewCharmTag("cs:precise/missing"), + names.NewEnvironTag(uuid.String()), + names.NewFilesystemTag("20/20"), + names.NewLocalUserTag("user"), + names.NewMachineTag("42"), + names.NewNetworkTag("public"), + names.NewRelationTag("wordpress:mysql mysql:db"), + names.NewServiceTag("wordpress"), + names.NewSpaceTag("apps"), + names.NewStorageTag("foo/42"), + names.NewUnitTag("wordpress/5"), + names.NewUserTag("joe"), + names.NewVolumeTag("80/20"), + } + for i, allowedTag := range allTags { + c.Logf("test #%d: allowedTag: %v", i, allowedTag) + + var allowedKind string + if allowedTag != nil { + allowedKind = allowedTag.Kind() + } + getAuthFunc := common.AuthFuncForTagKind(allowedKind) + + authFunc, err := getAuthFunc() + if allowedKind == "" { + c.Check(err, gc.ErrorMatches, "tag kind cannot be empty") + c.Check(authFunc, gc.IsNil) + continue + } else if !c.Check(err, jc.ErrorIsNil) { + continue + } + + for j, givenTag := range allTags { + c.Logf("test #%d.%d: givenTag: %v", i, j, givenTag) + + var givenKind string + if givenTag != nil { + givenKind = givenTag.Kind() + } + if allowedKind == givenKind { + c.Check(authFunc(givenTag), jc.IsTrue) + } else { + c.Check(authFunc(givenTag), jc.IsFalse) + } + } + } +} === added file 'src/github.com/juju/juju/apiserver/common/environdestroy.go' --- src/github.com/juju/juju/apiserver/common/environdestroy.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/common/environdestroy.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,116 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package common + +import ( + "github.com/juju/errors" + "github.com/juju/names" + + "github.com/juju/juju/apiserver/metricsender" + "github.com/juju/juju/environs" + "github.com/juju/juju/instance" + "github.com/juju/juju/state" +) + +var sendMetrics = func(st *state.State) error { + err := metricsender.SendMetrics(st, metricsender.DefaultMetricSender(), metricsender.DefaultMaxBatchesPerSend()) + return errors.Trace(err) +} + +// DestroyEnvironment destroys all services and non-manager machine +// instances in the specified environment. This function assumes that all +// necessary authentication checks have been done. +func DestroyEnvironment(st *state.State, environTag names.EnvironTag) error { + var err error + if environTag != st.EnvironTag() { + if st, err = st.ForEnviron(environTag); err != nil { + return errors.Trace(err) + } + defer st.Close() + } + + check := NewBlockChecker(st) + if err = check.DestroyAllowed(); err != nil { + return errors.Trace(err) + } + + env, err := st.Environment() + if err != nil { + return errors.Trace(err) + } + + if err = env.Destroy(); err != nil { + return errors.Trace(err) + } + + machines, err := st.AllMachines() + if err != nil { + return errors.Trace(err) + } + + err = sendMetrics(st) + if err != nil { + logger.Warningf("failed to send leftover metrics: %v", err) + } + + // We must destroy instances server-side to support JES (Juju Environment + // Server), as there's no CLI to fall back on. In that case, we only ever + // destroy non-state machines; we leave destroying state servers in non- + // hosted environments to the CLI, as otherwise the API server may get cut + // off. + if err := destroyNonManagerMachines(st, machines); err != nil { + return errors.Trace(err) + } + + // If this is not the state server environment, remove all documents from + // state associated with the environment. + if env.EnvironTag() != env.ServerTag() { + return errors.Trace(st.RemoveAllEnvironDocs()) + } + + // Return to the caller. If it's the CLI, it will finish up + // by calling the provider's Destroy method, which will + // destroy the state servers, any straggler instances, and + // other provider-specific resources. + return nil +} + +// destroyNonManagerMachines directly destroys all non-manager, non-manual +// machine instances. +func destroyNonManagerMachines(st *state.State, machines []*state.Machine) error { + var ids []instance.Id + for _, m := range machines { + if m.IsManager() { + continue + } + if _, isContainer := m.ParentId(); isContainer { + continue + } + manual, err := m.IsManual() + if err != nil { + return err + } else if manual { + continue + } + // There is a possible race here if a machine is being + // provisioned, but hasn't yet come up. + id, err := m.InstanceId() + if err != nil { + continue + } + ids = append(ids, id) + } + if len(ids) == 0 { + return nil + } + envcfg, err := st.EnvironConfig() + if err != nil { + return err + } + env, err := environs.New(envcfg) + if err != nil { + return err + } + return env.StopInstances(ids...) +} === added file 'src/github.com/juju/juju/apiserver/common/environdestroy_test.go' --- src/github.com/juju/juju/apiserver/common/environdestroy_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/common/environdestroy_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,312 @@ +// Copyright 2012-2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package common_test + +import ( + "fmt" + + "github.com/juju/errors" + "github.com/juju/names" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/client" + "github.com/juju/juju/apiserver/common" + commontesting "github.com/juju/juju/apiserver/common/testing" + apiservertesting "github.com/juju/juju/apiserver/testing" + "github.com/juju/juju/environs" + "github.com/juju/juju/instance" + "github.com/juju/juju/juju/testing" + "github.com/juju/juju/provider/dummy" + "github.com/juju/juju/state" + jujutesting "github.com/juju/juju/testing" + "github.com/juju/juju/testing/factory" + jtesting "github.com/juju/testing" +) + +type destroyEnvironmentSuite struct { + testing.JujuConnSuite + commontesting.BlockHelper + metricSender *testMetricSender +} + +var _ = gc.Suite(&destroyEnvironmentSuite{}) + +func (s *destroyEnvironmentSuite) SetUpTest(c *gc.C) { + s.JujuConnSuite.SetUpTest(c) + s.BlockHelper = commontesting.NewBlockHelper(s.APIState) + s.AddCleanup(func(*gc.C) { s.BlockHelper.Close() }) + + s.metricSender = &testMetricSender{} + s.PatchValue(common.SendMetrics, s.metricSender.SendMetrics) +} + +// setUpManual adds "manually provisioned" machines to state: +// one manager machine, and one non-manager. +func (s *destroyEnvironmentSuite) setUpManual(c *gc.C) (m0, m1 *state.Machine) { + m0, err := s.State.AddMachine("precise", state.JobManageEnviron) + c.Assert(err, jc.ErrorIsNil) + err = m0.SetProvisioned(instance.Id("manual:0"), "manual:0:fake_nonce", nil) + c.Assert(err, jc.ErrorIsNil) + m1, err = s.State.AddMachine("precise", state.JobHostUnits) + c.Assert(err, jc.ErrorIsNil) + err = m1.SetProvisioned(instance.Id("manual:1"), "manual:1:fake_nonce", nil) + c.Assert(err, jc.ErrorIsNil) + return m0, m1 +} + +// setUpInstances adds machines to state backed by instances: +// one manager machine, one non-manager, and a container in the +// non-manager. +func (s *destroyEnvironmentSuite) setUpInstances(c *gc.C) (m0, m1, m2 *state.Machine) { + m0, err := s.State.AddMachine("precise", state.JobManageEnviron) + c.Assert(err, jc.ErrorIsNil) + inst, _ := testing.AssertStartInstance(c, s.Environ, m0.Id()) + err = m0.SetProvisioned(inst.Id(), "fake_nonce", nil) + c.Assert(err, jc.ErrorIsNil) + + m1, err = s.State.AddMachine("precise", state.JobHostUnits) + c.Assert(err, jc.ErrorIsNil) + inst, _ = testing.AssertStartInstance(c, s.Environ, m1.Id()) + err = m1.SetProvisioned(inst.Id(), "fake_nonce", nil) + c.Assert(err, jc.ErrorIsNil) + + m2, err = s.State.AddMachineInsideMachine(state.MachineTemplate{ + Series: "precise", + Jobs: []state.MachineJob{state.JobHostUnits}, + }, m1.Id(), instance.LXC) + c.Assert(err, jc.ErrorIsNil) + err = m2.SetProvisioned("container0", "fake_nonce", nil) + c.Assert(err, jc.ErrorIsNil) + + return m0, m1, m2 +} + +func (s *destroyEnvironmentSuite) TestDestroyEnvironmentManual(c *gc.C) { + _, nonManager := s.setUpManual(c) + + // If there are any non-manager manual machines in state, DestroyEnvironment will + // error. It will not set the Dying flag on the environment. + err := common.DestroyEnvironment(s.State, s.State.EnvironTag()) + c.Assert(err, gc.ErrorMatches, fmt.Sprintf("failed to destroy environment: manually provisioned machines must first be destroyed with `juju destroy-machine %s`", nonManager.Id())) + env, err := s.State.Environment() + c.Assert(err, jc.ErrorIsNil) + c.Assert(env.Life(), gc.Equals, state.Alive) + + // If we remove the non-manager machine, it should pass. + // Manager machines will remain. + err = nonManager.EnsureDead() + c.Assert(err, jc.ErrorIsNil) + err = nonManager.Remove() + c.Assert(err, jc.ErrorIsNil) + err = common.DestroyEnvironment(s.State, s.State.EnvironTag()) + c.Assert(err, jc.ErrorIsNil) + err = env.Refresh() + c.Assert(err, jc.ErrorIsNil) + c.Assert(env.Life(), gc.Equals, state.Dying) + + s.metricSender.CheckCalls(c, []jtesting.StubCall{{FuncName: "SendMetrics"}}) +} + +func (s *destroyEnvironmentSuite) TestDestroyEnvironment(c *gc.C) { + manager, nonManager, _ := s.setUpInstances(c) + managerId, _ := manager.InstanceId() + nonManagerId, _ := nonManager.InstanceId() + + instances, err := s.Environ.Instances([]instance.Id{managerId, nonManagerId}) + c.Assert(err, jc.ErrorIsNil) + for _, inst := range instances { + c.Assert(inst, gc.NotNil) + } + + services, err := s.State.AllServices() + c.Assert(err, jc.ErrorIsNil) + + err = common.DestroyEnvironment(s.State, s.State.EnvironTag()) + c.Assert(err, jc.ErrorIsNil) + + s.metricSender.CheckCalls(c, []jtesting.StubCall{{FuncName: "SendMetrics"}}) + + // After DestroyEnvironment returns, we should have: + // - all non-manager instances stopped + instances, err = s.Environ.Instances([]instance.Id{managerId, nonManagerId}) + c.Assert(err, gc.Equals, environs.ErrPartialInstances) + c.Assert(instances[0], gc.NotNil) + c.Assert(instances[1], jc.ErrorIsNil) + // - all services in state are Dying or Dead (or removed altogether), + // after running the state Cleanups. + needsCleanup, err := s.State.NeedsCleanup() + c.Assert(err, jc.ErrorIsNil) + c.Assert(needsCleanup, jc.IsTrue) + err = s.State.Cleanup() + c.Assert(err, jc.ErrorIsNil) + for _, s := range services { + err = s.Refresh() + if err != nil { + c.Assert(err, jc.Satisfies, errors.IsNotFound) + } else { + c.Assert(s.Life(), gc.Not(gc.Equals), state.Alive) + } + } + // - environment is Dying + env, err := s.State.Environment() + c.Assert(err, jc.ErrorIsNil) + c.Assert(env.Life(), gc.Equals, state.Dying) +} + +func (s *destroyEnvironmentSuite) TestDestroyEnvironmentWithContainers(c *gc.C) { + ops := make(chan dummy.Operation, 500) + dummy.Listen(ops) + + _, nonManager, _ := s.setUpInstances(c) + nonManagerId, _ := nonManager.InstanceId() + + err := common.DestroyEnvironment(s.State, s.State.EnvironTag()) + c.Assert(err, jc.ErrorIsNil) + for op := range ops { + if op, ok := op.(dummy.OpStopInstances); ok { + c.Assert(op.Ids, jc.SameContents, []instance.Id{nonManagerId}) + break + } + } + + s.metricSender.CheckCalls(c, []jtesting.StubCall{{FuncName: "SendMetrics"}}) +} + +func (s *destroyEnvironmentSuite) TestBlockDestroyDestroyEnvironment(c *gc.C) { + // Setup environment + s.setUpInstances(c) + s.BlockDestroyEnvironment(c, "TestBlockDestroyDestroyEnvironment") + err := common.DestroyEnvironment(s.State, s.State.EnvironTag()) + s.AssertBlocked(c, err, "TestBlockDestroyDestroyEnvironment") + s.metricSender.CheckCalls(c, []jtesting.StubCall{}) +} + +func (s *destroyEnvironmentSuite) TestBlockRemoveDestroyEnvironment(c *gc.C) { + // Setup environment + s.setUpInstances(c) + s.BlockRemoveObject(c, "TestBlockRemoveDestroyEnvironment") + err := common.DestroyEnvironment(s.State, s.State.EnvironTag()) + s.AssertBlocked(c, err, "TestBlockRemoveDestroyEnvironment") + s.metricSender.CheckCalls(c, []jtesting.StubCall{}) +} + +func (s *destroyEnvironmentSuite) TestBlockChangesDestroyEnvironment(c *gc.C) { + // Setup environment + s.setUpInstances(c) + // lock environment: can't destroy locked environment + s.BlockAllChanges(c, "TestBlockChangesDestroyEnvironment") + err := common.DestroyEnvironment(s.State, s.State.EnvironTag()) + s.AssertBlocked(c, err, "TestBlockChangesDestroyEnvironment") + s.metricSender.CheckCalls(c, []jtesting.StubCall{}) +} + +type destroyTwoEnvironmentsSuite struct { + testing.JujuConnSuite + otherState *state.State + otherEnvOwner names.UserTag + otherEnvClient *client.Client + metricSender *testMetricSender +} + +var _ = gc.Suite(&destroyTwoEnvironmentsSuite{}) + +func (s *destroyTwoEnvironmentsSuite) SetUpTest(c *gc.C) { + s.JujuConnSuite.SetUpTest(c) + _, err := s.State.AddUser("jess", "jess", "", "test") + c.Assert(err, jc.ErrorIsNil) + s.otherEnvOwner = names.NewUserTag("jess") + s.otherState = factory.NewFactory(s.State).MakeEnvironment(c, &factory.EnvParams{ + Owner: s.otherEnvOwner, + Prepare: true, + ConfigAttrs: jujutesting.Attrs{ + "state-server": false, + }, + }) + s.AddCleanup(func(*gc.C) { s.otherState.Close() }) + + // get the client for the other environment + auth := apiservertesting.FakeAuthorizer{ + Tag: s.otherEnvOwner, + EnvironManager: false, + } + s.otherEnvClient, err = client.NewClient(s.otherState, common.NewResources(), auth) + c.Assert(err, jc.ErrorIsNil) + + s.metricSender = &testMetricSender{} + s.PatchValue(common.SendMetrics, s.metricSender.SendMetrics) +} + +func (s *destroyTwoEnvironmentsSuite) TestCleanupEnvironDocs(c *gc.C) { + otherFactory := factory.NewFactory(s.otherState) + otherFactory.MakeMachine(c, nil) + m := otherFactory.MakeMachine(c, nil) + otherFactory.MakeMachineNested(c, m.Id(), nil) + + err := common.DestroyEnvironment(s.otherState, s.otherState.EnvironTag()) + c.Assert(err, jc.ErrorIsNil) + + _, err = s.otherState.Environment() + c.Assert(errors.IsNotFound(err), jc.IsTrue) + + _, err = s.State.Environment() + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.otherState.EnsureEnvironmentRemoved(), jc.ErrorIsNil) + s.metricSender.CheckCalls(c, []jtesting.StubCall{{FuncName: "SendMetrics"}}) +} + +func (s *destroyTwoEnvironmentsSuite) TestDifferentStateEnv(c *gc.C) { + otherFactory := factory.NewFactory(s.otherState) + otherFactory.MakeMachine(c, nil) + m := otherFactory.MakeMachine(c, nil) + otherFactory.MakeMachineNested(c, m.Id(), nil) + + // NOTE: pass in the main test State instance, which is 'bound' + // to the state server environment. + err := common.DestroyEnvironment(s.State, s.otherState.EnvironTag()) + c.Assert(err, jc.ErrorIsNil) + + _, err = s.otherState.Environment() + c.Assert(errors.IsNotFound(err), jc.IsTrue) + + _, err = s.State.Environment() + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.otherState.EnsureEnvironmentRemoved(), jc.ErrorIsNil) + + s.metricSender.CheckCalls(c, []jtesting.StubCall{{FuncName: "SendMetrics"}}) +} + +func (s *destroyTwoEnvironmentsSuite) TestDestroyStateServerAfterNonStateServerIsDestroyed(c *gc.C) { + err := common.DestroyEnvironment(s.State, s.State.EnvironTag()) + c.Assert(err, gc.ErrorMatches, "failed to destroy environment: hosting 1 other environments") + err = common.DestroyEnvironment(s.State, s.otherState.EnvironTag()) + c.Assert(err, jc.ErrorIsNil) + err = common.DestroyEnvironment(s.State, s.State.EnvironTag()) + c.Assert(err, jc.ErrorIsNil) + s.metricSender.CheckCalls(c, []jtesting.StubCall{{FuncName: "SendMetrics"}, {FuncName: "SendMetrics"}}) +} + +func (s *destroyTwoEnvironmentsSuite) TestCanDestroyNonBlockedEnv(c *gc.C) { + bh := commontesting.NewBlockHelper(s.APIState) + defer bh.Close() + + bh.BlockDestroyEnvironment(c, "TestBlockDestroyDestroyEnvironment") + + err := common.DestroyEnvironment(s.State, s.otherState.EnvironTag()) + c.Assert(err, jc.ErrorIsNil) + + err = common.DestroyEnvironment(s.State, s.State.EnvironTag()) + bh.AssertBlocked(c, err, "TestBlockDestroyDestroyEnvironment") + + s.metricSender.CheckCalls(c, []jtesting.StubCall{{FuncName: "SendMetrics"}}) +} + +type testMetricSender struct { + jtesting.Stub +} + +func (t *testMetricSender) SendMetrics(st *state.State) error { + t.AddCall("SendMetrics") + return nil +} === modified file 'src/github.com/juju/juju/apiserver/common/errors.go' --- src/github.com/juju/juju/apiserver/common/errors.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/common/errors.go 2015-10-23 18:29:32 +0000 @@ -144,8 +144,12 @@ code = params.CodeNotProvisioned case state.IsUpgradeInProgressError(err): code = params.CodeUpgradeInProgress + case state.IsHasAttachmentsError(err): + code = params.CodeMachineHasAttachedStorage case IsUnknownEnviromentError(err): code = params.CodeNotFound + case errors.IsNotSupported(err): + code = params.CodeNotSupported default: code = params.ErrCode(err) } === modified file 'src/github.com/juju/juju/apiserver/common/errors_test.go' --- src/github.com/juju/juju/apiserver/common/errors_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/common/errors_test.go 2015-10-23 18:29:32 +0000 @@ -118,6 +118,10 @@ code: params.CodeOperationBlocked, helperFunc: params.IsCodeOperationBlocked, }, { + err: errors.NotSupportedf("needed feature"), + code: params.CodeNotSupported, + helperFunc: params.IsCodeNotSupported, +}, { err: stderrors.New("an error"), code: "", }, { === modified file 'src/github.com/juju/juju/apiserver/common/export_test.go' --- src/github.com/juju/juju/apiserver/common/export_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/common/export_test.go 2015-10-23 18:29:32 +0000 @@ -11,6 +11,7 @@ WrapNewFacade = wrapNewFacade NilFacadeRecord = facadeRecord{} EnvtoolsFindTools = &envtoolsFindTools + SendMetrics = &sendMetrics ) type Patcher interface { === modified file 'src/github.com/juju/juju/apiserver/common/filesystems.go' --- src/github.com/juju/juju/apiserver/common/filesystems.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/common/filesystems.go 2015-10-23 18:29:32 +0000 @@ -4,8 +4,6 @@ package common import ( - "fmt" - "github.com/juju/errors" "github.com/juju/names" @@ -15,30 +13,27 @@ "github.com/juju/juju/storage/poolmanager" ) -type filesystemAlreadyProvisionedError struct { - error -} - -// IsFilesystemAlreadyProvisioned returns true if the specified error -// is caused by a filesystem already being provisioned. -func IsFilesystemAlreadyProvisioned(err error) bool { - _, ok := err.(*filesystemAlreadyProvisionedError) - return ok -} - -// FilesystemParams returns the parameters for creating the given filesystem. +// FilesystemParams returns the parameters for creating or destroying the +// given filesystem. func FilesystemParams( f state.Filesystem, storageInstance state.StorageInstance, environConfig *config.Config, poolManager poolmanager.PoolManager, ) (params.FilesystemParams, error) { - stateFilesystemParams, ok := f.Params() - if !ok { - err := &filesystemAlreadyProvisionedError{fmt.Errorf( - "filesystem %q is already provisioned", f.Tag().Id(), - )} - return params.FilesystemParams{}, err + + var pool string + var size uint64 + if stateFilesystemParams, ok := f.Params(); ok { + pool = stateFilesystemParams.Pool + size = stateFilesystemParams.Size + } else { + filesystemInfo, err := f.Info() + if err != nil { + return params.FilesystemParams{}, errors.Trace(err) + } + pool = filesystemInfo.Pool + size = filesystemInfo.Size } filesystemTags, err := storageTags(storageInstance, environConfig) @@ -46,14 +41,14 @@ return params.FilesystemParams{}, errors.Annotate(err, "computing storage tags") } - providerType, cfg, err := StoragePoolConfig(stateFilesystemParams.Pool, poolManager) + providerType, cfg, err := StoragePoolConfig(pool, poolManager) if err != nil { return params.FilesystemParams{}, errors.Trace(err) } result := params.FilesystemParams{ f.Tag().String(), "", // volume tag - stateFilesystemParams.Size, + size, string(providerType), cfg.Attrs(), filesystemTags, === modified file 'src/github.com/juju/juju/apiserver/common/getstatus.go' --- src/github.com/juju/juju/apiserver/common/getstatus.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/common/getstatus.go 2015-10-23 18:29:32 +0000 @@ -188,3 +188,13 @@ } return result, nil } + +// EntityStatusFromState converts a state.StatusInfo into a params.EntityStatus. +func EntityStatusFromState(status state.StatusInfo) params.EntityStatus { + return params.EntityStatus{ + params.Status(status.Status), + status.Message, + status.Data, + status.Since, + } +} === modified file 'src/github.com/juju/juju/apiserver/common/interfaces.go' --- src/github.com/juju/juju/apiserver/common/interfaces.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/apiserver/common/interfaces.go 2015-10-23 18:29:32 +0000 @@ -4,6 +4,7 @@ package common import ( + "github.com/juju/errors" "github.com/juju/names" ) @@ -77,3 +78,21 @@ }, nil } } + +// AuthFuncForTagKind returns a GetAuthFunc which creates an AuthFunc +// allowing only the given tag kind and denies all others. Passing an +// empty kind is an error. +func AuthFuncForTagKind(kind string) GetAuthFunc { + return func() (AuthFunc, error) { + if kind == "" { + return nil, errors.Errorf("tag kind cannot be empty") + } + return func(tag names.Tag) bool { + // Allow only the given tag kind. + if tag == nil { + return false + } + return tag.Kind() == kind + }, nil + } +} === added file 'src/github.com/juju/juju/apiserver/common/networking.go' --- src/github.com/juju/juju/apiserver/common/networking.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/common/networking.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,126 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package common + +import ( + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/environs/config" + "github.com/juju/juju/network" + providercommon "github.com/juju/juju/provider/common" +) + +// BackingSubnet defines the methods supported by a Subnet entity +// stored persistently. +// +// TODO(dimitern): Once the state backing is implemented, remove this +// and just use *state.Subnet. +type BackingSubnet interface { + CIDR() string + VLANTag() int + ProviderId() string + AvailabilityZones() []string + Status() string + SpaceName() string + Life() params.Life +} + +// BackingSubnetInfo describes a single subnet to be added in the +// backing store. +// +// TODO(dimitern): Replace state.SubnetInfo with this and remove +// BackingSubnetInfo, once the rest of state backing methods and the +// following pre-reqs are done: +// * subnetDoc.AvailabilityZone becomes subnetDoc.AvailabilityZones, +// adding an upgrade step to migrate existing non empty zones on +// subnet docs. Also change state.Subnet.AvailabilityZone to +// * add subnetDoc.SpaceName - no upgrade step needed, as it will only +// be used for new space-aware subnets. +// * Subnets need a reference count to calculate Status. +// * ensure EC2 and MAAS providers accept empty IDs as Subnets() args +// and return all subnets, including the AvailabilityZones (for EC2; +// empty for MAAS as zones are orthogonal to networks). +type BackingSubnetInfo struct { + // ProviderId is a provider-specific network id. This may be empty. + ProviderId string + + // CIDR of the network, in 123.45.67.89/24 format. + CIDR string + + // VLANTag needs to be between 1 and 4094 for VLANs and 0 for normal + // networks. It's defined by IEEE 802.1Q standard. + VLANTag int + + // AllocatableIPHigh and Low describe the allocatable portion of the + // subnet. The remainder, if any, is reserved by the provider. + // Either both of these must be set or neither, if they're empty it + // means that none of the subnet is allocatable. If present they must + // be valid IP addresses within the subnet CIDR. + AllocatableIPHigh string + AllocatableIPLow string + + // AvailabilityZones describes which availability zone(s) this + // subnet is in. It can be empty if the provider does not support + // availability zones. + AvailabilityZones []string + + // SpaceName holds the juju network space this subnet is + // associated with. Can be empty if not supported. + SpaceName string + + // Status holds the status of the subnet. Normally this will be + // calculated from the reference count and Life of a subnet. + Status string + + // Live holds the life of the subnet + Life params.Life +} + +// BackingSpace defines the methods supported by a Space entity stored +// persistently. +type BackingSpace interface { + // Name returns the space name. + Name() string + + // Subnets returns the subnets in the space + Subnets() ([]BackingSubnet, error) + + // ProviderId returns the network ID of the provider + ProviderId() network.Id + + // Zones returns a list of availability zone(s) that this + // space is in. It can be empty if the provider does not support + // availability zones. + Zones() []string + + // Life returns the lifecycle state of the space + Life() params.Life +} + +// Backing defines the methods needed by the API facade to store and +// retrieve information from the underlying persistency layer (state +// DB). +type NetworkBacking interface { + // EnvironConfig returns the current environment config. + EnvironConfig() (*config.Config, error) + + // AvailabilityZones returns all cached availability zones (i.e. + // not from the provider, but in state). + AvailabilityZones() ([]providercommon.AvailabilityZone, error) + + // SetAvailabilityZones replaces the cached list of availability + // zones with the given zones. + SetAvailabilityZones([]providercommon.AvailabilityZone) error + + // AddSpace creates a space + AddSpace(Name string, Subnets []string, Public bool) error + + // AllSpaces returns all known Juju network spaces. + AllSpaces() ([]BackingSpace, error) + + // AddSubnet creates a backing subnet for an existing subnet. + AddSubnet(BackingSubnetInfo) (BackingSubnet, error) + + // AllSubnets returns all backing subnets. + AllSubnets() ([]BackingSubnet, error) +} === modified file 'src/github.com/juju/juju/apiserver/common/registry.go' --- src/github.com/juju/juju/apiserver/common/registry.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/common/registry.go 2015-10-23 18:29:32 +0000 @@ -10,6 +10,7 @@ "sort" "github.com/juju/errors" + "github.com/juju/names" "github.com/juju/utils/featureflag" "github.com/juju/juju/state" @@ -118,6 +119,41 @@ return wrapped, funcValue.Type().Out(0), nil } +// NewHookContextFacadeFn specifies the function signature that can be +// used to register a hook context facade. +type NewHookContextFacadeFn func(*state.State, *state.Unit) (interface{}, error) + +// RegisterHookContextFacade registers facades for use within a hook +// context. This function handles the translation from a +// 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. +func RegisterHookContextFacade(name string, version int, newHookContextFacade NewHookContextFacadeFn, facadeType reflect.Type) { + + newFacade := func(st *state.State, _ *Resources, authorizer Authorizer, _ string) (interface{}, error) { + + if !authorizer.AuthUnitAgent() { + return nil, ErrPerm + } + + // Verify that the unit's ID matches a unit that we know + // about. + tag := authorizer.GetAuthTag() + if _, ok := tag.(names.UnitTag); !ok { + return nil, errors.Errorf("expected names.UnitTag, got %T", tag) + } + + unit, err := st.Unit(tag.Id()) + if err != nil { + return nil, errors.Trace(err) + } + + return newHookContextFacade(st, unit) + } + + RegisterFacade(name, version, newFacade, facadeType) +} + // 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) === modified file 'src/github.com/juju/juju/apiserver/common/setstatus_test.go' --- src/github.com/juju/juju/apiserver/common/setstatus_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/common/setstatus_test.go 2015-10-23 18:29:32 +0000 @@ -34,7 +34,7 @@ func (s *statusSetterSuite) TestUnauthorized(c *gc.C) { tag := names.NewMachineTag("42") s.badTag = tag - result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatus{{ + result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatusArgs{{ Tag: tag.String(), Status: params.StatusExecuting, }}}) @@ -44,7 +44,7 @@ } func (s *statusSetterSuite) TestNotATag(c *gc.C) { - result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatus{{ + result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatusArgs{{ Tag: "not a tag", Status: params.StatusExecuting, }}}) @@ -54,7 +54,7 @@ } func (s *statusSetterSuite) TestNotFound(c *gc.C) { - result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatus{{ + result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatusArgs{{ Tag: names.NewMachineTag("42").String(), Status: params.StatusDown, }}}) @@ -65,7 +65,7 @@ func (s *statusSetterSuite) TestSetMachineStatus(c *gc.C) { machine := s.Factory.MakeMachine(c, nil) - result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatus{{ + result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatusArgs{{ Tag: machine.Tag().String(), Status: params.StatusStarted, }}}) @@ -87,7 +87,7 @@ unit := s.Factory.MakeUnit(c, &factory.UnitParams{Status: &state.StatusInfo{ Status: state.StatusMaintenance, }}) - result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatus{{ + result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatusArgs{{ Tag: unit.Tag().String(), Status: params.StatusActive, }}}) @@ -109,7 +109,7 @@ service := s.Factory.MakeService(c, &factory.ServiceParams{Status: &state.StatusInfo{ Status: state.StatusMaintenance, }}) - result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatus{{ + result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatusArgs{{ Tag: service.Tag().String(), Status: params.StatusActive, }}}) @@ -126,7 +126,7 @@ func (s *statusSetterSuite) TestBulk(c *gc.C) { s.badTag = names.NewMachineTag("42") - result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatus{{ + result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatusArgs{{ Tag: s.badTag.String(), Status: params.StatusActive, }, { @@ -158,7 +158,7 @@ // Machines are unauthorized since they are not units tag := names.NewUnitTag("foo/0") s.badTag = tag - result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatus{{ + result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatusArgs{{ Tag: tag.String(), Status: params.StatusActive, }}}) @@ -168,7 +168,7 @@ } func (s *serviceStatusSetterSuite) TestNotATag(c *gc.C) { - result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatus{{ + result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatusArgs{{ Tag: "not a tag", Status: params.StatusActive, }}}) @@ -178,7 +178,7 @@ } func (s *serviceStatusSetterSuite) TestNotFound(c *gc.C) { - result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatus{{ + result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatusArgs{{ Tag: names.NewUnitTag("foo/0").String(), Status: params.StatusActive, }}}) @@ -189,7 +189,7 @@ func (s *serviceStatusSetterSuite) TestSetMachineStatus(c *gc.C) { machine := s.Factory.MakeMachine(c, nil) - result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatus{{ + result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatusArgs{{ Tag: machine.Tag().String(), Status: params.StatusActive, }}}) @@ -206,7 +206,7 @@ service := s.Factory.MakeService(c, &factory.ServiceParams{Status: &state.StatusInfo{ Status: state.StatusMaintenance, }}) - result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatus{{ + result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatusArgs{{ Tag: service.Tag().String(), Status: params.StatusActive, }}}) @@ -222,7 +222,7 @@ unit := s.Factory.MakeUnit(c, &factory.UnitParams{Status: &state.StatusInfo{ Status: state.StatusMaintenance, }}) - result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatus{{ + result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatusArgs{{ Tag: unit.Tag().String(), Status: params.StatusActive, }}}) @@ -245,7 +245,7 @@ service.Name(), unit.Name(), time.Minute) - result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatus{{ + result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatusArgs{{ Tag: unit.Tag().String(), Status: params.StatusActive, }}}) @@ -264,7 +264,7 @@ func (s *serviceStatusSetterSuite) TestBulk(c *gc.C) { s.badTag = names.NewMachineTag("42") machine := s.Factory.MakeMachine(c, nil) - result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatus{{ + result, err := s.setter.SetStatus(params.SetStatus{[]params.EntityStatusArgs{{ Tag: s.badTag.String(), Status: params.StatusActive, }, { === modified file 'src/github.com/juju/juju/apiserver/common/storage.go' --- src/github.com/juju/juju/apiserver/common/storage.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/common/storage.go 2015-10-23 18:29:32 +0000 @@ -4,8 +4,6 @@ package common import ( - "path" - "github.com/juju/errors" "github.com/juju/names" @@ -43,17 +41,29 @@ WatchStorageAttachment(names.StorageTag, names.UnitTag) state.NotifyWatcher // WatchFilesystemAttachment watches for changes to the filesystem - // attachment corresponding to the identfified machien and filesystem. + // attachment corresponding to the identfified machine and filesystem. WatchFilesystemAttachment(names.MachineTag, names.FilesystemTag) state.NotifyWatcher // WatchVolumeAttachment watches for changes to the volume attachment - // corresponding to the identfified machien and volume. + // corresponding to the identfified machine and volume. WatchVolumeAttachment(names.MachineTag, names.VolumeTag) state.NotifyWatcher + + // WatchBlockDevices watches for changes to block devices associated + // with the specified machine. + WatchBlockDevices(names.MachineTag) state.NotifyWatcher + + // BlockDevices returns information about block devices published + // for the specified machine. + BlockDevices(names.MachineTag) ([]state.BlockDeviceInfo, error) } // StorageAttachmentInfo returns the StorageAttachmentInfo for the specified // StorageAttachment by gathering information from related entities (volumes, // filesystems). +// +// StorageAttachmentInfo returns an error satisfying errors.IsNotProvisioned +// if the storage attachment is not yet fully provisioned and ready for use +// by a charm. func StorageAttachmentInfo( st StorageInterface, att state.StorageAttachment, @@ -94,9 +104,26 @@ if err != nil { return nil, errors.Annotate(err, "getting volume attachment info") } + blockDevices, err := st.BlockDevices(machineTag) + if err != nil { + return nil, errors.Annotate(err, "getting block devices") + } + blockDevice, ok := MatchingBlockDevice( + blockDevices, + volumeInfo, + volumeAttachmentInfo, + ) + if !ok { + // We must not say that a block-kind storage attachment is + // provisioned until its block device has shown up on the + // machine, otherwise the charm may attempt to use it and + // fail. + return nil, errors.NotProvisionedf("%v", names.ReadableString(storageTag)) + } devicePath, err := volumeAttachmentDevicePath( volumeInfo, volumeAttachmentInfo, + *blockDevice, ) if err != nil { return nil, errors.Trace(err) @@ -132,8 +159,8 @@ } // WatchStorageAttachment returns a state.NotifyWatcher that reacts to changes -// to the VolumeAttachmentInfo or FilesystemAttachmentInfo corresponding to the tags -// specified. +// to the VolumeAttachmentInfo or FilesystemAttachmentInfo corresponding to the +// tags specified. func WatchStorageAttachment( st StorageInterface, storageTag names.StorageTag, @@ -144,42 +171,64 @@ if err != nil { return nil, errors.Annotate(err, "getting storage instance") } - var w state.NotifyWatcher + var watchers []state.NotifyWatcher switch storageInstance.Kind() { case state.StorageKindBlock: volume, err := st.StorageInstanceVolume(storageTag) if err != nil { return nil, errors.Annotate(err, "getting storage volume") } - w = st.WatchVolumeAttachment(machineTag, volume.VolumeTag()) + // We need to watch both the volume attachment, and the + // machine's block devices. A volume attachment's block + // device could change (most likely, become present). + watchers = []state.NotifyWatcher{ + st.WatchVolumeAttachment(machineTag, volume.VolumeTag()), + // TODO(axw) 2015-09-30 #1501203 + // We should filter the events to only those relevant + // to the volume attachment. This means we would need + // to either start th block device watcher after we + // have provisioned the volume attachment (cleaner?), + // or have the filter ignore changes until the volume + // attachment is provisioned. + st.WatchBlockDevices(machineTag), + } case state.StorageKindFilesystem: filesystem, err := st.StorageInstanceFilesystem(storageTag) if err != nil { return nil, errors.Annotate(err, "getting storage filesystem") } - w = st.WatchFilesystemAttachment(machineTag, filesystem.FilesystemTag()) + watchers = []state.NotifyWatcher{ + st.WatchFilesystemAttachment(machineTag, filesystem.FilesystemTag()), + } default: return nil, errors.Errorf("invalid storage kind %v", storageInstance.Kind()) } - w2 := st.WatchStorageAttachment(storageTag, unitTag) - return newMultiNotifyWatcher(w, w2), nil + watchers = append(watchers, st.WatchStorageAttachment(storageTag, unitTag)) + return newMultiNotifyWatcher(watchers...), nil } -var errNoDevicePath = errors.New("cannot determine device path: no serial or persistent device name") - // volumeAttachmentDevicePath returns the absolute device path for // a volume attachment. The value is only meaningful in the context // of the machine that the volume is attached to. func volumeAttachmentDevicePath( volumeInfo state.VolumeInfo, volumeAttachmentInfo state.VolumeAttachmentInfo, + blockDevice state.BlockDeviceInfo, ) (string, error) { - if volumeInfo.HardwareId != "" { - return path.Join("/dev/disk/by-id", volumeInfo.HardwareId), nil - } else if volumeAttachmentInfo.DeviceName != "" { - return path.Join("/dev", volumeAttachmentInfo.DeviceName), nil + if volumeInfo.HardwareId != "" || volumeAttachmentInfo.DeviceName != "" || volumeAttachmentInfo.DeviceLink != "" { + // Prefer the volume attachment's information over what is + // in the published block device information. + var deviceLinks []string + if volumeAttachmentInfo.DeviceLink != "" { + deviceLinks = []string{volumeAttachmentInfo.DeviceLink} + } + return storage.BlockDevicePath(storage.BlockDevice{ + HardwareId: volumeInfo.HardwareId, + DeviceName: volumeAttachmentInfo.DeviceName, + DeviceLinks: deviceLinks, + }) } - return "", errNoDevicePath + return storage.BlockDevicePath(BlockDeviceFromState(blockDevice)) } // MaybeAssignedStorageInstance calls the provided function to get a === modified file 'src/github.com/juju/juju/apiserver/common/storage_test.go' --- src/github.com/juju/juju/apiserver/common/storage_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/common/storage_test.go 2015-10-23 18:29:32 +0000 @@ -4,20 +4,238 @@ package common_test import ( + "path/filepath" + + "github.com/juju/errors" + "github.com/juju/names" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/common" "github.com/juju/juju/state" - "github.com/juju/names" + statetesting "github.com/juju/juju/state/testing" + "github.com/juju/juju/storage" ) -type fakeStorageInstance struct { - state.StorageInstance - tag names.StorageTag - owner names.Tag -} - -func (i *fakeStorageInstance) Tag() names.Tag { - return i.tag -} - -func (i *fakeStorageInstance) Owner() names.Tag { - return i.owner +type storageAttachmentInfoSuite struct { + machineTag names.MachineTag + volumeTag names.VolumeTag + storageTag names.StorageTag + st *fakeStorage + storageInstance *fakeStorageInstance + storageAttachment *fakeStorageAttachment + volume *fakeVolume + volumeAttachment *fakeVolumeAttachment + blockDevices []state.BlockDeviceInfo +} + +var _ = gc.Suite(&storageAttachmentInfoSuite{}) + +func (s *storageAttachmentInfoSuite) SetUpTest(c *gc.C) { + s.machineTag = names.NewMachineTag("0") + s.volumeTag = names.NewVolumeTag("0") + s.storageTag = names.NewStorageTag("osd-devices/0") + s.storageInstance = &fakeStorageInstance{ + tag: s.storageTag, + owner: s.machineTag, + kind: state.StorageKindBlock, + } + s.storageAttachment = &fakeStorageAttachment{ + storageTag: s.storageTag, + } + s.volume = &fakeVolume{ + tag: s.volumeTag, + info: &state.VolumeInfo{ + VolumeId: "vol-ume", + Pool: "radiance", + Size: 1024, + }, + } + s.volumeAttachment = &fakeVolumeAttachment{ + info: &state.VolumeAttachmentInfo{}, + } + s.blockDevices = []state.BlockDeviceInfo{{ + DeviceName: "sda", + DeviceLinks: []string{"/dev/disk/by-id/verbatim"}, + HardwareId: "whatever", + }} + s.st = &fakeStorage{ + storageInstance: func(tag names.StorageTag) (state.StorageInstance, error) { + return s.storageInstance, nil + }, + storageInstanceVolume: func(tag names.StorageTag) (state.Volume, error) { + return s.volume, nil + }, + volumeAttachment: func(m names.MachineTag, v names.VolumeTag) (state.VolumeAttachment, error) { + return s.volumeAttachment, nil + }, + blockDevices: func(m names.MachineTag) ([]state.BlockDeviceInfo, error) { + return s.blockDevices, nil + }, + } +} + +func (s *storageAttachmentInfoSuite) TestStorageAttachmentInfoPersistentDeviceName(c *gc.C) { + s.volumeAttachment.info.DeviceName = "sda" + info, err := common.StorageAttachmentInfo(s.st, s.storageAttachment, s.machineTag) + c.Assert(err, jc.ErrorIsNil) + s.st.CheckCallNames(c, "StorageInstance", "StorageInstanceVolume", "VolumeAttachment", "BlockDevices") + c.Assert(info, jc.DeepEquals, &storage.StorageAttachmentInfo{ + Kind: storage.StorageKindBlock, + Location: filepath.FromSlash("/dev/sda"), + }) +} + +func (s *storageAttachmentInfoSuite) TestStorageAttachmentInfoMissingBlockDevice(c *gc.C) { + // If the block device has not shown up yet, + // then we should get a NotProvisioned error. + s.blockDevices = nil + s.volumeAttachment.info.DeviceName = "sda" + _, err := common.StorageAttachmentInfo(s.st, s.storageAttachment, s.machineTag) + c.Assert(err, jc.Satisfies, errors.IsNotProvisioned) + s.st.CheckCallNames(c, "StorageInstance", "StorageInstanceVolume", "VolumeAttachment", "BlockDevices") +} + +func (s *storageAttachmentInfoSuite) TestStorageAttachmentInfoPersistentDeviceLink(c *gc.C) { + s.volumeAttachment.info.DeviceLink = "/dev/disk/by-id/verbatim" + info, err := common.StorageAttachmentInfo(s.st, s.storageAttachment, s.machineTag) + c.Assert(err, jc.ErrorIsNil) + s.st.CheckCallNames(c, "StorageInstance", "StorageInstanceVolume", "VolumeAttachment", "BlockDevices") + c.Assert(info, jc.DeepEquals, &storage.StorageAttachmentInfo{ + Kind: storage.StorageKindBlock, + Location: "/dev/disk/by-id/verbatim", + }) +} + +func (s *storageAttachmentInfoSuite) TestStorageAttachmentInfoPersistentHardwareId(c *gc.C) { + s.volume.info.HardwareId = "whatever" + info, err := common.StorageAttachmentInfo(s.st, s.storageAttachment, s.machineTag) + c.Assert(err, jc.ErrorIsNil) + s.st.CheckCallNames(c, "StorageInstance", "StorageInstanceVolume", "VolumeAttachment", "BlockDevices") + c.Assert(info, jc.DeepEquals, &storage.StorageAttachmentInfo{ + Kind: storage.StorageKindBlock, + Location: filepath.FromSlash("/dev/disk/by-id/whatever"), + }) +} + +func (s *storageAttachmentInfoSuite) TestStorageAttachmentInfoMatchingBlockDevice(c *gc.C) { + // The bus address alone is not enough to produce a path to the block + // device; we need to find a published block device with the matching + // bus address. + s.volumeAttachment.info.BusAddress = "scsi@1:2.3.4" + s.blockDevices = []state.BlockDeviceInfo{{ + DeviceName: "sda", + }, { + DeviceName: "sdb", + BusAddress: s.volumeAttachment.info.BusAddress, + }} + info, err := common.StorageAttachmentInfo(s.st, s.storageAttachment, s.machineTag) + c.Assert(err, jc.ErrorIsNil) + s.st.CheckCallNames(c, "StorageInstance", "StorageInstanceVolume", "VolumeAttachment", "BlockDevices") + c.Assert(info, jc.DeepEquals, &storage.StorageAttachmentInfo{ + Kind: storage.StorageKindBlock, + Location: filepath.FromSlash("/dev/sdb"), + }) +} + +func (s *storageAttachmentInfoSuite) TestStorageAttachmentInfoNoBlockDevice(c *gc.C) { + // Neither the volume nor the volume attachment has enough information + // to persistently identify the path, so we must enquire about block + // devices; there are none (yet), so NotProvisioned is returned. + s.volumeAttachment.info.BusAddress = "scsi@1:2.3.4" + _, err := common.StorageAttachmentInfo(s.st, s.storageAttachment, s.machineTag) + c.Assert(err, jc.Satisfies, errors.IsNotProvisioned) + s.st.CheckCallNames(c, "StorageInstance", "StorageInstanceVolume", "VolumeAttachment", "BlockDevices") +} + +type watchStorageAttachmentSuite struct { + storageTag names.StorageTag + machineTag names.MachineTag + unitTag names.UnitTag + st *fakeStorage + storageInstance *fakeStorageInstance + volume *fakeVolume + volumeAttachmentWatcher *fakeNotifyWatcher + blockDevicesWatcher *fakeNotifyWatcher + storageAttachmentWatcher *fakeNotifyWatcher +} + +var _ = gc.Suite(&watchStorageAttachmentSuite{}) + +func (s *watchStorageAttachmentSuite) SetUpTest(c *gc.C) { + s.storageTag = names.NewStorageTag("osd-devices/0") + s.machineTag = names.NewMachineTag("0") + s.unitTag = names.NewUnitTag("ceph/0") + s.storageInstance = &fakeStorageInstance{ + tag: s.storageTag, + owner: s.machineTag, + kind: state.StorageKindBlock, + } + s.volume = &fakeVolume{tag: names.NewVolumeTag("0")} + s.volumeAttachmentWatcher = &fakeNotifyWatcher{changes: make(chan struct{}, 1)} + s.blockDevicesWatcher = &fakeNotifyWatcher{changes: make(chan struct{}, 1)} + s.storageAttachmentWatcher = &fakeNotifyWatcher{changes: make(chan struct{}, 1)} + s.volumeAttachmentWatcher.changes <- struct{}{} + s.blockDevicesWatcher.changes <- struct{}{} + s.storageAttachmentWatcher.changes <- struct{}{} + s.st = &fakeStorage{ + storageInstance: func(tag names.StorageTag) (state.StorageInstance, error) { + return s.storageInstance, nil + }, + storageInstanceVolume: func(tag names.StorageTag) (state.Volume, error) { + return s.volume, nil + }, + watchVolumeAttachment: func(names.MachineTag, names.VolumeTag) state.NotifyWatcher { + return s.volumeAttachmentWatcher + }, + watchBlockDevices: func(names.MachineTag) state.NotifyWatcher { + return s.blockDevicesWatcher + }, + watchStorageAttachment: func(names.StorageTag, names.UnitTag) state.NotifyWatcher { + return s.storageAttachmentWatcher + }, + } +} + +func (s *watchStorageAttachmentSuite) TestWatchStorageAttachmentVolumeAttachmentChanges(c *gc.C) { + s.testWatchBlockStorageAttachment(c, func() { + s.volumeAttachmentWatcher.changes <- struct{}{} + }) +} + +func (s *watchStorageAttachmentSuite) TestWatchStorageAttachmentStorageAttachmentChanges(c *gc.C) { + s.testWatchBlockStorageAttachment(c, func() { + s.storageAttachmentWatcher.changes <- struct{}{} + }) +} + +func (s *watchStorageAttachmentSuite) TestWatchStorageAttachmentBlockDevicesChange(c *gc.C) { + s.testWatchBlockStorageAttachment(c, func() { + s.blockDevicesWatcher.changes <- struct{}{} + }) +} + +func (s *watchStorageAttachmentSuite) testWatchBlockStorageAttachment(c *gc.C, change func()) { + s.testWatchStorageAttachment(c, change) + s.st.CheckCallNames(c, + "StorageInstance", + "StorageInstanceVolume", + "WatchVolumeAttachment", + "WatchBlockDevices", + "WatchStorageAttachment", + ) +} + +func (s *watchStorageAttachmentSuite) testWatchStorageAttachment(c *gc.C, change func()) { + w, err := common.WatchStorageAttachment( + s.st, + s.storageTag, + s.machineTag, + s.unitTag, + ) + c.Assert(err, jc.ErrorIsNil) + wc := statetesting.NewNotifyWatcherC(c, nopSyncStarter{}, w) + wc.AssertOneChange() + change() + wc.AssertOneChange() } === added file 'src/github.com/juju/juju/apiserver/common/storagemock_test.go' --- src/github.com/juju/juju/apiserver/common/storagemock_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/common/storagemock_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,143 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package common_test + +import ( + "github.com/juju/errors" + "github.com/juju/names" + "github.com/juju/testing" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/state" + "github.com/juju/juju/storage" + "github.com/juju/juju/storage/poolmanager" +) + +type fakeStorage struct { + testing.Stub + common.StorageInterface + storageInstance func(names.StorageTag) (state.StorageInstance, error) + storageInstanceVolume func(names.StorageTag) (state.Volume, error) + volumeAttachment func(names.MachineTag, names.VolumeTag) (state.VolumeAttachment, error) + blockDevices func(names.MachineTag) ([]state.BlockDeviceInfo, error) + watchVolumeAttachment func(names.MachineTag, names.VolumeTag) state.NotifyWatcher + watchBlockDevices func(names.MachineTag) state.NotifyWatcher + watchStorageAttachment func(names.StorageTag, names.UnitTag) state.NotifyWatcher +} + +func (s *fakeStorage) StorageInstance(tag names.StorageTag) (state.StorageInstance, error) { + s.MethodCall(s, "StorageInstance", tag) + return s.storageInstance(tag) +} + +func (s *fakeStorage) StorageInstanceVolume(tag names.StorageTag) (state.Volume, error) { + s.MethodCall(s, "StorageInstanceVolume", tag) + return s.storageInstanceVolume(tag) +} + +func (s *fakeStorage) VolumeAttachment(m names.MachineTag, v names.VolumeTag) (state.VolumeAttachment, error) { + s.MethodCall(s, "VolumeAttachment", m, v) + return s.volumeAttachment(m, v) +} + +func (s *fakeStorage) BlockDevices(m names.MachineTag) ([]state.BlockDeviceInfo, error) { + s.MethodCall(s, "BlockDevices", m) + return s.blockDevices(m) +} + +func (s *fakeStorage) WatchVolumeAttachment(m names.MachineTag, v names.VolumeTag) state.NotifyWatcher { + s.MethodCall(s, "WatchVolumeAttachment", m, v) + return s.watchVolumeAttachment(m, v) +} + +func (s *fakeStorage) WatchBlockDevices(m names.MachineTag) state.NotifyWatcher { + s.MethodCall(s, "WatchBlockDevices", m) + return s.watchBlockDevices(m) +} + +func (s *fakeStorage) WatchStorageAttachment(st names.StorageTag, u names.UnitTag) state.NotifyWatcher { + s.MethodCall(s, "WatchStorageAttachment", st, u) + return s.watchStorageAttachment(st, u) +} + +type fakeStorageInstance struct { + state.StorageInstance + tag names.StorageTag + owner names.Tag + kind state.StorageKind +} + +func (i *fakeStorageInstance) StorageTag() names.StorageTag { + return i.tag +} + +func (i *fakeStorageInstance) Tag() names.Tag { + return i.tag +} + +func (i *fakeStorageInstance) Owner() names.Tag { + return i.owner +} + +func (i *fakeStorageInstance) Kind() state.StorageKind { + return i.kind +} + +type fakeStorageAttachment struct { + state.StorageAttachment + storageTag names.StorageTag +} + +func (a *fakeStorageAttachment) StorageInstance() names.StorageTag { + return a.storageTag +} + +type fakeVolume struct { + state.Volume + tag names.VolumeTag + params *state.VolumeParams + info *state.VolumeInfo +} + +func (v *fakeVolume) VolumeTag() names.VolumeTag { + return v.tag +} + +func (v *fakeVolume) Tag() names.Tag { + return v.tag +} + +func (v *fakeVolume) Params() (state.VolumeParams, bool) { + if v.params == nil { + return state.VolumeParams{}, false + } + return *v.params, true +} + +func (v *fakeVolume) Info() (state.VolumeInfo, error) { + if v.info == nil { + return state.VolumeInfo{}, errors.NotProvisionedf("volume %v", v.tag.Id()) + } + return *v.info, nil +} + +type fakeVolumeAttachment struct { + state.VolumeAttachment + info *state.VolumeAttachmentInfo +} + +func (v *fakeVolumeAttachment) Info() (state.VolumeAttachmentInfo, error) { + if v.info == nil { + return state.VolumeAttachmentInfo{}, errors.NotProvisionedf("volume attachment") + } + return *v.info, nil +} + +type fakePoolManager struct { + poolmanager.PoolManager +} + +func (pm *fakePoolManager) Get(name string) (*storage.Config, error) { + return nil, errors.NotFoundf("pool") +} === modified file 'src/github.com/juju/juju/apiserver/common/testing/block.go' --- src/github.com/juju/juju/apiserver/common/testing/block.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/common/testing/block.go 2015-10-23 18:29:32 +0000 @@ -19,13 +19,13 @@ // It provides easy access to switch blocks on // as well as test whether operations are blocked or not. type BlockHelper struct { - ApiState *api.State + ApiState api.Connection client *block.Client } // NewBlockHelper creates a block switch used in testing // to manage desired juju blocks. -func NewBlockHelper(st *api.State) BlockHelper { +func NewBlockHelper(st api.Connection) BlockHelper { return BlockHelper{ ApiState: st, client: block.NewClient(st), === modified file 'src/github.com/juju/juju/apiserver/common/tools.go' --- src/github.com/juju/juju/apiserver/common/tools.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/apiserver/common/tools.go 2015-10-23 18:29:32 +0000 @@ -263,8 +263,9 @@ return nil, err } filter := toolsFilter(args) + stream := envtools.PreferredStream(&args.Number, cfg.Development(), cfg.AgentStream()) simplestreamsList, err := envtoolsFindTools( - env, args.MajorVersion, args.MinorVersion, filter, + env, args.MajorVersion, args.MinorVersion, stream, filter, ) if len(storageList) == 0 && err != nil { return nil, err === modified file 'src/github.com/juju/juju/apiserver/common/tools_test.go' --- src/github.com/juju/juju/apiserver/common/tools_test.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/apiserver/common/tools_test.go 2015-10-23 18:29:32 +0000 @@ -15,6 +15,7 @@ "github.com/juju/juju/apiserver/params" apiservertesting "github.com/juju/juju/apiserver/testing" "github.com/juju/juju/environs" + "github.com/juju/juju/juju/arch" "github.com/juju/juju/juju/testing" "github.com/juju/juju/network" "github.com/juju/juju/state" @@ -157,9 +158,10 @@ SHA256: "feedface", }} - s.PatchValue(common.EnvtoolsFindTools, func(e environs.Environ, major, minor int, filter coretools.Filter) (coretools.List, error) { + s.PatchValue(common.EnvtoolsFindTools, func(e environs.Environ, major, minor int, stream string, filter coretools.Filter) (coretools.List, error) { c.Assert(major, gc.Equals, 123) c.Assert(minor, gc.Equals, 456) + c.Assert(stream, gc.Equals, "released") c.Assert(filter.Series, gc.Equals, "win81") c.Assert(filter.Arch, gc.Equals, "alpha") return envtoolsList, nil @@ -185,7 +187,7 @@ } func (s *toolsSuite) TestFindToolsNotFound(c *gc.C) { - s.PatchValue(common.EnvtoolsFindTools, func(e environs.Environ, major, minor int, filter coretools.Filter) (list coretools.List, err error) { + 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")) @@ -196,23 +198,39 @@ func (s *toolsSuite) TestFindToolsExactInStorage(c *gc.C) { mockToolsStorage := &mockToolsStorage{ - metadata: []toolstorage.Metadata{{Version: version.Current}}, + metadata: []toolstorage.Metadata{ + {Version: version.MustParseBinary("1.22-beta1-trusty-amd64")}, + {Version: version.MustParseBinary("1.22.0-trusty-amd64")}, + }, } - s.testFindToolsExact(c, mockToolsStorage, true) + + s.PatchValue(&arch.HostArch, func() string { return arch.AMD64 }) + s.PatchValue(&version.Current, version.MustParseBinary("1.22-beta1-trusty-amd64")) + s.testFindToolsExact(c, mockToolsStorage, true, true) + s.PatchValue(&version.Current, version.MustParseBinary("1.22.0-trusty-amd64")) + s.testFindToolsExact(c, mockToolsStorage, true, false) } func (s *toolsSuite) TestFindToolsExactNotInStorage(c *gc.C) { mockToolsStorage := &mockToolsStorage{} - s.testFindToolsExact(c, mockToolsStorage, false) + s.PatchValue(&version.Current.Number, version.MustParse("1.22-beta1")) + s.testFindToolsExact(c, mockToolsStorage, false, true) + s.PatchValue(&version.Current.Number, version.MustParse("1.22.0")) + s.testFindToolsExact(c, mockToolsStorage, false, false) } -func (s *toolsSuite) testFindToolsExact(c *gc.C, t common.ToolsStorageGetter, inStorage bool) { +func (s *toolsSuite) testFindToolsExact(c *gc.C, t common.ToolsStorageGetter, inStorage bool, develVersion bool) { var called bool - s.PatchValue(common.EnvtoolsFindTools, func(e environs.Environ, major, minor int, filter coretools.Filter) (list coretools.List, err error) { + s.PatchValue(common.EnvtoolsFindTools, func(e environs.Environ, major, minor int, stream string, filter coretools.Filter) (list coretools.List, err error) { called = true c.Assert(filter.Number, gc.Equals, version.Current.Number) c.Assert(filter.Series, gc.Equals, version.Current.Series) - c.Assert(filter.Arch, gc.Equals, version.Current.Arch) + c.Assert(filter.Arch, gc.Equals, arch.HostArch()) + if develVersion { + c.Assert(stream, gc.Equals, "devel") + } else { + c.Assert(stream, gc.Equals, "released") + } return nil, errors.NotFoundf("tools") }) toolsFinder := common.NewToolsFinder(s.State, t, sprintfURLGetter("tools:%s")) @@ -221,7 +239,7 @@ MajorVersion: -1, MinorVersion: -1, Series: version.Current.Series, - Arch: version.Current.Arch, + Arch: arch.HostArch(), }) c.Assert(err, jc.ErrorIsNil) if inStorage { @@ -235,7 +253,7 @@ func (s *toolsSuite) TestFindToolsToolsStorageError(c *gc.C) { var called bool - s.PatchValue(common.EnvtoolsFindTools, func(e environs.Environ, major, minor int, filter coretools.Filter) (list coretools.List, err error) { + s.PatchValue(common.EnvtoolsFindTools, func(e environs.Environ, major, minor int, stream string, filter coretools.Filter) (list coretools.List, err error) { called = true return nil, errors.NotFoundf("tools") }) === modified file 'src/github.com/juju/juju/apiserver/common/volumes.go' --- src/github.com/juju/juju/apiserver/common/volumes.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/common/volumes.go 2015-10-23 18:29:32 +0000 @@ -4,8 +4,6 @@ package common import ( - "fmt" - "github.com/juju/errors" "github.com/juju/names" @@ -28,19 +26,27 @@ return ok } -// VolumeParams returns the parameters for creating the given volume. +// VolumeParams returns the parameters for creating or destroying +// the given volume. func VolumeParams( v state.Volume, storageInstance state.StorageInstance, environConfig *config.Config, poolManager poolmanager.PoolManager, ) (params.VolumeParams, error) { - stateVolumeParams, ok := v.Params() - if !ok { - err := &volumeAlreadyProvisionedError{fmt.Errorf( - "volume %q is already provisioned", v.Tag().Id(), - )} - return params.VolumeParams{}, err + + var pool string + var size uint64 + if stateVolumeParams, ok := v.Params(); ok { + pool = stateVolumeParams.Pool + size = stateVolumeParams.Size + } else { + volumeInfo, err := v.Info() + if err != nil { + return params.VolumeParams{}, errors.Trace(err) + } + pool = volumeInfo.Pool + size = volumeInfo.Size } volumeTags, err := storageTags(storageInstance, environConfig) @@ -48,13 +54,13 @@ return params.VolumeParams{}, errors.Annotate(err, "computing storage tags") } - providerType, cfg, err := StoragePoolConfig(stateVolumeParams.Pool, poolManager) + providerType, cfg, err := StoragePoolConfig(pool, poolManager) if err != nil { return params.VolumeParams{}, errors.Trace(err) } return params.VolumeParams{ v.Tag().String(), - stateVolumeParams.Size, + size, string(providerType), cfg.Attrs(), volumeTags, @@ -123,15 +129,20 @@ } return params.Volume{ v.VolumeTag().String(), - params.VolumeInfo{ - info.VolumeId, - info.HardwareId, - info.Size, - info.Persistent, - }, + VolumeInfoFromState(info), }, nil } +// VolumeInfoFromState converts a state.VolumeInfo to params.VolumeInfo. +func VolumeInfoFromState(info state.VolumeInfo) params.VolumeInfo { + return params.VolumeInfo{ + info.VolumeId, + info.HardwareId, + info.Size, + info.Persistent, + } +} + // VolumeAttachmentFromState converts a state.VolumeAttachment to params.VolumeAttachment. func VolumeAttachmentFromState(v state.VolumeAttachment) (params.VolumeAttachment, error) { info, err := v.Info() @@ -141,14 +152,20 @@ return params.VolumeAttachment{ v.Volume().String(), v.Machine().String(), - params.VolumeAttachmentInfo{ - info.DeviceName, - info.BusAddress, - info.ReadOnly, - }, + VolumeAttachmentInfoFromState(info), }, nil } +// VolumeAttachmentInfoFromState converts a state.VolumeAttachmentInfo to params.VolumeAttachmentInfo. +func VolumeAttachmentInfoFromState(info state.VolumeAttachmentInfo) params.VolumeAttachmentInfo { + return params.VolumeAttachmentInfo{ + info.DeviceName, + info.DeviceLink, + info.BusAddress, + info.ReadOnly, + } +} + // VolumeAttachmentInfosToState converts a map of volume tags to // params.VolumeAttachmentInfo to a map of volume tags to // state.VolumeAttachmentInfo. @@ -184,6 +201,7 @@ func VolumeAttachmentInfoToState(in params.VolumeAttachmentInfo) state.VolumeAttachmentInfo { return state.VolumeAttachmentInfo{ in.DeviceName, + in.DeviceLink, in.BusAddress, in.ReadOnly, } === modified file 'src/github.com/juju/juju/apiserver/common/volumes_test.go' --- src/github.com/juju/juju/apiserver/common/volumes_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/common/volumes_test.go 2015-10-23 18:29:32 +0000 @@ -4,7 +4,6 @@ package common_test import ( - "github.com/juju/errors" "github.com/juju/names" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" @@ -13,8 +12,6 @@ "github.com/juju/juju/apiserver/params" "github.com/juju/juju/environs/tags" "github.com/juju/juju/state" - "github.com/juju/juju/storage" - "github.com/juju/juju/storage/poolmanager" "github.com/juju/juju/testing" ) @@ -22,46 +19,24 @@ var _ = gc.Suite(&volumesSuite{}) -type fakeVolume struct { - state.Volume - tag names.Tag - provisioned bool -} - -func (v *fakeVolume) Tag() names.Tag { - return v.tag -} - -func (v *fakeVolume) Params() (state.VolumeParams, bool) { - return state.VolumeParams{ - Pool: "loop", - Size: 1024, - }, !v.provisioned -} - -func (*volumesSuite) TestVolumeParamsAlreadyProvisioned(c *gc.C) { - tag := names.NewVolumeTag("100") - _, err := common.VolumeParams( - &fakeVolume{tag: tag, provisioned: true}, - nil, // StorageInstance - testing.EnvironConfig(c), - nil, // PoolManager - ) - c.Assert(err, jc.Satisfies, common.IsVolumeAlreadyProvisioned) -} - -type fakePoolManager struct { - poolmanager.PoolManager -} - -func (pm *fakePoolManager) Get(name string) (*storage.Config, error) { - return nil, errors.NotFoundf("pool") -} - -func (*volumesSuite) TestVolumeParams(c *gc.C) { +func (s *volumesSuite) TestVolumeParams(c *gc.C) { + s.testVolumeParams(c, &state.VolumeParams{ + Pool: "loop", + Size: 1024, + }, nil) +} + +func (s *volumesSuite) TestVolumeParamsAlreadyProvisioned(c *gc.C) { + s.testVolumeParams(c, nil, &state.VolumeInfo{ + Pool: "loop", + Size: 1024, + }) +} + +func (*volumesSuite) testVolumeParams(c *gc.C, volumeParams *state.VolumeParams, info *state.VolumeInfo) { tag := names.NewVolumeTag("100") p, err := common.VolumeParams( - &fakeVolume{tag: tag}, + &fakeVolume{tag: tag, params: volumeParams, info: info}, nil, // StorageInstance testing.CustomEnvironConfig(c, testing.Attrs{ "resource-tags": "a=b c=", @@ -86,7 +61,9 @@ storageTag := names.NewStorageTag("mystore/0") unitTag := names.NewUnitTag("mysql/123") p, err := common.VolumeParams( - &fakeVolume{tag: volumeTag}, + &fakeVolume{tag: volumeTag, params: &state.VolumeParams{ + Pool: "loop", Size: 1024, + }}, &fakeStorageInstance{tag: storageTag, owner: unitTag}, testing.CustomEnvironConfig(c, nil), &fakePoolManager{}, === modified file 'src/github.com/juju/juju/apiserver/debuglog.go' --- src/github.com/juju/juju/apiserver/debuglog.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/debuglog.go 2015-10-23 18:29:32 +0000 @@ -7,32 +7,53 @@ "encoding/json" "fmt" "io" + "net" "net/http" "net/url" - "os" - "path/filepath" - "regexp" "strconv" - "strings" + "syscall" + "github.com/juju/errors" "github.com/juju/loggo" - "github.com/juju/names" - "github.com/juju/utils/tailer" "golang.org/x/net/websocket" - "launchpad.net/tomb" "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/state" ) // debugLogHandler takes requests to watch the debug log. +// +// It provides the underlying framework for the 2 debug-log +// variants. The supplied handle func allows for varied handling of +// requests. type debugLogHandler struct { httpHandler - logDir string -} - -var maxLinesReached = fmt.Errorf("max lines reached") - -// ServeHTTP will serve up connections as a websocket. + stop <-chan struct{} + handle debugLogHandlerFunc +} + +type debugLogHandlerFunc func( + state.LoggingState, + *debugLogParams, + debugLogSocket, + <-chan struct{}, +) error + +func newDebugLogHandler( + statePool *state.StatePool, + stop <-chan struct{}, + handle debugLogHandlerFunc, +) *debugLogHandler { + return &debugLogHandler{ + httpHandler: httpHandler{statePool: statePool}, + stop: stop, + handle: handle, + } +} + +// ServeHTTP will serve up connections as a websocket for the +// debug-log API. +// // Args for the HTTP request are as follows: // includeEntity -> []string - lists entity tags to include in the response // - tags may finish with a '*' to match a prefix e.g.: unit-mysql-*, machine-2 @@ -50,63 +71,34 @@ // replay -> string - one of [true, false], if true, start the file from the start func (h *debugLogHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { server := websocket.Server{ - Handler: func(socket *websocket.Conn) { + Handler: func(conn *websocket.Conn) { + socket := &debugLogSocketImpl{conn} + defer socket.Close() + logger.Infof("debug log handler starting") // Validate before authenticate because the authentication is // dependent on the state connection that is determined during the // validation. stateWrapper, err := h.validateEnvironUUID(req) if err != nil { - h.sendError(socket, err) - socket.Close() + socket.sendError(err) return } - defer stateWrapper.cleanup() - // TODO (thumper): We need to work out how we are going to filter - // logging information based on environment. if err := stateWrapper.authenticateUser(req); err != nil { - h.sendError(socket, fmt.Errorf("auth failed: %v", err)) - socket.Close() - return - } - stream, err := newLogStream(req.URL.Query()) - if err != nil { - h.sendError(socket, err) - socket.Close() - return - } - // Open log file. - logLocation := filepath.Join(h.logDir, "all-machines.log") - logFile, err := os.Open(logLocation) - if err != nil { - h.sendError(socket, fmt.Errorf("cannot open log file: %v", err)) - socket.Close() - return - } - defer logFile.Close() - if err := stream.positionLogFile(logFile); err != nil { - h.sendError(socket, fmt.Errorf("cannot position log file: %v", err)) - socket.Close() - return - } - - // If we get to here, no more errors to report, so we report a nil - // error. This way the first line of the socket is always a json - // formatted simple error. - if err := h.sendError(socket, nil); err != nil { - logger.Errorf("could not send good log stream start") - socket.Close() - return - } - - stream.start(logFile, socket) - go func() { - defer stream.tomb.Done() - defer socket.Close() - stream.tomb.Kill(stream.loop()) - }() - if err := stream.tomb.Wait(); err != nil { - if err != maxLinesReached { + socket.sendError(fmt.Errorf("auth failed: %v", err)) + return + } + + params, err := readDebugLogParams(req.URL.Query()) + if err != nil { + socket.sendError(err) + return + } + + if err := h.handle(stateWrapper.state, params, socket, h.stop); err != nil { + if isBrokenPipe(err) { + logger.Tracef("debug-log handler stopped (client disconnected)") + } else { logger.Errorf("debug-log handler error: %v", err) } } @@ -114,58 +106,40 @@ server.ServeHTTP(w, req) } -func newLogStream(queryMap url.Values) (*logStream, error) { - maxLines := uint(0) - if value := queryMap.Get("maxLines"); value != "" { - num, err := strconv.ParseUint(value, 10, 64) - if err != nil { - return nil, fmt.Errorf("maxLines value %q is not a valid unsigned number", value) - } - maxLines = uint(num) - } - - fromTheStart := false - if value := queryMap.Get("replay"); value != "" { - replay, err := strconv.ParseBool(value) - if err != nil { - return nil, fmt.Errorf("replay value %q is not a valid boolean", value) - } - fromTheStart = replay - } - - backlog := uint(0) - if value := queryMap.Get("backlog"); value != "" { - num, err := strconv.ParseUint(value, 10, 64) - if err != nil { - return nil, fmt.Errorf("backlog value %q is not a valid unsigned number", value) - } - backlog = uint(num) - } - - level := loggo.UNSPECIFIED - if value := queryMap.Get("level"); value != "" { - var ok bool - level, ok = loggo.ParseLevel(value) - if !ok || level < loggo.TRACE || level > loggo.ERROR { - return nil, fmt.Errorf("level value %q is not one of %q, %q, %q, %q, %q", - value, loggo.TRACE, loggo.DEBUG, loggo.INFO, loggo.WARNING, loggo.ERROR) - } - } - - return &logStream{ - includeEntity: queryMap["includeEntity"], - includeModule: queryMap["includeModule"], - excludeEntity: queryMap["excludeEntity"], - excludeModule: queryMap["excludeModule"], - maxLines: maxLines, - fromTheStart: fromTheStart, - backlog: backlog, - filterLevel: level, - }, nil -} - -// sendError sends a JSON-encoded error response. -func (h *debugLogHandler) sendError(w io.Writer, err error) error { +func isBrokenPipe(err error) bool { + err = errors.Cause(err) + if opErr, ok := err.(*net.OpError); ok { + return opErr.Err == syscall.EPIPE + } + return false +} + +// debugLogSocket describes the functionality required for the +// debuglog handlers to send logs to the client. +type debugLogSocket interface { + io.Writer + + // sendOk sends a nil error response, indicating there were no errors. + sendOk() error + + // sendError sends a JSON-encoded error response. + sendError(err error) error +} + +// debugLogSocketImpl implements the debugLogSocket interface. It +// wraps a websocket.Conn and provides a few debug-log specific helper +// methods. +type debugLogSocketImpl struct { + *websocket.Conn +} + +// sendOK implements debugLogSocket. +func (s *debugLogSocketImpl) sendOk() error { + return s.sendError(nil) +} + +// sendErr implements debugLogSocket. +func (s *debugLogSocketImpl) sendError(err error) error { response := ¶ms.ErrorResult{} if err != nil { response.Error = ¶ms.Error{Message: fmt.Sprint(err)} @@ -177,196 +151,63 @@ return err } message = append(message, []byte("\n")...) - _, err = w.Write(message) + _, err = s.Conn.Write(message) return err } -type logLine struct { - line string - agentTag string - agentName string - level loggo.Level - module string -} - -func parseLogLine(line string) *logLine { - const ( - agentTagIndex = 0 - levelIndex = 3 - moduleIndex = 4 - ) - fields := strings.Fields(line) - result := &logLine{ - line: line, - } - if len(fields) > agentTagIndex { - agentTag := fields[agentTagIndex] - // Drop mandatory trailing colon (:). - // Since colon is mandatory, agentTag without it is invalid and will be empty (""). - if strings.HasSuffix(agentTag, ":") { - result.agentTag = agentTag[:len(agentTag)-1] - } - /* - Drop unit suffix. - In logs, unit information may be prefixed with either a unit_tag by itself or a unit_tag[nnnn]. - The code below caters for both scenarios. - */ - if bracketIndex := strings.Index(agentTag, "["); bracketIndex != -1 { - result.agentTag = agentTag[:bracketIndex] - } - // If, at this stage, result.agentTag is empty, we could not deduce the tag. No point getting the name... - if result.agentTag != "" { - // Entity Name deduced from entity tag - entityTag, err := names.ParseTag(result.agentTag) - if err != nil { - /* - Logging error but effectively swallowing it as there is no where to propogate. - We don't expect ParseTag to fail since the tag was generated by juju in the first place. - */ - logger.Errorf("Could not deduce name from tag %q: %v\n", result.agentTag, err) - } - result.agentName = entityTag.Id() - } - } - if len(fields) > moduleIndex { - if level, valid := loggo.ParseLevel(fields[levelIndex]); valid { - result.level = level - result.module = fields[moduleIndex] - } - } - - return result -} - -// logStream runs the tailer to read a log file and stream -// it via a web socket. -type logStream struct { - tomb tomb.Tomb - logTailer *tailer.Tailer +// debugLogParams contains the parsed debuglog API request parameters. +type debugLogParams struct { + maxLines uint + fromTheStart bool + backlog uint filterLevel loggo.Level includeEntity []string + excludeEntity []string includeModule []string - excludeEntity []string excludeModule []string - backlog uint - maxLines uint - lineCount uint - fromTheStart bool -} - -// positionLogFile will update the internal read position of the logFile to be -// at the end of the file or somewhere in the middle if backlog has been specified. -func (stream *logStream) positionLogFile(logFile io.ReadSeeker) error { - // Seek to the end, or lines back from the end if we need to. - if !stream.fromTheStart { - return tailer.SeekLastLines(logFile, stream.backlog, stream.filterLine) - } - return nil -} - -// start the tailer listening to the logFile, and sending the matching -// lines to the writer. -func (stream *logStream) start(logFile io.ReadSeeker, writer io.Writer) { - stream.logTailer = tailer.NewTailer(logFile, writer, stream.countedFilterLine) -} - -// loop starts the tailer with the log file and the web socket. -func (stream *logStream) loop() error { - select { - case <-stream.logTailer.Dead(): - return stream.logTailer.Err() - case <-stream.tomb.Dying(): - stream.logTailer.Stop() - } - return nil -} - -// filterLine checks the received line for one of the configured tags. -func (stream *logStream) filterLine(line []byte) bool { - log := parseLogLine(string(line)) - return stream.checkIncludeEntity(log) && - stream.checkIncludeModule(log) && - !stream.exclude(log) && - stream.checkLevel(log) -} - -// countedFilterLine checks the received line for one of the configured tags, -// and also checks to make sure the stream doesn't send more than the -// specified number of lines. -func (stream *logStream) countedFilterLine(line []byte) bool { - result := stream.filterLine(line) - if result && stream.maxLines > 0 { - stream.lineCount++ - result = stream.lineCount <= stream.maxLines - if stream.lineCount == stream.maxLines { - stream.tomb.Kill(maxLinesReached) - } - } - return result -} - -func (stream *logStream) checkIncludeEntity(line *logLine) bool { - if len(stream.includeEntity) == 0 { - return true - } - for _, value := range stream.includeEntity { - if agentMatchesFilter(line, value) { - return true - } - } - return false -} - -// agentMatchesFilter checks if agentTag tag or agentTag name match given filter -func agentMatchesFilter(line *logLine, aFilter string) bool { - return hasMatch(line.agentName, aFilter) || hasMatch(line.agentTag, aFilter) -} - -// hasMatch determines if value contains filter using regular expressions. -// All wildcard occurrences are changed to `.*` -// Currently, all match exceptions are logged and not propagated. -func hasMatch(value, aFilter string) bool { - /* Special handling: out of 12 regexp metacharacters \^$.|?+()[*{ - only asterix (*) can be legally used as a wildcard in this context. - Both machine and unit tag and name specifications do not allow any other metas. - Consequently, if aFilter contains wildcard (*), do not escape it - - transform it into a regexp "any character(s)" sequence. - */ - aFilter = strings.Replace(aFilter, "*", `.*`, -1) - matches, err := regexp.MatchString("^"+aFilter+"$", value) - if err != nil { - // logging errors here... but really should they be swallowed? - logger.Errorf("\nCould not match filter %q and regular expression %q\n.%v\n", value, aFilter, err) - } - return matches -} - -func (stream *logStream) checkIncludeModule(line *logLine) bool { - if len(stream.includeModule) == 0 { - return true - } - for _, value := range stream.includeModule { - if strings.HasPrefix(line.module, value) { - return true - } - } - return false -} - -func (stream *logStream) exclude(line *logLine) bool { - for _, value := range stream.excludeEntity { - if agentMatchesFilter(line, value) { - return true - } - } - for _, value := range stream.excludeModule { - if strings.HasPrefix(line.module, value) { - return true - } - } - return false -} - -func (stream *logStream) checkLevel(line *logLine) bool { - return line.level >= stream.filterLevel +} + +func readDebugLogParams(queryMap url.Values) (*debugLogParams, error) { + params := new(debugLogParams) + + if value := queryMap.Get("maxLines"); value != "" { + num, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return nil, errors.Errorf("maxLines value %q is not a valid unsigned number", value) + } + params.maxLines = uint(num) + } + + if value := queryMap.Get("replay"); value != "" { + replay, err := strconv.ParseBool(value) + if err != nil { + return nil, errors.Errorf("replay value %q is not a valid boolean", value) + } + params.fromTheStart = replay + } + + if value := queryMap.Get("backlog"); value != "" { + num, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return nil, errors.Errorf("backlog value %q is not a valid unsigned number", value) + } + params.backlog = uint(num) + } + + if value := queryMap.Get("level"); value != "" { + var ok bool + level, ok := loggo.ParseLevel(value) + if !ok || level < loggo.TRACE || level > loggo.ERROR { + return nil, errors.Errorf("level value %q is not one of %q, %q, %q, %q, %q", + value, loggo.TRACE, loggo.DEBUG, loggo.INFO, loggo.WARNING, loggo.ERROR) + } + params.filterLevel = level + } + + params.includeEntity = queryMap["includeEntity"] + params.excludeEntity = queryMap["excludeEntity"] + params.includeModule = queryMap["includeModule"] + params.excludeModule = queryMap["excludeModule"] + + return params, nil } === added file 'src/github.com/juju/juju/apiserver/debuglog_db.go' --- src/github.com/juju/juju/apiserver/debuglog_db.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/debuglog_db.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,95 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package apiserver + +import ( + "fmt" + "net/http" + "time" + + "github.com/juju/errors" + + "github.com/juju/juju/state" +) + +func newDebugLogDBHandler(statePool *state.StatePool, stop <-chan struct{}) http.Handler { + return newDebugLogHandler(statePool, stop, handleDebugLogDBRequest) +} + +func handleDebugLogDBRequest( + st state.LoggingState, + reqParams *debugLogParams, + socket debugLogSocket, + stop <-chan struct{}, +) error { + params := makeLogTailerParams(reqParams) + tailer := newLogTailer(st, params) + defer tailer.Stop() + + // Indicate that all is well. + if err := socket.sendOk(); err != nil { + return errors.Trace(err) + } + + var lineCount uint + for { + select { + case <-stop: + return nil + case rec, ok := <-tailer.Logs(): + if !ok { + return errors.Annotate(tailer.Err(), "tailer stopped") + } + + line := formatLogRecord(rec) + _, err := socket.Write([]byte(line)) + if err != nil { + return errors.Annotate(err, "sending failed") + } + + lineCount++ + if reqParams.maxLines > 0 && lineCount == reqParams.maxLines { + return nil + } + } + } + + return nil +} + +func makeLogTailerParams(reqParams *debugLogParams) *state.LogTailerParams { + params := &state.LogTailerParams{ + MinLevel: reqParams.filterLevel, + InitialLines: int(reqParams.backlog), + IncludeEntity: reqParams.includeEntity, + ExcludeEntity: reqParams.excludeEntity, + IncludeModule: reqParams.includeModule, + ExcludeModule: reqParams.excludeModule, + } + if reqParams.fromTheStart { + params.InitialLines = 0 + } + return params +} + +func formatLogRecord(r *state.LogRecord) string { + return fmt.Sprintf("%s: %s %s %s %s %s\n", + r.Entity, + formatTime(r.Time), + r.Level.String(), + r.Module, + r.Location, + r.Message, + ) +} + +func formatTime(t time.Time) string { + return t.In(time.UTC).Format("2006-01-02 15:04:05") +} + +var newLogTailer = _newLogTailer // For replacing in tests + +func _newLogTailer(st state.LoggingState, params *state.LogTailerParams) state.LogTailer { + return state.NewLogTailer(st, params) +} === added file 'src/github.com/juju/juju/apiserver/debuglog_db_internal_test.go' --- src/github.com/juju/juju/apiserver/debuglog_db_internal_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/debuglog_db_internal_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,250 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package apiserver + +import ( + "fmt" + "time" + + "github.com/juju/loggo" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/state" + coretesting "github.com/juju/juju/testing" +) + +type debugLogDBIntSuite struct { + coretesting.BaseSuite + sock *fakeDebugLogSocket +} + +var _ = gc.Suite(&debugLogDBIntSuite{}) + +func (s *debugLogDBIntSuite) SetUpTest(c *gc.C) { + s.BaseSuite.SetUpTest(c) + s.sock = newFakeDebugLogSocket() +} + +func (s *debugLogDBIntSuite) TestParamConversion(c *gc.C) { + reqParams := &debugLogParams{ + fromTheStart: false, + backlog: 11, + filterLevel: loggo.INFO, + includeEntity: []string{"foo"}, + includeModule: []string{"bar"}, + excludeEntity: []string{"baz"}, + excludeModule: []string{"qux"}, + } + + called := false + s.PatchValue(&newLogTailer, func(_ state.LoggingState, params *state.LogTailerParams) state.LogTailer { + called = true + + // Start time will be used once the client is extended to send + // time range arguments. + c.Assert(params.StartTime.IsZero(), jc.IsTrue) + + c.Assert(params.MinLevel, gc.Equals, loggo.INFO) + c.Assert(params.InitialLines, gc.Equals, 11) + c.Assert(params.IncludeEntity, jc.DeepEquals, []string{"foo"}) + c.Assert(params.IncludeModule, jc.DeepEquals, []string{"bar"}) + c.Assert(params.ExcludeEntity, jc.DeepEquals, []string{"baz"}) + c.Assert(params.ExcludeModule, jc.DeepEquals, []string{"qux"}) + + return newFakeLogTailer() + }) + + stop := make(chan struct{}) + close(stop) // Stop the request immediately. + err := handleDebugLogDBRequest(nil, reqParams, s.sock, stop) + c.Assert(err, jc.ErrorIsNil) + c.Assert(called, jc.IsTrue) +} + +func (s *debugLogDBIntSuite) TestParamConversionReplay(c *gc.C) { + reqParams := &debugLogParams{ + fromTheStart: true, + backlog: 123, + } + + called := false + s.PatchValue(&newLogTailer, func(_ state.LoggingState, params *state.LogTailerParams) state.LogTailer { + called = true + + c.Assert(params.StartTime.IsZero(), jc.IsTrue) + c.Assert(params.InitialLines, gc.Equals, 0) + + return newFakeLogTailer() + }) + + stop := make(chan struct{}) + close(stop) // Stop the request immediately. + err := handleDebugLogDBRequest(nil, reqParams, s.sock, stop) + c.Assert(err, jc.ErrorIsNil) + c.Assert(called, jc.IsTrue) +} + +func (s *debugLogDBIntSuite) TestFullRequest(c *gc.C) { + // Set up a fake log tailer with a 2 log records ready to send. + tailer := newFakeLogTailer() + tailer.logsCh <- &state.LogRecord{ + Time: time.Date(2015, 6, 19, 15, 34, 37, 0, time.UTC), + Entity: "machine-99", + Module: "some.where", + Location: "code.go:42", + Level: loggo.INFO, + Message: "stuff happened", + } + tailer.logsCh <- &state.LogRecord{ + Time: time.Date(2015, 6, 19, 15, 36, 40, 0, time.UTC), + Entity: "unit-foo-2", + Module: "else.where", + Location: "go.go:22", + Level: loggo.ERROR, + Message: "whoops", + } + s.PatchValue(&newLogTailer, func(_ state.LoggingState, params *state.LogTailerParams) state.LogTailer { + return tailer + }) + + stop := make(chan struct{}) + done := s.runRequest(&debugLogParams{}, stop) + + s.assertOutput(c, []string{ + "ok", // sendOk() call needs to happen first. + "machine-99: 2015-06-19 15:34:37 INFO some.where code.go:42 stuff happened\n", + "unit-foo-2: 2015-06-19 15:36:40 ERROR else.where go.go:22 whoops\n", + }) + + // Check the request stops when requested. + close(stop) + s.assertStops(c, done, tailer) +} + +func (s *debugLogDBIntSuite) TestRequestStopsWhenTailerStops(c *gc.C) { + tailer := newFakeLogTailer() + s.PatchValue(&newLogTailer, func(_ state.LoggingState, params *state.LogTailerParams) state.LogTailer { + close(tailer.logsCh) // make the request stop immediately + return tailer + }) + + err := handleDebugLogDBRequest(nil, &debugLogParams{}, s.sock, nil) + c.Assert(err, jc.ErrorIsNil) + c.Assert(tailer.stopped, jc.IsTrue) +} + +func (s *debugLogDBIntSuite) TestMaxLines(c *gc.C) { + // Set up a fake log tailer with a 5 log records ready to send. + tailer := newFakeLogTailer() + for i := 0; i < 5; i++ { + tailer.logsCh <- &state.LogRecord{ + Time: time.Date(2015, 6, 19, 15, 34, 37, 0, time.UTC), + Entity: "machine-99", + Module: "some.where", + Location: "code.go:42", + Level: loggo.INFO, + Message: "stuff happened", + } + } + s.PatchValue(&newLogTailer, func(_ state.LoggingState, params *state.LogTailerParams) state.LogTailer { + return tailer + }) + + done := s.runRequest(&debugLogParams{maxLines: 3}, nil) + + s.assertOutput(c, []string{ + "ok", // sendOk() call needs to happen first. + "machine-99: 2015-06-19 15:34:37 INFO some.where code.go:42 stuff happened\n", + "machine-99: 2015-06-19 15:34:37 INFO some.where code.go:42 stuff happened\n", + "machine-99: 2015-06-19 15:34:37 INFO some.where code.go:42 stuff happened\n", + }) + + // The tailer should now stop by itself after the line limit was reached. + s.assertStops(c, done, tailer) +} + +func (s *debugLogDBIntSuite) runRequest(params *debugLogParams, stop chan struct{}) chan error { + done := make(chan error) + go func() { + done <- handleDebugLogDBRequest(&fakeState{}, params, s.sock, stop) + }() + return done +} + +func (s *debugLogDBIntSuite) assertOutput(c *gc.C, expectedWrites []string) { + timeout := time.After(coretesting.LongWait) + for i, expectedWrite := range expectedWrites { + select { + case actualWrite := <-s.sock.writes: + c.Assert(actualWrite, gc.Equals, expectedWrite) + case <-timeout: + c.Fatalf("timed out waiting for socket write (received %d)", i) + } + } +} + +func (s *debugLogDBIntSuite) assertStops(c *gc.C, done chan error, tailer *fakeLogTailer) { + select { + case err := <-done: + c.Assert(err, jc.ErrorIsNil) + c.Assert(tailer.stopped, jc.IsTrue) + case <-time.After(coretesting.LongWait): + c.Fatal("timed out waiting for request handler to stop") + } +} + +type fakeState struct { + state.LoggingState +} + +func newFakeLogTailer() *fakeLogTailer { + return &fakeLogTailer{ + logsCh: make(chan *state.LogRecord, 10), + } +} + +type fakeLogTailer struct { + state.LogTailer + logsCh chan *state.LogRecord + stopped bool +} + +func (t *fakeLogTailer) Logs() <-chan *state.LogRecord { + return t.logsCh +} + +func (t *fakeLogTailer) Stop() error { + t.stopped = true + return nil +} + +func (t *fakeLogTailer) Err() error { + return nil +} + +func newFakeDebugLogSocket() *fakeDebugLogSocket { + return &fakeDebugLogSocket{ + writes: make(chan string, 10), + } +} + +type fakeDebugLogSocket struct { + writes chan string +} + +func (s *fakeDebugLogSocket) sendOk() error { + s.writes <- "ok" + return nil +} + +func (s *fakeDebugLogSocket) sendError(err error) error { + s.writes <- fmt.Sprintf("err: %v", err) + return nil +} + +func (s *fakeDebugLogSocket) Write(buf []byte) (int, error) { + s.writes <- string(buf) + return len(buf), nil +} === added file 'src/github.com/juju/juju/apiserver/debuglog_db_test.go' --- src/github.com/juju/juju/apiserver/debuglog_db_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/debuglog_db_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,23 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package apiserver_test + +import gc "gopkg.in/check.v1" + +// debugLogDBSuite runs the common debuglog API tests when the db-log +// feature flag is enabled. These tests are inherited from +// debugLogBaseSuite. +type debugLogDBSuite struct { + debugLogBaseSuite +} + +var _ = gc.Suite(&debugLogDBSuite{}) + +func (s *debugLogDBSuite) SetUpSuite(c *gc.C) { + s.SetInitialFeatureFlags("db-log") + s.debugLogBaseSuite.SetUpSuite(c) +} + +// See debuglog_db_internal_test.go for DB specific unit tests and the +// featuretests package for an end-to-end integration test. === added file 'src/github.com/juju/juju/apiserver/debuglog_file.go' --- src/github.com/juju/juju/apiserver/debuglog_file.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/debuglog_file.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,253 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package apiserver + +import ( + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/juju/juju/state" + "github.com/juju/loggo" + "github.com/juju/names" + "github.com/juju/utils/tailer" +) + +func newDebugLogFileHandler(statePool *state.StatePool, stop <-chan struct{}, logDir string) http.Handler { + fileHandler := &debugLogFileHandler{logDir: logDir} + return newDebugLogHandler(statePool, stop, fileHandler.handle) +} + +// debugLogFileHandler handles requests to watch all-machines.log. +type debugLogFileHandler struct { + logDir string +} + +func (h *debugLogFileHandler) handle( + _ state.LoggingState, + params *debugLogParams, + socket debugLogSocket, + stop <-chan struct{}, +) error { + stream := newLogFileStream(params) + + // Open log file. + logLocation := filepath.Join(h.logDir, "all-machines.log") + logFile, err := os.Open(logLocation) + if err != nil { + socket.sendError(fmt.Errorf("cannot open log file: %v", err)) + return err + } + defer logFile.Close() + + if err := stream.positionLogFile(logFile); err != nil { + socket.sendError(fmt.Errorf("cannot position log file: %v", err)) + return err + } + + // If we get to here, no more errors to report. + if err := socket.sendOk(); err != nil { + return err + } + + stream.start(logFile, socket) + return stream.wait(stop) +} + +func newLogFileStream(params *debugLogParams) *logFileStream { + return &logFileStream{ + debugLogParams: params, + maxLinesReached: make(chan bool), + } +} + +type logFileLine struct { + line string + agentTag string + agentName string + level loggo.Level + module string +} + +func parseLogLine(line string) *logFileLine { + const ( + agentTagIndex = 0 + levelIndex = 3 + moduleIndex = 4 + ) + fields := strings.Fields(line) + result := &logFileLine{ + line: line, + } + if len(fields) > agentTagIndex { + agentTag := fields[agentTagIndex] + // Drop mandatory trailing colon (:). + // Since colon is mandatory, agentTag without it is invalid and will be empty (""). + if strings.HasSuffix(agentTag, ":") { + result.agentTag = agentTag[:len(agentTag)-1] + } + /* + Drop unit suffix. + In logs, unit information may be prefixed with either a unit_tag by itself or a unit_tag[nnnn]. + The code below caters for both scenarios. + */ + if bracketIndex := strings.Index(agentTag, "["); bracketIndex != -1 { + result.agentTag = agentTag[:bracketIndex] + } + // If, at this stage, result.agentTag is empty, we could not deduce the tag. No point getting the name... + if result.agentTag != "" { + // Entity Name deduced from entity tag + entityTag, err := names.ParseTag(result.agentTag) + if err != nil { + /* + Logging error but effectively swallowing it as there is no where to propogate. + We don't expect ParseTag to fail since the tag was generated by juju in the first place. + */ + logger.Errorf("Could not deduce name from tag %q: %v\n", result.agentTag, err) + } + result.agentName = entityTag.Id() + } + } + if len(fields) > moduleIndex { + if level, valid := loggo.ParseLevel(fields[levelIndex]); valid { + result.level = level + result.module = fields[moduleIndex] + } + } + + return result +} + +// logFileStream runs the tailer to read a log file and stream it via +// a web socket. +type logFileStream struct { + *debugLogParams + logTailer *tailer.Tailer + lineCount uint + maxLinesReached chan bool +} + +// positionLogFile will update the internal read position of the logFile to be +// at the end of the file or somewhere in the middle if backlog has been specified. +func (stream *logFileStream) positionLogFile(logFile io.ReadSeeker) error { + // Seek to the end, or lines back from the end if we need to. + if !stream.fromTheStart { + return tailer.SeekLastLines(logFile, stream.backlog, stream.filterLine) + } + return nil +} + +// start the tailer listening to the logFile, and sending the matching +// lines to the writer. +func (stream *logFileStream) start(logFile io.ReadSeeker, writer io.Writer) { + stream.logTailer = tailer.NewTailer(logFile, writer, stream.countedFilterLine) +} + +// wait blocks until the logTailer is done or the maximum line count +// has been reached or the stop channel is closed. +func (stream *logFileStream) wait(stop <-chan struct{}) error { + select { + case <-stream.logTailer.Dead(): + return stream.logTailer.Err() + case <-stream.maxLinesReached: + stream.logTailer.Stop() + case <-stop: + stream.logTailer.Stop() + } + return nil +} + +// filterLine checks the received line for one of the configured tags. +func (stream *logFileStream) filterLine(line []byte) bool { + log := parseLogLine(string(line)) + return stream.checkIncludeEntity(log) && + stream.checkIncludeModule(log) && + !stream.exclude(log) && + stream.checkLevel(log) +} + +// countedFilterLine checks the received line for one of the configured tags, +// and also checks to make sure the stream doesn't send more than the +// specified number of lines. +func (stream *logFileStream) countedFilterLine(line []byte) bool { + result := stream.filterLine(line) + if result && stream.maxLines > 0 { + stream.lineCount++ + result = stream.lineCount <= stream.maxLines + if stream.lineCount == stream.maxLines { + close(stream.maxLinesReached) + } + } + return result +} + +func (stream *logFileStream) checkIncludeEntity(line *logFileLine) bool { + if len(stream.includeEntity) == 0 { + return true + } + for _, value := range stream.includeEntity { + if agentMatchesFilter(line, value) { + return true + } + } + return false +} + +// agentMatchesFilter checks if agentTag tag or agentTag name match given filter +func agentMatchesFilter(line *logFileLine, aFilter string) bool { + return hasMatch(line.agentName, aFilter) || hasMatch(line.agentTag, aFilter) +} + +// hasMatch determines if value contains filter using regular expressions. +// All wildcard occurrences are changed to `.*` +// Currently, all match exceptions are logged and not propagated. +func hasMatch(value, aFilter string) bool { + /* Special handling: out of 12 regexp metacharacters \^$.|?+()[*{ + only asterix (*) can be legally used as a wildcard in this context. + Both machine and unit tag and name specifications do not allow any other metas. + Consequently, if aFilter contains wildcard (*), do not escape it - + transform it into a regexp "any character(s)" sequence. + */ + aFilter = strings.Replace(aFilter, "*", `.*`, -1) + matches, err := regexp.MatchString("^"+aFilter+"$", value) + if err != nil { + // logging errors here... but really should they be swallowed? + logger.Errorf("\nCould not match filter %q and regular expression %q\n.%v\n", value, aFilter, err) + } + return matches +} + +func (stream *logFileStream) checkIncludeModule(line *logFileLine) bool { + if len(stream.includeModule) == 0 { + return true + } + for _, value := range stream.includeModule { + if strings.HasPrefix(line.module, value) { + return true + } + } + return false +} + +func (stream *logFileStream) exclude(line *logFileLine) bool { + for _, value := range stream.excludeEntity { + if agentMatchesFilter(line, value) { + return true + } + } + for _, value := range stream.excludeModule { + if strings.HasPrefix(line.module, value) { + return true + } + } + return false +} + +func (stream *logFileStream) checkLevel(line *logFileLine) bool { + return line.level >= stream.filterLevel +} === added file 'src/github.com/juju/juju/apiserver/debuglog_file_internal_test.go' --- src/github.com/juju/juju/apiserver/debuglog_file_internal_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/debuglog_file_internal_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,459 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +// This is an internal package test. + +package apiserver + +import ( + "bytes" + "net/url" + "os" + "path/filepath" + "time" + + "github.com/juju/loggo" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/testing" +) + +type debugLogFileIntSuite struct { + testing.BaseSuite +} + +var _ = gc.Suite(&debugLogFileIntSuite{}) + +func (s *debugLogFileIntSuite) TestParseLogLine(c *gc.C) { + line := "machine-0: 2014-03-24 22:34:25 INFO juju.cmd.jujud machine.go:127 machine agent machine-0 start (1.17.7.1-trusty-amd64 [gc])" + logLine := parseLogLine(line) + c.Assert(logLine.line, gc.Equals, line) + c.Assert(logLine.agentTag, gc.Equals, "machine-0") + c.Assert(logLine.level, gc.Equals, loggo.INFO) + c.Assert(logLine.module, gc.Equals, "juju.cmd.jujud") +} + +func (s *debugLogFileIntSuite) TestParseLogLineMachineMultiline(c *gc.C) { + line := "machine-1: continuation line" + logLine := parseLogLine(line) + c.Assert(logLine.line, gc.Equals, line) + c.Assert(logLine.agentTag, gc.Equals, "machine-1") + c.Assert(logLine.level, gc.Equals, loggo.UNSPECIFIED) + c.Assert(logLine.module, gc.Equals, "") +} + +func (s *debugLogFileIntSuite) TestParseLogLineInvalid(c *gc.C) { + line := "not a full line" + logLine := parseLogLine(line) + c.Assert(logLine.line, gc.Equals, line) + c.Assert(logLine.agentTag, gc.Equals, "") + c.Assert(logLine.level, gc.Equals, loggo.UNSPECIFIED) + c.Assert(logLine.module, gc.Equals, "") +} + +func checkLevel(logValue, streamValue loggo.Level) bool { + line := &logFileLine{level: logValue} + params := debugLogParams{} + if streamValue != loggo.UNSPECIFIED { + params.filterLevel = streamValue + } + return newLogFileStream(¶ms).checkLevel(line) +} + +func (s *debugLogFileIntSuite) TestCheckLevel(c *gc.C) { + c.Check(checkLevel(loggo.UNSPECIFIED, loggo.UNSPECIFIED), jc.IsTrue) + c.Check(checkLevel(loggo.TRACE, loggo.UNSPECIFIED), jc.IsTrue) + c.Check(checkLevel(loggo.DEBUG, loggo.UNSPECIFIED), jc.IsTrue) + c.Check(checkLevel(loggo.INFO, loggo.UNSPECIFIED), jc.IsTrue) + c.Check(checkLevel(loggo.WARNING, loggo.UNSPECIFIED), jc.IsTrue) + c.Check(checkLevel(loggo.ERROR, loggo.UNSPECIFIED), jc.IsTrue) + c.Check(checkLevel(loggo.CRITICAL, loggo.UNSPECIFIED), jc.IsTrue) + + c.Check(checkLevel(loggo.UNSPECIFIED, loggo.TRACE), jc.IsFalse) + c.Check(checkLevel(loggo.TRACE, loggo.TRACE), jc.IsTrue) + c.Check(checkLevel(loggo.DEBUG, loggo.TRACE), jc.IsTrue) + c.Check(checkLevel(loggo.INFO, loggo.TRACE), jc.IsTrue) + c.Check(checkLevel(loggo.WARNING, loggo.TRACE), jc.IsTrue) + c.Check(checkLevel(loggo.ERROR, loggo.TRACE), jc.IsTrue) + c.Check(checkLevel(loggo.CRITICAL, loggo.TRACE), jc.IsTrue) + + c.Check(checkLevel(loggo.UNSPECIFIED, loggo.INFO), jc.IsFalse) + c.Check(checkLevel(loggo.TRACE, loggo.INFO), jc.IsFalse) + c.Check(checkLevel(loggo.DEBUG, loggo.INFO), jc.IsFalse) + c.Check(checkLevel(loggo.INFO, loggo.INFO), jc.IsTrue) + c.Check(checkLevel(loggo.WARNING, loggo.INFO), jc.IsTrue) + c.Check(checkLevel(loggo.ERROR, loggo.INFO), jc.IsTrue) + c.Check(checkLevel(loggo.CRITICAL, loggo.INFO), jc.IsTrue) +} + +func checkIncludeEntity(logValue string, agent ...string) bool { + stream := newLogFileStream(&debugLogParams{ + includeEntity: agent, + }) + line := &logFileLine{agentTag: logValue} + return stream.checkIncludeEntity(line) +} + +func (s *debugLogFileIntSuite) TestCheckIncludeEntity(c *gc.C) { + c.Check(checkIncludeEntity("machine-0"), jc.IsTrue) + c.Check(checkIncludeEntity("machine-0", "machine-0"), jc.IsTrue) + c.Check(checkIncludeEntity("machine-1", "machine-0"), jc.IsFalse) + c.Check(checkIncludeEntity("machine-1", "machine-0", "machine-1"), jc.IsTrue) + c.Check(checkIncludeEntity("machine-0-lxc-0", "machine-0"), jc.IsFalse) + c.Check(checkIncludeEntity("machine-0-lxc-0", "machine-0*"), jc.IsTrue) + c.Check(checkIncludeEntity("machine-0-lxc-0", "machine-0-lxc-*"), jc.IsTrue) +} + +func checkIncludeModule(logValue string, module ...string) bool { + stream := newLogFileStream(&debugLogParams{ + includeModule: module, + }) + line := &logFileLine{module: logValue} + return stream.checkIncludeModule(line) +} + +func (s *debugLogFileIntSuite) TestCheckIncludeModule(c *gc.C) { + c.Check(checkIncludeModule("juju"), jc.IsTrue) + c.Check(checkIncludeModule("juju", "juju"), jc.IsTrue) + c.Check(checkIncludeModule("juju", "juju.environ"), jc.IsFalse) + c.Check(checkIncludeModule("juju.provisioner", "juju"), jc.IsTrue) + c.Check(checkIncludeModule("juju.provisioner", "juju*"), jc.IsFalse) + c.Check(checkIncludeModule("juju.provisioner", "juju.environ"), jc.IsFalse) + c.Check(checkIncludeModule("unit.mysql/1", "juju", "unit"), jc.IsTrue) +} + +func checkExcludeEntity(logValue string, agent ...string) bool { + stream := newLogFileStream(&debugLogParams{ + excludeEntity: agent, + }) + line := &logFileLine{agentTag: logValue} + return stream.exclude(line) +} + +func (s *debugLogFileIntSuite) TestCheckExcludeEntity(c *gc.C) { + c.Check(checkExcludeEntity("machine-0"), jc.IsFalse) + c.Check(checkExcludeEntity("machine-0", "machine-0"), jc.IsTrue) + c.Check(checkExcludeEntity("machine-1", "machine-0"), jc.IsFalse) + c.Check(checkExcludeEntity("machine-1", "machine-0", "machine-1"), jc.IsTrue) + c.Check(checkExcludeEntity("machine-0-lxc-0", "machine-0"), jc.IsFalse) + c.Check(checkExcludeEntity("machine-0-lxc-0", "machine-0*"), jc.IsTrue) + c.Check(checkExcludeEntity("machine-0-lxc-0", "machine-0-lxc-*"), jc.IsTrue) +} + +func checkExcludeModule(logValue string, module ...string) bool { + stream := newLogFileStream(&debugLogParams{ + excludeModule: module, + }) + line := &logFileLine{module: logValue} + return stream.exclude(line) +} + +func (s *debugLogFileIntSuite) TestCheckExcludeModule(c *gc.C) { + c.Check(checkExcludeModule("juju"), jc.IsFalse) + c.Check(checkExcludeModule("juju", "juju"), jc.IsTrue) + c.Check(checkExcludeModule("juju", "juju.environ"), jc.IsFalse) + c.Check(checkExcludeModule("juju.provisioner", "juju"), jc.IsTrue) + c.Check(checkExcludeModule("juju.provisioner", "juju*"), jc.IsFalse) + c.Check(checkExcludeModule("juju.provisioner", "juju.environ"), jc.IsFalse) + c.Check(checkExcludeModule("unit.mysql/1", "juju", "unit"), jc.IsTrue) +} + +func (s *debugLogFileIntSuite) TestFilterLine(c *gc.C) { + stream := newLogFileStream(&debugLogParams{ + filterLevel: loggo.INFO, + includeEntity: []string{"machine-0", "unit-mysql*"}, + includeModule: []string{"juju"}, + excludeEntity: []string{"unit-mysql-2"}, + excludeModule: []string{"juju.foo"}, + }) + c.Check(stream.filterLine([]byte( + "machine-0: date time WARNING juju")), jc.IsTrue) + c.Check(stream.filterLine([]byte( + "machine-1: date time WARNING juju")), jc.IsFalse) + c.Check(stream.filterLine([]byte( + "unit-mysql-0: date time WARNING juju")), jc.IsTrue) + c.Check(stream.filterLine([]byte( + "unit-mysql-1: date time WARNING juju")), jc.IsTrue) + c.Check(stream.filterLine([]byte( + "unit-mysql-2: date time WARNING juju")), jc.IsFalse) + c.Check(stream.filterLine([]byte( + "unit-wordpress-0: date time WARNING juju")), jc.IsFalse) + c.Check(stream.filterLine([]byte( + "machine-0: date time DEBUG juju")), jc.IsFalse) + c.Check(stream.filterLine([]byte( + "machine-0: date time WARNING juju.foo.bar")), jc.IsFalse) +} + +func (s *debugLogFileIntSuite) TestCountedFilterLineWithLimit(c *gc.C) { + stream := newLogFileStream(&debugLogParams{ + filterLevel: loggo.INFO, + maxLines: 5, + }) + line := []byte("machine-0: date time WARNING juju") + c.Check(stream.countedFilterLine(line), jc.IsTrue) + c.Check(stream.countedFilterLine(line), jc.IsTrue) + c.Check(stream.countedFilterLine(line), jc.IsTrue) + c.Check(stream.countedFilterLine(line), jc.IsTrue) + c.Check(stream.countedFilterLine(line), jc.IsTrue) + c.Check(stream.countedFilterLine(line), jc.IsFalse) + c.Check(stream.countedFilterLine(line), jc.IsFalse) +} + +type chanWriter struct { + ch chan []byte +} + +func (w *chanWriter) Write(buf []byte) (n int, err error) { + bufcopy := append([]byte{}, buf...) + w.ch <- bufcopy + return len(buf), nil +} + +func (s *debugLogFileIntSuite) testStreamInternal(c *gc.C, fromTheStart bool, backlog, maxLines uint, expected, errMatch string) { + + dir := c.MkDir() + logPath := filepath.Join(dir, "logfile.txt") + logFile, err := os.Create(logPath) + c.Assert(err, jc.ErrorIsNil) + defer logFile.Close() + logFileReader, err := os.Open(logPath) + c.Assert(err, jc.ErrorIsNil) + defer logFileReader.Close() + + logFile.WriteString(`line 1 +line 2 +line 3 +`) + + stream := newLogFileStream(&debugLogParams{ + fromTheStart: fromTheStart, + backlog: backlog, + maxLines: maxLines, + }) + err = stream.positionLogFile(logFileReader) + c.Assert(err, jc.ErrorIsNil) + var output bytes.Buffer + writer := &chanWriter{make(chan []byte)} + stream.start(logFileReader, writer) + defer stream.logTailer.Stop() + + logFile.WriteString("line 4\n") + logFile.WriteString("line 5\n") + + timeout := time.After(testing.LongWait) + for output.String() != expected { + select { + case buf := <-writer.ch: + output.Write(buf) + case <-timeout: + c.Fatalf("expected data didn't arrive:\n\tobtained: %#v\n\texpected: %#v", output.String(), expected) + } + } + + stream.logTailer.Stop() + + err = stream.wait(nil) + if errMatch == "" { + c.Assert(err, jc.ErrorIsNil) + } else { + c.Assert(err, gc.ErrorMatches, errMatch) + } +} + +func (s *debugLogFileIntSuite) TestLogStreamLoopFromTheStart(c *gc.C) { + expected := `line 1 +line 2 +line 3 +line 4 +line 5 +` + s.testStreamInternal(c, true, 0, 0, expected, "") +} + +func (s *debugLogFileIntSuite) TestLogStreamLoopFromTheStartMaxLines(c *gc.C) { + expected := `line 1 +line 2 +line 3 +` + s.testStreamInternal(c, true, 0, 3, expected, "") +} + +func (s *debugLogFileIntSuite) TestLogStreamLoopJustTail(c *gc.C) { + expected := `line 4 +line 5 +` + s.testStreamInternal(c, false, 0, 0, expected, "") +} + +func (s *debugLogFileIntSuite) TestLogStreamLoopBackOneLimitTwo(c *gc.C) { + expected := `line 3 +line 4 +` + s.testStreamInternal(c, false, 1, 2, expected, "") +} + +func (s *debugLogFileIntSuite) TestLogStreamLoopTailMaxLinesNotYetReached(c *gc.C) { + expected := `line 4 +line 5 +` + s.testStreamInternal(c, false, 0, 3, expected, "") +} + +func assertStreamParams(c *gc.C, obtained, expected *logFileStream) { + c.Check(obtained.includeEntity, jc.DeepEquals, expected.includeEntity) + c.Check(obtained.includeModule, jc.DeepEquals, expected.includeModule) + c.Check(obtained.excludeEntity, jc.DeepEquals, expected.excludeEntity) + c.Check(obtained.excludeModule, jc.DeepEquals, expected.excludeModule) + c.Check(obtained.maxLines, gc.Equals, expected.maxLines) + c.Check(obtained.fromTheStart, gc.Equals, expected.fromTheStart) + c.Check(obtained.filterLevel, gc.Equals, expected.filterLevel) + c.Check(obtained.backlog, gc.Equals, expected.backlog) +} + +func (s *debugLogFileIntSuite) TestNewLogStream(c *gc.C) { + params, err := readDebugLogParams(url.Values{ + "includeEntity": []string{"machine-1*", "machine-2"}, + "includeModule": []string{"juju", "unit"}, + "excludeEntity": []string{"machine-1-lxc*"}, + "excludeModule": []string{"juju.provisioner"}, + "maxLines": []string{"300"}, + "backlog": []string{"100"}, + "level": []string{"INFO"}, + // OK, just a little nonsense + "replay": []string{"true"}, + }) + c.Assert(err, jc.ErrorIsNil) + + assertStreamParams(c, newLogFileStream(params), &logFileStream{ + debugLogParams: &debugLogParams{ + includeEntity: []string{"machine-1*", "machine-2"}, + includeModule: []string{"juju", "unit"}, + excludeEntity: []string{"machine-1-lxc*"}, + excludeModule: []string{"juju.provisioner"}, + maxLines: 300, + backlog: 100, + filterLevel: loggo.INFO, + fromTheStart: true, + }, + }) +} + +func (s *debugLogFileIntSuite) TestParamErrors(c *gc.C) { + + _, err := readDebugLogParams(url.Values{"maxLines": []string{"foo"}}) + c.Assert(err, gc.ErrorMatches, `maxLines value "foo" is not a valid unsigned number`) + + _, err = readDebugLogParams(url.Values{"backlog": []string{"foo"}}) + c.Assert(err, gc.ErrorMatches, `backlog value "foo" is not a valid unsigned number`) + + _, err = readDebugLogParams(url.Values{"replay": []string{"foo"}}) + c.Assert(err, gc.ErrorMatches, `replay value "foo" is not a valid boolean`) + + _, err = readDebugLogParams(url.Values{"level": []string{"foo"}}) + c.Assert(err, gc.ErrorMatches, `level value "foo" is not one of "TRACE", "DEBUG", "INFO", "WARNING", "ERROR"`) +} + +type agentMatchTest struct { + about string + line string + filter string + expected bool +} + +var agentMatchTests []agentMatchTest = []agentMatchTest{ + { + about: "Matching with wildcard - match everything", + line: "machine-1: sdscsc", + filter: "*", + expected: true, + }, { + about: "Matching with wildcard as suffix - match machine tag...", + line: "machine-1: sdscsc", + filter: "mach*", + expected: true, + }, { + about: "Matching with wildcard as prefix - match machine tag...", + line: "machine-1: sdscsc", + filter: "*ch*", + expected: true, + }, { + about: "Matching with wildcard in the middle - match machine tag...", + line: "machine-1: sdscsc", + filter: "mach*1", + expected: true, + }, { + about: "Matching with wildcard - match machine name", + line: "machine-1: sdscsc", + filter: "1*", + expected: true, + }, { + about: "Matching exact machine name", + line: "machine-1: sdscsc", + filter: "2", + expected: false, + }, { + about: "Matching invalid filter", + line: "machine-1: sdscsc", + filter: "my-service", + expected: false, + }, { + about: "Matching exact machine tag", + line: "machine-1: sdscsc", + filter: "machine-1", + expected: true, + }, { + about: "Matching exact machine tag = not equal", + line: "machine-1: sdscsc", + filter: "machine-3", + expected: false, + }, { + about: "Matching with wildcard - match unit tag...", + line: "unit-ubuntu-1: sdscsc", + filter: "un*", + expected: true, + }, { + about: "Matching with wildcard - match unit name", + line: "unit-ubuntu-1: sdscsc", + filter: "ubuntu*", + expected: true, + }, { + about: "Matching exact unit name", + line: "unit-ubuntu-1: sdscsc", + filter: "ubuntu/2", + expected: false, + }, { + about: "Matching exact unit tag", + line: "unit-ubuntu-1: sdscsc", + filter: "unit-ubuntu-1", + expected: true, + }, { + about: "Matching exact unit tag = not equal", + line: "unit-ubuntu-2: sdscsc", + filter: "unit-ubuntu-1", + expected: false, + }, +} + +// TestAgentMatchesFilter tests that line agent matches desired filter as expected +func (s *debugLogFileIntSuite) TestAgentMatchesFilter(c *gc.C) { + for i, test := range agentMatchTests { + c.Logf("test %d: %v\n", i, test.about) + matched := AgentMatchesFilter(ParseLogLine(test.line), test.filter) + c.Assert(matched, gc.Equals, test.expected) + } +} + +// TestAgentLineFragmentParsing tests that agent tag and name are parsed correctly from log line +func (s *debugLogFileIntSuite) TestAgentLineFragmentParsing(c *gc.C) { + checkAgentParsing(c, "Drop trailing colon", "machine-1: sdscsc", "machine-1", "1") + checkAgentParsing(c, "Drop unit specific [", "unit-ubuntu-1[blah777787]: scscdcdc", "unit-ubuntu-1", "ubuntu/1") + checkAgentParsing(c, "No colon in log line - invalid", "unit-ubuntu-1 scscdcdc", "", "") +} + +func checkAgentParsing(c *gc.C, about, line, tag, name string) { + c.Logf("test %q\n", about) + logLine := ParseLogLine(line) + c.Assert(logLine.LogLineAgentTag(), gc.Equals, tag) + c.Assert(logLine.LogLineAgentName(), gc.Equals, name) +} === added file 'src/github.com/juju/juju/apiserver/debuglog_file_test.go' --- src/github.com/juju/juju/apiserver/debuglog_file_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/debuglog_file_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,377 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package apiserver_test + +import ( + "bufio" + "fmt" + "net/url" + "os" + "path/filepath" + "strings" + + jc "github.com/juju/testing/checkers" + "github.com/juju/utils" + gc "gopkg.in/check.v1" +) + +type debugLogFileSuite struct { + debugLogBaseSuite + logFile *os.File + last int +} + +var _ = gc.Suite(&debugLogFileSuite{}) + +func (s *debugLogFileSuite) TestNoLogfile(c *gc.C) { + reader := s.openWebsocket(c, nil) + assertJSONError(c, reader, "cannot open log file: .*: "+utils.NoSuchFileErrRegexp) + s.assertWebsocketClosed(c, reader) +} + +func (s *debugLogFileSuite) assertLogReader(c *gc.C, reader *bufio.Reader) { + s.assertLogFollowing(c, reader) + s.writeLogLines(c, logLineCount) + + linesRead := s.readLogLines(c, reader, logLineCount) + c.Assert(linesRead, jc.DeepEquals, logLines) +} + +func (s *debugLogFileSuite) TestServesLog(c *gc.C) { + s.ensureLogFile(c) + reader := s.openWebsocket(c, nil) + s.assertLogReader(c, reader) +} + +func (s *debugLogFileSuite) TestReadFromTopLevelPath(c *gc.C) { + // Backwards compatibility check, that we can read the log file at + // https://host:port/log + s.ensureLogFile(c) + reader := s.openWebsocketCustomPath(c, "/log") + s.assertLogReader(c, reader) +} + +func (s *debugLogFileSuite) TestReadFromEnvUUIDPath(c *gc.C) { + // Check that we can read the log at https://host:port/ENVUUID/log + environ, err := s.State.Environment() + c.Assert(err, jc.ErrorIsNil) + s.ensureLogFile(c) + reader := s.openWebsocketCustomPath(c, fmt.Sprintf("/environment/%s/log", environ.UUID())) + s.assertLogReader(c, reader) +} + +func (s *debugLogFileSuite) TestReadRejectsWrongEnvUUIDPath(c *gc.C) { + // Check that we cannot pull logs from https://host:port/BADENVUUID/log + s.ensureLogFile(c) + reader := s.openWebsocketCustomPath(c, "/environment/dead-beef-123456/log") + assertJSONError(c, reader, `unknown environment: "dead-beef-123456"`) + s.assertWebsocketClosed(c, reader) +} + +func (s *debugLogFileSuite) TestReadsFromEnd(c *gc.C) { + s.writeLogLines(c, 10) + + reader := s.openWebsocket(c, nil) + s.assertLogFollowing(c, reader) + s.writeLogLines(c, logLineCount) + + linesRead := s.readLogLines(c, reader, logLineCount-10) + c.Assert(linesRead, jc.DeepEquals, logLines[10:]) +} + +func (s *debugLogFileSuite) TestReplayFromStart(c *gc.C) { + s.writeLogLines(c, 10) + + reader := s.openWebsocket(c, url.Values{"replay": {"true"}}) + s.assertLogFollowing(c, reader) + s.writeLogLines(c, logLineCount) + + linesRead := s.readLogLines(c, reader, logLineCount) + c.Assert(linesRead, jc.DeepEquals, logLines) +} + +func (s *debugLogFileSuite) TestBacklog(c *gc.C) { + s.writeLogLines(c, 10) + + reader := s.openWebsocket(c, url.Values{"backlog": {"5"}}) + s.assertLogFollowing(c, reader) + s.writeLogLines(c, logLineCount) + + linesRead := s.readLogLines(c, reader, logLineCount-5) + c.Assert(linesRead, jc.DeepEquals, logLines[5:]) +} + +func (s *debugLogFileSuite) TestMaxLines(c *gc.C) { + s.writeLogLines(c, 10) + + reader := s.openWebsocket(c, url.Values{"maxLines": {"10"}}) + s.assertLogFollowing(c, reader) + s.writeLogLines(c, logLineCount) + + linesRead := s.readLogLines(c, reader, 10) + c.Assert(linesRead, jc.DeepEquals, logLines[10:20]) + s.assertWebsocketClosed(c, reader) +} + +func (s *debugLogFileSuite) TestBacklogWithMaxLines(c *gc.C) { + s.writeLogLines(c, 10) + + reader := s.openWebsocket(c, url.Values{"backlog": {"5"}, "maxLines": {"10"}}) + s.assertLogFollowing(c, reader) + s.writeLogLines(c, logLineCount) + + linesRead := s.readLogLines(c, reader, 10) + c.Assert(linesRead, jc.DeepEquals, logLines[5:15]) + s.assertWebsocketClosed(c, reader) +} + +type filterTest struct { + about string + filter url.Values + filtered []string +} + +var filterTests []filterTest = []filterTest{ + { + about: "Filter from original test", + filter: url.Values{ + "includeEntity": {"machine-0", "unit-ubuntu-0"}, + "includeModule": {"juju.cmd"}, + "excludeModule": {"juju.cmd.jujud"}, + }, + filtered: []string{logLines[0], logLines[40]}, + }, { + about: "Filter from original test inverted", + filter: url.Values{ + "excludeEntity": {"machine-1"}, + }, + filtered: []string{logLines[0], logLines[1]}, + }, { + about: "Include Entity Filter with only wildcard", + filter: url.Values{ + "includeEntity": {"*"}, + }, + filtered: []string{logLines[0], logLines[1]}, + }, { + about: "Exclude Entity Filter with only wildcard", + filter: url.Values{ + "excludeEntity": {"*"}, // exclude everything :-) + }, + filtered: []string{}, + }, { + about: "Include Entity Filter with 1 wildcard", + filter: url.Values{ + "includeEntity": {"unit-*"}, + }, + filtered: []string{logLines[40], logLines[41]}, + }, { + about: "Exclude Entity Filter with 1 wildcard", + filter: url.Values{ + "excludeEntity": {"machine-*"}, + }, + filtered: []string{logLines[40], logLines[41]}, + }, { + about: "Include Entity Filter using machine tag", + filter: url.Values{ + "includeEntity": {"machine-1"}, + }, + filtered: []string{logLines[27], logLines[28]}, + }, { + about: "Include Entity Filter using machine name", + filter: url.Values{ + "includeEntity": {"1"}, + }, + filtered: []string{logLines[27], logLines[28]}, + }, { + about: "Include Entity Filter using unit tag", + filter: url.Values{ + "includeEntity": {"unit-ubuntu-0"}, + }, + filtered: []string{logLines[40], logLines[41]}, + }, { + about: "Include Entity Filter using unit name", + filter: url.Values{ + "includeEntity": {"ubuntu/0"}, + }, + filtered: []string{logLines[40], logLines[41]}, + }, { + about: "Include Entity Filter using combination of machine tag and unit name", + filter: url.Values{ + "includeEntity": {"machine-1", "ubuntu/0"}, + "includeModule": {"juju.agent"}, + }, + filtered: []string{logLines[29], logLines[34], logLines[41]}, + }, { + about: "Exclude Entity Filter using machine tag", + filter: url.Values{ + "excludeEntity": {"machine-0"}, + }, + filtered: []string{logLines[27], logLines[28]}, + }, { + about: "Exclude Entity Filter using machine name", + filter: url.Values{ + "excludeEntity": {"0"}, + }, + filtered: []string{logLines[27], logLines[28]}, + }, { + about: "Exclude Entity Filter using unit tag", + filter: url.Values{ + "excludeEntity": {"machine-0", "machine-1", "unit-ubuntu-0"}, + }, + filtered: []string{logLines[54], logLines[55]}, + }, { + about: "Exclude Entity Filter using unit name", + filter: url.Values{ + "excludeEntity": {"machine-0", "machine-1", "ubuntu/0"}, + }, + filtered: []string{logLines[54], logLines[55]}, + }, { + about: "Exclude Entity Filter using combination of machine tag and unit name", + filter: url.Values{ + "excludeEntity": {"0", "1", "ubuntu/0"}, + }, + filtered: []string{logLines[54], logLines[55]}, + }, +} + +// TestFilter tests that filters are processed correctly given specific debug-log configuration. +func (s *debugLogFileSuite) TestFilter(c *gc.C) { + for i, test := range filterTests { + c.Logf("test %d: %v\n", i, test.about) + + // ensures log file + path := filepath.Join(s.LogDir, "all-machines.log") + var err error + s.logFile, err = os.Create(path) + c.Assert(err, jc.ErrorIsNil) + + // opens web socket + conn := s.dialWebsocket(c, test.filter) + reader := bufio.NewReader(conn) + + s.assertLogFollowing(c, reader) + s.writeLogLines(c, logLineCount) + /* + This will filter and return as many lines as filtered wanted to examine. + So, if specified filter can potentially return 40 lines from sample log but filtered only wanted 2, + then the first 2 lines that match the filter will be returned here. + */ + linesRead := s.readLogLines(c, reader, len(test.filtered)) + // compare retrieved lines with expected + c.Assert(linesRead, jc.DeepEquals, test.filtered) + + // release resources + conn.Close() + s.logFile.Close() + s.logFile = nil + s.last = 0 + } +} + +// readLogLines filters and returns as many lines as filtered wanted to examine. +// So, if specified filter can potentially return 40 lines from sample log but filtered only wanted 2, +// then the first 2 lines that match the filter will be returned here. +func (s *debugLogFileSuite) readLogLines(c *gc.C, reader *bufio.Reader, count int) (linesRead []string) { + for len(linesRead) < count { + line, err := reader.ReadString('\n') + c.Assert(err, jc.ErrorIsNil) + // Trim off the trailing \n + linesRead = append(linesRead, line[:len(line)-1]) + } + return linesRead +} + +func (s *debugLogFileSuite) ensureLogFile(c *gc.C) { + if s.logFile != nil { + return + } + path := filepath.Join(s.LogDir, "all-machines.log") + var err error + s.logFile, err = os.Create(path) + c.Assert(err, jc.ErrorIsNil) + s.AddCleanup(func(c *gc.C) { + s.logFile.Close() + s.logFile = nil + s.last = 0 + }) +} + +func (s *debugLogFileSuite) writeLogLines(c *gc.C, count int) { + s.ensureLogFile(c) + for i := 0; i < count && s.last < logLineCount; i++ { + s.logFile.WriteString(logLines[s.last] + "\n") + s.last++ + } +} + +func (s *debugLogFileSuite) assertLogFollowing(c *gc.C, reader *bufio.Reader) { + errResult := readJSONErrorLine(c, reader) + c.Assert(errResult.Error, gc.IsNil) +} + +var ( + logLines = strings.Split(` +machine-0: 2014-03-24 22:34:25 INFO juju.cmd supercommand.go:297 running juju-1.17.7.1-trusty-amd64 [gc] +machine-0: 2014-03-24 22:34:25 INFO juju.cmd.jujud machine.go:127 machine agent machine-0 start (1.17.7.1-trusty-amd64 [gc]) +machine-0: 2014-03-24 22:34:25 DEBUG juju.agent agent.go:384 read agent config, format "1.18" +machine-0: 2014-03-24 22:34:25 INFO juju.cmd.jujud machine.go:155 Starting StateWorker for machine-0 +machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "state" +machine-0: 2014-03-24 22:34:25 INFO juju.state open.go:80 opening state; mongo addresses: ["localhost:37017"]; entity "machine-0" +machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "api" +machine-0: 2014-03-24 22:34:25 INFO juju apiclient.go:114 api: dialing "wss://localhost:17070/" +machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "termination" +machine-0: 2014-03-24 22:34:25 ERROR juju apiclient.go:119 api: websocket.Dial wss://localhost:17070/: dial tcp 127.0.0.1:17070: connection refused +machine-0: 2014-03-24 22:34:25 ERROR juju runner.go:220 worker: exited "api": websocket.Dial wss://localhost:17070/: dial tcp 127.0.0.1:17070: connection refused +machine-0: 2014-03-24 22:34:25 INFO juju runner.go:254 worker: restarting "api" in 3s +machine-0: 2014-03-24 22:34:25 INFO juju.state open.go:118 connection established +machine-0: 2014-03-24 22:34:25 DEBUG juju.utils gomaxprocs.go:24 setting GOMAXPROCS to 8 +machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "local-storage" +machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "instancepoller" +machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "apiserver" +machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "resumer" +machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "cleaner" +machine-0: 2014-03-24 22:34:25 INFO juju.apiserver apiserver.go:43 listening on "[::]:17070" +machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "minunitsworker" +machine-0: 2014-03-24 22:34:28 INFO juju runner.go:262 worker: start "api" +machine-0: 2014-03-24 22:34:28 INFO juju apiclient.go:114 api: dialing "wss://localhost:17070/" +machine-0: 2014-03-24 22:34:28 INFO juju.apiserver apiserver.go:131 [1] API connection from 127.0.0.1:36491 +machine-0: 2014-03-24 22:34:28 INFO juju apiclient.go:124 api: connection established +machine-0: 2014-03-24 22:34:28 DEBUG juju.apiserver apiserver.go:120 <- [1] {"RequestId":1,"Type":"Admin","Request":"Login","Params":{"AuthTag":"machine-0","Password":"ARbW7iCV4LuMugFEG+Y4e0yr","Nonce":"user-admin:bootstrap"}} +machine-0: 2014-03-24 22:34:28 DEBUG juju.apiserver apiserver.go:127 -> [1] machine-0 10.305679ms {"RequestId":1,"Response":{}} Admin[""].Login +machine-1: 2014-03-24 22:36:28 INFO juju.cmd supercommand.go:297 running juju-1.17.7.1-precise-amd64 [gc] +machine-1: 2014-03-24 22:36:28 INFO juju.cmd.jujud machine.go:127 machine agent machine-1 start (1.17.7.1-precise-amd64 [gc]) +machine-1: 2014-03-24 22:36:28 DEBUG juju.agent agent.go:384 read agent config, format "1.18" +machine-1: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "api" +machine-1: 2014-03-24 22:36:28 INFO juju apiclient.go:114 api: dialing "wss://10.0.3.1:17070/" +machine-1: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "termination" +machine-1: 2014-03-24 22:36:28 INFO juju apiclient.go:124 api: connection established +machine-1: 2014-03-24 22:36:28 DEBUG juju.agent agent.go:523 writing configuration file +machine-1: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "upgrader" +machine-1: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "upgrade-steps" +machine-1: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "machiner" +machine-1: 2014-03-24 22:36:28 INFO juju.cmd.jujud machine.go:458 upgrade to 1.17.7.1-precise-amd64 already completed. +machine-1: 2014-03-24 22:36:28 INFO juju.cmd.jujud machine.go:445 upgrade to 1.17.7.1-precise-amd64 completed. +unit-ubuntu-0[32423]: 2014-03-24 22:36:28 INFO juju.cmd supercommand.go:297 running juju-1.17.7.1-precise-amd64 [gc] +unit-ubuntu-0[34543]: 2014-03-24 22:36:28 DEBUG juju.agent agent.go:384 read agent config, format "1.18" +unit-ubuntu-0: 2014-03-24 22:36:28 INFO juju.jujud unit.go:76 unit agent unit-ubuntu-0 start (1.17.7.1-precise-amd64 [gc]) +unit-ubuntu-0: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "api" +unit-ubuntu-0: 2014-03-24 22:36:28 INFO juju apiclient.go:114 api: dialing "wss://10.0.3.1:17070/" +unit-ubuntu-0: 2014-03-24 22:36:28 INFO juju apiclient.go:124 api: connection established +unit-ubuntu-0: 2014-03-24 22:36:28 DEBUG juju.agent agent.go:523 writing configuration file +unit-ubuntu-0: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "upgrader" +unit-ubuntu-0: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "logger" +unit-ubuntu-0: 2014-03-24 22:36:28 DEBUG juju.worker.logger logger.go:35 initial log config: "=DEBUG" +unit-ubuntu-0: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "uniter" +unit-ubuntu-0: 2014-03-24 22:36:28 DEBUG juju.worker.logger logger.go:60 logger setup +unit-ubuntu-0: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "rsyslog" +unit-ubuntu-0: 2014-03-24 22:36:28 DEBUG juju.worker.rsyslog worker.go:76 starting rsyslog worker mode 1 for "unit-ubuntu-0" "tim-local" +unit-ubuntu-1: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "logger" +unit-ubuntu-1: 2014-03-24 22:36:28 DEBUG juju.worker.logger logger.go:35 initial log config: "=DEBUG" +unit-ubuntu-1: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "uniter" +unit-ubuntu-1: 2014-03-24 22:36:28 DEBUG juju.worker.logger logger.go:60 logger setup +unit-ubuntu-1: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "rsyslog" +unit-ubuntu-1: 2014-03-24 22:36:28 DEBUG juju.worker.rsyslog worker.go:76 starting rsyslog worker mode 1 for "unit-ubuntu-0" "tim-local" +`[1:], "\n") + logLineCount = len(logLines) +) === removed file 'src/github.com/juju/juju/apiserver/debuglog_internal_test.go' --- src/github.com/juju/juju/apiserver/debuglog_internal_test.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/apiserver/debuglog_internal_test.go 1970-01-01 00:00:00 +0000 @@ -1,455 +0,0 @@ -// Copyright 2014 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -// This is an internal package test. - -package apiserver - -import ( - "bytes" - "net/url" - "os" - "path/filepath" - "time" - - "github.com/juju/loggo" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/testing" -) - -type debugInternalSuite struct { - testing.BaseSuite -} - -var _ = gc.Suite(&debugInternalSuite{}) - -func (s *debugInternalSuite) TestParseLogLine(c *gc.C) { - line := "machine-0: 2014-03-24 22:34:25 INFO juju.cmd.jujud machine.go:127 machine agent machine-0 start (1.17.7.1-trusty-amd64 [gc])" - logLine := parseLogLine(line) - c.Assert(logLine.line, gc.Equals, line) - c.Assert(logLine.agentTag, gc.Equals, "machine-0") - c.Assert(logLine.level, gc.Equals, loggo.INFO) - c.Assert(logLine.module, gc.Equals, "juju.cmd.jujud") -} - -func (s *debugInternalSuite) TestParseLogLineMachineMultiline(c *gc.C) { - line := "machine-1: continuation line" - logLine := parseLogLine(line) - c.Assert(logLine.line, gc.Equals, line) - c.Assert(logLine.agentTag, gc.Equals, "machine-1") - c.Assert(logLine.level, gc.Equals, loggo.UNSPECIFIED) - c.Assert(logLine.module, gc.Equals, "") -} - -func (s *debugInternalSuite) TestParseLogLineInvalid(c *gc.C) { - line := "not a full line" - logLine := parseLogLine(line) - c.Assert(logLine.line, gc.Equals, line) - c.Assert(logLine.agentTag, gc.Equals, "") - c.Assert(logLine.level, gc.Equals, loggo.UNSPECIFIED) - c.Assert(logLine.module, gc.Equals, "") -} - -func checkLevel(logValue, streamValue loggo.Level) bool { - stream := &logStream{} - if streamValue != loggo.UNSPECIFIED { - stream.filterLevel = streamValue - } - line := &logLine{level: logValue} - return stream.checkLevel(line) -} - -func (s *debugInternalSuite) TestCheckLevel(c *gc.C) { - c.Check(checkLevel(loggo.UNSPECIFIED, loggo.UNSPECIFIED), jc.IsTrue) - c.Check(checkLevel(loggo.TRACE, loggo.UNSPECIFIED), jc.IsTrue) - c.Check(checkLevel(loggo.DEBUG, loggo.UNSPECIFIED), jc.IsTrue) - c.Check(checkLevel(loggo.INFO, loggo.UNSPECIFIED), jc.IsTrue) - c.Check(checkLevel(loggo.WARNING, loggo.UNSPECIFIED), jc.IsTrue) - c.Check(checkLevel(loggo.ERROR, loggo.UNSPECIFIED), jc.IsTrue) - c.Check(checkLevel(loggo.CRITICAL, loggo.UNSPECIFIED), jc.IsTrue) - - c.Check(checkLevel(loggo.UNSPECIFIED, loggo.TRACE), jc.IsFalse) - c.Check(checkLevel(loggo.TRACE, loggo.TRACE), jc.IsTrue) - c.Check(checkLevel(loggo.DEBUG, loggo.TRACE), jc.IsTrue) - c.Check(checkLevel(loggo.INFO, loggo.TRACE), jc.IsTrue) - c.Check(checkLevel(loggo.WARNING, loggo.TRACE), jc.IsTrue) - c.Check(checkLevel(loggo.ERROR, loggo.TRACE), jc.IsTrue) - c.Check(checkLevel(loggo.CRITICAL, loggo.TRACE), jc.IsTrue) - - c.Check(checkLevel(loggo.UNSPECIFIED, loggo.INFO), jc.IsFalse) - c.Check(checkLevel(loggo.TRACE, loggo.INFO), jc.IsFalse) - c.Check(checkLevel(loggo.DEBUG, loggo.INFO), jc.IsFalse) - c.Check(checkLevel(loggo.INFO, loggo.INFO), jc.IsTrue) - c.Check(checkLevel(loggo.WARNING, loggo.INFO), jc.IsTrue) - c.Check(checkLevel(loggo.ERROR, loggo.INFO), jc.IsTrue) - c.Check(checkLevel(loggo.CRITICAL, loggo.INFO), jc.IsTrue) -} - -func checkIncludeEntity(logValue string, agent ...string) bool { - stream := &logStream{includeEntity: agent} - line := &logLine{agentTag: logValue} - return stream.checkIncludeEntity(line) -} - -func (s *debugInternalSuite) TestCheckIncludeEntity(c *gc.C) { - c.Check(checkIncludeEntity("machine-0"), jc.IsTrue) - c.Check(checkIncludeEntity("machine-0", "machine-0"), jc.IsTrue) - c.Check(checkIncludeEntity("machine-1", "machine-0"), jc.IsFalse) - c.Check(checkIncludeEntity("machine-1", "machine-0", "machine-1"), jc.IsTrue) - c.Check(checkIncludeEntity("machine-0-lxc-0", "machine-0"), jc.IsFalse) - c.Check(checkIncludeEntity("machine-0-lxc-0", "machine-0*"), jc.IsTrue) - c.Check(checkIncludeEntity("machine-0-lxc-0", "machine-0-lxc-*"), jc.IsTrue) -} - -func checkIncludeModule(logValue string, module ...string) bool { - stream := &logStream{includeModule: module} - line := &logLine{module: logValue} - return stream.checkIncludeModule(line) -} - -func (s *debugInternalSuite) TestCheckIncludeModule(c *gc.C) { - c.Check(checkIncludeModule("juju"), jc.IsTrue) - c.Check(checkIncludeModule("juju", "juju"), jc.IsTrue) - c.Check(checkIncludeModule("juju", "juju.environ"), jc.IsFalse) - c.Check(checkIncludeModule("juju.provisioner", "juju"), jc.IsTrue) - c.Check(checkIncludeModule("juju.provisioner", "juju*"), jc.IsFalse) - c.Check(checkIncludeModule("juju.provisioner", "juju.environ"), jc.IsFalse) - c.Check(checkIncludeModule("unit.mysql/1", "juju", "unit"), jc.IsTrue) -} - -func checkExcludeEntity(logValue string, agent ...string) bool { - stream := &logStream{excludeEntity: agent} - line := &logLine{agentTag: logValue} - return stream.exclude(line) -} - -func (s *debugInternalSuite) TestCheckExcludeEntity(c *gc.C) { - c.Check(checkExcludeEntity("machine-0"), jc.IsFalse) - c.Check(checkExcludeEntity("machine-0", "machine-0"), jc.IsTrue) - c.Check(checkExcludeEntity("machine-1", "machine-0"), jc.IsFalse) - c.Check(checkExcludeEntity("machine-1", "machine-0", "machine-1"), jc.IsTrue) - c.Check(checkExcludeEntity("machine-0-lxc-0", "machine-0"), jc.IsFalse) - c.Check(checkExcludeEntity("machine-0-lxc-0", "machine-0*"), jc.IsTrue) - c.Check(checkExcludeEntity("machine-0-lxc-0", "machine-0-lxc-*"), jc.IsTrue) -} - -func checkExcludeModule(logValue string, module ...string) bool { - stream := &logStream{excludeModule: module} - line := &logLine{module: logValue} - return stream.exclude(line) -} - -func (s *debugInternalSuite) TestCheckExcludeModule(c *gc.C) { - c.Check(checkExcludeModule("juju"), jc.IsFalse) - c.Check(checkExcludeModule("juju", "juju"), jc.IsTrue) - c.Check(checkExcludeModule("juju", "juju.environ"), jc.IsFalse) - c.Check(checkExcludeModule("juju.provisioner", "juju"), jc.IsTrue) - c.Check(checkExcludeModule("juju.provisioner", "juju*"), jc.IsFalse) - c.Check(checkExcludeModule("juju.provisioner", "juju.environ"), jc.IsFalse) - c.Check(checkExcludeModule("unit.mysql/1", "juju", "unit"), jc.IsTrue) -} - -func (s *debugInternalSuite) TestFilterLine(c *gc.C) { - stream := &logStream{ - filterLevel: loggo.INFO, - includeEntity: []string{"machine-0", "unit-mysql*"}, - includeModule: []string{"juju"}, - excludeEntity: []string{"unit-mysql-2"}, - excludeModule: []string{"juju.foo"}, - } - c.Check(stream.filterLine([]byte( - "machine-0: date time WARNING juju")), jc.IsTrue) - c.Check(stream.filterLine([]byte( - "machine-1: date time WARNING juju")), jc.IsFalse) - c.Check(stream.filterLine([]byte( - "unit-mysql-0: date time WARNING juju")), jc.IsTrue) - c.Check(stream.filterLine([]byte( - "unit-mysql-1: date time WARNING juju")), jc.IsTrue) - c.Check(stream.filterLine([]byte( - "unit-mysql-2: date time WARNING juju")), jc.IsFalse) - c.Check(stream.filterLine([]byte( - "unit-wordpress-0: date time WARNING juju")), jc.IsFalse) - c.Check(stream.filterLine([]byte( - "machine-0: date time DEBUG juju")), jc.IsFalse) - c.Check(stream.filterLine([]byte( - "machine-0: date time WARNING juju.foo.bar")), jc.IsFalse) -} - -func (s *debugInternalSuite) TestCountedFilterLineWithLimit(c *gc.C) { - stream := &logStream{ - filterLevel: loggo.INFO, - maxLines: 5, - } - line := []byte("machine-0: date time WARNING juju") - c.Check(stream.countedFilterLine(line), jc.IsTrue) - c.Check(stream.countedFilterLine(line), jc.IsTrue) - c.Check(stream.countedFilterLine(line), jc.IsTrue) - c.Check(stream.countedFilterLine(line), jc.IsTrue) - c.Check(stream.countedFilterLine(line), jc.IsTrue) - c.Check(stream.countedFilterLine(line), jc.IsFalse) - c.Check(stream.countedFilterLine(line), jc.IsFalse) -} - -type chanWriter struct { - ch chan []byte -} - -func (w *chanWriter) Write(buf []byte) (n int, err error) { - bufcopy := append([]byte{}, buf...) - w.ch <- bufcopy - return len(buf), nil -} - -func (s *debugInternalSuite) testStreamInternal(c *gc.C, fromTheStart bool, backlog, maxLines uint, expected, errMatch string) { - - dir := c.MkDir() - logPath := filepath.Join(dir, "logfile.txt") - logFile, err := os.Create(logPath) - c.Assert(err, jc.ErrorIsNil) - defer logFile.Close() - logFileReader, err := os.Open(logPath) - c.Assert(err, jc.ErrorIsNil) - defer logFileReader.Close() - - logFile.WriteString(`line 1 -line 2 -line 3 -`) - stream := &logStream{ - fromTheStart: fromTheStart, - backlog: backlog, - maxLines: maxLines, - } - err = stream.positionLogFile(logFileReader) - c.Assert(err, jc.ErrorIsNil) - var output bytes.Buffer - writer := &chanWriter{make(chan []byte)} - stream.start(logFileReader, writer) - defer stream.logTailer.Wait() - - go func() { - defer stream.tomb.Done() - stream.tomb.Kill(stream.loop()) - }() - - logFile.WriteString("line 4\n") - logFile.WriteString("line 5\n") - - timeout := time.After(testing.LongWait) - for output.String() != expected { - select { - case buf := <-writer.ch: - output.Write(buf) - case <-timeout: - c.Fatalf("expected data didn't arrive:\n\tobtained: %#v\n\texpected: %#v", output.String(), expected) - } - } - - stream.logTailer.Stop() - - err = stream.tomb.Wait() - if errMatch == "" { - c.Assert(err, jc.ErrorIsNil) - } else { - c.Assert(err, gc.ErrorMatches, errMatch) - } -} - -func (s *debugInternalSuite) TestLogStreamLoopFromTheStart(c *gc.C) { - expected := `line 1 -line 2 -line 3 -line 4 -line 5 -` - s.testStreamInternal(c, true, 0, 0, expected, "") -} - -func (s *debugInternalSuite) TestLogStreamLoopFromTheStartMaxLines(c *gc.C) { - expected := `line 1 -line 2 -line 3 -` - s.testStreamInternal(c, true, 0, 3, expected, "max lines reached") -} - -func (s *debugInternalSuite) TestLogStreamLoopJustTail(c *gc.C) { - expected := `line 4 -line 5 -` - s.testStreamInternal(c, false, 0, 0, expected, "") -} - -func (s *debugInternalSuite) TestLogStreamLoopBackOneLimitTwo(c *gc.C) { - expected := `line 3 -line 4 -` - s.testStreamInternal(c, false, 1, 2, expected, "max lines reached") -} - -func (s *debugInternalSuite) TestLogStreamLoopTailMaxLinesNotYetReached(c *gc.C) { - expected := `line 4 -line 5 -` - s.testStreamInternal(c, false, 0, 3, expected, "") -} - -func assertStreamParams(c *gc.C, obtained, expected *logStream) { - c.Check(obtained.includeEntity, jc.DeepEquals, expected.includeEntity) - c.Check(obtained.includeModule, jc.DeepEquals, expected.includeModule) - c.Check(obtained.excludeEntity, jc.DeepEquals, expected.excludeEntity) - c.Check(obtained.excludeModule, jc.DeepEquals, expected.excludeModule) - c.Check(obtained.maxLines, gc.Equals, expected.maxLines) - c.Check(obtained.fromTheStart, gc.Equals, expected.fromTheStart) - c.Check(obtained.filterLevel, gc.Equals, expected.filterLevel) - c.Check(obtained.backlog, gc.Equals, expected.backlog) -} - -func (s *debugInternalSuite) TestNewLogStream(c *gc.C) { - obtained, err := newLogStream(nil) - c.Assert(err, jc.ErrorIsNil) - assertStreamParams(c, obtained, &logStream{}) - - values := url.Values{ - "includeEntity": []string{"machine-1*", "machine-2"}, - "includeModule": []string{"juju", "unit"}, - "excludeEntity": []string{"machine-1-lxc*"}, - "excludeModule": []string{"juju.provisioner"}, - "maxLines": []string{"300"}, - "backlog": []string{"100"}, - "level": []string{"INFO"}, - // OK, just a little nonsense - "replay": []string{"true"}, - } - expected := &logStream{ - includeEntity: []string{"machine-1*", "machine-2"}, - includeModule: []string{"juju", "unit"}, - excludeEntity: []string{"machine-1-lxc*"}, - excludeModule: []string{"juju.provisioner"}, - maxLines: 300, - backlog: 100, - filterLevel: loggo.INFO, - fromTheStart: true, - } - obtained, err = newLogStream(values) - c.Assert(err, jc.ErrorIsNil) - assertStreamParams(c, obtained, expected) - - _, err = newLogStream(url.Values{"maxLines": []string{"foo"}}) - c.Assert(err, gc.ErrorMatches, `maxLines value "foo" is not a valid unsigned number`) - - _, err = newLogStream(url.Values{"backlog": []string{"foo"}}) - c.Assert(err, gc.ErrorMatches, `backlog value "foo" is not a valid unsigned number`) - - _, err = newLogStream(url.Values{"replay": []string{"foo"}}) - c.Assert(err, gc.ErrorMatches, `replay value "foo" is not a valid boolean`) - - _, err = newLogStream(url.Values{"level": []string{"foo"}}) - c.Assert(err, gc.ErrorMatches, `level value "foo" is not one of "TRACE", "DEBUG", "INFO", "WARNING", "ERROR"`) -} - -type agentMatchTest struct { - about string - line string - filter string - expected bool -} - -var agentMatchTests []agentMatchTest = []agentMatchTest{ - { - about: "Matching with wildcard - match everything", - line: "machine-1: sdscsc", - filter: "*", - expected: true, - }, { - about: "Matching with wildcard as suffix - match machine tag...", - line: "machine-1: sdscsc", - filter: "mach*", - expected: true, - }, { - about: "Matching with wildcard as prefix - match machine tag...", - line: "machine-1: sdscsc", - filter: "*ch*", - expected: true, - }, { - about: "Matching with wildcard in the middle - match machine tag...", - line: "machine-1: sdscsc", - filter: "mach*1", - expected: true, - }, { - about: "Matching with wildcard - match machine name", - line: "machine-1: sdscsc", - filter: "1*", - expected: true, - }, { - about: "Matching exact machine name", - line: "machine-1: sdscsc", - filter: "2", - expected: false, - }, { - about: "Matching invalid filter", - line: "machine-1: sdscsc", - filter: "my-service", - expected: false, - }, { - about: "Matching exact machine tag", - line: "machine-1: sdscsc", - filter: "machine-1", - expected: true, - }, { - about: "Matching exact machine tag = not equal", - line: "machine-1: sdscsc", - filter: "machine-3", - expected: false, - }, { - about: "Matching with wildcard - match unit tag...", - line: "unit-ubuntu-1: sdscsc", - filter: "un*", - expected: true, - }, { - about: "Matching with wildcard - match unit name", - line: "unit-ubuntu-1: sdscsc", - filter: "ubuntu*", - expected: true, - }, { - about: "Matching exact unit name", - line: "unit-ubuntu-1: sdscsc", - filter: "ubuntu/2", - expected: false, - }, { - about: "Matching exact unit tag", - line: "unit-ubuntu-1: sdscsc", - filter: "unit-ubuntu-1", - expected: true, - }, { - about: "Matching exact unit tag = not equal", - line: "unit-ubuntu-2: sdscsc", - filter: "unit-ubuntu-1", - expected: false, - }, -} - -// TestAgentMatchesFilter tests that line agent matches desired filter as expected -func (s *debugInternalSuite) TestAgentMatchesFilter(c *gc.C) { - for i, test := range agentMatchTests { - c.Logf("test %d: %v\n", i, test.about) - matched := AgentMatchesFilter(ParseLogLine(test.line), test.filter) - c.Assert(matched, gc.Equals, test.expected) - } -} - -// TestAgentLineFragmentParsing tests that agent tag and name are parsed correctly from log line -func (s *debugInternalSuite) TestAgentLineFragmentParsing(c *gc.C) { - checkAgentParsing(c, "Drop trailing colon", "machine-1: sdscsc", "machine-1", "1") - checkAgentParsing(c, "Drop unit specific [", "unit-ubuntu-1[blah777787]: scscdcdc", "unit-ubuntu-1", "ubuntu/1") - checkAgentParsing(c, "No colon in log line - invalid", "unit-ubuntu-1 scscdcdc", "", "") -} - -func checkAgentParsing(c *gc.C, about, line, tag, name string) { - c.Logf("test %q\n", about) - logLine := ParseLogLine(line) - c.Assert(logLine.LogLineAgentTag(), gc.Equals, tag) - c.Assert(logLine.LogLineAgentName(), gc.Equals, name) -} === modified file 'src/github.com/juju/juju/apiserver/debuglog_test.go' --- src/github.com/juju/juju/apiserver/debuglog_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/debuglog_test.go 2015-10-23 18:29:32 +0000 @@ -1,46 +1,47 @@ -// Copyright 2014 Canonical Ltd. +// Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package apiserver_test import ( "bufio" - "fmt" "net/http" "net/url" - "os" - "path/filepath" - "strings" - "github.com/juju/juju/testing/factory" jc "github.com/juju/testing/checkers" "github.com/juju/utils" "golang.org/x/net/websocket" gc "gopkg.in/check.v1" + + "github.com/juju/juju/testing/factory" ) -type debugLogSuite struct { +// debugLogBaseSuite has tests that should be run for both the file +// and DB based variants of debuglog, as well as some test helpers. +type debugLogBaseSuite struct { userAuthHttpSuite - logFile *os.File - last int -} - -var _ = gc.Suite(&debugLogSuite{}) - -func (s *debugLogSuite) TestWithHTTP(c *gc.C) { +} + +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) +} + +func (s *debugLogBaseSuite) TestWithHTTP(c *gc.C) { uri := s.logURL(c, "http", nil).String() _, err := s.sendRequest(c, "", "", "GET", uri, "", nil) c.Assert(err, gc.ErrorMatches, `.*malformed HTTP response.*`) } -func (s *debugLogSuite) TestWithHTTPS(c *gc.C) { +func (s *debugLogBaseSuite) TestWithHTTPS(c *gc.C) { uri := s.logURL(c, "https", nil).String() response, err := s.sendRequest(c, "", "", "GET", uri, "", nil) c.Assert(err, jc.ErrorIsNil) c.Assert(response.StatusCode, gc.Equals, http.StatusBadRequest) } -func (s *debugLogSuite) TestNoAuth(c *gc.C) { +func (s *debugLogBaseSuite) TestNoAuth(c *gc.C) { conn := s.dialWebsocketInternal(c, nil, nil) defer conn.Close() reader := bufio.NewReader(conn) @@ -49,7 +50,7 @@ s.assertWebsocketClosed(c, reader) } -func (s *debugLogSuite) TestAgentLoginsRejected(c *gc.C) { +func (s *debugLogBaseSuite) TestAgentLoginsRejected(c *gc.C) { m, password := s.Factory.MakeMachineReturningPassword(c, &factory.MachineParams{ Nonce: "foo-nonce", }) @@ -63,277 +64,13 @@ s.assertWebsocketClosed(c, reader) } -func (s *debugLogSuite) TestNoLogfile(c *gc.C) { - reader := s.openWebsocket(c, nil) - assertJSONError(c, reader, "cannot open log file: .*: "+utils.NoSuchFileErrRegexp) - s.assertWebsocketClosed(c, reader) -} - -func (s *debugLogSuite) 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) -} - -func (s *debugLogSuite) assertLogReader(c *gc.C, reader *bufio.Reader) { - s.assertLogFollowing(c, reader) - s.writeLogLines(c, logLineCount) - - linesRead := s.readLogLines(c, reader, logLineCount) - c.Assert(linesRead, jc.DeepEquals, logLines) -} - -func (s *debugLogSuite) TestServesLog(c *gc.C) { - s.ensureLogFile(c) - reader := s.openWebsocket(c, nil) - s.assertLogReader(c, reader) -} - -func (s *debugLogSuite) TestReadFromTopLevelPath(c *gc.C) { - // Backwards compatibility check, that we can read the log file at - // https://host:port/log - s.ensureLogFile(c) - reader := s.openWebsocketCustomPath(c, "/log") - s.assertLogReader(c, reader) -} - -func (s *debugLogSuite) TestReadFromEnvUUIDPath(c *gc.C) { - // Check that we can read the log at https://host:port/ENVUUID/log - environ, err := s.State.Environment() - c.Assert(err, jc.ErrorIsNil) - s.ensureLogFile(c) - reader := s.openWebsocketCustomPath(c, fmt.Sprintf("/environment/%s/log", environ.UUID())) - s.assertLogReader(c, reader) -} - -func (s *debugLogSuite) TestReadRejectsWrongEnvUUIDPath(c *gc.C) { - // Check that we cannot pull logs from https://host:port/BADENVUUID/log - s.ensureLogFile(c) - reader := s.openWebsocketCustomPath(c, "/environment/dead-beef-123456/log") - assertJSONError(c, reader, `unknown environment: "dead-beef-123456"`) - s.assertWebsocketClosed(c, reader) -} - -func (s *debugLogSuite) TestReadsFromEnd(c *gc.C) { - s.writeLogLines(c, 10) - - reader := s.openWebsocket(c, nil) - s.assertLogFollowing(c, reader) - s.writeLogLines(c, logLineCount) - - linesRead := s.readLogLines(c, reader, logLineCount-10) - c.Assert(linesRead, jc.DeepEquals, logLines[10:]) -} - -func (s *debugLogSuite) TestReplayFromStart(c *gc.C) { - s.writeLogLines(c, 10) - - reader := s.openWebsocket(c, url.Values{"replay": {"true"}}) - s.assertLogFollowing(c, reader) - s.writeLogLines(c, logLineCount) - - linesRead := s.readLogLines(c, reader, logLineCount) - c.Assert(linesRead, jc.DeepEquals, logLines) -} - -func (s *debugLogSuite) TestBacklog(c *gc.C) { - s.writeLogLines(c, 10) - - reader := s.openWebsocket(c, url.Values{"backlog": {"5"}}) - s.assertLogFollowing(c, reader) - s.writeLogLines(c, logLineCount) - - linesRead := s.readLogLines(c, reader, logLineCount-5) - c.Assert(linesRead, jc.DeepEquals, logLines[5:]) -} - -func (s *debugLogSuite) TestMaxLines(c *gc.C) { - s.writeLogLines(c, 10) - - reader := s.openWebsocket(c, url.Values{"maxLines": {"10"}}) - s.assertLogFollowing(c, reader) - s.writeLogLines(c, logLineCount) - - linesRead := s.readLogLines(c, reader, 10) - c.Assert(linesRead, jc.DeepEquals, logLines[10:20]) - s.assertWebsocketClosed(c, reader) -} - -func (s *debugLogSuite) TestBacklogWithMaxLines(c *gc.C) { - s.writeLogLines(c, 10) - - reader := s.openWebsocket(c, url.Values{"backlog": {"5"}, "maxLines": {"10"}}) - s.assertLogFollowing(c, reader) - s.writeLogLines(c, logLineCount) - - linesRead := s.readLogLines(c, reader, 10) - c.Assert(linesRead, jc.DeepEquals, logLines[5:15]) - s.assertWebsocketClosed(c, reader) -} - -type filterTest struct { - about string - filter url.Values - filtered []string -} - -var filterTests []filterTest = []filterTest{ - { - about: "Filter from original test", - filter: url.Values{ - "includeEntity": {"machine-0", "unit-ubuntu-0"}, - "includeModule": {"juju.cmd"}, - "excludeModule": {"juju.cmd.jujud"}, - }, - filtered: []string{logLines[0], logLines[40]}, - }, { - about: "Filter from original test inverted", - filter: url.Values{ - "excludeEntity": {"machine-1"}, - }, - filtered: []string{logLines[0], logLines[1]}, - }, { - about: "Include Entity Filter with only wildcard", - filter: url.Values{ - "includeEntity": {"*"}, - }, - filtered: []string{logLines[0], logLines[1]}, - }, { - about: "Exclude Entity Filter with only wildcard", - filter: url.Values{ - "excludeEntity": {"*"}, // exclude everything :-) - }, - filtered: []string{}, - }, { - about: "Include Entity Filter with 1 wildcard", - filter: url.Values{ - "includeEntity": {"unit-*"}, - }, - filtered: []string{logLines[40], logLines[41]}, - }, { - about: "Exclude Entity Filter with 1 wildcard", - filter: url.Values{ - "excludeEntity": {"machine-*"}, - }, - filtered: []string{logLines[40], logLines[41]}, - }, { - about: "Include Entity Filter using machine tag", - filter: url.Values{ - "includeEntity": {"machine-1"}, - }, - filtered: []string{logLines[27], logLines[28]}, - }, { - about: "Include Entity Filter using machine name", - filter: url.Values{ - "includeEntity": {"1"}, - }, - filtered: []string{logLines[27], logLines[28]}, - }, { - about: "Include Entity Filter using unit tag", - filter: url.Values{ - "includeEntity": {"unit-ubuntu-0"}, - }, - filtered: []string{logLines[40], logLines[41]}, - }, { - about: "Include Entity Filter using unit name", - filter: url.Values{ - "includeEntity": {"ubuntu/0"}, - }, - filtered: []string{logLines[40], logLines[41]}, - }, { - about: "Include Entity Filter using combination of machine tag and unit name", - filter: url.Values{ - "includeEntity": {"machine-1", "ubuntu/0"}, - "includeModule": {"juju.agent"}, - }, - filtered: []string{logLines[29], logLines[34], logLines[41]}, - }, { - about: "Exclude Entity Filter using machine tag", - filter: url.Values{ - "excludeEntity": {"machine-0"}, - }, - filtered: []string{logLines[27], logLines[28]}, - }, { - about: "Exclude Entity Filter using machine name", - filter: url.Values{ - "excludeEntity": {"0"}, - }, - filtered: []string{logLines[27], logLines[28]}, - }, { - about: "Exclude Entity Filter using unit tag", - filter: url.Values{ - "excludeEntity": {"machine-0", "machine-1", "unit-ubuntu-0"}, - }, - filtered: []string{logLines[54], logLines[55]}, - }, { - about: "Exclude Entity Filter using unit name", - filter: url.Values{ - "excludeEntity": {"machine-0", "machine-1", "ubuntu/0"}, - }, - filtered: []string{logLines[54], logLines[55]}, - }, { - about: "Exclude Entity Filter using combination of machine tag and unit name", - filter: url.Values{ - "excludeEntity": {"0", "1", "ubuntu/0"}, - }, - filtered: []string{logLines[54], logLines[55]}, - }, -} - -// TestFilter tests that filters are processed correctly given specific debug-log configuration. -func (s *debugLogSuite) TestFilter(c *gc.C) { - for i, test := range filterTests { - c.Logf("test %d: %v\n", i, test.about) - - // ensures log file - path := filepath.Join(s.LogDir, "all-machines.log") - var err error - s.logFile, err = os.Create(path) - c.Assert(err, jc.ErrorIsNil) - - // opens web socket - conn := s.dialWebsocket(c, test.filter) - reader := bufio.NewReader(conn) - - s.assertLogFollowing(c, reader) - s.writeLogLines(c, logLineCount) - /* - This will filter and return as many lines as filtered wanted to examine. - So, if specified filter can potentially return 40 lines from sample log but filtered only wanted 2, - then the first 2 lines that match the filter will be returned here. - */ - linesRead := s.readLogLines(c, reader, len(test.filtered)) - // compare retrieved lines with expected - c.Assert(linesRead, jc.DeepEquals, test.filtered) - - // release resources - conn.Close() - s.logFile.Close() - s.logFile = nil - s.last = 0 - } -} - -// readLogLines filters and returns as many lines as filtered wanted to examine. -// So, if specified filter can potentially return 40 lines from sample log but filtered only wanted 2, -// then the first 2 lines that match the filter will be returned here. -func (s *debugLogSuite) readLogLines(c *gc.C, reader *bufio.Reader, count int) (linesRead []string) { - for len(linesRead) < count { - line, err := reader.ReadString('\n') - c.Assert(err, jc.ErrorIsNil) - // Trim off the trailing \n - linesRead = append(linesRead, line[:len(line)-1]) - } - return linesRead -} - -func (s *debugLogSuite) openWebsocket(c *gc.C, values url.Values) *bufio.Reader { +func (s *debugLogBaseSuite) openWebsocket(c *gc.C, values url.Values) *bufio.Reader { conn := s.dialWebsocket(c, values) s.AddCleanup(func(_ *gc.C) { conn.Close() }) return bufio.NewReader(conn) } -func (s *debugLogSuite) openWebsocketCustomPath(c *gc.C, path string) *bufio.Reader { +func (s *debugLogBaseSuite) openWebsocketCustomPath(c *gc.C, path string) *bufio.Reader { server := s.logURL(c, "wss", nil) server.Path = path header := utils.BasicAuthHeader(s.userTag.String(), s.password) @@ -342,110 +79,16 @@ return bufio.NewReader(conn) } -func (s *debugLogSuite) ensureLogFile(c *gc.C) { - if s.logFile != nil { - return - } - path := filepath.Join(s.LogDir, "all-machines.log") - var err error - s.logFile, err = os.Create(path) - c.Assert(err, jc.ErrorIsNil) - s.AddCleanup(func(c *gc.C) { - s.logFile.Close() - s.logFile = nil - s.last = 0 - }) -} - -func (s *debugLogSuite) writeLogLines(c *gc.C, count int) { - s.ensureLogFile(c) - for i := 0; i < count && s.last < logLineCount; i++ { - s.logFile.WriteString(logLines[s.last] + "\n") - s.last++ - } -} - -func (s *debugLogSuite) dialWebsocketInternal(c *gc.C, queryParams url.Values, header http.Header) *websocket.Conn { +func (s *debugLogBaseSuite) logURL(c *gc.C, scheme string, queryParams url.Values) *url.URL { + return s.makeURL(c, scheme, "/log", queryParams) +} + +func (s *debugLogBaseSuite) dialWebsocket(c *gc.C, queryParams url.Values) *websocket.Conn { + header := utils.BasicAuthHeader(s.userTag.String(), s.password) + return s.dialWebsocketInternal(c, queryParams, header) +} + +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) } - -func (s *debugLogSuite) dialWebsocket(c *gc.C, queryParams url.Values) *websocket.Conn { - header := utils.BasicAuthHeader(s.userTag.String(), s.password) - return s.dialWebsocketInternal(c, queryParams, header) -} - -func (s *debugLogSuite) logURL(c *gc.C, scheme string, queryParams url.Values) *url.URL { - return s.makeURL(c, scheme, "/log", queryParams) -} - -func (s *debugLogSuite) assertLogFollowing(c *gc.C, reader *bufio.Reader) { - errResult := readJSONErrorLine(c, reader) - c.Assert(errResult.Error, gc.IsNil) -} - -var ( - logLines = strings.Split(` -machine-0: 2014-03-24 22:34:25 INFO juju.cmd supercommand.go:297 running juju-1.17.7.1-trusty-amd64 [gc] -machine-0: 2014-03-24 22:34:25 INFO juju.cmd.jujud machine.go:127 machine agent machine-0 start (1.17.7.1-trusty-amd64 [gc]) -machine-0: 2014-03-24 22:34:25 DEBUG juju.agent agent.go:384 read agent config, format "1.18" -machine-0: 2014-03-24 22:34:25 INFO juju.cmd.jujud machine.go:155 Starting StateWorker for machine-0 -machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "state" -machine-0: 2014-03-24 22:34:25 INFO juju.state open.go:80 opening state; mongo addresses: ["localhost:37017"]; entity "machine-0" -machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "api" -machine-0: 2014-03-24 22:34:25 INFO juju apiclient.go:114 api: dialing "wss://localhost:17070/" -machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "termination" -machine-0: 2014-03-24 22:34:25 ERROR juju apiclient.go:119 api: websocket.Dial wss://localhost:17070/: dial tcp 127.0.0.1:17070: connection refused -machine-0: 2014-03-24 22:34:25 ERROR juju runner.go:220 worker: exited "api": websocket.Dial wss://localhost:17070/: dial tcp 127.0.0.1:17070: connection refused -machine-0: 2014-03-24 22:34:25 INFO juju runner.go:254 worker: restarting "api" in 3s -machine-0: 2014-03-24 22:34:25 INFO juju.state open.go:118 connection established -machine-0: 2014-03-24 22:34:25 DEBUG juju.utils gomaxprocs.go:24 setting GOMAXPROCS to 8 -machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "local-storage" -machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "instancepoller" -machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "apiserver" -machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "resumer" -machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "cleaner" -machine-0: 2014-03-24 22:34:25 INFO juju.apiserver apiserver.go:43 listening on "[::]:17070" -machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "minunitsworker" -machine-0: 2014-03-24 22:34:28 INFO juju runner.go:262 worker: start "api" -machine-0: 2014-03-24 22:34:28 INFO juju apiclient.go:114 api: dialing "wss://localhost:17070/" -machine-0: 2014-03-24 22:34:28 INFO juju.apiserver apiserver.go:131 [1] API connection from 127.0.0.1:36491 -machine-0: 2014-03-24 22:34:28 INFO juju apiclient.go:124 api: connection established -machine-0: 2014-03-24 22:34:28 DEBUG juju.apiserver apiserver.go:120 <- [1] {"RequestId":1,"Type":"Admin","Request":"Login","Params":{"AuthTag":"machine-0","Password":"ARbW7iCV4LuMugFEG+Y4e0yr","Nonce":"user-admin:bootstrap"}} -machine-0: 2014-03-24 22:34:28 DEBUG juju.apiserver apiserver.go:127 -> [1] machine-0 10.305679ms {"RequestId":1,"Response":{}} Admin[""].Login -machine-1: 2014-03-24 22:36:28 INFO juju.cmd supercommand.go:297 running juju-1.17.7.1-precise-amd64 [gc] -machine-1: 2014-03-24 22:36:28 INFO juju.cmd.jujud machine.go:127 machine agent machine-1 start (1.17.7.1-precise-amd64 [gc]) -machine-1: 2014-03-24 22:36:28 DEBUG juju.agent agent.go:384 read agent config, format "1.18" -machine-1: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "api" -machine-1: 2014-03-24 22:36:28 INFO juju apiclient.go:114 api: dialing "wss://10.0.3.1:17070/" -machine-1: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "termination" -machine-1: 2014-03-24 22:36:28 INFO juju apiclient.go:124 api: connection established -machine-1: 2014-03-24 22:36:28 DEBUG juju.agent agent.go:523 writing configuration file -machine-1: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "upgrader" -machine-1: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "upgrade-steps" -machine-1: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "machiner" -machine-1: 2014-03-24 22:36:28 INFO juju.cmd.jujud machine.go:458 upgrade to 1.17.7.1-precise-amd64 already completed. -machine-1: 2014-03-24 22:36:28 INFO juju.cmd.jujud machine.go:445 upgrade to 1.17.7.1-precise-amd64 completed. -unit-ubuntu-0[32423]: 2014-03-24 22:36:28 INFO juju.cmd supercommand.go:297 running juju-1.17.7.1-precise-amd64 [gc] -unit-ubuntu-0[34543]: 2014-03-24 22:36:28 DEBUG juju.agent agent.go:384 read agent config, format "1.18" -unit-ubuntu-0: 2014-03-24 22:36:28 INFO juju.jujud unit.go:76 unit agent unit-ubuntu-0 start (1.17.7.1-precise-amd64 [gc]) -unit-ubuntu-0: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "api" -unit-ubuntu-0: 2014-03-24 22:36:28 INFO juju apiclient.go:114 api: dialing "wss://10.0.3.1:17070/" -unit-ubuntu-0: 2014-03-24 22:36:28 INFO juju apiclient.go:124 api: connection established -unit-ubuntu-0: 2014-03-24 22:36:28 DEBUG juju.agent agent.go:523 writing configuration file -unit-ubuntu-0: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "upgrader" -unit-ubuntu-0: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "logger" -unit-ubuntu-0: 2014-03-24 22:36:28 DEBUG juju.worker.logger logger.go:35 initial log config: "=DEBUG" -unit-ubuntu-0: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "uniter" -unit-ubuntu-0: 2014-03-24 22:36:28 DEBUG juju.worker.logger logger.go:60 logger setup -unit-ubuntu-0: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "rsyslog" -unit-ubuntu-0: 2014-03-24 22:36:28 DEBUG juju.worker.rsyslog worker.go:76 starting rsyslog worker mode 1 for "unit-ubuntu-0" "tim-local" -unit-ubuntu-1: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "logger" -unit-ubuntu-1: 2014-03-24 22:36:28 DEBUG juju.worker.logger logger.go:35 initial log config: "=DEBUG" -unit-ubuntu-1: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "uniter" -unit-ubuntu-1: 2014-03-24 22:36:28 DEBUG juju.worker.logger logger.go:60 logger setup -unit-ubuntu-1: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "rsyslog" -unit-ubuntu-1: 2014-03-24 22:36:28 DEBUG juju.worker.rsyslog worker.go:76 starting rsyslog worker mode 1 for "unit-ubuntu-0" "tim-local" -`[1:], "\n") - logLineCount = len(logLines) -) === modified file 'src/github.com/juju/juju/apiserver/diskmanager/diskmanager.go' --- src/github.com/juju/juju/apiserver/diskmanager/diskmanager.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/diskmanager/diskmanager.go 2015-10-23 18:29:32 +0000 @@ -97,6 +97,7 @@ for i, dev := range devices { result[i] = state.BlockDeviceInfo{ dev.DeviceName, + dev.DeviceLinks, dev.Label, dev.UUID, dev.HardwareId, === modified file 'src/github.com/juju/juju/apiserver/environment/toolsversionupdate.go' --- src/github.com/juju/juju/apiserver/environment/toolsversionupdate.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/environment/toolsversionupdate.go 2015-10-23 18:29:32 +0000 @@ -28,7 +28,7 @@ Environment() (*state.Environment, error) } -type toolsFinder func(environs.Environ, int, int, coretools.Filter) (coretools.List, error) +type toolsFinder func(environs.Environ, int, int, string, coretools.Filter) (coretools.List, error) type envVersionUpdater func(*state.Environment, version.Number) error var newEnvirons = environs.New @@ -46,9 +46,15 @@ // finder receives major and minor as parameters as it uses them to filter versions and // only return patches for the passed major.minor (from major.minor.patch). - vers, err := finder(env, currentVersion.Major, currentVersion.Minor, coretools.Filter{}) + // We'll try the released stream first, then fall back to the current configured stream + // if no released tools are found. + vers, err := finder(env, currentVersion.Major, currentVersion.Minor, tools.ReleasedStream, coretools.Filter{}) + preferredStream := tools.PreferredStream(¤tVersion, cfg.Development(), cfg.AgentStream()) + if preferredStream != tools.ReleasedStream && errors.Cause(err) == coretools.ErrNoMatches { + vers, err = finder(env, currentVersion.Major, currentVersion.Minor, preferredStream, coretools.Filter{}) + } if err != nil { - return version.Zero, errors.Annotatef(err, "canot find available tools") + return version.Zero, errors.Annotatef(err, "cannot find available tools") } // Newest also returns a list of the items in this list matching with the // newest version. @@ -76,6 +82,10 @@ } ver, err := checkToolsAvailability(cfg, finder) if err != nil { + if errors.Cause(err) == coretools.ErrNoMatches { + // No newer tools, so exit silently. + return nil + } return errors.Annotate(err, "cannot get latest version") } if ver == version.Zero { === modified file 'src/github.com/juju/juju/apiserver/environment/toolsversionupdate_test.go' --- src/github.com/juju/juju/apiserver/environment/toolsversionupdate_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/environment/toolsversionupdate_test.go 2015-10-23 18:29:32 +0000 @@ -25,59 +25,77 @@ environs.Environ } -// SampleConfig() returns an environment configuration with all required -// attributes set. -func sampleConfig() coretesting.Attrs { - return coretesting.Attrs{ - "type": "dummy", - "name": "only", - "uuid": coretesting.EnvironmentTag.Id(), - "authorized-keys": coretesting.FakeAuthKeys, - "firewall-mode": config.FwInstance, - "admin-secret": coretesting.DefaultMongoPassword, - "ca-cert": coretesting.CACert, - "ca-private-key": coretesting.CAKey, - "ssl-hostname-verification": true, - "development": false, - "state-port": 1234, - "api-port": 4321, - "syslog-port": 2345, - "default-series": config.LatestLtsSeries(), - - "secret": "pork", - "state-server": true, - "prefer-ipv6": true, - } -} - func (s *updaterSuite) TestCheckTools(c *gc.C) { - sConfig := sampleConfig() - sConfig["agent-version"] = "2.5.0" - 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 ( - calledWithEnviron environs.Environ - calledWithMajor, calledWithMinor int - calledWithFilter coretools.Filter - ) - fakeToolFinder := func(e environs.Environ, maj int, min int, filter coretools.Filter) (coretools.List, error) { - calledWithEnviron = e - calledWithMajor = maj - calledWithMinor = min - calledWithFilter = filter - ver := version.Binary{Number: version.Number{Major: maj, Minor: min}} - t := coretools.Tools{Version: ver, URL: "http://example.com", Size: 1} - c.Assert(calledWithMajor, gc.Equals, 2) - c.Assert(calledWithMinor, gc.Equals, 5) - return coretools.List{&t}, nil - } - - ver, err := checkToolsAvailability(cfg, fakeToolFinder) - c.Assert(err, jc.ErrorIsNil) + sConfig := coretesting.FakeConfig() + sConfig = sConfig.Merge(coretesting.Attrs{ + "agent-version": "2.5.0", + }) + 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 ( + calledWithEnviron environs.Environ + calledWithMajor, calledWithMinor int + calledWithFilter coretools.Filter + ) + fakeToolFinder := func(e environs.Environ, maj int, min int, stream string, filter coretools.Filter) (coretools.List, error) { + calledWithEnviron = e + calledWithMajor = maj + calledWithMinor = min + calledWithFilter = filter + ver := version.Binary{Number: version.Number{Major: maj, Minor: min}} + t := coretools.Tools{Version: ver, URL: "http://example.com", Size: 1} + c.Assert(calledWithMajor, gc.Equals, 2) + c.Assert(calledWithMinor, gc.Equals, 5) + c.Assert(stream, gc.Equals, "released") + return coretools.List{&t}, nil + } + + ver, err := checkToolsAvailability(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}) +} + +func (s *updaterSuite) TestCheckToolsNonReleasedStream(c *gc.C) { + sConfig := coretesting.FakeConfig() + sConfig = sConfig.Merge(coretesting.Attrs{ + "agent-version": "2.5-alpha1", + "agent-stream": "proposed", + }) + 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 ( + calledWithEnviron environs.Environ + calledWithMajor, calledWithMinor int + calledWithFilter coretools.Filter + calledWithStreams []string + ) + fakeToolFinder := func(e environs.Environ, maj int, min int, stream string, filter coretools.Filter) (coretools.List, error) { + calledWithEnviron = e + calledWithMajor = maj + calledWithMinor = min + calledWithFilter = filter + calledWithStreams = append(calledWithStreams, stream) + if stream == "released" { + return nil, coretools.ErrNoMatches + } + ver := version.Binary{Number: version.Number{Major: maj, Minor: min}} + t := coretools.Tools{Version: ver, URL: "http://example.com", Size: 1} + c.Assert(calledWithMajor, gc.Equals, 2) + c.Assert(calledWithMinor, gc.Equals, 5) + return coretools.List{&t}, nil + } + ver, err := checkToolsAvailability(cfg, fakeToolFinder) + c.Assert(err, jc.ErrorIsNil) + c.Assert(calledWithStreams, gc.DeepEquals, []string{"released", "proposed"}) c.Assert(ver, gc.Not(gc.Equals), version.Zero) c.Assert(ver, gc.Equals, version.Number{Major: 2, Minor: 5, Patch: 0}) } @@ -96,13 +114,15 @@ s.PatchValue(&newEnvirons, fakeNewEnvirons) fakeEnvConfig := func(_ *state.Environment) (*config.Config, error) { - sConfig := sampleConfig() - sConfig["agent-version"] = "2.5.0" + sConfig := coretesting.FakeConfig() + sConfig = sConfig.Merge(coretesting.Attrs{ + "agent-version": "2.5.0", + }) return config.New(config.NoDefaults, sConfig) } s.PatchValue(&envConfig, fakeEnvConfig) - fakeToolFinder := func(_ environs.Environ, _ int, _ int, _ coretools.Filter) (coretools.List, error) { + fakeToolFinder := func(_ environs.Environ, _ int, _ int, _ string, _ coretools.Filter) (coretools.List, error) { ver := version.Binary{Number: version.Number{Major: 2, Minor: 5, Patch: 2}} olderVer := version.Binary{Number: version.Number{Major: 2, Minor: 5, Patch: 1}} t := coretools.Tools{Version: ver, URL: "http://example.com", Size: 1} @@ -122,3 +142,33 @@ c.Assert(ver, gc.Not(gc.Equals), version.Zero) c.Assert(ver, gc.Equals, version.Number{Major: 2, Minor: 5, Patch: 2}) } + +func (s *updaterSuite) TestUpdateToolsAvailabilityNoMatches(c *gc.C) { + fakeNewEnvirons := func(*config.Config) (environs.Environ, error) { + return dummyEnviron{}, nil + } + s.PatchValue(&newEnvirons, fakeNewEnvirons) + + fakeEnvConfig := func(_ *state.Environment) (*config.Config, error) { + sConfig := coretesting.FakeConfig() + sConfig = sConfig.Merge(coretesting.Attrs{ + "agent-version": "2.5.0", + }) + return config.New(config.NoDefaults, sConfig) + } + s.PatchValue(&envConfig, fakeEnvConfig) + + // No new tools available. + fakeToolFinder := func(_ environs.Environ, _ int, _ int, _ string, _ coretools.Filter) (coretools.List, error) { + return nil, coretools.ErrNoMatches + } + + // Update should never be called. + fakeUpdate := func(_ *state.Environment, v version.Number) error { + c.Fail() + return nil + } + + err := updateToolsAvailability(&envGetter{}, fakeToolFinder, fakeUpdate) + c.Assert(err, jc.ErrorIsNil) +} === modified file 'src/github.com/juju/juju/apiserver/environmentmanager/environmentmanager.go' --- src/github.com/juju/juju/apiserver/environmentmanager/environmentmanager.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/environmentmanager/environmentmanager.go 2015-10-23 18:29:32 +0000 @@ -6,6 +6,8 @@ package environmentmanager import ( + "time" + "github.com/juju/errors" "github.com/juju/loggo" "github.com/juju/names" @@ -31,7 +33,7 @@ type EnvironmentManager interface { ConfigSkeleton(args params.EnvironmentSkeletonConfigArgs) (params.EnvironConfigResult, error) CreateEnvironment(args params.EnvironmentCreateArgs) (params.Environment, error) - ListEnvironments(user params.Entity) (params.EnvironmentList, error) + ListEnvironments(user params.Entity) (params.UserEnvironmentList, error) } // EnvironmentManagerAPI implements the environment manager interface and is @@ -63,14 +65,25 @@ }, nil } -func (em *EnvironmentManagerAPI) authCheck(user, adminUser names.UserTag) error { - authTag := em.authorizer.GetAuthTag() - apiUser, ok := authTag.(names.UserTag) - if !ok { - return errors.Errorf("auth tag should be a user, but isn't: %q", authTag.String()) - } - logger.Tracef("comparing api user %q against owner %q and admin %q", apiUser, user, adminUser) - if apiUser == user || apiUser == adminUser { +// authCheck checks if the user is acting on their own behalf, or if they +// are an administrator acting on behalf of another user. +func (em *EnvironmentManagerAPI) authCheck(user names.UserTag) error { + // Since we know this is a user tag (because AuthClient is true), + // we just do the type assertion to the UserTag. + apiUser, _ := em.authorizer.GetAuthTag().(names.UserTag) + isAdmin, err := em.state.IsSystemAdministrator(apiUser) + if err != nil { + return errors.Trace(err) + } + if isAdmin { + logger.Tracef("%q is a system admin", apiUser.Username()) + return nil + } + + // We can't just compare the UserTags themselves as the provider part + // may be unset, and gets replaced with 'local'. We must compare against + // the Username of the user tag. + if apiUser.Username() == user.Username() { return nil } return common.ErrPerm @@ -196,6 +209,10 @@ if err != nil { return nil, errors.Trace(err) } + cfg, err = provider.PrepareForCreateEnvironment(cfg) + if err != nil { + return nil, errors.Trace(err) + } cfg, err = provider.Validate(cfg, nil) if err != nil { return nil, errors.Annotate(err, "provider validation failed") @@ -282,7 +299,6 @@ if err != nil { return result, errors.Trace(err) } - adminUser := stateServerEnv.Owner() ownerTag, err := names.ParseUserTag(args.OwnerTag) if err != nil { @@ -292,7 +308,7 @@ // Any user is able to create themselves an environment (until real fine // grain permissions are available), and admins (the creator of the state // server environment) are able to create environments for other people. - err = em.authCheck(ownerTag, adminUser) + err = em.authCheck(ownerTag) if err != nil { return result, errors.Trace(err) } @@ -321,21 +337,15 @@ // has access to in the current server. Only that state server owner // can list environments for any user (at this stage). Other users // can only ask about their own environments. -func (em *EnvironmentManagerAPI) ListEnvironments(user params.Entity) (params.EnvironmentList, error) { - result := params.EnvironmentList{} - - stateServerEnv, err := em.state.StateServerEnvironment() - if err != nil { - return result, errors.Trace(err) - } - adminUser := stateServerEnv.Owner() +func (em *EnvironmentManagerAPI) ListEnvironments(user params.Entity) (params.UserEnvironmentList, error) { + result := params.UserEnvironmentList{} userTag, err := names.ParseUserTag(user.Tag) if err != nil { return result, errors.Trace(err) } - err = em.authCheck(userTag, adminUser) + err = em.authCheck(userTag) if err != nil { return result, errors.Trace(err) } @@ -346,10 +356,22 @@ } for _, env := range environments { - result.Environments = append(result.Environments, params.Environment{ - Name: env.Name(), - UUID: env.UUID(), - OwnerTag: env.Owner().String(), + var lastConn *time.Time + userLastConn, err := env.LastConnection() + if err != nil { + if !state.IsNeverConnectedError(err) { + return result, errors.Trace(err) + } + } else { + lastConn = &userLastConn + } + result.UserEnvironments = append(result.UserEnvironments, params.UserEnvironment{ + Environment: params.Environment{ + Name: env.Name(), + UUID: env.UUID(), + OwnerTag: env.Owner().String(), + }, + LastConnection: lastConn, }) logger.Debugf("list env: %s, %s, %s", env.Name(), env.UUID(), env.Owner()) } === modified file 'src/github.com/juju/juju/apiserver/environmentmanager/environmentmanager_test.go' --- src/github.com/juju/juju/apiserver/environmentmanager/environmentmanager_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/environmentmanager/environmentmanager_test.go 2015-10-23 18:29:32 +0000 @@ -28,7 +28,7 @@ "github.com/juju/juju/version" ) -type envManagerSuite struct { +type envManagerBaseSuite struct { jujutesting.JujuConnSuite envmanager *environmentmanager.EnvironmentManagerAPI @@ -36,9 +36,7 @@ authoriser apiservertesting.FakeAuthorizer } -var _ = gc.Suite(&envManagerSuite{}) - -func (s *envManagerSuite) SetUpTest(c *gc.C) { +func (s *envManagerBaseSuite) SetUpTest(c *gc.C) { s.JujuConnSuite.SetUpTest(c) s.resources = common.NewResources() s.AddCleanup(func(_ *gc.C) { s.resources.StopAll() }) @@ -50,6 +48,19 @@ loggo.GetLogger("juju.apiserver.environmentmanager").SetLogLevel(loggo.TRACE) } +func (s *envManagerBaseSuite) setAPIUser(c *gc.C, user names.UserTag) { + s.authoriser.Tag = user + envmanager, err := environmentmanager.NewEnvironmentManagerAPI(s.State, s.resources, s.authoriser) + c.Assert(err, jc.ErrorIsNil) + s.envmanager = envmanager +} + +type envManagerSuite struct { + envManagerBaseSuite +} + +var _ = gc.Suite(&envManagerSuite{}) + func (s *envManagerSuite) TestNewAPIAcceptsClient(c *gc.C) { anAuthoriser := s.authoriser anAuthoriser.Tag = names.NewUserTag("external@remote") @@ -85,13 +96,6 @@ return params } -func (s *envManagerSuite) setAPIUser(c *gc.C, user names.UserTag) { - s.authoriser.Tag = user - envmanager, err := environmentmanager.NewEnvironmentManagerAPI(s.State, s.resources, s.authoriser) - c.Assert(err, jc.ErrorIsNil) - s.envmanager = envmanager -} - func (s *envManagerSuite) TestUserCanCreateEnvironment(c *gc.C) { owner := names.NewUserTag("external@remote") s.setAPIUser(c, owner) @@ -108,6 +112,17 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(env.OwnerTag, gc.Equals, owner.String()) c.Assert(env.Name, gc.Equals, "test-env") + // Make sure that the environment created does actually have the correct + // owner, and that owner is actually allowed to use the environment. + newState, err := s.State.ForEnviron(names.NewEnvironTag(env.UUID)) + c.Assert(err, jc.ErrorIsNil) + defer newState.Close() + + newEnv, err := newState.Environment() + c.Assert(err, jc.ErrorIsNil) + c.Assert(newEnv.Owner(), gc.Equals, owner) + _, err = newState.EnvironmentUser(owner) + c.Assert(err, jc.ErrorIsNil) } func (s *envManagerSuite) TestNonAdminCannotCreateEnvironmentForSomeoneElse(c *gc.C) { @@ -140,7 +155,7 @@ provider: "local", expected: []string{ "type", "ca-cert", "state-port", "api-port", "syslog-port", "rsyslog-ca-cert", "rsyslog-ca-key", - "container", "network-bridge", "root-dir"}, + "container", "network-bridge", "root-dir", "proxy-ssh"}, }, { provider: "maas", expected: []string{ @@ -151,6 +166,11 @@ expected: []string{ "type", "ca-cert", "state-port", "api-port", "syslog-port", "rsyslog-ca-cert", "rsyslog-ca-key", "region", "auth-url", "auth-mode"}, + }, { + provider: "ec2", + expected: []string{ + "type", "ca-cert", "state-port", "api-port", "syslog-port", "rsyslog-ca-cert", "rsyslog-ca-key", + "region"}, }, } { c.Logf("%d: %s provider", i, test.provider) @@ -295,7 +315,17 @@ s.setAPIUser(c, user) result, err := s.envmanager.ListEnvironments(params.Entity{user.String()}) c.Assert(err, jc.ErrorIsNil) - c.Assert(result.Environments, gc.HasLen, 0) + c.Assert(result.UserEnvironments, gc.HasLen, 0) +} + +func (s *envManagerSuite) TestListEnvironmentsForSelfLocalUser(c *gc.C) { + // When the user's credentials cache stores the simple name, but the + // api server converts it to a fully qualified name. + user := names.NewUserTag("local-user") + s.setAPIUser(c, names.NewUserTag("local-user@local")) + result, err := s.envmanager.ListEnvironments(params.Entity{user.String()}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result.UserEnvironments, gc.HasLen, 0) } func (s *envManagerSuite) checkEnvironmentMatches(c *gc.C, env params.Environment, expected *state.Environment) { @@ -309,10 +339,10 @@ s.setAPIUser(c, user) result, err := s.envmanager.ListEnvironments(params.Entity{user.String()}) c.Assert(err, jc.ErrorIsNil) - c.Assert(result.Environments, gc.HasLen, 1) + c.Assert(result.UserEnvironments, gc.HasLen, 1) expected, err := s.State.Environment() c.Assert(err, jc.ErrorIsNil) - s.checkEnvironmentMatches(c, result.Environments[0], expected) + s.checkEnvironmentMatches(c, result.UserEnvironments[0].Environment, expected) } func (s *envManagerSuite) TestListEnvironmentsAdminListsOther(c *gc.C) { @@ -321,7 +351,7 @@ other := names.NewUserTag("external@remote") result, err := s.envmanager.ListEnvironments(params.Entity{other.String()}) c.Assert(err, jc.ErrorIsNil) - c.Assert(result.Environments, gc.HasLen, 0) + c.Assert(result.UserEnvironments, gc.HasLen, 0) } func (s *envManagerSuite) TestListEnvironmentsDenied(c *gc.C) { @@ -340,6 +370,10 @@ return cfg, nil } +func (*fakeProvider) PrepareForCreateEnvironment(cfg *config.Config) (*config.Config, error) { + return cfg, nil +} + func init() { environs.RegisterProvider("fake", &fakeProvider{}) } === modified file 'src/github.com/juju/juju/apiserver/environmentmanager/state.go' --- src/github.com/juju/juju/apiserver/environmentmanager/state.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/environmentmanager/state.go 2015-10-23 18:29:32 +0000 @@ -15,9 +15,10 @@ } type stateInterface interface { + EnvironmentsForUser(names.UserTag) ([]*state.UserEnvironment, error) + IsSystemAdministrator(user names.UserTag) (bool, error) + NewEnvironment(*config.Config, names.UserTag) (*state.Environment, *state.State, error) StateServerEnvironment() (*state.Environment, error) - NewEnvironment(*config.Config, names.UserTag) (*state.Environment, *state.State, error) - EnvironmentsForUser(names.UserTag) ([]*state.Environment, error) } type stateShim struct { === modified file 'src/github.com/juju/juju/apiserver/export_test.go' --- src/github.com/juju/juju/apiserver/export_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/export_test.go 2015-10-23 18:29:32 +0000 @@ -23,11 +23,10 @@ NewPingTimeout = newPingTimeout MaxClientPingInterval = &maxClientPingInterval MongoPingInterval = &mongoPingInterval - NewTimer = &newTimer - ResetTimer = &resetTimer NewBackups = &newBackups ParseLogLine = parseLogLine AgentMatchesFilter = agentMatchesFilter + NewLogTailer = &newLogTailer ) func ApiHandlerWithEntity(entity state.Entity) *apiHandler { @@ -62,14 +61,7 @@ // *barely* connected to anything. Just enough to let you probe some // of the interfaces, but not enough to actually do any RPC calls. func TestingApiRoot(st *state.State) rpc.MethodFinder { - return newApiRoot(st, false, common.NewResources(), nil) -} - -// TestApiRootEx creates an apiRoot for testing. It's not connected to -// anything but allows access to some functionality. -func TestingApiRootEx(st *state.State, closeState bool) (*apiRoot, *common.Resources) { - resources := common.NewResources() - return newApiRoot(st, closeState, resources, nil), resources + return newApiRoot(st, common.NewResources(), nil) } // TestingApiHandler gives you an ApiHandler that isn't connected to @@ -172,12 +164,12 @@ return newAboutToRestoreRoot(r) } -// LogLineAgentTag gives tests access to an internal logLine attribute -func (logLine *logLine) LogLineAgentTag() string { - return logLine.agentTag +// LogLineAgentTag gives tests access to an internal logFileLine attribute +func (logFileLine *logFileLine) LogLineAgentTag() string { + return logFileLine.agentTag } -// LogLineAgentName gives tests access to an internal logLine attribute -func (logLine *logLine) LogLineAgentName() string { - return logLine.agentName +// LogLineAgentName gives tests access to an internal logFileLine attribute +func (logFileLine *logFileLine) LogLineAgentName() string { + return logFileLine.agentName } === modified file 'src/github.com/juju/juju/apiserver/firewaller/firewaller.go' --- src/github.com/juju/juju/apiserver/firewaller/firewaller.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/firewaller/firewaller.go 2015-10-23 18:29:32 +0000 @@ -48,10 +48,10 @@ return nil, common.ErrPerm } // Set up the various authorization checkers. - accessEnviron := getAuthFuncForTagKind(names.EnvironTagKind) - accessUnit := getAuthFuncForTagKind(names.UnitTagKind) - accessService := getAuthFuncForTagKind(names.ServiceTagKind) - accessMachine := getAuthFuncForTagKind(names.MachineTagKind) + accessEnviron := common.AuthFuncForTagKind(names.EnvironTagKind) + accessUnit := common.AuthFuncForTagKind(names.UnitTagKind) + accessService := common.AuthFuncForTagKind(names.ServiceTagKind) + accessMachine := common.AuthFuncForTagKind(names.MachineTagKind) accessUnitOrService := common.AuthEither(accessUnit, accessService) accessUnitServiceOrMachine := common.AuthEither(accessUnitOrService, accessMachine) @@ -328,17 +328,3 @@ // machine. return entity.(*state.Machine), nil } - -// getAuthFuncForTagKind returns a GetAuthFunc which creates an -// AuthFunc allowing only the given tag kind and denies all -// others. In the special case where a single empty string is given, -// it's assumed only environment tags are allowed, but not specified -// (for now). -func getAuthFuncForTagKind(kind string) common.GetAuthFunc { - return func() (common.AuthFunc, error) { - return func(tag names.Tag) bool { - // Allow only the given tag kind. - return tag.Kind() == kind - }, nil - } -} === modified file 'src/github.com/juju/juju/apiserver/highavailability/highavailability_test.go' --- src/github.com/juju/juju/apiserver/highavailability/highavailability_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/highavailability/highavailability_test.go 2015-10-23 18:29:32 +0000 @@ -289,6 +289,7 @@ c.Assert(ensureAvailabilityResult.Converted, gc.HasLen, 0) machines, err = s.State.AllMachines() + c.Assert(err, jc.ErrorIsNil) c.Assert(machines, gc.HasLen, 4) } === modified file 'src/github.com/juju/juju/apiserver/httphandler.go' --- src/github.com/juju/juju/apiserver/httphandler.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/httphandler.go 2015-10-23 18:29:32 +0000 @@ -23,9 +23,8 @@ // httpHandler handles http requests through HTTPS in the API server. type httpHandler struct { - // rename the state variable to catch all uses of it - // state server state connection, used for validation - ssState *state.State + // A cache of State instances for different environments. + statePool *state.StatePool // strictValidation means that empty envUUID values are not valid. strictValidation bool // stateServerEnvOnly only validates the state server environment @@ -34,8 +33,7 @@ // httpStateWrapper reflects a state connection for a given http connection. type httpStateWrapper struct { - state *state.State - cleanupFunc func() + state *state.State } func (h *httpHandler) getEnvironUUID(r *http.Request) string { @@ -50,8 +48,8 @@ func (h *httpHandler) validateEnvironUUID(r *http.Request) (*httpStateWrapper, error) { envUUID := h.getEnvironUUID(r) - envState, needsClosing, err := validateEnvironUUID(validateArgs{ - st: h.ssState, + envState, err := validateEnvironUUID(validateArgs{ + statePool: h.statePool, envUUID: envUUID, strict: h.strictValidation, stateServerEnvOnly: h.stateServerEnvOnly, @@ -59,14 +57,7 @@ if err != nil { return nil, errors.Trace(err) } - wrapper := &httpStateWrapper{state: envState} - if needsClosing { - wrapper.cleanupFunc = func() { - logger.Debugf("close connection to environment: %s", envState.EnvironUUID()) - envState.Close() - } - } - return wrapper, nil + return &httpStateWrapper{state: envState}, nil } // authenticate parses HTTP basic authentication and authorizes the @@ -127,9 +118,3 @@ return nil, common.ErrBadCreds } } - -func (h *httpStateWrapper) cleanup() { - if h.cleanupFunc != nil { - h.cleanupFunc() - } -} === added directory 'src/github.com/juju/juju/apiserver/imagemetadata' === added file 'src/github.com/juju/juju/apiserver/imagemetadata/export_test.go' --- src/github.com/juju/juju/apiserver/imagemetadata/export_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/imagemetadata/export_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,9 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package imagemetadata + +var ( + CreateAPI = createAPI + ParseMetadataFromParams = parseMetadataFromParams +) === added file 'src/github.com/juju/juju/apiserver/imagemetadata/functions_test.go' --- src/github.com/juju/juju/apiserver/imagemetadata/functions_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/imagemetadata/functions_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,74 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package imagemetadata_test + +import ( + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/imagemetadata" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/state/cloudimagemetadata" + "github.com/juju/juju/testing" +) + +type funcSuite struct { + testing.BaseSuite + + expected cloudimagemetadata.Metadata +} + +func (s *funcSuite) SetUpTest(c *gc.C) { + s.BaseSuite.SetUpTest(c) + + s.expected = cloudimagemetadata.Metadata{ + cloudimagemetadata.MetadataAttributes{ + Stream: "released", + Source: cloudimagemetadata.Custom, + }, + "", + } +} + +var _ = gc.Suite(&funcSuite{}) + +func (s *funcSuite) TestParseMetadataSourcePanic(c *gc.C) { + m := func() { imagemetadata.ParseMetadataFromParams(params.CloudImageMetadata{}) } + c.Assert(m, gc.PanicMatches, `unknown cloud image metadata source ""`) +} + +func (s *funcSuite) TestParseMetadataCustom(c *gc.C) { + m := imagemetadata.ParseMetadataFromParams(params.CloudImageMetadata{Source: "custom"}) + c.Assert(m, gc.DeepEquals, s.expected) + + m = imagemetadata.ParseMetadataFromParams(params.CloudImageMetadata{Source: "CusTOM"}) + c.Assert(m, gc.DeepEquals, s.expected) +} + +func (s *funcSuite) TestParseMetadataPublic(c *gc.C) { + s.expected.Source = cloudimagemetadata.Public + + m := imagemetadata.ParseMetadataFromParams(params.CloudImageMetadata{Source: "public"}) + c.Assert(m, gc.DeepEquals, s.expected) + + m = imagemetadata.ParseMetadataFromParams(params.CloudImageMetadata{Source: "PubLic"}) + c.Assert(m, gc.DeepEquals, s.expected) +} + +func (s *funcSuite) TestParseMetadataAnyStream(c *gc.C) { + stream := "happy stream" + s.expected.Stream = stream + + m := imagemetadata.ParseMetadataFromParams(params.CloudImageMetadata{ + Source: "custom", + Stream: stream, + }) + c.Assert(m, gc.DeepEquals, s.expected) +} + +func (s *funcSuite) TestParseMetadataDefaultStream(c *gc.C) { + m := imagemetadata.ParseMetadataFromParams(params.CloudImageMetadata{ + Source: "custom", + }) + c.Assert(m, gc.DeepEquals, s.expected) +} === added file 'src/github.com/juju/juju/apiserver/imagemetadata/metadata.go' --- src/github.com/juju/juju/apiserver/imagemetadata/metadata.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/imagemetadata/metadata.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,141 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package imagemetadata + +import ( + "fmt" + "strings" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/state" + "github.com/juju/juju/state/cloudimagemetadata" +) + +func init() { + common.RegisterStandardFacade("ImageMetadata", 1, NewAPI) +} + +// API is the concrete implementation of the api end point +// for loud image metadata manipulations. +type API struct { + metadata metadataAcess + authorizer common.Authorizer +} + +// createAPI returns a new image metadata API facade. +func createAPI( + st metadataAcess, + resources *common.Resources, + authorizer common.Authorizer, +) (*API, error) { + if !authorizer.AuthClient() && !authorizer.AuthEnvironManager() { + return nil, common.ErrPerm + } + + return &API{ + metadata: st, + authorizer: authorizer, + }, nil +} + +// NewAPI returns a new cloud image metadata API facade. +func NewAPI( + st *state.State, + resources *common.Resources, + authorizer common.Authorizer, +) (*API, error) { + return createAPI(getState(st), resources, authorizer) +} + +// List returns all found cloud image metadata that satisfy +// given filter. +// Returned list contains metadata for custom images first, then public. +func (api *API) List(filter params.ImageMetadataFilter) (params.ListCloudImageMetadataResult, error) { + found, err := api.metadata.FindMetadata(cloudimagemetadata.MetadataFilter{ + Region: filter.Region, + Series: filter.Series, + Arches: filter.Arches, + Stream: filter.Stream, + VirtualType: filter.VirtualType, + RootStorageType: filter.RootStorageType, + }) + if err != nil { + return params.ListCloudImageMetadataResult{}, common.ServerError(err) + } + + var all []params.CloudImageMetadata + addAll := func(ms []cloudimagemetadata.Metadata) { + for _, m := range ms { + all = append(all, parseMetadataToParams(m)) + } + } + + // First return metadata for custom images, then public. + // No other source for cloud images should exist at the moment. + // Once new source is identified, the order of returned metadata + // may need to be changed. + addAll(found[cloudimagemetadata.Custom]) + addAll(found[cloudimagemetadata.Public]) + + return params.ListCloudImageMetadataResult{Result: all}, nil +} + +// Save stores given cloud image metadata. +// It supports bulk calls. +func (api *API) Save(metadata params.MetadataSaveParams) (params.ErrorResults, error) { + all := make([]params.ErrorResult, len(metadata.Metadata)) + for i, one := range metadata.Metadata { + err := api.metadata.SaveMetadata(parseMetadataFromParams(one)) + all[i] = params.ErrorResult{Error: common.ServerError(err)} + } + return params.ErrorResults{Results: all}, nil +} + +func parseMetadataToParams(p cloudimagemetadata.Metadata) params.CloudImageMetadata { + result := params.CloudImageMetadata{ + ImageId: p.ImageId, + Stream: p.Stream, + Region: p.Region, + Series: p.Series, + Arch: p.Arch, + VirtualType: p.VirtualType, + RootStorageType: p.RootStorageType, + RootStorageSize: p.RootStorageSize, + Source: string(p.Source), + } + return result +} + +func parseMetadataFromParams(p params.CloudImageMetadata) cloudimagemetadata.Metadata { + + parseSource := func(s string) cloudimagemetadata.SourceType { + switch cloudimagemetadata.SourceType(strings.ToLower(s)) { + case cloudimagemetadata.Public: + return cloudimagemetadata.Public + case cloudimagemetadata.Custom: + return cloudimagemetadata.Custom + default: + panic(fmt.Sprintf("unknown cloud image metadata source %q", s)) + } + } + + result := cloudimagemetadata.Metadata{ + cloudimagemetadata.MetadataAttributes{ + Stream: p.Stream, + Region: p.Region, + Series: p.Series, + Arch: p.Arch, + VirtualType: p.VirtualType, + RootStorageType: p.RootStorageType, + RootStorageSize: p.RootStorageSize, + Source: parseSource(p.Source), + }, + p.ImageId, + } + if p.Stream == "" { + result.Stream = "released" + } + return result +} === added file 'src/github.com/juju/juju/apiserver/imagemetadata/metadata_test.go' --- src/github.com/juju/juju/apiserver/imagemetadata/metadata_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/imagemetadata/metadata_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,130 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package imagemetadata_test + +import ( + "github.com/juju/errors" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/state/cloudimagemetadata" +) + +type metadataSuite struct { + baseImageMetadataSuite +} + +var _ = gc.Suite(&metadataSuite{}) + +func (s *metadataSuite) TestFindNil(c *gc.C) { + found, err := s.api.List(params.ImageMetadataFilter{}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(found.Result, gc.HasLen, 0) + s.assertCalls(c, []string{findMetadata}) +} + +func (s *metadataSuite) TestFindEmpty(c *gc.C) { + s.state.findMetadata = func(f cloudimagemetadata.MetadataFilter) (map[cloudimagemetadata.SourceType][]cloudimagemetadata.Metadata, error) { + s.calls = append(s.calls, findMetadata) + return map[cloudimagemetadata.SourceType][]cloudimagemetadata.Metadata{}, nil + } + + found, err := s.api.List(params.ImageMetadataFilter{}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(found.Result, gc.HasLen, 0) + s.assertCalls(c, []string{findMetadata}) +} + +func (s *metadataSuite) TestFindEmptyGroups(c *gc.C) { + s.state.findMetadata = func(f cloudimagemetadata.MetadataFilter) (map[cloudimagemetadata.SourceType][]cloudimagemetadata.Metadata, error) { + s.calls = append(s.calls, findMetadata) + return map[cloudimagemetadata.SourceType][]cloudimagemetadata.Metadata{ + cloudimagemetadata.Public: []cloudimagemetadata.Metadata{}, + cloudimagemetadata.Custom: []cloudimagemetadata.Metadata{}, + }, nil + } + + found, err := s.api.List(params.ImageMetadataFilter{}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(found.Result, gc.HasLen, 0) + s.assertCalls(c, []string{findMetadata}) +} + +func (s *metadataSuite) TestFindError(c *gc.C) { + msg := "find error" + s.state.findMetadata = func(f cloudimagemetadata.MetadataFilter) (map[cloudimagemetadata.SourceType][]cloudimagemetadata.Metadata, error) { + s.calls = append(s.calls, findMetadata) + return nil, errors.New(msg) + } + + found, err := s.api.List(params.ImageMetadataFilter{}) + c.Assert(err, gc.ErrorMatches, msg) + c.Assert(found.Result, gc.HasLen, 0) + s.assertCalls(c, []string{findMetadata}) +} + +func (s *metadataSuite) TestFindOrder(c *gc.C) { + customImageId := "custom1" + customImageId2 := "custom2" + customImageId3 := "custom3" + publicImageId := "public1" + + s.state.findMetadata = func(f cloudimagemetadata.MetadataFilter) (map[cloudimagemetadata.SourceType][]cloudimagemetadata.Metadata, error) { + s.calls = append(s.calls, findMetadata) + return map[cloudimagemetadata.SourceType][]cloudimagemetadata.Metadata{ + cloudimagemetadata.Public: []cloudimagemetadata.Metadata{ + cloudimagemetadata.Metadata{ImageId: publicImageId}, + }, + cloudimagemetadata.Custom: []cloudimagemetadata.Metadata{ + cloudimagemetadata.Metadata{ImageId: customImageId}, + cloudimagemetadata.Metadata{ImageId: customImageId2}, + cloudimagemetadata.Metadata{ImageId: customImageId3}, + }, + }, + nil + } + + found, err := s.api.List(params.ImageMetadataFilter{}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(found.Result, gc.HasLen, 4) + c.Assert(found.Result, jc.SameContents, []params.CloudImageMetadata{ + params.CloudImageMetadata{ImageId: customImageId}, + params.CloudImageMetadata{ImageId: customImageId2}, + params.CloudImageMetadata{ImageId: customImageId3}, + params.CloudImageMetadata{ImageId: publicImageId}, + }) + s.assertCalls(c, []string{findMetadata}) +} + +func (s *metadataSuite) TestSaveEmpty(c *gc.C) { + 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, []string{}) +} + +func (s *metadataSuite) TestSave(c *gc.C) { + m := params.CloudImageMetadata{ + Source: "custom", + } + msg := "save error" + + s.state.saveMetadata = func(m cloudimagemetadata.Metadata) error { + s.calls = append(s.calls, saveMetadata) + if len(s.calls) == 1 { + // don't err on first call + return nil + } + return errors.New(msg) + } + + errs, err := s.api.Save(params.MetadataSaveParams{Metadata: []params.CloudImageMetadata{m, m}}) + c.Assert(err, jc.ErrorIsNil) + 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, []string{saveMetadata, saveMetadata}) +} === added file 'src/github.com/juju/juju/apiserver/imagemetadata/package_test.go' --- src/github.com/juju/juju/apiserver/imagemetadata/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/imagemetadata/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,82 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package imagemetadata_test + +import ( + stdtesting "testing" + + "github.com/juju/names" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/imagemetadata" + "github.com/juju/juju/apiserver/testing" + "github.com/juju/juju/state/cloudimagemetadata" + coretesting "github.com/juju/juju/testing" +) + +func TestAll(t *stdtesting.T) { + gc.TestingT(t) +} + +type baseImageMetadataSuite struct { + coretesting.BaseSuite + + resources *common.Resources + authorizer testing.FakeAuthorizer + + api *imagemetadata.API + state *mockState + + calls []string +} + +func (s *baseImageMetadataSuite) SetUpTest(c *gc.C) { + s.BaseSuite.SetUpTest(c) + s.resources = common.NewResources() + s.authorizer = testing.FakeAuthorizer{names.NewUserTag("testuser"), true} + + s.calls = []string{} + s.state = s.constructState() + + var err error + s.api, err = imagemetadata.CreateAPI(s.state, s.resources, s.authorizer) + c.Assert(err, jc.ErrorIsNil) +} + +func (s *baseImageMetadataSuite) assertCalls(c *gc.C, expectedCalls []string) { + c.Assert(s.calls, jc.SameContents, expectedCalls) +} + +const ( + findMetadata = "findMetadata" + saveMetadata = "saveMetadata" +) + +func (s *baseImageMetadataSuite) constructState() *mockState { + return &mockState{ + findMetadata: func(f cloudimagemetadata.MetadataFilter) (map[cloudimagemetadata.SourceType][]cloudimagemetadata.Metadata, error) { + s.calls = append(s.calls, findMetadata) + return nil, nil + }, + saveMetadata: func(m cloudimagemetadata.Metadata) error { + s.calls = append(s.calls, saveMetadata) + return nil + }, + } +} + +type mockState struct { + findMetadata func(f cloudimagemetadata.MetadataFilter) (map[cloudimagemetadata.SourceType][]cloudimagemetadata.Metadata, error) + saveMetadata func(m cloudimagemetadata.Metadata) error +} + +func (st *mockState) FindMetadata(f cloudimagemetadata.MetadataFilter) (map[cloudimagemetadata.SourceType][]cloudimagemetadata.Metadata, error) { + return st.findMetadata(f) +} + +func (st *mockState) SaveMetadata(m cloudimagemetadata.Metadata) error { + return st.saveMetadata(m) +} === added file 'src/github.com/juju/juju/apiserver/imagemetadata/state.go' --- src/github.com/juju/juju/apiserver/imagemetadata/state.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/imagemetadata/state.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,30 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package imagemetadata + +import ( + "github.com/juju/juju/state" + "github.com/juju/juju/state/cloudimagemetadata" +) + +type metadataAcess interface { + FindMetadata(cloudimagemetadata.MetadataFilter) (map[cloudimagemetadata.SourceType][]cloudimagemetadata.Metadata, error) + SaveMetadata(cloudimagemetadata.Metadata) error +} + +var getState = func(st *state.State) metadataAcess { + return stateShim{st} +} + +type stateShim struct { + *state.State +} + +func (s stateShim) FindMetadata(f cloudimagemetadata.MetadataFilter) (map[cloudimagemetadata.SourceType][]cloudimagemetadata.Metadata, error) { + return s.State.CloudImageMetadataStorage.FindMetadata(f) +} + +func (s stateShim) SaveMetadata(m cloudimagemetadata.Metadata) error { + return s.State.CloudImageMetadataStorage.SaveMetadata(m) +} === modified file 'src/github.com/juju/juju/apiserver/images.go' --- src/github.com/juju/juju/apiserver/images.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/images.go 2015-10-23 18:29:32 +0000 @@ -31,6 +31,7 @@ type imagesDownloadHandler struct { httpHandler dataDir string + state *state.State } func (h *imagesDownloadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -39,7 +40,6 @@ h.sendError(w, http.StatusNotFound, err.Error()) return } - defer stateWrapper.cleanup() switch r.Method { case "GET": err := h.processGet(r, w, stateWrapper.state) @@ -138,7 +138,11 @@ // fetchAndCacheLxcImage fetches an lxc image tarball from http://cloud-images.ubuntu.com // and caches it in the state blobstore. func (h *imagesDownloadHandler) fetchAndCacheLxcImage(storage imagestorage.Storage, envuuid, series, arch string) error { - imageURL, err := container.ImageDownloadURL(instance.LXC, series, arch) + cfg, err := h.state.EnvironConfig() + if err != nil { + return errors.Trace(err) + } + imageURL, err := container.ImageDownloadURL(instance.LXC, series, arch, cfg.CloudImageBaseURL()) if err != nil { return errors.Annotatef(err, "cannot determine LXC image URL: %v", err) } === added directory 'src/github.com/juju/juju/apiserver/instancepoller' === added file 'src/github.com/juju/juju/apiserver/instancepoller/export_test.go' --- src/github.com/juju/juju/apiserver/instancepoller/export_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/instancepoller/export_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,18 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package instancepoller + +import ( + "github.com/juju/juju/state" +) + +type Patcher interface { + PatchValue(ptr, value interface{}) +} + +func PatchState(p Patcher, st StateInterface) { + p.PatchValue(&getState, func(*state.State) StateInterface { + return st + }) +} === added file 'src/github.com/juju/juju/apiserver/instancepoller/instancepoller.go' --- src/github.com/juju/juju/apiserver/instancepoller/instancepoller.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/instancepoller/instancepoller.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,215 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package instancepoller + +import ( + "fmt" + + "github.com/juju/loggo" + "github.com/juju/names" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/state" +) + +func init() { + common.RegisterStandardFacade("InstancePoller", 1, NewInstancePollerAPI) +} + +var logger = loggo.GetLogger("juju.apiserver.instancepoller") + +// InstancePollerAPI provides access to the InstancePoller API facade. +type InstancePollerAPI struct { + *common.LifeGetter + *common.EnvironWatcher + *common.EnvironMachinesWatcher + *common.InstanceIdGetter + *common.StatusGetter + + st StateInterface + resources *common.Resources + authorizer common.Authorizer + accessMachine common.GetAuthFunc +} + +// NewInstancePollerAPI creates a new server-side InstancePoller API +// facade. +func NewInstancePollerAPI( + st *state.State, + resources *common.Resources, + authorizer common.Authorizer, +) (*InstancePollerAPI, error) { + + if !authorizer.AuthEnvironManager() { + // InstancePoller must run as environment manager. + return nil, common.ErrPerm + } + accessMachine := common.AuthFuncForTagKind(names.MachineTagKind) + sti := getState(st) + + // Life() is supported for machines. + lifeGetter := common.NewLifeGetter( + sti, + accessMachine, + ) + // EnvironConfig() and WatchForEnvironConfigChanges() are allowed + // with unrestriced access. + environWatcher := common.NewEnvironWatcher( + sti, + resources, + authorizer, + ) + // WatchEnvironMachines() is allowed with unrestricted access. + machinesWatcher := common.NewEnvironMachinesWatcher( + sti, + resources, + authorizer, + ) + // InstanceId() is supported for machines. + instanceIdGetter := common.NewInstanceIdGetter( + sti, + accessMachine, + ) + // Status() is supported for machines. + statusGetter := common.NewStatusGetter( + sti, + accessMachine, + ) + + return &InstancePollerAPI{ + LifeGetter: lifeGetter, + EnvironWatcher: environWatcher, + EnvironMachinesWatcher: machinesWatcher, + InstanceIdGetter: instanceIdGetter, + StatusGetter: statusGetter, + st: sti, + resources: resources, + authorizer: authorizer, + accessMachine: accessMachine, + }, nil +} + +func (a *InstancePollerAPI) getOneMachine(tag string, canAccess common.AuthFunc) (StateMachine, error) { + machineTag, err := names.ParseMachineTag(tag) + if err != nil { + return nil, err + } + if !canAccess(machineTag) { + return nil, common.ErrPerm + } + entity, err := a.st.FindEntity(machineTag) + if err != nil { + return nil, err + } + machine, ok := entity.(StateMachine) + if !ok { + return nil, common.NotSupportedError( + machineTag, fmt.Sprintf("expected machine, got %T", entity), + ) + } + return machine, nil +} + +// ProviderAddresses returns the list of all known provider addresses +// for each given entity. Only machine tags are accepted. +func (a *InstancePollerAPI) ProviderAddresses(args params.Entities) (params.MachineAddressesResults, error) { + result := params.MachineAddressesResults{ + Results: make([]params.MachineAddressesResult, len(args.Entities)), + } + canAccess, err := a.accessMachine() + if err != nil { + return result, err + } + for i, arg := range args.Entities { + machine, err := a.getOneMachine(arg.Tag, canAccess) + if err == nil { + addrs := machine.ProviderAddresses() + result.Results[i].Addresses = params.FromNetworkAddresses(addrs) + } + result.Results[i].Error = common.ServerError(err) + } + return result, nil +} + +// SetProviderAddresses updates the list of known provider addresses +// for each given entity. Only machine tags are accepted. +func (a *InstancePollerAPI) SetProviderAddresses(args params.SetMachinesAddresses) (params.ErrorResults, error) { + result := params.ErrorResults{ + Results: make([]params.ErrorResult, len(args.MachineAddresses)), + } + canAccess, err := a.accessMachine() + if err != nil { + return result, err + } + for i, arg := range args.MachineAddresses { + machine, err := a.getOneMachine(arg.Tag, canAccess) + if err == nil { + addrsToSet := params.NetworkAddresses(arg.Addresses) + err = machine.SetProviderAddresses(addrsToSet...) + } + result.Results[i].Error = common.ServerError(err) + } + return result, nil +} + +// InstanceStatus returns the instance status for each given entity. +// Only machine tags are accepted. +func (a *InstancePollerAPI) InstanceStatus(args params.Entities) (params.StringResults, error) { + result := params.StringResults{ + Results: make([]params.StringResult, len(args.Entities)), + } + canAccess, err := a.accessMachine() + if err != nil { + return result, err + } + for i, arg := range args.Entities { + machine, err := a.getOneMachine(arg.Tag, canAccess) + if err == nil { + result.Results[i].Result, err = machine.InstanceStatus() + } + result.Results[i].Error = common.ServerError(err) + } + return result, nil +} + +// SetInstanceStatus updates the instance status for each given +// entity. Only machine tags are accepted. +func (a *InstancePollerAPI) SetInstanceStatus(args params.SetInstancesStatus) (params.ErrorResults, error) { + result := params.ErrorResults{ + Results: make([]params.ErrorResult, len(args.Entities)), + } + canAccess, err := a.accessMachine() + if err != nil { + return result, err + } + for i, arg := range args.Entities { + machine, err := a.getOneMachine(arg.Tag, canAccess) + if err == nil { + err = machine.SetInstanceStatus(arg.Status) + } + result.Results[i].Error = common.ServerError(err) + } + return result, nil +} + +// AreManuallyProvisioned returns whether each given entity is +// manually provisioned or not. Only machine tags are accepted. +func (a *InstancePollerAPI) AreManuallyProvisioned(args params.Entities) (params.BoolResults, error) { + result := params.BoolResults{ + Results: make([]params.BoolResult, len(args.Entities)), + } + canAccess, err := a.accessMachine() + if err != nil { + return result, err + } + for i, arg := range args.Entities { + machine, err := a.getOneMachine(arg.Tag, canAccess) + if err == nil { + result.Results[i].Result, err = machine.IsManual() + } + result.Results[i].Error = common.ServerError(err) + } + return result, nil +} === added file 'src/github.com/juju/juju/apiserver/instancepoller/instancepoller_test.go' --- src/github.com/juju/juju/apiserver/instancepoller/instancepoller_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/instancepoller/instancepoller_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,697 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package instancepoller_test + +import ( + "time" + + "github.com/juju/errors" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/instancepoller" + "github.com/juju/juju/apiserver/params" + apiservertesting "github.com/juju/juju/apiserver/testing" + "github.com/juju/juju/network" + "github.com/juju/juju/state" + statetesting "github.com/juju/juju/state/testing" + coretesting "github.com/juju/juju/testing" +) + +type InstancePollerSuite struct { + coretesting.BaseSuite + + st *mockState + api *instancepoller.InstancePollerAPI + authoriser apiservertesting.FakeAuthorizer + resources *common.Resources + + machineEntities params.Entities + machineErrorResults params.ErrorResults + + mixedEntities params.Entities + mixedErrorResults params.ErrorResults +} + +var _ = gc.Suite(&InstancePollerSuite{}) + +func (s *InstancePollerSuite) SetUpTest(c *gc.C) { + s.BaseSuite.SetUpTest(c) + + s.authoriser = apiservertesting.FakeAuthorizer{ + EnvironManager: true, + } + s.resources = common.NewResources() + s.AddCleanup(func(*gc.C) { s.resources.StopAll() }) + + s.st = NewMockState() + instancepoller.PatchState(s, s.st) + + var err error + s.api, err = instancepoller.NewInstancePollerAPI(nil, s.resources, s.authoriser) + c.Assert(err, jc.ErrorIsNil) + + s.machineEntities = params.Entities{ + Entities: []params.Entity{ + {Tag: "machine-1"}, + {Tag: "machine-2"}, + {Tag: "machine-3"}, + }} + s.machineErrorResults = params.ErrorResults{ + Results: []params.ErrorResult{ + {Error: apiservertesting.ServerError("pow!")}, + {Error: apiservertesting.ServerError("FAIL")}, + {Error: apiservertesting.NotProvisionedError("42")}, + }} + + s.mixedEntities = params.Entities{ + Entities: []params.Entity{ + {Tag: "machine-1"}, + {Tag: "machine-2"}, + {Tag: "machine-42"}, + {Tag: "service-unknown"}, + {Tag: "invalid-tag"}, + {Tag: "unit-missing-1"}, + {Tag: ""}, + {Tag: "42"}, + }} + s.mixedErrorResults = params.ErrorResults{ + Results: []params.ErrorResult{ + {Error: nil}, + {Error: nil}, + {Error: apiservertesting.NotFoundError("machine 42")}, + {Error: apiservertesting.ServerError(`"service-unknown" is not a valid machine tag`)}, + {Error: apiservertesting.ServerError(`"invalid-tag" is not a valid tag`)}, + {Error: apiservertesting.ServerError(`"unit-missing-1" is not a valid machine tag`)}, + {Error: apiservertesting.ServerError(`"" is not a valid tag`)}, + {Error: apiservertesting.ServerError(`"42" is not a valid tag`)}, + }} +} + +func (s *InstancePollerSuite) TestNewInstancePollerAPIRequiresEnvironManager(c *gc.C) { + anAuthoriser := s.authoriser + anAuthoriser.EnvironManager = false + api, err := instancepoller.NewInstancePollerAPI(nil, s.resources, anAuthoriser) + c.Assert(api, gc.IsNil) + c.Assert(err, gc.ErrorMatches, "permission denied") +} + +func (s *InstancePollerSuite) TestEnvironConfigFailure(c *gc.C) { + s.st.SetErrors(errors.New("boom")) + + result, err := s.api.EnvironConfig() + c.Assert(err, gc.ErrorMatches, "boom") + c.Assert(result, jc.DeepEquals, params.EnvironConfigResult{}) + + s.st.CheckCallNames(c, "EnvironConfig") +} + +func (s *InstancePollerSuite) TestEnvironConfigSuccess(c *gc.C) { + envConfig := coretesting.EnvironConfig(c) + s.st.SetConfig(c, envConfig) + + result, err := s.api.EnvironConfig() + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, params.EnvironConfigResult{ + Config: envConfig.AllAttrs(), + }) + + s.st.CheckCallNames(c, "EnvironConfig") +} + +func (s *InstancePollerSuite) TestWatchForEnvironConfigChangesFailure(c *gc.C) { + // Force the Changes() method of the mock watcher to return a + // closed channel by setting an error. + s.st.SetErrors(errors.New("boom")) + + result, err := s.api.WatchForEnvironConfigChanges() + c.Assert(err, gc.ErrorMatches, "boom") + c.Assert(result, jc.DeepEquals, params.NotifyWatchResult{}) + + c.Assert(s.resources.Count(), gc.Equals, 0) // no watcher registered + s.st.CheckCallNames(c, "WatchForEnvironConfigChanges") +} + +func (s *InstancePollerSuite) TestWatchForEnvironConfigChangesSuccess(c *gc.C) { + result, err := s.api.WatchForEnvironConfigChanges() + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, params.NotifyWatchResult{ + Error: nil, NotifyWatcherId: "1", + }) + + // Verify the watcher resource was registered. + c.Assert(s.resources.Count(), gc.Equals, 1) + resource := s.resources.Get("1") + defer statetesting.AssertStop(c, resource) + + // Check that the watcher has consumed the initial event + wc := statetesting.NewNotifyWatcherC(c, s.st, resource.(state.NotifyWatcher)) + wc.AssertNoChange() + + s.st.CheckCallNames(c, "WatchForEnvironConfigChanges") + + // Try changing the config to verify an event is reported. + envConfig := coretesting.EnvironConfig(c) + s.st.SetConfig(c, envConfig) + wc.AssertOneChange() +} + +func (s *InstancePollerSuite) TestWatchEnvironMachinesFailure(c *gc.C) { + // Force the Changes() method of the mock watcher to return a + // closed channel by setting an error. + s.st.SetErrors(errors.Errorf("boom")) + + result, err := s.api.WatchEnvironMachines() + c.Assert(err, gc.ErrorMatches, "cannot obtain initial environment machines: boom") + c.Assert(result, jc.DeepEquals, params.StringsWatchResult{}) + + c.Assert(s.resources.Count(), gc.Equals, 0) // no watcher registered + s.st.CheckCallNames(c, "WatchEnvironMachines") +} + +func (s *InstancePollerSuite) TestWatchEnvironMachinesSuccess(c *gc.C) { + // Add a couple of machines. + s.st.SetMachineInfo(c, machineInfo{id: "2"}) + s.st.SetMachineInfo(c, machineInfo{id: "1"}) + + expectedResult := params.StringsWatchResult{ + Error: nil, + StringsWatcherId: "1", + Changes: []string{"1", "2"}, // initial event (sorted ids) + } + result, err := s.api.WatchEnvironMachines() + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, expectedResult) + + // Verify the watcher resource was registered. + c.Assert(s.resources.Count(), gc.Equals, 1) + resource1 := s.resources.Get("1") + defer func() { + if resource1 != nil { + statetesting.AssertStop(c, resource1) + } + }() + + // Check that the watcher has consumed the initial event + wc1 := statetesting.NewStringsWatcherC(c, s.st, resource1.(state.StringsWatcher)) + wc1.AssertNoChange() + + s.st.CheckCallNames(c, "WatchEnvironMachines") + + // Add another watcher to verify events coalescence. + result, err = s.api.WatchEnvironMachines() + c.Assert(err, jc.ErrorIsNil) + expectedResult.StringsWatcherId = "2" + c.Assert(result, jc.DeepEquals, expectedResult) + s.st.CheckCallNames(c, "WatchEnvironMachines", "WatchEnvironMachines") + c.Assert(s.resources.Count(), gc.Equals, 2) + resource2 := s.resources.Get("2") + defer statetesting.AssertStop(c, resource2) + wc2 := statetesting.NewStringsWatcherC(c, s.st, resource2.(state.StringsWatcher)) + wc2.AssertNoChange() + + // Remove machine 1, check it's reported. + s.st.RemoveMachine(c, "1") + wc1.AssertChangeInSingleEvent("1") + + // Make separate changes, check they're combined. + s.st.SetMachineInfo(c, machineInfo{id: "2", life: state.Dying}) + s.st.SetMachineInfo(c, machineInfo{id: "3"}) + s.st.RemoveMachine(c, "42") // ignored + wc1.AssertChangeInSingleEvent("2", "3") + wc2.AssertChangeInSingleEvent("1", "2", "3") + + // Stop the first watcher and assert its changes chan is closed. + c.Assert(resource1.Stop(), jc.ErrorIsNil) + wc1.AssertClosed() + resource1 = nil +} + +func (s *InstancePollerSuite) TestLifeSuccess(c *gc.C) { + s.st.SetMachineInfo(c, machineInfo{id: "1", life: state.Alive}) + s.st.SetMachineInfo(c, machineInfo{id: "2", life: state.Dying}) + + result, err := s.api.Life(s.mixedEntities) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, params.LifeResults{ + Results: []params.LifeResult{ + {Life: params.Alive}, + {Life: params.Dying}, + {Error: apiservertesting.NotFoundError("machine 42")}, + {Error: apiservertesting.ErrUnauthorized}, + {Error: apiservertesting.ErrUnauthorized}, + {Error: apiservertesting.ErrUnauthorized}, + {Error: apiservertesting.ErrUnauthorized}, + {Error: apiservertesting.ErrUnauthorized}, + }}, + ) + + s.st.CheckFindEntityCall(c, 0, "1") + s.st.CheckCall(c, 1, "Life") + s.st.CheckFindEntityCall(c, 2, "2") + s.st.CheckCall(c, 3, "Life") + s.st.CheckFindEntityCall(c, 4, "42") +} + +func (s *InstancePollerSuite) TestLifeFailure(c *gc.C) { + s.st.SetErrors( + errors.New("pow!"), // m1 := FindEntity("1"); Life not called + nil, // m2 := FindEntity("2") + errors.New("FAIL"), // m2.Life() - unused + errors.NotProvisionedf("machine 42"), // FindEntity("3") (ensure wrapping is preserved) + ) + s.st.SetMachineInfo(c, machineInfo{id: "1", life: state.Alive}) + s.st.SetMachineInfo(c, machineInfo{id: "2", life: state.Dead}) + s.st.SetMachineInfo(c, machineInfo{id: "3", life: state.Dying}) + + result, err := s.api.Life(s.machineEntities) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, params.LifeResults{ + Results: []params.LifeResult{ + {Error: apiservertesting.ServerError("pow!")}, + {Life: params.Dead}, + {Error: apiservertesting.NotProvisionedError("42")}, + }}, + ) + + s.st.CheckFindEntityCall(c, 0, "1") + s.st.CheckFindEntityCall(c, 1, "2") + s.st.CheckCall(c, 2, "Life") + s.st.CheckFindEntityCall(c, 3, "3") +} + +func (s *InstancePollerSuite) TestInstanceIdSuccess(c *gc.C) { + s.st.SetMachineInfo(c, machineInfo{id: "1", instanceId: "i-foo"}) + s.st.SetMachineInfo(c, machineInfo{id: "2", instanceId: ""}) + + result, err := s.api.InstanceId(s.mixedEntities) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, params.StringResults{ + Results: []params.StringResult{ + {Result: "i-foo"}, + {Result: ""}, + {Error: apiservertesting.NotFoundError("machine 42")}, + {Error: apiservertesting.ErrUnauthorized}, + {Error: apiservertesting.ErrUnauthorized}, + {Error: apiservertesting.ErrUnauthorized}, + {Error: apiservertesting.ErrUnauthorized}, + {Error: apiservertesting.ErrUnauthorized}, + }}, + ) + + s.st.CheckFindEntityCall(c, 0, "1") + s.st.CheckCall(c, 1, "InstanceId") + s.st.CheckFindEntityCall(c, 2, "2") + s.st.CheckCall(c, 3, "InstanceId") + s.st.CheckFindEntityCall(c, 4, "42") +} + +func (s *InstancePollerSuite) TestInstanceIdFailure(c *gc.C) { + s.st.SetErrors( + errors.New("pow!"), // m1 := FindEntity("1"); InstanceId not called + nil, // m2 := FindEntity("2") + errors.New("FAIL"), // m2.InstanceId() + errors.NotProvisionedf("machine 42"), // FindEntity("3") (ensure wrapping is preserved) + ) + s.st.SetMachineInfo(c, machineInfo{id: "1", instanceId: ""}) + s.st.SetMachineInfo(c, machineInfo{id: "2", instanceId: "i-bar"}) + + result, err := s.api.InstanceId(s.machineEntities) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, params.StringResults{ + Results: []params.StringResult{ + {Error: apiservertesting.ServerError("pow!")}, + {Error: apiservertesting.ServerError("FAIL")}, + {Error: apiservertesting.NotProvisionedError("42")}, + }}, + ) + + s.st.CheckFindEntityCall(c, 0, "1") + s.st.CheckFindEntityCall(c, 1, "2") + s.st.CheckCall(c, 2, "InstanceId") + s.st.CheckFindEntityCall(c, 3, "3") +} + +func (s *InstancePollerSuite) TestStatusSuccess(c *gc.C) { + now := time.Now() + s1 := state.StatusInfo{ + Status: state.StatusError, + Message: "not really", + Data: map[string]interface{}{ + "price": 4.2, + "bool": false, + "bar": []string{"a", "b"}, + }, + Since: &now, + } + s2 := state.StatusInfo{} + s.st.SetMachineInfo(c, machineInfo{id: "1", status: s1}) + s.st.SetMachineInfo(c, machineInfo{id: "2", status: s2}) + + result, err := s.api.Status(s.mixedEntities) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, params.StatusResults{ + Results: []params.StatusResult{ + { + Status: params.StatusError, + Info: s1.Message, + Data: s1.Data, + Since: s1.Since, + }, + {Status: "", Info: "", Data: nil, Since: nil}, + {Error: apiservertesting.NotFoundError("machine 42")}, + {Error: apiservertesting.ErrUnauthorized}, + {Error: apiservertesting.ServerError(`"invalid-tag" is not a valid tag`)}, + {Error: apiservertesting.ErrUnauthorized}, + {Error: apiservertesting.ServerError(`"" is not a valid tag`)}, + {Error: apiservertesting.ServerError(`"42" is not a valid tag`)}, + }}, + ) + + s.st.CheckFindEntityCall(c, 0, "1") + s.st.CheckCall(c, 1, "Status") + s.st.CheckFindEntityCall(c, 2, "2") + s.st.CheckCall(c, 3, "Status") + s.st.CheckFindEntityCall(c, 4, "42") +} + +func (s *InstancePollerSuite) TestStatusFailure(c *gc.C) { + s.st.SetErrors( + errors.New("pow!"), // m1 := FindEntity("1"); Status not called + nil, // m2 := FindEntity("2") + errors.New("FAIL"), // m2.Status() + errors.NotProvisionedf("machine 42"), // FindEntity("3") (ensure wrapping is preserved) + ) + s.st.SetMachineInfo(c, machineInfo{id: "1"}) + s.st.SetMachineInfo(c, machineInfo{id: "2"}) + + result, err := s.api.Status(s.machineEntities) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, params.StatusResults{ + Results: []params.StatusResult{ + {Error: apiservertesting.ServerError("pow!")}, + {Error: apiservertesting.ServerError("FAIL")}, + {Error: apiservertesting.NotProvisionedError("42")}, + }}, + ) + + s.st.CheckFindEntityCall(c, 0, "1") + s.st.CheckFindEntityCall(c, 1, "2") + s.st.CheckCall(c, 2, "Status") + s.st.CheckFindEntityCall(c, 3, "3") +} + +func (s *InstancePollerSuite) TestProviderAddressesSuccess(c *gc.C) { + addrs := network.NewAddresses("0.1.2.3", "127.0.0.1", "8.8.8.8") + expectedAddresses := params.FromNetworkAddresses(addrs) + s.st.SetMachineInfo(c, machineInfo{id: "1", providerAddresses: addrs}) + s.st.SetMachineInfo(c, machineInfo{id: "2", providerAddresses: nil}) + + result, err := s.api.ProviderAddresses(s.mixedEntities) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, params.MachineAddressesResults{ + Results: []params.MachineAddressesResult{ + {Addresses: expectedAddresses}, + {Addresses: nil}, + {Error: apiservertesting.NotFoundError("machine 42")}, + {Error: apiservertesting.ServerError(`"service-unknown" is not a valid machine tag`)}, + {Error: apiservertesting.ServerError(`"invalid-tag" is not a valid tag`)}, + {Error: apiservertesting.ServerError(`"unit-missing-1" is not a valid machine tag`)}, + {Error: apiservertesting.ServerError(`"" is not a valid tag`)}, + {Error: apiservertesting.ServerError(`"42" is not a valid tag`)}, + }}, + ) + + s.st.CheckFindEntityCall(c, 0, "1") + s.st.CheckCall(c, 1, "ProviderAddresses") + s.st.CheckFindEntityCall(c, 2, "2") + s.st.CheckCall(c, 3, "ProviderAddresses") + s.st.CheckFindEntityCall(c, 4, "42") +} + +func (s *InstancePollerSuite) TestProviderAddressesFailure(c *gc.C) { + s.st.SetErrors( + errors.New("pow!"), // m1 := FindEntity("1") + nil, // m2 := FindEntity("2") + errors.New("FAIL"), // m2.ProviderAddresses()- unused + errors.NotProvisionedf("machine 42"), // FindEntity("3") (ensure wrapping is preserved) + ) + s.st.SetMachineInfo(c, machineInfo{id: "1"}) + s.st.SetMachineInfo(c, machineInfo{id: "2"}) + + result, err := s.api.ProviderAddresses(s.machineEntities) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, params.MachineAddressesResults{ + Results: []params.MachineAddressesResult{ + {Error: apiservertesting.ServerError("pow!")}, + {Addresses: nil}, + {Error: apiservertesting.NotProvisionedError("42")}, + }}, + ) + + s.st.CheckFindEntityCall(c, 0, "1") + s.st.CheckFindEntityCall(c, 1, "2") + s.st.CheckCall(c, 2, "ProviderAddresses") + s.st.CheckFindEntityCall(c, 3, "3") +} + +func (s *InstancePollerSuite) TestSetProviderAddressesSuccess(c *gc.C) { + oldAddrs := network.NewAddresses("0.1.2.3", "127.0.0.1", "8.8.8.8") + newAddrs := network.NewAddresses("1.2.3.4", "8.4.4.8", "2001:db8::") + s.st.SetMachineInfo(c, machineInfo{id: "1", providerAddresses: oldAddrs}) + s.st.SetMachineInfo(c, machineInfo{id: "2", providerAddresses: nil}) + + result, err := s.api.SetProviderAddresses(params.SetMachinesAddresses{ + MachineAddresses: []params.MachineAddresses{ + {Tag: "machine-1", Addresses: nil}, + {Tag: "machine-2", Addresses: params.FromNetworkAddresses(newAddrs)}, + {Tag: "machine-42"}, + {Tag: "service-unknown"}, + {Tag: "invalid-tag"}, + {Tag: "unit-missing-1"}, + {Tag: ""}, + {Tag: "42"}, + }}, + ) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, s.mixedErrorResults) + + s.st.CheckFindEntityCall(c, 0, "1") + s.st.CheckSetProviderAddressesCall(c, 1, []network.Address{}) + s.st.CheckFindEntityCall(c, 2, "2") + s.st.CheckSetProviderAddressesCall(c, 3, newAddrs) + s.st.CheckFindEntityCall(c, 4, "42") + + // Ensure machines were updated. + machine, err := s.st.Machine("1") + c.Assert(err, jc.ErrorIsNil) + c.Assert(machine.ProviderAddresses(), gc.HasLen, 0) + + machine, err = s.st.Machine("2") + c.Assert(err, jc.ErrorIsNil) + c.Assert(machine.ProviderAddresses(), jc.DeepEquals, newAddrs) +} + +func (s *InstancePollerSuite) TestSetProviderAddressesFailure(c *gc.C) { + s.st.SetErrors( + errors.New("pow!"), // m1 := FindEntity("1") + nil, // m2 := FindEntity("2") + errors.New("FAIL"), // m2.SetProviderAddresses() + errors.NotProvisionedf("machine 42"), // FindEntity("3") (ensure wrapping is preserved) + ) + oldAddrs := network.NewAddresses("0.1.2.3", "127.0.0.1", "8.8.8.8") + newAddrs := network.NewAddresses("1.2.3.4", "8.4.4.8", "2001:db8::") + s.st.SetMachineInfo(c, machineInfo{id: "1", providerAddresses: oldAddrs}) + s.st.SetMachineInfo(c, machineInfo{id: "2", providerAddresses: nil}) + + result, err := s.api.SetProviderAddresses(params.SetMachinesAddresses{ + MachineAddresses: []params.MachineAddresses{ + {Tag: "machine-1", Addresses: nil}, + {Tag: "machine-2", Addresses: params.FromNetworkAddresses(newAddrs)}, + {Tag: "machine-3"}, + }}, + ) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, s.machineErrorResults) + + s.st.CheckFindEntityCall(c, 0, "1") + s.st.CheckFindEntityCall(c, 1, "2") + s.st.CheckSetProviderAddressesCall(c, 2, newAddrs) + s.st.CheckFindEntityCall(c, 3, "3") + + // Ensure machine 2 wasn't updated. + machine, err := s.st.Machine("2") + c.Assert(err, jc.ErrorIsNil) + c.Assert(machine.ProviderAddresses(), gc.HasLen, 0) +} + +func (s *InstancePollerSuite) TestInstanceStatusSuccess(c *gc.C) { + s.st.SetMachineInfo(c, machineInfo{id: "1", instanceStatus: "foo"}) + s.st.SetMachineInfo(c, machineInfo{id: "2", instanceStatus: ""}) + + result, err := s.api.InstanceStatus(s.mixedEntities) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, params.StringResults{ + Results: []params.StringResult{ + {Result: "foo"}, + {Result: ""}, + {Error: apiservertesting.NotFoundError("machine 42")}, + {Error: apiservertesting.ServerError(`"service-unknown" is not a valid machine tag`)}, + {Error: apiservertesting.ServerError(`"invalid-tag" is not a valid tag`)}, + {Error: apiservertesting.ServerError(`"unit-missing-1" is not a valid machine tag`)}, + {Error: apiservertesting.ServerError(`"" is not a valid tag`)}, + {Error: apiservertesting.ServerError(`"42" is not a valid tag`)}, + }}, + ) + + s.st.CheckFindEntityCall(c, 0, "1") + s.st.CheckCall(c, 1, "InstanceStatus") + s.st.CheckFindEntityCall(c, 2, "2") + s.st.CheckCall(c, 3, "InstanceStatus") + s.st.CheckFindEntityCall(c, 4, "42") +} + +func (s *InstancePollerSuite) TestInstanceStatusFailure(c *gc.C) { + s.st.SetErrors( + errors.New("pow!"), // m1 := FindEntity("1") + nil, // m2 := FindEntity("2") + errors.New("FAIL"), // m2.InstanceStatus() + errors.NotProvisionedf("machine 42"), // FindEntity("3") (ensure wrapping is preserved) + ) + s.st.SetMachineInfo(c, machineInfo{id: "1", instanceStatus: "foo"}) + s.st.SetMachineInfo(c, machineInfo{id: "2", instanceStatus: ""}) + + result, err := s.api.InstanceStatus(s.machineEntities) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, params.StringResults{ + Results: []params.StringResult{ + {Error: apiservertesting.ServerError("pow!")}, + {Error: apiservertesting.ServerError("FAIL")}, + {Error: apiservertesting.NotProvisionedError("42")}, + }}, + ) + + s.st.CheckFindEntityCall(c, 0, "1") + s.st.CheckFindEntityCall(c, 1, "2") + s.st.CheckCall(c, 2, "InstanceStatus") + s.st.CheckFindEntityCall(c, 3, "3") +} + +func (s *InstancePollerSuite) TestSetInstanceStatusSuccess(c *gc.C) { + s.st.SetMachineInfo(c, machineInfo{id: "1", instanceStatus: "foo"}) + s.st.SetMachineInfo(c, machineInfo{id: "2", instanceStatus: ""}) + + result, err := s.api.SetInstanceStatus(params.SetInstancesStatus{ + Entities: []params.InstanceStatus{ + {Tag: "machine-1", Status: ""}, + {Tag: "machine-2", Status: "new status"}, + {Tag: "machine-42"}, + {Tag: "service-unknown"}, + {Tag: "invalid-tag"}, + {Tag: "unit-missing-1"}, + {Tag: ""}, + {Tag: "42"}, + }}, + ) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, s.mixedErrorResults) + + s.st.CheckFindEntityCall(c, 0, "1") + s.st.CheckCall(c, 1, "SetInstanceStatus", "") + s.st.CheckFindEntityCall(c, 2, "2") + s.st.CheckCall(c, 3, "SetInstanceStatus", "new status") + s.st.CheckFindEntityCall(c, 4, "42") + + // Ensure machines were updated. + machine, err := s.st.Machine("1") + c.Assert(err, jc.ErrorIsNil) + setStatus, err := machine.InstanceStatus() + c.Assert(err, jc.ErrorIsNil) + c.Assert(setStatus, gc.Equals, "") + + machine, err = s.st.Machine("2") + c.Assert(err, jc.ErrorIsNil) + setStatus, err = machine.InstanceStatus() + c.Assert(err, jc.ErrorIsNil) + c.Assert(setStatus, gc.Equals, "new status") +} + +func (s *InstancePollerSuite) TestSetInstanceStatusFailure(c *gc.C) { + s.st.SetErrors( + errors.New("pow!"), // m1 := FindEntity("1") + nil, // m2 := FindEntity("2") + errors.New("FAIL"), // m2.SetInstanceStatus() + errors.NotProvisionedf("machine 42"), // FindEntity("3") (ensure wrapping is preserved) + ) + s.st.SetMachineInfo(c, machineInfo{id: "1", instanceStatus: "foo"}) + s.st.SetMachineInfo(c, machineInfo{id: "2", instanceStatus: ""}) + + result, err := s.api.SetInstanceStatus(params.SetInstancesStatus{ + Entities: []params.InstanceStatus{ + {Tag: "machine-1", Status: "new"}, + {Tag: "machine-2", Status: "invalid"}, + {Tag: "machine-3", Status: ""}, + }}, + ) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, s.machineErrorResults) + + s.st.CheckFindEntityCall(c, 0, "1") + s.st.CheckFindEntityCall(c, 1, "2") + s.st.CheckCall(c, 2, "SetInstanceStatus", "invalid") + s.st.CheckFindEntityCall(c, 3, "3") +} + +func (s *InstancePollerSuite) TestAreManuallyProvisionedSuccess(c *gc.C) { + s.st.SetMachineInfo(c, machineInfo{id: "1", isManual: true}) + s.st.SetMachineInfo(c, machineInfo{id: "2", isManual: false}) + + result, err := s.api.AreManuallyProvisioned(s.mixedEntities) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, params.BoolResults{ + Results: []params.BoolResult{ + {Result: true}, + {Result: false}, + {Error: apiservertesting.NotFoundError("machine 42")}, + {Error: apiservertesting.ServerError(`"service-unknown" is not a valid machine tag`)}, + {Error: apiservertesting.ServerError(`"invalid-tag" is not a valid tag`)}, + {Error: apiservertesting.ServerError(`"unit-missing-1" is not a valid machine tag`)}, + {Error: apiservertesting.ServerError(`"" is not a valid tag`)}, + {Error: apiservertesting.ServerError(`"42" is not a valid tag`)}, + }}, + ) + + s.st.CheckFindEntityCall(c, 0, "1") + s.st.CheckCall(c, 1, "IsManual") + s.st.CheckFindEntityCall(c, 2, "2") + s.st.CheckCall(c, 3, "IsManual") + s.st.CheckFindEntityCall(c, 4, "42") +} + +func (s *InstancePollerSuite) TestAreManuallyProvisionedFailure(c *gc.C) { + s.st.SetErrors( + errors.New("pow!"), // m1 := FindEntity("1") + nil, // m2 := FindEntity("2") + errors.New("FAIL"), // m2.IsManual() + errors.NotProvisionedf("machine 42"), // FindEntity("3") (ensure wrapping is preserved) + ) + s.st.SetMachineInfo(c, machineInfo{id: "1", isManual: true}) + s.st.SetMachineInfo(c, machineInfo{id: "2", isManual: false}) + + result, err := s.api.AreManuallyProvisioned(s.machineEntities) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, params.BoolResults{ + Results: []params.BoolResult{ + {Error: apiservertesting.ServerError("pow!")}, + {Error: apiservertesting.ServerError("FAIL")}, + {Error: apiservertesting.NotProvisionedError("42")}, + }}, + ) + + s.st.CheckFindEntityCall(c, 0, "1") + s.st.CheckFindEntityCall(c, 1, "2") + s.st.CheckCall(c, 2, "IsManual") + s.st.CheckFindEntityCall(c, 3, "3") +} === added file 'src/github.com/juju/juju/apiserver/instancepoller/mock_test.go' --- src/github.com/juju/juju/apiserver/instancepoller/mock_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/instancepoller/mock_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,459 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package instancepoller_test + +import ( + "sort" + "sync" + + "github.com/juju/errors" + "github.com/juju/names" + "github.com/juju/testing" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/instancepoller" + "github.com/juju/juju/environs/config" + "github.com/juju/juju/instance" + "github.com/juju/juju/network" + "github.com/juju/juju/state" +) + +// mockState implements StateInterface and allows inspection of called +// methods. +type mockState struct { + *testing.Stub + + mu sync.Mutex + + configWatchers []*mockConfigWatcher + machinesWatchers []*mockMachinesWatcher + + config *config.Config + machines map[string]*mockMachine +} + +func NewMockState() *mockState { + return &mockState{ + Stub: &testing.Stub{}, + machines: make(map[string]*mockMachine), + } +} + +var _ instancepoller.StateInterface = (*mockState)(nil) + +// CheckFindEntityCall is a helper wrapper aroud +// testing.Stub.CheckCall for FindEntity. +func (m *mockState) CheckFindEntityCall(c *gc.C, index int, machineId string) { + m.CheckCall(c, index, "FindEntity", interface{}(names.NewMachineTag(machineId))) +} + +// CheckSetProviderAddressesCall is a helper wrapper aroud +// testing.Stub.CheckCall for SetProviderAddresses. +func (m *mockState) CheckSetProviderAddressesCall(c *gc.C, index int, addrs []network.Address) { + args := make([]interface{}, len(addrs)) + for i, addr := range addrs { + args[i] = addr + } + m.CheckCall(c, index, "SetProviderAddresses", args...) +} + +// WatchForEnvironConfigChanges implements StateInterface. +func (m *mockState) WatchForEnvironConfigChanges() state.NotifyWatcher { + m.mu.Lock() + defer m.mu.Unlock() + + m.MethodCall(m, "WatchForEnvironConfigChanges") + + w := NewMockConfigWatcher(m.NextErr()) + m.configWatchers = append(m.configWatchers, w) + return w +} + +// EnvironConfig implements StateInterface. +func (m *mockState) EnvironConfig() (*config.Config, error) { + m.mu.Lock() + defer m.mu.Unlock() + + m.MethodCall(m, "EnvironConfig") + + if err := m.NextErr(); err != nil { + return nil, err + } + return m.config, nil +} + +// SetConfig updates the environ config stored internally. Triggers a +// change event for all created config watchers. +func (m *mockState) SetConfig(c *gc.C, newConfig *config.Config) { + m.mu.Lock() + defer m.mu.Unlock() + + m.config = newConfig + + // Notify any watchers for the changes. + for _, w := range m.configWatchers { + w.incoming <- struct{}{} + } +} + +// WatchEnvironMachines implements StateInterface. +func (m *mockState) WatchEnvironMachines() state.StringsWatcher { + m.mu.Lock() + defer m.mu.Unlock() + + m.MethodCall(m, "WatchEnvironMachines") + + ids := make([]string, 0, len(m.machines)) + // Initial event - all machine ids, sorted. + for id := range m.machines { + ids = append(ids, id) + } + sort.Strings(ids) + + w := NewMockMachinesWatcher(ids, m.NextErr()) + m.machinesWatchers = append(m.machinesWatchers, w) + return w +} + +// FindEntity implements StateInterface. +func (m *mockState) FindEntity(tag names.Tag) (state.Entity, error) { + m.mu.Lock() + defer m.mu.Unlock() + + m.MethodCall(m, "FindEntity", tag) + + if err := m.NextErr(); err != nil { + return nil, err + } + if tag == nil { + return nil, errors.NotValidf("nil tag is") // +" not valid" + } + machine, found := m.machines[tag.Id()] + if !found { + return nil, errors.NotFoundf("machine %s", tag.Id()) + } + return machine, nil +} + +// SetMachineInfo adds a new or updates existing mockMachine info. +// Triggers any created mock machines watchers to return a change. +func (m *mockState) SetMachineInfo(c *gc.C, args machineInfo) { + m.mu.Lock() + defer m.mu.Unlock() + + c.Assert(args.id, gc.Not(gc.Equals), "") + + machine, found := m.machines[args.id] + if !found { + machine = &mockMachine{ + Stub: m.Stub, // reuse parent stub. + machineInfo: args, + } + } else { + machine.machineInfo = args + } + m.machines[args.id] = machine + + // Notify any watchers for the changes. + ids := []string{args.id} + for _, w := range m.machinesWatchers { + w.incoming <- ids + } +} + +// RemoveMachine removes an existing mockMachine with the given id. +// Triggers the machines watchers on success. If the id is not found +// no error occurs and no change is reported by the watchers. +func (m *mockState) RemoveMachine(c *gc.C, id string) { + m.mu.Lock() + defer m.mu.Unlock() + + if _, found := m.machines[id]; !found { + return + } + + delete(m.machines, id) + + // Notify any watchers for the changes. + ids := []string{id} + for _, w := range m.machinesWatchers { + w.incoming <- ids + } +} + +// Machine implements StateInterface. +func (m *mockState) Machine(id string) (instancepoller.StateMachine, error) { + m.mu.Lock() + defer m.mu.Unlock() + + m.MethodCall(m, "Machine", id) + + if err := m.NextErr(); err != nil { + return nil, err + } + machine, found := m.machines[id] + if !found { + return nil, errors.NotFoundf("machine %s", id) + } + return machine, nil +} + +// StartSync implements statetesting.SyncStarter, so mockState can be +// used with watcher helpers/checkers. +func (m *mockState) StartSync() {} + +type machineInfo struct { + id string + instanceId instance.Id + status state.StatusInfo + instanceStatus string + providerAddresses []network.Address + life state.Life + isManual bool +} + +type mockMachine struct { + *testing.Stub + instancepoller.StateMachine + + mu sync.Mutex + + machineInfo +} + +var _ instancepoller.StateMachine = (*mockMachine)(nil) + +// InstanceId implements StateMachine. +func (m *mockMachine) InstanceId() (instance.Id, error) { + m.mu.Lock() + defer m.mu.Unlock() + + m.MethodCall(m, "InstanceId") + if err := m.NextErr(); err != nil { + return "", err + } + return m.instanceId, nil +} + +// ProviderAddresses implements StateMachine. +func (m *mockMachine) ProviderAddresses() []network.Address { + m.mu.Lock() + defer m.mu.Unlock() + + m.MethodCall(m, "ProviderAddresses") + m.NextErr() // consume the unused error + return m.providerAddresses +} + +// SetProviderAddresses implements StateMachine. +func (m *mockMachine) SetProviderAddresses(addrs ...network.Address) error { + m.mu.Lock() + defer m.mu.Unlock() + + args := make([]interface{}, len(addrs)) + for i, addr := range addrs { + args[i] = addr + } + m.MethodCall(m, "SetProviderAddresses", args...) + if err := m.NextErr(); err != nil { + return err + } + m.providerAddresses = addrs + return nil +} + +// InstanceStatus implements StateMachine. +func (m *mockMachine) InstanceStatus() (string, error) { + m.mu.Lock() + defer m.mu.Unlock() + + m.MethodCall(m, "InstanceStatus") + if err := m.NextErr(); err != nil { + return "", err + } + return m.instanceStatus, nil +} + +// SetInstanceStatus implements StateMachine. +func (m *mockMachine) SetInstanceStatus(status string) error { + m.mu.Lock() + defer m.mu.Unlock() + + m.MethodCall(m, "SetInstanceStatus", status) + if err := m.NextErr(); err != nil { + return err + } + m.instanceStatus = status + return nil +} + +// Life implements StateMachine. +func (m *mockMachine) Life() state.Life { + m.mu.Lock() + defer m.mu.Unlock() + + m.MethodCall(m, "Life") + m.NextErr() // consume the unused error + return m.life +} + +// IsManual implements StateMachine. +func (m *mockMachine) IsManual() (bool, error) { + m.mu.Lock() + defer m.mu.Unlock() + + m.MethodCall(m, "IsManual") + return m.isManual, m.NextErr() +} + +// Status implements StateMachine. +func (m *mockMachine) Status() (state.StatusInfo, error) { + m.mu.Lock() + defer m.mu.Unlock() + + m.MethodCall(m, "Status") + return m.status, m.NextErr() +} + +type mockBaseWatcher struct { + err error + + closeChanges func() + done chan struct{} +} + +var _ state.Watcher = (*mockBaseWatcher)(nil) + +func NewMockBaseWatcher(err error, closeChanges func()) *mockBaseWatcher { + w := &mockBaseWatcher{ + err: err, + closeChanges: closeChanges, + done: make(chan struct{}), + } + if err != nil { + // Don't start the loop if we should fail. + w.Stop() + } + return w +} + +// Kill implements state.Watcher. +func (m *mockBaseWatcher) Kill() {} + +// Stop implements state.Watcher. +func (m *mockBaseWatcher) Stop() error { + select { + case <-m.done: + // already closed + default: + // Signal the loop we want to stop. + close(m.done) + // Signal the clients we've closed. + m.closeChanges() + } + return m.err +} + +// Wait implements state.Watcher. +func (m *mockBaseWatcher) Wait() error { + return m.Stop() +} + +// Err implements state.Watcher. +func (m *mockBaseWatcher) Err() error { + return m.err +} + +type mockConfigWatcher struct { + *mockBaseWatcher + + incoming chan struct{} + changes chan struct{} +} + +var _ state.NotifyWatcher = (*mockConfigWatcher)(nil) + +func NewMockConfigWatcher(err error) *mockConfigWatcher { + changes := make(chan struct{}) + w := &mockConfigWatcher{ + changes: changes, + incoming: make(chan struct{}), + mockBaseWatcher: NewMockBaseWatcher(err, func() { close(changes) }), + } + if err == nil { + go w.loop() + } + return w +} + +func (m *mockConfigWatcher) loop() { + // Prepare initial event. + outChanges := m.changes + // Forward any incoming changes until stopped. + for { + select { + case <-m.done: + // We're about to quit. + return + case outChanges <- struct{}{}: + outChanges = nil + case <-m.incoming: + outChanges = m.changes + } + } +} + +// Changes implements state.NotifyWatcher. +func (m *mockConfigWatcher) Changes() <-chan struct{} { + return m.changes +} + +type mockMachinesWatcher struct { + *mockBaseWatcher + + initial []string + incoming chan []string + changes chan []string +} + +func NewMockMachinesWatcher(initial []string, err error) *mockMachinesWatcher { + changes := make(chan []string) + w := &mockMachinesWatcher{ + initial: initial, + changes: changes, + incoming: make(chan []string), + mockBaseWatcher: NewMockBaseWatcher(err, func() { close(changes) }), + } + if err == nil { + go w.loop() + } + return w +} + +func (m *mockMachinesWatcher) loop() { + // Prepare initial event. + unsent := m.initial + outChanges := m.changes + // Forward any incoming changes until stopped. + for { + select { + case <-m.done: + // We're about to quit. + return + case outChanges <- unsent: + outChanges = nil + unsent = nil + case ids := <-m.incoming: + unsent = append(unsent, ids...) + outChanges = m.changes + } + } +} + +// Changes implements state.StringsWatcher. +func (m *mockMachinesWatcher) Changes() <-chan []string { + return m.changes +} + +var _ state.StringsWatcher = (*mockMachinesWatcher)(nil) === added file 'src/github.com/juju/juju/apiserver/instancepoller/package_test.go' --- src/github.com/juju/juju/apiserver/instancepoller/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/instancepoller/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,14 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package instancepoller_test + +import ( + "testing" + + gc "gopkg.in/check.v1" +) + +func TestPackage(t *testing.T) { + gc.TestingT(t) +} === added file 'src/github.com/juju/juju/apiserver/instancepoller/state.go' --- src/github.com/juju/juju/apiserver/instancepoller/state.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/instancepoller/state.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,46 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package instancepoller + +import ( + "github.com/juju/juju/instance" + "github.com/juju/juju/network" + "github.com/juju/juju/state" +) + +type StateMachine interface { + state.Entity + + Id() string + InstanceId() (instance.Id, error) + ProviderAddresses() []network.Address + SetProviderAddresses(...network.Address) error + InstanceStatus() (string, error) + SetInstanceStatus(status string) error + String() string + Refresh() error + Life() state.Life + Status() (state.StatusInfo, error) + IsManual() (bool, error) +} + +type StateInterface interface { + state.EnvironAccessor + state.EnvironMachinesWatcher + state.EntityFinder + + Machine(id string) (StateMachine, error) +} + +type stateShim struct { + *state.State +} + +func (s stateShim) Machine(id string) (StateMachine, error) { + return s.State.Machine(id) +} + +var getState = func(st *state.State) StateInterface { + return stateShim{st} +} === modified file 'src/github.com/juju/juju/apiserver/leadership/leadership.go' --- src/github.com/juju/juju/apiserver/leadership/leadership.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/leadership/leadership.go 2015-10-23 18:29:32 +0000 @@ -51,27 +51,29 @@ return NewLeadershipService(state.LeadershipClaimer(), authorizer) } -// NewLeadershipService returns a new LeadershipService. +// NewLeadershipService constructs a new LeadershipService. func NewLeadershipService( claimer leadership.Claimer, authorizer common.Authorizer, ) (LeadershipService, error) { + if !authorizer.AuthUnitAgent() { return nil, errors.Unauthorizedf("permission denied") } + return &leadershipService{ claimer: claimer, authorizer: authorizer, }, nil } -// LeadershipService implements the LeadershipService interface and +// leadershipService implements the LeadershipService interface and // is the concrete implementation of the API endpoint. type leadershipService struct { claimer leadership.Claimer authorizer common.Authorizer } -// ClaimLeadership implements the LeadershipService interface. +// ClaimLeadership is part of the LeadershipService interface. func (m *leadershipService) ClaimLeadership(args params.ClaimLeadershipBulkParams) (params.ClaimLeadershipBulkResults, error) { results := make([]params.ErrorResult, len(args.Params)) @@ -111,6 +113,7 @@ if !m.authMember(serviceTag) { return params.ErrorResult{Error: common.ServerError(common.ErrPerm)}, nil } + if err := m.claimer.BlockUntilLeadershipReleased(serviceTag.Id()); err != nil { return params.ErrorResult{Error: common.ServerError(err)}, nil } === modified file 'src/github.com/juju/juju/apiserver/logsink.go' --- src/github.com/juju/juju/apiserver/logsink.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/logsink.go 2015-10-23 18:29:32 +0000 @@ -7,19 +7,55 @@ "encoding/json" "io" "net/http" + "os" + "path/filepath" + "strings" "time" "github.com/juju/errors" "github.com/juju/loggo" + "github.com/juju/utils" "golang.org/x/net/websocket" + "gopkg.in/natefinch/lumberjack.v2" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" ) +func newLogSinkHandler(h httpHandler, logDir string) http.Handler { + + logPath := filepath.Join(logDir, "logsink.log") + if err := primeLogFile(logPath); err != nil { + // This isn't a fatal error so log and continue if priming + // fails. + logger.Errorf("Unable to prime %s (proceeding anyway): %v", logPath, err) + } + + return &logSinkHandler{ + httpHandler: h, + fileLogger: &lumberjack.Logger{ + Filename: logPath, + MaxSize: 500, // MB + MaxBackups: 1, + }, + } +} + +// primeLogFile ensures the logsink log file is created with the +// correct mode and ownership. +func primeLogFile(path string) error { + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + return errors.Trace(err) + } + f.Close() + err = utils.ChownPath(path, "syslog") + return errors.Trace(err) +} + type logSinkHandler struct { httpHandler - st *state.State + fileLogger io.WriteCloser } // LogMessage is used to transmit log messages to the logsink API @@ -51,7 +87,6 @@ } return } - defer stateWrapper.cleanup() tag, err := stateWrapper.authenticateAgent(req) if err != nil { @@ -70,18 +105,30 @@ return } - dbLogger := state.NewDbLogger(stateWrapper.state, tag) + st := stateWrapper.state + filePrefix := st.EnvironUUID() + " " + tag.String() + ":" + dbLogger := state.NewDbLogger(st, tag) defer dbLogger.Close() - var m LogMessage + m := new(LogMessage) for { - if err := websocket.JSON.Receive(socket, &m); err != nil { + if err := websocket.JSON.Receive(socket, m); err != nil { if err != io.EOF { logger.Errorf("error while receiving logs: %v", err) } break } - if err := dbLogger.Log(m.Time, m.Module, m.Location, m.Level, m.Message); err != nil { + + fileErr := h.logToFile(filePrefix, m) + if fileErr != nil { + logger.Errorf("logging to logsink.log failed: %v", fileErr) + } + + dbErr := dbLogger.Log(m.Time, m.Module, m.Location, m.Level, m.Message) + if dbErr != nil { logger.Errorf("logging to DB failed: %v", err) + } + + if fileErr != nil || dbErr != nil { break } } @@ -105,3 +152,16 @@ _, err = w.Write(message) return errors.Trace(err) } + +// logToFile writes a single log message to the logsink log file. +func (h *logSinkHandler) logToFile(prefix string, m *LogMessage) error { + _, err := h.fileLogger.Write([]byte(strings.Join([]string{ + prefix, + m.Time.In(time.UTC).Format("2006-01-02 15:04:05"), + m.Level.String(), + m.Module, + m.Location, + m.Message, + }, " ") + "\n")) + return err +} === modified file 'src/github.com/juju/juju/apiserver/logsink_test.go' --- src/github.com/juju/juju/apiserver/logsink_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/logsink_test.go 2015-10-23 18:29:32 +0000 @@ -5,8 +5,12 @@ import ( "bufio" + "io/ioutil" "net/http" "net/url" + "os" + "path/filepath" + "runtime" "time" "github.com/juju/loggo" @@ -18,7 +22,6 @@ "gopkg.in/mgo.v2/bson" "github.com/juju/juju/apiserver" - "github.com/juju/juju/feature" coretesting "github.com/juju/juju/testing" "github.com/juju/juju/testing/factory" ) @@ -43,7 +46,7 @@ var _ = gc.Suite(&logsinkSuite{}) func (s *logsinkSuite) SetUpTest(c *gc.C) { - s.SetInitialFeatureFlags(feature.DbLog) + s.SetInitialFeatureFlags("db-log") s.logsinkBaseSuite.SetUpTest(c) s.nonce = "nonce" m, password := s.Factory.MakeMachineReturningPassword(c, &factory.MachineParams{ @@ -105,7 +108,7 @@ errResult := readJSONErrorLine(c, reader) c.Assert(errResult.Error, gc.IsNil) - t0 := time.Now().Truncate(time.Millisecond) + t0 := time.Date(2015, time.June, 1, 23, 2, 1, 0, time.UTC) err := websocket.JSON.Send(conn, &apiserver.LogMessage{ Time: t0, Module: "some.where", @@ -115,7 +118,7 @@ }) c.Assert(err, jc.ErrorIsNil) - t1 := t0.Add(time.Second) + t1 := time.Date(2015, time.June, 1, 23, 2, 2, 0, time.UTC) err = websocket.JSON.Send(conn, &apiserver.LogMessage{ Time: t1, Module: "else.where", @@ -143,16 +146,17 @@ } // Check the recorded logs are correct. - c.Assert(docs[0]["t"], gc.Equals, t0) - c.Assert(docs[0]["e"], gc.Equals, s.State.EnvironUUID()) + envUUID := s.State.EnvironUUID() + c.Assert(docs[0]["t"].(time.Time).Sub(t0), gc.Equals, time.Duration(0)) + c.Assert(docs[0]["e"], gc.Equals, envUUID) c.Assert(docs[0]["n"], gc.Equals, s.machineTag.String()) c.Assert(docs[0]["m"], gc.Equals, "some.where") c.Assert(docs[0]["l"], gc.Equals, "foo.go:42") c.Assert(docs[0]["v"], gc.Equals, int(loggo.INFO)) c.Assert(docs[0]["x"], gc.Equals, "all is well") - c.Assert(docs[1]["t"], gc.Equals, t1) - c.Assert(docs[1]["e"], gc.Equals, s.State.EnvironUUID()) + c.Assert(docs[1]["t"].(time.Time).Sub(t1), gc.Equals, time.Duration(0)) + c.Assert(docs[1]["e"], gc.Equals, envUUID) c.Assert(docs[1]["n"], gc.Equals, s.machineTag.String()) c.Assert(docs[1]["m"], gc.Equals, "else.where") c.Assert(docs[1]["l"], gc.Equals, "bar.go:99") @@ -174,6 +178,23 @@ c.Assert(log, jc.LessThan, loggo.ERROR) } } + + // Check that the logsink log file was populated as expected + logPath := filepath.Join(s.LogDir, "logsink.log") + logContents, err := ioutil.ReadFile(logPath) + c.Assert(err, jc.ErrorIsNil) + line0 := envUUID + " machine-0: 2015-06-01 23:02:01 INFO some.where foo.go:42 all is well\n" + line1 := envUUID + " machine-0: 2015-06-01 23:02:02 ERROR else.where bar.go:99 oh noes\n" + c.Assert(string(logContents), gc.Equals, line0+line1) + + // Check the file mode is as expected. This doesn't work on + // Windows (but this code is very unlikely to run on Windows so + // it's ok). + if runtime.GOOS != "windows" { + info, err := os.Stat(logPath) + c.Assert(err, jc.ErrorIsNil) + c.Assert(info.Mode(), gc.Equals, os.FileMode(0600)) + } } func (s *logsinkSuite) dialWebsocket(c *gc.C) *websocket.Conn { === modified file 'src/github.com/juju/juju/apiserver/machine/machiner_test.go' --- src/github.com/juju/juju/apiserver/machine/machiner_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/machine/machiner_test.go 2015-10-23 18:29:32 +0000 @@ -60,7 +60,7 @@ c.Assert(err, jc.ErrorIsNil) args := params.SetStatus{ - Entities: []params.EntityStatus{ + Entities: []params.EntityStatusArgs{ {Tag: "machine-1", Status: params.StatusError, Info: "not really"}, {Tag: "machine-0", Status: params.StatusStopped, Info: "foobar"}, {Tag: "machine-42", Status: params.StatusStarted, Info: "blah"}, === modified file 'src/github.com/juju/juju/apiserver/machinemanager/machinemanager_test.go' --- src/github.com/juju/juju/apiserver/machinemanager/machinemanager_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/machinemanager/machinemanager_test.go 2015-10-23 18:29:32 +0000 @@ -151,7 +151,9 @@ panic("not implemented") } -type mockBlock struct{} +type mockBlock struct { + state.Block +} func (st *mockBlock) Id() string { return "id" @@ -168,3 +170,7 @@ func (st *mockBlock) Message() string { return "not allowed" } + +func (st *mockBlock) EnvUUID() string { + return "uuid" +} === added directory 'src/github.com/juju/juju/apiserver/meterstatus' === added file 'src/github.com/juju/juju/apiserver/meterstatus/meterstatus.go' --- src/github.com/juju/juju/apiserver/meterstatus/meterstatus.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/meterstatus/meterstatus.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,30 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +// Package meterstatus provides functions for getting meterstatus information +// about units. +package meterstatus + +import ( + "github.com/juju/errors" + + "github.com/juju/juju/state" +) + +// MeterStatusWrapper takes a MeterStatus and converts it into an 'api friendly' form where +// Not Set and Not Available (which are important distinctions in state) are converted +// into Amber and Red respecitvely in the api. +func MeterStatusWrapper(getter func() (state.MeterStatus, error)) (state.MeterStatus, error) { + status, err := getter() + if err != nil { + return state.MeterStatus{}, errors.Trace(err) + } + if status.Code == state.MeterNotSet { + return state.MeterStatus{state.MeterAmber, "not set"}, nil + } + if status.Code == state.MeterNotAvailable { + + return state.MeterStatus{state.MeterRed, "not available"}, nil + } + return status, nil +} === added file 'src/github.com/juju/juju/apiserver/meterstatus/meterstatus_test.go' --- src/github.com/juju/juju/apiserver/meterstatus/meterstatus_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/meterstatus/meterstatus_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,81 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package meterstatus_test + +import ( + "errors" + + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/meterstatus" + "github.com/juju/juju/state" +) + +type meterStatusSuite struct{} + +var _ = gc.Suite(&meterStatusSuite{}) + +func (s *meterStatusSuite) TestError(c *gc.C) { + _, err := meterstatus.MeterStatusWrapper(ErrorGetter) + c.Assert(err, gc.ErrorMatches, "an error") +} + +func (s *meterStatusSuite) TestWrapper(c *gc.C) { + tests := []struct { + about string + input func() (state.MeterStatus, error) + expectedOutput state.MeterStatus + }{{ + about: "notset in, amber out", + input: NotSetGetter, + expectedOutput: state.MeterStatus{state.MeterAmber, "not set"}, + }, { + about: "notavailable in, red out", + input: NotAvailableGetter, + expectedOutput: state.MeterStatus{state.MeterRed, "not available"}, + }, { + about: "red in, red out", + input: RedGetter, + expectedOutput: state.MeterStatus{state.MeterRed, "info"}, + }, { + about: "green in, green out", + input: GreenGetter, + expectedOutput: state.MeterStatus{state.MeterGreen, "info"}, + }, { + about: "amber in, amber out", + input: AmberGetter, + expectedOutput: state.MeterStatus{state.MeterAmber, "info"}, + }} + for i, test := range tests { + c.Logf("test %d: %s", i, test.about) + status, err := meterstatus.MeterStatusWrapper(test.input) + c.Assert(err, jc.ErrorIsNil) + c.Assert(status.Code, gc.Equals, test.expectedOutput.Code) + c.Assert(status.Info, gc.Equals, test.expectedOutput.Info) + } +} + +func ErrorGetter() (state.MeterStatus, error) { + return state.MeterStatus{}, errors.New("an error") +} + +func NotAvailableGetter() (state.MeterStatus, error) { + return state.MeterStatus{state.MeterNotAvailable, ""}, nil +} + +func NotSetGetter() (state.MeterStatus, error) { + return state.MeterStatus{state.MeterNotSet, ""}, nil +} + +func RedGetter() (state.MeterStatus, error) { + return state.MeterStatus{state.MeterRed, "info"}, nil +} + +func GreenGetter() (state.MeterStatus, error) { + return state.MeterStatus{state.MeterGreen, "info"}, nil +} +func AmberGetter() (state.MeterStatus, error) { + return state.MeterStatus{state.MeterAmber, "info"}, nil +} === added file 'src/github.com/juju/juju/apiserver/meterstatus/package_test.go' --- src/github.com/juju/juju/apiserver/meterstatus/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/meterstatus/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,14 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package meterstatus_test + +import ( + stdtesting "testing" + + gc "gopkg.in/check.v1" +) + +func TestAll(t *stdtesting.T) { + gc.TestingT(t) +} === modified file 'src/github.com/juju/juju/apiserver/metricsender/metricsender.go' --- src/github.com/juju/juju/apiserver/metricsender/metricsender.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/metricsender/metricsender.go 2015-10-23 18:29:32 +0000 @@ -23,6 +23,11 @@ Send([]*wireformat.MetricBatch) (*wireformat.Response, error) } +var ( + defaultMaxBatchesPerSend = 10 + defaultSender MetricSender = &NopSender{} +) + func handleResponse(mm *state.MetricsManager, st *state.State, response wireformat.Response) { for _, envResp := range response.EnvResponses { err := st.SetMetricBatchesSent(envResp.AcknowledgedBatches) @@ -102,3 +107,13 @@ return nil } + +// DefaultMaxBatchesPerSend returns the default number of batches per send. +func DefaultMaxBatchesPerSend() int { + return defaultMaxBatchesPerSend +} + +// DefaultMetricSender returns the default metric sender. +func DefaultMetricSender() MetricSender { + return defaultSender +} === modified file 'src/github.com/juju/juju/apiserver/metricsender/sender.go' --- src/github.com/juju/juju/apiserver/metricsender/sender.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/apiserver/metricsender/sender.go 2015-10-23 18:29:32 +0000 @@ -20,13 +20,13 @@ metricsHost string ) -// DefaultSender is the default used for sending +// HttpSender is the default used for sending // metrics to the collector service. -type DefaultSender struct { +type HttpSender struct { } // Send sends the given metrics to the collector service. -func (s *DefaultSender) Send(metrics []*wireformat.MetricBatch) (*wireformat.Response, error) { +func (s *HttpSender) Send(metrics []*wireformat.MetricBatch) (*wireformat.Response, error) { b, err := json.Marshal(metrics) if err != nil { return nil, errors.Trace(err) === modified file 'src/github.com/juju/juju/apiserver/metricsender/sender_test.go' --- src/github.com/juju/juju/apiserver/metricsender/sender_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/metricsender/sender_test.go 2015-10-23 18:29:32 +0000 @@ -66,11 +66,11 @@ } } -var _ metricsender.MetricSender = (*metricsender.DefaultSender)(nil) +var _ metricsender.MetricSender = (*metricsender.HttpSender)(nil) -// TestDefaultSender checks that if the default sender +// TestHttpSender checks that if the default sender // is in use metrics get sent -func (s *SenderSuite) TestDefaultSender(c *gc.C) { +func (s *SenderSuite) TestHttpSender(c *gc.C) { metricCount := 3 expectedCharmUrl, _ := s.unit.CharmURL() @@ -83,7 +83,7 @@ for i := range metrics { metrics[i] = s.Factory.MakeMetric(c, &factory.MetricParams{Unit: s.unit, Sent: false, Time: &now}) } - var sender metricsender.DefaultSender + var sender metricsender.HttpSender err := metricsender.SendMetrics(s.State, &sender, 10) c.Assert(err, jc.ErrorIsNil) @@ -165,7 +165,7 @@ for i := range batches { batches[i] = s.Factory.MakeMetric(c, &factory.MetricParams{Unit: s.unit, Sent: false, Time: &now}) } - var sender metricsender.DefaultSender + var sender metricsender.HttpSender err := metricsender.SendMetrics(s.State, &sender, 10) c.Assert(err, gc.ErrorMatches, test.expectedErr) for _, batch := range batches { @@ -194,7 +194,7 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(status.Code, gc.Equals, state.MeterNotSet) - var sender metricsender.DefaultSender + var sender metricsender.HttpSender err = metricsender.SendMetrics(s.State, &sender, 10) c.Assert(err, jc.ErrorIsNil) @@ -239,7 +239,7 @@ c.Assert(status.Code, gc.Equals, state.MeterNotSet) } - var sender metricsender.DefaultSender + var sender metricsender.HttpSender err := metricsender.SendMetrics(s.State, &sender, 10) c.Assert(err, jc.ErrorIsNil) @@ -261,7 +261,7 @@ _ = s.Factory.MakeMetric(c, &factory.MetricParams{Unit: s.unit, Sent: false}) cleanup := s.startServer(c, testHandler(c, nil, nil, 47*time.Hour)) defer cleanup() - var sender metricsender.DefaultSender + var sender metricsender.HttpSender err := metricsender.SendMetrics(s.State, &sender, 10) c.Assert(err, jc.ErrorIsNil) mm, err := s.State.MetricsManager() @@ -274,7 +274,7 @@ cleanup := s.startServer(c, testHandler(c, nil, nil, -47*time.Hour)) defer cleanup() - var sender metricsender.DefaultSender + var sender metricsender.HttpSender err := metricsender.SendMetrics(s.State, &sender, 10) c.Assert(err, jc.ErrorIsNil) mm, err := s.State.MetricsManager() @@ -287,7 +287,7 @@ cleanup := s.startServer(c, testHandler(c, nil, nil, 0)) defer cleanup() - var sender metricsender.DefaultSender + var sender metricsender.HttpSender err := metricsender.SendMetrics(s.State, &sender, 10) c.Assert(err, jc.ErrorIsNil) mm, err := s.State.MetricsManager() === modified file 'src/github.com/juju/juju/apiserver/metricsmanager/metricsmanager.go' --- src/github.com/juju/juju/apiserver/metricsmanager/metricsmanager.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/metricsmanager/metricsmanager.go 2015-10-23 18:29:32 +0000 @@ -18,9 +18,9 @@ var ( logger = loggo.GetLogger("juju.apiserver.metricsmanager") - maxBatchesPerSend = 1000 + maxBatchesPerSend = metricsender.DefaultMaxBatchesPerSend() - sender metricsender.MetricSender = &metricsender.NopSender{} + sender = metricsender.DefaultMetricSender() ) func init() { === modified file 'src/github.com/juju/juju/apiserver/params/apierror.go' --- src/github.com/juju/juju/apiserver/params/apierror.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/params/apierror.go 2015-10-23 18:29:32 +0000 @@ -17,11 +17,11 @@ Code string } -func (e *Error) Error() string { +func (e Error) Error() string { return e.Message } -func (e *Error) ErrorCode() string { +func (e Error) ErrorCode() string { return e.Code } @@ -35,25 +35,27 @@ // The Code constants hold error codes for some kinds of error. const ( - CodeNotFound = "not found" - CodeUnauthorized = "unauthorized access" - CodeCannotEnterScope = "cannot enter scope" - CodeCannotEnterScopeYet = "cannot enter scope yet" - CodeExcessiveContention = "excessive contention" - CodeUnitHasSubordinates = "unit has subordinates" - CodeNotAssigned = "not assigned" - CodeStopped = "stopped" - CodeDead = "dead" - CodeHasAssignedUnits = "machine has assigned units" - CodeNotProvisioned = "not provisioned" - CodeNoAddressSet = "no address set" - CodeTryAgain = "try again" - CodeNotImplemented = rpc.CodeNotImplemented - CodeAlreadyExists = "already exists" - CodeUpgradeInProgress = "upgrade in progress" - CodeActionNotAvailable = "action no longer available" - CodeOperationBlocked = "operation is blocked" - CodeLeadershipClaimDenied = "leadership claim denied" + CodeNotFound = "not found" + CodeUnauthorized = "unauthorized access" + CodeCannotEnterScope = "cannot enter scope" + CodeCannotEnterScopeYet = "cannot enter scope yet" + CodeExcessiveContention = "excessive contention" + CodeUnitHasSubordinates = "unit has subordinates" + CodeNotAssigned = "not assigned" + CodeStopped = "stopped" + CodeDead = "dead" + CodeHasAssignedUnits = "machine has assigned units" + CodeMachineHasAttachedStorage = "machine has attached storage" + CodeNotProvisioned = "not provisioned" + CodeNoAddressSet = "no address set" + CodeTryAgain = "try again" + CodeNotImplemented = rpc.CodeNotImplemented + CodeAlreadyExists = "already exists" + CodeUpgradeInProgress = "upgrade in progress" + CodeActionNotAvailable = "action no longer available" + CodeOperationBlocked = "operation is blocked" + CodeLeadershipClaimDenied = "leadership claim denied" + CodeNotSupported = "not supported" ) // ErrCode returns the error code associated with @@ -137,6 +139,10 @@ return ErrCode(err) == CodeHasAssignedUnits } +func IsCodeMachineHasAttachedStorage(err error) bool { + return ErrCode(err) == CodeMachineHasAttachedStorage +} + func IsCodeNotProvisioned(err error) bool { return ErrCode(err) == CodeNotProvisioned } @@ -168,3 +174,7 @@ func IsCodeLeadershipClaimDenied(err error) bool { return ErrCode(err) == CodeLeadershipClaimDenied } + +func IsCodeNotSupported(err error) bool { + return ErrCode(err) == CodeNotSupported +} === modified file 'src/github.com/juju/juju/apiserver/params/charms.go' --- src/github.com/juju/juju/apiserver/params/charms.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/params/charms.go 2015-10-23 18:29:32 +0000 @@ -17,3 +17,8 @@ type CharmsListResult struct { CharmURLs []string } + +// IsMeteredResult stores result from a charms.IsMetered call +type IsMeteredResult struct { + Metered bool +} === added file 'src/github.com/juju/juju/apiserver/params/image_metadata.go' --- src/github.com/juju/juju/apiserver/params/image_metadata.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/params/image_metadata.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,70 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package params + +// ImageMetadataFilter holds filter properties used to search for image metadata. +// It amalgamates both simplestreams.MetadataLookupParams and simplestreams.LookupParams +// and adds additional properties to satisfy existing and new use cases. +type ImageMetadataFilter struct { + // Region stores metadata region. + Region string `json:"region,omitempty"` + + // Series stores all desired series. + Series []string `json:"series,omitempty"` + + // Arches stores all desired architectures. + Arches []string `json:"arches,omitempty"` + + // Stream can be "" or "released" for the default "released" stream, + // or "daily" for daily images, or any other stream that the available + // simplestreams metadata supports. + Stream string `json:"stream,omitempty"` + + // VirtualType stores virtual type. + VirtualType string `json:"virtual_type,omitempty"` + + // RootStorageType stores storage type. + RootStorageType string `json:"root-storage-type,omitempty"` +} + +// CloudImageMetadata holds cloud image metadata properties. +type CloudImageMetadata struct { + // ImageId is an image identifier. + ImageId string `json:"image_id"` + + // Stream contains reference to a particular stream, + // for e.g. "daily" or "released" + Stream string `json:"stream,omitempty"` + + // Region is the name of cloud region associated with the image. + Region string `json:"region"` + + // Series is OS version, for e.g. "quantal". + Series string `json:"series"` + + // Arch is the architecture for this cloud image, for e.g. "amd64" + Arch string `json:"arch"` + + // VirtualType contains the type of the cloud image, for e.g. "pv", "hvm". "kvm". + VirtualType string `json:"virtual_type,omitempty"` + + // RootStorageType contains type of root storage, for e.g. "ebs", "instance". + RootStorageType string `json:"root_storage_type,omitempty"` + + // RootStorageSize contains size of root storage in gigabytes (GB). + RootStorageSize *uint64 `json:"root_storage_size,omitempty"` + + // Source describes where this image is coming from: is it public? custom? + Source string `json:"source"` +} + +// ListCloudImageMetadataResult holds the results of querying cloud image metadata. +type ListCloudImageMetadataResult struct { + Result []CloudImageMetadata `json:"result"` +} + +// MetadataSaveParams holds cloud image metadata details to save. +type MetadataSaveParams struct { + Metadata []CloudImageMetadata `json:"metadata"` +} === modified file 'src/github.com/juju/juju/apiserver/params/internal.go' --- src/github.com/juju/juju/apiserver/params/internal.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/params/internal.go 2015-10-23 18:29:32 +0000 @@ -116,9 +116,17 @@ ServerUUID string } -// EnvironmentList holds information about a list of environments. -type EnvironmentList struct { - Environments []Environment +// UserEnvironment holds information about an environment and the last +// time the environment was accessed for a particular user. +type UserEnvironment struct { + Environment + LastConnection *time.Time +} + +// UserEnvironmentList holds information about a list of environments +// for a particular user. +type UserEnvironmentList struct { + UserEnvironments []UserEnvironment } // ResolvedModeResult holds a resolved mode or an error. @@ -331,8 +339,16 @@ Machines []InstanceInfo } -// EntityStatus holds an entity tag, status and extra info. +// EntityStatus holds the status of an entity. type EntityStatus struct { + Status Status + Info string + Data map[string]interface{} + Since *time.Time +} + +// EntityStatus holds parameters for setting the status of a single entity. +type EntityStatusArgs struct { Tag string Status Status Info string @@ -341,56 +357,19 @@ // SetStatus holds the parameters for making a SetStatus/UpdateStatus call. type SetStatus struct { - Entities []EntityStatus -} - -type HistoryKind string - -const ( - KindCombined HistoryKind = "combined" - KindAgent HistoryKind = "agent" - KindWorkload HistoryKind = "workload" -) - -// StatusHistory holds the parameters to filter a status history query. -type StatusHistory struct { - Kind HistoryKind - Size int - Name string -} - -// StatusResult holds an entity status, extra information, or an -// error. -type StatusResult struct { - Error *Error - Id string - Life Life - Status Status - Info string - Data map[string]interface{} - Since *time.Time -} - -// StatusResults holds multiple status results. -type StatusResults struct { - Results []StatusResult -} - -// ServiceStatusResult holds results for a service Full Status -type ServiceStatusResult struct { - Service StatusResult - Units map[string]StatusResult - Error *Error -} - -// ServiceStatusResults holds multiple StatusResult. -type ServiceStatusResults struct { - Results []ServiceStatusResult -} - -// SetMachinesAddresses holds the parameters for making a SetMachineAddresses call. -type SetMachinesAddresses struct { - MachineAddresses []MachineAddresses + Entities []EntityStatusArgs +} + +// InstanceStatus holds an entity tag and instance status. +type InstanceStatus struct { + Tag string + Status string +} + +// SetInstancesStatus holds parameters for making a +// SetInstanceStatus() call. +type SetInstancesStatus struct { + Entities []InstanceStatus } // ConstraintsResult holds machine constraints or an error. @@ -489,6 +468,20 @@ Results []StringsWatchResult } +// EntityWatchResult holds a EntityWatcher id, changes and an error +// (if any). +type EntityWatchResult struct { + EntityWatcherId string `json:"EntityWatcherId"` + Changes []string `json:"Changes"` + Error *Error `json:"Error"` +} + +// EntityWatchResults holds the results for any API call which ends up +// returning a list of EntityWatchers. +type EntityWatchResults struct { + Results []EntityWatchResult +} + // RelationUnitsWatchResult holds a RelationUnitsWatcher id, changes // and an error (if any). type RelationUnitsWatchResult struct { @@ -558,13 +551,14 @@ // ProvisioningInfo holds machine provisioning info. type ProvisioningInfo struct { - Constraints constraints.Value - Series string - Placement string - Networks []string - Jobs []multiwatcher.MachineJob - Volumes []VolumeParams - Tags map[string]string + Constraints constraints.Value + Series string + Placement string + Networks []string + Jobs []multiwatcher.MachineJob + Volumes []VolumeParams + Tags map[string]string + SubnetsToZones map[string][]string } // ProvisioningInfoResult holds machine provisioning info or an error. === modified file 'src/github.com/juju/juju/apiserver/params/network.go' --- src/github.com/juju/juju/apiserver/params/network.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/params/network.go 2015-10-23 18:29:32 +0000 @@ -4,6 +4,8 @@ package params import ( + "net" + "github.com/juju/juju/network" ) @@ -11,6 +13,44 @@ // Parameters field types. // ----- +// Subnet describes a single subnet within a network. +type Subnet struct { + // CIDR of the subnet in IPv4 or IPv6 notation. + CIDR string `json:"CIDR"` + + // ProviderId is the provider-specific subnet ID (if applicable). + ProviderId string `json:"ProviderId,omitempty` + + // VLANTag needs to be between 1 and 4094 for VLANs and 0 for + // normal networks. It's defined by IEEE 802.1Q standard. + VLANTag int `json:"VLANTag"` + + // Life is the subnet's life cycle value - Alive means the subnet + // is in use by one or more machines, Dying or Dead means the + // subnet is about to be removed. + Life Life `json:"Life"` + + // SpaceTag is the Juju network space this subnet is associated + // with. + SpaceTag string `json:"SpaceTag"` + + // Zones contain one or more availability zones this subnet is + // associated with. + Zones []string `json:"Zones"` + + // StaticRangeLowIP (if available) is the lower bound of the + // subnet's static IP allocation range. + StaticRangeLowIP net.IP `json:"StaticRangeLowIP,omitempty"` + + // StaticRangeHighIP (if available) is the higher bound of the + // subnet's static IP allocation range. + StaticRangeHighIP net.IP `json:"StaticRangeHighIP,omitempty"` + + // Status returns the status of the subnet, whether it is in use, not + // in use or terminating. + Status string `json:"Status,omitempty"` +} + // Network describes a single network available on an instance. type Network struct { // Tag is the network's tag. @@ -323,6 +363,25 @@ Addresses []Address `json:"Addresses"` } +// SetMachinesAddresses holds the parameters for making an +// API call to update machine addresses. +type SetMachinesAddresses struct { + MachineAddresses []MachineAddresses `json:"MachineAddresses"` +} + +// MachineAddressesResult holds a list of machine addresses or an +// error. +type MachineAddressesResult struct { + Error *Error `json:"Error"` + Addresses []Address `json:"Addresses"` +} + +// MachineAddressesResults holds the results of calling an API method +// returning a list of addresses per machine. +type MachineAddressesResults struct { + Results []MachineAddressesResult `json:"Results"` +} + // MachinePortRange holds a single port range open on a machine for // the given unit and relation tags. type MachinePortRange struct { @@ -412,3 +471,96 @@ func (r APIHostPortsResult) NetworkHostsPorts() [][]network.HostPort { return NetworkHostsPorts(r.Servers) } + +// ZoneResult holds the result of an API call that returns an +// availability zone name and whether it's available for use. +type ZoneResult struct { + Error *Error `json:"Error"` + Name string `json:"Name"` + Available bool `json:"Available"` +} + +// ZoneResults holds multiple ZoneResult results +type ZoneResults struct { + Results []ZoneResult `json:"Results"` +} + +// SpaceResult holds a single space tag or an error. +type SpaceResult struct { + Error *Error `json:"Error"` + Tag string `json:"Tag"` +} + +// SpaceResults holds the bulk operation result of an API call +// that returns space tags or an errors. +type SpaceResults struct { + Results []SpaceResult `json:"Results"` +} + +// ListSubnetsResults holds the result of a ListSubnets API call. +type ListSubnetsResults struct { + Results []Subnet `json:"Results"` +} + +// SubnetsFilters holds an optional SpaceTag and Zone for filtering +// the subnets returned by a ListSubnets call. +type SubnetsFilters struct { + SpaceTag string `json:"SpaceTag,omitempty"` + Zone string `json:"Zone,omitempty"` +} + +// AddSubnetsParams holds the arguments of AddSubnets API call. +type AddSubnetsParams struct { + Subnets []AddSubnetParams `json:"Subnets"` +} + +// AddSubnetParams holds a subnet and space tags, subnet provider ID, +// and a list of zones to associate the subnet to. Either SubnetTag or +// SubnetProviderId must be set, but not both. Zones can be empty if +// they can be discovered +type AddSubnetParams struct { + SubnetTag string `json:"SubnetTag,omitempty"` + SubnetProviderId string `json:"SubnetProviderId,omitempty"` + SpaceTag string `json:"SpaceTag"` + Zones []string `json:"Zones,omitempty"` +} + +// CreateSubnetsParams holds the arguments of CreateSubnets API call. +type CreateSubnetsParams struct { + Subnets []CreateSubnetParams `json:"Subnets"` +} + +// CreateSubnetParams holds a subnet and space tags, vlan tag, +// and a list of zones to associate the subnet to. +type CreateSubnetParams struct { + SubnetTag string `json:"SubnetTag,omitempty"` + SpaceTag string `json:"SpaceTag"` + Zones []string `json:"Zones,omitempty"` + VLANTag int `json:"VLANTag,omitempty"` + IsPublic bool `json:"IsPublic"` +} + +// CreateSpacesParams olds the arguments of the AddSpaces API call. +type CreateSpacesParams struct { + Spaces []CreateSpaceParams `json:"Spaces"` +} + +// CreateSpaceParams holds the space tag and at least one subnet +// tag required to create a new space. +type CreateSpaceParams struct { + SubnetTags []string `json:"SubnetTags"` + SpaceTag string `json:"SpaceTag"` + Public bool `json:"Public"` +} + +// ListSpacesResults holds the list of all available spaces. +type ListSpacesResults struct { + Results []Space `json:"Results"` +} + +// Space holds the information about a single space and its associated subnets. +type Space struct { + Name string `json:"Name"` + Subnets []Subnet `json:"Subnets"` + Error *Error `json:"Error,omitempty"` +} === modified file 'src/github.com/juju/juju/apiserver/params/params.go' --- src/github.com/juju/juju/apiserver/params/params.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/params/params.go 2015-10-23 18:29:32 +0000 @@ -28,7 +28,7 @@ Prefixes []string `json:"prefixes"` } -// FindTagResults wraps the mapping between the requested prefix and the +// FindTagsResults wraps the mapping between the requested prefix and the // matching tags for each requested prefix. type FindTagsResults struct { Matches map[string][]Entity `json:"matches"` @@ -115,7 +115,7 @@ Endpoints []string } -// AddCharm holds the arguments for making an AddCharmWithAuthorization API call. +// AddCharmWithAuthorization holds the arguments for making an AddCharmWithAuthorization API call. type AddCharmWithAuthorization struct { URL string CharmStoreMacaroon *macaroon.Macaroon @@ -173,7 +173,7 @@ Machines []AddMachinesResult `json:"Machines"` } -// AddMachinesResults holds the name of a machine added by the +// AddMachinesResult holds the name of a machine added by the // api.client.AddMachine call for a single machine. type AddMachinesResult struct { Machine string `json:"Machine"` @@ -200,6 +200,7 @@ ConfigYAML string // Takes precedence over config if both are present. Constraints constraints.Value ToMachineSpec string + Placement []*instance.Placement Networks []string Storage map[string]storage.Constraints } @@ -334,6 +335,7 @@ ServiceName string NumUnits int ToMachineSpec string + Placement []*instance.Placement } // DestroyServiceUnits holds parameters for the DestroyUnits call. @@ -432,7 +434,7 @@ Mode ssh.ListMode } -// ModifySSHKeys stores parameters used for a KeyManager.Add|Delete|Import call for a user. +// ModifyUserSSHKeys stores parameters used for a KeyManager.Add|Delete|Import call for a user. type ModifyUserSSHKeys struct { User string Keys []string @@ -524,11 +526,6 @@ APIAddresses []string } -// StatusParams holds parameters for the Status call. -type StatusParams struct { - Patterns []string -} - // SetRsyslogCertParams holds parameters for the SetRsyslogCert call. type SetRsyslogCertParams struct { CACert []byte @@ -610,7 +607,7 @@ Credentials *string `json:"credentials,omitempty"` } -// LoginRequestV1 holds the result of an Admin v1 Login call. +// LoginResultV1 holds the result of an Admin v1 Login call. type LoginResultV1 struct { // Servers is the list of API server addresses. Servers [][]HostPort `json:"servers"` @@ -671,7 +668,7 @@ Results []StateServersChangeResult } -// StateServersChange lists the servers +// StateServersChanges lists the servers // that have been added, removed or maintained in the // pool as a result of an ensure-availability operation. type StateServersChanges struct { @@ -745,124 +742,3 @@ Result RebootAction `json:"result,omitempty"` Error *Error `json:"error,omitempty"` } - -// Life describes the lifecycle state of an entity ("alive", "dying" or "dead"). -type Life multiwatcher.Life - -const ( - Alive Life = "alive" - Dying Life = "dying" - Dead Life = "dead" -) - -// Status represents the status of an entity. -// It could be a unit, machine or its agent. -type Status multiwatcher.Status - -const ( - // Status values common to machine and unit agents. - - // The entity requires human intervention in order to operate - // correctly. - StatusError Status = "error" - - // The entity is actively participating in the environment. - // For unit agents, this is a state we preserve for backwards - // compatibility with scripts during the life of Juju 1.x. - // In Juju 2.x, the agent-state will remain “active” and scripts - // will watch the unit-state instead for signals of service readiness. - StatusStarted Status = "started" -) - -const ( - // Status values specific to machine agents. - - // The machine is not yet participating in the environment. - StatusPending Status = "pending" - - // The machine's agent will perform no further action, other than - // to set the unit to Dead at a suitable moment. - StatusStopped Status = "stopped" - - // The machine ought to be signalling activity, but it cannot be - // detected. - StatusDown Status = "down" -) - -const ( - // Status values specific to unit agents. - - // The machine on which a unit is to be hosted is still being - // spun up in the cloud. - StatusAllocating Status = "allocating" - - // The machine on which this agent is running is being rebooted. - // The juju-agent should move from rebooting to idle when the reboot is complete. - StatusRebooting Status = "rebooting" - - // The agent is running a hook or action. The human-readable message should reflect - // which hook or action is being run. - StatusExecuting Status = "executing" - - // Once the agent is installed and running it will notify the Juju server and its state - // becomes "idle". It will stay "idle" until some action (e.g. it needs to run a hook) or - // error (e.g it loses contact with the Juju server) moves it to a different state. - StatusIdle Status = "idle" - - // The unit agent has failed in some way,eg the agent ought to be signalling - // activity, but it cannot be detected. It might also be that the unit agent - // detected an unrecoverable condition and managed to tell the Juju server about it. - StatusFailed Status = "failed" - - // The juju agent has has not communicated with the juju server for an unexpectedly long time; - // the unit agent ought to be signalling activity, but none has been detected. - StatusLost Status = "lost" - - // ---- Outdated ---- - // The unit agent is downloading the charm and running the install hook. - StatusInstalling Status = "installing" - - // The unit is being destroyed; the agent will soon mark the unit as “dead”. - // In Juju 2.x this will describe the state of the agent rather than a unit. - StatusStopping Status = "stopping" -) - -const ( - // Status values specific to services and units, reflecting the - // state of the software itself. - - // The unit is not yet providing services, but is actively doing stuff - // in preparation for providing those services. - // This is a "spinning" state, not an error state. - // It reflects activity on the unit itself, not on peers or related units. - StatusMaintenance Status = "maintenance" - - // This unit used to exist, we have a record of it (perhaps because of storage - // allocated for it that was flagged to survive it). Nonetheless, it is now gone. - StatusTerminated Status = "terminated" - - // A unit-agent has finished calling install, config-changed, and start, - // but the charm has not called status-set yet. - StatusUnknown Status = "unknown" - - // The unit is unable to progress to an active state because a service to - // which it is related is not running. - StatusWaiting Status = "waiting" - - // The unit needs manual intervention to get back to the Running state. - StatusBlocked Status = "blocked" - - // The unit believes it is correctly offering all the services it has - // been asked to offer. - StatusActive Status = "active" -) - -const ( - // StorageReadyMessage is the message set to the agent status when all storage - // attachments are properly done. - StorageReadyMessage = "storage ready" - - // PreparingStorageMessage is the message set to the agent status before trying - // to attach storages. - PreparingStorageMessage = "preparing storage" -) === modified file 'src/github.com/juju/juju/apiserver/params/params_test.go' --- src/github.com/juju/juju/apiserver/params/params_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/params/params_test.go 2015-10-23 18:29:32 +0000 @@ -39,6 +39,7 @@ about: "MachineInfo Delta", value: multiwatcher.Delta{ Entity: &multiwatcher.MachineInfo{ + EnvUUID: "uuid", Id: "Benji", InstanceId: "Shazam", Status: "error", @@ -51,11 +52,12 @@ HardwareCharacteristics: &instance.HardwareCharacteristics{}, }, }, - json: `["machine","change",{"Id":"Benji","InstanceId":"Shazam","HasVote":false,"WantsVote":false,"Status":"error","StatusInfo":"foo","StatusData":null,"Life":"alive","Series":"trusty","SupportedContainers":["lxc"],"SupportedContainersKnown":false,"Jobs":["JobManageEnviron"],"Addresses":[],"HardwareCharacteristics":{}}]`, + json: `["machine","change",{"EnvUUID": "uuid", "Id":"Benji","InstanceId":"Shazam","HasVote":false,"WantsVote":false,"Status":"error","StatusInfo":"foo","StatusData":null,"Life":"alive","Series":"trusty","SupportedContainers":["lxc"],"SupportedContainersKnown":false,"Jobs":["JobManageEnviron"],"Addresses":[],"HardwareCharacteristics":{}}]`, }, { about: "ServiceInfo Delta", value: multiwatcher.Delta{ Entity: &multiwatcher.ServiceInfo{ + EnvUUID: "uuid", Name: "Benji", Exposed: true, CharmURL: "cs:quantal/name", @@ -73,11 +75,12 @@ }, }, }, - json: `["service","change",{"CharmURL": "cs:quantal/name","Name":"Benji","Exposed":true,"Life":"dying","OwnerTag":"test-owner","MinUnits":42,"Constraints":{"arch":"armhf", "mem": 1024},"Config": {"hello":"goodbye","foo":false},"Subordinate":false,"Status":{"Current":"active", "Message":"all good", "Version": "", "Err": null, "Data": null, "Since": null}}]`, + json: `["service","change",{"EnvUUID": "uuid", "CharmURL": "cs:quantal/name","Name":"Benji","Exposed":true,"Life":"dying","OwnerTag":"test-owner","MinUnits":42,"Constraints":{"arch":"armhf", "mem": 1024},"Config": {"hello":"goodbye","foo":false},"Subordinate":false,"Status":{"Current":"active", "Message":"all good", "Version": "", "Err": null, "Data": null, "Since": null}}]`, }, { about: "UnitInfo Delta", value: multiwatcher.Delta{ Entity: &multiwatcher.UnitInfo{ + EnvUUID: "uuid", Name: "Benji", Service: "Shazam", Series: "precise", @@ -105,40 +108,43 @@ }, }, }, - json: `["unit", "change", {"CharmURL": "cs:~user/precise/wordpress-42", "MachineId": "1", "Series": "precise", "Name": "Benji", "PublicAddress": "testing.invalid", "Service": "Shazam", "PrivateAddress": "10.0.0.1", "Ports": [{"Protocol": "http", "Number": 80}], "PortRanges": [{"FromPort": 80, "ToPort": 80, "Protocol": "http"}], "Status": "error", "StatusInfo": "foo", "StatusData": null, "WorkloadStatus":{"Current":"active", "Message":"all good", "Version": "", "Err": null, "Data": null, "Since": null}, "AgentStatus":{"Current":"idle", "Message":"", "Version": "", "Err": null, "Data": null, "Since": null}, "Subordinate": false}]`, + json: `["unit", "change", {"EnvUUID": "uuid", "CharmURL": "cs:~user/precise/wordpress-42", "MachineId": "1", "Series": "precise", "Name": "Benji", "PublicAddress": "testing.invalid", "Service": "Shazam", "PrivateAddress": "10.0.0.1", "Ports": [{"Protocol": "http", "Number": 80}], "PortRanges": [{"FromPort": 80, "ToPort": 80, "Protocol": "http"}], "Status": "error", "StatusInfo": "foo", "StatusData": null, "WorkloadStatus":{"Current":"active", "Message":"all good", "Version": "", "Err": null, "Data": null, "Since": null}, "AgentStatus":{"Current":"idle", "Message":"", "Version": "", "Err": null, "Data": null, "Since": null}, "Subordinate": false}]`, }, { about: "RelationInfo Delta", value: multiwatcher.Delta{ Entity: &multiwatcher.RelationInfo{ - Key: "Benji", - Id: 4711, + EnvUUID: "uuid", + Key: "Benji", + Id: 4711, Endpoints: []multiwatcher.Endpoint{ {ServiceName: "logging", Relation: charm.Relation{Name: "logging-directory", Role: "requirer", Interface: "logging", Optional: false, Limit: 1, Scope: "container"}}, {ServiceName: "wordpress", Relation: charm.Relation{Name: "logging-dir", Role: "provider", Interface: "logging", Optional: false, Limit: 0, Scope: "container"}}}, }, }, - json: `["relation","change",{"Key":"Benji", "Id": 4711, "Endpoints": [{"ServiceName":"logging", "Relation":{"Name":"logging-directory", "Role":"requirer", "Interface":"logging", "Optional":false, "Limit":1, "Scope":"container"}}, {"ServiceName":"wordpress", "Relation":{"Name":"logging-dir", "Role":"provider", "Interface":"logging", "Optional":false, "Limit":0, "Scope":"container"}}]}]`, + json: `["relation","change",{"EnvUUID": "uuid", "Key":"Benji", "Id": 4711, "Endpoints": [{"ServiceName":"logging", "Relation":{"Name":"logging-directory", "Role":"requirer", "Interface":"logging", "Optional":false, "Limit":1, "Scope":"container"}}, {"ServiceName":"wordpress", "Relation":{"Name":"logging-dir", "Role":"provider", "Interface":"logging", "Optional":false, "Limit":0, "Scope":"container"}}]}]`, }, { about: "AnnotationInfo Delta", value: multiwatcher.Delta{ Entity: &multiwatcher.AnnotationInfo{ - Tag: "machine-0", + EnvUUID: "uuid", + Tag: "machine-0", Annotations: map[string]string{ "foo": "bar", "arble": "2 4", }, }, }, - json: `["annotation","change",{"Tag":"machine-0","Annotations":{"foo":"bar","arble":"2 4"}}]`, + json: `["annotation","change",{"EnvUUID": "uuid", "Tag":"machine-0","Annotations":{"foo":"bar","arble":"2 4"}}]`, }, { about: "Delta Removed True", value: multiwatcher.Delta{ Removed: true, Entity: &multiwatcher.RelationInfo{ - Key: "Benji", + EnvUUID: "uuid", + Key: "Benji", }, }, - json: `["relation","remove",{"Key":"Benji", "Id": 0, "Endpoints": null}]`, + json: `["relation","remove",{"EnvUUID": "uuid", "Key":"Benji", "Id": 0, "Endpoints": null}]`, }} func (s *MarshalSuite) TestDeltaMarshalJSON(c *gc.C) { === added file 'src/github.com/juju/juju/apiserver/params/status.go' --- src/github.com/juju/juju/apiserver/params/status.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/params/status.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,353 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package params + +// TODO(ericsnow) Eliminate the juju-related imports. + +import ( + "time" + + "gopkg.in/juju/charm.v5" + + "github.com/juju/juju/instance" + "github.com/juju/juju/network" + "github.com/juju/juju/state/multiwatcher" +) + +// StatusParams holds parameters for the Status call. +type StatusParams struct { + Patterns []string +} + +// TODO(ericsnow) Add FullStatusResult. + +// Status holds information about the status of a juju environment. +type FullStatus struct { + EnvironmentName string + AvailableVersion string + Machines map[string]MachineStatus + Services map[string]ServiceStatus + Networks map[string]NetworkStatus + Relations []RelationStatus +} + +// MachineStatus holds status info about a machine. +type MachineStatus struct { + Agent AgentStatus + + // The following fields mirror fields in AgentStatus (introduced + // in 1.19.x). The old fields below are being kept for + // compatibility with old clients. + // They can be removed once API versioning lands. + AgentState Status + AgentStateInfo string + AgentVersion string + Life string + Err error + + DNSName string + InstanceId instance.Id + InstanceState string + Series string + Id string + Containers map[string]MachineStatus + Hardware string + Jobs []multiwatcher.MachineJob + HasVote bool + WantsVote bool +} + +// ServiceStatus holds status info about a service. +type ServiceStatus struct { + Err error + Charm string + Exposed bool + Life string + Relations map[string][]string + Networks NetworksSpecification + CanUpgradeTo string + SubordinateTo []string + Units map[string]UnitStatus + MeterStatuses map[string]MeterStatus + Status AgentStatus +} + +// MeterStatus represents the meter status of a unit. +type MeterStatus struct { + Color string + Message string +} + +// UnitStatus holds status info about a unit. +type UnitStatus struct { + // UnitAgent holds the status for a unit's agent. + UnitAgent AgentStatus + + // Workload holds the status for a unit's workload + Workload AgentStatus + + // Until Juju 2.0, we need to continue to return legacy agent state values + // as top level struct attributes when the "FullStatus" API is called. + AgentState Status + AgentStateInfo string + AgentVersion string + Life string + Err error + + Machine string + OpenedPorts []string + PublicAddress string + Charm string + Subordinates map[string]UnitStatus +} + +// TODO(ericsnow) Rename to ServiceNetworksSepcification. + +// NetworksSpecification holds the enabled and disabled networks for a +// service. +// TODO(dimitern): Drop this in a follow-up. +type NetworksSpecification struct { + Enabled []string + Disabled []string +} + +// NetworkStatus holds status info about a network. +type NetworkStatus struct { + Err error + ProviderId network.Id + CIDR string + VLANTag int +} + +// RelationStatus holds status info about a relation. +type RelationStatus struct { + Id int + Key string + Interface string + Scope charm.RelationScope + Endpoints []EndpointStatus +} + +// EndpointStatus holds status info about a single endpoint +type EndpointStatus struct { + ServiceName string + Name string + Role charm.RelationRole + Subordinate bool +} + +// TODO(ericsnow) Eliminate the String method. + +func (epStatus *EndpointStatus) String() string { + return epStatus.ServiceName + ":" + epStatus.Name +} + +// AgentStatus holds status info about a machine or unit agent. +type AgentStatus struct { + Status Status + Info string + Data map[string]interface{} + Since *time.Time + Kind HistoryKind + Version string + Life string + Err error +} + +// LegacyStatus holds minimal information on the status of a juju environment. +type LegacyStatus struct { + Machines map[string]LegacyMachineStatus +} + +// LegacyMachineStatus holds just the instance-id of a machine. +type LegacyMachineStatus struct { + InstanceId string // Not type instance.Id just to match original api. +} + +// TODO(ericsnow) Rename to StatusHistoryArgs. + +// StatusHistory holds the parameters to filter a status history query. +type StatusHistory struct { + Kind HistoryKind + Size int + Name string +} + +// TODO(ericsnow) Rename to UnitStatusHistoryResult. + +// UnitStatusHistory holds a slice of statuses. +type UnitStatusHistory struct { + Statuses []AgentStatus +} + +// StatusResult holds an entity status, extra information, or an +// error. +type StatusResult struct { + Error *Error + Id string + Life Life + Status Status + Info string + Data map[string]interface{} + Since *time.Time +} + +// StatusResults holds multiple status results. +type StatusResults struct { + Results []StatusResult +} + +// ServiceStatusResult holds results for a service Full Status +type ServiceStatusResult struct { + Service StatusResult + Units map[string]StatusResult + Error *Error +} + +// ServiceStatusResults holds multiple StatusResult. +type ServiceStatusResults struct { + Results []ServiceStatusResult +} + +type HistoryKind string + +const ( + KindCombined HistoryKind = "combined" + KindAgent HistoryKind = "agent" + KindWorkload HistoryKind = "workload" +) + +// Life describes the lifecycle state of an entity ("alive", "dying" or "dead"). +type Life multiwatcher.Life + +const ( + Alive Life = "alive" + Dying Life = "dying" + Dead Life = "dead" +) + +// Status represents the status of an entity. +// It could be a unit, machine or its agent. +type Status multiwatcher.Status + +const ( + // Status values common to machine and unit agents. + + // The entity requires human intervention in order to operate + // correctly. + StatusError Status = "error" + + // The entity is actively participating in the environment. + // For unit agents, this is a state we preserve for backwards + // compatibility with scripts during the life of Juju 1.x. + // In Juju 2.x, the agent-state will remain “active” and scripts + // will watch the unit-state instead for signals of service readiness. + StatusStarted Status = "started" +) + +const ( + // Status values specific to machine agents. + + // The machine is not yet participating in the environment. + StatusPending Status = "pending" + + // The machine's agent will perform no further action, other than + // to set the unit to Dead at a suitable moment. + StatusStopped Status = "stopped" + + // The machine ought to be signalling activity, but it cannot be + // detected. + StatusDown Status = "down" +) + +const ( + // Status values specific to unit agents. + + // The machine on which a unit is to be hosted is still being + // spun up in the cloud. + StatusAllocating Status = "allocating" + + // The machine on which this agent is running is being rebooted. + // The juju-agent should move from rebooting to idle when the reboot is complete. + StatusRebooting Status = "rebooting" + + // The agent is running a hook or action. The human-readable message should reflect + // which hook or action is being run. + StatusExecuting Status = "executing" + + // Once the agent is installed and running it will notify the Juju server and its state + // becomes "idle". It will stay "idle" until some action (e.g. it needs to run a hook) or + // error (e.g it loses contact with the Juju server) moves it to a different state. + StatusIdle Status = "idle" + + // The unit agent has failed in some way,eg the agent ought to be signalling + // activity, but it cannot be detected. It might also be that the unit agent + // detected an unrecoverable condition and managed to tell the Juju server about it. + StatusFailed Status = "failed" + + // The juju agent has has not communicated with the juju server for an unexpectedly long time; + // the unit agent ought to be signalling activity, but none has been detected. + StatusLost Status = "lost" + + // ---- Outdated ---- + // The unit agent is downloading the charm and running the install hook. + StatusInstalling Status = "installing" + + // The unit is being destroyed; the agent will soon mark the unit as “dead”. + // In Juju 2.x this will describe the state of the agent rather than a unit. + StatusStopping Status = "stopping" +) + +const ( + // Status values specific to services and units, reflecting the + // state of the software itself. + + // The unit is not yet providing services, but is actively doing stuff + // in preparation for providing those services. + // This is a "spinning" state, not an error state. + // It reflects activity on the unit itself, not on peers or related units. + StatusMaintenance Status = "maintenance" + + // This unit used to exist, we have a record of it (perhaps because of storage + // allocated for it that was flagged to survive it). Nonetheless, it is now gone. + StatusTerminated Status = "terminated" + + // A unit-agent has finished calling install, config-changed, and start, + // but the charm has not called status-set yet. + StatusUnknown Status = "unknown" + + // The unit is unable to progress to an active state because a service to + // which it is related is not running. + StatusWaiting Status = "waiting" + + // The unit needs manual intervention to get back to the Running state. + StatusBlocked Status = "blocked" + + // The unit believes it is correctly offering all the services it has + // been asked to offer. + StatusActive Status = "active" +) + +const ( + // Status values specific to storage. + + // StatusAttaching indicates that the storage is being attached + // to a machine. + StatusAttaching Status = "attaching" + + // StatusAttached indicates that the storage is attached to a + // machine. + StatusAttached Status = "attached" + + // StatusDetaching indicates that the storage is being detached + // from a machine. + StatusDetaching Status = "detaching" + + // StatusDetached indicates that the storage is not attached to + // any machine. + StatusDetached Status = "detached" + + // StatusDestroying indicates that the storage is being destroyed. + StatusDestroying Status = "destroying" +) === modified file 'src/github.com/juju/juju/apiserver/params/storage.go' --- src/github.com/juju/juju/apiserver/params/storage.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/params/storage.go 2015-10-23 18:29:32 +0000 @@ -192,6 +192,7 @@ // VolumeAttachmentInfo describes a volume attachment. type VolumeAttachmentInfo struct { DeviceName string `json:"devicename,omitempty"` + DeviceLink string `json:"devicelink,omitempty"` BusAddress string `json:"busaddress,omitempty"` ReadOnly bool `json:"read-only,omitempty"` } @@ -216,6 +217,7 @@ type VolumeAttachmentParams struct { VolumeTag string `json:"volumetag"` MachineTag string `json:"machinetag"` + VolumeId string `json:"volumeid,omitempty"` InstanceId string `json:"instanceid,omitempty"` Provider string `json:"provider"` ReadOnly bool `json:"read-only,omitempty"` @@ -334,6 +336,7 @@ type FilesystemAttachmentParams struct { FilesystemTag string `json:"filesystemtag"` MachineTag string `json:"machinetag"` + FilesystemId string `json:"filesystemid,omitempty"` InstanceId string `json:"instanceid,omitempty"` Provider string `json:"provider"` MountPoint string `json:"mountpoint,omitempty"` @@ -389,7 +392,33 @@ // StorageDetails holds information about storage. type StorageDetails struct { - + // StorageTag holds tag for this storage. + StorageTag string `json:"storagetag"` + + // OwnerTag holds tag for the owner of this storage, unit or service. + OwnerTag string `json:"ownertag"` + + // Kind holds what kind of storage this instance is. + Kind StorageKind `json:"kind"` + + // Status contains the status of the storage instance. + Status EntityStatus `json:"status"` + + // Persistent reports whether or not the underlying volume or + // filesystem is persistent; i.e. whether or not it outlives + // the machine that it is attached to. + Persistent bool + + // Attachments contains a mapping from unit tag to + // storage attachment details. + Attachments map[string]StorageAttachmentDetails `json:"attachments,omitempty"` +} + +// LegacyStorageDetails holds information about storage. +// +// NOTE(axw): this is for backwards compatibility only. This struct +// should not be changed! +type LegacyStorageDetails struct { // StorageTag holds tag for this storage. StorageTag string `json:"storagetag"` @@ -415,8 +444,9 @@ // StorageDetailsResult holds information about a storage instance // or error related to its retrieval. type StorageDetailsResult struct { - Result StorageDetails `json:"result"` - Error *Error `json:"error,omitempty"` + Result *StorageDetails `json:"details,omitempty"` + Legacy LegacyStorageDetails `json:"result"` + Error *Error `json:"error,omitempty"` } // StorageDetailsResults holds results for storage details or related storage error. @@ -424,16 +454,20 @@ Results []StorageDetailsResult `json:"results,omitempty"` } -// StorageInfo contains information about a storage as well as -// potentially an error related to information retrieval. -type StorageInfo struct { - StorageDetails `json:"result"` - Error *Error `json:"error,omitempty"` -} - -// StorageInfosResult holds storage details. -type StorageInfosResult struct { - Results []StorageInfo `json:"results,omitempty"` +// StorageAttachmentDetails holds detailed information about a storage attachment. +type StorageAttachmentDetails struct { + // StorageTag is the tag of the storage instance. + StorageTag string `json:"storagetag"` + + // UnitTag is the tag of the unit attached to the storage instance. + UnitTag string `json:"unittag"` + + // MachineTag is the tag of the machine that the attached unit is assigned to. + MachineTag string `json:"machinetag"` + + // Location holds location (mount point/device path) of + // the attached storage. + Location string `json:"location,omitempty"` } // StoragePool holds data for a pool instance. @@ -475,28 +509,47 @@ return len(f.Machines) == 0 } -// VolumeInstance describes a storage volume in the environment -// for the purpose of volume CLI commands. -// It is kept separate from Volume which is primarily used in uniter -// and may answer different concerns as well as serve different purposes. -type VolumeInstance struct { +// VolumeDetails describes a storage volume in the environment +// for the purpose of volume CLI commands. +// +// This is kept separate from Volume which contains only information +// specific to the volume model, whereas VolumeDetails is intended +// to contain complete information about a volume and related +// information (status, attachments, storage). +type VolumeDetails struct { + + // VolumeTag is the tag for the volume. + VolumeTag string `json:"volumetag"` + + // Info contains information about the volume. + Info VolumeInfo `json:"info"` + + // Status contains the status of the volume. + Status EntityStatus `json:"status"` + + // MachineAttachments contains a mapping from + // machine tag to volume attachment information. + MachineAttachments map[string]VolumeAttachmentInfo `json:"machineattachments,omitempty"` + + // Storage contains details about the storage instance + // that the volume is assigned to, if any. + Storage *StorageDetails `json:"storage,omitempty"` +} + +// LegacyVolumeDetails describes a storage volume in the environment +// for the purpose of volume CLI commands. +// +// This is kept separate from Volume which contains only information +// specific to the volume model, whereas LegacyVolumeDetails is intended +// to contain complete information about a volume. +// +// NOTE(axw): this is for backwards compatibility only. This struct +// should not be changed! +type LegacyVolumeDetails struct { // VolumeTag is tag for this volume instance. VolumeTag string `json:"volumetag"` - // VolumeId is a unique provider-supplied ID for the volume. - VolumeId string `json:"volumeid"` - - // HardwareId is the volume's hardware ID. - HardwareId string `json:"hardwareid,omitempty"` - - // Size is the size of the volume in MiB. - Size uint64 `json:"size"` - - // Persistent reflects whether the volume is destroyed with the - // machine to which it is attached. - Persistent bool `json:"persistent"` - // StorageInstance returns the tag of the storage instance that this // volume is assigned to, if any. StorageTag string `json:"storage,omitempty"` @@ -504,24 +557,54 @@ // UnitTag is the tag of the unit attached to storage instance // for this volume. UnitTag string `json:"unit,omitempty"` + + // VolumeId is a unique provider-supplied ID for the volume. + VolumeId string `json:"volumeid,omitempty"` + + // HardwareId is the volume's hardware ID. + HardwareId string `json:"hardwareid,omitempty"` + + // Size is the size of the volume in MiB. + Size uint64 `json:"size,omitempty"` + + // Persistent reflects whether the volume is destroyed with the + // machine to which it is attached. + Persistent bool `json:"persistent"` + + // Status contains the current status of the volume. + Status EntityStatus `json:"status"` } -// VolumeItem contain volume, its attachments -// and retrieval error. -type VolumeItem struct { - // Volume is storage volume. - Volume VolumeInstance `json:"volume,omitempty"` - - // Attachments are storage volume attachments. - Attachments []VolumeAttachment `json:"attachments,omitempty"` +// VolumeDetailsResult contains details about a volume, its attachments or +// an error preventing retrieving those details. +type VolumeDetailsResult struct { + + // Details describes the volume in detail. + Details *VolumeDetails `json:"details,omitempty"` + + // LegacyVolume describes the volume in detail. + // + // NOTE(axw): VolumeDetails contains redundant and nonsensical + // information. Use Details if it is available, and only use + // this for backwards-compatibility. + LegacyVolume *LegacyVolumeDetails `json:"volume,omitempty"` + + // LegacyAttachments describes the attachments of the volume to + // machines. + // + // NOTE(axw): this should have gone into VolumeDetails, but it's too + // late for that now. We'll continue to populate it, and use it + // if it's defined but Volume.Attachments is not. Please do not + // copy this structure. + LegacyAttachments []VolumeAttachment `json:"attachments,omitempty"` // Error contains volume retrieval error. Error *Error `json:"error,omitempty"` } -// VolumeItemsResult holds volumes. -type VolumeItemsResult struct { - Results []VolumeItem `json:"results,omitempty"` +// VolumeDetailsResults holds volume details. +type VolumeDetailsResults struct { + Results []VolumeDetailsResult `json:"results,omitempty"` } // StorageConstraints contains constraints for storage instance. === added file 'src/github.com/juju/juju/apiserver/params/systemmanager.go' --- src/github.com/juju/juju/apiserver/params/systemmanager.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/params/systemmanager.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,38 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package params + +// DestroySystemArgs holds the arguments for destroying a system. +type DestroySystemArgs struct { + // DestroyEnvironments specifies whether or not the hosted environments + // should be destroyed as well. If this is not specified, and there are + // other hosted environments, the destruction of the system will fail. + DestroyEnvironments bool `json:"destroy-environments"` + + // IgnoreBlocks specifies whether or not to ignore blocks + // on hosted environments. + IgnoreBlocks bool `json:"ignore-blocks"` +} + +// EnvironmentBlockInfo holds information about an environment and its +// current blocks. +type EnvironmentBlockInfo struct { + Name string `json:"name"` + UUID string `json:"env-uuid"` + OwnerTag string `json:"owner-tag"` + Blocks []string `json:"blocks"` +} + +// EnvironmentBlockInfoList holds information about the blocked environments +// for a system. +type EnvironmentBlockInfoList struct { + Environments []EnvironmentBlockInfo `json:"environments,omitempty"` +} + +// RemoveBlocksArgs holds the arguments for the RemoveBlocks command. It is a +// struct to facilitate the easy addition of being able to remove blocks for +// individual environments at a later date. +type RemoveBlocksArgs struct { + All bool `json:"all"` +} === modified file 'src/github.com/juju/juju/apiserver/pinger.go' --- src/github.com/juju/juju/apiserver/pinger.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/pinger.go 2015-10-23 18:29:32 +0000 @@ -20,11 +20,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 *common.Resources, authorizer common.Authorizer) (Pinger, error) { pingTimeout, ok := resources.Get("pingTimeout").(*pingTimeout) if !ok { return nullPinger{}, nil @@ -32,9 +28,10 @@ return pingTimeout, nil } -// pinger describes a type that can be pinged. -type pinger interface { +// pinger describes a resource that can be pinged and stopped. +type Pinger interface { Ping() + Stop() error } // pingTimeout listens for pings and will call the @@ -44,18 +41,18 @@ tomb tomb.Tomb action func() timeout time.Duration - reset chan struct{} + reset chan time.Duration } // 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) *pingTimeout { +func newPingTimeout(action func(), timeout time.Duration) Pinger { pt := &pingTimeout{ action: action, timeout: timeout, - reset: make(chan struct{}), + reset: make(chan time.Duration), } go func() { defer pt.tomb.Done() @@ -69,7 +66,7 @@ func (pt *pingTimeout) Ping() { select { case <-pt.tomb.Dying(): - case pt.reset <- struct{}{}: + case pt.reset <- pt.timeout: } } @@ -82,7 +79,7 @@ // loop waits for a reset signal, otherwise it performs // the initially passed action. func (pt *pingTimeout) loop() error { - timer := newTimer(pt.timeout) + timer := time.NewTimer(pt.timeout) defer timer.Stop() for { select { @@ -91,23 +88,14 @@ case <-timer.C: go pt.action() return errors.New("ping timeout") - case <-pt.reset: - resetTimer(timer, pt.timeout) + case duration := <-pt.reset: + timer.Reset(duration) } } } -// newTimer is patched out during some tests. -var newTimer = func(d time.Duration) *time.Timer { - return time.NewTimer(d) -} - -// resetTimer is patched out during some tests. -var resetTimer = func(timer *time.Timer, d time.Duration) bool { - return timer.Reset(d) -} - // nullPinger implements the pinger interface but just does nothing type nullPinger struct{} -func (nullPinger) Ping() {} +func (nullPinger) Ping() {} +func (nullPinger) Stop() error { return nil } === modified file 'src/github.com/juju/juju/apiserver/pinger_test.go' --- src/github.com/juju/juju/apiserver/pinger_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/pinger_test.go 2015-10-23 18:29:32 +0000 @@ -94,36 +94,83 @@ c.Assert(err, gc.ErrorMatches, "connection is shut down") } +func (s *pingerSuite) calculatePingTimeout(c *gc.C) time.Duration { + // Try opening an API connection a few times and take the max + // delay among the attempts. + attempt := utils.AttemptStrategy{ + Delay: coretesting.ShortWait, + Min: 3, + } + var maxTimeout time.Duration + for a := attempt.Start(); a.Next(); { + openStart := time.Now() + st, _ := s.OpenAPIAsNewMachine(c) + err := st.Ping() + if c.Check(err, jc.ErrorIsNil) { + openDelay := time.Since(openStart) + c.Logf("API open and initial ping took %v", openDelay) + if maxTimeout < openDelay { + maxTimeout = openDelay + } + } + if st != nil { + c.Check(st.Close(), jc.ErrorIsNil) + } + } + if !c.Failed() && maxTimeout > 0 { + return maxTimeout + } + c.Fatalf("cannot calculate ping timeout") + return 0 +} + func (s *pingerSuite) TestAgentConnectionDelaysShutdownWithPing(c *gc.C) { - var resetCount int - s.PatchValue(apiserver.ResetTimer, func(timer *time.Timer, d time.Duration) bool { - resetCount += 1 - return timer.Reset(d) - }) - // We patch out NewTimer so that we can call Reset on the timer - // right before we check the failure case below. - var timer *time.Timer - s.PatchValue(apiserver.NewTimer, func(d time.Duration) *time.Timer { - timer = time.NewTimer(d) - return timer - }) + // To negate the effects of an underpowered or heavily loaded + // machine running this test, tune the shortTimeout based on the + // maximum duration it takes to open an API connection. + shortTimeout := s.calculatePingTimeout(c) + attemptDelay := shortTimeout / 4 + + s.PatchValue(apiserver.MaxClientPingInterval, time.Duration(shortTimeout)) st, _ := s.OpenAPIAsNewMachine(c) err := st.Ping() c.Assert(err, jc.ErrorIsNil) + defer st.Close() // As long as we don't wait too long, the connection stays open - resetCount = 0 - const shortTimeout = 10 * time.Millisecond - for i := 0; i < 10; i++ { - time.Sleep(shortTimeout / 4) + attempt := utils.AttemptStrategy{ + Min: 10, + Delay: attemptDelay, + } + testStart := time.Now() + c.Logf( + "pinging %d times with %v delay, ping timeout %v, starting at %v", + attempt.Min, attempt.Delay, shortTimeout, testStart, + ) + var lastLoop time.Time + for a := attempt.Start(); a.Next(); { + testNow := time.Now() + loopDelta := testNow.Sub(lastLoop) + if lastLoop.IsZero() { + loopDelta = 0 + } + c.Logf("duration since last ping: %v", loopDelta) err = st.Ping() - c.Assert(err, jc.ErrorIsNil) + if !c.Check( + err, jc.ErrorIsNil, + gc.Commentf( + "ping timeout exceeded at %v (%v since the test start)", + testNow, testNow.Sub(testStart), + ), + ) { + c.Check(err, gc.ErrorMatches, "connection is shut down") + return + } + lastLoop = time.Now() } - c.Check(resetCount, gc.Equals, 10) // However, once we stop pinging for too long, the connection dies - timer.Reset(shortTimeout) time.Sleep(2 * shortTimeout) // Exceed the timeout. err = st.Ping() c.Assert(err, gc.ErrorMatches, "connection is shut down") === modified file 'src/github.com/juju/juju/apiserver/provisioner/container_test.go' --- src/github.com/juju/juju/apiserver/provisioner/container_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/provisioner/container_test.go 2015-10-23 18:29:32 +0000 @@ -4,6 +4,7 @@ package provisioner_test import ( + "encoding/hex" "fmt" "strings" @@ -140,6 +141,8 @@ // Check for any "regex:" prefixes first. Then replace // addresses in expected with the actual ones, so we can use // jc.DeepEquals on the whole result below. + // Also check MAC addresses are valid, but as they're randomly + // generated we can't test specific values. for i, expect := range expectResults.Results { cfg := results.Results[i].Config c.Assert(cfg, gc.HasLen, len(expect.Config)) @@ -149,8 +152,16 @@ c.Assert(cfg[j].Address, gc.Matches, rex) expectResults.Results[i].Config[j].Address = cfg[j].Address } + macAddress := cfg[j].MACAddress + c.Assert(macAddress[:8], gc.Equals, provisioner.MACAddressTemplate[:8]) + remainder := strings.Replace(macAddress[8:], ":", "", 3) + c.Assert(remainder, gc.HasLen, 6) + _, err = hex.DecodeString(remainder) + c.Assert(err, jc.ErrorIsNil) + expectResults.Results[i].Config[j].MACAddress = macAddress } } + c.Assert(results, jc.DeepEquals, *expectResults) } else { c.Assert(err, gc.ErrorMatches, expectErr) @@ -407,7 +418,7 @@ // Record each time allocateAddrTo, setAddrsTo, and setAddrState // are called along with the addresses to verify the logs later. var allocAttemptedAddrs, allocAddrsOK, setAddrs, releasedAddrs []string - s.PatchValue(provisioner.AllocateAddrTo, func(ip *state.IPAddress, m *state.Machine) error { + s.PatchValue(provisioner.AllocateAddrTo, func(ip *state.IPAddress, m *state.Machine, mac string) error { c.Logf("allocateAddrTo called for address %q, machine %q", ip.String(), m) c.Assert(m.Id(), gc.Equals, container.Id()) allocAttemptedAddrs = append(allocAttemptedAddrs, ip.Value()) @@ -512,7 +523,6 @@ DeviceIndex: 0, InterfaceName: "eth0", VLANTag: 0, - MACAddress: "aa:bb:cc:dd:ee:f0", Disabled: false, NoAutoStart: false, ConfigType: "static", @@ -594,7 +604,6 @@ DeviceIndex: 0, InterfaceName: "eth0", VLANTag: 0, - MACAddress: "aa:bb:cc:dd:ee:f0", Disabled: false, NoAutoStart: false, ConfigType: "static", @@ -631,7 +640,6 @@ DeviceIndex: 1, InterfaceName: "eth1", VLANTag: 1, - MACAddress: "aa:bb:cc:dd:ee:f1", Disabled: false, NoAutoStart: true, ConfigType: "static", @@ -795,7 +803,7 @@ addr := network.NewAddress(fmt.Sprintf("0.10.0.%d", i)) ipaddr, err := s.BackingState.AddIPAddress(addr, sub.ID()) c.Check(err, jc.ErrorIsNil) - err = ipaddr.AllocateTo(containerId, "") + err = ipaddr.AllocateTo(containerId, "", "") c.Check(err, jc.ErrorIsNil) } } === modified file 'src/github.com/juju/juju/apiserver/provisioner/provisioner.go' --- src/github.com/juju/juju/apiserver/provisioner/provisioner.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/provisioner/provisioner.go 2015-10-23 18:29:32 +0000 @@ -5,6 +5,7 @@ import ( "fmt" + "math/rand" "sort" "strings" @@ -412,12 +413,8 @@ if err != nil { return nil, errors.Trace(err) } - // TODO(dimitern) For now, since network names and - // provider ids are the same, we return what we got - // from state. In the future, when networks can be - // added before provisioning, we should convert both - // slices from juju network names to provider-specific - // ids before returning them. + // TODO(dimitern) Drop this once we only use spaces for + // deployments. networks, err := m.RequestedNetworks() if err != nil { return nil, err @@ -430,14 +427,19 @@ if err != nil { return nil, errors.Trace(err) } + subnetsToZones, err := p.machineSubnetsAndZones(m) + if err != nil { + return nil, errors.Annotate(err, "cannot match subnets to zones") + } return ¶ms.ProvisioningInfo{ - Constraints: cons, - Series: m.Series(), - Placement: m.Placement(), - Networks: networks, - Jobs: jobs, - Volumes: volumes, - Tags: tags, + Constraints: cons, + Series: m.Series(), + Placement: m.Placement(), + Networks: networks, + Jobs: jobs, + Volumes: volumes, + Tags: tags, + SubnetsToZones: subnetsToZones, }, nil } @@ -584,10 +586,7 @@ return nil, errors.Annotatef(err, "getting volume %q storage instance", volumeTag.Id()) } volumeParams, err := common.VolumeParams(volume, storageInstance, envConfig, poolManager) - if common.IsVolumeAlreadyProvisioned(err) { - // Already provisioned, so must be dynamic. - continue - } else if err != nil { + if err != nil { return nil, errors.Annotatef(err, "getting volume %q parameters", volumeTag.Id()) } provider, err := registry.StorageProvider(storage.ProviderType(volumeParams.Provider)) @@ -611,6 +610,7 @@ volumeParams.Attachment = ¶ms.VolumeAttachmentParams{ volumeTag.String(), m.Tag().String(), + "", // we're creating the volume, so it has no volume ID. "", // we're creating the machine, so it has no instance ID. volumeParams.Provider, volumeAttachmentParams.ReadOnly, @@ -654,6 +654,7 @@ } m[volumeTag] = state.VolumeAttachmentInfo{ v.Info.DeviceName, + v.Info.DeviceLink, v.Info.BusAddress, v.Info.ReadOnly, } @@ -911,6 +912,21 @@ return p.prepareOrGetContainerInterfaceInfo(args, false) } +// MACAddressTemplate is used to generate a unique MAC address for a +// container. Every '%x' is replaced by a random hexadecimal digit, +// while the rest is kept as-is. +const MACAddressTemplate = "00:16:3e:%02x:%02x:%02x" + +// generateMACAddress creates a random MAC address within the space defined by +// MACAddressTemplate above. +func generateMACAddress() string { + digits := make([]interface{}, 3) + for i := range digits { + digits[i] = rand.Intn(256) + } + return fmt.Sprintf(MACAddressTemplate, digits...) +} + // prepareOrGetContainerInterfaceInfo optionally allocates an address and returns information // for configuring networking on a container. It accepts container tags as arguments. func (p *ProvisionerAPI) prepareOrGetContainerInterfaceInfo( @@ -974,11 +990,12 @@ continue } - var addresses []*state.IPAddress + var macAddress string + var address *state.IPAddress if provisionContainer { // Allocate and set address. - addr, err := p.allocateAddress(environ, subnet, host, container, instId) - addresses = append(addresses, addr) + macAddress = generateMACAddress() + address, err = p.allocateAddress(environ, subnet, host, container, instId, macAddress) if err != nil { err = errors.Annotatef(err, "failed to allocate an address for %q", container) result.Results[i].Error = common.ServerError(err) @@ -986,7 +1003,7 @@ } } else { id := container.Id() - addresses, err = p.st.AllocatedIPAddresses(id) + addresses, err := p.st.AllocatedIPAddresses(id) if err != nil { logger.Warningf("failed to get Id for container %q: %v", tag, err) result.Results[i].Error = common.ServerError(err) @@ -1001,19 +1018,25 @@ result.Results[i].Error = common.ServerError(err) continue } + address = addresses[0] + macAddress = address.MACAddress() } // Store it on the machine, construct and set an interface result. dnsServers := make([]string, len(interfaceInfo.DNSServers)) for i, dns := range interfaceInfo.DNSServers { dnsServers[i] = dns.Value } + + if macAddress == "" { + macAddress = interfaceInfo.MACAddress + } // TODO(dimitern): Support allocating one address per NIC on // the host, effectively creating the same number of NICs in // the container. result.Results[i] = params.MachineNetworkConfigResult{ Config: []params.NetworkConfig{{ DeviceIndex: interfaceInfo.DeviceIndex, - MACAddress: interfaceInfo.MACAddress, + MACAddress: macAddress, CIDR: subnetInfo.CIDR, NetworkName: interfaceInfo.NetworkName, ProviderId: string(interfaceInfo.ProviderId), @@ -1024,7 +1047,7 @@ NoAutoStart: interfaceInfo.NoAutoStart, DNSServers: dnsServers, ConfigType: string(network.ConfigStatic), - Address: addresses[0].Value(), + Address: address.Value(), // container's gateway is the host's primary NIC's IP. GatewayAddress: interfaceInfo.Address.Value, ExtraConfig: interfaceInfo.ExtraConfig, @@ -1160,9 +1183,9 @@ // These are defined like this to allow mocking in tests. var ( - allocateAddrTo = func(a *state.IPAddress, m *state.Machine) error { + allocateAddrTo = func(a *state.IPAddress, m *state.Machine, macAddress string) error { // TODO(mfoord): populate proper interface ID (in state). - return a.AllocateTo(m.Id(), "") + return a.AllocateTo(m.Id(), "", macAddress) } setAddrsTo = func(a *state.IPAddress, m *state.Machine) error { return m.SetProviderAddresses(a.Address()) @@ -1179,9 +1202,11 @@ subnet *state.Subnet, host, container *state.Machine, instId instance.Id, + macAddress string, ) (*state.IPAddress, error) { subnetId := network.Id(subnet.ProviderId()) + name := names.NewMachineTag(container.Id()).String() for { addr, err := subnet.PickNewAddress() if err != nil { @@ -1189,7 +1214,7 @@ } logger.Tracef("picked new address %q on subnet %q", addr.String(), subnetId) // Attempt to allocate with environ. - err = environ.AllocateAddress(instId, subnetId, addr.Address()) + err = environ.AllocateAddress(instId, subnetId, addr.Address(), macAddress, name) if err != nil { logger.Warningf( "allocating address %q on instance %q and subnet %q failed: %v (retrying)", @@ -1215,7 +1240,7 @@ "allocated address %q on instance %q and subnet %q", addr.String(), instId, subnetId, ) - err = p.setAllocatedOrRelease(addr, environ, instId, container, subnetId) + err = p.setAllocatedOrRelease(addr, environ, instId, container, subnetId, macAddress) if err != nil { // Something went wrong - retry. continue @@ -1233,6 +1258,7 @@ instId instance.Id, container *state.Machine, subnetId network.Id, + macAddress string, ) (err error) { defer func() { if errors.Cause(err) == nil { @@ -1252,7 +1278,7 @@ addr.String(), state.AddressStateUnavailable, err, ) } - err = environ.ReleaseAddress(instId, subnetId, addr.Address()) + err = environ.ReleaseAddress(instId, subnetId, addr.Address(), addr.MACAddress()) if err == nil { logger.Infof("address %q released; trying to allocate new", addr.String()) return @@ -1265,7 +1291,7 @@ // Any errors returned below will trigger the release/cleanup // steps above. - if err = allocateAddrTo(addr, container); err != nil { + if err = allocateAddrTo(addr, container, macAddress); err != nil { return errors.Trace(err) } if err = setAddrsTo(addr, container); err != nil { @@ -1326,3 +1352,59 @@ } return machineTags, nil } + +// machineSubnetsAndZones returns a map of subnet provider-specific id +// to list of availability zone names for that subnet. The result can +// be empty if there are no spaces constraints specified for the +// machine, or there's an error fetching them. +func (p *ProvisionerAPI) machineSubnetsAndZones(m *state.Machine) (map[string][]string, error) { + mcons, err := m.Constraints() + if err != nil { + return nil, errors.Annotate(err, "cannot get machine constraints") + } + includeSpaces := mcons.IncludeSpaces() + if len(includeSpaces) < 1 { + // Nothing to do. + return nil, nil + } + // TODO(dimitern): For the network model MVP we only use the first + // included space and ignore the rest. + spaceName := includeSpaces[0] + if len(includeSpaces) > 1 { + logger.Debugf( + "using space %q from constraints for machine %q (ignoring remaining: %v)", + spaceName, m.Id(), includeSpaces[1:], + ) + } + space, err := p.st.Space(spaceName) + if err != nil { + return nil, errors.Trace(err) + } + subnets, err := space.Subnets() + if err != nil { + return nil, errors.Trace(err) + } + subnetsToZones := make(map[string][]string, len(subnets)) + for _, subnet := range subnets { + warningPrefix := fmt.Sprintf( + "not using subnet %q in space %q for machine %q provisioning: ", + subnet.CIDR(), spaceName, m.Id(), + ) + // TODO(dimitern): state.Subnet.ProviderId needs to be of type + // network.Id. + providerId := subnet.ProviderId() + if providerId == "" { + logger.Warningf(warningPrefix + "no ProviderId set") + continue + } + // TODO(dimitern): Once state.Subnet supports multiple zones, + // use all of them below. + zone := subnet.AvailabilityZone() + if zone == "" { + logger.Warningf(warningPrefix + "no availability zone(s) set") + continue + } + subnetsToZones[providerId] = []string{zone} + } + return subnetsToZones, nil +} === modified file 'src/github.com/juju/juju/apiserver/provisioner/provisioner_test.go' --- src/github.com/juju/juju/apiserver/provisioner/provisioner_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/provisioner/provisioner_test.go 2015-10-23 18:29:32 +0000 @@ -331,7 +331,7 @@ c.Assert(err, jc.ErrorIsNil) args := params.SetStatus{ - Entities: []params.EntityStatus{ + Entities: []params.EntityStatusArgs{ {Tag: s.machines[0].Tag().String(), Status: params.StatusError, Info: "not really", Data: map[string]interface{}{"foo": "bar"}}, {Tag: s.machines[1].Tag().String(), Status: params.StatusStopped, Info: "foobar"}, @@ -743,21 +743,38 @@ } func (s *withoutStateServerSuite) TestProvisioningInfo(c *gc.C) { + // Add a couple of spaces. + _, err := s.State.AddSpace("space1", nil, true) + c.Assert(err, jc.ErrorIsNil) + _, err = s.State.AddSpace("space2", nil, false) + c.Assert(err, jc.ErrorIsNil) + // Add 1 subnet into space1, and 2 into space2. + // Only the first subnet of space2 has AllocatableIPLow|High set. + // Each subnet is in a matching zone (e.g "subnet-#" in "zone#"). + testing.AddSubnetsWithTemplate(c, s.State, 3, state.SubnetInfo{ + CIDR: "10.10.{{.}}.0/24", + ProviderId: "subnet-{{.}}", + AllocatableIPLow: "{{if (eq . 1)}}10.10.{{.}}.5{{end}}", + AllocatableIPHigh: "{{if (eq . 1)}}10.10.{{.}}.254{{end}}", + AvailabilityZone: "zone{{.}}", + SpaceName: "{{if (eq . 0)}}space1{{else}}space2{{end}}", + VLANTag: 42, + }) + registry.RegisterProvider("static", &storagedummy.StorageProvider{IsDynamic: false}) defer registry.RegisterProvider("static", nil) registry.RegisterEnvironStorageProviders("dummy", "static") pm := poolmanager.New(state.NewStateSettings(s.State)) - _, err := pm.Create("static-pool", "static", map[string]interface{}{"foo": "bar"}) + _, err = pm.Create("static-pool", "static", map[string]interface{}{"foo": "bar"}) c.Assert(err, jc.ErrorIsNil) - cons := constraints.MustParse("cpu-cores=123 mem=8G networks=^net3,^net4") + cons := constraints.MustParse("cpu-cores=123 mem=8G spaces=^space1,space2") template := state.MachineTemplate{ - Series: "quantal", - Jobs: []state.MachineJob{state.JobHostUnits}, - Constraints: cons, - Placement: "valid", - RequestedNetworks: []string{"net1", "net2"}, + Series: "quantal", + Jobs: []state.MachineJob{state.JobHostUnits}, + Constraints: cons, + Placement: "valid", Volumes: []state.MachineVolumeParams{ {Volume: state.VolumeParams{Size: 1000, Pool: "static-pool"}}, {Volume: state.VolumeParams{Size: 2000, Pool: "static-pool"}}, @@ -795,6 +812,10 @@ Tags: map[string]string{ tags.JujuEnv: coretesting.EnvironmentTag.Id(), }, + SubnetsToZones: map[string][]string{ + "subnet-1": []string{"zone1"}, + "subnet-2": []string{"zone2"}, + }, Volumes: []params.VolumeParams{{ VolumeTag: "volume-0", Size: 1000, @@ -828,8 +849,9 @@ {Error: apiservertesting.ErrUnauthorized}, }, } - // The order of volumes is not predictable, so we make sure we compare the right ones. This only - // applies to Results[1] since it is the only result to contain volumes. + // The order of volumes is not predictable, so we make sure we + // compare the right ones. This only applies to Results[1] since + // it is the only result to contain volumes. if expected.Results[1].Result.Volumes[0].VolumeTag != result.Results[1].Result.Volumes[0].VolumeTag { vols := expected.Results[1].Result.Volumes vols[0], vols[1] = vols[1], vols[0] === modified file 'src/github.com/juju/juju/apiserver/restricted_root.go' --- src/github.com/juju/juju/apiserver/restricted_root.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/restricted_root.go 2015-10-23 18:29:32 +0000 @@ -26,7 +26,9 @@ // of the API server. Any facade added here needs to work across environment // boundaries. var restrictedRootNames = set.NewStrings( + "AllEnvWatcher", "EnvironmentManager", + "SystemManager", "UserManager", ) === modified file 'src/github.com/juju/juju/apiserver/restricted_root_test.go' --- src/github.com/juju/juju/apiserver/restricted_root_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/restricted_root_test.go 2015-10-23 18:29:32 +0000 @@ -35,12 +35,20 @@ } func (r *restrictedRootSuite) TestFindAllowedMethod(c *gc.C) { + r.assertMethodAllowed(c, "AllEnvWatcher", 1, "Next") + r.assertMethodAllowed(c, "AllEnvWatcher", 1, "Stop") + r.assertMethodAllowed(c, "EnvironmentManager", 1, "CreateEnvironment") r.assertMethodAllowed(c, "EnvironmentManager", 1, "ListEnvironments") r.assertMethodAllowed(c, "UserManager", 0, "AddUser") r.assertMethodAllowed(c, "UserManager", 0, "SetPassword") r.assertMethodAllowed(c, "UserManager", 0, "UserInfo") + + r.assertMethodAllowed(c, "SystemManager", 1, "AllEnvironments") + r.assertMethodAllowed(c, "SystemManager", 1, "DestroySystem") + r.assertMethodAllowed(c, "SystemManager", 1, "EnvironmentConfig") + r.assertMethodAllowed(c, "SystemManager", 1, "ListBlockedEnvironments") } func (r *restrictedRootSuite) TestFindDisallowedMethod(c *gc.C) { === added directory 'src/github.com/juju/juju/apiserver/resumer' === added file 'src/github.com/juju/juju/apiserver/resumer/export_test.go' --- src/github.com/juju/juju/apiserver/resumer/export_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/resumer/export_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,20 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package resumer + +import ( + "github.com/juju/juju/state" +) + +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 + }) +} === added file 'src/github.com/juju/juju/apiserver/resumer/package_test.go' --- src/github.com/juju/juju/apiserver/resumer/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/resumer/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,14 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package resumer_test + +import ( + "testing" + + gc "gopkg.in/check.v1" +) + +func TestPackage(t *testing.T) { + gc.TestingT(t) +} === added file 'src/github.com/juju/juju/apiserver/resumer/resumer.go' --- src/github.com/juju/juju/apiserver/resumer/resumer.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/resumer/resumer.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,40 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +// The resumer package implements the API interface +// used by the resumer worker. +package resumer + +import ( + "github.com/juju/loggo" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/state" +) + +func init() { + common.RegisterStandardFacade("Resumer", 1, NewResumerAPI) +} + +var logger = loggo.GetLogger("juju.apiserver.resumer") + +// ResumerAPI implements the API used by the resumer worker. +type ResumerAPI struct { + st stateInterface + auth common.Authorizer +} + +// NewResumerAPI creates a new instance of the Resumer API. +func NewResumerAPI(st *state.State, _ *common.Resources, authorizer common.Authorizer) (*ResumerAPI, error) { + if !authorizer.AuthEnvironManager() { + return nil, common.ErrPerm + } + return &ResumerAPI{ + st: getState(st), + auth: authorizer, + }, nil +} + +func (api *ResumerAPI) ResumeTransactions() error { + return api.st.ResumeTransactions() +} === added file 'src/github.com/juju/juju/apiserver/resumer/resumer_test.go' --- src/github.com/juju/juju/apiserver/resumer/resumer_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/resumer/resumer_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,76 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package resumer_test + +import ( + "errors" + + "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/resumer" + apiservertesting "github.com/juju/juju/apiserver/testing" + coretesting "github.com/juju/juju/testing" +) + +type ResumerSuite struct { + coretesting.BaseSuite + + st *mockState + api *resumer.ResumerAPI + authoriser apiservertesting.FakeAuthorizer +} + +var _ = gc.Suite(&ResumerSuite{}) + +func (s *ResumerSuite) SetUpTest(c *gc.C) { + s.BaseSuite.SetUpTest(c) + + s.authoriser = apiservertesting.FakeAuthorizer{ + EnvironManager: true, + } + s.st = &mockState{&testing.Stub{}} + resumer.PatchState(s, s.st) + var err error + s.api, err = resumer.NewResumerAPI(nil, nil, s.authoriser) + c.Assert(err, jc.ErrorIsNil) +} + +func (s *ResumerSuite) TestNewResumerAPIRequiresEnvironManager(c *gc.C) { + anAuthoriser := s.authoriser + anAuthoriser.EnvironManager = false + api, err := resumer.NewResumerAPI(nil, nil, anAuthoriser) + c.Assert(api, gc.IsNil) + c.Assert(err, gc.ErrorMatches, "permission denied") +} + +func (s *ResumerSuite) TestResumeTransactionsFailure(c *gc.C) { + s.st.SetErrors(errors.New("boom!")) + + err := s.api.ResumeTransactions() + c.Assert(err, gc.ErrorMatches, "boom!") + s.st.CheckCalls(c, []testing.StubCall{{ + FuncName: "ResumeTransactions", + Args: nil, + }}) +} + +func (s *ResumerSuite) TestResumeTransactionsSuccess(c *gc.C) { + err := s.api.ResumeTransactions() + c.Assert(err, jc.ErrorIsNil) + s.st.CheckCalls(c, []testing.StubCall{{ + FuncName: "ResumeTransactions", + Args: nil, + }}) +} + +type mockState struct { + *testing.Stub +} + +func (st *mockState) ResumeTransactions() error { + st.MethodCall(st, "ResumeTransactions") + return st.NextErr() +} === added file 'src/github.com/juju/juju/apiserver/resumer/state.go' --- src/github.com/juju/juju/apiserver/resumer/state.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/resumer/state.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,20 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package resumer + +import ( + "github.com/juju/juju/state" +) + +type stateInterface interface { + ResumeTransactions() error +} + +type stateShim struct { + *state.State +} + +var getState = func(st *state.State) stateInterface { + return stateShim{st} +} === modified file 'src/github.com/juju/juju/apiserver/root.go' --- src/github.com/juju/juju/apiserver/root.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/root.go 2015-10-23 18:29:32 +0000 @@ -42,11 +42,10 @@ // after it has logged in. It contains an rpc.MethodFinder which it // uses to dispatch Api calls appropriately. type apiHandler struct { - state *state.State - closeState bool - rpcConn *rpc.Conn - resources *common.Resources - entity state.Entity + state *state.State + rpcConn *rpc.Conn + resources *common.Resources + entity state.Entity // An empty envUUID means that the user has logged in through the // root of the API server rather than the /environment/:env-uuid/api // path, logins processed with v2 or later will only offer the @@ -59,11 +58,10 @@ // newApiHandler returns a new apiHandler. func newApiHandler(srv *Server, st *state.State, rpcConn *rpc.Conn, reqNotifier *requestNotifier, envUUID string) (*apiHandler, error) { r := &apiHandler{ - state: st, - closeState: st.EnvironUUID() != srv.state.EnvironUUID(), - resources: common.NewResources(), - rpcConn: rpcConn, - envUUID: envUUID, + state: st, + resources: common.NewResources(), + rpcConn: rpcConn, + envUUID: envUUID, } if err := r.resources.RegisterNamed("machineID", common.StringResource(srv.tag.Id())); err != nil { return nil, errors.Trace(err) @@ -91,14 +89,6 @@ r.resources.StopAll() } -// Cleanup implements rpc.Cleaner, closing the handler's State instance -// if required. -func (r *apiHandler) Cleanup() { - if r.closeState { - r.state.Close() - } -} - // srvCaller is our implementation of the rpcreflect.MethodCaller interface. // It lives just long enough to encapsulate the methods that should be // available for an RPC call and allow the RPC code to instantiate an object @@ -134,7 +124,6 @@ // apiRoot implements basic method dispatching to the facade registry. type apiRoot struct { state *state.State - closeState bool resources *common.Resources authorizer common.Authorizer objectMutex sync.RWMutex @@ -142,10 +131,9 @@ } // newApiRoot returns a new apiRoot. -func newApiRoot(st *state.State, closeState bool, resources *common.Resources, authorizer common.Authorizer) *apiRoot { +func newApiRoot(st *state.State, resources *common.Resources, authorizer common.Authorizer) *apiRoot { r := &apiRoot{ state: st, - closeState: closeState, resources: resources, authorizer: authorizer, objectCache: make(map[objectKey]reflect.Value), @@ -158,14 +146,6 @@ r.resources.StopAll() } -// Cleanup implements rpc.Cleaner, closing the root's State instance if -// required. -func (r *apiRoot) Cleanup() { - if r.closeState { - r.state.Close() - } -} - // FindMethod looks up the given rootName and version in our facade registry // and returns a MethodCaller that will be used by the RPC code to place calls on // that facade. @@ -340,16 +320,3 @@ } return result } - -type stateResource struct { - state *state.State -} - -func (s stateResource) Stop() error { - logger.Debugf("close state connection: %s", s.state.EnvironUUID()) - return s.state.Close() -} - -func (s stateResource) String() string { - return s.state.EnvironUUID() -} === modified file 'src/github.com/juju/juju/apiserver/server_test.go' --- src/github.com/juju/juju/apiserver/server_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/server_test.go 2015-10-23 18:29:32 +0000 @@ -9,7 +9,6 @@ "fmt" "io" "net" - "strconv" stdtesting "testing" "time" @@ -61,12 +60,15 @@ machine, password := s.Factory.MakeMachineReturningPassword( c, &factory.MachineParams{Nonce: "fake_nonce"}) + // A net.TCPAddr cannot be directly stringified into a valid hostname. + address := fmt.Sprintf("localhost:%d", srv.Addr().Port) + // Note we can't use openAs because we're not connecting to apiInfo := &api.Info{ Tag: machine.Tag(), Password: password, Nonce: "fake_nonce", - Addrs: []string{srv.Addr()}, + Addrs: []string{address}, CACert: coretesting.CACert, EnvironTag: s.State.EnvironTag(), } @@ -109,17 +111,8 @@ c.Assert(err, jc.ErrorIsNil) defer srv.Stop() - // srv.Addr() always reports "localhost" together - // with the port as address. This way it can be used - // as hostname to construct URLs which will work - // for both IPv4 and IPv6-only networks, as - // localhost resolves as both 127.0.0.1 and ::1. - // Retrieve the port as string and integer. - hostname, portString, err := net.SplitHostPort(srv.Addr()) - c.Assert(err, jc.ErrorIsNil) - c.Assert(hostname, gc.Equals, "localhost") - port, err := strconv.Atoi(portString) - c.Assert(err, jc.ErrorIsNil) + port := srv.Addr().Port + portString := fmt.Sprintf("%d", port) machine, password := s.Factory.MakeMachineReturningPassword( c, &factory.MachineParams{Nonce: "fake_nonce"}) @@ -290,10 +283,7 @@ defer srv.Stop() // We have to use 'localhost' because that is what the TLS cert says. - // So find just the Port for the server - _, portString, err := net.SplitHostPort(srv.Addr()) - c.Assert(err, jc.ErrorIsNil) - addr := "localhost:" + portString + addr := fmt.Sprintf("localhost:%d", srv.Addr().Port) // '/' should be fine conn, err := dialWebsocket(c, addr, "/") c.Assert(err, jc.ErrorIsNil) @@ -320,64 +310,3 @@ r.stopped = true return nil } - -func (s *serverSuite) TestRootTeardown(c *gc.C) { - s.checkRootTeardown(c, false) -} - -func (s *serverSuite) TestRootTeardownClosingState(c *gc.C) { - s.checkRootTeardown(c, true) -} - -func (s *serverSuite) checkRootTeardown(c *gc.C, closeState bool) { - root, resources := apiserver.TestingApiRootEx(s.State, closeState) - resource := new(fakeResource) - resources.Register(resource) - - c.Assert(resource.stopped, jc.IsFalse) - root.Kill() - c.Assert(resource.stopped, jc.IsTrue) - - assertStateIsOpen(c, s.State) - root.Cleanup() - if closeState { - assertStateIsClosed(c, s.State) - } else { - assertStateIsOpen(c, s.State) - } -} - -func (s *serverSuite) TestApiHandlerTeardownInitialEnviron(c *gc.C) { - s.checkApiHandlerTeardown(c, s.State, s.State) -} - -func (s *serverSuite) TestApiHandlerTeardownOtherEnviron(c *gc.C) { - otherState := s.Factory.MakeEnvironment(c, nil) - s.checkApiHandlerTeardown(c, s.State, otherState) -} - -func (s *serverSuite) checkApiHandlerTeardown(c *gc.C, srvSt, st *state.State) { - handler, resources := apiserver.TestingApiHandler(c, srvSt, st) - resource := new(fakeResource) - resources.Register(resource) - - c.Assert(resource.stopped, jc.IsFalse) - handler.Kill() - c.Assert(resource.stopped, jc.IsTrue) - - assertStateIsOpen(c, st) - handler.Cleanup() - if srvSt == st { - assertStateIsOpen(c, st) - } else { - assertStateIsClosed(c, st) - } -} - -func assertStateIsOpen(c *gc.C, st *state.State) { - c.Assert(st.Ping(), jc.ErrorIsNil) -} - -func assertStateIsClosed(c *gc.C, st *state.State) { - c.Assert(func() { st.Ping() }, gc.PanicMatches, "Session already closed") -} === modified file 'src/github.com/juju/juju/apiserver/service/service.go' --- src/github.com/juju/juju/apiserver/service/service.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/service/service.go 2015-10-23 18:29:32 +0000 @@ -83,6 +83,12 @@ // ServicesDeploy fetches the charms from the charm store and deploys them. func (api *API) ServicesDeploy(args params.ServicesDeploy) (params.ErrorResults, error) { + return api.ServicesDeployWithPlacement(args) +} + +// ServicesDeployWithPlacement fetches the charms from the charm store and deploys them +// using the specified placement directives. +func (api *API) ServicesDeployWithPlacement(args params.ServicesDeploy) (params.ErrorResults, error) { result := params.ErrorResults{ Results: make([]params.ErrorResult, len(args.Services)), } @@ -109,7 +115,8 @@ return errors.Errorf("charm url must include revision") } - if args.ToMachineSpec != "" && names.IsValidMachine(args.ToMachineSpec) { + // Do a quick but not complete validation check before going any further. + if len(args.Placement) == 0 && args.ToMachineSpec != "" && names.IsValidMachine(args.ToMachineSpec) { _, err = st.Machine(args.ToMachineSpec) if err != nil { return errors.Annotatef(err, `cannot deploy "%v" to machine %v`, args.ServiceName, args.ToMachineSpec) @@ -159,6 +166,7 @@ ConfigSettings: settings, Constraints: args.Constraints, ToMachineSpec: args.ToMachineSpec, + Placement: args.Placement, Networks: requestedNetworks, Storage: args.Storage, }) === modified file 'src/github.com/juju/juju/apiserver/service/service_test.go' --- src/github.com/juju/juju/apiserver/service/service_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/service/service_test.go 2015-10-23 18:29:32 +0000 @@ -22,6 +22,7 @@ "github.com/juju/juju/apiserver/service" apiservertesting "github.com/juju/juju/apiserver/testing" "github.com/juju/juju/constraints" + "github.com/juju/juju/instance" jujutesting "github.com/juju/juju/juju/testing" "github.com/juju/juju/state" statestorage "github.com/juju/juju/state/storage" @@ -176,7 +177,7 @@ options = map[string]string{ "yummy": "didgeridoo", } - settings, err = service.ParseSettingsCompatible(ch, options) + _, err = service.ParseSettingsCompatible(ch, options) c.Assert(err, gc.ErrorMatches, `unknown option "yummy"`) } @@ -321,6 +322,56 @@ }) } +func (s *serviceSuite) TestClientServiceDeployWithPlacement(c *gc.C) { + curl, ch := s.UploadCharm(c, "precise/dummy-42", "dummy") + err := service.AddCharmWithAuthorization(s.State, params.AddCharmWithAuthorization{URL: curl.String()}) + c.Assert(err, jc.ErrorIsNil) + var cons constraints.Value + args := params.ServiceDeploy{ + ServiceName: "service", + CharmUrl: curl.String(), + NumUnits: 1, + Constraints: cons, + Placement: []*instance.Placement{ + {"deadbeef-0bad-400d-8000-4b1d0d06f00d", "valid"}, + }, + ToMachineSpec: "will be ignored", + } + results, err := s.serviceApi.ServicesDeploy(params.ServicesDeploy{ + Services: []params.ServiceDeploy{args}}, + ) + c.Assert(err, jc.ErrorIsNil) + c.Assert(results, gc.DeepEquals, params.ErrorResults{ + Results: []params.ErrorResult{{Error: nil}}, + }) + svc := apiservertesting.AssertPrincipalServiceDeployed(c, s.State, "service", curl, false, ch, cons) + units, err := svc.AllUnits() + c.Assert(err, jc.ErrorIsNil) + c.Assert(units, gc.HasLen, 1) +} + +func (s *serviceSuite) TestClientServiceDeployWithInvalidPlacement(c *gc.C) { + curl, _ := s.UploadCharm(c, "precise/dummy-42", "dummy") + err := service.AddCharmWithAuthorization(s.State, params.AddCharmWithAuthorization{URL: curl.String()}) + c.Assert(err, jc.ErrorIsNil) + var cons constraints.Value + args := params.ServiceDeploy{ + ServiceName: "service", + CharmUrl: curl.String(), + NumUnits: 1, + Constraints: cons, + Placement: []*instance.Placement{ + {"deadbeef-0bad-400d-8000-4b1d0d06f00d", "invalid"}, + }, + } + results, err := s.serviceApi.ServicesDeploy(params.ServicesDeploy{ + Services: []params.ServiceDeploy{args}}, + ) + c.Assert(err, jc.ErrorIsNil) + c.Assert(results.Results, gc.HasLen, 1) + c.Assert(results.Results[0].Error.Error(), gc.Matches, ".* invalid placement is invalid") +} + // TODO(wallyworld) - the following charm tests have been moved from the apiserver/client // package in order to use the fake charm store testing infrastructure. They are legacy tests // written to use the api client instead of the apiserver logic. They need to be rewritten and === added directory 'src/github.com/juju/juju/apiserver/spaces' === added file 'src/github.com/juju/juju/apiserver/spaces/export_test.go' --- src/github.com/juju/juju/apiserver/spaces/export_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/spaces/export_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,6 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package spaces + +var NewAPIWithBacking = newAPIWithBacking === added file 'src/github.com/juju/juju/apiserver/spaces/package_test.go' --- src/github.com/juju/juju/apiserver/spaces/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/spaces/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,14 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package spaces_test + +import ( + stdtesting "testing" + + gc "gopkg.in/check.v1" +) + +func TestPackage(t *stdtesting.T) { + gc.TestingT(t) +} === added file 'src/github.com/juju/juju/apiserver/spaces/shims.go' --- src/github.com/juju/juju/apiserver/spaces/shims.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/spaces/shims.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,106 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package spaces + +import ( + "github.com/juju/errors" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/environs/config" + "github.com/juju/juju/state" +) + +// NOTE: All of the following code is only tested with a feature test. + +// subnetShim forwards and adapts state.Subnets methods to +// common.BackingSubnet. +type subnetShim struct { + common.BackingSubnet + subnet *state.Subnet +} + +func (s *subnetShim) CIDR() string { + return s.subnet.CIDR() +} + +func (s *subnetShim) VLANTag() int { + return s.subnet.VLANTag() +} + +func (s *subnetShim) ProviderId() string { + return s.subnet.ProviderId() +} + +func (s *subnetShim) AvailabilityZones() []string { + // TODO(dimitern): Add multiple zones to state.Subnet. + return []string{s.subnet.AvailabilityZone()} +} + +func (s *subnetShim) Life() params.Life { + return params.Life(s.subnet.Life().String()) +} + +func (s *subnetShim) Status() string { + // TODO(dimitern): This should happen in a cleaner way. + if s.Life() != params.Alive { + return "terminating" + } + return "in-use" +} + +func (s *subnetShim) SpaceName() string { + return s.subnet.SpaceName() +} + +// spaceShim forwards and adapts state.Space methods to BackingSpace. +type spaceShim struct { + common.BackingSpace + space *state.Space +} + +func (s *spaceShim) Name() string { + return s.space.Name() +} + +func (s *spaceShim) Subnets() ([]common.BackingSubnet, error) { + results, err := s.space.Subnets() + if err != nil { + return nil, errors.Trace(err) + } + subnets := make([]common.BackingSubnet, len(results)) + for i, result := range results { + subnets[i] = &subnetShim{subnet: result} + } + return subnets, nil +} + +// stateShim forwards and adapts state.State methods to Backing +// method. +type stateShim struct { + Backing + st *state.State +} + +func (s *stateShim) EnvironConfig() (*config.Config, error) { + return s.st.EnvironConfig() +} + +func (s *stateShim) AddSpace(name string, subnetIds []string, public bool) error { + _, err := s.st.AddSpace(name, subnetIds, public) + return err +} + +func (s *stateShim) AllSpaces() ([]common.BackingSpace, error) { + // TODO(dimitern): Make this ListSpaces() instead. + results, err := s.st.AllSpaces() + if err != nil { + return nil, errors.Trace(err) + } + spaces := make([]common.BackingSpace, len(results)) + for i, result := range results { + spaces[i] = &spaceShim{space: result} + } + return spaces, nil +} === added file 'src/github.com/juju/juju/apiserver/spaces/spaces.go' --- src/github.com/juju/juju/apiserver/spaces/spaces.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/spaces/spaces.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,192 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package spaces + +import ( + "github.com/juju/errors" + "github.com/juju/loggo" + "github.com/juju/names" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/environs" + "github.com/juju/juju/environs/config" + "github.com/juju/juju/state" +) + +var logger = loggo.GetLogger("juju.apiserver.spaces") + +func init() { + common.RegisterStandardFacade("Spaces", 1, NewAPI) +} + +// API defines the methods the Spaces API facade implements. +type API interface { + CreateSpaces(params.CreateSpacesParams) (params.ErrorResults, error) + ListSpaces() (params.ListSpacesResults, error) +} + +// Backing defines the state methods this facede needs, so they can be +// mocked for testing. +type Backing interface { + // EnvironConfig returns the configuration of the environment. + EnvironConfig() (*config.Config, error) + + // AddSpace creates a space. + AddSpace(name string, subnetIds []string, public bool) error + + // AllSpaces returns all known Juju network spaces. + AllSpaces() ([]common.BackingSpace, error) +} + +// spacesAPI implements the API interface. +type spacesAPI struct { + backing Backing + resources *common.Resources + authorizer common.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) { + return newAPIWithBacking(&stateShim{st: st}, res, auth) +} + +// newAPIWithBacking creates a new server-side Spaces API facade with +// the given Backing. +func newAPIWithBacking(backing Backing, resources *common.Resources, authorizer common.Authorizer) (API, error) { + // Only clients can access the Spaces facade. + if !authorizer.AuthClient() { + return nil, common.ErrPerm + } + return &spacesAPI{ + backing: backing, + resources: resources, + authorizer: authorizer, + }, nil +} + +// CreateSpaces creates a new Juju network space, associating the +// specified subnets with it (optional; can be empty). +func (api *spacesAPI) CreateSpaces(args params.CreateSpacesParams) (results params.ErrorResults, err error) { + err = api.supportsSpaces() + if err != nil { + return results, common.ServerError(errors.Trace(err)) + } + + results.Results = make([]params.ErrorResult, len(args.Spaces)) + + for i, space := range args.Spaces { + err := api.createOneSpace(space) + if err == nil { + continue + } + results.Results[i].Error = common.ServerError(errors.Trace(err)) + } + + return results, nil +} + +func (api *spacesAPI) createOneSpace(args params.CreateSpaceParams) error { + // Validate the args, assemble information for api.backing.AddSpaces + var subnets []string + + spaceTag, err := names.ParseSpaceTag(args.SpaceTag) + if err != nil { + return errors.Trace(err) + } + + for _, tag := range args.SubnetTags { + subnetTag, err := names.ParseSubnetTag(tag) + if err != nil { + return errors.Trace(err) + } + subnets = append(subnets, subnetTag.Id()) + } + + // Add the validated space + err = api.backing.AddSpace(spaceTag.Id(), subnets, args.Public) + if err != nil { + return errors.Trace(err) + } + return nil +} + +func backingSubnetToParamsSubnet(subnet common.BackingSubnet) params.Subnet { + cidr := subnet.CIDR() + vlantag := subnet.VLANTag() + providerid := subnet.ProviderId() + zones := subnet.AvailabilityZones() + status := subnet.Status() + var spaceTag names.SpaceTag + if subnet.SpaceName() != "" { + spaceTag = names.NewSpaceTag(subnet.SpaceName()) + } + + return params.Subnet{ + CIDR: cidr, + VLANTag: vlantag, + ProviderId: providerid, + Zones: zones, + Status: status, + SpaceTag: spaceTag.String(), + Life: subnet.Life(), + } +} + +// ListSpaces lists all the available spaces and their associated subnets. +func (api *spacesAPI) ListSpaces() (results params.ListSpacesResults, err error) { + err = api.supportsSpaces() + if err != nil { + return results, common.ServerError(errors.Trace(err)) + } + + spaces, err := api.backing.AllSpaces() + if err != nil { + return results, errors.Trace(err) + } + + results.Results = make([]params.Space, len(spaces)) + for i, space := range spaces { + result := params.Space{} + result.Name = space.Name() + + subnets, err := space.Subnets() + if err != nil { + err = errors.Annotatef(err, "fetching subnets") + result.Error = common.ServerError(err) + results.Results[i] = result + continue + } + + result.Subnets = make([]params.Subnet, len(subnets)) + for i, subnet := range subnets { + result.Subnets[i] = backingSubnetToParamsSubnet(subnet) + } + results.Results[i] = result + } + return results, nil +} + +// supportsSpaces checks if the environment implements NetworkingEnviron +// and also if it supports spaces. +func (api *spacesAPI) supportsSpaces() error { + config, err := api.backing.EnvironConfig() + if err != nil { + return errors.Annotate(err, "getting environment config") + } + env, err := environs.New(config) + if err != nil { + return errors.Annotate(err, "validating environment config") + } + netEnv, ok := environs.SupportsNetworking(env) + if !ok { + return errors.NotSupportedf("networking") + } + ok, err = netEnv.SupportsSpaces() + if err != nil { + logger.Warningf("environment does not support spaces: %v", err) + } + return err +} === added file 'src/github.com/juju/juju/apiserver/spaces/spaces_test.go' --- src/github.com/juju/juju/apiserver/spaces/spaces_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/spaces/spaces_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,349 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package spaces_test + +import ( + "fmt" + + "github.com/juju/errors" + "github.com/juju/names" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/apiserver/spaces" + apiservertesting "github.com/juju/juju/apiserver/testing" + coretesting "github.com/juju/juju/testing" +) + +type SpacesSuite struct { + coretesting.BaseSuite + apiservertesting.StubNetwork + + resources *common.Resources + authorizer apiservertesting.FakeAuthorizer + facade spaces.API +} + +var _ = gc.Suite(&SpacesSuite{}) + +func (s *SpacesSuite) SetUpSuite(c *gc.C) { + s.StubNetwork.SetUpSuite(c) + s.BaseSuite.SetUpSuite(c) +} + +func (s *SpacesSuite) TearDownSuite(c *gc.C) { + s.BaseSuite.TearDownSuite(c) +} + +func (s *SpacesSuite) SetUpTest(c *gc.C) { + s.BaseSuite.SetUpTest(c) + apiservertesting.BackingInstance.SetUp(c, apiservertesting.StubZonedNetworkingEnvironName, apiservertesting.WithZones, apiservertesting.WithSpaces, apiservertesting.WithSubnets) + + s.resources = common.NewResources() + s.authorizer = apiservertesting.FakeAuthorizer{ + Tag: names.NewUserTag("admin"), + EnvironManager: false, + } + + var err error + s.facade, err = spaces.NewAPIWithBacking( + apiservertesting.BackingInstance, s.resources, s.authorizer, + ) + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.facade, gc.NotNil) +} + +func (s *SpacesSuite) TearDownTest(c *gc.C) { + if s.resources != nil { + s.resources.StopAll() + } + s.BaseSuite.TearDownTest(c) +} + +func (s *SpacesSuite) TestNewAPIWithBacking(c *gc.C) { + // Clients are allowed. + facade, err := spaces.NewAPIWithBacking( + apiservertesting.BackingInstance, s.resources, s.authorizer, + ) + c.Assert(err, jc.ErrorIsNil) + c.Assert(facade, gc.NotNil) + // No calls so far. + apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub) + + // Agents are not allowed + agentAuthorizer := s.authorizer + agentAuthorizer.Tag = names.NewMachineTag("42") + facade, err = spaces.NewAPIWithBacking( + apiservertesting.BackingInstance, s.resources, agentAuthorizer, + ) + c.Assert(err, jc.DeepEquals, common.ErrPerm) + c.Assert(facade, gc.IsNil) + // No calls so far. + apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub) +} + +type checkAddSpacesParams struct { + Name string + Subnets []string + Error string + MakesCall bool + Public bool +} + +func (s *SpacesSuite) checkAddSpaces(c *gc.C, p checkAddSpacesParams) { + args := params.CreateSpaceParams{} + if p.Name != "" { + args.SpaceTag = "space-" + p.Name + } + if len(p.Subnets) > 0 { + for _, cidr := range p.Subnets { + args.SubnetTags = append(args.SubnetTags, "subnet-"+cidr) + } + } + args.Public = p.Public + + spaces := params.CreateSpacesParams{} + spaces.Spaces = append(spaces.Spaces, args) + results, err := s.facade.CreateSpaces(spaces) + + c.Assert(len(results.Results), gc.Equals, 1) + c.Assert(err, gc.IsNil) + if p.Error == "" { + c.Assert(results.Results[0].Error, gc.IsNil) + } else { + c.Assert(results.Results[0].Error, gc.NotNil) + c.Assert(results.Results[0].Error, gc.ErrorMatches, p.Error) + } + + baseCalls := []apiservertesting.StubMethodCall{ + apiservertesting.BackingCall("EnvironConfig"), + apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), + apiservertesting.ZonedNetworkingEnvironCall("SupportsSpaces"), + } + addSpaceCalls := append(baseCalls, apiservertesting.BackingCall("AddSpace", p.Name, p.Subnets, p.Public)) + + if p.Error == "" || p.MakesCall { + apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub, addSpaceCalls...) + } else { + apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub, baseCalls...) + } +} + +func (s *SpacesSuite) TestAddSpacesOneSubnet(c *gc.C) { + p := checkAddSpacesParams{ + Name: "foo", + Subnets: []string{"10.0.0.0/24"}, + } + s.checkAddSpaces(c, p) +} + +func (s *SpacesSuite) TestAddSpacesTwoSubnets(c *gc.C) { + p := checkAddSpacesParams{ + Name: "foo", + Subnets: []string{"10.0.0.0/24", "10.0.1.0/24"}, + } + s.checkAddSpaces(c, p) +} + +func (s *SpacesSuite) TestAddSpacesManySubnets(c *gc.C) { + p := checkAddSpacesParams{ + Name: "foo", + Subnets: []string{"10.0.0.0/24", "10.0.1.0/24", "10.0.2.0/24", + "10.0.3.0/24", "10.0.4.0/24", "10.0.5.0/24", "10.0.6.0/24"}, + } + s.checkAddSpaces(c, p) +} + +func (s *SpacesSuite) TestAddSpacesAPIError(c *gc.C) { + apiservertesting.SharedStub.SetErrors( + nil, // Backing.EnvironConfig() + nil, // Provider.Open() + nil, // ZonedNetworkingEnviron.SupportsSpaces() + errors.AlreadyExistsf("space-foo"), // Backing.AddSpace() + ) + p := checkAddSpacesParams{ + Name: "foo", + Subnets: []string{"10.0.0.0/24"}, + MakesCall: true, + Error: "space-foo already exists", + } + s.checkAddSpaces(c, p) +} + +func (s *SpacesSuite) TestCreateInvalidSpace(c *gc.C) { + p := checkAddSpacesParams{ + Name: "-", + Subnets: []string{"10.0.0.0/24"}, + Error: `"space--" is not a valid space tag`, + } + s.checkAddSpaces(c, p) +} + +func (s *SpacesSuite) TestCreateInvalidSubnet(c *gc.C) { + p := checkAddSpacesParams{ + Name: "foo", + Subnets: []string{"bar"}, + Error: `"subnet-bar" is not a valid subnet tag`, + } + s.checkAddSpaces(c, p) +} + +func (s *SpacesSuite) TestPublic(c *gc.C) { + p := checkAddSpacesParams{ + Name: "foo", + Subnets: []string{"10.0.0.0/24"}, + Public: true, + } + s.checkAddSpaces(c, p) +} + +func (s *SpacesSuite) TestEmptySpaceName(c *gc.C) { + p := checkAddSpacesParams{ + Subnets: []string{"10.0.0.0/24"}, + Error: `"" is not a valid tag`, + } + s.checkAddSpaces(c, p) +} + +func (s *SpacesSuite) TestNoSubnets(c *gc.C) { + p := checkAddSpacesParams{ + Name: "foo", + Subnets: nil, + } + s.checkAddSpaces(c, p) +} + +func (s *SpacesSuite) TestListSpacesDefault(c *gc.C) { + expected := []params.Space{{ + Name: "default", + Subnets: []params.Subnet{{ + CIDR: "192.168.0.0/24", + ProviderId: "provider-192.168.0.0/24", + Zones: []string{"foo"}, + Status: "in-use", + SpaceTag: "space-default", + }, { + CIDR: "192.168.3.0/24", + ProviderId: "provider-192.168.3.0/24", + VLANTag: 23, + Zones: []string{"bar", "bam"}, + SpaceTag: "space-default", + }}, + }, { + Name: "dmz", + Subnets: []params.Subnet{{ + CIDR: "192.168.1.0/24", + ProviderId: "provider-192.168.1.0/24", + VLANTag: 23, + Zones: []string{"bar", "bam"}, + SpaceTag: "space-dmz", + }}, + }, { + Name: "private", + Subnets: []params.Subnet{{ + CIDR: "192.168.2.0/24", + ProviderId: "provider-192.168.2.0/24", + Zones: []string{"foo"}, + Status: "in-use", + SpaceTag: "space-private", + }}, + }} + spaces, err := s.facade.ListSpaces() + c.Assert(err, jc.ErrorIsNil) + c.Assert(spaces.Results, jc.DeepEquals, expected) +} + +func (s *SpacesSuite) TestListSpacesAllSpacesError(c *gc.C) { + boom := errors.New("backing boom") + apiservertesting.BackingInstance.SetErrors(boom) + _, err := s.facade.ListSpaces() + c.Assert(err, gc.ErrorMatches, "getting environment config: backing boom") +} + +func (s *SpacesSuite) TestListSpacesSubnetsError(c *gc.C) { + apiservertesting.SharedStub.SetErrors( + nil, // Backing.EnvironConfig() + nil, // Provider.Open() + nil, // ZonedNetworkingEnviron.SupportsSpaces() + nil, // Backing.AllSpaces() + errors.New("space0 subnets failed"), // Space.Subnets() + errors.New("space1 subnets failed"), // Space.Subnets() + errors.New("space2 subnets failed"), // Space.Subnets() + ) + + results, err := s.facade.ListSpaces() + for i, space := range results.Results { + errmsg := fmt.Sprintf("fetching subnets: space%d subnets failed", i) + c.Assert(space.Error, gc.ErrorMatches, errmsg) + } + c.Assert(err, jc.ErrorIsNil) +} + +func (s *SpacesSuite) TestListSpacesSubnetsSingleSubnetError(c *gc.C) { + boom := errors.New("boom") + apiservertesting.SharedStub.SetErrors( + nil, // Backing.EnvironConfig() + nil, // Provider.Open() + nil, // ZonedNetworkingEnviron.SupportsSpaces() + nil, // Backing.AllSpaces() + nil, // Space.Subnets() (1st no error) + boom, // Space.Subnets() (2nd with error) + ) + + results, err := s.facade.ListSpaces() + for i, space := range results.Results { + if i == 1 { + c.Assert(space.Error, gc.ErrorMatches, "fetching subnets: boom") + } else { + c.Assert(space.Error, gc.IsNil) + } + } + c.Assert(err, jc.ErrorIsNil) +} + +func (s *SpacesSuite) TestCreateSpacesEnvironConfigError(c *gc.C) { + apiservertesting.SharedStub.SetErrors( + errors.New("boom"), // Backing.EnvironConfig() + ) + + spaces := params.CreateSpacesParams{} + _, err := s.facade.CreateSpaces(spaces) + c.Assert(err, gc.ErrorMatches, "getting environment config: boom") +} + +func (s *SpacesSuite) TestCreateSpacesProviderOpenError(c *gc.C) { + apiservertesting.SharedStub.SetErrors( + nil, // Backing.EnvironConfig() + errors.New("boom"), // Provider.Open() + ) + + spaces := params.CreateSpacesParams{} + _, err := s.facade.CreateSpaces(spaces) + c.Assert(err, gc.ErrorMatches, "validating environment config: boom") +} + +func (s *SpacesSuite) TestCreateSpacesNotSupportedError(c *gc.C) { + apiservertesting.SharedStub.SetErrors( + nil, // Backing.EnvironConfig() + nil, // Provider.Open() + errors.NotSupportedf("spaces"), // ZonedNetworkingEnviron.SupportsSpaces() + ) + + spaces := params.CreateSpacesParams{} + _, err := s.facade.CreateSpaces(spaces) + c.Assert(err, gc.ErrorMatches, "spaces not supported") +} + +func (s *SpacesSuite) TestListSpacesNotSupportedError(c *gc.C) { + apiservertesting.SharedStub.SetErrors( + nil, // Backing.EnvironConfig() + nil, // Provider.Open + errors.NotSupportedf("spaces"), // ZonedNetworkingEnviron.SupportsSpaces() + ) + + _, err := s.facade.ListSpaces() + c.Assert(err, gc.ErrorMatches, "spaces not supported") +} === modified file 'src/github.com/juju/juju/apiserver/storage/export_test.go' --- src/github.com/juju/juju/apiserver/storage/export_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/storage/export_test.go 2015-10-23 18:29:32 +0000 @@ -4,18 +4,9 @@ package storage var ( - IsValidPoolListFilter = (*API).isValidPoolListFilter - ValidateNames = (*API).isValidNameCriteria - ValidateProviders = (*API).isValidProviderCriteria - CreateVolumeItem = (*API).createVolumeItem - GetVolumeItems = (*API).getVolumeItems - FilterVolumes = (*API).filterVolumes - VolumeAttachments = (*API).volumeAttachments - ListVolumeAttachments = (*API).listVolumeAttachments - ConvertStateVolumeToParams = (*API).convertStateVolumeToParams + IsValidPoolListFilter = (*API).isValidPoolListFilter + ValidateNames = (*API).isValidNameCriteria + ValidateProviders = (*API).isValidProviderCriteria - CreateAPI = createAPI - GroupAttachmentsByVolume = groupAttachmentsByVolume - ConvertStateVolumeAttachmentToParams = convertStateVolumeAttachmentToParams - ConvertStateVolumeAttachmentsToParams = convertStateVolumeAttachmentsToParams + CreateAPI = createAPI ) === modified file 'src/github.com/juju/juju/apiserver/storage/package_test.go' --- src/github.com/juju/juju/apiserver/storage/package_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/storage/package_test.go 2015-10-23 18:29:32 +0000 @@ -40,8 +40,8 @@ machineTag names.MachineTag volumeTag names.VolumeTag - volume state.Volume - volumeAttachment state.VolumeAttachment + volume *mockVolume + volumeAttachment *mockVolumeAttachment calls []string poolManager *mockPoolManager @@ -51,6 +51,7 @@ } func (s *baseStorageSuite) SetUpTest(c *gc.C) { + s.BaseSuite.SetUpTest(c) s.resources = common.NewResources() s.authorizer = testing.FakeAuthorizer{names.NewUserTag("testuser"), true} s.calls = []string{} @@ -82,6 +83,7 @@ allVolumesCall = "allVolumes" addStorageForUnitCall = "addStorageForUnit" getBlockForTypeCall = "getBlockForType" + volumeAttachmentCall = "volumeAttachment" ) func (s *baseStorageSuite) constructState(c *gc.C) *mockState { @@ -139,6 +141,10 @@ c.Assert(t, gc.DeepEquals, s.storageTag) return s.volume, nil }, + volumeAttachment: func(names.MachineTag, names.VolumeTag) (state.VolumeAttachment, error) { + s.calls = append(s.calls, volumeAttachmentCall) + return s.volumeAttachment, nil + }, unitAssignedMachine: func(u names.UnitTag) (names.MachineTag, error) { s.calls = append(s.calls, unitAssignedMachineCall) c.Assert(u, gc.DeepEquals, s.unitTag) @@ -177,7 +183,10 @@ } func (s *baseStorageSuite) addBlock(c *gc.C, t state.BlockType, msg string) { - s.blocks[t] = mockBlock{t, msg} + s.blocks[t] = mockBlock{ + t: t, + msg: msg, + } } func (s *baseStorageSuite) blockAllChanges(c *gc.C, msg string) { @@ -255,12 +264,13 @@ storageInstanceAttachments func(names.StorageTag) ([]state.StorageAttachment, error) unitAssignedMachine func(u names.UnitTag) (names.MachineTag, error) storageInstanceVolume func(names.StorageTag) (state.Volume, error) - storageInstanceVolumeAttachment func(names.MachineTag, names.VolumeTag) (state.VolumeAttachment, error) + volumeAttachment func(names.MachineTag, names.VolumeTag) (state.VolumeAttachment, error) storageInstanceFilesystem func(names.StorageTag) (state.Filesystem, error) storageInstanceFilesystemAttachment func(m names.MachineTag, f names.FilesystemTag) (state.FilesystemAttachment, error) watchStorageAttachment func(names.StorageTag, names.UnitTag) state.NotifyWatcher watchFilesystemAttachment func(names.MachineTag, names.FilesystemTag) state.NotifyWatcher watchVolumeAttachment func(names.MachineTag, names.VolumeTag) state.NotifyWatcher + watchBlockDevices func(names.MachineTag) state.NotifyWatcher envName string volume func(tag names.VolumeTag) (state.Volume, error) machineVolumeAttachments func(machine names.MachineTag) ([]state.VolumeAttachment, error) @@ -268,6 +278,7 @@ allVolumes func() ([]state.Volume, error) addStorageForUnit func(u names.UnitTag, name string, cons state.StorageConstraints) error getBlockForType func(t state.BlockType) (state.Block, bool, error) + blockDevices func(names.MachineTag) ([]state.BlockDeviceInfo, error) } func (st *mockState) StorageInstance(s names.StorageTag) (state.StorageInstance, error) { @@ -299,7 +310,7 @@ } func (st *mockState) VolumeAttachment(m names.MachineTag, v names.VolumeTag) (state.VolumeAttachment, error) { - return st.storageInstanceVolumeAttachment(m, v) + return st.volumeAttachment(m, v) } func (st *mockState) WatchStorageAttachment(s names.StorageTag, u names.UnitTag) state.NotifyWatcher { @@ -314,6 +325,10 @@ return st.watchVolumeAttachment(mtag, v) } +func (st *mockState) WatchBlockDevices(mtag names.MachineTag) state.NotifyWatcher { + return st.watchBlockDevices(mtag) +} + func (st *mockState) EnvName() (string, error) { return st.envName, nil } @@ -342,6 +357,13 @@ return st.getBlockForType(t) } +func (st *mockState) BlockDevices(m names.MachineTag) ([]state.BlockDeviceInfo, error) { + if st.blockDevices != nil { + return st.blockDevices(m) + } + return []state.BlockDeviceInfo{}, nil +} + type mockNotifyWatcher struct { state.NotifyWatcher changes chan struct{} @@ -356,6 +378,7 @@ tag names.VolumeTag storage names.StorageTag hasNoStorage bool + info *state.VolumeInfo } func (m *mockVolume) StorageInstance() (names.StorageTag, error) { @@ -377,9 +400,16 @@ } func (m *mockVolume) Info() (state.VolumeInfo, error) { + if m.info != nil { + return *m.info, nil + } return state.VolumeInfo{}, errors.NotProvisionedf("%v", m.tag) } +func (m *mockVolume) Status() (state.StatusInfo, error) { + return state.StatusInfo{Status: state.StatusAttached}, nil +} + type mockFilesystem struct { state.Filesystem tag names.FilesystemTag @@ -445,6 +475,7 @@ type mockVolumeAttachment struct { VolumeTag names.VolumeTag MachineTag names.MachineTag + info *state.VolumeAttachmentInfo } func (va *mockVolumeAttachment) Volume() names.VolumeTag { @@ -460,7 +491,10 @@ } func (va *mockVolumeAttachment) Info() (state.VolumeAttachmentInfo, error) { - return state.VolumeAttachmentInfo{}, errors.New("not interested yet") + if va.info != nil { + return *va.info, nil + } + return state.VolumeAttachmentInfo{}, errors.NotProvisionedf("volume attachment") } func (va *mockVolumeAttachment) Params() (state.VolumeAttachmentParams, bool) { @@ -468,18 +502,11 @@ } type mockBlock struct { + state.Block t state.BlockType msg string } -func (b mockBlock) Id() string { - panic("not implemented for test") -} - -func (b mockBlock) Tag() (names.Tag, error) { - panic("not implemented for test") -} - func (b mockBlock) Type() state.BlockType { return b.t } === modified file 'src/github.com/juju/juju/apiserver/storage/state.go' --- src/github.com/juju/juju/apiserver/storage/state.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/storage/state.go 2015-10-23 18:29:32 +0000 @@ -44,6 +44,12 @@ // 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) + // EnvName is required for pool functionality. EnvName() (string, error) === modified file 'src/github.com/juju/juju/apiserver/storage/storage.go' --- src/github.com/juju/juju/apiserver/storage/storage.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/storage/storage.go 2015-10-23 18:29:32 +0000 @@ -4,7 +4,10 @@ package storage import ( + "time" + "github.com/juju/errors" + "github.com/juju/loggo" "github.com/juju/names" "github.com/juju/utils/set" @@ -16,6 +19,8 @@ "github.com/juju/juju/storage/provider/registry" ) +var logger = loggo.GetLogger("juju.apiserver.storage") + func init() { common.RegisterStandardFacade("Storage", 1, NewAPI) } @@ -63,191 +68,149 @@ // identified by supplied tags. If specified storage cannot be retrieved, // individual error is returned instead of storage information. func (api *API) Show(entities params.Entities) (params.StorageDetailsResults, error) { - var all []params.StorageDetailsResult - for _, entity := range entities.Entities { + results := make([]params.StorageDetailsResult, len(entities.Entities)) + for i, entity := range entities.Entities { storageTag, err := names.ParseStorageTag(entity.Tag) if err != nil { - all = append(all, params.StorageDetailsResult{ - Error: common.ServerError(err), - }) + results[i].Error = common.ServerError(err) continue } - found, instance, serverErr := api.getStorageInstance(storageTag) + storageInstance, err := api.storage.StorageInstance(storageTag) if err != nil { - all = append(all, params.StorageDetailsResult{Error: serverErr}) + results[i].Error = common.ServerError(err) continue } - if found { - results := api.createStorageDetailsResult(storageTag, instance) - all = append(all, results...) - } + results[i] = api.createStorageDetailsResult(storageInstance) } - return params.StorageDetailsResults{Results: all}, nil + return params.StorageDetailsResults{Results: results}, nil } // List returns all currently known storage. Unlike Show(), // if errors encountered while retrieving a particular // storage, this error is treated as part of the returned storage detail. -func (api *API) List() (params.StorageInfosResult, error) { +func (api *API) List() (params.StorageDetailsResults, error) { stateInstances, err := api.storage.AllStorageInstances() if err != nil { - return params.StorageInfosResult{}, common.ServerError(err) - } - var infos []params.StorageInfo - for _, stateInstance := range stateInstances { - storageTag := stateInstance.StorageTag() - persistent, err := api.isPersistent(stateInstance) - if err != nil { - return params.StorageInfosResult{}, err - } - instance := createParamsStorageInstance(stateInstance, persistent) - - // It is possible to encounter errors here related to getting individual - // storage details such as getting attachments, getting machine from the unit, - // etc. - // Current approach is to do what status command does - treat error - // as another valid property, i.e. augment storage details. - attachments := api.createStorageDetailsResult(storageTag, instance) - for _, one := range attachments { - aParam := params.StorageInfo{one.Result, one.Error} - infos = append(infos, aParam) - } - } - return params.StorageInfosResult{Results: infos}, nil -} - -func (api *API) createStorageDetailsResult( - storageTag names.StorageTag, - instance params.StorageDetails, -) []params.StorageDetailsResult { - attachments, err := api.getStorageAttachments(storageTag, instance) - if err != nil { - return []params.StorageDetailsResult{params.StorageDetailsResult{Result: instance, Error: err}} - } - if len(attachments) > 0 { - // If any attachments were found for this storage instance, - // return them instead. - result := make([]params.StorageDetailsResult, len(attachments)) - for i, attachment := range attachments { - result[i] = params.StorageDetailsResult{Result: attachment} - } - return result - } - // If we are here then this storage instance is unattached. - return []params.StorageDetailsResult{params.StorageDetailsResult{Result: instance}} -} - -func (api *API) getStorageAttachments( - storageTag names.StorageTag, - instance params.StorageDetails, -) ([]params.StorageDetails, *params.Error) { - serverError := func(err error) *params.Error { - return common.ServerError(errors.Annotatef(err, "getting attachments for storage %v", storageTag.Id())) - } - stateAttachments, err := api.storage.StorageAttachments(storageTag) - if err != nil { - return nil, serverError(err) - } - result := make([]params.StorageDetails, len(stateAttachments)) - for i, one := range stateAttachments { - paramsStorageAttachment, err := api.createParamsStorageAttachment(instance, one) - if err != nil { - return nil, serverError(err) - } - result[i] = paramsStorageAttachment - } - return result, nil -} - -func (api *API) createParamsStorageAttachment(si params.StorageDetails, sa state.StorageAttachment) (params.StorageDetails, error) { - result := params.StorageDetails{Status: "pending"} - result.StorageTag = sa.StorageInstance().String() - if result.StorageTag != si.StorageTag { - panic("attachment does not belong to storage instance") - } - result.UnitTag = sa.Unit().String() - result.OwnerTag = si.OwnerTag - result.Kind = si.Kind - result.Persistent = si.Persistent - // TODO(axw) set status according to whether storage has been provisioned. - - // This is only for provisioned attachments - machineTag, err := api.storage.UnitAssignedMachine(sa.Unit()) - if err != nil { - return params.StorageDetails{}, errors.Annotate(err, "getting unit for storage attachment") - } - info, err := common.StorageAttachmentInfo(api.storage, sa, machineTag) - if err != nil { - if errors.IsNotProvisioned(err) { - // If Info returns an error, then the storage has not yet been provisioned. - return result, nil - } - return params.StorageDetails{}, errors.Annotate(err, "getting storage attachment info") - } - result.Location = info.Location - if result.Location != "" { - result.Status = "attached" - } - return result, nil -} - -func (api *API) getStorageInstance(tag names.StorageTag) (bool, params.StorageDetails, *params.Error) { - nothing := params.StorageDetails{} - serverError := func(err error) *params.Error { - return common.ServerError(errors.Annotatef(err, "getting %v", tag)) - } - stateInstance, err := api.storage.StorageInstance(tag) - if err != nil { - if errors.IsNotFound(err) { - return false, nothing, nil - } - return false, nothing, serverError(err) - } - persistent, err := api.isPersistent(stateInstance) - if err != nil { - return false, nothing, serverError(err) - } - return true, createParamsStorageInstance(stateInstance, persistent), nil -} - -func createParamsStorageInstance(si state.StorageInstance, persistent bool) params.StorageDetails { - result := params.StorageDetails{ - OwnerTag: si.Owner().String(), - StorageTag: si.Tag().String(), - Kind: params.StorageKind(si.Kind()), - Status: "pending", - Persistent: persistent, - } - return result -} - -// TODO(axw) move this and createParamsStorageInstance to -// apiserver/common/storage.go, alongside StorageAttachmentInfo. -func (api *API) isPersistent(si state.StorageInstance) (bool, error) { + return params.StorageDetailsResults{}, common.ServerError(err) + } + results := make([]params.StorageDetailsResult, len(stateInstances)) + for i, stateInstance := range stateInstances { + results[i] = api.createStorageDetailsResult(stateInstance) + } + return params.StorageDetailsResults{Results: results}, nil +} + +func (api *API) createStorageDetailsResult(si state.StorageInstance) params.StorageDetailsResult { + details, err := createStorageDetails(api.storage, si) + if err != nil { + return params.StorageDetailsResult{Error: common.ServerError(err)} + } + + legacy := params.LegacyStorageDetails{ + details.StorageTag, + details.OwnerTag, + details.Kind, + string(details.Status.Status), + "", // unit tag set below + "", // location set below + details.Persistent, + } + if len(details.Attachments) == 1 { + for unitTag, attachmentDetails := range details.Attachments { + legacy.UnitTag = unitTag + legacy.Location = attachmentDetails.Location + } + } + + return params.StorageDetailsResult{Result: details, Legacy: legacy} +} + +func createStorageDetails(st storageAccess, si state.StorageInstance) (*params.StorageDetails, error) { + // Get information from underlying volume or filesystem. + var persistent bool + var entityStatus params.EntityStatus if si.Kind() != state.StorageKindBlock { // TODO(axw) when we support persistent filesystems, - // e.g. CephFS, we'll need to do the same thing as - // we do for volumes for filesystems. - return false, nil - } - volume, err := api.storage.StorageInstanceVolume(si.StorageTag()) - if err != nil { - return false, err - } - // If the volume is not provisioned, we read its config attributes. - if params, ok := volume.Params(); ok { - _, cfg, err := common.StoragePoolConfig(params.Pool, api.poolManager) - if err != nil { - return false, err - } - return cfg.IsPersistent(), nil - } - // If the volume is provisioned, we look at its provisioning info. - info, err := volume.Info() - if err != nil { - return false, err - } - return info.Persistent, nil + // e.g. CephFS, we'll need to do set "persistent" + // here too. + nowUTC := time.Now().UTC() + entityStatus.Status = params.StatusUnknown + entityStatus.Since = &nowUTC + } else { + volume, err := st.StorageInstanceVolume(si.StorageTag()) + if err != nil { + return nil, errors.Trace(err) + } + if info, err := volume.Info(); err == nil { + persistent = info.Persistent + } + status, err := volume.Status() + if err != nil { + return nil, errors.Trace(err) + } + entityStatus = common.EntityStatusFromState(status) + } + + // Get unit storage attachments. + var storageAttachmentDetails map[string]params.StorageAttachmentDetails + storageAttachments, err := st.StorageAttachments(si.StorageTag()) + if err != nil { + return nil, errors.Trace(err) + } + if len(storageAttachments) > 0 { + storageAttachmentDetails = make(map[string]params.StorageAttachmentDetails) + for _, a := range storageAttachments { + machineTag, location, err := storageAttachmentInfo(st, a) + if err != nil { + return nil, errors.Trace(err) + } + details := params.StorageAttachmentDetails{ + a.StorageInstance().String(), + a.Unit().String(), + machineTag.String(), + location, + } + storageAttachmentDetails[a.Unit().String()] = details + } + } + + // Hack to set filesystem status. + // + // TODO(axw) we can undo this in 1.26, + // where we have proper filesystem status. + if entityStatus.Status == params.StatusUnknown { + entityStatus.Status = params.StatusPending + for _, details := range storageAttachmentDetails { + if details.Location != "" { + entityStatus.Status = params.StatusAttached + } + } + } + + return ¶ms.StorageDetails{ + StorageTag: si.Tag().String(), + OwnerTag: si.Owner().String(), + Kind: params.StorageKind(si.Kind()), + Status: entityStatus, + Persistent: persistent, + Attachments: storageAttachmentDetails, + }, nil +} + +func storageAttachmentInfo(st storageAccess, a state.StorageAttachment) (_ names.MachineTag, location string, _ error) { + machineTag, err := st.UnitAssignedMachine(a.Unit()) + if errors.IsNotAssigned(err) { + return names.MachineTag{}, "", nil + } else if err != nil { + return names.MachineTag{}, "", errors.Trace(err) + } + info, err := common.StorageAttachmentInfo(st, a, machineTag) + if errors.IsNotProvisioned(err) { + return machineTag, "", nil + } else if err != nil { + return names.MachineTag{}, "", errors.Trace(err) + } + return machineTag, info.Location, nil } // ListPools returns a list of pools. @@ -396,179 +359,165 @@ return err } -func (a *API) ListVolumes(filter params.VolumeFilter) (params.VolumeItemsResult, error) { - if !filter.IsEmpty() { - return params.VolumeItemsResult{Results: a.filterVolumes(filter)}, nil - } - volumes, err := a.listVolumeAttachments() - if err != nil { - return params.VolumeItemsResult{}, common.ServerError(err) - } - return params.VolumeItemsResult{Results: volumes}, nil -} - -func (a *API) listVolumeAttachments() ([]params.VolumeItem, error) { - all, err := a.storage.AllVolumes() - if err != nil { - return nil, errors.Trace(err) - } - return a.volumeAttachments(all), nil -} - -func (a *API) volumeAttachments(all []state.Volume) []params.VolumeItem { - if all == nil || len(all) == 0 { - return nil - } - - result := make([]params.VolumeItem, len(all)) - for i, v := range all { - volume, err := a.convertStateVolumeToParams(v) +func (a *API) ListVolumes(filter params.VolumeFilter) (params.VolumeDetailsResults, error) { + volumes, volumeAttachments, err := filterVolumes(a.storage, filter) + if err != nil { + return params.VolumeDetailsResults{}, common.ServerError(err) + } + results := createVolumeDetailsResults(a.storage, volumes, volumeAttachments) + return params.VolumeDetailsResults{Results: results}, nil +} + +func filterVolumes( + st storageAccess, + f params.VolumeFilter, +) ([]state.Volume, map[names.VolumeTag][]state.VolumeAttachment, error) { + if f.IsEmpty() { + // No filter was specified: get all volumes, and all attachments. + volumes, err := st.AllVolumes() if err != nil { - result[i] = params.VolumeItem{ - Error: common.ServerError(errors.Trace(err)), + return nil, nil, errors.Trace(err) + } + volumeAttachments := make(map[names.VolumeTag][]state.VolumeAttachment) + for _, v := range volumes { + attachments, err := st.VolumeAttachments(v.VolumeTag()) + if err != nil { + return nil, nil, errors.Trace(err) } - continue - } - result[i] = params.VolumeItem{Volume: volume} - atts, err := a.storage.VolumeAttachments(v.VolumeTag()) - if err != nil { - result[i].Error = common.ServerError(errors.Annotatef( - err, "attachments for volume %v", v.VolumeTag())) - continue - } - result[i].Attachments = convertStateVolumeAttachmentsToParams(atts) - } - return result -} - -func (a *API) filterVolumes(f params.VolumeFilter) []params.VolumeItem { - var attachments []state.VolumeAttachment - var errs []params.VolumeItem - - addErr := func(err error) { - errs = append(errs, - params.VolumeItem{Error: common.ServerError(err)}) - } - + volumeAttachments[v.VolumeTag()] = attachments + } + return volumes, volumeAttachments, nil + } + volumesByTag := make(map[names.VolumeTag]state.Volume) + volumeAttachments := make(map[names.VolumeTag][]state.VolumeAttachment) for _, machine := range f.Machines { - tag, err := names.ParseMachineTag(machine) - if err != nil { - addErr(errors.Annotatef(err, "parsing machine tag %v", machine)) - } - machineAttachments, err := a.storage.MachineVolumeAttachments(tag) - if err != nil { - addErr(errors.Annotatef(err, - "getting volume attachments for machine %v", - machine)) - } - attachments = append(attachments, machineAttachments...) - } - return append(errs, a.getVolumeItems(attachments)...) -} - -func (a *API) convertStateVolumeToParams(st state.Volume) (params.VolumeInstance, error) { - volume := params.VolumeInstance{VolumeTag: st.VolumeTag().String()} - - if storage, err := st.StorageInstance(); err == nil { - volume.StorageTag = storage.String() - storageInstance, err := a.storage.StorageInstance(storage) - if err != nil { - err = errors.Annotatef(err, - "getting storage instance %v for volume %v", - storage, volume.VolumeTag) - return params.VolumeInstance{}, err - } - owner := storageInstance.Owner() - // only interested in Unit for now - if unitTag, ok := owner.(names.UnitTag); ok { - volume.UnitTag = unitTag.String() - } - } - if info, err := st.Info(); err == nil { - volume.HardwareId = info.HardwareId - volume.Size = info.Size - volume.Persistent = info.Persistent - volume.VolumeId = info.VolumeId - } - return volume, nil -} - -func convertStateVolumeAttachmentsToParams(all []state.VolumeAttachment) []params.VolumeAttachment { - if len(all) == 0 { - return nil - } - result := make([]params.VolumeAttachment, len(all)) - for i, one := range all { - result[i] = convertStateVolumeAttachmentToParams(one) - } - return result -} - -func convertStateVolumeAttachmentToParams(attachment state.VolumeAttachment) params.VolumeAttachment { - result := params.VolumeAttachment{ - VolumeTag: attachment.Volume().String(), - MachineTag: attachment.Machine().String()} - if info, err := attachment.Info(); err == nil { - result.Info = params.VolumeAttachmentInfo{ - info.DeviceName, - info.BusAddress, - info.ReadOnly, - } - } - return result -} - -func (a *API) getVolumeItems(all []state.VolumeAttachment) []params.VolumeItem { - group := groupAttachmentsByVolume(all) - - if len(group) == 0 { - return nil - } - - result := make([]params.VolumeItem, len(group)) - i := 0 - for volumeTag, attachments := range group { - result[i] = a.createVolumeItem(volumeTag, attachments) - i++ - } - return result -} - -func (a *API) createVolumeItem(volumeTag string, attachments []params.VolumeAttachment) params.VolumeItem { - result := params.VolumeItem{Attachments: attachments} - - tag, err := names.ParseVolumeTag(volumeTag) - if err != nil { - result.Error = common.ServerError(errors.Annotatef(err, "parsing volume tag %v", volumeTag)) - return result - } - st, err := a.storage.Volume(tag) - if err != nil { - result.Error = common.ServerError(errors.Annotatef(err, "getting volume for tag %v", tag)) - return result - } - volume, err := a.convertStateVolumeToParams(st) - if err != nil { - result.Error = common.ServerError(errors.Trace(err)) - return result - } - result.Volume = volume - return result -} - -// groupAttachmentsByVolume constructs map of attachments grouped by volumeTag -func groupAttachmentsByVolume(all []state.VolumeAttachment) map[string][]params.VolumeAttachment { - if len(all) == 0 { - return nil - } - group := make(map[string][]params.VolumeAttachment) - for _, one := range all { - attachment := convertStateVolumeAttachmentToParams(one) - group[attachment.VolumeTag] = append( - group[attachment.VolumeTag], - attachment) - } - return group + machineTag, err := names.ParseMachineTag(machine) + if err != nil { + return nil, nil, errors.Trace(err) + } + attachments, err := st.MachineVolumeAttachments(machineTag) + if err != nil { + return nil, nil, errors.Trace(err) + } + for _, attachment := range attachments { + volumeTag := attachment.Volume() + volumesByTag[volumeTag] = nil + volumeAttachments[volumeTag] = append(volumeAttachments[volumeTag], attachment) + } + } + for volumeTag := range volumesByTag { + volume, err := st.Volume(volumeTag) + if err != nil { + return nil, nil, errors.Trace(err) + } + volumesByTag[volumeTag] = volume + } + volumes := make([]state.Volume, 0, len(volumesByTag)) + for _, volume := range volumesByTag { + volumes = append(volumes, volume) + } + return volumes, volumeAttachments, nil +} + +func createVolumeDetailsResults( + st storageAccess, + volumes []state.Volume, + attachments map[names.VolumeTag][]state.VolumeAttachment, +) []params.VolumeDetailsResult { + + if len(volumes) == 0 { + return nil + } + + results := make([]params.VolumeDetailsResult, len(volumes)) + for i, v := range volumes { + details, err := createVolumeDetails(st, v, attachments[v.VolumeTag()]) + if err != nil { + results[i].Error = common.ServerError(err) + continue + } + result := params.VolumeDetailsResult{ + Details: details, + } + + // We need to populate the legacy fields for old clients. + if len(details.MachineAttachments) > 0 { + result.LegacyAttachments = make([]params.VolumeAttachment, 0, len(details.MachineAttachments)) + for machineTag, attachmentInfo := range details.MachineAttachments { + result.LegacyAttachments = append(result.LegacyAttachments, params.VolumeAttachment{ + VolumeTag: details.VolumeTag, + MachineTag: machineTag, + Info: attachmentInfo, + }) + } + } + result.LegacyVolume = ¶ms.LegacyVolumeDetails{ + VolumeTag: details.VolumeTag, + VolumeId: details.Info.VolumeId, + HardwareId: details.Info.HardwareId, + Size: details.Info.Size, + Persistent: details.Info.Persistent, + Status: details.Status, + } + if details.Storage != nil { + result.LegacyVolume.StorageTag = details.Storage.StorageTag + kind, err := names.TagKind(details.Storage.OwnerTag) + if err != nil { + results[i].Error = common.ServerError(err) + continue + } + if kind == names.UnitTagKind { + result.LegacyVolume.UnitTag = details.Storage.OwnerTag + } + } + results[i] = result + } + return results +} + +func createVolumeDetails( + st storageAccess, v state.Volume, attachments []state.VolumeAttachment, +) (*params.VolumeDetails, error) { + + details := ¶ms.VolumeDetails{ + VolumeTag: v.VolumeTag().String(), + } + + if info, err := v.Info(); err == nil { + details.Info = common.VolumeInfoFromState(info) + } + + if len(attachments) > 0 { + details.MachineAttachments = make(map[string]params.VolumeAttachmentInfo, len(attachments)) + for _, attachment := range attachments { + stateInfo, err := attachment.Info() + var info params.VolumeAttachmentInfo + if err == nil { + info = common.VolumeAttachmentInfoFromState(stateInfo) + } + details.MachineAttachments[attachment.Machine().String()] = info + } + } + + status, err := v.Status() + if err != nil { + return nil, errors.Trace(err) + } + details.Status = common.EntityStatusFromState(status) + + if storageTag, err := v.StorageInstance(); err == nil { + storageInstance, err := st.StorageInstance(storageTag) + if err != nil { + return nil, errors.Trace(err) + } + storageDetails, err := createStorageDetails(st, storageInstance) + if err != nil { + return nil, errors.Trace(err) + } + details.Storage = storageDetails + } + + return details, nil } // AddToUnit validates and creates additional storage instances for units. === modified file 'src/github.com/juju/juju/apiserver/storage/storage_test.go' --- src/github.com/juju/juju/apiserver/storage/storage_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/storage/storage_test.go 2015-10-23 18:29:32 +0000 @@ -48,8 +48,10 @@ s.assertCalls(c, expectedCalls) c.Assert(found.Results, gc.HasLen, 1) - wantedDetails := s.createTestStorageInfo() - wantedDetails.UnitTag = s.unitTag.String() + wantedDetails := s.createTestStorageDetailsResult() + + c.Assert(found.Results[0].Result.Status.Since, gc.NotNil) + found.Results[0].Result.Status.Since = nil s.assertInstanceInfoError(c, found.Results[0], wantedDetails, "") } @@ -69,9 +71,11 @@ s.assertCalls(c, expectedCalls) c.Assert(found.Results, gc.HasLen, 1) - wantedDetails := s.createTestStorageInfo() - wantedDetails.Kind = params.StorageKindBlock - wantedDetails.UnitTag = s.unitTag.String() + wantedDetails := s.createTestStorageDetailsResult() + wantedDetails.Result.Kind = params.StorageKindBlock + wantedDetails.Result.Status.Status = params.StatusAttached + wantedDetails.Legacy.Kind = params.StorageKindBlock + wantedDetails.Legacy.Status = "attached" s.assertInstanceInfoError(c, found.Results[0], wantedDetails, "") } @@ -96,7 +100,7 @@ msg := "list test error" s.state.storageInstance = func(sTag names.StorageTag) (state.StorageInstance, error) { s.calls = append(s.calls, storageInstanceCall) - c.Assert(sTag, gc.DeepEquals, s.storageTag) + c.Assert(sTag, jc.DeepEquals, s.storageTag) return nil, errors.Errorf(msg) } @@ -111,7 +115,7 @@ } s.assertCalls(c, expectedCalls) c.Assert(found.Results, gc.HasLen, 1) - wanted := s.createTestStorageInfoWithError("", + wanted := s.createTestStorageDetailsResultWithError("", fmt.Sprintf("getting storage attachment info: getting storage instance: %v", msg)) s.assertInstanceInfoError(c, found.Results[0], wanted, msg) } @@ -119,7 +123,7 @@ func (s *storageSuite) TestStorageListAttachmentError(c *gc.C) { s.state.storageInstanceAttachments = func(tag names.StorageTag) ([]state.StorageAttachment, error) { s.calls = append(s.calls, storageInstanceAttachmentsCall) - c.Assert(tag, gc.DeepEquals, s.storageTag) + c.Assert(tag, jc.DeepEquals, s.storageTag) return []state.StorageAttachment{}, errors.Errorf("list test error") } @@ -133,7 +137,7 @@ s.assertCalls(c, expectedCalls) c.Assert(found.Results, gc.HasLen, 1) expectedErr := "list test error" - wanted := s.createTestStorageInfoWithError("", expectedErr) + wanted := s.createTestStorageDetailsResultWithError("", expectedErr) s.assertInstanceInfoError(c, found.Results[0], wanted, expectedErr) } @@ -141,7 +145,7 @@ msg := "list test error" s.state.unitAssignedMachine = func(u names.UnitTag) (names.MachineTag, error) { s.calls = append(s.calls, unitAssignedMachineCall) - c.Assert(u, gc.DeepEquals, s.unitTag) + c.Assert(u, jc.DeepEquals, s.unitTag) return names.MachineTag{}, errors.Errorf(msg) } @@ -155,7 +159,7 @@ } s.assertCalls(c, expectedCalls) c.Assert(found.Results, gc.HasLen, 1) - wanted := s.createTestStorageInfoWithError("", + wanted := s.createTestStorageDetailsResultWithError("", fmt.Sprintf("getting unit for storage attachment: %v", msg)) s.assertInstanceInfoError(c, found.Results[0], wanted, msg) } @@ -164,7 +168,7 @@ msg := "list test error" s.state.storageInstanceFilesystem = func(sTag names.StorageTag) (state.Filesystem, error) { s.calls = append(s.calls, storageInstanceFilesystemCall) - c.Assert(sTag, gc.DeepEquals, s.storageTag) + c.Assert(sTag, jc.DeepEquals, s.storageTag) return nil, errors.Errorf(msg) } @@ -180,7 +184,7 @@ } s.assertCalls(c, expectedCalls) c.Assert(found.Results, gc.HasLen, 1) - wanted := s.createTestStorageInfoWithError("", + wanted := s.createTestStorageDetailsResultWithError("", fmt.Sprintf("getting storage attachment info: getting filesystem: %v", msg)) s.assertInstanceInfoError(c, found.Results[0], wanted, msg) } @@ -189,7 +193,7 @@ msg := "list test error" s.state.unitAssignedMachine = func(u names.UnitTag) (names.MachineTag, error) { s.calls = append(s.calls, unitAssignedMachineCall) - c.Assert(u, gc.DeepEquals, s.unitTag) + c.Assert(u, jc.DeepEquals, s.unitTag) return s.machineTag, errors.Errorf(msg) } @@ -203,23 +207,40 @@ } s.assertCalls(c, expectedCalls) c.Assert(found.Results, gc.HasLen, 1) - wanted := s.createTestStorageInfoWithError("", + wanted := s.createTestStorageDetailsResultWithError("", fmt.Sprintf("getting unit for storage attachment: %v", msg)) s.assertInstanceInfoError(c, found.Results[0], wanted, msg) } -func (s *storageSuite) createTestStorageInfoWithError(code, msg string) params.StorageInfo { - wanted := s.createTestStorageInfo() +func (s *storageSuite) createTestStorageDetailsResultWithError(code, msg string) params.StorageDetailsResult { + wanted := s.createTestStorageDetailsResult() wanted.Error = ¶ms.Error{Code: code, Message: fmt.Sprintf("getting attachments for storage data/0: %v", msg)} return wanted } -func (s *storageSuite) createTestStorageInfo() params.StorageInfo { - return params.StorageInfo{ - params.StorageDetails{ - StorageTag: s.storageTag.String(), - OwnerTag: s.unitTag.String(), +func (s *storageSuite) createTestStorageDetailsResult() params.StorageDetailsResult { + return params.StorageDetailsResult{ + ¶ms.StorageDetails{ + StorageTag: s.storageTag.String(), + OwnerTag: s.unitTag.String(), + Kind: params.StorageKindFilesystem, + Status: params.EntityStatus{ + Status: "pending", + }, + Attachments: map[string]params.StorageAttachmentDetails{ + s.unitTag.String(): params.StorageAttachmentDetails{ + s.storageTag.String(), + s.unitTag.String(), + s.machineTag.String(), + "", // location + }, + }, + }, + params.LegacyStorageDetails{ + StorageTag: s.storageTag.String(), + OwnerTag: s.unitTag.String(), + UnitTag: s.unitTag.String(), Kind: params.StorageKindFilesystem, Status: "pending", }, @@ -227,13 +248,15 @@ } } -func (s *storageSuite) assertInstanceInfoError(c *gc.C, obtained params.StorageInfo, wanted params.StorageInfo, expected string) { +func (s *storageSuite) assertInstanceInfoError(c *gc.C, obtained params.StorageDetailsResult, wanted params.StorageDetailsResult, expected string) { if expected != "" { c.Assert(errors.Cause(obtained.Error), gc.ErrorMatches, fmt.Sprintf(".*%v.*", expected)) + c.Assert(obtained.Result, gc.IsNil) + c.Assert(obtained.Legacy, jc.DeepEquals, params.LegacyStorageDetails{}) } else { c.Assert(obtained.Error, gc.IsNil) + c.Assert(obtained, jc.DeepEquals, wanted) } - c.Assert(obtained, gc.DeepEquals, wanted) } func (s *storageSuite) TestShowStorageEmpty(c *gc.C) { @@ -264,10 +287,21 @@ StorageTag: s.storageTag.String(), OwnerTag: s.unitTag.String(), Kind: params.StorageKindFilesystem, - UnitTag: s.unitTag.String(), - Status: "pending", + Status: params.EntityStatus{ + Status: "pending", + }, + Attachments: map[string]params.StorageAttachmentDetails{ + s.unitTag.String(): params.StorageAttachmentDetails{ + s.storageTag.String(), + s.unitTag.String(), + s.machineTag.String(), + "", + }, + }, } - c.Assert(one.Result, gc.DeepEquals, expected) + c.Assert(one.Result.Status.Since, gc.NotNil) + one.Result.Status.Since = nil + c.Assert(one.Result, jc.DeepEquals, &expected) } func (s *storageSuite) TestShowStorageInvalidId(c *gc.C) { @@ -277,10 +311,5 @@ found, err := s.api.Show(params.Entities{Entities: []params.Entity{entity}}) c.Assert(err, jc.ErrorIsNil) c.Assert(found.Results, gc.HasLen, 1) - - instance := found.Results[0] - c.Assert(instance.Error, gc.ErrorMatches, `"foo" is not a valid tag`) - - expected := params.StorageDetails{Kind: params.StorageKindUnknown} - c.Assert(instance.Result, gc.DeepEquals, expected) + s.assertInstanceInfoError(c, found.Results[0], params.StorageDetailsResult{}, `"foo" is not a valid tag`) } === modified file 'src/github.com/juju/juju/apiserver/storage/volumelist_test.go' --- src/github.com/juju/juju/apiserver/storage/volumelist_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/storage/volumelist_test.go 2015-10-23 18:29:32 +0000 @@ -4,13 +4,14 @@ package storage_test import ( + "path/filepath" + "github.com/juju/errors" "github.com/juju/names" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/apiserver/storage" "github.com/juju/juju/state" ) @@ -20,260 +21,191 @@ var _ = gc.Suite(&volumeSuite{}) -func (s *volumeSuite) TestGroupAttachmentsByVolumeEmpty(c *gc.C) { - c.Assert(storage.GroupAttachmentsByVolume(nil), gc.IsNil) - c.Assert(storage.GroupAttachmentsByVolume([]state.VolumeAttachment{}), gc.IsNil) -} - -func (s *volumeSuite) TestGroupAttachmentsByVolume(c *gc.C) { - volumeTag1 := names.NewVolumeTag("0") - volumeTag2 := names.NewVolumeTag("0/1") - machineTag := names.NewMachineTag("0") - attachments := []state.VolumeAttachment{ - &mockVolumeAttachment{VolumeTag: volumeTag1, MachineTag: machineTag}, - &mockVolumeAttachment{VolumeTag: volumeTag2, MachineTag: machineTag}, - &mockVolumeAttachment{VolumeTag: volumeTag2, MachineTag: machineTag}, - } - expected := map[string][]params.VolumeAttachment{ - volumeTag1.String(): { - storage.ConvertStateVolumeAttachmentToParams(attachments[0])}, - volumeTag2.String(): { - storage.ConvertStateVolumeAttachmentToParams(attachments[1]), - storage.ConvertStateVolumeAttachmentToParams(attachments[2]), - }, - } - c.Assert( - storage.GroupAttachmentsByVolume(attachments), - jc.DeepEquals, - expected) -} - -func (s *volumeSuite) TestCreateVolumeItemInvalidTag(c *gc.C) { - found := storage.CreateVolumeItem(s.api, "666", nil) - c.Assert(found.Error, gc.ErrorMatches, ".*not a valid tag.*") -} - -func (s *volumeSuite) TestCreateVolumeItemNonexistingVolume(c *gc.C) { - s.state.volume = func(tag names.VolumeTag) (state.Volume, error) { - return s.volume, errors.Errorf("not volume for tag %v", tag) - } - found := storage.CreateVolumeItem(s.api, names.NewVolumeTag("666").String(), nil) - c.Assert(found.Error, gc.ErrorMatches, ".*volume for tag.*") -} - -func (s *volumeSuite) TestCreateVolumeItemNoUnit(c *gc.C) { - s.storageInstance.owner = names.NewServiceTag("test-service") - found := storage.CreateVolumeItem(s.api, s.volumeTag.String(), nil) - c.Assert(found.Error, gc.IsNil) - c.Assert(found.Error, gc.IsNil) - expected, err := storage.ConvertStateVolumeToParams(s.api, s.volume) - c.Assert(err, jc.ErrorIsNil) - c.Assert(found.Volume, gc.DeepEquals, expected) -} - -func (s *volumeSuite) TestCreateVolumeItemNoStorageInstance(c *gc.C) { - s.volume = &mockVolume{tag: s.volumeTag, hasNoStorage: true} - found := storage.CreateVolumeItem(s.api, s.volumeTag.String(), nil) - c.Assert(found.Error, gc.IsNil) - c.Assert(found.Error, gc.IsNil) - expected, err := storage.ConvertStateVolumeToParams(s.api, s.volume) - c.Assert(err, jc.ErrorIsNil) - c.Assert(found.Volume, gc.DeepEquals, expected) -} - -func (s *volumeSuite) TestCreateVolumeItem(c *gc.C) { - found := storage.CreateVolumeItem(s.api, s.volumeTag.String(), nil) - c.Assert(found.Error, gc.IsNil) - c.Assert(found.Error, gc.IsNil) - expected, err := storage.ConvertStateVolumeToParams(s.api, s.volume) - c.Assert(err, jc.ErrorIsNil) - c.Assert(found.Volume, gc.DeepEquals, expected) -} - -func (s *volumeSuite) TestGetVolumeItemsEmpty(c *gc.C) { - c.Assert(storage.GetVolumeItems(s.api, nil), gc.IsNil) - c.Assert(storage.GetVolumeItems(s.api, []state.VolumeAttachment{}), gc.IsNil) -} - -func (s *volumeSuite) TestGetVolumeItems(c *gc.C) { - machineTag := names.NewMachineTag("0") - attachments := []state.VolumeAttachment{ - &mockVolumeAttachment{VolumeTag: s.volumeTag, MachineTag: machineTag}, - &mockVolumeAttachment{VolumeTag: s.volumeTag, MachineTag: machineTag}, - } - expectedVolume, err := storage.ConvertStateVolumeToParams(s.api, s.volume) - c.Assert(err, jc.ErrorIsNil) - expected := []params.VolumeItem{ - params.VolumeItem{ - Volume: expectedVolume, - Attachments: storage.ConvertStateVolumeAttachmentsToParams(attachments)}, - } - c.Assert( - storage.GetVolumeItems(s.api, attachments), - jc.DeepEquals, - expected) -} - -func (s *volumeSuite) TestFilterVolumesNoItems(c *gc.C) { - s.state.machineVolumeAttachments = - func(machine names.MachineTag) ([]state.VolumeAttachment, error) { - return nil, nil - } - filter := params.VolumeFilter{ - Machines: []string{s.machineTag.String()}} - - c.Assert(storage.FilterVolumes(s.api, filter), gc.IsNil) -} - -func (s *volumeSuite) TestFilterVolumesErrorMachineAttachments(c *gc.C) { - s.state.machineVolumeAttachments = - func(machine names.MachineTag) ([]state.VolumeAttachment, error) { - return nil, errors.Errorf("not for machine %v", machine) - } - filter := params.VolumeFilter{ - Machines: []string{s.machineTag.String()}} - - found := storage.FilterVolumes(s.api, filter) - c.Assert(found, gc.HasLen, 1) - c.Assert(found[0].Error, gc.ErrorMatches, ".*for machine.*") -} - -func (s *volumeSuite) TestFilterVolumes(c *gc.C) { - filter := params.VolumeFilter{ - Machines: []string{s.machineTag.String()}} - - expectedVolume, err := storage.ConvertStateVolumeToParams(s.api, s.volume) - c.Assert(err, jc.ErrorIsNil) - expected := params.VolumeItem{ - Volume: expectedVolume, - Attachments: storage.ConvertStateVolumeAttachmentsToParams( - []state.VolumeAttachment{s.volumeAttachment}, - ), - } - found := storage.FilterVolumes(s.api, filter) - c.Assert(found, gc.HasLen, 1) - c.Assert(found[0], gc.DeepEquals, expected) -} - -func (s *volumeSuite) TestVolumeAttachments(c *gc.C) { - expectedVolume, err := storage.ConvertStateVolumeToParams(s.api, s.volume) - c.Assert(err, jc.ErrorIsNil) - expected := params.VolumeItem{ - Volume: expectedVolume, - Attachments: storage.ConvertStateVolumeAttachmentsToParams( - []state.VolumeAttachment{s.volumeAttachment}, - ), - } - - found := storage.VolumeAttachments(s.api, []state.Volume{s.volume}) - c.Assert(found, gc.HasLen, 1) - c.Assert(found[0], gc.DeepEquals, expected) -} - -func (s *volumeSuite) TestVolumeAttachmentsEmpty(c *gc.C) { - s.state.volumeAttachments = - func(volume names.VolumeTag) ([]state.VolumeAttachment, error) { - return nil, nil - } - expectedVolume, err := storage.ConvertStateVolumeToParams(s.api, s.volume) - c.Assert(err, jc.ErrorIsNil) - expected := params.VolumeItem{ - Volume: expectedVolume, - } - - found := storage.VolumeAttachments(s.api, []state.Volume{s.volume}) - c.Assert(found, gc.HasLen, 1) - c.Assert(found[0], gc.DeepEquals, expected) -} - -func (s *volumeSuite) TestVolumeAttachmentsError(c *gc.C) { - s.state.volumeAttachments = - func(volume names.VolumeTag) ([]state.VolumeAttachment, error) { - return nil, errors.Errorf("not for volume %v", volume) - } - - found := storage.VolumeAttachments(s.api, []state.Volume{s.volume}) - c.Assert(found, gc.HasLen, 1) - c.Assert(found[0].Error, gc.ErrorMatches, ".*for volume.*") -} - -func (s *volumeSuite) TestListVolumeAttachmentsEmpty(c *gc.C) { - s.state.allVolumes = - func() ([]state.Volume, error) { - return nil, nil - } - items, err := storage.ListVolumeAttachments(s.api) - c.Assert(err, jc.ErrorIsNil) - c.Assert(items, gc.IsNil) -} - -func (s *volumeSuite) TestListVolumeAttachmentsError(c *gc.C) { - msg := "inventing error" - s.state.allVolumes = - func() ([]state.Volume, error) { - return nil, errors.New(msg) - } - items, err := storage.ListVolumeAttachments(s.api) - c.Assert(errors.Cause(err), gc.ErrorMatches, msg) - c.Assert(items, gc.IsNil) -} - -func (s *volumeSuite) TestListVolumeAttachments(c *gc.C) { - expectedVolume, err := storage.ConvertStateVolumeToParams(s.api, s.volume) - c.Assert(err, jc.ErrorIsNil) - expected := params.VolumeItem{ - Volume: expectedVolume, - Attachments: storage.ConvertStateVolumeAttachmentsToParams( - []state.VolumeAttachment{s.volumeAttachment}, - ), - } - - items, err := storage.ListVolumeAttachments(s.api) - c.Assert(err, jc.ErrorIsNil) - c.Assert(items, gc.HasLen, 1) - c.Assert(items[0], gc.DeepEquals, expected) +func (s *volumeSuite) expectedVolumeDetailsResult() params.VolumeDetailsResult { + return params.VolumeDetailsResult{ + Details: ¶ms.VolumeDetails{ + VolumeTag: s.volumeTag.String(), + Status: params.EntityStatus{ + Status: "attached", + }, + MachineAttachments: map[string]params.VolumeAttachmentInfo{ + s.machineTag.String(): params.VolumeAttachmentInfo{}, + }, + Storage: ¶ms.StorageDetails{ + StorageTag: "storage-data-0", + OwnerTag: "unit-mysql-0", + Kind: params.StorageKindFilesystem, + Status: params.EntityStatus{ + Status: "pending", + }, + Attachments: map[string]params.StorageAttachmentDetails{ + "unit-mysql-0": params.StorageAttachmentDetails{ + StorageTag: "storage-data-0", + UnitTag: "unit-mysql-0", + MachineTag: "machine-66", + }, + }, + }, + }, + LegacyVolume: ¶ms.LegacyVolumeDetails{ + VolumeTag: s.volumeTag.String(), + StorageTag: "storage-data-0", + UnitTag: "unit-mysql-0", + Status: params.EntityStatus{ + Status: "attached", + }, + }, + LegacyAttachments: []params.VolumeAttachment{{ + VolumeTag: s.volumeTag.String(), + MachineTag: s.machineTag.String(), + }}, + } +} + +// TODO(axw) drop this in 1.26. This exists only because we don't have +// Filesystem.Status, and so we use time.Now() to get Status.Since. +func (s *volumeSuite) assertAndClearStorageStatus(c *gc.C, details *params.VolumeDetails) { + c.Assert(details.Storage.Status.Since, gc.NotNil) + details.Storage.Status.Since = nil } func (s *volumeSuite) TestListVolumesEmptyFilter(c *gc.C) { - expectedVolume, err := storage.ConvertStateVolumeToParams(s.api, s.volume) - c.Assert(err, jc.ErrorIsNil) - expected := params.VolumeItem{ - Volume: expectedVolume, - Attachments: storage.ConvertStateVolumeAttachmentsToParams( - []state.VolumeAttachment{s.volumeAttachment}, - ), - } found, err := s.api.ListVolumes(params.VolumeFilter{}) c.Assert(err, jc.ErrorIsNil) c.Assert(found.Results, gc.HasLen, 1) - c.Assert(found.Results[0], gc.DeepEquals, expected) + s.assertAndClearStorageStatus(c, found.Results[0].Details) + c.Assert(found.Results[0], gc.DeepEquals, s.expectedVolumeDetailsResult()) } func (s *volumeSuite) TestListVolumesError(c *gc.C) { msg := "inventing error" - s.state.allVolumes = - func() ([]state.Volume, error) { - return nil, errors.New(msg) - } + s.state.allVolumes = func() ([]state.Volume, error) { + return nil, errors.New(msg) + } + _, err := s.api.ListVolumes(params.VolumeFilter{}) + c.Assert(err, gc.ErrorMatches, msg) +} - items, err := s.api.ListVolumes(params.VolumeFilter{}) - c.Assert(errors.Cause(err), gc.ErrorMatches, msg) - c.Assert(items, gc.DeepEquals, params.VolumeItemsResult{}) +func (s *volumeSuite) TestListVolumesNoVolumes(c *gc.C) { + s.state.allVolumes = func() ([]state.Volume, error) { + return nil, nil + } + results, err := s.api.ListVolumes(params.VolumeFilter{}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(results.Results, gc.HasLen, 0) } func (s *volumeSuite) TestListVolumesFilter(c *gc.C) { - expectedVolume, err := storage.ConvertStateVolumeToParams(s.api, s.volume) - c.Assert(err, jc.ErrorIsNil) - expected := params.VolumeItem{ - Volume: expectedVolume, - Attachments: storage.ConvertStateVolumeAttachmentsToParams( - []state.VolumeAttachment{s.volumeAttachment}, - ), - } - filter := params.VolumeFilter{ - Machines: []string{s.machineTag.String()}} - found, err := s.api.ListVolumes(filter) - c.Assert(err, jc.ErrorIsNil) - c.Assert(found.Results, gc.HasLen, 1) - c.Assert(found.Results[0], gc.DeepEquals, expected) + filter := params.VolumeFilter{ + Machines: []string{s.machineTag.String()}, + } + found, err := s.api.ListVolumes(filter) + c.Assert(err, jc.ErrorIsNil) + c.Assert(found.Results, gc.HasLen, 1) + s.assertAndClearStorageStatus(c, found.Results[0].Details) + c.Assert(found.Results[0], jc.DeepEquals, s.expectedVolumeDetailsResult()) +} + +func (s *volumeSuite) TestListVolumesFilterNonMatching(c *gc.C) { + filter := params.VolumeFilter{ + Machines: []string{"machine-42"}, + } + found, err := s.api.ListVolumes(filter) + c.Assert(err, jc.ErrorIsNil) + c.Assert(found.Results, gc.HasLen, 0) +} + +func (s *volumeSuite) TestListVolumesVolumeInfo(c *gc.C) { + s.volume.info = &state.VolumeInfo{ + Size: 123, + HardwareId: "abc", + Persistent: true, + } + expected := s.expectedVolumeDetailsResult() + expected.Details.Info.Size = 123 + expected.Details.Info.HardwareId = "abc" + expected.Details.Info.Persistent = true + expected.LegacyVolume.Size = 123 + expected.LegacyVolume.HardwareId = "abc" + expected.LegacyVolume.Persistent = true + found, err := s.api.ListVolumes(params.VolumeFilter{}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(found.Results, gc.HasLen, 1) + s.assertAndClearStorageStatus(c, found.Results[0].Details) + c.Assert(found.Results[0], jc.DeepEquals, expected) +} + +func (s *volumeSuite) TestListVolumesAttachmentInfo(c *gc.C) { + s.volumeAttachment.info = &state.VolumeAttachmentInfo{ + DeviceName: "xvdf1", + ReadOnly: true, + } + expected := s.expectedVolumeDetailsResult() + expected.Details.MachineAttachments[s.machineTag.String()] = params.VolumeAttachmentInfo{ + DeviceName: "xvdf1", + ReadOnly: true, + } + expected.LegacyAttachments[0].Info = params.VolumeAttachmentInfo{ + DeviceName: "xvdf1", + ReadOnly: true, + } + found, err := s.api.ListVolumes(params.VolumeFilter{}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(found.Results, gc.HasLen, 1) + s.assertAndClearStorageStatus(c, found.Results[0].Details) + c.Assert(found.Results[0], jc.DeepEquals, expected) +} + +func (s *volumeSuite) TestListVolumesStorageLocationNoBlockDevice(c *gc.C) { + s.storageInstance.kind = state.StorageKindBlock + s.volume.info = &state.VolumeInfo{} + s.volumeAttachment.info = &state.VolumeAttachmentInfo{ + ReadOnly: true, + } + expected := s.expectedVolumeDetailsResult() + expected.Details.Storage.Kind = params.StorageKindBlock + expected.Details.Storage.Status.Status = params.StatusAttached + expected.Details.MachineAttachments[s.machineTag.String()] = params.VolumeAttachmentInfo{ + ReadOnly: true, + } + expected.LegacyAttachments[0].Info = params.VolumeAttachmentInfo{ + ReadOnly: true, + } + found, err := s.api.ListVolumes(params.VolumeFilter{}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(found.Results, gc.HasLen, 1) + c.Assert(found.Results[0], jc.DeepEquals, expected) +} + +func (s *volumeSuite) TestListVolumesStorageLocationBlockDevicePath(c *gc.C) { + s.state.blockDevices = func(names.MachineTag) ([]state.BlockDeviceInfo, error) { + return []state.BlockDeviceInfo{{ + BusAddress: "bus-addr", + DeviceName: "sdd", + }}, nil + } + s.storageInstance.kind = state.StorageKindBlock + s.volume.info = &state.VolumeInfo{} + s.volumeAttachment.info = &state.VolumeAttachmentInfo{ + BusAddress: "bus-addr", + ReadOnly: true, + } + expected := s.expectedVolumeDetailsResult() + expected.Details.Storage.Kind = params.StorageKindBlock + expected.Details.Storage.Status.Status = params.StatusAttached + storageAttachmentDetails := expected.Details.Storage.Attachments["unit-mysql-0"] + storageAttachmentDetails.Location = filepath.FromSlash("/dev/sdd") + expected.Details.Storage.Attachments["unit-mysql-0"] = storageAttachmentDetails + expected.Details.MachineAttachments[s.machineTag.String()] = params.VolumeAttachmentInfo{ + BusAddress: "bus-addr", + ReadOnly: true, + } + expected.LegacyAttachments[0].Info = params.VolumeAttachmentInfo{ + BusAddress: "bus-addr", + ReadOnly: true, + } + found, err := s.api.ListVolumes(params.VolumeFilter{}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(found.Results, gc.HasLen, 1) + c.Assert(found.Results[0], jc.DeepEquals, expected) } === modified file 'src/github.com/juju/juju/apiserver/storageprovisioner/state.go' --- src/github.com/juju/juju/apiserver/storageprovisioner/state.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/storageprovisioner/state.go 2015-10-23 18:29:32 +0000 @@ -39,6 +39,11 @@ 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 === modified file 'src/github.com/juju/juju/apiserver/storageprovisioner/storageprovisioner.go' --- src/github.com/juju/juju/apiserver/storageprovisioner/storageprovisioner.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/storageprovisioner/storageprovisioner.go 2015-10-23 18:29:32 +0000 @@ -28,6 +28,7 @@ *common.DeadEnsurer *common.EnvironWatcher *common.InstanceIdGetter + *common.StatusSetter st provisionerState settings poolmanager.SettingsManager @@ -108,7 +109,7 @@ return canAccessStorageEntity(tag, false) }, nil } - lifeAuthFunc := func() (common.AuthFunc, error) { + getLifeAuthFunc := func() (common.AuthFunc, error) { return func(tag names.Tag) bool { return canAccessStorageEntity(tag, true) }, nil @@ -160,10 +161,11 @@ stateInterface := getState(st) settings := getSettingsManager(st) return &StorageProvisionerAPI{ - LifeGetter: common.NewLifeGetter(stateInterface, lifeAuthFunc), + LifeGetter: common.NewLifeGetter(stateInterface, getLifeAuthFunc), DeadEnsurer: common.NewDeadEnsurer(stateInterface, getStorageEntityAuthFunc), EnvironWatcher: common.NewEnvironWatcher(stateInterface, resources, authorizer), InstanceIdGetter: common.NewInstanceIdGetter(st, getMachineAuthFunc), + StatusSetter: common.NewStatusSetter(st, getStorageEntityAuthFunc), st: stateInterface, settings: settings, @@ -534,8 +536,8 @@ return results, nil } -// VolumeParams returns the parameters for creating the volumes -// with the specified tags. +// VolumeParams returns the parameters for creating or destroying +// the volumes with the specified tags. func (s *StorageProvisionerAPI) VolumeParams(args params.Entities) (params.VolumeParamsResults, error) { canAccess, err := s.getStorageEntityAuthFunc() if err != nil { @@ -599,6 +601,7 @@ volumeParams.Attachment = ¶ms.VolumeAttachmentParams{ tag.String(), machineTag.String(), + "", // volume ID string(instanceId), volumeParams.Provider, volumeAttachmentParams.ReadOnly, @@ -702,6 +705,7 @@ if err != nil { return params.VolumeAttachmentParams{}, err } + var volumeId string var pool string if volumeParams, ok := volume.Params(); ok { pool = volumeParams.Pool @@ -710,6 +714,7 @@ if err != nil { return params.VolumeAttachmentParams{}, err } + volumeId = volumeInfo.VolumeId pool = volumeInfo.Pool } providerType, _, err := common.StoragePoolConfig(pool, poolManager) @@ -731,6 +736,7 @@ return params.VolumeAttachmentParams{ volumeAttachment.Volume().String(), volumeAttachment.Machine().String(), + volumeId, string(instanceId), string(providerType), readOnly, @@ -778,6 +784,7 @@ if err != nil { return params.FilesystemAttachmentParams{}, err } + var filesystemId string var pool string if filesystemParams, ok := filesystem.Params(); ok { pool = filesystemParams.Pool @@ -786,6 +793,7 @@ if err != nil { return params.FilesystemAttachmentParams{}, err } + filesystemId = filesystemInfo.FilesystemId pool = filesystemInfo.Pool } providerType, _, err := common.StoragePoolConfig(pool, poolManager) @@ -810,6 +818,7 @@ return params.FilesystemAttachmentParams{ filesystemAttachment.Filesystem().String(), filesystemAttachment.Machine().String(), + filesystemId, string(instanceId), string(providerType), // TODO(axw) dealias MountPoint. We now have @@ -1086,3 +1095,77 @@ } return results, nil } + +// Remove removes volumes and filesystems from state. +func (s *StorageProvisionerAPI) Remove(args params.Entities) (params.ErrorResults, error) { + canAccess, err := s.getStorageEntityAuthFunc() + if err != nil { + return params.ErrorResults{}, err + } + results := params.ErrorResults{ + Results: make([]params.ErrorResult, len(args.Entities)), + } + one := func(arg params.Entity) error { + tag, err := names.ParseTag(arg.Tag) + if err != nil { + return errors.Trace(err) + } + if !canAccess(tag) { + return common.ErrPerm + } + switch tag := tag.(type) { + case names.FilesystemTag: + return s.st.RemoveFilesystem(tag) + case names.VolumeTag: + return s.st.RemoveVolume(tag) + default: + // should have been picked up by canAccess + logger.Debugf("unexpected %v tag", tag.Kind()) + return common.ErrPerm + } + } + for i, arg := range args.Entities { + err := one(arg) + results.Results[i].Error = common.ServerError(err) + } + return results, nil +} + +// RemoveAttachments removes the specified machine storage attachments +// from state. +func (s *StorageProvisionerAPI) RemoveAttachment(args params.MachineStorageIds) (params.ErrorResults, error) { + canAccess, err := s.getAttachmentAuthFunc() + if err != nil { + return params.ErrorResults{}, err + } + results := params.ErrorResults{ + Results: make([]params.ErrorResult, len(args.Ids)), + } + removeAttachment := func(arg params.MachineStorageId) error { + machineTag, err := names.ParseMachineTag(arg.MachineTag) + if err != nil { + return err + } + attachmentTag, err := names.ParseTag(arg.AttachmentTag) + if err != nil { + return err + } + if !canAccess(machineTag, attachmentTag) { + return common.ErrPerm + } + switch attachmentTag := attachmentTag.(type) { + case names.VolumeTag: + return s.st.RemoveVolumeAttachment(machineTag, attachmentTag) + case names.FilesystemTag: + return s.st.RemoveFilesystemAttachment(machineTag, attachmentTag) + default: + return common.ErrPerm + } + } + for i, arg := range args.Ids { + if err := removeAttachment(arg); err != nil { + results.Results[i].Error = common.ServerError(err) + } + } + return results, nil +} === modified file 'src/github.com/juju/juju/apiserver/storageprovisioner/storageprovisioner_test.go' --- src/github.com/juju/juju/apiserver/storageprovisioner/storageprovisioner_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/storageprovisioner/storageprovisioner_test.go 2015-10-23 18:29:32 +0000 @@ -363,7 +363,20 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(results, jc.DeepEquals, params.VolumeParamsResults{ Results: []params.VolumeParamsResult{ - {Error: ¶ms.Error{`volume "0/0" is already provisioned`, ""}}, + {Result: params.VolumeParams{ + VolumeTag: "volume-0-0", + Size: 1024, + Provider: "machinescoped", + Tags: map[string]string{ + tags.JujuEnv: testing.EnvironmentTag.Id(), + }, + Attachment: ¶ms.VolumeAttachmentParams{ + MachineTag: "machine-0", + VolumeTag: "volume-0-0", + Provider: "machinescoped", + InstanceId: "inst-id", + }, + }}, {Result: params.VolumeParams{ VolumeTag: "volume-1", Size: 2048, @@ -412,7 +425,14 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(results, jc.DeepEquals, params.FilesystemParamsResults{ Results: []params.FilesystemParamsResult{ - {Error: ¶ms.Error{`filesystem "0/0" is already provisioned`, ""}}, + {Result: params.FilesystemParams{ + FilesystemTag: "filesystem-0-0", + Size: 1024, + Provider: "machinescoped", + Tags: map[string]string{ + tags.JujuEnv: testing.EnvironmentTag.Id(), + }, + }}, {Result: params.FilesystemParams{ FilesystemTag: "filesystem-1", Size: 2048, @@ -428,9 +448,15 @@ func (s *provisionerSuite) TestVolumeAttachmentParams(c *gc.C) { s.setupVolumes(c) - s.authorizer.EnvironManager = true - - err := s.State.SetVolumeAttachmentInfo( + + err := s.State.SetVolumeInfo(names.NewVolumeTag("3"), state.VolumeInfo{ + HardwareId: "123", + VolumeId: "xyz", + Size: 1024, + }) + c.Assert(err, jc.ErrorIsNil) + + err = s.State.SetVolumeAttachmentInfo( names.NewMachineTag("0"), names.NewVolumeTag("3"), state.VolumeAttachmentInfo{ @@ -465,6 +491,7 @@ MachineTag: "machine-0", VolumeTag: "volume-0-0", InstanceId: "inst-id", + VolumeId: "abc", Provider: "machinescoped", }}, {Result: params.VolumeAttachmentParams{ @@ -477,6 +504,7 @@ MachineTag: "machine-0", VolumeTag: "volume-3", InstanceId: "inst-id", + VolumeId: "xyz", Provider: "environscoped", ReadOnly: true, }}, @@ -492,14 +520,18 @@ func (s *provisionerSuite) TestFilesystemAttachmentParams(c *gc.C) { s.setupFilesystems(c) - s.authorizer.EnvironManager = true - - err := s.State.SetFilesystemAttachmentInfo( - names.NewMachineTag("2"), - names.NewFilesystemTag("3"), + + err := s.State.SetFilesystemInfo(names.NewFilesystemTag("1"), state.FilesystemInfo{ + FilesystemId: "fsid", + Size: 1024, + }) + c.Assert(err, jc.ErrorIsNil) + + err = s.State.SetFilesystemAttachmentInfo( + names.NewMachineTag("0"), + names.NewFilesystemTag("1"), state.FilesystemAttachmentInfo{ - MountPoint: "/srv", - ReadOnly: true, + MountPoint: "/in/the/place", }, ) c.Assert(err, jc.ErrorIsNil) @@ -526,6 +558,7 @@ MachineTag: "machine-0", FilesystemTag: "filesystem-0-0", InstanceId: "inst-id", + FilesystemId: "abc", Provider: "machinescoped", MountPoint: "/srv", ReadOnly: true, @@ -534,14 +567,14 @@ MachineTag: "machine-0", FilesystemTag: "filesystem-1", InstanceId: "inst-id", + FilesystemId: "fsid", Provider: "environscoped", + MountPoint: "/in/the/place", }}, {Result: params.FilesystemAttachmentParams{ MachineTag: "machine-2", FilesystemTag: "filesystem-3", Provider: "environscoped", - MountPoint: "/srv", - ReadOnly: true, }}, {Error: ¶ms.Error{"permission denied", "unauthorized access"}}, }, @@ -550,7 +583,12 @@ func (s *provisionerSuite) TestSetVolumeAttachmentInfo(c *gc.C) { s.setupVolumes(c) - s.authorizer.EnvironManager = true + + err := s.State.SetVolumeInfo(names.NewVolumeTag("4"), state.VolumeInfo{ + VolumeId: "whatever", + Size: 1024, + }) + c.Assert(err, jc.ErrorIsNil) results, err := s.api.SetVolumeAttachmentInfo(params.VolumeAttachments{ VolumeAttachments: []params.VolumeAttachment{{ @@ -584,8 +622,8 @@ c.Assert(results, jc.DeepEquals, params.ErrorResults{ Results: []params.ErrorResult{ {}, - {}, // TODO(axw) this should fail, since volume is not provisioned - {}, // TODO(axw) this should fail, since machine is not provisioned + {Error: ¶ms.Error{`cannot set info for volume attachment 1:0: volume "1" not provisioned`, "not provisioned"}}, + {Error: ¶ms.Error{`cannot set info for volume attachment 4:2: machine 2 not provisioned`, "not provisioned"}}, {Error: ¶ms.Error{"permission denied", "unauthorized access"}}, }, }) @@ -593,7 +631,12 @@ func (s *provisionerSuite) TestSetFilesystemAttachmentInfo(c *gc.C) { s.setupFilesystems(c) - s.authorizer.EnvironManager = true + + err := s.State.SetFilesystemInfo(names.NewFilesystemTag("3"), state.FilesystemInfo{ + FilesystemId: "whatever", + Size: 1024, + }) + c.Assert(err, jc.ErrorIsNil) results, err := s.api.SetFilesystemAttachmentInfo(params.FilesystemAttachments{ FilesystemAttachments: []params.FilesystemAttachment{{ @@ -627,8 +670,8 @@ c.Assert(results, jc.DeepEquals, params.ErrorResults{ Results: []params.ErrorResult{ {}, - {}, // TODO(axw) this should fail, since filesystem is not provisioned - {}, // TODO(axw) this should fail, since machine is not provisioned + {Error: ¶ms.Error{`cannot set info for filesystem attachment 1:0: filesystem "1" not provisioned`, "not provisioned"}}, + {Error: ¶ms.Error{`cannot set info for filesystem attachment 3:2: machine 2 not provisioned`, "not provisioned"}}, {Error: ¶ms.Error{"permission denied", "unauthorized access"}}, }, }) @@ -956,7 +999,6 @@ func (s *provisionerSuite) TestAttachmentLife(c *gc.C) { s.setupVolumes(c) - s.authorizer.EnvironManager = true // TODO(axw) test filesystem attachment life // TODO(axw) test Dying @@ -1024,7 +1066,6 @@ } func (s *provisionerSuite) TestEnvironConfig(c *gc.C) { - s.authorizer.EnvironManager = true stateEnvironConfig, err := s.State.EnvironConfig() c.Assert(err, jc.ErrorIsNil) @@ -1033,6 +1074,184 @@ c.Assert(result.Config, jc.DeepEquals, params.EnvironConfig(stateEnvironConfig.AllAttrs())) } +func (s *provisionerSuite) TestRemoveVolumesEnvironManager(c *gc.C) { + s.setupVolumes(c) + args := params.Entities{Entities: []params.Entity{ + {"volume-1-0"}, {"volume-1"}, {"volume-2"}, {"volume-42"}, + {"volume-invalid"}, {"machine-0"}, + }} + + err := s.State.DetachVolume(names.NewMachineTag("0"), names.NewVolumeTag("1")) + c.Assert(err, jc.ErrorIsNil) + err = s.State.RemoveVolumeAttachment(names.NewMachineTag("0"), names.NewVolumeTag("1")) + c.Assert(err, jc.ErrorIsNil) + err = s.State.DestroyVolume(names.NewVolumeTag("1")) + c.Assert(err, jc.ErrorIsNil) + + result, err := s.api.Remove(args) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, gc.DeepEquals, params.ErrorResults{ + Results: []params.ErrorResult{ + {Error: ¶ms.Error{"permission denied", "unauthorized access"}}, + {Error: nil}, + {Error: ¶ms.Error{Message: "removing volume 2: volume is not dead"}}, + {Error: nil}, + {Error: ¶ms.Error{Message: `"volume-invalid" is not a valid volume tag`}}, + {Error: ¶ms.Error{"permission denied", "unauthorized access"}}, + }, + }) +} + +func (s *provisionerSuite) TestRemoveFilesystemsEnvironManager(c *gc.C) { + s.setupFilesystems(c) + args := params.Entities{Entities: []params.Entity{ + {"filesystem-1-0"}, {"filesystem-1"}, {"filesystem-2"}, {"filesystem-42"}, + {"filesystem-invalid"}, {"machine-0"}, + }} + + err := s.State.DetachFilesystem(names.NewMachineTag("0"), names.NewFilesystemTag("1")) + c.Assert(err, jc.ErrorIsNil) + err = s.State.RemoveFilesystemAttachment(names.NewMachineTag("0"), names.NewFilesystemTag("1")) + c.Assert(err, jc.ErrorIsNil) + err = s.State.DestroyFilesystem(names.NewFilesystemTag("1")) + c.Assert(err, jc.ErrorIsNil) + + result, err := s.api.Remove(args) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, gc.DeepEquals, params.ErrorResults{ + Results: []params.ErrorResult{ + {Error: ¶ms.Error{"permission denied", "unauthorized access"}}, + {Error: nil}, + {Error: ¶ms.Error{Message: "removing filesystem 2: filesystem is not dead"}}, + {Error: nil}, + {Error: ¶ms.Error{Message: `"filesystem-invalid" is not a valid filesystem tag`}}, + {Error: ¶ms.Error{"permission denied", "unauthorized access"}}, + }, + }) +} + +func (s *provisionerSuite) TestRemoveVolumesMachineAgent(c *gc.C) { + s.setupVolumes(c) + s.authorizer.EnvironManager = false + args := params.Entities{Entities: []params.Entity{ + {"volume-0-0"}, {"volume-0-42"}, {"volume-42"}, + {"volume-invalid"}, {"machine-0"}, + }} + + err := s.State.DetachVolume(names.NewMachineTag("0"), names.NewVolumeTag("0/0")) + c.Assert(err, jc.ErrorIsNil) + err = s.State.RemoveVolumeAttachment(names.NewMachineTag("0"), names.NewVolumeTag("0/0")) + c.Assert(err, jc.ErrorIsNil) + err = s.State.DestroyVolume(names.NewVolumeTag("0/0")) + c.Assert(err, jc.ErrorIsNil) + + result, err := s.api.Remove(args) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, gc.DeepEquals, params.ErrorResults{ + Results: []params.ErrorResult{ + {Error: nil}, + {Error: nil}, + {Error: ¶ms.Error{"permission denied", "unauthorized access"}}, + {Error: ¶ms.Error{Message: `"volume-invalid" is not a valid volume tag`}}, + {Error: ¶ms.Error{"permission denied", "unauthorized access"}}, + }, + }) +} + +func (s *provisionerSuite) TestRemoveFilesystemsMachineAgent(c *gc.C) { + s.setupFilesystems(c) + s.authorizer.EnvironManager = false + args := params.Entities{Entities: []params.Entity{ + {"filesystem-0-0"}, {"filesystem-0-42"}, {"filesystem-42"}, + {"filesystem-invalid"}, {"machine-0"}, + }} + + err := s.State.DetachFilesystem(names.NewMachineTag("0"), names.NewFilesystemTag("0/0")) + c.Assert(err, jc.ErrorIsNil) + err = s.State.RemoveFilesystemAttachment(names.NewMachineTag("0"), names.NewFilesystemTag("0/0")) + c.Assert(err, jc.ErrorIsNil) + err = s.State.DestroyFilesystem(names.NewFilesystemTag("0/0")) + c.Assert(err, jc.ErrorIsNil) + + result, err := s.api.Remove(args) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, gc.DeepEquals, params.ErrorResults{ + Results: []params.ErrorResult{ + {Error: nil}, + {Error: nil}, + {Error: ¶ms.Error{"permission denied", "unauthorized access"}}, + {Error: ¶ms.Error{Message: `"filesystem-invalid" is not a valid filesystem tag`}}, + {Error: ¶ms.Error{"permission denied", "unauthorized access"}}, + }, + }) +} + +func (s *provisionerSuite) TestRemoveVolumeAttachments(c *gc.C) { + s.setupVolumes(c) + s.authorizer.EnvironManager = false + + err := s.State.DetachVolume(names.NewMachineTag("0"), names.NewVolumeTag("1")) + c.Assert(err, jc.ErrorIsNil) + + results, err := s.api.RemoveAttachment(params.MachineStorageIds{ + Ids: []params.MachineStorageId{{ + MachineTag: "machine-0", + AttachmentTag: "volume-0-0", + }, { + MachineTag: "machine-0", + AttachmentTag: "volume-1", + }, { + MachineTag: "machine-2", + AttachmentTag: "volume-4", + }, { + MachineTag: "machine-0", + AttachmentTag: "volume-42", + }}, + }) + c.Assert(err, jc.ErrorIsNil) + c.Assert(results, jc.DeepEquals, params.ErrorResults{ + Results: []params.ErrorResult{ + {Error: ¶ms.Error{Message: "removing attachment of volume 0/0 from machine 0: volume attachment is not dying"}}, + {Error: nil}, + {Error: ¶ms.Error{"permission denied", "unauthorized access"}}, + {Error: ¶ms.Error{`removing attachment of volume 42 from machine 0: volume "42" on machine "0" not found`, "not found"}}, + }, + }) +} + +func (s *provisionerSuite) TestRemoveFilesystemAttachments(c *gc.C) { + s.setupFilesystems(c) + s.authorizer.EnvironManager = false + + err := s.State.DetachFilesystem(names.NewMachineTag("0"), names.NewFilesystemTag("1")) + c.Assert(err, jc.ErrorIsNil) + + results, err := s.api.RemoveAttachment(params.MachineStorageIds{ + Ids: []params.MachineStorageId{{ + MachineTag: "machine-0", + AttachmentTag: "filesystem-0-0", + }, { + MachineTag: "machine-0", + AttachmentTag: "filesystem-1", + }, { + MachineTag: "machine-2", + AttachmentTag: "filesystem-4", + }, { + MachineTag: "machine-0", + AttachmentTag: "filesystem-42", + }}, + }) + c.Assert(err, jc.ErrorIsNil) + c.Assert(results, jc.DeepEquals, params.ErrorResults{ + Results: []params.ErrorResult{ + {Error: ¶ms.Error{Message: "removing attachment of filesystem 0/0 from machine 0: filesystem attachment is not dying"}}, + {Error: nil}, + {Error: ¶ms.Error{"permission denied", "unauthorized access"}}, + {Error: ¶ms.Error{`removing attachment of filesystem 42 from machine 0: filesystem "42" on machine "0" not found`, "not found"}}, + }, + }) +} + type byMachineAndEntity []params.MachineStorageId func (b byMachineAndEntity) Len() int { === added directory 'src/github.com/juju/juju/apiserver/subnets' === added file 'src/github.com/juju/juju/apiserver/subnets/export_test.go' --- src/github.com/juju/juju/apiserver/subnets/export_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/subnets/export_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,6 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package subnets + +var NewAPIWithBacking = newAPIWithBacking === added file 'src/github.com/juju/juju/apiserver/subnets/package_test.go' --- src/github.com/juju/juju/apiserver/subnets/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/subnets/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,14 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package subnets_test + +import ( + stdtesting "testing" + + gc "gopkg.in/check.v1" +) + +func TestPackage(t *stdtesting.T) { + gc.TestingT(t) +} === added file 'src/github.com/juju/juju/apiserver/subnets/shims.go' --- src/github.com/juju/juju/apiserver/subnets/shims.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/subnets/shims.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,145 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package subnets + +import ( + "github.com/juju/errors" + "github.com/juju/juju/environs/config" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/params" + providercommon "github.com/juju/juju/provider/common" + "github.com/juju/juju/state" +) + +// NOTE: All of the following code is only tested with a feature test. + +// subnetShim forwards and adapts state.Subnets methods to +// common.BackingSubnet. +type subnetShim struct { + common.BackingSubnet + subnet *state.Subnet +} + +func (s *subnetShim) CIDR() string { + return s.subnet.CIDR() +} + +func (s *subnetShim) VLANTag() int { + return s.subnet.VLANTag() +} + +func (s *subnetShim) ProviderId() string { + return s.subnet.ProviderId() +} + +func (s *subnetShim) AvailabilityZones() []string { + // TODO(dimitern): Add multiple zones to state.Subnet. + return []string{s.subnet.AvailabilityZone()} +} + +func (s *subnetShim) Life() params.Life { + return params.Life(s.subnet.Life().String()) +} + +func (s *subnetShim) Status() string { + // TODO(dimitern): This should happen in a cleaner way. + if s.Life() != params.Alive { + return "terminating" + } + return "in-use" +} + +func (s *subnetShim) SpaceName() string { + return s.subnet.SpaceName() +} + +// spaceShim forwards and adapts state.Space methods to BackingSpace. +type spaceShim struct { + common.BackingSpace + space *state.Space +} + +func (s *spaceShim) Name() string { + return s.space.Name() +} + +func (s *spaceShim) Subnets() ([]common.BackingSubnet, error) { + results, err := s.space.Subnets() + if err != nil { + return nil, errors.Trace(err) + } + subnets := make([]common.BackingSubnet, len(results)) + for i, result := range results { + subnets[i] = &subnetShim{subnet: result} + } + return subnets, nil +} + +// stateShim forwards and adapts state.State methods to Backing +// method. +type stateShim struct { + common.NetworkBacking + st *state.State +} + +func (s *stateShim) EnvironConfig() (*config.Config, error) { + return s.st.EnvironConfig() +} + +func (s *stateShim) AllSpaces() ([]common.BackingSpace, error) { + results, err := s.st.AllSpaces() + if err != nil { + return nil, errors.Trace(err) + } + spaces := make([]common.BackingSpace, len(results)) + for i, result := range results { + spaces[i] = &spaceShim{space: result} + } + return spaces, nil +} + +func (s *stateShim) AddSubnet(info common.BackingSubnetInfo) (common.BackingSubnet, error) { + // TODO(dimitern): Add multiple AZs per subnet in state. + var firstZone string + if len(info.AvailabilityZones) > 0 { + firstZone = info.AvailabilityZones[0] + } + _, err := s.st.AddSubnet(state.SubnetInfo{ + CIDR: info.CIDR, + VLANTag: info.VLANTag, + ProviderId: info.ProviderId, + AvailabilityZone: firstZone, + SpaceName: info.SpaceName, + }) + return nil, err // Drop the first result, as it's unused. +} + +func (s *stateShim) AllSubnets() ([]common.BackingSubnet, error) { + results, err := s.st.AllSubnets() + if err != nil { + return nil, errors.Trace(err) + } + subnets := make([]common.BackingSubnet, len(results)) + for i, result := range results { + subnets[i] = &subnetShim{subnet: result} + } + return subnets, nil +} + +type availZoneShim struct{} + +func (availZoneShim) Name() string { return "not-set" } +func (availZoneShim) Available() bool { return true } + +func (s *stateShim) AvailabilityZones() ([]providercommon.AvailabilityZone, error) { + // TODO(dimitern): Fix this to get them from state when available! + logger.Debugf("not getting availability zones from state yet") + return nil, nil +} + +func (s *stateShim) SetAvailabilityZones(zones []providercommon.AvailabilityZone) error { + logger.Debugf("not setting availability zones in state yet: %+v", zones) + return nil +} === added file 'src/github.com/juju/juju/apiserver/subnets/subnets.go' --- src/github.com/juju/juju/apiserver/subnets/subnets.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/subnets/subnets.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,605 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package subnets + +import ( + "fmt" + "net" + "strings" + + "github.com/juju/errors" + "github.com/juju/loggo" + "github.com/juju/names" + "github.com/juju/utils/set" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/environs" + "github.com/juju/juju/instance" + "github.com/juju/juju/network" + providercommon "github.com/juju/juju/provider/common" + "github.com/juju/juju/state" +) + +var logger = loggo.GetLogger("juju.apiserver.subnets") + +func init() { + common.RegisterStandardFacade("Subnets", 1, NewAPI) +} + +// API defines the methods the Subnets API facade implements. +type API interface { + // AllZones returns all availability zones known to Juju. If a + // zone is unusable, unavailable, or deprecated the Available + // field will be false. + AllZones() (params.ZoneResults, error) + + // AllSpaces returns the tags of all network spaces known to Juju. + AllSpaces() (params.SpaceResults, error) + + // AddSubnets adds existing subnets to Juju. + AddSubnets(args params.AddSubnetsParams) (params.ErrorResults, error) + + // ListSubnets returns the matching subnets after applying + // optional filters. + ListSubnets(args params.SubnetsFilters) (params.ListSubnetsResults, error) +} + +// subnetsAPI implements the API interface. +type subnetsAPI struct { + backing common.NetworkBacking + resources *common.Resources + authorizer common.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) (API, error) { + return newAPIWithBacking(&stateShim{st: st}, res, auth) +} + +// newAPIWithBacking creates a new server-side Subnets API facade with +// a common.NetworkBacking +func newAPIWithBacking(backing common.NetworkBacking, resources *common.Resources, authorizer common.Authorizer) (API, error) { + // Only clients can access the Subnets facade. + if !authorizer.AuthClient() { + return nil, common.ErrPerm + } + return &subnetsAPI{ + backing: backing, + resources: resources, + authorizer: authorizer, + }, nil +} + +// AllZones is defined on the API interface. +func (api *subnetsAPI) AllZones() (params.ZoneResults, error) { + var results params.ZoneResults + + zonesAsString := func(zones []providercommon.AvailabilityZone) string { + results := make([]string, len(zones)) + for i, zone := range zones { + results[i] = zone.Name() + } + return `"` + strings.Join(results, `", "`) + `"` + } + + // Try fetching cached zones first. + zones, err := api.backing.AvailabilityZones() + if err != nil { + return results, errors.Trace(err) + } + + if len(zones) == 0 { + // This is likely the first time we're called. + // Fetch all zones from the provider and update. + zones, err = api.updateZones() + if err != nil { + return results, errors.Annotate(err, "cannot update known zones") + } + logger.Debugf( + "updated the list of known zones from the environment: %s", zonesAsString(zones), + ) + } else { + logger.Debugf("using cached list of known zones: %s", zonesAsString(zones)) + } + + results.Results = make([]params.ZoneResult, len(zones)) + for i, zone := range zones { + results.Results[i].Name = zone.Name() + results.Results[i].Available = zone.Available() + } + return results, nil +} + +// AllSpaces is defined on the API interface. +func (api *subnetsAPI) AllSpaces() (params.SpaceResults, error) { + var results params.SpaceResults + + spaces, err := api.backing.AllSpaces() + if err != nil { + return results, errors.Trace(err) + } + + results.Results = make([]params.SpaceResult, len(spaces)) + for i, space := range spaces { + // TODO(dimitern): Add a Tag() a method and use it here. Too + // early to do it now as it will just complicate the tests. + tag := names.NewSpaceTag(space.Name()) + results.Results[i].Tag = tag.String() + } + return results, nil +} + +// zonedEnviron returns a providercommon.ZonedEnviron instance from +// the current environment config. If the environment does not support +// zones, an error satisfying errors.IsNotSupported() will be +// returned. +func (api *subnetsAPI) zonedEnviron() (providercommon.ZonedEnviron, error) { + envConfig, err := api.backing.EnvironConfig() + if err != nil { + return nil, errors.Annotate(err, "getting environment config") + } + + env, err := environs.New(envConfig) + if err != nil { + return nil, errors.Annotate(err, "opening environment") + } + if zonedEnv, ok := env.(providercommon.ZonedEnviron); ok { + return zonedEnv, nil + } + return nil, errors.NotSupportedf("availability zones") +} + +// networkingEnviron returns a environs.NetworkingEnviron instance +// from the current environment config, if supported. If the +// environment does not support environs.Networking, an error +// satisfying errors.IsNotSupported() will be returned. +func (api *subnetsAPI) networkingEnviron() (environs.NetworkingEnviron, error) { + envConfig, err := api.backing.EnvironConfig() + if err != nil { + return nil, errors.Annotate(err, "getting environment config") + } + + env, err := environs.New(envConfig) + if err != nil { + return nil, errors.Annotate(err, "opening environment") + } + if netEnv, ok := environs.SupportsNetworking(env); ok { + return netEnv, nil + } + return nil, errors.NotSupportedf("environment networking features") // " not supported" +} + +// updateZones attempts to retrieve all availability zones from the +// environment provider (if supported) and then updates the persisted +// list of zones in state, returning them as well on success. +func (api *subnetsAPI) updateZones() ([]providercommon.AvailabilityZone, error) { + zoned, err := api.zonedEnviron() + if err != nil { + return nil, errors.Trace(err) + } + zones, err := zoned.AvailabilityZones() + if err != nil { + return nil, errors.Trace(err) + } + + if err := api.backing.SetAvailabilityZones(zones); err != nil { + return nil, errors.Trace(err) + } + return zones, nil +} + +// addSubnetsCache holds cached lists of spaces, zones, and subnets, +// used for fast lookups while adding subnets. +type addSubnetsCache struct { + api *subnetsAPI + allSpaces set.Strings // all defined backing spaces + allZones set.Strings // all known provider zones + availableZones set.Strings // all the available zones + allSubnets []network.SubnetInfo // all (valid) provider subnets + // providerIdsByCIDR maps possibly duplicated CIDRs to one or more ids. + providerIdsByCIDR map[string]set.Strings + // subnetsByProviderId maps unique subnet ProviderIds to pointers + // to entries in allSubnets. + subnetsByProviderId map[string]*network.SubnetInfo +} + +func newAddSubnetsCache(api *subnetsAPI) *addSubnetsCache { + // Empty cache initially. + return &addSubnetsCache{ + api: api, + allSpaces: nil, + allZones: nil, + availableZones: nil, + allSubnets: nil, + providerIdsByCIDR: nil, + subnetsByProviderId: nil, + } +} + +// validateSpace parses the given spaceTag and verifies it exists by +// looking it up in the cache (or populates the cache if empty). +func (cache *addSubnetsCache) validateSpace(spaceTag string) (*names.SpaceTag, error) { + if spaceTag == "" { + return nil, errors.Errorf("SpaceTag is required") + } + tag, err := names.ParseSpaceTag(spaceTag) + if err != nil { + return nil, errors.Annotate(err, "given SpaceTag is invalid") + } + + // Otherwise we need the cache to validate. + if cache.allSpaces == nil { + // Not yet cached. + logger.Debugf("caching known spaces") + + allSpaces, err := cache.api.backing.AllSpaces() + if err != nil { + return nil, errors.Annotate(err, "cannot validate given SpaceTag") + } + cache.allSpaces = set.NewStrings() + for _, space := range allSpaces { + if cache.allSpaces.Contains(space.Name()) { + logger.Warningf("ignoring duplicated space %q", space.Name()) + continue + } + cache.allSpaces.Add(space.Name()) + } + } + if cache.allSpaces.IsEmpty() { + return nil, errors.Errorf("no spaces defined") + } + logger.Tracef("using cached spaces: %v", cache.allSpaces.SortedValues()) + + if !cache.allSpaces.Contains(tag.Id()) { + return nil, errors.NotFoundf("space %q", tag.Id()) // " not found" + } + return &tag, nil +} + +// cacheZones populates the allZones and availableZones cache, if it's +// empty. +func (cache *addSubnetsCache) cacheZones() error { + if cache.allZones != nil { + // Already cached. + logger.Tracef("using cached zones: %v", cache.allZones.SortedValues()) + return nil + } + + allZones, err := cache.api.AllZones() + if err != nil { + return errors.Annotate(err, "given Zones cannot be validated") + } + cache.allZones = set.NewStrings() + cache.availableZones = set.NewStrings() + for _, zone := range allZones.Results { + // AllZones() does not use the Error result field, so no + // need to check it here. + if cache.allZones.Contains(zone.Name) { + logger.Warningf("ignoring duplicated zone %q", zone.Name) + continue + } + + if zone.Available { + cache.availableZones.Add(zone.Name) + } + cache.allZones.Add(zone.Name) + } + logger.Debugf( + "%d known and %d available zones cached: %v", + cache.allZones.Size(), cache.availableZones.Size(), cache.allZones.SortedValues(), + ) + if cache.allZones.IsEmpty() { + cache.allZones = nil + // Cached an empty list. + return errors.Errorf("no zones defined") + } + return nil +} + +// validateZones ensures givenZones are valid. When providerZones are +// also set, givenZones must be a subset of them or match exactly. +// With non-empty providerZones and empty givenZones, it returns the +// providerZones (i.e. trusts the provider to know better). When no +// providerZones and only givenZones are set, only then the cache is +// used to validate givenZones. +func (cache *addSubnetsCache) validateZones(providerZones, givenZones []string) ([]string, error) { + givenSet := set.NewStrings(givenZones...) + providerSet := set.NewStrings(providerZones...) + + // First check if we can validate without using the cache. + switch { + case providerSet.IsEmpty() && givenSet.IsEmpty(): + return nil, errors.Errorf("Zones cannot be discovered from the provider and must be set") + case !providerSet.IsEmpty() && givenSet.IsEmpty(): + // Use provider zones when none given. + return providerSet.SortedValues(), nil + case !providerSet.IsEmpty() && !givenSet.IsEmpty(): + // Ensure givenZones either match providerZones or are a + // subset of them. + extraGiven := givenSet.Difference(providerSet) + if !extraGiven.IsEmpty() { + extra := `"` + strings.Join(extraGiven.SortedValues(), `", "`) + `"` + msg := fmt.Sprintf("Zones contain zones not allowed by the provider: %s", extra) + return nil, errors.Errorf(msg) + } + } + + // Otherwise we need the cache to validate. + if err := cache.cacheZones(); err != nil { + return nil, errors.Trace(err) + } + + diffAvailable := givenSet.Difference(cache.availableZones) + diffAll := givenSet.Difference(cache.allZones) + + if !diffAll.IsEmpty() { + extra := `"` + strings.Join(diffAll.SortedValues(), `", "`) + `"` + return nil, errors.Errorf("Zones contain unknown zones: %s", extra) + } + if !diffAvailable.IsEmpty() { + extra := `"` + strings.Join(diffAvailable.SortedValues(), `", "`) + `"` + return nil, errors.Errorf("Zones contain unavailable zones: %s", extra) + } + // All good - given zones are a subset and none are + // unavailable. + return givenSet.SortedValues(), nil +} + +// cacheSubnets tries to get and cache once all known provider +// subnets. It handles the case when subnets have duplicated CIDRs but +// distinct ProviderIds. It also handles weird edge cases, like no +// CIDR and/or ProviderId set for a subnet. +func (cache *addSubnetsCache) cacheSubnets() error { + if cache.allSubnets != nil { + // Already cached. + logger.Tracef("using %d cached subnets", len(cache.allSubnets)) + return nil + } + + netEnv, err := cache.api.networkingEnviron() + if err != nil { + return errors.Trace(err) + } + subnetInfo, err := netEnv.Subnets(instance.UnknownId, nil) + if err != nil { + return errors.Annotate(err, "cannot get provider subnets") + } + logger.Debugf("got %d subnets to cache from the provider", len(subnetInfo)) + + if len(subnetInfo) > 0 { + // Trying to avoid reallocations. + cache.allSubnets = make([]network.SubnetInfo, 0, len(subnetInfo)) + } + cache.providerIdsByCIDR = make(map[string]set.Strings) + cache.subnetsByProviderId = make(map[string]*network.SubnetInfo) + + for i, _ := range subnetInfo { + subnet := subnetInfo[i] + cidr := subnet.CIDR + providerId := string(subnet.ProviderId) + logger.Debugf( + "caching subnet with CIDR %q, ProviderId %q, Zones: %q", + cidr, providerId, subnet.AvailabilityZones, + ) + + if providerId == "" && cidr == "" { + logger.Warningf("found subnet with empty CIDR and ProviderId") + // But we still save it for lookups, which will probably fail anyway. + } else if providerId == "" { + logger.Warningf("found subnet with CIDR %q and empty ProviderId", cidr) + // But we still save it for lookups. + } else { + _, ok := cache.subnetsByProviderId[providerId] + if ok { + logger.Warningf( + "found subnet with CIDR %q and duplicated ProviderId %q", + cidr, providerId, + ) + // We just overwrite what's there for the same id. + // It's a weird case and it shouldn't happen with + // properly written providers, but anyway.. + } + } + cache.subnetsByProviderId[providerId] = &subnet + + if ids, ok := cache.providerIdsByCIDR[cidr]; !ok { + cache.providerIdsByCIDR[cidr] = set.NewStrings(providerId) + } else { + ids.Add(providerId) + logger.Debugf( + "duplicated subnet CIDR %q; collected ProviderIds so far: %v", + cidr, ids.SortedValues(), + ) + cache.providerIdsByCIDR[cidr] = ids + } + + cache.allSubnets = append(cache.allSubnets, subnet) + } + logger.Debugf("%d provider subnets cached", len(cache.allSubnets)) + if len(cache.allSubnets) == 0 { + // Cached an empty list. + return errors.Errorf("no subnets defined") + } + return nil +} + +// validateSubnet ensures either subnetTag or providerId is valid (not +// both), then uses the cache to validate and lookup the provider +// SubnetInfo for the subnet, if found. +func (cache *addSubnetsCache) validateSubnet(subnetTag, providerId string) (*network.SubnetInfo, error) { + haveTag := subnetTag != "" + haveProviderId := providerId != "" + + if !haveTag && !haveProviderId { + return nil, errors.Errorf("either SubnetTag or SubnetProviderId is required") + } else if haveTag && haveProviderId { + return nil, errors.Errorf("SubnetTag and SubnetProviderId cannot be both set") + } + var tag names.SubnetTag + if haveTag { + var err error + tag, err = names.ParseSubnetTag(subnetTag) + if err != nil { + return nil, errors.Annotate(err, "given SubnetTag is invalid") + } + } + + // Otherwise we need the cache to validate. + if err := cache.cacheSubnets(); err != nil { + return nil, errors.Trace(err) + } + + if haveTag { + providerIds, ok := cache.providerIdsByCIDR[tag.Id()] + if !ok || providerIds.IsEmpty() { + return nil, errors.NotFoundf("subnet with CIDR %q", tag.Id()) + } + if providerIds.Size() > 1 { + ids := `"` + strings.Join(providerIds.SortedValues(), `", "`) + `"` + return nil, errors.Errorf( + "multiple subnets with CIDR %q: retry using ProviderId from: %s", + tag.Id(), ids, + ) + } + // A single CIDR matched. + providerId = providerIds.Values()[0] + } + + info, ok := cache.subnetsByProviderId[providerId] + if !ok || info == nil { + return nil, errors.NotFoundf( + "subnet with CIDR %q and ProviderId %q", + tag.Id(), providerId, + ) + } + // Do last-call validation. + if !names.IsValidSubnet(info.CIDR) { + _, ipnet, err := net.ParseCIDR(info.CIDR) + if err != nil && info.CIDR != "" { + // The underlying error is not important here, just that + // the CIDR is invalid. + return nil, errors.Errorf( + "subnet with CIDR %q and ProviderId %q: invalid CIDR", + info.CIDR, providerId, + ) + } + if info.CIDR == "" { + return nil, errors.Errorf( + "subnet with ProviderId %q: empty CIDR", providerId, + ) + } + return nil, errors.Errorf( + "subnet with ProviderId %q: incorrect CIDR format %q, expected %q", + providerId, info.CIDR, ipnet.String(), + ) + } + return info, nil +} + +// addOneSubnet validates the given arguments, using cache for lookups +// (initialized on first use), then adds it to the backing store, if +// successful. +func (api *subnetsAPI) addOneSubnet(args params.AddSubnetParams, cache *addSubnetsCache) error { + subnetInfo, err := cache.validateSubnet(args.SubnetTag, args.SubnetProviderId) + if err != nil { + return errors.Trace(err) + } + spaceTag, err := cache.validateSpace(args.SpaceTag) + if err != nil { + return errors.Trace(err) + } + zones, err := cache.validateZones(subnetInfo.AvailabilityZones, args.Zones) + if err != nil { + return errors.Trace(err) + } + + // Try adding the subnet. + backingInfo := common.BackingSubnetInfo{ + ProviderId: string(subnetInfo.ProviderId), + CIDR: subnetInfo.CIDR, + VLANTag: subnetInfo.VLANTag, + AvailabilityZones: zones, + SpaceName: spaceTag.Id(), + } + if subnetInfo.AllocatableIPLow != nil { + backingInfo.AllocatableIPLow = subnetInfo.AllocatableIPLow.String() + } + if subnetInfo.AllocatableIPHigh != nil { + backingInfo.AllocatableIPHigh = subnetInfo.AllocatableIPHigh.String() + } + if _, err := api.backing.AddSubnet(backingInfo); err != nil { + return errors.Trace(err) + } + return nil +} + +// AddSubnets is defined on the API interface. +func (api *subnetsAPI) AddSubnets(args params.AddSubnetsParams) (params.ErrorResults, error) { + results := params.ErrorResults{ + Results: make([]params.ErrorResult, len(args.Subnets)), + } + + if len(args.Subnets) == 0 { + return results, nil + } + + cache := newAddSubnetsCache(api) + for i, arg := range args.Subnets { + err := api.addOneSubnet(arg, cache) + if err != nil { + results.Results[i].Error = common.ServerError(err) + } + } + return results, nil +} + +// ListSubnets lists all the available subnets or only those matching +// all given optional filters. +func (api *subnetsAPI) ListSubnets(args params.SubnetsFilters) (results params.ListSubnetsResults, err error) { + subnets, err := api.backing.AllSubnets() + if err != nil { + return results, errors.Trace(err) + } + + var spaceFilter string + if args.SpaceTag != "" { + tag, err := names.ParseSpaceTag(args.SpaceTag) + if err != nil { + return results, errors.Trace(err) + } + spaceFilter = tag.Id() + } + zoneFilter := args.Zone + + for _, subnet := range subnets { + if spaceFilter != "" && subnet.SpaceName() != spaceFilter { + logger.Tracef( + "filtering subnet %q from space %q not matching filter %q", + subnet.CIDR(), subnet.SpaceName(), spaceFilter, + ) + continue + } + zoneSet := set.NewStrings(subnet.AvailabilityZones()...) + if zoneFilter != "" && !zoneSet.IsEmpty() && !zoneSet.Contains(zoneFilter) { + logger.Tracef( + "filtering subnet %q with zones %v not matching filter %q", + subnet.CIDR(), subnet.AvailabilityZones(), zoneFilter, + ) + continue + } + result := params.Subnet{ + CIDR: subnet.CIDR(), + ProviderId: subnet.ProviderId(), + VLANTag: subnet.VLANTag(), + Life: subnet.Life(), + SpaceTag: names.NewSpaceTag(subnet.SpaceName()).String(), + Zones: subnet.AvailabilityZones(), + Status: subnet.Status(), + } + results.Results = append(results.Results, result) + } + return results, nil +} === added file 'src/github.com/juju/juju/apiserver/subnets/subnets_test.go' --- src/github.com/juju/juju/apiserver/subnets/subnets_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/subnets/subnets_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,838 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package subnets_test + +import ( + "github.com/juju/errors" + "github.com/juju/names" + jc "github.com/juju/testing/checkers" + "github.com/juju/utils/set" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/apiserver/subnets" + apiservertesting "github.com/juju/juju/apiserver/testing" + "github.com/juju/juju/instance" + "github.com/juju/juju/network" + providercommon "github.com/juju/juju/provider/common" + coretesting "github.com/juju/juju/testing" +) + +type SubnetsSuite struct { + coretesting.BaseSuite + apiservertesting.StubNetwork + + resources *common.Resources + authorizer apiservertesting.FakeAuthorizer + facade subnets.API +} + +var _ = gc.Suite(&SubnetsSuite{}) + +func (s *SubnetsSuite) SetUpSuite(c *gc.C) { + s.StubNetwork.SetUpSuite(c) + s.BaseSuite.SetUpSuite(c) +} + +func (s *SubnetsSuite) TearDownSuite(c *gc.C) { + s.BaseSuite.TearDownSuite(c) +} + +func (s *SubnetsSuite) SetUpTest(c *gc.C) { + s.BaseSuite.SetUpTest(c) + apiservertesting.BackingInstance.SetUp(c, apiservertesting.StubZonedEnvironName, apiservertesting.WithZones, apiservertesting.WithSpaces, apiservertesting.WithSubnets) + + s.resources = common.NewResources() + s.authorizer = apiservertesting.FakeAuthorizer{ + Tag: names.NewUserTag("admin"), + EnvironManager: false, + } + + var err error + s.facade, err = subnets.NewAPIWithBacking( + apiservertesting.BackingInstance, s.resources, s.authorizer, + ) + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.facade, gc.NotNil) +} + +func (s *SubnetsSuite) TearDownTest(c *gc.C) { + if s.resources != nil { + s.resources.StopAll() + } + s.BaseSuite.TearDownTest(c) +} + +// AssertAllZonesResult makes it easier to verify AllZones results. +func (s *SubnetsSuite) AssertAllZonesResult(c *gc.C, got params.ZoneResults, expected []providercommon.AvailabilityZone) { + results := make([]params.ZoneResult, len(expected)) + for i, zone := range expected { + results[i].Name = zone.Name() + results[i].Available = zone.Available() + } + c.Assert(got, jc.DeepEquals, params.ZoneResults{Results: results}) +} + +// AssertAllSpacesResult makes it easier to verify AllSpaces results. +func (s *SubnetsSuite) AssertAllSpacesResult(c *gc.C, got params.SpaceResults, expected []common.BackingSpace) { + seen := set.Strings{} + results := []params.SpaceResult{} + for _, space := range expected { + if seen.Contains(space.Name()) { + continue + } + seen.Add(space.Name()) + result := params.SpaceResult{} + result.Tag = names.NewSpaceTag(space.Name()).String() + results = append(results, result) + } + c.Assert(got, jc.DeepEquals, params.SpaceResults{Results: results}) +} + +func (s *SubnetsSuite) TestNewAPIWithBacking(c *gc.C) { + // Clients are allowed. + facade, err := subnets.NewAPIWithBacking( + apiservertesting.BackingInstance, s.resources, s.authorizer, + ) + c.Assert(err, jc.ErrorIsNil) + c.Assert(facade, gc.NotNil) + // No calls so far. + apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub) + + // Agents are not allowed + agentAuthorizer := s.authorizer + agentAuthorizer.Tag = names.NewMachineTag("42") + facade, err = subnets.NewAPIWithBacking( + apiservertesting.BackingInstance, s.resources, agentAuthorizer, + ) + c.Assert(err, jc.DeepEquals, common.ErrPerm) + c.Assert(facade, gc.IsNil) + // No calls so far. + apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub) +} + +func (s *SubnetsSuite) TestAllZonesWhenBackingAvailabilityZonesFails(c *gc.C) { + apiservertesting.SharedStub.SetErrors(errors.NotSupportedf("zones")) + + results, err := s.facade.AllZones() + c.Assert(err, gc.ErrorMatches, "zones not supported") + // Verify the cause is not obscured. + c.Assert(err, jc.Satisfies, errors.IsNotSupported) + c.Assert(results, jc.DeepEquals, params.ZoneResults{}) + + apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub, + apiservertesting.BackingCall("AvailabilityZones"), + ) +} + +func (s *SubnetsSuite) TestAllZonesUsesBackingZonesWhenAvailable(c *gc.C) { + results, err := s.facade.AllZones() + c.Assert(err, jc.ErrorIsNil) + s.AssertAllZonesResult(c, results, apiservertesting.BackingInstance.Zones) + + apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub, + apiservertesting.BackingCall("AvailabilityZones"), + ) +} + +func (s *SubnetsSuite) TestAllZonesWithNoBackingZonesUpdates(c *gc.C) { + apiservertesting.BackingInstance.SetUp(c, apiservertesting.StubZonedEnvironName, apiservertesting.WithoutZones, apiservertesting.WithSpaces, apiservertesting.WithSubnets) + + results, err := s.facade.AllZones() + c.Assert(err, jc.ErrorIsNil) + s.AssertAllZonesResult(c, results, apiservertesting.ProviderInstance.Zones) + + apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub, + apiservertesting.BackingCall("AvailabilityZones"), + apiservertesting.BackingCall("EnvironConfig"), + apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), + apiservertesting.ZonedEnvironCall("AvailabilityZones"), + apiservertesting.BackingCall("SetAvailabilityZones", apiservertesting.ProviderInstance.Zones), + ) +} + +func (s *SubnetsSuite) TestAllZonesWithNoBackingZonesAndSetFails(c *gc.C) { + apiservertesting.BackingInstance.SetUp(c, apiservertesting.StubZonedEnvironName, apiservertesting.WithoutZones, apiservertesting.WithSpaces, apiservertesting.WithSubnets) + apiservertesting.SharedStub.SetErrors( + nil, // Backing.AvailabilityZones + nil, // Backing.EnvironConfig + nil, // Provider.Open + nil, // ZonedEnviron.AvailabilityZones + errors.NotSupportedf("setting"), // Backing.SetAvailabilityZones + ) + + results, err := s.facade.AllZones() + c.Assert(err, gc.ErrorMatches, + `cannot update known zones: setting not supported`, + ) + // Verify the cause is not obscured. + c.Assert(err, jc.Satisfies, errors.IsNotSupported) + c.Assert(results, jc.DeepEquals, params.ZoneResults{}) + + apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub, + apiservertesting.BackingCall("AvailabilityZones"), + apiservertesting.BackingCall("EnvironConfig"), + apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), + apiservertesting.ZonedEnvironCall("AvailabilityZones"), + apiservertesting.BackingCall("SetAvailabilityZones", apiservertesting.ProviderInstance.Zones), + ) +} + +func (s *SubnetsSuite) TestAllZonesWithNoBackingZonesAndFetchingZonesFails(c *gc.C) { + apiservertesting.BackingInstance.SetUp(c, apiservertesting.StubZonedEnvironName, apiservertesting.WithoutZones, apiservertesting.WithSpaces, apiservertesting.WithSubnets) + apiservertesting.SharedStub.SetErrors( + nil, // Backing.AvailabilityZones + nil, // Backing.EnvironConfig + nil, // Provider.Open + errors.NotValidf("foo"), // ZonedEnviron.AvailabilityZones + ) + + results, err := s.facade.AllZones() + c.Assert(err, gc.ErrorMatches, + `cannot update known zones: foo not valid`, + ) + // Verify the cause is not obscured. + c.Assert(err, jc.Satisfies, errors.IsNotValid) + c.Assert(results, jc.DeepEquals, params.ZoneResults{}) + + apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub, + apiservertesting.BackingCall("AvailabilityZones"), + apiservertesting.BackingCall("EnvironConfig"), + apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), + apiservertesting.ZonedEnvironCall("AvailabilityZones"), + ) +} + +func (s *SubnetsSuite) TestAllZonesWithNoBackingZonesAndEnvironConfigFails(c *gc.C) { + apiservertesting.BackingInstance.SetUp(c, apiservertesting.StubZonedEnvironName, apiservertesting.WithoutZones, apiservertesting.WithSpaces, apiservertesting.WithSubnets) + apiservertesting.SharedStub.SetErrors( + nil, // Backing.AvailabilityZones + errors.NotFoundf("config"), // Backing.EnvironConfig + ) + + results, err := s.facade.AllZones() + c.Assert(err, gc.ErrorMatches, + `cannot update known zones: getting environment config: config not found`, + ) + // Verify the cause is not obscured. + c.Assert(err, jc.Satisfies, errors.IsNotFound) + c.Assert(results, jc.DeepEquals, params.ZoneResults{}) + + apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub, + apiservertesting.BackingCall("AvailabilityZones"), + apiservertesting.BackingCall("EnvironConfig"), + ) +} + +func (s *SubnetsSuite) TestAllZonesWithNoBackingZonesAndOpenFails(c *gc.C) { + apiservertesting.BackingInstance.SetUp(c, apiservertesting.StubZonedEnvironName, apiservertesting.WithoutZones, apiservertesting.WithSpaces, apiservertesting.WithSubnets) + apiservertesting.SharedStub.SetErrors( + nil, // Backing.AvailabilityZones + nil, // Backing.EnvironConfig + errors.NotValidf("config"), // Provider.Open + ) + + results, err := s.facade.AllZones() + c.Assert(err, gc.ErrorMatches, + `cannot update known zones: opening environment: config not valid`, + ) + // Verify the cause is not obscured. + c.Assert(err, jc.Satisfies, errors.IsNotValid) + c.Assert(results, jc.DeepEquals, params.ZoneResults{}) + + apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub, + apiservertesting.BackingCall("AvailabilityZones"), + apiservertesting.BackingCall("EnvironConfig"), + apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), + ) +} + +func (s *SubnetsSuite) TestAllZonesWithNoBackingZonesAndZonesNotSupported(c *gc.C) { + apiservertesting.BackingInstance.SetUp(c, apiservertesting.StubEnvironName, apiservertesting.WithoutZones, apiservertesting.WithSpaces, apiservertesting.WithSubnets) + // ZonedEnviron not supported + + results, err := s.facade.AllZones() + c.Assert(err, gc.ErrorMatches, + `cannot update known zones: availability zones not supported`, + ) + // Verify the cause is not obscured. + c.Assert(err, jc.Satisfies, errors.IsNotSupported) + c.Assert(results, jc.DeepEquals, params.ZoneResults{}) + + apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub, + apiservertesting.BackingCall("AvailabilityZones"), + apiservertesting.BackingCall("EnvironConfig"), + apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), + ) +} + +func (s *SubnetsSuite) TestAllSpacesWithExistingSuccess(c *gc.C) { + s.testAllSpacesSuccess(c, apiservertesting.WithSpaces) +} + +func (s *SubnetsSuite) TestAllSpacesNoExistingSuccess(c *gc.C) { + s.testAllSpacesSuccess(c, apiservertesting.WithoutSpaces) +} + +func (s *SubnetsSuite) testAllSpacesSuccess(c *gc.C, withBackingSpaces apiservertesting.SetUpFlag) { + apiservertesting.BackingInstance.SetUp(c, apiservertesting.StubZonedEnvironName, apiservertesting.WithZones, withBackingSpaces, apiservertesting.WithSubnets) + + results, err := s.facade.AllSpaces() + c.Assert(err, jc.ErrorIsNil) + s.AssertAllSpacesResult(c, results, apiservertesting.BackingInstance.Spaces) + + apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub, + apiservertesting.BackingCall("AllSpaces"), + ) +} + +func (s *SubnetsSuite) TestAllSpacesFailure(c *gc.C) { + apiservertesting.SharedStub.SetErrors(errors.NotFoundf("boom")) + + results, err := s.facade.AllSpaces() + c.Assert(err, gc.ErrorMatches, "boom not found") + // Verify the cause is not obscured. + c.Assert(err, jc.Satisfies, errors.IsNotFound) + c.Assert(results, jc.DeepEquals, params.SpaceResults{}) + + apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub, + apiservertesting.BackingCall("AllSpaces"), + ) +} + +func (s *SubnetsSuite) TestAddSubnetsParamsCombinations(c *gc.C) { + apiservertesting.BackingInstance.SetUp(c, apiservertesting.StubNetworkingEnvironName, apiservertesting.WithZones, apiservertesting.WithSpaces, apiservertesting.WithSubnets) + + args := params.AddSubnetsParams{Subnets: []params.AddSubnetParams{{ + // nothing set; early exit: no calls + }, { + // neither tag nor id set: the rest is ignored; same as above + SpaceTag: "any", + Zones: []string{"any", "ignored"}, + }, { + // both tag and id set; same as above + SubnetTag: "any", + SubnetProviderId: "any", + }, { + // lookup by id needed, no cached subnets; EnvironConfig(): error + SubnetProviderId: "any", + }, { + // same as above, need to cache subnets; EnvironConfig(): ok; Open(): error + SubnetProviderId: "ignored", + }, { + // as above, caching again; EnvironConfig(), Open(): ok; Subnets(): error + SubnetProviderId: "unimportant", + }, { + // exactly as above, except all 3 calls ok; cached lookup: id not found + SubnetProviderId: "missing", + }, { + // cached lookup by id (no calls): not found error + SubnetProviderId: "void", + }, { + // cached lookup by id: ok; parsing space tag: invalid tag error + SubnetProviderId: "sn-deadbeef", + SpaceTag: "invalid", + }, { + // as above, but slightly different error: invalid space tag error + SubnetProviderId: "sn-zadf00d", + SpaceTag: "unit-foo", + }, { + // as above; yet another similar error (valid tag with another kind) + SubnetProviderId: "vlan-42", + SpaceTag: "unit-foo-0", + }, { + // invalid tag (no kind): error (no calls) + SubnetTag: "invalid", + }, { + // invalid subnet tag (another kind): same as above + SubnetTag: "service-bar", + }, { + // cached lookup by missing CIDR: not found error + SubnetTag: "subnet-1.2.3.0/24", + }, { + // cached lookup by duplicate CIDR: multiple choices error + SubnetTag: "subnet-10.10.0.0/24", + }, { + // cached lookup by CIDR with empty provider id: ok; space tag is required error + SubnetTag: "subnet-10.20.0.0/16", + }, { + // cached lookup by id with invalid CIDR: cannot be added error + SubnetProviderId: "sn-invalid", + }, { + // cached lookup by id with empty CIDR: cannot be added error + SubnetProviderId: "sn-empty", + }, { + // cached lookup by id with incorrectly specified CIDR: cannot be added error + SubnetProviderId: "sn-awesome", + }, { + // cached lookup by CIDR: ok; valid tag; caching spaces: AllSpaces(): error + SubnetTag: "subnet-10.30.1.0/24", + SpaceTag: "space-unverified", + }, { + // exactly as above, except AllSpaces(): ok; cached lookup: space not found + SubnetTag: "subnet-2001:db8::/32", + SpaceTag: "space-missing", + }, { + // both cached lookups (CIDR, space): ok; no provider or given zones: error + SubnetTag: "subnet-10.42.0.0/16", + SpaceTag: "space-dmz", + }, { + // like above; with provider zones, extra given: error + SubnetProviderId: "vlan-42", + SpaceTag: "space-private", + Zones: []string{ + "zone2", // not allowed, existing, unavailable + "zone3", // allowed, existing, available + "missing", // not allowed, non-existing + "zone3", // duplicates are ignored (should they ?) + "zone1", // not allowed, existing, available + }, + }, { + // like above; no provider, only given zones; caching: AllZones(): error + SubnetTag: "subnet-10.42.0.0/16", + SpaceTag: "space-dmz", + Zones: []string{"any", "ignored"}, + }, { + // as above, but unknown zones given: cached: AllZones(): ok; unknown zones error + SubnetTag: "subnet-10.42.0.0/16", + SpaceTag: "space-dmz", + Zones: []string{"missing", "gone"}, + }, { + // as above, but unknown and unavailable zones given: same error (no calls) + SubnetTag: "subnet-10.42.0.0/16", + SpaceTag: "space-dmz", + Zones: []string{"zone4", "missing", "zone2"}, + }, { + // as above, but unavailable zones given: Zones contains unavailable error + SubnetTag: "subnet-10.42.0.0/16", + SpaceTag: "space-dmz", + Zones: []string{"zone2", "zone4"}, + }, { + // as above, but available and unavailable zones given: same error as above + SubnetTag: "subnet-10.42.0.0/16", + SpaceTag: "space-dmz", + Zones: []string{"zone4", "zone3"}, + }, { + // everything succeeds, using caches as needed, until: AddSubnet(): error + SubnetProviderId: "sn-ipv6", + SpaceTag: "space-dmz", + Zones: []string{"zone1"}, + // restriction of provider zones [zone1, zone3] + }, { + // cached lookups by CIDR, space: ok; duplicated provider id: unavailable zone2 + SubnetTag: "subnet-10.99.88.0/24", + SpaceTag: "space-dmz", + Zones: []string{"zone2"}, + // due to the duplicate ProviderId provider zones from subnet + // with the last ProviderId=sn-deadbeef are used + // (10.10.0.0/24); [zone2], not the 10.99.88.0/24 provider + // zones: [zone1, zone2]. + }, { + // same as above, but AddSubnet(): ok; success (backing verified later) + SubnetProviderId: "sn-ipv6", + SpaceTag: "space-dmz", + Zones: []string{"zone1"}, + // restriction of provider zones [zone1, zone3] + }, { + // success (CIDR lookup; with provider (no given) zones): AddSubnet(): ok + SubnetTag: "subnet-10.30.1.0/24", + SpaceTag: "space-private", + // Zones not given, so provider zones are used instead: [zone3] + }, { + // success (id lookup; given zones match provider zones) AddSubnet(): ok + SubnetProviderId: "sn-zadf00d", + SpaceTag: "space-private", + Zones: []string{"zone1"}, + }}} + apiservertesting.SharedStub.SetErrors( + // caching subnets (1st attempt): fails + errors.NotFoundf("config"), // BackingInstance.EnvironConfig (1st call) + + // caching subnets (2nd attepmt): fails + nil, // BackingInstance.EnvironConfig (2nd call) + errors.NotFoundf("provider"), // ProviderInstance.Open (1st call) + + // caching subnets (3rd attempt): fails + nil, // BackingInstance.EnvironConfig (3rd call) + nil, // ProviderInstance.Open (2nd call) + errors.NotFoundf("subnets"), // NetworkingEnvironInstance.Subnets (1st call) + + // caching subnets (4th attempt): succeeds + nil, // BackingInstance.EnvironConfig (4th call) + nil, // ProviderInstance.Open (3rd call) + nil, // NetworkingEnvironInstance.Subnets (2nd call) + + // caching spaces (1st and 2nd attempts) + errors.NotFoundf("spaces"), // BackingInstance.AllSpaces (1st call) + nil, // BackingInstance.AllSpaces (2nd call) + + // cacing zones (1st and 2nd attempts) + errors.NotFoundf("zones"), // BackingInstance.AvailabilityZones (1st call) + nil, // BackingInstance.AvailabilityZones (2nd call) + + // validation done; adding subnets to backing store + errors.NotFoundf("state"), // BackingInstance.AddSubnet (1st call) + // the next 3 BackingInstance.AddSubnet calls succeed(2nd call) + ) + + expectedErrors := []struct { + message string + satisfier func(error) bool + }{ + {"either SubnetTag or SubnetProviderId is required", nil}, + {"either SubnetTag or SubnetProviderId is required", nil}, + {"SubnetTag and SubnetProviderId cannot be both set", nil}, + {"getting environment config: config not found", params.IsCodeNotFound}, + {"opening environment: provider not found", params.IsCodeNotFound}, + {"cannot get provider subnets: subnets not found", params.IsCodeNotFound}, + {`subnet with CIDR "" and ProviderId "missing" not found`, params.IsCodeNotFound}, + {`subnet with CIDR "" and ProviderId "void" not found`, params.IsCodeNotFound}, + {`given SpaceTag is invalid: "invalid" is not a valid tag`, nil}, + {`given SpaceTag is invalid: "unit-foo" is not a valid unit tag`, nil}, + {`given SpaceTag is invalid: "unit-foo-0" is not a valid space tag`, nil}, + {`given SubnetTag is invalid: "invalid" is not a valid tag`, nil}, + {`given SubnetTag is invalid: "service-bar" is not a valid subnet tag`, nil}, + {`subnet with CIDR "1.2.3.0/24" not found`, params.IsCodeNotFound}, + { + `multiple subnets with CIDR "10.10.0.0/24": ` + + `retry using ProviderId from: "sn-deadbeef", "sn-zadf00d"`, nil, + }, + {"SpaceTag is required", nil}, + {`subnet with CIDR "invalid" and ProviderId "sn-invalid": invalid CIDR`, nil}, + {`subnet with ProviderId "sn-empty": empty CIDR`, nil}, + { + `subnet with ProviderId "sn-awesome": ` + + `incorrect CIDR format "0.1.2.3/4", expected "0.0.0.0/4"`, nil, + }, + {"cannot validate given SpaceTag: spaces not found", params.IsCodeNotFound}, + {`space "missing" not found`, params.IsCodeNotFound}, + {"Zones cannot be discovered from the provider and must be set", nil}, + {`Zones contain zones not allowed by the provider: "missing", "zone1", "zone2"`, nil}, + {"given Zones cannot be validated: zones not found", params.IsCodeNotFound}, + {`Zones contain unknown zones: "gone", "missing"`, nil}, + {`Zones contain unknown zones: "missing"`, nil}, + {`Zones contain unavailable zones: "zone2", "zone4"`, nil}, + {`Zones contain unavailable zones: "zone4"`, nil}, + {"state not found", params.IsCodeNotFound}, + {`Zones contain unavailable zones: "zone2"`, nil}, + {"", nil}, + {"", nil}, + {"", nil}, + } + expectedBackingInfos := []common.BackingSubnetInfo{{ + ProviderId: "sn-ipv6", + CIDR: "2001:db8::/32", + VLANTag: 0, + AllocatableIPHigh: "", + AllocatableIPLow: "", + AvailabilityZones: []string{"zone1"}, + SpaceName: "dmz", + }, { + ProviderId: "vlan-42", + CIDR: "10.30.1.0/24", + VLANTag: 42, + AllocatableIPHigh: "", + AllocatableIPLow: "", + AvailabilityZones: []string{"zone3"}, + SpaceName: "private", + }, { + ProviderId: "sn-zadf00d", + CIDR: "10.10.0.0/24", + VLANTag: 0, + AllocatableIPHigh: "10.10.0.100", + AllocatableIPLow: "10.10.0.10", + AvailabilityZones: []string{"zone1"}, + SpaceName: "private", + }} + c.Check(expectedErrors, gc.HasLen, len(args.Subnets)) + results, err := s.facade.AddSubnets(args) + c.Assert(err, jc.ErrorIsNil) + c.Assert(len(results.Results), gc.Equals, len(args.Subnets)) + for i, result := range results.Results { + c.Logf("result #%d: expected: %q", i, expectedErrors[i].message) + if expectedErrors[i].message == "" { + if !c.Check(result.Error, gc.IsNil) { + c.Logf("unexpected error: %v; args: %#v", result.Error, args.Subnets[i]) + } + continue + } + if !c.Check(result.Error, gc.NotNil) { + c.Logf("unexpected success; args: %#v", args.Subnets[i]) + continue + } + c.Check(result.Error.Message, gc.Equals, expectedErrors[i].message) + if expectedErrors[i].satisfier != nil { + c.Check(result.Error, jc.Satisfies, expectedErrors[i].satisfier) + } else { + c.Check(result.Error.Code, gc.Equals, "") + } + } + + apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub, + // caching subnets (1st attempt): fails + apiservertesting.BackingCall("EnvironConfig"), + + // caching subnets (2nd attepmt): fails + apiservertesting.BackingCall("EnvironConfig"), + apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), + + // caching subnets (3rd attempt): fails + apiservertesting.BackingCall("EnvironConfig"), + apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), + apiservertesting.NetworkingEnvironCall("Subnets", instance.UnknownId, []network.Id(nil)), + + // caching subnets (4th attempt): succeeds + apiservertesting.BackingCall("EnvironConfig"), + apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), + apiservertesting.NetworkingEnvironCall("Subnets", instance.UnknownId, []network.Id(nil)), + + // caching spaces (1st and 2nd attempts) + apiservertesting.BackingCall("AllSpaces"), + apiservertesting.BackingCall("AllSpaces"), + + // cacing zones (1st and 2nd attempts) + apiservertesting.BackingCall("AvailabilityZones"), + apiservertesting.BackingCall("AvailabilityZones"), + + // validation done; adding subnets to backing store + apiservertesting.BackingCall("AddSubnet", expectedBackingInfos[0]), + apiservertesting.BackingCall("AddSubnet", expectedBackingInfos[0]), + apiservertesting.BackingCall("AddSubnet", expectedBackingInfos[1]), + apiservertesting.BackingCall("AddSubnet", expectedBackingInfos[2]), + ) + apiservertesting.ResetStub(apiservertesting.SharedStub) + + // Finally, check that no params yields no results. + results, err = s.facade.AddSubnets(params.AddSubnetsParams{}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(results.Results, gc.NotNil) + c.Assert(results.Results, gc.HasLen, 0) + + apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub) +} + +func (s *SubnetsSuite) CheckAddSubnetsFails( + c *gc.C, envName string, + withZones, withSpaces, withSubnets apiservertesting.SetUpFlag, + expectedError string, + expectedSatisfies func(error) bool, +) { + apiservertesting.BackingInstance.SetUp(c, envName, withZones, withSpaces, withSubnets) + + // These calls always happen. + expectedCalls := []apiservertesting.StubMethodCall{ + apiservertesting.BackingCall("EnvironConfig"), + apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), + } + + // Subnets is also always called. but the receiver is different. + switch envName { + case apiservertesting.StubNetworkingEnvironName: + expectedCalls = append( + expectedCalls, + apiservertesting.NetworkingEnvironCall("Subnets", instance.UnknownId, []network.Id(nil)), + ) + case apiservertesting.StubZonedNetworkingEnvironName: + expectedCalls = append( + expectedCalls, + apiservertesting.ZonedNetworkingEnvironCall("Subnets", instance.UnknownId, []network.Id(nil)), + ) + } + + if !withSubnets { + // Set provider subnets to empty for this test. + originalSubnets := make([]network.SubnetInfo, len(apiservertesting.ProviderInstance.Subnets)) + copy(originalSubnets, apiservertesting.ProviderInstance.Subnets) + apiservertesting.ProviderInstance.Subnets = []network.SubnetInfo{} + + defer func() { + apiservertesting.ProviderInstance.Subnets = make([]network.SubnetInfo, len(originalSubnets)) + copy(apiservertesting.ProviderInstance.Subnets, originalSubnets) + }() + + if envName == apiservertesting.StubEnvironName || envName == apiservertesting.StubNetworkingEnvironName { + // networking is either not supported or no subnets are + // defined, so expected the same calls for each of the two + // arguments to AddSubnets() below. + expectedCalls = append(expectedCalls, expectedCalls...) + } + } else { + // Having subnets implies spaces will be cached as well. + expectedCalls = append(expectedCalls, apiservertesting.BackingCall("AllSpaces")) + } + + if withSpaces && withSubnets { + // Having both subnets and spaces means we'll also cache zones. + expectedCalls = append(expectedCalls, apiservertesting.BackingCall("AvailabilityZones")) + } + + if !withZones && withSpaces { + // Set provider zones to empty for this test. + originalZones := make([]providercommon.AvailabilityZone, len(apiservertesting.ProviderInstance.Zones)) + copy(originalZones, apiservertesting.ProviderInstance.Zones) + apiservertesting.ProviderInstance.Zones = []providercommon.AvailabilityZone{} + + defer func() { + apiservertesting.ProviderInstance.Zones = make([]providercommon.AvailabilityZone, len(originalZones)) + copy(apiservertesting.ProviderInstance.Zones, originalZones) + }() + + // updateZones tries to constructs a ZonedEnviron with these calls. + zoneCalls := append([]apiservertesting.StubMethodCall{}, + apiservertesting.BackingCall("EnvironConfig"), + apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), + ) + // Receiver can differ according to envName, but + // AvailabilityZones() will be called on either receiver. + switch envName { + case apiservertesting.StubZonedEnvironName: + zoneCalls = append( + zoneCalls, + apiservertesting.ZonedEnvironCall("AvailabilityZones"), + ) + case apiservertesting.StubZonedNetworkingEnvironName: + zoneCalls = append( + zoneCalls, + apiservertesting.ZonedNetworkingEnvironCall("AvailabilityZones"), + ) + } + // Finally after caching provider zones backing zones are + // updated. + zoneCalls = append( + zoneCalls, + apiservertesting.BackingCall("SetAvailabilityZones", apiservertesting.ProviderInstance.Zones), + ) + + // Now, because we have 2 arguments to AddSubnets() below, we + // need to expect the same zoneCalls twice, with a + // AvailabilityZones backing lookup between them. + expectedCalls = append(expectedCalls, zoneCalls...) + expectedCalls = append(expectedCalls, apiservertesting.BackingCall("AvailabilityZones")) + expectedCalls = append(expectedCalls, zoneCalls...) + } + + // Pass 2 arguments covering all cases we need. + args := params.AddSubnetsParams{ + Subnets: []params.AddSubnetParams{{ + SubnetTag: "subnet-10.42.0.0/16", + SpaceTag: "space-dmz", + Zones: []string{"zone1"}, + }, { + SubnetProviderId: "vlan-42", + SpaceTag: "space-private", + Zones: []string{"zone3"}, + }}, + } + results, err := s.facade.AddSubnets(args) + c.Assert(err, jc.ErrorIsNil) + c.Assert(results.Results, gc.HasLen, len(args.Subnets)) + for _, result := range results.Results { + if !c.Check(result.Error, gc.NotNil) { + continue + } + c.Check(result.Error, gc.ErrorMatches, expectedError) + if expectedSatisfies != nil { + c.Check(result.Error, jc.Satisfies, expectedSatisfies) + } else { + c.Check(result.Error.Code, gc.Equals, "") + } + } + + apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub, expectedCalls...) +} + +func (s *SubnetsSuite) TestAddSubnetsWithNoProviderSubnetsFails(c *gc.C) { + s.CheckAddSubnetsFails( + c, apiservertesting.StubNetworkingEnvironName, + apiservertesting.WithoutZones, apiservertesting.WithoutSpaces, apiservertesting.WithoutSubnets, + "no subnets defined", + nil, + ) +} + +func (s *SubnetsSuite) TestAddSubnetsWithNoBackingSpacesFails(c *gc.C) { + s.CheckAddSubnetsFails( + c, apiservertesting.StubNetworkingEnvironName, + apiservertesting.WithoutZones, apiservertesting.WithoutSpaces, apiservertesting.WithSubnets, + "no spaces defined", + nil, + ) +} + +func (s *SubnetsSuite) TestAddSubnetsWithNoProviderZonesFails(c *gc.C) { + s.CheckAddSubnetsFails( + c, apiservertesting.StubZonedNetworkingEnvironName, + apiservertesting.WithoutZones, apiservertesting.WithSpaces, apiservertesting.WithSubnets, + "no zones defined", + nil, + ) +} + +func (s *SubnetsSuite) TestAddSubnetsWhenNetworkingEnvironNotSupported(c *gc.C) { + s.CheckAddSubnetsFails( + c, apiservertesting.StubEnvironName, + apiservertesting.WithoutZones, apiservertesting.WithoutSpaces, apiservertesting.WithoutSubnets, + "environment networking features not supported", + params.IsCodeNotSupported, + ) +} + +func (s *SubnetsSuite) TestListSubnetsAndFiltering(c *gc.C) { + expected := []params.Subnet{{ + CIDR: "10.10.0.0/24", + ProviderId: "sn-zadf00d", + VLANTag: 0, + Life: "", + SpaceTag: "space-private", + Zones: []string{"zone1"}, + Status: "", + }, { + CIDR: "2001:db8::/32", + ProviderId: "sn-ipv6", + VLANTag: 0, + Life: "", + SpaceTag: "space-dmz", + Zones: []string{"zone1", "zone3"}, + Status: "", + }} + // No filtering. + args := params.SubnetsFilters{} + subnets, err := s.facade.ListSubnets(args) + c.Assert(err, jc.ErrorIsNil) + c.Assert(subnets.Results, jc.DeepEquals, expected) + + // Filter by space only. + args.SpaceTag = "space-dmz" + subnets, err = s.facade.ListSubnets(args) + c.Assert(err, jc.ErrorIsNil) + c.Assert(subnets.Results, jc.DeepEquals, expected[1:]) + + // Filter by zone only. + args.SpaceTag = "" + args.Zone = "zone3" + subnets, err = s.facade.ListSubnets(args) + c.Assert(err, jc.ErrorIsNil) + c.Assert(subnets.Results, jc.DeepEquals, expected[1:]) + + // Filter by both space and zone. + args.SpaceTag = "space-private" + args.Zone = "zone1" + subnets, err = s.facade.ListSubnets(args) + c.Assert(err, jc.ErrorIsNil) + c.Assert(subnets.Results, jc.DeepEquals, expected[:1]) +} + +func (s *SubnetsSuite) TestListSubnetsInvalidSpaceTag(c *gc.C) { + args := params.SubnetsFilters{SpaceTag: "invalid"} + _, err := s.facade.ListSubnets(args) + c.Assert(err, gc.ErrorMatches, `"invalid" is not a valid tag`) +} + +func (s *SubnetsSuite) TestListSubnetsAllSubnetError(c *gc.C) { + boom := errors.New("no subnets for you") + apiservertesting.BackingInstance.SetErrors(boom) + _, err := s.facade.ListSubnets(params.SubnetsFilters{}) + c.Assert(err, gc.ErrorMatches, "no subnets for you") +} === added directory 'src/github.com/juju/juju/apiserver/systemmanager' === added file 'src/github.com/juju/juju/apiserver/systemmanager/destroy_test.go' --- src/github.com/juju/juju/apiserver/systemmanager/destroy_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/systemmanager/destroy_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,182 @@ +// Copyright 2012-2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package systemmanager_test + +import ( + "github.com/juju/errors" + "github.com/juju/names" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/common" + commontesting "github.com/juju/juju/apiserver/common/testing" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/apiserver/systemmanager" + apiservertesting "github.com/juju/juju/apiserver/testing" + jujutesting "github.com/juju/juju/juju/testing" + "github.com/juju/juju/state" + "github.com/juju/juju/testing" + "github.com/juju/juju/testing/factory" +) + +// NOTE: the testing of the general environment destruction code +// is found in apiserver/common/environdestroy_test.go. +// +// The tests here are around the validation and behaviour of +// the flags passed in to the system manager destroy system call. + +type destroySystemSuite struct { + jujutesting.JujuConnSuite + commontesting.BlockHelper + + systemManager *systemmanager.SystemManagerAPI + + otherState *state.State + otherEnvOwner names.UserTag + otherEnvUUID string +} + +var _ = gc.Suite(&destroySystemSuite{}) + +func (s *destroySystemSuite) SetUpTest(c *gc.C) { + s.JujuConnSuite.SetUpTest(c) + + s.BlockHelper = commontesting.NewBlockHelper(s.APIState) + s.AddCleanup(func(*gc.C) { s.BlockHelper.Close() }) + + resources := common.NewResources() + s.AddCleanup(func(_ *gc.C) { resources.StopAll() }) + + authoriser := apiservertesting.FakeAuthorizer{ + Tag: s.AdminUserTag(c), + } + systemManager, err := systemmanager.NewSystemManagerAPI(s.State, resources, authoriser) + c.Assert(err, jc.ErrorIsNil) + s.systemManager = systemManager + + s.otherEnvOwner = names.NewUserTag("jess@dummy") + s.otherState = factory.NewFactory(s.State).MakeEnvironment(c, &factory.EnvParams{ + Name: "dummytoo", + Owner: s.otherEnvOwner, + Prepare: true, + ConfigAttrs: testing.Attrs{ + "state-server": false, + }, + }) + s.AddCleanup(func(c *gc.C) { s.otherState.Close() }) + s.otherEnvUUID = s.otherState.EnvironUUID() +} + +func (s *destroySystemSuite) TestDestroySystemKillsHostedEnvsWithBlocks(c *gc.C) { + s.BlockDestroyEnvironment(c, "TestBlockDestroyEnvironment") + s.BlockRemoveObject(c, "TestBlockRemoveObject") + s.otherState.SwitchBlockOn(state.DestroyBlock, "TestBlockDestroyEnvironment") + s.otherState.SwitchBlockOn(state.ChangeBlock, "TestChangeBlock") + + err := s.systemManager.DestroySystem(params.DestroySystemArgs{ + DestroyEnvironments: true, + IgnoreBlocks: true, + }) + c.Assert(err, jc.ErrorIsNil) + + _, err = s.otherState.Environment() + c.Assert(errors.IsNotFound(err), jc.IsTrue) + + env, err := s.State.Environment() + c.Assert(err, jc.ErrorIsNil) + c.Assert(env.Life(), gc.Equals, state.Dying) +} + +func (s *destroySystemSuite) TestDestroySystemReturnsBlockedEnvironmentsErr(c *gc.C) { + s.BlockDestroyEnvironment(c, "TestBlockDestroyEnvironment") + s.BlockRemoveObject(c, "TestBlockRemoveObject") + s.otherState.SwitchBlockOn(state.DestroyBlock, "TestBlockDestroyEnvironment") + s.otherState.SwitchBlockOn(state.ChangeBlock, "TestChangeBlock") + + err := s.systemManager.DestroySystem(params.DestroySystemArgs{ + DestroyEnvironments: true, + }) + c.Assert(params.IsCodeOperationBlocked(err), jc.IsTrue) + + numBlocks, err := s.State.AllBlocksForSystem() + c.Assert(err, jc.ErrorIsNil) + c.Assert(len(numBlocks), gc.Equals, 4) + + _, err = s.otherState.Environment() + c.Assert(err, jc.ErrorIsNil) +} + +func (s *destroySystemSuite) TestDestroySystemKillsHostedEnvs(c *gc.C) { + err := s.systemManager.DestroySystem(params.DestroySystemArgs{ + DestroyEnvironments: true, + }) + c.Assert(err, jc.ErrorIsNil) + + _, err = s.otherState.Environment() + c.Assert(errors.IsNotFound(err), jc.IsTrue) + + env, err := s.State.Environment() + c.Assert(err, jc.ErrorIsNil) + c.Assert(env.Life(), gc.Equals, state.Dying) +} + +func (s *destroySystemSuite) TestDestroySystemLeavesBlocksIfNotKillAll(c *gc.C) { + s.BlockDestroyEnvironment(c, "TestBlockDestroyEnvironment") + s.BlockRemoveObject(c, "TestBlockRemoveObject") + s.otherState.SwitchBlockOn(state.DestroyBlock, "TestBlockDestroyEnvironment") + s.otherState.SwitchBlockOn(state.ChangeBlock, "TestChangeBlock") + + err := s.systemManager.DestroySystem(params.DestroySystemArgs{ + IgnoreBlocks: true, + }) + c.Assert(err, gc.ErrorMatches, "state server environment cannot be destroyed before all other environments are destroyed") + + numBlocks, err := s.State.AllBlocksForSystem() + c.Assert(err, jc.ErrorIsNil) + c.Assert(len(numBlocks), gc.Equals, 4) +} + +func (s *destroySystemSuite) TestDestroySystemNoHostedEnvs(c *gc.C) { + err := common.DestroyEnvironment(s.State, s.otherState.EnvironTag()) + c.Assert(err, jc.ErrorIsNil) + + err = s.systemManager.DestroySystem(params.DestroySystemArgs{}) + c.Assert(err, jc.ErrorIsNil) + + env, err := s.State.Environment() + c.Assert(err, jc.ErrorIsNil) + c.Assert(env.Life(), gc.Equals, state.Dying) +} + +func (s *destroySystemSuite) TestDestroySystemNoHostedEnvsWithBlock(c *gc.C) { + err := common.DestroyEnvironment(s.State, s.otherState.EnvironTag()) + c.Assert(err, jc.ErrorIsNil) + + s.BlockDestroyEnvironment(c, "TestBlockDestroyEnvironment") + s.BlockRemoveObject(c, "TestBlockRemoveObject") + + err = s.systemManager.DestroySystem(params.DestroySystemArgs{ + IgnoreBlocks: true, + }) + c.Assert(err, jc.ErrorIsNil) + + env, err := s.State.Environment() + c.Assert(err, jc.ErrorIsNil) + c.Assert(env.Life(), gc.Equals, state.Dying) +} + +func (s *destroySystemSuite) TestDestroySystemNoHostedEnvsWithBlockFail(c *gc.C) { + err := common.DestroyEnvironment(s.State, s.otherState.EnvironTag()) + c.Assert(err, jc.ErrorIsNil) + + s.BlockDestroyEnvironment(c, "TestBlockDestroyEnvironment") + s.BlockRemoveObject(c, "TestBlockRemoveObject") + + err = s.systemManager.DestroySystem(params.DestroySystemArgs{}) + c.Assert(params.IsCodeOperationBlocked(err), jc.IsTrue) + + numBlocks, err := s.State.AllBlocksForSystem() + c.Assert(err, jc.ErrorIsNil) + c.Assert(len(numBlocks), gc.Equals, 2) +} === added file 'src/github.com/juju/juju/apiserver/systemmanager/package_test.go' --- src/github.com/juju/juju/apiserver/systemmanager/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/systemmanager/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,14 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package systemmanager_test + +import ( + stdtesting "testing" + + "github.com/juju/juju/testing" +) + +func TestAll(t *stdtesting.T) { + testing.MgoTestPackage(t) +} === added file 'src/github.com/juju/juju/apiserver/systemmanager/systemmanager.go' --- src/github.com/juju/juju/apiserver/systemmanager/systemmanager.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/systemmanager/systemmanager.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,333 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +// The systemmanager package defines an API end point for functions dealing +// with systems as a whole. Primarily the destruction of systems. +package systemmanager + +import ( + "sort" + + "github.com/juju/errors" + "github.com/juju/loggo" + "github.com/juju/names" + "github.com/juju/utils/set" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/feature" + "github.com/juju/juju/state" +) + +var logger = loggo.GetLogger("juju.apiserver.systemmanager") + +func init() { + common.RegisterStandardFacadeForFeature("SystemManager", 1, NewSystemManagerAPI, feature.JES) +} + +// SystemManager defines the methods on the systemmanager API end point. +type SystemManager interface { + AllEnvironments() (params.UserEnvironmentList, error) + DestroySystem(args params.DestroySystemArgs) error + EnvironmentConfig() (params.EnvironmentConfigResults, error) + ListBlockedEnvironments() (params.EnvironmentBlockInfoList, error) + RemoveBlocks(args params.RemoveBlocksArgs) error + WatchAllEnvs() (params.AllWatcherId, error) +} + +// SystemManagerAPI implements the environment manager interface and is +// the concrete implementation of the api end point. +type SystemManagerAPI struct { + state *state.State + authorizer common.Authorizer + apiUser names.UserTag + resources *common.Resources +} + +var _ SystemManager = (*SystemManagerAPI)(nil) + +// NewSystemManagerAPI creates a new api server endpoint for managing +// environments. +func NewSystemManagerAPI( + st *state.State, + resources *common.Resources, + authorizer common.Authorizer, +) (*SystemManagerAPI, error) { + if !authorizer.AuthClient() { + return nil, errors.Trace(common.ErrPerm) + } + + // Since we know this is a user tag (because AuthClient is true), + // we just do the type assertion to the UserTag. + apiUser, _ := authorizer.GetAuthTag().(names.UserTag) + isAdmin, err := st.IsSystemAdministrator(apiUser) + if err != nil { + return nil, errors.Trace(err) + } + // The entire end point is only accessible to system administrators. + if !isAdmin { + return nil, errors.Trace(common.ErrPerm) + } + + return &SystemManagerAPI{ + state: st, + authorizer: authorizer, + apiUser: apiUser, + resources: resources, + }, nil +} + +// AllEnvironments allows system administrators to get the list of all the +// environments in the system. +func (s *SystemManagerAPI) AllEnvironments() (params.UserEnvironmentList, error) { + result := params.UserEnvironmentList{} + + // Get all the environments that the authenticated user can see, and + // supplement that with the other environments that exist that the user + // cannot see. The reason we do this is to get the LastConnection time for + // the environments that the user is able to see, so we have consistent + // output when listing with or without --all when an admin user. + environments, err := s.state.EnvironmentsForUser(s.apiUser) + if err != nil { + return result, errors.Trace(err) + } + visibleEnvironments := set.NewStrings() + for _, env := range environments { + lastConn, err := env.LastConnection() + if err != nil && !state.IsNeverConnectedError(err) { + return result, errors.Trace(err) + } + visibleEnvironments.Add(env.UUID()) + result.UserEnvironments = append(result.UserEnvironments, params.UserEnvironment{ + Environment: params.Environment{ + Name: env.Name(), + UUID: env.UUID(), + OwnerTag: env.Owner().String(), + }, + LastConnection: &lastConn, + }) + } + + allEnvs, err := s.state.AllEnvironments() + if err != nil { + return result, errors.Trace(err) + } + + for _, env := range allEnvs { + if !visibleEnvironments.Contains(env.UUID()) { + result.UserEnvironments = append(result.UserEnvironments, params.UserEnvironment{ + Environment: params.Environment{ + Name: env.Name(), + UUID: env.UUID(), + OwnerTag: env.Owner().String(), + }, + // No LastConnection as this user hasn't. + }) + } + } + + // Sort the resulting sequence by environment name, then owner. + sort.Sort(orderedUserEnvironments(result.UserEnvironments)) + + return result, nil +} + +// ListBlockedEnvironments returns a list of all environments on the system +// which have a block in place. The resulting slice is sorted by environment +// name, then owner. Callers must be system administrators to retrieve the +// list. +func (s *SystemManagerAPI) ListBlockedEnvironments() (params.EnvironmentBlockInfoList, error) { + results := params.EnvironmentBlockInfoList{} + + blocks, err := s.state.AllBlocksForSystem() + if err != nil { + return results, errors.Trace(err) + } + + envBlocks := make(map[string][]string) + for _, block := range blocks { + uuid := block.EnvUUID() + types, ok := envBlocks[uuid] + if !ok { + types = []string{block.Type().String()} + } else { + types = append(types, block.Type().String()) + } + envBlocks[uuid] = types + } + + for uuid, blocks := range envBlocks { + envInfo, err := s.state.GetEnvironment(names.NewEnvironTag(uuid)) + if err != nil { + logger.Debugf("Unable to get name for environment: %s", uuid) + continue + } + results.Environments = append(results.Environments, params.EnvironmentBlockInfo{ + UUID: envInfo.UUID(), + Name: envInfo.Name(), + OwnerTag: envInfo.Owner().String(), + Blocks: blocks, + }) + } + + // Sort the resulting sequence by environment name, then owner. + sort.Sort(orderedBlockInfo(results.Environments)) + + return results, nil +} + +// DestroySystem will attempt to destroy the system. If the args specify the +// removal of blocks or the destruction of the environments, this method will +// attempt to do so. +func (s *SystemManagerAPI) DestroySystem(args params.DestroySystemArgs) error { + // Get list of all environments in the system. + allEnvs, err := s.state.AllEnvironments() + if err != nil { + return errors.Trace(err) + } + + // If there are hosted environments and DestroyEnvironments was not + // specified, don't bother trying to destroy the system, as it will fail. + if len(allEnvs) > 1 && !args.DestroyEnvironments { + return errors.Errorf("state server environment cannot be destroyed before all other environments are destroyed") + } + + // If there are blocks, and we aren't being told to ignore them, let the + // user know. + blocks, err := s.state.AllBlocksForSystem() + if err != nil { + logger.Debugf("Unable to get blocks for system: %s", err) + if !args.IgnoreBlocks { + return errors.Trace(err) + } + } + if len(blocks) > 0 { + if !args.IgnoreBlocks { + return common.ErrOperationBlocked("found blocks in system environments") + } + + err := s.state.RemoveAllBlocksForSystem() + if err != nil { + return errors.Trace(err) + } + } + + systemEnv, err := s.state.StateServerEnvironment() + if err != nil { + return errors.Trace(err) + } + systemTag := systemEnv.EnvironTag() + + if args.DestroyEnvironments { + for _, env := range allEnvs { + environTag := env.EnvironTag() + if environTag != systemTag { + if err := common.DestroyEnvironment(s.state, environTag); err != nil { + logger.Errorf("unable to destroy environment %q: %s", env.UUID(), err) + } + } + } + } + + return errors.Trace(common.DestroyEnvironment(s.state, systemTag)) +} + +// EnvironmentConfig returns the environment config for the system +// environment. For information on the current environment, use +// client.EnvironmentGet +func (s *SystemManagerAPI) EnvironmentConfig() (params.EnvironmentConfigResults, error) { + result := params.EnvironmentConfigResults{} + + stateServerEnv, err := s.state.StateServerEnvironment() + if err != nil { + return result, errors.Trace(err) + } + + config, err := stateServerEnv.Config() + if err != nil { + return result, errors.Trace(err) + } + + result.Config = config.AllAttrs() + return result, nil +} + +// RemoveBlocks removes all the blocks in the system. +func (s *SystemManagerAPI) RemoveBlocks(args params.RemoveBlocksArgs) error { + if !args.All { + return errors.New("not supported") + } + return errors.Trace(s.state.RemoveAllBlocksForSystem()) +} + +// WatchAllEnvs starts watching events for all environments in the +// system. The returned AllWatcherId should be used with Next on the +// AllEnvWatcher endpoint to receive deltas. +func (c *SystemManagerAPI) WatchAllEnvs() (params.AllWatcherId, error) { + w := c.state.WatchAllEnvs() + return params.AllWatcherId{ + AllWatcherId: c.resources.Register(w), + }, nil +} + +type orderedBlockInfo []params.EnvironmentBlockInfo + +func (o orderedBlockInfo) Len() int { + return len(o) +} + +func (o orderedBlockInfo) Less(i, j int) bool { + if o[i].Name < o[j].Name { + return true + } + if o[i].Name > o[j].Name { + return false + } + + if o[i].OwnerTag < o[j].OwnerTag { + return true + } + if o[i].OwnerTag > o[j].OwnerTag { + return false + } + + // Unreachable based on the rules of there not being duplicate + // environments of the same name for the same owner, but return false + // instead of panicing. + return false +} + +func (o orderedBlockInfo) Swap(i, j int) { + o[i], o[j] = o[j], o[i] +} + +type orderedUserEnvironments []params.UserEnvironment + +func (o orderedUserEnvironments) Len() int { + return len(o) +} + +func (o orderedUserEnvironments) Less(i, j int) bool { + if o[i].Name < o[j].Name { + return true + } + if o[i].Name > o[j].Name { + return false + } + + if o[i].OwnerTag < o[j].OwnerTag { + return true + } + if o[i].OwnerTag > o[j].OwnerTag { + return false + } + + // Unreachable based on the rules of there not being duplicate + // environments of the same name for the same owner, but return false + // instead of panicing. + return false +} + +func (o orderedUserEnvironments) Swap(i, j int) { + o[i], o[j] = o[j], o[i] +} === added file 'src/github.com/juju/juju/apiserver/systemmanager/systemmanager_test.go' --- src/github.com/juju/juju/apiserver/systemmanager/systemmanager_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/systemmanager/systemmanager_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,218 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package systemmanager_test + +import ( + "time" + + "github.com/juju/loggo" + "github.com/juju/names" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver" + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/apiserver/systemmanager" + apiservertesting "github.com/juju/juju/apiserver/testing" + jujutesting "github.com/juju/juju/juju/testing" + "github.com/juju/juju/state" + "github.com/juju/juju/state/multiwatcher" + "github.com/juju/juju/testing" + "github.com/juju/juju/testing/factory" +) + +type systemManagerSuite struct { + jujutesting.JujuConnSuite + + systemManager *systemmanager.SystemManagerAPI + resources *common.Resources + authorizer apiservertesting.FakeAuthorizer +} + +var _ = gc.Suite(&systemManagerSuite{}) + +func (s *systemManagerSuite) SetUpTest(c *gc.C) { + s.JujuConnSuite.SetUpTest(c) + s.resources = common.NewResources() + s.AddCleanup(func(_ *gc.C) { s.resources.StopAll() }) + + s.authorizer = apiservertesting.FakeAuthorizer{ + Tag: s.AdminUserTag(c), + } + + systemManager, err := systemmanager.NewSystemManagerAPI(s.State, s.resources, s.authorizer) + c.Assert(err, jc.ErrorIsNil) + s.systemManager = systemManager + + loggo.GetLogger("juju.apiserver.systemmanager").SetLogLevel(loggo.TRACE) +} + +func (s *systemManagerSuite) TestNewAPIRefusesNonClient(c *gc.C) { + anAuthoriser := apiservertesting.FakeAuthorizer{ + Tag: names.NewUnitTag("mysql/0"), + } + endPoint, err := systemmanager.NewSystemManagerAPI(s.State, s.resources, anAuthoriser) + c.Assert(endPoint, gc.IsNil) + c.Assert(err, gc.ErrorMatches, "permission denied") +} + +func (s *systemManagerSuite) TestNewAPIRefusesNonAdmins(c *gc.C) { + user := s.Factory.MakeUser(c, &factory.UserParams{NoEnvUser: true}) + anAuthoriser := apiservertesting.FakeAuthorizer{ + Tag: user.Tag(), + } + endPoint, err := systemmanager.NewSystemManagerAPI(s.State, s.resources, anAuthoriser) + c.Assert(endPoint, gc.IsNil) + c.Assert(err, gc.ErrorMatches, "permission denied") +} + +func (s *systemManagerSuite) checkEnvironmentMatches(c *gc.C, env params.Environment, expected *state.Environment) { + c.Check(env.Name, gc.Equals, expected.Name()) + c.Check(env.UUID, gc.Equals, expected.UUID()) + c.Check(env.OwnerTag, gc.Equals, expected.Owner().String()) +} + +func (s *systemManagerSuite) TestAllEnvironments(c *gc.C) { + admin := s.Factory.MakeUser(c, &factory.UserParams{Name: "foobar"}) + + s.Factory.MakeEnvironment(c, &factory.EnvParams{ + Name: "owned", Owner: admin.UserTag()}).Close() + remoteUserTag := names.NewUserTag("user@remote") + st := s.Factory.MakeEnvironment(c, &factory.EnvParams{ + Name: "user", Owner: remoteUserTag}) + defer st.Close() + st.AddEnvironmentUser(admin.UserTag(), remoteUserTag, "Foo Bar") + + s.Factory.MakeEnvironment(c, &factory.EnvParams{ + Name: "no-access", Owner: remoteUserTag}).Close() + + response, err := s.systemManager.AllEnvironments() + c.Assert(err, jc.ErrorIsNil) + // The results are sorted. + expected := []string{"dummyenv", "no-access", "owned", "user"} + var obtained []string + for _, env := range response.UserEnvironments { + obtained = append(obtained, env.Name) + stateEnv, err := s.State.GetEnvironment(names.NewEnvironTag(env.UUID)) + c.Assert(err, jc.ErrorIsNil) + s.checkEnvironmentMatches(c, env.Environment, stateEnv) + } + c.Assert(obtained, jc.DeepEquals, expected) +} + +func (s *systemManagerSuite) TestListBlockedEnvironments(c *gc.C) { + st := s.Factory.MakeEnvironment(c, &factory.EnvParams{ + Name: "test"}) + defer st.Close() + + s.State.SwitchBlockOn(state.DestroyBlock, "TestBlockDestroyEnvironment") + s.State.SwitchBlockOn(state.ChangeBlock, "TestChangeBlock") + st.SwitchBlockOn(state.DestroyBlock, "TestBlockDestroyEnvironment") + st.SwitchBlockOn(state.ChangeBlock, "TestChangeBlock") + + list, err := s.systemManager.ListBlockedEnvironments() + c.Assert(err, jc.ErrorIsNil) + + c.Assert(list.Environments, jc.DeepEquals, []params.EnvironmentBlockInfo{ + params.EnvironmentBlockInfo{ + Name: "dummyenv", + UUID: s.State.EnvironUUID(), + OwnerTag: s.AdminUserTag(c).String(), + Blocks: []string{ + "BlockDestroy", + "BlockChange", + }, + }, + params.EnvironmentBlockInfo{ + Name: "test", + UUID: st.EnvironUUID(), + OwnerTag: s.AdminUserTag(c).String(), + Blocks: []string{ + "BlockDestroy", + "BlockChange", + }, + }, + }) + +} + +func (s *systemManagerSuite) TestListBlockedEnvironmentsNoBlocks(c *gc.C) { + list, err := s.systemManager.ListBlockedEnvironments() + c.Assert(err, jc.ErrorIsNil) + c.Assert(list.Environments, gc.HasLen, 0) +} + +func (s *systemManagerSuite) TestEnvironmentConfig(c *gc.C) { + env, err := s.systemManager.EnvironmentConfig() + c.Assert(err, jc.ErrorIsNil) + c.Assert(env.Config["name"], gc.Equals, "dummyenv") +} + +func (s *systemManagerSuite) TestEnvironmentConfigFromNonStateServer(c *gc.C) { + st := s.Factory.MakeEnvironment(c, &factory.EnvParams{ + Name: "test"}) + defer st.Close() + + authorizer := &apiservertesting.FakeAuthorizer{Tag: s.AdminUserTag(c)} + systemManager, err := systemmanager.NewSystemManagerAPI(st, common.NewResources(), authorizer) + c.Assert(err, jc.ErrorIsNil) + env, err := systemManager.EnvironmentConfig() + c.Assert(err, jc.ErrorIsNil) + c.Assert(env.Config["name"], gc.Equals, "dummyenv") +} + +func (s *systemManagerSuite) TestRemoveBlocks(c *gc.C) { + st := s.Factory.MakeEnvironment(c, &factory.EnvParams{ + Name: "test"}) + defer st.Close() + + s.State.SwitchBlockOn(state.DestroyBlock, "TestBlockDestroyEnvironment") + s.State.SwitchBlockOn(state.ChangeBlock, "TestChangeBlock") + st.SwitchBlockOn(state.DestroyBlock, "TestBlockDestroyEnvironment") + st.SwitchBlockOn(state.ChangeBlock, "TestChangeBlock") + + err := s.systemManager.RemoveBlocks(params.RemoveBlocksArgs{All: true}) + c.Assert(err, jc.ErrorIsNil) + + blocks, err := s.State.AllBlocksForSystem() + c.Assert(err, jc.ErrorIsNil) + c.Assert(blocks, gc.HasLen, 0) +} + +func (s *systemManagerSuite) TestRemoveBlocksNotAll(c *gc.C) { + err := s.systemManager.RemoveBlocks(params.RemoveBlocksArgs{}) + c.Assert(err, gc.ErrorMatches, "not supported") +} + +func (s *systemManagerSuite) TestWatchAllEnvs(c *gc.C) { + watcherId, err := s.systemManager.WatchAllEnvs() + c.Assert(err, jc.ErrorIsNil) + + watcherAPI_, err := apiserver.NewAllWatcher(s.State, s.resources, s.authorizer, watcherId.AllWatcherId) + c.Assert(err, jc.ErrorIsNil) + watcherAPI := watcherAPI_.(*apiserver.SrvAllWatcher) + defer func() { + err := watcherAPI.Stop() + c.Assert(err, jc.ErrorIsNil) + }() + + resultC := make(chan params.AllWatcherNextResults) + go func() { + result, err := watcherAPI.Next() + c.Assert(err, jc.ErrorIsNil) + resultC <- result + }() + + select { + case result := <-resultC: + // Expect to see the initial environment be reported. + deltas := result.Deltas + c.Assert(deltas, gc.HasLen, 1) + envInfo := deltas[0].Entity.(*multiwatcher.EnvironmentInfo) + c.Assert(envInfo.EnvUUID, gc.Equals, s.State.EnvironUUID()) + case <-time.After(testing.LongWait): + c.Fatal("timed out") + } +} === added file 'src/github.com/juju/juju/apiserver/testing/stub_network.go' --- src/github.com/juju/juju/apiserver/testing/stub_network.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/apiserver/testing/stub_network.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,635 @@ +package testing + +import ( + "fmt" + "net" + "strconv" + "strings" + + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/environs" + "github.com/juju/juju/environs/config" + "github.com/juju/juju/instance" + "github.com/juju/juju/network" + providercommon "github.com/juju/juju/provider/common" + coretesting "github.com/juju/juju/testing" + "github.com/juju/testing" + "github.com/juju/utils" + "github.com/juju/utils/set" +) + +type StubNetwork struct { +} + +var ( + // SharedStub records all method calls to any of the stubs. + SharedStub = &testing.Stub{} + + BackingInstance = &StubBacking{Stub: SharedStub} + ProviderInstance = &StubProvider{Stub: SharedStub} + EnvironInstance = &StubEnviron{Stub: SharedStub} + ZonedEnvironInstance = &StubZonedEnviron{Stub: SharedStub} + NetworkingEnvironInstance = &StubNetworkingEnviron{Stub: SharedStub} + ZonedNetworkingEnvironInstance = &StubZonedNetworkingEnviron{Stub: SharedStub} +) + +const ( + StubProviderType = "stub-provider" + StubEnvironName = "stub-environ" + StubZonedEnvironName = "stub-zoned-environ" + StubNetworkingEnvironName = "stub-networking-environ" + StubZonedNetworkingEnvironName = "stub-zoned-networking-environ" +) + +func (s StubNetwork) SetUpSuite(c *gc.C) { + ProviderInstance.Zones = []providercommon.AvailabilityZone{ + &FakeZone{"zone1", true}, + &FakeZone{"zone2", false}, + &FakeZone{"zone3", true}, + &FakeZone{"zone4", false}, + &FakeZone{"zone4", false}, // duplicates are ignored + } + ProviderInstance.Subnets = []network.SubnetInfo{{ + CIDR: "10.10.0.0/24", + ProviderId: "sn-zadf00d", + AvailabilityZones: []string{"zone1"}, + AllocatableIPLow: net.ParseIP("10.10.0.10"), + AllocatableIPHigh: net.ParseIP("10.10.0.100"), + }, { + CIDR: "2001:db8::/32", + ProviderId: "sn-ipv6", + AvailabilityZones: []string{"zone1", "zone3"}, + }, { + // no CIDR or provider id -> cached, but cannot be added + CIDR: "", + ProviderId: "", + }, { + // no CIDR, just provider id -> cached, but can only be added by id + CIDR: "", + ProviderId: "sn-empty", + }, { + // invalid CIDR and provider id -> cannot be added, but is cached + CIDR: "invalid", + ProviderId: "sn-invalid", + }, { + // incorrectly specified CIDR, with provider id -> cached, cannot be added + CIDR: "0.1.2.3/4", + ProviderId: "sn-awesome", + }, { + // no zones, no provider-id -> cached, but can only be added by CIDR + CIDR: "10.20.0.0/16", + }, { + // with zones, duplicate provider-id -> overwritten by the last + // subnet with the same provider id when caching. + CIDR: "10.99.88.0/24", + ProviderId: "sn-deadbeef", + AvailabilityZones: []string{"zone1", "zone2"}, + }, { + // no zones + CIDR: "10.42.0.0/16", + ProviderId: "sn-42", + }, { + // in an unavailable zone, duplicate CIDR -> cannot be added, but is cached + CIDR: "10.10.0.0/24", + ProviderId: "sn-deadbeef", + AvailabilityZones: []string{"zone2"}, + }, { + CIDR: "10.30.1.0/24", + ProviderId: "vlan-42", + VLANTag: 42, + AvailabilityZones: []string{"zone3"}, + }} + + environs.RegisterProvider(StubProviderType, ProviderInstance) +} + +type errReturner func() error + +// FakeSpace implements common.BackingSpace for testing. +type FakeSpace struct { + SpaceName string + SubnetIds []string + Public bool + NextErr errReturner +} + +var _ common.BackingSpace = (*FakeSpace)(nil) + +func (f *FakeSpace) Name() string { + return f.SpaceName +} + +func (f *FakeSpace) Subnets() (bs []common.BackingSubnet, err error) { + outputSubnets := []common.BackingSubnet{} + + if err = f.NextErr(); err != nil { + return outputSubnets, err + } + + for _, subnetId := range f.SubnetIds { + providerId := "provider-" + subnetId + + // Pick the third element of the IP address and use this to + // decide how we construct the Subnet. It provides variation of + // test data. + first, err := strconv.Atoi(strings.Split(subnetId, ".")[2]) + if err != nil { + return outputSubnets, err + } + vlantag := 0 + zones := []string{"foo"} + status := "in-use" + if first%2 == 1 { + vlantag = 23 + zones = []string{"bar", "bam"} + status = "" + } + + backing := common.BackingSubnetInfo{ + CIDR: subnetId, + SpaceName: f.SpaceName, + ProviderId: providerId, + VLANTag: vlantag, + AvailabilityZones: zones, + Status: status, + } + outputSubnets = append(outputSubnets, &FakeSubnet{info: backing}) + } + + return outputSubnets, nil +} + +func (f *FakeSpace) ProviderId() (netID network.Id) { + return +} + +func (f *FakeSpace) Zones() []string { + return []string{""} +} + +func (f *FakeSpace) Life() (life params.Life) { + return +} + +// GoString implements fmt.GoStringer. +func (f *FakeSpace) GoString() string { + return fmt.Sprintf("&FakeSpace{%q}", f.SpaceName) +} + +// StubMethodCall is like testing.StubCall, but includes the receiver +// as well. +type StubMethodCall struct { + Receiver interface{} + FuncName string + Args []interface{} +} + +// BackingCall makes it easy to check method calls on BackingInstance. +func BackingCall(name string, args ...interface{}) StubMethodCall { + return StubMethodCall{ + Receiver: BackingInstance, + FuncName: name, + Args: args, + } +} + +// ProviderCall makes it easy to check method calls on ProviderInstance. +func ProviderCall(name string, args ...interface{}) StubMethodCall { + return StubMethodCall{ + Receiver: ProviderInstance, + FuncName: name, + Args: args, + } +} + +// EnvironCall makes it easy to check method calls on EnvironInstance. +func EnvironCall(name string, args ...interface{}) StubMethodCall { + return StubMethodCall{ + Receiver: EnvironInstance, + FuncName: name, + Args: args, + } +} + +// ZonedEnvironCall makes it easy to check method calls on +// ZonedEnvironInstance. +func ZonedEnvironCall(name string, args ...interface{}) StubMethodCall { + return StubMethodCall{ + Receiver: ZonedEnvironInstance, + FuncName: name, + Args: args, + } +} + +// NetworkingEnvironCall makes it easy to check method calls on +// NetworkingEnvironInstance. +func NetworkingEnvironCall(name string, args ...interface{}) StubMethodCall { + return StubMethodCall{ + Receiver: NetworkingEnvironInstance, + FuncName: name, + Args: args, + } +} + +// ZonedNetworkingEnvironCall makes it easy to check method calls on +// ZonedNetworkingEnvironInstance. +func ZonedNetworkingEnvironCall(name string, args ...interface{}) StubMethodCall { + return StubMethodCall{ + Receiver: ZonedNetworkingEnvironInstance, + FuncName: name, + Args: args, + } +} + +// CheckMethodCalls works like testing.Stub.CheckCalls, but also +// checks the receivers. +func CheckMethodCalls(c *gc.C, stub *testing.Stub, calls ...StubMethodCall) { + receivers := make([]interface{}, len(calls)) + for i, call := range calls { + receivers[i] = call.Receiver + } + stub.CheckReceivers(c, receivers...) + c.Check(stub.Calls(), gc.HasLen, len(calls)) + for i, call := range calls { + stub.CheckCall(c, i, call.FuncName, call.Args...) + } +} + +// FakeZone implements providercommon.AvailabilityZone for testing. +type FakeZone struct { + ZoneName string + ZoneAvailable bool +} + +var _ providercommon.AvailabilityZone = (*FakeZone)(nil) + +func (f *FakeZone) Name() string { + return f.ZoneName +} + +func (f *FakeZone) Available() bool { + return f.ZoneAvailable +} + +// GoString implements fmt.GoStringer. +func (f *FakeZone) GoString() string { + return fmt.Sprintf("&FakeZone{%q, %v}", f.ZoneName, f.ZoneAvailable) +} + +// FakeSubnet implements common.BackingSubnet for testing. +type FakeSubnet struct { + info common.BackingSubnetInfo +} + +var _ common.BackingSubnet = (*FakeSubnet)(nil) + +// GoString implements fmt.GoStringer. +func (f *FakeSubnet) GoString() string { + return fmt.Sprintf("&FakeSubnet{%#v}", f.info) +} + +func (f *FakeSubnet) Status() string { + return f.info.Status +} + +func (f *FakeSubnet) CIDR() string { + return f.info.CIDR +} + +func (f *FakeSubnet) AvailabilityZones() []string { + return f.info.AvailabilityZones +} + +func (f *FakeSubnet) ProviderId() string { + return f.info.ProviderId +} + +func (f *FakeSubnet) VLANTag() int { + return f.info.VLANTag +} + +func (f *FakeSubnet) SpaceName() string { + return f.info.SpaceName +} + +func (f *FakeSubnet) Life() params.Life { + return f.info.Life +} + +// ResetStub resets all recorded calls and errors of the given stub. +func ResetStub(stub *testing.Stub) { + *stub = testing.Stub{} +} + +// StubBacking implements common.NetworkBacking and records calls to its +// methods. +type StubBacking struct { + *testing.Stub + + EnvConfig *config.Config + + Zones []providercommon.AvailabilityZone + Spaces []common.BackingSpace + Subnets []common.BackingSubnet +} + +var _ common.NetworkBacking = (*StubBacking)(nil) + +type SetUpFlag bool + +const ( + WithZones SetUpFlag = true + WithoutZones SetUpFlag = false + WithSpaces SetUpFlag = true + WithoutSpaces SetUpFlag = false + WithSubnets SetUpFlag = true + WithoutSubnets SetUpFlag = false +) + +func (sb *StubBacking) SetUp(c *gc.C, envName string, withZones, withSpaces, withSubnets SetUpFlag) { + // This method must be called at the beginning of each test, which + // needs access to any of the mocks, to reset the recorded calls + // and errors, as well as to initialize the mocks as needed. + ResetStub(sb.Stub) + + // Make sure we use the stub provider. + extraAttrs := coretesting.Attrs{ + "uuid": utils.MustNewUUID().String(), + "type": StubProviderType, + "name": envName, + } + sb.EnvConfig = coretesting.CustomEnvironConfig(c, extraAttrs) + sb.Zones = []providercommon.AvailabilityZone{} + if withZones { + sb.Zones = make([]providercommon.AvailabilityZone, len(ProviderInstance.Zones)) + copy(sb.Zones, ProviderInstance.Zones) + } + sb.Spaces = []common.BackingSpace{} + if withSpaces { + // Note that full subnet data is generated from the SubnetIds in + // FakeSpace.Subnets(). + sb.Spaces = []common.BackingSpace{ + &FakeSpace{ + SpaceName: "default", + SubnetIds: []string{"192.168.0.0/24", "192.168.3.0/24"}, + NextErr: sb.NextErr}, + &FakeSpace{ + SpaceName: "dmz", + SubnetIds: []string{"192.168.1.0/24"}, + NextErr: sb.NextErr}, + &FakeSpace{ + SpaceName: "private", + SubnetIds: []string{"192.168.2.0/24"}, + NextErr: sb.NextErr}, + &FakeSpace{ + SpaceName: "private", + SubnetIds: []string{"192.168.2.0/24"}, + NextErr: sb.NextErr}, // duplicates are ignored when caching spaces. + } + } + sb.Subnets = []common.BackingSubnet{} + if withSubnets { + info0 := common.BackingSubnetInfo{ + CIDR: ProviderInstance.Subnets[0].CIDR, + ProviderId: string(ProviderInstance.Subnets[0].ProviderId), + AllocatableIPLow: ProviderInstance.Subnets[0].AllocatableIPLow.String(), + AllocatableIPHigh: ProviderInstance.Subnets[0].AllocatableIPHigh.String(), + AvailabilityZones: ProviderInstance.Subnets[0].AvailabilityZones, + SpaceName: "private", + } + info1 := common.BackingSubnetInfo{ + CIDR: ProviderInstance.Subnets[1].CIDR, + ProviderId: string(ProviderInstance.Subnets[1].ProviderId), + AvailabilityZones: ProviderInstance.Subnets[1].AvailabilityZones, + SpaceName: "dmz", + } + + sb.Subnets = []common.BackingSubnet{ + &FakeSubnet{info0}, + &FakeSubnet{info1}, + } + } +} + +func (sb *StubBacking) EnvironConfig() (*config.Config, error) { + sb.MethodCall(sb, "EnvironConfig") + if err := sb.NextErr(); err != nil { + return nil, err + } + return sb.EnvConfig, nil +} + +func (sb *StubBacking) AvailabilityZones() ([]providercommon.AvailabilityZone, error) { + sb.MethodCall(sb, "AvailabilityZones") + if err := sb.NextErr(); err != nil { + return nil, err + } + return sb.Zones, nil +} + +func (sb *StubBacking) SetAvailabilityZones(zones []providercommon.AvailabilityZone) error { + sb.MethodCall(sb, "SetAvailabilityZones", zones) + return sb.NextErr() +} + +func (sb *StubBacking) AllSpaces() ([]common.BackingSpace, error) { + sb.MethodCall(sb, "AllSpaces") + if err := sb.NextErr(); err != nil { + return nil, err + } + + // Filter duplicates. + seen := set.Strings{} + output := []common.BackingSpace{} + for _, space := range sb.Spaces { + if seen.Contains(space.Name()) { + continue + } + seen.Add(space.Name()) + output = append(output, space) + } + return output, nil +} + +func (sb *StubBacking) AllSubnets() ([]common.BackingSubnet, error) { + sb.MethodCall(sb, "AllSubnets") + if err := sb.NextErr(); err != nil { + return nil, err + } + + // Filter duplicates. + seen := set.Strings{} + output := []common.BackingSubnet{} + for _, subnet := range sb.Subnets { + if seen.Contains(subnet.CIDR()) { + continue + } + seen.Add(subnet.CIDR()) + output = append(output, subnet) + } + return output, nil +} + +func (sb *StubBacking) AddSubnet(subnetInfo common.BackingSubnetInfo) (common.BackingSubnet, error) { + sb.MethodCall(sb, "AddSubnet", subnetInfo) + if err := sb.NextErr(); err != nil { + return nil, err + } + fs := &FakeSubnet{info: subnetInfo} + sb.Subnets = append(sb.Subnets, fs) + return fs, nil +} + +func (sb *StubBacking) AddSpace(name string, subnets []string, public bool) error { + sb.MethodCall(sb, "AddSpace", name, subnets, public) + if err := sb.NextErr(); err != nil { + return err + } + fs := &FakeSpace{SpaceName: name, SubnetIds: subnets, Public: public} + sb.Spaces = append(sb.Spaces, fs) + return nil +} + +// GoString implements fmt.GoStringer. +func (se *StubBacking) GoString() string { + return "&StubBacking{}" +} + +// StubProvider implements a subset of environs.EnvironProvider +// methods used in tests. +type StubProvider struct { + *testing.Stub + + Zones []providercommon.AvailabilityZone + Subnets []network.SubnetInfo + + environs.EnvironProvider // panic on any not implemented method call. +} + +var _ environs.EnvironProvider = (*StubProvider)(nil) + +func (sp *StubProvider) Open(cfg *config.Config) (environs.Environ, error) { + sp.MethodCall(sp, "Open", cfg) + if err := sp.NextErr(); err != nil { + return nil, err + } + switch cfg.Name() { + case StubEnvironName: + return EnvironInstance, nil + case StubZonedEnvironName: + return ZonedEnvironInstance, nil + case StubNetworkingEnvironName: + return NetworkingEnvironInstance, nil + case StubZonedNetworkingEnvironName: + return ZonedNetworkingEnvironInstance, nil + } + panic("unexpected environment name: " + cfg.Name()) +} + +// GoString implements fmt.GoStringer. +func (se *StubProvider) GoString() string { + return "&StubProvider{}" +} + +// StubEnviron is used in tests where environs.Environ is needed. +type StubEnviron struct { + *testing.Stub + + environs.Environ // panic on any not implemented method call +} + +var _ environs.Environ = (*StubEnviron)(nil) + +// GoString implements fmt.GoStringer. +func (se *StubEnviron) GoString() string { + return "&StubEnviron{}" +} + +// StubZonedEnviron is used in tests where providercommon.ZonedEnviron +// is needed. +type StubZonedEnviron struct { + *testing.Stub + + providercommon.ZonedEnviron // panic on any not implemented method call +} + +var _ providercommon.ZonedEnviron = (*StubZonedEnviron)(nil) + +func (se *StubZonedEnviron) AvailabilityZones() ([]providercommon.AvailabilityZone, error) { + se.MethodCall(se, "AvailabilityZones") + if err := se.NextErr(); err != nil { + return nil, err + } + return ProviderInstance.Zones, nil +} + +// GoString implements fmt.GoStringer. +func (se *StubZonedEnviron) GoString() string { + return "&StubZonedEnviron{}" +} + +// StubNetworkingEnviron is used in tests where +// environs.NetworkingEnviron is needed. +type StubNetworkingEnviron struct { + *testing.Stub + + environs.NetworkingEnviron // panic on any not implemented method call +} + +var _ environs.NetworkingEnviron = (*StubNetworkingEnviron)(nil) + +func (se *StubNetworkingEnviron) Subnets(instId instance.Id, subIds []network.Id) ([]network.SubnetInfo, error) { + se.MethodCall(se, "Subnets", instId, subIds) + if err := se.NextErr(); err != nil { + return nil, err + } + return ProviderInstance.Subnets, nil +} + +// GoString implements fmt.GoStringer. +func (se *StubNetworkingEnviron) GoString() string { + return "&StubNetworkingEnviron{}" +} + +// StubZonedNetworkingEnviron is used in tests where features from +// both environs.Networking and providercommon.ZonedEnviron are +// needed. +type StubZonedNetworkingEnviron struct { + *testing.Stub + + // panic on any not implemented method call + providercommon.ZonedEnviron + environs.Networking +} + +// GoString implements fmt.GoStringer. +func (se *StubZonedNetworkingEnviron) GoString() string { + return "&StubZonedNetworkingEnviron{}" +} + +func (se *StubZonedNetworkingEnviron) SupportsSpaces() (bool, error) { + se.MethodCall(se, "SupportsSpaces") + if err := se.NextErr(); err != nil { + return false, err + } + return true, nil +} + +func (se *StubZonedNetworkingEnviron) Subnets(instId instance.Id, subIds []network.Id) ([]network.SubnetInfo, error) { + se.MethodCall(se, "Subnets", instId, subIds) + if err := se.NextErr(); err != nil { + return nil, err + } + return ProviderInstance.Subnets, nil +} + +func (se *StubZonedNetworkingEnviron) AvailabilityZones() ([]providercommon.AvailabilityZone, error) { + se.MethodCall(se, "AvailabilityZones") + if err := se.NextErr(); err != nil { + return nil, err + } + return ProviderInstance.Zones, nil +} === modified file 'src/github.com/juju/juju/apiserver/tools.go' --- src/github.com/juju/juju/apiserver/tools.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/tools.go 2015-10-23 18:29:32 +0000 @@ -50,7 +50,6 @@ h.sendExistingError(w, http.StatusNotFound, err) return } - defer stateWrapper.cleanup() switch r.Method { case "GET": @@ -74,7 +73,6 @@ h.sendExistingError(w, http.StatusNotFound, err) return } - defer stateWrapper.cleanup() if err := stateWrapper.authenticateUser(r); err != nil { h.authError(w, h) === modified file 'src/github.com/juju/juju/apiserver/tools_test.go' --- src/github.com/juju/juju/apiserver/tools_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/tools_test.go 2015-10-23 18:29:32 +0000 @@ -27,6 +27,7 @@ "github.com/juju/juju/state" "github.com/juju/juju/state/toolstorage" + "github.com/juju/juju/testing" coretools "github.com/juju/juju/tools" "github.com/juju/juju/version" ) @@ -296,6 +297,7 @@ func (s *toolsSuite) TestDownloadFetchesAndVerifiesSize(c *gc.C) { // Upload fake tools, then upload over the top so the SHA256 hash does not match. + s.PatchValue(&version.Current.Number, testing.FakeVersionNumber) stor := s.DefaultToolsStorage envtesting.RemoveTools(c, stor, "released") tools := envtesting.AssertUploadFakeToolsVersions(c, stor, "released", "released", version.Current)[0] @@ -310,6 +312,7 @@ func (s *toolsSuite) TestDownloadFetchesAndVerifiesHash(c *gc.C) { // Upload fake tools, then upload over the top so the SHA256 hash does not match. + s.PatchValue(&version.Current.Number, testing.FakeVersionNumber) stor := s.DefaultToolsStorage envtesting.RemoveTools(c, stor, "released") tools := envtesting.AssertUploadFakeToolsVersions(c, stor, "released", "released", version.Current)[0] === modified file 'src/github.com/juju/juju/apiserver/uniter/state.go' --- src/github.com/juju/juju/apiserver/uniter/state.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/uniter/state.go 2015-10-23 18:29:32 +0000 @@ -25,8 +25,10 @@ WatchStorageAttachment(names.StorageTag, names.UnitTag) state.NotifyWatcher WatchFilesystemAttachment(names.MachineTag, names.FilesystemTag) state.NotifyWatcher WatchVolumeAttachment(names.MachineTag, names.VolumeTag) state.NotifyWatcher + WatchBlockDevices(names.MachineTag) state.NotifyWatcher AddStorageForUnit(tag names.UnitTag, name string, cons state.StorageConstraints) error UnitStorageConstraints(u names.UnitTag) (map[string]state.StorageConstraints, error) + BlockDevices(names.MachineTag) ([]state.BlockDeviceInfo, error) } type storageStateShim struct { === modified file 'src/github.com/juju/juju/apiserver/uniter/storage_test.go' --- src/github.com/juju/juju/apiserver/uniter/storage_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/uniter/storage_test.go 2015-10-23 18:29:32 +0000 @@ -80,6 +80,10 @@ changes: make(chan struct{}, 1), } volumeWatcher.changes <- struct{}{} + blockDevicesWatcher := &mockNotifyWatcher{ + changes: make(chan struct{}, 1), + } + blockDevicesWatcher.changes <- struct{}{} var calls []string state := &mockStorageState{ storageInstance: func(s names.StorageTag) (state.StorageInstance, error) { @@ -109,6 +113,11 @@ c.Assert(v, gc.DeepEquals, volumeTag) return volumeWatcher }, + watchBlockDevices: func(m names.MachineTag) state.NotifyWatcher { + calls = append(calls, "WatchBlockDevices") + c.Assert(m, gc.DeepEquals, machineTag) + return blockDevicesWatcher + }, } storage, err := uniter.NewStorageAPI(state, resources, getCanAccess) @@ -130,6 +139,7 @@ "StorageInstance", "StorageInstanceVolume", "WatchVolumeAttachment", + "WatchBlockDevices", "WatchStorageAttachment", }) } @@ -475,6 +485,7 @@ watchStorageAttachment func(names.StorageTag, names.UnitTag) state.NotifyWatcher watchFilesystemAttachment func(names.MachineTag, names.FilesystemTag) state.NotifyWatcher watchVolumeAttachment func(names.MachineTag, names.VolumeTag) state.NotifyWatcher + watchBlockDevices func(names.MachineTag) state.NotifyWatcher addUnitStorage func(u names.UnitTag, name string, cons state.StorageConstraints) error unitStorageConstraints func(u names.UnitTag) (map[string]state.StorageConstraints, error) } @@ -519,6 +530,10 @@ return m.watchVolumeAttachment(mtag, v) } +func (m *mockStorageState) WatchBlockDevices(mtag names.MachineTag) state.NotifyWatcher { + return m.watchBlockDevices(mtag) +} + func (m *mockStorageState) AddStorageForUnit(tag names.UnitTag, name string, cons state.StorageConstraints) error { return m.addUnitStorage(tag, name, cons) } === modified file 'src/github.com/juju/juju/apiserver/uniter/uniter_base.go' --- src/github.com/juju/juju/apiserver/uniter/uniter_base.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/uniter/uniter_base.go 2015-10-23 18:29:32 +0000 @@ -16,6 +16,7 @@ "github.com/juju/juju/apiserver/common" leadershipapiserver "github.com/juju/juju/apiserver/leadership" + "github.com/juju/juju/apiserver/meterstatus" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/leadership" "github.com/juju/juju/network" @@ -103,8 +104,8 @@ RebootRequester: common.NewRebootRequester(st, accessMachine), LeadershipSettingsAccessor: leadershipSettingsAccessorFactory(st, resources, authorizer), - // TODO(fwereade): so *every* unit should be allowed to - // get/set its own status *and* its service's? FFS. + // TODO(fwereade): so *every* unit should be allowed to get/set its + // own status *and* its service's? This is not a pleasing arrangement. StatusAPI: NewStatusAPI(st, accessUnitOrService), st: st, @@ -136,10 +137,11 @@ var unit *state.Unit unit, err = u.getUnit(tag) if err == nil { - address, ok := unit.PublicAddress() - if ok { - result.Results[i].Result = address - } else { + var address network.Address + address, err = unit.PublicAddress() + if err == nil { + result.Results[i].Result = address.Value + } else if network.IsNoAddress(err) { err = common.NoAddressSetError(tag, "public") } } @@ -169,10 +171,11 @@ var unit *state.Unit unit, err = u.getUnit(tag) if err == nil { - address, ok := unit.PrivateAddress() - if ok { - result.Results[i].Result = address - } else { + var address network.Address + address, err = unit.PrivateAddress() + if err == nil { + result.Results[i].Result = address.Value + } else if network.IsNoAddress(err) { err = common.NoAddressSetError(tag, "private") } } @@ -1024,7 +1027,7 @@ // private address (we already know it). privateAddress, _ := relUnit.PrivateAddress() settings := map[string]interface{}{ - "private-address": privateAddress, + "private-address": privateAddress.Value, } err = relUnit.EnterScope(settings) } @@ -1233,7 +1236,7 @@ var unit *state.Unit unit, err = u.getUnit(unitTag) if err == nil { - status, err = unit.GetMeterStatus() + status, err = meterstatus.MeterStatusWrapper(unit.GetMeterStatus) } result.Results[i].Code = status.Code.String() result.Results[i].Info = status.Info === modified file 'src/github.com/juju/juju/apiserver/uniter/uniter_base_test.go' --- src/github.com/juju/juju/apiserver/uniter/uniter_base_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/uniter/uniter_base_test.go 2015-10-23 18:29:32 +0000 @@ -135,7 +135,7 @@ c.Assert(err, jc.ErrorIsNil) args := params.SetStatus{ - Entities: []params.EntityStatus{ + Entities: []params.EntityStatusArgs{ {Tag: "unit-mysql-0", Status: params.StatusError, Info: "not really"}, {Tag: "unit-wordpress-0", Status: params.StatusRebooting, Info: "foobar"}, {Tag: "unit-foo-42", Status: params.StatusActive, Info: "blah"}, @@ -174,7 +174,7 @@ c.Assert(err, jc.ErrorIsNil) args := params.SetStatus{ - Entities: []params.EntityStatus{ + Entities: []params.EntityStatusArgs{ {Tag: "unit-mysql-0", Status: params.StatusError, Info: "not really"}, {Tag: "unit-wordpress-0", Status: params.StatusExecuting, Info: "foobar"}, {Tag: "unit-foo-42", Status: params.StatusRebooting, Info: "blah"}, @@ -213,7 +213,7 @@ c.Assert(err, jc.ErrorIsNil) args := params.SetStatus{ - Entities: []params.EntityStatus{ + Entities: []params.EntityStatusArgs{ {Tag: "unit-mysql-0", Status: params.StatusError, Info: "not really"}, {Tag: "unit-wordpress-0", Status: params.StatusTerminated, Info: "foobar"}, {Tag: "unit-foo-42", Status: params.StatusActive, Info: "blah"}, @@ -442,9 +442,9 @@ network.NewScopedAddress("1.2.3.4", network.ScopePublic), ) c.Assert(err, jc.ErrorIsNil) - address, ok := s.wordpressUnit.PublicAddress() - c.Assert(address, gc.Equals, "1.2.3.4") - c.Assert(ok, jc.IsTrue) + address, err := s.wordpressUnit.PublicAddress() + c.Assert(address.Value, gc.Equals, "1.2.3.4") + c.Assert(err, jc.ErrorIsNil) result, err = facade.PublicAddress(args) c.Assert(err, jc.ErrorIsNil) @@ -487,9 +487,9 @@ network.NewScopedAddress("1.2.3.4", network.ScopeCloudLocal), ) c.Assert(err, jc.ErrorIsNil) - address, ok := s.wordpressUnit.PrivateAddress() - c.Assert(address, gc.Equals, "1.2.3.4") - c.Assert(ok, jc.IsTrue) + address, err := s.wordpressUnit.PrivateAddress() + c.Assert(address.Value, gc.Equals, "1.2.3.4") + c.Assert(err, jc.ErrorIsNil) result, err = facade.PrivateAddress(args) c.Assert(err, jc.ErrorIsNil) @@ -2171,8 +2171,8 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(result.Results, gc.HasLen, 1) c.Assert(result.Results[0].Error, gc.IsNil) - c.Assert(result.Results[0].Code, gc.Equals, "NOT SET") - c.Assert(result.Results[0].Info, gc.Equals, "") + c.Assert(result.Results[0].Code, gc.Equals, "AMBER") + c.Assert(result.Results[0].Info, gc.Equals, "not set") newCode := "GREEN" newInfo := "All is ok." @@ -2255,6 +2255,7 @@ wc.AssertNoChange() err = s.wordpressUnit.SetMeterStatus("GREEN", "No additional information.") + c.Assert(err, jc.ErrorIsNil) wc.AssertOneChange() } === modified file 'src/github.com/juju/juju/apiserver/uniter/uniter_v2_test.go' --- src/github.com/juju/juju/apiserver/uniter/uniter_v2_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/uniter/uniter_v2_test.go 2015-10-23 18:29:32 +0000 @@ -59,6 +59,9 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(volumeAttachments, gc.HasLen, 1) + err = machine.SetProvisioned("inst-id", "fake_nonce", nil) + c.Assert(err, jc.ErrorIsNil) + err = s.State.SetVolumeInfo( volumeAttachments[0].Volume(), state.VolumeInfo{VolumeId: "vol-123", Size: 456}, === modified file 'src/github.com/juju/juju/apiserver/usermanager/usermanager.go' --- src/github.com/juju/juju/apiserver/usermanager/usermanager.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/apiserver/usermanager/usermanager.go 2015-10-23 18:29:32 +0000 @@ -4,6 +4,8 @@ package usermanager import ( + "time" + "github.com/juju/errors" "github.com/juju/loggo" "github.com/juju/names" @@ -166,20 +168,29 @@ // 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() + if err != nil { + if !state.IsNeverLoggedInError(err) { + logger.Debugf("error getting last login: %v", err) + } + } else { + lastLogin = &userLastLogin + } return params.UserInfoResult{ Result: ¶ms.UserInfo{ Username: user.Name(), DisplayName: user.DisplayName(), CreatedBy: user.CreatedBy(), DateCreated: user.DateCreated(), - LastConnection: user.LastLogin(), + LastConnection: lastLogin, Disabled: user.IsDisabled(), }, } } - var results params.UserInfoResults argCount := len(request.Entities) if argCount == 0 { users, err := api.state.AllUsers(request.IncludeDisabled) === modified file 'src/github.com/juju/juju/apiserver/usermanager/usermanager_test.go' --- src/github.com/juju/juju/apiserver/usermanager/usermanager_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/usermanager/usermanager_test.go 2015-10-23 18:29:32 +0000 @@ -4,6 +4,8 @@ package usermanager_test import ( + "time" + "github.com/juju/errors" "github.com/juju/names" jc "github.com/juju/testing/checkers" @@ -14,6 +16,7 @@ apiservertesting "github.com/juju/juju/apiserver/testing" "github.com/juju/juju/apiserver/usermanager" jujutesting "github.com/juju/juju/juju/testing" + "github.com/juju/juju/state" "github.com/juju/juju/testing/factory" ) @@ -308,40 +311,47 @@ results, err := s.usermanager.UserInfo(args) c.Assert(err, jc.ErrorIsNil) - expected := params.UserInfoResults{ - Results: []params.UserInfoResult{ - { - Result: ¶ms.UserInfo{ - Username: "foobar", - DisplayName: "Foo Bar", - CreatedBy: s.adminName, - DateCreated: userFoo.DateCreated(), - LastConnection: userFoo.LastLogin(), - }, - }, { - Result: ¶ms.UserInfo{ - Username: "barfoo", - DisplayName: "Bar Foo", - CreatedBy: s.adminName, - DateCreated: userBar.DateCreated(), - LastConnection: userBar.LastLogin(), - Disabled: true, - }, - }, { - Error: ¶ms.Error{ - Message: "permission denied", - Code: params.CodeUnauthorized, - }, - }, { - Error: ¶ms.Error{ - Message: "permission denied", - Code: params.CodeUnauthorized, - }, - }, { - Error: ¶ms.Error{ - Message: `"not-a-tag" is not a valid tag`, - }, - }}, + var expected params.UserInfoResults + for _, r := range []struct { + user *state.User + info *params.UserInfo + err *params.Error + }{ + { + user: userFoo, + info: ¶ms.UserInfo{ + Username: "foobar", + DisplayName: "Foo Bar", + }, + }, { + user: userBar, + info: ¶ms.UserInfo{ + Username: "barfoo", + DisplayName: "Bar Foo", + Disabled: true, + }, + }, { + err: ¶ms.Error{ + Message: "permission denied", + Code: params.CodeUnauthorized, + }, + }, { + err: ¶ms.Error{ + Message: "permission denied", + Code: params.CodeUnauthorized, + }, + }, { + err: ¶ms.Error{ + Message: `"not-a-tag" is not a valid tag`, + }, + }, + } { + if r.info != nil { + r.info.DateCreated = r.user.DateCreated() + r.info.LastConnection = lastLoginPointer(c, r.user) + r.info.CreatedBy = s.adminName + } + expected.Results = append(expected.Results, params.UserInfoResult{Result: r.info, Error: r.err}) } c.Assert(results, jc.DeepEquals, expected) @@ -356,34 +366,36 @@ args := params.UserInfoRequest{IncludeDisabled: true} results, err := s.usermanager.UserInfo(args) c.Assert(err, jc.ErrorIsNil) - expected := params.UserInfoResults{ - Results: []params.UserInfoResult{ - { - Result: ¶ms.UserInfo{ - Username: "barfoo", - DisplayName: "Bar Foo", - CreatedBy: s.adminName, - DateCreated: userBar.DateCreated(), - LastConnection: userBar.LastLogin(), - Disabled: true, - }, - }, { - Result: ¶ms.UserInfo{ - Username: s.adminName, - DisplayName: admin.DisplayName(), - CreatedBy: s.adminName, - DateCreated: admin.DateCreated(), - LastConnection: admin.LastLogin(), - }, - }, { - Result: ¶ms.UserInfo{ - Username: "foobar", - DisplayName: "Foo Bar", - CreatedBy: s.adminName, - DateCreated: userFoo.DateCreated(), - LastConnection: userFoo.LastLogin(), - }, - }}, + var expected params.UserInfoResults + for _, r := range []struct { + user *state.User + info *params.UserInfo + }{ + { + user: userBar, + info: ¶ms.UserInfo{ + Username: "barfoo", + DisplayName: "Bar Foo", + Disabled: true, + }, + }, { + user: admin, + info: ¶ms.UserInfo{ + Username: s.adminName, + DisplayName: admin.DisplayName(), + }, + }, { + user: userFoo, + info: ¶ms.UserInfo{ + Username: "foobar", + DisplayName: "Foo Bar", + }, + }, + } { + r.info.CreatedBy = s.adminName + r.info.DateCreated = r.user.DateCreated() + r.info.LastConnection = lastLoginPointer(c, r.user) + expected.Results = append(expected.Results, params.UserInfoResult{Result: r.info}) } c.Assert(results, jc.DeepEquals, expected) @@ -394,6 +406,17 @@ c.Assert(results, jc.DeepEquals, expected) } +func lastLoginPointer(c *gc.C, user *state.User) *time.Time { + lastLogin, err := user.LastLogin() + if err != nil { + if state.IsNeverLoggedInError(err) { + return nil + } + c.Fatal(err) + } + return &lastLogin +} + func (s *userManagerSuite) TestSetPassword(c *gc.C) { alex := s.Factory.MakeUser(c, &factory.UserParams{Name: "alex"}) === modified file 'src/github.com/juju/juju/apiserver/utils.go' --- src/github.com/juju/juju/apiserver/utils.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/utils.go 2015-10-23 18:29:32 +0000 @@ -36,24 +36,24 @@ } type validateArgs struct { - st *state.State - envUUID string + statePool *state.StatePool + envUUID string // strict validation does not allow empty UUID values strict bool // stateServerEnvOnly only validates the state server environment stateServerEnvOnly bool } -// validateEnvironUUID is the common validator for the various apiserver -// components that need to check for a valid environment UUID. -// An empty envUUID means that the connection has come in at the root -// of the URL space and refers to the state server environment. -// The *state.State parameter is expected to be the state server State -// connection. The return *state.State is a connection for the specified -// environment UUID if the UUID refers to an environment contained in the -// database. If the bool return value is true, the state connection must -// be closed by the caller at the end of serving the client connection. -func validateEnvironUUID(args validateArgs) (*state.State, bool, error) { +// validateEnvironUUID is the common validator for the various +// apiserver components that need to check for a valid environment +// UUID. An empty envUUID means that the connection has come in at +// the root of the URL space and refers to the state server +// environment. The returned *state.State is a connection for the +// specified environment UUID if the UUID refers to an environment +// contained in the database. +func validateEnvironUUID(args validateArgs) (*state.State, error) { + ssState := args.statePool.SystemState() + if args.envUUID == "" { // We allow the environUUID to be empty for 2 cases // 1) Compatibility with older clients @@ -62,31 +62,29 @@ // if the connection comes over a sufficiently up to date // login command. if args.strict { - return nil, false, errors.Trace(common.UnknownEnvironmentError(args.envUUID)) + return nil, errors.Trace(common.UnknownEnvironmentError(args.envUUID)) } logger.Debugf("validate env uuid: empty envUUID") - return args.st, false, nil + return ssState, nil } - if args.envUUID == args.st.EnvironUUID() { + if args.envUUID == ssState.EnvironUUID() { logger.Debugf("validate env uuid: state server environment - %s", args.envUUID) - return args.st, false, nil + return ssState, nil } if args.stateServerEnvOnly { - return nil, false, errors.Unauthorizedf("requested environment %q is not the state server environment", args.envUUID) + return nil, errors.Unauthorizedf("requested environment %q is not the state server environment", args.envUUID) } if !names.IsValidEnvironment(args.envUUID) { - return nil, false, errors.Trace(common.UnknownEnvironmentError(args.envUUID)) + return nil, errors.Trace(common.UnknownEnvironmentError(args.envUUID)) } envTag := names.NewEnvironTag(args.envUUID) - if env, err := args.st.GetEnvironment(envTag); err != nil { - return nil, false, errors.Wrap(err, common.UnknownEnvironmentError(args.envUUID)) - } else if env.Life() != state.Alive { - return nil, false, errors.Errorf("environment %q is no longer live", args.envUUID) + if _, err := ssState.GetEnvironment(envTag); err != nil { + return nil, errors.Wrap(err, common.UnknownEnvironmentError(args.envUUID)) } logger.Debugf("validate env uuid: %s", args.envUUID) - result, err := args.st.ForEnviron(envTag) + st, err := args.statePool.Get(args.envUUID) if err != nil { - return nil, false, errors.Trace(err) + return nil, errors.Trace(err) } - return result, true, nil + return st, nil } === modified file 'src/github.com/juju/juju/apiserver/utils_test.go' --- src/github.com/juju/juju/apiserver/utils_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/utils_test.go 2015-10-23 18:29:32 +0000 @@ -7,62 +7,67 @@ jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" + "github.com/juju/juju/state" "github.com/juju/juju/state/testing" ) type utilsSuite struct { testing.StateSuite + pool *state.StatePool } var _ = gc.Suite(&utilsSuite{}) +func (s *utilsSuite) SetUpTest(c *gc.C) { + s.StateSuite.SetUpTest(c) + s.pool = state.NewStatePool(s.State) + s.AddCleanup(func(*gc.C) { s.pool.Close() }) +} + func (s *utilsSuite) TestValidateEmpty(c *gc.C) { - st, needsClosing, err := validateEnvironUUID( + st, err := validateEnvironUUID( validateArgs{ - st: s.State, + statePool: s.pool, }) c.Assert(err, jc.ErrorIsNil) - c.Assert(needsClosing, jc.IsFalse) c.Assert(st.EnvironUUID(), gc.Equals, s.State.EnvironUUID()) } func (s *utilsSuite) TestValidateEmptyStrict(c *gc.C) { - _, _, err := validateEnvironUUID( + _, err := validateEnvironUUID( validateArgs{ - st: s.State, - strict: true, + statePool: s.pool, + strict: true, }) c.Assert(err, gc.ErrorMatches, `unknown environment: ""`) } func (s *utilsSuite) TestValidateStateServer(c *gc.C) { - st, needsClosing, err := validateEnvironUUID( + st, err := validateEnvironUUID( validateArgs{ - st: s.State, - envUUID: s.State.EnvironUUID(), + statePool: s.pool, + envUUID: s.State.EnvironUUID(), }) c.Assert(err, jc.ErrorIsNil) - c.Assert(needsClosing, jc.IsFalse) c.Assert(st.EnvironUUID(), gc.Equals, s.State.EnvironUUID()) } func (s *utilsSuite) TestValidateStateServerStrict(c *gc.C) { - st, needsClosing, err := validateEnvironUUID( + st, err := validateEnvironUUID( validateArgs{ - st: s.State, - envUUID: s.State.EnvironUUID(), - strict: true, + statePool: s.pool, + envUUID: s.State.EnvironUUID(), + strict: true, }) c.Assert(err, jc.ErrorIsNil) - c.Assert(needsClosing, jc.IsFalse) c.Assert(st.EnvironUUID(), gc.Equals, s.State.EnvironUUID()) } func (s *utilsSuite) TestValidateBadEnvUUID(c *gc.C) { - _, _, err := validateEnvironUUID( + _, err := validateEnvironUUID( validateArgs{ - st: s.State, - envUUID: "bad", + statePool: s.pool, + envUUID: "bad", }) c.Assert(err, gc.ErrorMatches, `unknown environment: "bad"`) } @@ -71,13 +76,12 @@ envState := s.Factory.MakeEnvironment(c, nil) defer envState.Close() - st, needsClosing, err := validateEnvironUUID( + st, err := validateEnvironUUID( validateArgs{ - st: s.State, - envUUID: envState.EnvironUUID(), + statePool: s.pool, + envUUID: envState.EnvironUUID(), }) c.Assert(err, jc.ErrorIsNil) - c.Assert(needsClosing, jc.IsTrue) c.Assert(st.EnvironUUID(), gc.Equals, envState.EnvironUUID()) st.Close() } @@ -86,27 +90,11 @@ envState := s.Factory.MakeEnvironment(c, nil) defer envState.Close() - _, _, err := validateEnvironUUID( + _, err := validateEnvironUUID( validateArgs{ - st: s.State, + statePool: s.pool, envUUID: envState.EnvironUUID(), stateServerEnvOnly: true, }) c.Assert(err, gc.ErrorMatches, `requested environment ".*" is not the state server environment`) } - -func (s *utilsSuite) TestValidateNonAliveEnvironment(c *gc.C) { - envState := s.Factory.MakeEnvironment(c, nil) - defer envState.Close() - env, err := envState.Environment() - c.Assert(err, jc.ErrorIsNil) - err = env.Destroy() - c.Assert(err, jc.ErrorIsNil) - - _, _, err = validateEnvironUUID( - validateArgs{ - st: s.State, - envUUID: envState.EnvironUUID(), - }) - c.Assert(err, gc.ErrorMatches, `environment ".*" is no longer live`) -} === modified file 'src/github.com/juju/juju/apiserver/watcher.go' --- src/github.com/juju/juju/apiserver/watcher.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/apiserver/watcher.go 2015-10-23 18:29:32 +0000 @@ -6,6 +6,8 @@ import ( "reflect" + "github.com/juju/errors" + "github.com/juju/juju/apiserver/common" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/state" @@ -13,8 +15,16 @@ func init() { common.RegisterFacade( - "AllWatcher", 0, newClientAllWatcher, - reflect.TypeOf((*srvClientAllWatcher)(nil)), + "AllWatcher", 0, NewAllWatcher, + reflect.TypeOf((*SrvAllWatcher)(nil)), + ) + // Note: AllEnvWatcher uses the same infrastructure as AllWatcher + // but they are get under separate names as it possible the may + // diverge in the future (especially in terms of authorisation + // checks). + common.RegisterFacade( + "AllEnvWatcher", 1, NewAllWatcher, + reflect.TypeOf((*SrvAllWatcher)(nil)), ) common.RegisterFacade( "NotifyWatcher", 0, newNotifyWatcher, @@ -36,40 +46,48 @@ "FilesystemAttachmentsWatcher", 1, newFilesystemAttachmentsWatcher, reflect.TypeOf((*srvMachineStorageIdsWatcher)(nil)), ) + common.RegisterFacade( + "EntityWatcher", 1, newEntityWatcher, + reflect.TypeOf((*srvEntityWatcher)(nil)), + ) } -func newClientAllWatcher(st *state.State, resources *common.Resources, auth common.Authorizer, id string) (interface{}, error) { +// NewAllEnvWatcher returns a new API server endpoint for interacting +// with a watcher created by the WatchAll and WatchAllEnvs API calls. +func NewAllWatcher(st *state.State, resources *common.Resources, auth common.Authorizer, id string) (interface{}, error) { if !auth.AuthClient() { return nil, common.ErrPerm } + watcher, ok := resources.Get(id).(*state.Multiwatcher) if !ok { return nil, common.ErrUnknownWatcher } - return &srvClientAllWatcher{ + return &SrvAllWatcher{ watcher: watcher, id: id, resources: resources, }, nil } -// srvClientAllWatcher defines the API methods on a state.Multiwatcher. -// which watches any changes to the state. Each client has its own current set -// of watchers, stored in resources. -type srvClientAllWatcher struct { +// SrvAllWatcher defines the API methods on a state.Multiwatcher. +// which watches any changes to the state. Each client has its own +// current set of watchers, stored in resources. It is used by both +// the AllWatcher and AllEnvWatcher facades. +type SrvAllWatcher struct { watcher *state.Multiwatcher id string resources *common.Resources } -func (aw *srvClientAllWatcher) Next() (params.AllWatcherNextResults, error) { +func (aw *SrvAllWatcher) Next() (params.AllWatcherNextResults, error) { deltas, err := aw.watcher.Next() return params.AllWatcherNextResults{ Deltas: deltas, }, err } -func (w *srvClientAllWatcher) Stop() error { +func (w *SrvAllWatcher) Stop() error { return w.resources.Stop(w.id) } @@ -287,3 +305,68 @@ func (w *srvMachineStorageIdsWatcher) Stop() error { return w.resources.Stop(w.id) } + +// EntityWatcher defines an interface based on the StringsWatcher +// but also providing a method for the mapping of the received +// strings to the tags of the according entities. +type EntityWatcher interface { + state.StringsWatcher + + // MapChanges maps the received strings to their according tag strings. + // The EntityFinder interface representing state or a mock has to be + // upcasted into the needed sub-interface of state for the real mapping. + MapChanges(in []string) ([]string, error) +} + +// srvEntityWatcher defines the API for methods on a state.StringsWatcher. +// Each client has its own current set of watchers, stored in resources. +// srvEntityWatcher notifies about changes for all entities of a given kind, +// sending the changes as a list of strings, which could be transformed +// from state entity ids to their corresponding entity tags. +type srvEntityWatcher struct { + st *state.State + resources *common.Resources + id string + watcher EntityWatcher +} + +func newEntityWatcher(st *state.State, resources *common.Resources, auth common.Authorizer, id string) (interface{}, error) { + if !isAgent(auth) { + return nil, common.ErrPerm + } + watcher, ok := resources.Get(id).(EntityWatcher) + if !ok { + return nil, common.ErrUnknownWatcher + } + return &srvEntityWatcher{ + st: st, + resources: resources, + id: id, + watcher: watcher, + }, nil +} + +// Next returns when a change has occured to an entity of the +// collection being watched since the most recent call to Next +// or the Watch call that created the srvEntityWatcher. +func (w *srvEntityWatcher) Next() (params.EntityWatchResult, error) { + if changes, ok := <-w.watcher.Changes(); ok { + mapped, err := w.watcher.MapChanges(changes) + if err != nil { + return params.EntityWatchResult{}, errors.Annotate(err, "cannot map changes") + } + return params.EntityWatchResult{ + Changes: mapped, + }, nil + } + err := w.watcher.Err() + if err == nil { + err = common.ErrStoppedWatcher + } + return params.EntityWatchResult{}, err +} + +// Stop stops the watcher. +func (w *srvEntityWatcher) Stop() error { + return w.resources.Stop(w.id) +} === modified file 'src/github.com/juju/juju/cert/cert_test.go' --- src/github.com/juju/juju/cert/cert_test.go 2015-04-14 14:11:54 +0000 +++ src/github.com/juju/juju/cert/cert_test.go 2015-10-23 18:29:32 +0000 @@ -8,7 +8,6 @@ "crypto/rsa" "crypto/tls" "crypto/x509" - "fmt" "io" "io/ioutil" "net" @@ -226,55 +225,48 @@ clientCertPool := x509.NewCertPool() clientCertPool.AddCert(caCert) - var inBytes, outBytes bytes.Buffer + var outBytes bytes.Buffer const msg = "hello to the server" p0, p1 := net.Pipe() - p0 = bufferedConn(p0, 3) - p0 = recordingConn(p0, &inBytes, &outBytes) + p0 = &recordingConn{ + Conn: p0, + Writer: io.MultiWriter(p0, &outBytes), + } var clientState tls.ConnectionState done := make(chan error) go func() { - clientConn := tls.Client(p0, &tls.Config{ - ServerName: "anyServer", - RootCAs: clientCertPool, - }) - defer clientConn.Close() - - _, err := clientConn.Write([]byte(msg)) - if err != nil { - done <- fmt.Errorf("client: %v", err) - } - clientState = clientConn.ConnectionState() - done <- nil - }() - go func() { - serverConn := tls.Server(p1, &tls.Config{ - Certificates: []tls.Certificate{ - newTLSCert(c, srvCert, srvKey), - }, - }) - defer serverConn.Close() - data, err := ioutil.ReadAll(serverConn) - if err != nil { - done <- fmt.Errorf("server: %v", err) - return - } - if string(data) != msg { - done <- fmt.Errorf("server: got %q; expected %q", data, msg) - return - } - - done <- nil - }() - - for i := 0; i < 2; i++ { - err := <-done - c.Check(err, jc.ErrorIsNil) - } - - outData := string(outBytes.Bytes()) + config := tls.Config{ + Certificates: []tls.Certificate{{ + Certificate: [][]byte{srvCert.Raw}, + PrivateKey: srvKey, + }}, + } + + conn := tls.Server(p1, &config) + defer conn.Close() + data, err := ioutil.ReadAll(conn) + c.Assert(err, jc.ErrorIsNil) + c.Assert(string(data), gc.Equals, msg) + close(done) + }() + + clientConn := tls.Client(p0, &tls.Config{ + ServerName: "anyServer", + RootCAs: clientCertPool, + }) + defer clientConn.Close() + + _, err := clientConn.Write([]byte(msg)) + c.Assert(err, jc.ErrorIsNil) + clientState = clientConn.ConnectionState() + clientConn.Close() + + // wait for server to exit + <-done + + outData := outBytes.String() c.Assert(outData, gc.Not(gc.HasLen), 0) if strings.Index(outData, msg) != -1 { c.Fatalf("TLS connection not encrypted") @@ -284,43 +276,13 @@ return clientState.VerifiedChains[0][1].Subject.CommonName } -func newTLSCert(c *gc.C, cert *x509.Certificate, key *rsa.PrivateKey) tls.Certificate { - return tls.Certificate{ - Certificate: [][]byte{cert.Raw}, - PrivateKey: key, - } -} - -// bufferedConn adds buffering for at least -// n writes to the given connection. -func bufferedConn(c net.Conn, n int) net.Conn { - for i := 0; i < n; i++ { - p0, p1 := net.Pipe() - go copyClose(p1, c) - go copyClose(c, p1) - c = p0 - } - return c -} - -// recordingConn returns a connection which -// records traffic in or out of the given connection. -func recordingConn(c net.Conn, in, out io.Writer) net.Conn { - p0, p1 := net.Pipe() - go func() { - io.Copy(io.MultiWriter(c, out), p1) - c.Close() - }() - go func() { - io.Copy(io.MultiWriter(p1, in), c) - p1.Close() - }() - return p0 -} - -func copyClose(w io.WriteCloser, r io.Reader) { - io.Copy(w, r) - w.Close() +type recordingConn struct { + net.Conn + io.Writer +} + +func (c recordingConn) Write(buf []byte) (int, error) { + return c.Writer.Write(buf) } // roundTime returns t rounded to the previous whole second. === added file 'src/github.com/juju/juju/cloudconfig/BSD3-License.txt' --- src/github.com/juju/juju/cloudconfig/BSD3-License.txt 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cloudconfig/BSD3-License.txt 2015-10-23 18:29:32 +0000 @@ -0,0 +1,11 @@ +BSD License + +Copyright (c) 2009, Vladimir Vasiltsov All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Names of its contributors may not be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. === modified file 'src/github.com/juju/juju/cloudconfig/instancecfg/instancecfg.go' --- src/github.com/juju/juju/cloudconfig/instancecfg/instancecfg.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cloudconfig/instancecfg/instancecfg.go 2015-10-23 18:29:32 +0000 @@ -110,6 +110,9 @@ MachineContainerHostname string // Networks holds a list of networks the instances should be on. + // + // TODO(dimitern): Drop this in a follow-up in favor or spaces + // constraints. Networks []string // AuthorizedKeys specifies the keys that are allowed to @@ -375,15 +378,11 @@ return nil } -// DataDir is the default data directory. Tests can override this -// where needed, so they don't need to mess with global system state. -var DataDir = agent.DefaultDataDir - // logDir returns a filesystem path to the location where applications // may create a folder containing logs var logDir = paths.MustSucceed(paths.LogDir(version.Current.Series)) -// DefaultBridgeName is the network bridge device namme used for LXC and KVM +// DefaultBridgeName is the network bridge device name used for LXC and KVM // containers const DefaultBridgeName = "juju-br0" @@ -515,14 +514,13 @@ return errors.Trace(err) } - if multiwatcher.AnyJobNeedsState(icfg.Jobs...) { + if isStateInstanceConfig(icfg) { // Add NUMACTL preference. Needed to work for both bootstrap and high availability // Only makes sense for state server logger.Debugf("Setting numa ctl preference to %v", cfg.NumaCtlPreference()) // Unfortunately, AgentEnvironment can only take strings as values icfg.AgentEnvironment[agent.NumaCtlPreference] = fmt.Sprintf("%v", cfg.NumaCtlPreference()) } - // The following settings are only appropriate at bootstrap time. At the // moment, the only state server is the bootstrap node, but this // will probably change. @@ -609,3 +607,15 @@ } return cfg, nil } + +// isStateInstanceConfig determines if given machine configuration +// is for State Server by iterating over machine's jobs. +// If JobManageEnviron is present, this is a state server. +func isStateInstanceConfig(icfg *InstanceConfig) bool { + for _, aJob := range icfg.Jobs { + if aJob == multiwatcher.JobManageEnviron { + return true + } + } + return false +} === modified file 'src/github.com/juju/juju/cloudconfig/powershell_helpers.go' --- src/github.com/juju/juju/cloudconfig/powershell_helpers.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cloudconfig/powershell_helpers.go 2015-10-23 18:29:32 +0000 @@ -1,6 +1,7 @@ // Copyright 2014, 2015 Canonical Ltd. // Copyright 2014, 2015 Cloudbase Solutions // Copyright 2012 Aaron Jensen +// Copyright (c) 2009, Vladimir Vasiltsov All rights reserved. // // Licensed under the AGPLv3, see LICENCE file for details. // @@ -10,6 +11,14 @@ // compatible we can and have licensed this derived work under AGPLv3. The original // Apache-2.0 license for the external source can be found inside Apache-License.txt. // Copyright statement of the external source: Copyright 2012 Aaron Jensen +// +// This file borrowed some code from https://code.google.com/p/tar-cs/ which is +// This external source is licensed under BSD3 License which is compatible with +// AGPLv3 license. Because it's compatible we can have have licensed this +// derived work under AGPLv3. The original BSD3 license for the external source +// can be found inside BSD3-License.txt. +// Copyright statement of the external source: +// Copyright (c) 2009, Vladimir Vasiltsov All rights reserved. package cloudconfig @@ -19,34 +28,34 @@ function ExecRetry($command, $maxRetryCount = 10, $retryInterval=2) { - $currErrorActionPreference = $ErrorActionPreference - $ErrorActionPreference = "Continue" - - $retryCount = 0 - while ($true) - { - try - { - & $command - break - } - catch [System.Exception] - { - $retryCount++ - if ($retryCount -ge $maxRetryCount) - { - $ErrorActionPreference = $currErrorActionPreference - throw - } - else - { - Write-Error $_.Exception - Start-Sleep $retryInterval - } - } - } - - $ErrorActionPreference = $currErrorActionPreference + $currErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = "Continue" + + $retryCount = 0 + while ($true) + { + try + { + & $command + break + } + catch [System.Exception] + { + $retryCount++ + if ($retryCount -ge $maxRetryCount) + { + $ErrorActionPreference = $currErrorActionPreference + throw + } + else + { + Write-Error $_.Exception + Start-Sleep $retryInterval + } + } + } + + $ErrorActionPreference = $currErrorActionPreference } function create-account ([string]$accountName, [string]$accountDescription, [string]$password) { @@ -60,7 +69,11 @@ $User.UserFlags[0] = $User.UserFlags[0] -bor 0x10000 $user.SetInfo() - $objOU = [ADSI]"WinNT://$hostname/Administrators,group" + # This gets the Administrator group name that is localized on different windows versions. + # However the SID S-1-5-32-544 is the same on all versions. + $adminGroup = (New-Object System.Security.Principal.SecurityIdentifier("S-1-5-32-544")).Translate([System.Security.Principal.NTAccount]).Value.Split("\")[1] + + $objOU = [ADSI]"WinNT://$hostname/$adminGroup,group" $objOU.add("WinNT://$hostname/$accountName") } @@ -71,29 +84,29 @@ namespace PSCloudbase { - public sealed class Win32CryptApi - { - public static long CRYPT_SILENT = 0x00000040; - public static long CRYPT_VERIFYCONTEXT = 0xF0000000; - public static int PROV_RSA_FULL = 1; - - [DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)] - [return : MarshalAs(UnmanagedType.Bool)] - public static extern bool CryptAcquireContext(ref IntPtr hProv, - StringBuilder pszContainer, // Don't use string, as Powershell replaces $null with an empty string - StringBuilder pszProvider, // Don't use string, as Powershell replaces $null with an empty string - uint dwProvType, - uint dwFlags); - - [DllImport("Advapi32.dll", EntryPoint = "CryptReleaseContext", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern bool CryptReleaseContext(IntPtr hProv, Int32 dwFlags); - - [DllImport("advapi32.dll", SetLastError=true)] - public static extern bool CryptGenRandom(IntPtr hProv, uint dwLen, byte[] pbBuffer); - - [DllImport("Kernel32.dll")] - public static extern uint GetLastError(); - } + public sealed class Win32CryptApi + { + public static long CRYPT_SILENT = 0x00000040; + public static long CRYPT_VERIFYCONTEXT = 0xF0000000; + public static int PROV_RSA_FULL = 1; + + [DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)] + [return : MarshalAs(UnmanagedType.Bool)] + public static extern bool CryptAcquireContext(ref IntPtr hProv, + StringBuilder pszContainer, // Don't use string, as Powershell replaces $null with an empty string + StringBuilder pszProvider, // Don't use string, as Powershell replaces $null with an empty string + uint dwProvType, + uint dwFlags); + + [DllImport("Advapi32.dll", EntryPoint = "CryptReleaseContext", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern bool CryptReleaseContext(IntPtr hProv, Int32 dwFlags); + + [DllImport("advapi32.dll", SetLastError=true)] + public static extern bool CryptGenRandom(IntPtr hProv, uint dwLen, byte[] pbBuffer); + + [DllImport("Kernel32.dll")] + public static extern uint GetLastError(); + } } "@ @@ -101,42 +114,42 @@ function Get-RandomPassword { - [CmdletBinding()] - param - ( - [parameter(Mandatory=$true)] - [int]$Length - ) - process - { - $hProvider = 0 - try - { - if(![PSCloudbase.Win32CryptApi]::CryptAcquireContext([ref]$hProvider, $null, $null, - [PSCloudbase.Win32CryptApi]::PROV_RSA_FULL, - ([PSCloudbase.Win32CryptApi]::CRYPT_VERIFYCONTEXT -bor - [PSCloudbase.Win32CryptApi]::CRYPT_SILENT))) - { - throw "CryptAcquireContext failed with error: 0x" + "{0:X0}" -f [PSCloudbase.Win32CryptApi]::GetLastError() - } - - $buffer = New-Object byte[] $Length - if(![PSCloudbase.Win32CryptApi]::CryptGenRandom($hProvider, $Length, $buffer)) - { - throw "CryptGenRandom failed with error: 0x" + "{0:X0}" -f [PSCloudbase.Win32CryptApi]::GetLastError() - } - - $buffer | ForEach-Object { $password += "{0:X0}" -f $_ } - return $password - } - finally - { - if($hProvider) - { - $retVal = [PSCloudbase.Win32CryptApi]::CryptReleaseContext($hProvider, 0) - } - } - } + [CmdletBinding()] + param + ( + [parameter(Mandatory=$true)] + [int]$Length + ) + process + { + $hProvider = 0 + try + { + if(![PSCloudbase.Win32CryptApi]::CryptAcquireContext([ref]$hProvider, $null, $null, + [PSCloudbase.Win32CryptApi]::PROV_RSA_FULL, + ([PSCloudbase.Win32CryptApi]::CRYPT_VERIFYCONTEXT -bor + [PSCloudbase.Win32CryptApi]::CRYPT_SILENT))) + { + throw "CryptAcquireContext failed with error: 0x" + "{0:X0}" -f [PSCloudbase.Win32CryptApi]::GetLastError() + } + + $buffer = New-Object byte[] $Length + if(![PSCloudbase.Win32CryptApi]::CryptGenRandom($hProvider, $Length, $buffer)) + { + throw "CryptGenRandom failed with error: 0x" + "{0:X0}" -f [PSCloudbase.Win32CryptApi]::GetLastError() + } + + $buffer | ForEach-Object { $password += "{0:X0}" -f $_ } + return $password + } + finally + { + if($hProvider) + { + $retVal = [PSCloudbase.Win32CryptApi]::CryptReleaseContext($hProvider, 0) + } + } + } } $SourcePolicy = @" @@ -153,640 +166,667 @@ namespace PSCarbon { - public sealed class Lsa - { - // ReSharper disable InconsistentNaming - [StructLayout(LayoutKind.Sequential)] - internal struct LSA_UNICODE_STRING - { - internal LSA_UNICODE_STRING(string inputString) - { - if (inputString == null) - { - Buffer = IntPtr.Zero; - Length = 0; - MaximumLength = 0; - } - else - { - Buffer = Marshal.StringToHGlobalAuto(inputString); - Length = (ushort)(inputString.Length * UnicodeEncoding.CharSize); - MaximumLength = (ushort)((inputString.Length + 1) * UnicodeEncoding.CharSize); - } - } - - internal ushort Length; - internal ushort MaximumLength; - internal IntPtr Buffer; - } - - [StructLayout(LayoutKind.Sequential)] - internal struct LSA_OBJECT_ATTRIBUTES - { - internal uint Length; - internal IntPtr RootDirectory; - internal LSA_UNICODE_STRING ObjectName; - internal uint Attributes; - internal IntPtr SecurityDescriptor; - internal IntPtr SecurityQualityOfService; - } - - [StructLayout(LayoutKind.Sequential)] - public struct LUID - { - public uint LowPart; - public int HighPart; - } - - // ReSharper disable UnusedMember.Local - private const uint POLICY_VIEW_LOCAL_INFORMATION = 0x00000001; - private const uint POLICY_VIEW_AUDIT_INFORMATION = 0x00000002; - private const uint POLICY_GET_PRIVATE_INFORMATION = 0x00000004; - private const uint POLICY_TRUST_ADMIN = 0x00000008; - private const uint POLICY_CREATE_ACCOUNT = 0x00000010; - private const uint POLICY_CREATE_SECRET = 0x00000014; - private const uint POLICY_CREATE_PRIVILEGE = 0x00000040; - private const uint POLICY_SET_DEFAULT_QUOTA_LIMITS = 0x00000080; - private const uint POLICY_SET_AUDIT_REQUIREMENTS = 0x00000100; - private const uint POLICY_AUDIT_LOG_ADMIN = 0x00000200; - private const uint POLICY_SERVER_ADMIN = 0x00000400; - private const uint POLICY_LOOKUP_NAMES = 0x00000800; - private const uint POLICY_NOTIFICATION = 0x00001000; - // ReSharper restore UnusedMember.Local - - [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] - public static extern bool LookupPrivilegeValue( - [MarshalAs(UnmanagedType.LPTStr)] string lpSystemName, - [MarshalAs(UnmanagedType.LPTStr)] string lpName, - out LUID lpLuid); - - [DllImport("advapi32.dll", CharSet = CharSet.Unicode)] - private static extern uint LsaAddAccountRights( - IntPtr PolicyHandle, - IntPtr AccountSid, - LSA_UNICODE_STRING[] UserRights, - uint CountOfRights); - - [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = false)] - private static extern uint LsaClose(IntPtr ObjectHandle); - - [DllImport("advapi32.dll", SetLastError = true)] - private static extern uint LsaEnumerateAccountRights(IntPtr PolicyHandle, - IntPtr AccountSid, - out IntPtr UserRights, - out uint CountOfRights - ); - - [DllImport("advapi32.dll", SetLastError = true)] - private static extern uint LsaFreeMemory(IntPtr pBuffer); - - [DllImport("advapi32.dll")] - private static extern int LsaNtStatusToWinError(long status); - - [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)] - private static extern uint LsaOpenPolicy(ref LSA_UNICODE_STRING SystemName, ref LSA_OBJECT_ATTRIBUTES ObjectAttributes, uint DesiredAccess, out IntPtr PolicyHandle ); - - [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)] - static extern uint LsaRemoveAccountRights( - IntPtr PolicyHandle, - IntPtr AccountSid, - [MarshalAs(UnmanagedType.U1)] - bool AllRights, - LSA_UNICODE_STRING[] UserRights, - uint CountOfRights); - // ReSharper restore InconsistentNaming - - private static IntPtr GetIdentitySid(string identity) - { - var sid = - new NTAccount(identity).Translate(typeof (SecurityIdentifier)) as SecurityIdentifier; - if (sid == null) - { - throw new ArgumentException(string.Format("Account {0} not found.", identity)); - } - var sidBytes = new byte[sid.BinaryLength]; - sid.GetBinaryForm(sidBytes, 0); - var sidPtr = Marshal.AllocHGlobal(sidBytes.Length); - Marshal.Copy(sidBytes, 0, sidPtr, sidBytes.Length); - return sidPtr; - } - - private static IntPtr GetLsaPolicyHandle() - { - var computerName = Environment.MachineName; - IntPtr hPolicy; - var objectAttributes = new LSA_OBJECT_ATTRIBUTES - { - Length = 0, - RootDirectory = IntPtr.Zero, - Attributes = 0, - SecurityDescriptor = IntPtr.Zero, - SecurityQualityOfService = IntPtr.Zero - }; - - const uint ACCESS_MASK = POLICY_CREATE_SECRET | POLICY_LOOKUP_NAMES | POLICY_VIEW_LOCAL_INFORMATION; - var machineNameLsa = new LSA_UNICODE_STRING(computerName); - var result = LsaOpenPolicy(ref machineNameLsa, ref objectAttributes, ACCESS_MASK, out hPolicy); - HandleLsaResult(result); - return hPolicy; - } - - public static string[] GetPrivileges(string identity) - { - var sidPtr = GetIdentitySid(identity); - var hPolicy = GetLsaPolicyHandle(); - var rightsPtr = IntPtr.Zero; - - try - { - - var privileges = new List(); - - uint rightsCount; - var result = LsaEnumerateAccountRights(hPolicy, sidPtr, out rightsPtr, out rightsCount); - var win32ErrorCode = LsaNtStatusToWinError(result); - // the user has no privileges - if( win32ErrorCode == STATUS_OBJECT_NAME_NOT_FOUND ) - { - return new string[0]; - } - HandleLsaResult(result); - - var myLsaus = new LSA_UNICODE_STRING(); - for (ulong i = 0; i < rightsCount; i++) - { - var itemAddr = new IntPtr(rightsPtr.ToInt64() + (long) (i*(ulong) Marshal.SizeOf(myLsaus))); - myLsaus = (LSA_UNICODE_STRING) Marshal.PtrToStructure(itemAddr, myLsaus.GetType()); - var cvt = new char[myLsaus.Length/UnicodeEncoding.CharSize]; - Marshal.Copy(myLsaus.Buffer, cvt, 0, myLsaus.Length/UnicodeEncoding.CharSize); - var thisRight = new string(cvt); - privileges.Add(thisRight); - } - return privileges.ToArray(); - } - finally - { - Marshal.FreeHGlobal(sidPtr); - var result = LsaClose(hPolicy); - HandleLsaResult(result); - result = LsaFreeMemory(rightsPtr); - HandleLsaResult(result); - } - } - - public static void GrantPrivileges(string identity, string[] privileges) - { - var sidPtr = GetIdentitySid(identity); - var hPolicy = GetLsaPolicyHandle(); - - try - { - var lsaPrivileges = StringsToLsaStrings(privileges); - var result = LsaAddAccountRights(hPolicy, sidPtr, lsaPrivileges, (uint)lsaPrivileges.Length); - HandleLsaResult(result); - } - finally - { - Marshal.FreeHGlobal(sidPtr); - var result = LsaClose(hPolicy); - HandleLsaResult(result); - } - } - - const int STATUS_SUCCESS = 0x0; - const int STATUS_OBJECT_NAME_NOT_FOUND = 0x00000002; - const int STATUS_ACCESS_DENIED = 0x00000005; - const int STATUS_INVALID_HANDLE = 0x00000006; - const int STATUS_UNSUCCESSFUL = 0x0000001F; - const int STATUS_INVALID_PARAMETER = 0x00000057; - const int STATUS_NO_SUCH_PRIVILEGE = 0x00000521; - const int STATUS_INVALID_SERVER_STATE = 0x00000548; - const int STATUS_INTERNAL_DB_ERROR = 0x00000567; - const int STATUS_INSUFFICIENT_RESOURCES = 0x000005AA; - - private static readonly Dictionary ErrorMessages = new Dictionary - { - {STATUS_OBJECT_NAME_NOT_FOUND, "Object name not found. An object in the LSA policy database was not found. The object may have been specified either by SID or by name, depending on its type."}, - {STATUS_ACCESS_DENIED, "Access denied. Caller does not have the appropriate access to complete the operation."}, - {STATUS_INVALID_HANDLE, "Invalid handle. Indicates an object or RPC handle is not valid in the context used."}, - {STATUS_UNSUCCESSFUL, "Unsuccessful. Generic failure, such as RPC connection failure."}, - {STATUS_INVALID_PARAMETER, "Invalid parameter. One of the parameters is not valid."}, - {STATUS_NO_SUCH_PRIVILEGE, "No such privilege. Indicates a specified privilege does not exist."}, - {STATUS_INVALID_SERVER_STATE, "Invalid server state. Indicates the LSA server is currently disabled."}, - {STATUS_INTERNAL_DB_ERROR, "Internal database error. The LSA database contains an internal inconsistency."}, - {STATUS_INSUFFICIENT_RESOURCES, "Insufficient resources. There are not enough system resources (such as memory to allocate buffers) to complete the call."} - }; - - private static void HandleLsaResult(uint returnCode) - { - var win32ErrorCode = LsaNtStatusToWinError(returnCode); - - if( win32ErrorCode == STATUS_SUCCESS) - return; - - if( ErrorMessages.ContainsKey(win32ErrorCode) ) - { - throw new Win32Exception(win32ErrorCode, ErrorMessages[win32ErrorCode]); - } - - throw new Win32Exception(win32ErrorCode); - } - - public static void RevokePrivileges(string identity, string[] privileges) - { - var sidPtr = GetIdentitySid(identity); - var hPolicy = GetLsaPolicyHandle(); - - try - { - var currentPrivileges = GetPrivileges(identity); - if (currentPrivileges.Length == 0) - { - return; - } - var lsaPrivileges = StringsToLsaStrings(privileges); - var result = LsaRemoveAccountRights(hPolicy, sidPtr, false, lsaPrivileges, (uint)lsaPrivileges.Length); - HandleLsaResult(result); - } - finally - { - Marshal.FreeHGlobal(sidPtr); - var result = LsaClose(hPolicy); - HandleLsaResult(result); - } - - } - - private static LSA_UNICODE_STRING[] StringsToLsaStrings(string[] privileges) - { - var lsaPrivileges = new LSA_UNICODE_STRING[privileges.Length]; - for (var idx = 0; idx < privileges.Length; ++idx) - { - lsaPrivileges[idx] = new LSA_UNICODE_STRING(privileges[idx]); - } - return lsaPrivileges; - } - } + public sealed class Lsa + { + // ReSharper disable InconsistentNaming + [StructLayout(LayoutKind.Sequential)] + internal struct LSA_UNICODE_STRING + { + internal LSA_UNICODE_STRING(string inputString) + { + if (inputString == null) + { + Buffer = IntPtr.Zero; + Length = 0; + MaximumLength = 0; + } + else + { + Buffer = Marshal.StringToHGlobalAuto(inputString); + Length = (ushort)(inputString.Length * UnicodeEncoding.CharSize); + MaximumLength = (ushort)((inputString.Length + 1) * UnicodeEncoding.CharSize); + } + } + + internal ushort Length; + internal ushort MaximumLength; + internal IntPtr Buffer; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct LSA_OBJECT_ATTRIBUTES + { + internal uint Length; + internal IntPtr RootDirectory; + internal LSA_UNICODE_STRING ObjectName; + internal uint Attributes; + internal IntPtr SecurityDescriptor; + internal IntPtr SecurityQualityOfService; + } + + [StructLayout(LayoutKind.Sequential)] + public struct LUID + { + public uint LowPart; + public int HighPart; + } + + // ReSharper disable UnusedMember.Local + private const uint POLICY_VIEW_LOCAL_INFORMATION = 0x00000001; + private const uint POLICY_VIEW_AUDIT_INFORMATION = 0x00000002; + private const uint POLICY_GET_PRIVATE_INFORMATION = 0x00000004; + private const uint POLICY_TRUST_ADMIN = 0x00000008; + private const uint POLICY_CREATE_ACCOUNT = 0x00000010; + private const uint POLICY_CREATE_SECRET = 0x00000014; + private const uint POLICY_CREATE_PRIVILEGE = 0x00000040; + private const uint POLICY_SET_DEFAULT_QUOTA_LIMITS = 0x00000080; + private const uint POLICY_SET_AUDIT_REQUIREMENTS = 0x00000100; + private const uint POLICY_AUDIT_LOG_ADMIN = 0x00000200; + private const uint POLICY_SERVER_ADMIN = 0x00000400; + private const uint POLICY_LOOKUP_NAMES = 0x00000800; + private const uint POLICY_NOTIFICATION = 0x00001000; + // ReSharper restore UnusedMember.Local + + [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern bool LookupPrivilegeValue( + [MarshalAs(UnmanagedType.LPTStr)] string lpSystemName, + [MarshalAs(UnmanagedType.LPTStr)] string lpName, + out LUID lpLuid); + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode)] + private static extern uint LsaAddAccountRights( + IntPtr PolicyHandle, + IntPtr AccountSid, + LSA_UNICODE_STRING[] UserRights, + uint CountOfRights); + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = false)] + private static extern uint LsaClose(IntPtr ObjectHandle); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern uint LsaEnumerateAccountRights(IntPtr PolicyHandle, + IntPtr AccountSid, + out IntPtr UserRights, + out uint CountOfRights + ); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern uint LsaFreeMemory(IntPtr pBuffer); + + [DllImport("advapi32.dll")] + private static extern int LsaNtStatusToWinError(long status); + + [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)] + private static extern uint LsaOpenPolicy(ref LSA_UNICODE_STRING SystemName, ref LSA_OBJECT_ATTRIBUTES ObjectAttributes, uint DesiredAccess, out IntPtr PolicyHandle ); + + [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)] + static extern uint LsaRemoveAccountRights( + IntPtr PolicyHandle, + IntPtr AccountSid, + [MarshalAs(UnmanagedType.U1)] + bool AllRights, + LSA_UNICODE_STRING[] UserRights, + uint CountOfRights); + // ReSharper restore InconsistentNaming + + private static IntPtr GetIdentitySid(string identity) + { + var sid = + new NTAccount(identity).Translate(typeof (SecurityIdentifier)) as SecurityIdentifier; + if (sid == null) + { + throw new ArgumentException(string.Format("Account {0} not found.", identity)); + } + var sidBytes = new byte[sid.BinaryLength]; + sid.GetBinaryForm(sidBytes, 0); + var sidPtr = Marshal.AllocHGlobal(sidBytes.Length); + Marshal.Copy(sidBytes, 0, sidPtr, sidBytes.Length); + return sidPtr; + } + + private static IntPtr GetLsaPolicyHandle() + { + var computerName = Environment.MachineName; + IntPtr hPolicy; + var objectAttributes = new LSA_OBJECT_ATTRIBUTES + { + Length = 0, + RootDirectory = IntPtr.Zero, + Attributes = 0, + SecurityDescriptor = IntPtr.Zero, + SecurityQualityOfService = IntPtr.Zero + }; + + const uint ACCESS_MASK = POLICY_CREATE_SECRET | POLICY_LOOKUP_NAMES | POLICY_VIEW_LOCAL_INFORMATION; + var machineNameLsa = new LSA_UNICODE_STRING(computerName); + var result = LsaOpenPolicy(ref machineNameLsa, ref objectAttributes, ACCESS_MASK, out hPolicy); + HandleLsaResult(result); + return hPolicy; + } + + public static string[] GetPrivileges(string identity) + { + var sidPtr = GetIdentitySid(identity); + var hPolicy = GetLsaPolicyHandle(); + var rightsPtr = IntPtr.Zero; + + try + { + + var privileges = new List(); + + uint rightsCount; + var result = LsaEnumerateAccountRights(hPolicy, sidPtr, out rightsPtr, out rightsCount); + var win32ErrorCode = LsaNtStatusToWinError(result); + // the user has no privileges + if( win32ErrorCode == STATUS_OBJECT_NAME_NOT_FOUND ) + { + return new string[0]; + } + HandleLsaResult(result); + + var myLsaus = new LSA_UNICODE_STRING(); + for (ulong i = 0; i < rightsCount; i++) + { + var itemAddr = new IntPtr(rightsPtr.ToInt64() + (long) (i*(ulong) Marshal.SizeOf(myLsaus))); + myLsaus = (LSA_UNICODE_STRING) Marshal.PtrToStructure(itemAddr, myLsaus.GetType()); + var cvt = new char[myLsaus.Length/UnicodeEncoding.CharSize]; + Marshal.Copy(myLsaus.Buffer, cvt, 0, myLsaus.Length/UnicodeEncoding.CharSize); + var thisRight = new string(cvt); + privileges.Add(thisRight); + } + return privileges.ToArray(); + } + finally + { + Marshal.FreeHGlobal(sidPtr); + var result = LsaClose(hPolicy); + HandleLsaResult(result); + result = LsaFreeMemory(rightsPtr); + HandleLsaResult(result); + } + } + + public static void GrantPrivileges(string identity, string[] privileges) + { + var sidPtr = GetIdentitySid(identity); + var hPolicy = GetLsaPolicyHandle(); + + try + { + var lsaPrivileges = StringsToLsaStrings(privileges); + var result = LsaAddAccountRights(hPolicy, sidPtr, lsaPrivileges, (uint)lsaPrivileges.Length); + HandleLsaResult(result); + } + finally + { + Marshal.FreeHGlobal(sidPtr); + var result = LsaClose(hPolicy); + HandleLsaResult(result); + } + } + + const int STATUS_SUCCESS = 0x0; + const int STATUS_OBJECT_NAME_NOT_FOUND = 0x00000002; + const int STATUS_ACCESS_DENIED = 0x00000005; + const int STATUS_INVALID_HANDLE = 0x00000006; + const int STATUS_UNSUCCESSFUL = 0x0000001F; + const int STATUS_INVALID_PARAMETER = 0x00000057; + const int STATUS_NO_SUCH_PRIVILEGE = 0x00000521; + const int STATUS_INVALID_SERVER_STATE = 0x00000548; + const int STATUS_INTERNAL_DB_ERROR = 0x00000567; + const int STATUS_INSUFFICIENT_RESOURCES = 0x000005AA; + + private static Dictionary ErrorMessages = new Dictionary + { + {STATUS_OBJECT_NAME_NOT_FOUND, "Object name not found. An object in the LSA policy database was not found. The object may have been specified either by SID or by name, depending on its type."}, + {STATUS_ACCESS_DENIED, "Access denied. Caller does not have the appropriate access to complete the operation."}, + {STATUS_INVALID_HANDLE, "Invalid handle. Indicates an object or RPC handle is not valid in the context used."}, + {STATUS_UNSUCCESSFUL, "Unsuccessful. Generic failure, such as RPC connection failure."}, + {STATUS_INVALID_PARAMETER, "Invalid parameter. One of the parameters is not valid."}, + {STATUS_NO_SUCH_PRIVILEGE, "No such privilege. Indicates a specified privilege does not exist."}, + {STATUS_INVALID_SERVER_STATE, "Invalid server state. Indicates the LSA server is currently disabled."}, + {STATUS_INTERNAL_DB_ERROR, "Internal database error. The LSA database contains an internal inconsistency."}, + {STATUS_INSUFFICIENT_RESOURCES, "Insufficient resources. There are not enough system resources (such as memory to allocate buffers) to complete the call."} + }; + + private static void HandleLsaResult(uint returnCode) + { + var win32ErrorCode = LsaNtStatusToWinError(returnCode); + + if( win32ErrorCode == STATUS_SUCCESS) + return; + + if( ErrorMessages.ContainsKey(win32ErrorCode) ) + { + throw new Win32Exception(win32ErrorCode, ErrorMessages[win32ErrorCode]); + } + + throw new Win32Exception(win32ErrorCode); + } + + public static void RevokePrivileges(string identity, string[] privileges) + { + var sidPtr = GetIdentitySid(identity); + var hPolicy = GetLsaPolicyHandle(); + + try + { + var currentPrivileges = GetPrivileges(identity); + if (currentPrivileges.Length == 0) + { + return; + } + var lsaPrivileges = StringsToLsaStrings(privileges); + var result = LsaRemoveAccountRights(hPolicy, sidPtr, false, lsaPrivileges, (uint)lsaPrivileges.Length); + HandleLsaResult(result); + } + finally + { + Marshal.FreeHGlobal(sidPtr); + var result = LsaClose(hPolicy); + HandleLsaResult(result); + } + + } + + private static LSA_UNICODE_STRING[] StringsToLsaStrings(string[] privileges) + { + var lsaPrivileges = new LSA_UNICODE_STRING[privileges.Length]; + for (var idx = 0; idx < privileges.Length; ++idx) + { + lsaPrivileges[idx] = new LSA_UNICODE_STRING(privileges[idx]); + } + return lsaPrivileges; + } + } } "@ Add-Type -TypeDefinition $SourcePolicy -Language CSharp -$ServiceChangeErrors = @{} -$ServiceChangeErrors.Add(1, "Not Supported") -$ServiceChangeErrors.Add(2, "Access Denied") -$ServiceChangeErrors.Add(3, "Dependent Services Running") -$ServiceChangeErrors.Add(4, "Invalid Service Control") -$ServiceChangeErrors.Add(5, "Service Cannot Accept Control") -$ServiceChangeErrors.Add(6, "Service Not Active") -$ServiceChangeErrors.Add(7, "Service Request Timeout") -$ServiceChangeErrors.Add(8, "Unknown Failure") -$ServiceChangeErrors.Add(9, "Path Not Found") -$ServiceChangeErrors.Add(10, "Service Already Running") -$ServiceChangeErrors.Add(11, "Service Database Locked") -$ServiceChangeErrors.Add(12, "Service Dependency Deleted") -$ServiceChangeErrors.Add(13, "Service Dependency Failure") -$ServiceChangeErrors.Add(14, "Service Disabled") -$ServiceChangeErrors.Add(15, "Service Logon Failure") -$ServiceChangeErrors.Add(16, "Service Marked For Deletion") -$ServiceChangeErrors.Add(17, "Service No Thread") -$ServiceChangeErrors.Add(18, "Status Circular Dependency") -$ServiceChangeErrors.Add(19, "Status Duplicate Name") -$ServiceChangeErrors.Add(20, "Status Invalid Name") -$ServiceChangeErrors.Add(21, "Status Invalid Parameter") -$ServiceChangeErrors.Add(22, "Status Invalid Service Account") -$ServiceChangeErrors.Add(23, "Status Service Exists") -$ServiceChangeErrors.Add(24, "Service Already Paused") - - function SetAssignPrimaryTokenPrivilege($UserName) { - $privilege = "SeAssignPrimaryTokenPrivilege" - if (![PSCarbon.Lsa]::GetPrivileges($UserName).Contains($privilege)) - { - [PSCarbon.Lsa]::GrantPrivileges($UserName, $privilege) - } + $privilege = "SeAssignPrimaryTokenPrivilege" + if (![PSCarbon.Lsa]::GetPrivileges($UserName).Contains($privilege)) + { + [PSCarbon.Lsa]::GrantPrivileges($UserName, $privilege) + } } function SetUserLogonAsServiceRights($UserName) { - $privilege = "SeServiceLogonRight" - if (![PSCarbon.Lsa]::GetPrivileges($UserName).Contains($privilege)) - { - [PSCarbon.Lsa]::GrantPrivileges($UserName, $privilege) - } + $privilege = "SeServiceLogonRight" + if (![PSCarbon.Lsa]::GetPrivileges($UserName).Contains($privilege)) + { + [PSCarbon.Lsa]::GrantPrivileges($UserName, $privilege) + } } $Source = @" using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Net; using System.Text; -using System.Runtime.InteropServices; -using System.Security.Principal; -using System.ComponentModel; -namespace PSCloudbase +namespace Tarer { - public class ProcessManager - { - const int LOGON32_LOGON_SERVICE = 5; - const int LOGON32_PROVIDER_DEFAULT = 0; - const int TOKEN_ALL_ACCESS = 0x000f01ff; - const uint GENERIC_ALL_ACCESS = 0x10000000; - const uint INFINITE = 0xFFFFFFFF; - const uint PI_NOUI = 0x00000001; - const uint WAIT_FAILED = 0xFFFFFFFF; - - enum SECURITY_IMPERSONATION_LEVEL - { - SecurityAnonymous, - SecurityIdentification, - SecurityImpersonation, - SecurityDelegation - } - - enum TOKEN_TYPE - { - TokenPrimary = 1, - TokenImpersonation - } - - [StructLayout(LayoutKind.Sequential)] - struct SECURITY_ATTRIBUTES - { - public int nLength; - public IntPtr lpSecurityDescriptor; - public int bInheritHandle; - } - - [StructLayout(LayoutKind.Sequential)] - struct PROCESS_INFORMATION - { - public IntPtr hProcess; - public IntPtr hThread; - public int dwProcessId; - public int dwThreadId; - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - struct STARTUPINFO - { - public Int32 cb; - public string lpReserved; - public string lpDesktop; - public string lpTitle; - public Int32 dwX; - public Int32 dwY; - public Int32 dwXSize; - public Int32 dwYSize; - public Int32 dwXCountChars; - public Int32 dwYCountChars; - public Int32 dwFillAttribute; - public Int32 dwFlags; - public Int16 wShowWindow; - public Int16 cbReserved2; - public IntPtr lpReserved2; - public IntPtr hStdInput; - public IntPtr hStdOutput; - public IntPtr hStdError; - } - - [StructLayout(LayoutKind.Sequential)] - struct PROFILEINFO { - public int dwSize; - public uint dwFlags; - [MarshalAs(UnmanagedType.LPTStr)] - public String lpUserName; - [MarshalAs(UnmanagedType.LPTStr)] - public String lpProfilePath; - [MarshalAs(UnmanagedType.LPTStr)] - public String lpDefaultPath; - [MarshalAs(UnmanagedType.LPTStr)] - public String lpServerName; - [MarshalAs(UnmanagedType.LPTStr)] - public String lpPolicyPath; - public IntPtr hProfile; - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - public struct USER_INFO_4 - { - public string name; - public string password; - public int password_age; - public uint priv; - public string home_dir; - public string comment; - public uint flags; - public string script_path; - public uint auth_flags; - public string full_name; - public string usr_comment; - public string parms; - public string workstations; - public int last_logon; - public int last_logoff; - public int acct_expires; - public int max_storage; - public int units_per_week; - public IntPtr logon_hours; // This is a PBYTE - public int bad_pw_count; - public int num_logons; - public string logon_server; - public int country_code; - public int code_page; - public IntPtr user_sid; // This is a PSID - public int primary_group_id; - public string profile; - public string home_dir_drive; - public int password_expired; - } - - [DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)] - extern static bool DuplicateTokenEx( - IntPtr hExistingToken, - uint dwDesiredAccess, - ref SECURITY_ATTRIBUTES lpTokenAttributes, - SECURITY_IMPERSONATION_LEVEL ImpersonationLevel, - TOKEN_TYPE TokenType, - out IntPtr phNewToken); - - [DllImport("advapi32.dll", SetLastError=true)] - static extern bool LogonUser( - string lpszUsername, - string lpszDomain, - string lpszPassword, - int dwLogonType, - int dwLogonProvider, - out IntPtr phToken); - - [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Auto)] - static extern bool CreateProcessAsUser( - IntPtr hToken, - string lpApplicationName, - string lpCommandLine, - ref SECURITY_ATTRIBUTES lpProcessAttributes, - ref SECURITY_ATTRIBUTES lpThreadAttributes, - bool bInheritHandles, - uint dwCreationFlags, - IntPtr lpEnvironment, - string lpCurrentDirectory, - ref STARTUPINFO lpStartupInfo, - out PROCESS_INFORMATION lpProcessInformation); - - [DllImport("kernel32.dll", SetLastError=true)] - static extern UInt32 WaitForSingleObject(IntPtr hHandle, - UInt32 dwMilliseconds); - - [DllImport("Kernel32.dll")] - static extern int GetLastError(); - - [DllImport("Kernel32.dll")] - extern static int CloseHandle(IntPtr handle); - - [DllImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - static extern bool GetExitCodeProcess(IntPtr hProcess, - out uint lpExitCode); - - [DllImport("userenv.dll", SetLastError=true, CharSet=CharSet.Auto)] - [return: MarshalAs(UnmanagedType.Bool)] - static extern bool LoadUserProfile(IntPtr hToken, - ref PROFILEINFO lpProfileInfo); - - [DllImport("userenv.dll", SetLastError=true, CharSet=CharSet.Auto)] - [return: MarshalAs(UnmanagedType.Bool)] - static extern bool UnloadUserProfile(IntPtr hToken, IntPtr hProfile); - - [DllImport("Netapi32.dll", CharSet=CharSet.Unicode, ExactSpelling=true)] - extern static int NetUserGetInfo( - [MarshalAs(UnmanagedType.LPWStr)] string ServerName, - [MarshalAs(UnmanagedType.LPWStr)] string UserName, - int level, out IntPtr BufPtr); - - public static uint RunProcess(string userName, string password, - string domain, string cmd, - string arguments, - bool loadUserProfile = true) - { - bool retValue; - IntPtr phToken = IntPtr.Zero; - IntPtr phTokenDup = IntPtr.Zero; - PROCESS_INFORMATION pInfo = new PROCESS_INFORMATION(); - PROFILEINFO pi = new PROFILEINFO(); - - try - { - retValue = LogonUser(userName, domain, password, - LOGON32_LOGON_SERVICE, - LOGON32_PROVIDER_DEFAULT, - out phToken); - if(!retValue) - throw new Win32Exception(GetLastError()); - - var sa = new SECURITY_ATTRIBUTES(); - sa.nLength = Marshal.SizeOf(sa); - - retValue = DuplicateTokenEx( - phToken, GENERIC_ALL_ACCESS, ref sa, - SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, - TOKEN_TYPE.TokenPrimary, out phTokenDup); - if(!retValue) - throw new Win32Exception(GetLastError()); - - STARTUPINFO sInfo = new STARTUPINFO(); - sInfo.lpDesktop = ""; - - if(loadUserProfile) - { - IntPtr userInfoPtr = IntPtr.Zero; - int retValueNetUser = NetUserGetInfo(null, userName, 4, - out userInfoPtr); - if(retValueNetUser != 0) - throw new Win32Exception(retValueNetUser); - - USER_INFO_4 userInfo = (USER_INFO_4)Marshal.PtrToStructure( - userInfoPtr, typeof(USER_INFO_4)); - - pi.dwSize = Marshal.SizeOf(pi); - pi.dwFlags = PI_NOUI; - pi.lpUserName = userName; - pi.lpProfilePath = userInfo.profile; - - retValue = LoadUserProfile(phTokenDup, ref pi); - if(!retValue) - throw new Win32Exception(GetLastError()); - } - - retValue = CreateProcessAsUser(phTokenDup, cmd, arguments, - ref sa, ref sa, false, 0, - IntPtr.Zero, null, - ref sInfo, out pInfo); - if(!retValue) - throw new Win32Exception(GetLastError()); - - if(WaitForSingleObject(pInfo.hProcess, INFINITE) == WAIT_FAILED) - throw new Win32Exception(GetLastError()); - - uint exitCode; - retValue = GetExitCodeProcess(pInfo.hProcess, out exitCode); - if(!retValue) - throw new Win32Exception(GetLastError()); - - return exitCode; - } - finally - { - if(pi.hProfile != IntPtr.Zero) - UnloadUserProfile(phTokenDup, pi.hProfile); - if(phToken != IntPtr.Zero) - CloseHandle(phToken); - if(phTokenDup != IntPtr.Zero) - CloseHandle(phTokenDup); - if(pInfo.hProcess != IntPtr.Zero) - CloseHandle(pInfo.hProcess); - } - } - } + public enum EntryType : byte + { + File = 0, + FileObsolete = 0x30, + HardLink = 0x31, + SymLink = 0x32, + CharDevice = 0x33, + BlockDevice = 0x34, + Directory = 0x35, + Fifo = 0x36, + } + + public interface ITarHeader + { + string FileName { get; set; } + long SizeInBytes { get; set; } + DateTime LastModification { get; set; } + int HeaderSize { get; } + EntryType EntryType { get; set; } + } + + public class Tar + { + private byte[] dataBuffer = new byte[512]; + private UsTarHeader header; + private Stream inStream; + private long remainingBytesInFile; + + public Tar(Stream tarredData) { + inStream = tarredData; + header = new UsTarHeader(); + } + + public ITarHeader FileInfo + { + get { return header; } + } + + public void ReadToEnd(string destDirectory) + { + while (MoveNext()) + { + string fileNameFromArchive = FileInfo.FileName; + string totalPath = destDirectory + Path.DirectorySeparatorChar + fileNameFromArchive; + if(UsTarHeader.IsPathSeparator(fileNameFromArchive[fileNameFromArchive.Length -1]) || FileInfo.EntryType == EntryType.Directory) + { + Directory.CreateDirectory(totalPath); + continue; + } + string fileName = Path.GetFileName(totalPath); + string directory = totalPath.Remove(totalPath.Length - fileName.Length); + Directory.CreateDirectory(directory); + using (FileStream file = File.Create(totalPath)) + { + Read(file); + } + } + } + + public void Read(Stream dataDestination) + { + int readBytes; + byte[] read; + while ((readBytes = Read(out read)) != -1) + { + dataDestination.Write(read, 0, readBytes); + } + } + + protected int Read(out byte[] buffer) + { + if(remainingBytesInFile == 0) + { + buffer = null; + return -1; + } + int align512 = -1; + long toRead = remainingBytesInFile - 512; + + if (toRead > 0) + { + toRead = 512; + } + else + { + align512 = 512 - (int)remainingBytesInFile; + toRead = remainingBytesInFile; + } + + int bytesRead = 0; + long bytesRemainingToRead = toRead; + while (bytesRead < toRead && bytesRemainingToRead > 0) + { + bytesRead = inStream.Read(dataBuffer, (int)(toRead-bytesRemainingToRead), (int)bytesRemainingToRead); + bytesRemainingToRead -= bytesRead; + remainingBytesInFile -= bytesRead; + } + + if(inStream.CanSeek && align512 > 0) + { + inStream.Seek(align512, SeekOrigin.Current); + } + else + { + while(align512 > 0) + { + inStream.ReadByte(); + --align512; + } + } + + buffer = dataBuffer; + return bytesRead; + } + + private static bool IsEmpty(IEnumerable buffer) + { + foreach(byte b in buffer) + { + if (b != 0) + { + return false; + } + } + return true; + } + + public bool MoveNext() + { + byte[] bytes = header.GetBytes(); + int headerRead; + int bytesRemaining = header.HeaderSize; + while (bytesRemaining > 0) + { + headerRead = inStream.Read(bytes, header.HeaderSize - bytesRemaining, bytesRemaining); + bytesRemaining -= headerRead; + if (headerRead <= 0 && bytesRemaining > 0) + { + throw new Exception("Error reading tar header. Header size invalid"); + } + } + + if(IsEmpty(bytes)) + { + bytesRemaining = header.HeaderSize; + while (bytesRemaining > 0) + { + headerRead = inStream.Read(bytes, header.HeaderSize - bytesRemaining, bytesRemaining); + bytesRemaining -= headerRead; + if (headerRead <= 0 && bytesRemaining > 0) + { + throw new Exception("Broken archive"); + } + } + if (bytesRemaining == 0 && IsEmpty(bytes)) + { + return false; + } + throw new Exception("Error occured: expected end of archive"); + } + + if (!header.UpdateHeaderFromBytes()) + { + throw new Exception("Checksum check failed"); + } + + remainingBytesInFile = header.SizeInBytes; + return true; + } + } + + internal class TarHeader : ITarHeader + { + private byte[] buffer = new byte[512]; + private long headerChecksum; + + private string fileName; + protected DateTime dateTime1970 = new DateTime(1970, 1, 1, 0, 0, 0); + public EntryType EntryType { get; set; } + private static byte[] spaces = Encoding.ASCII.GetBytes(" "); + + public virtual string FileName + { + get { return fileName.Replace("\0",string.Empty); } + set { fileName = value; } + } + + public long SizeInBytes { get; set; } + + public string SizeString { get { return Convert.ToString(SizeInBytes, 8).PadLeft(11, '0'); } } + + public DateTime LastModification { get; set; } + + public virtual int HeaderSize { get { return 512; } } + + public byte[] GetBytes() + { + return buffer; + } + + public virtual bool UpdateHeaderFromBytes() + { + FileName = Encoding.UTF8.GetString(buffer, 0, 100); + + EntryType = (EntryType)buffer[156]; + + if((buffer[124] & 0x80) == 0x80) // if size in binary + { + long sizeBigEndian = BitConverter.ToInt64(buffer,0x80); + SizeInBytes = IPAddress.NetworkToHostOrder(sizeBigEndian); + } + else + { + SizeInBytes = Convert.ToInt64(Encoding.ASCII.GetString(buffer, 124, 11).Trim(), 8); + } + long unixTimeStamp = Convert.ToInt64(Encoding.ASCII.GetString(buffer,136,11).Trim(),8); + LastModification = dateTime1970.AddSeconds(unixTimeStamp); + + var storedChecksum = Convert.ToInt64(Encoding.ASCII.GetString(buffer,148,6).Trim(), 8); + RecalculateChecksum(buffer); + if (storedChecksum == headerChecksum) + { + return true; + } + + RecalculateAltChecksum(buffer); + return storedChecksum == headerChecksum; + } + + private void RecalculateAltChecksum(byte[] buf) + { + spaces.CopyTo(buf, 148); + headerChecksum = 0; + foreach(byte b in buf) + { + if((b & 0x80) == 0x80) + { + headerChecksum -= b ^ 0x80; + } + else + { + headerChecksum += b; + } + } + } + + protected virtual void RecalculateChecksum(byte[] buf) + { + // Set default value for checksum. That is 8 spaces. + spaces.CopyTo(buf, 148); + // Calculate checksum + headerChecksum = 0; + foreach (byte b in buf) + { + headerChecksum += b; + } + } + } + internal class UsTarHeader : TarHeader + { + private const string magic = "ustar"; + private const string version = " "; + + private string namePrefix = string.Empty; + + public override string FileName + { + get { return namePrefix.Replace("\0", string.Empty) + base.FileName.Replace("\0", string.Empty); } + set + { + if (value.Length > 255) + { + throw new Exception("UsTar fileName can not be longer than 255 chars"); + } + if (value.Length > 100) + { + int position = value.Length - 100; + while (!IsPathSeparator(value[position])) + { + ++position; + if (position == value.Length) + { + break; + } + } + if (position == value.Length) + { + position = value.Length - 100; + } + namePrefix = value.Substring(0, position); + base.FileName = value.Substring(position, value.Length - position); + } + else + { + base.FileName = value; + } + } + } + + public override bool UpdateHeaderFromBytes() + { + byte[] bytes = GetBytes(); + namePrefix = Encoding.UTF8.GetString(bytes, 347, 157); + return base.UpdateHeaderFromBytes(); + } + + internal static bool IsPathSeparator(char ch) + { + return (ch == '\\' || ch == '/' || ch == '|'); + } + } } "@ Add-Type -TypeDefinition $Source -Language CSharp -function Start-ProcessAsUser -{ - [CmdletBinding()] - param - ( - [parameter(Mandatory=$true, ValueFromPipeline=$true)] - [string]$Command, - - [parameter()] - [string]$Arguments, - - [parameter(Mandatory=$true)] - [PSCredential]$Credential, - - [parameter()] - [bool]$LoadUserProfile = $true - ) - process - { - $nc = $Credential.GetNetworkCredential() - - $domain = "." - if($nc.Domain) - { - $domain = $nc.Domain - } - - [PSCloudbase.ProcessManager]::RunProcess($nc.UserName, $nc.Password, - $domain, $Command, - $Arguments, $LoadUserProfile) - } -} - -$powershell = "$ENV:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -$cmdExe = "$ENV:SystemRoot\System32\cmd.exe" +Function GUnZip-File{ + Param( + $infile, + $outdir + ) + + $input = New-Object System.IO.FileStream $inFile, ([IO.FileMode]::Open), ([IO.FileAccess]::Read), ([IO.FileShare]::Read) + $tempFile = "$env:TEMP\jujud.tar" + $tempOut = New-Object System.IO.FileStream $tempFile, ([IO.FileMode]::Create), ([IO.FileAccess]::Write), ([IO.FileShare]::None) + $gzipStream = New-Object System.IO.Compression.GzipStream $input, ([IO.Compression.CompressionMode]::Decompress) + + $buffer = New-Object byte[](1024) + while($true){ + $read = $gzipstream.Read($buffer, 0, 1024) + if ($read -le 0){break} + $tempOut.Write($buffer, 0, $read) + } + $gzipStream.Close() + $tempOut.Close() + $input.Close() + + $in = New-Object System.IO.FileStream $tempFile, ([IO.FileMode]::Open), ([IO.FileAccess]::Read), ([IO.FileShare]::Read) + $tar = New-Object Tarer.Tar($in) + $tar.ReadToEnd($outdir) + $in.Close() + rm $tempFile +} + +Function Get-FileSHA256{ + Param( + $FilePath + ) + $hash = [Security.Cryptography.HashAlgorithm]::Create( "SHA256" ) + $stream = ([IO.StreamReader]$FilePath).BaseStream + $res = -join ($hash.ComputeHash($stream) | ForEach { "{0:x2}" -f $_ }) + $stream.Close() + return $res +} $juju_passwd = Get-RandomPassword 20 $juju_passwd += "^" @@ -797,23 +837,74 @@ SetUserLogonAsServiceRights $juju_user SetAssignPrimaryTokenPrivilege $juju_user -New-ItemProperty "HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList" -Name "jujud" -Value 0 -PropertyType "DWord" +$path = "HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList" +if(!(Test-Path $path)){ + New-Item -Path $path -force +} +New-ItemProperty $path -Name "jujud" -Value 0 -PropertyType "DWord" $secpasswd = ConvertTo-SecureString $juju_passwd -AsPlainText -Force $jujuCreds = New-Object System.Management.Automation.PSCredential ($juju_user, $secpasswd) ` -var winSetPasswdScript = ` - -Set-Content "C:\juju\bin\save_pass.ps1" @" -Param ( - [Parameter(Mandatory=` + "`$true" + `)] - [string]` + "`$pass" + ` -) - -` + "`$secpasswd" + ` = ConvertTo-SecureString ` + "`$pass" + ` -AsPlainText -Force -` + "`$secpasswd" + ` | convertfrom-securestring | Add-Content C:\Juju\Jujud.pass +var UserdataScript = `#ps1_sysnative +$userdata=@" +%s "@ +Function Decode-Base64 { + Param( + $inFile, + $outFile + ) + $bufferSize = 9000 # should be a multiplier of 4 + $buffer = New-Object char[] $bufferSize + + $reader = [System.IO.File]::OpenText($inFile) + $writer = [System.IO.File]::OpenWrite($outFile) + + $bytesRead = 0 + do + { + $bytesRead = $reader.Read($buffer, 0, $bufferSize); + $bytes = [Convert]::FromBase64CharArray($buffer, 0, $bytesRead); + $writer.Write($bytes, 0, $bytes.Length); + } while ($bytesRead -eq $bufferSize); + + $reader.Dispose() + $writer.Dispose() +} + +Function GUnZip-File { + Param( + $inFile, + $outFile + ) + $in = New-Object System.IO.FileStream $inFile, ([IO.FileMode]::Open), ([IO.FileAccess]::Read), ([IO.FileShare]::Read) + $out = New-Object System.IO.FileStream $outFile, ([IO.FileMode]::Create), ([IO.FileAccess]::Write), ([IO.FileShare]::None) + $gzipStream = New-Object System.IO.Compression.GZipStream $in, ([IO.Compression.CompressionMode]::Decompress) + $buffer = New-Object byte[](1024) + while($true){ + $read = $gzipstream.Read($buffer, 0, 1024) + if ($read -le 0){break} + $out.Write($buffer, 0, $read) + } + $gzipStream.Close() + $out.Close() + $in.Close() +} + +$b64File = "$env:TEMP\juju\udata.b64" +$gzFile = "$env:TEMP\juju\udata.gz" +$udataScript = "$env:TEMP\juju\udata.ps1" +mkdir "$env:TEMP\juju" + +Set-Content $b64File $userdata +Decode-Base64 -inFile $b64File -outFile $gzFile +GUnZip-File -inFile $gzFile -outFile $udataScript + +& $udataScript + +rm -Recurse "$env:TEMP\juju" ` === modified file 'src/github.com/juju/juju/cloudconfig/providerinit/providerinit.go' --- src/github.com/juju/juju/cloudconfig/providerinit/providerinit.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cloudconfig/providerinit/providerinit.go 2015-10-23 18:29:32 +0000 @@ -7,12 +7,14 @@ package providerinit import ( + "github.com/juju/errors" "github.com/juju/loggo" - "github.com/juju/utils" "github.com/juju/juju/cloudconfig" "github.com/juju/juju/cloudconfig/cloudinit" "github.com/juju/juju/cloudconfig/instancecfg" + "github.com/juju/juju/cloudconfig/providerinit/renderers" + "github.com/juju/juju/version" ) var logger = loggo.GetLogger("juju.cloudconfig.providerinit") @@ -40,25 +42,37 @@ // ComposeUserData fills out the provided cloudinit configuration structure // so it is suitable for initialising a machine with the given configuration, -// and then renders it and returns it as a binary (gzipped) blob of user data. +// and then renders it and encodes it using the supplied renderer. +// When calling ComposeUserData a encoding implementation must be chosen from +// the providerinit/encoders package according to the need of the provider. // // If the provided cloudcfg is nil, a new one will be created internally. -func ComposeUserData(icfg *instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig) ([]byte, error) { +func ComposeUserData(icfg *instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig, renderer renderers.ProviderRenderer) ([]byte, error) { if cloudcfg == nil { var err error cloudcfg, err = cloudinit.New(icfg.Series) if err != nil { - return nil, err + return nil, errors.Trace(err) } } _, err := configureCloudinit(icfg, cloudcfg) if err != nil { - return nil, err - } - data, err := cloudcfg.RenderYAML() - logger.Tracef("Generated cloud init:\n%s", string(data)) - if err != nil { - return nil, err - } - return utils.Gzip(data), nil + return nil, errors.Trace(err) + } + operatingSystem, err := version.GetOSFromSeries(icfg.Series) + if err != nil { + return nil, errors.Trace(err) + } + // This might get replaced by a renderer.RenderUserdata which will either + // render it as YAML or Bash since some CentOS images might ship without cloudnit + udata, err := cloudcfg.RenderYAML() + if err != nil { + return nil, errors.Trace(err) + } + udata, err = renderer.EncodeUserdata(udata, operatingSystem) + if err != nil { + return nil, errors.Trace(err) + } + logger.Tracef("Generated cloud init:\n%s", string(udata)) + return udata, err } === modified file 'src/github.com/juju/juju/cloudconfig/providerinit/providerinit_test.go' --- src/github.com/juju/juju/cloudconfig/providerinit/providerinit_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cloudconfig/providerinit/providerinit_test.go 2015-10-23 18:29:32 +0000 @@ -5,6 +5,8 @@ package providerinit_test import ( + "encoding/base64" + "fmt" "path" "time" @@ -18,6 +20,7 @@ "github.com/juju/juju/api" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/cert" + "github.com/juju/juju/cloudconfig" "github.com/juju/juju/cloudconfig/cloudinit" "github.com/juju/juju/cloudconfig/instancecfg" "github.com/juju/juju/cloudconfig/providerinit" @@ -26,6 +29,7 @@ "github.com/juju/juju/juju/paths" "github.com/juju/juju/mongo" "github.com/juju/juju/provider/dummy" + "github.com/juju/juju/provider/openstack" "github.com/juju/juju/state/multiwatcher" "github.com/juju/juju/testing" "github.com/juju/juju/tools" @@ -249,7 +253,7 @@ c.Assert(err, jc.ErrorIsNil) cloudcfg.AddRunCmd(script1) cloudcfg.AddRunCmd(script2) - result, err := providerinit.ComposeUserData(cfg, cloudcfg) + result, err := providerinit.ComposeUserData(cfg, cloudcfg, &openstack.OpenstackRenderer{}) c.Assert(err, jc.ErrorIsNil) unzipped, err := utils.Gunzip(result) @@ -295,3 +299,65 @@ c.Check(len(runCmd) > 2, jc.IsTrue) } } + +func (s *CloudInitSuite) TestWindowsUserdataEncoding(c *gc.C) { + series := "win8" + tools := &tools.Tools{ + URL: "http://foo.com/tools/released/juju1.2.3-win8-amd64.tgz", + Version: version.MustParseBinary("1.2.3-win8-amd64"), + Size: 10, + SHA256: "1234", + } + dataDir, err := paths.DataDir(series) + c.Assert(err, jc.ErrorIsNil) + logDir, err := paths.LogDir(series) + c.Assert(err, jc.ErrorIsNil) + + cfg := instancecfg.InstanceConfig{ + MachineId: "10", + AgentEnvironment: map[string]string{agent.ProviderType: "dummy"}, + Tools: tools, + Series: series, + Bootstrap: false, + Jobs: []multiwatcher.MachineJob{multiwatcher.JobHostUnits}, + MachineNonce: "FAKE_NONCE", + MongoInfo: &mongo.MongoInfo{ + Tag: names.NewMachineTag("10"), + Password: "arble", + Info: mongo.Info{ + CACert: "CA CERT\n" + testing.CACert, + Addrs: []string{"state-addr.testing.invalid:12345"}, + }, + }, + APIInfo: &api.Info{ + Addrs: []string{"state-addr.testing.invalid:54321"}, + Password: "bletch", + CACert: "CA CERT\n" + testing.CACert, + Tag: names.NewMachineTag("10"), + EnvironTag: testing.EnvironmentTag, + }, + MachineAgentServiceName: "jujud-machine-10", + DataDir: dataDir, + LogDir: path.Join(logDir, "juju"), + CloudInitOutputLog: path.Join(logDir, "cloud-init-output.log"), + } + + ci, err := cloudinit.New("win8") + c.Assert(err, jc.ErrorIsNil) + + udata, err := cloudconfig.NewUserdataConfig(&cfg, ci) + c.Assert(err, jc.ErrorIsNil) + err = udata.Configure() + c.Assert(err, jc.ErrorIsNil) + data, err := ci.RenderYAML() + c.Assert(err, jc.ErrorIsNil) + base64Data := base64.StdEncoding.EncodeToString(utils.Gzip(data)) + got := []byte(fmt.Sprintf(cloudconfig.UserdataScript, base64Data)) + + cicompose, err := cloudinit.New("win8") + c.Assert(err, jc.ErrorIsNil) + expected, err := providerinit.ComposeUserData(&cfg, cicompose, openstack.OpenstackRenderer{}) + c.Assert(err, jc.ErrorIsNil) + + c.Assert(string(expected), gc.Equals, string(got)) +} === added directory 'src/github.com/juju/juju/cloudconfig/providerinit/renderers' === added file 'src/github.com/juju/juju/cloudconfig/providerinit/renderers/common.go' --- src/github.com/juju/juju/cloudconfig/providerinit/renderers/common.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cloudconfig/providerinit/renderers/common.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,34 @@ +// Copyright 2015 Canonical Ltd. +// Copyright 2015 Cloudbase Solutions SRL +// Licensed under the AGPLv3, see LICENCE file for details. + +package renderers + +import ( + "encoding/base64" + "fmt" + + "github.com/juju/juju/cloudconfig" + "github.com/juju/utils" +) + +// ToBase64 just transforms whatever userdata it gets to base64 format +func ToBase64(data []byte) []byte { + buf := make([]byte, base64.StdEncoding.EncodedLen(len(data))) + base64.StdEncoding.Encode(buf, data) + return buf +} + +// WinEmbedInScript for now is used on windows and it returns a powershell script +// which has the userdata embedded as base64(gzip(userdata)) +func WinEmbedInScript(udata []byte) []byte { + encUserdata := ToBase64(utils.Gzip(udata)) + return []byte(fmt.Sprintf(cloudconfig.UserdataScript, encUserdata)) +} + +// AddPowershellTags adds ... to it's input +func AddPowershellTags(udata []byte) []byte { + return []byte(`` + + string(udata) + + ``) +} === added file 'src/github.com/juju/juju/cloudconfig/providerinit/renderers/common_test.go' --- src/github.com/juju/juju/cloudconfig/providerinit/renderers/common_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cloudconfig/providerinit/renderers/common_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,45 @@ +// Copyright 2015 Canonical Ltd. +// Copyright 2015 Cloudbase Solutions SRL +// Licensed under the AGPLv3, see LICENCE file for details. + +package renderers_test + +import ( + "encoding/base64" + "fmt" + + jc "github.com/juju/testing/checkers" + "github.com/juju/utils" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cloudconfig" + "github.com/juju/juju/cloudconfig/providerinit/renderers" + "github.com/juju/juju/testing" +) + +type RenderersSuite struct { + testing.BaseSuite +} + +var _ = gc.Suite(&RenderersSuite{}) + +func (s *RenderersSuite) TestToBase64(c *gc.C) { + in := []byte("test") + expected := base64.StdEncoding.EncodeToString(in) + out := renderers.ToBase64(in) + c.Assert(string(out), gc.Equals, expected) +} + +func (s *RenderersSuite) TestWinEmbedInScript(c *gc.C) { + in := []byte("test") + expected := []byte(fmt.Sprintf(cloudconfig.UserdataScript, renderers.ToBase64(utils.Gzip(in)))) + out := renderers.WinEmbedInScript(in) + c.Assert(out, jc.DeepEquals, expected) +} + +func (s *RenderersSuite) TestAddPowershellTags(c *gc.C) { + in := []byte("test") + expected := []byte(`` + string(in) + ``) + out := renderers.AddPowershellTags(in) + c.Assert(out, jc.DeepEquals, expected) +} === added file 'src/github.com/juju/juju/cloudconfig/providerinit/renderers/interface.go' --- src/github.com/juju/juju/cloudconfig/providerinit/renderers/interface.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cloudconfig/providerinit/renderers/interface.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,25 @@ +// Copyright 2015 Canonical Ltd. +// Copyright 2015 Cloudbase Solutions SRL +// Licensed under the AGPLv3, see LICENCE file for details. + +// The renderers package implements a way to encode the userdata +// depending on the OS and the provider. +// It currently holds an interface and common functions, while +// the implementations live in the particular providers. +package renderers + +import ( + "github.com/juju/juju/version" +) + +// ProviderRenderer defines a method to encode userdata depending on +// the OS and the provider. +// In the future this might support another method for rendering +// the userdata differently(bash vs yaml) since some providers might +// not ship cloudinit on every OS +type ProviderRenderer interface { + + // EncodeUserdata takes a []byte and encodes it in the right format. + // The implementations are based on the different providers and OSTypes. + EncodeUserdata([]byte, version.OSType) ([]byte, error) +} === added file 'src/github.com/juju/juju/cloudconfig/providerinit/renderers/package_test.go' --- src/github.com/juju/juju/cloudconfig/providerinit/renderers/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cloudconfig/providerinit/renderers/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,15 @@ +// Copyright 2015 Canonical Ltd. +// Copyright 2015 Cloudbase Solutions SRL +// Licensed under the AGPLv3, see LICENCE file for details. + +package renderers_test + +import ( + "testing" + + gc "gopkg.in/check.v1" +) + +func Test(t *testing.T) { + gc.TestingT(t) +} === modified file 'src/github.com/juju/juju/cloudconfig/sshinit/configure.go' --- src/github.com/juju/juju/cloudconfig/sshinit/configure.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cloudconfig/sshinit/configure.go 2015-10-23 18:29:32 +0000 @@ -54,10 +54,6 @@ // to have been returned by cloudinit ConfigureScript. func RunConfigureScript(script string, params ConfigureParams) error { logger.Tracef("Running script on %s: %s", params.Host, script) - client := params.Client - if client == nil { - client = ssh.DefaultClient - } cmd := ssh.Command(params.Host, []string{"sudo", "/bin/bash"}, nil) cmd.Stdin = strings.NewReader(script) cmd.Stderr = params.ProgressWriter === removed directory 'src/github.com/juju/juju/cloudconfig/testing' === removed file 'src/github.com/juju/juju/cloudconfig/testing/cloudinit.go' --- src/github.com/juju/juju/cloudconfig/testing/cloudinit.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cloudconfig/testing/cloudinit.go 1970-01-01 00:00:00 +0000 @@ -1,17 +0,0 @@ -// Copyright 2013, 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package testing - -import ( - "github.com/juju/juju/cloudconfig/instancecfg" -) - -// PatchDataDir temporarily overrides environs.DataDir for testing purposes. -// It returns a cleanup function that you must call later to restore the -// original value. -func PatchDataDir(path string) func() { - originalDataDir := instancecfg.DataDir - instancecfg.DataDir = path - return func() { instancecfg.DataDir = originalDataDir } -} === modified file 'src/github.com/juju/juju/cloudconfig/userdatacfg_test.go' --- src/github.com/juju/juju/cloudconfig/userdatacfg_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cloudconfig/userdatacfg_test.go 2015-10-23 18:29:32 +0000 @@ -6,6 +6,7 @@ import ( "encoding/base64" + "fmt" "path" "regexp" "strings" @@ -53,13 +54,20 @@ normalMachineJobs = []multiwatcher.MachineJob{ multiwatcher.JobHostUnits, } - - jujuLogDir = path.Join(logDir, "juju") - logDir = must(paths.LogDir("precise")) - dataDir = must(paths.DataDir("precise")) - cloudInitOutputLog = path.Join(logDir, "cloud-init-output.log") ) +func jujuLogDir(series string) string { + return path.Join(must(paths.LogDir(series)), "juju") +} + +func jujuDataDir(series string) string { + return must(paths.DataDir(series)) +} + +func cloudInitOutputLog(logDir string) string { + return path.Join(logDir, "cloud-init-output.log") +} + // TODO: add this to the utils package func must(s string, err error) string { if err != nil { @@ -68,8 +76,141 @@ return s } +var stateServingInfo = ¶ms.StateServingInfo{ + Cert: string(serverCert), + PrivateKey: string(serverKey), + CAPrivateKey: "ca-private-key", + StatePort: 37017, + APIPort: 17070, +} + +// testcfg wraps InstanceConfig and provides helpers to modify it as +// needed for specific test cases before using it. Most methods return +// the method receiver (cfg) after (possibly) modifying it to allow +// chaining calls. +type testInstanceConfig instancecfg.InstanceConfig + +// makeTestConfig returns a minimal instance config for a non state +// server machine (unless bootstrap is true) for the given series. +func makeTestConfig(series string, bootstrap bool) *testInstanceConfig { + const defaultMachineID = "99" + + cfg := new(testInstanceConfig) + cfg.AuthorizedKeys = "sshkey1" + cfg.AgentEnvironment = map[string]string{ + agent.ProviderType: "dummy", + } + cfg.MachineNonce = "FAKE_NONCE" + cfg.InstanceId = "i-machine" + cfg.Jobs = normalMachineJobs + // MongoInfo and APIInfo (sans Tag) must be initialized before + // calling setMachineID(). + cfg.MongoInfo = &mongo.MongoInfo{ + Password: "arble", + Info: mongo.Info{ + Addrs: []string{"state-addr.testing.invalid:12345"}, + CACert: "CA CERT\n" + testing.CACert, + }, + } + cfg.APIInfo = &api.Info{ + Addrs: []string{"state-addr.testing.invalid:54321"}, + Password: "bletch", + CACert: "CA CERT\n" + testing.CACert, + EnvironTag: testing.EnvironmentTag, + } + cfg.setMachineID(defaultMachineID) + cfg.setSeries(series) + if bootstrap { + return cfg.setStateServer() + } + return cfg +} + +// makeBootstrapConfig is a shortcut to call makeTestConfig(series, true). +func makeBootstrapConfig(series string) *testInstanceConfig { + return makeTestConfig(series, true) +} + +// makeNormalConfig is a shortcut to call makeTestConfig(series, +// false). +func makeNormalConfig(series string) *testInstanceConfig { + return makeTestConfig(series, false) +} + +// setMachineID updates MachineId, MachineAgentServiceName, +// MongoInfo.Tag, and APIInfo.Tag to match the given machine ID. If +// MongoInfo or APIInfo are nil, they're not changed. +func (cfg *testInstanceConfig) setMachineID(id string) *testInstanceConfig { + cfg.MachineId = id + cfg.MachineAgentServiceName = fmt.Sprintf("jujud-%s", names.NewMachineTag(id).String()) + if cfg.MongoInfo != nil { + cfg.MongoInfo.Tag = names.NewMachineTag(id) + } + if cfg.APIInfo != nil { + cfg.APIInfo.Tag = names.NewMachineTag(id) + } + return cfg +} + +// maybeSetEnvironConfig sets the Config field to the given envConfig, if not +// nil. +func (cfg *testInstanceConfig) maybeSetEnvironConfig(envConfig *config.Config) *testInstanceConfig { + if envConfig != nil { + cfg.Config = envConfig + } + return cfg +} + +// setEnableOSUpdateAndUpgrade sets EnableOSRefreshUpdate and EnableOSUpgrade +// fields to the given values. +func (cfg *testInstanceConfig) setEnableOSUpdateAndUpgrade(updateEnabled, upgradeEnabled bool) *testInstanceConfig { + cfg.EnableOSRefreshUpdate = updateEnabled + cfg.EnableOSUpgrade = upgradeEnabled + return cfg +} + +// setSeries sets the series-specific fields (Tools, Series, DataDir, +// LogDir, and CloudInitOutputLog) to match the given series. +func (cfg *testInstanceConfig) setSeries(series string) *testInstanceConfig { + cfg.Tools = newSimpleTools(fmt.Sprintf("1.2.3-%s-amd64", series)) + cfg.Series = series + cfg.DataDir = jujuDataDir(series) + cfg.LogDir = jujuLogDir(series) + cfg.CloudInitOutputLog = cloudInitOutputLog(series) + return cfg +} + +// setStateServer updates the config to be suitable for bootstrapping +// a state server instance. +func (cfg *testInstanceConfig) setStateServer() *testInstanceConfig { + cfg.setMachineID("0") + cfg.Constraints = envConstraints + cfg.Bootstrap = true + cfg.StateServingInfo = stateServingInfo + cfg.Jobs = allMachineJobs + cfg.InstanceId = "i-bootstrap" + cfg.MongoInfo.Tag = nil + cfg.APIInfo.Tag = nil + return cfg.setEnableOSUpdateAndUpgrade(true, false) +} + +// mutate calls mutator passing cfg to it, and returns the (possibly) +// modified cfg. +func (cfg *testInstanceConfig) mutate(mutator func(*testInstanceConfig)) *testInstanceConfig { + if mutator == nil { + panic("mutator is nil!") + } + mutator(cfg) + return cfg +} + +// render returns the config as InstanceConfig. +func (cfg *testInstanceConfig) render() instancecfg.InstanceConfig { + return instancecfg.InstanceConfig(*cfg) +} + type cloudinitTest struct { - cfg instancecfg.InstanceConfig + cfg *testInstanceConfig setEnvConfig bool expectScripts string // inexactMatch signifies whether we allow extra lines @@ -80,140 +221,66 @@ inexactMatch bool } -func minimalInstanceConfig(tweakers ...func(instancecfg.InstanceConfig)) instancecfg.InstanceConfig { - - baseConfig := instancecfg.InstanceConfig{ - MachineId: "0", - AuthorizedKeys: "sshkey1", - AgentEnvironment: map[string]string{agent.ProviderType: "dummy"}, - // raring provides mongo in the archive - Tools: newSimpleTools("1.2.3-raring-amd64"), - Series: "raring", - Bootstrap: true, - StateServingInfo: stateServingInfo, - MachineNonce: "FAKE_NONCE", - MongoInfo: &mongo.MongoInfo{ - Password: "arble", - Info: mongo.Info{ - CACert: "CA CERT\n" + testing.CACert, - }, - }, - APIInfo: &api.Info{ - Password: "bletch", - CACert: "CA CERT\n" + testing.CACert, - EnvironTag: testing.EnvironmentTag, - }, - Constraints: envConstraints, - DataDir: dataDir, - LogDir: logDir, - Jobs: allMachineJobs, - CloudInitOutputLog: cloudInitOutputLog, - InstanceId: "i-bootstrap", - MachineAgentServiceName: "jujud-machine-0", - EnableOSRefreshUpdate: false, - EnableOSUpgrade: false, - } - - for _, tweaker := range tweakers { - tweaker(baseConfig) - } - - return baseConfig -} - -func minimalConfig(c *gc.C) *config.Config { +func minimalEnvironConfig(c *gc.C) *config.Config { cfg, err := config.New(config.NoDefaults, testing.FakeConfig()) c.Assert(err, jc.ErrorIsNil) c.Assert(cfg, gc.NotNil) return cfg } -var stateServingInfo = ¶ms.StateServingInfo{ - Cert: string(serverCert), - PrivateKey: string(serverKey), - CAPrivateKey: "ca-private-key", - StatePort: 37017, - APIPort: 17070, -} - // Each test gives a cloudinit config - we check the // output to see if it looks correct. var cloudinitTests = []cloudinitTest{ // Test that cloudinit respects update/upgrade settings. { - cfg: minimalInstanceConfig(func(mc instancecfg.InstanceConfig) { - mc.EnableOSRefreshUpdate = false - mc.EnableOSUpgrade = false - }), - inexactMatch: true, - // We're just checking for apt-flags. We don't much care if - // the script matches. - expectScripts: "", - setEnvConfig: true, - }, - // Test that cloudinit respects update/upgrade settings. - { - cfg: minimalInstanceConfig(func(mc instancecfg.InstanceConfig) { - mc.EnableOSRefreshUpdate = true - mc.EnableOSUpgrade = false - }), - inexactMatch: true, - // We're just checking for apt-flags. We don't much care if - // the script matches. - expectScripts: "", - setEnvConfig: true, - }, - // Test that cloudinit respects update/upgrade settings. - { - cfg: minimalInstanceConfig(func(mc instancecfg.InstanceConfig) { - mc.EnableOSRefreshUpdate = false - mc.EnableOSUpgrade = true - }), - inexactMatch: true, - // We're just checking for apt-flags. We don't much care if - // the script matches. - expectScripts: "", - setEnvConfig: true, - }, - { - // precise state server - cfg: instancecfg.InstanceConfig{ - MachineId: "0", - AuthorizedKeys: "sshkey1", - AgentEnvironment: map[string]string{agent.ProviderType: "dummy"}, - // precise currently needs mongo from PPA - Tools: newSimpleTools("1.2.3-precise-amd64"), - Series: "precise", - Bootstrap: true, - StateServingInfo: stateServingInfo, - MachineNonce: "FAKE_NONCE", - MongoInfo: &mongo.MongoInfo{ - Password: "arble", - Info: mongo.Info{ - CACert: "CA CERT\n" + testing.CACert, - }, - }, - APIInfo: &api.Info{ - Password: "bletch", - CACert: "CA CERT\n" + testing.CACert, - EnvironTag: testing.EnvironmentTag, - }, - Constraints: envConstraints, - DataDir: dataDir, - LogDir: jujuLogDir, - Jobs: allMachineJobs, - CloudInitOutputLog: cloudInitOutputLog, - InstanceId: "i-bootstrap", - MachineAgentServiceName: "jujud-machine-0", - EnableOSRefreshUpdate: true, - }, + cfg: makeBootstrapConfig("quantal").setEnableOSUpdateAndUpgrade(false, false), + inexactMatch: true, + // We're just checking for apt-flags. We don't much care if + // the script matches. + expectScripts: "", + setEnvConfig: true, + }, + + // Test that cloudinit respects update/upgrade settings. + { + cfg: makeBootstrapConfig("quantal").setEnableOSUpdateAndUpgrade(true, false), + inexactMatch: true, + // We're just checking for apt-flags. We don't much care if + // the script matches. + expectScripts: "", + setEnvConfig: true, + }, + + // Test that cloudinit respects update/upgrade settings. + { + cfg: makeBootstrapConfig("quantal").setEnableOSUpdateAndUpgrade(false, true), + inexactMatch: true, + // We're just checking for apt-flags. We don't much care if + // the script matches. + expectScripts: "", + setEnvConfig: true, + }, + + // Test that cloudinit respects update/upgrade settings. + { + cfg: makeBootstrapConfig("quantal").setEnableOSUpdateAndUpgrade(true, true), + inexactMatch: true, + // We're just checking for apt-flags. We don't much care if + // the script matches. + expectScripts: "", + setEnvConfig: true, + }, + + // precise state server + { + cfg: makeBootstrapConfig("precise"), setEnvConfig: true, expectScripts: ` install -D -m 644 /dev/null '/etc/apt/preferences\.d/50-cloud-tools' printf '%s\\n' '.*' > '/etc/apt/preferences\.d/50-cloud-tools' set -xe -install -D -m 644 /dev/null '/etc/init/juju-clean-shutdown.conf' -printf '%s\\n' '\\nauthor "Juju Team "\\ndescription "Stop all network interfaces on shutdown"\\nstart on runlevel \[016\]\\ntask\\nconsole output\\n\\nexec /sbin/ifdown -a -v --force\\n' > '/etc/init/juju-clean-shutdown.conf' +install -D -m 644 /dev/null '/etc/init/juju-clean-shutdown\.conf' +printf '%s\\n' '.*"Stop all network interfaces.*' > '/etc/init/juju-clean-shutdown\.conf' install -D -m 644 /dev/null '/var/lib/juju/nonce.txt' printf '%s\\n' 'FAKE_NONCE' > '/var/lib/juju/nonce.txt' test -e /proc/self/fd/9 \|\| exec 9>&2 @@ -241,38 +308,11 @@ start jujud-machine-0 rm \$bin/tools\.tar\.gz && rm \$bin/juju1\.2\.3-precise-amd64\.sha256 `, - }, { - // raring state server - we just test the raring-specific parts of the output. - cfg: instancecfg.InstanceConfig{ - MachineId: "0", - AuthorizedKeys: "sshkey1", - AgentEnvironment: map[string]string{agent.ProviderType: "dummy"}, - // raring provides mongo in the archive - Tools: newSimpleTools("1.2.3-raring-amd64"), - Series: "raring", - Bootstrap: true, - StateServingInfo: stateServingInfo, - MachineNonce: "FAKE_NONCE", - MongoInfo: &mongo.MongoInfo{ - Password: "arble", - Info: mongo.Info{ - CACert: "CA CERT\n" + testing.CACert, - }, - }, - APIInfo: &api.Info{ - Password: "bletch", - CACert: "CA CERT\n" + testing.CACert, - EnvironTag: testing.EnvironmentTag, - }, - Constraints: envConstraints, - DataDir: dataDir, - LogDir: jujuLogDir, - Jobs: allMachineJobs, - CloudInitOutputLog: cloudInitOutputLog, - InstanceId: "i-bootstrap", - MachineAgentServiceName: "jujud-machine-0", - EnableOSRefreshUpdate: true, - }, + }, + + // raring state server - we just test the raring-specific parts of the output. + { + cfg: makeBootstrapConfig("raring"), setEnvConfig: true, inexactMatch: true, expectScripts: ` @@ -285,43 +325,15 @@ ln -s 1\.2\.3-raring-amd64 '/var/lib/juju/tools/machine-0' rm \$bin/tools\.tar\.gz && rm \$bin/juju1\.2\.3-raring-amd64\.sha256 `, - }, { - // non state server. - cfg: instancecfg.InstanceConfig{ - MachineId: "99", - AuthorizedKeys: "sshkey1", - AgentEnvironment: map[string]string{agent.ProviderType: "dummy"}, - DataDir: dataDir, - LogDir: jujuLogDir, - Jobs: normalMachineJobs, - CloudInitOutputLog: cloudInitOutputLog, - Bootstrap: false, - Tools: newSimpleTools("1.2.3-quantal-amd64"), - Series: "quantal", - MachineNonce: "FAKE_NONCE", - MongoInfo: &mongo.MongoInfo{ - Tag: names.NewMachineTag("99"), - Password: "arble", - Info: mongo.Info{ - Addrs: []string{"state-addr.testing.invalid:12345"}, - CACert: "CA CERT\n" + testing.CACert, - }, - }, - APIInfo: &api.Info{ - Addrs: []string{"state-addr.testing.invalid:54321"}, - Tag: names.NewMachineTag("99"), - Password: "bletch", - CACert: "CA CERT\n" + testing.CACert, - EnvironTag: testing.EnvironmentTag, - }, - MachineAgentServiceName: "jujud-machine-99", - PreferIPv6: true, - EnableOSRefreshUpdate: true, - }, + }, + + // quantal non state server. + { + cfg: makeNormalConfig("quantal"), expectScripts: ` set -xe -install -D -m 644 /dev/null '/etc/init/juju-clean-shutdown.conf' -printf '%s\\n' '\\nauthor "Juju Team "\\ndescription "Stop all network interfaces on shutdown"\\nstart on runlevel \[016\]\\ntask\\nconsole output\\n\\nexec /sbin/ifdown -a -v --force\\n' > '/etc/init/juju-clean-shutdown.conf' +install -D -m 644 /dev/null '/etc/init/juju-clean-shutdown\.conf' +printf '%s\\n' '.*"Stop all network interfaces on shutdown".*' > '/etc/init/juju-clean-shutdown\.conf' install -D -m 644 /dev/null '/var/lib/juju/nonce.txt' printf '%s\\n' 'FAKE_NONCE' > '/var/lib/juju/nonce.txt' test -e /proc/self/fd/9 \|\| exec 9>&2 @@ -347,108 +359,39 @@ start jujud-machine-99 rm \$bin/tools\.tar\.gz && rm \$bin/juju1\.2\.3-quantal-amd64\.sha256 `, - }, { - // non state server with systemd - cfg: instancecfg.InstanceConfig{ - MachineId: "99", - AuthorizedKeys: "sshkey1", - AgentEnvironment: map[string]string{agent.ProviderType: "dummy"}, - DataDir: dataDir, - LogDir: jujuLogDir, - Jobs: normalMachineJobs, - CloudInitOutputLog: cloudInitOutputLog, - Bootstrap: false, - Tools: newSimpleTools("1.2.3-vivid-amd64"), - Series: "vivid", - MachineNonce: "FAKE_NONCE", - MongoInfo: &mongo.MongoInfo{ - Tag: names.NewMachineTag("99"), - Password: "arble", - Info: mongo.Info{ - Addrs: []string{"state-addr.testing.invalid:12345"}, - CACert: "CA CERT\n" + testing.CACert, - }, - }, - APIInfo: &api.Info{ - Addrs: []string{"state-addr.testing.invalid:54321"}, - Tag: names.NewMachineTag("99"), - Password: "bletch", - CACert: "CA CERT\n" + testing.CACert, - EnvironTag: testing.EnvironmentTag, - }, - MachineAgentServiceName: "jujud-machine-99", - PreferIPv6: true, - EnableOSRefreshUpdate: true, - }, + }, + + // non state server with systemd (vivid) + { + cfg: makeNormalConfig("vivid"), + inexactMatch: true, expectScripts: ` set -xe -install -D -m 644 /dev/null '/etc/systemd/system/juju-clean-shutdown.service' -printf '%s\\n' '\\n\[Unit\]\\nDescription=Stop all network interfaces on shutdown\\nDefaultDependencies=false\\nAfter=final\.target\\n\\n\[Service\]\\nType=oneshot\\nExecStart=/sbin/ifdown -a -v --force\\nStandardOutput=tty\\nStandardError=tty\\n\\n\[Install\]\\nWantedBy=final\.target\\n' > '/etc/systemd/system/juju-clean-shutdown.service' +install -D -m 644 /dev/null '/etc/systemd/system/juju-clean-shutdown\.service' +printf '%s\\n' '\\n\[Unit\]\\n.*Stop all network interfaces.*WantedBy=final\.target\\n' > '/etc/systemd.*' /bin/systemctl enable '/etc/systemd/system/juju-clean-shutdown\.service' install -D -m 644 /dev/null '/var/lib/juju/nonce.txt' 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 -mkdir -p /var/lib/juju/locks -\(id ubuntu &> /dev/null\) && chown ubuntu:ubuntu /var/lib/juju/locks -mkdir -p /var/log/juju -chown syslog:adm /var/log/juju -bin='/var/lib/juju/tools/1\.2\.3-vivid-amd64' -mkdir -p \$bin -echo 'Fetching tools.* -curl .* --noproxy "\*" --insecure -o \$bin/tools\.tar\.gz 'https://state-addr\.testing\.invalid:54321/tools/1\.2\.3-vivid-amd64' -sha256sum \$bin/tools\.tar\.gz > \$bin/juju1\.2\.3-vivid-amd64\.sha256 -grep '1234' \$bin/juju1\.2\.3-vivid-amd64.sha256 \|\| \(echo "Tools checksum mismatch"; exit 1\) -tar zxf \$bin/tools.tar.gz -C \$bin -printf %s '{"version":"1\.2\.3-vivid-amd64","url":"http://foo\.com/tools/released/juju1\.2\.3-vivid-amd64\.tgz","sha256":"1234","size":10}' > \$bin/downloaded-tools\.txt -mkdir -p '/var/lib/juju/agents/machine-99' -cat > '/var/lib/juju/agents/machine-99/agent\.conf' << 'EOF'\\n.*\\nEOF -chmod 0600 '/var/lib/juju/agents/machine-99/agent\.conf' -ln -s 1\.2\.3-vivid-amd64 '/var/lib/juju/tools/machine-99' -echo 'Starting Juju machine agent \(jujud-machine-99\)'.* -mkdir -p '/var/lib/juju/init/jujud-machine-99' -cat > '/var/lib/juju/init/jujud-machine-99/exec-start\.sh' << 'EOF'\\n#!/usr/bin/env bash\\n\\n# Set up logging\.\\ntouch '/var/log/juju/machine-99\.log'\\nchown syslog:syslog '/var/log/juju/machine-99\.log'\\nchmod 0600 '/var/log/juju/machine-99\.log'\\nexec >> '/var/log/juju/machine-99\.log'\\nexec 2>&1\\n\\n# Run the script\.\\n'/var/lib/juju/tools/machine-99/jujud' machine --data-dir '/var/lib/juju' --machine-id 99 --debug\\nEOF -chmod 0755 '/var/lib/juju/init/jujud-machine-99/exec-start\.sh' -cat > '/var/lib/juju/init/jujud-machine-99/jujud-machine-99\.service' << 'EOF'\\n\[Unit\]\\nDescription=juju agent for machine-99\\nAfter=syslog\.target\\nAfter=network\.target\\nAfter=systemd-user-sessions\.service\\n\\n\[Service\]\\nLimitNOFILE=20000\\nExecStart=/var/lib/juju/init/jujud-machine-99/exec-start\.sh\\nRestart=on-failure\\nTimeoutSec=300\\n\\n\[Install\]\\nWantedBy=multi-user\.target\\n\\n\\nEOF -/bin/systemctl link '/var/lib/juju/init/jujud-machine-99/jujud-machine-99\.service' -/bin/systemctl daemon-reload -/bin/systemctl enable '/var/lib/juju/init/jujud-machine-99/jujud-machine-99\.service' -/bin/systemctl start jujud-machine-99\.service -rm \$bin/tools\.tar\.gz && rm \$bin/juju1\.2\.3-vivid-amd64\.sha256 -`, - }, { - // check that it works ok with compound machine ids. - cfg: instancecfg.InstanceConfig{ - MachineId: "2/lxc/1", - MachineContainerType: "lxc", - AuthorizedKeys: "sshkey1", - AgentEnvironment: map[string]string{agent.ProviderType: "dummy"}, - DataDir: dataDir, - LogDir: jujuLogDir, - Jobs: normalMachineJobs, - CloudInitOutputLog: cloudInitOutputLog, - Bootstrap: false, - Tools: newSimpleTools("1.2.3-quantal-amd64"), - Series: "quantal", - MachineNonce: "FAKE_NONCE", - MongoInfo: &mongo.MongoInfo{ - Tag: names.NewMachineTag("2/lxc/1"), - Password: "arble", - Info: mongo.Info{ - Addrs: []string{"state-addr.testing.invalid:12345"}, - CACert: "CA CERT\n" + testing.CACert, - }, - }, - APIInfo: &api.Info{ - Addrs: []string{"state-addr.testing.invalid:54321"}, - Tag: names.NewMachineTag("2/lxc/1"), - Password: "bletch", - CACert: "CA CERT\n" + testing.CACert, - EnvironTag: testing.EnvironmentTag, - }, - MachineAgentServiceName: "jujud-machine-2-lxc-1", - EnableOSRefreshUpdate: true, - }, +.* +`, + }, + + // CentOS non state server with systemd + { + cfg: makeNormalConfig("centos7"), + inexactMatch: true, + expectScripts: ` +systemctl is-enabled firewalld &> /dev/null && systemctl mask firewalld || true +systemctl is-active firewalld &> /dev/null && systemctl stop firewalld || true +sed -i "s/\^\.\*requiretty/#Defaults requiretty/" /etc/sudoers +`, + }, + + // check that it works ok with compound machine ids. + { + cfg: makeNormalConfig("quantal").mutate(func(cfg *testInstanceConfig) { + cfg.MachineContainerType = "lxc" + }).setMachineID("2/lxc/1"), inexactMatch: true, expectScripts: ` mkdir -p '/var/lib/juju/agents/machine-2-lxc-1' @@ -458,118 +401,43 @@ cat > /etc/init/jujud-machine-2-lxc-1\.conf << 'EOF'\\ndescription "juju agent for machine-2-lxc-1"\\nauthor "Juju Team "\\nstart on runlevel \[2345\]\\nstop on runlevel \[!2345\]\\nrespawn\\nnormal exit 0\\n\\nlimit nofile 20000 20000\\n\\nscript\\n\\n\\n # Ensure log files are properly protected\\n touch /var/log/juju/machine-2-lxc-1\.log\\n chown syslog:syslog /var/log/juju/machine-2-lxc-1\.log\\n chmod 0600 /var/log/juju/machine-2-lxc-1\.log\\n\\n exec '/var/lib/juju/tools/machine-2-lxc-1/jujud' machine --data-dir '/var/lib/juju' --machine-id 2/lxc/1 --debug >> /var/log/juju/machine-2-lxc-1\.log 2>&1\\nend script\\nEOF\\n start jujud-machine-2-lxc-1 `, - }, { - // hostname verification disabled. - cfg: instancecfg.InstanceConfig{ - MachineId: "99", - AuthorizedKeys: "sshkey1", - AgentEnvironment: map[string]string{agent.ProviderType: "dummy"}, - DataDir: dataDir, - LogDir: jujuLogDir, - Jobs: normalMachineJobs, - CloudInitOutputLog: cloudInitOutputLog, - Bootstrap: false, - Tools: newSimpleTools("1.2.3-quantal-amd64"), - Series: "quantal", - MachineNonce: "FAKE_NONCE", - MongoInfo: &mongo.MongoInfo{ - Tag: names.NewMachineTag("99"), - Password: "arble", - Info: mongo.Info{ - Addrs: []string{"state-addr.testing.invalid:12345"}, - CACert: "CA CERT\n" + testing.CACert, - }, - }, - APIInfo: &api.Info{ - Addrs: []string{"state-addr.testing.invalid:54321"}, - Tag: names.NewMachineTag("99"), - Password: "bletch", - CACert: "CA CERT\n" + testing.CACert, - EnvironTag: testing.EnvironmentTag, - }, - DisableSSLHostnameVerification: true, - MachineAgentServiceName: "jujud-machine-99", - EnableOSRefreshUpdate: true, - }, + }, + + // hostname verification disabled. + { + cfg: makeNormalConfig("quantal").mutate(func(cfg *testInstanceConfig) { + cfg.DisableSSLHostnameVerification = true + }), inexactMatch: true, expectScripts: ` curl .* --noproxy "\*" --insecure -o \$bin/tools\.tar\.gz 'https://state-addr\.testing\.invalid:54321/tools/1\.2\.3-quantal-amd64' `, - }, { - // empty contraints. - cfg: instancecfg.InstanceConfig{ - MachineId: "0", - AuthorizedKeys: "sshkey1", - AgentEnvironment: map[string]string{agent.ProviderType: "dummy"}, - // precise currently needs mongo from PPA - Tools: newSimpleTools("1.2.3-precise-amd64"), - Series: "precise", - Bootstrap: true, - StateServingInfo: stateServingInfo, - MachineNonce: "FAKE_NONCE", - MongoInfo: &mongo.MongoInfo{ - Password: "arble", - Info: mongo.Info{ - CACert: "CA CERT\n" + testing.CACert, - }, - }, - APIInfo: &api.Info{ - Password: "bletch", - CACert: "CA CERT\n" + testing.CACert, - EnvironTag: testing.EnvironmentTag, - }, - DataDir: dataDir, - LogDir: jujuLogDir, - Jobs: allMachineJobs, - CloudInitOutputLog: cloudInitOutputLog, - InstanceId: "i-bootstrap", - MachineAgentServiceName: "jujud-machine-0", - EnableOSRefreshUpdate: true, - }, + }, + + // empty bootstrap contraints. + { + cfg: makeBootstrapConfig("precise").mutate(func(cfg *testInstanceConfig) { + cfg.Constraints = constraints.Value{} + }), setEnvConfig: true, inexactMatch: true, expectScripts: ` /var/lib/juju/tools/1\.2\.3-precise-amd64/jujud bootstrap-state --data-dir '/var/lib/juju' --env-config '[^']*' --instance-id 'i-bootstrap' --debug `, - }, { - // custom image metadata. - cfg: instancecfg.InstanceConfig{ - MachineId: "0", - AuthorizedKeys: "sshkey1", - AgentEnvironment: map[string]string{agent.ProviderType: "dummy"}, - // precise currently needs mongo from PPA - Tools: newSimpleTools("1.2.3-precise-amd64"), - Series: "precise", - Bootstrap: true, - StateServingInfo: stateServingInfo, - MachineNonce: "FAKE_NONCE", - MongoInfo: &mongo.MongoInfo{ - Password: "arble", - Info: mongo.Info{ - CACert: "CA CERT\n" + testing.CACert, - }, - }, - APIInfo: &api.Info{ - Password: "bletch", - CACert: "CA CERT\n" + testing.CACert, - EnvironTag: testing.EnvironmentTag, - }, - DataDir: dataDir, - LogDir: jujuLogDir, - Jobs: allMachineJobs, - CloudInitOutputLog: cloudInitOutputLog, - InstanceId: "i-bootstrap", - MachineAgentServiceName: "jujud-machine-0", - EnableOSRefreshUpdate: true, - CustomImageMetadata: []*imagemetadata.ImageMetadata{{ + }, + + // custom image metadata (at bootstrap). + { + cfg: makeBootstrapConfig("trusty").mutate(func(cfg *testInstanceConfig) { + cfg.CustomImageMetadata = []*imagemetadata.ImageMetadata{{ Id: "image-id", Storage: "ebs", VirtType: "pv", Arch: "amd64", Version: "14.04", RegionName: "us-east1", - }}, - }, + }} + }), setEnvConfig: true, inexactMatch: true, expectScripts: ` @@ -595,6 +463,7 @@ } func getAgentConfig(c *gc.C, tag string, scripts []string) (cfg string) { + c.Assert(scripts, gc.Not(gc.HasLen), 0) re := regexp.MustCompile(`cat > .*agents/` + regexp.QuoteMeta(tag) + `/agent\.conf' << 'EOF'\n((\n|.)+)\nEOF`) found := false for _, s := range scripts { @@ -611,6 +480,7 @@ // check that any --env-config $base64 is valid and matches t.cfg.Config func checkEnvConfig(c *gc.C, cfg *config.Config, x map[interface{}]interface{}, scripts []string) { + c.Assert(scripts, gc.Not(gc.HasLen), 0) re := regexp.MustCompile(`--env-config '([^']+)'`) found := false for _, s := range scripts { @@ -635,12 +505,14 @@ for i, test := range cloudinitTests { c.Logf("test %d", i) + var envConfig *config.Config if test.setEnvConfig { - test.cfg.Config = minimalConfig(c) + envConfig = minimalEnvironConfig(c) } - ci, err := cloudinit.New(test.cfg.Series) + testConfig := test.cfg.maybeSetEnvironConfig(envConfig).render() + ci, err := cloudinit.New(testConfig.Series) c.Assert(err, jc.ErrorIsNil) - udata, err := cloudconfig.NewUserdataConfig(&test.cfg, ci) + udata, err := cloudconfig.NewUserdataConfig(&testConfig, ci) c.Assert(err, jc.ErrorIsNil) err = udata.Configure() @@ -657,13 +529,13 @@ err = goyaml.Unmarshal(data, &configKeyValues) c.Assert(err, jc.ErrorIsNil) - if test.cfg.EnableOSRefreshUpdate { + if testConfig.EnableOSRefreshUpdate { c.Check(configKeyValues["package_update"], jc.IsTrue) } else { c.Check(configKeyValues["package_update"], jc.IsFalse) } - if test.cfg.EnableOSUpgrade { + if testConfig.EnableOSUpgrade { c.Check(configKeyValues["package_upgrade"], jc.IsTrue) } else { c.Check(configKeyValues["package_upgrade"], jc.IsFalse) @@ -671,30 +543,30 @@ scripts := getScripts(configKeyValues) assertScriptMatch(c, scripts, test.expectScripts, !test.inexactMatch) - if test.cfg.Config != nil { - checkEnvConfig(c, test.cfg.Config, configKeyValues, scripts) + if testConfig.Config != nil { + checkEnvConfig(c, testConfig.Config, configKeyValues, scripts) } // curl should always be installed, since it's required by jujud. checkPackage(c, configKeyValues, "curl", true) - tag := names.NewMachineTag(test.cfg.MachineId).String() + tag := names.NewMachineTag(testConfig.MachineId).String() acfg := getAgentConfig(c, tag, scripts) c.Assert(acfg, jc.Contains, "AGENT_SERVICE_NAME: jujud-"+tag) c.Assert(acfg, jc.Contains, "upgradedToVersion: 1.2.3\n") source := "deb http://ubuntu-cloud.archive.canonical.com/ubuntu precise-updates/cloud-tools main" - needCloudArchive := test.cfg.Series == "precise" + needCloudArchive := testConfig.Series == "precise" checkAptSource(c, configKeyValues, source, pacconf.UbuntuCloudArchiveSigningKey, needCloudArchive) } } func (*cloudinitSuite) TestCloudInitConfigure(c *gc.C) { for i, test := range cloudinitTests { - test.cfg.Config = minimalConfig(c) + testConfig := test.cfg.maybeSetEnvironConfig(minimalEnvironConfig(c)).render() c.Logf("test %d (Configure)", i) - cloudcfg, err := cloudinit.New(test.cfg.Series) + cloudcfg, err := cloudinit.New(testConfig.Series) c.Assert(err, jc.ErrorIsNil) - udata, err := cloudconfig.NewUserdataConfig(&test.cfg, cloudcfg) + udata, err := cloudconfig.NewUserdataConfig(&testConfig, cloudcfg) c.Assert(err, jc.ErrorIsNil) err = udata.Configure() c.Assert(err, jc.ErrorIsNil) @@ -703,12 +575,12 @@ func (*cloudinitSuite) TestCloudInitConfigureBootstrapLogging(c *gc.C) { loggo.GetLogger("").SetLogLevel(loggo.INFO) - instanceConfig := minimalInstanceConfig() - instanceConfig.Config = minimalConfig(c) - - cloudcfg, err := cloudinit.New(instanceConfig.Series) + envConfig := minimalEnvironConfig(c) + instConfig := makeBootstrapConfig("quantal").maybeSetEnvironConfig(envConfig) + rendered := instConfig.render() + cloudcfg, err := cloudinit.New(rendered.Series) c.Assert(err, jc.ErrorIsNil) - udata, err := cloudconfig.NewUserdataConfig(&instanceConfig, cloudcfg) + udata, err := cloudconfig.NewUserdataConfig(&rendered, cloudcfg) c.Assert(err, jc.ErrorIsNil) err = udata.Configure() @@ -736,8 +608,9 @@ c.Assert(err, jc.ErrorIsNil) script := "test script" cloudcfg.AddRunCmd(script) - cloudinitTests[0].cfg.Config = minimalConfig(c) - udata, err := cloudconfig.NewUserdataConfig(&cloudinitTests[0].cfg, cloudcfg) + envConfig := minimalEnvironConfig(c) + testConfig := cloudinitTests[0].cfg.maybeSetEnvironConfig(envConfig).render() + udata, err := cloudconfig.NewUserdataConfig(&testConfig, cloudcfg) c.Assert(err, jc.ErrorIsNil) err = udata.Configure() c.Assert(err, jc.ErrorIsNil) @@ -807,15 +680,15 @@ if exact { c.Fatalf("too few scripts found (expected %q at line %d)", pats[0].line, pats[0].index) } - c.Fatalf("could not find match for %q", pats[0].line) + c.Fatalf("could not find match for %q\ngot:\n%s", pats[0].line, strings.Join(got, "\n")) default: ok, err := regexp.MatchString(pats[0].line, scripts[0].line) - c.Assert(err, jc.ErrorIsNil) + c.Assert(err, jc.ErrorIsNil, gc.Commentf("invalid regexp: %q", pats[0].line)) if ok { pats = pats[1:] scripts = scripts[1:] } else if exact { - c.Assert(scripts[0].line, gc.Matches, pats[0].line, gc.Commentf("line %d", scripts[0].index)) + c.Assert(scripts[0].line, gc.Matches, pats[0].line, gc.Commentf("line %d; expected %q; got %q; paths: %#v", scripts[0].index, pats[0].line, scripts[0].line, pats)) } else { scripts = scripts[1:] } @@ -1068,11 +941,11 @@ CACert: testing.CACert, EnvironTag: testing.EnvironmentTag, }, - Config: minimalConfig(c), - DataDir: dataDir, - LogDir: logDir, + Config: minimalEnvironConfig(c), + DataDir: jujuDataDir("quantal"), + LogDir: jujuLogDir("quantal"), Jobs: normalMachineJobs, - CloudInitOutputLog: cloudInitOutputLog, + CloudInitOutputLog: cloudInitOutputLog("quantal"), InstanceId: "i-bootstrap", MachineNonce: "FAKE_NONCE", MachineAgentServiceName: "jujud-machine-99", @@ -1118,7 +991,7 @@ } func (s *cloudinitSuite) TestAptProxyNotWrittenIfNotSet(c *gc.C) { - environConfig := minimalConfig(c) + environConfig := minimalEnvironConfig(c) instanceCfg := s.createInstanceConfig(c, environConfig) cloudcfg, err := cloudinit.New("quantal") c.Assert(err, jc.ErrorIsNil) @@ -1132,7 +1005,7 @@ } func (s *cloudinitSuite) TestAptProxyWritten(c *gc.C) { - environConfig := minimalConfig(c) + environConfig := minimalEnvironConfig(c) environConfig, err := environConfig.Apply(map[string]interface{}{ "apt-http-proxy": "http://user@10.0.0.1", }) @@ -1151,7 +1024,7 @@ } func (s *cloudinitSuite) TestProxyWritten(c *gc.C) { - environConfig := minimalConfig(c) + environConfig := minimalEnvironConfig(c) environConfig, err := environConfig.Apply(map[string]interface{}{ "http-proxy": "http://user@10.0.0.1", "no-proxy": "localhost,10.0.3.1", @@ -1189,7 +1062,7 @@ } func (s *cloudinitSuite) TestAptMirror(c *gc.C) { - environConfig := minimalConfig(c) + environConfig := minimalEnvironConfig(c) environConfig, err := environConfig.Apply(map[string]interface{}{ "apt-mirror": "http://my.archive.ubuntu.com/ubuntu", }) @@ -1198,7 +1071,7 @@ } func (s *cloudinitSuite) TestAptMirrorNotSet(c *gc.C) { - environConfig := minimalConfig(c) + environConfig := minimalEnvironConfig(c) s.testAptMirror(c, environConfig, "") } @@ -1244,50 +1117,21 @@ `[1:]) var windowsCloudinitTests = []cloudinitTest{{ - cfg: instancecfg.InstanceConfig{ - MachineId: "10", - AgentEnvironment: map[string]string{agent.ProviderType: "dummy"}, - Tools: newSimpleTools("1.2.3-win8-amd64"), - Series: "win8", - Bootstrap: false, - Jobs: normalMachineJobs, - MachineNonce: "FAKE_NONCE", - CloudInitOutputLog: cloudInitOutputLog, - MongoInfo: &mongo.MongoInfo{ - Tag: names.NewMachineTag("10"), - Password: "arble", - Info: mongo.Info{ - CACert: "CA CERT\n" + string(serverCert), - Addrs: []string{"state-addr.testing.invalid:12345"}, - }, - }, - APIInfo: &api.Info{ - Addrs: []string{"state-addr.testing.invalid:54321"}, - Password: "bletch", - CACert: "CA CERT\n" + string(serverCert), - Tag: names.NewMachineTag("10"), - EnvironTag: testing.EnvironmentTag, - }, - MachineAgentServiceName: "jujud-machine-10", - }, + cfg: makeNormalConfig("win8").setMachineID("10").mutate(func(cfg *testInstanceConfig) { + cfg.MongoInfo.Info.CACert = "CA CERT\n" + string(serverCert) + cfg.APIInfo.CACert = "CA CERT\n" + string(serverCert) + }), setEnvConfig: false, expectScripts: WindowsUserdata, }} func (*cloudinitSuite) TestWindowsCloudInit(c *gc.C) { for i, test := range windowsCloudinitTests { + testConfig := test.cfg.render() c.Logf("test %d", i) - dataDir, err := paths.DataDir(test.cfg.Series) - c.Assert(err, jc.ErrorIsNil) - logDir, err := paths.LogDir(test.cfg.Series) - c.Assert(err, jc.ErrorIsNil) - - test.cfg.DataDir = dataDir - test.cfg.LogDir = path.Join(logDir, "juju") - ci, err := cloudinit.New("win8") c.Assert(err, jc.ErrorIsNil) - udata, err := cloudconfig.NewUserdataConfig(&test.cfg, ci) + udata, err := cloudconfig.NewUserdataConfig(&testConfig, ci) c.Assert(err, jc.ErrorIsNil) err = udata.Configure() === modified file 'src/github.com/juju/juju/cloudconfig/userdatacfg_unix.go' --- src/github.com/juju/juju/cloudconfig/userdatacfg_unix.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cloudconfig/userdatacfg_unix.go 2015-10-23 18:29:32 +0000 @@ -105,11 +105,18 @@ // Hopefully in the future we are going to move all the distirbutions to // having a "juju" user case version.CentOS: - script := fmt.Sprintf(initUbuntuScript, utils.ShQuote(w.icfg.AuthorizedKeys)) - w.conf.AddScripts(script) - w.conf.AddScripts("systemctl stop firewalld") - w.conf.AddScripts("systemctl disable firewalld") - w.conf.AddScripts(`sed -i "s/^.*requiretty/#Defaults requiretty/" /etc/sudoers`) + w.conf.AddScripts( + fmt.Sprintf(initUbuntuScript, utils.ShQuote(w.icfg.AuthorizedKeys)), + + // Mask and stop firewalld, if enabled, so it cannot start. See + // http://pad.lv/1492066. firewalld might be missing, in which case + // is-enabled and is-active prints an error, which is why the output + // is surpressed. + "systemctl is-enabled firewalld &> /dev/null && systemctl mask firewalld || true", + "systemctl is-active firewalld &> /dev/null && systemctl stop firewalld || true", + + `sed -i "s/^.*requiretty/#Defaults requiretty/" /etc/sudoers`, + ) w.addCleanShutdownJob(service.InitSystemSystemd) } w.conf.SetOutput(cloudinit.OutAll, "| tee -a "+w.icfg.CloudInitOutputLog, "") @@ -125,18 +132,15 @@ return nil } -const ( - cleanShutdownUpstartPath = "/etc/init/juju-clean-shutdown.conf" - cleanShutdownSystemdPath = "/etc/systemd/system/juju-clean-shutdown.service" -) - func (w *unixConfigure) addCleanShutdownJob(initSystem string) { switch initSystem { case service.InitSystemUpstart: - w.conf.AddRunTextFile(cleanShutdownUpstartPath, upstart.CleanShutdownJob, 0644) + path, contents := upstart.CleanShutdownJobPath, upstart.CleanShutdownJob + w.conf.AddRunTextFile(path, contents, 0644) case service.InitSystemSystemd: - w.conf.AddRunTextFile(cleanShutdownSystemdPath, systemd.CleanShutdownService, 0644) - w.conf.AddScripts(fmt.Sprintf("/bin/systemctl enable '%s'", cleanShutdownSystemdPath)) + path, contents := systemd.CleanShutdownServicePath, systemd.CleanShutdownService + w.conf.AddRunTextFile(path, contents, 0644) + w.conf.AddScripts(fmt.Sprintf("/bin/systemctl enable '%s'", path)) } } === modified file 'src/github.com/juju/juju/cloudconfig/userdatacfg_win.go' --- src/github.com/juju/juju/cloudconfig/userdatacfg_win.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cloudconfig/userdatacfg_win.go 2015-10-23 18:29:32 +0000 @@ -11,10 +11,19 @@ "github.com/juju/errors" "github.com/juju/names" + "github.com/juju/utils/featureflag" + "github.com/juju/juju/juju/osenv" "github.com/juju/juju/juju/paths" ) +type aclType string + +const ( + fileSystem aclType = "FileSystem" + registryEntry aclType = "Registry" +) + type windowsConfigure struct { baseConfigure } @@ -42,14 +51,19 @@ w.conf.AddScripts( fmt.Sprintf(`%s`, winPowershellHelperFunctions), - fmt.Sprintf(`icacls "%s" /grant "jujud:(OI)(CI)(F)" /T`, renderer.FromSlash(baseDir)), + + // Some providers create a baseDir before this step, but we need to + // make sure it exists before applying icacls + fmt.Sprintf(`mkdir -Force "%s"`, renderer.FromSlash(baseDir)), fmt.Sprintf(`mkdir %s`, renderer.FromSlash(tmpDir)), fmt.Sprintf(`mkdir "%s"`, binDir), - fmt.Sprintf(`%s`, winSetPasswdScript), - fmt.Sprintf(`Start-ProcessAsUser -Command $powershell -Arguments "-File C:\juju\bin\save_pass.ps1 $juju_passwd" -Credential $jujuCreds`), fmt.Sprintf(`mkdir "%s\locks"`, renderer.FromSlash(dataDir)), - fmt.Sprintf(`Start-ProcessAsUser -Command $cmdExe -Arguments '/C setx PATH "%%PATH%%;C:\Juju\bin"' -Credential $jujuCreds`), ) + + // This is necessary for setACLs to work + w.conf.AddScripts(`$adminsGroup = (New-Object System.Security.Principal.SecurityIdentifier("S-1-5-32-544")).Translate([System.Security.Principal.NTAccount])`) + w.conf.AddScripts(setACLs(renderer.FromSlash(baseDir), fileSystem)...) + w.conf.AddScripts(`setx /m PATH "$env:PATH;C:\Juju\bin\"`) noncefile := renderer.Join(dataDir, NonceFile) w.conf.AddScripts( fmt.Sprintf(`Set-Content "%s" "%s"`, noncefile, shquote(w.icfg.MachineNonce)), @@ -61,33 +75,35 @@ if err := w.icfg.VerifyConfig(); err != nil { return errors.Trace(err) } + if w.icfg.Bootstrap == true { + // Bootstrap machine not supported on windows + return errors.Errorf("bootstrapping is not supported on windows") + } + toolsJson, err := json.Marshal(w.icfg.Tools) if err != nil { return errors.Annotate(err, "while serializing the tools") } - const python = `${env:ProgramFiles(x86)}\Cloudbase Solutions\Cloudbase-Init\Python27\python.exe` renderer := w.conf.ShellRenderer() w.conf.AddScripts( fmt.Sprintf(`$binDir="%s"`, renderer.FromSlash(w.icfg.JujuTools())), - `$tmpBinDir=$binDir.Replace('\', '\\')`, fmt.Sprintf(`mkdir '%s'`, renderer.FromSlash(w.icfg.LogDir)), `mkdir $binDir`, `$WebClient = New-Object System.Net.WebClient`, `[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}`, fmt.Sprintf(`ExecRetry { $WebClient.DownloadFile('%s', "$binDir\tools.tar.gz") }`, w.icfg.Tools.URL), - `$dToolsHash = (Get-FileHash -Algorithm SHA256 "$binDir\tools.tar.gz").hash`, + `$dToolsHash = Get-FileSHA256 -FilePath "$binDir\tools.tar.gz"`, fmt.Sprintf(`$dToolsHash > "$binDir\juju%s.sha256"`, w.icfg.Tools.Version), fmt.Sprintf(`if ($dToolsHash.ToLower() -ne "%s"){ Throw "Tools checksum mismatch"}`, w.icfg.Tools.SHA256), - fmt.Sprintf(`& "%s" -c "import tarfile;archive = tarfile.open('$tmpBinDir\\tools.tar.gz');archive.extractall(path='$tmpBinDir')"`, python), + fmt.Sprintf(`GUnZip-File -infile $binDir\tools.tar.gz -outdir $binDir`), `rm "$binDir\tools.tar*"`, fmt.Sprintf(`Set-Content $binDir\downloaded-tools.txt '%s'`, string(toolsJson)), ) - if w.icfg.Bootstrap == true { - // Bootstrap machine not supported on windows - return errors.Errorf("bootstrapping is not supported on windows") + for _, cmd := range CreateJujuRegistryKeyCmds() { + w.conf.AddRunCmd(cmd) } machineTag := names.NewMachineTag(w.icfg.MachineId) @@ -97,3 +113,47 @@ } return w.addMachineAgentToBoot() } + +// CreateJujuRegistryKey is going to create a juju registry key and set +// permissions on it such that it's only accessible to administrators +// It is exported because it is used in an upgrade step +func CreateJujuRegistryKeyCmds() []string { + aclCmds := setACLs(osenv.JujuRegistryKey, registryEntry) + regCmds := []string{ + + // Create a registry key for storing juju related information + fmt.Sprintf(`New-Item -Path '%s'`, osenv.JujuRegistryKey), + + // Create a JUJU_DEV_FEATURE_FLAGS entry which may or may not be empty. + fmt.Sprintf(`New-ItemProperty -Path '%s' -Name '%s'`, + osenv.JujuRegistryKey, + osenv.JujuFeatureFlagEnvKey), + fmt.Sprintf(`Set-ItemProperty -Path '%s' -Name '%s' -Value '%s'`, + osenv.JujuRegistryKey, + osenv.JujuFeatureFlagEnvKey, + featureflag.AsEnvironmentValue()), + } + return append(regCmds[:1], append(aclCmds, regCmds[1:]...)...) +} + +func setACLs(path string, permType aclType) []string { + ruleModel := `$rule = New-Object System.Security.AccessControl.%sAccessRule %s` + permModel := `%s = "%s", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow"` + adminPermVar := `$adminPerm` + jujudPermVar := `$jujudPerm` + return []string{ + fmt.Sprintf(`$acl = Get-Acl -Path '%s'`, path), + + // Reset the ACL's on it and add administrator access only. + `$acl.SetAccessRuleProtection($true, $false)`, + + // $adminsGroup must be defined before calling setACLs + fmt.Sprintf(permModel, adminPermVar, `$adminsGroup`), + fmt.Sprintf(permModel, jujudPermVar, `jujud`), + fmt.Sprintf(ruleModel, permType, adminPermVar), + `$acl.AddAccessRule($rule)`, + fmt.Sprintf(ruleModel, permType, jujudPermVar), + `$acl.AddAccessRule($rule)`, + fmt.Sprintf(`Set-Acl -Path '%s' -AclObject $acl`, path), + } +} === modified file 'src/github.com/juju/juju/cloudconfig/windows_userdata_test.go' --- src/github.com/juju/juju/cloudconfig/windows_userdata_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cloudconfig/windows_userdata_test.go 2015-10-23 18:29:32 +0000 @@ -21,34 +21,34 @@ function ExecRetry($command, $maxRetryCount = 10, $retryInterval=2) { - $currErrorActionPreference = $ErrorActionPreference - $ErrorActionPreference = "Continue" - - $retryCount = 0 - while ($true) - { - try - { - & $command - break - } - catch [System.Exception] - { - $retryCount++ - if ($retryCount -ge $maxRetryCount) - { - $ErrorActionPreference = $currErrorActionPreference - throw - } - else - { - Write-Error $_.Exception - Start-Sleep $retryInterval - } - } - } - - $ErrorActionPreference = $currErrorActionPreference + $currErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = "Continue" + + $retryCount = 0 + while ($true) + { + try + { + & $command + break + } + catch [System.Exception] + { + $retryCount++ + if ($retryCount -ge $maxRetryCount) + { + $ErrorActionPreference = $currErrorActionPreference + throw + } + else + { + Write-Error $_.Exception + Start-Sleep $retryInterval + } + } + } + + $ErrorActionPreference = $currErrorActionPreference } function create-account ([string]$accountName, [string]$accountDescription, [string]$password) { @@ -62,7 +62,11 @@ $User.UserFlags[0] = $User.UserFlags[0] -bor 0x10000 $user.SetInfo() - $objOU = [ADSI]"WinNT://$hostname/Administrators,group" + # This gets the Administrator group name that is localized on different windows versions. + # However the SID S-1-5-32-544 is the same on all versions. + $adminGroup = (New-Object System.Security.Principal.SecurityIdentifier("S-1-5-32-544")).Translate([System.Security.Principal.NTAccount]).Value.Split("\")[1] + + $objOU = [ADSI]"WinNT://$hostname/$adminGroup,group" $objOU.add("WinNT://$hostname/$accountName") } @@ -73,29 +77,29 @@ namespace PSCloudbase { - public sealed class Win32CryptApi - { - public static long CRYPT_SILENT = 0x00000040; - public static long CRYPT_VERIFYCONTEXT = 0xF0000000; - public static int PROV_RSA_FULL = 1; - - [DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)] - [return : MarshalAs(UnmanagedType.Bool)] - public static extern bool CryptAcquireContext(ref IntPtr hProv, - StringBuilder pszContainer, // Don't use string, as Powershell replaces $null with an empty string - StringBuilder pszProvider, // Don't use string, as Powershell replaces $null with an empty string - uint dwProvType, - uint dwFlags); - - [DllImport("Advapi32.dll", EntryPoint = "CryptReleaseContext", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern bool CryptReleaseContext(IntPtr hProv, Int32 dwFlags); - - [DllImport("advapi32.dll", SetLastError=true)] - public static extern bool CryptGenRandom(IntPtr hProv, uint dwLen, byte[] pbBuffer); - - [DllImport("Kernel32.dll")] - public static extern uint GetLastError(); - } + public sealed class Win32CryptApi + { + public static long CRYPT_SILENT = 0x00000040; + public static long CRYPT_VERIFYCONTEXT = 0xF0000000; + public static int PROV_RSA_FULL = 1; + + [DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)] + [return : MarshalAs(UnmanagedType.Bool)] + public static extern bool CryptAcquireContext(ref IntPtr hProv, + StringBuilder pszContainer, // Don't use string, as Powershell replaces $null with an empty string + StringBuilder pszProvider, // Don't use string, as Powershell replaces $null with an empty string + uint dwProvType, + uint dwFlags); + + [DllImport("Advapi32.dll", EntryPoint = "CryptReleaseContext", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern bool CryptReleaseContext(IntPtr hProv, Int32 dwFlags); + + [DllImport("advapi32.dll", SetLastError=true)] + public static extern bool CryptGenRandom(IntPtr hProv, uint dwLen, byte[] pbBuffer); + + [DllImport("Kernel32.dll")] + public static extern uint GetLastError(); + } } "@ @@ -103,42 +107,42 @@ function Get-RandomPassword { - [CmdletBinding()] - param - ( - [parameter(Mandatory=$true)] - [int]$Length - ) - process - { - $hProvider = 0 - try - { - if(![PSCloudbase.Win32CryptApi]::CryptAcquireContext([ref]$hProvider, $null, $null, - [PSCloudbase.Win32CryptApi]::PROV_RSA_FULL, - ([PSCloudbase.Win32CryptApi]::CRYPT_VERIFYCONTEXT -bor - [PSCloudbase.Win32CryptApi]::CRYPT_SILENT))) - { - throw "CryptAcquireContext failed with error: 0x" + "{0:X0}" -f [PSCloudbase.Win32CryptApi]::GetLastError() - } - - $buffer = New-Object byte[] $Length - if(![PSCloudbase.Win32CryptApi]::CryptGenRandom($hProvider, $Length, $buffer)) - { - throw "CryptGenRandom failed with error: 0x" + "{0:X0}" -f [PSCloudbase.Win32CryptApi]::GetLastError() - } - - $buffer | ForEach-Object { $password += "{0:X0}" -f $_ } - return $password - } - finally - { - if($hProvider) - { - $retVal = [PSCloudbase.Win32CryptApi]::CryptReleaseContext($hProvider, 0) - } - } - } + [CmdletBinding()] + param + ( + [parameter(Mandatory=$true)] + [int]$Length + ) + process + { + $hProvider = 0 + try + { + if(![PSCloudbase.Win32CryptApi]::CryptAcquireContext([ref]$hProvider, $null, $null, + [PSCloudbase.Win32CryptApi]::PROV_RSA_FULL, + ([PSCloudbase.Win32CryptApi]::CRYPT_VERIFYCONTEXT -bor + [PSCloudbase.Win32CryptApi]::CRYPT_SILENT))) + { + throw "CryptAcquireContext failed with error: 0x" + "{0:X0}" -f [PSCloudbase.Win32CryptApi]::GetLastError() + } + + $buffer = New-Object byte[] $Length + if(![PSCloudbase.Win32CryptApi]::CryptGenRandom($hProvider, $Length, $buffer)) + { + throw "CryptGenRandom failed with error: 0x" + "{0:X0}" -f [PSCloudbase.Win32CryptApi]::GetLastError() + } + + $buffer | ForEach-Object { $password += "{0:X0}" -f $_ } + return $password + } + finally + { + if($hProvider) + { + $retVal = [PSCloudbase.Win32CryptApi]::CryptReleaseContext($hProvider, 0) + } + } + } } $SourcePolicy = @" @@ -155,640 +159,667 @@ namespace PSCarbon { - public sealed class Lsa - { - // ReSharper disable InconsistentNaming - [StructLayout(LayoutKind.Sequential)] - internal struct LSA_UNICODE_STRING - { - internal LSA_UNICODE_STRING(string inputString) - { - if (inputString == null) - { - Buffer = IntPtr.Zero; - Length = 0; - MaximumLength = 0; - } - else - { - Buffer = Marshal.StringToHGlobalAuto(inputString); - Length = (ushort)(inputString.Length * UnicodeEncoding.CharSize); - MaximumLength = (ushort)((inputString.Length + 1) * UnicodeEncoding.CharSize); - } - } - - internal ushort Length; - internal ushort MaximumLength; - internal IntPtr Buffer; - } - - [StructLayout(LayoutKind.Sequential)] - internal struct LSA_OBJECT_ATTRIBUTES - { - internal uint Length; - internal IntPtr RootDirectory; - internal LSA_UNICODE_STRING ObjectName; - internal uint Attributes; - internal IntPtr SecurityDescriptor; - internal IntPtr SecurityQualityOfService; - } - - [StructLayout(LayoutKind.Sequential)] - public struct LUID - { - public uint LowPart; - public int HighPart; - } - - // ReSharper disable UnusedMember.Local - private const uint POLICY_VIEW_LOCAL_INFORMATION = 0x00000001; - private const uint POLICY_VIEW_AUDIT_INFORMATION = 0x00000002; - private const uint POLICY_GET_PRIVATE_INFORMATION = 0x00000004; - private const uint POLICY_TRUST_ADMIN = 0x00000008; - private const uint POLICY_CREATE_ACCOUNT = 0x00000010; - private const uint POLICY_CREATE_SECRET = 0x00000014; - private const uint POLICY_CREATE_PRIVILEGE = 0x00000040; - private const uint POLICY_SET_DEFAULT_QUOTA_LIMITS = 0x00000080; - private const uint POLICY_SET_AUDIT_REQUIREMENTS = 0x00000100; - private const uint POLICY_AUDIT_LOG_ADMIN = 0x00000200; - private const uint POLICY_SERVER_ADMIN = 0x00000400; - private const uint POLICY_LOOKUP_NAMES = 0x00000800; - private const uint POLICY_NOTIFICATION = 0x00001000; - // ReSharper restore UnusedMember.Local - - [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] - public static extern bool LookupPrivilegeValue( - [MarshalAs(UnmanagedType.LPTStr)] string lpSystemName, - [MarshalAs(UnmanagedType.LPTStr)] string lpName, - out LUID lpLuid); - - [DllImport("advapi32.dll", CharSet = CharSet.Unicode)] - private static extern uint LsaAddAccountRights( - IntPtr PolicyHandle, - IntPtr AccountSid, - LSA_UNICODE_STRING[] UserRights, - uint CountOfRights); - - [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = false)] - private static extern uint LsaClose(IntPtr ObjectHandle); - - [DllImport("advapi32.dll", SetLastError = true)] - private static extern uint LsaEnumerateAccountRights(IntPtr PolicyHandle, - IntPtr AccountSid, - out IntPtr UserRights, - out uint CountOfRights - ); - - [DllImport("advapi32.dll", SetLastError = true)] - private static extern uint LsaFreeMemory(IntPtr pBuffer); - - [DllImport("advapi32.dll")] - private static extern int LsaNtStatusToWinError(long status); - - [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)] - private static extern uint LsaOpenPolicy(ref LSA_UNICODE_STRING SystemName, ref LSA_OBJECT_ATTRIBUTES ObjectAttributes, uint DesiredAccess, out IntPtr PolicyHandle ); - - [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)] - static extern uint LsaRemoveAccountRights( - IntPtr PolicyHandle, - IntPtr AccountSid, - [MarshalAs(UnmanagedType.U1)] - bool AllRights, - LSA_UNICODE_STRING[] UserRights, - uint CountOfRights); - // ReSharper restore InconsistentNaming - - private static IntPtr GetIdentitySid(string identity) - { - var sid = - new NTAccount(identity).Translate(typeof (SecurityIdentifier)) as SecurityIdentifier; - if (sid == null) - { - throw new ArgumentException(string.Format("Account {0} not found.", identity)); - } - var sidBytes = new byte[sid.BinaryLength]; - sid.GetBinaryForm(sidBytes, 0); - var sidPtr = Marshal.AllocHGlobal(sidBytes.Length); - Marshal.Copy(sidBytes, 0, sidPtr, sidBytes.Length); - return sidPtr; - } - - private static IntPtr GetLsaPolicyHandle() - { - var computerName = Environment.MachineName; - IntPtr hPolicy; - var objectAttributes = new LSA_OBJECT_ATTRIBUTES - { - Length = 0, - RootDirectory = IntPtr.Zero, - Attributes = 0, - SecurityDescriptor = IntPtr.Zero, - SecurityQualityOfService = IntPtr.Zero - }; - - const uint ACCESS_MASK = POLICY_CREATE_SECRET | POLICY_LOOKUP_NAMES | POLICY_VIEW_LOCAL_INFORMATION; - var machineNameLsa = new LSA_UNICODE_STRING(computerName); - var result = LsaOpenPolicy(ref machineNameLsa, ref objectAttributes, ACCESS_MASK, out hPolicy); - HandleLsaResult(result); - return hPolicy; - } - - public static string[] GetPrivileges(string identity) - { - var sidPtr = GetIdentitySid(identity); - var hPolicy = GetLsaPolicyHandle(); - var rightsPtr = IntPtr.Zero; - - try - { - - var privileges = new List(); - - uint rightsCount; - var result = LsaEnumerateAccountRights(hPolicy, sidPtr, out rightsPtr, out rightsCount); - var win32ErrorCode = LsaNtStatusToWinError(result); - // the user has no privileges - if( win32ErrorCode == STATUS_OBJECT_NAME_NOT_FOUND ) - { - return new string[0]; - } - HandleLsaResult(result); - - var myLsaus = new LSA_UNICODE_STRING(); - for (ulong i = 0; i < rightsCount; i++) - { - var itemAddr = new IntPtr(rightsPtr.ToInt64() + (long) (i*(ulong) Marshal.SizeOf(myLsaus))); - myLsaus = (LSA_UNICODE_STRING) Marshal.PtrToStructure(itemAddr, myLsaus.GetType()); - var cvt = new char[myLsaus.Length/UnicodeEncoding.CharSize]; - Marshal.Copy(myLsaus.Buffer, cvt, 0, myLsaus.Length/UnicodeEncoding.CharSize); - var thisRight = new string(cvt); - privileges.Add(thisRight); - } - return privileges.ToArray(); - } - finally - { - Marshal.FreeHGlobal(sidPtr); - var result = LsaClose(hPolicy); - HandleLsaResult(result); - result = LsaFreeMemory(rightsPtr); - HandleLsaResult(result); - } - } - - public static void GrantPrivileges(string identity, string[] privileges) - { - var sidPtr = GetIdentitySid(identity); - var hPolicy = GetLsaPolicyHandle(); - - try - { - var lsaPrivileges = StringsToLsaStrings(privileges); - var result = LsaAddAccountRights(hPolicy, sidPtr, lsaPrivileges, (uint)lsaPrivileges.Length); - HandleLsaResult(result); - } - finally - { - Marshal.FreeHGlobal(sidPtr); - var result = LsaClose(hPolicy); - HandleLsaResult(result); - } - } - - const int STATUS_SUCCESS = 0x0; - const int STATUS_OBJECT_NAME_NOT_FOUND = 0x00000002; - const int STATUS_ACCESS_DENIED = 0x00000005; - const int STATUS_INVALID_HANDLE = 0x00000006; - const int STATUS_UNSUCCESSFUL = 0x0000001F; - const int STATUS_INVALID_PARAMETER = 0x00000057; - const int STATUS_NO_SUCH_PRIVILEGE = 0x00000521; - const int STATUS_INVALID_SERVER_STATE = 0x00000548; - const int STATUS_INTERNAL_DB_ERROR = 0x00000567; - const int STATUS_INSUFFICIENT_RESOURCES = 0x000005AA; - - private static readonly Dictionary ErrorMessages = new Dictionary - { - {STATUS_OBJECT_NAME_NOT_FOUND, "Object name not found. An object in the LSA policy database was not found. The object may have been specified either by SID or by name, depending on its type."}, - {STATUS_ACCESS_DENIED, "Access denied. Caller does not have the appropriate access to complete the operation."}, - {STATUS_INVALID_HANDLE, "Invalid handle. Indicates an object or RPC handle is not valid in the context used."}, - {STATUS_UNSUCCESSFUL, "Unsuccessful. Generic failure, such as RPC connection failure."}, - {STATUS_INVALID_PARAMETER, "Invalid parameter. One of the parameters is not valid."}, - {STATUS_NO_SUCH_PRIVILEGE, "No such privilege. Indicates a specified privilege does not exist."}, - {STATUS_INVALID_SERVER_STATE, "Invalid server state. Indicates the LSA server is currently disabled."}, - {STATUS_INTERNAL_DB_ERROR, "Internal database error. The LSA database contains an internal inconsistency."}, - {STATUS_INSUFFICIENT_RESOURCES, "Insufficient resources. There are not enough system resources (such as memory to allocate buffers) to complete the call."} - }; - - private static void HandleLsaResult(uint returnCode) - { - var win32ErrorCode = LsaNtStatusToWinError(returnCode); - - if( win32ErrorCode == STATUS_SUCCESS) - return; - - if( ErrorMessages.ContainsKey(win32ErrorCode) ) - { - throw new Win32Exception(win32ErrorCode, ErrorMessages[win32ErrorCode]); - } - - throw new Win32Exception(win32ErrorCode); - } - - public static void RevokePrivileges(string identity, string[] privileges) - { - var sidPtr = GetIdentitySid(identity); - var hPolicy = GetLsaPolicyHandle(); - - try - { - var currentPrivileges = GetPrivileges(identity); - if (currentPrivileges.Length == 0) - { - return; - } - var lsaPrivileges = StringsToLsaStrings(privileges); - var result = LsaRemoveAccountRights(hPolicy, sidPtr, false, lsaPrivileges, (uint)lsaPrivileges.Length); - HandleLsaResult(result); - } - finally - { - Marshal.FreeHGlobal(sidPtr); - var result = LsaClose(hPolicy); - HandleLsaResult(result); - } - - } - - private static LSA_UNICODE_STRING[] StringsToLsaStrings(string[] privileges) - { - var lsaPrivileges = new LSA_UNICODE_STRING[privileges.Length]; - for (var idx = 0; idx < privileges.Length; ++idx) - { - lsaPrivileges[idx] = new LSA_UNICODE_STRING(privileges[idx]); - } - return lsaPrivileges; - } - } + public sealed class Lsa + { + // ReSharper disable InconsistentNaming + [StructLayout(LayoutKind.Sequential)] + internal struct LSA_UNICODE_STRING + { + internal LSA_UNICODE_STRING(string inputString) + { + if (inputString == null) + { + Buffer = IntPtr.Zero; + Length = 0; + MaximumLength = 0; + } + else + { + Buffer = Marshal.StringToHGlobalAuto(inputString); + Length = (ushort)(inputString.Length * UnicodeEncoding.CharSize); + MaximumLength = (ushort)((inputString.Length + 1) * UnicodeEncoding.CharSize); + } + } + + internal ushort Length; + internal ushort MaximumLength; + internal IntPtr Buffer; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct LSA_OBJECT_ATTRIBUTES + { + internal uint Length; + internal IntPtr RootDirectory; + internal LSA_UNICODE_STRING ObjectName; + internal uint Attributes; + internal IntPtr SecurityDescriptor; + internal IntPtr SecurityQualityOfService; + } + + [StructLayout(LayoutKind.Sequential)] + public struct LUID + { + public uint LowPart; + public int HighPart; + } + + // ReSharper disable UnusedMember.Local + private const uint POLICY_VIEW_LOCAL_INFORMATION = 0x00000001; + private const uint POLICY_VIEW_AUDIT_INFORMATION = 0x00000002; + private const uint POLICY_GET_PRIVATE_INFORMATION = 0x00000004; + private const uint POLICY_TRUST_ADMIN = 0x00000008; + private const uint POLICY_CREATE_ACCOUNT = 0x00000010; + private const uint POLICY_CREATE_SECRET = 0x00000014; + private const uint POLICY_CREATE_PRIVILEGE = 0x00000040; + private const uint POLICY_SET_DEFAULT_QUOTA_LIMITS = 0x00000080; + private const uint POLICY_SET_AUDIT_REQUIREMENTS = 0x00000100; + private const uint POLICY_AUDIT_LOG_ADMIN = 0x00000200; + private const uint POLICY_SERVER_ADMIN = 0x00000400; + private const uint POLICY_LOOKUP_NAMES = 0x00000800; + private const uint POLICY_NOTIFICATION = 0x00001000; + // ReSharper restore UnusedMember.Local + + [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern bool LookupPrivilegeValue( + [MarshalAs(UnmanagedType.LPTStr)] string lpSystemName, + [MarshalAs(UnmanagedType.LPTStr)] string lpName, + out LUID lpLuid); + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode)] + private static extern uint LsaAddAccountRights( + IntPtr PolicyHandle, + IntPtr AccountSid, + LSA_UNICODE_STRING[] UserRights, + uint CountOfRights); + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = false)] + private static extern uint LsaClose(IntPtr ObjectHandle); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern uint LsaEnumerateAccountRights(IntPtr PolicyHandle, + IntPtr AccountSid, + out IntPtr UserRights, + out uint CountOfRights + ); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern uint LsaFreeMemory(IntPtr pBuffer); + + [DllImport("advapi32.dll")] + private static extern int LsaNtStatusToWinError(long status); + + [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)] + private static extern uint LsaOpenPolicy(ref LSA_UNICODE_STRING SystemName, ref LSA_OBJECT_ATTRIBUTES ObjectAttributes, uint DesiredAccess, out IntPtr PolicyHandle ); + + [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)] + static extern uint LsaRemoveAccountRights( + IntPtr PolicyHandle, + IntPtr AccountSid, + [MarshalAs(UnmanagedType.U1)] + bool AllRights, + LSA_UNICODE_STRING[] UserRights, + uint CountOfRights); + // ReSharper restore InconsistentNaming + + private static IntPtr GetIdentitySid(string identity) + { + var sid = + new NTAccount(identity).Translate(typeof (SecurityIdentifier)) as SecurityIdentifier; + if (sid == null) + { + throw new ArgumentException(string.Format("Account {0} not found.", identity)); + } + var sidBytes = new byte[sid.BinaryLength]; + sid.GetBinaryForm(sidBytes, 0); + var sidPtr = Marshal.AllocHGlobal(sidBytes.Length); + Marshal.Copy(sidBytes, 0, sidPtr, sidBytes.Length); + return sidPtr; + } + + private static IntPtr GetLsaPolicyHandle() + { + var computerName = Environment.MachineName; + IntPtr hPolicy; + var objectAttributes = new LSA_OBJECT_ATTRIBUTES + { + Length = 0, + RootDirectory = IntPtr.Zero, + Attributes = 0, + SecurityDescriptor = IntPtr.Zero, + SecurityQualityOfService = IntPtr.Zero + }; + + const uint ACCESS_MASK = POLICY_CREATE_SECRET | POLICY_LOOKUP_NAMES | POLICY_VIEW_LOCAL_INFORMATION; + var machineNameLsa = new LSA_UNICODE_STRING(computerName); + var result = LsaOpenPolicy(ref machineNameLsa, ref objectAttributes, ACCESS_MASK, out hPolicy); + HandleLsaResult(result); + return hPolicy; + } + + public static string[] GetPrivileges(string identity) + { + var sidPtr = GetIdentitySid(identity); + var hPolicy = GetLsaPolicyHandle(); + var rightsPtr = IntPtr.Zero; + + try + { + + var privileges = new List(); + + uint rightsCount; + var result = LsaEnumerateAccountRights(hPolicy, sidPtr, out rightsPtr, out rightsCount); + var win32ErrorCode = LsaNtStatusToWinError(result); + // the user has no privileges + if( win32ErrorCode == STATUS_OBJECT_NAME_NOT_FOUND ) + { + return new string[0]; + } + HandleLsaResult(result); + + var myLsaus = new LSA_UNICODE_STRING(); + for (ulong i = 0; i < rightsCount; i++) + { + var itemAddr = new IntPtr(rightsPtr.ToInt64() + (long) (i*(ulong) Marshal.SizeOf(myLsaus))); + myLsaus = (LSA_UNICODE_STRING) Marshal.PtrToStructure(itemAddr, myLsaus.GetType()); + var cvt = new char[myLsaus.Length/UnicodeEncoding.CharSize]; + Marshal.Copy(myLsaus.Buffer, cvt, 0, myLsaus.Length/UnicodeEncoding.CharSize); + var thisRight = new string(cvt); + privileges.Add(thisRight); + } + return privileges.ToArray(); + } + finally + { + Marshal.FreeHGlobal(sidPtr); + var result = LsaClose(hPolicy); + HandleLsaResult(result); + result = LsaFreeMemory(rightsPtr); + HandleLsaResult(result); + } + } + + public static void GrantPrivileges(string identity, string[] privileges) + { + var sidPtr = GetIdentitySid(identity); + var hPolicy = GetLsaPolicyHandle(); + + try + { + var lsaPrivileges = StringsToLsaStrings(privileges); + var result = LsaAddAccountRights(hPolicy, sidPtr, lsaPrivileges, (uint)lsaPrivileges.Length); + HandleLsaResult(result); + } + finally + { + Marshal.FreeHGlobal(sidPtr); + var result = LsaClose(hPolicy); + HandleLsaResult(result); + } + } + + const int STATUS_SUCCESS = 0x0; + const int STATUS_OBJECT_NAME_NOT_FOUND = 0x00000002; + const int STATUS_ACCESS_DENIED = 0x00000005; + const int STATUS_INVALID_HANDLE = 0x00000006; + const int STATUS_UNSUCCESSFUL = 0x0000001F; + const int STATUS_INVALID_PARAMETER = 0x00000057; + const int STATUS_NO_SUCH_PRIVILEGE = 0x00000521; + const int STATUS_INVALID_SERVER_STATE = 0x00000548; + const int STATUS_INTERNAL_DB_ERROR = 0x00000567; + const int STATUS_INSUFFICIENT_RESOURCES = 0x000005AA; + + private static Dictionary ErrorMessages = new Dictionary + { + {STATUS_OBJECT_NAME_NOT_FOUND, "Object name not found. An object in the LSA policy database was not found. The object may have been specified either by SID or by name, depending on its type."}, + {STATUS_ACCESS_DENIED, "Access denied. Caller does not have the appropriate access to complete the operation."}, + {STATUS_INVALID_HANDLE, "Invalid handle. Indicates an object or RPC handle is not valid in the context used."}, + {STATUS_UNSUCCESSFUL, "Unsuccessful. Generic failure, such as RPC connection failure."}, + {STATUS_INVALID_PARAMETER, "Invalid parameter. One of the parameters is not valid."}, + {STATUS_NO_SUCH_PRIVILEGE, "No such privilege. Indicates a specified privilege does not exist."}, + {STATUS_INVALID_SERVER_STATE, "Invalid server state. Indicates the LSA server is currently disabled."}, + {STATUS_INTERNAL_DB_ERROR, "Internal database error. The LSA database contains an internal inconsistency."}, + {STATUS_INSUFFICIENT_RESOURCES, "Insufficient resources. There are not enough system resources (such as memory to allocate buffers) to complete the call."} + }; + + private static void HandleLsaResult(uint returnCode) + { + var win32ErrorCode = LsaNtStatusToWinError(returnCode); + + if( win32ErrorCode == STATUS_SUCCESS) + return; + + if( ErrorMessages.ContainsKey(win32ErrorCode) ) + { + throw new Win32Exception(win32ErrorCode, ErrorMessages[win32ErrorCode]); + } + + throw new Win32Exception(win32ErrorCode); + } + + public static void RevokePrivileges(string identity, string[] privileges) + { + var sidPtr = GetIdentitySid(identity); + var hPolicy = GetLsaPolicyHandle(); + + try + { + var currentPrivileges = GetPrivileges(identity); + if (currentPrivileges.Length == 0) + { + return; + } + var lsaPrivileges = StringsToLsaStrings(privileges); + var result = LsaRemoveAccountRights(hPolicy, sidPtr, false, lsaPrivileges, (uint)lsaPrivileges.Length); + HandleLsaResult(result); + } + finally + { + Marshal.FreeHGlobal(sidPtr); + var result = LsaClose(hPolicy); + HandleLsaResult(result); + } + + } + + private static LSA_UNICODE_STRING[] StringsToLsaStrings(string[] privileges) + { + var lsaPrivileges = new LSA_UNICODE_STRING[privileges.Length]; + for (var idx = 0; idx < privileges.Length; ++idx) + { + lsaPrivileges[idx] = new LSA_UNICODE_STRING(privileges[idx]); + } + return lsaPrivileges; + } + } } "@ Add-Type -TypeDefinition $SourcePolicy -Language CSharp -$ServiceChangeErrors = @{} -$ServiceChangeErrors.Add(1, "Not Supported") -$ServiceChangeErrors.Add(2, "Access Denied") -$ServiceChangeErrors.Add(3, "Dependent Services Running") -$ServiceChangeErrors.Add(4, "Invalid Service Control") -$ServiceChangeErrors.Add(5, "Service Cannot Accept Control") -$ServiceChangeErrors.Add(6, "Service Not Active") -$ServiceChangeErrors.Add(7, "Service Request Timeout") -$ServiceChangeErrors.Add(8, "Unknown Failure") -$ServiceChangeErrors.Add(9, "Path Not Found") -$ServiceChangeErrors.Add(10, "Service Already Running") -$ServiceChangeErrors.Add(11, "Service Database Locked") -$ServiceChangeErrors.Add(12, "Service Dependency Deleted") -$ServiceChangeErrors.Add(13, "Service Dependency Failure") -$ServiceChangeErrors.Add(14, "Service Disabled") -$ServiceChangeErrors.Add(15, "Service Logon Failure") -$ServiceChangeErrors.Add(16, "Service Marked For Deletion") -$ServiceChangeErrors.Add(17, "Service No Thread") -$ServiceChangeErrors.Add(18, "Status Circular Dependency") -$ServiceChangeErrors.Add(19, "Status Duplicate Name") -$ServiceChangeErrors.Add(20, "Status Invalid Name") -$ServiceChangeErrors.Add(21, "Status Invalid Parameter") -$ServiceChangeErrors.Add(22, "Status Invalid Service Account") -$ServiceChangeErrors.Add(23, "Status Service Exists") -$ServiceChangeErrors.Add(24, "Service Already Paused") - - function SetAssignPrimaryTokenPrivilege($UserName) { - $privilege = "SeAssignPrimaryTokenPrivilege" - if (![PSCarbon.Lsa]::GetPrivileges($UserName).Contains($privilege)) - { - [PSCarbon.Lsa]::GrantPrivileges($UserName, $privilege) - } + $privilege = "SeAssignPrimaryTokenPrivilege" + if (![PSCarbon.Lsa]::GetPrivileges($UserName).Contains($privilege)) + { + [PSCarbon.Lsa]::GrantPrivileges($UserName, $privilege) + } } function SetUserLogonAsServiceRights($UserName) { - $privilege = "SeServiceLogonRight" - if (![PSCarbon.Lsa]::GetPrivileges($UserName).Contains($privilege)) - { - [PSCarbon.Lsa]::GrantPrivileges($UserName, $privilege) - } + $privilege = "SeServiceLogonRight" + if (![PSCarbon.Lsa]::GetPrivileges($UserName).Contains($privilege)) + { + [PSCarbon.Lsa]::GrantPrivileges($UserName, $privilege) + } } $Source = @" using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Net; using System.Text; -using System.Runtime.InteropServices; -using System.Security.Principal; -using System.ComponentModel; -namespace PSCloudbase +namespace Tarer { - public class ProcessManager - { - const int LOGON32_LOGON_SERVICE = 5; - const int LOGON32_PROVIDER_DEFAULT = 0; - const int TOKEN_ALL_ACCESS = 0x000f01ff; - const uint GENERIC_ALL_ACCESS = 0x10000000; - const uint INFINITE = 0xFFFFFFFF; - const uint PI_NOUI = 0x00000001; - const uint WAIT_FAILED = 0xFFFFFFFF; - - enum SECURITY_IMPERSONATION_LEVEL - { - SecurityAnonymous, - SecurityIdentification, - SecurityImpersonation, - SecurityDelegation - } - - enum TOKEN_TYPE - { - TokenPrimary = 1, - TokenImpersonation - } - - [StructLayout(LayoutKind.Sequential)] - struct SECURITY_ATTRIBUTES - { - public int nLength; - public IntPtr lpSecurityDescriptor; - public int bInheritHandle; - } - - [StructLayout(LayoutKind.Sequential)] - struct PROCESS_INFORMATION - { - public IntPtr hProcess; - public IntPtr hThread; - public int dwProcessId; - public int dwThreadId; - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - struct STARTUPINFO - { - public Int32 cb; - public string lpReserved; - public string lpDesktop; - public string lpTitle; - public Int32 dwX; - public Int32 dwY; - public Int32 dwXSize; - public Int32 dwYSize; - public Int32 dwXCountChars; - public Int32 dwYCountChars; - public Int32 dwFillAttribute; - public Int32 dwFlags; - public Int16 wShowWindow; - public Int16 cbReserved2; - public IntPtr lpReserved2; - public IntPtr hStdInput; - public IntPtr hStdOutput; - public IntPtr hStdError; - } - - [StructLayout(LayoutKind.Sequential)] - struct PROFILEINFO { - public int dwSize; - public uint dwFlags; - [MarshalAs(UnmanagedType.LPTStr)] - public String lpUserName; - [MarshalAs(UnmanagedType.LPTStr)] - public String lpProfilePath; - [MarshalAs(UnmanagedType.LPTStr)] - public String lpDefaultPath; - [MarshalAs(UnmanagedType.LPTStr)] - public String lpServerName; - [MarshalAs(UnmanagedType.LPTStr)] - public String lpPolicyPath; - public IntPtr hProfile; - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - public struct USER_INFO_4 - { - public string name; - public string password; - public int password_age; - public uint priv; - public string home_dir; - public string comment; - public uint flags; - public string script_path; - public uint auth_flags; - public string full_name; - public string usr_comment; - public string parms; - public string workstations; - public int last_logon; - public int last_logoff; - public int acct_expires; - public int max_storage; - public int units_per_week; - public IntPtr logon_hours; // This is a PBYTE - public int bad_pw_count; - public int num_logons; - public string logon_server; - public int country_code; - public int code_page; - public IntPtr user_sid; // This is a PSID - public int primary_group_id; - public string profile; - public string home_dir_drive; - public int password_expired; - } - - [DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)] - extern static bool DuplicateTokenEx( - IntPtr hExistingToken, - uint dwDesiredAccess, - ref SECURITY_ATTRIBUTES lpTokenAttributes, - SECURITY_IMPERSONATION_LEVEL ImpersonationLevel, - TOKEN_TYPE TokenType, - out IntPtr phNewToken); - - [DllImport("advapi32.dll", SetLastError=true)] - static extern bool LogonUser( - string lpszUsername, - string lpszDomain, - string lpszPassword, - int dwLogonType, - int dwLogonProvider, - out IntPtr phToken); - - [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Auto)] - static extern bool CreateProcessAsUser( - IntPtr hToken, - string lpApplicationName, - string lpCommandLine, - ref SECURITY_ATTRIBUTES lpProcessAttributes, - ref SECURITY_ATTRIBUTES lpThreadAttributes, - bool bInheritHandles, - uint dwCreationFlags, - IntPtr lpEnvironment, - string lpCurrentDirectory, - ref STARTUPINFO lpStartupInfo, - out PROCESS_INFORMATION lpProcessInformation); - - [DllImport("kernel32.dll", SetLastError=true)] - static extern UInt32 WaitForSingleObject(IntPtr hHandle, - UInt32 dwMilliseconds); - - [DllImport("Kernel32.dll")] - static extern int GetLastError(); - - [DllImport("Kernel32.dll")] - extern static int CloseHandle(IntPtr handle); - - [DllImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - static extern bool GetExitCodeProcess(IntPtr hProcess, - out uint lpExitCode); - - [DllImport("userenv.dll", SetLastError=true, CharSet=CharSet.Auto)] - [return: MarshalAs(UnmanagedType.Bool)] - static extern bool LoadUserProfile(IntPtr hToken, - ref PROFILEINFO lpProfileInfo); - - [DllImport("userenv.dll", SetLastError=true, CharSet=CharSet.Auto)] - [return: MarshalAs(UnmanagedType.Bool)] - static extern bool UnloadUserProfile(IntPtr hToken, IntPtr hProfile); - - [DllImport("Netapi32.dll", CharSet=CharSet.Unicode, ExactSpelling=true)] - extern static int NetUserGetInfo( - [MarshalAs(UnmanagedType.LPWStr)] string ServerName, - [MarshalAs(UnmanagedType.LPWStr)] string UserName, - int level, out IntPtr BufPtr); - - public static uint RunProcess(string userName, string password, - string domain, string cmd, - string arguments, - bool loadUserProfile = true) - { - bool retValue; - IntPtr phToken = IntPtr.Zero; - IntPtr phTokenDup = IntPtr.Zero; - PROCESS_INFORMATION pInfo = new PROCESS_INFORMATION(); - PROFILEINFO pi = new PROFILEINFO(); - - try - { - retValue = LogonUser(userName, domain, password, - LOGON32_LOGON_SERVICE, - LOGON32_PROVIDER_DEFAULT, - out phToken); - if(!retValue) - throw new Win32Exception(GetLastError()); - - var sa = new SECURITY_ATTRIBUTES(); - sa.nLength = Marshal.SizeOf(sa); - - retValue = DuplicateTokenEx( - phToken, GENERIC_ALL_ACCESS, ref sa, - SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, - TOKEN_TYPE.TokenPrimary, out phTokenDup); - if(!retValue) - throw new Win32Exception(GetLastError()); - - STARTUPINFO sInfo = new STARTUPINFO(); - sInfo.lpDesktop = ""; - - if(loadUserProfile) - { - IntPtr userInfoPtr = IntPtr.Zero; - int retValueNetUser = NetUserGetInfo(null, userName, 4, - out userInfoPtr); - if(retValueNetUser != 0) - throw new Win32Exception(retValueNetUser); - - USER_INFO_4 userInfo = (USER_INFO_4)Marshal.PtrToStructure( - userInfoPtr, typeof(USER_INFO_4)); - - pi.dwSize = Marshal.SizeOf(pi); - pi.dwFlags = PI_NOUI; - pi.lpUserName = userName; - pi.lpProfilePath = userInfo.profile; - - retValue = LoadUserProfile(phTokenDup, ref pi); - if(!retValue) - throw new Win32Exception(GetLastError()); - } - - retValue = CreateProcessAsUser(phTokenDup, cmd, arguments, - ref sa, ref sa, false, 0, - IntPtr.Zero, null, - ref sInfo, out pInfo); - if(!retValue) - throw new Win32Exception(GetLastError()); - - if(WaitForSingleObject(pInfo.hProcess, INFINITE) == WAIT_FAILED) - throw new Win32Exception(GetLastError()); - - uint exitCode; - retValue = GetExitCodeProcess(pInfo.hProcess, out exitCode); - if(!retValue) - throw new Win32Exception(GetLastError()); - - return exitCode; - } - finally - { - if(pi.hProfile != IntPtr.Zero) - UnloadUserProfile(phTokenDup, pi.hProfile); - if(phToken != IntPtr.Zero) - CloseHandle(phToken); - if(phTokenDup != IntPtr.Zero) - CloseHandle(phTokenDup); - if(pInfo.hProcess != IntPtr.Zero) - CloseHandle(pInfo.hProcess); - } - } - } + public enum EntryType : byte + { + File = 0, + FileObsolete = 0x30, + HardLink = 0x31, + SymLink = 0x32, + CharDevice = 0x33, + BlockDevice = 0x34, + Directory = 0x35, + Fifo = 0x36, + } + + public interface ITarHeader + { + string FileName { get; set; } + long SizeInBytes { get; set; } + DateTime LastModification { get; set; } + int HeaderSize { get; } + EntryType EntryType { get; set; } + } + + public class Tar + { + private byte[] dataBuffer = new byte[512]; + private UsTarHeader header; + private Stream inStream; + private long remainingBytesInFile; + + public Tar(Stream tarredData) { + inStream = tarredData; + header = new UsTarHeader(); + } + + public ITarHeader FileInfo + { + get { return header; } + } + + public void ReadToEnd(string destDirectory) + { + while (MoveNext()) + { + string fileNameFromArchive = FileInfo.FileName; + string totalPath = destDirectory + Path.DirectorySeparatorChar + fileNameFromArchive; + if(UsTarHeader.IsPathSeparator(fileNameFromArchive[fileNameFromArchive.Length -1]) || FileInfo.EntryType == EntryType.Directory) + { + Directory.CreateDirectory(totalPath); + continue; + } + string fileName = Path.GetFileName(totalPath); + string directory = totalPath.Remove(totalPath.Length - fileName.Length); + Directory.CreateDirectory(directory); + using (FileStream file = File.Create(totalPath)) + { + Read(file); + } + } + } + + public void Read(Stream dataDestination) + { + int readBytes; + byte[] read; + while ((readBytes = Read(out read)) != -1) + { + dataDestination.Write(read, 0, readBytes); + } + } + + protected int Read(out byte[] buffer) + { + if(remainingBytesInFile == 0) + { + buffer = null; + return -1; + } + int align512 = -1; + long toRead = remainingBytesInFile - 512; + + if (toRead > 0) + { + toRead = 512; + } + else + { + align512 = 512 - (int)remainingBytesInFile; + toRead = remainingBytesInFile; + } + + int bytesRead = 0; + long bytesRemainingToRead = toRead; + while (bytesRead < toRead && bytesRemainingToRead > 0) + { + bytesRead = inStream.Read(dataBuffer, (int)(toRead-bytesRemainingToRead), (int)bytesRemainingToRead); + bytesRemainingToRead -= bytesRead; + remainingBytesInFile -= bytesRead; + } + + if(inStream.CanSeek && align512 > 0) + { + inStream.Seek(align512, SeekOrigin.Current); + } + else + { + while(align512 > 0) + { + inStream.ReadByte(); + --align512; + } + } + + buffer = dataBuffer; + return bytesRead; + } + + private static bool IsEmpty(IEnumerable buffer) + { + foreach(byte b in buffer) + { + if (b != 0) + { + return false; + } + } + return true; + } + + public bool MoveNext() + { + byte[] bytes = header.GetBytes(); + int headerRead; + int bytesRemaining = header.HeaderSize; + while (bytesRemaining > 0) + { + headerRead = inStream.Read(bytes, header.HeaderSize - bytesRemaining, bytesRemaining); + bytesRemaining -= headerRead; + if (headerRead <= 0 && bytesRemaining > 0) + { + throw new Exception("Error reading tar header. Header size invalid"); + } + } + + if(IsEmpty(bytes)) + { + bytesRemaining = header.HeaderSize; + while (bytesRemaining > 0) + { + headerRead = inStream.Read(bytes, header.HeaderSize - bytesRemaining, bytesRemaining); + bytesRemaining -= headerRead; + if (headerRead <= 0 && bytesRemaining > 0) + { + throw new Exception("Broken archive"); + } + } + if (bytesRemaining == 0 && IsEmpty(bytes)) + { + return false; + } + throw new Exception("Error occured: expected end of archive"); + } + + if (!header.UpdateHeaderFromBytes()) + { + throw new Exception("Checksum check failed"); + } + + remainingBytesInFile = header.SizeInBytes; + return true; + } + } + + internal class TarHeader : ITarHeader + { + private byte[] buffer = new byte[512]; + private long headerChecksum; + + private string fileName; + protected DateTime dateTime1970 = new DateTime(1970, 1, 1, 0, 0, 0); + public EntryType EntryType { get; set; } + private static byte[] spaces = Encoding.ASCII.GetBytes(" "); + + public virtual string FileName + { + get { return fileName.Replace("\0",string.Empty); } + set { fileName = value; } + } + + public long SizeInBytes { get; set; } + + public string SizeString { get { return Convert.ToString(SizeInBytes, 8).PadLeft(11, '0'); } } + + public DateTime LastModification { get; set; } + + public virtual int HeaderSize { get { return 512; } } + + public byte[] GetBytes() + { + return buffer; + } + + public virtual bool UpdateHeaderFromBytes() + { + FileName = Encoding.UTF8.GetString(buffer, 0, 100); + + EntryType = (EntryType)buffer[156]; + + if((buffer[124] & 0x80) == 0x80) // if size in binary + { + long sizeBigEndian = BitConverter.ToInt64(buffer,0x80); + SizeInBytes = IPAddress.NetworkToHostOrder(sizeBigEndian); + } + else + { + SizeInBytes = Convert.ToInt64(Encoding.ASCII.GetString(buffer, 124, 11).Trim(), 8); + } + long unixTimeStamp = Convert.ToInt64(Encoding.ASCII.GetString(buffer,136,11).Trim(),8); + LastModification = dateTime1970.AddSeconds(unixTimeStamp); + + var storedChecksum = Convert.ToInt64(Encoding.ASCII.GetString(buffer,148,6).Trim(), 8); + RecalculateChecksum(buffer); + if (storedChecksum == headerChecksum) + { + return true; + } + + RecalculateAltChecksum(buffer); + return storedChecksum == headerChecksum; + } + + private void RecalculateAltChecksum(byte[] buf) + { + spaces.CopyTo(buf, 148); + headerChecksum = 0; + foreach(byte b in buf) + { + if((b & 0x80) == 0x80) + { + headerChecksum -= b ^ 0x80; + } + else + { + headerChecksum += b; + } + } + } + + protected virtual void RecalculateChecksum(byte[] buf) + { + // Set default value for checksum. That is 8 spaces. + spaces.CopyTo(buf, 148); + // Calculate checksum + headerChecksum = 0; + foreach (byte b in buf) + { + headerChecksum += b; + } + } + } + internal class UsTarHeader : TarHeader + { + private const string magic = "ustar"; + private const string version = " "; + + private string namePrefix = string.Empty; + + public override string FileName + { + get { return namePrefix.Replace("\0", string.Empty) + base.FileName.Replace("\0", string.Empty); } + set + { + if (value.Length > 255) + { + throw new Exception("UsTar fileName can not be longer than 255 chars"); + } + if (value.Length > 100) + { + int position = value.Length - 100; + while (!IsPathSeparator(value[position])) + { + ++position; + if (position == value.Length) + { + break; + } + } + if (position == value.Length) + { + position = value.Length - 100; + } + namePrefix = value.Substring(0, position); + base.FileName = value.Substring(position, value.Length - position); + } + else + { + base.FileName = value; + } + } + } + + public override bool UpdateHeaderFromBytes() + { + byte[] bytes = GetBytes(); + namePrefix = Encoding.UTF8.GetString(bytes, 347, 157); + return base.UpdateHeaderFromBytes(); + } + + internal static bool IsPathSeparator(char ch) + { + return (ch == '\\' || ch == '/' || ch == '|'); + } + } } "@ Add-Type -TypeDefinition $Source -Language CSharp -function Start-ProcessAsUser -{ - [CmdletBinding()] - param - ( - [parameter(Mandatory=$true, ValueFromPipeline=$true)] - [string]$Command, - - [parameter()] - [string]$Arguments, - - [parameter(Mandatory=$true)] - [PSCredential]$Credential, - - [parameter()] - [bool]$LoadUserProfile = $true - ) - process - { - $nc = $Credential.GetNetworkCredential() - - $domain = "." - if($nc.Domain) - { - $domain = $nc.Domain - } - - [PSCloudbase.ProcessManager]::RunProcess($nc.UserName, $nc.Password, - $domain, $Command, - $Arguments, $LoadUserProfile) - } -} - -$powershell = "$ENV:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -$cmdExe = "$ENV:SystemRoot\System32\cmd.exe" +Function GUnZip-File{ + Param( + $infile, + $outdir + ) + + $input = New-Object System.IO.FileStream $inFile, ([IO.FileMode]::Open), ([IO.FileAccess]::Read), ([IO.FileShare]::Read) + $tempFile = "$env:TEMP\jujud.tar" + $tempOut = New-Object System.IO.FileStream $tempFile, ([IO.FileMode]::Create), ([IO.FileAccess]::Write), ([IO.FileShare]::None) + $gzipStream = New-Object System.IO.Compression.GzipStream $input, ([IO.Compression.CompressionMode]::Decompress) + + $buffer = New-Object byte[](1024) + while($true){ + $read = $gzipstream.Read($buffer, 0, 1024) + if ($read -le 0){break} + $tempOut.Write($buffer, 0, $read) + } + $gzipStream.Close() + $tempOut.Close() + $input.Close() + + $in = New-Object System.IO.FileStream $tempFile, ([IO.FileMode]::Open), ([IO.FileAccess]::Read), ([IO.FileShare]::Read) + $tar = New-Object Tarer.Tar($in) + $tar.ReadToEnd($outdir) + $in.Close() + rm $tempFile +} + +Function Get-FileSHA256{ + Param( + $FilePath + ) + $hash = [Security.Cryptography.HashAlgorithm]::Create( "SHA256" ) + $stream = ([IO.StreamReader]$FilePath).BaseStream + $res = -join ($hash.ComputeHash($stream) | ForEach { "{0:x2}" -f $_ }) + $stream.Close() + return $res +} $juju_passwd = Get-RandomPassword 20 $juju_passwd += "^" @@ -799,45 +830,56 @@ SetUserLogonAsServiceRights $juju_user SetAssignPrimaryTokenPrivilege $juju_user -New-ItemProperty "HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList" -Name "jujud" -Value 0 -PropertyType "DWord" +$path = "HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList" +if(!(Test-Path $path)){ + New-Item -Path $path -force +} +New-ItemProperty $path -Name "jujud" -Value 0 -PropertyType "DWord" $secpasswd = ConvertTo-SecureString $juju_passwd -AsPlainText -Force $jujuCreds = New-Object System.Management.Automation.PSCredential ($juju_user, $secpasswd) -icacls "C:\Juju" /grant "jujud:(OI)(CI)(F)" /T +mkdir -Force "C:\Juju" mkdir C:\Juju\tmp mkdir "C:\Juju\bin" - - -Set-Content "C:\juju\bin\save_pass.ps1" @" -Param ( - [Parameter(Mandatory=` + "`" + `$true)] - [string]` + "`" + `$pass -) - -` + "`" + `$secpasswd = ConvertTo-SecureString ` + "`" + `$pass -AsPlainText -Force -` + "`" + `$secpasswd | convertfrom-securestring | Add-Content C:\Juju\Jujud.pass -"@ - - -Start-ProcessAsUser -Command $powershell -Arguments "-File C:\juju\bin\save_pass.ps1 $juju_passwd" -Credential $jujuCreds mkdir "C:\Juju\lib\juju\locks" -Start-ProcessAsUser -Command $cmdExe -Arguments '/C setx PATH "%PATH%` + ";" + `C:\Juju\bin"' -Credential $jujuCreds +$adminsGroup = (New-Object System.Security.Principal.SecurityIdentifier("S-1-5-32-544")).Translate([System.Security.Principal.NTAccount]) +$acl = Get-Acl -Path 'C:\Juju' +$acl.SetAccessRuleProtection($true, $false) +$adminPerm = "$adminsGroup", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" +$jujudPerm = "jujud", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" +$rule = New-Object System.Security.AccessControl.FileSystemAccessRule $adminPerm +$acl.AddAccessRule($rule) +$rule = New-Object System.Security.AccessControl.FileSystemAccessRule $jujudPerm +$acl.AddAccessRule($rule) +Set-Acl -Path 'C:\Juju' -AclObject $acl +setx /m PATH "$env:PATH;C:\Juju\bin\" Set-Content "C:\Juju\lib\juju\nonce.txt" "'FAKE_NONCE'" $binDir="C:\Juju\lib\juju\tools\1.2.3-win8-amd64" -$tmpBinDir=$binDir.Replace('\', '\\') mkdir 'C:\Juju\log\juju' mkdir $binDir $WebClient = New-Object System.Net.WebClient [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true} ExecRetry { $WebClient.DownloadFile('http://foo.com/tools/released/juju1.2.3-win8-amd64.tgz', "$binDir\tools.tar.gz") } -$dToolsHash = (Get-FileHash -Algorithm SHA256 "$binDir\tools.tar.gz").hash +$dToolsHash = Get-FileSHA256 -FilePath "$binDir\tools.tar.gz" $dToolsHash > "$binDir\juju1.2.3-win8-amd64.sha256" if ($dToolsHash.ToLower() -ne "1234"){ Throw "Tools checksum mismatch"} -& "${env:ProgramFiles(x86)}\Cloudbase Solutions\Cloudbase-Init\Python27\python.exe" -c "import tarfile;archive = tarfile.open('$tmpBinDir\\tools.tar.gz');archive.extractall(path='$tmpBinDir')" +GUnZip-File -infile $binDir\tools.tar.gz -outdir $binDir rm "$binDir\tools.tar*" Set-Content $binDir\downloaded-tools.txt '{"version":"1.2.3-win8-amd64","url":"http://foo.com/tools/released/juju1.2.3-win8-amd64.tgz","sha256":"1234","size":10}' +New-Item -Path 'HKLM:\SOFTWARE\juju-core' +$acl = Get-Acl -Path 'HKLM:\SOFTWARE\juju-core' +$acl.SetAccessRuleProtection($true, $false) +$adminPerm = "$adminsGroup", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" +$jujudPerm = "jujud", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" +$rule = New-Object System.Security.AccessControl.RegistryAccessRule $adminPerm +$acl.AddAccessRule($rule) +$rule = New-Object System.Security.AccessControl.RegistryAccessRule $jujudPerm +$acl.AddAccessRule($rule) +Set-Acl -Path 'HKLM:\SOFTWARE\juju-core' -AclObject $acl +New-ItemProperty -Path 'HKLM:\SOFTWARE\juju-core' -Name 'JUJU_DEV_FEATURE_FLAGS' +Set-ItemProperty -Path 'HKLM:\SOFTWARE\juju-core' -Name 'JUJU_DEV_FEATURE_FLAGS' -Value '' mkdir 'C:\Juju\lib\juju\agents\machine-10' Set-Content 'C:/Juju/lib/juju/agents/machine-10/agent.conf' @" # format 1.18 === modified file 'src/github.com/juju/juju/cmd/envcmd/environmentcommand.go' --- src/github.com/juju/juju/cmd/envcmd/environmentcommand.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/envcmd/environmentcommand.go 2015-10-23 18:29:32 +0000 @@ -1,16 +1,12 @@ -// Copyright 2013 Canonical Ltd. +// Copyright 2013-2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package envcmd import ( - "fmt" "io" - "io/ioutil" "os" - "path/filepath" "strconv" - "strings" "github.com/juju/cmd" "github.com/juju/errors" @@ -28,40 +24,11 @@ var logger = loggo.GetLogger("juju.cmd.envcmd") -const CurrentEnvironmentFilename = "current-environment" - // ErrNoEnvironmentSpecified is returned by commands that operate on // an environment if there is no current environment, no environment // has been explicitly specified, and there is no default environment. var ErrNoEnvironmentSpecified = errors.New("no environment specified") -func getCurrentEnvironmentFilePath() string { - return filepath.Join(osenv.JujuHome(), CurrentEnvironmentFilename) -} - -// Read the file $JUJU_HOME/current-environment and return the value stored -// there. If the file doesn't exist, or there is a problem reading the file, -// an empty string is returned. -func ReadCurrentEnvironment() string { - current, err := ioutil.ReadFile(getCurrentEnvironmentFilePath()) - // The file not being there, or not readable isn't really an error for us - // here. We treat it as "can't tell, so you get the default". - if err != nil { - return "" - } - return strings.TrimSpace(string(current)) -} - -// Write the envName to the file $JUJU_HOME/current-environment file. -func WriteCurrentEnvironment(envName string) error { - path := getCurrentEnvironmentFilePath() - err := ioutil.WriteFile(path, []byte(envName+"\n"), 0644) - if err != nil { - return fmt.Errorf("unable to write to the environment file: %q, %s", path, err) - } - return nil -} - // GetDefaultEnvironment returns the name of the Juju default environment. // There is simple ordering for the default environment. Firstly check the // JUJU_ENV environment variable. If that is set, it gets used. If it isn't @@ -73,9 +40,16 @@ if defaultEnv := os.Getenv(osenv.JujuEnvEnvKey); defaultEnv != "" { return defaultEnv, nil } - if currentEnv := ReadCurrentEnvironment(); currentEnv != "" { + if currentEnv, err := ReadCurrentEnvironment(); err != nil { + return "", errors.Trace(err) + } else if currentEnv != "" { return currentEnv, nil } + if currentSystem, err := ReadCurrentSystem(); err != nil { + return "", errors.Trace(err) + } else if currentSystem != "" { + return "", errors.Errorf("not operating on an environment, using system %q", currentSystem) + } envs, err := environs.ReadEnvirons("") if environs.IsNoEnv(err) { // That's fine, not an error here. @@ -108,6 +82,9 @@ // compatVersion defines the minimum CLI version // that this command should be compatible with. compatVerson *int + + envGetterClient EnvironmentGetter + envGetterErr error } func (c *EnvCommandBase) SetEnvName(envName string) { @@ -122,7 +99,21 @@ return root.Client(), nil } -func (c *EnvCommandBase) NewAPIRoot() (*api.State, error) { +// NewEnvironmentGetter returns a new object which implements the +// EnvironmentGetter interface. +func (c *EnvCommandBase) NewEnvironmentGetter() (EnvironmentGetter, error) { + if c.envGetterErr != nil { + return nil, c.envGetterErr + } + + if c.envGetterClient != nil { + return c.envGetterClient, nil + } + + return c.NewAPIClient() +} + +func (c *EnvCommandBase) NewAPIRoot() (api.Connection, error) { // This is work in progress as we remove the EnvName from downstream code. // We want to be able to specify the environment in a number of ways, one of // which is the connection name on the client machine. @@ -132,12 +123,34 @@ return juju.NewAPIFromName(c.envName) } -func (c *EnvCommandBase) Config(store configstore.Storage) (*config.Config, error) { +// Config returns the configuration for the environment; obtaining bootstrap +// information from the API if necessary. If callers already have an active +// client API connection, it will be used. Otherwise, a new API connection will +// be used if necessary. +func (c *EnvCommandBase) Config(store configstore.Storage, client EnvironmentGetter) (*config.Config, error) { if c.envName == "" { return nil, errors.Trace(ErrNoEnvironmentSpecified) } cfg, _, err := environs.ConfigForName(c.envName, store) - return cfg, err + if err == nil { + return cfg, nil + } else if !environs.IsEmptyConfig(err) { + return nil, errors.Trace(err) + } + + if client == nil { + client, err = c.NewEnvironmentGetter() + if err != nil { + return nil, errors.Trace(err) + } + defer client.Close() + } + + bootstrapCfg, err := client.EnvironmentGet() + if err != nil { + return nil, errors.Trace(err) + } + return config.New(config.NoDefaults, bootstrapCfg) } // ConnectionCredentials returns the credentials used to connect to the API for @@ -328,6 +341,7 @@ type EnvironmentGetter interface { EnvironmentGet() (map[string]interface{}, error) + Close() error } // GetEnvironmentVersion retrieves the environment's agent-version === modified file 'src/github.com/juju/juju/cmd/envcmd/environmentcommand_test.go' --- src/github.com/juju/juju/cmd/envcmd/environmentcommand_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/envcmd/environmentcommand_test.go 2015-10-23 18:29:32 +0000 @@ -5,9 +5,7 @@ import ( "io" - "io/ioutil" "os" - "testing" "github.com/juju/cmd" "github.com/juju/cmd/cmdtesting" @@ -17,14 +15,15 @@ gc "gopkg.in/check.v1" "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/environs/config" "github.com/juju/juju/environs/configstore" "github.com/juju/juju/juju/osenv" - coretesting "github.com/juju/juju/testing" + "github.com/juju/juju/testing" "github.com/juju/juju/version" ) type EnvironmentCommandSuite struct { - coretesting.FakeJujuHomeSuite + testing.FakeJujuHomeSuite } func (s *EnvironmentCommandSuite) SetUpTest(c *gc.C) { @@ -34,20 +33,6 @@ var _ = gc.Suite(&EnvironmentCommandSuite{}) -func Test(t *testing.T) { gc.TestingT(t) } - -func (s *EnvironmentCommandSuite) TestReadCurrentEnvironmentUnset(c *gc.C) { - env := envcmd.ReadCurrentEnvironment() - c.Assert(env, gc.Equals, "") -} - -func (s *EnvironmentCommandSuite) TestReadCurrentEnvironmentSet(c *gc.C) { - err := envcmd.WriteCurrentEnvironment("fubar") - c.Assert(err, jc.ErrorIsNil) - env := envcmd.ReadCurrentEnvironment() - c.Assert(env, gc.Equals, "fubar") -} - func (s *EnvironmentCommandSuite) TestGetDefaultEnvironment(c *gc.C) { env, err := envcmd.GetDefaultEnvironment() c.Assert(env, gc.Equals, "erewhemos") @@ -87,21 +72,6 @@ c.Assert(err, jc.ErrorIsNil) } -func (s *EnvironmentCommandSuite) TestWriteAddsNewline(c *gc.C) { - err := envcmd.WriteCurrentEnvironment("fubar") - c.Assert(err, jc.ErrorIsNil) - current, err := ioutil.ReadFile(envcmd.GetCurrentEnvironmentFilePath()) - c.Assert(err, jc.ErrorIsNil) - c.Assert(string(current), gc.Equals, "fubar\n") -} - -func (*EnvironmentCommandSuite) TestErrorWritingFile(c *gc.C) { - // Can't write a file over a directory. - os.MkdirAll(envcmd.GetCurrentEnvironmentFilePath(), 0777) - err := envcmd.WriteCurrentEnvironment("fubar") - c.Assert(err, gc.ErrorMatches, "unable to write to the environment file: .*") -} - func (s *EnvironmentCommandSuite) TestEnvironCommandInitExplicit(c *gc.C) { // Take environment name from command line arg. testEnsureEnvName(c, "explicit", "-e", "explicit") @@ -109,15 +79,15 @@ func (s *EnvironmentCommandSuite) TestEnvironCommandInitMultipleConfigs(c *gc.C) { // Take environment name from the default. - coretesting.WriteEnvironments(c, coretesting.MultipleEnvConfig) - testEnsureEnvName(c, coretesting.SampleEnvName) + testing.WriteEnvironments(c, testing.MultipleEnvConfig) + testEnsureEnvName(c, testing.SampleEnvName) } func (s *EnvironmentCommandSuite) TestEnvironCommandInitSingleConfig(c *gc.C) { // Take environment name from the one and only environment, // even if it is not explicitly marked as default. - coretesting.WriteEnvironments(c, coretesting.SingleEnvConfigNoDefault) - testEnsureEnvName(c, coretesting.SampleEnvName) + testing.WriteEnvironments(c, testing.SingleEnvConfigNoDefault) + testEnsureEnvName(c, testing.SampleEnvName) } func (s *EnvironmentCommandSuite) TestEnvironCommandInitEnvFile(c *gc.C) { @@ -127,6 +97,14 @@ testEnsureEnvName(c, "fubar") } +func (s *EnvironmentCommandSuite) TestEnvironCommandInitSystemFile(c *gc.C) { + // If there is a current-system file, error raised. + err := envcmd.WriteCurrentSystem("fubar") + c.Assert(err, jc.ErrorIsNil) + _, err = initTestCommand(c) + c.Assert(err, gc.ErrorMatches, `not operating on an environment, using system "fubar"`) +} + func (s *EnvironmentCommandSuite) TestEnvironCommandInitNoEnvFile(c *gc.C) { envPath := gitjujutesting.HomePath(".juju", "environments.yaml") err := os.Remove(envPath) @@ -136,7 +114,7 @@ func (s *EnvironmentCommandSuite) TestEnvironCommandInitMultipleConfigNoDefault(c *gc.C) { // If there are multiple environments but no default, the connection name is empty. - coretesting.WriteEnvironments(c, coretesting.MultipleEnvConfigNoDefault) + testing.WriteEnvironments(c, testing.MultipleEnvConfigNoDefault) testEnsureEnvName(c, "") } @@ -195,7 +173,7 @@ } type ConnectionEndpointSuite struct { - coretesting.FakeJujuHomeSuite + testing.FakeJujuHomeSuite store configstore.Storage endpoint configstore.APIEndpoint } @@ -277,11 +255,17 @@ type fakeEnvGetter struct { agentVersion interface{} err error + results map[string]interface{} + closeCalled bool + getCalled bool } func (g *fakeEnvGetter) EnvironmentGet() (map[string]interface{}, error) { + g.getCalled = true if g.err != nil { return nil, g.err + } else if g.results != nil { + return g.results, nil } else if g.agentVersion == nil { return map[string]interface{}{}, nil } else { @@ -291,6 +275,11 @@ } } +func (g *fakeEnvGetter) Close() error { + g.closeCalled = true + return nil +} + func (s *EnvironmentVersionSuite) SetUpTest(*gc.C) { s.fake = new(fakeEnvGetter) } @@ -325,3 +314,113 @@ c.Assert(err, jc.ErrorIsNil) c.Assert(v.Compare(version.MustParse(vs)), gc.Equals, 0) } + +type EnvConfigSuite struct { + testing.FakeJujuHomeSuite + client *fakeEnvGetter + store configstore.Storage + envName string +} + +var _ = gc.Suite(&EnvConfigSuite{}) + +func createBootstrapInfo(c *gc.C, name string) map[string]interface{} { + bootstrapCfg, err := config.New(config.UseDefaults, map[string]interface{}{ + "type": "dummy", + "name": name, + "state-server": "true", + "state-id": "1", + }) + c.Assert(err, jc.ErrorIsNil) + return bootstrapCfg.AllAttrs() +} + +func (s *EnvConfigSuite) SetUpTest(c *gc.C) { + s.FakeJujuHomeSuite.SetUpTest(c) + s.envName = "test-env" + s.client = &fakeEnvGetter{results: createBootstrapInfo(c, s.envName)} + + var err error + s.store, err = configstore.Default() + c.Assert(err, jc.ErrorIsNil) +} + +func (s *EnvConfigSuite) writeStore(c *gc.C, bootstrapInfo bool) { + info := s.store.CreateInfo(s.envName) + info.SetAPIEndpoint(configstore.APIEndpoint{ + Addresses: []string{"localhost"}, + CACert: testing.CACert, + EnvironUUID: s.envName + "-UUID", + ServerUUID: s.envName + "-UUID", + }) + + if bootstrapInfo { + info.SetBootstrapConfig(createBootstrapInfo(c, s.envName)) + } + err := info.Write() + c.Assert(err, jc.ErrorIsNil) +} + +func (s *EnvConfigSuite) TestConfigWithBootstrapInfo(c *gc.C) { + cmd := envcmd.NewEnvCommandBase(s.envName, s.client, nil) + s.writeStore(c, true) + + cfg, err := cmd.Config(s.store, s.client) + c.Assert(err, jc.ErrorIsNil) + c.Check(cfg.Name(), gc.Equals, s.envName) + c.Check(s.client.getCalled, jc.IsFalse) + c.Check(s.client.closeCalled, jc.IsFalse) +} + +func (s *EnvConfigSuite) TestConfigWithNoBootstrapWithClient(c *gc.C) { + cmd := envcmd.NewEnvCommandBase(s.envName, s.client, nil) + s.writeStore(c, false) + + cfg, err := cmd.Config(s.store, s.client) + c.Assert(err, jc.ErrorIsNil) + c.Check(cfg.Name(), gc.Equals, s.envName) + c.Check(s.client.getCalled, jc.IsTrue) + c.Check(s.client.closeCalled, jc.IsFalse) +} + +func (s *EnvConfigSuite) TestConfigWithNoBootstrapNoClient(c *gc.C) { + cmd := envcmd.NewEnvCommandBase(s.envName, s.client, nil) + s.writeStore(c, false) + + cfg, err := cmd.Config(s.store, nil) + c.Assert(err, jc.ErrorIsNil) + c.Check(cfg.Name(), gc.Equals, s.envName) + c.Check(s.client.getCalled, jc.IsTrue) + c.Check(s.client.closeCalled, jc.IsTrue) +} + +func (s *EnvConfigSuite) TestConfigWithNoBootstrapWithClientErr(c *gc.C) { + cmd := envcmd.NewEnvCommandBase(s.envName, s.client, errors.New("problem opening connection")) + s.writeStore(c, false) + + _, err := cmd.Config(s.store, nil) + c.Assert(err, gc.ErrorMatches, "problem opening connection") + c.Check(s.client.getCalled, jc.IsFalse) + c.Check(s.client.closeCalled, jc.IsFalse) +} + +func (s *EnvConfigSuite) TestConfigWithNoBootstrapWithEnvGetError(c *gc.C) { + cmd := envcmd.NewEnvCommandBase(s.envName, s.client, nil) + s.writeStore(c, false) + s.client.err = errors.New("problem getting environment attributes") + + _, err := cmd.Config(s.store, nil) + c.Assert(err, gc.ErrorMatches, "problem getting environment attributes") + c.Check(s.client.getCalled, jc.IsTrue) + c.Check(s.client.closeCalled, jc.IsTrue) +} + +func (s *EnvConfigSuite) TestConfigEnvDoesntExist(c *gc.C) { + cmd := envcmd.NewEnvCommandBase("dummy", s.client, nil) + s.writeStore(c, false) + + _, err := cmd.Config(s.store, nil) + c.Assert(err, jc.Satisfies, errors.IsNotFound) + c.Check(s.client.getCalled, jc.IsFalse) + c.Check(s.client.closeCalled, jc.IsFalse) +} === modified file 'src/github.com/juju/juju/cmd/envcmd/export_test.go' --- src/github.com/juju/juju/cmd/envcmd/export_test.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/envcmd/export_test.go 2015-10-23 18:29:32 +0000 @@ -5,6 +5,17 @@ var ( GetCurrentEnvironmentFilePath = getCurrentEnvironmentFilePath + GetCurrentSystemFilePath = getCurrentSystemFilePath GetConfigStore = &getConfigStore EndpointRefresher = &endpointRefresher ) + +// NewEnvCommandBase returns a new EnvCommandBase with the environment name, client, +// and error as specified for testing purposes. +func NewEnvCommandBase(name string, client EnvironmentGetter, err error) *EnvCommandBase { + return &EnvCommandBase{ + envName: name, + envGetterClient: client, + envGetterErr: err, + } +} === added file 'src/github.com/juju/juju/cmd/envcmd/files.go' --- src/github.com/juju/juju/cmd/envcmd/files.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/envcmd/files.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,220 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package envcmd + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/utils/fslock" + + "github.com/juju/juju/juju/osenv" +) + +const ( + CurrentEnvironmentFilename = "current-environment" + CurrentSystemFilename = "current-system" + + lockName = "current.lock" + + systemSuffix = " (system)" +) + +var ( + // 5 seconds should be way more than enough to write or read any files + // even on heavily loaded systems. + lockTimeout = 5 * time.Second +) + +// ServerFile describes the information that is needed for a user +// to connect to an api server. +type ServerFile struct { + Addresses []string `yaml:"addresses"` + CACert string `yaml:"ca-cert,omitempty"` + Username string `yaml:"username"` + Password string `yaml:"password"` +} + +// NOTE: synchronisation across functions in this file. +// +// Each of the read and write functions use a fslock to synchronise calls +// across both the current executable and across different executables. + +func getCurrentEnvironmentFilePath() string { + return filepath.Join(osenv.JujuHome(), CurrentEnvironmentFilename) +} + +func getCurrentSystemFilePath() string { + return filepath.Join(osenv.JujuHome(), CurrentSystemFilename) +} + +// Read the file $JUJU_HOME/current-environment and return the value stored +// there. If the file doesn't exist an empty string is returned and no error. +func ReadCurrentEnvironment() (string, error) { + lock, err := acquireEnvironmentLock("read current-environment") + if err != nil { + return "", errors.Trace(err) + } + defer lock.Unlock() + + current, err := ioutil.ReadFile(getCurrentEnvironmentFilePath()) + if err != nil { + if os.IsNotExist(err) { + return "", nil + } + return "", errors.Trace(err) + } + return strings.TrimSpace(string(current)), nil +} + +// Read the file $JUJU_HOME/current-system and return the value stored there. +// If the file doesn't exist an empty string is returned and no error. +func ReadCurrentSystem() (string, error) { + lock, err := acquireEnvironmentLock("read current-system") + if err != nil { + return "", errors.Trace(err) + } + defer lock.Unlock() + + current, err := ioutil.ReadFile(getCurrentSystemFilePath()) + if err != nil { + if os.IsNotExist(err) { + return "", nil + } + return "", errors.Trace(err) + } + return strings.TrimSpace(string(current)), nil +} + +// Write the envName to the file $JUJU_HOME/current-environment file. +func WriteCurrentEnvironment(envName string) error { + lock, err := acquireEnvironmentLock("write current-environment") + if err != nil { + return errors.Trace(err) + } + defer lock.Unlock() + + path := getCurrentEnvironmentFilePath() + err = ioutil.WriteFile(path, []byte(envName+"\n"), 0644) + if err != nil { + return errors.Errorf("unable to write to the environment file: %q, %s", path, err) + } + // If there is a current system file, remove it. + if err := os.Remove(getCurrentSystemFilePath()); err != nil && !os.IsNotExist(err) { + logger.Debugf("removing the current environment file due to %s", err) + // Best attempt to remove the file we just wrote. + os.Remove(path) + return err + } + return nil +} + +// Write the systemName to the file $JUJU_HOME/current-system file. +func WriteCurrentSystem(systemName string) error { + lock, err := acquireEnvironmentLock("write current-system") + if err != nil { + return errors.Trace(err) + } + defer lock.Unlock() + + path := getCurrentSystemFilePath() + err = ioutil.WriteFile(path, []byte(systemName+"\n"), 0644) + if err != nil { + return errors.Errorf("unable to write to the system file: %q, %s", path, err) + } + // If there is a current environment file, remove it. + if err := os.Remove(getCurrentEnvironmentFilePath()); err != nil && !os.IsNotExist(err) { + logger.Debugf("removing the current system file due to %s", err) + // Best attempt to remove the file we just wrote. + os.Remove(path) + return err + } + return nil +} + +func acquireEnvironmentLock(operation string) (*fslock.Lock, error) { + // NOTE: any reading or writing from the directory should be done with a + // fslock to make sure we have a consistent read or write. Also worth + // noting, we should use a very short timeout. + lock, err := fslock.NewLock(osenv.JujuHome(), lockName) + if err != nil { + return nil, errors.Trace(err) + } + err = lock.LockWithTimeout(lockTimeout, operation) + if err != nil { + return nil, errors.Trace(err) + } + return lock, nil +} + +// CurrentConnectionName looks at both the current environment file +// and the current system file to determine which is active. +// The name of the current environment or system is returned along with +// a boolean to express whether the name refers to a system or environment. +func CurrentConnectionName() (name string, is_system bool, err error) { + currentEnv, err := ReadCurrentEnvironment() + if err != nil { + return "", false, errors.Trace(err) + } else if currentEnv != "" { + return currentEnv, false, nil + } + + currentSystem, err := ReadCurrentSystem() + if err != nil { + return "", false, errors.Trace(err) + } else if currentSystem != "" { + return currentSystem, true, nil + } + + return "", false, nil +} + +func currentName() (string, error) { + name, isSystem, err := CurrentConnectionName() + if err != nil { + return "", errors.Trace(err) + } + if isSystem { + name = name + systemSuffix + } + if name != "" { + name += " " + } + return name, nil +} + +// SetCurrentEnvironment writes out the current environment file and writes a +// standard message to the command context. +func SetCurrentEnvironment(context *cmd.Context, environmentName string) error { + current, err := currentName() + if err != nil { + return errors.Trace(err) + } + err = WriteCurrentEnvironment(environmentName) + if err != nil { + return errors.Trace(err) + } + context.Infof("%s-> %s", current, environmentName) + return nil +} + +// SetCurrentSystem writes out the current system file and writes a standard +// message to the command context. +func SetCurrentSystem(context *cmd.Context, systemName string) error { + current, err := currentName() + if err != nil { + return errors.Trace(err) + } + err = WriteCurrentSystem(systemName) + if err != nil { + return errors.Trace(err) + } + context.Infof("%s-> %s%s", current, systemName, systemSuffix) + return nil +} === added file 'src/github.com/juju/juju/cmd/envcmd/files_test.go' --- src/github.com/juju/juju/cmd/envcmd/files_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/envcmd/files_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,180 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package envcmd_test + +import ( + "io/ioutil" + "os" + + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/testing" +) + +type filesSuite struct { + testing.FakeJujuHomeSuite +} + +var _ = gc.Suite(&filesSuite{}) + +func (s *filesSuite) assertCurrentEnvironment(c *gc.C, environmentName string) { + current, err := envcmd.ReadCurrentEnvironment() + c.Assert(err, jc.ErrorIsNil) + c.Assert(current, gc.Equals, environmentName) +} + +func (s *filesSuite) assertCurrentSystem(c *gc.C, systemName string) { + current, err := envcmd.ReadCurrentSystem() + c.Assert(err, jc.ErrorIsNil) + c.Assert(current, gc.Equals, systemName) +} + +func (s *filesSuite) TestReadCurrentEnvironmentUnset(c *gc.C) { + s.assertCurrentEnvironment(c, "") +} + +func (s *filesSuite) TestReadCurrentSystemUnset(c *gc.C) { + s.assertCurrentSystem(c, "") +} + +func (s *filesSuite) TestReadCurrentEnvironmentSet(c *gc.C) { + err := envcmd.WriteCurrentEnvironment("fubar") + c.Assert(err, jc.ErrorIsNil) + s.assertCurrentEnvironment(c, "fubar") +} + +func (s *filesSuite) TestReadCurrentSystemSet(c *gc.C) { + err := envcmd.WriteCurrentSystem("fubar") + c.Assert(err, jc.ErrorIsNil) + s.assertCurrentSystem(c, "fubar") +} + +func (s *filesSuite) TestWriteEnvironmentAddsNewline(c *gc.C) { + err := envcmd.WriteCurrentEnvironment("fubar") + c.Assert(err, jc.ErrorIsNil) + current, err := ioutil.ReadFile(envcmd.GetCurrentEnvironmentFilePath()) + c.Assert(err, jc.ErrorIsNil) + c.Assert(string(current), gc.Equals, "fubar\n") +} + +func (s *filesSuite) TestWriteSystemAddsNewline(c *gc.C) { + err := envcmd.WriteCurrentSystem("fubar") + c.Assert(err, jc.ErrorIsNil) + current, err := ioutil.ReadFile(envcmd.GetCurrentSystemFilePath()) + c.Assert(err, jc.ErrorIsNil) + c.Assert(string(current), gc.Equals, "fubar\n") +} + +func (s *filesSuite) TestWriteEnvironmentRemovesSystemFile(c *gc.C) { + err := envcmd.WriteCurrentSystem("baz") + c.Assert(err, jc.ErrorIsNil) + err = envcmd.WriteCurrentEnvironment("fubar") + c.Assert(err, jc.ErrorIsNil) + c.Assert(envcmd.GetCurrentSystemFilePath(), jc.DoesNotExist) +} + +func (s *filesSuite) TestWriteSystemRemovesEnvironmentFile(c *gc.C) { + err := envcmd.WriteCurrentEnvironment("fubar") + c.Assert(err, jc.ErrorIsNil) + err = envcmd.WriteCurrentSystem("baz") + c.Assert(err, jc.ErrorIsNil) + c.Assert(envcmd.GetCurrentEnvironmentFilePath(), jc.DoesNotExist) +} + +func (*filesSuite) TestErrorWritingCurrentEnvironment(c *gc.C) { + // Can't write a file over a directory. + os.MkdirAll(envcmd.GetCurrentEnvironmentFilePath(), 0777) + err := envcmd.WriteCurrentEnvironment("fubar") + c.Assert(err, gc.ErrorMatches, "unable to write to the environment file: .*") +} + +func (*filesSuite) TestErrorWritingCurrentSystem(c *gc.C) { + // Can't write a file over a directory. + os.MkdirAll(envcmd.GetCurrentSystemFilePath(), 0777) + err := envcmd.WriteCurrentSystem("fubar") + c.Assert(err, gc.ErrorMatches, "unable to write to the system file: .*") +} + +func (*filesSuite) TestCurrentCommenctionNameMissing(c *gc.C) { + name, isSystem, err := envcmd.CurrentConnectionName() + c.Assert(err, jc.ErrorIsNil) + c.Assert(isSystem, jc.IsFalse) + c.Assert(name, gc.Equals, "") +} + +func (*filesSuite) TestCurrentCommenctionNameEnvironment(c *gc.C) { + err := envcmd.WriteCurrentEnvironment("fubar") + c.Assert(err, jc.ErrorIsNil) + name, isSystem, err := envcmd.CurrentConnectionName() + c.Assert(err, jc.ErrorIsNil) + c.Assert(isSystem, jc.IsFalse) + c.Assert(name, gc.Equals, "fubar") +} + +func (*filesSuite) TestCurrentCommenctionNameSystem(c *gc.C) { + err := envcmd.WriteCurrentSystem("baz") + c.Assert(err, jc.ErrorIsNil) + name, isSystem, err := envcmd.CurrentConnectionName() + c.Assert(err, jc.ErrorIsNil) + c.Assert(isSystem, jc.IsTrue) + c.Assert(name, gc.Equals, "baz") +} + +func (s *filesSuite) TestSetCurrentEnvironment(c *gc.C) { + ctx := testing.Context(c) + err := envcmd.SetCurrentEnvironment(ctx, "new-env") + c.Assert(err, jc.ErrorIsNil) + s.assertCurrentEnvironment(c, "new-env") + c.Assert(testing.Stderr(ctx), gc.Equals, "-> new-env\n") +} + +func (s *filesSuite) TestSetCurrentEnvironmentExistingEnv(c *gc.C) { + err := envcmd.WriteCurrentEnvironment("fubar") + c.Assert(err, jc.ErrorIsNil) + ctx := testing.Context(c) + err = envcmd.SetCurrentEnvironment(ctx, "new-env") + c.Assert(err, jc.ErrorIsNil) + s.assertCurrentEnvironment(c, "new-env") + c.Assert(testing.Stderr(ctx), gc.Equals, "fubar -> new-env\n") +} + +func (s *filesSuite) TestSetCurrentEnvironmentExistingSystem(c *gc.C) { + err := envcmd.WriteCurrentSystem("fubar") + c.Assert(err, jc.ErrorIsNil) + ctx := testing.Context(c) + err = envcmd.SetCurrentEnvironment(ctx, "new-env") + c.Assert(err, jc.ErrorIsNil) + s.assertCurrentEnvironment(c, "new-env") + c.Assert(testing.Stderr(ctx), gc.Equals, "fubar (system) -> new-env\n") +} + +func (s *filesSuite) TestSetCurrentSystem(c *gc.C) { + ctx := testing.Context(c) + err := envcmd.SetCurrentSystem(ctx, "new-sys") + c.Assert(err, jc.ErrorIsNil) + s.assertCurrentSystem(c, "new-sys") + c.Assert(testing.Stderr(ctx), gc.Equals, "-> new-sys (system)\n") +} + +func (s *filesSuite) TestSetCurrentSystemExistingEnv(c *gc.C) { + err := envcmd.WriteCurrentEnvironment("fubar") + c.Assert(err, jc.ErrorIsNil) + ctx := testing.Context(c) + err = envcmd.SetCurrentSystem(ctx, "new-sys") + c.Assert(err, jc.ErrorIsNil) + s.assertCurrentSystem(c, "new-sys") + c.Assert(testing.Stderr(ctx), gc.Equals, "fubar -> new-sys (system)\n") +} + +func (s *filesSuite) TestSetCurrentSystemExistingSystem(c *gc.C) { + err := envcmd.WriteCurrentSystem("fubar") + c.Assert(err, jc.ErrorIsNil) + ctx := testing.Context(c) + err = envcmd.SetCurrentSystem(ctx, "new-sys") + c.Assert(err, jc.ErrorIsNil) + s.assertCurrentSystem(c, "new-sys") + c.Assert(testing.Stderr(ctx), gc.Equals, "fubar (system) -> new-sys (system)\n") +} === added file 'src/github.com/juju/juju/cmd/envcmd/package_test.go' --- src/github.com/juju/juju/cmd/envcmd/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/envcmd/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,14 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package envcmd_test + +import ( + "testing" + + gc "gopkg.in/check.v1" +) + +func Test(t *testing.T) { + gc.TestingT(t) +} === added file 'src/github.com/juju/juju/cmd/envcmd/systemcommand.go' --- src/github.com/juju/juju/cmd/envcmd/systemcommand.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/envcmd/systemcommand.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,180 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package envcmd + +import ( + "github.com/juju/cmd" + "github.com/juju/errors" + "launchpad.net/gnuflag" + + "github.com/juju/juju/api" + "github.com/juju/juju/api/environmentmanager" + "github.com/juju/juju/api/systemmanager" + "github.com/juju/juju/api/usermanager" + "github.com/juju/juju/environs/configstore" + "github.com/juju/juju/juju" +) + +// ErrNoSystemSpecified is returned by commands that operate on +// a system if there is no current system, no system has been +// explicitly specified, and there is no default system. +var ErrNoSystemSpecified = errors.New("no system specified") + +// SystemCommand is intended to be a base for all commands +// that need to operate on systems as opposed to environments. +type SystemCommand interface { + cmd.Command + + // SetSystemName is called prior to the wrapped command's Init method with + // the active system name. The system name is guaranteed to be non-empty + // at entry of Init. + SetSystemName(systemName string) + + // SystemName returns the name of the system or environment used to + // determine that API end point. + SystemName() string +} + +// SysCommandBase is a convenience type for embedding in commands +// that wish to implement SystemCommand. +type SysCommandBase struct { + cmd.CommandBase + systemName string +} + +// SetSystemName records the current environment name in the SysCommandBase +func (c *SysCommandBase) SetSystemName(systemName string) { + c.systemName = systemName +} + +// SystemName returns the name of the system or environment used to determine +// that API end point. +func (c *SysCommandBase) SystemName() string { + return c.systemName +} + +// NewEnvironmentManagerAPIClient returns an API client for the +// EnvironmentManager on the current system using the current credentials. +func (c *SysCommandBase) NewEnvironmentManagerAPIClient() (*environmentmanager.Client, error) { + root, err := c.newAPIRoot() + if err != nil { + return nil, errors.Trace(err) + } + return environmentmanager.NewClient(root), nil +} + +// NewSystemManagerAPIClient returns an API client for the SystemManager on +// the current system using the current credentials. +func (c *SysCommandBase) NewSystemManagerAPIClient() (*systemmanager.Client, error) { + root, err := c.newAPIRoot() + if err != nil { + return nil, errors.Trace(err) + } + return systemmanager.NewClient(root), nil +} + +// NewUserManagerAPIClient returns an API client for the UserManager on the +// current system using the current credentials. +func (c *SysCommandBase) NewUserManagerAPIClient() (*usermanager.Client, error) { + root, err := c.newAPIRoot() + if err != nil { + return nil, errors.Trace(err) + } + return usermanager.NewClient(root), nil +} + +// newAPIRoot returns a restricted API for the current system using the current +// credentials. Only the UserManager and EnvironmentManager may be accessed +// through this API connection. +func (c *SysCommandBase) newAPIRoot() (api.Connection, error) { + if c.systemName == "" { + return nil, errors.Trace(ErrNoSystemSpecified) + } + return juju.NewAPIFromName(c.systemName) +} + +// ConnectionCredentials returns the credentials used to connect to the API for +// the specified system. +func (c *SysCommandBase) ConnectionCredentials() (configstore.APICredentials, error) { + // TODO: the user may soon be specified through the command line + // or through an environment setting, so return these when they are ready. + var emptyCreds configstore.APICredentials + info, err := c.ConnectionInfo() + if err != nil { + return emptyCreds, errors.Trace(err) + } + return info.APICredentials(), nil +} + +// ConnectionEndpoint returns the endpoint details used to connect to the API for +// the specified system. +func (c *SysCommandBase) ConnectionEndpoint() (configstore.APIEndpoint, error) { + // TODO: the user may soon be specified through the command line + // or through an environment setting, so return these when they are ready. + var empty configstore.APIEndpoint + info, err := c.ConnectionInfo() + if err != nil { + return empty, errors.Trace(err) + } + return info.APIEndpoint(), nil +} + +// ConnectionInfo returns the environ info from the cached config store. +func (c *SysCommandBase) ConnectionInfo() (configstore.EnvironInfo, error) { + // TODO: the user may soon be specified through the command line + // or through an environment setting, so return these when they are ready. + if c.systemName == "" { + return nil, errors.Trace(ErrNoSystemSpecified) + } + info, err := ConnectionInfoForName(c.systemName) + if err != nil { + return nil, errors.Trace(err) + } + return info, nil +} + +// Wrap wraps the specified SystemCommand, returning a Command +// that proxies to each of the SystemCommand methods. +func WrapSystem(c SystemCommand) cmd.Command { + return &sysCommandWrapper{SystemCommand: c} +} + +type sysCommandWrapper struct { + SystemCommand + systemName string +} + +// SetFlags implements Command.SetFlags, then calls the wrapped command's SetFlags. +func (w *sysCommandWrapper) SetFlags(f *gnuflag.FlagSet) { + f.StringVar(&w.systemName, "s", "", "juju system to operate in") + f.StringVar(&w.systemName, "system", "", "") + w.SystemCommand.SetFlags(f) +} + +func (w *sysCommandWrapper) getDefaultSystemName() (string, error) { + if currentSystem, err := ReadCurrentSystem(); err != nil { + return "", errors.Trace(err) + } else if currentSystem != "" { + return currentSystem, nil + } + if currentEnv, err := ReadCurrentEnvironment(); err != nil { + return "", errors.Trace(err) + } else if currentEnv != "" { + return currentEnv, nil + } + return "", errors.Trace(ErrNoSystemSpecified) +} + +// Init implements Command.Init, then calls the wrapped command's Init. +func (w *sysCommandWrapper) Init(args []string) error { + if w.systemName == "" { + name, err := w.getDefaultSystemName() + if err != nil { + return errors.Trace(err) + } + w.systemName = name + } + w.SetSystemName(w.systemName) + return w.SystemCommand.Init(args) +} === added file 'src/github.com/juju/juju/cmd/envcmd/systemcommand_test.go' --- src/github.com/juju/juju/cmd/envcmd/systemcommand_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/envcmd/systemcommand_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,85 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package envcmd_test + +import ( + "os" + + "github.com/juju/cmd" + "github.com/juju/cmd/cmdtesting" + gitjujutesting "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/testing" +) + +type SystemCommandSuite struct { + testing.FakeJujuHomeSuite +} + +var _ = gc.Suite(&SystemCommandSuite{}) + +func (s *SystemCommandSuite) TestSystemCommandInitMultipleConfigs(c *gc.C) { + // The environments.yaml file is ignored for system commands. + testing.WriteEnvironments(c, testing.MultipleEnvConfig) + _, err := initTestSystemCommand(c) + c.Assert(err, gc.ErrorMatches, "no system specified") +} + +func (s *SystemCommandSuite) TestSystemCommandInitNoEnvFile(c *gc.C) { + // Since we ignore the environments.yaml file, we don't care if it isn't + // there. + envPath := gitjujutesting.HomePath(".juju", "environments.yaml") + err := os.Remove(envPath) + _, err = initTestSystemCommand(c) + c.Assert(err, gc.ErrorMatches, "no system specified") +} + +func (s *SystemCommandSuite) TestSystemCommandInitSystemFile(c *gc.C) { + // If there is a current-system file, use that. + err := envcmd.WriteCurrentSystem("fubar") + c.Assert(err, jc.ErrorIsNil) + testEnsureSystemName(c, "fubar") +} +func (s *SystemCommandSuite) TestSystemCommandInitEnvFile(c *gc.C) { + // If there is a current-environment file, use that. + err := envcmd.WriteCurrentEnvironment("fubar") + c.Assert(err, jc.ErrorIsNil) + testEnsureSystemName(c, "fubar") +} + +func (s *SystemCommandSuite) TestSystemCommandInitExplicit(c *gc.C) { + // Take system name from command line arg, and it trumps the current- + // system file. + err := envcmd.WriteCurrentSystem("fubar") + c.Assert(err, jc.ErrorIsNil) + testEnsureSystemName(c, "explicit", "-s", "explicit") + testEnsureSystemName(c, "explicit", "--system", "explicit") +} + +type testSystemCommand struct { + envcmd.SysCommandBase +} + +func (c *testSystemCommand) Info() *cmd.Info { + panic("should not be called") +} + +func (c *testSystemCommand) Run(ctx *cmd.Context) error { + panic("should not be called") +} + +func initTestSystemCommand(c *gc.C, args ...string) (*testSystemCommand, error) { + cmd := new(testSystemCommand) + wrapped := envcmd.WrapSystem(cmd) + return cmd, cmdtesting.InitCommand(wrapped, args) +} + +func testEnsureSystemName(c *gc.C, expect string, args ...string) { + cmd, err := initTestSystemCommand(c, args...) + c.Assert(err, jc.ErrorIsNil) + c.Assert(cmd.SystemName(), gc.Equals, expect) +} === added file 'src/github.com/juju/juju/cmd/helpers.go' --- src/github.com/juju/juju/cmd/helpers.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/helpers.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,44 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package cmd + +import ( + "bufio" + "io" + "strings" + + "github.com/juju/cmd" + "github.com/juju/errors" +) + +// This file contains helper functions for generic operations commonly needed +// when implementing a command. + +type userAbortedError string + +func (e userAbortedError) Error() string { + return string(e) +} + +// IsUserAbortedError returns true if err is of type userAbortedError. +func IsUserAbortedError(err error) bool { + _, ok := errors.Cause(err).(userAbortedError) + return ok +} + +// UserConfirmYes returns an error if we do not read a "y" or "yes" from user +// input. +func UserConfirmYes(ctx *cmd.Context) error { + scanner := bufio.NewScanner(ctx.Stdin) + scanner.Scan() + err := scanner.Err() + if err != nil && err != io.EOF { + return errors.Trace(err) + } + answer := strings.ToLower(scanner.Text()) + if answer != "y" && answer != "yes" { + return errors.Trace(userAbortedError("aborted")) + } + return nil +} === modified file 'src/github.com/juju/juju/cmd/juju/action/action_test.go' --- src/github.com/juju/juju/cmd/juju/action/action_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/action/action_test.go 2015-10-23 18:29:32 +0000 @@ -28,7 +28,7 @@ ctx, err := testing.RunCommand(c, s.command, "--help") c.Assert(err, gc.IsNil) - expected := "(?s).*^usage: juju action .+" + expected := "(?s).*^usage: juju action \\[options\\] .+" c.Check(testing.Stdout(ctx), gc.Matches, expected) supercommand, ok := s.command.(*cmd.SuperCommand) === modified file 'src/github.com/juju/juju/cmd/juju/action/common.go' --- src/github.com/juju/juju/cmd/juju/action/common.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/action/common.go 2015-10-23 18:29:32 +0000 @@ -15,51 +15,6 @@ var logger = loggo.GetLogger("juju.cmd.juju.action") -// conform ensures all keys of any nested maps are strings. This is -// necessary because YAML unmarshals map[interface{}]interface{} in nested -// maps, which cannot be serialized by bson. Also, handle []interface{}. -// cf. gopkg.in/juju/charm.v4/actions.go cleanse -func conform(input interface{}) (interface{}, error) { - switch typedInput := input.(type) { - - case map[string]interface{}: - newMap := make(map[string]interface{}) - for key, value := range typedInput { - newValue, err := conform(value) - if err != nil { - return nil, err - } - newMap[key] = newValue - } - return newMap, nil - - case map[interface{}]interface{}: - newMap := make(map[string]interface{}) - for key, value := range typedInput { - typedKey, ok := key.(string) - if !ok { - return nil, errors.New("map keyed with non-string value") - } - newMap[typedKey] = value - } - return conform(newMap) - - case []interface{}: - newSlice := make([]interface{}, len(typedInput)) - for i, sliceValue := range typedInput { - newSliceValue, err := conform(sliceValue) - if err != nil { - return nil, errors.New("map keyed with non-string value") - } - newSlice[i] = newSliceValue - } - return newSlice, nil - - default: - return input, nil - } -} - // displayActionResult returns any error from an ActionResult and displays // its response values otherwise. func displayActionResult(result params.ActionResult, ctx *cmd.Context, out cmd.Output) error { === modified file 'src/github.com/juju/juju/cmd/juju/action/common_test.go' --- src/github.com/juju/juju/cmd/juju/action/common_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/action/common_test.go 2015-10-23 18:29:32 +0000 @@ -1,120 +1,19 @@ // Copyright 2014-2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. -// This is necessary since it must test a recursive unexported function, -// i.e., the function cannot be exported via a var -package action +package action_test import ( jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/juju/action" ) type CommonSuite struct{} var _ = gc.Suite(&CommonSuite{}) -func (s *CommonSuite) TestConform(c *gc.C) { - var goodInterfaceTests = []struct { - description string - inputInterface interface{} - expectedInterface map[string]interface{} - expectedError string - }{{ - description: "An interface requiring no changes.", - inputInterface: map[string]interface{}{ - "key1": "value1", - "key2": "value2", - "key3": map[string]interface{}{ - "foo1": "val1", - "foo2": "val2"}}, - expectedInterface: map[string]interface{}{ - "key1": "value1", - "key2": "value2", - "key3": map[string]interface{}{ - "foo1": "val1", - "foo2": "val2"}}, - }, { - description: "Substitute a single inner map[i]i.", - inputInterface: map[string]interface{}{ - "key1": "value1", - "key2": "value2", - "key3": map[interface{}]interface{}{ - "foo1": "val1", - "foo2": "val2"}}, - expectedInterface: map[string]interface{}{ - "key1": "value1", - "key2": "value2", - "key3": map[string]interface{}{ - "foo1": "val1", - "foo2": "val2"}}, - }, { - description: "Substitute nested inner map[i]i.", - inputInterface: map[string]interface{}{ - "key1a": "val1a", - "key2a": "val2a", - "key3a": map[interface{}]interface{}{ - "key1b": "val1b", - "key2b": map[interface{}]interface{}{ - "key1c": "val1c"}}}, - expectedInterface: map[string]interface{}{ - "key1a": "val1a", - "key2a": "val2a", - "key3a": map[string]interface{}{ - "key1b": "val1b", - "key2b": map[string]interface{}{ - "key1c": "val1c"}}}, - }, { - description: "Substitute nested map[i]i within []i.", - inputInterface: map[string]interface{}{ - "key1a": "val1a", - "key2a": []interface{}{5, "foo", map[string]interface{}{ - "key1b": "val1b", - "key2b": map[interface{}]interface{}{ - "key1c": "val1c"}}}}, - expectedInterface: map[string]interface{}{ - "key1a": "val1a", - "key2a": []interface{}{5, "foo", map[string]interface{}{ - "key1b": "val1b", - "key2b": map[string]interface{}{ - "key1c": "val1c"}}}}, - }, { - description: "An inner map[interface{}]interface{} with an int key.", - inputInterface: map[string]interface{}{ - "key1": "value1", - "key2": "value2", - "key3": map[interface{}]interface{}{ - "foo1": "val1", - 5: "val2"}}, - expectedError: "map keyed with non-string value", - }, { - description: "An inner []interface{} containing a map[i]i with an int key.", - inputInterface: map[string]interface{}{ - "key1a": "val1b", - "key2a": "val2b", - "key3a": []interface{}{"foo1", 5, map[interface{}]interface{}{ - "key1b": "val1b", - "key2b": map[interface{}]interface{}{ - "key1c": "val1c", - 5: "val2c"}}}}, - expectedError: "map keyed with non-string value", - }} - - for i, test := range goodInterfaceTests { - c.Logf("test %d: %s", i, test.description) - input := test.inputInterface - cleansedInterfaceMap, err := conform(input) - if test.expectedError == "" { - if !c.Check(err, jc.ErrorIsNil) { - continue - } - c.Check(cleansedInterfaceMap, gc.DeepEquals, test.expectedInterface) - } else { - c.Check(err, gc.ErrorMatches, test.expectedError) - } - } -} - type insertSliceValue struct { valuePath []string value interface{} @@ -158,7 +57,7 @@ }} { c.Logf("test %d: should %s", i, t.should) for _, sVal := range t.insertSlices { - addValueToMap(sVal.valuePath, sVal.value, t.startingMap) + action.AddValueToMap(sVal.valuePath, sVal.value, t.startingMap) } // note addValueToMap mutates target. c.Check(t.startingMap, jc.DeepEquals, t.expectedMap) === modified file 'src/github.com/juju/juju/cmd/juju/action/do.go' --- src/github.com/juju/juju/cmd/juju/action/do.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/action/do.go 2015-10-23 18:29:32 +0000 @@ -15,6 +15,7 @@ "launchpad.net/gnuflag" "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/juju/common" ) var keyRule = regexp.MustCompile("^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$") @@ -181,7 +182,7 @@ return err } - conformantParams, err := conform(actionParams) + conformantParams, err := common.ConformYAML(actionParams) if err != nil { return err } @@ -211,7 +212,7 @@ addValueToMap(keys, cleansedValue, actionParams) } - conformantParams, err := conform(actionParams) + conformantParams, err := common.ConformYAML(actionParams) if err != nil { return err } === modified file 'src/github.com/juju/juju/cmd/juju/action/export_test.go' --- src/github.com/juju/juju/cmd/juju/action/export_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/action/export_test.go 2015-10-23 18:29:32 +0000 @@ -11,6 +11,7 @@ var ( NewActionAPIClient = &newAPIClient + AddValueToMap = addValueToMap ) func (c *DefinedCommand) ServiceTag() names.ServiceTag { === removed file 'src/github.com/juju/juju/cmd/juju/addrelation.go' --- src/github.com/juju/juju/cmd/juju/addrelation.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/addrelation.go 1970-01-01 00:00:00 +0000 @@ -1,45 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - - "github.com/juju/cmd" - - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/cmd/juju/block" -) - -// AddRelationCommand adds a relation between two service endpoints. -type AddRelationCommand struct { - envcmd.EnvCommandBase - Endpoints []string -} - -func (c *AddRelationCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "add-relation", - Args: "[:] [:]", - Purpose: "add a relation between two services", - } -} - -func (c *AddRelationCommand) Init(args []string) error { - if len(args) != 2 { - return fmt.Errorf("a relation must involve two services") - } - c.Endpoints = args - return nil -} - -func (c *AddRelationCommand) Run(_ *cmd.Context) error { - client, err := c.NewAPIClient() - if err != nil { - return err - } - defer client.Close() - _, err = client.AddRelation(c.Endpoints...) - return block.ProcessBlockedError(err, block.BlockChange) -} === removed file 'src/github.com/juju/juju/cmd/juju/addrelation_test.go' --- src/github.com/juju/juju/cmd/juju/addrelation_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/addrelation_test.go 1970-01-01 00:00:00 +0000 @@ -1,190 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/cmd/envcmd" - jujutesting "github.com/juju/juju/juju/testing" - "github.com/juju/juju/testcharms" - "github.com/juju/juju/testing" -) - -type AddRelationSuite struct { - jujutesting.RepoSuite - CmdBlockHelper -} - -func (s *AddRelationSuite) SetUpTest(c *gc.C) { - s.RepoSuite.SetUpTest(c) - s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) - c.Assert(s.CmdBlockHelper, gc.NotNil) - s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) -} - -var _ = gc.Suite(&AddRelationSuite{}) - -func runAddRelation(c *gc.C, args ...string) error { - _, err := testing.RunCommand(c, envcmd.Wrap(&AddRelationCommand{}), args...) - return err -} - -var msWpAlreadyExists = `cannot add relation "wp:db ms:server": relation already exists` -var msLgAlreadyExists = `cannot add relation "lg:info ms:juju-info": relation already exists` -var wpLgAlreadyExists = `cannot add relation "lg:logging-directory wp:logging-dir": relation already exists` -var wpLgAlreadyExistsJuju = `cannot add relation "lg:info wp:juju-info": relation already exists` - -var addRelationTests = []struct { - args []string - err string -}{ - { - args: []string{"rk", "ms"}, - err: "no relations found", - }, { - err: "a relation must involve two services", - }, { - args: []string{"rk"}, - err: "a relation must involve two services", - }, { - args: []string{"rk:ring"}, - err: "a relation must involve two services", - }, { - args: []string{"ping:pong", "tic:tac", "icki:wacki"}, - err: "a relation must involve two services", - }, - - // Add a real relation, and check various ways of failing to re-add it. - { - args: []string{"ms", "wp"}, - }, { - args: []string{"ms", "wp"}, - err: msWpAlreadyExists, - }, { - args: []string{"wp", "ms"}, - err: msWpAlreadyExists, - }, { - args: []string{"ms", "wp:db"}, - err: msWpAlreadyExists, - }, { - args: []string{"ms:server", "wp"}, - err: msWpAlreadyExists, - }, { - args: []string{"ms:server", "wp:db"}, - err: msWpAlreadyExists, - }, - - // Add a real relation using an implicit endpoint. - { - args: []string{"ms", "lg"}, - }, { - args: []string{"ms", "lg"}, - err: msLgAlreadyExists, - }, { - args: []string{"lg", "ms"}, - err: msLgAlreadyExists, - }, { - args: []string{"ms:juju-info", "lg"}, - err: msLgAlreadyExists, - }, { - args: []string{"ms", "lg:info"}, - err: msLgAlreadyExists, - }, { - args: []string{"ms:juju-info", "lg:info"}, - err: msLgAlreadyExists, - }, - - // Add a real relation using an explicit endpoint, avoiding the potential implicit one. - { - args: []string{"wp", "lg"}, - }, { - args: []string{"wp", "lg"}, - err: wpLgAlreadyExists, - }, { - args: []string{"lg", "wp"}, - err: wpLgAlreadyExists, - }, { - args: []string{"wp:logging-dir", "lg"}, - err: wpLgAlreadyExists, - }, { - args: []string{"wp", "lg:logging-directory"}, - err: wpLgAlreadyExists, - }, { - args: []string{"wp:logging-dir", "lg:logging-directory"}, - err: wpLgAlreadyExists, - }, - - // Check we can still use the implicit endpoint if specified explicitly. - { - args: []string{"wp:juju-info", "lg"}, - }, { - args: []string{"wp:juju-info", "lg"}, - err: wpLgAlreadyExistsJuju, - }, { - args: []string{"lg", "wp:juju-info"}, - err: wpLgAlreadyExistsJuju, - }, { - args: []string{"wp:juju-info", "lg"}, - err: wpLgAlreadyExistsJuju, - }, { - args: []string{"wp", "lg:info"}, - err: wpLgAlreadyExistsJuju, - }, { - args: []string{"wp:juju-info", "lg:info"}, - err: wpLgAlreadyExistsJuju, - }, -} - -func (s *AddRelationSuite) TestAddRelation(c *gc.C) { - testcharms.Repo.CharmArchivePath(s.SeriesPath, "wordpress") - err := runDeploy(c, "local:wordpress", "wp") - c.Assert(err, jc.ErrorIsNil) - testcharms.Repo.CharmArchivePath(s.SeriesPath, "mysql") - err = runDeploy(c, "local:mysql", "ms") - c.Assert(err, jc.ErrorIsNil) - testcharms.Repo.CharmArchivePath(s.SeriesPath, "riak") - err = runDeploy(c, "local:riak", "rk") - c.Assert(err, jc.ErrorIsNil) - testcharms.Repo.CharmArchivePath(s.SeriesPath, "logging") - err = runDeploy(c, "local:logging", "lg") - c.Assert(err, jc.ErrorIsNil) - - for i, t := range addRelationTests { - c.Logf("test %d: %v", i, t.args) - err := runAddRelation(c, t.args...) - if t.err != "" { - c.Assert(err, gc.ErrorMatches, t.err) - } - } -} - -func (s *AddRelationSuite) TestBlockAddRelation(c *gc.C) { - testcharms.Repo.CharmArchivePath(s.SeriesPath, "wordpress") - err := runDeploy(c, "local:wordpress", "wp") - c.Assert(err, jc.ErrorIsNil) - testcharms.Repo.CharmArchivePath(s.SeriesPath, "mysql") - err = runDeploy(c, "local:mysql", "ms") - c.Assert(err, jc.ErrorIsNil) - testcharms.Repo.CharmArchivePath(s.SeriesPath, "riak") - err = runDeploy(c, "local:riak", "rk") - c.Assert(err, jc.ErrorIsNil) - testcharms.Repo.CharmArchivePath(s.SeriesPath, "logging") - err = runDeploy(c, "local:logging", "lg") - c.Assert(err, jc.ErrorIsNil) - - // Block operation - s.BlockAllChanges(c, "TestBlockAddRelation") - - for i, t := range addRelationTests { - c.Logf("test %d: %v", i, t.args) - err := runAddRelation(c, t.args...) - if len(t.args) == 2 { - // Only worry about Run being blocked. - // For len(t.args) != 2, an Init will fail - s.AssertBlocked(c, err, ".*TestBlockAddRelation.*") - } - } -} === removed file 'src/github.com/juju/juju/cmd/juju/apiinfo.go' --- src/github.com/juju/juju/cmd/juju/apiinfo.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/apiinfo.go 1970-01-01 00:00:00 +0000 @@ -1,219 +0,0 @@ -// Copyright 2014 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - "strings" - - "github.com/juju/cmd" - "github.com/juju/errors" - "launchpad.net/gnuflag" - - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/environs/configstore" -) - -// APIInfoCommand returns the fields used to connect to an API server. -type APIInfoCommand struct { - envcmd.EnvCommandBase - out cmd.Output - refresh bool - user bool - password bool - cacert bool - servers bool - envuuid bool - srvuuid bool - fields []string -} - -const apiInfoDoc = ` -Print the field values used to connect to the environment's API servers" - -The exact fields to output can be specified on the command line. The -available fields are: - user - password - environ-uuid - state-servers - ca-cert - -If "password" is included as a field, or the --password option is given, the -password value will be shown. - - -Examples: - $ juju api-info - user: admin - environ-uuid: 373b309b-4a86-4f13-88e2-c213d97075b8 - state-servers: - - localhost:17070 - - 10.0.3.1:17070 - - 192.168.2.21:17070 - ca-cert: '-----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- - ' - - $ juju api-info user - admin - - $ juju api-info user password - user: admin - password: sekrit - - -` - -func (c *APIInfoCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "api-info", - Args: "[field ...]", - Purpose: "print the field values used to connect to the environment's API servers", - Doc: apiInfoDoc, - } -} - -func (c *APIInfoCommand) Init(args []string) error { - c.fields = args - if len(args) == 0 { - c.user = true - c.envuuid = true - c.srvuuid = true - c.servers = true - c.cacert = true - return nil - } - - var unknown []string - for _, name := range args { - switch name { - case "user": - c.user = true - case "password": - c.password = true - case "environ-uuid": - c.envuuid = true - case "state-servers": - c.servers = true - case "ca-cert": - c.cacert = true - case "server-uuid": - c.srvuuid = true - default: - unknown = append(unknown, fmt.Sprintf("%q", name)) - } - } - if len(unknown) > 0 { - return errors.Errorf("unknown fields: %s", strings.Join(unknown, ", ")) - } - - return nil -} - -func (c *APIInfoCommand) SetFlags(f *gnuflag.FlagSet) { - c.out.AddFlags(f, "default", map[string]cmd.Formatter{ - "default": c.format, - "yaml": cmd.FormatYaml, - "json": cmd.FormatJson, - }) - f.BoolVar(&c.refresh, "refresh", false, "connect to the API to ensure an up-to-date endpoint location") - f.BoolVar(&c.password, "password", false, "include the password in the output fields") -} - -func connectionEndpoint(c envcmd.EnvCommandBase, refresh bool) (configstore.APIEndpoint, error) { - return c.ConnectionEndpoint(refresh) -} - -func connectionCredentials(c envcmd.EnvCommandBase) (configstore.APICredentials, error) { - return c.ConnectionCredentials() -} - -var ( - endpoint = connectionEndpoint - creds = connectionCredentials -) - -// Print out the addresses of the API server endpoints. -func (c *APIInfoCommand) Run(ctx *cmd.Context) error { - apiendpoint, err := endpoint(c.EnvCommandBase, c.refresh) - if err != nil { - return err - } - credentials, err := creds(c.EnvCommandBase) - if err != nil { - return err - } - - var result InfoData - if c.user { - result.User = credentials.User - } - if c.password { - result.Password = credentials.Password - } - if c.envuuid { - result.EnvironUUID = apiendpoint.EnvironUUID - } - if c.servers { - result.StateServers = apiendpoint.Addresses - } - if c.cacert { - result.CACert = apiendpoint.CACert - } - if c.srvuuid { - result.ServerUUID = apiendpoint.ServerUUID - } - - return c.out.Write(ctx, result) -} - -func (c *APIInfoCommand) format(value interface{}) ([]byte, error) { - if len(c.fields) == 1 { - data := value.(InfoData) - field, err := data.field(c.fields[0]) - if err != nil { - return nil, err - } - switch value := field.(type) { - case []string: - return []byte(strings.Join(value, "\n")), nil - case string: - return []byte(value), nil - default: - return nil, errors.Errorf("Unsupported type %T", field) - } - } - - return cmd.FormatYaml(value) -} - -type InfoData struct { - User string `json:"user,omitempty" yaml:",omitempty"` - Password string `json:"password,omitempty" yaml:",omitempty"` - EnvironUUID string `json:"environ-uuid,omitempty" yaml:"environ-uuid,omitempty"` - ServerUUID string `json:"server-uuid,omitempty" yaml:"server-uuid,omitempty"` - StateServers []string `json:"state-servers,omitempty" yaml:"state-servers,omitempty"` - CACert string `json:"ca-cert,omitempty" yaml:"ca-cert,omitempty"` -} - -func (i *InfoData) field(name string) (interface{}, error) { - switch name { - case "user": - return i.User, nil - case "password": - return i.Password, nil - case "environ-uuid": - return i.EnvironUUID, nil - case "state-servers": - return i.StateServers, nil - case "ca-cert": - return i.CACert, nil - case "server-uuid": - return i.ServerUUID, nil - default: - return "", errors.Errorf("unknown field %q", name) - } -} === removed file 'src/github.com/juju/juju/cmd/juju/apiinfo_test.go' --- src/github.com/juju/juju/cmd/juju/apiinfo_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/apiinfo_test.go 1970-01-01 00:00:00 +0000 @@ -1,282 +0,0 @@ -// Copyright 2014 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/environs/configstore" - "github.com/juju/juju/testing" -) - -type APIInfoSuite struct { - testing.FakeJujuHomeSuite -} - -var _ = gc.Suite(&APIInfoSuite{}) - -func (s *APIInfoSuite) TestArgParsing(c *gc.C) { - for i, test := range []struct { - message string - args []string - refresh bool - user bool - password bool - cacert bool - servers bool - envuuid bool - srvuuid bool - errMatch string - }{ - { - message: "no args skips password", - user: true, - cacert: true, - servers: true, - envuuid: true, - srvuuid: true, - }, { - message: "password shown if user specifies", - args: []string{"--password"}, - user: true, - password: true, - cacert: true, - servers: true, - envuuid: true, - srvuuid: true, - }, { - message: "refresh the cache", - args: []string{"--refresh"}, - refresh: true, - user: true, - cacert: true, - servers: true, - envuuid: true, - srvuuid: true, - }, { - message: "just show the user field", - args: []string{"user"}, - user: true, - }, { - message: "just show the password field", - args: []string{"password"}, - password: true, - }, { - message: "just show the cacert field", - args: []string{"ca-cert"}, - cacert: true, - }, { - message: "just show the servers field", - args: []string{"state-servers"}, - servers: true, - }, { - message: "just show the envuuid field", - args: []string{"environ-uuid"}, - envuuid: true, - }, { - message: "just show the srvuuid field", - args: []string{"server-uuid"}, - srvuuid: true, - }, { - message: "show the user and password field", - args: []string{"user", "password"}, - user: true, - password: true, - }, { - message: "unknown field field", - args: []string{"foo"}, - errMatch: `unknown fields: "foo"`, - }, { - message: "multiple unknown fields", - args: []string{"user", "pwd", "foo"}, - errMatch: `unknown fields: "pwd", "foo"`, - }, - } { - c.Logf("test %v: %s", i, test.message) - command := &APIInfoCommand{} - err := testing.InitCommand(envcmd.Wrap(command), test.args) - if test.errMatch == "" { - c.Check(err, jc.ErrorIsNil) - c.Check(command.refresh, gc.Equals, test.refresh) - c.Check(command.user, gc.Equals, test.user) - c.Check(command.password, gc.Equals, test.password) - c.Check(command.cacert, gc.Equals, test.cacert) - c.Check(command.servers, gc.Equals, test.servers) - c.Check(command.envuuid, gc.Equals, test.envuuid) - c.Check(command.srvuuid, gc.Equals, test.srvuuid) - } else { - c.Check(err, gc.ErrorMatches, test.errMatch) - } - } -} - -func (s *APIInfoSuite) TestOutput(c *gc.C) { - s.PatchValue(&endpoint, func(c envcmd.EnvCommandBase, refresh bool) (configstore.APIEndpoint, error) { - return configstore.APIEndpoint{ - Addresses: []string{"localhost:12345", "10.0.3.1:12345"}, - CACert: "this is the cacert", - EnvironUUID: "deadbeef-dead-beef-dead-deaddeaddead", - ServerUUID: "bad0f00d-dead-beef-0000-01234567899a", - }, nil - }) - s.PatchValue(&creds, func(c envcmd.EnvCommandBase) (configstore.APICredentials, error) { - return configstore.APICredentials{ - User: "tester", - Password: "sekrit", - }, nil - }) - - for i, test := range []struct { - args []string - output string - errMatch string - }{ - { - output: "" + - "user: tester\n" + - "environ-uuid: deadbeef-dead-beef-dead-deaddeaddead\n" + - "server-uuid: bad0f00d-dead-beef-0000-01234567899a\n" + - "state-servers:\n" + - "- localhost:12345\n" + - "- 10.0.3.1:12345\n" + - "ca-cert: this is the cacert\n", - }, { - args: []string{"--password"}, - output: "" + - "user: tester\n" + - "password: sekrit\n" + - "environ-uuid: deadbeef-dead-beef-dead-deaddeaddead\n" + - "server-uuid: bad0f00d-dead-beef-0000-01234567899a\n" + - "state-servers:\n" + - "- localhost:12345\n" + - "- 10.0.3.1:12345\n" + - "ca-cert: this is the cacert\n", - }, { - args: []string{"--format=yaml"}, - output: "" + - "user: tester\n" + - "environ-uuid: deadbeef-dead-beef-dead-deaddeaddead\n" + - "server-uuid: bad0f00d-dead-beef-0000-01234567899a\n" + - "state-servers:\n" + - "- localhost:12345\n" + - "- 10.0.3.1:12345\n" + - "ca-cert: this is the cacert\n", - }, { - args: []string{"--format=json"}, - output: `{"user":"tester",` + - `"environ-uuid":"deadbeef-dead-beef-dead-deaddeaddead",` + - `"server-uuid":"bad0f00d-dead-beef-0000-01234567899a",` + - `"state-servers":["localhost:12345","10.0.3.1:12345"],` + - `"ca-cert":"this is the cacert"}` + "\n", - }, { - args: []string{"user"}, - output: "tester\n", - }, { - args: []string{"user", "password"}, - output: "" + - "user: tester\n" + - "password: sekrit\n", - }, { - args: []string{"state-servers"}, - output: "" + - "localhost:12345\n" + - "10.0.3.1:12345\n", - }, { - args: []string{"--format=yaml", "user"}, - output: "user: tester\n", - }, { - args: []string{"--format=yaml", "user", "password"}, - output: "" + - "user: tester\n" + - "password: sekrit\n", - }, { - args: []string{"--format=yaml", "state-servers"}, - output: "" + - "state-servers:\n" + - "- localhost:12345\n" + - "- 10.0.3.1:12345\n", - }, { - args: []string{"--format=json", "user"}, - output: `{"user":"tester"}` + "\n", - }, { - args: []string{"--format=json", "user", "password"}, - output: `{"user":"tester","password":"sekrit"}` + "\n", - }, { - args: []string{"--format=json", "state-servers"}, - output: `{"state-servers":["localhost:12345","10.0.3.1:12345"]}` + "\n", - }, - } { - c.Logf("test %v: %v", i, test.args) - command := &APIInfoCommand{} - ctx, err := testing.RunCommand(c, envcmd.Wrap(command), test.args...) - if test.errMatch == "" { - c.Check(err, jc.ErrorIsNil) - c.Check(testing.Stdout(ctx), gc.Equals, test.output) - } else { - c.Check(err, gc.ErrorMatches, test.errMatch) - } - } -} - -func (s *APIInfoSuite) TestOutputNoServerUUID(c *gc.C) { - s.PatchValue(&endpoint, func(c envcmd.EnvCommandBase, refresh bool) (configstore.APIEndpoint, error) { - return configstore.APIEndpoint{ - Addresses: []string{"localhost:12345", "10.0.3.1:12345"}, - CACert: "this is the cacert", - EnvironUUID: "deadbeef-dead-beef-dead-deaddeaddead", - }, nil - }) - s.PatchValue(&creds, func(c envcmd.EnvCommandBase) (configstore.APICredentials, error) { - return configstore.APICredentials{ - User: "tester", - Password: "sekrit", - }, nil - }) - - expected := "" + - "user: tester\n" + - "environ-uuid: deadbeef-dead-beef-dead-deaddeaddead\n" + - "state-servers:\n" + - "- localhost:12345\n" + - "- 10.0.3.1:12345\n" + - "ca-cert: this is the cacert\n" - command := &APIInfoCommand{} - ctx, err := testing.RunCommand(c, envcmd.Wrap(command)) - c.Check(err, jc.ErrorIsNil) - c.Check(testing.Stdout(ctx), gc.Equals, expected) -} - -func (s *APIInfoSuite) TestEndpointError(c *gc.C) { - s.PatchValue(&endpoint, func(c envcmd.EnvCommandBase, refresh bool) (configstore.APIEndpoint, error) { - return configstore.APIEndpoint{}, fmt.Errorf("oops, no endpoint") - }) - s.PatchValue(&creds, func(c envcmd.EnvCommandBase) (configstore.APICredentials, error) { - return configstore.APICredentials{}, nil - }) - command := &APIInfoCommand{} - _, err := testing.RunCommand(c, envcmd.Wrap(command)) - c.Assert(err, gc.ErrorMatches, "oops, no endpoint") -} - -func (s *APIInfoSuite) TestCredentialsError(c *gc.C) { - s.PatchValue(&endpoint, func(c envcmd.EnvCommandBase, refresh bool) (configstore.APIEndpoint, error) { - return configstore.APIEndpoint{}, nil - }) - s.PatchValue(&creds, func(c envcmd.EnvCommandBase) (configstore.APICredentials, error) { - return configstore.APICredentials{}, fmt.Errorf("oops, no creds") - }) - command := &APIInfoCommand{} - _, err := testing.RunCommand(c, envcmd.Wrap(command)) - c.Assert(err, gc.ErrorMatches, "oops, no creds") -} - -func (s *APIInfoSuite) TestNoEnvironment(c *gc.C) { - command := &APIInfoCommand{} - _, err := testing.RunCommand(c, envcmd.Wrap(command)) - c.Assert(err, gc.ErrorMatches, `environment "erewhemos" not found`) -} === removed file 'src/github.com/juju/juju/cmd/juju/authorizedkeys.go' --- src/github.com/juju/juju/cmd/juju/authorizedkeys.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/authorizedkeys.go 1970-01-01 00:00:00 +0000 @@ -1,57 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "github.com/juju/cmd" - "launchpad.net/gnuflag" - - "github.com/juju/juju/api/keymanager" - "github.com/juju/juju/cmd/envcmd" -) - -var authKeysDoc = ` -"juju authorized-keys" is used to manage the ssh keys allowed to log on to -nodes in the Juju environment. - -` - -type AuthorizedKeysCommand struct { - *cmd.SuperCommand -} - -type AuthorizedKeysBase struct { - envcmd.EnvCommandBase -} - -// NewKeyManagerClient returns a keymanager client for the root api endpoint -// that the environment command returns. -func (c *AuthorizedKeysBase) NewKeyManagerClient() (*keymanager.Client, error) { - root, err := c.NewAPIRoot() - if err != nil { - return nil, err - } - return keymanager.NewClient(root), nil -} - -func NewAuthorizedKeysCommand() cmd.Command { - sshkeyscmd := &AuthorizedKeysCommand{ - SuperCommand: cmd.NewSuperCommand(cmd.SuperCommandParams{ - Name: "authorized-keys", - Doc: authKeysDoc, - UsagePrefix: "juju", - Purpose: "manage authorized ssh keys", - Aliases: []string{"authorised-keys"}, - }), - } - sshkeyscmd.Register(envcmd.Wrap(&AddKeysCommand{})) - sshkeyscmd.Register(envcmd.Wrap(&DeleteKeysCommand{})) - sshkeyscmd.Register(envcmd.Wrap(&ImportKeysCommand{})) - sshkeyscmd.Register(envcmd.Wrap(&ListKeysCommand{})) - return sshkeyscmd -} - -func (c *AuthorizedKeysCommand) SetFlags(f *gnuflag.FlagSet) { - c.SetCommonFlags(f) -} === removed file 'src/github.com/juju/juju/cmd/juju/authorizedkeys_add.go' --- src/github.com/juju/juju/cmd/juju/authorizedkeys_add.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/authorizedkeys_add.go 1970-01-01 00:00:00 +0000 @@ -1,67 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "errors" - "fmt" - - "github.com/juju/cmd" - "launchpad.net/gnuflag" - - "github.com/juju/juju/cmd/juju/block" -) - -var addKeysDoc = ` -Add new authorized ssh keys to allow the holder of those keys to log on to Juju nodes or machines. -` - -// AddKeysCommand is used to add a new authorized ssh key for a user. -type AddKeysCommand struct { - AuthorizedKeysBase - user string - sshKeys []string -} - -func (c *AddKeysCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "add", - Args: " [...]", - Doc: addKeysDoc, - Purpose: "add new authorized ssh keys for a Juju user", - } -} - -func (c *AddKeysCommand) Init(args []string) error { - switch len(args) { - case 0: - return errors.New("no ssh key specified") - default: - c.sshKeys = args - } - return nil -} - -func (c *AddKeysCommand) SetFlags(f *gnuflag.FlagSet) { - f.StringVar(&c.user, "user", "admin", "the user for which to add the keys") -} - -func (c *AddKeysCommand) Run(context *cmd.Context) error { - client, err := c.NewKeyManagerClient() - if err != nil { - return err - } - defer client.Close() - - results, err := client.AddKeys(c.user, c.sshKeys...) - if err != nil { - return block.ProcessBlockedError(err, block.BlockChange) - } - for i, result := range results { - if result.Error != nil { - fmt.Fprintf(context.Stderr, "cannot add key %q: %v\n", c.sshKeys[i], result.Error) - } - } - return nil -} === removed file 'src/github.com/juju/juju/cmd/juju/authorizedkeys_delete.go' --- src/github.com/juju/juju/cmd/juju/authorizedkeys_delete.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/authorizedkeys_delete.go 1970-01-01 00:00:00 +0000 @@ -1,69 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "errors" - "fmt" - - "github.com/juju/cmd" - "launchpad.net/gnuflag" - - "github.com/juju/juju/cmd/juju/block" -) - -var deleteKeysDoc = ` -Delete existing authorized ssh keys to remove ssh access for the holder of those keys. -The keys to delete are found by specifying either the "comment" portion of the ssh key, -typically something like "user@host", or the key fingerprint found by using ssh-keygen. -` - -// DeleteKeysCommand is used to delete authorized ssh keys for a user. -type DeleteKeysCommand struct { - AuthorizedKeysBase - user string - keyIds []string -} - -func (c *DeleteKeysCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "delete", - Args: " [...]", - Doc: deleteKeysDoc, - Purpose: "delete authorized ssh keys for a Juju user", - } -} - -func (c *DeleteKeysCommand) Init(args []string) error { - switch len(args) { - case 0: - return errors.New("no ssh key id specified") - default: - c.keyIds = args - } - return nil -} - -func (c *DeleteKeysCommand) SetFlags(f *gnuflag.FlagSet) { - f.StringVar(&c.user, "user", "admin", "the user for which to delete the keys") -} - -func (c *DeleteKeysCommand) Run(context *cmd.Context) error { - client, err := c.NewKeyManagerClient() - if err != nil { - return err - } - defer client.Close() - - results, err := client.DeleteKeys(c.user, c.keyIds...) - if err != nil { - return block.ProcessBlockedError(err, block.BlockChange) - } - for i, result := range results { - if result.Error != nil { - fmt.Fprintf(context.Stderr, "cannot delete key id %q: %v\n", c.keyIds[i], result.Error) - } - } - return nil -} === removed file 'src/github.com/juju/juju/cmd/juju/authorizedkeys_import.go' --- src/github.com/juju/juju/cmd/juju/authorizedkeys_import.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/authorizedkeys_import.go 1970-01-01 00:00:00 +0000 @@ -1,68 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "errors" - "fmt" - - "github.com/juju/cmd" - "launchpad.net/gnuflag" - - "github.com/juju/juju/cmd/juju/block" -) - -var importKeysDoc = ` -Import new authorized ssh keys to allow the holder of those keys to log on to Juju nodes or machines. -The keys are imported using ssh-import-id. -` - -// ImportKeysCommand is used to add new authorized ssh keys for a user. -type ImportKeysCommand struct { - AuthorizedKeysBase - user string - sshKeyIds []string -} - -func (c *ImportKeysCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "import", - Args: " [...]", - Doc: importKeysDoc, - Purpose: "using ssh-import-id, import new authorized ssh keys for a Juju user", - } -} - -func (c *ImportKeysCommand) Init(args []string) error { - switch len(args) { - case 0: - return errors.New("no ssh key id specified") - default: - c.sshKeyIds = args - } - return nil -} - -func (c *ImportKeysCommand) SetFlags(f *gnuflag.FlagSet) { - f.StringVar(&c.user, "user", "admin", "the user for which to import the keys") -} - -func (c *ImportKeysCommand) Run(context *cmd.Context) error { - client, err := c.NewKeyManagerClient() - if err != nil { - return err - } - defer client.Close() - - results, err := client.ImportKeys(c.user, c.sshKeyIds...) - if err != nil { - return block.ProcessBlockedError(err, block.BlockChange) - } - for i, result := range results { - if result.Error != nil { - fmt.Fprintf(context.Stderr, "cannot import key id %q: %v\n", c.sshKeyIds[i], result.Error) - } - } - return nil -} === removed file 'src/github.com/juju/juju/cmd/juju/authorizedkeys_list.go' --- src/github.com/juju/juju/cmd/juju/authorizedkeys_list.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/authorizedkeys_list.go 1970-01-01 00:00:00 +0000 @@ -1,64 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - "strings" - - "github.com/juju/cmd" - "launchpad.net/gnuflag" - - "github.com/juju/juju/utils/ssh" -) - -var listKeysDoc = ` -List a user's authorized ssh keys, allowing the holders of those keys to log on to Juju nodes. -By default, just the key fingerprint is printed. Use --full to display the entire key. - -` - -// ListKeysCommand is used to list the authorized ssh keys. -type ListKeysCommand struct { - AuthorizedKeysBase - showFullKey bool - user string -} - -func (c *ListKeysCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "list", - Doc: listKeysDoc, - Purpose: "list authorized ssh keys for a specified user", - } -} - -func (c *ListKeysCommand) SetFlags(f *gnuflag.FlagSet) { - f.BoolVar(&c.showFullKey, "full", false, "show full key instead of just the key fingerprint") - f.StringVar(&c.user, "user", "admin", "the user for which to list the keys") -} - -func (c *ListKeysCommand) Run(context *cmd.Context) error { - client, err := c.NewKeyManagerClient() - if err != nil { - return err - } - defer client.Close() - - mode := ssh.Fingerprints - if c.showFullKey { - mode = ssh.FullKeys - } - results, err := client.ListKeys(mode, c.user) - if err != nil { - return err - } - result := results[0] - if result.Error != nil { - return result.Error - } - fmt.Fprintf(context.Stdout, "Keys for user %s:\n", c.user) - fmt.Fprintln(context.Stdout, strings.Join(result.Result, "\n")) - return nil -} === removed file 'src/github.com/juju/juju/cmd/juju/authorizedkeys_test.go' --- src/github.com/juju/juju/cmd/juju/authorizedkeys_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/authorizedkeys_test.go 1970-01-01 00:00:00 +0000 @@ -1,289 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - "strings" - - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - keymanagerserver "github.com/juju/juju/apiserver/keymanager" - keymanagertesting "github.com/juju/juju/apiserver/keymanager/testing" - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/juju/osenv" - jujutesting "github.com/juju/juju/juju/testing" - coretesting "github.com/juju/juju/testing" - "github.com/juju/juju/testing/factory" - sshtesting "github.com/juju/juju/utils/ssh/testing" -) - -type AuthorizedKeysSuite struct { - coretesting.FakeJujuHomeSuite -} - -var _ = gc.Suite(&AuthorizedKeysSuite{}) - -var authKeysCommandNames = []string{ - "add", - "delete", - "help", - "import", - "list", -} - -func (s *AuthorizedKeysSuite) TestHelpCommands(c *gc.C) { - // Check that we have correctly registered all the sub commands - // by checking the help output. - out := badrun(c, 0, "authorized-keys", "--help") - lines := strings.Split(out, "\n") - var names []string - subcommandsFound := false - for _, line := range lines { - f := strings.Fields(line) - if len(f) == 1 && f[0] == "commands:" { - subcommandsFound = true - continue - } - if !subcommandsFound || len(f) == 0 || !strings.HasPrefix(line, " ") { - continue - } - names = append(names, f[0]) - } - // The names should be output in alphabetical order, so don't sort. - c.Assert(names, gc.DeepEquals, authKeysCommandNames) -} - -func (s *AuthorizedKeysSuite) assertHelpOutput(c *gc.C, cmd, args string) { - if args != "" { - args = " " + args - } - expected := fmt.Sprintf("usage: juju authorized-keys %s [options]%s", cmd, args) - out := badrun(c, 0, "authorized-keys", cmd, "--help") - lines := strings.Split(out, "\n") - c.Assert(lines[0], gc.Equals, expected) -} - -func (s *AuthorizedKeysSuite) TestHelpList(c *gc.C) { - s.assertHelpOutput(c, "list", "") -} - -func (s *AuthorizedKeysSuite) TestHelpAdd(c *gc.C) { - s.assertHelpOutput(c, "add", " [...]") -} - -func (s *AuthorizedKeysSuite) TestHelpDelete(c *gc.C) { - s.assertHelpOutput(c, "delete", " [...]") -} - -func (s *AuthorizedKeysSuite) TestHelpImport(c *gc.C) { - s.assertHelpOutput(c, "import", " [...]") -} - -type keySuiteBase struct { - jujutesting.JujuConnSuite - CmdBlockHelper -} - -func (s *keySuiteBase) SetUpSuite(c *gc.C) { - s.JujuConnSuite.SetUpSuite(c) - s.PatchEnvironment(osenv.JujuEnvEnvKey, "dummyenv") -} - -func (s *keySuiteBase) SetUpTest(c *gc.C) { - s.JujuConnSuite.SetUpTest(c) - s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) - c.Assert(s.CmdBlockHelper, gc.NotNil) - s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) -} - -func (s *keySuiteBase) setAuthorizedKeys(c *gc.C, keys ...string) { - keyString := strings.Join(keys, "\n") - err := s.State.UpdateEnvironConfig(map[string]interface{}{"authorized-keys": keyString}, nil, nil) - c.Assert(err, jc.ErrorIsNil) - envConfig, err := s.State.EnvironConfig() - c.Assert(err, jc.ErrorIsNil) - c.Assert(envConfig.AuthorizedKeys(), gc.Equals, keyString) -} - -func (s *keySuiteBase) assertEnvironKeys(c *gc.C, expected ...string) { - envConfig, err := s.State.EnvironConfig() - c.Assert(err, jc.ErrorIsNil) - keys := envConfig.AuthorizedKeys() - c.Assert(keys, gc.Equals, strings.Join(expected, "\n")) -} - -type ListKeysSuite struct { - keySuiteBase -} - -var _ = gc.Suite(&ListKeysSuite{}) - -func (s *ListKeysSuite) TestListKeys(c *gc.C) { - key1 := sshtesting.ValidKeyOne.Key + " user@host" - key2 := sshtesting.ValidKeyTwo.Key + " another@host" - s.setAuthorizedKeys(c, key1, key2) - - context, err := coretesting.RunCommand(c, envcmd.Wrap(&ListKeysCommand{})) - c.Assert(err, jc.ErrorIsNil) - output := strings.TrimSpace(coretesting.Stdout(context)) - c.Assert(err, jc.ErrorIsNil) - c.Assert(output, gc.Matches, "Keys for user admin:\n.*\\(user@host\\)\n.*\\(another@host\\)") -} - -func (s *ListKeysSuite) TestListFullKeys(c *gc.C) { - key1 := sshtesting.ValidKeyOne.Key + " user@host" - key2 := sshtesting.ValidKeyTwo.Key + " another@host" - s.setAuthorizedKeys(c, key1, key2) - - context, err := coretesting.RunCommand(c, envcmd.Wrap(&ListKeysCommand{}), "--full") - c.Assert(err, jc.ErrorIsNil) - output := strings.TrimSpace(coretesting.Stdout(context)) - c.Assert(err, jc.ErrorIsNil) - c.Assert(output, gc.Matches, "Keys for user admin:\n.*user@host\n.*another@host") -} - -func (s *ListKeysSuite) TestListKeysNonDefaultUser(c *gc.C) { - key1 := sshtesting.ValidKeyOne.Key + " user@host" - key2 := sshtesting.ValidKeyTwo.Key + " another@host" - s.setAuthorizedKeys(c, key1, key2) - s.Factory.MakeUser(c, &factory.UserParams{Name: "fred"}) - - context, err := coretesting.RunCommand(c, envcmd.Wrap(&ListKeysCommand{}), "--user", "fred") - c.Assert(err, jc.ErrorIsNil) - output := strings.TrimSpace(coretesting.Stdout(context)) - c.Assert(err, jc.ErrorIsNil) - c.Assert(output, gc.Matches, "Keys for user fred:\n.*\\(user@host\\)\n.*\\(another@host\\)") -} - -func (s *ListKeysSuite) TestTooManyArgs(c *gc.C) { - _, err := coretesting.RunCommand(c, envcmd.Wrap(&ListKeysCommand{}), "foo") - c.Assert(err, gc.ErrorMatches, `unrecognized args: \["foo"\]`) -} - -type AddKeySuite struct { - keySuiteBase -} - -var _ = gc.Suite(&AddKeySuite{}) - -func (s *AddKeySuite) TestAddKey(c *gc.C) { - key1 := sshtesting.ValidKeyOne.Key + " user@host" - s.setAuthorizedKeys(c, key1) - - key2 := sshtesting.ValidKeyTwo.Key + " another@host" - context, err := coretesting.RunCommand(c, envcmd.Wrap(&AddKeysCommand{}), key2, "invalid-key") - c.Assert(err, jc.ErrorIsNil) - c.Assert(coretesting.Stderr(context), gc.Matches, `cannot add key "invalid-key".*\n`) - s.assertEnvironKeys(c, key1, key2) -} - -func (s *AddKeySuite) TestBlockAddKey(c *gc.C) { - key1 := sshtesting.ValidKeyOne.Key + " user@host" - s.setAuthorizedKeys(c, key1) - - key2 := sshtesting.ValidKeyTwo.Key + " another@host" - // Block operation - s.BlockAllChanges(c, "TestBlockAddKey") - _, err := coretesting.RunCommand(c, envcmd.Wrap(&AddKeysCommand{}), key2, "invalid-key") - s.AssertBlocked(c, err, ".*TestBlockAddKey.*") -} - -func (s *AddKeySuite) TestAddKeyNonDefaultUser(c *gc.C) { - key1 := sshtesting.ValidKeyOne.Key + " user@host" - s.setAuthorizedKeys(c, key1) - s.Factory.MakeUser(c, &factory.UserParams{Name: "fred"}) - - key2 := sshtesting.ValidKeyTwo.Key + " another@host" - context, err := coretesting.RunCommand(c, envcmd.Wrap(&AddKeysCommand{}), "--user", "fred", key2) - c.Assert(err, jc.ErrorIsNil) - c.Assert(coretesting.Stderr(context), gc.Equals, "") - s.assertEnvironKeys(c, key1, key2) -} - -type DeleteKeySuite struct { - keySuiteBase -} - -var _ = gc.Suite(&DeleteKeySuite{}) - -func (s *DeleteKeySuite) TestDeleteKeys(c *gc.C) { - key1 := sshtesting.ValidKeyOne.Key + " user@host" - key2 := sshtesting.ValidKeyTwo.Key + " another@host" - s.setAuthorizedKeys(c, key1, key2) - - context, err := coretesting.RunCommand(c, envcmd.Wrap(&DeleteKeysCommand{}), - sshtesting.ValidKeyTwo.Fingerprint, "invalid-key") - c.Assert(err, jc.ErrorIsNil) - c.Assert(coretesting.Stderr(context), gc.Matches, `cannot delete key id "invalid-key".*\n`) - s.assertEnvironKeys(c, key1) -} - -func (s *DeleteKeySuite) TestBlockDeleteKeys(c *gc.C) { - key1 := sshtesting.ValidKeyOne.Key + " user@host" - key2 := sshtesting.ValidKeyTwo.Key + " another@host" - s.setAuthorizedKeys(c, key1, key2) - - // Block operation - s.BlockAllChanges(c, "TestBlockDeleteKeys") - _, err := coretesting.RunCommand(c, envcmd.Wrap(&DeleteKeysCommand{}), - sshtesting.ValidKeyTwo.Fingerprint, "invalid-key") - s.AssertBlocked(c, err, ".*TestBlockDeleteKeys.*") -} - -func (s *DeleteKeySuite) TestDeleteKeyNonDefaultUser(c *gc.C) { - key1 := sshtesting.ValidKeyOne.Key + " user@host" - key2 := sshtesting.ValidKeyTwo.Key + " another@host" - s.setAuthorizedKeys(c, key1, key2) - s.Factory.MakeUser(c, &factory.UserParams{Name: "fred"}) - - context, err := coretesting.RunCommand(c, envcmd.Wrap(&DeleteKeysCommand{}), - "--user", "fred", sshtesting.ValidKeyTwo.Fingerprint) - c.Assert(err, jc.ErrorIsNil) - c.Assert(coretesting.Stderr(context), gc.Equals, "") - s.assertEnvironKeys(c, key1) -} - -type ImportKeySuite struct { - keySuiteBase -} - -var _ = gc.Suite(&ImportKeySuite{}) - -func (s *ImportKeySuite) SetUpTest(c *gc.C) { - s.keySuiteBase.SetUpTest(c) - s.PatchValue(&keymanagerserver.RunSSHImportId, keymanagertesting.FakeImport) -} - -func (s *ImportKeySuite) TestImportKeys(c *gc.C) { - key1 := sshtesting.ValidKeyOne.Key + " user@host" - s.setAuthorizedKeys(c, key1) - - context, err := coretesting.RunCommand(c, envcmd.Wrap(&ImportKeysCommand{}), "lp:validuser", "invalid-key") - c.Assert(err, jc.ErrorIsNil) - c.Assert(coretesting.Stderr(context), gc.Matches, `cannot import key id "invalid-key".*\n`) - s.assertEnvironKeys(c, key1, sshtesting.ValidKeyThree.Key) -} - -func (s *ImportKeySuite) TestBlockImportKeys(c *gc.C) { - key1 := sshtesting.ValidKeyOne.Key + " user@host" - s.setAuthorizedKeys(c, key1) - - // Block operation - s.BlockAllChanges(c, "TestBlockImportKeys") - _, err := coretesting.RunCommand(c, envcmd.Wrap(&ImportKeysCommand{}), "lp:validuser", "invalid-key") - s.AssertBlocked(c, err, ".*TestBlockImportKeys.*") -} - -func (s *ImportKeySuite) TestImportKeyNonDefaultUser(c *gc.C) { - key1 := sshtesting.ValidKeyOne.Key + " user@host" - s.setAuthorizedKeys(c, key1) - s.Factory.MakeUser(c, &factory.UserParams{Name: "fred"}) - - context, err := coretesting.RunCommand(c, envcmd.Wrap(&ImportKeysCommand{}), "--user", "fred", "lp:validuser") - c.Assert(err, jc.ErrorIsNil) - c.Assert(coretesting.Stderr(context), gc.Equals, "") - s.assertEnvironKeys(c, key1, sshtesting.ValidKeyThree.Key) -} === modified file 'src/github.com/juju/juju/cmd/juju/backups/backups.go' --- src/github.com/juju/juju/cmd/juju/backups/backups.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/backups/backups.go 2015-10-23 18:29:32 +0000 @@ -10,11 +10,13 @@ "github.com/juju/cmd" "github.com/juju/errors" + "github.com/juju/utils/featureflag" "github.com/juju/juju/api/backups" apiserverbackups "github.com/juju/juju/apiserver/backups" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/feature" statebackups "github.com/juju/juju/state/backups" ) @@ -22,6 +24,14 @@ "juju backups" is used to manage backups of the state of a juju environment. ` +var jesBackupsDoc = ` +"juju backups" is used to manage backups of the state of a juju system. +Backups are only supported on juju systems, not hosted environments. For +more information on juju systems, see: + + juju help juju-systems +` + const backupsPurpose = "create, manage, and restore backups of juju's state" // Command is the top-level command wrapping all backups functionality. @@ -31,6 +41,10 @@ // NewCommand returns a new backups super-command. func NewCommand() cmd.Command { + if featureflag.Enabled(feature.JES) { + backupsDoc = jesBackupsDoc + } + backupsCmd := Command{ SuperCommand: *cmd.NewSuperCommand( cmd.SuperCommandParams{ === modified file 'src/github.com/juju/juju/cmd/juju/backups/backups_test.go' --- src/github.com/juju/juju/cmd/juju/backups/backups_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/backups/backups_test.go 2015-10-23 18:29:32 +0000 @@ -49,7 +49,7 @@ ctx, err := testing.RunCommand(c, s.command, "--help") c.Assert(err, jc.ErrorIsNil) - expected := "(?s)usage: juju backups .+" + expected := "(?s)usage: juju backups \\[options\\] .+" c.Check(testing.Stdout(ctx), gc.Matches, expected) expected = "(?sm).*^purpose: " + s.command.Purpose + "$.*" c.Check(testing.Stdout(ctx), gc.Matches, expected) === modified file 'src/github.com/juju/juju/cmd/juju/backups/restore.go' --- src/github.com/juju/juju/cmd/juju/backups/restore.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/backups/restore.go 2015-10-23 18:29:32 +0000 @@ -134,7 +134,7 @@ if err != nil { return errors.Trace(err) } - cfg, err := c.Config(store) + cfg, err := c.Config(store, nil) if err != nil { return errors.Trace(err) } === modified file 'src/github.com/juju/juju/cmd/juju/block/unblock.go' --- src/github.com/juju/juju/cmd/juju/block/unblock.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/block/unblock.go 2015-10-23 18:29:32 +0000 @@ -29,7 +29,7 @@ This is done by blocking certain commands from successful execution. Blocked commands must be manually unblocked to proceed. -Some comands offer a --force option that can be used to bypass a block. +Some commands offer a --force option that can be used to bypass a block. Commands that can be unblocked are grouped based on logical operations as follows: === removed file 'src/github.com/juju/juju/cmd/juju/bootstrap.go' --- src/github.com/juju/juju/cmd/juju/bootstrap.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/bootstrap.go 1970-01-01 00:00:00 +0000 @@ -1,447 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - "os" - "strings" - "time" - - "github.com/juju/cmd" - "github.com/juju/errors" - "github.com/juju/utils" - "github.com/juju/utils/featureflag" - "gopkg.in/juju/charm.v5" - "launchpad.net/gnuflag" - - apiblock "github.com/juju/juju/api/block" - "github.com/juju/juju/apiserver" - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/cmd/juju/block" - "github.com/juju/juju/constraints" - "github.com/juju/juju/environs" - "github.com/juju/juju/environs/bootstrap" - "github.com/juju/juju/environs/configstore" - "github.com/juju/juju/feature" - "github.com/juju/juju/instance" - "github.com/juju/juju/juju" - "github.com/juju/juju/juju/osenv" - "github.com/juju/juju/network" - "github.com/juju/juju/provider" -) - -// provisionalProviders is the names of providers that are hidden behind -// feature flags. -var provisionalProviders = map[string]string{ - "cloudsigma": feature.CloudSigma, - "vsphere": feature.VSphereProvider, -} - -const bootstrapDoc = ` -bootstrap starts a new environment of the current type (it will return an error -if the environment has already been bootstrapped). Bootstrapping an environment -will provision a new machine in the environment and run the juju state server on -that machine. - -If constraints are specified in the bootstrap command, they will apply to the -machine provisioned for the juju state server. They will also be set as default -constraints on the environment for all future machines, exactly as if the -constraints were set with juju set-constraints. - -It is possible to override constraints and the automatic machine selection -algorithm by using the "--to" flag. The value associated with "--to" is a -"placement directive", which tells Juju how to identify the first machine to use. -For more information on placement directives, see "juju help placement". - -Bootstrap initializes the cloud environment synchronously and displays information -about the current installation steps. The time for bootstrap to complete varies -across cloud providers from a few seconds to several minutes. Once bootstrap has -completed, you can run other juju commands against your environment. You can change -the default timeout and retry delays used during the bootstrap by changing the -following settings in your environments.yaml (all values represent number of seconds): - - # How long to wait for a connection to the state server. - bootstrap-timeout: 600 # default: 10 minutes - # How long to wait between connection attempts to a state server address. - bootstrap-retry-delay: 5 # default: 5 seconds - # How often to refresh state server addresses from the API server. - bootstrap-addresses-delay: 10 # default: 10 seconds - -Private clouds may need to specify their own custom image metadata, and possibly upload -Juju tools to cloud storage if no outgoing Internet access is available. In this case, -use the --metadata-source paramater to tell bootstrap a local directory from which to -upload tools and/or image metadata. - -See Also: - juju help switch - juju help constraints - juju help set-constraints - juju help placement -` - -// BootstrapCommand is responsible for launching the first machine in a juju -// environment, and setting up everything necessary to continue working. -type BootstrapCommand struct { - envcmd.EnvCommandBase - Constraints constraints.Value - UploadTools bool - Series []string - seriesOld []string - MetadataSource string - Placement string - KeepBrokenEnvironment bool -} - -func (c *BootstrapCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "bootstrap", - Purpose: "start up an environment from scratch", - Doc: bootstrapDoc, - } -} - -func (c *BootstrapCommand) SetFlags(f *gnuflag.FlagSet) { - f.Var(constraints.ConstraintsValue{Target: &c.Constraints}, "constraints", "set environment constraints") - f.BoolVar(&c.UploadTools, "upload-tools", false, "upload local version of tools before bootstrapping") - f.Var(newSeriesValue(nil, &c.Series), "upload-series", "upload tools for supplied comma-separated series list (OBSOLETE)") - f.Var(newSeriesValue(nil, &c.seriesOld), "series", "see --upload-series (OBSOLETE)") - f.StringVar(&c.MetadataSource, "metadata-source", "", "local path to use as tools and/or metadata source") - f.StringVar(&c.Placement, "to", "", "a placement directive indicating an instance to bootstrap") - f.BoolVar(&c.KeepBrokenEnvironment, "keep-broken", false, "do not destroy the environment if bootstrap fails") -} - -func (c *BootstrapCommand) Init(args []string) (err error) { - if len(c.Series) > 0 && !c.UploadTools { - return fmt.Errorf("--upload-series requires --upload-tools") - } - if len(c.seriesOld) > 0 && !c.UploadTools { - return fmt.Errorf("--series requires --upload-tools") - } - if len(c.Series) > 0 && len(c.seriesOld) > 0 { - return fmt.Errorf("--upload-series and --series can't be used together") - } - - // Parse the placement directive. Bootstrap currently only - // supports provider-specific placement directives. - if c.Placement != "" { - _, err = instance.ParsePlacement(c.Placement) - if err != instance.ErrPlacementScopeMissing { - // We only support unscoped placement directives for bootstrap. - return fmt.Errorf("unsupported bootstrap placement directive %q", c.Placement) - } - } - return cmd.CheckEmpty(args) -} - -type seriesValue struct { - *cmd.StringsValue -} - -// newSeriesValue is used to create the type passed into the gnuflag.FlagSet Var function. -func newSeriesValue(defaultValue []string, target *[]string) *seriesValue { - v := seriesValue{(*cmd.StringsValue)(target)} - *(v.StringsValue) = defaultValue - return &v -} - -// Implements gnuflag.Value Set. -func (v *seriesValue) Set(s string) error { - if err := v.StringsValue.Set(s); err != nil { - return err - } - for _, name := range *(v.StringsValue) { - if !charm.IsValidSeries(name) { - v.StringsValue = nil - return fmt.Errorf("invalid series name %q", name) - } - } - return nil -} - -// bootstrap functionality that Run calls to support cleaner testing -type BootstrapInterface interface { - EnsureNotBootstrapped(env environs.Environ) error - Bootstrap(ctx environs.BootstrapContext, environ environs.Environ, args bootstrap.BootstrapParams) error -} - -type bootstrapFuncs struct{} - -func (b bootstrapFuncs) EnsureNotBootstrapped(env environs.Environ) error { - return bootstrap.EnsureNotBootstrapped(env) -} - -func (b bootstrapFuncs) Bootstrap(ctx environs.BootstrapContext, env environs.Environ, args bootstrap.BootstrapParams) error { - return bootstrap.Bootstrap(ctx, env, args) -} - -var getBootstrapFuncs = func() BootstrapInterface { - return &bootstrapFuncs{} -} - -var getEnvName = func(c *BootstrapCommand) string { - return c.ConnectionName() -} - -// Run connects to the environment specified on the command line and bootstraps -// 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) { - bootstrapFuncs := getBootstrapFuncs() - - if len(c.seriesOld) > 0 { - fmt.Fprintln(ctx.Stderr, "Use of --series is obsolete. --upload-tools now expands to all supported series of the same operating system.") - } - if len(c.Series) > 0 { - fmt.Fprintln(ctx.Stderr, "Use of --upload-series is obsolete. --upload-tools now expands to all supported series of the same operating system.") - } - - envName := getEnvName(c) - if envName == "" { - return errors.Errorf("the name of the environment must be specified") - } - if err := checkProviderType(envName); errors.IsNotFound(err) { - // This error will get handled later. - } else if err != nil { - return errors.Trace(err) - } - - environ, cleanup, err := environFromName( - ctx, - envName, - "Bootstrap", - bootstrapFuncs.EnsureNotBootstrapped, - ) - - // If we error out for any reason, clean up the environment. - defer func() { - if resultErr != nil && cleanup != nil { - if c.KeepBrokenEnvironment { - logger.Warningf("bootstrap failed but --keep-broken was specified so environment is not being destroyed.\n" + - "When you are finished diagnosing the problem, remember to run juju destroy-environment --force\n" + - "to clean up the environment.") - } else { - handleBootstrapError(ctx, resultErr, cleanup) - } - } - }() - - // Handle any errors from environFromName(...). - if err != nil { - return errors.Annotatef(err, "there was an issue examining the environment") - } - - // Check to see if this environment is already bootstrapped. If it - // is, we inform the user and exit early. If an error is returned - // but it is not that the environment is already bootstrapped, - // then we're in an unknown state. - if err := bootstrapFuncs.EnsureNotBootstrapped(environ); nil != err { - if environs.ErrAlreadyBootstrapped == err { - logger.Warningf("This juju environment is already bootstrapped. If you want to start a new Juju\nenvironment, first run juju destroy-environment to clean up, or switch to an\nalternative environment.") - return err - } - return errors.Annotatef(err, "cannot determine if environment is already bootstrapped.") - } - - // Block interruption during bootstrap. Providers may also - // register for interrupt notification so they can exit early. - interrupted := make(chan os.Signal, 1) - defer close(interrupted) - ctx.InterruptNotify(interrupted) - defer ctx.StopInterruptNotify(interrupted) - go func() { - for _ = range interrupted { - ctx.Infof("Interrupt signalled: waiting for bootstrap to exit") - } - }() - - // If --metadata-source is specified, override the default tools metadata source so - // SyncTools can use it, and also upload any image metadata. - var metadataDir string - if c.MetadataSource != "" { - metadataDir = ctx.AbsPath(c.MetadataSource) - } - - // TODO (wallyworld): 2013-09-20 bug 1227931 - // We can set a custom tools data source instead of doing an - // unnecessary upload. - if environ.Config().Type() == provider.Local { - c.UploadTools = true - } - - err = bootstrapFuncs.Bootstrap(envcmd.BootstrapContext(ctx), environ, bootstrap.BootstrapParams{ - Constraints: c.Constraints, - Placement: c.Placement, - UploadTools: c.UploadTools, - MetadataDir: metadataDir, - }) - if err != nil { - return errors.Annotate(err, "failed to bootstrap environment") - } - err = c.SetBootstrapEndpointAddress(environ) - if err != nil { - return errors.Annotate(err, "saving bootstrap endpoint address") - } - // To avoid race conditions when running scripted bootstraps, wait - // for the state server's machine agent to be ready to accept commands - // before exiting this bootstrap command. - return c.waitForAgentInitialisation(ctx) -} - -var ( - bootstrapReadyPollDelay = 1 * time.Second - bootstrapReadyPollCount = 60 - blockAPI = getBlockAPI -) - -// getBlockAPI returns a block api for listing blocks. -func getBlockAPI(c *envcmd.EnvCommandBase) (block.BlockListAPI, error) { - root, err := c.NewAPIRoot() - if err != nil { - return nil, err - } - return apiblock.NewClient(root), nil -} - -// waitForAgentInitialisation polls the bootstrapped state server with a read-only -// command which will fail until the state server is fully initialised. -// TODO(wallyworld) - add a bespoke command to maybe the admin facade for this purpose. -func (c *BootstrapCommand) waitForAgentInitialisation(ctx *cmd.Context) (err error) { - attempts := utils.AttemptStrategy{ - Min: bootstrapReadyPollCount, - Delay: bootstrapReadyPollDelay, - } - var client block.BlockListAPI - for attempt := attempts.Start(); attempt.Next(); { - client, err = blockAPI(&c.EnvCommandBase) - if err != nil { - return err - } - _, err = client.List() - client.Close() - if err == nil { - ctx.Infof("Bootstrap complete") - return nil - } - // As the API server is coming up, it goes through a number of steps. - // Initially the upgrade steps run, but the api server allows some - // calls to be processed during the upgrade, but not the list blocks. - // It is also possible that the underlying database causes connections - // to be dropped as it is initialising, or reconfiguring. These can - // lead to EOF or "connection is shut down" error messages. We skip - // these too, hoping that things come back up before the end of the - // retry poll count. - errorMessage := err.Error() - if strings.Contains(errorMessage, apiserver.UpgradeInProgressError.Error()) || - strings.HasSuffix(errorMessage, "EOF") || - strings.HasSuffix(errorMessage, "connection is shut down") { - ctx.Infof("Waiting for API to become available") - continue - } - return err - } - return err -} - -var environType = func(envName string) (string, error) { - store, err := configstore.Default() - if err != nil { - return "", errors.Trace(err) - } - cfg, _, err := environs.ConfigForName(envName, store) - if err != nil { - return "", errors.Trace(err) - } - return cfg.Type(), nil -} - -// checkProviderType ensures the provider type is okay. -func checkProviderType(envName string) error { - envType, err := environType(envName) - if err != nil { - return errors.Trace(err) - } - - featureflag.SetFlagsFromEnvironment(osenv.JujuFeatureFlagEnvKey) - flag, ok := provisionalProviders[envType] - if ok && !featureflag.Enabled(flag) { - msg := `the %q provider is provisional in this version of Juju. To use it anyway, set JUJU_DEV_FEATURE_FLAGS="%s" in your shell environment` - return errors.Errorf(msg, envType, flag) - } - - return nil -} - -// handleBootstrapError is called to clean up if bootstrap fails. -func handleBootstrapError(ctx *cmd.Context, err error, cleanup func()) { - ch := make(chan os.Signal, 1) - ctx.InterruptNotify(ch) - defer ctx.StopInterruptNotify(ch) - defer close(ch) - go func() { - for _ = range ch { - fmt.Fprintln(ctx.GetStderr(), "Cleaning up failed bootstrap") - } - }() - cleanup() -} - -var allInstances = func(environ environs.Environ) ([]instance.Instance, error) { - return environ.AllInstances() -} - -var prepareEndpointsForCaching = juju.PrepareEndpointsForCaching - -// SetBootstrapEndpointAddress writes the API endpoint address of the -// bootstrap server into the connection information. This should only be run -// once directly after Bootstrap. It assumes that there is just one instance -// in the environment - the bootstrap instance. -func (c *BootstrapCommand) SetBootstrapEndpointAddress(environ environs.Environ) error { - instances, err := allInstances(environ) - if err != nil { - return errors.Trace(err) - } - length := len(instances) - if length == 0 { - return errors.Errorf("found no instances, expected at least one") - } - if length > 1 { - logger.Warningf("expected one instance, got %d", length) - } - bootstrapInstance := instances[0] - cfg := environ.Config() - info, err := envcmd.ConnectionInfoForName(c.ConnectionName()) - if err != nil { - return errors.Annotate(err, "failed to get connection info") - } - - // Don't use c.ConnectionEndpoint as it attempts to contact the state - // server if no addresses are found in connection info. - endpoint := info.APIEndpoint() - netAddrs, err := bootstrapInstance.Addresses() - if err != nil { - return errors.Annotate(err, "failed to get bootstrap instance addresses") - } - apiPort := cfg.APIPort() - apiHostPorts := network.AddressesWithPort(netAddrs, apiPort) - addrs, hosts, addrsChanged := prepareEndpointsForCaching( - info, [][]network.HostPort{apiHostPorts}, network.HostPort{}, - ) - if !addrsChanged { - // Something's wrong we already have cached addresses? - return errors.Annotate(err, "cached API endpoints unexpectedly exist") - } - endpoint.Addresses = addrs - endpoint.Hostnames = hosts - writer, err := c.ConnectionWriter() - if err != nil { - return errors.Annotate(err, "failed to get connection writer") - } - writer.SetAPIEndpoint(endpoint) - err = writer.Write() - if err != nil { - return errors.Annotate(err, "failed to write API endpoint to connection info") - } - return nil -} === removed file 'src/github.com/juju/juju/cmd/juju/bootstrap_test.go' --- src/github.com/juju/juju/cmd/juju/bootstrap_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/bootstrap_test.go 1970-01-01 00:00:00 +0000 @@ -1,861 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - "os" - "runtime" - "strings" - "time" - - "github.com/juju/cmd" - "github.com/juju/errors" - "github.com/juju/loggo" - gitjujutesting "github.com/juju/testing" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/cmd/juju/block" - cmdtesting "github.com/juju/juju/cmd/testing" - "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/configstore" - "github.com/juju/juju/environs/filestorage" - "github.com/juju/juju/environs/imagemetadata" - "github.com/juju/juju/environs/simplestreams" - "github.com/juju/juju/environs/sync" - envtesting "github.com/juju/juju/environs/testing" - envtools "github.com/juju/juju/environs/tools" - toolstesting "github.com/juju/juju/environs/tools/testing" - "github.com/juju/juju/instance" - "github.com/juju/juju/juju" - "github.com/juju/juju/juju/arch" - "github.com/juju/juju/juju/osenv" - "github.com/juju/juju/network" - "github.com/juju/juju/provider/dummy" - coretesting "github.com/juju/juju/testing" - coretools "github.com/juju/juju/tools" - "github.com/juju/juju/version" -) - -type BootstrapSuite struct { - coretesting.FakeJujuHomeSuite - gitjujutesting.MgoSuite - envtesting.ToolsFixture - mockBlockClient *mockBlockClient -} - -var _ = gc.Suite(&BootstrapSuite{}) - -func (s *BootstrapSuite) SetUpSuite(c *gc.C) { - s.FakeJujuHomeSuite.SetUpSuite(c) - s.MgoSuite.SetUpSuite(c) -} - -func (s *BootstrapSuite) SetUpTest(c *gc.C) { - s.FakeJujuHomeSuite.SetUpTest(c) - s.MgoSuite.SetUpTest(c) - s.ToolsFixture.SetUpTest(c) - - // Set version.Current to a known value, for which we - // will make tools available. Individual tests may - // override this. - s.PatchValue(&version.Current, v100p64) - - // Set up a local source with tools. - sourceDir := createToolsSource(c, vAll) - s.PatchValue(&envtools.DefaultBaseURL, sourceDir) - - s.PatchValue(&envtools.BundleTools, toolstesting.GetMockBundleTools(c)) - - s.mockBlockClient = &mockBlockClient{} - s.PatchValue(&blockAPI, func(c *envcmd.EnvCommandBase) (block.BlockListAPI, error) { - return s.mockBlockClient, nil - }) -} - -func (s *BootstrapSuite) TearDownSuite(c *gc.C) { - s.MgoSuite.TearDownSuite(c) - s.FakeJujuHomeSuite.TearDownSuite(c) -} - -func (s *BootstrapSuite) TearDownTest(c *gc.C) { - s.ToolsFixture.TearDownTest(c) - s.MgoSuite.TearDownTest(c) - s.FakeJujuHomeSuite.TearDownTest(c) - dummy.Reset() -} - -type mockBlockClient struct { - retry_count int - num_retries int -} - -func (c *mockBlockClient) List() ([]params.Block, error) { - c.retry_count += 1 - if c.retry_count == 5 { - return nil, fmt.Errorf("upgrade in progress") - } - if c.num_retries < 0 { - return nil, fmt.Errorf("other error") - } - if c.retry_count < c.num_retries { - return nil, fmt.Errorf("upgrade in progress") - } - return []params.Block{}, nil -} - -func (c *mockBlockClient) Close() error { - return nil -} - -func (s *BootstrapSuite) TestBootstrapAPIReadyRetries(c *gc.C) { - s.PatchValue(&bootstrapReadyPollDelay, 1*time.Millisecond) - s.PatchValue(&bootstrapReadyPollCount, 5) - defaultSeriesVersion := version.Current - // Force a dev version by having a non zero build number. - // This is because we have not uploaded any tools and auto - // upload is only enabled for dev versions. - defaultSeriesVersion.Build = 1234 - s.PatchValue(&version.Current, defaultSeriesVersion) - for _, t := range []struct { - num_retries int - err string - }{ - {0, ""}, // agent ready immediately - {2, ""}, // agent ready after 2 polls - {6, "upgrade in progress"}, // agent ready after 6 polls but that's too long - {-1, "other error"}, // another error is returned - } { - resetJujuHome(c, "devenv") - - s.mockBlockClient.num_retries = t.num_retries - s.mockBlockClient.retry_count = 0 - _, err := coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}), "-e", "devenv") - if t.err == "" { - c.Check(err, jc.ErrorIsNil) - } else { - c.Check(err, gc.ErrorMatches, t.err) - } - expectedRetries := t.num_retries - if t.num_retries <= 0 { - expectedRetries = 1 - } - // Only retry maximum of bootstrapReadyPollCount times. - if expectedRetries > 5 { - expectedRetries = 5 - } - c.Check(s.mockBlockClient.retry_count, gc.Equals, expectedRetries) - } -} - -func (s *BootstrapSuite) TestRunTests(c *gc.C) { - for i, test := range bootstrapTests { - c.Logf("\ntest %d: %s", i, test.info) - restore := s.run(c, test) - restore() - } -} - -type bootstrapTest struct { - info string - // binary version string used to set version.Current - version string - sync bool - args []string - err string - // binary version string for expected tools; if set, no default tools - // will be uploaded before running the test. - upload string - constraints constraints.Value - placement string - hostArch string - keepBroken bool -} - -func (s *BootstrapSuite) run(c *gc.C, test bootstrapTest) (restore gitjujutesting.Restorer) { - // Create home with dummy provider and remove all - // of its envtools. - env := resetJujuHome(c, "peckham") - - // Although we're testing PrepareEndpointsForCaching interactions - // separately in the juju package, here we just ensure it gets - // called with the right arguments. - prepareCalled := false - addrConnectedTo := "localhost:17070" - restore = gitjujutesting.PatchValue( - &prepareEndpointsForCaching, - func(info configstore.EnvironInfo, hps [][]network.HostPort, addr network.HostPort) (_, _ []string, _ bool) { - prepareCalled = true - addrs, hosts, changed := juju.PrepareEndpointsForCaching(info, hps, addr) - // Because we're bootstrapping the addresses will always - // change, as there's no .jenv file saved yet. - c.Assert(changed, jc.IsTrue) - return addrs, hosts, changed - }, - ) - - if test.version != "" { - useVersion := strings.Replace(test.version, "%LTS%", config.LatestLtsSeries(), 1) - origVersion := version.Current - version.Current = version.MustParseBinary(useVersion) - restore = restore.Add(func() { - version.Current = origVersion - }) - } - - if test.hostArch != "" { - origArch := arch.HostArch - arch.HostArch = func() string { - return test.hostArch - } - restore = restore.Add(func() { - arch.HostArch = origArch - }) - } - - // Run command and check for uploads. - opc, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), envcmd.Wrap(new(BootstrapCommand)), test.args...) - // Check for remaining operations/errors. - if test.err != "" { - err := <-errc - stripped := strings.Replace(err.Error(), "\n", "", -1) - c.Check(stripped, gc.Matches, test.err) - return restore - } - if !c.Check(<-errc, gc.IsNil) { - return restore - } - - opBootstrap := (<-opc).(dummy.OpBootstrap) - c.Check(opBootstrap.Env, gc.Equals, "peckham") - c.Check(opBootstrap.Args.Constraints, gc.DeepEquals, test.constraints) - c.Check(opBootstrap.Args.Placement, gc.Equals, test.placement) - - opFinalizeBootstrap := (<-opc).(dummy.OpFinalizeBootstrap) - c.Check(opFinalizeBootstrap.Env, gc.Equals, "peckham") - c.Check(opFinalizeBootstrap.InstanceConfig.Tools, gc.NotNil) - if test.upload != "" { - c.Check(opFinalizeBootstrap.InstanceConfig.Tools.Version.String(), gc.Equals, test.upload) - } - - store, err := configstore.Default() - c.Assert(err, jc.ErrorIsNil) - // Check a CA cert/key was generated by reloading the environment. - env, err = environs.NewFromName("peckham", store) - c.Assert(err, jc.ErrorIsNil) - _, hasCert := env.Config().CACert() - c.Check(hasCert, jc.IsTrue) - _, hasKey := env.Config().CAPrivateKey() - c.Check(hasKey, jc.IsTrue) - info, err := store.ReadInfo("peckham") - c.Assert(err, jc.ErrorIsNil) - c.Assert(info, gc.NotNil) - c.Assert(prepareCalled, jc.IsTrue) - c.Assert(info.APIEndpoint().Addresses, gc.DeepEquals, []string{addrConnectedTo}) - return restore -} - -var bootstrapTests = []bootstrapTest{{ - info: "no args, no error, no upload, no constraints", -}, { - info: "bad --constraints", - args: []string{"--constraints", "bad=wrong"}, - err: `invalid value "bad=wrong" for flag --constraints: unknown constraint "bad"`, -}, { - info: "conflicting --constraints", - args: []string{"--constraints", "instance-type=foo mem=4G"}, - err: `failed to bootstrap environment: ambiguous constraints: "instance-type" overlaps with "mem"`, -}, { - info: "bad --series", - args: []string{"--series", "1bad1"}, - err: `invalid value "1bad1" for flag --series: invalid series name "1bad1"`, -}, { - info: "lonely --series", - args: []string{"--series", "fine"}, - err: `--series requires --upload-tools`, -}, { - info: "lonely --upload-series", - args: []string{"--upload-series", "fine"}, - err: `--upload-series requires --upload-tools`, -}, { - info: "--upload-series with --series", - args: []string{"--upload-tools", "--upload-series", "foo", "--series", "bar"}, - err: `--upload-series and --series can't be used together`, -}, { - info: "bad environment", - version: "1.2.3-%LTS%-amd64", - args: []string{"-e", "brokenenv"}, - err: `failed to bootstrap environment: dummy.Bootstrap is broken`, -}, { - info: "constraints", - args: []string{"--constraints", "mem=4G cpu-cores=4"}, - constraints: constraints.MustParse("mem=4G cpu-cores=4"), -}, { - info: "unsupported constraint passed through but no error", - args: []string{"--constraints", "mem=4G cpu-cores=4 cpu-power=10"}, - constraints: constraints.MustParse("mem=4G cpu-cores=4 cpu-power=10"), -}, { - info: "--upload-tools uses arch from constraint if it matches current version", - version: "1.3.3-saucy-ppc64el", - hostArch: "ppc64el", - args: []string{"--upload-tools", "--constraints", "arch=ppc64el"}, - upload: "1.3.3.1-raring-ppc64el", // from version.Current - constraints: constraints.MustParse("arch=ppc64el"), -}, { - info: "--upload-tools rejects mismatched arch", - version: "1.3.3-saucy-amd64", - hostArch: "amd64", - args: []string{"--upload-tools", "--constraints", "arch=ppc64el"}, - err: `failed to bootstrap environment: cannot build tools for "ppc64el" using a machine running on "amd64"`, -}, { - info: "--upload-tools rejects non-supported arch", - version: "1.3.3-saucy-arm64", - hostArch: "arm64", - args: []string{"--upload-tools"}, - err: `failed to bootstrap environment: environment "peckham" of type dummy does not support instances running on "arm64"`, -}, { - info: "--upload-tools always bumps build number", - version: "1.2.3.4-raring-amd64", - args: []string{"--upload-tools"}, - upload: "1.2.3.5-raring-amd64", -}, { - info: "placement", - args: []string{"--to", "something"}, - placement: "something", -}, { - info: "keep broken", - args: []string{"--keep-broken"}, - keepBroken: true, -}, { - info: "additional args", - args: []string{"anything", "else"}, - err: `unrecognized args: \["anything" "else"\]`, -}} - -func (s *BootstrapSuite) TestRunEnvNameMissing(c *gc.C) { - s.PatchValue(&getEnvName, func(*BootstrapCommand) string { return "" }) - - _, err := coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{})) - - c.Check(err, gc.ErrorMatches, "the name of the environment must be specified") -} - -const provisionalEnvs = ` -environments: - devenv: - type: dummy - cloudsigma: - type: cloudsigma - vsphere: - type: vsphere -` - -func (s *BootstrapSuite) TestCheckProviderProvisional(c *gc.C) { - coretesting.WriteEnvironments(c, provisionalEnvs) - - err := checkProviderType("devenv") - c.Assert(err, jc.ErrorIsNil) - - for name, flag := range provisionalProviders { - // vsphere is disabled for gccgo. See lp:1440940. - if name == "vsphere" && runtime.Compiler == "gccgo" { - continue - } - c.Logf(" - trying %q -", name) - err := checkProviderType(name) - c.Check(err, gc.ErrorMatches, ".* provider is provisional .* set JUJU_DEV_FEATURE_FLAGS=.*") - - err = os.Setenv(osenv.JujuFeatureFlagEnvKey, flag) - c.Assert(err, jc.ErrorIsNil) - err = checkProviderType(name) - c.Check(err, jc.ErrorIsNil) - } -} - -func (s *BootstrapSuite) TestBootstrapTwice(c *gc.C) { - env := resetJujuHome(c, "devenv") - defaultSeriesVersion := version.Current - defaultSeriesVersion.Series = config.PreferredSeries(env.Config()) - // Force a dev version by having a non zero build number. - // This is because we have not uploaded any tools and auto - // upload is only enabled for dev versions. - defaultSeriesVersion.Build = 1234 - s.PatchValue(&version.Current, defaultSeriesVersion) - - _, err := coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}), "-e", "devenv") - c.Assert(err, jc.ErrorIsNil) - - _, err = coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}), "-e", "devenv") - c.Assert(err, gc.ErrorMatches, "environment is already bootstrapped") -} - -type mockBootstrapInstance struct { - instance.Instance -} - -func (*mockBootstrapInstance) Addresses() ([]network.Address, error) { - return []network.Address{{Value: "localhost"}}, nil -} - -func (s *BootstrapSuite) TestSeriesDeprecation(c *gc.C) { - ctx := s.checkSeriesArg(c, "--series") - c.Check(coretesting.Stderr(ctx), gc.Equals, - "Use of --series is obsolete. --upload-tools now expands to all supported series of the same operating system.\nBootstrap complete\n") -} - -func (s *BootstrapSuite) TestUploadSeriesDeprecation(c *gc.C) { - ctx := s.checkSeriesArg(c, "--upload-series") - c.Check(coretesting.Stderr(ctx), gc.Equals, - "Use of --upload-series is obsolete. --upload-tools now expands to all supported series of the same operating system.\nBootstrap complete\n") -} - -func (s *BootstrapSuite) checkSeriesArg(c *gc.C, argVariant string) *cmd.Context { - _bootstrap := &fakeBootstrapFuncs{} - s.PatchValue(&getBootstrapFuncs, func() BootstrapInterface { - return _bootstrap - }) - resetJujuHome(c, "devenv") - s.PatchValue(&allInstances, func(environ environs.Environ) ([]instance.Instance, error) { - return []instance.Instance{&mockBootstrapInstance{}}, nil - }) - - ctx, err := coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}), "--upload-tools", argVariant, "foo,bar") - - c.Assert(err, jc.ErrorIsNil) - return ctx -} - -// In the case where we cannot examine an environment, we want the -// error to propagate back up to the user. -func (s *BootstrapSuite) TestBootstrapPropagatesEnvErrors(c *gc.C) { - //TODO(bogdanteleaga): fix this for windows once permissions are fixed - if runtime.GOOS == "windows" { - c.Skip("bug 1403084: this is very platform specific. When/if we will support windows state machine, this will probably be rewritten.") - } - - const envName = "devenv" - env := resetJujuHome(c, envName) - defaultSeriesVersion := version.Current - defaultSeriesVersion.Series = config.PreferredSeries(env.Config()) - // Force a dev version by having a non zero build number. - // This is because we have not uploaded any tools and auto - // upload is only enabled for dev versions. - defaultSeriesVersion.Build = 1234 - s.PatchValue(&version.Current, defaultSeriesVersion) - s.PatchValue(&environType, func(string) (string, error) { return "", nil }) - - _, err := coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}), "-e", envName) - c.Assert(err, jc.ErrorIsNil) - - // Change permissions on the jenv file to simulate some kind of - // unexpected error when trying to read info from the environment - jenvFile := gitjujutesting.HomePath(".juju", "environments", envName+".jenv") - err = os.Chmod(jenvFile, os.FileMode(0200)) - c.Assert(err, jc.ErrorIsNil) - - // The second bootstrap should fail b/c of the propogated error - _, err = coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}), "-e", envName) - c.Assert(err, gc.ErrorMatches, "there was an issue examining the environment: .*") -} - -func (s *BootstrapSuite) TestBootstrapCleansUpIfEnvironPrepFails(c *gc.C) { - - cleanupRan := false - - s.PatchValue(&environType, func(string) (string, error) { return "", nil }) - s.PatchValue( - &environFromName, - func( - *cmd.Context, - string, - string, - func(environs.Environ) error, - ) (environs.Environ, func(), error) { - return nil, func() { cleanupRan = true }, fmt.Errorf("mock") - }, - ) - - ctx := coretesting.Context(c) - _, errc := cmdtesting.RunCommand(ctx, envcmd.Wrap(new(BootstrapCommand)), "-e", "peckham") - c.Check(<-errc, gc.Not(gc.IsNil)) - c.Check(cleanupRan, jc.IsTrue) -} - -// When attempting to bootstrap, check that when prepare errors out, -// the code cleans up the created jenv file, but *not* any existing -// environment that may have previously been bootstrapped. -func (s *BootstrapSuite) TestBootstrapFailToPrepareDiesGracefully(c *gc.C) { - - destroyedEnvRan := false - destroyedInfoRan := false - - // Mock functions - mockDestroyPreparedEnviron := func( - *cmd.Context, - environs.Environ, - configstore.Storage, - string, - ) { - destroyedEnvRan = true - } - - mockDestroyEnvInfo := func( - ctx *cmd.Context, - cfgName string, - store configstore.Storage, - action string, - ) { - destroyedInfoRan = true - } - - mockEnvironFromName := func( - ctx *cmd.Context, - envName string, - action string, - _ func(environs.Environ) error, - ) (environs.Environ, func(), error) { - // Always show that the environment is bootstrapped. - return environFromNameProductionFunc( - ctx, - envName, - action, - func(env environs.Environ) error { - return environs.ErrAlreadyBootstrapped - }) - } - - mockPrepare := func( - string, - environs.BootstrapContext, - configstore.Storage, - ) (environs.Environ, error) { - return nil, fmt.Errorf("mock-prepare") - } - - // Simulation: prepare should fail and we should only clean up the - // jenv file. Any existing environment should not be destroyed. - s.PatchValue(&destroyPreparedEnviron, mockDestroyPreparedEnviron) - s.PatchValue(&environType, func(string) (string, error) { return "", nil }) - s.PatchValue(&environFromName, mockEnvironFromName) - s.PatchValue(&environs.PrepareFromName, mockPrepare) - s.PatchValue(&destroyEnvInfo, mockDestroyEnvInfo) - - ctx := coretesting.Context(c) - _, errc := cmdtesting.RunCommand(ctx, envcmd.Wrap(new(BootstrapCommand)), "-e", "peckham") - c.Check(<-errc, gc.ErrorMatches, ".*mock-prepare$") - c.Check(destroyedEnvRan, jc.IsFalse) - c.Check(destroyedInfoRan, jc.IsTrue) -} - -func (s *BootstrapSuite) TestBootstrapJenvWarning(c *gc.C) { - env := resetJujuHome(c, "devenv") - defaultSeriesVersion := version.Current - defaultSeriesVersion.Series = config.PreferredSeries(env.Config()) - // Force a dev version by having a non zero build number. - // This is because we have not uploaded any tools and auto - // upload is only enabled for dev versions. - defaultSeriesVersion.Build = 1234 - s.PatchValue(&version.Current, defaultSeriesVersion) - - store, err := configstore.Default() - c.Assert(err, jc.ErrorIsNil) - ctx := coretesting.Context(c) - environs.PrepareFromName("devenv", envcmd.BootstrapContext(ctx), store) - - logger := "jenv.warning.test" - var testWriter loggo.TestWriter - loggo.RegisterWriter(logger, &testWriter, loggo.WARNING) - defer loggo.RemoveWriter(logger) - - _, errc := cmdtesting.RunCommand(ctx, envcmd.Wrap(new(BootstrapCommand)), "-e", "devenv") - c.Assert(<-errc, gc.IsNil) - c.Assert(testWriter.Log(), jc.LogMatches, []string{"ignoring environments.yaml: using bootstrap config in .*"}) -} - -func (s *BootstrapSuite) TestInvalidLocalSource(c *gc.C) { - s.PatchValue(&version.Current.Number, version.MustParse("1.2.0")) - env := resetJujuHome(c, "devenv") - - // Bootstrap the environment with an invalid source. - // The command returns with an error. - _, err := coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}), "--metadata-source", c.MkDir()) - c.Check(err, gc.ErrorMatches, `failed to bootstrap environment: Juju cannot bootstrap because no tools are available for your environment(.|\n)*`) - - // Now check that there are no tools available. - _, err = envtools.FindTools( - env, version.Current.Major, version.Current.Minor, coretools.Filter{}) - c.Assert(err, gc.FitsTypeOf, errors.NotFoundf("")) -} - -// createImageMetadata creates some image metadata in a local directory. -func createImageMetadata(c *gc.C) (string, []*imagemetadata.ImageMetadata) { - // Generate some image metadata. - im := []*imagemetadata.ImageMetadata{ - { - Id: "1234", - Arch: "amd64", - Version: "13.04", - RegionName: "region", - Endpoint: "endpoint", - }, - } - cloudSpec := &simplestreams.CloudSpec{ - Region: "region", - Endpoint: "endpoint", - } - sourceDir := c.MkDir() - sourceStor, err := filestorage.NewFileStorageWriter(sourceDir) - c.Assert(err, jc.ErrorIsNil) - err = imagemetadata.MergeAndWriteMetadata("raring", im, cloudSpec, sourceStor) - c.Assert(err, jc.ErrorIsNil) - return sourceDir, im -} - -func (s *BootstrapSuite) TestBootstrapCalledWithMetadataDir(c *gc.C) { - sourceDir, _ := createImageMetadata(c) - resetJujuHome(c, "devenv") - - _bootstrap := &fakeBootstrapFuncs{} - s.PatchValue(&getBootstrapFuncs, func() BootstrapInterface { - return _bootstrap - }) - - coretesting.RunCommand( - c, envcmd.Wrap(&BootstrapCommand{}), - "--metadata-source", sourceDir, "--constraints", "mem=4G", - ) - c.Assert(_bootstrap.args.MetadataDir, gc.Equals, sourceDir) -} - -func (s *BootstrapSuite) TestAutoSyncLocalSource(c *gc.C) { - sourceDir := createToolsSource(c, vAll) - s.PatchValue(&version.Current.Number, version.MustParse("1.2.0")) - env := resetJujuHome(c, "peckham") - - // Bootstrap the environment with the valid source. - // The bootstrapping has to show no error, because the tools - // are automatically synchronized. - _, err := coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}), "--metadata-source", sourceDir) - c.Assert(err, jc.ErrorIsNil) - - // Now check the available tools which are the 1.2.0 envtools. - checkTools(c, env, v120All) -} - -func (s *BootstrapSuite) setupAutoUploadTest(c *gc.C, vers, series string) environs.Environ { - s.PatchValue(&envtools.BundleTools, toolstesting.GetMockBundleTools(c)) - sourceDir := createToolsSource(c, vAll) - s.PatchValue(&envtools.DefaultBaseURL, sourceDir) - - // Change the tools location to be the test location and also - // the version and ensure their later restoring. - // Set the current version to be something for which there are no tools - // so we can test that an upload is forced. - s.PatchValue(&version.Current, version.MustParseBinary(vers+"-"+series+"-"+version.Current.Arch)) - - // Create home with dummy provider and remove all - // of its envtools. - return resetJujuHome(c, "devenv") -} - -func (s *BootstrapSuite) TestAutoUploadAfterFailedSync(c *gc.C) { - s.PatchValue(&version.Current.Series, config.LatestLtsSeries()) - s.setupAutoUploadTest(c, "1.7.3", "quantal") - // Run command and check for that upload has been run for tools matching - // the current juju version. - opc, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), envcmd.Wrap(new(BootstrapCommand)), "-e", "devenv") - c.Assert(<-errc, gc.IsNil) - c.Check((<-opc).(dummy.OpBootstrap).Env, gc.Equals, "devenv") - icfg := (<-opc).(dummy.OpFinalizeBootstrap).InstanceConfig - c.Assert(icfg, gc.NotNil) - c.Assert(icfg.Tools.Version.String(), gc.Equals, "1.7.3.1-raring-"+version.Current.Arch) -} - -func (s *BootstrapSuite) TestAutoUploadOnlyForDev(c *gc.C) { - s.setupAutoUploadTest(c, "1.8.3", "precise") - _, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), envcmd.Wrap(new(BootstrapCommand))) - err := <-errc - c.Assert(err, gc.ErrorMatches, - "failed to bootstrap environment: Juju cannot bootstrap because no tools are available for your environment(.|\n)*") -} - -func (s *BootstrapSuite) TestMissingToolsError(c *gc.C) { - s.setupAutoUploadTest(c, "1.8.3", "precise") - - _, err := coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{})) - c.Assert(err, gc.ErrorMatches, - "failed to bootstrap environment: Juju cannot bootstrap because no tools are available for your environment(.|\n)*") -} - -func (s *BootstrapSuite) TestMissingToolsUploadFailedError(c *gc.C) { - - buildToolsTarballAlwaysFails := func(forceVersion *version.Number, stream string) (*sync.BuiltTools, error) { - return nil, fmt.Errorf("an error") - } - - s.setupAutoUploadTest(c, "1.7.3", "precise") - s.PatchValue(&sync.BuildToolsTarball, buildToolsTarballAlwaysFails) - - ctx, err := coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}), "-e", "devenv") - - c.Check(coretesting.Stderr(ctx), gc.Equals, fmt.Sprintf(` -Bootstrapping environment "devenv" -Starting new instance for initial state server -Building tools to upload (1.7.3.1-raring-%s) -`[1:], version.Current.Arch)) - c.Check(err, gc.ErrorMatches, "failed to bootstrap environment: cannot upload bootstrap tools: an error") -} - -func (s *BootstrapSuite) TestBootstrapDestroy(c *gc.C) { - resetJujuHome(c, "devenv") - devVersion := version.Current - // Force a dev version by having a non zero build number. - // This is because we have not uploaded any tools and auto - // upload is only enabled for dev versions. - devVersion.Build = 1234 - s.PatchValue(&version.Current, devVersion) - opc, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), envcmd.Wrap(new(BootstrapCommand)), "-e", "brokenenv") - err := <-errc - c.Assert(err, gc.ErrorMatches, "failed to bootstrap environment: dummy.Bootstrap is broken") - var opDestroy *dummy.OpDestroy - for opDestroy == nil { - select { - case op := <-opc: - switch op := op.(type) { - case dummy.OpDestroy: - opDestroy = &op - } - default: - c.Error("expected call to env.Destroy") - return - } - } - c.Assert(opDestroy.Error, gc.ErrorMatches, "dummy.Destroy is broken") -} - -func (s *BootstrapSuite) TestBootstrapKeepBroken(c *gc.C) { - resetJujuHome(c, "devenv") - devVersion := version.Current - // Force a dev version by having a non zero build number. - // This is because we have not uploaded any tools and auto - // upload is only enabled for dev versions. - devVersion.Build = 1234 - s.PatchValue(&version.Current, devVersion) - opc, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), envcmd.Wrap(new(BootstrapCommand)), "-e", "brokenenv", "--keep-broken") - err := <-errc - c.Assert(err, gc.ErrorMatches, "failed to bootstrap environment: dummy.Bootstrap is broken") - done := false - for !done { - select { - case op, ok := <-opc: - if !ok { - done = true - break - } - switch op.(type) { - case dummy.OpDestroy: - c.Error("unexpected call to env.Destroy") - break - } - default: - break - } - } -} - -// createToolsSource writes the mock tools and metadata into a temporary -// directory and returns it. -func createToolsSource(c *gc.C, versions []version.Binary) string { - versionStrings := make([]string, len(versions)) - for i, vers := range versions { - versionStrings[i] = vers.String() - } - source := c.MkDir() - toolstesting.MakeTools(c, source, "released", versionStrings) - return source -} - -// resetJujuHome restores an new, clean Juju home environment without tools. -func resetJujuHome(c *gc.C, envName string) environs.Environ { - jenvDir := gitjujutesting.HomePath(".juju", "environments") - err := os.RemoveAll(jenvDir) - c.Assert(err, jc.ErrorIsNil) - coretesting.WriteEnvironments(c, envConfig) - dummy.Reset() - store, err := configstore.Default() - c.Assert(err, jc.ErrorIsNil) - env, err := environs.PrepareFromName(envName, envcmd.BootstrapContext(cmdtesting.NullContext(c)), store) - c.Assert(err, jc.ErrorIsNil) - return env -} - -// checkTools check if the environment contains the passed envtools. -func checkTools(c *gc.C, env environs.Environ, expected []version.Binary) { - list, err := envtools.FindTools( - env, version.Current.Major, version.Current.Minor, coretools.Filter{}) - c.Check(err, jc.ErrorIsNil) - c.Logf("found: " + list.String()) - urls := list.URLs() - c.Check(urls, gc.HasLen, len(expected)) -} - -var ( - v100d64 = version.MustParseBinary("1.0.0-raring-amd64") - v100p64 = version.MustParseBinary("1.0.0-precise-amd64") - v100q32 = version.MustParseBinary("1.0.0-quantal-i386") - v100q64 = version.MustParseBinary("1.0.0-quantal-amd64") - v120d64 = version.MustParseBinary("1.2.0-raring-amd64") - v120p64 = version.MustParseBinary("1.2.0-precise-amd64") - v120q32 = version.MustParseBinary("1.2.0-quantal-i386") - v120q64 = version.MustParseBinary("1.2.0-quantal-amd64") - v120t32 = version.MustParseBinary("1.2.0-trusty-i386") - v120t64 = version.MustParseBinary("1.2.0-trusty-amd64") - v190p32 = version.MustParseBinary("1.9.0-precise-i386") - v190q64 = version.MustParseBinary("1.9.0-quantal-amd64") - v200p64 = version.MustParseBinary("2.0.0-precise-amd64") - v100All = []version.Binary{ - v100d64, v100p64, v100q64, v100q32, - } - v120All = []version.Binary{ - v120d64, v120p64, v120q64, v120q32, v120t32, v120t64, - } - v190All = []version.Binary{ - v190p32, v190q64, - } - v200All = []version.Binary{ - v200p64, - } - vAll = joinBinaryVersions(v100All, v120All, v190All, v200All) -) - -func joinBinaryVersions(versions ...[]version.Binary) []version.Binary { - var all []version.Binary - for _, versions := range versions { - all = append(all, versions...) - } - return all -} - -// TODO(menn0): This fake BootstrapInterface implementation is -// currently quite minimal but could be easily extended to cover more -// test scenarios. This could help improve some of the tests in this -// file which execute large amounts of external functionality. -type fakeBootstrapFuncs struct { - args bootstrap.BootstrapParams -} - -func (fake *fakeBootstrapFuncs) EnsureNotBootstrapped(env environs.Environ) error { - return nil -} - -func (fake *fakeBootstrapFuncs) Bootstrap(ctx environs.BootstrapContext, env environs.Environ, args bootstrap.BootstrapParams) error { - fake.args = args - return nil -} === removed file 'src/github.com/juju/juju/cmd/juju/cmd_test.go' --- src/github.com/juju/juju/cmd/juju/cmd_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/cmd_test.go 1970-01-01 00:00:00 +0000 @@ -1,284 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "os" - - "github.com/juju/cmd" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/cmd/juju/service" - cmdtesting "github.com/juju/juju/cmd/testing" - "github.com/juju/juju/juju/osenv" - "github.com/juju/juju/juju/testing" - coretesting "github.com/juju/juju/testing" -) - -func badrun(c *gc.C, exit int, args ...string) string { - args = append([]string{"juju"}, args...) - return cmdtesting.BadRun(c, exit, args...) -} - -type CmdSuite struct { - testing.JujuConnSuite -} - -var _ = gc.Suite(&CmdSuite{}) - -const envConfig = ` -default: - peckham -environments: - peckham: - type: dummy - state-server: false - admin-secret: arble - authorized-keys: i-am-a-key - default-series: raring - walthamstow: - type: dummy - state-server: false - authorized-keys: i-am-a-key - brokenenv: - type: dummy - broken: Bootstrap Destroy - state-server: false - authorized-keys: i-am-a-key - agent-stream: proposed - devenv: - type: dummy - state-server: false - admin-secret: arble - authorized-keys: i-am-a-key - default-series: raring - agent-stream: proposed -` - -func (s *CmdSuite) SetUpTest(c *gc.C) { - s.JujuConnSuite.SetUpTest(c) - coretesting.WriteEnvironments(c, envConfig, "peckham", "walthamstow", "brokenenv") -} - -func (s *CmdSuite) TearDownTest(c *gc.C) { - s.JujuConnSuite.TearDownTest(c) -} - -// testInit checks that a command initialises correctly -// with the given set of arguments. -func testInit(c *gc.C, com cmd.Command, args []string, errPat string) { - err := coretesting.InitCommand(com, args) - if errPat != "" { - c.Assert(err, gc.ErrorMatches, errPat) - } else { - c.Assert(err, jc.ErrorIsNil) - } -} - -type HasConnectionName interface { - ConnectionName() string -} - -// assertEnvName asserts that the Command is using -// the given environment name. -// Since every command has a different type, -// we use reflection to look at the value of the -// Conn field in the value. -func assertEnvName(c *gc.C, com cmd.Command, name string) { - i, ok := com.(HasConnectionName) - c.Assert(ok, jc.IsTrue) - c.Assert(i.ConnectionName(), gc.Equals, name) -} - -// All members of EnvironmentInitTests are tested for the -environment and -e -// flags, and that extra arguments will cause parsing to fail. -var EnvironmentInitTests = []func() (envcmd.EnvironCommand, []string){ - func() (envcmd.EnvironCommand, []string) { return new(BootstrapCommand), nil }, - func() (envcmd.EnvironCommand, []string) { - return new(DeployCommand), []string{"charm-name", "service-name"} - }, - func() (envcmd.EnvironCommand, []string) { return new(StatusCommand), nil }, -} - -// TestEnvironmentInit tests that all commands which accept -// the --environment variable initialise their -// environment name correctly. -func (*CmdSuite) TestEnvironmentInit(c *gc.C) { - for i, cmdFunc := range EnvironmentInitTests { - c.Logf("test %d", i) - com, args := cmdFunc() - testInit(c, envcmd.Wrap(com), args, "") - assertEnvName(c, com, "peckham") - - com, args = cmdFunc() - testInit(c, envcmd.Wrap(com), append(args, "-e", "walthamstow"), "") - assertEnvName(c, com, "walthamstow") - - com, args = cmdFunc() - testInit(c, envcmd.Wrap(com), append(args, "--environment", "walthamstow"), "") - assertEnvName(c, com, "walthamstow") - - // JUJU_ENV is the final place the environment can be overriden - com, args = cmdFunc() - oldenv := os.Getenv(osenv.JujuEnvEnvKey) - os.Setenv(osenv.JujuEnvEnvKey, "walthamstow") - testInit(c, envcmd.Wrap(com), args, "") - os.Setenv(osenv.JujuEnvEnvKey, oldenv) - assertEnvName(c, com, "walthamstow") - } -} - -var deployTests = []struct { - args []string - com *DeployCommand -}{ - { - []string{"charm-name"}, - &DeployCommand{}, - }, { - []string{"charm-name", "service-name"}, - &DeployCommand{ServiceName: "service-name"}, - }, { - []string{"--repository", "/path/to/another-repo", "charm-name"}, - &DeployCommand{RepoPath: "/path/to/another-repo"}, - }, { - []string{"--upgrade", "charm-name"}, - &DeployCommand{BumpRevision: true}, - }, { - []string{"-u", "charm-name"}, - &DeployCommand{BumpRevision: true}, - }, { - []string{"--num-units", "33", "charm-name"}, - &DeployCommand{UnitCommandBase: service.UnitCommandBase{NumUnits: 33}}, - }, { - []string{"-n", "104", "charm-name"}, - &DeployCommand{UnitCommandBase: service.UnitCommandBase{NumUnits: 104}}, - }, -} - -func initExpectations(com *DeployCommand) { - if com.CharmName == "" { - com.CharmName = "charm-name" - } - if com.NumUnits == 0 { - com.NumUnits = 1 - } - if com.RepoPath == "" { - com.RepoPath = "/path/to/repo" - } - com.SetEnvName("peckham") -} - -func initDeployCommand(args ...string) (*DeployCommand, error) { - com := &DeployCommand{} - return com, coretesting.InitCommand(envcmd.Wrap(com), args) -} - -func (*CmdSuite) TestDeployCommandInit(c *gc.C) { - defer os.Setenv(osenv.JujuRepositoryEnvKey, os.Getenv(osenv.JujuRepositoryEnvKey)) - os.Setenv(osenv.JujuRepositoryEnvKey, "/path/to/repo") - - for _, t := range deployTests { - initExpectations(t.com) - com, err := initDeployCommand(t.args...) - c.Assert(err, jc.ErrorIsNil) - c.Assert(com, gc.DeepEquals, t.com) - } - - // test relative --config path - ctx := coretesting.Context(c) - expected := []byte("test: data") - path := ctx.AbsPath("testconfig.yaml") - file, err := os.Create(path) - c.Assert(err, jc.ErrorIsNil) - _, err = file.Write(expected) - c.Assert(err, jc.ErrorIsNil) - file.Close() - - com, err := initDeployCommand("--config", "testconfig.yaml", "charm-name") - c.Assert(err, jc.ErrorIsNil) - actual, err := com.Config.Read(ctx) - c.Assert(err, jc.ErrorIsNil) - c.Assert(expected, gc.DeepEquals, actual) - - // missing args - _, err = initDeployCommand() - c.Assert(err, gc.ErrorMatches, "no charm specified") - - // bad unit count - _, err = initDeployCommand("charm-name", "--num-units", "0") - c.Assert(err, gc.ErrorMatches, "--num-units must be a positive integer") - _, err = initDeployCommand("charm-name", "-n", "0") - c.Assert(err, gc.ErrorMatches, "--num-units must be a positive integer") - - // environment tested elsewhere -} - -func initExposeCommand(args ...string) (*ExposeCommand, error) { - com := &ExposeCommand{} - return com, coretesting.InitCommand(com, args) -} - -func (*CmdSuite) TestExposeCommandInit(c *gc.C) { - // missing args - _, err := initExposeCommand() - c.Assert(err, gc.ErrorMatches, "no service name specified") - - // environment tested elsewhere -} - -func initUnexposeCommand(args ...string) (*UnexposeCommand, error) { - com := &UnexposeCommand{} - return com, coretesting.InitCommand(com, args) -} - -func (*CmdSuite) TestUnexposeCommandInit(c *gc.C) { - // missing args - _, err := initUnexposeCommand() - c.Assert(err, gc.ErrorMatches, "no service name specified") - - // environment tested elsewhere -} - -func initSSHCommand(args ...string) (*SSHCommand, error) { - com := &SSHCommand{} - return com, coretesting.InitCommand(com, args) -} - -func (*CmdSuite) TestSSHCommandInit(c *gc.C) { - // missing args - _, err := initSSHCommand() - c.Assert(err, gc.ErrorMatches, "no target name specified") -} - -func initSCPCommand(args ...string) (*SCPCommand, error) { - com := &SCPCommand{} - return com, coretesting.InitCommand(com, args) -} - -func (*CmdSuite) TestSCPCommandInit(c *gc.C) { - // missing args - _, err := initSCPCommand() - c.Assert(err, gc.ErrorMatches, "at least two arguments required") - - // not enough args - _, err = initSCPCommand("mysql/0:foo") - c.Assert(err, gc.ErrorMatches, "at least two arguments required") -} - -func initRemoveUnitCommand(args ...string) (*RemoveUnitCommand, error) { - com := &RemoveUnitCommand{} - return com, coretesting.InitCommand(com, args) -} - -func (*CmdSuite) TestRemoveUnitCommandInit(c *gc.C) { - // missing args - _, err := initRemoveUnitCommand() - c.Assert(err, gc.ErrorMatches, "no units specified") - // not a unit - _, err = initRemoveUnitCommand("seven/nine") - c.Assert(err, gc.ErrorMatches, `invalid unit name "seven/nine"`) -} === removed file 'src/github.com/juju/juju/cmd/juju/cmdblockhelper_test.go' --- src/github.com/juju/juju/cmd/juju/cmdblockhelper_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/cmdblockhelper_test.go 1970-01-01 00:00:00 +0000 @@ -1,62 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "strings" - - "github.com/juju/cmd" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/api" - "github.com/juju/juju/api/block" - cmdblock "github.com/juju/juju/cmd/juju/block" -) - -type CmdBlockHelper struct { - blockClient *block.Client -} - -// NewCmdBlockHelper creates a block switch used in testing -// to manage desired juju blocks. -func NewCmdBlockHelper(st *api.State) CmdBlockHelper { - return CmdBlockHelper{ - blockClient: block.NewClient(st), - } -} - -// on switches on desired block and -// asserts that no errors were encountered. -func (s *CmdBlockHelper) on(c *gc.C, blockType, msg string) { - c.Assert(s.blockClient.SwitchBlockOn(cmdblock.TypeFromOperation(blockType), msg), gc.IsNil) -} - -// BlockAllChanges switches changes block on. -// This prevents all changes to juju environment. -func (s *CmdBlockHelper) BlockAllChanges(c *gc.C, msg string) { - s.on(c, "all-changes", msg) -} - -// BlockRemoveObject switches remove block on. -// This prevents any object/entity removal on juju environment -func (s *CmdBlockHelper) BlockRemoveObject(c *gc.C, msg string) { - s.on(c, "remove-object", msg) -} - -// BlockDestroyEnvironment switches destory block on. -// This prevents juju environment destruction. -func (s *CmdBlockHelper) BlockDestroyEnvironment(c *gc.C, msg string) { - s.on(c, "destroy-environment", msg) -} - -func (s *CmdBlockHelper) Close() { - s.blockClient.Close() -} - -func (s *CmdBlockHelper) AssertBlocked(c *gc.C, err error, msg string) { - c.Assert(err, gc.ErrorMatches, cmd.ErrSilent.Error()) - // msg is logged - stripped := strings.Replace(c.GetTestLog(), "\n", "", -1) - c.Check(stripped, gc.Matches, msg) -} === added directory 'src/github.com/juju/juju/cmd/juju/commands' === added file 'src/github.com/juju/juju/cmd/juju/commands/addrelation.go' --- src/github.com/juju/juju/cmd/juju/commands/addrelation.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/addrelation.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,45 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "fmt" + + "github.com/juju/cmd" + + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/block" +) + +// AddRelationCommand adds a relation between two service endpoints. +type AddRelationCommand struct { + envcmd.EnvCommandBase + Endpoints []string +} + +func (c *AddRelationCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "add-relation", + Args: "[:] [:]", + Purpose: "add a relation between two services", + } +} + +func (c *AddRelationCommand) Init(args []string) error { + if len(args) != 2 { + return fmt.Errorf("a relation must involve two services") + } + c.Endpoints = args + return nil +} + +func (c *AddRelationCommand) Run(_ *cmd.Context) error { + client, err := c.NewAPIClient() + if err != nil { + return err + } + defer client.Close() + _, err = client.AddRelation(c.Endpoints...) + return block.ProcessBlockedError(err, block.BlockChange) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/addrelation_test.go' --- src/github.com/juju/juju/cmd/juju/commands/addrelation_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/addrelation_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,190 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/envcmd" + jujutesting "github.com/juju/juju/juju/testing" + "github.com/juju/juju/testcharms" + "github.com/juju/juju/testing" +) + +type AddRelationSuite struct { + jujutesting.RepoSuite + CmdBlockHelper +} + +func (s *AddRelationSuite) SetUpTest(c *gc.C) { + s.RepoSuite.SetUpTest(c) + s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) + c.Assert(s.CmdBlockHelper, gc.NotNil) + s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) +} + +var _ = gc.Suite(&AddRelationSuite{}) + +func runAddRelation(c *gc.C, args ...string) error { + _, err := testing.RunCommand(c, envcmd.Wrap(&AddRelationCommand{}), args...) + return err +} + +var msWpAlreadyExists = `cannot add relation "wp:db ms:server": relation already exists` +var msLgAlreadyExists = `cannot add relation "lg:info ms:juju-info": relation already exists` +var wpLgAlreadyExists = `cannot add relation "lg:logging-directory wp:logging-dir": relation already exists` +var wpLgAlreadyExistsJuju = `cannot add relation "lg:info wp:juju-info": relation already exists` + +var addRelationTests = []struct { + args []string + err string +}{ + { + args: []string{"rk", "ms"}, + err: "no relations found", + }, { + err: "a relation must involve two services", + }, { + args: []string{"rk"}, + err: "a relation must involve two services", + }, { + args: []string{"rk:ring"}, + err: "a relation must involve two services", + }, { + args: []string{"ping:pong", "tic:tac", "icki:wacki"}, + err: "a relation must involve two services", + }, + + // Add a real relation, and check various ways of failing to re-add it. + { + args: []string{"ms", "wp"}, + }, { + args: []string{"ms", "wp"}, + err: msWpAlreadyExists, + }, { + args: []string{"wp", "ms"}, + err: msWpAlreadyExists, + }, { + args: []string{"ms", "wp:db"}, + err: msWpAlreadyExists, + }, { + args: []string{"ms:server", "wp"}, + err: msWpAlreadyExists, + }, { + args: []string{"ms:server", "wp:db"}, + err: msWpAlreadyExists, + }, + + // Add a real relation using an implicit endpoint. + { + args: []string{"ms", "lg"}, + }, { + args: []string{"ms", "lg"}, + err: msLgAlreadyExists, + }, { + args: []string{"lg", "ms"}, + err: msLgAlreadyExists, + }, { + args: []string{"ms:juju-info", "lg"}, + err: msLgAlreadyExists, + }, { + args: []string{"ms", "lg:info"}, + err: msLgAlreadyExists, + }, { + args: []string{"ms:juju-info", "lg:info"}, + err: msLgAlreadyExists, + }, + + // Add a real relation using an explicit endpoint, avoiding the potential implicit one. + { + args: []string{"wp", "lg"}, + }, { + args: []string{"wp", "lg"}, + err: wpLgAlreadyExists, + }, { + args: []string{"lg", "wp"}, + err: wpLgAlreadyExists, + }, { + args: []string{"wp:logging-dir", "lg"}, + err: wpLgAlreadyExists, + }, { + args: []string{"wp", "lg:logging-directory"}, + err: wpLgAlreadyExists, + }, { + args: []string{"wp:logging-dir", "lg:logging-directory"}, + err: wpLgAlreadyExists, + }, + + // Check we can still use the implicit endpoint if specified explicitly. + { + args: []string{"wp:juju-info", "lg"}, + }, { + args: []string{"wp:juju-info", "lg"}, + err: wpLgAlreadyExistsJuju, + }, { + args: []string{"lg", "wp:juju-info"}, + err: wpLgAlreadyExistsJuju, + }, { + args: []string{"wp:juju-info", "lg"}, + err: wpLgAlreadyExistsJuju, + }, { + args: []string{"wp", "lg:info"}, + err: wpLgAlreadyExistsJuju, + }, { + args: []string{"wp:juju-info", "lg:info"}, + err: wpLgAlreadyExistsJuju, + }, +} + +func (s *AddRelationSuite) TestAddRelation(c *gc.C) { + testcharms.Repo.CharmArchivePath(s.SeriesPath, "wordpress") + err := runDeploy(c, "local:wordpress", "wp") + c.Assert(err, jc.ErrorIsNil) + testcharms.Repo.CharmArchivePath(s.SeriesPath, "mysql") + err = runDeploy(c, "local:mysql", "ms") + c.Assert(err, jc.ErrorIsNil) + testcharms.Repo.CharmArchivePath(s.SeriesPath, "riak") + err = runDeploy(c, "local:riak", "rk") + c.Assert(err, jc.ErrorIsNil) + testcharms.Repo.CharmArchivePath(s.SeriesPath, "logging") + err = runDeploy(c, "local:logging", "lg") + c.Assert(err, jc.ErrorIsNil) + + for i, t := range addRelationTests { + c.Logf("test %d: %v", i, t.args) + err := runAddRelation(c, t.args...) + if t.err != "" { + c.Assert(err, gc.ErrorMatches, t.err) + } + } +} + +func (s *AddRelationSuite) TestBlockAddRelation(c *gc.C) { + testcharms.Repo.CharmArchivePath(s.SeriesPath, "wordpress") + err := runDeploy(c, "local:wordpress", "wp") + c.Assert(err, jc.ErrorIsNil) + testcharms.Repo.CharmArchivePath(s.SeriesPath, "mysql") + err = runDeploy(c, "local:mysql", "ms") + c.Assert(err, jc.ErrorIsNil) + testcharms.Repo.CharmArchivePath(s.SeriesPath, "riak") + err = runDeploy(c, "local:riak", "rk") + c.Assert(err, jc.ErrorIsNil) + testcharms.Repo.CharmArchivePath(s.SeriesPath, "logging") + err = runDeploy(c, "local:logging", "lg") + c.Assert(err, jc.ErrorIsNil) + + // Block operation + s.BlockAllChanges(c, "TestBlockAddRelation") + + for i, t := range addRelationTests { + c.Logf("test %d: %v", i, t.args) + err := runAddRelation(c, t.args...) + if len(t.args) == 2 { + // Only worry about Run being blocked. + // For len(t.args) != 2, an Init will fail + s.AssertBlocked(c, err, ".*TestBlockAddRelation.*") + } + } +} === added file 'src/github.com/juju/juju/cmd/juju/commands/apiinfo.go' --- src/github.com/juju/juju/cmd/juju/commands/apiinfo.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/apiinfo.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,219 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "fmt" + "strings" + + "github.com/juju/cmd" + "github.com/juju/errors" + "launchpad.net/gnuflag" + + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/environs/configstore" +) + +// APIInfoCommand returns the fields used to connect to an API server. +type APIInfoCommand struct { + envcmd.EnvCommandBase + out cmd.Output + refresh bool + user bool + password bool + cacert bool + servers bool + envuuid bool + srvuuid bool + fields []string +} + +const apiInfoDoc = ` +Print the field values used to connect to the environment's API servers" + +The exact fields to output can be specified on the command line. The +available fields are: + user + password + environ-uuid + state-servers + ca-cert + +If "password" is included as a field, or the --password option is given, the +password value will be shown. + + +Examples: + $ juju api-info + user: admin + environ-uuid: 373b309b-4a86-4f13-88e2-c213d97075b8 + state-servers: + - localhost:17070 + - 10.0.3.1:17070 + - 192.168.2.21:17070 + ca-cert: '-----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- + ' + + $ juju api-info user + admin + + $ juju api-info user password + user: admin + password: sekrit + + +` + +func (c *APIInfoCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "api-info", + Args: "[field ...]", + Purpose: "print the field values used to connect to the environment's API servers", + Doc: apiInfoDoc, + } +} + +func (c *APIInfoCommand) Init(args []string) error { + c.fields = args + if len(args) == 0 { + c.user = true + c.envuuid = true + c.srvuuid = true + c.servers = true + c.cacert = true + return nil + } + + var unknown []string + for _, name := range args { + switch name { + case "user": + c.user = true + case "password": + c.password = true + case "environ-uuid": + c.envuuid = true + case "state-servers": + c.servers = true + case "ca-cert": + c.cacert = true + case "server-uuid": + c.srvuuid = true + default: + unknown = append(unknown, fmt.Sprintf("%q", name)) + } + } + if len(unknown) > 0 { + return errors.Errorf("unknown fields: %s", strings.Join(unknown, ", ")) + } + + return nil +} + +func (c *APIInfoCommand) SetFlags(f *gnuflag.FlagSet) { + c.out.AddFlags(f, "default", map[string]cmd.Formatter{ + "default": c.format, + "yaml": cmd.FormatYaml, + "json": cmd.FormatJson, + }) + f.BoolVar(&c.refresh, "refresh", false, "connect to the API to ensure an up-to-date endpoint location") + f.BoolVar(&c.password, "password", false, "include the password in the output fields") +} + +func connectionEndpoint(c envcmd.EnvCommandBase, refresh bool) (configstore.APIEndpoint, error) { + return c.ConnectionEndpoint(refresh) +} + +func connectionCredentials(c envcmd.EnvCommandBase) (configstore.APICredentials, error) { + return c.ConnectionCredentials() +} + +var ( + endpoint = connectionEndpoint + creds = connectionCredentials +) + +// Print out the addresses of the API server endpoints. +func (c *APIInfoCommand) Run(ctx *cmd.Context) error { + apiendpoint, err := endpoint(c.EnvCommandBase, c.refresh) + if err != nil { + return err + } + credentials, err := creds(c.EnvCommandBase) + if err != nil { + return err + } + + var result InfoData + if c.user { + result.User = credentials.User + } + if c.password { + result.Password = credentials.Password + } + if c.envuuid { + result.EnvironUUID = apiendpoint.EnvironUUID + } + if c.servers { + result.StateServers = apiendpoint.Addresses + } + if c.cacert { + result.CACert = apiendpoint.CACert + } + if c.srvuuid { + result.ServerUUID = apiendpoint.ServerUUID + } + + return c.out.Write(ctx, result) +} + +func (c *APIInfoCommand) format(value interface{}) ([]byte, error) { + if len(c.fields) == 1 { + data := value.(InfoData) + field, err := data.field(c.fields[0]) + if err != nil { + return nil, err + } + switch value := field.(type) { + case []string: + return []byte(strings.Join(value, "\n")), nil + case string: + return []byte(value), nil + default: + return nil, errors.Errorf("Unsupported type %T", field) + } + } + + return cmd.FormatYaml(value) +} + +type InfoData struct { + User string `json:"user,omitempty" yaml:",omitempty"` + Password string `json:"password,omitempty" yaml:",omitempty"` + EnvironUUID string `json:"environ-uuid,omitempty" yaml:"environ-uuid,omitempty"` + ServerUUID string `json:"server-uuid,omitempty" yaml:"server-uuid,omitempty"` + StateServers []string `json:"state-servers,omitempty" yaml:"state-servers,omitempty"` + CACert string `json:"ca-cert,omitempty" yaml:"ca-cert,omitempty"` +} + +func (i *InfoData) field(name string) (interface{}, error) { + switch name { + case "user": + return i.User, nil + case "password": + return i.Password, nil + case "environ-uuid": + return i.EnvironUUID, nil + case "state-servers": + return i.StateServers, nil + case "ca-cert": + return i.CACert, nil + case "server-uuid": + return i.ServerUUID, nil + default: + return "", errors.Errorf("unknown field %q", name) + } +} === added file 'src/github.com/juju/juju/cmd/juju/commands/apiinfo_test.go' --- src/github.com/juju/juju/cmd/juju/commands/apiinfo_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/apiinfo_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,282 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "fmt" + + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/environs/configstore" + "github.com/juju/juju/testing" +) + +type APIInfoSuite struct { + testing.FakeJujuHomeSuite +} + +var _ = gc.Suite(&APIInfoSuite{}) + +func (s *APIInfoSuite) TestArgParsing(c *gc.C) { + for i, test := range []struct { + message string + args []string + refresh bool + user bool + password bool + cacert bool + servers bool + envuuid bool + srvuuid bool + errMatch string + }{ + { + message: "no args skips password", + user: true, + cacert: true, + servers: true, + envuuid: true, + srvuuid: true, + }, { + message: "password shown if user specifies", + args: []string{"--password"}, + user: true, + password: true, + cacert: true, + servers: true, + envuuid: true, + srvuuid: true, + }, { + message: "refresh the cache", + args: []string{"--refresh"}, + refresh: true, + user: true, + cacert: true, + servers: true, + envuuid: true, + srvuuid: true, + }, { + message: "just show the user field", + args: []string{"user"}, + user: true, + }, { + message: "just show the password field", + args: []string{"password"}, + password: true, + }, { + message: "just show the cacert field", + args: []string{"ca-cert"}, + cacert: true, + }, { + message: "just show the servers field", + args: []string{"state-servers"}, + servers: true, + }, { + message: "just show the envuuid field", + args: []string{"environ-uuid"}, + envuuid: true, + }, { + message: "just show the srvuuid field", + args: []string{"server-uuid"}, + srvuuid: true, + }, { + message: "show the user and password field", + args: []string{"user", "password"}, + user: true, + password: true, + }, { + message: "unknown field field", + args: []string{"foo"}, + errMatch: `unknown fields: "foo"`, + }, { + message: "multiple unknown fields", + args: []string{"user", "pwd", "foo"}, + errMatch: `unknown fields: "pwd", "foo"`, + }, + } { + c.Logf("test %v: %s", i, test.message) + command := &APIInfoCommand{} + err := testing.InitCommand(envcmd.Wrap(command), test.args) + if test.errMatch == "" { + c.Check(err, jc.ErrorIsNil) + c.Check(command.refresh, gc.Equals, test.refresh) + c.Check(command.user, gc.Equals, test.user) + c.Check(command.password, gc.Equals, test.password) + c.Check(command.cacert, gc.Equals, test.cacert) + c.Check(command.servers, gc.Equals, test.servers) + c.Check(command.envuuid, gc.Equals, test.envuuid) + c.Check(command.srvuuid, gc.Equals, test.srvuuid) + } else { + c.Check(err, gc.ErrorMatches, test.errMatch) + } + } +} + +func (s *APIInfoSuite) TestOutput(c *gc.C) { + s.PatchValue(&endpoint, func(c envcmd.EnvCommandBase, refresh bool) (configstore.APIEndpoint, error) { + return configstore.APIEndpoint{ + Addresses: []string{"localhost:12345", "10.0.3.1:12345"}, + CACert: "this is the cacert", + EnvironUUID: "deadbeef-dead-beef-dead-deaddeaddead", + ServerUUID: "bad0f00d-dead-beef-0000-01234567899a", + }, nil + }) + s.PatchValue(&creds, func(c envcmd.EnvCommandBase) (configstore.APICredentials, error) { + return configstore.APICredentials{ + User: "tester", + Password: "sekrit", + }, nil + }) + + for i, test := range []struct { + args []string + output string + errMatch string + }{ + { + output: "" + + "user: tester\n" + + "environ-uuid: deadbeef-dead-beef-dead-deaddeaddead\n" + + "server-uuid: bad0f00d-dead-beef-0000-01234567899a\n" + + "state-servers:\n" + + "- localhost:12345\n" + + "- 10.0.3.1:12345\n" + + "ca-cert: this is the cacert\n", + }, { + args: []string{"--password"}, + output: "" + + "user: tester\n" + + "password: sekrit\n" + + "environ-uuid: deadbeef-dead-beef-dead-deaddeaddead\n" + + "server-uuid: bad0f00d-dead-beef-0000-01234567899a\n" + + "state-servers:\n" + + "- localhost:12345\n" + + "- 10.0.3.1:12345\n" + + "ca-cert: this is the cacert\n", + }, { + args: []string{"--format=yaml"}, + output: "" + + "user: tester\n" + + "environ-uuid: deadbeef-dead-beef-dead-deaddeaddead\n" + + "server-uuid: bad0f00d-dead-beef-0000-01234567899a\n" + + "state-servers:\n" + + "- localhost:12345\n" + + "- 10.0.3.1:12345\n" + + "ca-cert: this is the cacert\n", + }, { + args: []string{"--format=json"}, + output: `{"user":"tester",` + + `"environ-uuid":"deadbeef-dead-beef-dead-deaddeaddead",` + + `"server-uuid":"bad0f00d-dead-beef-0000-01234567899a",` + + `"state-servers":["localhost:12345","10.0.3.1:12345"],` + + `"ca-cert":"this is the cacert"}` + "\n", + }, { + args: []string{"user"}, + output: "tester\n", + }, { + args: []string{"user", "password"}, + output: "" + + "user: tester\n" + + "password: sekrit\n", + }, { + args: []string{"state-servers"}, + output: "" + + "localhost:12345\n" + + "10.0.3.1:12345\n", + }, { + args: []string{"--format=yaml", "user"}, + output: "user: tester\n", + }, { + args: []string{"--format=yaml", "user", "password"}, + output: "" + + "user: tester\n" + + "password: sekrit\n", + }, { + args: []string{"--format=yaml", "state-servers"}, + output: "" + + "state-servers:\n" + + "- localhost:12345\n" + + "- 10.0.3.1:12345\n", + }, { + args: []string{"--format=json", "user"}, + output: `{"user":"tester"}` + "\n", + }, { + args: []string{"--format=json", "user", "password"}, + output: `{"user":"tester","password":"sekrit"}` + "\n", + }, { + args: []string{"--format=json", "state-servers"}, + output: `{"state-servers":["localhost:12345","10.0.3.1:12345"]}` + "\n", + }, + } { + c.Logf("test %v: %v", i, test.args) + command := &APIInfoCommand{} + ctx, err := testing.RunCommand(c, envcmd.Wrap(command), test.args...) + if test.errMatch == "" { + c.Check(err, jc.ErrorIsNil) + c.Check(testing.Stdout(ctx), gc.Equals, test.output) + } else { + c.Check(err, gc.ErrorMatches, test.errMatch) + } + } +} + +func (s *APIInfoSuite) TestOutputNoServerUUID(c *gc.C) { + s.PatchValue(&endpoint, func(c envcmd.EnvCommandBase, refresh bool) (configstore.APIEndpoint, error) { + return configstore.APIEndpoint{ + Addresses: []string{"localhost:12345", "10.0.3.1:12345"}, + CACert: "this is the cacert", + EnvironUUID: "deadbeef-dead-beef-dead-deaddeaddead", + }, nil + }) + s.PatchValue(&creds, func(c envcmd.EnvCommandBase) (configstore.APICredentials, error) { + return configstore.APICredentials{ + User: "tester", + Password: "sekrit", + }, nil + }) + + expected := "" + + "user: tester\n" + + "environ-uuid: deadbeef-dead-beef-dead-deaddeaddead\n" + + "state-servers:\n" + + "- localhost:12345\n" + + "- 10.0.3.1:12345\n" + + "ca-cert: this is the cacert\n" + command := &APIInfoCommand{} + ctx, err := testing.RunCommand(c, envcmd.Wrap(command)) + c.Check(err, jc.ErrorIsNil) + c.Check(testing.Stdout(ctx), gc.Equals, expected) +} + +func (s *APIInfoSuite) TestEndpointError(c *gc.C) { + s.PatchValue(&endpoint, func(c envcmd.EnvCommandBase, refresh bool) (configstore.APIEndpoint, error) { + return configstore.APIEndpoint{}, fmt.Errorf("oops, no endpoint") + }) + s.PatchValue(&creds, func(c envcmd.EnvCommandBase) (configstore.APICredentials, error) { + return configstore.APICredentials{}, nil + }) + command := &APIInfoCommand{} + _, err := testing.RunCommand(c, envcmd.Wrap(command)) + c.Assert(err, gc.ErrorMatches, "oops, no endpoint") +} + +func (s *APIInfoSuite) TestCredentialsError(c *gc.C) { + s.PatchValue(&endpoint, func(c envcmd.EnvCommandBase, refresh bool) (configstore.APIEndpoint, error) { + return configstore.APIEndpoint{}, nil + }) + s.PatchValue(&creds, func(c envcmd.EnvCommandBase) (configstore.APICredentials, error) { + return configstore.APICredentials{}, fmt.Errorf("oops, no creds") + }) + command := &APIInfoCommand{} + _, err := testing.RunCommand(c, envcmd.Wrap(command)) + c.Assert(err, gc.ErrorMatches, "oops, no creds") +} + +func (s *APIInfoSuite) TestNoEnvironment(c *gc.C) { + command := &APIInfoCommand{} + _, err := testing.RunCommand(c, envcmd.Wrap(command)) + c.Assert(err, gc.ErrorMatches, `environment "erewhemos" not found`) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/authorizedkeys.go' --- src/github.com/juju/juju/cmd/juju/commands/authorizedkeys.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/authorizedkeys.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,57 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "github.com/juju/cmd" + "launchpad.net/gnuflag" + + "github.com/juju/juju/api/keymanager" + "github.com/juju/juju/cmd/envcmd" +) + +var authKeysDoc = ` +"juju authorized-keys" is used to manage the ssh keys allowed to log on to +nodes in the Juju environment. + +` + +type AuthorizedKeysCommand struct { + *cmd.SuperCommand +} + +type AuthorizedKeysBase struct { + envcmd.EnvCommandBase +} + +// NewKeyManagerClient returns a keymanager client for the root api endpoint +// that the environment command returns. +func (c *AuthorizedKeysBase) NewKeyManagerClient() (*keymanager.Client, error) { + root, err := c.NewAPIRoot() + if err != nil { + return nil, err + } + return keymanager.NewClient(root), nil +} + +func NewAuthorizedKeysCommand() cmd.Command { + sshkeyscmd := &AuthorizedKeysCommand{ + SuperCommand: cmd.NewSuperCommand(cmd.SuperCommandParams{ + Name: "authorized-keys", + Doc: authKeysDoc, + UsagePrefix: "juju", + Purpose: "manage authorised ssh keys", + Aliases: []string{"authorised-keys"}, + }), + } + sshkeyscmd.Register(envcmd.Wrap(&AddKeysCommand{})) + sshkeyscmd.Register(envcmd.Wrap(&DeleteKeysCommand{})) + sshkeyscmd.Register(envcmd.Wrap(&ImportKeysCommand{})) + sshkeyscmd.Register(envcmd.Wrap(&ListKeysCommand{})) + return sshkeyscmd +} + +func (c *AuthorizedKeysCommand) SetFlags(f *gnuflag.FlagSet) { + c.SetCommonFlags(f) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/authorizedkeys_add.go' --- src/github.com/juju/juju/cmd/juju/commands/authorizedkeys_add.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/authorizedkeys_add.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,67 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "errors" + "fmt" + + "github.com/juju/cmd" + "launchpad.net/gnuflag" + + "github.com/juju/juju/cmd/juju/block" +) + +var addKeysDoc = ` +Add new authorised ssh keys to allow the holder of those keys to log on to Juju nodes or machines. +` + +// AddKeysCommand is used to add a new authorized ssh key for a user. +type AddKeysCommand struct { + AuthorizedKeysBase + user string + sshKeys []string +} + +func (c *AddKeysCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "add", + Args: " [...]", + Doc: addKeysDoc, + Purpose: "add new authorized ssh keys for a Juju user", + } +} + +func (c *AddKeysCommand) Init(args []string) error { + switch len(args) { + case 0: + return errors.New("no ssh key specified") + default: + c.sshKeys = args + } + return nil +} + +func (c *AddKeysCommand) SetFlags(f *gnuflag.FlagSet) { + f.StringVar(&c.user, "user", "admin", "the user for which to add the keys") +} + +func (c *AddKeysCommand) Run(context *cmd.Context) error { + client, err := c.NewKeyManagerClient() + if err != nil { + return err + } + defer client.Close() + + results, err := client.AddKeys(c.user, c.sshKeys...) + if err != nil { + return block.ProcessBlockedError(err, block.BlockChange) + } + for i, result := range results { + if result.Error != nil { + fmt.Fprintf(context.Stderr, "cannot add key %q: %v\n", c.sshKeys[i], result.Error) + } + } + return nil +} === added file 'src/github.com/juju/juju/cmd/juju/commands/authorizedkeys_delete.go' --- src/github.com/juju/juju/cmd/juju/commands/authorizedkeys_delete.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/authorizedkeys_delete.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,69 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "errors" + "fmt" + + "github.com/juju/cmd" + "launchpad.net/gnuflag" + + "github.com/juju/juju/cmd/juju/block" +) + +var deleteKeysDoc = ` +Delete existing authorized ssh keys to remove ssh access for the holder of those keys. +The keys to delete are found by specifying either the "comment" portion of the ssh key, +typically something like "user@host", or the key fingerprint found by using ssh-keygen. +` + +// DeleteKeysCommand is used to delete authorised ssh keys for a user. +type DeleteKeysCommand struct { + AuthorizedKeysBase + user string + keyIds []string +} + +func (c *DeleteKeysCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "delete", + Args: " [...]", + Doc: deleteKeysDoc, + Purpose: "delete authorized ssh keys for a Juju user", + } +} + +func (c *DeleteKeysCommand) Init(args []string) error { + switch len(args) { + case 0: + return errors.New("no ssh key id specified") + default: + c.keyIds = args + } + return nil +} + +func (c *DeleteKeysCommand) SetFlags(f *gnuflag.FlagSet) { + f.StringVar(&c.user, "user", "admin", "the user for which to delete the keys") +} + +func (c *DeleteKeysCommand) Run(context *cmd.Context) error { + client, err := c.NewKeyManagerClient() + if err != nil { + return err + } + defer client.Close() + + results, err := client.DeleteKeys(c.user, c.keyIds...) + if err != nil { + return block.ProcessBlockedError(err, block.BlockChange) + } + for i, result := range results { + if result.Error != nil { + fmt.Fprintf(context.Stderr, "cannot delete key id %q: %v\n", c.keyIds[i], result.Error) + } + } + return nil +} === added file 'src/github.com/juju/juju/cmd/juju/commands/authorizedkeys_import.go' --- src/github.com/juju/juju/cmd/juju/commands/authorizedkeys_import.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/authorizedkeys_import.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,68 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "errors" + "fmt" + + "github.com/juju/cmd" + "launchpad.net/gnuflag" + + "github.com/juju/juju/cmd/juju/block" +) + +var importKeysDoc = ` +Import new authorised ssh keys to allow the holder of those keys to log on to Juju nodes or machines. +The keys are imported using ssh-import-id. +` + +// ImportKeysCommand is used to add new authorized ssh keys for a user. +type ImportKeysCommand struct { + AuthorizedKeysBase + user string + sshKeyIds []string +} + +func (c *ImportKeysCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "import", + Args: " [...]", + Doc: importKeysDoc, + Purpose: "using ssh-import-id, import new authorized ssh keys for a Juju user", + } +} + +func (c *ImportKeysCommand) Init(args []string) error { + switch len(args) { + case 0: + return errors.New("no ssh key id specified") + default: + c.sshKeyIds = args + } + return nil +} + +func (c *ImportKeysCommand) SetFlags(f *gnuflag.FlagSet) { + f.StringVar(&c.user, "user", "admin", "the user for which to import the keys") +} + +func (c *ImportKeysCommand) Run(context *cmd.Context) error { + client, err := c.NewKeyManagerClient() + if err != nil { + return err + } + defer client.Close() + + results, err := client.ImportKeys(c.user, c.sshKeyIds...) + if err != nil { + return block.ProcessBlockedError(err, block.BlockChange) + } + for i, result := range results { + if result.Error != nil { + fmt.Fprintf(context.Stderr, "cannot import key id %q: %v\n", c.sshKeyIds[i], result.Error) + } + } + return nil +} === added file 'src/github.com/juju/juju/cmd/juju/commands/authorizedkeys_list.go' --- src/github.com/juju/juju/cmd/juju/commands/authorizedkeys_list.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/authorizedkeys_list.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,64 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "fmt" + "strings" + + "github.com/juju/cmd" + "launchpad.net/gnuflag" + + "github.com/juju/juju/utils/ssh" +) + +var listKeysDoc = ` +List a user's authorized ssh keys, allowing the holders of those keys to log on to Juju nodes. +By default, just the key fingerprint is printed. Use --full to display the entire key. + +` + +// ListKeysCommand is used to list the authorized ssh keys. +type ListKeysCommand struct { + AuthorizedKeysBase + showFullKey bool + user string +} + +func (c *ListKeysCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "list", + Doc: listKeysDoc, + Purpose: "list authorised ssh keys for a specified user", + } +} + +func (c *ListKeysCommand) SetFlags(f *gnuflag.FlagSet) { + f.BoolVar(&c.showFullKey, "full", false, "show full key instead of just the key fingerprint") + f.StringVar(&c.user, "user", "admin", "the user for which to list the keys") +} + +func (c *ListKeysCommand) Run(context *cmd.Context) error { + client, err := c.NewKeyManagerClient() + if err != nil { + return err + } + defer client.Close() + + mode := ssh.Fingerprints + if c.showFullKey { + mode = ssh.FullKeys + } + results, err := client.ListKeys(mode, c.user) + if err != nil { + return err + } + result := results[0] + if result.Error != nil { + return result.Error + } + fmt.Fprintf(context.Stdout, "Keys for user %s:\n", c.user) + fmt.Fprintln(context.Stdout, strings.Join(result.Result, "\n")) + return nil +} === added file 'src/github.com/juju/juju/cmd/juju/commands/authorizedkeys_test.go' --- src/github.com/juju/juju/cmd/juju/commands/authorizedkeys_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/authorizedkeys_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,289 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "fmt" + "strings" + + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + keymanagerserver "github.com/juju/juju/apiserver/keymanager" + keymanagertesting "github.com/juju/juju/apiserver/keymanager/testing" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/juju/osenv" + jujutesting "github.com/juju/juju/juju/testing" + coretesting "github.com/juju/juju/testing" + "github.com/juju/juju/testing/factory" + sshtesting "github.com/juju/juju/utils/ssh/testing" +) + +type AuthorizedKeysSuite struct { + coretesting.FakeJujuHomeSuite +} + +var _ = gc.Suite(&AuthorizedKeysSuite{}) + +var authKeysCommandNames = []string{ + "add", + "delete", + "help", + "import", + "list", +} + +func (s *AuthorizedKeysSuite) TestHelpCommands(c *gc.C) { + // Check that we have correctly registered all the sub commands + // by checking the help output. + out := badrun(c, 0, "authorized-keys", "--help") + lines := strings.Split(out, "\n") + var names []string + subcommandsFound := false + for _, line := range lines { + f := strings.Fields(line) + if len(f) == 1 && f[0] == "commands:" { + subcommandsFound = true + continue + } + if !subcommandsFound || len(f) == 0 || !strings.HasPrefix(line, " ") { + continue + } + names = append(names, f[0]) + } + // The names should be output in alphabetical order, so don't sort. + c.Assert(names, gc.DeepEquals, authKeysCommandNames) +} + +func (s *AuthorizedKeysSuite) assertHelpOutput(c *gc.C, cmd, args string) { + if args != "" { + args = " " + args + } + expected := fmt.Sprintf("usage: juju authorized-keys %s [options]%s", cmd, args) + out := badrun(c, 0, "authorized-keys", cmd, "--help") + lines := strings.Split(out, "\n") + c.Assert(lines[0], gc.Equals, expected) +} + +func (s *AuthorizedKeysSuite) TestHelpList(c *gc.C) { + s.assertHelpOutput(c, "list", "") +} + +func (s *AuthorizedKeysSuite) TestHelpAdd(c *gc.C) { + s.assertHelpOutput(c, "add", " [...]") +} + +func (s *AuthorizedKeysSuite) TestHelpDelete(c *gc.C) { + s.assertHelpOutput(c, "delete", " [...]") +} + +func (s *AuthorizedKeysSuite) TestHelpImport(c *gc.C) { + s.assertHelpOutput(c, "import", " [...]") +} + +type keySuiteBase struct { + jujutesting.JujuConnSuite + CmdBlockHelper +} + +func (s *keySuiteBase) SetUpSuite(c *gc.C) { + s.JujuConnSuite.SetUpSuite(c) + s.PatchEnvironment(osenv.JujuEnvEnvKey, "dummyenv") +} + +func (s *keySuiteBase) SetUpTest(c *gc.C) { + s.JujuConnSuite.SetUpTest(c) + s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) + c.Assert(s.CmdBlockHelper, gc.NotNil) + s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) +} + +func (s *keySuiteBase) setAuthorizedKeys(c *gc.C, keys ...string) { + keyString := strings.Join(keys, "\n") + err := s.State.UpdateEnvironConfig(map[string]interface{}{"authorized-keys": keyString}, nil, nil) + c.Assert(err, jc.ErrorIsNil) + envConfig, err := s.State.EnvironConfig() + c.Assert(err, jc.ErrorIsNil) + c.Assert(envConfig.AuthorizedKeys(), gc.Equals, keyString) +} + +func (s *keySuiteBase) assertEnvironKeys(c *gc.C, expected ...string) { + envConfig, err := s.State.EnvironConfig() + c.Assert(err, jc.ErrorIsNil) + keys := envConfig.AuthorizedKeys() + c.Assert(keys, gc.Equals, strings.Join(expected, "\n")) +} + +type ListKeysSuite struct { + keySuiteBase +} + +var _ = gc.Suite(&ListKeysSuite{}) + +func (s *ListKeysSuite) TestListKeys(c *gc.C) { + key1 := sshtesting.ValidKeyOne.Key + " user@host" + key2 := sshtesting.ValidKeyTwo.Key + " another@host" + s.setAuthorizedKeys(c, key1, key2) + + context, err := coretesting.RunCommand(c, envcmd.Wrap(&ListKeysCommand{})) + c.Assert(err, jc.ErrorIsNil) + output := strings.TrimSpace(coretesting.Stdout(context)) + c.Assert(err, jc.ErrorIsNil) + c.Assert(output, gc.Matches, "Keys for user admin:\n.*\\(user@host\\)\n.*\\(another@host\\)") +} + +func (s *ListKeysSuite) TestListFullKeys(c *gc.C) { + key1 := sshtesting.ValidKeyOne.Key + " user@host" + key2 := sshtesting.ValidKeyTwo.Key + " another@host" + s.setAuthorizedKeys(c, key1, key2) + + context, err := coretesting.RunCommand(c, envcmd.Wrap(&ListKeysCommand{}), "--full") + c.Assert(err, jc.ErrorIsNil) + output := strings.TrimSpace(coretesting.Stdout(context)) + c.Assert(err, jc.ErrorIsNil) + c.Assert(output, gc.Matches, "Keys for user admin:\n.*user@host\n.*another@host") +} + +func (s *ListKeysSuite) TestListKeysNonDefaultUser(c *gc.C) { + key1 := sshtesting.ValidKeyOne.Key + " user@host" + key2 := sshtesting.ValidKeyTwo.Key + " another@host" + s.setAuthorizedKeys(c, key1, key2) + s.Factory.MakeUser(c, &factory.UserParams{Name: "fred"}) + + context, err := coretesting.RunCommand(c, envcmd.Wrap(&ListKeysCommand{}), "--user", "fred") + c.Assert(err, jc.ErrorIsNil) + output := strings.TrimSpace(coretesting.Stdout(context)) + c.Assert(err, jc.ErrorIsNil) + c.Assert(output, gc.Matches, "Keys for user fred:\n.*\\(user@host\\)\n.*\\(another@host\\)") +} + +func (s *ListKeysSuite) TestTooManyArgs(c *gc.C) { + _, err := coretesting.RunCommand(c, envcmd.Wrap(&ListKeysCommand{}), "foo") + c.Assert(err, gc.ErrorMatches, `unrecognized args: \["foo"\]`) +} + +type AddKeySuite struct { + keySuiteBase +} + +var _ = gc.Suite(&AddKeySuite{}) + +func (s *AddKeySuite) TestAddKey(c *gc.C) { + key1 := sshtesting.ValidKeyOne.Key + " user@host" + s.setAuthorizedKeys(c, key1) + + key2 := sshtesting.ValidKeyTwo.Key + " another@host" + context, err := coretesting.RunCommand(c, envcmd.Wrap(&AddKeysCommand{}), key2, "invalid-key") + c.Assert(err, jc.ErrorIsNil) + c.Assert(coretesting.Stderr(context), gc.Matches, `cannot add key "invalid-key".*\n`) + s.assertEnvironKeys(c, key1, key2) +} + +func (s *AddKeySuite) TestBlockAddKey(c *gc.C) { + key1 := sshtesting.ValidKeyOne.Key + " user@host" + s.setAuthorizedKeys(c, key1) + + key2 := sshtesting.ValidKeyTwo.Key + " another@host" + // Block operation + s.BlockAllChanges(c, "TestBlockAddKey") + _, err := coretesting.RunCommand(c, envcmd.Wrap(&AddKeysCommand{}), key2, "invalid-key") + s.AssertBlocked(c, err, ".*TestBlockAddKey.*") +} + +func (s *AddKeySuite) TestAddKeyNonDefaultUser(c *gc.C) { + key1 := sshtesting.ValidKeyOne.Key + " user@host" + s.setAuthorizedKeys(c, key1) + s.Factory.MakeUser(c, &factory.UserParams{Name: "fred"}) + + key2 := sshtesting.ValidKeyTwo.Key + " another@host" + context, err := coretesting.RunCommand(c, envcmd.Wrap(&AddKeysCommand{}), "--user", "fred", key2) + c.Assert(err, jc.ErrorIsNil) + c.Assert(coretesting.Stderr(context), gc.Equals, "") + s.assertEnvironKeys(c, key1, key2) +} + +type DeleteKeySuite struct { + keySuiteBase +} + +var _ = gc.Suite(&DeleteKeySuite{}) + +func (s *DeleteKeySuite) TestDeleteKeys(c *gc.C) { + key1 := sshtesting.ValidKeyOne.Key + " user@host" + key2 := sshtesting.ValidKeyTwo.Key + " another@host" + s.setAuthorizedKeys(c, key1, key2) + + context, err := coretesting.RunCommand(c, envcmd.Wrap(&DeleteKeysCommand{}), + sshtesting.ValidKeyTwo.Fingerprint, "invalid-key") + c.Assert(err, jc.ErrorIsNil) + c.Assert(coretesting.Stderr(context), gc.Matches, `cannot delete key id "invalid-key".*\n`) + s.assertEnvironKeys(c, key1) +} + +func (s *DeleteKeySuite) TestBlockDeleteKeys(c *gc.C) { + key1 := sshtesting.ValidKeyOne.Key + " user@host" + key2 := sshtesting.ValidKeyTwo.Key + " another@host" + s.setAuthorizedKeys(c, key1, key2) + + // Block operation + s.BlockAllChanges(c, "TestBlockDeleteKeys") + _, err := coretesting.RunCommand(c, envcmd.Wrap(&DeleteKeysCommand{}), + sshtesting.ValidKeyTwo.Fingerprint, "invalid-key") + s.AssertBlocked(c, err, ".*TestBlockDeleteKeys.*") +} + +func (s *DeleteKeySuite) TestDeleteKeyNonDefaultUser(c *gc.C) { + key1 := sshtesting.ValidKeyOne.Key + " user@host" + key2 := sshtesting.ValidKeyTwo.Key + " another@host" + s.setAuthorizedKeys(c, key1, key2) + s.Factory.MakeUser(c, &factory.UserParams{Name: "fred"}) + + context, err := coretesting.RunCommand(c, envcmd.Wrap(&DeleteKeysCommand{}), + "--user", "fred", sshtesting.ValidKeyTwo.Fingerprint) + c.Assert(err, jc.ErrorIsNil) + c.Assert(coretesting.Stderr(context), gc.Equals, "") + s.assertEnvironKeys(c, key1) +} + +type ImportKeySuite struct { + keySuiteBase +} + +var _ = gc.Suite(&ImportKeySuite{}) + +func (s *ImportKeySuite) SetUpTest(c *gc.C) { + s.keySuiteBase.SetUpTest(c) + s.PatchValue(&keymanagerserver.RunSSHImportId, keymanagertesting.FakeImport) +} + +func (s *ImportKeySuite) TestImportKeys(c *gc.C) { + key1 := sshtesting.ValidKeyOne.Key + " user@host" + s.setAuthorizedKeys(c, key1) + + context, err := coretesting.RunCommand(c, envcmd.Wrap(&ImportKeysCommand{}), "lp:validuser", "invalid-key") + c.Assert(err, jc.ErrorIsNil) + c.Assert(coretesting.Stderr(context), gc.Matches, `cannot import key id "invalid-key".*\n`) + s.assertEnvironKeys(c, key1, sshtesting.ValidKeyThree.Key) +} + +func (s *ImportKeySuite) TestBlockImportKeys(c *gc.C) { + key1 := sshtesting.ValidKeyOne.Key + " user@host" + s.setAuthorizedKeys(c, key1) + + // Block operation + s.BlockAllChanges(c, "TestBlockImportKeys") + _, err := coretesting.RunCommand(c, envcmd.Wrap(&ImportKeysCommand{}), "lp:validuser", "invalid-key") + s.AssertBlocked(c, err, ".*TestBlockImportKeys.*") +} + +func (s *ImportKeySuite) TestImportKeyNonDefaultUser(c *gc.C) { + key1 := sshtesting.ValidKeyOne.Key + " user@host" + s.setAuthorizedKeys(c, key1) + s.Factory.MakeUser(c, &factory.UserParams{Name: "fred"}) + + context, err := coretesting.RunCommand(c, envcmd.Wrap(&ImportKeysCommand{}), "--user", "fred", "lp:validuser") + c.Assert(err, jc.ErrorIsNil) + c.Assert(coretesting.Stderr(context), gc.Equals, "") + s.assertEnvironKeys(c, key1, sshtesting.ValidKeyThree.Key) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/bootstrap.go' --- src/github.com/juju/juju/cmd/juju/commands/bootstrap.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/bootstrap.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,481 @@ +// Copyright 2012, 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/errors" + "github.com/juju/utils" + "github.com/juju/utils/featureflag" + "gopkg.in/juju/charm.v5" + "launchpad.net/gnuflag" + + apiblock "github.com/juju/juju/api/block" + "github.com/juju/juju/apiserver" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/block" + "github.com/juju/juju/constraints" + "github.com/juju/juju/environs" + "github.com/juju/juju/environs/bootstrap" + "github.com/juju/juju/environs/configstore" + "github.com/juju/juju/feature" + "github.com/juju/juju/instance" + "github.com/juju/juju/juju" + "github.com/juju/juju/juju/osenv" + "github.com/juju/juju/network" + "github.com/juju/juju/provider" + "github.com/juju/juju/version" +) + +// provisionalProviders is the names of providers that are hidden behind +// feature flags. +var provisionalProviders = map[string]string{ + "vsphere": feature.VSphereProvider, +} + +const bootstrapDoc = ` +bootstrap starts a new environment of the current type (it will return an error +if the environment has already been bootstrapped). Bootstrapping an environment +will provision a new machine in the environment and run the juju state server on +that machine. + +If constraints are specified in the bootstrap command, they will apply to the +machine provisioned for the juju state server. They will also be set as default +constraints on the environment for all future machines, exactly as if the +constraints were set with juju set-constraints. + +It is possible to override constraints and the automatic machine selection +algorithm by using the "--to" flag. The value associated with "--to" is a +"placement directive", which tells Juju how to identify the first machine to use. +For more information on placement directives, see "juju help placement". + +Bootstrap initialises the cloud environment synchronously and displays information +about the current installation steps. The time for bootstrap to complete varies +across cloud providers from a few seconds to several minutes. Once bootstrap has +completed, you can run other juju commands against your environment. You can change +the default timeout and retry delays used during the bootstrap by changing the +following settings in your environments.yaml (all values represent number of seconds): + + # How long to wait for a connection to the state server. + bootstrap-timeout: 600 # default: 10 minutes + # How long to wait between connection attempts to a state server address. + bootstrap-retry-delay: 5 # default: 5 seconds + # How often to refresh state server addresses from the API server. + bootstrap-addresses-delay: 10 # default: 10 seconds + +Private clouds may need to specify their own custom image metadata, and +possibly upload Juju tools to cloud storage if no outgoing Internet access is +available. In this case, use the --metadata-source parameter to point +bootstrap to a local directory from which to upload tools and/or image +metadata. + +If agent-version is specifed, this is the default tools version to use when running the Juju agents. +Only the numeric version is relevant. To enable ease of scripting, the full binary version +is accepted (eg 1.24.4-trusty-amd64) but only the numeric version (eg 1.24.4) is used. +An alias for bootstrapping Juju with the exact same version as the client is to use the +--no-auto-upgrade parameter. + +See Also: + juju help switch + juju help constraints + juju help set-constraints + juju help placement +` + +// BootstrapCommand is responsible for launching the first machine in a juju +// environment, and setting up everything necessary to continue working. +type BootstrapCommand struct { + envcmd.EnvCommandBase + Constraints constraints.Value + UploadTools bool + Series []string + seriesOld []string + MetadataSource string + Placement string + KeepBrokenEnvironment bool + NoAutoUpgrade bool + AgentVersionParam string + AgentVersion *version.Number +} + +func (c *BootstrapCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "bootstrap", + Purpose: "start up an environment from scratch", + Doc: bootstrapDoc, + } +} + +func (c *BootstrapCommand) SetFlags(f *gnuflag.FlagSet) { + f.Var(constraints.ConstraintsValue{Target: &c.Constraints}, "constraints", "set environment constraints") + f.BoolVar(&c.UploadTools, "upload-tools", false, "upload local version of tools before bootstrapping") + f.Var(newSeriesValue(nil, &c.Series), "upload-series", "upload tools for supplied comma-separated series list (OBSOLETE)") + f.Var(newSeriesValue(nil, &c.seriesOld), "series", "see --upload-series (OBSOLETE)") + f.StringVar(&c.MetadataSource, "metadata-source", "", "local path to use as tools and/or metadata source") + f.StringVar(&c.Placement, "to", "", "a placement directive indicating an instance to bootstrap") + f.BoolVar(&c.KeepBrokenEnvironment, "keep-broken", false, "do not destroy the environment if bootstrap fails") + f.BoolVar(&c.NoAutoUpgrade, "no-auto-upgrade", false, "do not upgrade to newer tools on first bootstrap") + f.StringVar(&c.AgentVersionParam, "agent-version", "", "the version of tools to initially use for Juju agents") +} + +func (c *BootstrapCommand) Init(args []string) (err error) { + if len(c.Series) > 0 && !c.UploadTools { + return fmt.Errorf("--upload-series requires --upload-tools") + } + if len(c.seriesOld) > 0 && !c.UploadTools { + return fmt.Errorf("--series requires --upload-tools") + } + if len(c.Series) > 0 && len(c.seriesOld) > 0 { + return fmt.Errorf("--upload-series and --series can't be used together") + } + if c.AgentVersionParam != "" && c.UploadTools { + return fmt.Errorf("--agent-version and --upload-tools can't be used together") + } + if c.AgentVersionParam != "" && c.NoAutoUpgrade { + return fmt.Errorf("--agent-version and --no-auto-upgrade can't be used together") + } + + // Parse the placement directive. Bootstrap currently only + // supports provider-specific placement directives. + if c.Placement != "" { + _, err = instance.ParsePlacement(c.Placement) + if err != instance.ErrPlacementScopeMissing { + // We only support unscoped placement directives for bootstrap. + return fmt.Errorf("unsupported bootstrap placement directive %q", c.Placement) + } + } + if c.NoAutoUpgrade { + vers := version.Current.Number + c.AgentVersion = &vers + } else if c.AgentVersionParam != "" { + if vers, err := version.ParseBinary(c.AgentVersionParam); err == nil { + c.AgentVersion = &vers.Number + } else if vers, err := version.Parse(c.AgentVersionParam); err == nil { + c.AgentVersion = &vers + } else { + return err + } + } + if c.AgentVersion != nil && (c.AgentVersion.Major != version.Current.Major || c.AgentVersion.Minor != version.Current.Minor) { + return fmt.Errorf("requested agent version major.minor mismatch") + } + return cmd.CheckEmpty(args) +} + +type seriesValue struct { + *cmd.StringsValue +} + +// newSeriesValue is used to create the type passed into the gnuflag.FlagSet Var function. +func newSeriesValue(defaultValue []string, target *[]string) *seriesValue { + v := seriesValue{(*cmd.StringsValue)(target)} + *(v.StringsValue) = defaultValue + return &v +} + +// Implements gnuflag.Value Set. +func (v *seriesValue) Set(s string) error { + if err := v.StringsValue.Set(s); err != nil { + return err + } + for _, name := range *(v.StringsValue) { + if !charm.IsValidSeries(name) { + v.StringsValue = nil + return fmt.Errorf("invalid series name %q", name) + } + } + return nil +} + +// bootstrap functionality that Run calls to support cleaner testing +type BootstrapInterface interface { + EnsureNotBootstrapped(env environs.Environ) error + Bootstrap(ctx environs.BootstrapContext, environ environs.Environ, args bootstrap.BootstrapParams) error +} + +type bootstrapFuncs struct{} + +func (b bootstrapFuncs) EnsureNotBootstrapped(env environs.Environ) error { + return bootstrap.EnsureNotBootstrapped(env) +} + +func (b bootstrapFuncs) Bootstrap(ctx environs.BootstrapContext, env environs.Environ, args bootstrap.BootstrapParams) error { + return bootstrap.Bootstrap(ctx, env, args) +} + +var getBootstrapFuncs = func() BootstrapInterface { + return &bootstrapFuncs{} +} + +var getEnvName = func(c *BootstrapCommand) string { + return c.ConnectionName() +} + +// Run connects to the environment specified on the command line and bootstraps +// 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) { + bootstrapFuncs := getBootstrapFuncs() + + if len(c.seriesOld) > 0 { + fmt.Fprintln(ctx.Stderr, "Use of --series is obsolete. --upload-tools now expands to all supported series of the same operating system.") + } + if len(c.Series) > 0 { + fmt.Fprintln(ctx.Stderr, "Use of --upload-series is obsolete. --upload-tools now expands to all supported series of the same operating system.") + } + + envName := getEnvName(c) + if envName == "" { + return errors.Errorf("the name of the environment must be specified") + } + if err := checkProviderType(envName); errors.IsNotFound(err) { + // This error will get handled later. + } else if err != nil { + return errors.Trace(err) + } + + environ, cleanup, err := environFromName( + ctx, + envName, + "Bootstrap", + bootstrapFuncs.EnsureNotBootstrapped, + ) + + // If we error out for any reason, clean up the environment. + defer func() { + if resultErr != nil && cleanup != nil { + if c.KeepBrokenEnvironment { + logger.Warningf("bootstrap failed but --keep-broken was specified so environment is not being destroyed.\n" + + "When you are finished diagnosing the problem, remember to run juju destroy-environment --force\n" + + "to clean up the environment.") + } else { + handleBootstrapError(ctx, resultErr, cleanup) + } + } + }() + + // Handle any errors from environFromName(...). + if err != nil { + return errors.Annotatef(err, "there was an issue examining the environment") + } + + // Check to see if this environment is already bootstrapped. If it + // is, we inform the user and exit early. If an error is returned + // but it is not that the environment is already bootstrapped, + // then we're in an unknown state. + if err := bootstrapFuncs.EnsureNotBootstrapped(environ); nil != err { + if environs.ErrAlreadyBootstrapped == err { + logger.Warningf("This juju environment is already bootstrapped. If you want to start a new Juju\nenvironment, first run juju destroy-environment to clean up, or switch to an\nalternative environment.") + return err + } + return errors.Annotatef(err, "cannot determine if environment is already bootstrapped.") + } + + // Block interruption during bootstrap. Providers may also + // register for interrupt notification so they can exit early. + interrupted := make(chan os.Signal, 1) + defer close(interrupted) + ctx.InterruptNotify(interrupted) + defer ctx.StopInterruptNotify(interrupted) + go func() { + for _ = range interrupted { + ctx.Infof("Interrupt signalled: waiting for bootstrap to exit") + } + }() + + // If --metadata-source is specified, override the default tools metadata source so + // SyncTools can use it, and also upload any image metadata. + var metadataDir string + if c.MetadataSource != "" { + metadataDir = ctx.AbsPath(c.MetadataSource) + } + + // TODO (wallyworld): 2013-09-20 bug 1227931 + // We can set a custom tools data source instead of doing an + // unnecessary upload. + if environ.Config().Type() == provider.Local { + c.UploadTools = true + } + + err = bootstrapFuncs.Bootstrap(envcmd.BootstrapContext(ctx), environ, bootstrap.BootstrapParams{ + Constraints: c.Constraints, + Placement: c.Placement, + UploadTools: c.UploadTools, + AgentVersion: c.AgentVersion, + MetadataDir: metadataDir, + }) + if err != nil { + return errors.Annotate(err, "failed to bootstrap environment") + } + err = c.SetBootstrapEndpointAddress(environ) + if err != nil { + return errors.Annotate(err, "saving bootstrap endpoint address") + } + // To avoid race conditions when running scripted bootstraps, wait + // for the state server's machine agent to be ready to accept commands + // before exiting this bootstrap command. + return c.waitForAgentInitialisation(ctx) +} + +var ( + bootstrapReadyPollDelay = 1 * time.Second + bootstrapReadyPollCount = 60 + blockAPI = getBlockAPI +) + +// getBlockAPI returns a block api for listing blocks. +func getBlockAPI(c *envcmd.EnvCommandBase) (block.BlockListAPI, error) { + root, err := c.NewAPIRoot() + if err != nil { + return nil, err + } + return apiblock.NewClient(root), nil +} + +// waitForAgentInitialisation polls the bootstrapped state server with a read-only +// command which will fail until the state server is fully initialised. +// TODO(wallyworld) - add a bespoke command to maybe the admin facade for this purpose. +func (c *BootstrapCommand) waitForAgentInitialisation(ctx *cmd.Context) (err error) { + attempts := utils.AttemptStrategy{ + Min: bootstrapReadyPollCount, + Delay: bootstrapReadyPollDelay, + } + var client block.BlockListAPI + for attempt := attempts.Start(); attempt.Next(); { + client, err = blockAPI(&c.EnvCommandBase) + if err != nil { + return err + } + _, err = client.List() + client.Close() + if err == nil { + ctx.Infof("Bootstrap complete") + return nil + } + // As the API server is coming up, it goes through a number of steps. + // Initially the upgrade steps run, but the api server allows some + // calls to be processed during the upgrade, but not the list blocks. + // It is also possible that the underlying database causes connections + // to be dropped as it is initialising, or reconfiguring. These can + // lead to EOF or "connection is shut down" error messages. We skip + // these too, hoping that things come back up before the end of the + // retry poll count. + errorMessage := err.Error() + if strings.Contains(errorMessage, apiserver.UpgradeInProgressError.Error()) || + strings.HasSuffix(errorMessage, "EOF") || + strings.HasSuffix(errorMessage, "connection is shut down") { + ctx.Infof("Waiting for API to become available") + continue + } + return err + } + return err +} + +var environType = func(envName string) (string, error) { + store, err := configstore.Default() + if err != nil { + return "", errors.Trace(err) + } + cfg, _, err := environs.ConfigForName(envName, store) + if err != nil { + return "", errors.Trace(err) + } + return cfg.Type(), nil +} + +// checkProviderType ensures the provider type is okay. +func checkProviderType(envName string) error { + envType, err := environType(envName) + if err != nil { + return errors.Trace(err) + } + + featureflag.SetFlagsFromEnvironment(osenv.JujuFeatureFlagEnvKey) + flag, ok := provisionalProviders[envType] + if ok && !featureflag.Enabled(flag) { + msg := `the %q provider is provisional in this version of Juju. To use it anyway, set JUJU_DEV_FEATURE_FLAGS="%s" in your shell environment` + return errors.Errorf(msg, envType, flag) + } + + return nil +} + +// handleBootstrapError is called to clean up if bootstrap fails. +func handleBootstrapError(ctx *cmd.Context, err error, cleanup func()) { + ch := make(chan os.Signal, 1) + ctx.InterruptNotify(ch) + defer ctx.StopInterruptNotify(ch) + defer close(ch) + go func() { + for _ = range ch { + fmt.Fprintln(ctx.GetStderr(), "Cleaning up failed bootstrap") + } + }() + cleanup() +} + +var allInstances = func(environ environs.Environ) ([]instance.Instance, error) { + return environ.AllInstances() +} + +var prepareEndpointsForCaching = juju.PrepareEndpointsForCaching + +// SetBootstrapEndpointAddress writes the API endpoint address of the +// bootstrap server into the connection information. This should only be run +// once directly after Bootstrap. It assumes that there is just one instance +// in the environment - the bootstrap instance. +func (c *BootstrapCommand) SetBootstrapEndpointAddress(environ environs.Environ) error { + instances, err := allInstances(environ) + if err != nil { + return errors.Trace(err) + } + length := len(instances) + if length == 0 { + return errors.Errorf("found no instances, expected at least one") + } + if length > 1 { + logger.Warningf("expected one instance, got %d", length) + } + bootstrapInstance := instances[0] + cfg := environ.Config() + info, err := envcmd.ConnectionInfoForName(c.ConnectionName()) + if err != nil { + return errors.Annotate(err, "failed to get connection info") + } + + // Don't use c.ConnectionEndpoint as it attempts to contact the state + // server if no addresses are found in connection info. + endpoint := info.APIEndpoint() + netAddrs, err := bootstrapInstance.Addresses() + if err != nil { + return errors.Annotate(err, "failed to get bootstrap instance addresses") + } + apiPort := cfg.APIPort() + apiHostPorts := network.AddressesWithPort(netAddrs, apiPort) + addrs, hosts, addrsChanged := prepareEndpointsForCaching( + info, [][]network.HostPort{apiHostPorts}, network.HostPort{}, + ) + if !addrsChanged { + // Something's wrong we already have cached addresses? + return errors.Annotate(err, "cached API endpoints unexpectedly exist") + } + endpoint.Addresses = addrs + endpoint.Hostnames = hosts + writer, err := c.ConnectionWriter() + if err != nil { + return errors.Annotate(err, "failed to get connection writer") + } + writer.SetAPIEndpoint(endpoint) + err = writer.Write() + if err != nil { + return errors.Annotate(err, "failed to write API endpoint to connection info") + } + return nil +} === added file 'src/github.com/juju/juju/cmd/juju/commands/bootstrap_test.go' --- src/github.com/juju/juju/cmd/juju/commands/bootstrap_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/bootstrap_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,935 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "fmt" + "os" + "runtime" + "strings" + "time" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/loggo" + gitjujutesting "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/block" + cmdtesting "github.com/juju/juju/cmd/testing" + "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/configstore" + "github.com/juju/juju/environs/filestorage" + "github.com/juju/juju/environs/imagemetadata" + "github.com/juju/juju/environs/simplestreams" + "github.com/juju/juju/environs/sync" + envtesting "github.com/juju/juju/environs/testing" + envtools "github.com/juju/juju/environs/tools" + toolstesting "github.com/juju/juju/environs/tools/testing" + "github.com/juju/juju/instance" + "github.com/juju/juju/juju" + "github.com/juju/juju/juju/arch" + "github.com/juju/juju/juju/osenv" + "github.com/juju/juju/network" + "github.com/juju/juju/provider/dummy" + coretesting "github.com/juju/juju/testing" + coretools "github.com/juju/juju/tools" + "github.com/juju/juju/version" +) + +type BootstrapSuite struct { + coretesting.FakeJujuHomeSuite + gitjujutesting.MgoSuite + envtesting.ToolsFixture + mockBlockClient *mockBlockClient +} + +var _ = gc.Suite(&BootstrapSuite{}) + +func (s *BootstrapSuite) SetUpSuite(c *gc.C) { + s.FakeJujuHomeSuite.SetUpSuite(c) + s.MgoSuite.SetUpSuite(c) +} + +func (s *BootstrapSuite) SetUpTest(c *gc.C) { + s.FakeJujuHomeSuite.SetUpTest(c) + s.MgoSuite.SetUpTest(c) + s.ToolsFixture.SetUpTest(c) + + // Set version.Current to a known value, for which we + // will make tools available. Individual tests may + // override this. + s.PatchValue(&version.Current, v100p64) + + // Set up a local source with tools. + sourceDir := createToolsSource(c, vAll) + s.PatchValue(&envtools.DefaultBaseURL, sourceDir) + + s.PatchValue(&envtools.BundleTools, toolstesting.GetMockBundleTools(c)) + + s.mockBlockClient = &mockBlockClient{} + s.PatchValue(&blockAPI, func(c *envcmd.EnvCommandBase) (block.BlockListAPI, error) { + return s.mockBlockClient, nil + }) +} + +func (s *BootstrapSuite) TearDownSuite(c *gc.C) { + s.MgoSuite.TearDownSuite(c) + s.FakeJujuHomeSuite.TearDownSuite(c) +} + +func (s *BootstrapSuite) TearDownTest(c *gc.C) { + s.ToolsFixture.TearDownTest(c) + s.MgoSuite.TearDownTest(c) + s.FakeJujuHomeSuite.TearDownTest(c) + dummy.Reset() +} + +type mockBlockClient struct { + retry_count int + num_retries int +} + +func (c *mockBlockClient) List() ([]params.Block, error) { + c.retry_count += 1 + if c.retry_count == 5 { + return nil, fmt.Errorf("upgrade in progress") + } + if c.num_retries < 0 { + return nil, fmt.Errorf("other error") + } + if c.retry_count < c.num_retries { + return nil, fmt.Errorf("upgrade in progress") + } + return []params.Block{}, nil +} + +func (c *mockBlockClient) Close() error { + return nil +} + +func (s *BootstrapSuite) TestBootstrapAPIReadyRetries(c *gc.C) { + s.PatchValue(&bootstrapReadyPollDelay, 1*time.Millisecond) + s.PatchValue(&bootstrapReadyPollCount, 5) + defaultSeriesVersion := version.Current + // Force a dev version by having a non zero build number. + // This is because we have not uploaded any tools and auto + // upload is only enabled for dev versions. + defaultSeriesVersion.Build = 1234 + s.PatchValue(&version.Current, defaultSeriesVersion) + for _, t := range []struct { + num_retries int + err string + }{ + {0, ""}, // agent ready immediately + {2, ""}, // agent ready after 2 polls + {6, "upgrade in progress"}, // agent ready after 6 polls but that's too long + {-1, "other error"}, // another error is returned + } { + resetJujuHome(c, "devenv") + + s.mockBlockClient.num_retries = t.num_retries + s.mockBlockClient.retry_count = 0 + _, err := coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}), "-e", "devenv") + if t.err == "" { + c.Check(err, jc.ErrorIsNil) + } else { + c.Check(err, gc.ErrorMatches, t.err) + } + expectedRetries := t.num_retries + if t.num_retries <= 0 { + expectedRetries = 1 + } + // Only retry maximum of bootstrapReadyPollCount times. + if expectedRetries > 5 { + expectedRetries = 5 + } + c.Check(s.mockBlockClient.retry_count, gc.Equals, expectedRetries) + } +} + +func (s *BootstrapSuite) TestRunTests(c *gc.C) { + for i, test := range bootstrapTests { + c.Logf("\ntest %d: %s", i, test.info) + restore := s.run(c, test) + restore() + } +} + +type bootstrapTest struct { + info string + // binary version string used to set version.Current + version string + sync bool + args []string + err string + // binary version string for expected tools; if set, no default tools + // will be uploaded before running the test. + upload string + constraints constraints.Value + placement string + hostArch string + keepBroken bool +} + +func (s *BootstrapSuite) run(c *gc.C, test bootstrapTest) (restore gitjujutesting.Restorer) { + // Create home with dummy provider and remove all + // of its envtools. + env := resetJujuHome(c, "peckham") + + // Although we're testing PrepareEndpointsForCaching interactions + // separately in the juju package, here we just ensure it gets + // called with the right arguments. + prepareCalled := false + addrConnectedTo := "localhost:17070" + restore = gitjujutesting.PatchValue( + &prepareEndpointsForCaching, + func(info configstore.EnvironInfo, hps [][]network.HostPort, addr network.HostPort) (_, _ []string, _ bool) { + prepareCalled = true + addrs, hosts, changed := juju.PrepareEndpointsForCaching(info, hps, addr) + // Because we're bootstrapping the addresses will always + // change, as there's no .jenv file saved yet. + c.Assert(changed, jc.IsTrue) + return addrs, hosts, changed + }, + ) + + if test.version != "" { + useVersion := strings.Replace(test.version, "%LTS%", config.LatestLtsSeries(), 1) + origVersion := version.Current + version.Current = version.MustParseBinary(useVersion) + restore = restore.Add(func() { + version.Current = origVersion + }) + } + + if test.hostArch != "" { + origArch := arch.HostArch + arch.HostArch = func() string { + return test.hostArch + } + restore = restore.Add(func() { + arch.HostArch = origArch + }) + } + + // Run command and check for uploads. + opc, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), envcmd.Wrap(new(BootstrapCommand)), test.args...) + // Check for remaining operations/errors. + if test.err != "" { + err := <-errc + c.Assert(err, gc.NotNil) + stripped := strings.Replace(err.Error(), "\n", "", -1) + c.Check(stripped, gc.Matches, test.err) + return restore + } + if !c.Check(<-errc, gc.IsNil) { + return restore + } + + opBootstrap := (<-opc).(dummy.OpBootstrap) + c.Check(opBootstrap.Env, gc.Equals, "peckham") + c.Check(opBootstrap.Args.Constraints, gc.DeepEquals, test.constraints) + c.Check(opBootstrap.Args.Placement, gc.Equals, test.placement) + + opFinalizeBootstrap := (<-opc).(dummy.OpFinalizeBootstrap) + c.Check(opFinalizeBootstrap.Env, gc.Equals, "peckham") + c.Check(opFinalizeBootstrap.InstanceConfig.Tools, gc.NotNil) + if test.upload != "" { + c.Check(opFinalizeBootstrap.InstanceConfig.Tools.Version.String(), gc.Equals, test.upload) + } + + store, err := configstore.Default() + c.Assert(err, jc.ErrorIsNil) + // Check a CA cert/key was generated by reloading the environment. + env, err = environs.NewFromName("peckham", store) + c.Assert(err, jc.ErrorIsNil) + _, hasCert := env.Config().CACert() + c.Check(hasCert, jc.IsTrue) + _, hasKey := env.Config().CAPrivateKey() + c.Check(hasKey, jc.IsTrue) + info, err := store.ReadInfo("peckham") + c.Assert(err, jc.ErrorIsNil) + c.Assert(info, gc.NotNil) + c.Assert(prepareCalled, jc.IsTrue) + c.Assert(info.APIEndpoint().Addresses, gc.DeepEquals, []string{addrConnectedTo}) + return restore +} + +var bootstrapTests = []bootstrapTest{{ + info: "no args, no error, no upload, no constraints", +}, { + info: "bad --constraints", + args: []string{"--constraints", "bad=wrong"}, + err: `invalid value "bad=wrong" for flag --constraints: unknown constraint "bad"`, +}, { + info: "conflicting --constraints", + args: []string{"--constraints", "instance-type=foo mem=4G"}, + err: `failed to bootstrap environment: ambiguous constraints: "instance-type" overlaps with "mem"`, +}, { + info: "bad --series", + args: []string{"--series", "1bad1"}, + err: `invalid value "1bad1" for flag --series: invalid series name "1bad1"`, +}, { + info: "lonely --series", + args: []string{"--series", "fine"}, + err: `--series requires --upload-tools`, +}, { + info: "lonely --upload-series", + args: []string{"--upload-series", "fine"}, + err: `--upload-series requires --upload-tools`, +}, { + info: "--upload-series with --series", + args: []string{"--upload-tools", "--upload-series", "foo", "--series", "bar"}, + err: `--upload-series and --series can't be used together`, +}, { + info: "bad environment", + version: "1.2.3-%LTS%-amd64", + args: []string{"-e", "brokenenv"}, + err: `failed to bootstrap environment: dummy.Bootstrap is broken`, +}, { + info: "constraints", + args: []string{"--constraints", "mem=4G cpu-cores=4"}, + constraints: constraints.MustParse("mem=4G cpu-cores=4"), +}, { + info: "unsupported constraint passed through but no error", + args: []string{"--constraints", "mem=4G cpu-cores=4 cpu-power=10"}, + constraints: constraints.MustParse("mem=4G cpu-cores=4 cpu-power=10"), +}, { + info: "--upload-tools uses arch from constraint if it matches current version", + version: "1.3.3-saucy-ppc64el", + hostArch: "ppc64el", + args: []string{"--upload-tools", "--constraints", "arch=ppc64el"}, + upload: "1.3.3.1-raring-ppc64el", // from version.Current + constraints: constraints.MustParse("arch=ppc64el"), +}, { + info: "--upload-tools rejects mismatched arch", + version: "1.3.3-saucy-amd64", + hostArch: "amd64", + args: []string{"--upload-tools", "--constraints", "arch=ppc64el"}, + err: `failed to bootstrap environment: cannot build tools for "ppc64el" using a machine running on "amd64"`, +}, { + info: "--upload-tools rejects non-supported arch", + version: "1.3.3-saucy-mips64", + hostArch: "mips64", + args: []string{"--upload-tools"}, + err: `failed to bootstrap environment: environment "peckham" of type dummy does not support instances running on "mips64"`, +}, { + info: "--upload-tools always bumps build number", + version: "1.2.3.4-raring-amd64", + hostArch: "amd64", + args: []string{"--upload-tools"}, + upload: "1.2.3.5-raring-amd64", +}, { + info: "placement", + args: []string{"--to", "something"}, + placement: "something", +}, { + info: "keep broken", + args: []string{"--keep-broken"}, + keepBroken: true, +}, { + info: "additional args", + args: []string{"anything", "else"}, + err: `unrecognized args: \["anything" "else"\]`, +}, { + info: "--agent-version with --upload-tools", + args: []string{"--agent-version", "1.1.0", "--upload-tools"}, + err: `--agent-version and --upload-tools can't be used together`, +}, { + info: "--agent-version with --no-auto-upgrade", + args: []string{"--agent-version", "1.1.0", "--no-auto-upgrade"}, + err: `--agent-version and --no-auto-upgrade can't be used together`, +}, { + info: "invalid --agent-version value", + args: []string{"--agent-version", "foo"}, + err: `invalid version "foo"`, +}, { + info: "agent-version doesn't match client version major", + version: "1.3.3-saucy-ppc64el", + args: []string{"--agent-version", "2.3.0"}, + err: `requested agent version major.minor mismatch`, +}, { + info: "agent-version doesn't match client version minor", + version: "1.3.3-saucy-ppc64el", + args: []string{"--agent-version", "1.4.0"}, + err: `requested agent version major.minor mismatch`, +}} + +func (s *BootstrapSuite) TestRunEnvNameMissing(c *gc.C) { + s.PatchValue(&getEnvName, func(*BootstrapCommand) string { return "" }) + + _, err := coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{})) + + c.Check(err, gc.ErrorMatches, "the name of the environment must be specified") +} + +const provisionalEnvs = ` +environments: + devenv: + type: dummy + cloudsigma: + type: cloudsigma + vsphere: + type: vsphere +` + +func (s *BootstrapSuite) TestCheckProviderProvisional(c *gc.C) { + coretesting.WriteEnvironments(c, provisionalEnvs) + + err := checkProviderType("devenv") + c.Assert(err, jc.ErrorIsNil) + + for name, flag := range provisionalProviders { + // vsphere is disabled for gccgo. See lp:1440940. + if name == "vsphere" && runtime.Compiler == "gccgo" { + continue + } + c.Logf(" - trying %q -", name) + err := checkProviderType(name) + c.Check(err, gc.ErrorMatches, ".* provider is provisional .* set JUJU_DEV_FEATURE_FLAGS=.*") + + err = os.Setenv(osenv.JujuFeatureFlagEnvKey, flag) + c.Assert(err, jc.ErrorIsNil) + err = checkProviderType(name) + c.Check(err, jc.ErrorIsNil) + } +} + +func (s *BootstrapSuite) TestBootstrapTwice(c *gc.C) { + env := resetJujuHome(c, "devenv") + defaultSeriesVersion := version.Current + defaultSeriesVersion.Series = config.PreferredSeries(env.Config()) + // Force a dev version by having a non zero build number. + // This is because we have not uploaded any tools and auto + // upload is only enabled for dev versions. + defaultSeriesVersion.Build = 1234 + s.PatchValue(&version.Current, defaultSeriesVersion) + + _, err := coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}), "-e", "devenv") + c.Assert(err, jc.ErrorIsNil) + + _, err = coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}), "-e", "devenv") + c.Assert(err, gc.ErrorMatches, "environment is already bootstrapped") +} + +type mockBootstrapInstance struct { + instance.Instance +} + +func (*mockBootstrapInstance) Addresses() ([]network.Address, error) { + return []network.Address{{Value: "localhost"}}, nil +} + +func (s *BootstrapSuite) TestSeriesDeprecation(c *gc.C) { + ctx := s.checkSeriesArg(c, "--series") + c.Check(coretesting.Stderr(ctx), gc.Equals, + "Use of --series is obsolete. --upload-tools now expands to all supported series of the same operating system.\nBootstrap complete\n") +} + +func (s *BootstrapSuite) TestUploadSeriesDeprecation(c *gc.C) { + ctx := s.checkSeriesArg(c, "--upload-series") + c.Check(coretesting.Stderr(ctx), gc.Equals, + "Use of --upload-series is obsolete. --upload-tools now expands to all supported series of the same operating system.\nBootstrap complete\n") +} + +func (s *BootstrapSuite) checkSeriesArg(c *gc.C, argVariant string) *cmd.Context { + _bootstrap := &fakeBootstrapFuncs{} + s.PatchValue(&getBootstrapFuncs, func() BootstrapInterface { + return _bootstrap + }) + resetJujuHome(c, "devenv") + s.PatchValue(&allInstances, func(environ environs.Environ) ([]instance.Instance, error) { + return []instance.Instance{&mockBootstrapInstance{}}, nil + }) + + ctx, err := coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}), "--upload-tools", argVariant, "foo,bar") + + c.Assert(err, jc.ErrorIsNil) + return ctx +} + +// In the case where we cannot examine an environment, we want the +// error to propagate back up to the user. +func (s *BootstrapSuite) TestBootstrapPropagatesEnvErrors(c *gc.C) { + //TODO(bogdanteleaga): fix this for windows once permissions are fixed + if runtime.GOOS == "windows" { + c.Skip("bug 1403084: this is very platform specific. When/if we will support windows state machine, this will probably be rewritten.") + } + + const envName = "devenv" + env := resetJujuHome(c, envName) + defaultSeriesVersion := version.Current + defaultSeriesVersion.Series = config.PreferredSeries(env.Config()) + // Force a dev version by having a non zero build number. + // This is because we have not uploaded any tools and auto + // upload is only enabled for dev versions. + defaultSeriesVersion.Build = 1234 + s.PatchValue(&version.Current, defaultSeriesVersion) + s.PatchValue(&environType, func(string) (string, error) { return "", nil }) + + _, err := coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}), "-e", envName) + c.Assert(err, jc.ErrorIsNil) + + // Change permissions on the jenv file to simulate some kind of + // unexpected error when trying to read info from the environment + jenvFile := gitjujutesting.HomePath(".juju", "environments", envName+".jenv") + err = os.Chmod(jenvFile, os.FileMode(0200)) + c.Assert(err, jc.ErrorIsNil) + + // The second bootstrap should fail b/c of the propogated error + _, err = coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}), "-e", envName) + c.Assert(err, gc.ErrorMatches, "there was an issue examining the environment: .*") +} + +func (s *BootstrapSuite) TestBootstrapCleansUpIfEnvironPrepFails(c *gc.C) { + + cleanupRan := false + + s.PatchValue(&environType, func(string) (string, error) { return "", nil }) + s.PatchValue( + &environFromName, + func( + *cmd.Context, + string, + string, + func(environs.Environ) error, + ) (environs.Environ, func(), error) { + return nil, func() { cleanupRan = true }, fmt.Errorf("mock") + }, + ) + + ctx := coretesting.Context(c) + _, errc := cmdtesting.RunCommand(ctx, envcmd.Wrap(new(BootstrapCommand)), "-e", "peckham") + c.Check(<-errc, gc.Not(gc.IsNil)) + c.Check(cleanupRan, jc.IsTrue) +} + +// When attempting to bootstrap, check that when prepare errors out, +// the code cleans up the created jenv file, but *not* any existing +// environment that may have previously been bootstrapped. +func (s *BootstrapSuite) TestBootstrapFailToPrepareDiesGracefully(c *gc.C) { + + destroyedEnvRan := false + destroyedInfoRan := false + + // Mock functions + mockDestroyPreparedEnviron := func( + *cmd.Context, + environs.Environ, + configstore.Storage, + string, + ) { + destroyedEnvRan = true + } + + mockDestroyEnvInfo := func( + ctx *cmd.Context, + cfgName string, + store configstore.Storage, + action string, + ) { + destroyedInfoRan = true + } + + mockEnvironFromName := func( + ctx *cmd.Context, + envName string, + action string, + _ func(environs.Environ) error, + ) (environs.Environ, func(), error) { + // Always show that the environment is bootstrapped. + return environFromNameProductionFunc( + ctx, + envName, + action, + func(env environs.Environ) error { + return environs.ErrAlreadyBootstrapped + }) + } + + mockPrepare := func( + string, + environs.BootstrapContext, + configstore.Storage, + ) (environs.Environ, error) { + return nil, fmt.Errorf("mock-prepare") + } + + // Simulation: prepare should fail and we should only clean up the + // jenv file. Any existing environment should not be destroyed. + s.PatchValue(&destroyPreparedEnviron, mockDestroyPreparedEnviron) + s.PatchValue(&environType, func(string) (string, error) { return "", nil }) + s.PatchValue(&environFromName, mockEnvironFromName) + s.PatchValue(&environs.PrepareFromName, mockPrepare) + s.PatchValue(&destroyEnvInfo, mockDestroyEnvInfo) + + ctx := coretesting.Context(c) + _, errc := cmdtesting.RunCommand(ctx, envcmd.Wrap(new(BootstrapCommand)), "-e", "peckham") + c.Check(<-errc, gc.ErrorMatches, ".*mock-prepare$") + c.Check(destroyedEnvRan, jc.IsFalse) + c.Check(destroyedInfoRan, jc.IsTrue) +} + +func (s *BootstrapSuite) TestBootstrapJenvWarning(c *gc.C) { + env := resetJujuHome(c, "devenv") + defaultSeriesVersion := version.Current + defaultSeriesVersion.Series = config.PreferredSeries(env.Config()) + // Force a dev version by having a non zero build number. + // This is because we have not uploaded any tools and auto + // upload is only enabled for dev versions. + defaultSeriesVersion.Build = 1234 + s.PatchValue(&version.Current, defaultSeriesVersion) + + store, err := configstore.Default() + c.Assert(err, jc.ErrorIsNil) + ctx := coretesting.Context(c) + environs.PrepareFromName("devenv", envcmd.BootstrapContext(ctx), store) + + logger := "jenv.warning.test" + var testWriter loggo.TestWriter + loggo.RegisterWriter(logger, &testWriter, loggo.WARNING) + defer loggo.RemoveWriter(logger) + + _, errc := cmdtesting.RunCommand(ctx, envcmd.Wrap(new(BootstrapCommand)), "-e", "devenv") + c.Assert(<-errc, gc.IsNil) + c.Assert(testWriter.Log(), jc.LogMatches, []string{"ignoring environments.yaml: using bootstrap config in .*"}) +} + +func (s *BootstrapSuite) TestInvalidLocalSource(c *gc.C) { + s.PatchValue(&version.Current.Number, version.MustParse("1.2.0")) + env := resetJujuHome(c, "devenv") + + // Bootstrap the environment with an invalid source. + // The command returns with an error. + _, err := coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}), "--metadata-source", c.MkDir()) + c.Check(err, gc.ErrorMatches, `failed to bootstrap environment: Juju cannot bootstrap because no tools are available for your environment(.|\n)*`) + + // Now check that there are no tools available. + _, err = envtools.FindTools( + env, version.Current.Major, version.Current.Minor, "released", coretools.Filter{}) + c.Assert(err, gc.FitsTypeOf, errors.NotFoundf("")) +} + +// createImageMetadata creates some image metadata in a local directory. +func createImageMetadata(c *gc.C) (string, []*imagemetadata.ImageMetadata) { + // Generate some image metadata. + im := []*imagemetadata.ImageMetadata{ + { + Id: "1234", + Arch: "amd64", + Version: "13.04", + RegionName: "region", + Endpoint: "endpoint", + }, + } + cloudSpec := &simplestreams.CloudSpec{ + Region: "region", + Endpoint: "endpoint", + } + sourceDir := c.MkDir() + sourceStor, err := filestorage.NewFileStorageWriter(sourceDir) + c.Assert(err, jc.ErrorIsNil) + err = imagemetadata.MergeAndWriteMetadata("raring", im, cloudSpec, sourceStor) + c.Assert(err, jc.ErrorIsNil) + return sourceDir, im +} + +func (s *BootstrapSuite) TestBootstrapCalledWithMetadataDir(c *gc.C) { + sourceDir, _ := createImageMetadata(c) + resetJujuHome(c, "devenv") + + _bootstrap := &fakeBootstrapFuncs{} + s.PatchValue(&getBootstrapFuncs, func() BootstrapInterface { + return _bootstrap + }) + + coretesting.RunCommand( + c, envcmd.Wrap(&BootstrapCommand{}), + "--metadata-source", sourceDir, "--constraints", "mem=4G", + ) + c.Assert(_bootstrap.args.MetadataDir, gc.Equals, sourceDir) +} + +func (s *BootstrapSuite) checkBootstrapWithVersion(c *gc.C, vers, expect string) { + resetJujuHome(c, "devenv") + + _bootstrap := &fakeBootstrapFuncs{} + s.PatchValue(&getBootstrapFuncs, func() BootstrapInterface { + return _bootstrap + }) + + currentVersion := version.Current + currentVersion.Major = 2 + currentVersion.Minor = 3 + s.PatchValue(&version.Current, currentVersion) + coretesting.RunCommand( + c, envcmd.Wrap(&BootstrapCommand{}), + "--agent-version", vers, + ) + c.Assert(_bootstrap.args.AgentVersion, gc.NotNil) + c.Assert(*_bootstrap.args.AgentVersion, gc.Equals, version.MustParse(expect)) +} + +func (s *BootstrapSuite) TestBootstrapWithVersionNumber(c *gc.C) { + s.checkBootstrapWithVersion(c, "2.3.4", "2.3.4") +} + +func (s *BootstrapSuite) TestBootstrapWithBinaryVersionNumber(c *gc.C) { + s.checkBootstrapWithVersion(c, "2.3.4-trusty-ppc64", "2.3.4") +} + +func (s *BootstrapSuite) TestBootstrapWithNoAutoUpgrade(c *gc.C) { + resetJujuHome(c, "devenv") + + _bootstrap := &fakeBootstrapFuncs{} + s.PatchValue(&getBootstrapFuncs, func() BootstrapInterface { + return _bootstrap + }) + + currentVersion := version.Current + currentVersion.Major = 2 + currentVersion.Minor = 22 + currentVersion.Patch = 46 + currentVersion.Series = "trusty" + currentVersion.Arch = "amd64" + s.PatchValue(&version.Current, currentVersion) + coretesting.RunCommand( + c, envcmd.Wrap(&BootstrapCommand{}), + "--no-auto-upgrade", + ) + c.Assert(*_bootstrap.args.AgentVersion, gc.Equals, version.MustParse("2.22.46")) +} + +func (s *BootstrapSuite) TestAutoSyncLocalSource(c *gc.C) { + sourceDir := createToolsSource(c, vAll) + s.PatchValue(&version.Current.Number, version.MustParse("1.2.0")) + env := resetJujuHome(c, "peckham") + + // Bootstrap the environment with the valid source. + // The bootstrapping has to show no error, because the tools + // are automatically synchronized. + _, err := coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}), "--metadata-source", sourceDir) + c.Assert(err, jc.ErrorIsNil) + + // Now check the available tools which are the 1.2.0 envtools. + checkTools(c, env, v120All) +} + +func (s *BootstrapSuite) setupAutoUploadTest(c *gc.C, vers, series string) environs.Environ { + s.PatchValue(&envtools.BundleTools, toolstesting.GetMockBundleTools(c)) + sourceDir := createToolsSource(c, vAll) + s.PatchValue(&envtools.DefaultBaseURL, sourceDir) + + // Change the tools location to be the test location and also + // the version and ensure their later restoring. + // Set the current version to be something for which there are no tools + // so we can test that an upload is forced. + s.PatchValue(&version.Current, version.MustParseBinary(vers+"-"+series+"-"+arch.HostArch())) + + // Create home with dummy provider and remove all + // of its envtools. + return resetJujuHome(c, "devenv") +} + +func (s *BootstrapSuite) TestAutoUploadAfterFailedSync(c *gc.C) { + s.PatchValue(&version.Current.Series, config.LatestLtsSeries()) + s.setupAutoUploadTest(c, "1.7.3", "quantal") + // Run command and check for that upload has been run for tools matching + // the current juju version. + opc, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), envcmd.Wrap(new(BootstrapCommand)), "-e", "devenv") + c.Assert(<-errc, gc.IsNil) + c.Check((<-opc).(dummy.OpBootstrap).Env, gc.Equals, "devenv") + icfg := (<-opc).(dummy.OpFinalizeBootstrap).InstanceConfig + c.Assert(icfg, gc.NotNil) + c.Assert(icfg.Tools.Version.String(), gc.Equals, "1.7.3.1-raring-"+arch.HostArch()) +} + +func (s *BootstrapSuite) TestAutoUploadOnlyForDev(c *gc.C) { + s.setupAutoUploadTest(c, "1.8.3", "precise") + _, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), envcmd.Wrap(new(BootstrapCommand))) + err := <-errc + c.Assert(err, gc.ErrorMatches, + "failed to bootstrap environment: Juju cannot bootstrap because no tools are available for your environment(.|\n)*") +} + +func (s *BootstrapSuite) TestMissingToolsError(c *gc.C) { + s.setupAutoUploadTest(c, "1.8.3", "precise") + + _, err := coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{})) + c.Assert(err, gc.ErrorMatches, + "failed to bootstrap environment: Juju cannot bootstrap because no tools are available for your environment(.|\n)*") +} + +func (s *BootstrapSuite) TestMissingToolsUploadFailedError(c *gc.C) { + + buildToolsTarballAlwaysFails := func(forceVersion *version.Number, stream string) (*sync.BuiltTools, error) { + return nil, fmt.Errorf("an error") + } + + s.setupAutoUploadTest(c, "1.7.3", "precise") + s.PatchValue(&sync.BuildToolsTarball, buildToolsTarballAlwaysFails) + + ctx, err := coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}), "-e", "devenv") + + c.Check(coretesting.Stderr(ctx), gc.Equals, fmt.Sprintf(` +Bootstrapping environment "devenv" +Starting new instance for initial state server +Building tools to upload (1.7.3.1-raring-%s) +`[1:], arch.HostArch())) + c.Check(err, gc.ErrorMatches, "failed to bootstrap environment: cannot upload bootstrap tools: an error") +} + +func (s *BootstrapSuite) TestBootstrapDestroy(c *gc.C) { + resetJujuHome(c, "devenv") + devVersion := version.Current + // Force a dev version by having a non zero build number. + // This is because we have not uploaded any tools and auto + // upload is only enabled for dev versions. + devVersion.Build = 1234 + s.PatchValue(&version.Current, devVersion) + opc, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), envcmd.Wrap(new(BootstrapCommand)), "-e", "brokenenv") + err := <-errc + c.Assert(err, gc.ErrorMatches, "failed to bootstrap environment: dummy.Bootstrap is broken") + var opDestroy *dummy.OpDestroy + for opDestroy == nil { + select { + case op := <-opc: + switch op := op.(type) { + case dummy.OpDestroy: + opDestroy = &op + } + default: + c.Error("expected call to env.Destroy") + return + } + } + c.Assert(opDestroy.Error, gc.ErrorMatches, "dummy.Destroy is broken") +} + +func (s *BootstrapSuite) TestBootstrapKeepBroken(c *gc.C) { + resetJujuHome(c, "devenv") + devVersion := version.Current + // Force a dev version by having a non zero build number. + // This is because we have not uploaded any tools and auto + // upload is only enabled for dev versions. + devVersion.Build = 1234 + s.PatchValue(&version.Current, devVersion) + opc, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), envcmd.Wrap(new(BootstrapCommand)), "-e", "brokenenv", "--keep-broken") + err := <-errc + c.Assert(err, gc.ErrorMatches, "failed to bootstrap environment: dummy.Bootstrap is broken") + done := false + for !done { + select { + case op, ok := <-opc: + if !ok { + done = true + break + } + switch op.(type) { + case dummy.OpDestroy: + c.Error("unexpected call to env.Destroy") + break + } + default: + break + } + } +} + +// createToolsSource writes the mock tools and metadata into a temporary +// directory and returns it. +func createToolsSource(c *gc.C, versions []version.Binary) string { + versionStrings := make([]string, len(versions)) + for i, vers := range versions { + versionStrings[i] = vers.String() + } + source := c.MkDir() + toolstesting.MakeTools(c, source, "released", versionStrings) + return source +} + +// resetJujuHome restores an new, clean Juju home environment without tools. +func resetJujuHome(c *gc.C, envName string) environs.Environ { + jenvDir := gitjujutesting.HomePath(".juju", "environments") + err := os.RemoveAll(jenvDir) + c.Assert(err, jc.ErrorIsNil) + coretesting.WriteEnvironments(c, envConfig) + dummy.Reset() + store, err := configstore.Default() + c.Assert(err, jc.ErrorIsNil) + env, err := environs.PrepareFromName(envName, envcmd.BootstrapContext(cmdtesting.NullContext(c)), store) + c.Assert(err, jc.ErrorIsNil) + return env +} + +// checkTools check if the environment contains the passed envtools. +func checkTools(c *gc.C, env environs.Environ, expected []version.Binary) { + list, err := envtools.FindTools( + env, version.Current.Major, version.Current.Minor, "released", coretools.Filter{}) + c.Check(err, jc.ErrorIsNil) + c.Logf("found: " + list.String()) + urls := list.URLs() + c.Check(urls, gc.HasLen, len(expected)) +} + +var ( + v100d64 = version.MustParseBinary("1.0.0-raring-amd64") + v100p64 = version.MustParseBinary("1.0.0-precise-amd64") + v100q32 = version.MustParseBinary("1.0.0-quantal-i386") + v100q64 = version.MustParseBinary("1.0.0-quantal-amd64") + v120d64 = version.MustParseBinary("1.2.0-raring-amd64") + v120p64 = version.MustParseBinary("1.2.0-precise-amd64") + v120q32 = version.MustParseBinary("1.2.0-quantal-i386") + v120q64 = version.MustParseBinary("1.2.0-quantal-amd64") + v120t32 = version.MustParseBinary("1.2.0-trusty-i386") + v120t64 = version.MustParseBinary("1.2.0-trusty-amd64") + v190p32 = version.MustParseBinary("1.9.0-precise-i386") + v190q64 = version.MustParseBinary("1.9.0-quantal-amd64") + v200p64 = version.MustParseBinary("2.0.0-precise-amd64") + v100All = []version.Binary{ + v100d64, v100p64, v100q64, v100q32, + } + v120All = []version.Binary{ + v120d64, v120p64, v120q64, v120q32, v120t32, v120t64, + } + v190All = []version.Binary{ + v190p32, v190q64, + } + v200All = []version.Binary{ + v200p64, + } + vAll = joinBinaryVersions(v100All, v120All, v190All, v200All) +) + +func joinBinaryVersions(versions ...[]version.Binary) []version.Binary { + var all []version.Binary + for _, versions := range versions { + all = append(all, versions...) + } + return all +} + +// TODO(menn0): This fake BootstrapInterface implementation is +// currently quite minimal but could be easily extended to cover more +// test scenarios. This could help improve some of the tests in this +// file which execute large amounts of external functionality. +type fakeBootstrapFuncs struct { + args bootstrap.BootstrapParams +} + +func (fake *fakeBootstrapFuncs) EnsureNotBootstrapped(env environs.Environ) error { + return nil +} + +func (fake *fakeBootstrapFuncs) Bootstrap(ctx environs.BootstrapContext, env environs.Environ, args bootstrap.BootstrapParams) error { + fake.args = args + return nil +} === added file 'src/github.com/juju/juju/cmd/juju/commands/cmd_test.go' --- src/github.com/juju/juju/cmd/juju/commands/cmd_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/cmd_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,285 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "os" + + "github.com/juju/cmd" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/service" + "github.com/juju/juju/cmd/juju/status" + cmdtesting "github.com/juju/juju/cmd/testing" + "github.com/juju/juju/juju/osenv" + "github.com/juju/juju/juju/testing" + coretesting "github.com/juju/juju/testing" +) + +func badrun(c *gc.C, exit int, args ...string) string { + args = append([]string{"juju"}, args...) + return cmdtesting.BadRun(c, exit, args...) +} + +type CmdSuite struct { + testing.JujuConnSuite +} + +var _ = gc.Suite(&CmdSuite{}) + +const envConfig = ` +default: + peckham +environments: + peckham: + type: dummy + state-server: false + admin-secret: arble + authorized-keys: i-am-a-key + default-series: raring + walthamstow: + type: dummy + state-server: false + authorized-keys: i-am-a-key + brokenenv: + type: dummy + broken: Bootstrap Destroy + state-server: false + authorized-keys: i-am-a-key + agent-stream: proposed + devenv: + type: dummy + state-server: false + admin-secret: arble + authorized-keys: i-am-a-key + default-series: raring + agent-stream: proposed +` + +func (s *CmdSuite) SetUpTest(c *gc.C) { + s.JujuConnSuite.SetUpTest(c) + coretesting.WriteEnvironments(c, envConfig, "peckham", "walthamstow", "brokenenv") +} + +func (s *CmdSuite) TearDownTest(c *gc.C) { + s.JujuConnSuite.TearDownTest(c) +} + +// testInit checks that a command initialises correctly +// with the given set of arguments. +func testInit(c *gc.C, com cmd.Command, args []string, errPat string) { + err := coretesting.InitCommand(com, args) + if errPat != "" { + c.Assert(err, gc.ErrorMatches, errPat) + } else { + c.Assert(err, jc.ErrorIsNil) + } +} + +type HasConnectionName interface { + ConnectionName() string +} + +// assertEnvName asserts that the Command is using +// the given environment name. +// Since every command has a different type, +// we use reflection to look at the value of the +// Conn field in the value. +func assertEnvName(c *gc.C, com cmd.Command, name string) { + i, ok := com.(HasConnectionName) + c.Assert(ok, jc.IsTrue) + c.Assert(i.ConnectionName(), gc.Equals, name) +} + +// All members of EnvironmentInitTests are tested for the -environment and -e +// flags, and that extra arguments will cause parsing to fail. +var EnvironmentInitTests = []func() (envcmd.EnvironCommand, []string){ + func() (envcmd.EnvironCommand, []string) { return new(BootstrapCommand), nil }, + func() (envcmd.EnvironCommand, []string) { + return new(DeployCommand), []string{"charm-name", "service-name"} + }, + func() (envcmd.EnvironCommand, []string) { return new(status.StatusCommand), nil }, +} + +// TestEnvironmentInit tests that all commands which accept +// the --environment variable initialise their +// environment name correctly. +func (*CmdSuite) TestEnvironmentInit(c *gc.C) { + for i, cmdFunc := range EnvironmentInitTests { + c.Logf("test %d", i) + com, args := cmdFunc() + testInit(c, envcmd.Wrap(com), args, "") + assertEnvName(c, com, "peckham") + + com, args = cmdFunc() + testInit(c, envcmd.Wrap(com), append(args, "-e", "walthamstow"), "") + assertEnvName(c, com, "walthamstow") + + com, args = cmdFunc() + testInit(c, envcmd.Wrap(com), append(args, "--environment", "walthamstow"), "") + assertEnvName(c, com, "walthamstow") + + // JUJU_ENV is the final place the environment can be overriden + com, args = cmdFunc() + oldenv := os.Getenv(osenv.JujuEnvEnvKey) + os.Setenv(osenv.JujuEnvEnvKey, "walthamstow") + testInit(c, envcmd.Wrap(com), args, "") + os.Setenv(osenv.JujuEnvEnvKey, oldenv) + assertEnvName(c, com, "walthamstow") + } +} + +var deployTests = []struct { + args []string + com *DeployCommand +}{ + { + []string{"charm-name"}, + &DeployCommand{}, + }, { + []string{"charm-name", "service-name"}, + &DeployCommand{ServiceName: "service-name"}, + }, { + []string{"--repository", "/path/to/another-repo", "charm-name"}, + &DeployCommand{RepoPath: "/path/to/another-repo"}, + }, { + []string{"--upgrade", "charm-name"}, + &DeployCommand{BumpRevision: true}, + }, { + []string{"-u", "charm-name"}, + &DeployCommand{BumpRevision: true}, + }, { + []string{"--num-units", "33", "charm-name"}, + &DeployCommand{UnitCommandBase: service.UnitCommandBase{NumUnits: 33}}, + }, { + []string{"-n", "104", "charm-name"}, + &DeployCommand{UnitCommandBase: service.UnitCommandBase{NumUnits: 104}}, + }, +} + +func initExpectations(com *DeployCommand) { + if com.CharmName == "" { + com.CharmName = "charm-name" + } + if com.NumUnits == 0 { + com.NumUnits = 1 + } + if com.RepoPath == "" { + com.RepoPath = "/path/to/repo" + } + com.SetEnvName("peckham") +} + +func initDeployCommand(args ...string) (*DeployCommand, error) { + com := &DeployCommand{} + return com, coretesting.InitCommand(envcmd.Wrap(com), args) +} + +func (*CmdSuite) TestDeployCommandInit(c *gc.C) { + defer os.Setenv(osenv.JujuRepositoryEnvKey, os.Getenv(osenv.JujuRepositoryEnvKey)) + os.Setenv(osenv.JujuRepositoryEnvKey, "/path/to/repo") + + for _, t := range deployTests { + initExpectations(t.com) + com, err := initDeployCommand(t.args...) + c.Assert(err, jc.ErrorIsNil) + c.Assert(com, gc.DeepEquals, t.com) + } + + // test relative --config path + ctx := coretesting.Context(c) + expected := []byte("test: data") + path := ctx.AbsPath("testconfig.yaml") + file, err := os.Create(path) + c.Assert(err, jc.ErrorIsNil) + _, err = file.Write(expected) + c.Assert(err, jc.ErrorIsNil) + file.Close() + + com, err := initDeployCommand("--config", "testconfig.yaml", "charm-name") + c.Assert(err, jc.ErrorIsNil) + actual, err := com.Config.Read(ctx) + c.Assert(err, jc.ErrorIsNil) + c.Assert(expected, gc.DeepEquals, actual) + + // missing args + _, err = initDeployCommand() + c.Assert(err, gc.ErrorMatches, "no charm specified") + + // bad unit count + _, err = initDeployCommand("charm-name", "--num-units", "0") + c.Assert(err, gc.ErrorMatches, "--num-units must be a positive integer") + _, err = initDeployCommand("charm-name", "-n", "0") + c.Assert(err, gc.ErrorMatches, "--num-units must be a positive integer") + + // environment tested elsewhere +} + +func initExposeCommand(args ...string) (*ExposeCommand, error) { + com := &ExposeCommand{} + return com, coretesting.InitCommand(com, args) +} + +func (*CmdSuite) TestExposeCommandInit(c *gc.C) { + // missing args + _, err := initExposeCommand() + c.Assert(err, gc.ErrorMatches, "no service name specified") + + // environment tested elsewhere +} + +func initUnexposeCommand(args ...string) (*UnexposeCommand, error) { + com := &UnexposeCommand{} + return com, coretesting.InitCommand(com, args) +} + +func (*CmdSuite) TestUnexposeCommandInit(c *gc.C) { + // missing args + _, err := initUnexposeCommand() + c.Assert(err, gc.ErrorMatches, "no service name specified") + + // environment tested elsewhere +} + +func initSSHCommand(args ...string) (*SSHCommand, error) { + com := &SSHCommand{} + return com, coretesting.InitCommand(com, args) +} + +func (*CmdSuite) TestSSHCommandInit(c *gc.C) { + // missing args + _, err := initSSHCommand() + c.Assert(err, gc.ErrorMatches, "no target name specified") +} + +func initSCPCommand(args ...string) (*SCPCommand, error) { + com := &SCPCommand{} + return com, coretesting.InitCommand(com, args) +} + +func (*CmdSuite) TestSCPCommandInit(c *gc.C) { + // missing args + _, err := initSCPCommand() + c.Assert(err, gc.ErrorMatches, "at least two arguments required") + + // not enough args + _, err = initSCPCommand("mysql/0:foo") + c.Assert(err, gc.ErrorMatches, "at least two arguments required") +} + +func initRemoveUnitCommand(args ...string) (*RemoveUnitCommand, error) { + com := &RemoveUnitCommand{} + return com, coretesting.InitCommand(com, args) +} + +func (*CmdSuite) TestRemoveUnitCommandInit(c *gc.C) { + // missing args + _, err := initRemoveUnitCommand() + c.Assert(err, gc.ErrorMatches, "no units specified") + // not a unit + _, err = initRemoveUnitCommand("seven/nine") + c.Assert(err, gc.ErrorMatches, `invalid unit name "seven/nine"`) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/cmdblockhelper_test.go' --- src/github.com/juju/juju/cmd/juju/commands/cmdblockhelper_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/cmdblockhelper_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,62 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "strings" + + "github.com/juju/cmd" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/api" + "github.com/juju/juju/api/block" + cmdblock "github.com/juju/juju/cmd/juju/block" +) + +type CmdBlockHelper struct { + blockClient *block.Client +} + +// NewCmdBlockHelper creates a block switch used in testing +// to manage desired juju blocks. +func NewCmdBlockHelper(st api.Connection) CmdBlockHelper { + return CmdBlockHelper{ + blockClient: block.NewClient(st), + } +} + +// on switches on desired block and +// asserts that no errors were encountered. +func (s *CmdBlockHelper) on(c *gc.C, blockType, msg string) { + c.Assert(s.blockClient.SwitchBlockOn(cmdblock.TypeFromOperation(blockType), msg), gc.IsNil) +} + +// BlockAllChanges switches changes block on. +// This prevents all changes to juju environment. +func (s *CmdBlockHelper) BlockAllChanges(c *gc.C, msg string) { + s.on(c, "all-changes", msg) +} + +// BlockRemoveObject switches remove block on. +// This prevents any object/entity removal on juju environment +func (s *CmdBlockHelper) BlockRemoveObject(c *gc.C, msg string) { + s.on(c, "remove-object", msg) +} + +// BlockDestroyEnvironment switches destroy block on. +// This prevents juju environment destruction. +func (s *CmdBlockHelper) BlockDestroyEnvironment(c *gc.C, msg string) { + s.on(c, "destroy-environment", msg) +} + +func (s *CmdBlockHelper) Close() { + s.blockClient.Close() +} + +func (s *CmdBlockHelper) AssertBlocked(c *gc.C, err error, msg string) { + c.Assert(err, gc.ErrorMatches, cmd.ErrSilent.Error()) + // msg is logged + stripped := strings.Replace(c.GetTestLog(), "\n", "", -1) + c.Check(stripped, gc.Matches, msg) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/commands.go' --- src/github.com/juju/juju/cmd/juju/commands/commands.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/commands.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,33 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "github.com/juju/cmd" + + "github.com/juju/juju/cmd/envcmd" +) + +// TODO(ericsnow) Replace all this with a better registry mechanism, +// likely over in the cmd repo. + +var ( + registeredCommands []func() cmd.Command + registeredEnvCommands []func() envcmd.EnvironCommand +) + +// RegisterCommand adds the provided func to the set of those that will +// be called when the juju command runs. Each returned command will be +// registered with the "juju" supercommand. +func RegisterCommand(newCommand func() cmd.Command) { + registeredCommands = append(registeredCommands, newCommand) +} + +// RegisterCommand adds the provided func to the set of those that will +// be called when the juju command runs. Each returned command will be +// wrapped in envCmdWrapper, which is what gets registered with the +// "juju" supercommand. +func RegisterEnvCommand(newCommand func() envcmd.EnvironCommand) { + registeredEnvCommands = append(registeredEnvCommands, newCommand) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/commands_test.go' --- src/github.com/juju/juju/cmd/juju/commands/commands_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/commands_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,121 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/testing" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/envcmd" +) + +var _ = gc.Suite(&commandsSuite{}) + +type commandsSuite struct { + stub *testing.Stub + command *stubCommand +} + +func (s *commandsSuite) SetUpTest(c *gc.C) { + s.stub = &testing.Stub{} + s.command = &stubCommand{stub: s.stub} +} + +func (s *commandsSuite) TearDownTest(c *gc.C) { + registeredCommands = nil + registeredEnvCommands = nil +} + +func (s *commandsSuite) TestRegisterCommand(c *gc.C) { + RegisterCommand(func() cmd.Command { + return s.command + }) + + // We can't compare functions directly, so... + c.Check(registeredEnvCommands, gc.HasLen, 0) + c.Assert(registeredCommands, gc.HasLen, 1) + command := registeredCommands[0]() + c.Check(command, gc.Equals, s.command) +} + +func (s *commandsSuite) TestRegisterEnvCommand(c *gc.C) { + RegisterEnvCommand(func() envcmd.EnvironCommand { + return s.command + }) + + // We can't compare functions directly, so... + c.Assert(registeredCommands, gc.HasLen, 0) + c.Assert(registeredEnvCommands, gc.HasLen, 1) + command := registeredEnvCommands[0]() + c.Check(command, gc.Equals, s.command) +} + +type stubCommand struct { + cmd.CommandBase + stub *testing.Stub + info *cmd.Info +} + +func (c *stubCommand) Info() *cmd.Info { + c.stub.AddCall("Info") + c.stub.NextErr() // pop one off + + if c.info == nil { + return &cmd.Info{ + Name: "some-command", + } + } + return c.info +} + +func (c *stubCommand) Run(ctx *cmd.Context) error { + c.stub.AddCall("Run", ctx) + if err := c.stub.NextErr(); err != nil { + return errors.Trace(err) + } + + return nil +} + +func (c *stubCommand) SetEnvName(name string) { + c.stub.AddCall("SetEnvName", name) + c.stub.NextErr() // pop one off + + // Do nothing. +} + +type stubRegistry struct { + stub *testing.Stub + + names []string +} + +func (r *stubRegistry) Register(subcmd cmd.Command) { + r.stub.AddCall("Register", subcmd) + r.stub.NextErr() // pop one off + + r.names = append(r.names, subcmd.Info().Name) + for _, name := range subcmd.Info().Aliases { + r.names = append(r.names, name) + } +} + +func (r *stubRegistry) RegisterSuperAlias(name, super, forName string, check cmd.DeprecationCheck) { + r.stub.AddCall("RegisterSuperAlias", name, super, forName) + r.stub.NextErr() // pop one off + + r.names = append(r.names, name) +} + +func (r *stubRegistry) RegisterDeprecated(subcmd cmd.Command, check cmd.DeprecationCheck) { + r.stub.AddCall("RegisterDeprecated", subcmd, check) + r.stub.NextErr() // pop one off + + r.names = append(r.names, subcmd.Info().Name) + for _, name := range subcmd.Info().Aliases { + r.names = append(r.names, name) + } +} === added file 'src/github.com/juju/juju/cmd/juju/commands/common.go' --- src/github.com/juju/juju/cmd/juju/commands/common.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/common.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,260 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "fmt" + "net/http" + "path" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/loggo" + "github.com/juju/persistent-cookiejar" + "github.com/juju/utils" + "golang.org/x/net/publicsuffix" + "gopkg.in/juju/charm.v5" + "gopkg.in/juju/charm.v5/charmrepo" + "gopkg.in/juju/charmstore.v4/csclient" + "gopkg.in/macaroon-bakery.v0/httpbakery" + "gopkg.in/macaroon.v1" + + "github.com/juju/juju/api" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/environs" + "github.com/juju/juju/environs/config" + "github.com/juju/juju/environs/configstore" +) + +// destroyPreparedEnviron destroys the environment and logs an error +// if it fails. +var destroyPreparedEnviron = destroyPreparedEnvironProductionFunc + +var logger = loggo.GetLogger("juju.cmd.juju") + +func destroyPreparedEnvironProductionFunc( + ctx *cmd.Context, + env environs.Environ, + store configstore.Storage, + action string, +) { + ctx.Infof("%s failed, destroying environment", action) + if err := environs.Destroy(env, store); err != nil { + logger.Errorf("the environment could not be destroyed: %v", err) + } +} + +var destroyEnvInfo = destroyEnvInfoProductionFunc + +func destroyEnvInfoProductionFunc( + ctx *cmd.Context, + cfgName string, + store configstore.Storage, + action string, +) { + ctx.Infof("%s failed, cleaning up the environment.", action) + if err := environs.DestroyInfo(cfgName, store); err != nil { + logger.Errorf("the environment jenv file could not be cleaned up: %v", err) + } +} + +// environFromName loads an existing environment or prepares a new +// one. If there are no errors, it returns the environ and a closure to +// clean up in case we need to further up the stack. If an error has +// occurred, the environment and cleanup function will be nil, and the +// error will be filled in. +var environFromName = environFromNameProductionFunc + +func environFromNameProductionFunc( + ctx *cmd.Context, + envName string, + action string, + ensureNotBootstrapped func(environs.Environ) error, +) (env environs.Environ, cleanup func(), err error) { + + store, err := configstore.Default() + if err != nil { + return nil, nil, err + } + + envExisted := false + if environInfo, err := store.ReadInfo(envName); err == nil { + envExisted = true + logger.Warningf( + "ignoring environments.yaml: using bootstrap config in %s", + environInfo.Location(), + ) + } else if !errors.IsNotFound(err) { + return nil, nil, err + } + + cleanup = func() { + // Distinguish b/t removing the jenv file or tearing down the + // environment. We want to remove the jenv file if preparation + // was not successful. We want to tear down the environment + // only in the case where the environment didn't already + // exist. + if env == nil { + logger.Debugf("Destroying environment info.") + destroyEnvInfo(ctx, envName, store, action) + } else if !envExisted && ensureNotBootstrapped(env) != environs.ErrAlreadyBootstrapped { + logger.Debugf("Destroying environment.") + destroyPreparedEnviron(ctx, env, store, action) + } + } + + if env, err = environs.PrepareFromName(envName, envcmd.BootstrapContext(ctx), store); err != nil { + return nil, cleanup, err + } + + return env, cleanup, err +} + +// resolveCharmURL resolves the given charm URL string +// by looking it up in the appropriate charm repository. +// If it is a charm store charm URL, the given csParams will +// be used to access the charm store repository. +// If it is a local charm URL, the local charm repository at +// the given repoPath will be used. The given configuration +// will be used to add any necessary attributes to the repo +// and to resolve the default series if possible. +// +// resolveCharmURL also returns the charm repository holding +// the charm. +func resolveCharmURL(curlStr string, csParams charmrepo.NewCharmStoreParams, repoPath string, conf *config.Config) (*charm.URL, charmrepo.Interface, error) { + ref, err := charm.ParseReference(curlStr) + if err != nil { + return nil, nil, errors.Trace(err) + } + repo, err := charmrepo.InferRepository(ref, csParams, repoPath) + if err != nil { + return nil, nil, errors.Trace(err) + } + repo = config.SpecializeCharmRepo(repo, conf) + if ref.Series == "" { + if defaultSeries, ok := conf.DefaultSeries(); ok { + ref.Series = defaultSeries + } + } + if ref.Schema == "local" && ref.Series == "" { + possibleURL := *ref + possibleURL.Series = "trusty" + logger.Errorf("The series is not specified in the environment (default-series) or with the charm. Did you mean:\n\t%s", &possibleURL) + return nil, nil, errors.Errorf("cannot resolve series for charm: %q", ref) + } + if ref.Series != "" && ref.Revision != -1 { + // The URL is already fully resolved; do not + // bother with an unnecessary round-trip to the + // charm store. + curl, err := ref.URL("") + if err != nil { + panic(err) + } + return curl, repo, nil + } + curl, err := repo.Resolve(ref) + if err != nil { + return nil, nil, errors.Trace(err) + } + return curl, repo, nil +} + +// addCharmViaAPI calls the appropriate client API calls to add the +// given charm URL to state. For non-public charm URLs, this function also +// handles the macaroon authorization process using the given csClient. +// The resulting charm URL of the added charm is displayed on stdout. +func addCharmViaAPI(client *api.Client, ctx *cmd.Context, curl *charm.URL, repo charmrepo.Interface, csclient *csClient) (*charm.URL, error) { + switch curl.Schema { + case "local": + ch, err := repo.Get(curl) + if err != nil { + return nil, err + } + stateCurl, err := client.AddLocalCharm(curl, ch) + if err != nil { + return nil, err + } + curl = stateCurl + case "cs": + if err := client.AddCharm(curl); err != nil { + if !params.IsCodeUnauthorized(err) { + return nil, errors.Mask(err) + } + m, err := csclient.authorize(curl) + if err != nil { + return nil, errors.Mask(err) + } + if err := client.AddCharmWithAuthorization(curl, m); err != nil { + return nil, errors.Mask(err) + } + } + default: + return nil, fmt.Errorf("unsupported charm URL schema: %q", curl.Schema) + } + ctx.Infof("Added charm %q to the environment.", curl) + return curl, nil +} + +// csClient gives access to the charm store server and provides parameters +// for connecting to the charm store. +type csClient struct { + jar *cookiejar.Jar + params charmrepo.NewCharmStoreParams +} + +// newCharmStoreClient is called to obtain a charm store client +// including the parameters for connecting to the charm store, and +// helpers to save the local authorization cookies and to authorize +// non-public charm deployments. It is defined as a variable so it can +// be changed for testing purposes. +var newCharmStoreClient = func() (*csClient, error) { + jar, client, err := newHTTPClient() + if err != nil { + return nil, errors.Mask(err) + } + return &csClient{ + jar: jar, + params: charmrepo.NewCharmStoreParams{ + HTTPClient: client, + VisitWebPage: httpbakery.OpenWebBrowser, + }, + }, nil +} + +func newHTTPClient() (*cookiejar.Jar, *http.Client, error) { + cookieFile := path.Join(utils.Home(), ".go-cookies") + jar, err := cookiejar.New(&cookiejar.Options{ + PublicSuffixList: publicsuffix.List, + }) + if err != nil { + panic(err) + } + if err := jar.Load(cookieFile); err != nil { + return nil, nil, err + } + client := httpbakery.NewHTTPClient() + client.Jar = jar + return jar, client, nil +} + +// authorize acquires and return the charm store delegatable macaroon to be +// used to add the charm corresponding to the given URL. +// The macaroon is properly attenuated so that it can only be used to deploy +// the given charm URL. +func (c *csClient) authorize(curl *charm.URL) (*macaroon.Macaroon, error) { + client := csclient.New(csclient.Params{ + URL: c.params.URL, + HTTPClient: c.params.HTTPClient, + VisitWebPage: c.params.VisitWebPage, + }) + var m *macaroon.Macaroon + if err := client.Get("/delegatable-macaroon", &m); err != nil { + return nil, errors.Trace(err) + } + if err := m.AddFirstPartyCaveat("is-entity " + curl.String()); err != nil { + return nil, errors.Trace(err) + } + return m, nil +} === added file 'src/github.com/juju/juju/cmd/juju/commands/debughooks.go' --- src/github.com/juju/juju/cmd/juju/commands/debughooks.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/debughooks.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,114 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "encoding/base64" + "fmt" + "sort" + + "github.com/juju/cmd" + "github.com/juju/names" + "gopkg.in/juju/charm.v5/hooks" + + unitdebug "github.com/juju/juju/worker/uniter/runner/debug" +) + +// DebugHooksCommand is responsible for launching a ssh shell on a given unit or machine. +type DebugHooksCommand struct { + SSHCommand + hooks []string +} + +const debugHooksDoc = ` +Interactively debug a hook remotely on a service unit. +` + +func (c *DebugHooksCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "debug-hooks", + Args: " [hook names]", + Purpose: "launch a tmux session to debug a hook", + Doc: debugHooksDoc, + } +} + +func (c *DebugHooksCommand) Init(args []string) error { + if len(args) < 1 { + return fmt.Errorf("no unit name specified") + } + c.Target = args[0] + if !names.IsValidUnit(c.Target) { + return fmt.Errorf("%q is not a valid unit name", c.Target) + } + + // If any of the hooks is "*", then debug all hooks. + c.hooks = append([]string{}, args[1:]...) + for _, h := range c.hooks { + if h == "*" { + c.hooks = nil + break + } + } + return nil +} + +func (c *DebugHooksCommand) validateHooks() error { + if len(c.hooks) == 0 { + return nil + } + service, err := names.UnitService(c.Target) + if err != nil { + return err + } + relations, err := c.apiClient.ServiceCharmRelations(service) + if err != nil { + return err + } + + validHooks := make(map[string]bool) + for _, hook := range hooks.UnitHooks() { + validHooks[string(hook)] = true + } + for _, relation := range relations { + for _, hook := range hooks.RelationHooks() { + hook := fmt.Sprintf("%s-%s", relation, hook) + validHooks[hook] = true + } + } + for _, hook := range c.hooks { + if !validHooks[hook] { + names := make([]string, 0, len(validHooks)) + for hookName := range validHooks { + names = append(names, hookName) + } + sort.Strings(names) + logger.Infof("unknown hook %s, valid hook names: %v", hook, names) + return fmt.Errorf("unit %q does not contain hook %q", c.Target, hook) + } + } + return nil +} + +// Run ensures c.Target is a unit, and resolves its address, +// and connects to it via SSH to execute the debug-hooks +// script. +func (c *DebugHooksCommand) Run(ctx *cmd.Context) error { + var err error + c.apiClient, err = c.initAPIClient() + if err != nil { + return err + } + defer c.apiClient.Close() + err = c.validateHooks() + if err != nil { + return err + } + debugctx := unitdebug.NewHooksContext(c.Target) + script := base64.StdEncoding.EncodeToString([]byte(unitdebug.ClientScript(debugctx, c.hooks))) + innercmd := fmt.Sprintf(`F=$(mktemp); echo %s | base64 -d > $F; . $F`, script) + args := []string{fmt.Sprintf("sudo /bin/bash -c '%s'", innercmd)} + c.Args = args + return c.SSHCommand.Run(ctx) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/debughooks_test.go' --- src/github.com/juju/juju/cmd/juju/commands/debughooks_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/debughooks_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,102 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "regexp" + "runtime" + + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/envcmd" + coretesting "github.com/juju/juju/testing" +) + +var _ = gc.Suite(&DebugHooksSuite{}) + +type DebugHooksSuite struct { + SSHCommonSuite +} + +const debugHooksArgs = sshArgs +const debugHooksArgsNoProxy = sshArgsNoProxy + +var debugHooksTests = []struct { + info string + args []string + error string + proxy bool + result string +}{{ + args: []string{"mysql/0"}, + result: regexp.QuoteMeta(debugHooksArgsNoProxy + "ubuntu@dummyenv-0.dns sudo /bin/bash -c 'F=$(mktemp); echo IyEvYmluL2Jhc2gKKAojIExvY2sgdGhlIGp1anUtPHVuaXQ+LWRlYnVnIGxvY2tmaWxlLgpmbG9jayAtbiA4IHx8IChlY2hvICJGYWlsZWQgdG8gYWNxdWlyZSAvdG1wL2p1anUtdW5pdC1teXNxbC0wLWRlYnVnLWhvb2tzOiB1bml0IGlzIGFscmVhZHkgYmVpbmcgZGVidWdnZWQiIDI+JjE7IGV4aXQgMSkKKAojIENsb3NlIHRoZSBpbmhlcml0ZWQgbG9jayBGRCwgb3IgdG11eCB3aWxsIGtlZXAgaXQgb3Blbi4KZXhlYyA4PiYtCgojIFdyaXRlIG91dCB0aGUgZGVidWctaG9va3MgYXJncy4KZWNobyAiZTMwSyIgfCBiYXNlNjQgLWQgPiAvdG1wL2p1anUtdW5pdC1teXNxbC0wLWRlYnVnLWhvb2tzCgojIExvY2sgdGhlIGp1anUtPHVuaXQ+LWRlYnVnLWV4aXQgbG9ja2ZpbGUuCmZsb2NrIC1uIDkgfHwgZXhpdCAxCgojIFdhaXQgZm9yIHRtdXggdG8gYmUgaW5zdGFsbGVkLgp3aGlsZSBbICEgLWYgL3Vzci9iaW4vdG11eCBdOyBkbwogICAgc2xlZXAgMQpkb25lCgppZiBbICEgLWYgfi8udG11eC5jb25mIF07IHRoZW4KICAgICAgICBpZiBbIC1mIC91c3Ivc2hhcmUvYnlvYnUvcHJvZmlsZXMvdG11eCBdOyB0aGVuCiAgICAgICAgICAgICAgICAjIFVzZSBieW9idS90bXV4IHByb2ZpbGUgZm9yIGZhbWlsaWFyIGtleWJpbmRpbmdzIGFuZCBicmFuZGluZwogICAgICAgICAgICAgICAgZWNobyAic291cmNlLWZpbGUgL3Vzci9zaGFyZS9ieW9idS9wcm9maWxlcy90bXV4IiA+IH4vLnRtdXguY29uZgogICAgICAgIGVsc2UKICAgICAgICAgICAgICAgICMgT3RoZXJ3aXNlLCB1c2UgdGhlIGxlZ2FjeSBqdWp1L3RtdXggY29uZmlndXJhdGlvbgogICAgICAgICAgICAgICAgY2F0ID4gfi8udG11eC5jb25mIDw8RU5ECiAgICAgICAgICAgICAgICAKIyBTdGF0dXMgYmFyCnNldC1vcHRpb24gLWcgc3RhdHVzLWJnIGJsYWNrCnNldC1vcHRpb24gLWcgc3RhdHVzLWZnIHdoaXRlCgpzZXQtd2luZG93LW9wdGlvbiAtZyB3aW5kb3ctc3RhdHVzLWN1cnJlbnQtYmcgcmVkCnNldC13aW5kb3ctb3B0aW9uIC1nIHdpbmRvdy1zdGF0dXMtY3VycmVudC1hdHRyIGJyaWdodAoKc2V0LW9wdGlvbiAtZyBzdGF0dXMtcmlnaHQgJycKCiMgUGFuZXMKc2V0LW9wdGlvbiAtZyBwYW5lLWJvcmRlci1mZyB3aGl0ZQpzZXQtb3B0aW9uIC1nIHBhbmUtYWN0aXZlLWJvcmRlci1mZyB3aGl0ZQoKIyBNb25pdG9yIGFjdGl2aXR5IG9uIHdpbmRvd3MKc2V0LXdpbmRvdy1vcHRpb24gLWcgbW9uaXRvci1hY3Rpdml0eSBvbgoKIyBTY3JlZW4gYmluZGluZ3MsIHNpbmNlIHBlb3BsZSBhcmUgbW9yZSBmYW1pbGlhciB3aXRoIHRoYXQuCnNldC1vcHRpb24gLWcgcHJlZml4IEMtYQpiaW5kIEMtYSBsYXN0LXdpbmRvdwpiaW5kIGEgc2VuZC1rZXkgQy1hCgpiaW5kIHwgc3BsaXQtd2luZG93IC1oCmJpbmQgLSBzcGxpdC13aW5kb3cgLXYKCiMgRml4IENUUkwtUEdVUC9QR0RPV04gZm9yIHZpbQpzZXQtd2luZG93LW9wdGlvbiAtZyB4dGVybS1rZXlzIG9uCgojIFByZXZlbnQgRVNDIGtleSBmcm9tIGFkZGluZyBkZWxheSBhbmQgYnJlYWtpbmcgVmltJ3MgRVNDID4gYXJyb3cga2V5CnNldC1vcHRpb24gLXMgZXNjYXBlLXRpbWUgMAoKRU5ECiAgICAgICAgZmkKZmkKCigKICAgICMgQ2xvc2UgdGhlIGluaGVyaXRlZCBsb2NrIEZELCBvciB0bXV4IHdpbGwga2VlcCBpdCBvcGVuLgogICAgZXhlYyA5PiYtCiAgICBleGVjIHRtdXggbmV3LXNlc3Npb24gLXMgbXlzcWwvMAopCikgOT4vdG1wL2p1anUtdW5pdC1teXNxbC0wLWRlYnVnLWhvb2tzLWV4aXQKKSA4Pi90bXAvanVqdS11bml0LW15c3FsLTAtZGVidWctaG9va3MKZXhpdCAkPwo= | base64 -d > $F; . $F'\n"), +}, { + args: []string{"mongodb/1"}, + result: regexp.QuoteMeta(debugHooksArgsNoProxy + "ubuntu@dummyenv-2.dns sudo /bin/bash -c 'F=$(mktemp); echo IyEvYmluL2Jhc2gKKAojIExvY2sgdGhlIGp1anUtPHVuaXQ+LWRlYnVnIGxvY2tmaWxlLgpmbG9jayAtbiA4IHx8IChlY2hvICJGYWlsZWQgdG8gYWNxdWlyZSAvdG1wL2p1anUtdW5pdC1tb25nb2RiLTEtZGVidWctaG9va3M6IHVuaXQgaXMgYWxyZWFkeSBiZWluZyBkZWJ1Z2dlZCIgMj4mMTsgZXhpdCAxKQooCiMgQ2xvc2UgdGhlIGluaGVyaXRlZCBsb2NrIEZELCBvciB0bXV4IHdpbGwga2VlcCBpdCBvcGVuLgpleGVjIDg+Ji0KCiMgV3JpdGUgb3V0IHRoZSBkZWJ1Zy1ob29rcyBhcmdzLgplY2hvICJlMzBLIiB8IGJhc2U2NCAtZCA+IC90bXAvanVqdS11bml0LW1vbmdvZGItMS1kZWJ1Zy1ob29rcwoKIyBMb2NrIHRoZSBqdWp1LTx1bml0Pi1kZWJ1Zy1leGl0IGxvY2tmaWxlLgpmbG9jayAtbiA5IHx8IGV4aXQgMQoKIyBXYWl0IGZvciB0bXV4IHRvIGJlIGluc3RhbGxlZC4Kd2hpbGUgWyAhIC1mIC91c3IvYmluL3RtdXggXTsgZG8KICAgIHNsZWVwIDEKZG9uZQoKaWYgWyAhIC1mIH4vLnRtdXguY29uZiBdOyB0aGVuCiAgICAgICAgaWYgWyAtZiAvdXNyL3NoYXJlL2J5b2J1L3Byb2ZpbGVzL3RtdXggXTsgdGhlbgogICAgICAgICAgICAgICAgIyBVc2UgYnlvYnUvdG11eCBwcm9maWxlIGZvciBmYW1pbGlhciBrZXliaW5kaW5ncyBhbmQgYnJhbmRpbmcKICAgICAgICAgICAgICAgIGVjaG8gInNvdXJjZS1maWxlIC91c3Ivc2hhcmUvYnlvYnUvcHJvZmlsZXMvdG11eCIgPiB+Ly50bXV4LmNvbmYKICAgICAgICBlbHNlCiAgICAgICAgICAgICAgICAjIE90aGVyd2lzZSwgdXNlIHRoZSBsZWdhY3kganVqdS90bXV4IGNvbmZpZ3VyYXRpb24KICAgICAgICAgICAgICAgIGNhdCA+IH4vLnRtdXguY29uZiA8PEVORAogICAgICAgICAgICAgICAgCiMgU3RhdHVzIGJhcgpzZXQtb3B0aW9uIC1nIHN0YXR1cy1iZyBibGFjawpzZXQtb3B0aW9uIC1nIHN0YXR1cy1mZyB3aGl0ZQoKc2V0LXdpbmRvdy1vcHRpb24gLWcgd2luZG93LXN0YXR1cy1jdXJyZW50LWJnIHJlZApzZXQtd2luZG93LW9wdGlvbiAtZyB3aW5kb3ctc3RhdHVzLWN1cnJlbnQtYXR0ciBicmlnaHQKCnNldC1vcHRpb24gLWcgc3RhdHVzLXJpZ2h0ICcnCgojIFBhbmVzCnNldC1vcHRpb24gLWcgcGFuZS1ib3JkZXItZmcgd2hpdGUKc2V0LW9wdGlvbiAtZyBwYW5lLWFjdGl2ZS1ib3JkZXItZmcgd2hpdGUKCiMgTW9uaXRvciBhY3Rpdml0eSBvbiB3aW5kb3dzCnNldC13aW5kb3ctb3B0aW9uIC1nIG1vbml0b3ItYWN0aXZpdHkgb24KCiMgU2NyZWVuIGJpbmRpbmdzLCBzaW5jZSBwZW9wbGUgYXJlIG1vcmUgZmFtaWxpYXIgd2l0aCB0aGF0LgpzZXQtb3B0aW9uIC1nIHByZWZpeCBDLWEKYmluZCBDLWEgbGFzdC13aW5kb3cKYmluZCBhIHNlbmQta2V5IEMtYQoKYmluZCB8IHNwbGl0LXdpbmRvdyAtaApiaW5kIC0gc3BsaXQtd2luZG93IC12CgojIEZpeCBDVFJMLVBHVVAvUEdET1dOIGZvciB2aW0Kc2V0LXdpbmRvdy1vcHRpb24gLWcgeHRlcm0ta2V5cyBvbgoKIyBQcmV2ZW50IEVTQyBrZXkgZnJvbSBhZGRpbmcgZGVsYXkgYW5kIGJyZWFraW5nIFZpbSdzIEVTQyA+IGFycm93IGtleQpzZXQtb3B0aW9uIC1zIGVzY2FwZS10aW1lIDAKCkVORAogICAgICAgIGZpCmZpCgooCiAgICAjIENsb3NlIHRoZSBpbmhlcml0ZWQgbG9jayBGRCwgb3IgdG11eCB3aWxsIGtlZXAgaXQgb3Blbi4KICAgIGV4ZWMgOT4mLQogICAgZXhlYyB0bXV4IG5ldy1zZXNzaW9uIC1zIG1vbmdvZGIvMQopCikgOT4vdG1wL2p1anUtdW5pdC1tb25nb2RiLTEtZGVidWctaG9va3MtZXhpdAopIDg+L3RtcC9qdWp1LXVuaXQtbW9uZ29kYi0xLWRlYnVnLWhvb2tzCmV4aXQgJD8K | base64 -d > $F; . $F'\n"), +}, { + args: []string{"mysql/0"}, + proxy: true, + result: regexp.QuoteMeta(debugHooksArgs + "ubuntu@dummyenv-0.internal sudo /bin/bash -c 'F=$(mktemp); echo IyEvYmluL2Jhc2gKKAojIExvY2sgdGhlIGp1anUtPHVuaXQ+LWRlYnVnIGxvY2tmaWxlLgpmbG9jayAtbiA4IHx8IChlY2hvICJGYWlsZWQgdG8gYWNxdWlyZSAvdG1wL2p1anUtdW5pdC1teXNxbC0wLWRlYnVnLWhvb2tzOiB1bml0IGlzIGFscmVhZHkgYmVpbmcgZGVidWdnZWQiIDI+JjE7IGV4aXQgMSkKKAojIENsb3NlIHRoZSBpbmhlcml0ZWQgbG9jayBGRCwgb3IgdG11eCB3aWxsIGtlZXAgaXQgb3Blbi4KZXhlYyA4PiYtCgojIFdyaXRlIG91dCB0aGUgZGVidWctaG9va3MgYXJncy4KZWNobyAiZTMwSyIgfCBiYXNlNjQgLWQgPiAvdG1wL2p1anUtdW5pdC1teXNxbC0wLWRlYnVnLWhvb2tzCgojIExvY2sgdGhlIGp1anUtPHVuaXQ+LWRlYnVnLWV4aXQgbG9ja2ZpbGUuCmZsb2NrIC1uIDkgfHwgZXhpdCAxCgojIFdhaXQgZm9yIHRtdXggdG8gYmUgaW5zdGFsbGVkLgp3aGlsZSBbICEgLWYgL3Vzci9iaW4vdG11eCBdOyBkbwogICAgc2xlZXAgMQpkb25lCgppZiBbICEgLWYgfi8udG11eC5jb25mIF07IHRoZW4KICAgICAgICBpZiBbIC1mIC91c3Ivc2hhcmUvYnlvYnUvcHJvZmlsZXMvdG11eCBdOyB0aGVuCiAgICAgICAgICAgICAgICAjIFVzZSBieW9idS90bXV4IHByb2ZpbGUgZm9yIGZhbWlsaWFyIGtleWJpbmRpbmdzIGFuZCBicmFuZGluZwogICAgICAgICAgICAgICAgZWNobyAic291cmNlLWZpbGUgL3Vzci9zaGFyZS9ieW9idS9wcm9maWxlcy90bXV4IiA+IH4vLnRtdXguY29uZgogICAgICAgIGVsc2UKICAgICAgICAgICAgICAgICMgT3RoZXJ3aXNlLCB1c2UgdGhlIGxlZ2FjeSBqdWp1L3RtdXggY29uZmlndXJhdGlvbgogICAgICAgICAgICAgICAgY2F0ID4gfi8udG11eC5jb25mIDw8RU5ECiAgICAgICAgICAgICAgICAKIyBTdGF0dXMgYmFyCnNldC1vcHRpb24gLWcgc3RhdHVzLWJnIGJsYWNrCnNldC1vcHRpb24gLWcgc3RhdHVzLWZnIHdoaXRlCgpzZXQtd2luZG93LW9wdGlvbiAtZyB3aW5kb3ctc3RhdHVzLWN1cnJlbnQtYmcgcmVkCnNldC13aW5kb3ctb3B0aW9uIC1nIHdpbmRvdy1zdGF0dXMtY3VycmVudC1hdHRyIGJyaWdodAoKc2V0LW9wdGlvbiAtZyBzdGF0dXMtcmlnaHQgJycKCiMgUGFuZXMKc2V0LW9wdGlvbiAtZyBwYW5lLWJvcmRlci1mZyB3aGl0ZQpzZXQtb3B0aW9uIC1nIHBhbmUtYWN0aXZlLWJvcmRlci1mZyB3aGl0ZQoKIyBNb25pdG9yIGFjdGl2aXR5IG9uIHdpbmRvd3MKc2V0LXdpbmRvdy1vcHRpb24gLWcgbW9uaXRvci1hY3Rpdml0eSBvbgoKIyBTY3JlZW4gYmluZGluZ3MsIHNpbmNlIHBlb3BsZSBhcmUgbW9yZSBmYW1pbGlhciB3aXRoIHRoYXQuCnNldC1vcHRpb24gLWcgcHJlZml4IEMtYQpiaW5kIEMtYSBsYXN0LXdpbmRvdwpiaW5kIGEgc2VuZC1rZXkgQy1hCgpiaW5kIHwgc3BsaXQtd2luZG93IC1oCmJpbmQgLSBzcGxpdC13aW5kb3cgLXYKCiMgRml4IENUUkwtUEdVUC9QR0RPV04gZm9yIHZpbQpzZXQtd2luZG93LW9wdGlvbiAtZyB4dGVybS1rZXlzIG9uCgojIFByZXZlbnQgRVNDIGtleSBmcm9tIGFkZGluZyBkZWxheSBhbmQgYnJlYWtpbmcgVmltJ3MgRVNDID4gYXJyb3cga2V5CnNldC1vcHRpb24gLXMgZXNjYXBlLXRpbWUgMAoKRU5ECiAgICAgICAgZmkKZmkKCigKICAgICMgQ2xvc2UgdGhlIGluaGVyaXRlZCBsb2NrIEZELCBvciB0bXV4IHdpbGwga2VlcCBpdCBvcGVuLgogICAgZXhlYyA5PiYtCiAgICBleGVjIHRtdXggbmV3LXNlc3Npb24gLXMgbXlzcWwvMAopCikgOT4vdG1wL2p1anUtdW5pdC1teXNxbC0wLWRlYnVnLWhvb2tzLWV4aXQKKSA4Pi90bXAvanVqdS11bml0LW15c3FsLTAtZGVidWctaG9va3MKZXhpdCAkPwo= | base64 -d > $F; . $F'\n"), +}, { + info: `"*" is a valid hook name: it means hook everything`, + args: []string{"mysql/0", "*"}, + result: ".*\n", +}, { + info: `"*" mixed with named hooks is equivalent to "*"`, + args: []string{"mysql/0", "*", "relation-get"}, + result: ".*\n", +}, { + info: `multiple named hooks may be specified`, + args: []string{"mysql/0", "start", "stop"}, + result: ".*\n", +}, { + info: `relation hooks have the relation name prefixed`, + args: []string{"mysql/0", "juju-info-relation-joined"}, + result: ".*\n", +}, { + info: `invalid unit syntax`, + args: []string{"mysql"}, + error: `"mysql" is not a valid unit name`, +}, { + info: `invalid unit`, + args: []string{"nonexistent/123"}, + error: `unit "nonexistent/123" not found`, +}, { + info: `invalid hook`, + args: []string{"mysql/0", "invalid-hook"}, + error: `unit "mysql/0" does not contain hook "invalid-hook"`, +}} + +func (s *DebugHooksSuite) TestDebugHooksCommand(c *gc.C) { + //TODO(bogdanteleaga): Fix once debughooks are supported on windows + if runtime.GOOS == "windows" { + c.Skip("bug 1403084: Skipping on windows for now") + } + machines := s.makeMachines(3, c, true) + dummy := s.AddTestingCharm(c, "dummy") + srv := s.AddTestingService(c, "mysql", dummy) + s.addUnit(srv, machines[0], c) + + srv = s.AddTestingService(c, "mongodb", dummy) + s.addUnit(srv, machines[1], c) + s.addUnit(srv, machines[2], c) + + for i, t := range debugHooksTests { + c.Logf("test %d: %s\n\t%s\n", i, t.info, t.args) + ctx := coretesting.Context(c) + + debugHooksCmd := &DebugHooksCommand{} + debugHooksCmd.proxy = true + err := envcmd.Wrap(debugHooksCmd).Init(t.args) + if err == nil { + err = debugHooksCmd.Run(ctx) + } + if t.error != "" { + c.Assert(err, gc.ErrorMatches, t.error) + } else { + c.Assert(err, jc.ErrorIsNil) + } + } +} === added file 'src/github.com/juju/juju/cmd/juju/commands/debuglog.go' --- src/github.com/juju/juju/cmd/juju/commands/debuglog.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/debuglog.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,100 @@ +// Copyright 2013, 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "fmt" + "io" + + "github.com/juju/cmd" + "github.com/juju/loggo" + "launchpad.net/gnuflag" + + "github.com/juju/juju/api" + "github.com/juju/juju/cmd/envcmd" +) + +type DebugLogCommand struct { + envcmd.EnvCommandBase + + level string + params api.DebugLogParams +} + +var DefaultLogLocation = "/var/log/juju/all-machines.log" + +// defaultLineCount is the default number of lines to +// display, from the end of the consolidated log. +const defaultLineCount = 10 + +const debuglogDoc = ` +Stream the consolidated debug log file. This file contains the log messages +from all nodes in the environment. +` + +func (c *DebugLogCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "debug-log", + Purpose: "display the consolidated log file", + Doc: debuglogDoc, + } +} + +func (c *DebugLogCommand) SetFlags(f *gnuflag.FlagSet) { + f.Var(cmd.NewAppendStringsValue(&c.params.IncludeEntity), "i", "only show log messages for these entities") + f.Var(cmd.NewAppendStringsValue(&c.params.IncludeEntity), "include", "only show log messages for these entities") + f.Var(cmd.NewAppendStringsValue(&c.params.ExcludeEntity), "x", "do not show log messages for these entities") + f.Var(cmd.NewAppendStringsValue(&c.params.ExcludeEntity), "exclude", "do not show log messages for these entities") + f.Var(cmd.NewAppendStringsValue(&c.params.IncludeModule), "include-module", "only show log messages for these logging modules") + f.Var(cmd.NewAppendStringsValue(&c.params.ExcludeModule), "exclude-module", "do not show log messages for these logging modules") + + f.StringVar(&c.level, "l", "", "log level to show, one of [TRACE, DEBUG, INFO, WARNING, ERROR]") + f.StringVar(&c.level, "level", "", "") + + f.UintVar(&c.params.Backlog, "n", defaultLineCount, "go back this many lines from the end before starting to filter") + f.UintVar(&c.params.Backlog, "lines", defaultLineCount, "") + f.UintVar(&c.params.Limit, "limit", 0, "show at most this many lines") + f.BoolVar(&c.params.Replay, "replay", false, "start filtering from the start") +} + +func (c *DebugLogCommand) Init(args []string) error { + if c.level != "" { + level, ok := loggo.ParseLevel(c.level) + if !ok || level < loggo.TRACE || level > loggo.ERROR { + return fmt.Errorf("level value %q is not one of %q, %q, %q, %q, %q", + c.level, loggo.TRACE, loggo.DEBUG, loggo.INFO, loggo.WARNING, loggo.ERROR) + } + c.params.Level = level + } + return cmd.CheckEmpty(args) +} + +type DebugLogAPI interface { + WatchDebugLog(params api.DebugLogParams) (io.ReadCloser, error) + Close() error +} + +var getDebugLogAPI = func(c *DebugLogCommand) (DebugLogAPI, error) { + return c.NewAPIClient() +} + +// Run retrieves the debug log via the API. +func (c *DebugLogCommand) Run(ctx *cmd.Context) (err error) { + client, err := getDebugLogAPI(c) + if err != nil { + return err + } + defer client.Close() + debugLog, err := client.WatchDebugLog(c.params) + if err != nil { + return err + } + defer debugLog.Close() + _, err = io.Copy(ctx.Stdout, debugLog) + return err +} + +var runSSHCommand = func(sshCmd *SSHCommand, ctx *cmd.Context) error { + return sshCmd.Run(ctx) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/debuglog_test.go' --- src/github.com/juju/juju/cmd/juju/commands/debuglog_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/debuglog_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,152 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "io" + "io/ioutil" + "strings" + + "github.com/juju/loggo" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/api" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/testing" +) + +type DebugLogSuite struct { + testing.FakeJujuHomeSuite +} + +var _ = gc.Suite(&DebugLogSuite{}) + +func (s *DebugLogSuite) TestArgParsing(c *gc.C) { + for i, test := range []struct { + args []string + expected api.DebugLogParams + errMatch string + }{ + { + expected: api.DebugLogParams{ + Backlog: 10, + }, + }, { + args: []string{"-n0"}, + }, { + args: []string{"--lines=50"}, + expected: api.DebugLogParams{ + Backlog: 50, + }, + }, { + args: []string{"-l", "foo"}, + errMatch: `level value "foo" is not one of "TRACE", "DEBUG", "INFO", "WARNING", "ERROR"`, + }, { + args: []string{"--level=INFO"}, + expected: api.DebugLogParams{ + Backlog: 10, + Level: loggo.INFO, + }, + }, { + args: []string{"--include", "machine-1", "-i", "machine-2"}, + expected: api.DebugLogParams{ + IncludeEntity: []string{"machine-1", "machine-2"}, + Backlog: 10, + }, + }, { + args: []string{"--exclude", "machine-1", "-x", "machine-2"}, + expected: api.DebugLogParams{ + ExcludeEntity: []string{"machine-1", "machine-2"}, + Backlog: 10, + }, + }, { + args: []string{"--include-module", "juju.foo", "--include-module", "unit"}, + expected: api.DebugLogParams{ + IncludeModule: []string{"juju.foo", "unit"}, + Backlog: 10, + }, + }, { + args: []string{"--exclude-module", "juju.foo", "--exclude-module", "unit"}, + expected: api.DebugLogParams{ + ExcludeModule: []string{"juju.foo", "unit"}, + Backlog: 10, + }, + }, { + args: []string{"--replay"}, + expected: api.DebugLogParams{ + Backlog: 10, + Replay: true, + }, + }, { + args: []string{"--limit", "100"}, + expected: api.DebugLogParams{ + Backlog: 10, + Limit: 100, + }, + }, + } { + c.Logf("test %v", i) + command := &DebugLogCommand{} + err := testing.InitCommand(envcmd.Wrap(command), test.args) + if test.errMatch == "" { + c.Check(err, jc.ErrorIsNil) + c.Check(command.params, jc.DeepEquals, test.expected) + } else { + c.Check(err, gc.ErrorMatches, test.errMatch) + } + } +} + +func (s *DebugLogSuite) TestParamsPassed(c *gc.C) { + fake := &fakeDebugLogAPI{} + s.PatchValue(&getDebugLogAPI, func(_ *DebugLogCommand) (DebugLogAPI, error) { + return fake, nil + }) + _, err := testing.RunCommand(c, envcmd.Wrap(&DebugLogCommand{}), + "-i", "machine-1*", "-x", "machine-1-lxc-1", + "--include-module=juju.provisioner", + "--lines=500", + "--level=WARNING", + ) + c.Assert(err, jc.ErrorIsNil) + c.Assert(fake.params, gc.DeepEquals, api.DebugLogParams{ + IncludeEntity: []string{"machine-1*"}, + IncludeModule: []string{"juju.provisioner"}, + ExcludeEntity: []string{"machine-1-lxc-1"}, + Backlog: 500, + Level: loggo.WARNING, + }) +} + +func (s *DebugLogSuite) TestLogOutput(c *gc.C) { + s.PatchValue(&getDebugLogAPI, func(_ *DebugLogCommand) (DebugLogAPI, error) { + return &fakeDebugLogAPI{log: "this is the log output"}, nil + }) + ctx, err := testing.RunCommand(c, envcmd.Wrap(&DebugLogCommand{})) + c.Assert(err, jc.ErrorIsNil) + c.Assert(testing.Stdout(ctx), gc.Equals, "this is the log output") +} + +func newFakeDebugLogAPI(log string) DebugLogAPI { + return &fakeDebugLogAPI{log: log} +} + +type fakeDebugLogAPI struct { + log string + params api.DebugLogParams + err error +} + +func (fake *fakeDebugLogAPI) WatchDebugLog(params api.DebugLogParams) (io.ReadCloser, error) { + if fake.err != nil { + return nil, fake.err + } + fake.params = params + return ioutil.NopCloser(strings.NewReader(fake.log)), nil +} + +func (fake *fakeDebugLogAPI) Close() error { + return nil +} === added file 'src/github.com/juju/juju/cmd/juju/commands/deploy.go' --- src/github.com/juju/juju/cmd/juju/commands/deploy.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/deploy.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,333 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "fmt" + "os" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/names" + "gopkg.in/juju/charm.v5" + "launchpad.net/gnuflag" + + "github.com/juju/juju/api" + apiservice "github.com/juju/juju/api/service" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/block" + "github.com/juju/juju/cmd/juju/service" + "github.com/juju/juju/constraints" + "github.com/juju/juju/juju/osenv" + "github.com/juju/juju/storage" +) + +type DeployCommand struct { + envcmd.EnvCommandBase + service.UnitCommandBase + CharmName string + ServiceName string + Config cmd.FileVar + Constraints constraints.Value + Networks string // TODO(dimitern): Drop this in a follow-up and fix docs. + BumpRevision bool // Remove this once the 1.16 support is dropped. + RepoPath string // defaults to JUJU_REPOSITORY + RegisterURL string + + // TODO(axw) move this to UnitCommandBase once we support --storage + // on add-unit too. + // + // Storage is a map of storage constraints, keyed on the storage name + // defined in charm storage metadata. + Storage map[string]storage.Constraints +} + +const deployDoc = ` + can be a charm URL, or an unambiguously condensed form of it; +assuming a current series of "precise", the following forms will be accepted: + +For cs:precise/mysql + mysql + precise/mysql + +For cs:~user/precise/mysql + cs:~user/mysql + +The current series is determined first by the default-series environment +setting, followed by the preferred series for the charm in the charm store. + +In these cases, a versioned charm URL will be expanded as expected (for example, +mysql-33 becomes cs:precise/mysql-33). + +However, for local charms, when the default-series is not specified in the +environment, one must specify the series. For example: + local:precise/mysql + +, if omitted, will be derived from . + +Constraints can be specified when using deploy by specifying the --constraints +flag. When used with deploy, service-specific constraints are set so that later +machines provisioned with add-unit will use the same constraints (unless changed +by set-constraints). + +Charms can be deployed to a specific machine using the --to argument. +If the destination is an LXC container the default is to use lxc-clone +to create the container where possible. For Ubuntu deployments, lxc-clone +is supported for the trusty OS series and later. A 'template' container is +created with the name + juju--template +where is the OS series, for example 'juju-trusty-template'. + +You can override the use of clone by changing the provider configuration: + lxc-clone: false + +In more complex scenarios, Juju's network spaces are used to partition the cloud +networking layer into sets of subnets. Instances hosting units inside the +same space can communicate with each other without any firewalls. Traffic +crossing space boundaries could be subject to firewall and access restrictions. +Using spaces as deployment targets, rather than their individual subnets allows +Juju to perform automatic distribution of units across availability zones to +support high availability for services. Spaces help isolate services and their +units, both for security purposes and to manage both traffic segregation and +congestion. + +When deploying a service or adding machines, the "spaces" constraint can be +used to define a comma-delimited list of required and forbidden spaces +(the latter prefixed with "^", similar to the "tags" constraint). + +If you have the main container directory mounted on a btrfs partition, +then the clone will be using btrfs snapshots to create the containers. +This means that clones use up much less disk space. If you do not have btrfs, +lxc will attempt to use aufs (an overlay type filesystem). You can +explicitly ask Juju to create full containers and not overlays by specifying +the following in the provider configuration: + lxc-clone-aufs: false + +Examples: + juju deploy mysql --to 23 (deploy to machine 23) + juju deploy mysql --to 24/lxc/3 (deploy to lxc container 3 on host machine 24) + juju deploy mysql --to lxc:25 (deploy to a new lxc container on host machine 25) + + juju deploy mysql -n 5 --constraints mem=8G + (deploy 5 instances of mysql with at least 8 GB of RAM each) + + juju deploy haproxy -n 2 --constraints spaces=dmz,^cms,^database + (deploy 2 instances of haproxy on cloud instances being part of the dmz + space but not of the cmd and the database space) + +See Also: + juju help spaces + juju help constraints + juju help set-constraints + juju help get-constraints +` + +func (c *DeployCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "deploy", + Args: " []", + Purpose: "deploy a new service", + Doc: deployDoc, + } +} + +func (c *DeployCommand) SetFlags(f *gnuflag.FlagSet) { + c.UnitCommandBase.SetFlags(f) + f.IntVar(&c.NumUnits, "n", 1, "number of service units to deploy for principal charms") + f.BoolVar(&c.BumpRevision, "u", false, "increment local charm directory revision (DEPRECATED)") + f.BoolVar(&c.BumpRevision, "upgrade", false, "") + f.Var(&c.Config, "config", "path to yaml-formatted service config") + f.Var(constraints.ConstraintsValue{Target: &c.Constraints}, "constraints", "set service constraints") + f.StringVar(&c.Networks, "networks", "", "deprecated and ignored: use space constraints instead.") + f.StringVar(&c.RepoPath, "repository", os.Getenv(osenv.JujuRepositoryEnvKey), "local charm repository") + f.Var(storageFlag{&c.Storage}, "storage", "charm storage constraints") +} + +func (c *DeployCommand) Init(args []string) error { + switch len(args) { + case 2: + if !names.IsValidService(args[1]) { + return fmt.Errorf("invalid service name %q", args[1]) + } + c.ServiceName = args[1] + fallthrough + case 1: + if _, err := charm.InferURL(args[0], "fake"); err != nil { + return fmt.Errorf("invalid charm name %q", args[0]) + } + c.CharmName = args[0] + case 0: + return errors.New("no charm specified") + default: + return cmd.CheckEmpty(args[2:]) + } + return c.UnitCommandBase.Init(args) +} + +func (c *DeployCommand) newServiceAPIClient() (*apiservice.Client, error) { + root, err := c.NewAPIRoot() + if err != nil { + return nil, errors.Trace(err) + } + return apiservice.NewClient(root), nil +} + +func (c *DeployCommand) Run(ctx *cmd.Context) error { + client, err := c.NewAPIClient() + if err != nil { + return err + } + defer client.Close() + + conf, err := service.GetClientConfig(client) + if err != nil { + return err + } + + if err := c.CheckProvider(conf); err != nil { + return err + } + + csClient, err := newCharmStoreClient() + if err != nil { + return errors.Trace(err) + } + defer csClient.jar.Save() + curl, repo, err := resolveCharmURL(c.CharmName, csClient.params, ctx.AbsPath(c.RepoPath), conf) + if err != nil { + return errors.Trace(err) + } + + curl, err = addCharmViaAPI(client, ctx, curl, repo, csClient) + if err != nil { + return block.ProcessBlockedError(err, block.BlockChange) + } + + if c.BumpRevision { + ctx.Infof("--upgrade (or -u) is deprecated and ignored; charms are always deployed with a unique revision.") + } + + charmInfo, err := client.CharmInfo(curl.String()) + if err != nil { + return err + } + + numUnits := c.NumUnits + if charmInfo.Meta.Subordinate { + if !constraints.IsEmpty(&c.Constraints) { + return errors.New("cannot use --constraints with subordinate service") + } + if numUnits == 1 && c.PlacementSpec == "" { + numUnits = 0 + } else { + return errors.New("cannot use --num-units or --to with subordinate service") + } + } + serviceName := c.ServiceName + if serviceName == "" { + serviceName = charmInfo.Meta.Name + } + + var configYAML []byte + if c.Config.Path != "" { + configYAML, err = c.Config.Read(ctx) + if err != nil { + return err + } + } + + // If storage or placement is specified, we attempt to use a new API on the service facade. + if len(c.Storage) > 0 || len(c.Placement) > 0 { + notSupported := errors.New("cannot deploy charms with storage or placement: not supported by the API server") + serviceClient, err := c.newServiceAPIClient() + if err != nil { + return notSupported + } + defer serviceClient.Close() + for i, p := range c.Placement { + if p.Scope == "env-uuid" { + p.Scope = serviceClient.EnvironmentUUID() + } + c.Placement[i] = p + } + err = serviceClient.ServiceDeploy( + curl.String(), + serviceName, + numUnits, + string(configYAML), + c.Constraints, + c.PlacementSpec, + c.Placement, + []string{}, + c.Storage, + ) + if params.IsCodeNotImplemented(err) { + return notSupported + } + return block.ProcessBlockedError(err, block.BlockChange) + } + + if len(c.Networks) > 0 { + ctx.Infof("use of --networks is deprecated and is ignored. Please use spaces to manage placement within networks") + } + + err = client.ServiceDeploy( + curl.String(), + serviceName, + numUnits, + string(configYAML), + c.Constraints, + c.PlacementSpec) + + if err != nil { + return block.ProcessBlockedError(err, block.BlockChange) + } + + state, err := c.NewAPIRoot() + if err != nil { + return err + } + err = registerMeteredCharm(c.RegisterURL, state, csClient.jar, curl.String(), serviceName, client.EnvironmentUUID()) + if params.IsCodeNotImplemented(err) { + // The state server is too old to support metering. Warn + // the user, but don't return an error. + logger.Warningf("current state server version does not support charm metering") + return nil + } + + return block.ProcessBlockedError(err, block.BlockChange) +} + +type metricCredentialsAPI interface { + SetMetricCredentials(string, []byte) error + Close() error +} + +type metricsCredentialsAPIImpl struct { + api *apiservice.Client + state api.Connection +} + +// SetMetricCredentials sets the credentials on the service. +func (s *metricsCredentialsAPIImpl) SetMetricCredentials(serviceName string, data []byte) error { + return s.api.SetMetricCredentials(serviceName, data) +} + +// Close closes the api connection +func (s *metricsCredentialsAPIImpl) Close() error { + err := s.api.Close() + if err != nil { + return errors.Trace(err) + } + err = s.state.Close() + if err != nil { + return errors.Trace(err) + } + return nil +} + +var getMetricCredentialsAPI = func(state api.Connection) (metricCredentialsAPI, error) { + return &metricsCredentialsAPIImpl{api: apiservice.NewClient(state), state: state}, nil +} === added file 'src/github.com/juju/juju/cmd/juju/commands/deploy_test.go' --- src/github.com/juju/juju/cmd/juju/commands/deploy_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/deploy_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,663 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "strings" + + "github.com/juju/errors" + "github.com/juju/persistent-cookiejar" + jujutesting "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + "github.com/juju/utils" + gc "gopkg.in/check.v1" + "gopkg.in/juju/charm.v5" + "gopkg.in/juju/charm.v5/charmrepo" + "gopkg.in/juju/charmstore.v4" + "gopkg.in/juju/charmstore.v4/charmstoretesting" + "gopkg.in/juju/charmstore.v4/csclient" + "gopkg.in/macaroon-bakery.v0/bakery/checkers" + "gopkg.in/macaroon-bakery.v0/bakerytest" + + "github.com/juju/juju/api" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/service" + "github.com/juju/juju/constraints" + "github.com/juju/juju/environs/config" + "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" +) + +type DeploySuite struct { + testing.RepoSuite + CmdBlockHelper +} + +func (s *DeploySuite) SetUpTest(c *gc.C) { + s.RepoSuite.SetUpTest(c) + s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) + c.Assert(s.CmdBlockHelper, gc.NotNil) + s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) +} + +var _ = gc.Suite(&DeploySuite{}) + +func runDeploy(c *gc.C, args ...string) error { + _, err := coretesting.RunCommand(c, envcmd.Wrap(&DeployCommand{}), args...) + return err +} + +var initErrorTests = []struct { + args []string + err string +}{ + { + args: nil, + err: `no charm specified`, + }, { + args: []string{"charm-name", "service-name", "hotdog"}, + err: `unrecognized args: \["hotdog"\]`, + }, { + args: []string{"craz~ness"}, + err: `invalid charm name "craz~ness"`, + }, { + args: []string{"craziness", "burble-1"}, + err: `invalid service name "burble-1"`, + }, { + args: []string{"craziness", "burble1", "-n", "0"}, + err: `--num-units must be a positive integer`, + }, { + args: []string{"craziness", "burble1", "--to", "#:foo"}, + err: `invalid --to parameter "#:foo"`, + }, { + args: []string{"craziness", "burble1", "--constraints", "gibber=plop"}, + err: `invalid value "gibber=plop" for flag --constraints: unknown constraint "gibber"`, + }, +} + +func (s *DeploySuite) TestInitErrors(c *gc.C) { + for i, t := range initErrorTests { + c.Logf("test %d", i) + err := coretesting.InitCommand(envcmd.Wrap(&DeployCommand{}), t.args) + c.Assert(err, gc.ErrorMatches, t.err) + } +} + +func (s *DeploySuite) TestNoCharm(c *gc.C) { + err := runDeploy(c, "local:unknown-123") + c.Assert(err, gc.ErrorMatches, `charm not found in ".*": local:trusty/unknown-123`) +} + +func (s *DeploySuite) TestBlockDeploy(c *gc.C) { + // Block operation + s.BlockAllChanges(c, "TestBlockDeploy") + testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") + err := runDeploy(c, "local:dummy", "some-service-name") + s.AssertBlocked(c, err, ".*TestBlockDeploy.*") +} + +func (s *DeploySuite) TestCharmDir(c *gc.C) { + testcharms.Repo.ClonedDirPath(s.SeriesPath, "dummy") + err := runDeploy(c, "local:dummy") + c.Assert(err, jc.ErrorIsNil) + curl := charm.MustParseURL("local:trusty/dummy-1") + s.AssertService(c, "dummy", curl, 1, 0) +} + +func (s *DeploySuite) TestUpgradeReportsDeprecated(c *gc.C) { + testcharms.Repo.ClonedDirPath(s.SeriesPath, "dummy") + ctx, err := coretesting.RunCommand(c, envcmd.Wrap(&DeployCommand{}), "local:dummy", "-u") + c.Assert(err, jc.ErrorIsNil) + + c.Assert(coretesting.Stdout(ctx), gc.Equals, "") + output := strings.Split(coretesting.Stderr(ctx), "\n") + c.Check(output[0], gc.Matches, `Added charm ".*" to the environment.`) + c.Check(output[1], gc.Equals, "--upgrade (or -u) is deprecated and ignored; charms are always deployed with a unique revision.") +} + +func (s *DeploySuite) TestUpgradeCharmDir(c *gc.C) { + // Add the charm, so the url will exist and a new revision will be + // picked in ServiceDeploy. + dummyCharm := s.AddTestingCharm(c, "dummy") + + dirPath := testcharms.Repo.ClonedDirPath(s.SeriesPath, "dummy") + err := runDeploy(c, "local:quantal/dummy") + c.Assert(err, jc.ErrorIsNil) + upgradedRev := dummyCharm.Revision() + 1 + curl := dummyCharm.URL().WithRevision(upgradedRev) + s.AssertService(c, "dummy", curl, 1, 0) + // Check the charm dir was left untouched. + ch, err := charm.ReadCharmDir(dirPath) + c.Assert(err, jc.ErrorIsNil) + c.Assert(ch.Revision(), gc.Equals, 1) +} + +func (s *DeploySuite) TestCharmBundle(c *gc.C) { + testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") + err := runDeploy(c, "local:dummy", "some-service-name") + c.Assert(err, jc.ErrorIsNil) + curl := charm.MustParseURL("local:trusty/dummy-1") + s.AssertService(c, "some-service-name", curl, 1, 0) +} + +func (s *DeploySuite) TestSubordinateCharm(c *gc.C) { + testcharms.Repo.CharmArchivePath(s.SeriesPath, "logging") + err := runDeploy(c, "local:logging") + c.Assert(err, jc.ErrorIsNil) + curl := charm.MustParseURL("local:trusty/logging-1") + s.AssertService(c, "logging", curl, 0, 0) +} + +func (s *DeploySuite) TestConfig(c *gc.C) { + testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") + path := setupConfigFile(c, c.MkDir()) + err := runDeploy(c, "local:dummy", "dummy-service", "--config", path) + c.Assert(err, jc.ErrorIsNil) + service, err := s.State.Service("dummy-service") + c.Assert(err, jc.ErrorIsNil) + settings, err := service.ConfigSettings() + c.Assert(err, jc.ErrorIsNil) + c.Assert(settings, gc.DeepEquals, charm.Settings{ + "skill-level": int64(9000), + "username": "admin001", + }) +} + +func (s *DeploySuite) TestRelativeConfigPath(c *gc.C) { + testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") + // Putting a config file in home is okay as $HOME is set to a tempdir + setupConfigFile(c, utils.Home()) + err := runDeploy(c, "local:dummy", "dummy-service", "--config", "~/testconfig.yaml") + c.Assert(err, jc.ErrorIsNil) +} + +func (s *DeploySuite) TestConfigError(c *gc.C) { + testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") + path := setupConfigFile(c, c.MkDir()) + err := runDeploy(c, "local:dummy", "other-service", "--config", path) + c.Assert(err, gc.ErrorMatches, `no settings found for "other-service"`) + _, err = s.State.Service("other-service") + c.Assert(err, jc.Satisfies, errors.IsNotFound) +} + +func (s *DeploySuite) TestConstraints(c *gc.C) { + testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") + err := runDeploy(c, "local:dummy", "--constraints", "mem=2G cpu-cores=2") + c.Assert(err, jc.ErrorIsNil) + curl := charm.MustParseURL("local:trusty/dummy-1") + service, _ := s.AssertService(c, "dummy", curl, 1, 0) + cons, err := service.Constraints() + c.Assert(err, jc.ErrorIsNil) + c.Assert(cons, jc.DeepEquals, constraints.MustParse("mem=2G cpu-cores=2")) +} + +func (s *DeploySuite) TestNetworksIsDeprecated(c *gc.C) { + testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") + err := runDeploy(c, "local:dummy", "--networks", ", net1, net2 , ", "--constraints", "mem=2G cpu-cores=2 networks=net1,net0,^net3,^net4") + c.Assert(err, gc.ErrorMatches, "use of --networks is deprecated. Please use spaces") +} + +// 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) + + testcharms.Repo.CharmArchivePath(s.SeriesPath, "storage-block") + err = runDeploy(c, "local:storage-block", "--storage", "data=loop-pool,1G") + c.Assert(err, jc.ErrorIsNil) + curl := charm.MustParseURL("local:trusty/storage-block-1") + service, _ := s.AssertService(c, "storage-block", curl, 1, 0) + + cons, err := service.StorageConstraints() + c.Assert(err, jc.ErrorIsNil) + c.Assert(cons, jc.DeepEquals, map[string]state.StorageConstraints{ + "data": { + Pool: "loop-pool", + Count: 1, + Size: 1024, + }, + "allecto": { + Pool: "loop", + Count: 0, + Size: 1024, + }, + }) +} + +// TODO(wallyworld) - add another test that deploy with placement fails for older environments +// (need deploy client to be refactored to use API stub) +func (s *DeploySuite) TestPlacement(c *gc.C) { + testcharms.Repo.ClonedDirPath(s.SeriesPath, "dummy") + // Add a machine that will be ignored due to placement directive. + machine, err := s.State.AddMachine(coretesting.FakeDefaultSeries, state.JobHostUnits) + c.Assert(err, jc.ErrorIsNil) + + err = runDeploy(c, "local:dummy", "-n", "1", "--to", "valid") + c.Assert(err, jc.ErrorIsNil) + + svc, err := s.State.Service("dummy") + c.Assert(err, jc.ErrorIsNil) + units, err := svc.AllUnits() + c.Assert(err, jc.ErrorIsNil) + c.Assert(units, gc.HasLen, 1) + mid, err := units[0].AssignedMachineId() + c.Assert(err, jc.ErrorIsNil) + c.Assert(mid, gc.Not(gc.Equals), machine.Id()) +} + +func (s *DeploySuite) TestSubordinateConstraints(c *gc.C) { + testcharms.Repo.CharmArchivePath(s.SeriesPath, "logging") + err := runDeploy(c, "local:logging", "--constraints", "mem=1G") + c.Assert(err, gc.ErrorMatches, "cannot use --constraints with subordinate service") +} + +func (s *DeploySuite) TestNumUnits(c *gc.C) { + testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") + err := runDeploy(c, "local:dummy", "-n", "13") + c.Assert(err, jc.ErrorIsNil) + curl := charm.MustParseURL("local:trusty/dummy-1") + s.AssertService(c, "dummy", curl, 13, 0) +} + +func (s *DeploySuite) TestNumUnitsSubordinate(c *gc.C) { + testcharms.Repo.CharmArchivePath(s.SeriesPath, "logging") + err := runDeploy(c, "--num-units", "3", "local:logging") + c.Assert(err, gc.ErrorMatches, "cannot use --num-units or --to with subordinate service") + _, err = s.State.Service("dummy") + c.Assert(err, gc.ErrorMatches, `service "dummy" not found`) +} + +func (s *DeploySuite) assertForceMachine(c *gc.C, machineId string) { + svc, err := s.State.Service("portlandia") + c.Assert(err, jc.ErrorIsNil) + units, err := svc.AllUnits() + c.Assert(err, jc.ErrorIsNil) + c.Assert(units, gc.HasLen, 1) + mid, err := units[0].AssignedMachineId() + c.Assert(err, jc.ErrorIsNil) + c.Assert(mid, gc.Equals, machineId) +} + +func (s *DeploySuite) TestForceMachine(c *gc.C) { + testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") + machine, err := s.State.AddMachine(coretesting.FakeDefaultSeries, state.JobHostUnits) + c.Assert(err, jc.ErrorIsNil) + err = runDeploy(c, "--to", machine.Id(), "local:dummy", "portlandia") + c.Assert(err, jc.ErrorIsNil) + s.assertForceMachine(c, machine.Id()) +} + +func (s *DeploySuite) TestForceMachineExistingContainer(c *gc.C) { + testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") + template := state.MachineTemplate{ + Series: coretesting.FakeDefaultSeries, + Jobs: []state.MachineJob{state.JobHostUnits}, + } + container, err := s.State.AddMachineInsideNewMachine(template, template, instance.LXC) + c.Assert(err, jc.ErrorIsNil) + err = runDeploy(c, "--to", container.Id(), "local:dummy", "portlandia") + c.Assert(err, jc.ErrorIsNil) + s.assertForceMachine(c, container.Id()) + machines, err := s.State.AllMachines() + c.Assert(err, jc.ErrorIsNil) + c.Assert(machines, gc.HasLen, 2) +} + +func (s *DeploySuite) TestForceMachineNewContainer(c *gc.C) { + testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") + machine, err := s.State.AddMachine(coretesting.FakeDefaultSeries, state.JobHostUnits) + c.Assert(err, jc.ErrorIsNil) + err = runDeploy(c, "--to", "lxc:"+machine.Id(), "local:dummy", "portlandia") + c.Assert(err, jc.ErrorIsNil) + s.assertForceMachine(c, machine.Id()+"/lxc/0") + machines, err := s.State.AllMachines() + c.Assert(err, jc.ErrorIsNil) + c.Assert(machines, gc.HasLen, 2) +} + +func (s *DeploySuite) TestForceMachineNotFound(c *gc.C) { + testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") + err := runDeploy(c, "--to", "42", "local:dummy", "portlandia") + c.Assert(err, gc.ErrorMatches, `cannot deploy "portlandia" to machine 42: machine 42 not found`) + _, err = s.State.Service("portlandia") + c.Assert(err, gc.ErrorMatches, `service "portlandia" not found`) +} + +func (s *DeploySuite) TestForceMachineSubordinate(c *gc.C) { + machine, err := s.State.AddMachine(coretesting.FakeDefaultSeries, state.JobHostUnits) + c.Assert(err, jc.ErrorIsNil) + testcharms.Repo.CharmArchivePath(s.SeriesPath, "logging") + err = runDeploy(c, "--to", machine.Id(), "local:logging") + c.Assert(err, gc.ErrorMatches, "cannot use --num-units or --to with subordinate service") + _, err = s.State.Service("dummy") + c.Assert(err, gc.ErrorMatches, `service "dummy" not found`) +} + +func (s *DeploySuite) TestNonLocalCannotHostUnits(c *gc.C) { + err := runDeploy(c, "--to", "0", "local:dummy", "portlandia") + c.Assert(err, gc.Not(gc.ErrorMatches), "machine 0 is the state server for a local environment and cannot host units") +} + +type DeployLocalSuite struct { + testing.RepoSuite +} + +var _ = gc.Suite(&DeployLocalSuite{}) + +func (s *DeployLocalSuite) SetUpTest(c *gc.C) { + s.RepoSuite.SetUpTest(c) + + // override provider type + s.PatchValue(&service.GetClientConfig, func(client service.ServiceAddUnitAPI) (*config.Config, error) { + attrs, err := client.EnvironmentGet() + if err != nil { + return nil, err + } + attrs["type"] = "local" + + return config.New(config.NoDefaults, attrs) + }) +} + +func (s *DeployLocalSuite) TestLocalCannotHostUnits(c *gc.C) { + err := runDeploy(c, "--to", "0", "local:dummy", "portlandia") + c.Assert(err, gc.ErrorMatches, "machine 0 is the state server for a local environment and cannot host units") +} + +// setupConfigFile creates a configuration file for testing set +// with the --config argument specifying a configuration file. +func setupConfigFile(c *gc.C, dir string) string { + ctx := coretesting.ContextForDir(c, dir) + path := ctx.AbsPath("testconfig.yaml") + content := []byte("dummy-service:\n skill-level: 9000\n username: admin001\n\n") + err := ioutil.WriteFile(path, content, 0666) + c.Assert(err, jc.ErrorIsNil) + return path +} + +type DeployCharmStoreSuite struct { + charmStoreSuite +} + +var _ = gc.Suite(&DeployCharmStoreSuite{}) + +var deployAuthorizationTests = []struct { + about string + uploadURL string + deployURL string + readPermUser string + expectError string + expectOutput string +}{{ + about: "public charm, success", + uploadURL: "cs:~bob/trusty/wordpress1-10", + deployURL: "cs:~bob/trusty/wordpress1", + expectOutput: `Added charm "cs:~bob/trusty/wordpress1-10" to the environment.`, +}, { + about: "public charm, fully resolved, success", + uploadURL: "cs:~bob/trusty/wordpress2-10", + deployURL: "cs:~bob/trusty/wordpress2-10", + expectOutput: `Added charm "cs:~bob/trusty/wordpress2-10" to the environment.`, +}, { + about: "non-public charm, success", + uploadURL: "cs:~bob/trusty/wordpress3-10", + deployURL: "cs:~bob/trusty/wordpress3", + readPermUser: clientUserName, + expectOutput: `Added charm "cs:~bob/trusty/wordpress3-10" to the environment.`, +}, { + about: "non-public charm, fully resolved, success", + uploadURL: "cs:~bob/trusty/wordpress4-10", + deployURL: "cs:~bob/trusty/wordpress4-10", + readPermUser: clientUserName, + expectOutput: `Added charm "cs:~bob/trusty/wordpress4-10" to the environment.`, +}, { + about: "non-public charm, access denied", + uploadURL: "cs:~bob/trusty/wordpress5-10", + deployURL: "cs:~bob/trusty/wordpress5", + readPermUser: "bob", + expectError: `cannot resolve charm URL "cs:~bob/trusty/wordpress5": cannot get "/~bob/trusty/wordpress5/meta/any\?include=id": unauthorized: access denied for user "client-username"`, +}, { + about: "non-public charm, fully resolved, access denied", + uploadURL: "cs:~bob/trusty/wordpress6-47", + deployURL: "cs:~bob/trusty/wordpress6-47", + readPermUser: "bob", + expectError: `cannot retrieve charm "cs:~bob/trusty/wordpress6-47": cannot get archive: unauthorized: access denied for user "client-username"`, +}} + +func (s *DeployCharmStoreSuite) TestDeployAuthorization(c *gc.C) { + for i, test := range deployAuthorizationTests { + c.Logf("test %d: %s", i, test.about) + url, _ := s.uploadCharm(c, test.uploadURL, "wordpress") + if test.readPermUser != "" { + s.changeReadPerm(c, url, test.readPermUser) + } + ctx, err := coretesting.RunCommand(c, envcmd.Wrap(&DeployCommand{}), test.deployURL, fmt.Sprintf("wordpress%d", i)) + if test.expectError != "" { + c.Assert(err, gc.ErrorMatches, test.expectError) + continue + } + c.Assert(err, jc.ErrorIsNil) + output := strings.Trim(coretesting.Stderr(ctx), "\n") + c.Assert(output, gc.Equals, test.expectOutput) + } +} + +const ( + // clientUserCookie is the name of the cookie which is + // used to signal to the charmStoreSuite macaroon discharger + // that the client is a juju client rather than the juju environment. + clientUserCookie = "client" + + // clientUserName is the name chosen for the juju client + // when it has authorized. + clientUserName = "client-username" +) + +// charmStoreSuite is a suite fixture that puts the machinery in +// place to allow testing code that calls addCharmViaAPI. +type charmStoreSuite struct { + testing.JujuConnSuite + srv *charmstoretesting.Server + discharger *bakerytest.Discharger +} + +func (s *charmStoreSuite) SetUpTest(c *gc.C) { + s.JujuConnSuite.SetUpTest(c) + + // Set up the third party discharger. + s.discharger = bakerytest.NewDischarger(nil, func(req *http.Request, cond string, arg string) ([]checkers.Caveat, error) { + cookie, err := req.Cookie(clientUserCookie) + if err != nil { + return nil, errors.New("discharge denied to non-clients") + } + return []checkers.Caveat{ + checkers.DeclaredCaveat("username", cookie.Value), + }, nil + }) + + // Set up the charm store testing server. + s.srv = charmstoretesting.OpenServer(c, s.Session, charmstore.ServerParams{ + IdentityLocation: s.discharger.Location(), + PublicKeyLocator: s.discharger, + }) + + // Initialize the charm cache dir. + s.PatchValue(&charmrepo.CacheDir, c.MkDir()) + + // Point the CLI to the charm store testing server. + original := newCharmStoreClient + s.PatchValue(&newCharmStoreClient, func() (*csClient, error) { + csclient, err := original() + if err != nil { + return nil, err + } + csclient.params.URL = s.srv.URL() + // Add a cookie so that the discharger can detect whether the + // HTTP client is the juju environment or the juju client. + lurl, err := url.Parse(s.discharger.Location()) + if err != nil { + panic(err) + } + csclient.params.HTTPClient.Jar.SetCookies(lurl, []*http.Cookie{{ + Name: clientUserCookie, + Value: clientUserName, + }}) + return csclient, nil + }) + + // Point the Juju API server to the charm store testing server. + s.PatchValue(&csclient.ServerURL, s.srv.URL()) +} + +func (s *charmStoreSuite) TearDownTest(c *gc.C) { + s.discharger.Close() + s.srv.Close() + s.JujuConnSuite.TearDownTest(c) +} + +// uploadCharm adds a charm with the given URL and name to the charm store. +func (s *charmStoreSuite) uploadCharm(c *gc.C, url, name string) (*charm.URL, charm.Charm) { + id := charm.MustParseReference(url) + promulgated := false + if id.User == "" { + id.User = "who" + promulgated = true + } + ch := testcharms.Repo.CharmArchive(c.MkDir(), name) + id = s.srv.UploadCharm(c, ch, id, promulgated) + return (*charm.URL)(id), ch +} + +// changeReadPerm changes the read permission of the given charm URL. +// The charm must be present in the testing charm store. +func (s *charmStoreSuite) changeReadPerm(c *gc.C, url *charm.URL, perms ...string) { + err := s.srv.NewClient().Put("/"+url.Path()+"/meta/perm/read", perms) + c.Assert(err, jc.ErrorIsNil) +} + +type testMetricCredentialsSetter struct { + assert func(string, []byte) +} + +func (t *testMetricCredentialsSetter) SetMetricCredentials(serviceName string, data []byte) error { + t.assert(serviceName, data) + return nil +} + +func (t *testMetricCredentialsSetter) Close() error { + return nil +} + +func (s *DeploySuite) TestAddMetricCredentialsDefault(c *gc.C) { + var called bool + setter := &testMetricCredentialsSetter{ + assert: func(serviceName string, data []byte) { + called = true + c.Assert(serviceName, gc.DeepEquals, "metered") + var b []byte + err := json.Unmarshal(data, &b) + c.Assert(err, gc.IsNil) + c.Assert(string(b), gc.Equals, "hello registration") + }, + } + + cleanup := jujutesting.PatchValue(&getMetricCredentialsAPI, func(_ api.Connection) (metricCredentialsAPI, error) { + return setter, nil + }) + defer cleanup() + + handler := &testMetricsRegistrationHandler{} + server := httptest.NewServer(handler) + defer server.Close() + + testcharms.Repo.ClonedDirPath(s.SeriesPath, "metered") + _, err := coretesting.RunCommand(c, envcmd.Wrap(&DeployCommand{RegisterURL: server.URL}), "local:quantal/metered-1") + c.Assert(err, jc.ErrorIsNil) + curl := charm.MustParseURL("local:quantal/metered-1") + s.AssertService(c, "metered", curl, 1, 0) + c.Assert(called, jc.IsTrue) +} + +func (s *DeploySuite) TestAddMetricCredentialsDefaultForUnmeteredCharm(c *gc.C) { + var called bool + setter := &testMetricCredentialsSetter{ + assert: func(serviceName string, data []byte) { + called = true + c.Assert(serviceName, gc.DeepEquals, "dummy") + c.Assert(data, gc.DeepEquals, []byte{}) + }, + } + + cleanup := jujutesting.PatchValue(&getMetricCredentialsAPI, func(_ api.Connection) (metricCredentialsAPI, error) { + return setter, nil + }) + defer cleanup() + + testcharms.Repo.ClonedDirPath(s.SeriesPath, "dummy") + err := runDeploy(c, "local:dummy") + c.Assert(err, jc.ErrorIsNil) + curl := charm.MustParseURL("local:trusty/dummy-1") + s.AssertService(c, "dummy", curl, 1, 0) + c.Assert(called, jc.IsFalse) +} + +func (s *DeploySuite) TestAddMetricCredentialsHttp(c *gc.C) { + handler := &testMetricsRegistrationHandler{} + server := httptest.NewServer(handler) + defer server.Close() + + var called bool + setter := &testMetricCredentialsSetter{ + assert: func(serviceName string, data []byte) { + called = true + c.Assert(serviceName, gc.DeepEquals, "metered") + var b []byte + err := json.Unmarshal(data, &b) + c.Assert(err, gc.IsNil) + c.Assert(string(b), gc.Equals, "hello registration") + }, + } + + cleanup := jujutesting.PatchValue(&getMetricCredentialsAPI, func(_ api.Connection) (metricCredentialsAPI, error) { + return setter, nil + }) + defer cleanup() + + testcharms.Repo.ClonedDirPath(s.SeriesPath, "metered") + _, err := coretesting.RunCommand(c, envcmd.Wrap(&DeployCommand{RegisterURL: server.URL}), "local:quantal/metered-1") + c.Assert(err, jc.ErrorIsNil) + curl := charm.MustParseURL("local:quantal/metered-1") + s.AssertService(c, "metered", curl, 1, 0) + c.Assert(called, jc.IsTrue) + + c.Assert(handler.registrationCalls, gc.HasLen, 1) + c.Assert(handler.registrationCalls[0].CharmURL, gc.DeepEquals, "local:quantal/metered-1") + c.Assert(handler.registrationCalls[0].ServiceName, gc.DeepEquals, "metered") +} + +func (s *DeploySuite) TestDeployCharmsEndpointNotImplemented(c *gc.C) { + + s.PatchValue(®isterMeteredCharm, func(r string, s api.Connection, j *cookiejar.Jar, c string, sv, e string) error { + return ¶ms.Error{"IsMetered", params.CodeNotImplemented} + }) + + testcharms.Repo.ClonedDirPath(s.SeriesPath, "dummy") + _, err := coretesting.RunCommand(c, envcmd.Wrap(&DeployCommand{}), "local:dummy") + c.Assert(err, jc.ErrorIsNil) + c.Check(c.GetTestLog(), jc.Contains, "current state server version does not support charm metering") +} === added file 'src/github.com/juju/juju/cmd/juju/commands/destroyenvironment.go' --- src/github.com/juju/juju/cmd/juju/commands/destroyenvironment.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/destroyenvironment.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,237 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "bufio" + stderrors "errors" + "fmt" + "io" + "strings" + + "github.com/juju/cmd" + "github.com/juju/errors" + "launchpad.net/gnuflag" + + "github.com/juju/juju/api" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/block" + "github.com/juju/juju/environs" + "github.com/juju/juju/environs/config" + "github.com/juju/juju/environs/configstore" + "github.com/juju/juju/juju" +) + +var NoEnvironmentError = stderrors.New("no environment specified") +var DoubleEnvironmentError = stderrors.New("you cannot supply both -e and the envname as a positional argument") + +// DestroyEnvironmentCommand destroys an environment. +type DestroyEnvironmentCommand struct { + envcmd.EnvCommandBase + cmd.CommandBase + envName string + assumeYes bool + force bool +} + +func (c *DestroyEnvironmentCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "destroy-environment", + Args: "", + Purpose: "terminate all machines and other associated resources for an environment", + } +} + +func (c *DestroyEnvironmentCommand) SetFlags(f *gnuflag.FlagSet) { + f.BoolVar(&c.assumeYes, "y", false, "Do not ask for confirmation") + f.BoolVar(&c.assumeYes, "yes", false, "") + f.BoolVar(&c.force, "force", false, "Forcefully destroy the environment, directly through the environment provider") + f.StringVar(&c.envName, "e", "", "juju environment to operate in") + f.StringVar(&c.envName, "environment", "", "juju environment to operate in") +} + +func (c *DestroyEnvironmentCommand) Init(args []string) error { + if c.envName != "" { + logger.Warningf("-e/--environment flag is deprecated in 1.18, " + + "please supply environment as a positional parameter") + // They supplied the -e flag + if len(args) == 0 { + // We're happy, we have enough information + return nil + } + // You can't supply -e ENV and ENV as a positional argument + return DoubleEnvironmentError + } + // No -e flag means they must supply the environment positionally + switch len(args) { + case 0: + return NoEnvironmentError + case 1: + c.envName = args[0] + return nil + default: + return cmd.CheckEmpty(args[1:]) + } +} + +func (c *DestroyEnvironmentCommand) Run(ctx *cmd.Context) (result error) { + store, err := configstore.Default() + if err != nil { + return errors.Annotate(err, "cannot open environment info storage") + } + + cfgInfo, err := store.ReadInfo(c.envName) + if err != nil { + return errors.Annotate(err, "cannot read environment info") + } + + var hasBootstrapCfg bool + var serverEnviron environs.Environ + if bootstrapCfg := cfgInfo.BootstrapConfig(); bootstrapCfg != nil { + hasBootstrapCfg = true + serverEnviron, err = getServerEnv(bootstrapCfg) + if err != nil { + return errors.Trace(err) + } + } + + if c.force { + if hasBootstrapCfg { + // If --force is supplied on a server environment, then don't + // attempt to use the API. This is necessary to destroy broken + // environments, where the API server is inaccessible or faulty. + return environs.Destroy(serverEnviron, store) + } else { + // Force only makes sense on the server environment. + return errors.Errorf("cannot force destroy environment without bootstrap information") + } + } + + apiclient, err := juju.NewAPIClientFromName(c.envName) + if err != nil { + if errors.IsNotFound(err) { + logger.Warningf("environment not found, removing config file") + ctx.Infof("environment not found, removing config file") + return environs.DestroyInfo(c.envName, store) + } + return errors.Annotate(err, "cannot connect to API") + } + defer apiclient.Close() + info, err := apiclient.EnvironmentInfo() + if err != nil { + return errors.Annotate(err, "cannot get information for environment") + } + + if !c.assumeYes { + fmt.Fprintf(ctx.Stdout, destroyEnvMsg, c.envName, info.ProviderType) + + scanner := bufio.NewScanner(ctx.Stdin) + scanner.Scan() + err := scanner.Err() + if err != nil && err != io.EOF { + return errors.Annotate(err, "environment destruction aborted") + } + answer := strings.ToLower(scanner.Text()) + if answer != "y" && answer != "yes" { + return stderrors.New("environment destruction aborted") + } + } + + if info.UUID == info.ServerUUID { + if !hasBootstrapCfg { + // serverEnviron will be nil as we didn't have the jenv bootstrap + // config to build it. But we do have a connection to the API + // server, so get the config from there. + bootstrapCfg, err := apiclient.EnvironmentGet() + if err != nil { + return errors.Annotate(err, "environment destruction failed") + } + serverEnviron, err = getServerEnv(bootstrapCfg) + if err != nil { + return errors.Annotate(err, "environment destruction failed") + } + } + + if err := c.destroyEnv(apiclient); err != nil { + return errors.Annotate(err, "environment destruction failed") + } + if err := environs.Destroy(serverEnviron, store); err != nil { + return errors.Annotate(err, "environment destruction failed") + } + return environs.DestroyInfo(c.envName, store) + } + + // If this is not the server environment, there is no bootstrap info and + // we do not call Destroy on the provider. Destroying the environment via + // the API and cleaning up the jenv file is sufficient. + if err := c.destroyEnv(apiclient); err != nil { + errors.Annotate(err, "cannot destroy environment") + } + return environs.DestroyInfo(c.envName, store) +} + +func getServerEnv(bootstrapCfg map[string]interface{}) (environs.Environ, error) { + cfg, err := config.New(config.NoDefaults, bootstrapCfg) + if err != nil { + return nil, errors.Trace(err) + } + return environs.New(cfg) +} + +func (c *DestroyEnvironmentCommand) destroyEnv(apiclient *api.Client) (result error) { + defer func() { + result = c.ensureUserFriendlyErrorLog(result) + }() + err := apiclient.DestroyEnvironment() + if cmdErr := processDestroyError(err); cmdErr != nil { + return cmdErr + } + + return nil +} + +// processDestroyError determines how to format error message based on its code. +// Note that CodeNotImplemented errors have not be propogated in previous implementation. +// This behaviour was preserved. +func processDestroyError(err error) error { + if err == nil || params.IsCodeNotImplemented(err) { + return nil + } + if params.IsCodeOperationBlocked(err) { + return err + } + return errors.Annotate(err, "destroying environment") +} + +// ensureUserFriendlyErrorLog ensures that error will be logged and displayed +// in a user-friendly manner with readable and digestable error message. +func (c *DestroyEnvironmentCommand) ensureUserFriendlyErrorLog(err error) error { + if err == nil { + return nil + } + if params.IsCodeOperationBlocked(err) { + return block.ProcessBlockedError(err, block.BlockDestroy) + } + logger.Errorf(stdFailureMsg, c.envName) + return err +} + +var destroyEnvMsg = ` +WARNING! this command will destroy the %q environment (type: %s) +This includes all machines, services, data and other resources. + +Continue [y/N]? `[1:] + +var stdFailureMsg = `failed to destroy environment %q + +If the environment is unusable, then you may run + + juju destroy-environment --force + +to forcefully destroy the environment. Upon doing so, review +your environment provider console for any resources that need +to be cleaned up. Using force will also by-pass destroy-envrionment block. + +` === added file 'src/github.com/juju/juju/cmd/juju/commands/destroyenvironment_test.go' --- src/github.com/juju/juju/cmd/juju/commands/destroyenvironment_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/destroyenvironment_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,357 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "bytes" + + "github.com/juju/cmd" + "github.com/juju/errors" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/envcmd" + cmdtesting "github.com/juju/juju/cmd/testing" + "github.com/juju/juju/environs" + "github.com/juju/juju/environs/configstore" + "github.com/juju/juju/instance" + "github.com/juju/juju/juju/testing" + "github.com/juju/juju/provider/dummy" + coretesting "github.com/juju/juju/testing" + "github.com/juju/juju/testing/factory" +) + +type destroyEnvSuite struct { + testing.JujuConnSuite + CmdBlockHelper +} + +func (s *destroyEnvSuite) SetUpTest(c *gc.C) { + s.JujuConnSuite.SetUpTest(c) + s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) + c.Assert(s.CmdBlockHelper, gc.NotNil) + s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) +} + +var _ = gc.Suite(&destroyEnvSuite{}) + +func (s *destroyEnvSuite) TestDestroyEnvironmentCommand(c *gc.C) { + // Prepare the environment so we can destroy it. + _, err := environs.PrepareFromName("dummyenv", envcmd.BootstrapContext(cmdtesting.NullContext(c)), s.ConfigStore) + c.Assert(err, jc.ErrorIsNil) + + // check environment is mandatory + opc, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand)) + c.Check(<-errc, gc.Equals, NoEnvironmentError) + + // normal destroy + opc, errc = cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand), "dummyenv", "--yes") + c.Check(<-errc, gc.IsNil) + c.Check((<-opc).(dummy.OpDestroy).Env, gc.Equals, "dummyenv") + + // Verify that the environment information has been removed. + _, err = s.ConfigStore.ReadInfo("dummyenv") + c.Assert(err, jc.Satisfies, errors.IsNotFound) +} + +// startEnvironment prepare the environment so we can destroy it. +func (s *destroyEnvSuite) startEnvironment(c *gc.C, desiredEnvName string) { + _, err := environs.PrepareFromName(desiredEnvName, envcmd.BootstrapContext(cmdtesting.NullContext(c)), s.ConfigStore) + c.Assert(err, jc.ErrorIsNil) +} + +func (s *destroyEnvSuite) checkDestroyEnvironment(c *gc.C, blocked, force bool) { + //Setup environment + envName := "dummyenv" + s.startEnvironment(c, envName) + if blocked { + s.BlockDestroyEnvironment(c, "checkDestroyEnvironment") + } + opc := make(chan dummy.Operation) + errc := make(chan error) + if force { + opc, errc = cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand), envName, "--yes", "--force") + } else { + opc, errc = cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand), envName, "--yes") + } + if force || !blocked { + c.Check(<-errc, gc.IsNil) + c.Check((<-opc).(dummy.OpDestroy).Env, gc.Equals, envName) + // Verify that the environment information has been removed. + _, err := s.ConfigStore.ReadInfo(envName) + c.Assert(err, jc.Satisfies, errors.IsNotFound) + } else { + c.Check(<-errc, gc.Not(gc.IsNil)) + c.Check((<-opc), gc.IsNil) + // Verify that the environment information has not been removed. + _, err := s.ConfigStore.ReadInfo(envName) + c.Assert(err, jc.ErrorIsNil) + } +} + +func (s *destroyEnvSuite) TestDestroyLockedEnvironment(c *gc.C) { + // lock environment: can't destroy locked environment + s.checkDestroyEnvironment(c, true, false) +} + +func (s *destroyEnvSuite) TestDestroyUnlockedEnvironment(c *gc.C) { + s.checkDestroyEnvironment(c, false, false) +} + +func (s *destroyEnvSuite) TestForceDestroyLockedEnvironment(c *gc.C) { + s.checkDestroyEnvironment(c, true, true) +} + +func (s *destroyEnvSuite) TestForceDestroyUnlockedEnvironment(c *gc.C) { + s.checkDestroyEnvironment(c, false, true) +} + +func (s *destroyEnvSuite) TestDestroyEnvironmentCommandEFlag(c *gc.C) { + // Prepare the environment so we can destroy it. + _, err := environs.PrepareFromName("dummyenv", envcmd.BootstrapContext(cmdtesting.NullContext(c)), s.ConfigStore) + c.Assert(err, jc.ErrorIsNil) + + // check that either environment or the flag is mandatory + opc, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand)) + c.Check(<-errc, gc.Equals, NoEnvironmentError) + + // We don't allow them to supply both entries at the same time + opc, errc = cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand), "-e", "dummyenv", "dummyenv", "--yes") + c.Check(<-errc, gc.Equals, DoubleEnvironmentError) + // We treat --environment the same way + opc, errc = cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand), "--environment", "dummyenv", "dummyenv", "--yes") + c.Check(<-errc, gc.Equals, DoubleEnvironmentError) + + // destroy using the -e flag + opc, errc = cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand), "-e", "dummyenv", "--yes") + c.Check(<-errc, gc.IsNil) + c.Check((<-opc).(dummy.OpDestroy).Env, gc.Equals, "dummyenv") + + // Verify that the environment information has been removed. + _, err = s.ConfigStore.ReadInfo("dummyenv") + c.Assert(err, jc.Satisfies, errors.IsNotFound) +} + +func (s *destroyEnvSuite) TestDestroyEnvironmentCommandEmptyJenv(c *gc.C) { + oldinfo, err := s.ConfigStore.ReadInfo("dummyenv") + info := s.ConfigStore.CreateInfo("dummy-no-bootstrap") + info.SetAPICredentials(oldinfo.APICredentials()) + info.SetAPIEndpoint(oldinfo.APIEndpoint()) + err = info.Write() + c.Assert(err, jc.ErrorIsNil) + + opc, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand), "dummy-no-bootstrap", "--yes") + c.Check(<-errc, gc.IsNil) + c.Check((<-opc).(dummy.OpDestroy).Env, gc.Equals, "dummyenv") + + // Verify that the environment information has been removed. + _, err = s.ConfigStore.ReadInfo("dummyenv") + c.Assert(err, jc.Satisfies, errors.IsNotFound) +} + +func (s *destroyEnvSuite) TestDestroyEnvironmentCommandNonStateServer(c *gc.C) { + s.setupHostedEnviron(c, "dummy-non-state-server") + opc, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand), "dummy-non-state-server", "--yes") + c.Check(<-errc, gc.IsNil) + // Check that there are no operations on the provider, we do not want to call + // Destroy on it. + c.Check(<-opc, gc.IsNil) + + _, err := s.ConfigStore.ReadInfo("dummy-non-state-server") + c.Assert(err, jc.Satisfies, errors.IsNotFound) +} + +func (s *destroyEnvSuite) TestForceDestroyEnvironmentCommandOnNonStateServerFails(c *gc.C) { + s.setupHostedEnviron(c, "dummy-non-state-server") + opc, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand), "dummy-non-state-server", "--yes", "--force") + c.Check(<-errc, gc.ErrorMatches, "cannot force destroy environment without bootstrap information") + c.Check(<-opc, gc.IsNil) + + serverInfo, err := s.ConfigStore.ReadInfo("dummy-non-state-server") + c.Assert(err, jc.ErrorIsNil) + c.Assert(serverInfo, gc.Not(gc.IsNil)) +} + +func (s *destroyEnvSuite) TestForceDestroyEnvironmentCommandOnNonStateServerNoConfimFails(c *gc.C) { + s.setupHostedEnviron(c, "dummy-non-state-server") + opc, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand), "dummy-non-state-server", "--force") + c.Check(<-errc, gc.ErrorMatches, "cannot force destroy environment without bootstrap information") + c.Check(<-opc, gc.IsNil) + + serverInfo, err := s.ConfigStore.ReadInfo("dummy-non-state-server") + c.Assert(err, jc.ErrorIsNil) + c.Assert(serverInfo, gc.Not(gc.IsNil)) +} + +func (s *destroyEnvSuite) TestDestroyEnvironmentCommandTwiceOnNonStateServer(c *gc.C) { + s.setupHostedEnviron(c, "dummy-non-state-server") + oldInfo, err := s.ConfigStore.ReadInfo("dummy-non-state-server") + c.Assert(err, jc.ErrorIsNil) + + opc, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand), "dummy-non-state-server", "--yes") + c.Check(<-errc, gc.IsNil) + c.Check(<-opc, gc.IsNil) + + _, err = s.ConfigStore.ReadInfo("dummy-non-state-server") + c.Assert(err, jc.Satisfies, errors.IsNotFound) + + // Simluate another client calling destroy on the same environment. This + // client will have a local cache of the environ info, so write it back out. + info := s.ConfigStore.CreateInfo("dummy-non-state-server") + info.SetAPIEndpoint(oldInfo.APIEndpoint()) + info.SetAPICredentials(oldInfo.APICredentials()) + err = info.Write() + c.Assert(err, jc.ErrorIsNil) + + // Call destroy again. + context, err := coretesting.RunCommand(c, new(DestroyEnvironmentCommand), "dummy-non-state-server", "--yes") + c.Assert(err, jc.ErrorIsNil) + c.Assert(coretesting.Stderr(context), gc.Equals, "environment not found, removing config file\n") + + // Check that the client's cached info has been removed. + _, err = s.ConfigStore.ReadInfo("dummy-non-state-server") + c.Assert(err, jc.Satisfies, errors.IsNotFound) +} + +func (s *destroyEnvSuite) setupHostedEnviron(c *gc.C, name string) { + st := s.Factory.MakeEnvironment(c, &factory.EnvParams{ + Name: name, + Prepare: true, + ConfigAttrs: coretesting.Attrs{"state-server": false}, + }) + defer st.Close() + + ports, err := st.APIHostPorts() + c.Assert(err, jc.ErrorIsNil) + info := s.ConfigStore.CreateInfo(name) + endpoint := configstore.APIEndpoint{ + CACert: st.CACert(), + EnvironUUID: st.EnvironUUID(), + Addresses: []string{ports[0][0].String()}, + } + info.SetAPIEndpoint(endpoint) + + ssinfo, err := s.ConfigStore.ReadInfo("dummyenv") + c.Assert(err, jc.ErrorIsNil) + info.SetAPICredentials(ssinfo.APICredentials()) + err = info.Write() + c.Assert(err, jc.ErrorIsNil) +} + +func (s *destroyEnvSuite) TestDestroyEnvironmentCommandBroken(c *gc.C) { + oldinfo, err := s.ConfigStore.ReadInfo("dummyenv") + c.Assert(err, jc.ErrorIsNil) + bootstrapConfig := oldinfo.BootstrapConfig() + apiEndpoint := oldinfo.APIEndpoint() + apiCredentials := oldinfo.APICredentials() + err = oldinfo.Destroy() + c.Assert(err, jc.ErrorIsNil) + newinfo := s.ConfigStore.CreateInfo("dummyenv") + + bootstrapConfig["broken"] = "Destroy" + newinfo.SetBootstrapConfig(bootstrapConfig) + newinfo.SetAPIEndpoint(apiEndpoint) + newinfo.SetAPICredentials(apiCredentials) + err = newinfo.Write() + c.Assert(err, jc.ErrorIsNil) + + // Prepare the environment so we can destroy it. + _, err = environs.PrepareFromName("dummyenv", envcmd.BootstrapContext(cmdtesting.NullContext(c)), s.ConfigStore) + c.Assert(err, jc.ErrorIsNil) + + // destroy with broken environment + opc, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand), "dummyenv", "--yes") + op, ok := (<-opc).(dummy.OpDestroy) + c.Assert(ok, jc.IsTrue) + c.Assert(op.Error, gc.ErrorMatches, ".*dummy.Destroy is broken") + c.Check(<-errc, gc.ErrorMatches, ".*dummy.Destroy is broken") + c.Check(<-opc, gc.IsNil) +} + +func (*destroyEnvSuite) TestDestroyEnvironmentCommandConfirmationFlag(c *gc.C) { + com := new(DestroyEnvironmentCommand) + c.Check(coretesting.InitCommand(com, []string{"dummyenv"}), gc.IsNil) + c.Check(com.assumeYes, jc.IsFalse) + + com = new(DestroyEnvironmentCommand) + c.Check(coretesting.InitCommand(com, []string{"dummyenv", "-y"}), gc.IsNil) + c.Check(com.assumeYes, jc.IsTrue) + + com = new(DestroyEnvironmentCommand) + c.Check(coretesting.InitCommand(com, []string{"dummyenv", "--yes"}), gc.IsNil) + c.Check(com.assumeYes, jc.IsTrue) +} + +func (s *destroyEnvSuite) TestDestroyEnvironmentCommandConfirmation(c *gc.C) { + var stdin, stdout bytes.Buffer + ctx, err := cmd.DefaultContext() + c.Assert(err, jc.ErrorIsNil) + ctx.Stdout = &stdout + ctx.Stdin = &stdin + + // Prepare the environment so we can destroy it. + env, err := environs.PrepareFromName("dummyenv", envcmd.BootstrapContext(cmdtesting.NullContext(c)), s.ConfigStore) + c.Assert(err, jc.ErrorIsNil) + + assertEnvironNotDestroyed(c, env, s.ConfigStore) + + // Ensure confirmation is requested if "-y" is not specified. + stdin.WriteString("n") + opc, errc := cmdtesting.RunCommand(ctx, new(DestroyEnvironmentCommand), "dummyenv") + c.Check(<-errc, gc.ErrorMatches, "environment destruction aborted") + c.Check(<-opc, gc.IsNil) + c.Check(stdout.String(), gc.Matches, "WARNING!.*dummyenv.*\\(type: dummy\\)(.|\n)*") + assertEnvironNotDestroyed(c, env, s.ConfigStore) + + // EOF on stdin: equivalent to answering no. + stdin.Reset() + stdout.Reset() + opc, errc = cmdtesting.RunCommand(ctx, new(DestroyEnvironmentCommand), "dummyenv") + c.Check(<-opc, gc.IsNil) + c.Check(<-errc, gc.ErrorMatches, "environment destruction aborted") + assertEnvironNotDestroyed(c, env, s.ConfigStore) + + // "--yes" passed: no confirmation request. + stdin.Reset() + stdout.Reset() + opc, errc = cmdtesting.RunCommand(ctx, new(DestroyEnvironmentCommand), "dummyenv", "--yes") + c.Check(<-errc, gc.IsNil) + c.Check((<-opc).(dummy.OpDestroy).Env, gc.Equals, "dummyenv") + c.Check(stdout.String(), gc.Equals, "") + assertEnvironDestroyed(c, env, s.ConfigStore) + + // Any of casing of "y" and "yes" will confirm. + for _, answer := range []string{"y", "Y", "yes", "YES"} { + // Prepare the environment so we can destroy it. + s.Reset(c) + env, err := environs.PrepareFromName("dummyenv", envcmd.BootstrapContext(cmdtesting.NullContext(c)), s.ConfigStore) + c.Assert(err, jc.ErrorIsNil) + + stdin.Reset() + stdout.Reset() + stdin.WriteString(answer) + opc, errc = cmdtesting.RunCommand(ctx, new(DestroyEnvironmentCommand), "dummyenv") + c.Check(<-errc, gc.IsNil) + c.Check((<-opc).(dummy.OpDestroy).Env, gc.Equals, "dummyenv") + c.Check(stdout.String(), gc.Matches, "WARNING!.*dummyenv.*\\(type: dummy\\)(.|\n)*") + assertEnvironDestroyed(c, env, s.ConfigStore) + } +} + +func assertEnvironDestroyed(c *gc.C, env environs.Environ, store configstore.Storage) { + _, err := store.ReadInfo(env.Config().Name()) + c.Assert(err, jc.Satisfies, errors.IsNotFound) + + _, err = env.Instances([]instance.Id{"invalid"}) + c.Assert(err, gc.ErrorMatches, "environment has been destroyed") +} + +func assertEnvironNotDestroyed(c *gc.C, env environs.Environ, store configstore.Storage) { + info, err := store.ReadInfo(env.Config().Name()) + c.Assert(err, jc.ErrorIsNil) + c.Assert(info.Initialized(), jc.IsTrue) + + _, err = environs.NewFromName(env.Config().Name(), store) + c.Assert(err, jc.ErrorIsNil) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/endpoint.go' --- src/github.com/juju/juju/cmd/juju/commands/endpoint.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/endpoint.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,74 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "github.com/juju/cmd" + "github.com/juju/errors" + "launchpad.net/gnuflag" + + "github.com/juju/juju/cmd/envcmd" +) + +// EndpointCommand returns the API endpoints +type EndpointCommand struct { + envcmd.EnvCommandBase + out cmd.Output + refresh bool + all bool +} + +const endpointDoc = ` +Returns the address(es) of the current API server formatted as host:port. + +Without arguments apt-endpoints returns the last endpoint used to successfully +connect to the API server. If a cached endpoints information is available from +the current environment's .jenv file, it is returned without trying to connect +to the API server. When no cache is available or --refresh is given, api-endpoints +connects to the API server, retrieves all known endpoints and updates the .jenv +file before returning the first one. Example: +$ juju api-endpoints +10.0.3.1:17070 + +If --all is given, api-endpoints returns all known endpoints. Example: +$ juju api-endpoints --all + 10.0.3.1:17070 + localhost:170170 + +The first endpoint is guaranteed to be an IP address and port. If a single endpoint +is available and it's a hostname, juju tries to resolve it locally first. + +Additionally, you can use the --format argument to specify the output format. +Supported formats are: "yaml", "json", or "smart" (default - host:port, one per line). +` + +func (c *EndpointCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "api-endpoints", + Args: "", + Purpose: "print the API server address(es)", + Doc: endpointDoc, + } +} + +func (c *EndpointCommand) SetFlags(f *gnuflag.FlagSet) { + c.out.AddFlags(f, "smart", cmd.DefaultFormatters) + f.BoolVar(&c.refresh, "refresh", false, "connect to the API to ensure an up-to-date endpoint location") + f.BoolVar(&c.all, "all", false, "display all known endpoints, not just the first one") +} + +// Print out the addresses of the API server endpoints. +func (c *EndpointCommand) Run(ctx *cmd.Context) error { + apiendpoint, err := endpoint(c.EnvCommandBase, c.refresh) + if err != nil && !errors.IsNotFound(err) { + return err + } + if errors.IsNotFound(err) || len(apiendpoint.Addresses) == 0 { + return errors.Errorf("no API endpoints available") + } + if c.all { + return c.out.Write(ctx, apiendpoint.Addresses) + } + return c.out.Write(ctx, apiendpoint.Addresses[0:1]) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/endpoint_test.go' --- src/github.com/juju/juju/cmd/juju/commands/endpoint_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/endpoint_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,385 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/environs/config" + "github.com/juju/juju/environs/configstore" + envtesting "github.com/juju/juju/environs/testing" + "github.com/juju/juju/instance" + "github.com/juju/juju/juju/testing" + "github.com/juju/juju/network" + "github.com/juju/juju/provider/dummy" + coretesting "github.com/juju/juju/testing" +) + +type EndpointSuite struct { + testing.JujuConnSuite + + restoreTimeouts func() +} + +var _ = gc.Suite(&EndpointSuite{}) + +func (s *EndpointSuite) SetUpSuite(c *gc.C) { + // Use very short attempt strategies when getting instance addresses. + s.restoreTimeouts = envtesting.PatchAttemptStrategies() + s.JujuConnSuite.SetUpSuite(c) +} + +func (s *EndpointSuite) TearDownSuite(c *gc.C) { + s.JujuConnSuite.TearDownSuite(c) + s.restoreTimeouts() +} + +func (s *EndpointSuite) TestNoEndpoints(c *gc.C) { + // Reset all addresses. + s.setCachedAPIAddresses(c) + s.setServerAPIAddresses(c) + s.assertCachedAddresses(c) + + stdout, stderr, err := s.runCommand(c) + c.Assert(err, gc.ErrorMatches, "no API endpoints available") + c.Assert(stdout, gc.Equals, "") + c.Assert(stderr, gc.Equals, "") + + s.assertCachedAddresses(c) +} + +func (s *EndpointSuite) TestCachedAddressesUsedIfAvailable(c *gc.C) { + addresses := network.NewHostPorts(1234, + "10.0.0.1:1234", + "[2001:db8::1]:1234", + "0.1.2.3:1234", + "[fc00::1]:1234", + ) + // Set the cached addresses. + s.setCachedAPIAddresses(c, addresses...) + // Clear instance/state addresses to ensure we can't connect to + // the API server. + s.setServerAPIAddresses(c) + + testRun := func(i int, envPreferIPv6, bootPreferIPv6 bool) { + c.Logf( + "\ntest %d: prefer-ipv6 environ=%v, bootstrap=%v", + i, envPreferIPv6, bootPreferIPv6, + ) + s.setPreferIPv6EnvironConfig(c, envPreferIPv6) + s.setPreferIPv6BootstrapConfig(c, bootPreferIPv6) + + // Without arguments, verify the first cached address is returned. + s.runAndCheckOutput(c, "smart", expectOutput(addresses[0])) + s.assertCachedAddresses(c, addresses...) + + // With --all, ensure all are returned. + s.runAndCheckOutput(c, "smart", expectOutput(addresses...), "--all") + s.assertCachedAddresses(c, addresses...) + } + + // Ensure regardless of the prefer-ipv6 value we have the same + // result. + for i, envPreferIPv6 := range []bool{true, false} { + for j, bootPreferIPv6 := range []bool{true, false} { + testRun(i+j, envPreferIPv6, bootPreferIPv6) + } + } +} + +func (s *EndpointSuite) TestRefresh(c *gc.C) { + testRun := func(i int, address network.HostPort, explicitRefresh bool) { + c.Logf("\ntest %d: address=%q, explicitRefresh=%v", i, address, explicitRefresh) + + // Cache the address. + s.setCachedAPIAddresses(c, address) + s.assertCachedAddresses(c, address) + // Clear instance/state addresses to ensure only the cached + // one will be used. + s.setServerAPIAddresses(c) + + // Ensure we get and cache the first address (i.e. no changes) + if explicitRefresh { + s.runAndCheckOutput(c, "smart", expectOutput(address), "--refresh") + } else { + s.runAndCheckOutput(c, "smart", expectOutput(address)) + } + s.assertCachedAddresses(c, address) + } + + // Test both IPv4 and IPv6 endpoints separately, first with + // implicit refresh, then explicit. + for i, explicitRefresh := range []bool{true, false} { + for j, addr := range s.addressesWithAPIPort(c, "localhost", "::1") { + testRun(i+j, addr, explicitRefresh) + } + } +} + +func (s *EndpointSuite) TestSortingAndFilteringBeforeCachingRespectsPreferIPv6(c *gc.C) { + // Set the instance/state addresses to a mix of IPv4 and IPv6 + // addresses of all kinds. + addresses := s.addressesWithAPIPort(c, + // The following two are needed to actually connect to the + // test API server. + "127.0.0.1", + "::1", + // Other examples. + "192.0.0.0", + "2001:db8::1", + "169.254.1.2", // link-local - will be removed. + "fd00::1", + "ff01::1", // link-local - will be removed. + "fc00::1", + "localhost", + "0.1.2.3", + "127.0.1.1", // removed as a duplicate. + "::1", // removed as a duplicate. + "10.0.0.1", + "8.8.8.8", + ) + s.setServerAPIAddresses(c, addresses...) + + // Clear cached the address to force a refresh. + s.setCachedAPIAddresses(c) + s.assertCachedAddresses(c) + // Set prefer-ipv6 to true first. + s.setPreferIPv6BootstrapConfig(c, true) + + // Build the expected addresses list, after processing. + expectAddresses := s.addressesWithAPIPort(c, + "127.0.0.1", // This is always on top. + "2001:db8::1", + "0.1.2.3", + "192.0.0.0", + "8.8.8.8", + "localhost", + "fc00::1", + "fd00::1", + "10.0.0.1", + ) + s.runAndCheckOutput(c, "smart", expectOutput(expectAddresses...), "--all") + s.assertCachedAddresses(c, expectAddresses...) + + // Now run it again with prefer-ipv6: false. + // But first reset the cached addresses.. + s.setCachedAPIAddresses(c) + s.assertCachedAddresses(c) + s.setPreferIPv6BootstrapConfig(c, false) + + // Rebuild the expected addresses and rebuild them so IPv4 comes + // before IPv6. + expectAddresses = s.addressesWithAPIPort(c, + "127.0.0.1", // This is always on top. + "0.1.2.3", + "192.0.0.0", + "8.8.8.8", + "2001:db8::1", + "localhost", + "10.0.0.1", + "fc00::1", + "fd00::1", + ) + s.runAndCheckOutput(c, "smart", expectOutput(expectAddresses...), "--all") + s.assertCachedAddresses(c, expectAddresses...) +} + +func (s *EndpointSuite) TestAllFormats(c *gc.C) { + addresses := s.addressesWithAPIPort(c, + "127.0.0.1", + "8.8.8.8", + "2001:db8::1", + "::1", + "10.0.0.1", + "fc00::1", + ) + s.setServerAPIAddresses(c) + s.setCachedAPIAddresses(c, addresses...) + s.assertCachedAddresses(c, addresses...) + + for i, test := range []struct { + about string + args []string + format string + output []network.HostPort + }{{ + about: "default format (smart), no args", + format: "smart", + output: addresses[0:1], + }, { + about: "default format (smart), with --all", + args: []string{"--all"}, + format: "smart", + output: addresses, + }, { + about: "JSON format, without --all", + args: []string{"--format", "json"}, + format: "json", + output: addresses[0:1], + }, { + about: "JSON format, with --all", + args: []string{"--format", "json", "--all"}, + format: "json", + output: addresses, + }, { + about: "YAML format, without --all", + args: []string{"--format", "yaml"}, + format: "yaml", + output: addresses[0:1], + }, { + about: "YAML format, with --all", + args: []string{"--format", "yaml", "--all"}, + format: "yaml", + output: addresses, + }} { + c.Logf("\ntest %d: %s", i, test.about) + s.runAndCheckOutput(c, test.format, expectOutput(test.output...), test.args...) + } +} + +// runCommand runs the api-endpoints command with the given arguments +// and returns the output and any error. +func (s *EndpointSuite) runCommand(c *gc.C, args ...string) (string, string, error) { + command := &EndpointCommand{} + ctx, err := coretesting.RunCommand(c, envcmd.Wrap(command), args...) + if err != nil { + return "", "", err + } + return coretesting.Stdout(ctx), coretesting.Stderr(ctx), nil +} + +// runAndCheckOutput runs api-endpoints expecting no error and +// compares the output for the given format. +func (s *EndpointSuite) runAndCheckOutput(c *gc.C, format string, output []interface{}, args ...string) { + stdout, stderr, err := s.runCommand(c, args...) + if !c.Check(err, jc.ErrorIsNil) { + return + } + c.Check(stderr, gc.Equals, "") + switch format { + case "smart": + strOutput := "" + for _, line := range output { + strOutput += line.(string) + "\n" + } + c.Check(stdout, gc.Equals, strOutput) + case "json": + c.Check(stdout, jc.JSONEquals, output) + case "yaml": + c.Check(stdout, jc.YAMLEquals, output) + default: + c.Fatalf("unexpected format %q", format) + } +} + +// getStoreInfo returns the current environment's EnvironInfo. +func (s *EndpointSuite) getStoreInfo(c *gc.C) configstore.EnvironInfo { + env, err := s.State.Environment() + c.Assert(err, jc.ErrorIsNil) + info, err := s.ConfigStore.ReadInfo(env.Name()) + c.Assert(err, jc.ErrorIsNil) + return info +} + +// setPreferIPv6EnvironConfig sets the "prefer-ipv6" environment +// setting to given value. +func (s *EndpointSuite) setPreferIPv6EnvironConfig(c *gc.C, value bool) { + // Technically, because prefer-ipv6 is an immutable setting, what + // follows should be impossible, but the dummy provider doesn't + // seem to validate the new config against the current (old) one + // when calling SetConfig(). + allAttrs := s.Environ.Config().AllAttrs() + allAttrs["prefer-ipv6"] = value + cfg, err := config.New(config.NoDefaults, allAttrs) + c.Assert(err, jc.ErrorIsNil) + err = s.Environ.SetConfig(cfg) + c.Assert(err, jc.ErrorIsNil) + setValue := cfg.AllAttrs()["prefer-ipv6"].(bool) + c.Logf("environ config prefer-ipv6 set to %v", setValue) +} + +// setPreferIPv6BootstrapConfig sets the "prefer-ipv6" setting to the +// given value on the current environment's bootstrap config by +// recreating it (the only way to change bootstrap config once set). +func (s *EndpointSuite) setPreferIPv6BootstrapConfig(c *gc.C, value bool) { + currentInfo := s.getStoreInfo(c) + endpoint := currentInfo.APIEndpoint() + creds := currentInfo.APICredentials() + bootstrapConfig := currentInfo.BootstrapConfig() + delete(bootstrapConfig, "prefer-ipv6") + + // The only way to change the bootstrap config is to recreate the + // info. + err := currentInfo.Destroy() + c.Assert(err, jc.ErrorIsNil) + newInfo := s.ConfigStore.CreateInfo(s.Environ.Config().Name()) + newInfo.SetAPICredentials(creds) + newInfo.SetAPIEndpoint(endpoint) + newCfg := make(coretesting.Attrs) + newCfg["prefer-ipv6"] = value + newInfo.SetBootstrapConfig(newCfg.Merge(bootstrapConfig)) + err = newInfo.Write() + c.Assert(err, jc.ErrorIsNil) + setValue := newInfo.BootstrapConfig()["prefer-ipv6"].(bool) + c.Logf("bootstrap config prefer-ipv6 set to %v", setValue) +} + +// setCachedAPIAddresses sets the given addresses on the cached +// EnvironInfo endpoint. APIEndpoint.Hostnames are not touched, +// because the interactions between Addresses and Hostnames are +// separately tested in juju/api_test.go +func (s *EndpointSuite) setCachedAPIAddresses(c *gc.C, addresses ...network.HostPort) { + info := s.getStoreInfo(c) + endpoint := info.APIEndpoint() + endpoint.Addresses = network.HostPortsToStrings(addresses) + info.SetAPIEndpoint(endpoint) + err := info.Write() + c.Assert(err, jc.ErrorIsNil) + c.Logf("cached addresses set to %v", info.APIEndpoint().Addresses) +} + +// setServerAPIAddresses sets the given addresses on the dummy +// bootstrap instance and in state. +func (s *EndpointSuite) setServerAPIAddresses(c *gc.C, addresses ...network.HostPort) { + insts, err := s.Environ.Instances([]instance.Id{dummy.BootstrapInstanceId}) + c.Assert(err, jc.ErrorIsNil) + err = s.State.SetAPIHostPorts([][]network.HostPort{addresses}) + c.Assert(err, jc.ErrorIsNil) + dummy.SetInstanceAddresses(insts[0], network.HostsWithoutPort(addresses)) + instAddrs, err := insts[0].Addresses() + c.Assert(err, jc.ErrorIsNil) + stateAddrs, err := s.State.APIHostPorts() + c.Assert(err, jc.ErrorIsNil) + c.Logf("instance addresses set to %v", instAddrs) + c.Logf("state addresses set to %v", stateAddrs) +} + +// addressesWithAPIPort returns the given addresses appending the test +// API server listening port to each one. +func (s *EndpointSuite) addressesWithAPIPort(c *gc.C, addresses ...string) []network.HostPort { + apiPort := s.Environ.Config().APIPort() + return network.NewHostPorts(apiPort, addresses...) +} + +// assertCachedAddresses ensures the endpoint addresses (not +// hostnames) stored in the store match the given ones. +// APIEndpoint.Hostnames and APIEndpoint.Addresses interactions are +// separately testing in juju/api_test.go. +func (s *EndpointSuite) assertCachedAddresses(c *gc.C, addresses ...network.HostPort) { + info := s.getStoreInfo(c) + strAddresses := network.HostPortsToStrings(addresses) + c.Assert(info.APIEndpoint().Addresses, jc.DeepEquals, strAddresses) +} + +// expectOutput is a helper used to construct the expected ouput +// argument to runAndCheckOutput. +func expectOutput(addresses ...network.HostPort) []interface{} { + result := make([]interface{}, len(addresses)) + for i, addr := range addresses { + result[i] = addr.NetAddr() + } + return result +} === added file 'src/github.com/juju/juju/cmd/juju/commands/ensureavailability.go' --- src/github.com/juju/juju/cmd/juju/commands/ensureavailability.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/ensureavailability.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,245 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "bytes" + "fmt" + "strings" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/names" + "launchpad.net/gnuflag" + + "github.com/juju/juju/api/highavailability" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/block" + "github.com/juju/juju/constraints" + "github.com/juju/juju/instance" +) + +// EnsureAvailabilityCommand makes the system highly available. +type EnsureAvailabilityCommand struct { + envcmd.EnvCommandBase + out cmd.Output + haClient EnsureAvailabilityClient + + // NumStateServers specifies the number of state servers to make available. + NumStateServers 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 state servers. If there are more state servers 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 +} + +const ensureAvailabilityDoc = ` +To ensure availability of deployed services, the Juju infrastructure +must itself be highly available. Ensure-availability must be called +to ensure that the specified number of state servers are made available. + +An odd number of state servers is required. + +Examples: + juju ensure-availability + Ensure that the system is still in highly available mode. If + there is only 1 state server running, this will ensure there + are 3 running. If you have previously requested more than 3, + then that number will be ensured. + juju ensure-availability -n 5 --series=trusty + Ensure that 5 state servers are available, with newly created + state server machines having the "trusty" series. + juju ensure-availability -n 7 --constraints mem=8G + Ensure that 7 state servers are available, with newly created + state server machines having the default series, and at least + 8GB RAM. + juju ensure-availability -n 7 --to server1,server2 --constraints mem=8G + Ensure that 7 state servers are available, with machines server1 and + server2 used first, and if necessary, newly created state server + machines having the default series, and at least 8GB RAM. +` + +// formatSimple marshals value to a yaml-formatted []byte, unless value is nil. +func formatSimple(value interface{}) ([]byte, error) { + ensureAvailabilityResult, ok := value.(availabilityInfo) + if !ok { + return nil, fmt.Errorf("unexpected result type for ensure-availability call: %T", value) + } + + var buf bytes.Buffer + + for _, machineList := range []struct { + message string + list []string + }{ + { + "maintaining machines: %s\n", + ensureAvailabilityResult.Maintained, + }, + { + "adding machines: %s\n", + ensureAvailabilityResult.Added, + }, + { + "removing machines: %s\n", + ensureAvailabilityResult.Removed, + }, + { + "promoting machines: %s\n", + ensureAvailabilityResult.Promoted, + }, + { + "demoting machines: %s\n", + ensureAvailabilityResult.Demoted, + }, + { + "converting machines: %s\n", + ensureAvailabilityResult.Converted, + }, + } { + if len(machineList.list) == 0 { + continue + } + _, err := fmt.Fprintf(&buf, machineList.message, strings.Join(machineList.list, ", ")) + if err != nil { + return nil, err + } + } + + return buf.Bytes(), nil +} + +func (c *EnsureAvailabilityCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "ensure-availability", + Purpose: "ensure that sufficient state servers exist to provide redundancy", + Doc: ensureAvailabilityDoc, + } +} + +func (c *EnsureAvailabilityCommand) SetFlags(f *gnuflag.FlagSet) { + f.IntVar(&c.NumStateServers, "n", 0, "number of state servers to make available") + f.StringVar(&c.Series, "series", "", "the charm series") + f.StringVar(&c.PlacementSpec, "to", "", "the machine(s) to become state servers, bypasses constraints") + f.Var(constraints.ConstraintsValue{&c.Constraints}, "constraints", "additional machine constraints") + c.out.AddFlags(f, "simple", map[string]cmd.Formatter{ + "yaml": cmd.FormatYaml, + "json": cmd.FormatJson, + "simple": formatSimple, + }) + +} + +func (c *EnsureAvailabilityCommand) Init(args []string) error { + if c.NumStateServers < 0 || (c.NumStateServers%2 != 1 && c.NumStateServers != 0) { + return fmt.Errorf("must specify a number of state servers odd and non-negative") + } + if c.PlacementSpec != "" { + placementSpecs := strings.Split(c.PlacementSpec, ",") + c.Placement = make([]string, len(placementSpecs)) + for i, spec := range placementSpecs { + p, err := instance.ParsePlacement(strings.TrimSpace(spec)) + if err == nil && names.IsContainerMachine(p.Directive) { + return errors.New("ensure-availability cannot be used with container placement directives") + } + if err == nil && p.Scope == instance.MachineScope { + // Targeting machines is ok. + c.Placement[i] = p.String() + continue + } + if err != instance.ErrPlacementScopeMissing { + return fmt.Errorf("unsupported ensure-availability placement directive %q", spec) + } + c.Placement[i] = spec + } + } + return cmd.CheckEmpty(args) +} + +type availabilityInfo struct { + Maintained []string `json:"maintained,omitempty" yaml:"maintained,flow,omitempty"` + Removed []string `json:"removed,omitempty" yaml:"removed,flow,omitempty"` + Added []string `json:"added,omitempty" yaml:"added,flow,omitempty"` + Promoted []string `json:"promoted,omitempty" yaml:"promoted,flow,omitempty"` + Demoted []string `json:"demoted,omitempty" yaml:"demoted,flow,omitempty"` + Converted []string `json:"converted,omitempty" yaml:"converted,flow,omitempty"` +} + +// EnsureAvailabilityClient defines the methods +// on the client api that the ensure availability +// command calls. +type EnsureAvailabilityClient interface { + Close() error + EnsureAvailability( + numStateServers int, cons constraints.Value, series string, + placement []string) (params.StateServersChanges, error) +} + +func (c *EnsureAvailabilityCommand) getHAClient() (EnsureAvailabilityClient, error) { + if c.haClient != nil { + return c.haClient, nil + } + + root, err := c.NewAPIRoot() + if err != nil { + return nil, errors.Annotate(err, "cannot get API connection") + } + + // NewClient does not return an error, so we'll return nil + return highavailability.NewClient(root), nil +} + +// Run connects to the environment specified on the command line +// and calls EnsureAvailability. +func (c *EnsureAvailabilityCommand) Run(ctx *cmd.Context) error { + haClient, err := c.getHAClient() + if err != nil { + return err + } + + defer haClient.Close() + ensureAvailabilityResult, err := haClient.EnsureAvailability( + c.NumStateServers, + c.Constraints, + c.Series, + c.Placement, + ) + if err != nil { + return block.ProcessBlockedError(err, block.BlockChange) + } + + result := availabilityInfo{ + Added: machineTagsToIds(ensureAvailabilityResult.Added...), + Removed: machineTagsToIds(ensureAvailabilityResult.Removed...), + Maintained: machineTagsToIds(ensureAvailabilityResult.Maintained...), + Promoted: machineTagsToIds(ensureAvailabilityResult.Promoted...), + Demoted: machineTagsToIds(ensureAvailabilityResult.Demoted...), + Converted: machineTagsToIds(ensureAvailabilityResult.Converted...), + } + return c.out.Write(ctx, result) +} + +// Convert machine tags to ids, skipping any non-machine tags. +func machineTagsToIds(tags ...string) []string { + var result []string + + for _, rawTag := range tags { + tag, err := names.ParseTag(rawTag) + if err != nil { + continue + } + result = append(result, tag.Id()) + } + return result +} === added file 'src/github.com/juju/juju/cmd/juju/commands/ensureavailability_test.go' --- src/github.com/juju/juju/cmd/juju/commands/ensureavailability_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/ensureavailability_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,277 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + + "github.com/juju/cmd" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + goyaml "gopkg.in/yaml.v1" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/constraints" + "github.com/juju/juju/instance" + "github.com/juju/juju/juju/testing" + "github.com/juju/juju/state" + coretesting "github.com/juju/juju/testing" + "github.com/juju/juju/testing/factory" +) + +type EnsureAvailabilitySuite struct { + // TODO (cherylj) change this back to a FakeJujuHomeSuite to + // remove the mongo dependency once ensure-availability is + // moved under a supercommand again. + testing.JujuConnSuite + fake *fakeHAClient +} + +// invalidNumServers is a number of state servers that would +// never be generated by the ensure-availability command. +const invalidNumServers = -2 + +func (s *EnsureAvailabilitySuite) SetUpTest(c *gc.C) { + s.JujuConnSuite.SetUpTest(c) + + // Initialize numStateServers to an invalid number to validate + // that ensure-availability doesn't call into the API when its + // pre-checks fail + s.fake = &fakeHAClient{numStateServers: invalidNumServers} +} + +type fakeHAClient struct { + numStateServers int + cons constraints.Value + err error + series string + placement []string + result params.StateServersChanges +} + +func (f *fakeHAClient) Close() error { + return nil +} + +func (f *fakeHAClient) EnsureAvailability(numStateServers int, cons constraints.Value, + series string, placement []string) (params.StateServersChanges, error) { + + f.numStateServers = numStateServers + f.cons = cons + f.series = series + f.placement = placement + + if f.err != nil { + return f.result, f.err + } + + if numStateServers == 1 { + return f.result, nil + } + + // In the real HAClient, specifying a numStateServers value of 0 + // indicates that the default value (3) should be used + if numStateServers == 0 { + numStateServers = 3 + } + + f.result.Maintained = append(f.result.Maintained, "machine-0") + + for _, p := range placement { + m, err := instance.ParsePlacement(p) + if err == nil && m.Scope == instance.MachineScope { + f.result.Converted = append(f.result.Converted, "machine-"+m.Directive) + } + } + + // We may need to pretend that we added some machines. + for i := len(f.result.Converted) + 1; i < numStateServers; i++ { + f.result.Added = append(f.result.Added, fmt.Sprintf("machine-%d", i)) + } + + return f.result, nil +} + +var _ = gc.Suite(&EnsureAvailabilitySuite{}) + +func (s *EnsureAvailabilitySuite) runEnsureAvailability(c *gc.C, args ...string) (*cmd.Context, error) { + command := &EnsureAvailabilityCommand{haClient: s.fake} + return coretesting.RunCommand(c, envcmd.Wrap(command), args...) +} + +func (s *EnsureAvailabilitySuite) TestEnsureAvailability(c *gc.C) { + ctx, err := s.runEnsureAvailability(c, "-n", "1") + c.Assert(err, jc.ErrorIsNil) + c.Assert(coretesting.Stdout(ctx), gc.Equals, "") + + c.Assert(s.fake.numStateServers, 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) +} + +func (s *EnsureAvailabilitySuite) TestBlockEnsureAvailability(c *gc.C) { + s.fake.err = common.ErrOperationBlocked("TestBlockEnsureAvailability") + _, err := s.runEnsureAvailability(c, "-n", "1") + c.Assert(err, gc.ErrorMatches, cmd.ErrSilent.Error()) + + // msg is logged + stripped := strings.Replace(c.GetTestLog(), "\n", "", -1) + c.Check(stripped, gc.Matches, ".*TestBlockEnsureAvailability.*") +} + +func (s *EnsureAvailabilitySuite) TestEnsureAvailabilityFormatYaml(c *gc.C) { + expected := map[string][]string{ + "maintained": {"0"}, + "added": {"1", "2"}, + } + + ctx, err := s.runEnsureAvailability(c, "-n", "3", "--format", "yaml") + c.Assert(err, jc.ErrorIsNil) + + c.Assert(s.fake.numStateServers, 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 + err = goyaml.Unmarshal(ctx.Stdout.(*bytes.Buffer).Bytes(), &result) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, gc.DeepEquals, expected) +} + +func (s *EnsureAvailabilitySuite) TestEnsureAvailabilityFormatJson(c *gc.C) { + expected := map[string][]string{ + "maintained": {"0"}, + "added": {"1", "2"}, + } + + ctx, err := s.runEnsureAvailability(c, "-n", "3", "--format", "json") + c.Assert(err, jc.ErrorIsNil) + + c.Assert(s.fake.numStateServers, 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 + err = json.Unmarshal(ctx.Stdout.(*bytes.Buffer).Bytes(), &result) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, gc.DeepEquals, expected) +} + +func (s *EnsureAvailabilitySuite) TestEnsureAvailabilityWithSeries(c *gc.C) { + // Also test with -n 5 to validate numbers other than 1 and 3 + ctx, err := s.runEnsureAvailability(c, "--series", "series", "-n", "5") + c.Assert(err, jc.ErrorIsNil) + c.Assert(coretesting.Stdout(ctx), gc.Equals, + "maintaining machines: 0\n"+ + "adding machines: 1, 2, 3, 4\n\n") + + c.Assert(s.fake.numStateServers, 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) +} + +func (s *EnsureAvailabilitySuite) TestEnsureAvailabilityWithConstraints(c *gc.C) { + ctx, err := s.runEnsureAvailability(c, "--constraints", "mem=4G", "-n", "3") + c.Assert(err, jc.ErrorIsNil) + c.Assert(coretesting.Stdout(ctx), gc.Equals, + "maintaining machines: 0\n"+ + "adding machines: 1, 2\n\n") + + c.Assert(s.fake.numStateServers, 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) +} + +func (s *EnsureAvailabilitySuite) TestEnsureAvailabilityWithPlacement(c *gc.C) { + ctx, err := s.runEnsureAvailability(c, "--to", "valid", "-n", "3") + c.Assert(err, jc.ErrorIsNil) + c.Assert(coretesting.Stdout(ctx), gc.Equals, + "maintaining machines: 0\n"+ + "adding machines: 1, 2\n\n") + + c.Assert(s.fake.numStateServers, 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) +} + +func (s *EnsureAvailabilitySuite) TestEnsureAvailabilityErrors(c *gc.C) { + for _, n := range []int{-1, 2} { + _, err := s.runEnsureAvailability(c, "-n", fmt.Sprint(n)) + c.Assert(err, gc.ErrorMatches, "must specify a number of state servers odd and non-negative") + } + + // Verify that ensure-availability didn't call into the API + c.Assert(s.fake.numStateServers, gc.Equals, invalidNumServers) +} + +func (s *EnsureAvailabilitySuite) TestEnsureAvailabilityAllows0(c *gc.C) { + // If the number of state servers is specified as "0", the API will + // then use the default number of 3. + ctx, err := s.runEnsureAvailability(c, "-n", "0") + c.Assert(err, jc.ErrorIsNil) + c.Assert(coretesting.Stdout(ctx), gc.Equals, + "maintaining machines: 0\n"+ + "adding machines: 1, 2\n\n") + + c.Assert(s.fake.numStateServers, 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) +} + +func (s *EnsureAvailabilitySuite) TestEnsureAvailabilityDefaultsTo0(c *gc.C) { + // If the number of state servers is not specified, we pass in 0 to the + // API. The API will then use the default number of 3. + ctx, err := s.runEnsureAvailability(c) + c.Assert(err, jc.ErrorIsNil) + c.Assert(coretesting.Stdout(ctx), gc.Equals, + "maintaining machines: 0\n"+ + "adding machines: 1, 2\n\n") + + c.Assert(s.fake.numStateServers, 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) +} + +func (s *EnsureAvailabilitySuite) TestEnsureAvailabilityEndToEnd(c *gc.C) { + s.Factory.MakeMachine(c, &factory.MachineParams{ + Jobs: []state.MachineJob{state.JobManageEnviron}, + }) + ctx, err := coretesting.RunCommand(c, envcmd.Wrap(&EnsureAvailabilityCommand{}), "-n", "3") + c.Assert(err, jc.ErrorIsNil) + + // Machine 0 is demoted because it hasn't reported its presence + c.Assert(coretesting.Stdout(ctx), gc.Equals, + "adding machines: 1, 2, 3\n"+ + "demoting machines: 0\n\n") +} + +func (s *EnsureAvailabilitySuite) TestEnsureAvailabilityToExisting(c *gc.C) { + ctx, err := s.runEnsureAvailability(c, "--to", "1,2") + c.Assert(err, jc.ErrorIsNil) + c.Check(coretesting.Stdout(ctx), gc.Equals, ` +maintaining machines: 0 +converting machines: 1, 2 + +`[1:]) + + c.Check(s.fake.numStateServers, 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) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/expose.go' --- src/github.com/juju/juju/cmd/juju/commands/expose.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/expose.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,53 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "errors" + + "github.com/juju/cmd" + + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/block" +) + +// ExposeCommand is responsible exposing services. +type ExposeCommand struct { + envcmd.EnvCommandBase + ServiceName string +} + +var jujuExposeHelp = ` +Adjusts firewall rules and similar security mechanisms of the provider, to +allow the service to be accessed on its public address. + +` + +func (c *ExposeCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "expose", + Args: "", + Purpose: "expose a service", + Doc: jujuExposeHelp, + } +} + +func (c *ExposeCommand) Init(args []string) error { + if len(args) == 0 { + return errors.New("no service name specified") + } + c.ServiceName = args[0] + return cmd.CheckEmpty(args[1:]) +} + +// Run changes the juju-managed firewall to expose any +// ports that were also explicitly marked by units as open. +func (c *ExposeCommand) Run(_ *cmd.Context) error { + client, err := c.NewAPIClient() + if err != nil { + return err + } + defer client.Close() + return block.ProcessBlockedError(client.ServiceExpose(c.ServiceName), block.BlockChange) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/expose_test.go' --- src/github.com/juju/juju/cmd/juju/commands/expose_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/expose_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,70 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/juju/charm.v5" + + "github.com/juju/juju/cmd/envcmd" + jujutesting "github.com/juju/juju/juju/testing" + "github.com/juju/juju/testcharms" + "github.com/juju/juju/testing" +) + +type ExposeSuite struct { + jujutesting.RepoSuite + CmdBlockHelper +} + +func (s *ExposeSuite) SetUpTest(c *gc.C) { + s.RepoSuite.SetUpTest(c) + s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) + c.Assert(s.CmdBlockHelper, gc.NotNil) + s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) +} + +var _ = gc.Suite(&ExposeSuite{}) + +func runExpose(c *gc.C, args ...string) error { + _, err := testing.RunCommand(c, envcmd.Wrap(&ExposeCommand{}), args...) + return err +} + +func (s *ExposeSuite) assertExposed(c *gc.C, service string) { + svc, err := s.State.Service(service) + c.Assert(err, jc.ErrorIsNil) + exposed := svc.IsExposed() + c.Assert(exposed, jc.IsTrue) +} + +func (s *ExposeSuite) TestExpose(c *gc.C) { + testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") + err := runDeploy(c, "local:dummy", "some-service-name") + c.Assert(err, jc.ErrorIsNil) + curl := charm.MustParseURL("local:trusty/dummy-1") + s.AssertService(c, "some-service-name", curl, 1, 0) + + err = runExpose(c, "some-service-name") + c.Assert(err, jc.ErrorIsNil) + s.assertExposed(c, "some-service-name") + + err = runExpose(c, "nonexistent-service") + c.Assert(err, gc.ErrorMatches, `service "nonexistent-service" not found`) +} + +func (s *ExposeSuite) TestBlockExpose(c *gc.C) { + testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") + err := runDeploy(c, "local:dummy", "some-service-name") + c.Assert(err, jc.ErrorIsNil) + curl := charm.MustParseURL("local:trusty/dummy-1") + s.AssertService(c, "some-service-name", curl, 1, 0) + + // Block operation + s.BlockAllChanges(c, "TestBlockExpose") + + err = runExpose(c, "some-service-name") + s.AssertBlocked(c, err, ".*TestBlockExpose.*") +} === added file 'src/github.com/juju/juju/cmd/juju/commands/flags.go' --- src/github.com/juju/juju/cmd/juju/commands/flags.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/flags.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,43 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "fmt" + "strings" + + "github.com/juju/errors" + + "github.com/juju/juju/storage" +) + +type storageFlag struct { + stores *map[string]storage.Constraints +} + +// Set implements gnuflag.Value.Set. +func (f storageFlag) Set(s string) error { + fields := strings.SplitN(s, "=", 2) + if len(fields) < 2 { + return errors.New("expected =") + } + cons, err := storage.ParseConstraints(fields[1]) + if err != nil { + return errors.Annotate(err, "cannot parse disk constraints") + } + if *f.stores == nil { + *f.stores = make(map[string]storage.Constraints) + } + (*f.stores)[fields[0]] = cons + return nil +} + +// Set implements gnuflag.Value.String. +func (f storageFlag) String() string { + strs := make([]string, 0, len(*f.stores)) + for store, cons := range *f.stores { + strs = append(strs, fmt.Sprintf("%s=%v", store, cons)) + } + return strings.Join(strs, " ") +} === added file 'src/github.com/juju/juju/cmd/juju/commands/get.go' --- src/github.com/juju/juju/cmd/juju/commands/get.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/get.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,92 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "errors" + + "github.com/juju/cmd" + "launchpad.net/gnuflag" + + "github.com/juju/juju/cmd/envcmd" +) + +// GetCommand retrieves the configuration of a service. +type GetCommand struct { + envcmd.EnvCommandBase + ServiceName string + out cmd.Output +} + +const getDoc = ` +The command output includes the service and charm names, a detailed list of all config +settings for , including the setting name, whether it uses the default value +or not ("default: true"), description (if set), type, and current value. Example: + +$ juju get wordpress + +charm: wordpress +service: wordpress +settings: + engine: + default: true + description: 'Currently two ...' + type: string + value: nginx + tuning: + description: "This is the tuning level..." + type: string + value: optimized + +NOTE: In the example above the descriptions and most other settings were omitted for +brevity. The "engine" setting was left at its default value ("nginx"), while the +"tuning" setting was set to "optimized" (the default value is "single"). +` + +func (c *GetCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "get", + Args: "", + Purpose: "get service configuration options", + Doc: getDoc, + } +} + +func (c *GetCommand) SetFlags(f *gnuflag.FlagSet) { + // TODO(dfc) add json formatting ? + c.out.AddFlags(f, "yaml", map[string]cmd.Formatter{ + "yaml": cmd.FormatYaml, + }) +} + +func (c *GetCommand) Init(args []string) error { + // TODO(dfc) add --schema-only + if len(args) == 0 { + return errors.New("no service name specified") + } + c.ServiceName = args[0] + return cmd.CheckEmpty(args[1:]) +} + +// Run fetches the configuration of the service and formats +// the result as a YAML string. +func (c *GetCommand) Run(ctx *cmd.Context) error { + client, err := c.NewAPIClient() + if err != nil { + return err + } + defer client.Close() + + results, err := client.ServiceGet(c.ServiceName) + if err != nil { + return err + } + + resultsMap := map[string]interface{}{ + "service": results.Service, + "charm": results.Charm, + "settings": results.Config, + } + return c.out.Write(ctx, resultsMap) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/get_test.go' --- src/github.com/juju/juju/cmd/juju/commands/get_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/get_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,89 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "bytes" + + "github.com/juju/cmd" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/juju/charm.v5" + goyaml "gopkg.in/yaml.v1" + + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/juju/testing" + coretesting "github.com/juju/juju/testing" +) + +type GetSuite struct { + testing.JujuConnSuite +} + +var _ = gc.Suite(&GetSuite{}) + +var getTests = []struct { + service string + expected map[string]interface{} +}{ + { + "dummy-service", + map[string]interface{}{ + "service": "dummy-service", + "charm": "dummy", + "settings": map[string]interface{}{ + "title": map[string]interface{}{ + "description": "A descriptive title used for the service.", + "type": "string", + "value": "Nearly There", + }, + "skill-level": map[string]interface{}{ + "description": "A number indicating skill.", + "type": "int", + "default": true, + }, + "username": map[string]interface{}{ + "description": "The name of the initial account (given admin permissions).", + "type": "string", + "value": "admin001", + "default": true, + }, + "outlook": map[string]interface{}{ + "description": "No default outlook.", + "type": "string", + "default": true, + }, + }, + }, + }, + + // TODO(dfc) add additional services (need more charms) + // TODO(dfc) add set tests +} + +func (s *GetSuite) TestGetConfig(c *gc.C) { + sch := s.AddTestingCharm(c, "dummy") + svc := s.AddTestingService(c, "dummy-service", sch) + err := svc.UpdateConfigSettings(charm.Settings{"title": "Nearly There"}) + c.Assert(err, jc.ErrorIsNil) + for _, t := range getTests { + ctx := coretesting.Context(c) + code := cmd.Main(envcmd.Wrap(&GetCommand{}), ctx, []string{t.service}) + c.Check(code, gc.Equals, 0) + c.Assert(ctx.Stderr.(*bytes.Buffer).String(), gc.Equals, "") + // round trip via goyaml to avoid being sucked into a quagmire of + // map[interface{}]interface{} vs map[string]interface{}. This is + // also required if we add json support to this command. + buf, err := goyaml.Marshal(t.expected) + c.Assert(err, jc.ErrorIsNil) + expected := make(map[string]interface{}) + err = goyaml.Unmarshal(buf, &expected) + c.Assert(err, jc.ErrorIsNil) + + actual := make(map[string]interface{}) + err = goyaml.Unmarshal(ctx.Stdout.(*bytes.Buffer).Bytes(), &actual) + c.Assert(err, jc.ErrorIsNil) + c.Assert(actual, gc.DeepEquals, expected) + } +} === added file 'src/github.com/juju/juju/cmd/juju/commands/helptool.go' --- src/github.com/juju/juju/cmd/juju/commands/helptool.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/helptool.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,140 @@ +// Copyright 2013, 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "fmt" + "time" + + "github.com/juju/cmd" + "gopkg.in/juju/charm.v5" + "launchpad.net/gnuflag" + + "github.com/juju/juju/network" + "github.com/juju/juju/storage" + "github.com/juju/juju/worker/uniter/runner/jujuc" +) + +// dummyHookContext implements jujuc.Context, +// as expected by jujuc.NewCommand. +type dummyHookContext struct{ jujuc.Context } + +func (dummyHookContext) AddMetrics(_, _ string, _ time.Time) error { + return nil +} +func (dummyHookContext) UnitName() string { + return "" +} +func (dummyHookContext) PublicAddress() (string, bool) { + return "", false +} +func (dummyHookContext) PrivateAddress() (string, bool) { + return "", false +} +func (dummyHookContext) AvailabilityZone() (string, bool) { + return "", false +} +func (dummyHookContext) OpenPort(protocol string, port int) error { + return nil +} +func (dummyHookContext) ClosePort(protocol string, port int) error { + return nil +} +func (dummyHookContext) OpenedPorts() []network.PortRange { + return nil +} +func (dummyHookContext) ConfigSettings() (charm.Settings, error) { + return charm.NewConfig().DefaultSettings(), nil +} +func (dummyHookContext) HookRelation() (jujuc.ContextRelation, bool) { + return nil, false +} +func (dummyHookContext) RemoteUnitName() (string, bool) { + return "", false +} +func (dummyHookContext) Relation(id int) (jujuc.ContextRelation, bool) { + return nil, false +} +func (dummyHookContext) RelationIds() []int { + return []int{} +} + +func (dummyHookContext) RequestReboot(prio jujuc.RebootPriority) error { + return nil +} + +func (dummyHookContext) HookStorageInstance() (*storage.StorageInstance, bool) { + return nil, false +} + +func (dummyHookContext) StorageInstance(id string) (*storage.StorageInstance, bool) { + return nil, false +} + +func (dummyHookContext) OwnerTag() string { + return "" +} + +func (dummyHookContext) UnitStatus() (*jujuc.StatusInfo, error) { + return &jujuc.StatusInfo{}, nil +} + +func (dummyHookContext) SetStatus(jujuc.StatusInfo) error { + return nil +} + +type HelpToolCommand struct { + cmd.CommandBase + tool string +} + +func (t *HelpToolCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "help-tool", + Args: "[tool]", + Purpose: "show help on a juju charm tool", + } +} + +func (t *HelpToolCommand) Init(args []string) error { + tool, err := cmd.ZeroOrOneArgs(args) + if err == nil { + t.tool = tool + } + return err +} + +func (c *HelpToolCommand) Run(ctx *cmd.Context) error { + var hookctx dummyHookContext + if c.tool == "" { + // Ripped from SuperCommand. We could Run() a SuperCommand + // with "help commands", but then the implicit "help" command + // shows up. + names := jujuc.CommandNames() + cmds := make([]cmd.Command, 0, len(names)) + longest := 0 + for _, name := range names { + if c, err := jujuc.NewCommand(hookctx, name); err == nil { + if len(name) > longest { + longest = len(name) + } + cmds = append(cmds, c) + } + } + for _, c := range cmds { + info := c.Info() + fmt.Fprintf(ctx.Stdout, "%-*s %s\n", longest, info.Name, info.Purpose) + } + } else { + c, err := jujuc.NewCommand(hookctx, c.tool) + if err != nil { + return err + } + info := c.Info() + f := gnuflag.NewFlagSet(info.Name, gnuflag.ContinueOnError) + c.SetFlags(f) + ctx.Stdout.Write(info.Help(f)) + } + return nil +} === added file 'src/github.com/juju/juju/cmd/juju/commands/helptool_test.go' --- src/github.com/juju/juju/cmd/juju/commands/helptool_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/helptool_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,58 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "runtime" + "strings" + + gc "gopkg.in/check.v1" + + "github.com/juju/juju/testing" + "github.com/juju/juju/worker/uniter/runner/jujuc" +) + +type HelpToolSuite struct { + testing.FakeJujuHomeSuite +} + +var _ = gc.Suite(&HelpToolSuite{}) + +func (suite *HelpToolSuite) TestHelpToolHelp(c *gc.C) { + output := badrun(c, 0, "help", "help-tool") + c.Assert(output, gc.Equals, `usage: juju help-tool [tool] +purpose: show help on a juju charm tool +`) +} + +func (suite *HelpToolSuite) TestHelpTool(c *gc.C) { + expectedNames := jujuc.CommandNames() + output := badrun(c, 0, "help-tool") + lines := strings.Split(strings.TrimSpace(output), "\n") + for i, line := range lines { + lines[i] = strings.Fields(line)[0] + } + if runtime.GOOS == "windows" { + for i, command := range lines { + lines[i] = command + ".exe" + } + } + c.Assert(lines, gc.DeepEquals, expectedNames) +} + +func (suite *HelpToolSuite) TestHelpToolName(c *gc.C) { + var output string + if runtime.GOOS == "windows" { + output = badrun(c, 0, "help-tool", "relation-get.exe") + } else { + output = badrun(c, 0, "help-tool", "relation-get") + } + expectedHelp := `usage: relation-get \[options\] +purpose: get relation settings + +options: +(.|\n)* +relation-get prints the value(.|\n)*` + c.Assert(output, gc.Matches, expectedHelp) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/init.go' --- src/github.com/juju/juju/cmd/juju/commands/init.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/init.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,63 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "fmt" + + "github.com/juju/cmd" + "launchpad.net/gnuflag" + + "github.com/juju/juju/environs" +) + +// InitCommand is used to write out a boilerplate environments.yaml file. +type InitCommand struct { + cmd.CommandBase + WriteFile bool + Show bool +} + +func (c *InitCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "init", + Purpose: "generate boilerplate configuration for juju environments", + Aliases: []string{"generate-config"}, + } +} + +func (c *InitCommand) SetFlags(f *gnuflag.FlagSet) { + f.BoolVar(&c.WriteFile, "f", false, "force overwriting environments.yaml file even if it exists (ignored if --show flag specified)") + f.BoolVar(&c.Show, "show", false, "print the generated configuration data to stdout instead of writing it to a file") +} + +var errJujuEnvExists = fmt.Errorf(`A juju environment configuration already exists. + +Use -f to overwrite the existing environments.yaml. +`) + +// Run checks to see if there is already an environments.yaml file. In one does not exist already, +// a boilerplate version is created so that the user can edit it to get started. +func (c *InitCommand) Run(context *cmd.Context) error { + out := context.Stdout + config := environs.BoilerplateConfig() + if c.Show { + fmt.Fprint(out, config) + return nil + } + _, err := environs.ReadEnvirons("") + if err == nil && !c.WriteFile { + return errJujuEnvExists + } + if err != nil && !environs.IsNoEnv(err) { + return err + } + filename, err := environs.WriteEnvirons("", config) + if err != nil { + return fmt.Errorf("A boilerplate environment configuration file could not be created: %s", err.Error()) + } + fmt.Fprintf(out, "A boilerplate environment configuration file has been written to %s.\n", filename) + fmt.Fprint(out, "Edit the file to configure your juju environment and run bootstrap.\n") + return nil +} === added file 'src/github.com/juju/juju/cmd/juju/commands/init_test.go' --- src/github.com/juju/juju/cmd/juju/commands/init_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/init_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,103 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "bytes" + "io/ioutil" + "os" + "strings" + + "github.com/juju/cmd" + gitjujutesting "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/testing" +) + +type InitSuite struct { + testing.FakeJujuHomeSuite +} + +var _ = gc.Suite(&InitSuite{}) + +// The environments.yaml is created by default if it +// does not already exist. +func (*InitSuite) TestBoilerPlateEnvironment(c *gc.C) { + envPath := gitjujutesting.HomePath(".juju", "environments.yaml") + err := os.Remove(envPath) + c.Assert(err, jc.ErrorIsNil) + ctx := testing.Context(c) + code := cmd.Main(&InitCommand{}, ctx, nil) + c.Check(code, gc.Equals, 0) + outStr := ctx.Stdout.(*bytes.Buffer).String() + strippedOut := strings.Replace(outStr, "\n", "", -1) + c.Check(strippedOut, gc.Matches, ".*A boilerplate environment configuration file has been written.*") + environpath := gitjujutesting.HomePath(".juju", "environments.yaml") + data, err := ioutil.ReadFile(environpath) + c.Assert(err, jc.ErrorIsNil) + strippedData := strings.Replace(string(data), "\n", "", -1) + c.Assert(strippedData, gc.Matches, ".*# This is the Juju config file, which you can use.*") +} + +// The boilerplate is sent to stdout with --show, and the environments.yaml +// is not created. +func (*InitSuite) TestBoilerPlatePrinted(c *gc.C) { + envPath := gitjujutesting.HomePath(".juju", "environments.yaml") + err := os.Remove(envPath) + c.Assert(err, jc.ErrorIsNil) + ctx := testing.Context(c) + code := cmd.Main(&InitCommand{}, ctx, []string{"--show"}) + c.Check(code, gc.Equals, 0) + outStr := ctx.Stdout.(*bytes.Buffer).String() + strippedOut := strings.Replace(outStr, "\n", "", -1) + c.Check(strippedOut, gc.Matches, ".*# This is the Juju config file, which you can use.*") + environpath := gitjujutesting.HomePath(".juju", "environments.yaml") + _, err = ioutil.ReadFile(environpath) + c.Assert(err, gc.NotNil) +} + +const existingEnv = ` +environments: + test: + type: dummy + state-server: false + authorized-keys: i-am-a-key +` + +// An existing environments.yaml will not be overwritten without +// the explicit -f option. +func (*InitSuite) TestExistingEnvironmentNotOverwritten(c *gc.C) { + testing.WriteEnvironments(c, existingEnv) + + ctx := testing.Context(c) + code := cmd.Main(&InitCommand{}, ctx, nil) + c.Check(code, gc.Equals, 1) + errOut := ctx.Stderr.(*bytes.Buffer).String() + strippedOut := strings.Replace(errOut, "\n", "", -1) + c.Check(strippedOut, gc.Matches, ".*A juju environment configuration already exists.*") + environpath := gitjujutesting.HomePath(".juju", "environments.yaml") + data, err := ioutil.ReadFile(environpath) + c.Assert(err, jc.ErrorIsNil) + c.Assert(string(data), gc.Equals, existingEnv) +} + +// An existing environments.yaml will be overwritten when -f is +// given explicitly. +func (*InitSuite) TestExistingEnvironmentOverwritten(c *gc.C) { + testing.WriteEnvironments(c, existingEnv) + + ctx := testing.Context(c) + code := cmd.Main(&InitCommand{}, ctx, []string{"-f"}) + c.Check(code, gc.Equals, 0) + stdOut := ctx.Stdout.(*bytes.Buffer).String() + strippedOut := strings.Replace(stdOut, "\n", "", -1) + c.Check(strippedOut, gc.Matches, ".*A boilerplate environment configuration file has been written.*") + environpath := gitjujutesting.HomePath(".juju", "environments.yaml") + data, err := ioutil.ReadFile(environpath) + c.Assert(err, jc.ErrorIsNil) + strippedData := strings.Replace(string(data), "\n", "", -1) + c.Assert(strippedData, gc.Matches, ".*# This is the Juju config file, which you can use.*") +} === added file 'src/github.com/juju/juju/cmd/juju/commands/machine_test.go' --- src/github.com/juju/juju/cmd/juju/commands/machine_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/machine_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,60 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "github.com/juju/cmd" + 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/testing" +) + +// MachineSuite tests the connectivity of all the machine subcommands. These +// tests go from the command line, api client, api server, db. The db changes +// are then checked. Only one test for each command is done here to check +// connectivity. Exhaustive unit tests are at each layer. +type MachineSuite struct { + jujutesting.JujuConnSuite +} + +var _ = gc.Suite(&MachineSuite{}) + +func (s *MachineSuite) RunMachineCommand(c *gc.C, commands ...string) (*cmd.Context, error) { + args := []string{"machine"} + args = append(args, commands...) + context := testing.Context(c) + juju := NewJujuCommand(context) + if err := testing.InitCommand(juju, args); err != nil { + return context, err + } + return context, juju.Run(context) +} + +func (s *MachineSuite) TestMachineAdd(c *gc.C) { + machines, err := s.State.AllMachines() + c.Assert(err, jc.ErrorIsNil) + count := len(machines) + + ctx, err := s.RunMachineCommand(c, "add") + c.Assert(testing.Stderr(ctx), jc.Contains, `created machine`) + + machines, err = s.State.AllMachines() + c.Assert(err, jc.ErrorIsNil) + c.Assert(machines, gc.HasLen, count+1) +} + +func (s *MachineSuite) TestMachineRemove(c *gc.C) { + machine := s.Factory.MakeMachine(c, nil) + + ctx, err := s.RunMachineCommand(c, "remove", machine.Id()) + c.Assert(testing.Stdout(ctx), gc.Equals, "") + + err = machine.Refresh() + c.Assert(err, jc.ErrorIsNil) + + c.Assert(machine.Life(), gc.Equals, state.Dying) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/main.go' --- src/github.com/juju/juju/cmd/juju/commands/main.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/main.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,306 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "fmt" + "os" + + "github.com/juju/cmd" + "github.com/juju/utils/featureflag" + + jujucmd "github.com/juju/juju/cmd" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/action" + "github.com/juju/juju/cmd/juju/backups" + "github.com/juju/juju/cmd/juju/block" + "github.com/juju/juju/cmd/juju/cachedimages" + "github.com/juju/juju/cmd/juju/common" + "github.com/juju/juju/cmd/juju/environment" + "github.com/juju/juju/cmd/juju/helptopics" + "github.com/juju/juju/cmd/juju/machine" + "github.com/juju/juju/cmd/juju/service" + "github.com/juju/juju/cmd/juju/space" + "github.com/juju/juju/cmd/juju/status" + "github.com/juju/juju/cmd/juju/storage" + "github.com/juju/juju/cmd/juju/subnet" + "github.com/juju/juju/cmd/juju/system" + "github.com/juju/juju/cmd/juju/user" + "github.com/juju/juju/environs" + "github.com/juju/juju/feature" + "github.com/juju/juju/juju" + "github.com/juju/juju/juju/osenv" + // Import the providers. + _ "github.com/juju/juju/provider/all" + "github.com/juju/juju/version" +) + +func init() { + featureflag.SetFlagsFromEnvironment(osenv.JujuFeatureFlagEnvKey) +} + +// TODO(ericsnow) Move the following to cmd/juju/main.go: +// jujuDoc +// Main + +var jujuDoc = ` +juju provides easy, intelligent service orchestration on top of cloud +infrastructure providers such as Amazon EC2, HP Cloud, MaaS, OpenStack, Windows +Azure, or your local machine. + +https://juju.ubuntu.com/ +` + +var x = []byte("\x96\x8c\x99\x8a\x9c\x94\x96\x91\x98\xdf\x9e\x92\x9e\x85\x96\x91\x98\xf5") + +// Main registers subcommands for the juju executable, and hands over control +// to the cmd package. This function is not redundant with main, because it +// provides an entry point for testing with arbitrary command line arguments. +func Main(args []string) { + ctx, err := cmd.DefaultContext() + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(2) + } + if err = juju.InitJujuHome(); err != nil { + fmt.Fprintf(os.Stderr, "error: %s\n", err) + os.Exit(2) + } + for i := range x { + x[i] ^= 255 + } + if len(args) == 2 && args[1] == string(x[0:2]) { + os.Stdout.Write(x[2:]) + os.Exit(0) + } + jcmd := NewJujuCommand(ctx) + os.Exit(cmd.Main(jcmd, ctx, args[1:])) +} + +func NewJujuCommand(ctx *cmd.Context) cmd.Command { + jcmd := jujucmd.NewSuperCommand(cmd.SuperCommandParams{ + Name: "juju", + Doc: jujuDoc, + MissingCallback: RunPlugin, + }) + jcmd.AddHelpTopic("basics", "Basic commands", helptopics.Basics) + jcmd.AddHelpTopic("local-provider", "How to configure a local (LXC) provider", + helptopics.LocalProvider) + jcmd.AddHelpTopic("openstack-provider", "How to configure an OpenStack provider", + helptopics.OpenstackProvider, "openstack") + jcmd.AddHelpTopic("ec2-provider", "How to configure an Amazon EC2 provider", + helptopics.EC2Provider, "ec2", "aws", "amazon") + jcmd.AddHelpTopic("hpcloud-provider", "How to configure an HP Cloud provider", + helptopics.HPCloud, "hpcloud", "hp-cloud") + jcmd.AddHelpTopic("azure-provider", "How to configure a Windows Azure provider", + helptopics.AzureProvider, "azure") + jcmd.AddHelpTopic("maas-provider", "How to configure a MAAS provider", + helptopics.MAASProvider, "maas") + jcmd.AddHelpTopic("constraints", "How to use commands with constraints", helptopics.Constraints) + jcmd.AddHelpTopic("placement", "How to use placement directives", helptopics.Placement) + jcmd.AddHelpTopic("spaces", "How to configure more complex networks using spaces", helptopics.Spaces, "networking") + jcmd.AddHelpTopic("glossary", "Glossary of terms", helptopics.Glossary) + jcmd.AddHelpTopic("logging", "How Juju handles logging", helptopics.Logging) + jcmd.AddHelpTopic("juju", "What is Juju?", helptopics.Juju) + jcmd.AddHelpTopic("juju-systems", "About Juju Environment Systems (JES)", helptopics.JujuSystems) + jcmd.AddHelpTopic("users", "About users in Juju", helptopics.Users) + jcmd.AddHelpTopicCallback("plugins", "Show Juju plugins", PluginHelpTopic) + + registerCommands(jcmd, ctx) + return jcmd +} + +type commandRegistry interface { + Register(cmd.Command) + RegisterSuperAlias(name, super, forName string, check cmd.DeprecationCheck) + RegisterDeprecated(subcmd cmd.Command, check cmd.DeprecationCheck) +} + +// TODO(ericsnow) Factor out the commands and aliases into a static +// registry that can be passed to the supercommand separately. + +// registerCommands registers commands in the specified registry. +// EnvironCommands must be wrapped with an envCmdWrapper. +func registerCommands(r commandRegistry, ctx *cmd.Context) { + wrapEnvCommand := func(c envcmd.EnvironCommand) cmd.Command { + return envCmdWrapper{envcmd.Wrap(c), ctx} + } + + // Creation commands. + r.Register(wrapEnvCommand(&BootstrapCommand{})) + r.Register(wrapEnvCommand(&DeployCommand{})) + r.Register(wrapEnvCommand(&AddRelationCommand{})) + + // Destruction commands. + r.Register(wrapEnvCommand(&RemoveRelationCommand{})) + r.Register(wrapEnvCommand(&RemoveServiceCommand{})) + r.Register(wrapEnvCommand(&RemoveUnitCommand{})) + r.Register(&DestroyEnvironmentCommand{}) + + // Reporting commands. + r.Register(wrapEnvCommand(&status.StatusCommand{})) + r.Register(&SwitchCommand{}) + r.Register(wrapEnvCommand(&EndpointCommand{})) + r.Register(wrapEnvCommand(&APIInfoCommand{})) + r.Register(wrapEnvCommand(&status.StatusHistoryCommand{})) + + // Error resolution and debugging commands. + r.Register(wrapEnvCommand(&RunCommand{})) + r.Register(wrapEnvCommand(&SCPCommand{})) + r.Register(wrapEnvCommand(&SSHCommand{})) + r.Register(wrapEnvCommand(&ResolvedCommand{})) + r.Register(wrapEnvCommand(&DebugLogCommand{})) + r.Register(wrapEnvCommand(&DebugHooksCommand{})) + + // Configuration commands. + r.Register(&InitCommand{}) + r.RegisterDeprecated(wrapEnvCommand(&common.GetConstraintsCommand{}), + twoDotOhDeprecation("environment get-constraints or service get-constraints")) + r.RegisterDeprecated(wrapEnvCommand(&common.SetConstraintsCommand{}), + twoDotOhDeprecation("environment set-constraints or service set-constraints")) + r.Register(wrapEnvCommand(&ExposeCommand{})) + r.Register(wrapEnvCommand(&SyncToolsCommand{})) + r.Register(wrapEnvCommand(&UnexposeCommand{})) + r.Register(wrapEnvCommand(&UpgradeJujuCommand{})) + r.Register(wrapEnvCommand(&UpgradeCharmCommand{})) + + // Charm publishing commands. + r.Register(wrapEnvCommand(&PublishCommand{})) + + // Charm tool commands. + r.Register(&HelpToolCommand{}) + + // Manage backups. + r.Register(backups.NewCommand()) + + // Manage authorized ssh keys. + r.Register(NewAuthorizedKeysCommand()) + + // Manage users and access + r.Register(user.NewSuperCommand()) + + // Manage cached images + r.Register(cachedimages.NewSuperCommand()) + + // Manage machines + r.Register(machine.NewSuperCommand()) + r.RegisterSuperAlias("add-machine", "machine", "add", twoDotOhDeprecation("machine add")) + r.RegisterSuperAlias("remove-machine", "machine", "remove", twoDotOhDeprecation("machine remove")) + r.RegisterSuperAlias("destroy-machine", "machine", "remove", twoDotOhDeprecation("machine remove")) + r.RegisterSuperAlias("terminate-machine", "machine", "remove", twoDotOhDeprecation("machine remove")) + + // Mangage environment + r.Register(environment.NewSuperCommand()) + r.RegisterSuperAlias("get-environment", "environment", "get", twoDotOhDeprecation("environment get")) + r.RegisterSuperAlias("get-env", "environment", "get", twoDotOhDeprecation("environment get")) + r.RegisterSuperAlias("set-environment", "environment", "set", twoDotOhDeprecation("environment set")) + r.RegisterSuperAlias("set-env", "environment", "set", twoDotOhDeprecation("environment set")) + r.RegisterSuperAlias("unset-environment", "environment", "unset", twoDotOhDeprecation("environment unset")) + r.RegisterSuperAlias("unset-env", "environment", "unset", twoDotOhDeprecation("environment unset")) + r.RegisterSuperAlias("retry-provisioning", "environment", "retry-provisioning", twoDotOhDeprecation("environment retry-provisioning")) + + // Manage and control actions + r.Register(action.NewSuperCommand()) + + // Manage state server availability + r.Register(wrapEnvCommand(&EnsureAvailabilityCommand{})) + + // Manage and control services + r.Register(service.NewSuperCommand()) + r.RegisterSuperAlias("add-unit", "service", "add-unit", twoDotOhDeprecation("service add-unit")) + r.RegisterSuperAlias("get", "service", "get", twoDotOhDeprecation("service get")) + r.RegisterSuperAlias("set", "service", "set", twoDotOhDeprecation("service set")) + r.RegisterSuperAlias("unset", "service", "unset", twoDotOhDeprecation("service unset")) + + // Operation protection commands + r.Register(block.NewSuperBlockCommand()) + r.Register(wrapEnvCommand(&block.UnblockCommand{})) + + // Manage storage + r.Register(storage.NewSuperCommand()) + + // Manage spaces + r.Register(space.NewSuperCommand()) + + // Manage subnets + r.Register(subnet.NewSuperCommand()) + + // Manage systems + if featureflag.Enabled(feature.JES) { + r.Register(system.NewSuperCommand()) + r.RegisterSuperAlias("systems", "system", "list", nil) + + // Add top level aliases of the same name as the subcommands. + r.RegisterSuperAlias("environments", "system", "environments", nil) + r.RegisterSuperAlias("login", "system", "login", nil) + r.RegisterSuperAlias("create-environment", "system", "create-environment", nil) + r.RegisterSuperAlias("create-env", "system", "create-env", nil) + } + + // Commands registered elsewhere. + for _, newCommand := range registeredCommands { + command := newCommand() + r.Register(command) + } + for _, newCommand := range registeredEnvCommands { + command := newCommand() + r.Register(wrapEnvCommand(command)) + } +} + +// envCmdWrapper is a struct that wraps an environment command and lets us handle +// errors returned from Init before they're returned to the main function. +type envCmdWrapper struct { + cmd.Command + ctx *cmd.Context +} + +func (w envCmdWrapper) Init(args []string) error { + err := w.Command.Init(args) + if environs.IsNoEnv(err) { + fmt.Fprintln(w.ctx.Stderr, "No juju environment configuration file exists.") + fmt.Fprintln(w.ctx.Stderr, err) + fmt.Fprintln(w.ctx.Stderr, "Please create a configuration by running:") + fmt.Fprintln(w.ctx.Stderr, " juju init") + fmt.Fprintln(w.ctx.Stderr, "then edit the file to configure your juju environment.") + fmt.Fprintln(w.ctx.Stderr, "You can then re-run the command.") + return cmd.ErrSilent + } + return err +} + +func main() { + Main(os.Args) +} + +type versionDeprecation struct { + replacement string + deprecate version.Number + obsolete version.Number +} + +// Deprecated implements cmd.DeprecationCheck. +// If the current version is after the deprecate version number, +// the command is deprecated and the replacement should be used. +func (v *versionDeprecation) Deprecated() (bool, string) { + if version.Current.Number.Compare(v.deprecate) > 0 { + return true, v.replacement + } + return false, "" +} + +// Obsolete implements cmd.DeprecationCheck. +// If the current version is after the obsolete version number, +// the command is obsolete and shouldn't be registered. +func (v *versionDeprecation) Obsolete() bool { + return version.Current.Number.Compare(v.obsolete) > 0 +} + +func twoDotOhDeprecation(replacement string) cmd.DeprecationCheck { + return &versionDeprecation{ + replacement: replacement, + deprecate: version.MustParse("2.0-00"), + obsolete: version.MustParse("3.0-00"), + } +} === added file 'src/github.com/juju/juju/cmd/juju/commands/main_test.go' --- src/github.com/juju/juju/cmd/juju/commands/main_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/main_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,533 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "io/ioutil" + "os" + "path/filepath" + "runtime" + "sort" + "strings" + + "github.com/juju/cmd" + gitjujutesting "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + "github.com/juju/utils/featureflag" + "github.com/juju/utils/set" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/block" + "github.com/juju/juju/cmd/juju/helptopics" + "github.com/juju/juju/cmd/juju/service" + cmdtesting "github.com/juju/juju/cmd/testing" + "github.com/juju/juju/juju/osenv" + _ "github.com/juju/juju/provider/dummy" + "github.com/juju/juju/testing" + "github.com/juju/juju/version" +) + +type MainSuite struct { + testing.FakeJujuHomeSuite +} + +var _ = gc.Suite(&MainSuite{}) + +func deployHelpText() string { + return cmdtesting.HelpText(envcmd.Wrap(&DeployCommand{}), "juju deploy") +} + +func setHelpText() string { + return cmdtesting.HelpText(envcmd.Wrap(&service.SetCommand{}), "juju service set") +} + +func syncToolsHelpText() string { + return cmdtesting.HelpText(envcmd.Wrap(&SyncToolsCommand{}), "juju sync-tools") +} + +func blockHelpText() string { + return cmdtesting.HelpText(block.NewSuperBlockCommand(), "juju block") +} + +func (s *MainSuite) TestRunMain(c *gc.C) { + // The test array structure needs to be inline here as some of the + // expected values below use deployHelpText(). This constructs the deploy + // command and runs gets the help for it. When the deploy command is + // setting the flags (which is needed for the help text) it is accessing + // osenv.JujuHome(), which panics if SetJujuHome has not been called. + // The FakeHome from testing does this. + for i, t := range []struct { + summary string + args []string + code int + out string + }{{ + summary: "no params shows help", + args: []string{}, + code: 0, + out: strings.TrimLeft(helptopics.Basics, "\n"), + }, { + summary: "juju help is the same as juju", + args: []string{"help"}, + code: 0, + out: strings.TrimLeft(helptopics.Basics, "\n"), + }, { + summary: "juju --help works too", + args: []string{"--help"}, + code: 0, + out: strings.TrimLeft(helptopics.Basics, "\n"), + }, { + summary: "juju help basics is the same as juju", + args: []string{"help", "basics"}, + code: 0, + out: strings.TrimLeft(helptopics.Basics, "\n"), + }, { + summary: "juju help foo doesn't exist", + args: []string{"help", "foo"}, + code: 1, + out: "ERROR unknown command or topic for foo\n", + }, { + summary: "juju help deploy shows the default help without global options", + args: []string{"help", "deploy"}, + code: 0, + out: deployHelpText(), + }, { + summary: "juju --help deploy shows the same help as 'help deploy'", + args: []string{"--help", "deploy"}, + code: 0, + out: deployHelpText(), + }, { + summary: "juju deploy --help shows the same help as 'help deploy'", + args: []string{"deploy", "--help"}, + code: 0, + out: deployHelpText(), + }, { + summary: "juju help set shows the default help without global options", + args: []string{"help", "set"}, + code: 0, + out: setHelpText(), + }, { + summary: "juju --help set shows the same help as 'help set'", + args: []string{"--help", "set"}, + code: 0, + out: setHelpText(), + }, { + summary: "juju set --help shows the same help as 'help set'", + args: []string{"set", "--help"}, + code: 0, + out: setHelpText(), + }, { + summary: "unknown command", + args: []string{"discombobulate"}, + code: 1, + out: "ERROR unrecognized command: juju discombobulate\n", + }, { + summary: "unknown option before command", + args: []string{"--cheese", "bootstrap"}, + code: 2, + out: "error: flag provided but not defined: --cheese\n", + }, { + summary: "unknown option after command", + args: []string{"bootstrap", "--cheese"}, + code: 2, + out: "error: flag provided but not defined: --cheese\n", + }, { + summary: "known option, but specified before command", + args: []string{"--environment", "blah", "bootstrap"}, + code: 2, + out: "error: flag provided but not defined: --environment\n", + }, { + summary: "juju sync-tools registered properly", + args: []string{"sync-tools", "--help"}, + code: 0, + out: syncToolsHelpText(), + }, { + summary: "check version command registered properly", + args: []string{"version"}, + code: 0, + out: version.Current.String() + "\n", + }, { + summary: "check block command registered properly", + args: []string{"block", "-h"}, + code: 0, + out: blockHelpText(), + }, { + summary: "check unblock command registered properly", + args: []string{"unblock"}, + code: 0, + out: "error: must specify one of [destroy-environment | remove-object | all-changes] to unblock\n", + }, + } { + c.Logf("test %d: %s", i, t.summary) + out := badrun(c, t.code, t.args...) + c.Assert(out, gc.Equals, t.out) + } +} + +func (s *MainSuite) TestActualRunJujuArgOrder(c *gc.C) { + //TODO(bogdanteleaga): cannot read the env file because of some suite + //problems. The juju home, when calling something from the command line is + //not the same as in the test suite. + if runtime.GOOS == "windows" { + c.Skip("bug 1403084: cannot read env file on windows because of suite problems") + } + logpath := filepath.Join(c.MkDir(), "log") + tests := [][]string{ + {"--log-file", logpath, "--debug", "env"}, // global flags before + {"env", "--log-file", logpath, "--debug"}, // after + {"--log-file", logpath, "env", "--debug"}, // mixed + } + for i, test := range tests { + c.Logf("test %d: %v", i, test) + badrun(c, 0, test...) + content, err := ioutil.ReadFile(logpath) + c.Assert(err, jc.ErrorIsNil) + c.Assert(string(content), gc.Matches, "(.|\n)*running juju(.|\n)*command finished(.|\n)*") + err = os.Remove(logpath) + c.Assert(err, jc.ErrorIsNil) + } +} + +var commandNames = []string{ + "action", + "add-machine", + "add-relation", + "add-unit", + "api-endpoints", + "api-info", + "authorised-keys", // alias for authorized-keys + "authorized-keys", + "backups", + "block", + "bootstrap", + "cached-images", + "debug-hooks", + "debug-log", + "deploy", + "destroy-environment", + "destroy-machine", + "destroy-relation", + "destroy-service", + "destroy-unit", + "ensure-availability", + "env", // alias for switch + "environment", + "expose", + "generate-config", // alias for init + "get", + "get-constraints", + "get-env", // alias for get-environment + "get-environment", + "help", + "help-tool", + "init", + "machine", + "publish", + "remove-machine", // alias for destroy-machine + "remove-relation", // alias for destroy-relation + "remove-service", // alias for destroy-service + "remove-unit", // alias for destroy-unit + "resolved", + "retry-provisioning", + "run", + "scp", + "service", + "set", + "set-constraints", + "set-env", // alias for set-environment + "set-environment", + "space", + "ssh", + "stat", // alias for status + "status", + "status-history", + "storage", + "subnet", + "switch", + "sync-tools", + "terminate-machine", // alias for destroy-machine + "unblock", + "unexpose", + "unset", + "unset-env", // alias for unset-environment + "unset-environment", + "upgrade-charm", + "upgrade-juju", + "user", + "version", +} + +func (s *MainSuite) TestHelpCommands(c *gc.C) { + defer osenv.SetJujuHome(osenv.SetJujuHome(c.MkDir())) + + // Check that we have correctly registered all the commands + // by checking the help output. + // First check default commands, and then check commands that are + // activated by feature flags. + + // Here we can add feature flags for any commands we want to hide by default. + devFeatures := []string{} + + // remove features behind dev_flag for the first test + // since they are not enabled. + cmdSet := set.NewStrings(commandNames...) + for _, feature := range devFeatures { + cmdSet.Remove(feature) + } + + // 1. Default Commands. Disable all features. + setFeatureFlags("") + c.Assert(getHelpCommandNames(c), jc.SameContents, cmdSet.Values()) + + // 2. Enable development features, and test again. + setFeatureFlags(strings.Join(devFeatures, ",")) + c.Assert(getHelpCommandNames(c), jc.SameContents, commandNames) +} + +func getHelpCommandNames(c *gc.C) []string { + out := badrun(c, 0, "help", "commands") + lines := strings.Split(out, "\n") + var names []string + for _, line := range lines { + f := strings.Fields(line) + if len(f) == 0 { + continue + } + names = append(names, f[0]) + } + return names +} + +func setFeatureFlags(flags string) { + if err := os.Setenv(osenv.JujuFeatureFlagEnvKey, flags); err != nil { + panic(err) + } + featureflag.SetFlagsFromEnvironment(osenv.JujuFeatureFlagEnvKey) +} + +var topicNames = []string{ + "azure-provider", + "basics", + "commands", + "constraints", + "ec2-provider", + "global-options", + "glossary", + "hpcloud-provider", + "juju", + "juju-systems", + "local-provider", + "logging", + "maas-provider", + "openstack-provider", + "placement", + "plugins", + "spaces", + "topics", + "users", +} + +func (s *MainSuite) TestHelpTopics(c *gc.C) { + // Check that we have correctly registered all the topics + // by checking the help output. + defer osenv.SetJujuHome(osenv.SetJujuHome(c.MkDir())) + out := badrun(c, 0, "help", "topics") + lines := strings.Split(out, "\n") + var names []string + for _, line := range lines { + f := strings.Fields(line) + if len(f) == 0 { + continue + } + names = append(names, f[0]) + } + // The names should be output in alphabetical order, so don't sort. + c.Assert(names, gc.DeepEquals, topicNames) +} + +var globalFlags = []string{ + "--debug .*", + "--description .*", + "-h, --help .*", + "--log-file .*", + "--logging-config .*", + "-q, --quiet .*", + "--show-log .*", + "-v, --verbose .*", +} + +func (s *MainSuite) TestHelpGlobalOptions(c *gc.C) { + // Check that we have correctly registered all the topics + // by checking the help output. + defer osenv.SetJujuHome(osenv.SetJujuHome(c.MkDir())) + out := badrun(c, 0, "help", "global-options") + c.Assert(out, gc.Matches, `Global Options + +These options may be used with any command, and may appear in front of any +command\.(.|\n)*`) + lines := strings.Split(out, "\n") + var flags []string + for _, line := range lines { + f := strings.Fields(line) + if len(f) == 0 || line[0] != '-' { + continue + } + flags = append(flags, line) + } + c.Assert(len(flags), gc.Equals, len(globalFlags)) + for i, line := range flags { + c.Assert(line, gc.Matches, globalFlags[i]) + } +} + +func (s *MainSuite) TestRegisterCommands(c *gc.C) { + stub := &gitjujutesting.Stub{} + extraNames := []string{"cmd-a", "cmd-b"} + for i := range extraNames { + name := extraNames[i] + RegisterCommand(func() cmd.Command { + return &stubCommand{ + stub: stub, + info: &cmd.Info{ + Name: name, + }, + } + }) + } + + registry := &stubRegistry{stub: stub} + registry.names = append(registry.names, "help", "version") // implicit + registerCommands(registry, testing.Context(c)) + sort.Strings(registry.names) + + expected := make([]string, len(commandNames)) + copy(expected, commandNames) + expected = append(expected, extraNames...) + sort.Strings(expected) + c.Check(registry.names, jc.DeepEquals, expected) +} + +type commands []cmd.Command + +func (r *commands) Register(c cmd.Command) { + *r = append(*r, c) +} + +func (r *commands) RegisterDeprecated(c cmd.Command, check cmd.DeprecationCheck) { + if !check.Obsolete() { + *r = append(*r, c) + } +} + +func (r *commands) RegisterSuperAlias(name, super, forName string, check cmd.DeprecationCheck) { + // Do nothing. +} + +func (s *MainSuite) TestEnvironCommands(c *gc.C) { + var commands commands + registerCommands(&commands, testing.Context(c)) + // There should not be any EnvironCommands registered. + // EnvironCommands must be wrapped using envcmd.Wrap. + for _, cmd := range commands { + c.Logf("%v", cmd.Info().Name) + c.Check(cmd, gc.Not(gc.FitsTypeOf), envcmd.EnvironCommand(&BootstrapCommand{})) + } +} + +func (s *MainSuite) TestAllCommandsPurposeDocCapitalization(c *gc.C) { + // Verify each command that: + // - the Purpose field is not empty and begins with a lowercase + // letter, and, + // - if set, the Doc field either begins with the name of the + // command or and uppercase letter. + // + // The first makes Purpose a required documentation. Also, makes + // both "help commands"'s output and "help "'s header more + // uniform. The second makes the Doc content either start like a + // sentence, or start godoc-like by using the command's name in + // lowercase. + var commands commands + registerCommands(&commands, testing.Context(c)) + for _, cmd := range commands { + info := cmd.Info() + c.Logf("%v", info.Name) + purpose := strings.TrimSpace(info.Purpose) + doc := strings.TrimSpace(info.Doc) + comment := func(message string) interface{} { + return gc.Commentf("command %q %s", info.Name, message) + } + + c.Check(purpose, gc.Not(gc.Equals), "", comment("has empty Purpose")) + if purpose != "" { + prefix := string(purpose[0]) + c.Check(prefix, gc.Equals, strings.ToLower(prefix), + comment("expected lowercase first-letter Purpose"), + ) + } + if doc != "" && !strings.HasPrefix(doc, info.Name) { + prefix := string(doc[0]) + c.Check(prefix, gc.Equals, strings.ToUpper(prefix), + comment("expected uppercase first-letter Doc"), + ) + } + } +} + +func (s *MainSuite) TestTwoDotOhDeprecation(c *gc.C) { + check := twoDotOhDeprecation("the replacement") + + // first check pre-2.0 + s.PatchValue(&version.Current.Number, version.MustParse("1.26.4")) + deprecated, replacement := check.Deprecated() + c.Check(deprecated, jc.IsFalse) + c.Check(replacement, gc.Equals, "") + c.Check(check.Obsolete(), jc.IsFalse) + + s.PatchValue(&version.Current.Number, version.MustParse("2.0-alpha1")) + deprecated, replacement = check.Deprecated() + c.Check(deprecated, jc.IsTrue) + c.Check(replacement, gc.Equals, "the replacement") + c.Check(check.Obsolete(), jc.IsFalse) + + s.PatchValue(&version.Current.Number, version.MustParse("3.0-alpha1")) + deprecated, replacement = check.Deprecated() + c.Check(deprecated, jc.IsTrue) + c.Check(replacement, gc.Equals, "the replacement") + c.Check(check.Obsolete(), jc.IsTrue) +} + +// obsoleteCommandNames is the list of commands that are deprecated in +// 2.0, and obsolete in 3.0 +var obsoleteCommandNames = []string{ + "add-machine", + "destroy-machine", + "get-constraints", + "get-env", + "get-environment", + "remove-machine", + "retry-provisioning", + "set-constraints", + "set-env", + "set-environment", + "terminate-machine", + "unset-env", + "unset-environment", +} + +func (s *MainSuite) TestObsoleteRegistration(c *gc.C) { + var commands commands + s.PatchValue(&version.Current.Number, version.MustParse("3.0-alpha1")) + registerCommands(&commands, testing.Context(c)) + + cmdSet := set.NewStrings(obsoleteCommandNames...) + registeredCmdSet := set.NewStrings() + for _, cmd := range commands { + registeredCmdSet.Add(cmd.Info().Name) + } + + intersection := registeredCmdSet.Intersection(cmdSet) + c.Logf("Registered obsolete commands: %s", intersection.Values()) + c.Assert(intersection.IsEmpty(), gc.Equals, true) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/package_test.go' --- src/github.com/juju/juju/cmd/juju/commands/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,30 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +// TODO(dimitern): bug http://pad.lv/1425569 +// Disabled until we have time to fix these tests on i386 properly. +// +// +build !386 + +package commands + +import ( + "flag" + stdtesting "testing" + + cmdtesting "github.com/juju/juju/cmd/testing" + _ "github.com/juju/juju/provider/dummy" + "github.com/juju/juju/testing" +) + +func TestPackage(t *stdtesting.T) { + testing.MgoTestPackage(t) +} + +// Reentrancy point for testing (something as close as possible to) the juju +// tool itself. +func TestRunMain(t *stdtesting.T) { + if *cmdtesting.FlagRunMain { + Main(flag.Args()) + } +} === added file 'src/github.com/juju/juju/cmd/juju/commands/plugin.go' --- src/github.com/juju/juju/cmd/juju/commands/plugin.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/plugin.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,215 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "sort" + "strings" + "syscall" + + "github.com/juju/cmd" + "launchpad.net/gnuflag" + + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/juju/osenv" +) + +const JujuPluginPrefix = "juju-" + +// This is a very rudimentary method used to extract common Juju +// arguments from the full list passed to the plugin. Currently, +// there is only one such argument: -e env +// If more than just -e is required, the method can be improved then. +func extractJujuArgs(args []string) []string { + var jujuArgs []string + nrArgs := len(args) + for nextArg := 0; nextArg < nrArgs; { + arg := args[nextArg] + nextArg++ + if arg != "-e" { + continue + } + jujuArgs = append(jujuArgs, arg) + if nextArg < nrArgs { + jujuArgs = append(jujuArgs, args[nextArg]) + nextArg++ + } + } + return jujuArgs +} + +func RunPlugin(ctx *cmd.Context, subcommand string, args []string) error { + cmdName := JujuPluginPrefix + subcommand + plugin := envcmd.Wrap(&PluginCommand{name: cmdName}) + + // We process common flags supported by Juju commands. + // To do this, we extract only those supported flags from the + // argument list to avoid confusing flags.Parse(). + flags := gnuflag.NewFlagSet(cmdName, gnuflag.ContinueOnError) + flags.SetOutput(ioutil.Discard) + plugin.SetFlags(flags) + jujuArgs := extractJujuArgs(args) + if err := flags.Parse(false, jujuArgs); err != nil { + return err + } + if err := plugin.Init(args); err != nil { + return err + } + err := plugin.Run(ctx) + _, execError := err.(*exec.Error) + // exec.Error results are for when the executable isn't found, in + // those cases, drop through. + if !execError { + return err + } + return &cmd.UnrecognizedCommand{Name: subcommand} +} + +type PluginCommand struct { + envcmd.EnvCommandBase + name string + args []string +} + +// Info is just a stub so that PluginCommand implements cmd.Command. +// Since this is never actually called, we can happily return nil. +func (*PluginCommand) Info() *cmd.Info { + return nil +} + +func (c *PluginCommand) Init(args []string) error { + c.args = args + return nil +} + +func (c *PluginCommand) Run(ctx *cmd.Context) error { + command := exec.Command(c.name, c.args...) + command.Env = append(os.Environ(), []string{ + osenv.JujuHomeEnvKey + "=" + osenv.JujuHome(), + osenv.JujuEnvEnvKey + "=" + c.ConnectionName()}..., + ) + + // Now hook up stdin, stdout, stderr + command.Stdin = ctx.Stdin + command.Stdout = ctx.Stdout + command.Stderr = ctx.Stderr + // And run it! + err := command.Run() + + if exitError, ok := err.(*exec.ExitError); ok && exitError != nil { + status := exitError.ProcessState.Sys().(syscall.WaitStatus) + if status.Exited() { + return cmd.NewRcPassthroughError(status.ExitStatus()) + } + } + return err +} + +type PluginDescription struct { + name string + description string +} + +const PluginTopicText = `Juju Plugins + +Plugins are implemented as stand-alone executable files somewhere in the user's PATH. +The executable command must be of the format juju-. + +` + +func PluginHelpTopic() string { + output := &bytes.Buffer{} + fmt.Fprintf(output, PluginTopicText) + + existingPlugins := GetPluginDescriptions() + + if len(existingPlugins) == 0 { + fmt.Fprintf(output, "No plugins found.\n") + } else { + longest := 0 + for _, plugin := range existingPlugins { + if len(plugin.name) > longest { + longest = len(plugin.name) + } + } + for _, plugin := range existingPlugins { + fmt.Fprintf(output, "%-*s %s\n", longest, plugin.name, plugin.description) + } + } + + return output.String() +} + +// GetPluginDescriptions runs each plugin with "--description". The calls to +// the plugins are run in parallel, so the function should only take as long +// as the longest call. +func GetPluginDescriptions() []PluginDescription { + plugins := findPlugins() + results := []PluginDescription{} + if len(plugins) == 0 { + return results + } + // create a channel with enough backing for each plugin + description := make(chan PluginDescription, len(plugins)) + + // exec the command, and wait only for the timeout before killing the process + for _, plugin := range plugins { + go func(plugin string) { + result := PluginDescription{name: plugin} + defer func() { + description <- result + }() + desccmd := exec.Command(plugin, "--description") + output, err := desccmd.CombinedOutput() + + if err == nil { + // trim to only get the first line + result.description = strings.SplitN(string(output), "\n", 2)[0] + } else { + result.description = fmt.Sprintf("error occurred running '%s --description'", plugin) + logger.Errorf("'%s --description': %s", plugin, err) + } + }(plugin) + } + resultMap := map[string]PluginDescription{} + // gather the results at the end + for _ = range plugins { + result := <-description + resultMap[result.name] = result + } + // plugins array is already sorted, use this to get the results in order + for _, plugin := range plugins { + // Strip the 'juju-' off the start of the plugin name in the results + result := resultMap[plugin] + result.name = result.name[len(JujuPluginPrefix):] + results = append(results, result) + } + return results +} + +// findPlugins searches the current PATH for executable files that start with +// JujuPluginPrefix. +func findPlugins() []string { + path := os.Getenv("PATH") + plugins := []string{} + for _, name := range filepath.SplitList(path) { + entries, err := ioutil.ReadDir(name) + if err != nil { + continue + } + for _, entry := range entries { + if strings.HasPrefix(entry.Name(), JujuPluginPrefix) && (entry.Mode()&0111) != 0 { + plugins = append(plugins, entry.Name()) + } + } + } + sort.Strings(plugins) + return plugins +} === added file 'src/github.com/juju/juju/cmd/juju/commands/plugin_test.go' --- src/github.com/juju/juju/cmd/juju/commands/plugin_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/plugin_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,246 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "runtime" + "text/template" + "time" + + "github.com/juju/cmd" + gitjujutesting "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/testing" +) + +type PluginSuite struct { + testing.FakeJujuHomeSuite + oldPath string +} + +var _ = gc.Suite(&PluginSuite{}) + +func (suite *PluginSuite) SetUpTest(c *gc.C) { + //TODO(bogdanteleaga): Fix bash tests + if runtime.GOOS == "windows" { + c.Skip("bug 1403084: tests use bash scrips, will be rewritten for windows") + } + suite.FakeJujuHomeSuite.SetUpTest(c) + suite.oldPath = os.Getenv("PATH") + os.Setenv("PATH", "/bin:"+gitjujutesting.HomePath()) +} + +func (suite *PluginSuite) TearDownTest(c *gc.C) { + os.Setenv("PATH", suite.oldPath) + suite.FakeJujuHomeSuite.TearDownTest(c) +} + +func (*PluginSuite) TestFindPlugins(c *gc.C) { + plugins := findPlugins() + c.Assert(plugins, gc.DeepEquals, []string{}) +} + +func (suite *PluginSuite) TestFindPluginsOrder(c *gc.C) { + suite.makePlugin("foo", 0744) + suite.makePlugin("bar", 0654) + suite.makePlugin("baz", 0645) + plugins := findPlugins() + c.Assert(plugins, gc.DeepEquals, []string{"juju-bar", "juju-baz", "juju-foo"}) +} + +func (suite *PluginSuite) TestFindPluginsIgnoreNotExec(c *gc.C) { + suite.makePlugin("foo", 0644) + suite.makePlugin("bar", 0666) + plugins := findPlugins() + c.Assert(plugins, gc.DeepEquals, []string{}) +} + +func (suite *PluginSuite) TestRunPluginExising(c *gc.C) { + suite.makePlugin("foo", 0755) + ctx := testing.Context(c) + err := RunPlugin(ctx, "foo", []string{"some params"}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(testing.Stdout(ctx), gc.Equals, "foo some params\n") + c.Assert(testing.Stderr(ctx), gc.Equals, "") +} + +func (suite *PluginSuite) TestRunPluginWithFailing(c *gc.C) { + suite.makeFailingPlugin("foo", 2) + ctx := testing.Context(c) + err := RunPlugin(ctx, "foo", []string{"some params"}) + c.Assert(err, gc.ErrorMatches, "subprocess encountered error code 2") + c.Assert(err, jc.Satisfies, cmd.IsRcPassthroughError) + c.Assert(testing.Stdout(ctx), gc.Equals, "failing\n") + c.Assert(testing.Stderr(ctx), gc.Equals, "") +} + +func (suite *PluginSuite) TestGatherDescriptionsInParallel(c *gc.C) { + // Make plugins that will deadlock if we don't start them in parallel. + // Each plugin depends on another one being started before they will + // complete. They make a full loop, so no sequential ordering will ever + // succeed. + suite.makeFullPlugin(PluginParams{Name: "foo", Creates: "foo", DependsOn: "bar"}) + suite.makeFullPlugin(PluginParams{Name: "bar", Creates: "bar", DependsOn: "baz"}) + suite.makeFullPlugin(PluginParams{Name: "baz", Creates: "baz", DependsOn: "error"}) + suite.makeFullPlugin(PluginParams{Name: "error", ExitStatus: 1, Creates: "error", DependsOn: "foo"}) + + // If the code was wrong, GetPluginDescriptions would deadlock, + // so timeout after a short while + resultChan := make(chan []PluginDescription) + go func() { + resultChan <- GetPluginDescriptions() + }() + // 10 seconds is arbitrary but should always be generously long. Test + // actually only takes about 15ms in practice. But 10s allows for system hiccups, etc. + waitTime := 10 * time.Second + var results []PluginDescription + select { + case results = <-resultChan: + break + case <-time.After(waitTime): + c.Fatalf("took longer than %fs to complete.", waitTime.Seconds()) + } + + c.Assert(results, gc.HasLen, 4) + c.Assert(results[0].name, gc.Equals, "bar") + c.Assert(results[0].description, gc.Equals, "bar description") + c.Assert(results[1].name, gc.Equals, "baz") + c.Assert(results[1].description, gc.Equals, "baz description") + c.Assert(results[2].name, gc.Equals, "error") + c.Assert(results[2].description, gc.Equals, "error occurred running 'juju-error --description'") + c.Assert(results[3].name, gc.Equals, "foo") + c.Assert(results[3].description, gc.Equals, "foo description") +} + +func (suite *PluginSuite) TestHelpPluginsWithNoPlugins(c *gc.C) { + output := badrun(c, 0, "help", "plugins") + c.Assert(output, jc.HasPrefix, PluginTopicText) + c.Assert(output, jc.HasSuffix, "\n\nNo plugins found.\n") +} + +func (suite *PluginSuite) TestHelpPluginsWithPlugins(c *gc.C) { + suite.makeFullPlugin(PluginParams{Name: "foo"}) + suite.makeFullPlugin(PluginParams{Name: "bar"}) + output := badrun(c, 0, "help", "plugins") + c.Assert(output, jc.HasPrefix, PluginTopicText) + expectedPlugins := ` + +bar bar description +foo foo description +` + c.Assert(output, jc.HasSuffix, expectedPlugins) +} + +func (suite *PluginSuite) TestHelpPluginName(c *gc.C) { + suite.makeFullPlugin(PluginParams{Name: "foo"}) + output := badrun(c, 0, "help", "foo") + expectedHelp := `foo longer help + +something useful +` + c.Assert(output, gc.Matches, expectedHelp) +} + +func (suite *PluginSuite) TestHelpPluginNameNotAPlugin(c *gc.C) { + output := badrun(c, 0, "help", "foo") + expectedHelp := "ERROR unknown command or topic for foo\n" + c.Assert(output, gc.Matches, expectedHelp) +} + +func (suite *PluginSuite) TestHelpAsArg(c *gc.C) { + suite.makeFullPlugin(PluginParams{Name: "foo"}) + output := badrun(c, 0, "foo", "--help") + expectedHelp := `foo longer help + +something useful +` + c.Assert(output, gc.Matches, expectedHelp) +} + +func (suite *PluginSuite) TestDebugAsArg(c *gc.C) { + suite.makeFullPlugin(PluginParams{Name: "foo"}) + output := badrun(c, 0, "foo", "--debug") + expectedDebug := "some debug\n" + c.Assert(output, gc.Matches, expectedDebug) +} + +func (suite *PluginSuite) TestJujuEnvVars(c *gc.C) { + suite.makeFullPlugin(PluginParams{Name: "foo"}) + output := badrun(c, 0, "foo", "-e", "myenv", "-p", "pluginarg") + expectedDebug := `foo -e myenv -p pluginarg\n.*env is: myenv\n.*home is: .*\.juju\n` + c.Assert(output, gc.Matches, expectedDebug) +} + +func (suite *PluginSuite) makePlugin(name string, perm os.FileMode) { + content := fmt.Sprintf("#!/bin/bash --norc\necho %s $*", name) + filename := gitjujutesting.HomePath(JujuPluginPrefix + name) + ioutil.WriteFile(filename, []byte(content), perm) +} + +func (suite *PluginSuite) makeFailingPlugin(name string, exitStatus int) { + content := fmt.Sprintf("#!/bin/bash --norc\necho failing\nexit %d", exitStatus) + filename := gitjujutesting.HomePath(JujuPluginPrefix + name) + ioutil.WriteFile(filename, []byte(content), 0755) +} + +type PluginParams struct { + Name string + ExitStatus int + Creates string + DependsOn string +} + +const pluginTemplate = `#!/bin/bash --norc + +if [ "$1" = "--description" ]; then + if [ -n "{{.Creates}}" ]; then + touch "{{.Creates}}" + fi + if [ -n "{{.DependsOn}}" ]; then + # Sleep 10ms while waiting to allow other stuff to do work + while [ ! -e "{{.DependsOn}}" ]; do sleep 0.010; done + fi + echo "{{.Name}} description" + exit {{.ExitStatus}} +fi + +if [ "$1" = "--help" ]; then + echo "{{.Name}} longer help" + echo "" + echo "something useful" + exit {{.ExitStatus}} +fi + +if [ "$1" = "--debug" ]; then + echo "some debug" + exit {{.ExitStatus}} +fi + +echo {{.Name}} $* +echo "env is: " $JUJU_ENV +echo "home is: " $JUJU_HOME +exit {{.ExitStatus}} +` + +func (suite *PluginSuite) makeFullPlugin(params PluginParams) { + // Create a new template and parse the plugin into it. + t := template.Must(template.New("plugin").Parse(pluginTemplate)) + content := &bytes.Buffer{} + filename := gitjujutesting.HomePath("juju-" + params.Name) + // Create the files in the temp dirs, so we don't pollute the working space + if params.Creates != "" { + params.Creates = gitjujutesting.HomePath(params.Creates) + } + if params.DependsOn != "" { + params.DependsOn = gitjujutesting.HomePath(params.DependsOn) + } + t.Execute(content, params) + ioutil.WriteFile(filename, content.Bytes(), 0755) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/publish.go' --- src/github.com/juju/juju/cmd/juju/commands/publish.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/publish.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,190 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "fmt" + "os" + "strings" + "time" + + "github.com/juju/cmd" + "gopkg.in/juju/charm.v5" + "gopkg.in/juju/charm.v5/charmrepo" + "launchpad.net/gnuflag" + + "github.com/juju/juju/bzr" + "github.com/juju/juju/cmd/envcmd" +) + +type PublishCommand struct { + envcmd.EnvCommandBase + 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 == "" { + if err == nil { + 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.InferURL(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.Reference(), "/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 +} === added file 'src/github.com/juju/juju/cmd/juju/commands/publish_nix_test.go' --- src/github.com/juju/juju/cmd/juju/commands/publish_nix_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/publish_nix_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,9 @@ +// 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" === added file 'src/github.com/juju/juju/cmd/juju/commands/publish_test.go' --- src/github.com/juju/juju/cmd/juju/commands/publish_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/publish_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,403 @@ +// 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" + gc "gopkg.in/check.v1" + "gopkg.in/juju/charm.v5/charmrepo" + + "github.com/juju/juju/bzr" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/testing" +) + +// Sadly, this is a very slow test suite, heavily dominated by calls to bzr. + +type PublishSuite struct { + testing.FakeJujuHomeSuite + 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, envcmd.Wrap(&PublishCommand{}), args, s.dir) +} + +const pollDelay = testing.ShortWait + +func (s *PublishSuite) SetUpSuite(c *gc.C) { + s.FakeJujuHomeSuite.SetUpSuite(c) + s.HTTPSuite.SetUpSuite(c) + + s.oldBaseURL = charmrepo.LegacyStore.BaseURL + charmrepo.LegacyStore.BaseURL = s.URL("") +} + +func (s *PublishSuite) TearDownSuite(c *gc.C) { + s.FakeJujuHomeSuite.TearDownSuite(c) + s.HTTPSuite.TearDownSuite(c) + + charmrepo.LegacyStore.BaseURL = s.oldBaseURL +} + +func (s *PublishSuite) SetUpTest(c *gc.C) { + s.FakeJujuHomeSuite.SetUpTest(c) + s.HTTPSuite.SetUpTest(c) + s.PatchEnvironment("BZR_HOME", utils.Home()) + s.FakeJujuHomeSuite.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.FakeJujuHomeSuite.TearDownTest(c) +} + +func (s *PublishSuite) TestNoBranch(c *gc.C) { + dir := c.MkDir() + _, err := testing.RunCommandInDir(c, envcmd.Wrap(&PublishCommand{}), []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, envcmd.Wrap(&PublishCommand{}), []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) TestMissingSeries(c *gc.C) { + _, err := s.runPublish(c, "cs:wordpress") + c.Assert(err, gc.ErrorMatches, `cannot infer charm URL for "cs:wordpress": charm url series is not resolved`) +} + +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) TestInferURL(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, envcmd.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, envcmd.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, envcmd.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, envcmd.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") + } +} === added file 'src/github.com/juju/juju/cmd/juju/commands/publish_windows_test.go' --- src/github.com/juju/juju/cmd/juju/commands/publish_windows_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/publish_windows_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,9 @@ +// 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" === added file 'src/github.com/juju/juju/cmd/juju/commands/register.go' --- src/github.com/juju/juju/cmd/juju/commands/register.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/register.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,105 @@ +// Copyright 2015 Canonical Ltd. All rights reserved. + +package commands + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + + "github.com/juju/errors" + "github.com/juju/persistent-cookiejar" + "gopkg.in/macaroon-bakery.v0/httpbakery" + + "github.com/juju/juju/api" + "github.com/juju/juju/api/charms" +) + +var ( + openWebBrowser = func(_ *url.URL) error { return nil } +) + +type metricRegistrationPost struct { + EnvironmentUUID string `json:"env-uuid"` + CharmURL string `json:"charm-url"` + ServiceName string `json:"service-name"` +} + +var registerMeteredCharm = func(registrationURL string, state api.Connection, jar *cookiejar.Jar, charmURL string, serviceName, environmentUUID string) error { + charmsClient := charms.NewClient(state) + defer charmsClient.Close() + metered, err := charmsClient.IsMetered(charmURL) + if err != nil { + return err + } + if metered { + httpClient := httpbakery.NewHTTPClient() + httpClient.Jar = jar + credentials, err := registerMetrics(registrationURL, environmentUUID, charmURL, serviceName, httpClient, openWebBrowser) + if err != nil { + logger.Infof("failed to register metrics: %v", err) + return err + } + + api, cerr := getMetricCredentialsAPI(state) + if cerr != nil { + logger.Infof("failed to get the metrics credentials setter: %v", cerr) + } + err = api.SetMetricCredentials(serviceName, credentials) + if err != nil { + logger.Infof("failed to set metric credentials: %v", err) + return err + } + api.Close() + } + return nil +} + +func registerMetrics(registrationURL, environmentUUID, charmURL, serviceName string, client *http.Client, visitWebPage func(*url.URL) error) ([]byte, error) { + if registrationURL == "" { + return nil, errors.Errorf("no metric registration url is specified") + } + registerURL, err := url.Parse(registrationURL) + if err != nil { + return nil, errors.Trace(err) + } + + registrationPost := metricRegistrationPost{ + EnvironmentUUID: environmentUUID, + CharmURL: charmURL, + ServiceName: serviceName, + } + + buff := &bytes.Buffer{} + encoder := json.NewEncoder(buff) + err = encoder.Encode(registrationPost) + if err != nil { + return nil, errors.Trace(err) + } + + req, err := http.NewRequest("POST", registerURL.String(), nil) + if err != nil { + return nil, errors.Trace(err) + } + req.Header.Set("Content-Type", "application/json") + + bodyGetter := httpbakery.SeekerBody(bytes.NewReader(buff.Bytes())) + + response, err := httpbakery.DoWithBody(client, req, bodyGetter, visitWebPage) + if err != nil { + return nil, errors.Trace(err) + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, errors.Errorf("failed to register metrics: http response is %d", response.StatusCode) + } + + b, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, errors.Annotatef(err, "failed to read the response") + } + return b, nil +} === added file 'src/github.com/juju/juju/cmd/juju/commands/register_test.go' --- src/github.com/juju/juju/cmd/juju/commands/register_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/register_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,66 @@ +// Copyright 2015 Canonical Ltd. All rights reserved. + +package commands + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + + gc "gopkg.in/check.v1" +) + +var _ = gc.Suite(®istrationSuite{}) + +type registrationSuite struct { + handler *testMetricsRegistrationHandler + server *httptest.Server +} + +func (s *registrationSuite) SetUpTest(c *gc.C) { + s.handler = &testMetricsRegistrationHandler{} + s.server = httptest.NewServer(s.handler) +} + +func (s *registrationSuite) TearDownTest(c *gc.C) { + s.server.Close() +} + +func (s *registrationSuite) TestHttpMetricsRegistrar(c *gc.C) { + data, err := registerMetrics(s.server.URL, "environment uuid", "charm url", "service name", &http.Client{}, func(*url.URL) error { return nil }) + c.Assert(err, gc.IsNil) + var b []byte + err = json.Unmarshal(data, &b) + c.Assert(err, gc.IsNil) + c.Assert(string(b), gc.Equals, "hello registration") + c.Assert(s.handler.registrationCalls, gc.HasLen, 1) + c.Assert(s.handler.registrationCalls[0], gc.DeepEquals, metricRegistrationPost{EnvironmentUUID: "environment uuid", CharmURL: "charm url", ServiceName: "service name"}) +} + +type testMetricsRegistrationHandler struct { + registrationCalls []metricRegistrationPost +} + +// ServeHTTP implements http.Handler. +func (c *testMetricsRegistrationHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if req.Method != "POST" { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + var registrationPost metricRegistrationPost + decoder := json.NewDecoder(req.Body) + err := decoder.Decode(®istrationPost) + if err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + + err = json.NewEncoder(w).Encode([]byte("hello registration")) + if err != nil { + panic(err) + } + + c.registrationCalls = append(c.registrationCalls, registrationPost) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/removerelation.go' --- src/github.com/juju/juju/cmd/juju/commands/removerelation.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/removerelation.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,45 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "fmt" + + "github.com/juju/cmd" + + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/block" +) + +// RemoveRelationCommand causes an existing service relation to be shut down. +type RemoveRelationCommand struct { + envcmd.EnvCommandBase + Endpoints []string +} + +func (c *RemoveRelationCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "remove-relation", + Args: "[:] [:]", + Purpose: "remove a relation between two services", + Aliases: []string{"destroy-relation"}, + } +} + +func (c *RemoveRelationCommand) Init(args []string) error { + if len(args) != 2 { + return fmt.Errorf("a relation must involve two services") + } + c.Endpoints = args + return nil +} + +func (c *RemoveRelationCommand) Run(_ *cmd.Context) error { + client, err := c.NewAPIClient() + if err != nil { + return err + } + defer client.Close() + return block.ProcessBlockedError(client.DestroyRelation(c.Endpoints...), block.BlockRemove) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/removerelation_test.go' --- src/github.com/juju/juju/cmd/juju/commands/removerelation_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/removerelation_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,71 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/envcmd" + jujutesting "github.com/juju/juju/juju/testing" + "github.com/juju/juju/testcharms" + "github.com/juju/juju/testing" +) + +type RemoveRelationSuite struct { + jujutesting.RepoSuite + CmdBlockHelper +} + +func (s *RemoveRelationSuite) SetUpTest(c *gc.C) { + s.RepoSuite.SetUpTest(c) + s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) + c.Assert(s.CmdBlockHelper, gc.NotNil) + s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) +} + +var _ = gc.Suite(&RemoveRelationSuite{}) + +func runRemoveRelation(c *gc.C, args ...string) error { + _, err := testing.RunCommand(c, envcmd.Wrap(&RemoveRelationCommand{}), args...) + return err +} + +func (s *RemoveRelationSuite) setupRelationForRemove(c *gc.C) { + testcharms.Repo.CharmArchivePath(s.SeriesPath, "riak") + err := runDeploy(c, "local:riak", "riak") + c.Assert(err, jc.ErrorIsNil) + testcharms.Repo.CharmArchivePath(s.SeriesPath, "logging") + err = runDeploy(c, "local:logging", "logging") + c.Assert(err, jc.ErrorIsNil) + runAddRelation(c, "riak", "logging") +} + +func (s *RemoveRelationSuite) TestRemoveRelation(c *gc.C) { + s.setupRelationForRemove(c) + + // Destroy a relation that exists. + err := runRemoveRelation(c, "logging", "riak") + c.Assert(err, jc.ErrorIsNil) + + // Destroy a relation that used to exist. + err = runRemoveRelation(c, "riak", "logging") + c.Assert(err, gc.ErrorMatches, `relation "logging:info riak:juju-info" not found`) + + // Invalid removes. + err = runRemoveRelation(c, "ping", "pong") + c.Assert(err, gc.ErrorMatches, `service "ping" not found`) + err = runRemoveRelation(c, "riak") + c.Assert(err, gc.ErrorMatches, `a relation must involve two services`) +} + +func (s *RemoveRelationSuite) TestBlockRemoveRelation(c *gc.C) { + s.setupRelationForRemove(c) + + // block operation + s.BlockRemoveObject(c, "TestBlockRemoveRelation") + // Destroy a relation that exists. + err := runRemoveRelation(c, "logging", "riak") + s.AssertBlocked(c, err, ".*TestBlockRemoveRelation.*") +} === added file 'src/github.com/juju/juju/cmd/juju/commands/removeservice.go' --- src/github.com/juju/juju/cmd/juju/commands/removeservice.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/removeservice.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,60 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "fmt" + + "github.com/juju/cmd" + "github.com/juju/names" + + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/block" +) + +// RemoveServiceCommand causes an existing service to be destroyed. +type RemoveServiceCommand struct { + envcmd.EnvCommandBase + ServiceName string +} + +const removeServiceDoc = ` +Removing a service will remove all its units and relations. + +If this is the only service running, the machine on which +the service is hosted will also be destroyed, if possible. +The machine will be destroyed if: +- it is not a state server +- it is not hosting any Juju managed containers +` + +func (c *RemoveServiceCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "remove-service", + Args: "", + Purpose: "remove a service from the environment", + Doc: removeServiceDoc, + Aliases: []string{"destroy-service"}, + } +} + +func (c *RemoveServiceCommand) Init(args []string) error { + if len(args) == 0 { + return fmt.Errorf("no service specified") + } + if !names.IsValidService(args[0]) { + return fmt.Errorf("invalid service name %q", args[0]) + } + c.ServiceName, args = args[0], args[1:] + return cmd.CheckEmpty(args) +} + +func (c *RemoveServiceCommand) Run(_ *cmd.Context) error { + client, err := c.NewAPIClient() + if err != nil { + return err + } + defer client.Close() + return block.ProcessBlockedError(client.ServiceDestroy(c.ServiceName), block.BlockRemove) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/removeservice_test.go' --- src/github.com/juju/juju/cmd/juju/commands/removeservice_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/removeservice_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,77 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/envcmd" + jujutesting "github.com/juju/juju/juju/testing" + "github.com/juju/juju/state" + "github.com/juju/juju/testcharms" + "github.com/juju/juju/testing" +) + +type RemoveServiceSuite struct { + jujutesting.RepoSuite + CmdBlockHelper +} + +var _ = gc.Suite(&RemoveServiceSuite{}) + +func (s *RemoveServiceSuite) SetUpTest(c *gc.C) { + s.RepoSuite.SetUpTest(c) + s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) + c.Assert(s.CmdBlockHelper, gc.NotNil) + s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) +} + +func runRemoveService(c *gc.C, args ...string) error { + _, err := testing.RunCommand(c, envcmd.Wrap(&RemoveServiceCommand{}), args...) + return err +} + +func (s *RemoveServiceSuite) setupTestService(c *gc.C) { + // Destroy a service that exists. + testcharms.Repo.CharmArchivePath(s.SeriesPath, "riak") + err := runDeploy(c, "local:riak", "riak") + c.Assert(err, jc.ErrorIsNil) +} + +func (s *RemoveServiceSuite) TestSuccess(c *gc.C) { + s.setupTestService(c) + err := runRemoveService(c, "riak") + c.Assert(err, jc.ErrorIsNil) + riak, err := s.State.Service("riak") + c.Assert(err, jc.ErrorIsNil) + c.Assert(riak.Life(), gc.Equals, state.Dying) +} + +func (s *RemoveServiceSuite) TestBlockRemoveService(c *gc.C) { + s.setupTestService(c) + + // block operation + s.BlockRemoveObject(c, "TestBlockRemoveService") + err := runRemoveService(c, "riak") + s.AssertBlocked(c, err, ".*TestBlockRemoveService.*") + riak, err := s.State.Service("riak") + c.Assert(err, jc.ErrorIsNil) + c.Assert(riak.Life(), gc.Equals, state.Alive) +} + +func (s *RemoveServiceSuite) TestFailure(c *gc.C) { + // Destroy a service that does not exist. + err := runRemoveService(c, "gargleblaster") + c.Assert(err, gc.ErrorMatches, `service "gargleblaster" not found`) +} + +func (s *RemoveServiceSuite) TestInvalidArgs(c *gc.C) { + err := runRemoveService(c) + c.Assert(err, gc.ErrorMatches, `no service specified`) + err = runRemoveService(c, "ping", "pong") + c.Assert(err, gc.ErrorMatches, `unrecognized args: \["pong"\]`) + err = runRemoveService(c, "invalid:name") + c.Assert(err, gc.ErrorMatches, `invalid service name "invalid:name"`) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/removeunit.go' --- src/github.com/juju/juju/cmd/juju/commands/removeunit.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/removeunit.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,64 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "fmt" + + "github.com/juju/cmd" + "github.com/juju/names" + + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/block" +) + +// RemoveUnitCommand is responsible for destroying service units. +type RemoveUnitCommand struct { + envcmd.EnvCommandBase + UnitNames []string +} + +const removeUnitDoc = ` +Remove service units from the environment. + +If this is the only unit running, the machine on which +the unit is hosted will also be destroyed, if possible. +The machine will be destroyed if: +- it is not a state server +- it is not hosting any Juju managed containers +` + +func (c *RemoveUnitCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "remove-unit", + Args: " [...]", + Purpose: "remove service units from the environment", + Doc: removeUnitDoc, + Aliases: []string{"destroy-unit"}, + } +} + +func (c *RemoveUnitCommand) Init(args []string) error { + c.UnitNames = args + if len(c.UnitNames) == 0 { + return fmt.Errorf("no units specified") + } + for _, name := range c.UnitNames { + if !names.IsValidUnit(name) { + return fmt.Errorf("invalid unit name %q", name) + } + } + return nil +} + +// Run connects to the environment specified on the command line and destroys +// units therein. +func (c *RemoveUnitCommand) Run(_ *cmd.Context) error { + client, err := c.NewAPIClient() + if err != nil { + return err + } + defer client.Close() + return block.ProcessBlockedError(client.DestroyServiceUnits(c.UnitNames...), block.BlockRemove) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/removeunit_test.go' --- src/github.com/juju/juju/cmd/juju/commands/removeunit_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/removeunit_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,67 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "fmt" + + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/juju/charm.v5" + + "github.com/juju/juju/cmd/envcmd" + jujutesting "github.com/juju/juju/juju/testing" + "github.com/juju/juju/state" + "github.com/juju/juju/testcharms" + "github.com/juju/juju/testing" +) + +type RemoveUnitSuite struct { + jujutesting.RepoSuite + CmdBlockHelper +} + +func (s *RemoveUnitSuite) SetUpTest(c *gc.C) { + s.RepoSuite.SetUpTest(c) + s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) + c.Assert(s.CmdBlockHelper, gc.NotNil) + s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) +} + +var _ = gc.Suite(&RemoveUnitSuite{}) + +func runRemoveUnit(c *gc.C, args ...string) error { + _, err := testing.RunCommand(c, envcmd.Wrap(&RemoveUnitCommand{}), args...) + return err +} + +func (s *RemoveUnitSuite) setupUnitForRemove(c *gc.C) *state.Service { + testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") + err := runDeploy(c, "-n", "2", "local:dummy", "dummy") + c.Assert(err, jc.ErrorIsNil) + curl := charm.MustParseURL(fmt.Sprintf("local:%s/dummy-1", testing.FakeDefaultSeries)) + svc, _ := s.AssertService(c, "dummy", curl, 2, 0) + return svc +} + +func (s *RemoveUnitSuite) TestRemoveUnit(c *gc.C) { + svc := s.setupUnitForRemove(c) + + err := runRemoveUnit(c, "dummy/0", "dummy/1", "dummy/2", "sillybilly/17") + c.Assert(err, gc.ErrorMatches, `some units were not destroyed: unit "dummy/2" does not exist; unit "sillybilly/17" does not exist`) + units, err := svc.AllUnits() + c.Assert(err, jc.ErrorIsNil) + for _, u := range units { + c.Assert(u.Life(), gc.Equals, state.Dying) + } +} +func (s *RemoveUnitSuite) TestBlockRemoveUnit(c *gc.C) { + svc := s.setupUnitForRemove(c) + + // block operation + s.BlockRemoveObject(c, "TestBlockRemoveUnit") + err := runRemoveUnit(c, "dummy/0", "dummy/1") + s.AssertBlocked(c, err, ".*TestBlockRemoveUnit.*") + c.Assert(svc.Life(), gc.Equals, state.Alive) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/resolved.go' --- src/github.com/juju/juju/cmd/juju/commands/resolved.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/resolved.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,57 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "fmt" + + "github.com/juju/cmd" + "github.com/juju/names" + "launchpad.net/gnuflag" + + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/block" +) + +// ResolvedCommand marks a unit in an error state as ready to continue. +type ResolvedCommand struct { + envcmd.EnvCommandBase + UnitName string + Retry bool +} + +func (c *ResolvedCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "resolved", + Args: "", + Purpose: "marks unit errors resolved", + } +} + +func (c *ResolvedCommand) SetFlags(f *gnuflag.FlagSet) { + f.BoolVar(&c.Retry, "r", false, "re-execute failed hooks") + f.BoolVar(&c.Retry, "retry", false, "") +} + +func (c *ResolvedCommand) Init(args []string) error { + if len(args) > 0 { + c.UnitName = args[0] + if !names.IsValidUnit(c.UnitName) { + return fmt.Errorf("invalid unit name %q", c.UnitName) + } + args = args[1:] + } else { + return fmt.Errorf("no unit specified") + } + return cmd.CheckEmpty(args) +} + +func (c *ResolvedCommand) Run(_ *cmd.Context) error { + client, err := c.NewAPIClient() + if err != nil { + return err + } + defer client.Close() + return block.ProcessBlockedError(client.Resolved(c.UnitName, c.Retry), block.BlockChange) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/resolved_test.go' --- src/github.com/juju/juju/cmd/juju/commands/resolved_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/resolved_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,128 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/envcmd" + jujutesting "github.com/juju/juju/juju/testing" + "github.com/juju/juju/state" + "github.com/juju/juju/testcharms" + "github.com/juju/juju/testing" +) + +type ResolvedSuite struct { + jujutesting.RepoSuite + CmdBlockHelper +} + +func (s *ResolvedSuite) SetUpTest(c *gc.C) { + s.RepoSuite.SetUpTest(c) + s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) + c.Assert(s.CmdBlockHelper, gc.NotNil) + s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) +} + +var _ = gc.Suite(&ResolvedSuite{}) + +func runResolved(c *gc.C, args []string) error { + _, err := testing.RunCommand(c, envcmd.Wrap(&ResolvedCommand{}), args...) + return err +} + +var resolvedTests = []struct { + args []string + err string + unit string + mode state.ResolvedMode +}{ + { + err: `no unit specified`, + }, { + args: []string{"jeremy-fisher"}, + err: `invalid unit name "jeremy-fisher"`, + }, { + args: []string{"jeremy-fisher/99"}, + err: `unit "jeremy-fisher/99" not found`, + }, { + args: []string{"dummy/0"}, + err: `unit "dummy/0" is not in an error state`, + unit: "dummy/0", + mode: state.ResolvedNone, + }, { + args: []string{"dummy/1", "--retry"}, + err: `unit "dummy/1" is not in an error state`, + unit: "dummy/1", + mode: state.ResolvedNone, + }, { + args: []string{"dummy/2"}, + unit: "dummy/2", + mode: state.ResolvedNoHooks, + }, { + args: []string{"dummy/2", "--retry"}, + err: `cannot set resolved mode for unit "dummy/2": already resolved`, + unit: "dummy/2", + mode: state.ResolvedNoHooks, + }, { + args: []string{"dummy/3", "--retry"}, + unit: "dummy/3", + mode: state.ResolvedRetryHooks, + }, { + args: []string{"dummy/3"}, + err: `cannot set resolved mode for unit "dummy/3": already resolved`, + unit: "dummy/3", + mode: state.ResolvedRetryHooks, + }, { + args: []string{"dummy/4", "roflcopter"}, + err: `unrecognized args: \["roflcopter"\]`, + }, +} + +func (s *ResolvedSuite) TestResolved(c *gc.C) { + testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") + err := runDeploy(c, "-n", "5", "local:dummy", "dummy") + c.Assert(err, jc.ErrorIsNil) + + for _, name := range []string{"dummy/2", "dummy/3", "dummy/4"} { + u, err := s.State.Unit(name) + c.Assert(err, jc.ErrorIsNil) + err = u.SetAgentStatus(state.StatusError, "lol borken", nil) + c.Assert(err, jc.ErrorIsNil) + } + + for i, t := range resolvedTests { + c.Logf("test %d: %v", i, t.args) + err := runResolved(c, t.args) + if t.err != "" { + c.Assert(err, gc.ErrorMatches, t.err) + } else { + c.Assert(err, jc.ErrorIsNil) + } + if t.unit != "" { + unit, err := s.State.Unit(t.unit) + c.Assert(err, jc.ErrorIsNil) + c.Assert(unit.Resolved(), gc.Equals, t.mode) + } + } +} + +func (s *ResolvedSuite) TestBlockResolved(c *gc.C) { + testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") + err := runDeploy(c, "-n", "5", "local:dummy", "dummy") + c.Assert(err, jc.ErrorIsNil) + + for _, name := range []string{"dummy/2", "dummy/3", "dummy/4"} { + u, err := s.State.Unit(name) + c.Assert(err, jc.ErrorIsNil) + err = u.SetAgentStatus(state.StatusError, "lol borken", nil) + c.Assert(err, jc.ErrorIsNil) + } + + // Block operation + s.BlockAllChanges(c, "TestBlockResolved") + err = runResolved(c, []string{"dummy/2"}) + s.AssertBlocked(c, err, ".*TestBlockResolved.*") +} === added file 'src/github.com/juju/juju/cmd/juju/commands/run.go' --- src/github.com/juju/juju/cmd/juju/commands/run.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/run.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,232 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "encoding/base64" + "fmt" + "strings" + "time" + "unicode/utf8" + + "github.com/juju/cmd" + "github.com/juju/names" + "launchpad.net/gnuflag" + + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/block" +) + +// RunCommand is responsible for running arbitrary commands on remote machines. +type RunCommand struct { + envcmd.EnvCommandBase + out cmd.Output + all bool + timeout time.Duration + machines []string + services []string + units []string + commands string +} + +const runDoc = ` +Run the commands on the specified targets. + +Targets are specified using either machine ids, service names or unit +names. At least one target specifier is needed. + +Multiple values can be set for --machine, --service, and --unit by using +comma separated values. + +If the target is a machine, the command is run as the "ubuntu" user on +the remote machine. + +If the target is a service, the command is run on all units for that +service. For example, if there was a service "mysql" and that service +had two units, "mysql/0" and "mysql/1", then + --service mysql +is equivalent to + --unit mysql/0,mysql/1 + +Commands run for services or units are executed in a 'hook context' for +the unit. + +--all is provided as a simple way to run the command on all the machines +in the environment. If you specify --all you cannot provide additional +targets. + +` + +func (c *RunCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "run", + Args: "", + Purpose: "run the commands on the remote targets specified", + Doc: runDoc, + } +} + +func (c *RunCommand) SetFlags(f *gnuflag.FlagSet) { + c.out.AddFlags(f, "smart", cmd.DefaultFormatters) + f.BoolVar(&c.all, "all", false, "run the commands on all the machines") + f.DurationVar(&c.timeout, "timeout", 5*time.Minute, "how long to wait before the remote command is considered to have failed") + f.Var(cmd.NewStringsValue(nil, &c.machines), "machine", "one or more machine ids") + f.Var(cmd.NewStringsValue(nil, &c.services), "service", "one or more service names") + f.Var(cmd.NewStringsValue(nil, &c.units), "unit", "one or more unit ids") +} + +func (c *RunCommand) Init(args []string) error { + if len(args) == 0 { + return fmt.Errorf("no commands specified") + } + c.commands, args = args[0], args[1:] + + if c.all { + if len(c.machines) != 0 { + return fmt.Errorf("You cannot specify --all and individual machines") + } + if len(c.services) != 0 { + return fmt.Errorf("You cannot specify --all and individual services") + } + if len(c.units) != 0 { + return fmt.Errorf("You cannot specify --all and individual units") + } + } else { + if len(c.machines) == 0 && len(c.services) == 0 && len(c.units) == 0 { + return fmt.Errorf("You must specify a target, either through --all, --machine, --service or --unit") + } + } + + var nameErrors []string + for _, machineId := range c.machines { + if !names.IsValidMachine(machineId) { + nameErrors = append(nameErrors, fmt.Sprintf(" %q is not a valid machine id", machineId)) + } + } + for _, service := range c.services { + if !names.IsValidService(service) { + nameErrors = append(nameErrors, fmt.Sprintf(" %q is not a valid service name", service)) + } + } + for _, unit := range c.units { + if !names.IsValidUnit(unit) { + nameErrors = append(nameErrors, fmt.Sprintf(" %q is not a valid unit name", unit)) + } + } + if len(nameErrors) > 0 { + return fmt.Errorf("The following run targets are not valid:\n%s", + strings.Join(nameErrors, "\n")) + } + + return cmd.CheckEmpty(args) +} + +func encodeBytes(input []byte) (value string, encoding string) { + if utf8.Valid(input) { + value = string(input) + encoding = "utf8" + } else { + value = base64.StdEncoding.EncodeToString(input) + encoding = "base64" + } + return value, encoding +} + +func storeOutput(values map[string]interface{}, key string, input []byte) { + value, encoding := encodeBytes(input) + values[key] = value + if encoding != "utf8" { + values[key+".encoding"] = encoding + } +} + +// ConvertRunResults takes the results from the api and creates a map +// suitable for format converstion to YAML or JSON. +func ConvertRunResults(runResults []params.RunResult) interface{} { + var results = make([]interface{}, len(runResults)) + + for i, result := range runResults { + // We always want to have a string for stdout, but only show stderr, + // code and error if they are there. + values := make(map[string]interface{}) + values["MachineId"] = result.MachineId + if result.UnitId != "" { + values["UnitId"] = result.UnitId + + } + storeOutput(values, "Stdout", result.Stdout) + if len(result.Stderr) > 0 { + storeOutput(values, "Stderr", result.Stderr) + } + if result.Code != 0 { + values["ReturnCode"] = result.Code + } + if result.Error != "" { + values["Error"] = result.Error + } + results[i] = values + } + + return results +} + +func (c *RunCommand) Run(ctx *cmd.Context) error { + client, err := getRunAPIClient(c) + if err != nil { + return err + } + defer client.Close() + + var runResults []params.RunResult + if c.all { + runResults, err = client.RunOnAllMachines(c.commands, c.timeout) + } else { + params := params.RunParams{ + Commands: c.commands, + Timeout: c.timeout, + Machines: c.machines, + Services: c.services, + Units: c.units, + } + runResults, err = client.Run(params) + } + + if err != nil { + return block.ProcessBlockedError(err, block.BlockChange) + } + + // If we are just dealing with one result, AND we are using the smart + // format, then pretend we were running it locally. + if len(runResults) == 1 && c.out.Name() == "smart" { + result := runResults[0] + ctx.Stdout.Write(result.Stdout) + ctx.Stderr.Write(result.Stderr) + if result.Error != "" { + // Convert the error string back into an error object. + return fmt.Errorf("%s", result.Error) + } + if result.Code != 0 { + return cmd.NewRcPassthroughError(result.Code) + } + return nil + } + + c.out.Write(ctx, ConvertRunResults(runResults)) + return nil +} + +// In order to be able to easily mock out the API side for testing, +// the API client is got using a function. + +type RunClient interface { + Close() error + RunOnAllMachines(commands string, timeout time.Duration) ([]params.RunResult, error) + Run(run params.RunParams) ([]params.RunResult, error) +} + +// Here we need the signature to be correct for the interface. +var getRunAPIClient = func(c *RunCommand) (RunClient, error) { + return c.NewAPIClient() +} === added file 'src/github.com/juju/juju/cmd/juju/commands/run_test.go' --- src/github.com/juju/juju/cmd/juju/commands/run_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/run_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,486 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "fmt" + "sort" + "strings" + "time" + + "github.com/juju/cmd" + jc "github.com/juju/testing/checkers" + "github.com/juju/utils/exec" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/testing" +) + +type RunSuite struct { + testing.FakeJujuHomeSuite +} + +var _ = gc.Suite(&RunSuite{}) + +func (*RunSuite) TestTargetArgParsing(c *gc.C) { + for i, test := range []struct { + message string + args []string + all bool + machines []string + units []string + services []string + commands string + errMatch string + }{{ + message: "no args", + errMatch: "no commands specified", + }, { + message: "no target", + args: []string{"sudo reboot"}, + errMatch: "You must specify a target, either through --all, --machine, --service or --unit", + }, { + message: "too many args", + args: []string{"--all", "sudo reboot", "oops"}, + errMatch: `unrecognized args: \["oops"\]`, + }, { + message: "command to all machines", + args: []string{"--all", "sudo reboot"}, + all: true, + commands: "sudo reboot", + }, { + message: "all and defined machines", + args: []string{"--all", "--machine=1,2", "sudo reboot"}, + errMatch: `You cannot specify --all and individual machines`, + }, { + message: "command to machines 1, 2, and 1/kvm/0", + args: []string{"--machine=1,2,1/kvm/0", "sudo reboot"}, + commands: "sudo reboot", + machines: []string{"1", "2", "1/kvm/0"}, + }, { + message: "bad machine names", + args: []string{"--machine=foo,machine-2", "sudo reboot"}, + errMatch: "" + + "The following run targets are not valid:\n" + + " \"foo\" is not a valid machine id\n" + + " \"machine-2\" is not a valid machine id", + }, { + message: "all and defined services", + args: []string{"--all", "--service=wordpress,mysql", "sudo reboot"}, + errMatch: `You cannot specify --all and individual services`, + }, { + message: "command to services wordpress and mysql", + args: []string{"--service=wordpress,mysql", "sudo reboot"}, + commands: "sudo reboot", + services: []string{"wordpress", "mysql"}, + }, { + message: "bad service names", + args: []string{"--service", "foo,2,foo/0", "sudo reboot"}, + errMatch: "" + + "The following run targets are not valid:\n" + + " \"2\" is not a valid service name\n" + + " \"foo/0\" is not a valid service name", + }, { + message: "all and defined units", + args: []string{"--all", "--unit=wordpress/0,mysql/1", "sudo reboot"}, + errMatch: `You cannot specify --all and individual units`, + }, { + message: "command to valid units", + args: []string{"--unit=wordpress/0,wordpress/1,mysql/0", "sudo reboot"}, + commands: "sudo reboot", + units: []string{"wordpress/0", "wordpress/1", "mysql/0"}, + }, { + message: "bad unit names", + args: []string{"--unit", "foo,2,foo/0", "sudo reboot"}, + errMatch: "" + + "The following run targets are not valid:\n" + + " \"foo\" is not a valid unit name\n" + + " \"2\" is not a valid unit name", + }, { + message: "command to mixed valid targets", + args: []string{"--machine=0", "--unit=wordpress/0,wordpress/1", "--service=mysql", "sudo reboot"}, + commands: "sudo reboot", + machines: []string{"0"}, + services: []string{"mysql"}, + units: []string{"wordpress/0", "wordpress/1"}, + }} { + c.Log(fmt.Sprintf("%v: %s", i, test.message)) + runCmd := &RunCommand{} + testing.TestInit(c, envcmd.Wrap(runCmd), test.args, test.errMatch) + if test.errMatch == "" { + c.Check(runCmd.all, gc.Equals, test.all) + c.Check(runCmd.machines, gc.DeepEquals, test.machines) + c.Check(runCmd.services, gc.DeepEquals, test.services) + c.Check(runCmd.units, gc.DeepEquals, test.units) + c.Check(runCmd.commands, gc.Equals, test.commands) + } + } +} + +func (*RunSuite) TestTimeoutArgParsing(c *gc.C) { + for i, test := range []struct { + message string + args []string + errMatch string + timeout time.Duration + }{{ + message: "default time", + args: []string{"--all", "sudo reboot"}, + timeout: 5 * time.Minute, + }, { + message: "invalid time", + args: []string{"--timeout=foo", "--all", "sudo reboot"}, + errMatch: `invalid value "foo" for flag --timeout: time: invalid duration foo`, + }, { + message: "two hours", + args: []string{"--timeout=2h", "--all", "sudo reboot"}, + timeout: 2 * time.Hour, + }, { + message: "3 minutes 30 seconds", + args: []string{"--timeout=3m30s", "--all", "sudo reboot"}, + timeout: (3 * time.Minute) + (30 * time.Second), + }} { + c.Log(fmt.Sprintf("%v: %s", i, test.message)) + runCmd := &RunCommand{} + testing.TestInit(c, envcmd.Wrap(runCmd), test.args, test.errMatch) + if test.errMatch == "" { + c.Check(runCmd.timeout, gc.Equals, test.timeout) + } + } +} + +func (s *RunSuite) TestConvertRunResults(c *gc.C) { + for i, test := range []struct { + message string + results []params.RunResult + expected interface{} + }{{ + message: "empty", + expected: []interface{}{}, + }, { + message: "minimum is machine id and stdout", + results: []params.RunResult{ + makeRunResult(mockResponse{machineId: "1"}), + }, + expected: []interface{}{ + map[string]interface{}{ + "MachineId": "1", + "Stdout": "", + }}, + }, { + message: "other fields are copied if there", + results: []params.RunResult{ + makeRunResult(mockResponse{ + machineId: "1", + stdout: "stdout", + stderr: "stderr", + code: 42, + unitId: "unit/0", + error: "error", + }), + }, + expected: []interface{}{ + map[string]interface{}{ + "MachineId": "1", + "Stdout": "stdout", + "Stderr": "stderr", + "ReturnCode": 42, + "UnitId": "unit/0", + "Error": "error", + }}, + }, { + message: "stdout and stderr are base64 encoded if not valid utf8", + results: []params.RunResult{ + { + ExecResponse: exec.ExecResponse{ + Stdout: []byte{0xff}, + Stderr: []byte{0xfe}, + }, + MachineId: "jake", + }, + }, + expected: []interface{}{ + map[string]interface{}{ + "MachineId": "jake", + "Stdout": "/w==", + "Stdout.encoding": "base64", + "Stderr": "/g==", + "Stderr.encoding": "base64", + }}, + }, { + message: "more than one", + results: []params.RunResult{ + makeRunResult(mockResponse{machineId: "1"}), + makeRunResult(mockResponse{machineId: "2"}), + makeRunResult(mockResponse{machineId: "3"}), + }, + expected: []interface{}{ + map[string]interface{}{ + "MachineId": "1", + "Stdout": "", + }, + map[string]interface{}{ + "MachineId": "2", + "Stdout": "", + }, + map[string]interface{}{ + "MachineId": "3", + "Stdout": "", + }, + }, + }} { + c.Log(fmt.Sprintf("%v: %s", i, test.message)) + result := ConvertRunResults(test.results) + c.Check(result, jc.DeepEquals, test.expected) + } +} + +func (s *RunSuite) TestRunForMachineAndUnit(c *gc.C) { + mock := s.setupMockAPI() + machineResponse := mockResponse{ + stdout: "megatron\n", + machineId: "0", + } + unitResponse := mockResponse{ + stdout: "bumblebee", + machineId: "1", + unitId: "unit/0", + } + mock.setResponse("0", machineResponse) + mock.setResponse("unit/0", unitResponse) + + unformatted := ConvertRunResults([]params.RunResult{ + makeRunResult(machineResponse), + makeRunResult(unitResponse), + }) + + jsonFormatted, err := cmd.FormatJson(unformatted) + c.Assert(err, jc.ErrorIsNil) + + context, err := testing.RunCommand(c, envcmd.Wrap(&RunCommand{}), + "--format=json", "--machine=0", "--unit=unit/0", "hostname", + ) + c.Assert(err, jc.ErrorIsNil) + + c.Check(testing.Stdout(context), gc.Equals, string(jsonFormatted)+"\n") +} + +func (s *RunSuite) TestBlockRunForMachineAndUnit(c *gc.C) { + mock := s.setupMockAPI() + // Block operation + mock.block = true + _, err := testing.RunCommand(c, envcmd.Wrap(&RunCommand{}), + "--format=json", "--machine=0", "--unit=unit/0", "hostname", + "-e blah", + ) + c.Assert(err, gc.ErrorMatches, cmd.ErrSilent.Error()) + // msg is logged + stripped := strings.Replace(c.GetTestLog(), "\n", "", -1) + c.Check(stripped, gc.Matches, ".*To unblock changes.*") +} + +func (s *RunSuite) TestAllMachines(c *gc.C) { + mock := s.setupMockAPI() + mock.setMachinesAlive("0", "1") + response0 := mockResponse{ + stdout: "megatron\n", + machineId: "0", + } + response1 := mockResponse{ + error: "command timed out", + machineId: "1", + } + mock.setResponse("0", response0) + + unformatted := ConvertRunResults([]params.RunResult{ + makeRunResult(response0), + makeRunResult(response1), + }) + + jsonFormatted, err := cmd.FormatJson(unformatted) + c.Assert(err, jc.ErrorIsNil) + + context, err := testing.RunCommand(c, &RunCommand{}, "--format=json", "--all", "hostname") + c.Assert(err, jc.ErrorIsNil) + + c.Check(testing.Stdout(context), gc.Equals, string(jsonFormatted)+"\n") +} + +func (s *RunSuite) TestBlockAllMachines(c *gc.C) { + mock := s.setupMockAPI() + // Block operation + mock.block = true + _, err := testing.RunCommand(c, &RunCommand{}, "--format=json", "--all", "hostname") + c.Assert(err, gc.ErrorMatches, cmd.ErrSilent.Error()) + // msg is logged + stripped := strings.Replace(c.GetTestLog(), "\n", "", -1) + c.Check(stripped, gc.Matches, ".*To unblock changes.*") +} + +func (s *RunSuite) TestSingleResponse(c *gc.C) { + mock := s.setupMockAPI() + mock.setMachinesAlive("0") + mockResponse := mockResponse{ + stdout: "stdout\n", + stderr: "stderr\n", + code: 42, + machineId: "0", + } + mock.setResponse("0", mockResponse) + unformatted := ConvertRunResults([]params.RunResult{ + makeRunResult(mockResponse)}) + yamlFormatted, err := cmd.FormatYaml(unformatted) + c.Assert(err, jc.ErrorIsNil) + jsonFormatted, err := cmd.FormatJson(unformatted) + c.Assert(err, jc.ErrorIsNil) + + for i, test := range []struct { + message string + format string + stdout string + stderr string + errorMatch string + }{{ + message: "smart (default)", + stdout: "stdout\n", + stderr: "stderr\n", + errorMatch: "subprocess encountered error code 42", + }, { + message: "yaml output", + format: "yaml", + stdout: string(yamlFormatted) + "\n", + }, { + message: "json output", + format: "json", + stdout: string(jsonFormatted) + "\n", + }} { + c.Log(fmt.Sprintf("%v: %s", i, test.message)) + args := []string{} + if test.format != "" { + args = append(args, "--format", test.format) + } + args = append(args, "--all", "ignored") + context, err := testing.RunCommand(c, envcmd.Wrap(&RunCommand{}), args...) + if test.errorMatch != "" { + c.Check(err, gc.ErrorMatches, test.errorMatch) + } else { + c.Check(err, jc.ErrorIsNil) + } + c.Check(testing.Stdout(context), gc.Equals, test.stdout) + c.Check(testing.Stderr(context), gc.Equals, test.stderr) + } +} + +func (s *RunSuite) setupMockAPI() *mockRunAPI { + mock := &mockRunAPI{} + s.PatchValue(&getRunAPIClient, func(_ *RunCommand) (RunClient, error) { + return mock, nil + }) + return mock +} + +type mockRunAPI struct { + stdout string + stderr string + code int + // machines, services, units + machines map[string]bool + responses map[string]params.RunResult + block bool +} + +type mockResponse struct { + stdout string + stderr string + code int + error string + machineId string + unitId string +} + +var _ RunClient = (*mockRunAPI)(nil) + +func (m *mockRunAPI) setMachinesAlive(ids ...string) { + if m.machines == nil { + m.machines = make(map[string]bool) + } + for _, id := range ids { + m.machines[id] = true + } +} + +func makeRunResult(mock mockResponse) params.RunResult { + return params.RunResult{ + ExecResponse: exec.ExecResponse{ + Stdout: []byte(mock.stdout), + Stderr: []byte(mock.stderr), + Code: mock.code, + }, + MachineId: mock.machineId, + UnitId: mock.unitId, + Error: mock.error, + } +} + +func (m *mockRunAPI) setResponse(id string, mock mockResponse) { + if m.responses == nil { + m.responses = make(map[string]params.RunResult) + } + m.responses[id] = makeRunResult(mock) +} + +func (*mockRunAPI) Close() error { + return nil +} + +func (m *mockRunAPI) RunOnAllMachines(commands string, timeout time.Duration) ([]params.RunResult, error) { + var result []params.RunResult + + if m.block { + return result, common.ErrOperationBlocked("The operation has been blocked.") + } + sortedMachineIds := make([]string, 0, len(m.machines)) + for machineId := range m.machines { + sortedMachineIds = append(sortedMachineIds, machineId) + } + sort.Strings(sortedMachineIds) + + for _, machineId := range sortedMachineIds { + response, found := m.responses[machineId] + if !found { + // Consider this a timeout + response = params.RunResult{MachineId: machineId, Error: "command timed out"} + } + result = append(result, response) + } + + return result, nil +} + +func (m *mockRunAPI) Run(runParams params.RunParams) ([]params.RunResult, error) { + var result []params.RunResult + + if m.block { + return result, common.ErrOperationBlocked("The operation has been blocked.") + } + // Just add in ids that match in order. + for _, id := range runParams.Machines { + response, found := m.responses[id] + if found { + result = append(result, response) + } + } + // mock ignores services + for _, id := range runParams.Units { + response, found := m.responses[id] + if found { + result = append(result, response) + } + } + + return result, nil +} === added file 'src/github.com/juju/juju/cmd/juju/commands/scp.go' --- src/github.com/juju/juju/cmd/juju/commands/scp.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/scp.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,112 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "fmt" + "net" + "strings" + + "github.com/juju/cmd" + + "github.com/juju/juju/utils/ssh" +) + +// SCPCommand is responsible for launching a scp command to copy files to/from remote machine(s) +type SCPCommand struct { + SSHCommon +} + +const scpDoc = ` +Launch an scp command to copy files. Each argument ... +is either local file path or remote locations of the form [@]:, +where can be either a machine id as listed by "juju status" in the +"machines" section or a unit name as listed in the "services" section. If a +username is not specified, the username "ubuntu" will be used. + +To pass additional flags to "scp", separate "juju scp" from the options with +"--" to prevent Juju from attempting to interpret the flags. This is only +supported if the scp command can be found in the system PATH. Please refer to +the man page of scp(1) for the supported extra arguments. + +Examples: + +Copy a single file from machine 2 to the local machine: + + juju scp 2:/var/log/syslog . + +Copy 2 files from two units to the local backup/ directory, passing -v +to scp as an extra argument: + + juju scp -- -v ubuntu/0:/path/file1 ubuntu/1:/path/file2 backup/ + +Recursively copy the directory /var/log/mongodb/ on the first mongodb +server to the local directory remote-logs: + + juju scp -- -r mongodb/0:/var/log/mongodb/ remote-logs/ + +Copy a local file to the second apache unit of the environment "testing": + + juju scp -e testing foo.txt apache2/1: +` + +func (c *SCPCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "scp", + Args: " ... [scp-option...]", + Purpose: "launch a scp command to copy files to/from remote machine(s)", + Doc: scpDoc, + } +} + +func (c *SCPCommand) Init(args []string) error { + if len(args) < 2 { + return fmt.Errorf("at least two arguments required") + } + c.Args = args + return nil +} + +// expandArgs takes a list of arguments and looks for ones in the form of +// 0:some/path or service/0:some/path, and translates them into +// ubuntu@machine:some/path so they can be passed as arguments to scp, and pass +// the rest verbatim on to scp +func expandArgs(args []string, userHostFromTarget func(string) (string, string, error)) ([]string, error) { + outArgs := make([]string, len(args)) + for i, arg := range args { + v := strings.SplitN(arg, ":", 2) + if strings.HasPrefix(arg, "-") || len(v) <= 1 { + // Can't be an interesting target, so just pass it along + outArgs[i] = arg + continue + } + user, host, err := userHostFromTarget(v[0]) + if err != nil { + return nil, err + } + outArgs[i] = user + "@" + net.JoinHostPort(host, v[1]) + } + return outArgs, nil +} + +// Run resolves c.Target to a machine, or host of a unit and +// forks ssh with c.Args, if provided. +func (c *SCPCommand) Run(ctx *cmd.Context) error { + var err error + c.apiClient, err = c.initAPIClient() + if err != nil { + return err + } + defer c.apiClient.Close() + + options, err := c.getSSHOptions(false) + if err != nil { + return err + } + args, err := expandArgs(c.Args, c.userHostFromTarget) + if err != nil { + return err + } + return ssh.Copy(args, options) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/scp_unix_test.go' --- src/github.com/juju/juju/cmd/juju/commands/scp_unix_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/scp_unix_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,244 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +// +build !windows + +package commands + +import ( + "bytes" + "fmt" + "io/ioutil" + "path/filepath" + "strings" + + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/juju/charm.v5" + + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/network" + "github.com/juju/juju/testcharms" + coretesting "github.com/juju/juju/testing" +) + +var _ = gc.Suite(&SCPSuite{}) +var _ = gc.Suite(&expandArgsSuite{}) + +type SCPSuite struct { + SSHCommonSuite +} + +type expandArgsSuite struct{} + +var scpTests = []struct { + about string + args []string + result string + proxy bool + error string +}{ + { + about: "scp from machine 0 to current dir", + args: []string{"0:foo", "."}, + result: commonArgsNoProxy + "ubuntu@dummyenv-0.dns:foo .\n", + }, { + about: "scp from machine 0 to current dir with extra args", + args: []string{"0:foo", ".", "-rv", "-o", "SomeOption"}, + result: commonArgsNoProxy + "ubuntu@dummyenv-0.dns:foo . -rv -o SomeOption\n", + }, { + about: "scp from current dir to machine 0", + args: []string{"foo", "0:"}, + result: commonArgsNoProxy + "foo ubuntu@dummyenv-0.dns:\n", + }, { + about: "scp from current dir to machine 0 with extra args", + args: []string{"foo", "0:", "-r", "-v"}, + result: commonArgsNoProxy + "foo ubuntu@dummyenv-0.dns: -r -v\n", + }, { + about: "scp from machine 0 to unit mysql/0", + args: []string{"0:foo", "mysql/0:/foo"}, + result: commonArgsNoProxy + "ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n", + }, { + about: "scp from machine 0 to unit mysql/0 and extra args", + args: []string{"0:foo", "mysql/0:/foo", "-q"}, + result: commonArgsNoProxy + "ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo -q\n", + }, { + about: "scp from machine 0 to unit mysql/0 and extra args before", + args: []string{"-q", "-r", "0:foo", "mysql/0:/foo"}, + result: commonArgsNoProxy + "-q -r ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n", + }, { + about: "scp two local files to unit mysql/0", + args: []string{"file1", "file2", "mysql/0:/foo/"}, + result: commonArgsNoProxy + "file1 file2 ubuntu@dummyenv-0.dns:/foo/\n", + }, { + about: "scp from unit mongodb/1 to unit mongodb/0 and multiple extra args", + args: []string{"mongodb/1:foo", "mongodb/0:", "-r", "-v", "-q", "-l5"}, + result: commonArgsNoProxy + "ubuntu@dummyenv-2.dns:foo ubuntu@dummyenv-1.dns: -r -v -q -l5\n", + }, { + about: "scp works with IPv6 addresses", + args: []string{"ipv6-svc/0:foo", "bar"}, + result: commonArgsNoProxy + `ubuntu@[2001:db8::1]:foo bar` + "\n", + }, { + about: "scp from machine 0 to unit mysql/0 with proxy", + args: []string{"0:foo", "mysql/0:/foo"}, + result: commonArgs + "ubuntu@dummyenv-0.internal:foo ubuntu@dummyenv-0.internal:/foo\n", + proxy: true, + }, { + args: []string{"0:foo", ".", "-rv", "-o", "SomeOption"}, + result: commonArgsNoProxy + "ubuntu@dummyenv-0.dns:foo . -rv -o SomeOption\n", + }, { + args: []string{"foo", "0:", "-r", "-v"}, + result: commonArgsNoProxy + "foo ubuntu@dummyenv-0.dns: -r -v\n", + }, { + args: []string{"mongodb/1:foo", "mongodb/0:", "-r", "-v", "-q", "-l5"}, + result: commonArgsNoProxy + "ubuntu@dummyenv-2.dns:foo ubuntu@dummyenv-1.dns: -r -v -q -l5\n", + }, { + about: "scp from unit mongodb/1 to unit mongodb/0 with a --", + args: []string{"--", "-r", "-v", "mongodb/1:foo", "mongodb/0:", "-q", "-l5"}, + result: commonArgsNoProxy + "-- -r -v ubuntu@dummyenv-2.dns:foo ubuntu@dummyenv-1.dns: -q -l5\n", + }, { + about: "scp from unit mongodb/1 to current dir as 'mongo' user", + args: []string{"mongo@mongodb/1:foo", "."}, + result: commonArgsNoProxy + "mongo@dummyenv-2.dns:foo .\n", + }, { + about: "scp with no such machine", + args: []string{"5:foo", "bar"}, + error: "machine 5 not found", + }, +} + +func (s *SCPSuite) TestSCPCommand(c *gc.C) { + m := s.makeMachines(4, c, true) + ch := testcharms.Repo.CharmDir("dummy") + curl := charm.MustParseURL( + fmt.Sprintf("local:quantal/%s-%d", ch.Meta().Name, ch.Revision()), + ) + dummyCharm, err := s.State.AddCharm(ch, curl, "dummy-path", "dummy-1-sha256") + c.Assert(err, jc.ErrorIsNil) + srv := s.AddTestingService(c, "mysql", dummyCharm) + s.addUnit(srv, m[0], c) + + srv = s.AddTestingService(c, "mongodb", dummyCharm) + s.addUnit(srv, m[1], c) + s.addUnit(srv, m[2], c) + srv = s.AddTestingService(c, "ipv6-svc", dummyCharm) + s.addUnit(srv, m[3], c) + // Simulate machine 3 has a public IPv6 address. + ipv6Addr := network.NewScopedAddress("2001:db8::1", network.ScopePublic) + err = m[3].SetProviderAddresses(ipv6Addr) + c.Assert(err, jc.ErrorIsNil) + + for i, t := range scpTests { + c.Logf("test %d: %s -> %s\n", i, t.about, t.args) + ctx := coretesting.Context(c) + scpcmd := &SCPCommand{} + scpcmd.proxy = t.proxy + + err := envcmd.Wrap(scpcmd).Init(t.args) + c.Check(err, jc.ErrorIsNil) + err = scpcmd.Run(ctx) + if t.error != "" { + c.Check(err, gc.ErrorMatches, t.error) + c.Check(t.result, gc.Equals, "") + } else { + c.Check(err, jc.ErrorIsNil) + // we suppress stdout from scp + c.Check(ctx.Stderr.(*bytes.Buffer).String(), gc.Equals, "") + c.Check(ctx.Stdout.(*bytes.Buffer).String(), gc.Equals, "") + data, err := ioutil.ReadFile(filepath.Join(s.bin, "scp.args")) + c.Check(err, jc.ErrorIsNil) + actual := string(data) + if t.proxy { + actual = strings.Replace(actual, ".dns", ".internal", 2) + } + c.Check(actual, gc.Equals, t.result) + } + } +} + +type userHost struct { + user string + host string +} + +var userHostsFromTargets = map[string]userHost{ + "0": {"ubuntu", "dummyenv-0.dns"}, + "mysql/0": {"ubuntu", "dummyenv-0.dns"}, + "mongodb/0": {"ubuntu", "dummyenv-1.dns"}, + "mongodb/1": {"ubuntu", "dummyenv-2.dns"}, + "mongo@mongodb/1": {"mongo", "dummyenv-2.dns"}, + "ipv6-svc/0": {"ubuntu", "2001:db8::1"}, +} + +func dummyHostsFromTarget(target string) (string, string, error) { + if res, ok := userHostsFromTargets[target]; ok { + return res.user, res.host, nil + } + return "ubuntu", target, nil +} + +func (s *expandArgsSuite) TestSCPExpandArgs(c *gc.C) { + for i, t := range scpTests { + if t.error != "" { + // We are just running a focused set of tests on + // expandArgs, we aren't implementing the full + // userHostsFromTargets to actually trigger errors + continue + } + c.Logf("test %d: %s -> %s\n", i, t.about, t.args) + // expandArgs doesn't add the commonArgs prefix, so strip it + // off, along with the trailing '\n' + var argString string + if t.proxy { + c.Check(strings.HasPrefix(t.result, commonArgs), jc.IsTrue) + argString = t.result[len(commonArgs):] + } else { + c.Check(strings.HasPrefix(t.result, commonArgsNoProxy), jc.IsTrue) + argString = t.result[len(commonArgsNoProxy):] + } + c.Check(strings.HasSuffix(argString, "\n"), jc.IsTrue) + argString = argString[:len(argString)-1] + args := strings.Split(argString, " ") + expanded, err := expandArgs(t.args, func(target string) (string, string, error) { + if res, ok := userHostsFromTargets[target]; ok { + if t.proxy { + res.host = strings.Replace(res.host, ".dns", ".internal", 1) + } + return res.user, res.host, nil + } + return "ubuntu", target, nil + }) + c.Check(err, jc.ErrorIsNil) + c.Check(expanded, gc.DeepEquals, args) + } +} + +var expandTests = []struct { + about string + args []string + result []string +}{ + { + "don't expand params that start with '-'", + []string{"-0:stuff", "0:foo", "."}, + []string{"-0:stuff", "ubuntu@dummyenv-0.dns:foo", "."}, + }, +} + +func (s *expandArgsSuite) TestExpandArgs(c *gc.C) { + for i, t := range expandTests { + c.Logf("test %d: %s -> %s\n", i, t.about, t.args) + expanded, err := expandArgs(t.args, dummyHostsFromTarget) + c.Check(err, jc.ErrorIsNil) + c.Check(expanded, gc.DeepEquals, t.result) + } +} + +func (s *expandArgsSuite) TestExpandArgsPropagatesErrors(c *gc.C) { + erroringUserHostFromTargets := func(string) (string, string, error) { + return "", "", fmt.Errorf("this is my error") + } + expanded, err := expandArgs([]string{"foo:1", "bar"}, erroringUserHostFromTargets) + c.Assert(err, gc.ErrorMatches, "this is my error") + c.Check(expanded, gc.IsNil) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/ssh.go' --- src/github.com/juju/juju/cmd/juju/commands/ssh.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/ssh.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,269 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "fmt" + "net" + "os" + "os/exec" + "strings" + "time" + + "github.com/juju/cmd" + "github.com/juju/names" + "github.com/juju/utils" + "launchpad.net/gnuflag" + + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/environs/config" + "github.com/juju/juju/utils/ssh" +) + +// SSHCommand is responsible for launching a ssh shell on a given unit or machine. +type SSHCommand struct { + SSHCommon +} + +// SSHCommon provides common methods for SSHCommand, SCPCommand and DebugHooksCommand. +type SSHCommon struct { + envcmd.EnvCommandBase + proxy bool + pty bool + Target string + Args []string + apiClient sshAPIClient + apiAddr string +} + +func (c *SSHCommon) SetFlags(f *gnuflag.FlagSet) { + f.BoolVar(&c.proxy, "proxy", true, "proxy through the API server") + f.BoolVar(&c.pty, "pty", true, "enable pseudo-tty allocation") +} + +// setProxyCommand sets the proxy command option. +func (c *SSHCommon) setProxyCommand(options *ssh.Options) error { + apiServerHost, _, err := net.SplitHostPort(c.apiAddr) + if err != nil { + return fmt.Errorf("failed to get proxy address: %v", err) + } + juju, err := getJujuExecutable() + if err != nil { + return fmt.Errorf("failed to get juju executable path: %v", err) + } + options.SetProxyCommand(juju, "ssh", "--proxy=false", "--pty=false", apiServerHost, "nc", "%h", "%p") + return nil +} + +const sshDoc = ` +Launch an ssh shell on the machine identified by the parameter. + can be either a machine id as listed by "juju status" in the +"machines" section or a unit name as listed in the "services" section. +Any extra parameters are passed as extra parameters to the ssh command. + +Examples: + +Connect to machine 0: + + juju ssh 0 + +Connect to machine 1 and run 'uname -a': + + juju ssh 1 uname -a + +Connect to the first mysql unit: + + juju ssh mysql/0 + +Connect to the first mysql unit and run 'ls -la /var/log/juju': + + juju ssh mysql/0 ls -la /var/log/juju + +Connect to the first jenkins unit as the user jenkins: + + juju ssh jenkins@jenkins/0 +` + +func (c *SSHCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "ssh", + Args: " [...]", + Purpose: "launch an ssh shell on a given unit or machine", + Doc: sshDoc, + } +} + +func (c *SSHCommand) Init(args []string) error { + if len(args) == 0 { + return fmt.Errorf("no target name specified") + } + c.Target, c.Args = args[0], args[1:] + return nil +} + +// getJujuExecutable returns the path to the juju +// executable, or an error if it could not be found. +var getJujuExecutable = func() (string, error) { + return exec.LookPath(os.Args[0]) +} + +// getSSHOptions configures and returns SSH options and proxy settings. +func (c *SSHCommon) getSSHOptions(enablePty bool) (*ssh.Options, error) { + var options ssh.Options + + // TODO(waigani) do not save fingerprint only until this bug is addressed: + // lp:892552. Also see lp:1334481. + options.SetKnownHostsFile("/dev/null") + if enablePty { + options.EnablePTY() + } + var err error + if c.proxy, err = c.proxySSH(); err != nil { + return nil, err + } else if c.proxy { + if err := c.setProxyCommand(&options); err != nil { + return nil, err + } + } + return &options, nil +} + +// Run resolves c.Target to a machine, to the address of a i +// machine or unit forks ssh passing any arguments provided. +func (c *SSHCommand) Run(ctx *cmd.Context) error { + if c.apiClient == nil { + // If the apClient is not already opened and it is opened + // by ensureAPIClient, then close it when we're done. + defer func() { + if c.apiClient != nil { + c.apiClient.Close() + c.apiClient = nil + } + }() + } + options, err := c.getSSHOptions(c.pty) + if err != nil { + return err + } + + user, host, err := c.userHostFromTarget(c.Target) + if err != nil { + return err + } + cmd := ssh.Command(user+"@"+host, c.Args, options) + cmd.Stdin = ctx.Stdin + cmd.Stdout = ctx.Stdout + cmd.Stderr = ctx.Stderr + return cmd.Run() +} + +// proxySSH returns true iff both c.proxy and +// the proxy-ssh environment configuration +// are true. +func (c *SSHCommon) proxySSH() (bool, error) { + if !c.proxy { + return false, nil + } + if _, err := c.ensureAPIClient(); err != nil { + return false, err + } + var cfg *config.Config + attrs, err := c.apiClient.EnvironmentGet() + if err == nil { + cfg, err = config.New(config.NoDefaults, attrs) + } + if err != nil { + return false, err + } + logger.Debugf("proxy-ssh is %v", cfg.ProxySSH()) + return cfg.ProxySSH(), nil +} + +func (c *SSHCommon) ensureAPIClient() (sshAPIClient, error) { + if c.apiClient != nil { + return c.apiClient, nil + } + return c.initAPIClient() +} + +// initAPIClient initialises the API connection. +// It is the caller's responsibility to close the connection. +func (c *SSHCommon) initAPIClient() (sshAPIClient, error) { + st, err := c.NewAPIRoot() + if err != nil { + return nil, err + } + c.apiClient = st.Client() + c.apiAddr = st.Addr() + return c.apiClient, nil +} + +type sshAPIClient interface { + EnvironmentGet() (map[string]interface{}, error) + PublicAddress(target string) (string, error) + PrivateAddress(target string) (string, error) + ServiceCharmRelations(service string) ([]string, error) + Close() error +} + +// attemptStarter is an interface corresponding to utils.AttemptStrategy +type attemptStarter interface { + Start() attempt +} + +type attempt interface { + Next() bool +} + +type attemptStrategy utils.AttemptStrategy + +func (s attemptStrategy) Start() attempt { + return utils.AttemptStrategy(s).Start() +} + +var sshHostFromTargetAttemptStrategy attemptStarter = attemptStrategy{ + Total: 5 * time.Second, + Delay: 500 * time.Millisecond, +} + +func (c *SSHCommon) userHostFromTarget(target string) (user, host string, err error) { + if i := strings.IndexRune(target, '@'); i != -1 { + user = target[:i] + target = target[i+1:] + } else { + user = "ubuntu" + } + + // If the target is neither a machine nor a unit, + // assume it's a hostname and try it directly. + if !names.IsValidMachine(target) && !names.IsValidUnit(target) { + return user, target, nil + } + + // A target may not initially have an address (e.g. the + // address updater hasn't yet run), so we must do this in + // a loop. + if _, err := c.ensureAPIClient(); err != nil { + return "", "", err + } + for a := sshHostFromTargetAttemptStrategy.Start(); a.Next(); { + var addr string + if c.proxy { + addr, err = c.apiClient.PrivateAddress(target) + } else { + addr, err = c.apiClient.PublicAddress(target) + } + if err == nil { + return user, addr, nil + } + } + return "", "", err +} + +// AllowInterspersedFlags for ssh/scp is set to false so that +// flags after the unit name are passed through to ssh, for eg. +// `juju ssh -v service-name/0 uname -a`. +func (c *SSHCommon) AllowInterspersedFlags() bool { + return false +} === added file 'src/github.com/juju/juju/cmd/juju/commands/ssh_test.go' --- src/github.com/juju/juju/cmd/juju/commands/ssh_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/ssh_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,257 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + + "github.com/juju/cmd" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/juju/charm.v5" + + "github.com/juju/juju/apiserver" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/juju/testing" + "github.com/juju/juju/network" + "github.com/juju/juju/state" + "github.com/juju/juju/testcharms" + coretesting "github.com/juju/juju/testing" + "github.com/juju/juju/utils/ssh" +) + +var _ = gc.Suite(&SSHSuite{}) + +type SSHSuite struct { + SSHCommonSuite +} + +type SSHCommonSuite struct { + testing.JujuConnSuite + bin string +} + +func (s *SSHCommonSuite) SetUpTest(c *gc.C) { + s.JujuConnSuite.SetUpTest(c) + s.PatchValue(&getJujuExecutable, func() (string, error) { return "juju", nil }) + + s.bin = c.MkDir() + s.PatchEnvPathPrepend(s.bin) + for _, name := range patchedCommands { + f, err := os.OpenFile(filepath.Join(s.bin, name), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0777) + c.Assert(err, jc.ErrorIsNil) + _, err = f.Write([]byte(fakecommand)) + c.Assert(err, jc.ErrorIsNil) + err = f.Close() + c.Assert(err, jc.ErrorIsNil) + } + client, _ := ssh.NewOpenSSHClient() + s.PatchValue(&ssh.DefaultClient, client) +} + +const ( + noProxy = `-o StrictHostKeyChecking no -o PasswordAuthentication no -o ServerAliveInterval 30 ` + args = `-o StrictHostKeyChecking no -o ProxyCommand juju ssh --proxy=false --pty=false localhost nc %h %p -o PasswordAuthentication no -o ServerAliveInterval 30 ` + commonArgsNoProxy = noProxy + `-o UserKnownHostsFile /dev/null ` + commonArgs = args + `-o UserKnownHostsFile /dev/null ` + sshArgs = args + `-t -t -o UserKnownHostsFile /dev/null ` + sshArgsNoProxy = noProxy + `-t -t -o UserKnownHostsFile /dev/null ` +) + +var sshTests = []struct { + about string + args []string + result string +}{ + { + "connect to machine 0", + []string{"ssh", "0"}, + sshArgs + "ubuntu@dummyenv-0.internal", + }, + { + "connect to machine 0 and pass extra arguments", + []string{"ssh", "0", "uname", "-a"}, + sshArgs + "ubuntu@dummyenv-0.internal uname -a", + }, + { + "connect to unit mysql/0", + []string{"ssh", "mysql/0"}, + sshArgs + "ubuntu@dummyenv-0.internal", + }, + { + "connect to unit mongodb/1 as the mongo user", + []string{"ssh", "mongo@mongodb/1"}, + sshArgs + "mongo@dummyenv-2.internal", + }, + { + "connect to unit mongodb/1 and pass extra arguments", + []string{"ssh", "mongodb/1", "ls", "/"}, + sshArgs + "ubuntu@dummyenv-2.internal ls /", + }, + { + "connect to unit mysql/0 without proxy", + []string{"ssh", "--proxy=false", "mysql/0"}, + sshArgsNoProxy + "ubuntu@dummyenv-0.dns", + }, +} + +func (s *SSHSuite) TestSSHCommand(c *gc.C) { + m := s.makeMachines(3, c, true) + ch := testcharms.Repo.CharmDir("dummy") + curl := charm.MustParseURL( + fmt.Sprintf("local:quantal/%s-%d", ch.Meta().Name, ch.Revision()), + ) + dummy, err := s.State.AddCharm(ch, curl, "dummy-path", "dummy-1-sha256") + c.Assert(err, jc.ErrorIsNil) + srv := s.AddTestingService(c, "mysql", dummy) + s.addUnit(srv, m[0], c) + + srv = s.AddTestingService(c, "mongodb", dummy) + s.addUnit(srv, m[1], c) + s.addUnit(srv, m[2], c) + + for i, t := range sshTests { + c.Logf("test %d: %s -> %s", i, t.about, t.args) + ctx := coretesting.Context(c) + jujucmd := cmd.NewSuperCommand(cmd.SuperCommandParams{}) + jujucmd.Register(envcmd.Wrap(&SSHCommand{})) + + code := cmd.Main(jujucmd, ctx, t.args) + c.Check(code, gc.Equals, 0) + c.Check(ctx.Stderr.(*bytes.Buffer).String(), gc.Equals, "") + c.Check(strings.TrimRight(ctx.Stdout.(*bytes.Buffer).String(), "\r\n"), gc.Equals, t.result) + } +} + +func (s *SSHSuite) TestSSHCommandEnvironProxySSH(c *gc.C) { + s.makeMachines(1, c, true) + // Setting proxy-ssh=false in the environment overrides --proxy. + err := s.State.UpdateEnvironConfig(map[string]interface{}{"proxy-ssh": false}, nil, nil) + c.Assert(err, jc.ErrorIsNil) + ctx := coretesting.Context(c) + jujucmd := cmd.NewSuperCommand(cmd.SuperCommandParams{}) + jujucmd.Register(envcmd.Wrap(&SSHCommand{})) + code := cmd.Main(jujucmd, ctx, []string{"ssh", "0"}) + c.Check(code, gc.Equals, 0) + c.Check(ctx.Stderr.(*bytes.Buffer).String(), gc.Equals, "") + c.Check(strings.TrimRight(ctx.Stdout.(*bytes.Buffer).String(), "\r\n"), gc.Equals, sshArgsNoProxy+"ubuntu@dummyenv-0.dns") +} + +func (s *SSHSuite) TestSSHWillWorkInUpgrade(c *gc.C) { + // Check the API client interface used by "juju ssh" against what + // the API server will allow during upgrades. Ensure that the API + // server will allow all required API calls to support SSH. + type concrete struct { + sshAPIClient + } + t := reflect.TypeOf(concrete{}) + for i := 0; i < t.NumMethod(); i++ { + name := t.Method(i).Name + + // Close isn't an API method and ServiceCharmRelations is not + // relevant to "juju ssh". + if name == "Close" || name == "ServiceCharmRelations" { + continue + } + c.Logf("checking %q", name) + c.Check(apiserver.IsMethodAllowedDuringUpgrade("Client", name), jc.IsTrue) + } +} + +type callbackAttemptStarter struct { + next func() bool +} + +func (s *callbackAttemptStarter) Start() attempt { + return callbackAttempt{next: s.next} +} + +type callbackAttempt struct { + next func() bool +} + +func (a callbackAttempt) Next() bool { + return a.next() +} + +func (s *SSHSuite) TestSSHCommandHostAddressRetry(c *gc.C) { + s.testSSHCommandHostAddressRetry(c, false) +} + +func (s *SSHSuite) TestSSHCommandHostAddressRetryProxy(c *gc.C) { + s.testSSHCommandHostAddressRetry(c, true) +} + +func (s *SSHSuite) testSSHCommandHostAddressRetry(c *gc.C, proxy bool) { + m := s.makeMachines(1, c, false) + ctx := coretesting.Context(c) + + var called int + next := func() bool { + called++ + return called < 2 + } + attemptStarter := &callbackAttemptStarter{next: next} + s.PatchValue(&sshHostFromTargetAttemptStrategy, attemptStarter) + + // Ensure that the ssh command waits for a public address, or the attempt + // strategy's Done method returns false. + args := []string{"--proxy=" + fmt.Sprint(proxy), "0"} + code := cmd.Main(envcmd.Wrap(&SSHCommand{}), ctx, args) + c.Check(code, gc.Equals, 1) + c.Assert(called, gc.Equals, 2) + called = 0 + attemptStarter.next = func() bool { + called++ + if called > 1 { + s.setAddresses(m[0], c) + } + return true + } + code = cmd.Main(envcmd.Wrap(&SSHCommand{}), ctx, args) + c.Check(code, gc.Equals, 0) + c.Assert(called, gc.Equals, 2) +} + +func (s *SSHCommonSuite) setAddresses(m *state.Machine, c *gc.C) { + addrPub := network.NewScopedAddress( + fmt.Sprintf("dummyenv-%s.dns", m.Id()), + network.ScopePublic, + ) + addrPriv := network.NewScopedAddress( + fmt.Sprintf("dummyenv-%s.internal", m.Id()), + network.ScopeCloudLocal, + ) + err := m.SetProviderAddresses(addrPub, addrPriv) + c.Assert(err, jc.ErrorIsNil) +} + +func (s *SSHCommonSuite) makeMachines(n int, c *gc.C, setAddresses bool) []*state.Machine { + var machines = make([]*state.Machine, n) + for i := 0; i < n; i++ { + m, err := s.State.AddMachine("quantal", state.JobHostUnits) + c.Assert(err, jc.ErrorIsNil) + if setAddresses { + s.setAddresses(m, c) + } + // must set an instance id as the ssh command uses that as a signal the + // machine has been provisioned + inst, md := testing.AssertStartInstance(c, s.Environ, m.Id()) + c.Assert(m.SetProvisioned(inst.Id(), "fake_nonce", md), gc.IsNil) + machines[i] = m + } + return machines +} + +func (s *SSHCommonSuite) addUnit(srv *state.Service, m *state.Machine, c *gc.C) { + u, err := srv.AddUnit() + c.Assert(err, jc.ErrorIsNil) + err = u.AssignToMachine(m) + c.Assert(err, jc.ErrorIsNil) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/ssh_unix_test.go' --- src/github.com/juju/juju/cmd/juju/commands/ssh_unix_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/ssh_unix_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,16 @@ +// Copyright 2014 Canonical Ltd. +// Copyright 2014 Cloudbase Solutions SRL +// Licensed under the AGPLv3, see LICENCE file for details. + +// +build !windows + +package commands + +// Commands to patch +var patchedCommands = []string{"ssh", "scp"} + +// fakecommand outputs its arguments to stdout for verification +var fakecommand = `#!/bin/bash + +echo "$@" | tee $0.args +` === added file 'src/github.com/juju/juju/cmd/juju/commands/ssh_windows_test.go' --- src/github.com/juju/juju/cmd/juju/commands/ssh_windows_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/ssh_windows_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,23 @@ +// Copyright 2014 Canonical Ltd. +// Copyright 2014 Cloudbase Solutions SRL +// Licensed under the AGPLv3, see LICENCE file for details. + +// +build windows + +package commands + +// Commands to patch +var patchedCommands = []string{"scp.cmd", "ssh.cmd"} + +// fakecommand outputs its arguments to stdout for verification +var fakecommand = `@echo off +setlocal enabledelayedexpansion +set list=%1 +set argCount=0 +for %%x in (%*) do ( +set /A argCount+=1 +set "argVec[!argCount!]=%%~x" +) +for /L %%i in (2,1,%argCount%) do set list=!list! !argVec[%%i]! +echo %list% +` === added file 'src/github.com/juju/juju/cmd/juju/commands/switch.go' --- src/github.com/juju/juju/cmd/juju/commands/switch.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/switch.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,166 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "fmt" + "os" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/utils/set" + "launchpad.net/gnuflag" + + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/environs" + "github.com/juju/juju/environs/configstore" +) + +type SwitchCommand struct { + cmd.CommandBase + EnvName string + List bool +} + +var switchDoc = ` +Show or change the default juju environment or system name. + +If no command line parameters are passed, switch will output the current +environment as defined by the file $JUJU_HOME/current-environment. + +If a command line parameter is passed in, that value will is stored in the +current environment file if it represents a valid environment name as +specified in the environments.yaml file. +` + +const systemSuffix = " (system)" + +func (c *SwitchCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "switch", + Args: "[environment name]", + Purpose: "show or change the default juju environment or system name", + Doc: switchDoc, + Aliases: []string{"env"}, + } +} + +func (c *SwitchCommand) SetFlags(f *gnuflag.FlagSet) { + f.BoolVar(&c.List, "l", false, "list the environment names") + f.BoolVar(&c.List, "list", false, "") +} + +func (c *SwitchCommand) Init(args []string) (err error) { + c.EnvName, err = cmd.ZeroOrOneArgs(args) + return +} + +func getConfigstoreOptions() (set.Strings, set.Strings, error) { + store, err := configstore.Default() + if err != nil { + return nil, nil, errors.Annotate(err, "failed to get config store") + } + environmentNames, err := store.List() + if err != nil { + return nil, nil, errors.Annotate(err, "failed to list environments in config store") + } + systemNames, err := store.ListSystems() + if err != nil { + return nil, nil, errors.Annotate(err, "failed to list systems in config store") + } + // Also include the systems. + return set.NewStrings(environmentNames...), set.NewStrings(systemNames...), nil +} + +func (c *SwitchCommand) Run(ctx *cmd.Context) error { + // Switch is an alternative way of dealing with environments than using + // the JUJU_ENV environment setting, and as such, doesn't play too well. + // If JUJU_ENV is set we should report that as the current environment, + // and not allow switching when it is set. + + // Passing through the empty string reads the default environments.yaml file. + // If the environments.yaml file doesn't exist, just list environments in + // the configstore. + envFileExists := true + names := set.NewStrings() + environments, err := environs.ReadEnvirons("") + if err != nil { + if !environs.IsNoEnv(err) { + return errors.Annotate(err, "couldn't read the environment") + } + envFileExists = false + } else { + for _, name := range environments.Names() { + names.Add(name) + } + } + + configEnvirons, configSystems, err := getConfigstoreOptions() + if err != nil { + return err + } + names = names.Union(configEnvirons) + names = names.Union(configSystems) + + if c.List { + // List all environments and systems. + if c.EnvName != "" { + return errors.New("cannot switch and list at the same time") + } + for _, name := range names.SortedValues() { + if configSystems.Contains(name) && !configEnvirons.Contains(name) { + name += systemSuffix + } + fmt.Fprintf(ctx.Stdout, "%s\n", name) + } + return nil + } + + jujuEnv := os.Getenv("JUJU_ENV") + if jujuEnv != "" { + if c.EnvName == "" { + fmt.Fprintf(ctx.Stdout, "%s\n", jujuEnv) + return nil + } else { + return errors.Errorf("cannot switch when JUJU_ENV is overriding the environment (set to %q)", jujuEnv) + } + } + + current, isSystem, err := envcmd.CurrentConnectionName() + if err != nil { + return errors.Trace(err) + } + if current == "" { + if envFileExists { + current = environments.Default + } + } else if isSystem { + current += systemSuffix + } + + // Handle the different operation modes. + switch { + case c.EnvName == "" && current == "": + // Nothing specified and nothing to switch to. + return errors.New("no currently specified environment") + case c.EnvName == "": + // Simply print the current environment. + fmt.Fprintf(ctx.Stdout, "%s\n", current) + return nil + default: + // Switch the environment. + if !names.Contains(c.EnvName) { + return errors.Errorf("%q is not a name of an existing defined environment or system", c.EnvName) + } + // If the name is not in the environment set, but is in the system + // set, then write the name into the current system file. + logger.Debugf("systems: %v", configSystems) + logger.Debugf("environs: %v", configEnvirons) + newEnv := c.EnvName + if configSystems.Contains(newEnv) && !configEnvirons.Contains(newEnv) { + return envcmd.SetCurrentSystem(ctx, newEnv) + } + return envcmd.SetCurrentEnvironment(ctx, newEnv) + } +} === added file 'src/github.com/juju/juju/cmd/juju/commands/switch_test.go' --- src/github.com/juju/juju/cmd/juju/commands/switch_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/switch_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,205 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "os" + "runtime" + + gitjujutesting "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/environs/configstore" + "github.com/juju/juju/feature" + _ "github.com/juju/juju/juju" + "github.com/juju/juju/testing" +) + +type SwitchSimpleSuite struct { + testing.FakeJujuHomeSuite +} + +var _ = gc.Suite(&SwitchSimpleSuite{}) + +func (s *SwitchSimpleSuite) TestNoEnvironmentReadsConfigStore(c *gc.C) { + envPath := gitjujutesting.HomePath(".juju", "environments.yaml") + err := os.Remove(envPath) + c.Assert(err, jc.ErrorIsNil) + s.addTestSystem(c) + context, err := testing.RunCommand(c, &SwitchCommand{}, "--list") + c.Assert(err, jc.ErrorIsNil) + c.Assert(testing.Stdout(context), gc.Equals, "a-system (system)\n") +} + +func (s *SwitchSimpleSuite) TestErrorReadingEnvironmentsFile(c *gc.C) { + if runtime.GOOS == "windows" { + c.Skip("bug 1496997: os.Chmod doesn't exist on windows, checking this on one platform is sufficent to test this case") + } + + envPath := gitjujutesting.HomePath(".juju", "environments.yaml") + err := os.Chmod(envPath, 0) + c.Assert(err, jc.ErrorIsNil) + s.addTestSystem(c) + _, err = testing.RunCommand(c, &SwitchCommand{}, "--list") + c.Assert(err, gc.ErrorMatches, "couldn't read the environment: open .*: permission denied") +} + +func (*SwitchSimpleSuite) TestNoDefault(c *gc.C) { + testing.WriteEnvironments(c, testing.MultipleEnvConfigNoDefault) + _, err := testing.RunCommand(c, &SwitchCommand{}) + c.Assert(err, gc.ErrorMatches, "no currently specified environment") +} + +func (*SwitchSimpleSuite) TestNoDefaultNoEnvironmentsFile(c *gc.C) { + envPath := gitjujutesting.HomePath(".juju", "environments.yaml") + err := os.Remove(envPath) + c.Assert(err, jc.ErrorIsNil) + _, err = testing.RunCommand(c, &SwitchCommand{}) + c.Assert(err, gc.ErrorMatches, "no currently specified environment") +} + +func (*SwitchSimpleSuite) TestShowsDefault(c *gc.C) { + testing.WriteEnvironments(c, testing.MultipleEnvConfig) + context, err := testing.RunCommand(c, &SwitchCommand{}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(testing.Stdout(context), gc.Equals, "erewhemos\n") +} + +func (s *SwitchSimpleSuite) TestCurrentEnvironmentHasPrecedence(c *gc.C) { + testing.WriteEnvironments(c, testing.MultipleEnvConfig) + envcmd.WriteCurrentEnvironment("fubar") + context, err := testing.RunCommand(c, &SwitchCommand{}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(testing.Stdout(context), gc.Equals, "fubar\n") +} + +func (s *SwitchSimpleSuite) TestCurrentSystemHasPrecedence(c *gc.C) { + testing.WriteEnvironments(c, testing.MultipleEnvConfig) + envcmd.WriteCurrentSystem("fubar") + context, err := testing.RunCommand(c, &SwitchCommand{}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(testing.Stdout(context), gc.Equals, "fubar (system)\n") +} + +func (*SwitchSimpleSuite) TestShowsJujuEnv(c *gc.C) { + testing.WriteEnvironments(c, testing.MultipleEnvConfig) + os.Setenv("JUJU_ENV", "using-env") + context, err := testing.RunCommand(c, &SwitchCommand{}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(testing.Stdout(context), gc.Equals, "using-env\n") +} + +func (s *SwitchSimpleSuite) TestJujuEnvOverCurrentEnvironment(c *gc.C) { + testing.WriteEnvironments(c, testing.MultipleEnvConfig) + s.FakeHomeSuite.Home.AddFiles(c, gitjujutesting.TestFile{".juju/current-environment", "fubar"}) + os.Setenv("JUJU_ENV", "using-env") + context, err := testing.RunCommand(c, &SwitchCommand{}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(testing.Stdout(context), gc.Equals, "using-env\n") +} + +func (*SwitchSimpleSuite) TestSettingWritesFile(c *gc.C) { + testing.WriteEnvironments(c, testing.MultipleEnvConfig) + context, err := testing.RunCommand(c, &SwitchCommand{}, "erewhemos-2") + c.Assert(err, jc.ErrorIsNil) + c.Assert(testing.Stderr(context), gc.Equals, "-> erewhemos-2\n") + currentEnv, err := envcmd.ReadCurrentEnvironment() + c.Assert(err, jc.ErrorIsNil) + c.Assert(currentEnv, gc.Equals, "erewhemos-2") +} + +func (s *SwitchSimpleSuite) addTestSystem(c *gc.C) { + // First set up a system in the config store. + s.SetFeatureFlags(feature.JES) + store, err := configstore.Default() + c.Assert(err, jc.ErrorIsNil) + info := store.CreateInfo("a-system") + info.SetAPIEndpoint(configstore.APIEndpoint{ + Addresses: []string{"localhost"}, + CACert: testing.CACert, + ServerUUID: "server-uuid", + }) + err = info.Write() + c.Assert(err, jc.ErrorIsNil) +} + +func (s *SwitchSimpleSuite) TestSettingWritesSystemFile(c *gc.C) { + s.addTestSystem(c) + context, err := testing.RunCommand(c, &SwitchCommand{}, "a-system") + c.Assert(err, jc.ErrorIsNil) + c.Assert(testing.Stderr(context), gc.Equals, "-> a-system (system)\n") + currSystem, err := envcmd.ReadCurrentSystem() + c.Assert(err, jc.ErrorIsNil) + c.Assert(currSystem, gc.Equals, "a-system") +} + +func (s *SwitchSimpleSuite) TestListWithSystem(c *gc.C) { + s.addTestSystem(c) + context, err := testing.RunCommand(c, &SwitchCommand{}, "--list") + c.Assert(err, jc.ErrorIsNil) + c.Assert(testing.Stdout(context), gc.Equals, ` +a-system (system) +erewhemos +`[1:]) +} + +func (*SwitchSimpleSuite) TestSettingToUnknown(c *gc.C) { + testing.WriteEnvironments(c, testing.MultipleEnvConfig) + _, err := testing.RunCommand(c, &SwitchCommand{}, "unknown") + c.Assert(err, gc.ErrorMatches, `"unknown" is not a name of an existing defined environment or system`) +} + +func (*SwitchSimpleSuite) TestSettingWhenJujuEnvSet(c *gc.C) { + testing.WriteEnvironments(c, testing.MultipleEnvConfig) + os.Setenv("JUJU_ENV", "using-env") + _, err := testing.RunCommand(c, &SwitchCommand{}, "erewhemos-2") + c.Assert(err, gc.ErrorMatches, `cannot switch when JUJU_ENV is overriding the environment \(set to "using-env"\)`) +} + +const expectedEnvironments = `erewhemos +erewhemos-2 +` + +func (*SwitchSimpleSuite) TestListEnvironments(c *gc.C) { + testing.WriteEnvironments(c, testing.MultipleEnvConfig) + context, err := testing.RunCommand(c, &SwitchCommand{}, "--list") + c.Assert(err, jc.ErrorIsNil) + c.Assert(testing.Stdout(context), gc.Equals, expectedEnvironments) +} + +func (s *SwitchSimpleSuite) TestListEnvironmentsWithConfigstore(c *gc.C) { + memstore := configstore.NewMem() + s.PatchValue(&configstore.Default, func() (configstore.Storage, error) { + return memstore, nil + }) + info := memstore.CreateInfo("testing") + err := info.Write() + testing.WriteEnvironments(c, testing.MultipleEnvConfig) + context, err := testing.RunCommand(c, &SwitchCommand{}, "--list") + c.Assert(err, jc.ErrorIsNil) + expected := expectedEnvironments + "testing\n" + c.Assert(testing.Stdout(context), gc.Equals, expected) +} + +func (*SwitchSimpleSuite) TestListEnvironmentsOSJujuEnvSet(c *gc.C) { + testing.WriteEnvironments(c, testing.MultipleEnvConfig) + os.Setenv("JUJU_ENV", "using-env") + context, err := testing.RunCommand(c, &SwitchCommand{}, "--list") + c.Assert(err, jc.ErrorIsNil) + c.Assert(testing.Stdout(context), gc.Equals, expectedEnvironments) +} + +func (*SwitchSimpleSuite) TestListEnvironmentsAndChange(c *gc.C) { + testing.WriteEnvironments(c, testing.MultipleEnvConfig) + _, err := testing.RunCommand(c, &SwitchCommand{}, "--list", "erewhemos-2") + c.Assert(err, gc.ErrorMatches, "cannot switch and list at the same time") +} + +func (*SwitchSimpleSuite) TestTooManyParams(c *gc.C) { + testing.WriteEnvironments(c, testing.MultipleEnvConfig) + _, err := testing.RunCommand(c, &SwitchCommand{}, "foo", "bar") + c.Assert(err, gc.ErrorMatches, `unrecognized args: ."bar".`) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/synctools.go' --- src/github.com/juju/juju/cmd/juju/commands/synctools.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/synctools.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,174 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "bytes" + "io" + + "github.com/juju/cmd" + "github.com/juju/loggo" + "launchpad.net/gnuflag" + + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/block" + "github.com/juju/juju/environs/filestorage" + "github.com/juju/juju/environs/sync" + envtools "github.com/juju/juju/environs/tools" + coretools "github.com/juju/juju/tools" + "github.com/juju/juju/version" +) + +var syncTools = sync.SyncTools + +// SyncToolsCommand copies all the tools from the us-east-1 bucket to the local +// bucket. +type SyncToolsCommand struct { + envcmd.EnvCommandBase + allVersions bool + versionStr string + majorVersion int + minorVersion int + dryRun bool + dev bool + public bool + source string + stream string + localDir string + destination string +} + +var _ cmd.Command = (*SyncToolsCommand)(nil) + +func (c *SyncToolsCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "sync-tools", + Purpose: "copy tools from the official tool store into a local environment", + Doc: ` +This copies the Juju tools tarball from the official tools store (located +at https://streams.canonical.com/juju) into your environment. +This is generally done when you want Juju to be able to run without having to +access the Internet. Alternatively you can specify a local directory as source. + +Sometimes this is because the environment does not have public access, +and sometimes you just want to avoid having to access data outside of +the local cloud. +`, + } +} + +func (c *SyncToolsCommand) SetFlags(f *gnuflag.FlagSet) { + f.BoolVar(&c.allVersions, "all", false, "copy all versions, not just the latest") + f.StringVar(&c.versionStr, "version", "", "copy a specific major[.minor] version") + f.BoolVar(&c.dryRun, "dry-run", false, "don't copy, just print what would be copied") + f.BoolVar(&c.dev, "dev", false, "consider development versions as well as released ones\n DEPRECATED: use --stream instead") + f.BoolVar(&c.public, "public", false, "tools are for a public cloud, so generate mirrors information") + f.StringVar(&c.source, "source", "", "local source directory") + f.StringVar(&c.stream, "stream", "", "simplestreams stream for which to sync metadata") + f.StringVar(&c.localDir, "local-dir", "", "local destination directory") + f.StringVar(&c.destination, "destination", "", "local destination directory") +} + +func (c *SyncToolsCommand) Init(args []string) error { + if c.destination != "" { + // Override localDir with destination as localDir now replaces destination + c.localDir = c.destination + logger.Warningf("Use of the --destination flag is deprecated in 1.18. Please use --local-dir instead.") + } + if c.versionStr != "" { + var err error + if c.majorVersion, c.minorVersion, err = version.ParseMajorMinor(c.versionStr); err != nil { + return err + } + } + if c.dev { + c.stream = envtools.TestingStream + } + return cmd.CheckEmpty(args) +} + +// syncToolsAPI provides an interface with a subset of the +// api.Client API. This exists to enable mocking. +type syncToolsAPI interface { + FindTools(majorVersion, minorVersion int, series, arch string) (params.FindToolsResult, error) + UploadTools(r io.Reader, v version.Binary, series ...string) (*coretools.Tools, error) + Close() error +} + +var getSyncToolsAPI = func(c *SyncToolsCommand) (syncToolsAPI, error) { + return c.NewAPIClient() +} + +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) + defer loggo.RemoveWriter("synctools") + + sctx := &sync.SyncContext{ + AllVersions: c.allVersions, + MajorVersion: c.majorVersion, + MinorVersion: c.minorVersion, + DryRun: c.dryRun, + Stream: c.stream, + Source: c.source, + } + + if c.localDir != "" { + stor, err := filestorage.NewFileStorageWriter(c.localDir) + if err != nil { + return err + } + writeMirrors := envtools.DoNotWriteMirrors + if c.public { + writeMirrors = envtools.WriteMirrors + } + sctx.TargetToolsFinder = sync.StorageToolsFinder{Storage: stor} + sctx.TargetToolsUploader = sync.StorageToolsUploader{ + Storage: stor, + WriteMetadata: true, + WriteMirrors: writeMirrors, + } + } else { + if c.public { + logger.Warningf("--public is ignored unless --local-dir is specified") + } + api, err := getSyncToolsAPI(c) + if err != nil { + return err + } + defer api.Close() + adapter := syncToolsAPIAdapter{api} + sctx.TargetToolsFinder = adapter + sctx.TargetToolsUploader = adapter + } + return block.ProcessBlockedError(syncTools(sctx), block.BlockChange) +} + +// syncToolsAPIAdapter implements sync.ToolsFinder and +// sync.ToolsUploader, adapting a syncToolsAPI. This +// enables the use of sync.SyncTools with the client +// API. +type syncToolsAPIAdapter struct { + syncToolsAPI +} + +func (s syncToolsAPIAdapter) FindTools(majorVersion int, stream string) (coretools.List, error) { + result, err := s.syncToolsAPI.FindTools(majorVersion, -1, "", "") + if err != nil { + return nil, err + } + if result.Error != nil { + if params.IsCodeNotFound(result.Error) { + return nil, coretools.ErrNoMatches + } + return nil, result.Error + } + return result.List, nil +} + +func (s syncToolsAPIAdapter) UploadTools(toolsDir, stream string, tools *coretools.Tools, data []byte) error { + _, err := s.syncToolsAPI.UploadTools(bytes.NewReader(data), tools.Version) + return err +} === added file 'src/github.com/juju/juju/cmd/juju/commands/synctools_test.go' --- src/github.com/juju/juju/cmd/juju/commands/synctools_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/synctools_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,292 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "io" + "io/ioutil" + "strings" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/loggo" + jc "github.com/juju/testing/checkers" + "github.com/juju/utils" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/environs/sync" + envtools "github.com/juju/juju/environs/tools" + coretesting "github.com/juju/juju/testing" + coretools "github.com/juju/juju/tools" + "github.com/juju/juju/version" +) + +type syncToolsSuite struct { + coretesting.BaseSuite + fakeSyncToolsAPI *fakeSyncToolsAPI +} + +var _ = gc.Suite(&syncToolsSuite{}) + +func (s *syncToolsSuite) SetUpTest(c *gc.C) { + s.BaseSuite.SetUpTest(c) + s.fakeSyncToolsAPI = &fakeSyncToolsAPI{} + s.PatchValue(&getSyncToolsAPI, func(c *SyncToolsCommand) (syncToolsAPI, error) { + return s.fakeSyncToolsAPI, nil + }) +} + +func (s *syncToolsSuite) Reset(c *gc.C) { + s.TearDownTest(c) + s.SetUpTest(c) +} + +func runSyncToolsCommand(c *gc.C, args ...string) (*cmd.Context, error) { + return coretesting.RunCommand(c, envcmd.Wrap(&SyncToolsCommand{}), args...) +} + +var syncToolsCommandTests = []struct { + description string + args []string + sctx *sync.SyncContext + public bool +}{ + { + description: "environment as only argument", + args: []string{"-e", "test-target"}, + sctx: &sync.SyncContext{}, + }, + { + description: "specifying also the synchronization source", + args: []string{"-e", "test-target", "--source", "/foo/bar"}, + sctx: &sync.SyncContext{ + Source: "/foo/bar", + }, + }, + { + description: "synchronize all version including development", + args: []string{"-e", "test-target", "--all", "--dev"}, + sctx: &sync.SyncContext{ + AllVersions: true, + Stream: "testing", + }, + }, + { + description: "just make a dry run", + args: []string{"-e", "test-target", "--dry-run"}, + sctx: &sync.SyncContext{ + DryRun: true, + }, + }, + { + description: "specified public (ignored by API)", + args: []string{"-e", "test-target", "--public"}, + sctx: &sync.SyncContext{}, + }, + { + description: "specify version", + args: []string{"-e", "test-target", "--version", "1.2"}, + sctx: &sync.SyncContext{ + MajorVersion: 1, + MinorVersion: 2, + }, + }, +} + +func (s *syncToolsSuite) TestSyncToolsCommand(c *gc.C) { + for i, test := range syncToolsCommandTests { + c.Logf("test %d: %s", i, test.description) + called := false + syncTools = func(sctx *sync.SyncContext) error { + c.Assert(sctx.AllVersions, gc.Equals, test.sctx.AllVersions) + c.Assert(sctx.MajorVersion, gc.Equals, test.sctx.MajorVersion) + c.Assert(sctx.MinorVersion, gc.Equals, test.sctx.MinorVersion) + c.Assert(sctx.DryRun, gc.Equals, test.sctx.DryRun) + c.Assert(sctx.Stream, gc.Equals, test.sctx.Stream) + c.Assert(sctx.Source, gc.Equals, test.sctx.Source) + + c.Assert(sctx.TargetToolsFinder, gc.FitsTypeOf, syncToolsAPIAdapter{}) + finder := sctx.TargetToolsFinder.(syncToolsAPIAdapter) + c.Assert(finder.syncToolsAPI, gc.Equals, s.fakeSyncToolsAPI) + + c.Assert(sctx.TargetToolsUploader, gc.FitsTypeOf, syncToolsAPIAdapter{}) + uploader := sctx.TargetToolsUploader.(syncToolsAPIAdapter) + c.Assert(uploader.syncToolsAPI, gc.Equals, s.fakeSyncToolsAPI) + + called = true + return nil + } + ctx, err := runSyncToolsCommand(c, test.args...) + c.Assert(err, jc.ErrorIsNil) + c.Assert(ctx, gc.NotNil) + c.Assert(called, jc.IsTrue) + s.Reset(c) + } +} + +func (s *syncToolsSuite) TestSyncToolsCommandTargetDirectory(c *gc.C) { + called := false + dir := c.MkDir() + syncTools = func(sctx *sync.SyncContext) error { + c.Assert(sctx.AllVersions, jc.IsFalse) + c.Assert(sctx.DryRun, jc.IsFalse) + c.Assert(sctx.Stream, gc.Equals, "proposed") + c.Assert(sctx.Source, gc.Equals, "") + c.Assert(sctx.TargetToolsUploader, gc.FitsTypeOf, sync.StorageToolsUploader{}) + uploader := sctx.TargetToolsUploader.(sync.StorageToolsUploader) + c.Assert(uploader.WriteMirrors, gc.Equals, envtools.DoNotWriteMirrors) + url, err := uploader.Storage.URL("") + c.Assert(err, jc.ErrorIsNil) + c.Assert(url, gc.Equals, utils.MakeFileURL(dir)) + called = true + return nil + } + ctx, err := runSyncToolsCommand(c, "-e", "test-target", "--local-dir", dir, "--stream", "proposed") + c.Assert(err, jc.ErrorIsNil) + c.Assert(ctx, gc.NotNil) + c.Assert(called, jc.IsTrue) +} + +func (s *syncToolsSuite) TestSyncToolsCommandTargetDirectoryPublic(c *gc.C) { + called := false + dir := c.MkDir() + syncTools = func(sctx *sync.SyncContext) error { + c.Assert(sctx.TargetToolsUploader, gc.FitsTypeOf, sync.StorageToolsUploader{}) + uploader := sctx.TargetToolsUploader.(sync.StorageToolsUploader) + c.Assert(uploader.WriteMirrors, gc.Equals, envtools.WriteMirrors) + called = true + return nil + } + ctx, err := runSyncToolsCommand(c, "-e", "test-target", "--local-dir", dir, "--public") + c.Assert(err, jc.ErrorIsNil) + c.Assert(ctx, gc.NotNil) + c.Assert(called, jc.IsTrue) +} + +func (s *syncToolsSuite) TestSyncToolsCommandDeprecatedDestination(c *gc.C) { + called := false + dir := c.MkDir() + syncTools = func(sctx *sync.SyncContext) error { + c.Assert(sctx.AllVersions, jc.IsFalse) + c.Assert(sctx.DryRun, jc.IsFalse) + c.Assert(sctx.Stream, gc.Equals, "released") + c.Assert(sctx.Source, gc.Equals, "") + c.Assert(sctx.TargetToolsUploader, gc.FitsTypeOf, sync.StorageToolsUploader{}) + uploader := sctx.TargetToolsUploader.(sync.StorageToolsUploader) + url, err := uploader.Storage.URL("") + c.Assert(err, jc.ErrorIsNil) + c.Assert(url, gc.Equals, utils.MakeFileURL(dir)) + called = true + return nil + } + // Register writer. + var tw loggo.TestWriter + c.Assert(loggo.RegisterWriter("deprecated-tester", &tw, loggo.DEBUG), gc.IsNil) + defer loggo.RemoveWriter("deprecated-tester") + // Add deprecated message to be checked. + messages := []jc.SimpleMessage{ + {loggo.WARNING, "Use of the --destination flag is deprecated in 1.18. Please use --local-dir instead."}, + } + // Run sync-tools command with --destination flag. + ctx, err := runSyncToolsCommand(c, "-e", "test-target", "--destination", dir, "--stream", "released") + c.Assert(err, jc.ErrorIsNil) + c.Assert(ctx, gc.NotNil) + c.Assert(called, jc.IsTrue) + // Check deprecated message was logged. + c.Check(tw.Log(), jc.LogMatches, messages) +} + +func (s *syncToolsSuite) TestAPIAdapterFindTools(c *gc.C) { + var called bool + result := coretools.List{&coretools.Tools{}} + fake := fakeSyncToolsAPI{ + findTools: func(majorVersion, minorVersion int, series, arch string) (params.FindToolsResult, error) { + called = true + c.Assert(majorVersion, gc.Equals, 2) + c.Assert(minorVersion, gc.Equals, -1) + c.Assert(series, gc.Equals, "") + c.Assert(arch, gc.Equals, "") + return params.FindToolsResult{List: result}, nil + }, + } + a := syncToolsAPIAdapter{&fake} + list, err := a.FindTools(2, "released") + c.Assert(err, jc.ErrorIsNil) + c.Assert(list, jc.SameContents, result) + c.Assert(called, jc.IsTrue) +} + +func (s *syncToolsSuite) TestAPIAdapterFindToolsNotFound(c *gc.C) { + fake := fakeSyncToolsAPI{ + findTools: func(majorVersion, minorVersion int, series, arch string) (params.FindToolsResult, error) { + err := common.ServerError(errors.NotFoundf("tools")) + return params.FindToolsResult{Error: err}, nil + }, + } + a := syncToolsAPIAdapter{&fake} + list, err := a.FindTools(1, "released") + c.Assert(err, gc.Equals, coretools.ErrNoMatches) + c.Assert(list, gc.HasLen, 0) +} + +func (s *syncToolsSuite) TestAPIAdapterFindToolsAPIError(c *gc.C) { + findToolsErr := common.ServerError(errors.NotFoundf("tools")) + fake := fakeSyncToolsAPI{ + findTools: func(majorVersion, minorVersion int, series, arch string) (params.FindToolsResult, error) { + return params.FindToolsResult{Error: findToolsErr}, findToolsErr + }, + } + a := syncToolsAPIAdapter{&fake} + list, err := a.FindTools(1, "released") + c.Assert(err, gc.Equals, findToolsErr) // error comes through untranslated + c.Assert(list, gc.HasLen, 0) +} + +func (s *syncToolsSuite) TestAPIAdapterUploadTools(c *gc.C) { + uploadToolsErr := errors.New("uh oh") + fake := fakeSyncToolsAPI{ + uploadTools: func(r io.Reader, v version.Binary, additionalSeries ...string) (*coretools.Tools, error) { + data, err := ioutil.ReadAll(r) + c.Assert(err, jc.ErrorIsNil) + c.Assert(string(data), gc.Equals, "abc") + c.Assert(v, gc.Equals, version.Current) + return nil, uploadToolsErr + }, + } + a := syncToolsAPIAdapter{&fake} + err := a.UploadTools("released", "released", &coretools.Tools{Version: version.Current}, []byte("abc")) + c.Assert(err, gc.Equals, uploadToolsErr) +} + +func (s *syncToolsSuite) TestAPIAdapterBlockUploadTools(c *gc.C) { + syncTools = func(sctx *sync.SyncContext) error { + // Block operation + return common.ErrOperationBlocked("TestAPIAdapterBlockUploadTools") + } + _, err := runSyncToolsCommand(c, "-e", "test-target", "--destination", c.MkDir(), "--stream", "released") + c.Assert(err, gc.ErrorMatches, cmd.ErrSilent.Error()) + // msg is logged + stripped := strings.Replace(c.GetTestLog(), "\n", "", -1) + c.Check(stripped, gc.Matches, ".*TestAPIAdapterBlockUploadTools.*") +} + +type fakeSyncToolsAPI struct { + findTools func(majorVersion, minorVersion int, series, arch string) (params.FindToolsResult, error) + uploadTools func(r io.Reader, v version.Binary, additionalSeries ...string) (*coretools.Tools, error) +} + +func (f *fakeSyncToolsAPI) FindTools(majorVersion, minorVersion int, series, arch string) (params.FindToolsResult, error) { + return f.findTools(majorVersion, minorVersion, series, arch) +} + +func (f *fakeSyncToolsAPI) UploadTools(r io.Reader, v version.Binary, additionalSeries ...string) (*coretools.Tools, error) { + return f.uploadTools(r, v, additionalSeries...) +} + +func (f *fakeSyncToolsAPI) Close() error { + return nil +} === added file 'src/github.com/juju/juju/cmd/juju/commands/unexpose.go' --- src/github.com/juju/juju/cmd/juju/commands/unexpose.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/unexpose.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,46 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "errors" + + "github.com/juju/cmd" + + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/block" +) + +// UnexposeCommand is responsible exposing services. +type UnexposeCommand struct { + envcmd.EnvCommandBase + ServiceName string +} + +func (c *UnexposeCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "unexpose", + Args: "", + Purpose: "unexpose a service", + } +} + +func (c *UnexposeCommand) Init(args []string) error { + if len(args) == 0 { + return errors.New("no service name specified") + } + c.ServiceName = args[0] + return cmd.CheckEmpty(args[1:]) +} + +// Run changes the juju-managed firewall to hide any +// ports that were also explicitly marked by units as closed. +func (c *UnexposeCommand) Run(_ *cmd.Context) error { + client, err := c.NewAPIClient() + if err != nil { + return err + } + defer client.Close() + return block.ProcessBlockedError(client.ServiceUnexpose(c.ServiceName), block.BlockChange) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/unexpose_test.go' --- src/github.com/juju/juju/cmd/juju/commands/unexpose_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/unexpose_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,73 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/juju/charm.v5" + + "github.com/juju/juju/cmd/envcmd" + jujutesting "github.com/juju/juju/juju/testing" + "github.com/juju/juju/testcharms" + "github.com/juju/juju/testing" +) + +type UnexposeSuite struct { + jujutesting.RepoSuite + CmdBlockHelper +} + +func (s *UnexposeSuite) SetUpTest(c *gc.C) { + s.RepoSuite.SetUpTest(c) + s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) + c.Assert(s.CmdBlockHelper, gc.NotNil) + s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) +} + +var _ = gc.Suite(&UnexposeSuite{}) + +func runUnexpose(c *gc.C, args ...string) error { + _, err := testing.RunCommand(c, envcmd.Wrap(&UnexposeCommand{}), args...) + return err +} + +func (s *UnexposeSuite) assertExposed(c *gc.C, service string, expected bool) { + svc, err := s.State.Service(service) + c.Assert(err, jc.ErrorIsNil) + actual := svc.IsExposed() + c.Assert(actual, gc.Equals, expected) +} + +func (s *UnexposeSuite) TestUnexpose(c *gc.C) { + testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") + err := runDeploy(c, "local:dummy", "some-service-name") + c.Assert(err, jc.ErrorIsNil) + curl := charm.MustParseURL("local:trusty/dummy-1") + s.AssertService(c, "some-service-name", curl, 1, 0) + + err = runExpose(c, "some-service-name") + c.Assert(err, jc.ErrorIsNil) + s.assertExposed(c, "some-service-name", true) + + err = runUnexpose(c, "some-service-name") + c.Assert(err, jc.ErrorIsNil) + s.assertExposed(c, "some-service-name", false) + + err = runUnexpose(c, "nonexistent-service") + c.Assert(err, gc.ErrorMatches, `service "nonexistent-service" not found`) +} + +func (s *UnexposeSuite) TestBlockUnexpose(c *gc.C) { + testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") + err := runDeploy(c, "local:dummy", "some-service-name") + c.Assert(err, jc.ErrorIsNil) + curl := charm.MustParseURL("local:trusty/dummy-1") + s.AssertService(c, "some-service-name", curl, 1, 0) + + // Block operation + s.BlockAllChanges(c, "TestBlockUnexpose") + err = runExpose(c, "some-service-name") + s.AssertBlocked(c, err, ".*TestBlockUnexpose.*") +} === added file 'src/github.com/juju/juju/cmd/juju/commands/upgradecharm.go' --- src/github.com/juju/juju/cmd/juju/commands/upgradecharm.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/upgradecharm.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,162 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "fmt" + "os" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/names" + "gopkg.in/juju/charm.v5" + "launchpad.net/gnuflag" + + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/block" + "github.com/juju/juju/cmd/juju/service" +) + +// UpgradeCharm is responsible for upgrading a service's charm. +type UpgradeCharmCommand struct { + envcmd.EnvCommandBase + ServiceName string + Force bool + RepoPath string // defaults to JUJU_REPOSITORY + SwitchURL string + Revision int // defaults to -1 (latest) +} + +const upgradeCharmDoc = ` +When no flags are set, the service's charm will be upgraded to the latest +revision available in the repository from which it was originally deployed. An +explicit revision can be chosen with the --revision flag. + +If the charm came from a local repository, its path will be assumed to be +$JUJU_REPOSITORY unless overridden by --repository. + +The local repository behaviour is tuned specifically to the workflow of a charm +author working on a single client machine; use of local repositories from +multiple clients is not supported and may lead to confusing behaviour. Each +local charm gets uploaded with the revision specified in the charm, if possible, +otherwise it gets a unique revision (highest in state + 1). + +The --switch flag allows you to replace the charm with an entirely different +one. The new charm's URL and revision are inferred as they would be when running +a deploy command. + +Please note that --switch is dangerous, because juju only has limited +information with which to determine compatibility; the operation will succeed, +regardless of potential havoc, so long as the following conditions hold: + +- The new charm must declare all relations that the service is currently +participating in. +- All config settings shared by the old and new charms must +have the same types. + +The new charm may add new relations and configuration settings. + +--switch and --revision are mutually exclusive. To specify a given revision +number with --switch, give it in the charm URL, for instance "cs:wordpress-5" +would specify revision number 5 of the wordpress charm. + +Use of the --force flag is not generally recommended; units upgraded while in an +error state will not have upgrade-charm hooks executed, and may cause unexpected +behavior. +` + +func (c *UpgradeCharmCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "upgrade-charm", + Args: "", + Purpose: "upgrade a service's charm", + Doc: upgradeCharmDoc, + } +} + +func (c *UpgradeCharmCommand) SetFlags(f *gnuflag.FlagSet) { + f.BoolVar(&c.Force, "force", false, "upgrade all units immediately, even if in error state") + f.StringVar(&c.RepoPath, "repository", os.Getenv("JUJU_REPOSITORY"), "local charm repository path") + f.StringVar(&c.SwitchURL, "switch", "", "crossgrade to a different charm") + f.IntVar(&c.Revision, "revision", -1, "explicit revision of current charm") +} + +func (c *UpgradeCharmCommand) Init(args []string) error { + switch len(args) { + case 1: + if !names.IsValidService(args[0]) { + return fmt.Errorf("invalid service name %q", args[0]) + } + c.ServiceName = args[0] + case 0: + return fmt.Errorf("no service specified") + default: + return cmd.CheckEmpty(args[1:]) + } + if c.SwitchURL != "" && c.Revision != -1 { + return fmt.Errorf("--switch and --revision are mutually exclusive") + } + return nil +} + +// Run connects to the specified environment and starts the charm +// upgrade process. +func (c *UpgradeCharmCommand) Run(ctx *cmd.Context) error { + client, err := c.NewAPIClient() + if err != nil { + return err + } + defer client.Close() + oldURL, err := client.ServiceGetCharmURL(c.ServiceName) + if err != nil { + return err + } + + conf, err := service.GetClientConfig(client) + if err != nil { + return errors.Trace(err) + } + + var newRef *charm.Reference + if c.SwitchURL != "" { + newRef, err = charm.ParseReference(c.SwitchURL) + if err != nil { + return err + } + } else { + // No new URL specified, but revision might have been. + newRef = oldURL.WithRevision(c.Revision).Reference() + } + + csClient, err := newCharmStoreClient() + if err != nil { + return errors.Trace(err) + } + defer csClient.jar.Save() + newURL, repo, err := resolveCharmURL(newRef.String(), csClient.params, ctx.AbsPath(c.RepoPath), conf) + if err != nil { + return errors.Trace(err) + } + + // If no explicit revision was set with either SwitchURL + // or Revision flags, discover the latest. + if *newURL == *oldURL { + if newRef.Revision != -1 { + return fmt.Errorf("already running specified charm %q", newURL) + } + if newURL.Schema == "cs" { + // No point in trying to upgrade a charm store charm when + // we just determined that's the latest revision + // available. + return fmt.Errorf("already running latest charm %q", newURL) + } + } + + addedURL, err := addCharmViaAPI(client, ctx, newURL, repo, csClient) + if err != nil { + return block.ProcessBlockedError(err, block.BlockChange) + } + + return block.ProcessBlockedError(client.ServiceSetCharm(c.ServiceName, addedURL.String(), c.Force), block.BlockChange) +} === added file 'src/github.com/juju/juju/cmd/juju/commands/upgradecharm_test.go' --- src/github.com/juju/juju/cmd/juju/commands/upgradecharm_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/upgradecharm_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,335 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "bytes" + "io/ioutil" + "os" + "path" + + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/juju/charm.v5" + "gopkg.in/juju/charm.v5/charmrepo" + "gopkg.in/juju/charmstore.v4" + "gopkg.in/juju/charmstore.v4/charmstoretesting" + + "github.com/juju/juju/cmd/envcmd" + jujutesting "github.com/juju/juju/juju/testing" + "github.com/juju/juju/state" + "github.com/juju/juju/testcharms" + "github.com/juju/juju/testing" +) + +type UpgradeCharmErrorsSuite struct { + jujutesting.RepoSuite + srv *charmstoretesting.Server +} + +func (s *UpgradeCharmErrorsSuite) SetUpTest(c *gc.C) { + s.RepoSuite.SetUpTest(c) + s.srv = charmstoretesting.OpenServer(c, s.Session, charmstore.ServerParams{}) + s.PatchValue(&charmrepo.CacheDir, c.MkDir()) + original := newCharmStoreClient + s.PatchValue(&newCharmStoreClient, func() (*csClient, error) { + csclient, err := original() + c.Assert(err, jc.ErrorIsNil) + csclient.params.URL = s.srv.URL() + return csclient, nil + }) +} + +func (s *UpgradeCharmErrorsSuite) TearDownTest(c *gc.C) { + s.srv.Close() + s.RepoSuite.TearDownTest(c) +} + +var _ = gc.Suite(&UpgradeCharmErrorsSuite{}) + +func runUpgradeCharm(c *gc.C, args ...string) error { + _, err := testing.RunCommand(c, envcmd.Wrap(&UpgradeCharmCommand{}), args...) + return err +} + +func (s *UpgradeCharmErrorsSuite) TestInvalidArgs(c *gc.C) { + err := runUpgradeCharm(c) + c.Assert(err, gc.ErrorMatches, "no service specified") + err = runUpgradeCharm(c, "invalid:name") + c.Assert(err, gc.ErrorMatches, `invalid service name "invalid:name"`) + err = runUpgradeCharm(c, "foo", "bar") + c.Assert(err, gc.ErrorMatches, `unrecognized args: \["bar"\]`) +} + +func (s *UpgradeCharmErrorsSuite) TestWithInvalidRepository(c *gc.C) { + testcharms.Repo.ClonedDirPath(s.SeriesPath, "riak") + err := runDeploy(c, "local:riak", "riak") + c.Assert(err, jc.ErrorIsNil) + + err = runUpgradeCharm(c, "riak", "--repository=blah") + c.Assert(err, gc.ErrorMatches, `no repository found at ".*blah"`) + // Reset JUJU_REPOSITORY explicitly, because repoSuite.SetUpTest + // overwrites it (TearDownTest will revert it again). + os.Setenv("JUJU_REPOSITORY", "") + err = runUpgradeCharm(c, "riak", "--repository=") + c.Assert(err, gc.ErrorMatches, `charm not found in ".*": local:trusty/riak`) +} + +func (s *UpgradeCharmErrorsSuite) TestInvalidService(c *gc.C) { + err := runUpgradeCharm(c, "phony") + c.Assert(err, gc.ErrorMatches, `service "phony" not found`) +} + +func (s *UpgradeCharmErrorsSuite) deployService(c *gc.C) { + testcharms.Repo.ClonedDirPath(s.SeriesPath, "riak") + err := runDeploy(c, "local:riak", "riak") + c.Assert(err, jc.ErrorIsNil) +} + +func (s *UpgradeCharmErrorsSuite) TestInvalidSwitchURL(c *gc.C) { + s.deployService(c) + err := runUpgradeCharm(c, "riak", "--switch=blah") + c.Assert(err, gc.ErrorMatches, `cannot resolve charm URL "cs:trusty/blah": charm not found`) + err = runUpgradeCharm(c, "riak", "--switch=cs:missing/one") + c.Assert(err, gc.ErrorMatches, `cannot resolve charm URL "cs:missing/one": charm not found`) + // TODO(dimitern): add tests with incompatible charms +} + +func (s *UpgradeCharmErrorsSuite) TestSwitchAndRevisionFails(c *gc.C) { + s.deployService(c) + err := runUpgradeCharm(c, "riak", "--switch=riak", "--revision=2") + c.Assert(err, gc.ErrorMatches, "--switch and --revision are mutually exclusive") +} + +func (s *UpgradeCharmErrorsSuite) TestInvalidRevision(c *gc.C) { + s.deployService(c) + err := runUpgradeCharm(c, "riak", "--revision=blah") + c.Assert(err, gc.ErrorMatches, `invalid value "blah" for flag --revision: strconv.ParseInt: parsing "blah": invalid syntax`) +} + +type UpgradeCharmSuccessSuite struct { + jujutesting.RepoSuite + CmdBlockHelper + path string + riak *state.Service +} + +var _ = gc.Suite(&UpgradeCharmSuccessSuite{}) + +func (s *UpgradeCharmSuccessSuite) SetUpTest(c *gc.C) { + s.RepoSuite.SetUpTest(c) + s.path = testcharms.Repo.ClonedDirPath(s.SeriesPath, "riak") + err := runDeploy(c, "local:riak", "riak") + c.Assert(err, jc.ErrorIsNil) + s.riak, err = s.State.Service("riak") + c.Assert(err, jc.ErrorIsNil) + ch, forced, err := s.riak.Charm() + c.Assert(err, jc.ErrorIsNil) + c.Assert(ch.Revision(), gc.Equals, 7) + c.Assert(forced, jc.IsFalse) + + s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) + c.Assert(s.CmdBlockHelper, gc.NotNil) + s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) +} + +func (s *UpgradeCharmSuccessSuite) assertUpgraded(c *gc.C, revision int, forced bool) *charm.URL { + err := s.riak.Refresh() + c.Assert(err, jc.ErrorIsNil) + ch, force, err := s.riak.Charm() + c.Assert(err, jc.ErrorIsNil) + c.Assert(ch.Revision(), gc.Equals, revision) + c.Assert(force, gc.Equals, forced) + s.AssertCharmUploaded(c, ch.URL()) + return ch.URL() +} + +func (s *UpgradeCharmSuccessSuite) assertLocalRevision(c *gc.C, revision int, path string) { + dir, err := charm.ReadCharmDir(path) + c.Assert(err, jc.ErrorIsNil) + c.Assert(dir.Revision(), gc.Equals, revision) +} + +func (s *UpgradeCharmSuccessSuite) TestLocalRevisionUnchanged(c *gc.C) { + err := runUpgradeCharm(c, "riak") + c.Assert(err, jc.ErrorIsNil) + s.assertUpgraded(c, 8, false) + // Even though the remote revision is bumped, the local one should + // be unchanged. + s.assertLocalRevision(c, 7, s.path) +} + +func (s *UpgradeCharmSuccessSuite) TestBlockUpgradeCharm(c *gc.C) { + // Block operation + s.BlockAllChanges(c, "TestBlockUpgradeCharm") + err := runUpgradeCharm(c, "riak") + s.AssertBlocked(c, err, ".*TestBlockUpgradeCharm.*") +} + +func (s *UpgradeCharmSuccessSuite) TestRespectsLocalRevisionWhenPossible(c *gc.C) { + dir, err := charm.ReadCharmDir(s.path) + c.Assert(err, jc.ErrorIsNil) + err = dir.SetDiskRevision(42) + c.Assert(err, jc.ErrorIsNil) + + err = runUpgradeCharm(c, "riak") + c.Assert(err, jc.ErrorIsNil) + s.assertUpgraded(c, 42, false) + s.assertLocalRevision(c, 42, s.path) +} + +func (s *UpgradeCharmSuccessSuite) TestUpgradesWithBundle(c *gc.C) { + dir, err := charm.ReadCharmDir(s.path) + c.Assert(err, jc.ErrorIsNil) + dir.SetRevision(42) + buf := &bytes.Buffer{} + err = dir.ArchiveTo(buf) + c.Assert(err, jc.ErrorIsNil) + bundlePath := path.Join(s.SeriesPath, "riak.charm") + err = ioutil.WriteFile(bundlePath, buf.Bytes(), 0644) + c.Assert(err, jc.ErrorIsNil) + + err = runUpgradeCharm(c, "riak") + c.Assert(err, jc.ErrorIsNil) + s.assertUpgraded(c, 42, false) + s.assertLocalRevision(c, 7, s.path) +} + +func (s *UpgradeCharmSuccessSuite) TestBlockUpgradesWithBundle(c *gc.C) { + dir, err := charm.ReadCharmDir(s.path) + c.Assert(err, jc.ErrorIsNil) + dir.SetRevision(42) + buf := &bytes.Buffer{} + err = dir.ArchiveTo(buf) + c.Assert(err, jc.ErrorIsNil) + bundlePath := path.Join(s.SeriesPath, "riak.charm") + err = ioutil.WriteFile(bundlePath, buf.Bytes(), 0644) + c.Assert(err, jc.ErrorIsNil) + + // Block operation + s.BlockAllChanges(c, "TestBlockUpgradesWithBundle") + err = runUpgradeCharm(c, "riak") + s.AssertBlocked(c, err, ".*TestBlockUpgradesWithBundle.*") +} + +func (s *UpgradeCharmSuccessSuite) TestForcedUpgrade(c *gc.C) { + err := runUpgradeCharm(c, "riak", "--force") + c.Assert(err, jc.ErrorIsNil) + s.assertUpgraded(c, 8, true) + // Local revision is not changed. + s.assertLocalRevision(c, 7, s.path) +} + +func (s *UpgradeCharmSuccessSuite) TestBlockForcedUpgrade(c *gc.C) { + // Block operation + s.BlockAllChanges(c, "TestBlockForcedUpgrade") + err := runUpgradeCharm(c, "riak", "--force") + c.Assert(err, jc.ErrorIsNil) + s.assertUpgraded(c, 8, true) + // Local revision is not changed. + s.assertLocalRevision(c, 7, s.path) +} + +var myriakMeta = []byte(` +name: myriak +summary: "K/V storage engine" +description: "Scalable K/V Store in Erlang with Clocks :-)" +provides: + endpoint: + interface: http + admin: + interface: http +peers: + ring: + interface: riak +`) + +func (s *UpgradeCharmSuccessSuite) TestSwitch(c *gc.C) { + myriakPath := testcharms.Repo.RenamedClonedDirPath(s.SeriesPath, "riak", "myriak") + err := ioutil.WriteFile(path.Join(myriakPath, "metadata.yaml"), myriakMeta, 0644) + c.Assert(err, jc.ErrorIsNil) + + // Test with local repo and no explicit revsion. + err = runUpgradeCharm(c, "riak", "--switch=local:myriak") + c.Assert(err, jc.ErrorIsNil) + curl := s.assertUpgraded(c, 7, false) + c.Assert(curl.String(), gc.Equals, "local:trusty/myriak-7") + s.assertLocalRevision(c, 7, myriakPath) + + // Now try the same with explicit revision - should fail. + err = runUpgradeCharm(c, "riak", "--switch=local:myriak-7") + c.Assert(err, gc.ErrorMatches, `already running specified charm "local:trusty/myriak-7"`) + + // Change the revision to 42 and upgrade to it with explicit revision. + err = ioutil.WriteFile(path.Join(myriakPath, "revision"), []byte("42"), 0644) + c.Assert(err, jc.ErrorIsNil) + err = runUpgradeCharm(c, "riak", "--switch=local:myriak-42") + c.Assert(err, jc.ErrorIsNil) + curl = s.assertUpgraded(c, 42, false) + c.Assert(curl.String(), gc.Equals, "local:trusty/myriak-42") + s.assertLocalRevision(c, 42, myriakPath) +} + +type UpgradeCharmCharmStoreSuite struct { + charmStoreSuite +} + +var _ = gc.Suite(&UpgradeCharmCharmStoreSuite{}) + +var upgradeCharmAuthorizationTests = []struct { + about string + uploadURL string + switchURL string + readPermUser string + expectError string +}{{ + about: "public charm, success", + uploadURL: "cs:~bob/trusty/wordpress1-10", + switchURL: "cs:~bob/trusty/wordpress1", +}, { + about: "public charm, fully resolved, success", + uploadURL: "cs:~bob/trusty/wordpress2-10", + switchURL: "cs:~bob/trusty/wordpress2-10", +}, { + about: "non-public charm, success", + uploadURL: "cs:~bob/trusty/wordpress3-10", + switchURL: "cs:~bob/trusty/wordpress3", + readPermUser: clientUserName, +}, { + about: "non-public charm, fully resolved, success", + uploadURL: "cs:~bob/trusty/wordpress4-10", + switchURL: "cs:~bob/trusty/wordpress4-10", + readPermUser: clientUserName, +}, { + about: "non-public charm, access denied", + uploadURL: "cs:~bob/trusty/wordpress5-10", + switchURL: "cs:~bob/trusty/wordpress5", + readPermUser: "bob", + expectError: `cannot resolve charm URL "cs:~bob/trusty/wordpress5": cannot get "/~bob/trusty/wordpress5/meta/any\?include=id": unauthorized: access denied for user "client-username"`, +}, { + about: "non-public charm, fully resolved, access denied", + uploadURL: "cs:~bob/trusty/wordpress6-47", + switchURL: "cs:~bob/trusty/wordpress6-47", + readPermUser: "bob", + expectError: `cannot retrieve charm "cs:~bob/trusty/wordpress6-47": cannot get archive: unauthorized: access denied for user "client-username"`, +}} + +func (s *UpgradeCharmCharmStoreSuite) TestUpgradeCharmAuthorization(c *gc.C) { + s.uploadCharm(c, "cs:~other/trusty/wordpress-0", "wordpress") + err := runDeploy(c, "cs:~other/trusty/wordpress-0") + c.Assert(err, jc.ErrorIsNil) + for i, test := range upgradeCharmAuthorizationTests { + c.Logf("test %d: %s", i, test.about) + url, _ := s.uploadCharm(c, test.uploadURL, "wordpress") + if test.readPermUser != "" { + s.changeReadPerm(c, url, test.readPermUser) + } + err := runUpgradeCharm(c, "wordpress", "--switch", test.switchURL) + if test.expectError != "" { + c.Assert(err, gc.ErrorMatches, test.expectError) + continue + } + c.Assert(err, jc.ErrorIsNil) + } +} === added file 'src/github.com/juju/juju/cmd/juju/commands/upgradejuju.go' --- src/github.com/juju/juju/cmd/juju/commands/upgradejuju.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/upgradejuju.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,418 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "bufio" + stderrors "errors" + "fmt" + "io" + "os" + "path" + "strings" + + "github.com/juju/cmd" + "github.com/juju/errors" + "launchpad.net/gnuflag" + + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/block" + "github.com/juju/juju/environs/config" + "github.com/juju/juju/environs/sync" + coretools "github.com/juju/juju/tools" + "github.com/juju/juju/version" +) + +// UpgradeJujuCommand upgrades the agents in a juju installation. +type UpgradeJujuCommand struct { + envcmd.EnvCommandBase + vers string + Version version.Number + UploadTools bool + DryRun bool + ResetPrevious bool + AssumeYes bool + Series []string +} + +var upgradeJujuDoc = ` +The upgrade-juju command upgrades a running environment by setting a version +number for all juju agents to run. By default, it chooses the most recent +supported version compatible with the command-line tools version. + +A development version is defined to be any version with an odd minor +version or a nonzero build component (for example version 2.1.1, 3.3.0 +and 2.0.0.1 are development versions; 2.0.3 and 3.4.1 are not). A +development version may be chosen in two cases: + + - when the current agent version is a development one and there is + a more recent version available with the same major.minor numbers; + - when an explicit --version major.minor is given (e.g. --version 1.17, + or 1.17.2, but not just 1) + +For development use, the --upload-tools flag specifies that the juju tools will +packaged (or compiled locally, if no jujud binaries exists, for which you will +need the golang packages installed) and uploaded before the version is set. +Currently the tools will be uploaded as if they had the version of the current +juju tool, unless specified otherwise by the --version flag. + +When run without arguments. upgrade-juju will try to upgrade to the +following versions, in order of preference, depending on the current +value of the environment's agent-version setting: + + - The highest patch.build version of the *next* stable major.minor version. + - The highest patch.build version of the *current* major.minor version. + +Both of these depend on tools availability, which some situations (no +outgoing internet access) and provider types (such as maas) require that +you manage yourself; see the documentation for "sync-tools". + +The upgrade-juju command will abort if an upgrade is already in +progress. It will also abort if a previous upgrade was partially +completed - this can happen if one of the state servers in a high +availability environment failed to upgrade. If a failed upgrade has +been resolved, the --reset-previous-upgrade flag can be used to reset +the environment's upgrade tracking state, allowing further upgrades.` + +func (c *UpgradeJujuCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "upgrade-juju", + Purpose: "upgrade the tools in a juju environment", + Doc: upgradeJujuDoc, + } +} + +func (c *UpgradeJujuCommand) SetFlags(f *gnuflag.FlagSet) { + f.StringVar(&c.vers, "version", "", "upgrade to specific version") + f.BoolVar(&c.UploadTools, "upload-tools", false, "upload local version of tools") + f.BoolVar(&c.DryRun, "dry-run", false, "don't change anything, just report what would change") + f.BoolVar(&c.ResetPrevious, "reset-previous-upgrade", false, "clear the previous (incomplete) upgrade status (use with care)") + f.BoolVar(&c.AssumeYes, "y", false, "answer 'yes' to confirmation prompts") + f.BoolVar(&c.AssumeYes, "yes", false, "") + f.Var(newSeriesValue(nil, &c.Series), "series", "upload tools for supplied comma-separated series list (OBSOLETE)") +} + +func (c *UpgradeJujuCommand) Init(args []string) error { + if c.vers != "" { + vers, err := version.Parse(c.vers) + if err != nil { + return err + } + if vers.Major != version.Current.Major { + return fmt.Errorf("cannot upgrade to version incompatible with CLI") + } + if c.UploadTools && vers.Build != 0 { + // TODO(fwereade): when we start taking versions from actual built + // code, we should disable --version when used with --upload-tools. + // For now, it's the only way to experiment with version upgrade + // 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") + } + c.Version = vers + } + if len(c.Series) > 0 && !c.UploadTools { + return fmt.Errorf("--series requires --upload-tools") + } + return cmd.CheckEmpty(args) +} + +var errUpToDate = stderrors.New("no upgrades available") + +func formatTools(tools coretools.List) string { + formatted := make([]string, len(tools)) + for i, tools := range tools { + formatted[i] = fmt.Sprintf(" %s", tools.Version.String()) + } + return strings.Join(formatted, "\n") +} + +type upgradeJujuAPI interface { + EnvironmentGet() (map[string]interface{}, error) + FindTools(majorVersion, minorVersion int, series, arch string) (result params.FindToolsResult, err error) + UploadTools(r io.Reader, vers version.Binary, additionalSeries ...string) (*coretools.Tools, error) + AbortCurrentUpgrade() error + SetEnvironAgentVersion(version version.Number) error + Close() error +} + +var getUpgradeJujuAPI = func(c *UpgradeJujuCommand) (upgradeJujuAPI, error) { + return c.NewAPIClient() +} + +// Run changes the version proposed for the juju envtools. +func (c *UpgradeJujuCommand) Run(ctx *cmd.Context) (err error) { + if len(c.Series) > 0 { + fmt.Fprintln(ctx.Stderr, "Use of --series is obsolete. --upload-tools now expands to all supported series of the same operating system.") + } + + client, err := getUpgradeJujuAPI(c) + if err != nil { + return err + } + defer client.Close() + defer func() { + if err == errUpToDate { + ctx.Infof(err.Error()) + err = nil + } + }() + + // Determine the version to upgrade to, uploading tools if necessary. + attrs, err := client.EnvironmentGet() + if err != nil { + return err + } + cfg, err := config.New(config.NoDefaults, attrs) + if err != nil { + return err + } + context, err := c.initVersions(client, cfg) + if err != nil { + return err + } + if c.UploadTools && !c.DryRun { + if err := context.uploadTools(); err != nil { + return block.ProcessBlockedError(err, block.BlockChange) + } + } + if err := context.validate(); err != nil { + return err + } + // TODO(fwereade): this list may be incomplete, pending envtools.Upload change. + ctx.Infof("available tools:\n%s", formatTools(context.tools)) + ctx.Infof("best version:\n %s", context.chosen) + if c.DryRun { + ctx.Infof("upgrade to this version by running\n juju upgrade-juju --version=\"%s\"\n", context.chosen) + } else { + if c.ResetPrevious { + if ok, err := c.confirmResetPreviousUpgrade(ctx); !ok || err != nil { + const message = "previous upgrade not reset and no new upgrade triggered" + if err != nil { + return errors.Annotate(err, message) + } + return errors.New(message) + } + if err := client.AbortCurrentUpgrade(); err != nil { + return block.ProcessBlockedError(err, block.BlockChange) + } + } + if err := client.SetEnvironAgentVersion(context.chosen); err != nil { + if params.IsCodeUpgradeInProgress(err) { + return errors.Errorf("%s\n\n"+ + "Please wait for the upgrade to complete or if there was a problem with\n"+ + "the last upgrade that has been resolved, consider running the\n"+ + "upgrade-juju command with the --reset-previous-upgrade flag.", err, + ) + } else { + return block.ProcessBlockedError(err, block.BlockChange) + } + } + logger.Infof("started upgrade to %s", context.chosen) + } + return nil +} + +const resetPreviousUpgradeMessage = ` +WARNING! using --reset-previous-upgrade when an upgrade is in progress +will cause the upgrade to fail. Only use this option to clear an +incomplete upgrade where the root cause has been resolved. + +Continue [y/N]? ` + +func (c *UpgradeJujuCommand) confirmResetPreviousUpgrade(ctx *cmd.Context) (bool, error) { + if c.AssumeYes { + return true, nil + } + fmt.Fprintf(ctx.Stdout, resetPreviousUpgradeMessage) + scanner := bufio.NewScanner(ctx.Stdin) + scanner.Scan() + err := scanner.Err() + if err != nil && err != io.EOF { + return false, err + } + answer := strings.ToLower(scanner.Text()) + return answer == "y" || answer == "yes", nil +} + +// initVersions collects state relevant to an upgrade decision. The returned +// agent and client versions, and the list of currently available tools, will +// always be accurate; the chosen version, and the flag indicating development +// mode, may remain blank until uploadTools or validate is called. +func (c *UpgradeJujuCommand) initVersions(client upgradeJujuAPI, cfg *config.Config) (*upgradeContext, error) { + agent, ok := cfg.AgentVersion() + if !ok { + // Can't happen. In theory. + return nil, fmt.Errorf("incomplete environment configuration") + } + if c.Version == agent { + return nil, errUpToDate + } + clientVersion := version.Current.Number + findResult, err := client.FindTools(clientVersion.Major, -1, "", "") + if err != nil { + return nil, err + } + err = findResult.Error + if findResult.Error != nil { + if !params.IsCodeNotFound(err) { + return nil, err + } + if !c.UploadTools { + // No tools found and we shouldn't upload any, so if we are not asking for a + // major upgrade, pretend there is no more recent version available. + if c.Version == version.Zero && agent.Major == clientVersion.Major { + return nil, errUpToDate + } + return nil, err + } + } + return &upgradeContext{ + agent: agent, + client: clientVersion, + chosen: c.Version, + tools: findResult.List, + apiClient: client, + config: cfg, + }, nil +} + +// upgradeContext holds the version information for making upgrade decisions. +type upgradeContext struct { + agent version.Number + client version.Number + chosen version.Number + tools coretools.List + config *config.Config + apiClient upgradeJujuAPI +} + +// uploadTools compiles jujud from $GOPATH and uploads it into the supplied +// storage. If no version has been explicitly chosen, the version number +// reported by the built tools will be based on the client version number. +// In any case, the version number reported will have a build component higher +// than that of any otherwise-matching available envtools. +// uploadTools resets the chosen version and replaces the available tools +// with the ones just uploaded. +func (context *upgradeContext) uploadTools() (err error) { + // TODO(fwereade): this is kinda crack: we should not assume that + // version.Current matches whatever source happens to be built. The + // ideal would be: + // 1) compile jujud from $GOPATH into some build dir + // 2) get actual version with `jujud version` + // 3) check actual version for compatibility with CLI tools + // 4) generate unique build version with reference to available tools + // 5) force-version that unique version into the dir directly + // 6) archive and upload the build dir + // ...but there's no way we have time for that now. In the meantime, + // considering the use cases, this should work well enough; but it + // won't detect an incompatible major-version change, which is a shame. + if context.chosen == version.Zero { + context.chosen = context.client + } + context.chosen = uploadVersion(context.chosen, context.tools) + + builtTools, err := sync.BuildToolsTarball(&context.chosen, "upgrade") + if err != nil { + return err + } + defer os.RemoveAll(builtTools.Dir) + + var uploaded *coretools.Tools + toolsPath := path.Join(builtTools.Dir, builtTools.StorageName) + logger.Infof("uploading tools %v (%dkB) to Juju state server", builtTools.Version, (builtTools.Size+512)/1024) + f, err := os.Open(toolsPath) + if err != nil { + return err + } + defer f.Close() + additionalSeries := version.OSSupportedSeries(builtTools.Version.OS) + uploaded, err = context.apiClient.UploadTools(f, builtTools.Version, additionalSeries...) + if err != nil { + return err + } + context.tools = coretools.List{uploaded} + return nil +} + +// validate chooses an upgrade version, if one has not already been chosen, +// and ensures the tools list contains no entries that do not have that version. +// If validate returns no error, the environment agent-version can be set to +// the value of the chosen field. +func (context *upgradeContext) validate() (err error) { + if context.chosen == version.Zero { + // No explicitly specified version, so find the version to which we + // need to upgrade. If the CLI and agent major versions match, we find + // next available stable release to upgrade to by incrementing the + // minor version, starting from the current agent version and doing + // major.minor+1.patch=0. If the CLI has a greater major version, + // we just use the CLI version as is. + nextVersion := context.agent + if nextVersion.Major == context.client.Major { + nextVersion.Minor += 1 + nextVersion.Patch = 0 + } else { + nextVersion = context.client + } + + newestNextStable, found := context.tools.NewestCompatible(nextVersion) + if found { + logger.Debugf("found a more recent stable version %s", newestNextStable) + context.chosen = newestNextStable + } else { + newestCurrent, found := context.tools.NewestCompatible(context.agent) + if found { + logger.Debugf("found more recent current version %s", newestCurrent) + context.chosen = newestCurrent + } else { + if context.agent.Major != context.client.Major { + return fmt.Errorf("no compatible tools available") + } else { + return fmt.Errorf("no more recent supported versions available") + } + } + } + } else { + // If not completely specified already, pick a single tools version. + filter := coretools.Filter{Number: context.chosen} + if context.tools, err = context.tools.Match(filter); err != nil { + return err + } + context.chosen, context.tools = context.tools.Newest() + } + if context.chosen == context.agent { + return errUpToDate + } + + // Disallow major.minor version downgrades. + if context.chosen.Major < context.agent.Major || + context.chosen.Major == context.agent.Major && context.chosen.Minor < context.agent.Minor { + // TODO(fwereade): I'm a bit concerned about old agent/CLI tools even + // *connecting* to environments with higher agent-versions; but ofc they + // have to connect in order to discover they shouldn't. However, once + // 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("cannot change version from %s to %s", context.agent, context.chosen) + } + + return nil +} + +// uploadVersion returns a copy of the supplied version with a build number +// higher than any of the supplied tools that share its major, minor and patch. +func uploadVersion(vers version.Number, existing coretools.List) version.Number { + vers.Build++ + for _, t := range existing { + if t.Version.Major != vers.Major || t.Version.Minor != vers.Minor || t.Version.Patch != vers.Patch { + continue + } + if t.Version.Build >= vers.Build { + vers.Build = t.Version.Build + 1 + } + } + return vers +} === added file 'src/github.com/juju/juju/cmd/juju/commands/upgradejuju_test.go' --- src/github.com/juju/juju/cmd/juju/commands/upgradejuju_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/commands/upgradejuju_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,759 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package commands + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "io" + "io/ioutil" + "strings" + + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/params" + apiservertesting "github.com/juju/juju/apiserver/testing" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/environs/config" + "github.com/juju/juju/environs/filestorage" + "github.com/juju/juju/environs/sync" + envtesting "github.com/juju/juju/environs/testing" + "github.com/juju/juju/environs/tools" + toolstesting "github.com/juju/juju/environs/tools/testing" + jujutesting "github.com/juju/juju/juju/testing" + "github.com/juju/juju/network" + _ "github.com/juju/juju/provider/dummy" + "github.com/juju/juju/state" + coretesting "github.com/juju/juju/testing" + coretools "github.com/juju/juju/tools" + "github.com/juju/juju/version" +) + +type UpgradeJujuSuite struct { + jujutesting.JujuConnSuite + + resources *common.Resources + authoriser apiservertesting.FakeAuthorizer + + toolsDir string + CmdBlockHelper +} + +func (s *UpgradeJujuSuite) SetUpTest(c *gc.C) { + s.JujuConnSuite.SetUpTest(c) + s.resources = common.NewResources() + s.authoriser = apiservertesting.FakeAuthorizer{ + Tag: s.AdminUserTag(c), + } + + s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) + c.Assert(s.CmdBlockHelper, gc.NotNil) + s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) +} + +var _ = gc.Suite(&UpgradeJujuSuite{}) + +var upgradeJujuTests = []struct { + about string + tools []string + currentVersion string + agentVersion string + + args []string + expectInitErr string + expectErr string + expectVersion string + expectUploaded []string +}{{ + about: "unwanted extra argument", + currentVersion: "1.0.0-quantal-amd64", + args: []string{"foo"}, + expectInitErr: "unrecognized args:.*", +}, { + about: "removed arg --dev specified", + currentVersion: "1.0.0-quantal-amd64", + args: []string{"--dev"}, + expectInitErr: "flag provided but not defined: --dev", +}, { + about: "invalid --version value", + currentVersion: "1.0.0-quantal-amd64", + args: []string{"--version", "invalid-version"}, + expectInitErr: "invalid version .*", +}, { + about: "just major version, no minor specified", + currentVersion: "4.2.0-quantal-amd64", + args: []string{"--version", "4"}, + expectInitErr: `invalid version "4"`, +}, { + about: "major version upgrade to incompatible version", + currentVersion: "2.0.0-quantal-amd64", + args: []string{"--version", "5.2.0"}, + expectInitErr: "cannot upgrade to version incompatible with CLI", +}, { + about: "major version downgrade to incompatible version", + currentVersion: "4.2.0-quantal-amd64", + args: []string{"--version", "3.2.0"}, + expectInitErr: "cannot upgrade to version incompatible with CLI", +}, { + about: "invalid --series", + currentVersion: "4.2.0-quantal-amd64", + args: []string{"--series", "precise&quantal"}, + expectInitErr: `invalid value "precise&quantal" for flag --series: .*`, +}, { + about: "--series without --upload-tools", + currentVersion: "4.2.0-quantal-amd64", + args: []string{"--series", "precise,quantal"}, + expectInitErr: "--series requires --upload-tools", +}, { + about: "--upload-tools with inappropriate version 1", + currentVersion: "4.2.0-quantal-amd64", + args: []string{"--upload-tools", "--version", "3.1.0"}, + expectInitErr: "cannot upgrade to version incompatible with CLI", +}, { + about: "--upload-tools with inappropriate version 2", + currentVersion: "3.2.7-quantal-amd64", + args: []string{"--upload-tools", "--version", "3.2.8.4"}, + expectInitErr: "cannot specify build number when uploading tools", +}, { + about: "latest supported stable release", + tools: []string{"2.1.0-quantal-amd64", "2.1.2-quantal-i386", "2.1.3-quantal-amd64", "2.1-dev1-quantal-amd64"}, + currentVersion: "2.0.0-quantal-amd64", + agentVersion: "2.0.0", + expectVersion: "2.1.3", +}, { + about: "latest current release", + tools: []string{"2.0.5-quantal-amd64", "2.0.1-quantal-i386", "2.3.3-quantal-amd64"}, + currentVersion: "2.0.0-quantal-amd64", + agentVersion: "2.0.0", + expectVersion: "2.0.5", +}, { + about: "latest current release matching CLI, major version", + tools: []string{"3.2.0-quantal-amd64"}, + currentVersion: "3.2.0-quantal-amd64", + agentVersion: "2.8.2", + expectVersion: "3.2.0", +}, { + about: "latest current release matching CLI, major version, no matching major tools", + tools: []string{"2.8.2-quantal-amd64"}, + currentVersion: "3.2.0-quantal-amd64", + agentVersion: "2.8.2", + expectErr: "no matching tools available", +}, { + about: "latest current release matching CLI, major version, no matching tools", + tools: []string{"3.3.0-quantal-amd64"}, + currentVersion: "3.2.0-quantal-amd64", + agentVersion: "2.8.2", + expectErr: "no compatible tools available", +}, { + about: "no next supported available", + tools: []string{"2.2.0-quantal-amd64", "2.2.5-quantal-i386", "2.3.3-quantal-amd64", "2.1-dev1-quantal-amd64"}, + currentVersion: "2.0.0-quantal-amd64", + agentVersion: "2.0.0", + expectErr: "no more recent supported versions available", +}, { + about: "latest supported stable, when client is dev", + tools: []string{"2.1-dev1-quantal-amd64", "2.1.0-quantal-amd64", "2.3-dev0-quantal-amd64", "3.0.1-quantal-amd64"}, + currentVersion: "2.1-dev0-quantal-amd64", + agentVersion: "2.0.0", + expectVersion: "2.1.0", +}, { + about: "latest current, when agent is dev", + tools: []string{"2.1-dev1-quantal-amd64", "2.2.0-quantal-amd64", "2.3-dev0-quantal-amd64", "3.0.1-quantal-amd64"}, + currentVersion: "2.0.0-quantal-amd64", + agentVersion: "2.1-dev0", + expectVersion: "2.2.0", +}, { + about: "specified version", + tools: []string{"2.3-dev0-quantal-amd64"}, + currentVersion: "2.0.0-quantal-amd64", + agentVersion: "2.0.0", + args: []string{"--version", "2.3-dev0"}, + expectVersion: "2.3-dev0", +}, { + about: "specified major version", + tools: []string{"3.2.0-quantal-amd64"}, + currentVersion: "3.2.0-quantal-amd64", + agentVersion: "2.8.2", + args: []string{"--version", "3.2.0"}, + expectVersion: "3.2.0", +}, { + about: "specified version missing, but already set", + currentVersion: "3.0.0-quantal-amd64", + agentVersion: "3.0.0", + args: []string{"--version", "3.0.0"}, + expectVersion: "3.0.0", +}, { + about: "specified version, no tools", + currentVersion: "3.0.0-quantal-amd64", + agentVersion: "3.0.0", + args: []string{"--version", "3.2.0"}, + expectErr: "no tools available", +}, { + about: "specified version, no matching major version", + tools: []string{"4.2.0-quantal-amd64"}, + currentVersion: "3.0.0-quantal-amd64", + agentVersion: "3.0.0", + args: []string{"--version", "3.2.0"}, + expectErr: "no matching tools available", +}, { + about: "specified version, no matching minor version", + tools: []string{"3.4.0-quantal-amd64"}, + currentVersion: "3.0.0-quantal-amd64", + agentVersion: "3.0.0", + args: []string{"--version", "3.2.0"}, + expectErr: "no matching tools available", +}, { + about: "specified version, no matching patch version", + tools: []string{"3.2.5-quantal-amd64"}, + currentVersion: "3.0.0-quantal-amd64", + agentVersion: "3.0.0", + args: []string{"--version", "3.2.0"}, + expectErr: "no matching tools available", +}, { + about: "specified version, no matching build version", + tools: []string{"3.2.0.2-quantal-amd64"}, + currentVersion: "3.0.0-quantal-amd64", + agentVersion: "3.0.0", + args: []string{"--version", "3.2.0"}, + expectErr: "no matching tools available", +}, { + about: "major version downgrade to incompatible version", + tools: []string{"3.2.0-quantal-amd64"}, + currentVersion: "3.2.0-quantal-amd64", + agentVersion: "4.2.0", + args: []string{"--version", "3.2.0"}, + expectErr: "cannot change version from 4.2.0 to 3.2.0", +}, { + about: "minor version downgrade to incompatible version", + tools: []string{"3.2.0-quantal-amd64"}, + currentVersion: "3.2.0-quantal-amd64", + agentVersion: "3.3-dev0", + args: []string{"--version", "3.2.0"}, + expectErr: "cannot change version from 3.3-dev0 to 3.2.0", +}, { + about: "nothing available", + currentVersion: "2.0.0-quantal-amd64", + agentVersion: "2.0.0", + expectVersion: "2.0.0", +}, { + about: "nothing available 2", + currentVersion: "2.0.0-quantal-amd64", + tools: []string{"3.2.0-quantal-amd64"}, + agentVersion: "2.0.0", + expectVersion: "2.0.0", +}, { + about: "upload with default series", + currentVersion: "2.2.0-quantal-amd64", + agentVersion: "2.0.0", + args: []string{"--upload-tools"}, + expectVersion: "2.2.0.1", + expectUploaded: []string{"2.2.0.1-quantal-amd64", "2.2.0.1-%LTS%-amd64", "2.2.0.1-raring-amd64"}, +}, { + about: "upload with explicit version", + currentVersion: "2.2.0-quantal-amd64", + agentVersion: "2.0.0", + args: []string{"--upload-tools", "--version", "2.7.3"}, + expectVersion: "2.7.3.1", + expectUploaded: []string{"2.7.3.1-quantal-amd64", "2.7.3.1-%LTS%-amd64", "2.7.3.1-raring-amd64"}, +}, { + about: "upload with explicit series", + currentVersion: "2.2.0-quantal-amd64", + agentVersion: "2.0.0", + args: []string{"--upload-tools", "--series", "raring"}, + expectVersion: "2.2.0.1", + expectUploaded: []string{"2.2.0.1-quantal-amd64", "2.2.0.1-raring-amd64"}, +}, { + about: "upload dev version, currently on release version", + currentVersion: "2.1.0-quantal-amd64", + agentVersion: "2.0.0", + args: []string{"--upload-tools"}, + expectVersion: "2.1.0.1", + expectUploaded: []string{"2.1.0.1-quantal-amd64", "2.1.0.1-%LTS%-amd64", "2.1.0.1-raring-amd64"}, +}, { + about: "upload bumps version when necessary", + tools: []string{"2.4.6-quantal-amd64", "2.4.8-quantal-amd64"}, + currentVersion: "2.4.6-quantal-amd64", + agentVersion: "2.4.0", + args: []string{"--upload-tools"}, + expectVersion: "2.4.6.1", + expectUploaded: []string{"2.4.6.1-quantal-amd64", "2.4.6.1-%LTS%-amd64", "2.4.6.1-raring-amd64"}, +}, { + about: "upload re-bumps version when necessary", + tools: []string{"2.4.6-quantal-amd64", "2.4.6.2-saucy-i386", "2.4.8-quantal-amd64"}, + currentVersion: "2.4.6-quantal-amd64", + agentVersion: "2.4.6.2", + args: []string{"--upload-tools"}, + expectVersion: "2.4.6.3", + expectUploaded: []string{"2.4.6.3-quantal-amd64", "2.4.6.3-%LTS%-amd64", "2.4.6.3-raring-amd64"}, +}, { + about: "upload with explicit version bumps when necessary", + currentVersion: "2.2.0-quantal-amd64", + tools: []string{"2.7.3.1-quantal-amd64"}, + agentVersion: "2.0.0", + args: []string{"--upload-tools", "--version", "2.7.3"}, + expectVersion: "2.7.3.2", + expectUploaded: []string{"2.7.3.2-quantal-amd64", "2.7.3.2-%LTS%-amd64", "2.7.3.2-raring-amd64"}, +}, { + about: "latest supported stable release", + tools: []string{"1.21.3-quantal-amd64", "1.22.1-quantal-amd64"}, + currentVersion: "1.22.1-quantal-amd64", + agentVersion: "1.20.14", + expectVersion: "1.21.3", +}} + +func (s *UpgradeJujuSuite) TestUpgradeJuju(c *gc.C) { + oldVersion := version.Current + defer func() { + version.Current = oldVersion + }() + + for i, test := range upgradeJujuTests { + c.Logf("\ntest %d: %s", i, test.about) + s.Reset(c) + tools.DefaultBaseURL = "" + + // Set up apparent CLI version and initialize the command. + version.Current = version.MustParseBinary(test.currentVersion) + com := &UpgradeJujuCommand{} + if err := coretesting.InitCommand(envcmd.Wrap(com), test.args); err != nil { + if test.expectInitErr != "" { + c.Check(err, gc.ErrorMatches, test.expectInitErr) + } else { + c.Check(err, jc.ErrorIsNil) + } + continue + } + + // Set up state and environ, and run the command. + toolsDir := c.MkDir() + updateAttrs := map[string]interface{}{ + "agent-version": test.agentVersion, + "agent-metadata-url": "file://" + toolsDir + "/tools", + } + err := s.State.UpdateEnvironConfig(updateAttrs, nil, nil) + c.Assert(err, jc.ErrorIsNil) + versions := make([]version.Binary, len(test.tools)) + for i, v := range test.tools { + versions[i] = version.MustParseBinary(v) + } + if len(versions) > 0 { + stor, err := filestorage.NewFileStorageWriter(toolsDir) + c.Assert(err, jc.ErrorIsNil) + envtesting.MustUploadFakeToolsVersions(stor, s.Environ.Config().AgentStream(), versions...) + } + + err = com.Run(coretesting.Context(c)) + if test.expectErr != "" { + c.Check(err, gc.ErrorMatches, test.expectErr) + continue + } else if !c.Check(err, jc.ErrorIsNil) { + continue + } + + // Check expected changes to environ/state. + cfg, err := s.State.EnvironConfig() + c.Check(err, jc.ErrorIsNil) + agentVersion, ok := cfg.AgentVersion() + c.Check(ok, jc.IsTrue) + c.Check(agentVersion, gc.Equals, version.MustParse(test.expectVersion)) + + for _, uploaded := range test.expectUploaded { + // Substitute latest LTS for placeholder in expected series for uploaded tools + uploaded = strings.Replace(uploaded, "%LTS%", config.LatestLtsSeries(), 1) + vers := version.MustParseBinary(uploaded) + s.checkToolsUploaded(c, vers, agentVersion) + } + } +} + +func (s *UpgradeJujuSuite) checkToolsUploaded(c *gc.C, vers version.Binary, agentVersion version.Number) { + storage, err := s.State.ToolsStorage() + c.Assert(err, jc.ErrorIsNil) + defer storage.Close() + _, r, err := storage.Tools(vers) + if !c.Check(err, jc.ErrorIsNil) { + return + } + data, err := ioutil.ReadAll(r) + r.Close() + c.Check(err, jc.ErrorIsNil) + expectContent := version.Current + expectContent.Number = agentVersion + checkToolsContent(c, data, "jujud contents "+expectContent.String()) +} + +func checkToolsContent(c *gc.C, data []byte, uploaded string) { + zr, err := gzip.NewReader(bytes.NewReader(data)) + c.Check(err, jc.ErrorIsNil) + defer zr.Close() + tr := tar.NewReader(zr) + found := false + for { + hdr, err := tr.Next() + if err == io.EOF { + break + } + c.Check(err, jc.ErrorIsNil) + if strings.ContainsAny(hdr.Name, "/\\") { + c.Fail() + } + if hdr.Typeflag != tar.TypeReg { + c.Fail() + } + content, err := ioutil.ReadAll(tr) + c.Check(err, jc.ErrorIsNil) + c.Check(string(content), gc.Equals, uploaded) + found = true + } + c.Check(found, jc.IsTrue) +} + +// JujuConnSuite very helpfully uploads some default +// tools to the environment's storage. We don't want +// 'em there; but we do want a consistent default-series +// in the environment state. +func (s *UpgradeJujuSuite) Reset(c *gc.C) { + s.JujuConnSuite.Reset(c) + envtesting.RemoveTools(c, s.DefaultToolsStorage, s.Environ.Config().AgentStream()) + updateAttrs := map[string]interface{}{ + "default-series": "raring", + "agent-version": "1.2.3", + } + err := s.State.UpdateEnvironConfig(updateAttrs, nil, nil) + c.Assert(err, jc.ErrorIsNil) + s.PatchValue(&sync.BuildToolsTarball, toolstesting.GetMockBuildTools(c)) + + // Set API host ports so FindTools works. + hostPorts := [][]network.HostPort{ + network.NewHostPorts(1234, "0.1.2.3"), + } + err = s.State.SetAPIHostPorts(hostPorts) + c.Assert(err, jc.ErrorIsNil) + + s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) + c.Assert(s.CmdBlockHelper, gc.NotNil) + s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) +} + +func (s *UpgradeJujuSuite) TestUpgradeJujuWithRealUpload(c *gc.C) { + s.Reset(c) + cmd := envcmd.Wrap(&UpgradeJujuCommand{}) + _, err := coretesting.RunCommand(c, cmd, "--upload-tools") + c.Assert(err, jc.ErrorIsNil) + vers := version.Current + vers.Build = 1 + s.checkToolsUploaded(c, vers, vers.Number) +} + +func (s *UpgradeJujuSuite) TestBlockUpgradeJujuWithRealUpload(c *gc.C) { + s.Reset(c) + cmd := envcmd.Wrap(&UpgradeJujuCommand{}) + // Block operation + s.BlockAllChanges(c, "TestBlockUpgradeJujuWithRealUpload") + _, err := coretesting.RunCommand(c, cmd, "--upload-tools") + s.AssertBlocked(c, err, ".*TestBlockUpgradeJujuWithRealUpload.*") +} + +type DryRunTest struct { + about string + cmdArgs []string + tools []string + currentVersion string + agentVersion string + expectedCmdOutput string +} + +func (s *UpgradeJujuSuite) TestUpgradeDryRun(c *gc.C) { + tests := []DryRunTest{ + { + about: "dry run outputs and doesn't change anything when uploading tools", + cmdArgs: []string{"--upload-tools", "--dry-run"}, + tools: []string{"2.1.0-quantal-amd64", "2.1.2-quantal-i386", "2.1.3-quantal-amd64", "2.1-dev1-quantal-amd64", "2.2.3-quantal-amd64"}, + currentVersion: "2.0.0-quantal-amd64", + agentVersion: "2.0.0", + expectedCmdOutput: `available tools: + 2.1-dev1-quantal-amd64 + 2.1.0-quantal-amd64 + 2.1.2-quantal-i386 + 2.1.3-quantal-amd64 + 2.2.3-quantal-amd64 +best version: + 2.1.3 +upgrade to this version by running + juju upgrade-juju --version="2.1.3" +`, + }, + { + about: "dry run outputs and doesn't change anything", + cmdArgs: []string{"--dry-run"}, + tools: []string{"2.1.0-quantal-amd64", "2.1.2-quantal-i386", "2.1.3-quantal-amd64", "2.1-dev1-quantal-amd64", "2.2.3-quantal-amd64"}, + currentVersion: "2.0.0-quantal-amd64", + agentVersion: "2.0.0", + expectedCmdOutput: `available tools: + 2.1-dev1-quantal-amd64 + 2.1.0-quantal-amd64 + 2.1.2-quantal-i386 + 2.1.3-quantal-amd64 + 2.2.3-quantal-amd64 +best version: + 2.1.3 +upgrade to this version by running + juju upgrade-juju --version="2.1.3" +`, + }, + { + about: "dry run ignores unknown series", + cmdArgs: []string{"--dry-run"}, + tools: []string{"2.1.0-quantal-amd64", "2.1.2-quantal-i386", "2.1.3-quantal-amd64", "1.2.3-myawesomeseries-amd64"}, + currentVersion: "2.0.0-quantal-amd64", + agentVersion: "2.0.0", + expectedCmdOutput: `available tools: + 2.1.0-quantal-amd64 + 2.1.2-quantal-i386 + 2.1.3-quantal-amd64 +best version: + 2.1.3 +upgrade to this version by running + juju upgrade-juju --version="2.1.3" +`, + }, + } + + for i, test := range tests { + c.Logf("\ntest %d: %s", i, test.about) + s.Reset(c) + tools.DefaultBaseURL = "" + + s.PatchValue(&version.Current, version.MustParseBinary(test.currentVersion)) + com := &UpgradeJujuCommand{} + err := coretesting.InitCommand(envcmd.Wrap(com), test.cmdArgs) + c.Assert(err, jc.ErrorIsNil) + toolsDir := c.MkDir() + updateAttrs := map[string]interface{}{ + "agent-version": test.agentVersion, + "agent-metadata-url": "file://" + toolsDir + "/tools", + } + + err = s.State.UpdateEnvironConfig(updateAttrs, nil, nil) + c.Assert(err, jc.ErrorIsNil) + versions := make([]version.Binary, len(test.tools)) + for i, v := range test.tools { + versions[i], err = version.ParseBinary(v) + if err != nil { + c.Assert(err, jc.Satisfies, version.IsUnknownOSForSeriesError) + } + } + if len(versions) > 0 { + stor, err := filestorage.NewFileStorageWriter(toolsDir) + c.Assert(err, jc.ErrorIsNil) + envtesting.MustUploadFakeToolsVersions(stor, s.Environ.Config().AgentStream(), versions...) + } + + ctx := coretesting.Context(c) + err = com.Run(ctx) + c.Assert(err, jc.ErrorIsNil) + + // Check agent version doesn't change + cfg, err := s.State.EnvironConfig() + c.Assert(err, jc.ErrorIsNil) + agentVer, ok := cfg.AgentVersion() + c.Assert(ok, jc.IsTrue) + c.Assert(agentVer, gc.Equals, version.MustParse(test.agentVersion)) + output := coretesting.Stderr(ctx) + c.Assert(output, gc.Equals, test.expectedCmdOutput) + } +} + +func (s *UpgradeJujuSuite) TestUpgradeUnknownSeriesInStreams(c *gc.C) { + fakeAPI := NewFakeUpgradeJujuAPI(c, s.State) + fakeAPI.addTools("2.1.0-weird-amd64") + fakeAPI.patch(s) + + cmd := &UpgradeJujuCommand{} + err := coretesting.InitCommand(envcmd.Wrap(cmd), []string{}) + c.Assert(err, jc.ErrorIsNil) + + err = cmd.Run(coretesting.Context(c)) + c.Assert(err, gc.IsNil) + + // ensure find tools was called + c.Assert(fakeAPI.findToolsCalled, jc.IsTrue) + c.Assert(fakeAPI.tools, gc.DeepEquals, []string{"2.1.0-weird-amd64", fakeAPI.nextVersion.String()}) +} + +func (s *UpgradeJujuSuite) TestUpgradeInProgress(c *gc.C) { + fakeAPI := NewFakeUpgradeJujuAPI(c, s.State) + fakeAPI.setVersionErr = ¶ms.Error{ + Message: "a message from the server about the problem", + Code: params.CodeUpgradeInProgress, + } + fakeAPI.patch(s) + cmd := &UpgradeJujuCommand{} + err := coretesting.InitCommand(envcmd.Wrap(cmd), []string{}) + c.Assert(err, jc.ErrorIsNil) + + err = cmd.Run(coretesting.Context(c)) + c.Assert(err, gc.ErrorMatches, "a message from the server about the problem\n"+ + "\n"+ + "Please wait for the upgrade to complete or if there was a problem with\n"+ + "the last upgrade that has been resolved, consider running the\n"+ + "upgrade-juju command with the --reset-previous-upgrade flag.", + ) +} + +func (s *UpgradeJujuSuite) TestBlockUpgradeInProgress(c *gc.C) { + fakeAPI := NewFakeUpgradeJujuAPI(c, s.State) + fakeAPI.setVersionErr = common.ErrOperationBlocked("The operation has been blocked.") + fakeAPI.patch(s) + cmd := &UpgradeJujuCommand{} + err := coretesting.InitCommand(envcmd.Wrap(cmd), []string{}) + c.Assert(err, jc.ErrorIsNil) + + // Block operation + s.BlockAllChanges(c, "TestBlockUpgradeInProgress") + err = cmd.Run(coretesting.Context(c)) + s.AssertBlocked(c, err, ".*To unblock changes.*") +} + +func (s *UpgradeJujuSuite) TestResetPreviousUpgrade(c *gc.C) { + fakeAPI := NewFakeUpgradeJujuAPI(c, s.State) + fakeAPI.patch(s) + + ctx := coretesting.Context(c) + var stdin bytes.Buffer + ctx.Stdin = &stdin + + run := func(answer string, expect bool, args ...string) { + stdin.Reset() + if answer != "" { + stdin.WriteString(answer) + } + + fakeAPI.reset() + + cmd := &UpgradeJujuCommand{} + err := coretesting.InitCommand(envcmd.Wrap(cmd), + append([]string{"--reset-previous-upgrade"}, args...)) + c.Assert(err, jc.ErrorIsNil) + err = cmd.Run(ctx) + if expect { + c.Assert(err, jc.ErrorIsNil) + } else { + c.Assert(err, gc.ErrorMatches, "previous upgrade not reset and no new upgrade triggered") + } + + c.Assert(fakeAPI.abortCurrentUpgradeCalled, gc.Equals, expect) + expectedVersion := version.Number{} + if expect { + expectedVersion = fakeAPI.nextVersion.Number + } + c.Assert(fakeAPI.setVersionCalledWith, gc.Equals, expectedVersion) + } + + const expectUpgrade = true + const expectNoUpgrade = false + + // EOF on stdin - equivalent to answering no. + run("", expectNoUpgrade) + + // -y on command line - no confirmation required + run("", expectUpgrade, "-y") + + // --yes on command line - no confirmation required + run("", expectUpgrade, "--yes") + + // various ways of saying "yes" to the prompt + for _, answer := range []string{"y", "Y", "yes", "YES"} { + run(answer, expectUpgrade) + } + + // various ways of saying "no" to the prompt + for _, answer := range []string{"n", "N", "no", "foo"} { + run(answer, expectNoUpgrade) + } +} + +func NewFakeUpgradeJujuAPI(c *gc.C, st *state.State) *fakeUpgradeJujuAPI { + nextVersion := version.Current + nextVersion.Minor++ + return &fakeUpgradeJujuAPI{ + c: c, + st: st, + nextVersion: nextVersion, + } +} + +type fakeUpgradeJujuAPI struct { + c *gc.C + st *state.State + nextVersion version.Binary + setVersionErr error + abortCurrentUpgradeCalled bool + setVersionCalledWith version.Number + tools []string + findToolsCalled bool +} + +func (a *fakeUpgradeJujuAPI) reset() { + a.setVersionErr = nil + a.abortCurrentUpgradeCalled = false + a.setVersionCalledWith = version.Number{} + a.tools = []string{} + a.findToolsCalled = false +} + +func (a *fakeUpgradeJujuAPI) patch(s *UpgradeJujuSuite) { + s.PatchValue(&getUpgradeJujuAPI, func(*UpgradeJujuCommand) (upgradeJujuAPI, error) { + return a, nil + }) +} + +func (a *fakeUpgradeJujuAPI) addTools(tools ...string) { + for _, tool := range tools { + a.tools = append(a.tools, tool) + } +} + +func (a *fakeUpgradeJujuAPI) EnvironmentGet() (map[string]interface{}, error) { + config, err := a.st.EnvironConfig() + if err != nil { + return make(map[string]interface{}), err + } + return config.AllAttrs(), nil +} + +func (a *fakeUpgradeJujuAPI) FindTools(majorVersion, minorVersion int, series, arch string) ( + result params.FindToolsResult, err error, +) { + a.findToolsCalled = true + a.tools = append(a.tools, a.nextVersion.String()) + tools := toolstesting.MakeTools(a.c, a.c.MkDir(), "released", a.tools) + return params.FindToolsResult{ + List: tools, + Error: nil, + }, nil +} + +func (a *fakeUpgradeJujuAPI) UploadTools(r io.Reader, vers version.Binary, additionalSeries ...string) ( + *coretools.Tools, error, +) { + panic("not implemented") +} + +func (a *fakeUpgradeJujuAPI) AbortCurrentUpgrade() error { + a.abortCurrentUpgradeCalled = true + return nil +} + +func (a *fakeUpgradeJujuAPI) SetEnvironAgentVersion(v version.Number) error { + a.setVersionCalledWith = v + return a.setVersionErr +} + +func (a *fakeUpgradeJujuAPI) Close() error { + return nil +} === removed file 'src/github.com/juju/juju/cmd/juju/common.go' --- src/github.com/juju/juju/cmd/juju/common.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/common.go 1970-01-01 00:00:00 +0000 @@ -1,277 +0,0 @@ -// Copyright 2014 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - "net/http" - "path" - "time" - - "github.com/juju/cmd" - "github.com/juju/errors" - "github.com/juju/persistent-cookiejar" - "github.com/juju/utils" - "golang.org/x/net/publicsuffix" - "gopkg.in/juju/charm.v5" - "gopkg.in/juju/charm.v5/charmrepo" - "gopkg.in/juju/charmstore.v4/csclient" - "gopkg.in/macaroon-bakery.v0/httpbakery" - "gopkg.in/macaroon.v1" - - "github.com/juju/juju/api" - "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/environs" - "github.com/juju/juju/environs/config" - "github.com/juju/juju/environs/configstore" -) - -// destroyPreparedEnviron destroys the environment and logs an error -// if it fails. -var destroyPreparedEnviron = destroyPreparedEnvironProductionFunc - -func destroyPreparedEnvironProductionFunc( - ctx *cmd.Context, - env environs.Environ, - store configstore.Storage, - action string, -) { - ctx.Infof("%s failed, destroying environment", action) - if err := environs.Destroy(env, store); err != nil { - logger.Errorf("the environment could not be destroyed: %v", err) - } -} - -var destroyEnvInfo = destroyEnvInfoProductionFunc - -func destroyEnvInfoProductionFunc( - ctx *cmd.Context, - cfgName string, - store configstore.Storage, - action string, -) { - ctx.Infof("%s failed, cleaning up the environment.", action) - if err := environs.DestroyInfo(cfgName, store); err != nil { - logger.Errorf("the environment jenv file could not be cleaned up: %v", err) - } -} - -// environFromName loads an existing environment or prepares a new -// one. If there are no errors, it returns the environ and a closure to -// clean up in case we need to further up the stack. If an error has -// occurred, the environment and cleanup function will be nil, and the -// error will be filled in. -var environFromName = environFromNameProductionFunc - -func environFromNameProductionFunc( - ctx *cmd.Context, - envName string, - action string, - ensureNotBootstrapped func(environs.Environ) error, -) (env environs.Environ, cleanup func(), err error) { - - store, err := configstore.Default() - if err != nil { - return nil, nil, err - } - - envExisted := false - if environInfo, err := store.ReadInfo(envName); err == nil { - envExisted = true - logger.Warningf( - "ignoring environments.yaml: using bootstrap config in %s", - environInfo.Location(), - ) - } else if !errors.IsNotFound(err) { - return nil, nil, err - } - - cleanup = func() { - // Distinguish b/t removing the jenv file or tearing down the - // environment. We want to remove the jenv file if preparation - // was not successful. We want to tear down the environment - // only in the case where the environment didn't already - // exist. - if env == nil { - logger.Debugf("Destroying environment info.") - destroyEnvInfo(ctx, envName, store, action) - } else if !envExisted && ensureNotBootstrapped(env) != environs.ErrAlreadyBootstrapped { - logger.Debugf("Destroying environment.") - destroyPreparedEnviron(ctx, env, store, action) - } - } - - if env, err = environs.PrepareFromName(envName, envcmd.BootstrapContext(ctx), store); err != nil { - return nil, cleanup, err - } - - return env, cleanup, err -} - -// resolveCharmURL resolves the given charm URL string -// by looking it up in the appropriate charm repository. -// If it is a charm store charm URL, the given csParams will -// be used to access the charm store repository. -// If it is a local charm URL, the local charm repository at -// the given repoPath will be used. The given configuration -// will be used to add any necessary attributes to the repo -// and to resolve the default series if possible. -// -// resolveCharmURL also returns the charm repository holding -// the charm. -func resolveCharmURL(curlStr string, csParams charmrepo.NewCharmStoreParams, repoPath string, conf *config.Config) (*charm.URL, charmrepo.Interface, error) { - ref, err := charm.ParseReference(curlStr) - if err != nil { - return nil, nil, errors.Trace(err) - } - repo, err := charmrepo.InferRepository(ref, csParams, repoPath) - if err != nil { - return nil, nil, errors.Trace(err) - } - repo = config.SpecializeCharmRepo(repo, conf) - if ref.Series == "" { - if defaultSeries, ok := conf.DefaultSeries(); ok { - ref.Series = defaultSeries - } - } - if ref.Schema == "local" && ref.Series == "" { - possibleURL := *ref - possibleURL.Series = "trusty" - logger.Errorf("The series is not specified in the environment (default-series) or with the charm. Did you mean:\n\t%s", &possibleURL) - return nil, nil, errors.Errorf("cannot resolve series for charm: %q", ref) - } - if ref.Series != "" && ref.Revision != -1 { - // The URL is already fully resolved; do not - // bother with an unnecessary round-trip to the - // charm store. - curl, err := ref.URL("") - if err != nil { - panic(err) - } - return curl, repo, nil - } - curl, err := repo.Resolve(ref) - if err != nil { - return nil, nil, errors.Trace(err) - } - return curl, repo, nil -} - -// addCharmViaAPI calls the appropriate client API calls to add the -// given charm URL to state. For non-public charm URLs, this function also -// handles the macaroon authorization process using the given csClient. -// The resulting charm URL of the added charm is displayed on stdout. -func addCharmViaAPI(client *api.Client, ctx *cmd.Context, curl *charm.URL, repo charmrepo.Interface, csclient *csClient) (*charm.URL, error) { - switch curl.Schema { - case "local": - ch, err := repo.Get(curl) - if err != nil { - return nil, err - } - stateCurl, err := client.AddLocalCharm(curl, ch) - if err != nil { - return nil, err - } - curl = stateCurl - case "cs": - if err := client.AddCharm(curl); err != nil { - if !params.IsCodeUnauthorized(err) { - return nil, errors.Mask(err) - } - m, err := csclient.authorize(curl) - if err != nil { - return nil, errors.Mask(err) - } - if err := client.AddCharmWithAuthorization(curl, m); err != nil { - return nil, errors.Mask(err) - } - } - default: - return nil, fmt.Errorf("unsupported charm URL schema: %q", curl.Schema) - } - ctx.Infof("Added charm %q to the environment.", curl) - return curl, nil -} - -// csClient gives access to the charm store server and provides parameters -// for connecting to the charm store. -type csClient struct { - jar *cookiejar.Jar - params charmrepo.NewCharmStoreParams -} - -// newCharmStoreClient is called to obtain a charm store client -// including the parameters for connecting to the charm store, and -// helpers to save the local authorization cookies and to authorize -// non-public charm deployments. It is defined as a variable so it can -// be changed for testing purposes. -var newCharmStoreClient = func() (*csClient, error) { - jar, client, err := newHTTPClient() - if err != nil { - return nil, errors.Mask(err) - } - return &csClient{ - jar: jar, - params: charmrepo.NewCharmStoreParams{ - HTTPClient: client, - VisitWebPage: httpbakery.OpenWebBrowser, - }, - }, nil -} - -func newHTTPClient() (*cookiejar.Jar, *http.Client, error) { - cookieFile := path.Join(utils.Home(), ".go-cookies") - jar, err := cookiejar.New(&cookiejar.Options{ - PublicSuffixList: publicsuffix.List, - }) - if err != nil { - panic(err) - } - if err := jar.Load(cookieFile); err != nil { - return nil, nil, err - } - client := httpbakery.NewHTTPClient() - client.Jar = jar - return jar, client, nil -} - -// authorize acquires and return the charm store delegatable macaroon to be -// used to add the charm corresponding to the given URL. -// The macaroon is properly attenuated so that it can only be used to deploy -// the given charm URL. -func (c *csClient) authorize(curl *charm.URL) (*macaroon.Macaroon, error) { - client := csclient.New(csclient.Params{ - URL: c.params.URL, - HTTPClient: c.params.HTTPClient, - VisitWebPage: c.params.VisitWebPage, - }) - var m *macaroon.Macaroon - if err := client.Get("/delegatable-macaroon", &m); err != nil { - return nil, errors.Trace(err) - } - if err := m.AddFirstPartyCaveat("is-entity " + curl.String()); err != nil { - return nil, errors.Trace(err) - } - return m, nil -} - -// formatStatusTime returns a string with the local time -// formatted in an arbitrary format used for status or -// and localized tz or in utc timezone and format RFC3339 -// if u is specified. -func formatStatusTime(t *time.Time, formatISO bool) string { - if formatISO { - // If requested, use ISO time format. - // The format we use is RFC3339 without the "T". From the spec: - // NOTE: ISO 8601 defines date and time separated by "T". - // Applications using this syntax may choose, for the sake of - // readability, to specify a full-date and full-time separated by - // (say) a space character. - return t.UTC().Format("2006-01-02 15:04:05Z") - } else { - // Otherwise use local time. - return t.Local().Format("02 Jan 2006 15:04:05Z07:00") - } -} === added file 'src/github.com/juju/juju/cmd/juju/common/format.go' --- src/github.com/juju/juju/cmd/juju/common/format.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/common/format.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,72 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package common + +import ( + "time" + + "github.com/juju/errors" +) + +// FormatTime returns a string with the local time formatted +// in an arbitrary format used for status or and localized tz +// or in UTC timezone and format RFC3339 if u is specified. +func FormatTime(t *time.Time, formatISO bool) string { + if formatISO { + // If requested, use ISO time format. + // The format we use is RFC3339 without the "T". From the spec: + // NOTE: ISO 8601 defines date and time separated by "T". + // Applications using this syntax may choose, for the sake of + // readability, to specify a full-date and full-time separated by + // (say) a space character. + return t.UTC().Format("2006-01-02 15:04:05Z") + } + // Otherwise use local time. + return t.Local().Format("02 Jan 2006 15:04:05Z07:00") +} + +// ConformYAML ensures all keys of any nested maps are strings. This is +// necessary because YAML unmarshals map[interface{}]interface{} in nested +// maps, which cannot be serialized by bson. Also, handle []interface{}. +// cf. gopkg.in/juju/charm.v4/actions.go cleanse +func ConformYAML(input interface{}) (interface{}, error) { + switch typedInput := input.(type) { + + case map[string]interface{}: + newMap := make(map[string]interface{}) + for key, value := range typedInput { + newValue, err := ConformYAML(value) + if err != nil { + return nil, err + } + newMap[key] = newValue + } + return newMap, nil + + case map[interface{}]interface{}: + newMap := make(map[string]interface{}) + for key, value := range typedInput { + typedKey, ok := key.(string) + if !ok { + return nil, errors.New("map keyed with non-string value") + } + newMap[typedKey] = value + } + return ConformYAML(newMap) + + case []interface{}: + newSlice := make([]interface{}, len(typedInput)) + for i, sliceValue := range typedInput { + newSliceValue, err := ConformYAML(sliceValue) + if err != nil { + return nil, errors.New("map keyed with non-string value") + } + newSlice[i] = newSliceValue + } + return newSlice, nil + + default: + return input, nil + } +} === added file 'src/github.com/juju/juju/cmd/juju/common/format_test.go' --- src/github.com/juju/juju/cmd/juju/common/format_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/common/format_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,116 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package common_test + +import ( + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/juju/common" +) + +type ConformSuite struct{} + +var _ = gc.Suite(&ConformSuite{}) + +func (s *ConformSuite) TestConformYAML(c *gc.C) { + var goodInterfaceTests = []struct { + description string + inputInterface interface{} + expectedInterface map[string]interface{} + expectedError string + }{{ + description: "An interface requiring no changes.", + inputInterface: map[string]interface{}{ + "key1": "value1", + "key2": "value2", + "key3": map[string]interface{}{ + "foo1": "val1", + "foo2": "val2"}}, + expectedInterface: map[string]interface{}{ + "key1": "value1", + "key2": "value2", + "key3": map[string]interface{}{ + "foo1": "val1", + "foo2": "val2"}}, + }, { + description: "Substitute a single inner map[i]i.", + inputInterface: map[string]interface{}{ + "key1": "value1", + "key2": "value2", + "key3": map[interface{}]interface{}{ + "foo1": "val1", + "foo2": "val2"}}, + expectedInterface: map[string]interface{}{ + "key1": "value1", + "key2": "value2", + "key3": map[string]interface{}{ + "foo1": "val1", + "foo2": "val2"}}, + }, { + description: "Substitute nested inner map[i]i.", + inputInterface: map[string]interface{}{ + "key1a": "val1a", + "key2a": "val2a", + "key3a": map[interface{}]interface{}{ + "key1b": "val1b", + "key2b": map[interface{}]interface{}{ + "key1c": "val1c"}}}, + expectedInterface: map[string]interface{}{ + "key1a": "val1a", + "key2a": "val2a", + "key3a": map[string]interface{}{ + "key1b": "val1b", + "key2b": map[string]interface{}{ + "key1c": "val1c"}}}, + }, { + description: "Substitute nested map[i]i within []i.", + inputInterface: map[string]interface{}{ + "key1a": "val1a", + "key2a": []interface{}{5, "foo", map[string]interface{}{ + "key1b": "val1b", + "key2b": map[interface{}]interface{}{ + "key1c": "val1c"}}}}, + expectedInterface: map[string]interface{}{ + "key1a": "val1a", + "key2a": []interface{}{5, "foo", map[string]interface{}{ + "key1b": "val1b", + "key2b": map[string]interface{}{ + "key1c": "val1c"}}}}, + }, { + description: "An inner map[interface{}]interface{} with an int key.", + inputInterface: map[string]interface{}{ + "key1": "value1", + "key2": "value2", + "key3": map[interface{}]interface{}{ + "foo1": "val1", + 5: "val2"}}, + expectedError: "map keyed with non-string value", + }, { + description: "An inner []interface{} containing a map[i]i with an int key.", + inputInterface: map[string]interface{}{ + "key1a": "val1b", + "key2a": "val2b", + "key3a": []interface{}{"foo1", 5, map[interface{}]interface{}{ + "key1b": "val1b", + "key2b": map[interface{}]interface{}{ + "key1c": "val1c", + 5: "val2c"}}}}, + expectedError: "map keyed with non-string value", + }} + + for i, test := range goodInterfaceTests { + c.Logf("test %d: %s", i, test.description) + input := test.inputInterface + cleansedInterfaceMap, err := common.ConformYAML(input) + if test.expectedError == "" { + if !c.Check(err, jc.ErrorIsNil) { + continue + } + c.Check(cleansedInterfaceMap, gc.DeepEquals, test.expectedInterface) + } else { + c.Check(err, gc.ErrorMatches, test.expectedError) + } + } +} === added file 'src/github.com/juju/juju/cmd/juju/common/naturalsort.go' --- src/github.com/juju/juju/cmd/juju/common/naturalsort.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/common/naturalsort.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,57 @@ +// 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 +} === added file 'src/github.com/juju/juju/cmd/juju/common/naturalsort_test.go' --- src/github.com/juju/juju/cmd/juju/common/naturalsort_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/common/naturalsort_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,113 @@ +// 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) +} === removed file 'src/github.com/juju/juju/cmd/juju/debughooks.go' --- src/github.com/juju/juju/cmd/juju/debughooks.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/debughooks.go 1970-01-01 00:00:00 +0000 @@ -1,114 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "encoding/base64" - "fmt" - "sort" - - "github.com/juju/cmd" - "github.com/juju/names" - "gopkg.in/juju/charm.v5/hooks" - - unitdebug "github.com/juju/juju/worker/uniter/runner/debug" -) - -// DebugHooksCommand is responsible for launching a ssh shell on a given unit or machine. -type DebugHooksCommand struct { - SSHCommand - hooks []string -} - -const debugHooksDoc = ` -Interactively debug a hook remotely on a service unit. -` - -func (c *DebugHooksCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "debug-hooks", - Args: " [hook names]", - Purpose: "launch a tmux session to debug a hook", - Doc: debugHooksDoc, - } -} - -func (c *DebugHooksCommand) Init(args []string) error { - if len(args) < 1 { - return fmt.Errorf("no unit name specified") - } - c.Target = args[0] - if !names.IsValidUnit(c.Target) { - return fmt.Errorf("%q is not a valid unit name", c.Target) - } - - // If any of the hooks is "*", then debug all hooks. - c.hooks = append([]string{}, args[1:]...) - for _, h := range c.hooks { - if h == "*" { - c.hooks = nil - break - } - } - return nil -} - -func (c *DebugHooksCommand) validateHooks() error { - if len(c.hooks) == 0 { - return nil - } - service, err := names.UnitService(c.Target) - if err != nil { - return err - } - relations, err := c.apiClient.ServiceCharmRelations(service) - if err != nil { - return err - } - - validHooks := make(map[string]bool) - for _, hook := range hooks.UnitHooks() { - validHooks[string(hook)] = true - } - for _, relation := range relations { - for _, hook := range hooks.RelationHooks() { - hook := fmt.Sprintf("%s-%s", relation, hook) - validHooks[hook] = true - } - } - for _, hook := range c.hooks { - if !validHooks[hook] { - names := make([]string, 0, len(validHooks)) - for hookName := range validHooks { - names = append(names, hookName) - } - sort.Strings(names) - logger.Infof("unknown hook %s, valid hook names: %v", hook, names) - return fmt.Errorf("unit %q does not contain hook %q", c.Target, hook) - } - } - return nil -} - -// Run ensures c.Target is a unit, and resolves its address, -// and connects to it via SSH to execute the debug-hooks -// script. -func (c *DebugHooksCommand) Run(ctx *cmd.Context) error { - var err error - c.apiClient, err = c.initAPIClient() - if err != nil { - return err - } - defer c.apiClient.Close() - err = c.validateHooks() - if err != nil { - return err - } - debugctx := unitdebug.NewHooksContext(c.Target) - script := base64.StdEncoding.EncodeToString([]byte(unitdebug.ClientScript(debugctx, c.hooks))) - innercmd := fmt.Sprintf(`F=$(mktemp); echo %s | base64 -d > $F; . $F`, script) - args := []string{fmt.Sprintf("sudo /bin/bash -c '%s'", innercmd)} - c.Args = args - return c.SSHCommand.Run(ctx) -} === removed file 'src/github.com/juju/juju/cmd/juju/debughooks_test.go' --- src/github.com/juju/juju/cmd/juju/debughooks_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/debughooks_test.go 1970-01-01 00:00:00 +0000 @@ -1,102 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "regexp" - "runtime" - - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/cmd/envcmd" - coretesting "github.com/juju/juju/testing" -) - -var _ = gc.Suite(&DebugHooksSuite{}) - -type DebugHooksSuite struct { - SSHCommonSuite -} - -const debugHooksArgs = sshArgs -const debugHooksArgsNoProxy = sshArgsNoProxy - -var debugHooksTests = []struct { - info string - args []string - error string - proxy bool - result string -}{{ - args: []string{"mysql/0"}, - result: regexp.QuoteMeta(debugHooksArgsNoProxy + "ubuntu@dummyenv-0.dns sudo /bin/bash -c 'F=$(mktemp); echo IyEvYmluL2Jhc2gKKAojIExvY2sgdGhlIGp1anUtPHVuaXQ+LWRlYnVnIGxvY2tmaWxlLgpmbG9jayAtbiA4IHx8IChlY2hvICJGYWlsZWQgdG8gYWNxdWlyZSAvdG1wL2p1anUtdW5pdC1teXNxbC0wLWRlYnVnLWhvb2tzOiB1bml0IGlzIGFscmVhZHkgYmVpbmcgZGVidWdnZWQiIDI+JjE7IGV4aXQgMSkKKAojIENsb3NlIHRoZSBpbmhlcml0ZWQgbG9jayBGRCwgb3IgdG11eCB3aWxsIGtlZXAgaXQgb3Blbi4KZXhlYyA4PiYtCgojIFdyaXRlIG91dCB0aGUgZGVidWctaG9va3MgYXJncy4KZWNobyAiZTMwSyIgfCBiYXNlNjQgLWQgPiAvdG1wL2p1anUtdW5pdC1teXNxbC0wLWRlYnVnLWhvb2tzCgojIExvY2sgdGhlIGp1anUtPHVuaXQ+LWRlYnVnLWV4aXQgbG9ja2ZpbGUuCmZsb2NrIC1uIDkgfHwgZXhpdCAxCgojIFdhaXQgZm9yIHRtdXggdG8gYmUgaW5zdGFsbGVkLgp3aGlsZSBbICEgLWYgL3Vzci9iaW4vdG11eCBdOyBkbwogICAgc2xlZXAgMQpkb25lCgppZiBbICEgLWYgfi8udG11eC5jb25mIF07IHRoZW4KICAgICAgICBpZiBbIC1mIC91c3Ivc2hhcmUvYnlvYnUvcHJvZmlsZXMvdG11eCBdOyB0aGVuCiAgICAgICAgICAgICAgICAjIFVzZSBieW9idS90bXV4IHByb2ZpbGUgZm9yIGZhbWlsaWFyIGtleWJpbmRpbmdzIGFuZCBicmFuZGluZwogICAgICAgICAgICAgICAgZWNobyAic291cmNlLWZpbGUgL3Vzci9zaGFyZS9ieW9idS9wcm9maWxlcy90bXV4IiA+IH4vLnRtdXguY29uZgogICAgICAgIGVsc2UKICAgICAgICAgICAgICAgICMgT3RoZXJ3aXNlLCB1c2UgdGhlIGxlZ2FjeSBqdWp1L3RtdXggY29uZmlndXJhdGlvbgogICAgICAgICAgICAgICAgY2F0ID4gfi8udG11eC5jb25mIDw8RU5ECiAgICAgICAgICAgICAgICAKIyBTdGF0dXMgYmFyCnNldC1vcHRpb24gLWcgc3RhdHVzLWJnIGJsYWNrCnNldC1vcHRpb24gLWcgc3RhdHVzLWZnIHdoaXRlCgpzZXQtd2luZG93LW9wdGlvbiAtZyB3aW5kb3ctc3RhdHVzLWN1cnJlbnQtYmcgcmVkCnNldC13aW5kb3ctb3B0aW9uIC1nIHdpbmRvdy1zdGF0dXMtY3VycmVudC1hdHRyIGJyaWdodAoKc2V0LW9wdGlvbiAtZyBzdGF0dXMtcmlnaHQgJycKCiMgUGFuZXMKc2V0LW9wdGlvbiAtZyBwYW5lLWJvcmRlci1mZyB3aGl0ZQpzZXQtb3B0aW9uIC1nIHBhbmUtYWN0aXZlLWJvcmRlci1mZyB3aGl0ZQoKIyBNb25pdG9yIGFjdGl2aXR5IG9uIHdpbmRvd3MKc2V0LXdpbmRvdy1vcHRpb24gLWcgbW9uaXRvci1hY3Rpdml0eSBvbgoKIyBTY3JlZW4gYmluZGluZ3MsIHNpbmNlIHBlb3BsZSBhcmUgbW9yZSBmYW1pbGlhciB3aXRoIHRoYXQuCnNldC1vcHRpb24gLWcgcHJlZml4IEMtYQpiaW5kIEMtYSBsYXN0LXdpbmRvdwpiaW5kIGEgc2VuZC1rZXkgQy1hCgpiaW5kIHwgc3BsaXQtd2luZG93IC1oCmJpbmQgLSBzcGxpdC13aW5kb3cgLXYKCiMgRml4IENUUkwtUEdVUC9QR0RPV04gZm9yIHZpbQpzZXQtd2luZG93LW9wdGlvbiAtZyB4dGVybS1rZXlzIG9uCgojIFByZXZlbnQgRVNDIGtleSBmcm9tIGFkZGluZyBkZWxheSBhbmQgYnJlYWtpbmcgVmltJ3MgRVNDID4gYXJyb3cga2V5CnNldC1vcHRpb24gLXMgZXNjYXBlLXRpbWUgMAoKRU5ECiAgICAgICAgZmkKZmkKCigKICAgICMgQ2xvc2UgdGhlIGluaGVyaXRlZCBsb2NrIEZELCBvciB0bXV4IHdpbGwga2VlcCBpdCBvcGVuLgogICAgZXhlYyA5PiYtCiAgICBleGVjIHRtdXggbmV3LXNlc3Npb24gLXMgbXlzcWwvMAopCikgOT4vdG1wL2p1anUtdW5pdC1teXNxbC0wLWRlYnVnLWhvb2tzLWV4aXQKKSA4Pi90bXAvanVqdS11bml0LW15c3FsLTAtZGVidWctaG9va3MKZXhpdCAkPwo= | base64 -d > $F; . $F'\n"), -}, { - args: []string{"mongodb/1"}, - result: regexp.QuoteMeta(debugHooksArgsNoProxy + "ubuntu@dummyenv-2.dns sudo /bin/bash -c 'F=$(mktemp); echo IyEvYmluL2Jhc2gKKAojIExvY2sgdGhlIGp1anUtPHVuaXQ+LWRlYnVnIGxvY2tmaWxlLgpmbG9jayAtbiA4IHx8IChlY2hvICJGYWlsZWQgdG8gYWNxdWlyZSAvdG1wL2p1anUtdW5pdC1tb25nb2RiLTEtZGVidWctaG9va3M6IHVuaXQgaXMgYWxyZWFkeSBiZWluZyBkZWJ1Z2dlZCIgMj4mMTsgZXhpdCAxKQooCiMgQ2xvc2UgdGhlIGluaGVyaXRlZCBsb2NrIEZELCBvciB0bXV4IHdpbGwga2VlcCBpdCBvcGVuLgpleGVjIDg+Ji0KCiMgV3JpdGUgb3V0IHRoZSBkZWJ1Zy1ob29rcyBhcmdzLgplY2hvICJlMzBLIiB8IGJhc2U2NCAtZCA+IC90bXAvanVqdS11bml0LW1vbmdvZGItMS1kZWJ1Zy1ob29rcwoKIyBMb2NrIHRoZSBqdWp1LTx1bml0Pi1kZWJ1Zy1leGl0IGxvY2tmaWxlLgpmbG9jayAtbiA5IHx8IGV4aXQgMQoKIyBXYWl0IGZvciB0bXV4IHRvIGJlIGluc3RhbGxlZC4Kd2hpbGUgWyAhIC1mIC91c3IvYmluL3RtdXggXTsgZG8KICAgIHNsZWVwIDEKZG9uZQoKaWYgWyAhIC1mIH4vLnRtdXguY29uZiBdOyB0aGVuCiAgICAgICAgaWYgWyAtZiAvdXNyL3NoYXJlL2J5b2J1L3Byb2ZpbGVzL3RtdXggXTsgdGhlbgogICAgICAgICAgICAgICAgIyBVc2UgYnlvYnUvdG11eCBwcm9maWxlIGZvciBmYW1pbGlhciBrZXliaW5kaW5ncyBhbmQgYnJhbmRpbmcKICAgICAgICAgICAgICAgIGVjaG8gInNvdXJjZS1maWxlIC91c3Ivc2hhcmUvYnlvYnUvcHJvZmlsZXMvdG11eCIgPiB+Ly50bXV4LmNvbmYKICAgICAgICBlbHNlCiAgICAgICAgICAgICAgICAjIE90aGVyd2lzZSwgdXNlIHRoZSBsZWdhY3kganVqdS90bXV4IGNvbmZpZ3VyYXRpb24KICAgICAgICAgICAgICAgIGNhdCA+IH4vLnRtdXguY29uZiA8PEVORAogICAgICAgICAgICAgICAgCiMgU3RhdHVzIGJhcgpzZXQtb3B0aW9uIC1nIHN0YXR1cy1iZyBibGFjawpzZXQtb3B0aW9uIC1nIHN0YXR1cy1mZyB3aGl0ZQoKc2V0LXdpbmRvdy1vcHRpb24gLWcgd2luZG93LXN0YXR1cy1jdXJyZW50LWJnIHJlZApzZXQtd2luZG93LW9wdGlvbiAtZyB3aW5kb3ctc3RhdHVzLWN1cnJlbnQtYXR0ciBicmlnaHQKCnNldC1vcHRpb24gLWcgc3RhdHVzLXJpZ2h0ICcnCgojIFBhbmVzCnNldC1vcHRpb24gLWcgcGFuZS1ib3JkZXItZmcgd2hpdGUKc2V0LW9wdGlvbiAtZyBwYW5lLWFjdGl2ZS1ib3JkZXItZmcgd2hpdGUKCiMgTW9uaXRvciBhY3Rpdml0eSBvbiB3aW5kb3dzCnNldC13aW5kb3ctb3B0aW9uIC1nIG1vbml0b3ItYWN0aXZpdHkgb24KCiMgU2NyZWVuIGJpbmRpbmdzLCBzaW5jZSBwZW9wbGUgYXJlIG1vcmUgZmFtaWxpYXIgd2l0aCB0aGF0LgpzZXQtb3B0aW9uIC1nIHByZWZpeCBDLWEKYmluZCBDLWEgbGFzdC13aW5kb3cKYmluZCBhIHNlbmQta2V5IEMtYQoKYmluZCB8IHNwbGl0LXdpbmRvdyAtaApiaW5kIC0gc3BsaXQtd2luZG93IC12CgojIEZpeCBDVFJMLVBHVVAvUEdET1dOIGZvciB2aW0Kc2V0LXdpbmRvdy1vcHRpb24gLWcgeHRlcm0ta2V5cyBvbgoKIyBQcmV2ZW50IEVTQyBrZXkgZnJvbSBhZGRpbmcgZGVsYXkgYW5kIGJyZWFraW5nIFZpbSdzIEVTQyA+IGFycm93IGtleQpzZXQtb3B0aW9uIC1zIGVzY2FwZS10aW1lIDAKCkVORAogICAgICAgIGZpCmZpCgooCiAgICAjIENsb3NlIHRoZSBpbmhlcml0ZWQgbG9jayBGRCwgb3IgdG11eCB3aWxsIGtlZXAgaXQgb3Blbi4KICAgIGV4ZWMgOT4mLQogICAgZXhlYyB0bXV4IG5ldy1zZXNzaW9uIC1zIG1vbmdvZGIvMQopCikgOT4vdG1wL2p1anUtdW5pdC1tb25nb2RiLTEtZGVidWctaG9va3MtZXhpdAopIDg+L3RtcC9qdWp1LXVuaXQtbW9uZ29kYi0xLWRlYnVnLWhvb2tzCmV4aXQgJD8K | base64 -d > $F; . $F'\n"), -}, { - args: []string{"mysql/0"}, - proxy: true, - result: regexp.QuoteMeta(debugHooksArgs + "ubuntu@dummyenv-0.internal sudo /bin/bash -c 'F=$(mktemp); echo IyEvYmluL2Jhc2gKKAojIExvY2sgdGhlIGp1anUtPHVuaXQ+LWRlYnVnIGxvY2tmaWxlLgpmbG9jayAtbiA4IHx8IChlY2hvICJGYWlsZWQgdG8gYWNxdWlyZSAvdG1wL2p1anUtdW5pdC1teXNxbC0wLWRlYnVnLWhvb2tzOiB1bml0IGlzIGFscmVhZHkgYmVpbmcgZGVidWdnZWQiIDI+JjE7IGV4aXQgMSkKKAojIENsb3NlIHRoZSBpbmhlcml0ZWQgbG9jayBGRCwgb3IgdG11eCB3aWxsIGtlZXAgaXQgb3Blbi4KZXhlYyA4PiYtCgojIFdyaXRlIG91dCB0aGUgZGVidWctaG9va3MgYXJncy4KZWNobyAiZTMwSyIgfCBiYXNlNjQgLWQgPiAvdG1wL2p1anUtdW5pdC1teXNxbC0wLWRlYnVnLWhvb2tzCgojIExvY2sgdGhlIGp1anUtPHVuaXQ+LWRlYnVnLWV4aXQgbG9ja2ZpbGUuCmZsb2NrIC1uIDkgfHwgZXhpdCAxCgojIFdhaXQgZm9yIHRtdXggdG8gYmUgaW5zdGFsbGVkLgp3aGlsZSBbICEgLWYgL3Vzci9iaW4vdG11eCBdOyBkbwogICAgc2xlZXAgMQpkb25lCgppZiBbICEgLWYgfi8udG11eC5jb25mIF07IHRoZW4KICAgICAgICBpZiBbIC1mIC91c3Ivc2hhcmUvYnlvYnUvcHJvZmlsZXMvdG11eCBdOyB0aGVuCiAgICAgICAgICAgICAgICAjIFVzZSBieW9idS90bXV4IHByb2ZpbGUgZm9yIGZhbWlsaWFyIGtleWJpbmRpbmdzIGFuZCBicmFuZGluZwogICAgICAgICAgICAgICAgZWNobyAic291cmNlLWZpbGUgL3Vzci9zaGFyZS9ieW9idS9wcm9maWxlcy90bXV4IiA+IH4vLnRtdXguY29uZgogICAgICAgIGVsc2UKICAgICAgICAgICAgICAgICMgT3RoZXJ3aXNlLCB1c2UgdGhlIGxlZ2FjeSBqdWp1L3RtdXggY29uZmlndXJhdGlvbgogICAgICAgICAgICAgICAgY2F0ID4gfi8udG11eC5jb25mIDw8RU5ECiAgICAgICAgICAgICAgICAKIyBTdGF0dXMgYmFyCnNldC1vcHRpb24gLWcgc3RhdHVzLWJnIGJsYWNrCnNldC1vcHRpb24gLWcgc3RhdHVzLWZnIHdoaXRlCgpzZXQtd2luZG93LW9wdGlvbiAtZyB3aW5kb3ctc3RhdHVzLWN1cnJlbnQtYmcgcmVkCnNldC13aW5kb3ctb3B0aW9uIC1nIHdpbmRvdy1zdGF0dXMtY3VycmVudC1hdHRyIGJyaWdodAoKc2V0LW9wdGlvbiAtZyBzdGF0dXMtcmlnaHQgJycKCiMgUGFuZXMKc2V0LW9wdGlvbiAtZyBwYW5lLWJvcmRlci1mZyB3aGl0ZQpzZXQtb3B0aW9uIC1nIHBhbmUtYWN0aXZlLWJvcmRlci1mZyB3aGl0ZQoKIyBNb25pdG9yIGFjdGl2aXR5IG9uIHdpbmRvd3MKc2V0LXdpbmRvdy1vcHRpb24gLWcgbW9uaXRvci1hY3Rpdml0eSBvbgoKIyBTY3JlZW4gYmluZGluZ3MsIHNpbmNlIHBlb3BsZSBhcmUgbW9yZSBmYW1pbGlhciB3aXRoIHRoYXQuCnNldC1vcHRpb24gLWcgcHJlZml4IEMtYQpiaW5kIEMtYSBsYXN0LXdpbmRvdwpiaW5kIGEgc2VuZC1rZXkgQy1hCgpiaW5kIHwgc3BsaXQtd2luZG93IC1oCmJpbmQgLSBzcGxpdC13aW5kb3cgLXYKCiMgRml4IENUUkwtUEdVUC9QR0RPV04gZm9yIHZpbQpzZXQtd2luZG93LW9wdGlvbiAtZyB4dGVybS1rZXlzIG9uCgojIFByZXZlbnQgRVNDIGtleSBmcm9tIGFkZGluZyBkZWxheSBhbmQgYnJlYWtpbmcgVmltJ3MgRVNDID4gYXJyb3cga2V5CnNldC1vcHRpb24gLXMgZXNjYXBlLXRpbWUgMAoKRU5ECiAgICAgICAgZmkKZmkKCigKICAgICMgQ2xvc2UgdGhlIGluaGVyaXRlZCBsb2NrIEZELCBvciB0bXV4IHdpbGwga2VlcCBpdCBvcGVuLgogICAgZXhlYyA5PiYtCiAgICBleGVjIHRtdXggbmV3LXNlc3Npb24gLXMgbXlzcWwvMAopCikgOT4vdG1wL2p1anUtdW5pdC1teXNxbC0wLWRlYnVnLWhvb2tzLWV4aXQKKSA4Pi90bXAvanVqdS11bml0LW15c3FsLTAtZGVidWctaG9va3MKZXhpdCAkPwo= | base64 -d > $F; . $F'\n"), -}, { - info: `"*" is a valid hook name: it means hook everything`, - args: []string{"mysql/0", "*"}, - result: ".*\n", -}, { - info: `"*" mixed with named hooks is equivalent to "*"`, - args: []string{"mysql/0", "*", "relation-get"}, - result: ".*\n", -}, { - info: `multiple named hooks may be specified`, - args: []string{"mysql/0", "start", "stop"}, - result: ".*\n", -}, { - info: `relation hooks have the relation name prefixed`, - args: []string{"mysql/0", "juju-info-relation-joined"}, - result: ".*\n", -}, { - info: `invalid unit syntax`, - args: []string{"mysql"}, - error: `"mysql" is not a valid unit name`, -}, { - info: `invalid unit`, - args: []string{"nonexistent/123"}, - error: `unit "nonexistent/123" not found`, -}, { - info: `invalid hook`, - args: []string{"mysql/0", "invalid-hook"}, - error: `unit "mysql/0" does not contain hook "invalid-hook"`, -}} - -func (s *DebugHooksSuite) TestDebugHooksCommand(c *gc.C) { - //TODO(bogdanteleaga): Fix once debughooks are supported on windows - if runtime.GOOS == "windows" { - c.Skip("bug 1403084: Skipping on windows for now") - } - machines := s.makeMachines(3, c, true) - dummy := s.AddTestingCharm(c, "dummy") - srv := s.AddTestingService(c, "mysql", dummy) - s.addUnit(srv, machines[0], c) - - srv = s.AddTestingService(c, "mongodb", dummy) - s.addUnit(srv, machines[1], c) - s.addUnit(srv, machines[2], c) - - for i, t := range debugHooksTests { - c.Logf("test %d: %s\n\t%s\n", i, t.info, t.args) - ctx := coretesting.Context(c) - - debugHooksCmd := &DebugHooksCommand{} - debugHooksCmd.proxy = true - err := envcmd.Wrap(debugHooksCmd).Init(t.args) - if err == nil { - err = debugHooksCmd.Run(ctx) - } - if t.error != "" { - c.Assert(err, gc.ErrorMatches, t.error) - } else { - c.Assert(err, jc.ErrorIsNil) - } - } -} === removed file 'src/github.com/juju/juju/cmd/juju/debuglog.go' --- src/github.com/juju/juju/cmd/juju/debuglog.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/debuglog.go 1970-01-01 00:00:00 +0000 @@ -1,100 +0,0 @@ -// Copyright 2013, 2014 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - "io" - - "github.com/juju/cmd" - "github.com/juju/loggo" - "launchpad.net/gnuflag" - - "github.com/juju/juju/api" - "github.com/juju/juju/cmd/envcmd" -) - -type DebugLogCommand struct { - envcmd.EnvCommandBase - - level string - params api.DebugLogParams -} - -var DefaultLogLocation = "/var/log/juju/all-machines.log" - -// defaultLineCount is the default number of lines to -// display, from the end of the consolidated log. -const defaultLineCount = 10 - -const debuglogDoc = ` -Stream the consolidated debug log file. This file contains the log messages -from all nodes in the environment. -` - -func (c *DebugLogCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "debug-log", - Purpose: "display the consolidated log file", - Doc: debuglogDoc, - } -} - -func (c *DebugLogCommand) SetFlags(f *gnuflag.FlagSet) { - f.Var(cmd.NewAppendStringsValue(&c.params.IncludeEntity), "i", "only show log messages for these entities") - f.Var(cmd.NewAppendStringsValue(&c.params.IncludeEntity), "include", "only show log messages for these entities") - f.Var(cmd.NewAppendStringsValue(&c.params.ExcludeEntity), "x", "do not show log messages for these entities") - f.Var(cmd.NewAppendStringsValue(&c.params.ExcludeEntity), "exclude", "do not show log messages for these entities") - f.Var(cmd.NewAppendStringsValue(&c.params.IncludeModule), "include-module", "only show log messages for these logging modules") - f.Var(cmd.NewAppendStringsValue(&c.params.ExcludeModule), "exclude-module", "do not show log messages for these logging modules") - - f.StringVar(&c.level, "l", "", "log level to show, one of [TRACE, DEBUG, INFO, WARNING, ERROR]") - f.StringVar(&c.level, "level", "", "") - - f.UintVar(&c.params.Backlog, "n", defaultLineCount, "go back this many lines from the end before starting to filter") - f.UintVar(&c.params.Backlog, "lines", defaultLineCount, "") - f.UintVar(&c.params.Limit, "limit", 0, "show at most this many lines") - f.BoolVar(&c.params.Replay, "replay", false, "start filtering from the start") -} - -func (c *DebugLogCommand) Init(args []string) error { - if c.level != "" { - level, ok := loggo.ParseLevel(c.level) - if !ok || level < loggo.TRACE || level > loggo.ERROR { - return fmt.Errorf("level value %q is not one of %q, %q, %q, %q, %q", - c.level, loggo.TRACE, loggo.DEBUG, loggo.INFO, loggo.WARNING, loggo.ERROR) - } - c.params.Level = level - } - return cmd.CheckEmpty(args) -} - -type DebugLogAPI interface { - WatchDebugLog(params api.DebugLogParams) (io.ReadCloser, error) - Close() error -} - -var getDebugLogAPI = func(c *DebugLogCommand) (DebugLogAPI, error) { - return c.NewAPIClient() -} - -// Run retrieves the debug log via the API. -func (c *DebugLogCommand) Run(ctx *cmd.Context) (err error) { - client, err := getDebugLogAPI(c) - if err != nil { - return err - } - defer client.Close() - debugLog, err := client.WatchDebugLog(c.params) - if err != nil { - return err - } - defer debugLog.Close() - _, err = io.Copy(ctx.Stdout, debugLog) - return err -} - -var runSSHCommand = func(sshCmd *SSHCommand, ctx *cmd.Context) error { - return sshCmd.Run(ctx) -} === removed file 'src/github.com/juju/juju/cmd/juju/debuglog_test.go' --- src/github.com/juju/juju/cmd/juju/debuglog_test.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/debuglog_test.go 1970-01-01 00:00:00 +0000 @@ -1,152 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "io" - "io/ioutil" - "strings" - - "github.com/juju/loggo" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/api" - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/testing" -) - -type DebugLogSuite struct { - testing.FakeJujuHomeSuite -} - -var _ = gc.Suite(&DebugLogSuite{}) - -func (s *DebugLogSuite) TestArgParsing(c *gc.C) { - for i, test := range []struct { - args []string - expected api.DebugLogParams - errMatch string - }{ - { - expected: api.DebugLogParams{ - Backlog: 10, - }, - }, { - args: []string{"-n0"}, - }, { - args: []string{"--lines=50"}, - expected: api.DebugLogParams{ - Backlog: 50, - }, - }, { - args: []string{"-l", "foo"}, - errMatch: `level value "foo" is not one of "TRACE", "DEBUG", "INFO", "WARNING", "ERROR"`, - }, { - args: []string{"--level=INFO"}, - expected: api.DebugLogParams{ - Backlog: 10, - Level: loggo.INFO, - }, - }, { - args: []string{"--include", "machine-1", "-i", "machine-2"}, - expected: api.DebugLogParams{ - IncludeEntity: []string{"machine-1", "machine-2"}, - Backlog: 10, - }, - }, { - args: []string{"--exclude", "machine-1", "-x", "machine-2"}, - expected: api.DebugLogParams{ - ExcludeEntity: []string{"machine-1", "machine-2"}, - Backlog: 10, - }, - }, { - args: []string{"--include-module", "juju.foo", "--include-module", "unit"}, - expected: api.DebugLogParams{ - IncludeModule: []string{"juju.foo", "unit"}, - Backlog: 10, - }, - }, { - args: []string{"--exclude-module", "juju.foo", "--exclude-module", "unit"}, - expected: api.DebugLogParams{ - ExcludeModule: []string{"juju.foo", "unit"}, - Backlog: 10, - }, - }, { - args: []string{"--replay"}, - expected: api.DebugLogParams{ - Backlog: 10, - Replay: true, - }, - }, { - args: []string{"--limit", "100"}, - expected: api.DebugLogParams{ - Backlog: 10, - Limit: 100, - }, - }, - } { - c.Logf("test %v", i) - command := &DebugLogCommand{} - err := testing.InitCommand(envcmd.Wrap(command), test.args) - if test.errMatch == "" { - c.Check(err, jc.ErrorIsNil) - c.Check(command.params, jc.DeepEquals, test.expected) - } else { - c.Check(err, gc.ErrorMatches, test.errMatch) - } - } -} - -func (s *DebugLogSuite) TestParamsPassed(c *gc.C) { - fake := &fakeDebugLogAPI{} - s.PatchValue(&getDebugLogAPI, func(_ *DebugLogCommand) (DebugLogAPI, error) { - return fake, nil - }) - _, err := testing.RunCommand(c, envcmd.Wrap(&DebugLogCommand{}), - "-i", "machine-1*", "-x", "machine-1-lxc-1", - "--include-module=juju.provisioner", - "--lines=500", - "--level=WARNING", - ) - c.Assert(err, jc.ErrorIsNil) - c.Assert(fake.params, gc.DeepEquals, api.DebugLogParams{ - IncludeEntity: []string{"machine-1*"}, - IncludeModule: []string{"juju.provisioner"}, - ExcludeEntity: []string{"machine-1-lxc-1"}, - Backlog: 500, - Level: loggo.WARNING, - }) -} - -func (s *DebugLogSuite) TestLogOutput(c *gc.C) { - s.PatchValue(&getDebugLogAPI, func(_ *DebugLogCommand) (DebugLogAPI, error) { - return &fakeDebugLogAPI{log: "this is the log output"}, nil - }) - ctx, err := testing.RunCommand(c, envcmd.Wrap(&DebugLogCommand{})) - c.Assert(err, jc.ErrorIsNil) - c.Assert(testing.Stdout(ctx), gc.Equals, "this is the log output") -} - -func newFakeDebugLogAPI(log string) DebugLogAPI { - return &fakeDebugLogAPI{log: log} -} - -type fakeDebugLogAPI struct { - log string - params api.DebugLogParams - err error -} - -func (fake *fakeDebugLogAPI) WatchDebugLog(params api.DebugLogParams) (io.ReadCloser, error) { - if fake.err != nil { - return nil, fake.err - } - fake.params = params - return ioutil.NopCloser(strings.NewReader(fake.log)), nil -} - -func (fake *fakeDebugLogAPI) Close() error { - return nil -} === removed file 'src/github.com/juju/juju/cmd/juju/deploy.go' --- src/github.com/juju/juju/cmd/juju/deploy.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/deploy.go 1970-01-01 00:00:00 +0000 @@ -1,322 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - "os" - "strings" - - "github.com/juju/cmd" - "github.com/juju/errors" - "github.com/juju/names" - "gopkg.in/juju/charm.v5" - "launchpad.net/gnuflag" - - apiservice "github.com/juju/juju/api/service" - "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/cmd/juju/block" - "github.com/juju/juju/cmd/juju/service" - "github.com/juju/juju/constraints" - "github.com/juju/juju/juju/osenv" - "github.com/juju/juju/storage" -) - -type DeployCommand struct { - envcmd.EnvCommandBase - service.UnitCommandBase - CharmName string - ServiceName string - Config cmd.FileVar - Constraints constraints.Value - Networks string - BumpRevision bool // Remove this once the 1.16 support is dropped. - RepoPath string // defaults to JUJU_REPOSITORY - - // TODO(axw) move this to UnitCommandBase once we support --storage - // on add-unit too. - // - // Storage is a map of storage constraints, keyed on the storage name - // defined in charm storage metadata. - Storage map[string]storage.Constraints -} - -const deployDoc = ` - can be a charm URL, or an unambiguously condensed form of it; -assuming a current series of "precise", the following forms will be accepted: - -For cs:precise/mysql - mysql - precise/mysql - -For cs:~user/precise/mysql - cs:~user/mysql - -The current series is determined first by the default-series environment -setting, followed by the preferred series for the charm in the charm store. - -In these cases, a versioned charm URL will be expanded as expected (for example, -mysql-33 becomes cs:precise/mysql-33). - -However, for local charms, when the default-series is not specified in the -environment, one must specify the series. For example: - local:precise/mysql - -, if omitted, will be derived from . - -Constraints can be specified when using deploy by specifying the --constraints -flag. When used with deploy, service-specific constraints are set so that later -machines provisioned with add-unit will use the same constraints (unless changed -by set-constraints). - -Charms can be deployed to a specific machine using the --to argument. -If the destination is an LXC container the default is to use lxc-clone -to create the container where possible. For Ubuntu deployments, lxc-clone -is supported for the trusty OS series and later. A 'template' container is -created with the name - juju--template -where is the OS series, for example 'juju-trusty-template'. - -You can override the use of clone by changing the provider configuration: - lxc-clone: false - -If you have the main container directory mounted on a btrfs partition, -then the clone will be using btrfs snapshots to create the containers. -This means that clones use up much less disk space. If you do not have btrfs, -lxc will attempt to use aufs (an overlay type filesystem). You can -explicitly ask Juju to create full containers and not overlays by specifying -the following in the provider configuration: - lxc-clone-aufs: false - -Examples: - juju deploy mysql --to 23 (deploy to machine 23) - juju deploy mysql --to 24/lxc/3 (deploy to lxc container 3 on host machine 24) - juju deploy mysql --to lxc:25 (deploy to a new lxc container on host machine 25) - - juju deploy mysql -n 5 --constraints mem=8G - (deploy 5 instances of mysql with at least 8 GB of RAM each) - - juju deploy mysql --networks=storage,mynet --constraints networks=^logging,db - (deploy mysql on machines with "storage", "mynet" and "db" networks, - but not on machines with "logging" network, also configure "storage" and - "mynet" networks) - -Like constraints, service-specific network requirements can be -specified with the --networks argument, which takes a comma-delimited -list of juju-specific network names. Networks can also be specified with -constraints, but they only define what machine to pick, not what networks -to configure on it. The --networks argument instructs juju to add all the -networks specified with it to all new machines deployed to host units of -the service. Not supported on all providers. - -See Also: - juju help constraints - juju help set-constraints - juju help get-constraints -` - -func (c *DeployCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "deploy", - Args: " []", - Purpose: "deploy a new service", - Doc: deployDoc, - } -} - -func (c *DeployCommand) SetFlags(f *gnuflag.FlagSet) { - c.UnitCommandBase.SetFlags(f) - f.IntVar(&c.NumUnits, "n", 1, "number of service units to deploy for principal charms") - f.BoolVar(&c.BumpRevision, "u", false, "increment local charm directory revision (DEPRECATED)") - f.BoolVar(&c.BumpRevision, "upgrade", false, "") - f.Var(&c.Config, "config", "path to yaml-formatted service config") - f.Var(constraints.ConstraintsValue{Target: &c.Constraints}, "constraints", "set service constraints") - f.StringVar(&c.Networks, "networks", "", "bind the service to specific networks") - f.StringVar(&c.RepoPath, "repository", os.Getenv(osenv.JujuRepositoryEnvKey), "local charm repository") - f.Var(storageFlag{&c.Storage}, "storage", "charm storage constraints") -} - -func (c *DeployCommand) Init(args []string) error { - switch len(args) { - case 2: - if !names.IsValidService(args[1]) { - return fmt.Errorf("invalid service name %q", args[1]) - } - c.ServiceName = args[1] - fallthrough - case 1: - if _, err := charm.InferURL(args[0], "fake"); err != nil { - return fmt.Errorf("invalid charm name %q", args[0]) - } - c.CharmName = args[0] - case 0: - return errors.New("no charm specified") - default: - return cmd.CheckEmpty(args[2:]) - } - return c.UnitCommandBase.Init(args) -} - -func (c *DeployCommand) newServiceAPIClient() (*apiservice.Client, error) { - root, err := c.NewAPIRoot() - if err != nil { - return nil, errors.Trace(err) - } - return apiservice.NewClient(root), nil -} - -func (c *DeployCommand) Run(ctx *cmd.Context) error { - client, err := c.NewAPIClient() - if err != nil { - return err - } - defer client.Close() - - conf, err := service.GetClientConfig(client) - if err != nil { - return err - } - - if err := c.CheckProvider(conf); err != nil { - return err - } - - csClient, err := newCharmStoreClient() - if err != nil { - return errors.Trace(err) - } - defer csClient.jar.Save() - curl, repo, err := resolveCharmURL(c.CharmName, csClient.params, ctx.AbsPath(c.RepoPath), conf) - if err != nil { - return errors.Trace(err) - } - - curl, err = addCharmViaAPI(client, ctx, curl, repo, csClient) - if err != nil { - return block.ProcessBlockedError(err, block.BlockChange) - } - - if c.BumpRevision { - ctx.Infof("--upgrade (or -u) is deprecated and ignored; charms are always deployed with a unique revision.") - } - - requestedNetworks, err := networkNamesToTags(parseNetworks(c.Networks)) - if err != nil { - return err - } - // We need to ensure network names are valid below, but we don't need them here. - _, err = networkNamesToTags(c.Constraints.IncludeNetworks()) - if err != nil { - return err - } - _, err = networkNamesToTags(c.Constraints.ExcludeNetworks()) - if err != nil { - return err - } - haveNetworks := len(requestedNetworks) > 0 || c.Constraints.HaveNetworks() - - charmInfo, err := client.CharmInfo(curl.String()) - if err != nil { - return err - } - - numUnits := c.NumUnits - if charmInfo.Meta.Subordinate { - if !constraints.IsEmpty(&c.Constraints) { - return errors.New("cannot use --constraints with subordinate service") - } - if numUnits == 1 && c.ToMachineSpec == "" { - numUnits = 0 - } else { - return errors.New("cannot use --num-units or --to with subordinate service") - } - } - serviceName := c.ServiceName - if serviceName == "" { - serviceName = charmInfo.Meta.Name - } - - var configYAML []byte - if c.Config.Path != "" { - configYAML, err = c.Config.Read(ctx) - if err != nil { - return err - } - } - - // If storage is specified, we attempt to use a new API on the service facade. - if len(c.Storage) > 0 { - notSupported := errors.New("cannot deploy charms with storage: not supported by the API server") - serviceClient, err := c.newServiceAPIClient() - if err != nil { - return notSupported - } - defer serviceClient.Close() - err = serviceClient.ServiceDeploy( - curl.String(), - serviceName, - numUnits, - string(configYAML), - c.Constraints, - c.ToMachineSpec, - requestedNetworks, - c.Storage, - ) - if params.IsCodeNotImplemented(err) { - return notSupported - } - return block.ProcessBlockedError(err, block.BlockChange) - } - - err = client.ServiceDeployWithNetworks( - curl.String(), - serviceName, - numUnits, - string(configYAML), - c.Constraints, - c.ToMachineSpec, - requestedNetworks, - ) - if params.IsCodeNotImplemented(err) { - if haveNetworks { - return errors.New("cannot use --networks/--constraints networks=...: not supported by the API server") - } - err = client.ServiceDeploy( - curl.String(), - serviceName, - numUnits, - string(configYAML), - c.Constraints, - c.ToMachineSpec) - } - return block.ProcessBlockedError(err, block.BlockChange) -} - -// parseNetworks returns a list of network names by parsing the -// comma-delimited string value of --networks argument. -func parseNetworks(networksValue string) []string { - parts := strings.Split(networksValue, ",") - var networks []string - for _, part := range parts { - network := strings.TrimSpace(part) - if network != "" { - networks = append(networks, network) - } - } - return networks -} - -// networkNamesToTags returns the given network names converted to -// tags, or an error. -func networkNamesToTags(networks []string) ([]string, error) { - var tags []string - for _, network := range networks { - if !names.IsValidNetwork(network) { - return nil, fmt.Errorf("%q is not a valid network name", network) - } - tags = append(tags, names.NewNetworkTag(network).String()) - } - return tags, nil -} === removed file 'src/github.com/juju/juju/cmd/juju/deploy_test.go' --- src/github.com/juju/juju/cmd/juju/deploy_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/deploy_test.go 1970-01-01 00:00:00 +0000 @@ -1,535 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - "io/ioutil" - "net/http" - "net/url" - "strings" - - "github.com/juju/errors" - jc "github.com/juju/testing/checkers" - "github.com/juju/utils" - gc "gopkg.in/check.v1" - "gopkg.in/juju/charm.v5" - "gopkg.in/juju/charm.v5/charmrepo" - "gopkg.in/juju/charmstore.v4" - "gopkg.in/juju/charmstore.v4/charmstoretesting" - "gopkg.in/juju/charmstore.v4/csclient" - "gopkg.in/macaroon-bakery.v0/bakery/checkers" - "gopkg.in/macaroon-bakery.v0/bakerytest" - - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/cmd/juju/service" - "github.com/juju/juju/constraints" - "github.com/juju/juju/environs/config" - "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" -) - -type DeploySuite struct { - testing.RepoSuite - CmdBlockHelper -} - -func (s *DeploySuite) SetUpTest(c *gc.C) { - s.RepoSuite.SetUpTest(c) - s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) - c.Assert(s.CmdBlockHelper, gc.NotNil) - s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) -} - -var _ = gc.Suite(&DeploySuite{}) - -func runDeploy(c *gc.C, args ...string) error { - _, err := coretesting.RunCommand(c, envcmd.Wrap(&DeployCommand{}), args...) - return err -} - -var initErrorTests = []struct { - args []string - err string -}{ - { - args: nil, - err: `no charm specified`, - }, { - args: []string{"charm-name", "service-name", "hotdog"}, - err: `unrecognized args: \["hotdog"\]`, - }, { - args: []string{"craz~ness"}, - err: `invalid charm name "craz~ness"`, - }, { - args: []string{"craziness", "burble-1"}, - err: `invalid service name "burble-1"`, - }, { - args: []string{"craziness", "burble1", "-n", "0"}, - err: `--num-units must be a positive integer`, - }, { - args: []string{"craziness", "burble1", "--to", "bigglesplop"}, - err: `invalid --to parameter "bigglesplop"`, - }, { - args: []string{"craziness", "burble1", "-n", "2", "--to", "123"}, - err: `cannot use --num-units > 1 with --to`, - }, { - args: []string{"craziness", "burble1", "--constraints", "gibber=plop"}, - err: `invalid value "gibber=plop" for flag --constraints: unknown constraint "gibber"`, - }, -} - -func (s *DeploySuite) TestInitErrors(c *gc.C) { - for i, t := range initErrorTests { - c.Logf("test %d", i) - err := coretesting.InitCommand(envcmd.Wrap(&DeployCommand{}), t.args) - c.Assert(err, gc.ErrorMatches, t.err) - } -} - -func (s *DeploySuite) TestNoCharm(c *gc.C) { - err := runDeploy(c, "local:unknown-123") - c.Assert(err, gc.ErrorMatches, `charm not found in ".*": local:trusty/unknown-123`) -} - -func (s *DeploySuite) TestBlockDeploy(c *gc.C) { - // Block operation - s.BlockAllChanges(c, "TestBlockDeploy") - testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") - err := runDeploy(c, "local:dummy", "some-service-name") - s.AssertBlocked(c, err, ".*TestBlockDeploy.*") -} - -func (s *DeploySuite) TestCharmDir(c *gc.C) { - testcharms.Repo.ClonedDirPath(s.SeriesPath, "dummy") - err := runDeploy(c, "local:dummy") - c.Assert(err, jc.ErrorIsNil) - curl := charm.MustParseURL("local:trusty/dummy-1") - s.AssertService(c, "dummy", curl, 1, 0) -} - -func (s *DeploySuite) TestUpgradeReportsDeprecated(c *gc.C) { - testcharms.Repo.ClonedDirPath(s.SeriesPath, "dummy") - ctx, err := coretesting.RunCommand(c, envcmd.Wrap(&DeployCommand{}), "local:dummy", "-u") - c.Assert(err, jc.ErrorIsNil) - - c.Assert(coretesting.Stdout(ctx), gc.Equals, "") - output := strings.Split(coretesting.Stderr(ctx), "\n") - c.Check(output[0], gc.Matches, `Added charm ".*" to the environment.`) - c.Check(output[1], gc.Equals, "--upgrade (or -u) is deprecated and ignored; charms are always deployed with a unique revision.") -} - -func (s *DeploySuite) TestUpgradeCharmDir(c *gc.C) { - // Add the charm, so the url will exist and a new revision will be - // picked in ServiceDeploy. - dummyCharm := s.AddTestingCharm(c, "dummy") - - dirPath := testcharms.Repo.ClonedDirPath(s.SeriesPath, "dummy") - err := runDeploy(c, "local:quantal/dummy") - c.Assert(err, jc.ErrorIsNil) - upgradedRev := dummyCharm.Revision() + 1 - curl := dummyCharm.URL().WithRevision(upgradedRev) - s.AssertService(c, "dummy", curl, 1, 0) - // Check the charm dir was left untouched. - ch, err := charm.ReadCharmDir(dirPath) - c.Assert(err, jc.ErrorIsNil) - c.Assert(ch.Revision(), gc.Equals, 1) -} - -func (s *DeploySuite) TestCharmBundle(c *gc.C) { - testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") - err := runDeploy(c, "local:dummy", "some-service-name") - c.Assert(err, jc.ErrorIsNil) - curl := charm.MustParseURL("local:trusty/dummy-1") - s.AssertService(c, "some-service-name", curl, 1, 0) -} - -func (s *DeploySuite) TestSubordinateCharm(c *gc.C) { - testcharms.Repo.CharmArchivePath(s.SeriesPath, "logging") - err := runDeploy(c, "local:logging") - c.Assert(err, jc.ErrorIsNil) - curl := charm.MustParseURL("local:trusty/logging-1") - s.AssertService(c, "logging", curl, 0, 0) -} - -func (s *DeploySuite) TestConfig(c *gc.C) { - testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") - path := setupConfigFile(c, c.MkDir()) - err := runDeploy(c, "local:dummy", "dummy-service", "--config", path) - c.Assert(err, jc.ErrorIsNil) - service, err := s.State.Service("dummy-service") - c.Assert(err, jc.ErrorIsNil) - settings, err := service.ConfigSettings() - c.Assert(err, jc.ErrorIsNil) - c.Assert(settings, gc.DeepEquals, charm.Settings{ - "skill-level": int64(9000), - "username": "admin001", - }) -} - -func (s *DeploySuite) TestRelativeConfigPath(c *gc.C) { - testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") - // Putting a config file in home is okay as $HOME is set to a tempdir - setupConfigFile(c, utils.Home()) - err := runDeploy(c, "local:dummy", "dummy-service", "--config", "~/testconfig.yaml") - c.Assert(err, jc.ErrorIsNil) -} - -func (s *DeploySuite) TestConfigError(c *gc.C) { - testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") - path := setupConfigFile(c, c.MkDir()) - err := runDeploy(c, "local:dummy", "other-service", "--config", path) - c.Assert(err, gc.ErrorMatches, `no settings found for "other-service"`) - _, err = s.State.Service("other-service") - c.Assert(err, jc.Satisfies, errors.IsNotFound) -} - -func (s *DeploySuite) TestConstraints(c *gc.C) { - testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") - err := runDeploy(c, "local:dummy", "--constraints", "mem=2G cpu-cores=2 networks=net1,^net2") - c.Assert(err, jc.ErrorIsNil) - curl := charm.MustParseURL("local:trusty/dummy-1") - service, _ := s.AssertService(c, "dummy", curl, 1, 0) - cons, err := service.Constraints() - c.Assert(err, jc.ErrorIsNil) - c.Assert(cons, jc.DeepEquals, constraints.MustParse("mem=2G cpu-cores=2 networks=net1,^net2")) -} - -func (s *DeploySuite) TestNetworks(c *gc.C) { - testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") - err := runDeploy(c, "local:dummy", "--networks", ", net1, net2 , ", "--constraints", "mem=2G cpu-cores=2 networks=net1,net0,^net3,^net4") - c.Assert(err, jc.ErrorIsNil) - curl := charm.MustParseURL("local:trusty/dummy-1") - service, _ := s.AssertService(c, "dummy", curl, 1, 0) - networks, err := service.Networks() - c.Assert(err, jc.ErrorIsNil) - c.Assert(networks, jc.DeepEquals, []string{"net1", "net2"}) - cons, err := service.Constraints() - c.Assert(err, jc.ErrorIsNil) - c.Assert(cons, jc.DeepEquals, constraints.MustParse("mem=2G cpu-cores=2 networks=net1,net0,^net3,^net4")) -} - -// 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) - - testcharms.Repo.CharmArchivePath(s.SeriesPath, "storage-block") - err = runDeploy(c, "local:storage-block", "--storage", "data=loop-pool,1G") - c.Assert(err, jc.ErrorIsNil) - curl := charm.MustParseURL("local:trusty/storage-block-1") - service, _ := s.AssertService(c, "storage-block", curl, 1, 0) - - cons, err := service.StorageConstraints() - c.Assert(err, jc.ErrorIsNil) - c.Assert(cons, jc.DeepEquals, map[string]state.StorageConstraints{ - "data": { - Pool: "loop-pool", - Count: 1, - Size: 1024, - }, - "allecto": { - Pool: "loop", - Count: 0, - Size: 1024, - }, - }) -} - -func (s *DeploySuite) TestSubordinateConstraints(c *gc.C) { - testcharms.Repo.CharmArchivePath(s.SeriesPath, "logging") - err := runDeploy(c, "local:logging", "--constraints", "mem=1G") - c.Assert(err, gc.ErrorMatches, "cannot use --constraints with subordinate service") -} - -func (s *DeploySuite) TestNumUnits(c *gc.C) { - testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") - err := runDeploy(c, "local:dummy", "-n", "13") - c.Assert(err, jc.ErrorIsNil) - curl := charm.MustParseURL("local:trusty/dummy-1") - s.AssertService(c, "dummy", curl, 13, 0) -} - -func (s *DeploySuite) TestNumUnitsSubordinate(c *gc.C) { - testcharms.Repo.CharmArchivePath(s.SeriesPath, "logging") - err := runDeploy(c, "--num-units", "3", "local:logging") - c.Assert(err, gc.ErrorMatches, "cannot use --num-units or --to with subordinate service") - _, err = s.State.Service("dummy") - c.Assert(err, gc.ErrorMatches, `service "dummy" not found`) -} - -func (s *DeploySuite) assertForceMachine(c *gc.C, machineId string) { - svc, err := s.State.Service("portlandia") - c.Assert(err, jc.ErrorIsNil) - units, err := svc.AllUnits() - c.Assert(err, jc.ErrorIsNil) - c.Assert(units, gc.HasLen, 1) - mid, err := units[0].AssignedMachineId() - c.Assert(err, jc.ErrorIsNil) - c.Assert(mid, gc.Equals, machineId) -} - -func (s *DeploySuite) TestForceMachine(c *gc.C) { - testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") - machine, err := s.State.AddMachine(coretesting.FakeDefaultSeries, state.JobHostUnits) - c.Assert(err, jc.ErrorIsNil) - err = runDeploy(c, "--to", machine.Id(), "local:dummy", "portlandia") - c.Assert(err, jc.ErrorIsNil) - s.assertForceMachine(c, machine.Id()) -} - -func (s *DeploySuite) TestForceMachineExistingContainer(c *gc.C) { - testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") - template := state.MachineTemplate{ - Series: coretesting.FakeDefaultSeries, - Jobs: []state.MachineJob{state.JobHostUnits}, - } - container, err := s.State.AddMachineInsideNewMachine(template, template, instance.LXC) - c.Assert(err, jc.ErrorIsNil) - err = runDeploy(c, "--to", container.Id(), "local:dummy", "portlandia") - c.Assert(err, jc.ErrorIsNil) - s.assertForceMachine(c, container.Id()) - machines, err := s.State.AllMachines() - c.Assert(err, jc.ErrorIsNil) - c.Assert(machines, gc.HasLen, 2) -} - -func (s *DeploySuite) TestForceMachineNewContainer(c *gc.C) { - testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") - machine, err := s.State.AddMachine(coretesting.FakeDefaultSeries, state.JobHostUnits) - c.Assert(err, jc.ErrorIsNil) - err = runDeploy(c, "--to", "lxc:"+machine.Id(), "local:dummy", "portlandia") - c.Assert(err, jc.ErrorIsNil) - s.assertForceMachine(c, machine.Id()+"/lxc/0") - machines, err := s.State.AllMachines() - c.Assert(err, jc.ErrorIsNil) - c.Assert(machines, gc.HasLen, 2) -} - -func (s *DeploySuite) TestForceMachineNotFound(c *gc.C) { - testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") - err := runDeploy(c, "--to", "42", "local:dummy", "portlandia") - c.Assert(err, gc.ErrorMatches, `cannot deploy "portlandia" to machine 42: machine 42 not found`) - _, err = s.State.Service("portlandia") - c.Assert(err, gc.ErrorMatches, `service "portlandia" not found`) -} - -func (s *DeploySuite) TestForceMachineSubordinate(c *gc.C) { - machine, err := s.State.AddMachine(coretesting.FakeDefaultSeries, state.JobHostUnits) - c.Assert(err, jc.ErrorIsNil) - testcharms.Repo.CharmArchivePath(s.SeriesPath, "logging") - err = runDeploy(c, "--to", machine.Id(), "local:logging") - c.Assert(err, gc.ErrorMatches, "cannot use --num-units or --to with subordinate service") - _, err = s.State.Service("dummy") - c.Assert(err, gc.ErrorMatches, `service "dummy" not found`) -} - -func (s *DeploySuite) TestNonLocalCannotHostUnits(c *gc.C) { - err := runDeploy(c, "--to", "0", "local:dummy", "portlandia") - c.Assert(err, gc.Not(gc.ErrorMatches), "machine 0 is the state server for a local environment and cannot host units") -} - -type DeployLocalSuite struct { - testing.RepoSuite -} - -var _ = gc.Suite(&DeployLocalSuite{}) - -func (s *DeployLocalSuite) SetUpTest(c *gc.C) { - s.RepoSuite.SetUpTest(c) - - // override provider type - s.PatchValue(&service.GetClientConfig, func(client service.ServiceAddUnitAPI) (*config.Config, error) { - attrs, err := client.EnvironmentGet() - if err != nil { - return nil, err - } - attrs["type"] = "local" - - return config.New(config.NoDefaults, attrs) - }) -} - -func (s *DeployLocalSuite) TestLocalCannotHostUnits(c *gc.C) { - err := runDeploy(c, "--to", "0", "local:dummy", "portlandia") - c.Assert(err, gc.ErrorMatches, "machine 0 is the state server for a local environment and cannot host units") -} - -// setupConfigFile creates a configuration file for testing set -// with the --config argument specifying a configuration file. -func setupConfigFile(c *gc.C, dir string) string { - ctx := coretesting.ContextForDir(c, dir) - path := ctx.AbsPath("testconfig.yaml") - content := []byte("dummy-service:\n skill-level: 9000\n username: admin001\n\n") - err := ioutil.WriteFile(path, content, 0666) - c.Assert(err, jc.ErrorIsNil) - return path -} - -type DeployCharmStoreSuite struct { - charmStoreSuite -} - -var _ = gc.Suite(&DeployCharmStoreSuite{}) - -var deployAuthorizationTests = []struct { - about string - uploadURL string - deployURL string - readPermUser string - expectError string - expectOutput string -}{{ - about: "public charm, success", - uploadURL: "cs:~bob/trusty/wordpress1-10", - deployURL: "cs:~bob/trusty/wordpress1", - expectOutput: `Added charm "cs:~bob/trusty/wordpress1-10" to the environment.`, -}, { - about: "public charm, fully resolved, success", - uploadURL: "cs:~bob/trusty/wordpress2-10", - deployURL: "cs:~bob/trusty/wordpress2-10", - expectOutput: `Added charm "cs:~bob/trusty/wordpress2-10" to the environment.`, -}, { - about: "non-public charm, success", - uploadURL: "cs:~bob/trusty/wordpress3-10", - deployURL: "cs:~bob/trusty/wordpress3", - readPermUser: clientUserName, - expectOutput: `Added charm "cs:~bob/trusty/wordpress3-10" to the environment.`, -}, { - about: "non-public charm, fully resolved, success", - uploadURL: "cs:~bob/trusty/wordpress4-10", - deployURL: "cs:~bob/trusty/wordpress4-10", - readPermUser: clientUserName, - expectOutput: `Added charm "cs:~bob/trusty/wordpress4-10" to the environment.`, -}, { - about: "non-public charm, access denied", - uploadURL: "cs:~bob/trusty/wordpress5-10", - deployURL: "cs:~bob/trusty/wordpress5", - readPermUser: "bob", - expectError: `cannot resolve charm URL "cs:~bob/trusty/wordpress5": cannot get "/~bob/trusty/wordpress5/meta/any\?include=id": unauthorized: access denied for user "client-username"`, -}, { - about: "non-public charm, fully resolved, access denied", - uploadURL: "cs:~bob/trusty/wordpress6-47", - deployURL: "cs:~bob/trusty/wordpress6-47", - readPermUser: "bob", - expectError: `cannot retrieve charm "cs:~bob/trusty/wordpress6-47": cannot get archive: unauthorized: access denied for user "client-username"`, -}} - -func (s *DeployCharmStoreSuite) TestDeployAuthorization(c *gc.C) { - for i, test := range deployAuthorizationTests { - c.Logf("test %d: %s", i, test.about) - url, _ := s.uploadCharm(c, test.uploadURL, "wordpress") - if test.readPermUser != "" { - s.changeReadPerm(c, url, test.readPermUser) - } - ctx, err := coretesting.RunCommand(c, envcmd.Wrap(&DeployCommand{}), test.deployURL, fmt.Sprintf("wordpress%d", i)) - if test.expectError != "" { - c.Assert(err, gc.ErrorMatches, test.expectError) - continue - } - c.Assert(err, jc.ErrorIsNil) - output := strings.Trim(coretesting.Stderr(ctx), "\n") - c.Assert(output, gc.Equals, test.expectOutput) - } -} - -const ( - // clientUserCookie is the name of the cookie which is - // used to signal to the charmStoreSuite macaroon discharger - // that the client is a juju client rather than the juju environment. - clientUserCookie = "client" - - // clientUserName is the name chosen for the juju client - // when it has authorized. - clientUserName = "client-username" -) - -// charmStoreSuite is a suite fixture that puts the machinery in -// place to allow testing code that calls addCharmViaAPI. -type charmStoreSuite struct { - testing.JujuConnSuite - srv *charmstoretesting.Server - discharger *bakerytest.Discharger -} - -func (s *charmStoreSuite) SetUpTest(c *gc.C) { - s.JujuConnSuite.SetUpTest(c) - - // Set up the third party discharger. - s.discharger = bakerytest.NewDischarger(nil, func(req *http.Request, cond string, arg string) ([]checkers.Caveat, error) { - cookie, err := req.Cookie(clientUserCookie) - if err != nil { - return nil, errors.New("discharge denied to non-clients") - } - return []checkers.Caveat{ - checkers.DeclaredCaveat("username", cookie.Value), - }, nil - }) - - // Set up the charm store testing server. - s.srv = charmstoretesting.OpenServer(c, s.Session, charmstore.ServerParams{ - IdentityLocation: s.discharger.Location(), - PublicKeyLocator: s.discharger, - }) - - // Initialize the charm cache dir. - s.PatchValue(&charmrepo.CacheDir, c.MkDir()) - - // Point the CLI to the charm store testing server. - original := newCharmStoreClient - s.PatchValue(&newCharmStoreClient, func() (*csClient, error) { - csclient, err := original() - if err != nil { - return nil, err - } - csclient.params.URL = s.srv.URL() - // Add a cookie so that the discharger can detect whether the - // HTTP client is the juju environment or the juju client. - lurl, err := url.Parse(s.discharger.Location()) - if err != nil { - panic(err) - } - csclient.params.HTTPClient.Jar.SetCookies(lurl, []*http.Cookie{{ - Name: clientUserCookie, - Value: clientUserName, - }}) - return csclient, nil - }) - - // Point the Juju API server to the charm store testing server. - s.PatchValue(&csclient.ServerURL, s.srv.URL()) -} - -func (s *charmStoreSuite) TearDownTest(c *gc.C) { - s.discharger.Close() - s.srv.Close() - s.JujuConnSuite.TearDownTest(c) -} - -// uploadCharm adds a charm with the given URL and name to the charm store. -func (s *charmStoreSuite) uploadCharm(c *gc.C, url, name string) (*charm.URL, charm.Charm) { - id := charm.MustParseReference(url) - promulgated := false - if id.User == "" { - id.User = "who" - promulgated = true - } - ch := testcharms.Repo.CharmArchive(c.MkDir(), name) - id = s.srv.UploadCharm(c, ch, id, promulgated) - return (*charm.URL)(id), ch -} - -// changeReadPerm changes the read permission of the given charm URL. -// The charm must be present in the testing charm store. -func (s *charmStoreSuite) changeReadPerm(c *gc.C, url *charm.URL, perms ...string) { - err := s.srv.NewClient().Put("/"+url.Path()+"/meta/perm/read", perms) - c.Assert(err, jc.ErrorIsNil) -} === removed file 'src/github.com/juju/juju/cmd/juju/destroyenvironment.go' --- src/github.com/juju/juju/cmd/juju/destroyenvironment.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/destroyenvironment.go 1970-01-01 00:00:00 +0000 @@ -1,237 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "bufio" - stderrors "errors" - "fmt" - "io" - "strings" - - "github.com/juju/cmd" - "github.com/juju/errors" - "launchpad.net/gnuflag" - - "github.com/juju/juju/api" - "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/cmd/juju/block" - "github.com/juju/juju/environs" - "github.com/juju/juju/environs/config" - "github.com/juju/juju/environs/configstore" - "github.com/juju/juju/juju" -) - -var NoEnvironmentError = stderrors.New("no environment specified") -var DoubleEnvironmentError = stderrors.New("you cannot supply both -e and the envname as a positional argument") - -// DestroyEnvironmentCommand destroys an environment. -type DestroyEnvironmentCommand struct { - envcmd.EnvCommandBase - cmd.CommandBase - envName string - assumeYes bool - force bool -} - -func (c *DestroyEnvironmentCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "destroy-environment", - Args: "", - Purpose: "terminate all machines and other associated resources for an environment", - } -} - -func (c *DestroyEnvironmentCommand) SetFlags(f *gnuflag.FlagSet) { - f.BoolVar(&c.assumeYes, "y", false, "Do not ask for confirmation") - f.BoolVar(&c.assumeYes, "yes", false, "") - f.BoolVar(&c.force, "force", false, "Forcefully destroy the environment, directly through the environment provider") - f.StringVar(&c.envName, "e", "", "juju environment to operate in") - f.StringVar(&c.envName, "environment", "", "juju environment to operate in") -} - -func (c *DestroyEnvironmentCommand) Init(args []string) error { - if c.envName != "" { - logger.Warningf("-e/--environment flag is deprecated in 1.18, " + - "please supply environment as a positional parameter") - // They supplied the -e flag - if len(args) == 0 { - // We're happy, we have enough information - return nil - } - // You can't supply -e ENV and ENV as a positional argument - return DoubleEnvironmentError - } - // No -e flag means they must supply the environment positionally - switch len(args) { - case 0: - return NoEnvironmentError - case 1: - c.envName = args[0] - return nil - default: - return cmd.CheckEmpty(args[1:]) - } -} - -func (c *DestroyEnvironmentCommand) Run(ctx *cmd.Context) (result error) { - store, err := configstore.Default() - if err != nil { - return errors.Annotate(err, "cannot open environment info storage") - } - - cfgInfo, err := store.ReadInfo(c.envName) - if err != nil { - return errors.Annotate(err, "cannot read environment info") - } - - var hasBootstrapCfg bool - var serverEnviron environs.Environ - if bootstrapCfg := cfgInfo.BootstrapConfig(); bootstrapCfg != nil { - hasBootstrapCfg = true - serverEnviron, err = getServerEnv(bootstrapCfg) - if err != nil { - return errors.Trace(err) - } - } - - if c.force { - if hasBootstrapCfg { - // If --force is supplied on a server environment, then don't - // attempt to use the API. This is necessary to destroy broken - // environments, where the API server is inaccessible or faulty. - return environs.Destroy(serverEnviron, store) - } else { - // Force only makes sense on the server environment. - return errors.Errorf("cannot force destroy environment without bootstrap information") - } - } - - apiclient, err := juju.NewAPIClientFromName(c.envName) - if err != nil { - if errors.IsNotFound(err) { - logger.Warningf("environment not found, removing config file") - ctx.Infof("environment not found, removing config file") - return environs.DestroyInfo(c.envName, store) - } - return errors.Annotate(err, "cannot connect to API") - } - defer apiclient.Close() - info, err := apiclient.EnvironmentInfo() - if err != nil { - return errors.Annotate(err, "cannot get information for environment") - } - - if !c.assumeYes { - fmt.Fprintf(ctx.Stdout, destroyEnvMsg, c.envName, info.ProviderType) - - scanner := bufio.NewScanner(ctx.Stdin) - scanner.Scan() - err := scanner.Err() - if err != nil && err != io.EOF { - return errors.Annotate(err, "environment destruction aborted") - } - answer := strings.ToLower(scanner.Text()) - if answer != "y" && answer != "yes" { - return stderrors.New("environment destruction aborted") - } - } - - if info.UUID == info.ServerUUID { - if !hasBootstrapCfg { - // serverEnviron will be nil as we didn't have the jenv bootstrap - // config to build it. But we do have a connection to the API - // server, so get the config from there. - bootstrapCfg, err := apiclient.EnvironmentGet() - if err != nil { - return errors.Annotate(err, "environment destruction failed") - } - serverEnviron, err = getServerEnv(bootstrapCfg) - if err != nil { - return errors.Annotate(err, "environment destruction failed") - } - } - - if err := c.destroyEnv(apiclient); err != nil { - return errors.Annotate(err, "environment destruction failed") - } - if err := environs.Destroy(serverEnviron, store); err != nil { - return errors.Annotate(err, "environment destruction failed") - } - return environs.DestroyInfo(c.envName, store) - } - - // If this is not the server environment, there is no bootstrap info and - // we do not call Destroy on the provider. Destroying the environment via - // the API and cleaning up the jenv file is sufficient. - if err := c.destroyEnv(apiclient); err != nil { - errors.Annotate(err, "cannot destroy environment") - } - return environs.DestroyInfo(c.envName, store) -} - -func getServerEnv(bootstrapCfg map[string]interface{}) (environs.Environ, error) { - cfg, err := config.New(config.NoDefaults, bootstrapCfg) - if err != nil { - return nil, errors.Trace(err) - } - return environs.New(cfg) -} - -func (c *DestroyEnvironmentCommand) destroyEnv(apiclient *api.Client) (result error) { - defer func() { - result = c.ensureUserFriendlyErrorLog(result) - }() - err := apiclient.DestroyEnvironment() - if cmdErr := processDestroyError(err); cmdErr != nil { - return cmdErr - } - - return nil -} - -// processDestroyError determines how to format error message based on its code. -// Note that CodeNotImplemented errors have not be propogated in previous implementation. -// This behaviour was preserved. -func processDestroyError(err error) error { - if err == nil || params.IsCodeNotImplemented(err) { - return nil - } - if params.IsCodeOperationBlocked(err) { - return err - } - return errors.Annotate(err, "destroying environment") -} - -// ensureUserFriendlyErrorLog ensures that error will be logged and displayed -// in a user-friendly manner with readable and digestable error message. -func (c *DestroyEnvironmentCommand) ensureUserFriendlyErrorLog(err error) error { - if err == nil { - return nil - } - if params.IsCodeOperationBlocked(err) { - return block.ProcessBlockedError(err, block.BlockDestroy) - } - logger.Errorf(stdFailureMsg, c.envName) - return err -} - -var destroyEnvMsg = ` -WARNING! this command will destroy the %q environment (type: %s) -This includes all machines, services, data and other resources. - -Continue [y/N]? `[1:] - -var stdFailureMsg = `failed to destroy environment %q - -If the environment is unusable, then you may run - - juju destroy-environment --force - -to forcefully destroy the environment. Upon doing so, review -your environment provider console for any resources that need -to be cleaned up. Using force will also by-pass destroy-envrionment block. - -` === removed file 'src/github.com/juju/juju/cmd/juju/destroyenvironment_test.go' --- src/github.com/juju/juju/cmd/juju/destroyenvironment_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/destroyenvironment_test.go 1970-01-01 00:00:00 +0000 @@ -1,357 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "bytes" - - "github.com/juju/cmd" - "github.com/juju/errors" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/cmd/envcmd" - cmdtesting "github.com/juju/juju/cmd/testing" - "github.com/juju/juju/environs" - "github.com/juju/juju/environs/configstore" - "github.com/juju/juju/instance" - "github.com/juju/juju/juju/testing" - "github.com/juju/juju/provider/dummy" - coretesting "github.com/juju/juju/testing" - "github.com/juju/juju/testing/factory" -) - -type destroyEnvSuite struct { - testing.JujuConnSuite - CmdBlockHelper -} - -func (s *destroyEnvSuite) SetUpTest(c *gc.C) { - s.JujuConnSuite.SetUpTest(c) - s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) - c.Assert(s.CmdBlockHelper, gc.NotNil) - s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) -} - -var _ = gc.Suite(&destroyEnvSuite{}) - -func (s *destroyEnvSuite) TestDestroyEnvironmentCommand(c *gc.C) { - // Prepare the environment so we can destroy it. - _, err := environs.PrepareFromName("dummyenv", envcmd.BootstrapContext(cmdtesting.NullContext(c)), s.ConfigStore) - c.Assert(err, jc.ErrorIsNil) - - // check environment is mandatory - opc, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand)) - c.Check(<-errc, gc.Equals, NoEnvironmentError) - - // normal destroy - opc, errc = cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand), "dummyenv", "--yes") - c.Check(<-errc, gc.IsNil) - c.Check((<-opc).(dummy.OpDestroy).Env, gc.Equals, "dummyenv") - - // Verify that the environment information has been removed. - _, err = s.ConfigStore.ReadInfo("dummyenv") - c.Assert(err, jc.Satisfies, errors.IsNotFound) -} - -// startEnvironment prepare the environment so we can destroy it. -func (s *destroyEnvSuite) startEnvironment(c *gc.C, desiredEnvName string) { - _, err := environs.PrepareFromName(desiredEnvName, envcmd.BootstrapContext(cmdtesting.NullContext(c)), s.ConfigStore) - c.Assert(err, jc.ErrorIsNil) -} - -func (s *destroyEnvSuite) checkDestroyEnvironment(c *gc.C, blocked, force bool) { - //Setup environment - envName := "dummyenv" - s.startEnvironment(c, envName) - if blocked { - s.BlockDestroyEnvironment(c, "checkDestroyEnvironment") - } - opc := make(chan dummy.Operation) - errc := make(chan error) - if force { - opc, errc = cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand), envName, "--yes", "--force") - } else { - opc, errc = cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand), envName, "--yes") - } - if force || !blocked { - c.Check(<-errc, gc.IsNil) - c.Check((<-opc).(dummy.OpDestroy).Env, gc.Equals, envName) - // Verify that the environment information has been removed. - _, err := s.ConfigStore.ReadInfo(envName) - c.Assert(err, jc.Satisfies, errors.IsNotFound) - } else { - c.Check(<-errc, gc.Not(gc.IsNil)) - c.Check((<-opc), gc.IsNil) - // Verify that the environment information has not been removed. - _, err := s.ConfigStore.ReadInfo(envName) - c.Assert(err, jc.ErrorIsNil) - } -} - -func (s *destroyEnvSuite) TestDestroyLockedEnvironment(c *gc.C) { - // lock environment: can't destroy locked environment - s.checkDestroyEnvironment(c, true, false) -} - -func (s *destroyEnvSuite) TestDestroyUnlockedEnvironment(c *gc.C) { - s.checkDestroyEnvironment(c, false, false) -} - -func (s *destroyEnvSuite) TestForceDestroyLockedEnvironment(c *gc.C) { - s.checkDestroyEnvironment(c, true, true) -} - -func (s *destroyEnvSuite) TestForceDestroyUnlockedEnvironment(c *gc.C) { - s.checkDestroyEnvironment(c, false, true) -} - -func (s *destroyEnvSuite) TestDestroyEnvironmentCommandEFlag(c *gc.C) { - // Prepare the environment so we can destroy it. - _, err := environs.PrepareFromName("dummyenv", envcmd.BootstrapContext(cmdtesting.NullContext(c)), s.ConfigStore) - c.Assert(err, jc.ErrorIsNil) - - // check that either environment or the flag is mandatory - opc, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand)) - c.Check(<-errc, gc.Equals, NoEnvironmentError) - - // We don't allow them to supply both entries at the same time - opc, errc = cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand), "-e", "dummyenv", "dummyenv", "--yes") - c.Check(<-errc, gc.Equals, DoubleEnvironmentError) - // We treat --environment the same way - opc, errc = cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand), "--environment", "dummyenv", "dummyenv", "--yes") - c.Check(<-errc, gc.Equals, DoubleEnvironmentError) - - // destroy using the -e flag - opc, errc = cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand), "-e", "dummyenv", "--yes") - c.Check(<-errc, gc.IsNil) - c.Check((<-opc).(dummy.OpDestroy).Env, gc.Equals, "dummyenv") - - // Verify that the environment information has been removed. - _, err = s.ConfigStore.ReadInfo("dummyenv") - c.Assert(err, jc.Satisfies, errors.IsNotFound) -} - -func (s *destroyEnvSuite) TestDestroyEnvironmentCommandEmptyJenv(c *gc.C) { - oldinfo, err := s.ConfigStore.ReadInfo("dummyenv") - info := s.ConfigStore.CreateInfo("dummy-no-bootstrap") - info.SetAPICredentials(oldinfo.APICredentials()) - info.SetAPIEndpoint(oldinfo.APIEndpoint()) - err = info.Write() - c.Assert(err, jc.ErrorIsNil) - - opc, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand), "dummy-no-bootstrap", "--yes") - c.Check(<-errc, gc.IsNil) - c.Check((<-opc).(dummy.OpDestroy).Env, gc.Equals, "dummyenv") - - // Verify that the environment information has been removed. - _, err = s.ConfigStore.ReadInfo("dummyenv") - c.Assert(err, jc.Satisfies, errors.IsNotFound) -} - -func (s *destroyEnvSuite) TestDestroyEnvironmentCommandNonStateServer(c *gc.C) { - s.setupHostedEnviron(c, "dummy-non-state-server") - opc, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand), "dummy-non-state-server", "--yes") - c.Check(<-errc, gc.IsNil) - // Check that there are no operations on the provider, we do not want to call - // Destroy on it. - c.Check(<-opc, gc.IsNil) - - _, err := s.ConfigStore.ReadInfo("dummy-non-state-server") - c.Assert(err, jc.Satisfies, errors.IsNotFound) -} - -func (s *destroyEnvSuite) TestForceDestroyEnvironmentCommandOnNonStateServerFails(c *gc.C) { - s.setupHostedEnviron(c, "dummy-non-state-server") - opc, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand), "dummy-non-state-server", "--yes", "--force") - c.Check(<-errc, gc.ErrorMatches, "cannot force destroy environment without bootstrap information") - c.Check(<-opc, gc.IsNil) - - serverInfo, err := s.ConfigStore.ReadInfo("dummy-non-state-server") - c.Assert(err, jc.ErrorIsNil) - c.Assert(serverInfo, gc.Not(gc.IsNil)) -} - -func (s *destroyEnvSuite) TestForceDestroyEnvironmentCommandOnNonStateServerNoConfimFails(c *gc.C) { - s.setupHostedEnviron(c, "dummy-non-state-server") - opc, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand), "dummy-non-state-server", "--force") - c.Check(<-errc, gc.ErrorMatches, "cannot force destroy environment without bootstrap information") - c.Check(<-opc, gc.IsNil) - - serverInfo, err := s.ConfigStore.ReadInfo("dummy-non-state-server") - c.Assert(err, jc.ErrorIsNil) - c.Assert(serverInfo, gc.Not(gc.IsNil)) -} - -func (s *destroyEnvSuite) TestDestroyEnvironmentCommandTwiceOnNonStateServer(c *gc.C) { - s.setupHostedEnviron(c, "dummy-non-state-server") - oldInfo, err := s.ConfigStore.ReadInfo("dummy-non-state-server") - c.Assert(err, jc.ErrorIsNil) - - opc, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand), "dummy-non-state-server", "--yes") - c.Check(<-errc, gc.IsNil) - c.Check(<-opc, gc.IsNil) - - _, err = s.ConfigStore.ReadInfo("dummy-non-state-server") - c.Assert(err, jc.Satisfies, errors.IsNotFound) - - // Simluate another client calling destroy on the same environment. This - // client will have a local cache of the environ info, so write it back out. - info := s.ConfigStore.CreateInfo("dummy-non-state-server") - info.SetAPIEndpoint(oldInfo.APIEndpoint()) - info.SetAPICredentials(oldInfo.APICredentials()) - err = info.Write() - c.Assert(err, jc.ErrorIsNil) - - // Call destroy again. - context, err := coretesting.RunCommand(c, new(DestroyEnvironmentCommand), "dummy-non-state-server", "--yes") - c.Assert(err, jc.ErrorIsNil) - c.Assert(coretesting.Stderr(context), gc.Equals, "environment not found, removing config file\n") - - // Check that the client's cached info has been removed. - _, err = s.ConfigStore.ReadInfo("dummy-non-state-server") - c.Assert(err, jc.Satisfies, errors.IsNotFound) -} - -func (s *destroyEnvSuite) setupHostedEnviron(c *gc.C, name string) { - st := s.Factory.MakeEnvironment(c, &factory.EnvParams{ - Name: name, - Prepare: true, - ConfigAttrs: coretesting.Attrs{"state-server": false}, - }) - defer st.Close() - - ports, err := st.APIHostPorts() - c.Assert(err, jc.ErrorIsNil) - info := s.ConfigStore.CreateInfo(name) - endpoint := configstore.APIEndpoint{ - CACert: st.CACert(), - EnvironUUID: st.EnvironUUID(), - Addresses: []string{ports[0][0].String()}, - } - info.SetAPIEndpoint(endpoint) - - ssinfo, err := s.ConfigStore.ReadInfo("dummyenv") - c.Assert(err, jc.ErrorIsNil) - info.SetAPICredentials(ssinfo.APICredentials()) - err = info.Write() - c.Assert(err, jc.ErrorIsNil) -} - -func (s *destroyEnvSuite) TestDestroyEnvironmentCommandBroken(c *gc.C) { - oldinfo, err := s.ConfigStore.ReadInfo("dummyenv") - c.Assert(err, jc.ErrorIsNil) - bootstrapConfig := oldinfo.BootstrapConfig() - apiEndpoint := oldinfo.APIEndpoint() - apiCredentials := oldinfo.APICredentials() - err = oldinfo.Destroy() - c.Assert(err, jc.ErrorIsNil) - newinfo := s.ConfigStore.CreateInfo("dummyenv") - - bootstrapConfig["broken"] = "Destroy" - newinfo.SetBootstrapConfig(bootstrapConfig) - newinfo.SetAPIEndpoint(apiEndpoint) - newinfo.SetAPICredentials(apiCredentials) - err = newinfo.Write() - c.Assert(err, jc.ErrorIsNil) - - // Prepare the environment so we can destroy it. - _, err = environs.PrepareFromName("dummyenv", envcmd.BootstrapContext(cmdtesting.NullContext(c)), s.ConfigStore) - c.Assert(err, jc.ErrorIsNil) - - // destroy with broken environment - opc, errc := cmdtesting.RunCommand(cmdtesting.NullContext(c), new(DestroyEnvironmentCommand), "dummyenv", "--yes") - op, ok := (<-opc).(dummy.OpDestroy) - c.Assert(ok, jc.IsTrue) - c.Assert(op.Error, gc.ErrorMatches, ".*dummy.Destroy is broken") - c.Check(<-errc, gc.ErrorMatches, ".*dummy.Destroy is broken") - c.Check(<-opc, gc.IsNil) -} - -func (*destroyEnvSuite) TestDestroyEnvironmentCommandConfirmationFlag(c *gc.C) { - com := new(DestroyEnvironmentCommand) - c.Check(coretesting.InitCommand(com, []string{"dummyenv"}), gc.IsNil) - c.Check(com.assumeYes, jc.IsFalse) - - com = new(DestroyEnvironmentCommand) - c.Check(coretesting.InitCommand(com, []string{"dummyenv", "-y"}), gc.IsNil) - c.Check(com.assumeYes, jc.IsTrue) - - com = new(DestroyEnvironmentCommand) - c.Check(coretesting.InitCommand(com, []string{"dummyenv", "--yes"}), gc.IsNil) - c.Check(com.assumeYes, jc.IsTrue) -} - -func (s *destroyEnvSuite) TestDestroyEnvironmentCommandConfirmation(c *gc.C) { - var stdin, stdout bytes.Buffer - ctx, err := cmd.DefaultContext() - c.Assert(err, jc.ErrorIsNil) - ctx.Stdout = &stdout - ctx.Stdin = &stdin - - // Prepare the environment so we can destroy it. - env, err := environs.PrepareFromName("dummyenv", envcmd.BootstrapContext(cmdtesting.NullContext(c)), s.ConfigStore) - c.Assert(err, jc.ErrorIsNil) - - assertEnvironNotDestroyed(c, env, s.ConfigStore) - - // Ensure confirmation is requested if "-y" is not specified. - stdin.WriteString("n") - opc, errc := cmdtesting.RunCommand(ctx, new(DestroyEnvironmentCommand), "dummyenv") - c.Check(<-errc, gc.ErrorMatches, "environment destruction aborted") - c.Check(<-opc, gc.IsNil) - c.Check(stdout.String(), gc.Matches, "WARNING!.*dummyenv.*\\(type: dummy\\)(.|\n)*") - assertEnvironNotDestroyed(c, env, s.ConfigStore) - - // EOF on stdin: equivalent to answering no. - stdin.Reset() - stdout.Reset() - opc, errc = cmdtesting.RunCommand(ctx, new(DestroyEnvironmentCommand), "dummyenv") - c.Check(<-opc, gc.IsNil) - c.Check(<-errc, gc.ErrorMatches, "environment destruction aborted") - assertEnvironNotDestroyed(c, env, s.ConfigStore) - - // "--yes" passed: no confirmation request. - stdin.Reset() - stdout.Reset() - opc, errc = cmdtesting.RunCommand(ctx, new(DestroyEnvironmentCommand), "dummyenv", "--yes") - c.Check(<-errc, gc.IsNil) - c.Check((<-opc).(dummy.OpDestroy).Env, gc.Equals, "dummyenv") - c.Check(stdout.String(), gc.Equals, "") - assertEnvironDestroyed(c, env, s.ConfigStore) - - // Any of casing of "y" and "yes" will confirm. - for _, answer := range []string{"y", "Y", "yes", "YES"} { - // Prepare the environment so we can destroy it. - s.Reset(c) - env, err := environs.PrepareFromName("dummyenv", envcmd.BootstrapContext(cmdtesting.NullContext(c)), s.ConfigStore) - c.Assert(err, jc.ErrorIsNil) - - stdin.Reset() - stdout.Reset() - stdin.WriteString(answer) - opc, errc = cmdtesting.RunCommand(ctx, new(DestroyEnvironmentCommand), "dummyenv") - c.Check(<-errc, gc.IsNil) - c.Check((<-opc).(dummy.OpDestroy).Env, gc.Equals, "dummyenv") - c.Check(stdout.String(), gc.Matches, "WARNING!.*dummyenv.*\\(type: dummy\\)(.|\n)*") - assertEnvironDestroyed(c, env, s.ConfigStore) - } -} - -func assertEnvironDestroyed(c *gc.C, env environs.Environ, store configstore.Storage) { - _, err := store.ReadInfo(env.Config().Name()) - c.Assert(err, jc.Satisfies, errors.IsNotFound) - - _, err = env.Instances([]instance.Id{"invalid"}) - c.Assert(err, gc.ErrorMatches, "environment has been destroyed") -} - -func assertEnvironNotDestroyed(c *gc.C, env environs.Environ, store configstore.Storage) { - info, err := store.ReadInfo(env.Config().Name()) - c.Assert(err, jc.ErrorIsNil) - c.Assert(info.Initialized(), jc.IsTrue) - - _, err = environs.NewFromName(env.Config().Name(), store) - c.Assert(err, jc.ErrorIsNil) -} === removed file 'src/github.com/juju/juju/cmd/juju/endpoint.go' --- src/github.com/juju/juju/cmd/juju/endpoint.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/endpoint.go 1970-01-01 00:00:00 +0000 @@ -1,74 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "github.com/juju/cmd" - "github.com/juju/errors" - "launchpad.net/gnuflag" - - "github.com/juju/juju/cmd/envcmd" -) - -// EndpointCommand returns the API endpoints -type EndpointCommand struct { - envcmd.EnvCommandBase - out cmd.Output - refresh bool - all bool -} - -const endpointDoc = ` -Returns the address(es) of the current API server formatted as host:port. - -Without arguments apt-endpoints returns the last endpoint used to successfully -connect to the API server. If a cached endpoints information is available from -the current environment's .jenv file, it is returned without trying to connect -to the API server. When no cache is available or --refresh is given, api-endpoints -connects to the API server, retrieves all known endpoints and updates the .jenv -file before returning the first one. Example: -$ juju api-endpoints -10.0.3.1:17070 - -If --all is given, api-endpoints returns all known endpoints. Example: -$ juju api-endpoints --all - 10.0.3.1:17070 - localhost:170170 - -The first endpoint is guaranteed to be an IP address and port. If a single endpoint -is available and it's a hostname, juju tries to resolve it locally first. - -Additionally, you can use the --format argument to specify the output format. -Supported formats are: "yaml", "json", or "smart" (default - host:port, one per line). -` - -func (c *EndpointCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "api-endpoints", - Args: "", - Purpose: "print the API server address(es)", - Doc: endpointDoc, - } -} - -func (c *EndpointCommand) SetFlags(f *gnuflag.FlagSet) { - c.out.AddFlags(f, "smart", cmd.DefaultFormatters) - f.BoolVar(&c.refresh, "refresh", false, "connect to the API to ensure an up-to-date endpoint location") - f.BoolVar(&c.all, "all", false, "display all known endpoints, not just the first one") -} - -// Print out the addresses of the API server endpoints. -func (c *EndpointCommand) Run(ctx *cmd.Context) error { - apiendpoint, err := endpoint(c.EnvCommandBase, c.refresh) - if err != nil && !errors.IsNotFound(err) { - return err - } - if errors.IsNotFound(err) || len(apiendpoint.Addresses) == 0 { - return errors.Errorf("no API endpoints available") - } - if c.all { - return c.out.Write(ctx, apiendpoint.Addresses) - } - return c.out.Write(ctx, apiendpoint.Addresses[0:1]) -} === removed file 'src/github.com/juju/juju/cmd/juju/endpoint_test.go' --- src/github.com/juju/juju/cmd/juju/endpoint_test.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/endpoint_test.go 1970-01-01 00:00:00 +0000 @@ -1,384 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/environs/config" - "github.com/juju/juju/environs/configstore" - envtesting "github.com/juju/juju/environs/testing" - "github.com/juju/juju/instance" - "github.com/juju/juju/juju/testing" - "github.com/juju/juju/network" - "github.com/juju/juju/provider/dummy" - coretesting "github.com/juju/juju/testing" -) - -type EndpointSuite struct { - testing.JujuConnSuite - - restoreTimeouts func() -} - -var _ = gc.Suite(&EndpointSuite{}) - -func (s *EndpointSuite) SetUpSuite(c *gc.C) { - // Use very short attempt strategies when getting instance addresses. - s.restoreTimeouts = envtesting.PatchAttemptStrategies() - s.JujuConnSuite.SetUpSuite(c) -} - -func (s *EndpointSuite) TearDownSuite(c *gc.C) { - s.JujuConnSuite.TearDownSuite(c) - s.restoreTimeouts() -} - -func (s *EndpointSuite) TestNoEndpoints(c *gc.C) { - // Reset all addresses. - s.setCachedAPIAddresses(c) - s.setServerAPIAddresses(c) - s.assertCachedAddresses(c) - - stdout, stderr, err := s.runCommand(c) - c.Assert(err, gc.ErrorMatches, "no API endpoints available") - c.Assert(stdout, gc.Equals, "") - c.Assert(stderr, gc.Equals, "") - - s.assertCachedAddresses(c) -} - -func (s *EndpointSuite) TestCachedAddressesUsedIfAvailable(c *gc.C) { - addresses := network.NewHostPorts(1234, - "10.0.0.1:1234", - "[2001:db8::1]:1234", - "0.1.2.3:1234", - "[fc00::1]:1234", - ) - // Set the cached addresses. - s.setCachedAPIAddresses(c, addresses...) - // Clear instance/state addresses to ensure we can't connect to - // the API server. - s.setServerAPIAddresses(c) - - testRun := func(i int, envPreferIPv6, bootPreferIPv6 bool) { - c.Logf( - "\ntest %d: prefer-ipv6 environ=%v, bootstrap=%v", - i, envPreferIPv6, bootPreferIPv6, - ) - s.setPreferIPv6EnvironConfig(c, envPreferIPv6) - s.setPreferIPv6BootstrapConfig(c, bootPreferIPv6) - - // Without arguments, verify the first cached address is returned. - s.runAndCheckOutput(c, "smart", expectOutput(addresses[0])) - s.assertCachedAddresses(c, addresses...) - - // With --all, ensure all are returned. - s.runAndCheckOutput(c, "smart", expectOutput(addresses...), "--all") - s.assertCachedAddresses(c, addresses...) - } - - // Ensure regardless of the prefer-ipv6 value we have the same - // result. - for i, envPreferIPv6 := range []bool{true, false} { - for j, bootPreferIPv6 := range []bool{true, false} { - testRun(i+j, envPreferIPv6, bootPreferIPv6) - } - } -} - -func (s *EndpointSuite) TestRefresh(c *gc.C) { - testRun := func(i int, address network.HostPort, explicitRefresh bool) { - c.Logf("\ntest %d: address=%q, explicitRefresh=%v", i, address, explicitRefresh) - - // Cache the address. - s.setCachedAPIAddresses(c, address) - s.assertCachedAddresses(c, address) - // Clear instance/state addresses to ensure only the cached - // one will be used. - s.setServerAPIAddresses(c) - - // Ensure we get and cache the first address (i.e. no changes) - if explicitRefresh { - s.runAndCheckOutput(c, "smart", expectOutput(address), "--refresh") - } else { - s.runAndCheckOutput(c, "smart", expectOutput(address)) - } - s.assertCachedAddresses(c, address) - } - - // Test both IPv4 and IPv6 endpoints separately, first with - // implicit refresh, then explicit. - for i, explicitRefresh := range []bool{true, false} { - for j, addr := range s.addressesWithAPIPort(c, "localhost", "::1") { - testRun(i+j, addr, explicitRefresh) - } - } -} - -func (s *EndpointSuite) TestSortingAndFilteringBeforeCachingRespectsPreferIPv6(c *gc.C) { - // Set the instance/state addresses to a mix of IPv4 and IPv6 - // addresses of all kinds. - addresses := s.addressesWithAPIPort(c, - // The following two are needed to actually connect to the - // test API server. - "127.0.0.1", - "::1", - // Other examples. - "192.0.0.0", - "2001:db8::1", - "169.254.1.2", // link-local - will be removed. - "fd00::1", - "localhost", // will be put at the top as last successful. - "ff01::1", // link-local - will be removed. - "fc00::1", - "localhost", // will be removed as a duplicate. - "0.1.2.3", - "127.0.1.1", // removed as a duplicate. - "::1", // removed as a duplicate. - "10.0.0.1", - "8.8.8.8", - ) - s.setServerAPIAddresses(c, addresses...) - - // Clear cached the address to force a refresh. - s.setCachedAPIAddresses(c) - s.assertCachedAddresses(c) - // Set prefer-ipv6 to true first. - s.setPreferIPv6BootstrapConfig(c, true) - - // Build the expected addresses list, after processing. - expectAddresses := s.addressesWithAPIPort(c, - "localhost", // This is always on top. - "2001:db8::1", - "0.1.2.3", - "192.0.0.0", - "8.8.8.8", - "fc00::1", - "fd00::1", - "10.0.0.1", - ) - s.runAndCheckOutput(c, "smart", expectOutput(expectAddresses...), "--all") - s.assertCachedAddresses(c, expectAddresses...) - - // Now run it again with prefer-ipv6: false. - // But first reset the cached addresses.. - s.setCachedAPIAddresses(c) - s.assertCachedAddresses(c) - s.setPreferIPv6BootstrapConfig(c, false) - - // Rebuild the expected addresses and rebuild them so IPv4 comes - // before IPv6. - expectAddresses = s.addressesWithAPIPort(c, - "localhost", // This is always on top. - "0.1.2.3", - "192.0.0.0", - "8.8.8.8", - "2001:db8::1", - "10.0.0.1", - "fc00::1", - "fd00::1", - ) - s.runAndCheckOutput(c, "smart", expectOutput(expectAddresses...), "--all") - s.assertCachedAddresses(c, expectAddresses...) -} - -func (s *EndpointSuite) TestAllFormats(c *gc.C) { - addresses := s.addressesWithAPIPort(c, - "127.0.0.1", - "8.8.8.8", - "2001:db8::1", - "::1", - "10.0.0.1", - "fc00::1", - ) - s.setServerAPIAddresses(c) - s.setCachedAPIAddresses(c, addresses...) - s.assertCachedAddresses(c, addresses...) - - for i, test := range []struct { - about string - args []string - format string - output []network.HostPort - }{{ - about: "default format (smart), no args", - format: "smart", - output: addresses[0:1], - }, { - about: "default format (smart), with --all", - args: []string{"--all"}, - format: "smart", - output: addresses, - }, { - about: "JSON format, without --all", - args: []string{"--format", "json"}, - format: "json", - output: addresses[0:1], - }, { - about: "JSON format, with --all", - args: []string{"--format", "json", "--all"}, - format: "json", - output: addresses, - }, { - about: "YAML format, without --all", - args: []string{"--format", "yaml"}, - format: "yaml", - output: addresses[0:1], - }, { - about: "YAML format, with --all", - args: []string{"--format", "yaml", "--all"}, - format: "yaml", - output: addresses, - }} { - c.Logf("\ntest %d: %s", i, test.about) - s.runAndCheckOutput(c, test.format, expectOutput(test.output...), test.args...) - } -} - -// runCommand runs the api-endpoints command with the given arguments -// and returns the output and any error. -func (s *EndpointSuite) runCommand(c *gc.C, args ...string) (string, string, error) { - command := &EndpointCommand{} - ctx, err := coretesting.RunCommand(c, envcmd.Wrap(command), args...) - if err != nil { - return "", "", err - } - return coretesting.Stdout(ctx), coretesting.Stderr(ctx), nil -} - -// runAndCheckOutput runs api-endpoints expecting no error and -// compares the output for the given format. -func (s *EndpointSuite) runAndCheckOutput(c *gc.C, format string, output []interface{}, args ...string) { - stdout, stderr, err := s.runCommand(c, args...) - if !c.Check(err, jc.ErrorIsNil) { - return - } - c.Check(stderr, gc.Equals, "") - switch format { - case "smart": - strOutput := "" - for _, line := range output { - strOutput += line.(string) + "\n" - } - c.Check(stdout, gc.Equals, strOutput) - case "json": - c.Check(stdout, jc.JSONEquals, output) - case "yaml": - c.Check(stdout, jc.YAMLEquals, output) - default: - c.Fatalf("unexpected format %q", format) - } -} - -// getStoreInfo returns the current environment's EnvironInfo. -func (s *EndpointSuite) getStoreInfo(c *gc.C) configstore.EnvironInfo { - env, err := s.State.Environment() - c.Assert(err, jc.ErrorIsNil) - info, err := s.ConfigStore.ReadInfo(env.Name()) - c.Assert(err, jc.ErrorIsNil) - return info -} - -// setPreferIPv6EnvironConfig sets the "prefer-ipv6" environment -// setting to given value. -func (s *EndpointSuite) setPreferIPv6EnvironConfig(c *gc.C, value bool) { - // Technically, because prefer-ipv6 is an immutable setting, what - // follows should be impossible, but the dummy provider doesn't - // seem to validate the new config against the current (old) one - // when calling SetConfig(). - allAttrs := s.Environ.Config().AllAttrs() - allAttrs["prefer-ipv6"] = value - cfg, err := config.New(config.NoDefaults, allAttrs) - c.Assert(err, jc.ErrorIsNil) - err = s.Environ.SetConfig(cfg) - c.Assert(err, jc.ErrorIsNil) - setValue := cfg.AllAttrs()["prefer-ipv6"].(bool) - c.Logf("environ config prefer-ipv6 set to %v", setValue) -} - -// setPreferIPv6BootstrapConfig sets the "prefer-ipv6" setting to the -// given value on the current environment's bootstrap config by -// recreating it (the only way to change bootstrap config once set). -func (s *EndpointSuite) setPreferIPv6BootstrapConfig(c *gc.C, value bool) { - currentInfo := s.getStoreInfo(c) - endpoint := currentInfo.APIEndpoint() - creds := currentInfo.APICredentials() - bootstrapConfig := currentInfo.BootstrapConfig() - delete(bootstrapConfig, "prefer-ipv6") - - // The only way to change the bootstrap config is to recreate the - // info. - err := currentInfo.Destroy() - c.Assert(err, jc.ErrorIsNil) - newInfo := s.ConfigStore.CreateInfo(s.Environ.Config().Name()) - newInfo.SetAPICredentials(creds) - newInfo.SetAPIEndpoint(endpoint) - newCfg := make(coretesting.Attrs) - newCfg["prefer-ipv6"] = value - newInfo.SetBootstrapConfig(newCfg.Merge(bootstrapConfig)) - err = newInfo.Write() - c.Assert(err, jc.ErrorIsNil) - setValue := newInfo.BootstrapConfig()["prefer-ipv6"].(bool) - c.Logf("bootstrap config prefer-ipv6 set to %v", setValue) -} - -// setCachedAPIAddresses sets the given addresses on the cached -// EnvironInfo endpoint. APIEndpoint.Hostnames are not touched, -// because the interactions between Addresses and Hostnames are -// separately tested in juju/api_test.go -func (s *EndpointSuite) setCachedAPIAddresses(c *gc.C, addresses ...network.HostPort) { - info := s.getStoreInfo(c) - endpoint := info.APIEndpoint() - endpoint.Addresses = network.HostPortsToStrings(addresses) - info.SetAPIEndpoint(endpoint) - err := info.Write() - c.Assert(err, jc.ErrorIsNil) - c.Logf("cached addresses set to %v", info.APIEndpoint().Addresses) -} - -// setServerAPIAddresses sets the given addresses on the dummy -// bootstrap instance and in state. -func (s *EndpointSuite) setServerAPIAddresses(c *gc.C, addresses ...network.HostPort) { - insts, err := s.Environ.Instances([]instance.Id{dummy.BootstrapInstanceId}) - c.Assert(err, jc.ErrorIsNil) - err = s.State.SetAPIHostPorts([][]network.HostPort{addresses}) - c.Assert(err, jc.ErrorIsNil) - dummy.SetInstanceAddresses(insts[0], network.HostsWithoutPort(addresses)) - instAddrs, err := insts[0].Addresses() - c.Assert(err, jc.ErrorIsNil) - stateAddrs, err := s.State.APIHostPorts() - c.Assert(err, jc.ErrorIsNil) - c.Logf("instance addresses set to %v", instAddrs) - c.Logf("state addresses set to %v", stateAddrs) -} - -// addressesWithAPIPort returns the given addresses appending the test -// API server listening port to each one. -func (s *EndpointSuite) addressesWithAPIPort(c *gc.C, addresses ...string) []network.HostPort { - apiPort := s.Environ.Config().APIPort() - return network.NewHostPorts(apiPort, addresses...) -} - -// assertCachedAddresses ensures the endpoint addresses (not -// hostnames) stored in the store match the given ones. -// APIEndpoint.Hostnames and APIEndpoint.Addresses interactions are -// separately testing in juju/api_test.go. -func (s *EndpointSuite) assertCachedAddresses(c *gc.C, addresses ...network.HostPort) { - info := s.getStoreInfo(c) - strAddresses := network.HostPortsToStrings(addresses) - c.Assert(info.APIEndpoint().Addresses, jc.DeepEquals, strAddresses) -} - -// expectOutput is a helper used to construct the expected ouput -// argument to runAndCheckOutput. -func expectOutput(addresses ...network.HostPort) []interface{} { - result := make([]interface{}, len(addresses)) - for i, addr := range addresses { - result[i] = addr.NetAddr() - } - return result -} === removed file 'src/github.com/juju/juju/cmd/juju/ensureavailability.go' --- src/github.com/juju/juju/cmd/juju/ensureavailability.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/ensureavailability.go 1970-01-01 00:00:00 +0000 @@ -1,245 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "bytes" - "fmt" - "strings" - - "github.com/juju/cmd" - "github.com/juju/errors" - "github.com/juju/names" - "launchpad.net/gnuflag" - - "github.com/juju/juju/api/highavailability" - "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/cmd/juju/block" - "github.com/juju/juju/constraints" - "github.com/juju/juju/instance" -) - -// EnsureAvailabilityCommand makes the system highly available. -type EnsureAvailabilityCommand struct { - envcmd.EnvCommandBase - out cmd.Output - haClient EnsureAvailabilityClient - - // NumStateServers specifies the number of state servers to make available. - NumStateServers 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 state servers. If there are more state servers 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 -} - -const ensureAvailabilityDoc = ` -To ensure availability of deployed services, the Juju infrastructure -must itself be highly available. Ensure-availability must be called -to ensure that the specified number of state servers are made available. - -An odd number of state servers is required. - -Examples: - juju ensure-availability - Ensure that the system is still in highly available mode. If - there is only 1 state server running, this will ensure there - are 3 running. If you have previously requested more than 3, - then that number will be ensured. - juju ensure-availability -n 5 --series=trusty - Ensure that 5 state servers are available, with newly created - state server machines having the "trusty" series. - juju ensure-availability -n 7 --constraints mem=8G - Ensure that 7 state servers are available, with newly created - state server machines having the default series, and at least - 8GB RAM. - juju ensure-availability -n 7 --to server1,server2 --constraints mem=8G - Ensure that 7 state servers are available, with machines server1 and - server2 used first, and if necessary, newly created state server - machines having the default series, and at least 8GB RAM. -` - -// formatSimple marshals value to a yaml-formatted []byte, unless value is nil. -func formatSimple(value interface{}) ([]byte, error) { - ensureAvailabilityResult, ok := value.(availabilityInfo) - if !ok { - return nil, fmt.Errorf("unexpected result type for ensure-availability call: %T", value) - } - - var buf bytes.Buffer - - for _, machineList := range []struct { - message string - list []string - }{ - { - "maintaining machines: %s\n", - ensureAvailabilityResult.Maintained, - }, - { - "adding machines: %s\n", - ensureAvailabilityResult.Added, - }, - { - "removing machines: %s\n", - ensureAvailabilityResult.Removed, - }, - { - "promoting machines: %s\n", - ensureAvailabilityResult.Promoted, - }, - { - "demoting machines: %s\n", - ensureAvailabilityResult.Demoted, - }, - { - "converting machines: %s\n", - ensureAvailabilityResult.Converted, - }, - } { - if len(machineList.list) == 0 { - continue - } - _, err := fmt.Fprintf(&buf, machineList.message, strings.Join(machineList.list, ", ")) - if err != nil { - return nil, err - } - } - - return buf.Bytes(), nil -} - -func (c *EnsureAvailabilityCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "ensure-availability", - Purpose: "ensure that sufficient state servers exist to provide redundancy", - Doc: ensureAvailabilityDoc, - } -} - -func (c *EnsureAvailabilityCommand) SetFlags(f *gnuflag.FlagSet) { - f.IntVar(&c.NumStateServers, "n", 0, "number of state servers to make available") - f.StringVar(&c.Series, "series", "", "the charm series") - f.StringVar(&c.PlacementSpec, "to", "", "the machine(s) to become state servers, bypasses constraints") - f.Var(constraints.ConstraintsValue{&c.Constraints}, "constraints", "additional machine constraints") - c.out.AddFlags(f, "simple", map[string]cmd.Formatter{ - "yaml": cmd.FormatYaml, - "json": cmd.FormatJson, - "simple": formatSimple, - }) - -} - -func (c *EnsureAvailabilityCommand) Init(args []string) error { - if c.NumStateServers < 0 || (c.NumStateServers%2 != 1 && c.NumStateServers != 0) { - return fmt.Errorf("must specify a number of state servers odd and non-negative") - } - if c.PlacementSpec != "" { - placementSpecs := strings.Split(c.PlacementSpec, ",") - c.Placement = make([]string, len(placementSpecs)) - for i, spec := range placementSpecs { - p, err := instance.ParsePlacement(strings.TrimSpace(spec)) - if err == nil && names.IsContainerMachine(p.Directive) { - return errors.New("ensure-availability cannot be used with container placement directives") - } - if err == nil && p.Scope == instance.MachineScope { - // Targeting machines is ok. - c.Placement[i] = p.String() - continue - } - if err != instance.ErrPlacementScopeMissing { - return fmt.Errorf("unsupported ensure-availability placement directive %q", spec) - } - c.Placement[i] = spec - } - } - return cmd.CheckEmpty(args) -} - -type availabilityInfo struct { - Maintained []string `json:"maintained,omitempty" yaml:"maintained,flow,omitempty"` - Removed []string `json:"removed,omitempty" yaml:"removed,flow,omitempty"` - Added []string `json:"added,omitempty" yaml:"added,flow,omitempty"` - Promoted []string `json:"promoted,omitempty" yaml:"promoted,flow,omitempty"` - Demoted []string `json:"demoted,omitempty" yaml:"demoted,flow,omitempty"` - Converted []string `json:"converted,omitempty" yaml:"converted,flow,omitempty"` -} - -// EnsureAvailabilityClient defines the methods -// on the client api that the ensure availability -// command calls. -type EnsureAvailabilityClient interface { - Close() error - EnsureAvailability( - numStateServers int, cons constraints.Value, series string, - placement []string) (params.StateServersChanges, error) -} - -func (c *EnsureAvailabilityCommand) getHAClient() (EnsureAvailabilityClient, error) { - if c.haClient != nil { - return c.haClient, nil - } - - root, err := c.NewAPIRoot() - if err != nil { - return nil, errors.Annotate(err, "cannot get API connection") - } - - // NewClient does not return an error, so we'll return nil - return highavailability.NewClient(root), nil -} - -// Run connects to the environment specified on the command line -// and calls EnsureAvailability. -func (c *EnsureAvailabilityCommand) Run(ctx *cmd.Context) error { - haClient, err := c.getHAClient() - if err != nil { - return err - } - - defer haClient.Close() - ensureAvailabilityResult, err := haClient.EnsureAvailability( - c.NumStateServers, - c.Constraints, - c.Series, - c.Placement, - ) - if err != nil { - return block.ProcessBlockedError(err, block.BlockChange) - } - - result := availabilityInfo{ - Added: machineTagsToIds(ensureAvailabilityResult.Added...), - Removed: machineTagsToIds(ensureAvailabilityResult.Removed...), - Maintained: machineTagsToIds(ensureAvailabilityResult.Maintained...), - Promoted: machineTagsToIds(ensureAvailabilityResult.Promoted...), - Demoted: machineTagsToIds(ensureAvailabilityResult.Demoted...), - Converted: machineTagsToIds(ensureAvailabilityResult.Converted...), - } - return c.out.Write(ctx, result) -} - -// Convert machine tags to ids, skipping any non-machine tags. -func machineTagsToIds(tags ...string) []string { - var result []string - - for _, rawTag := range tags { - tag, err := names.ParseTag(rawTag) - if err != nil { - continue - } - result = append(result, tag.Id()) - } - return result -} === removed file 'src/github.com/juju/juju/cmd/juju/ensureavailability_test.go' --- src/github.com/juju/juju/cmd/juju/ensureavailability_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/ensureavailability_test.go 1970-01-01 00:00:00 +0000 @@ -1,277 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "bytes" - "encoding/json" - "fmt" - "strings" - - "github.com/juju/cmd" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - goyaml "gopkg.in/yaml.v1" - - "github.com/juju/juju/apiserver/common" - "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/constraints" - "github.com/juju/juju/instance" - "github.com/juju/juju/juju/testing" - "github.com/juju/juju/state" - coretesting "github.com/juju/juju/testing" - "github.com/juju/juju/testing/factory" -) - -type EnsureAvailabilitySuite struct { - // TODO (cherylj) change this back to a FakeJujuHomeSuite to - // remove the mongo dependency once ensure-availability is - // moved under a supercommand again. - testing.JujuConnSuite - fake *fakeHAClient -} - -// invalidNumServers is a number of state servers that would -// never be generated by the ensure-availability command. -const invalidNumServers = -2 - -func (s *EnsureAvailabilitySuite) SetUpTest(c *gc.C) { - s.JujuConnSuite.SetUpTest(c) - - // Initialize numStateServers to an invalid number to validate - // that ensure-availability doesn't call into the API when its - // pre-checks fail - s.fake = &fakeHAClient{numStateServers: invalidNumServers} -} - -type fakeHAClient struct { - numStateServers int - cons constraints.Value - err error - series string - placement []string - result params.StateServersChanges -} - -func (f *fakeHAClient) Close() error { - return nil -} - -func (f *fakeHAClient) EnsureAvailability(numStateServers int, cons constraints.Value, - series string, placement []string) (params.StateServersChanges, error) { - - f.numStateServers = numStateServers - f.cons = cons - f.series = series - f.placement = placement - - if f.err != nil { - return f.result, f.err - } - - if numStateServers == 1 { - return f.result, nil - } - - // In the real HAClient, specifying a numStateServers value of 0 - // indicates that the default value (3) should be used - if numStateServers == 0 { - numStateServers = 3 - } - - f.result.Maintained = append(f.result.Maintained, "machine-0") - - for _, p := range placement { - m, err := instance.ParsePlacement(p) - if err == nil && m.Scope == instance.MachineScope { - f.result.Converted = append(f.result.Converted, "machine-"+m.Directive) - } - } - - // We may need to pretend that we added some machines. - for i := len(f.result.Converted) + 1; i < numStateServers; i++ { - f.result.Added = append(f.result.Added, fmt.Sprintf("machine-%d", i)) - } - - return f.result, nil -} - -var _ = gc.Suite(&EnsureAvailabilitySuite{}) - -func (s *EnsureAvailabilitySuite) runEnsureAvailability(c *gc.C, args ...string) (*cmd.Context, error) { - command := &EnsureAvailabilityCommand{haClient: s.fake} - return coretesting.RunCommand(c, envcmd.Wrap(command), args...) -} - -func (s *EnsureAvailabilitySuite) TestEnsureAvailability(c *gc.C) { - ctx, err := s.runEnsureAvailability(c, "-n", "1") - c.Assert(err, jc.ErrorIsNil) - c.Assert(coretesting.Stdout(ctx), gc.Equals, "") - - c.Assert(s.fake.numStateServers, 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) -} - -func (s *EnsureAvailabilitySuite) TestBlockEnsureAvailability(c *gc.C) { - s.fake.err = common.ErrOperationBlocked("TestBlockEnsureAvailability") - _, err := s.runEnsureAvailability(c, "-n", "1") - c.Assert(err, gc.ErrorMatches, cmd.ErrSilent.Error()) - - // msg is logged - stripped := strings.Replace(c.GetTestLog(), "\n", "", -1) - c.Check(stripped, gc.Matches, ".*TestBlockEnsureAvailability.*") -} - -func (s *EnsureAvailabilitySuite) TestEnsureAvailabilityFormatYaml(c *gc.C) { - expected := map[string][]string{ - "maintained": {"0"}, - "added": {"1", "2"}, - } - - ctx, err := s.runEnsureAvailability(c, "-n", "3", "--format", "yaml") - c.Assert(err, jc.ErrorIsNil) - - c.Assert(s.fake.numStateServers, 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 - err = goyaml.Unmarshal(ctx.Stdout.(*bytes.Buffer).Bytes(), &result) - c.Assert(err, jc.ErrorIsNil) - c.Assert(result, gc.DeepEquals, expected) -} - -func (s *EnsureAvailabilitySuite) TestEnsureAvailabilityFormatJson(c *gc.C) { - expected := map[string][]string{ - "maintained": {"0"}, - "added": {"1", "2"}, - } - - ctx, err := s.runEnsureAvailability(c, "-n", "3", "--format", "json") - c.Assert(err, jc.ErrorIsNil) - - c.Assert(s.fake.numStateServers, 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 - err = json.Unmarshal(ctx.Stdout.(*bytes.Buffer).Bytes(), &result) - c.Assert(err, jc.ErrorIsNil) - c.Assert(result, gc.DeepEquals, expected) -} - -func (s *EnsureAvailabilitySuite) TestEnsureAvailabilityWithSeries(c *gc.C) { - // Also test with -n 5 to validate numbers other than 1 and 3 - ctx, err := s.runEnsureAvailability(c, "--series", "series", "-n", "5") - c.Assert(err, jc.ErrorIsNil) - c.Assert(coretesting.Stdout(ctx), gc.Equals, - "maintaining machines: 0\n"+ - "adding machines: 1, 2, 3, 4\n\n") - - c.Assert(s.fake.numStateServers, 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) -} - -func (s *EnsureAvailabilitySuite) TestEnsureAvailabilityWithConstraints(c *gc.C) { - ctx, err := s.runEnsureAvailability(c, "--constraints", "mem=4G", "-n", "3") - c.Assert(err, jc.ErrorIsNil) - c.Assert(coretesting.Stdout(ctx), gc.Equals, - "maintaining machines: 0\n"+ - "adding machines: 1, 2\n\n") - - c.Assert(s.fake.numStateServers, 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) -} - -func (s *EnsureAvailabilitySuite) TestEnsureAvailabilityWithPlacement(c *gc.C) { - ctx, err := s.runEnsureAvailability(c, "--to", "valid", "-n", "3") - c.Assert(err, jc.ErrorIsNil) - c.Assert(coretesting.Stdout(ctx), gc.Equals, - "maintaining machines: 0\n"+ - "adding machines: 1, 2\n\n") - - c.Assert(s.fake.numStateServers, 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) -} - -func (s *EnsureAvailabilitySuite) TestEnsureAvailabilityErrors(c *gc.C) { - for _, n := range []int{-1, 2} { - _, err := s.runEnsureAvailability(c, "-n", fmt.Sprint(n)) - c.Assert(err, gc.ErrorMatches, "must specify a number of state servers odd and non-negative") - } - - // Verify that ensure-availability didn't call into the API - c.Assert(s.fake.numStateServers, gc.Equals, invalidNumServers) -} - -func (s *EnsureAvailabilitySuite) TestEnsureAvailabilityAllows0(c *gc.C) { - // If the number of state servers is specified as "0", the API will - // then use the default number of 3. - ctx, err := s.runEnsureAvailability(c, "-n", "0") - c.Assert(err, jc.ErrorIsNil) - c.Assert(coretesting.Stdout(ctx), gc.Equals, - "maintaining machines: 0\n"+ - "adding machines: 1, 2\n\n") - - c.Assert(s.fake.numStateServers, 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) -} - -func (s *EnsureAvailabilitySuite) TestEnsureAvailabilityDefaultsTo0(c *gc.C) { - // If the number of state servers is not specified, we pass in 0 to the - // API. The API will then use the default number of 3. - ctx, err := s.runEnsureAvailability(c) - c.Assert(err, jc.ErrorIsNil) - c.Assert(coretesting.Stdout(ctx), gc.Equals, - "maintaining machines: 0\n"+ - "adding machines: 1, 2\n\n") - - c.Assert(s.fake.numStateServers, 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) -} - -func (s *EnsureAvailabilitySuite) TestEnsureAvailabilityEndToEnd(c *gc.C) { - s.Factory.MakeMachine(c, &factory.MachineParams{ - Jobs: []state.MachineJob{state.JobManageEnviron}, - }) - ctx, err := coretesting.RunCommand(c, envcmd.Wrap(&EnsureAvailabilityCommand{}), "-n", "3") - c.Assert(err, jc.ErrorIsNil) - - // Machine 0 is demoted because it hasn't reported its presence - c.Assert(coretesting.Stdout(ctx), gc.Equals, - "adding machines: 1, 2, 3\n"+ - "demoting machines: 0\n\n") -} - -func (s *EnsureAvailabilitySuite) TestEnsureAvailabilityToExisting(c *gc.C) { - ctx, err := s.runEnsureAvailability(c, "--to", "1,2") - c.Assert(err, jc.ErrorIsNil) - c.Check(coretesting.Stdout(ctx), gc.Equals, ` -maintaining machines: 0 -converting machines: 1, 2 - -`[1:]) - - c.Check(s.fake.numStateServers, 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) -} === modified file 'src/github.com/juju/juju/cmd/juju/environment/constraints.go' --- src/github.com/juju/juju/cmd/juju/environment/constraints.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/environment/constraints.go 2015-10-23 18:29:32 +0000 @@ -23,9 +23,9 @@ See Also: juju help constraints - juju environment help set-constraints + juju help environment set-constraints juju help deploy - juju machine help add + juju help machine add juju help add-unit ` @@ -46,9 +46,9 @@ See Also: juju help constraints - juju environment help get-constraints + juju help environment get-constraints juju help deploy - juju machine help add + juju help machine add juju help add-unit ` === removed file 'src/github.com/juju/juju/cmd/juju/environment/create.go' --- src/github.com/juju/juju/cmd/juju/environment/create.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/environment/create.go 1970-01-01 00:00:00 +0000 @@ -1,209 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package environment - -import ( - "strings" - - "github.com/juju/cmd" - "github.com/juju/errors" - "github.com/juju/utils/keyvalues" - "gopkg.in/yaml.v1" - "launchpad.net/gnuflag" - - "github.com/juju/juju/api/environmentmanager" - "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/environs" - "github.com/juju/juju/environs/config" - "github.com/juju/juju/environs/configstore" -) - -// CreateCommand calls the API to create a new environment. -type CreateCommand struct { - envcmd.EnvCommandBase - api CreateEnvironmentAPI - // These attributes are exported only for testing purposes. - Name string - // TODO: owner string - ConfigFile cmd.FileVar - ConfValues map[string]string -} - -const createEnvHelpDoc = ` -This command will create another environment within the current Juju -Environment Server. The provider has to match, and the environment config must -specify all the required configuration values for the provider. In the cases -of ‘ec2’ and ‘openstack’, the same environment variables are checked for the -access and secret keys. - -If configuration values are passed by both extra command line arguments and -the --config option, the command line args take priority. -` - -func (c *CreateCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "create", - Args: " [key=[value] ...]", - Purpose: "create an environment within the Juju Environment Server", - Doc: strings.TrimSpace(createEnvHelpDoc), - } -} - -func (c *CreateCommand) SetFlags(f *gnuflag.FlagSet) { - // TODO: support creating environments for someone else when we work - // out how to have the other user login and start using the environement. - // f.StringVar(&c.owner, "owner", "", "the owner of the new environment if not the current user") - f.Var(&c.ConfigFile, "config", "path to yaml-formatted file containing environment config values") -} - -func (c *CreateCommand) Init(args []string) error { - if len(args) == 0 { - return errors.New("environment name is required") - } - c.Name, args = args[0], args[1:] - - values, err := keyvalues.Parse(args, true) - if err != nil { - return err - } - c.ConfValues = values - return nil -} - -type CreateEnvironmentAPI interface { - Close() error - ConfigSkeleton(provider, region string) (params.EnvironConfig, error) - CreateEnvironment(owner string, account, config map[string]interface{}) (params.Environment, error) -} - -func (c *CreateCommand) getAPI() (CreateEnvironmentAPI, error) { - if c.api != nil { - return c.api, nil - } - root, err := c.NewAPIRoot() - if err != nil { - return nil, err - } - return environmentmanager.NewClient(root), nil -} - -func (c *CreateCommand) Run(ctx *cmd.Context) (err error) { - client, err := c.getAPI() - if err != nil { - return err - } - defer client.Close() - - // Create the configstore entry and write it to disk, as this will error - // if one with the same name already exists. - creds, err := c.ConnectionCredentials() - if err != nil { - return errors.Trace(err) - } - endpoint, err := c.ConnectionEndpoint(false) - if err != nil { - return errors.Trace(err) - } - - store, err := configstore.Default() - if err != nil { - return errors.Trace(err) - } - info := store.CreateInfo(c.Name) - info.SetAPICredentials(creds) - endpoint.EnvironUUID = "" - if err := info.Write(); err != nil { - if errors.Cause(err) == configstore.ErrEnvironInfoAlreadyExists { - newErr := errors.AlreadyExistsf("environment %q", c.Name) - return errors.Wrap(err, newErr) - } - return errors.Trace(err) - } - defer func() { - if err != nil { - e := info.Destroy() - if e != nil { - logger.Errorf("could not remove environment file: %v", e) - } - } - }() - - // TODO: support provider and region. - serverSkeleton, err := client.ConfigSkeleton("", "") - if err != nil { - return errors.Trace(err) - } - - attrs, err := c.getConfigValues(ctx, serverSkeleton) - if err != nil { - return errors.Trace(err) - } - - // We pass nil through for the account details until we implement that bit. - env, err := client.CreateEnvironment(creds.User, nil, attrs) - if err != nil { - // cleanup configstore - return errors.Trace(err) - } - - // update the .jenv file with the environment uuid - endpoint.EnvironUUID = env.UUID - info.SetAPIEndpoint(endpoint) - if err := info.Write(); err != nil { - return errors.Trace(err) - } - - return nil -} - -func (c *CreateCommand) getConfigValues(ctx *cmd.Context, serverSkeleton params.EnvironConfig) (map[string]interface{}, error) { - // The reading of the config YAML is done in the Run - // method because the Read method requires the cmd Context - // for the current directory. - fileConfig := make(map[string]interface{}) - if c.ConfigFile.Path != "" { - configYAML, err := c.ConfigFile.Read(ctx) - if err != nil { - return nil, err - } - err = yaml.Unmarshal(configYAML, &fileConfig) - if err != nil { - return nil, err - } - } - - configValues := make(map[string]interface{}) - for key, value := range serverSkeleton { - configValues[key] = value - } - for key, value := range fileConfig { - configValues[key] = value - } - for key, value := range c.ConfValues { - configValues[key] = value - } - configValues["name"] = c.Name - - cfg, err := config.New(config.UseDefaults, configValues) - if err != nil { - return nil, errors.Trace(err) - } - - provider, err := environs.Provider(cfg.Type()) - if err != nil { - return nil, errors.Trace(err) - } - - cfg, err = provider.PrepareForCreateEnvironment(cfg) - if err != nil { - return nil, errors.Trace(err) - } - - attrs := cfg.AllAttrs() - delete(attrs, "agent-version") - // TODO: allow version to be specified on the command line and add here. - - return attrs, nil -} === removed file 'src/github.com/juju/juju/cmd/juju/environment/create_test.go' --- src/github.com/juju/juju/cmd/juju/environment/create_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/environment/create_test.go 1970-01-01 00:00:00 +0000 @@ -1,254 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package environment_test - -import ( - "io/ioutil" - - "github.com/juju/cmd" - "github.com/juju/errors" - jc "github.com/juju/testing/checkers" - "github.com/juju/utils" - gc "gopkg.in/check.v1" - "gopkg.in/yaml.v1" - - "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/cmd/juju/environment" - "github.com/juju/juju/environs/configstore" - "github.com/juju/juju/feature" - "github.com/juju/juju/testing" -) - -type createSuite struct { - testing.FakeJujuHomeSuite - fake *fakeCreateClient - store configstore.Storage - serverUUID string - server configstore.EnvironInfo -} - -var _ = gc.Suite(&createSuite{}) - -func (s *createSuite) SetUpTest(c *gc.C) { - s.FakeJujuHomeSuite.SetUpTest(c) - s.SetFeatureFlags(feature.JES) - s.fake = &fakeCreateClient{} - store := configstore.Default - s.AddCleanup(func(*gc.C) { - configstore.Default = store - }) - s.store = configstore.NewMem() - configstore.Default = func() (configstore.Storage, error) { - return s.store, nil - } - // Set up the current environment, and write just enough info - // so we don't try to refresh - envName := "test-master" - s.serverUUID = "fake-server-uuid" - info := s.store.CreateInfo(envName) - info.SetAPIEndpoint(configstore.APIEndpoint{ - Addresses: []string{"localhost"}, - CACert: testing.CACert, - EnvironUUID: s.serverUUID, - ServerUUID: s.serverUUID, - }) - info.SetAPICredentials(configstore.APICredentials{User: "bob", Password: "sekrit"}) - err := info.Write() - c.Assert(err, jc.ErrorIsNil) - s.server = info - err = envcmd.WriteCurrentEnvironment(envName) - c.Assert(err, jc.ErrorIsNil) -} - -func (s *createSuite) run(c *gc.C, args ...string) (*cmd.Context, error) { - command := environment.NewCreateCommand(s.fake) - return testing.RunCommand(c, envcmd.Wrap(command), args...) -} - -func (s *createSuite) TestInit(c *gc.C) { - - for i, test := range []struct { - args []string - err string - name string - path string - values map[string]string - }{ - { - err: "environment name is required", - }, { - args: []string{"new-env"}, - name: "new-env", - }, { - args: []string{"new-env", "key=value", "key2=value2"}, - name: "new-env", - values: map[string]string{"key": "value", "key2": "value2"}, - }, { - args: []string{"new-env", "key=value", "key=value2"}, - err: `key "key" specified more than once`, - }, { - args: []string{"new-env", "another"}, - err: `expected "key=value", got "another"`, - }, { - args: []string{"new-env", "--config", "some-file"}, - name: "new-env", - path: "some-file", - }, - } { - c.Logf("test %d", i) - create := &environment.CreateCommand{} - err := testing.InitCommand(create, test.args) - if test.err != "" { - c.Assert(err, gc.ErrorMatches, test.err) - } else { - c.Assert(err, jc.ErrorIsNil) - c.Assert(create.Name, gc.Equals, test.name) - c.Assert(create.ConfigFile.Path, gc.Equals, test.path) - // The config value parse method returns an empty map - // if there were no values - if len(test.values) == 0 { - c.Assert(create.ConfValues, gc.HasLen, 0) - } else { - c.Assert(create.ConfValues, jc.DeepEquals, test.values) - } - } - } -} - -func (s *createSuite) TestCreateExistingName(c *gc.C) { - // Make a configstore entry with the same name. - info := s.store.CreateInfo("test") - err := info.Write() - c.Assert(err, jc.ErrorIsNil) - - _, err = s.run(c, "test") - c.Assert(err, gc.ErrorMatches, `environment "test" already exists`) -} - -func (s *createSuite) TestComandLineConfigPassedThrough(c *gc.C) { - _, err := s.run(c, "test", "account=magic", "cloud=special") - c.Assert(err, jc.ErrorIsNil) - - c.Assert(s.fake.config["account"], gc.Equals, "magic") - c.Assert(s.fake.config["cloud"], gc.Equals, "special") -} - -func (s *createSuite) TestConfigFileValuesPassedThrough(c *gc.C) { - config := map[string]string{ - "account": "magic", - "cloud": "9", - } - bytes, err := yaml.Marshal(config) - c.Assert(err, jc.ErrorIsNil) - file, err := ioutil.TempFile(c.MkDir(), "") - c.Assert(err, jc.ErrorIsNil) - file.Write(bytes) - file.Close() - - _, err = s.run(c, "test", "--config", file.Name()) - c.Assert(err, jc.ErrorIsNil) - c.Assert(s.fake.config["account"], gc.Equals, "magic") - c.Assert(s.fake.config["cloud"], gc.Equals, "9") -} - -func (s *createSuite) TestConfigFileFormatError(c *gc.C) { - file, err := ioutil.TempFile(c.MkDir(), "") - c.Assert(err, jc.ErrorIsNil) - file.Write(([]byte)("not: valid: yaml")) - file.Close() - - _, err = s.run(c, "test", "--config", file.Name()) - c.Assert(err, gc.ErrorMatches, `YAML error: .*`) -} - -func (s *createSuite) TestConfigFileDoesntExist(c *gc.C) { - _, err := s.run(c, "test", "--config", "missing-file") - errMsg := ".*" + utils.NoSuchFileErrRegexp - c.Assert(err, gc.ErrorMatches, errMsg) -} - -func (s *createSuite) TestConfigValuePrecedence(c *gc.C) { - config := map[string]string{ - "account": "magic", - "cloud": "9", - } - bytes, err := yaml.Marshal(config) - c.Assert(err, jc.ErrorIsNil) - file, err := ioutil.TempFile(c.MkDir(), "") - c.Assert(err, jc.ErrorIsNil) - file.Write(bytes) - file.Close() - - _, err = s.run(c, "test", "--config", file.Name(), "account=magic", "cloud=special") - c.Assert(err, jc.ErrorIsNil) - c.Assert(s.fake.config["account"], gc.Equals, "magic") - c.Assert(s.fake.config["cloud"], gc.Equals, "special") -} - -func (s *createSuite) TestCreateErrorRemoveConfigstoreInfo(c *gc.C) { - s.fake.err = errors.New("bah humbug") - - _, err := s.run(c, "test") - c.Assert(err, gc.ErrorMatches, "bah humbug") - - _, err = s.store.ReadInfo("test") - c.Assert(err, gc.ErrorMatches, `environment "test" not found`) -} - -func (s *createSuite) TestCreateStoresValues(c *gc.C) { - s.fake.env = params.Environment{ - Name: "test", - UUID: "fake-env-uuid", - OwnerTag: "ignored-for-now", - ServerUUID: s.serverUUID, - } - _, err := s.run(c, "test") - c.Assert(err, jc.ErrorIsNil) - - info, err := s.store.ReadInfo("test") - c.Assert(err, jc.ErrorIsNil) - // Stores the credentials of the original environment - c.Assert(info.APICredentials(), jc.DeepEquals, s.server.APICredentials()) - endpoint := info.APIEndpoint() - expected := s.server.APIEndpoint() - c.Assert(endpoint.Addresses, jc.DeepEquals, expected.Addresses) - c.Assert(endpoint.Hostnames, jc.DeepEquals, expected.Hostnames) - c.Assert(endpoint.ServerUUID, gc.Equals, expected.ServerUUID) - c.Assert(endpoint.CACert, gc.Equals, expected.CACert) - c.Assert(endpoint.EnvironUUID, gc.Equals, "fake-env-uuid") -} - -// fakeCreateClient is used to mock out the behavior of the real -// CreateEnvironment command. -type fakeCreateClient struct { - owner string - account map[string]interface{} - config map[string]interface{} - err error - env params.Environment -} - -var _ environment.CreateEnvironmentAPI = (*fakeCreateClient)(nil) - -func (*fakeCreateClient) Close() error { - return nil -} - -func (*fakeCreateClient) ConfigSkeleton(provider, region string) (params.EnvironConfig, error) { - return params.EnvironConfig{ - "type": "dummy", - "state-server": false, - }, nil -} -func (f *fakeCreateClient) CreateEnvironment(owner string, account, config map[string]interface{}) (params.Environment, error) { - var env params.Environment - if f.err != nil { - return env, f.err - } - f.owner = owner - f.account = account - f.config = config - return f.env, nil -} === added file 'src/github.com/juju/juju/cmd/juju/environment/destroy.go' --- src/github.com/juju/juju/cmd/juju/environment/destroy.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/environment/destroy.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,131 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for infos. + +package environment + +import ( + "fmt" + + "github.com/juju/cmd" + "github.com/juju/errors" + "launchpad.net/gnuflag" + + "github.com/juju/juju/apiserver/params" + jujucmd "github.com/juju/juju/cmd" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/block" + "github.com/juju/juju/environs" + "github.com/juju/juju/environs/configstore" +) + +// DestroyCommand destroys the specified environment. +type DestroyCommand struct { + envcmd.EnvCommandBase + envName string + assumeYes bool + api DestroyEnvironmentAPI +} + +var destroyDoc = `Destroys the specified environment` +var destroyEnvMsg = ` +WARNING! This command will destroy the %q environment. +This includes all machines, services, data and other resources. + +Continue [y/N]? `[1:] + +// DestroyEnvironmentAPI defines the methods on the environmentmanager +// API that the destroy command calls. It is exported for mocking in tests. +type DestroyEnvironmentAPI interface { + Close() error + DestroyEnvironment() error +} + +// Info implements Command.Info. +func (c *DestroyCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "destroy", + Args: "", + Purpose: "terminate all machines and other associated resources for a non-system environment", + Doc: destroyDoc, + } +} + +// SetFlags implements Command.SetFlags. +func (c *DestroyCommand) SetFlags(f *gnuflag.FlagSet) { + f.BoolVar(&c.assumeYes, "y", false, "Do not ask for confirmation") + f.BoolVar(&c.assumeYes, "yes", false, "") +} + +// Init implements Command.Init. +func (c *DestroyCommand) Init(args []string) error { + switch len(args) { + case 0: + return errors.New("no environment specified") + case 1: + c.envName = args[0] + c.SetEnvName(c.envName) + return nil + default: + return cmd.CheckEmpty(args[1:]) + } +} + +func (c *DestroyCommand) getAPI() (DestroyEnvironmentAPI, error) { + if c.api != nil { + return c.api, nil + } + return c.NewAPIClient() +} + +// Run implements Command.Run +func (c *DestroyCommand) Run(ctx *cmd.Context) error { + store, err := configstore.Default() + if err != nil { + return errors.Annotate(err, "cannot open environment info storage") + } + + cfgInfo, err := store.ReadInfo(c.envName) + if err != nil { + return errors.Annotate(err, "cannot read environment info") + } + + // Verify that we're not destroying a system + apiEndpoint := cfgInfo.APIEndpoint() + if apiEndpoint.ServerUUID != "" && apiEndpoint.EnvironUUID == apiEndpoint.ServerUUID { + return errors.Errorf("%q is a system; use 'juju system destroy' to destroy it", c.envName) + } + + if !c.assumeYes { + fmt.Fprintf(ctx.Stdout, destroyEnvMsg, c.envName) + + if err := jujucmd.UserConfirmYes(ctx); err != nil { + return errors.Annotate(err, "environment destruction") + } + } + + // Attempt to connect to the API. If we can't, fail the destroy. + api, err := c.getAPI() + if err != nil { + return errors.Annotate(err, "cannot connect to API") + } + defer api.Close() + + // Attempt to destroy the environment. + err = api.DestroyEnvironment() + if err != nil { + return c.handleError(errors.Annotate(err, "cannot destroy environment")) + } + + return environs.DestroyInfo(c.envName, store) +} + +func (c *DestroyCommand) handleError(err error) error { + if err == nil { + return nil + } + if params.IsCodeOperationBlocked(err) { + return block.ProcessBlockedError(err, block.BlockDestroy) + } + logger.Errorf(`failed to destroy environment %q`, c.envName) + return err +} === added file 'src/github.com/juju/juju/cmd/juju/environment/destroy_test.go' --- src/github.com/juju/juju/cmd/juju/environment/destroy_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/environment/destroy_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,214 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package environment_test + +import ( + "bytes" + "time" + + "github.com/juju/cmd" + "github.com/juju/errors" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/juju/environment" + cmdtesting "github.com/juju/juju/cmd/testing" + "github.com/juju/juju/environs/configstore" + _ "github.com/juju/juju/provider/dummy" + "github.com/juju/juju/testing" +) + +type DestroySuite struct { + testing.FakeJujuHomeSuite + api *fakeDestroyAPI + store configstore.Storage +} + +var _ = gc.Suite(&DestroySuite{}) + +// fakeDestroyAPI mocks out the cient API +type fakeDestroyAPI struct { + err error + env map[string]interface{} +} + +func (f *fakeDestroyAPI) Close() error { return nil } + +func (f *fakeDestroyAPI) DestroyEnvironment() error { + return f.err +} + +func (s *DestroySuite) SetUpTest(c *gc.C) { + s.FakeJujuHomeSuite.SetUpTest(c) + s.api = &fakeDestroyAPI{} + s.api.err = nil + + var err error + s.store, err = configstore.Default() + c.Assert(err, jc.ErrorIsNil) + + var envList = []struct { + name string + serverUUID string + envUUID string + }{ + { + name: "test1", + serverUUID: "test1-uuid", + envUUID: "test1-uuid", + }, { + name: "test2", + serverUUID: "test1-uuid", + envUUID: "test2-uuid", + }, + } + for _, env := range envList { + info := s.store.CreateInfo(env.name) + info.SetAPIEndpoint(configstore.APIEndpoint{ + Addresses: []string{"localhost"}, + CACert: testing.CACert, + EnvironUUID: env.envUUID, + ServerUUID: env.serverUUID, + }) + + err := info.Write() + c.Assert(err, jc.ErrorIsNil) + } +} + +func (s *DestroySuite) runDestroyCommand(c *gc.C, args ...string) (*cmd.Context, error) { + cmd := environment.NewDestroyCommand(s.api) + return testing.RunCommand(c, cmd, args...) +} + +func (s *DestroySuite) newDestroyCommand() *environment.DestroyCommand { + return environment.NewDestroyCommand(s.api) +} + +func checkEnvironmentExistsInStore(c *gc.C, name string, store configstore.Storage) { + _, err := store.ReadInfo(name) + c.Assert(err, jc.ErrorIsNil) +} + +func checkEnvironmentRemovedFromStore(c *gc.C, name string, store configstore.Storage) { + _, err := store.ReadInfo(name) + c.Assert(err, jc.Satisfies, errors.IsNotFound) +} + +func (s *DestroySuite) TestDestroyNoEnvironmentNameError(c *gc.C) { + _, err := s.runDestroyCommand(c) + c.Assert(err, gc.ErrorMatches, "no environment specified") +} + +func (s *DestroySuite) TestDestroyBadFlags(c *gc.C) { + _, err := s.runDestroyCommand(c, "-n") + c.Assert(err, gc.ErrorMatches, "flag provided but not defined: -n") +} + +func (s *DestroySuite) TestDestroyUnknownArgument(c *gc.C) { + _, err := s.runDestroyCommand(c, "environment", "whoops") + c.Assert(err, gc.ErrorMatches, `unrecognized args: \["whoops"\]`) +} + +func (s *DestroySuite) TestDestroyUnknownEnvironment(c *gc.C) { + _, err := s.runDestroyCommand(c, "foo") + c.Assert(err, gc.ErrorMatches, `cannot read environment info: environment "foo" not found`) +} + +func (s *DestroySuite) TestDestroyCannotConnectToAPI(c *gc.C) { + s.api.err = errors.New("connection refused") + _, err := s.runDestroyCommand(c, "test2", "-y") + c.Assert(err, gc.ErrorMatches, "cannot destroy environment: connection refused") + c.Check(c.GetTestLog(), jc.Contains, "failed to destroy environment \"test2\"") + checkEnvironmentExistsInStore(c, "test2", s.store) +} + +func (s *DestroySuite) TestSystemDestroyFails(c *gc.C) { + _, err := s.runDestroyCommand(c, "test1", "-y") + c.Assert(err, gc.ErrorMatches, `"test1" is a system; use 'juju system destroy' to destroy it`) + checkEnvironmentExistsInStore(c, "test1", s.store) +} + +func (s *DestroySuite) TestDestroy(c *gc.C) { + checkEnvironmentExistsInStore(c, "test2", s.store) + _, err := s.runDestroyCommand(c, "test2", "-y") + c.Assert(err, jc.ErrorIsNil) + checkEnvironmentRemovedFromStore(c, "test2", s.store) +} + +func (s *DestroySuite) TestFailedDestroyEnvironment(c *gc.C) { + s.api.err = errors.New("permission denied") + _, err := s.runDestroyCommand(c, "test2", "-y") + c.Assert(err, gc.ErrorMatches, "cannot destroy environment: permission denied") + checkEnvironmentExistsInStore(c, "test2", s.store) +} + +func (s *DestroySuite) resetEnvironment(c *gc.C) { + info := s.store.CreateInfo("test2") + info.SetAPIEndpoint(configstore.APIEndpoint{ + Addresses: []string{"localhost"}, + CACert: testing.CACert, + EnvironUUID: "test2-uuid", + ServerUUID: "test1-uuid", + }) + err := info.Write() + c.Assert(err, jc.ErrorIsNil) +} + +func (s *DestroySuite) TestDestroyCommandConfirmation(c *gc.C) { + var stdin, stdout bytes.Buffer + ctx, err := cmd.DefaultContext() + c.Assert(err, jc.ErrorIsNil) + ctx.Stdout = &stdout + ctx.Stdin = &stdin + + // Ensure confirmation is requested if "-y" is not specified. + stdin.WriteString("n") + _, errc := cmdtesting.RunCommand(ctx, s.newDestroyCommand(), "test2") + select { + case err := <-errc: + c.Check(err, gc.ErrorMatches, "environment destruction: aborted") + case <-time.After(testing.LongWait): + c.Fatalf("command took too long") + } + c.Check(testing.Stdout(ctx), gc.Matches, "WARNING!.*test2(.|\n)*") + checkEnvironmentExistsInStore(c, "test1", s.store) + + // EOF on stdin: equivalent to answering no. + stdin.Reset() + stdout.Reset() + _, errc = cmdtesting.RunCommand(ctx, s.newDestroyCommand(), "test2") + select { + case err := <-errc: + c.Check(err, gc.ErrorMatches, "environment destruction: aborted") + case <-time.After(testing.LongWait): + c.Fatalf("command took too long") + } + c.Check(testing.Stdout(ctx), gc.Matches, "WARNING!.*test2(.|\n)*") + checkEnvironmentExistsInStore(c, "test1", s.store) + + for _, answer := range []string{"y", "Y", "yes", "YES"} { + stdin.Reset() + stdout.Reset() + stdin.WriteString(answer) + _, errc = cmdtesting.RunCommand(ctx, s.newDestroyCommand(), "test2") + select { + case err := <-errc: + c.Check(err, jc.ErrorIsNil) + case <-time.After(testing.LongWait): + c.Fatalf("command took too long") + } + checkEnvironmentRemovedFromStore(c, "test2", s.store) + + // Add the test2 environment back into the store for the next test + s.resetEnvironment(c) + } +} + +func (s *DestroySuite) TestBlockedDestroy(c *gc.C) { + s.api.err = ¶ms.Error{Code: params.CodeOperationBlocked} + s.runDestroyCommand(c, "test2", "-y") + c.Check(c.GetTestLog(), jc.Contains, "To remove the block") +} === modified file 'src/github.com/juju/juju/cmd/juju/environment/environment.go' --- src/github.com/juju/juju/cmd/juju/environment/environment.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/environment/environment.go 2015-10-23 18:29:32 +0000 @@ -38,8 +38,8 @@ if featureflag.Enabled(feature.JES) { environmentCmd.Register(envcmd.Wrap(&ShareCommand{})) environmentCmd.Register(envcmd.Wrap(&UnshareCommand{})) - environmentCmd.Register(envcmd.Wrap(&CreateCommand{})) environmentCmd.Register(envcmd.Wrap(&UsersCommand{})) + environmentCmd.Register(envcmd.Wrap(&DestroyCommand{})) } return environmentCmd } === modified file 'src/github.com/juju/juju/cmd/juju/environment/environment_test.go' --- src/github.com/juju/juju/cmd/juju/environment/environment_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/environment/environment_test.go 2015-10-23 18:29:32 +0000 @@ -24,7 +24,7 @@ var _ = gc.Suite(&EnvironmentCommandSuite{}) var expectedCommmandNames = []string{ - "create", + "destroy", "get", "get-constraints", "help", @@ -48,7 +48,7 @@ // Remove "share" for the first test because the feature is not // enabled. - devFeatures := set.NewStrings("create", "share", "unshare", "users") + devFeatures := set.NewStrings("destroy", "share", "unshare", "users") // Remove features behind dev_flag for the first test since they are not // enabled. === modified file 'src/github.com/juju/juju/cmd/juju/environment/export_test.go' --- src/github.com/juju/juju/cmd/juju/environment/export_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/environment/export_test.go 2015-10-23 18:29:32 +0000 @@ -45,16 +45,16 @@ } } -// NewCreateCommand returns a CreateCommand with the api provided as specified. -func NewCreateCommand(api CreateEnvironmentAPI) *CreateCommand { - return &CreateCommand{ - api: api, - } -} - // NewUsersCommand returns a UsersCommand with the api provided as specified. func NewUsersCommand(api UsersAPI) *UsersCommand { return &UsersCommand{ api: api, } } + +// NewDestroyCommand returns a DestroyCommand with the api provided as specified. +func NewDestroyCommand(api DestroyEnvironmentAPI) *DestroyCommand { + return &DestroyCommand{ + api: api, + } +} === modified file 'src/github.com/juju/juju/cmd/juju/environment/jenv_test.go' --- src/github.com/juju/juju/cmd/juju/environment/jenv_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/environment/jenv_test.go 2015-10-23 18:29:32 +0000 @@ -230,7 +230,9 @@ // The default environment is now the newly imported one, and the output // reflects the change. - c.Assert(envcmd.ReadCurrentEnvironment(), gc.Equals, "testing") + currEnv, err := envcmd.ReadCurrentEnvironment() + c.Assert(err, jc.ErrorIsNil) + c.Assert(currEnv, gc.Equals, "testing") c.Assert(testing.Stdout(ctx), gc.Equals, "erewhemos -> testing\n") // Trying to import the jenv with the same name a second time raises an @@ -244,7 +246,10 @@ ctx, err = testing.RunCommand(c, jenvCmd, f.Name(), "another") c.Assert(err, jc.ErrorIsNil) assertJenvContents(c, contents, "another") - c.Assert(envcmd.ReadCurrentEnvironment(), gc.Equals, "another") + + currEnv, err = envcmd.ReadCurrentEnvironment() + c.Assert(err, jc.ErrorIsNil) + c.Assert(currEnv, gc.Equals, "another") c.Assert(testing.Stdout(ctx), gc.Equals, "testing -> another\n") } @@ -264,7 +269,9 @@ // The default environment is now the newly imported one, and the output // reflects the change. - c.Assert(envcmd.ReadCurrentEnvironment(), gc.Equals, "my-env") + currEnv, err := envcmd.ReadCurrentEnvironment() + c.Assert(err, jc.ErrorIsNil) + c.Assert(currEnv, gc.Equals, "my-env") c.Assert(testing.Stdout(ctx), gc.Equals, "erewhemos -> my-env\n") } === removed file 'src/github.com/juju/juju/cmd/juju/environment_test.go' --- src/github.com/juju/juju/cmd/juju/environment_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/environment_test.go 1970-01-01 00:00:00 +0000 @@ -1,128 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "strings" - - "github.com/juju/cmd" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/constraints" - "github.com/juju/juju/feature" - jujutesting "github.com/juju/juju/juju/testing" - "github.com/juju/juju/state" - "github.com/juju/juju/testing" - "github.com/juju/juju/testing/factory" -) - -// EnvironmentSuite tests the connectivity of all the environment subcommands. -// These tests go from the command line, api client, api server, db. The db -// changes are then checked. Only one test for each command is done here to -// check connectivity. Exhaustive unit tests are at each layer. -type EnvironmentSuite struct { - jujutesting.JujuConnSuite -} - -var _ = gc.Suite(&EnvironmentSuite{}) - -func (s *EnvironmentSuite) assertEnvValue(c *gc.C, key string, expected interface{}) { - envConfig, err := s.State.EnvironConfig() - c.Assert(err, jc.ErrorIsNil) - value, found := envConfig.AllAttrs()[key] - c.Assert(found, jc.IsTrue) - c.Assert(value, gc.Equals, expected) -} - -func (s *EnvironmentSuite) assertEnvValueMissing(c *gc.C, key string) { - envConfig, err := s.State.EnvironConfig() - c.Assert(err, jc.ErrorIsNil) - _, found := envConfig.AllAttrs()[key] - c.Assert(found, jc.IsFalse) -} - -func (s *EnvironmentSuite) RunEnvironmentCommand(c *gc.C, commands ...string) (*cmd.Context, error) { - args := []string{"environment"} - args = append(args, commands...) - context := testing.Context(c) - juju := NewJujuCommand(context) - if err := testing.InitCommand(juju, args); err != nil { - return context, err - } - return context, juju.Run(context) -} - -func (s *EnvironmentSuite) TestGet(c *gc.C) { - err := s.State.UpdateEnvironConfig(map[string]interface{}{"special": "known"}, nil, nil) - c.Assert(err, jc.ErrorIsNil) - - context, err := s.RunEnvironmentCommand(c, "get", "special") - c.Assert(err, jc.ErrorIsNil) - c.Assert(testing.Stdout(context), gc.Equals, "known\n") -} - -func (s *EnvironmentSuite) TestSet(c *gc.C) { - _, err := s.RunEnvironmentCommand(c, "set", "special=known") - c.Assert(err, jc.ErrorIsNil) - s.assertEnvValue(c, "special", "known") -} - -func (s *EnvironmentSuite) TestUnset(c *gc.C) { - err := s.State.UpdateEnvironConfig(map[string]interface{}{"special": "known"}, nil, nil) - c.Assert(err, jc.ErrorIsNil) - - _, err = s.RunEnvironmentCommand(c, "unset", "special") - c.Assert(err, jc.ErrorIsNil) - s.assertEnvValueMissing(c, "special") -} - -func (s *EnvironmentSuite) TestRetryProvisioning(c *gc.C) { - s.Factory.MakeMachine(c, &factory.MachineParams{ - Jobs: []state.MachineJob{state.JobManageEnviron}, - }) - ctx, err := s.RunEnvironmentCommand(c, "retry-provisioning", "0") - c.Assert(err, jc.ErrorIsNil) - - output := testing.Stderr(ctx) - stripped := strings.Replace(output, "\n", "", -1) - c.Check(stripped, gc.Equals, `machine 0 is not in an error state`) -} - -func (s *EnvironmentSuite) TestCreate(c *gc.C) { - s.SetFeatureFlags(feature.JES) - // 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 'state-server'. - context, err := s.RunEnvironmentCommand(c, "create", "new-env", "authorized-keys=fake-key", "state-server=false") - c.Check(err, jc.ErrorIsNil) - c.Check(testing.Stdout(context), gc.Equals, "") - c.Check(testing.Stderr(context), gc.Equals, "") -} - -func uint64p(val uint64) *uint64 { - return &val -} - -func (s *EnvironmentSuite) TestGetConstraints(c *gc.C) { - cons := constraints.Value{CpuPower: uint64p(250)} - err := s.State.SetEnvironConstraints(cons) - c.Assert(err, jc.ErrorIsNil) - - ctx, err := s.RunEnvironmentCommand(c, "get-constraints") - c.Assert(err, jc.ErrorIsNil) - c.Check(testing.Stdout(ctx), gc.Equals, "cpu-power=250\n") -} - -func (s *EnvironmentSuite) TestSetConstraints(c *gc.C) { - _, err := s.RunEnvironmentCommand(c, "set-constraints", "mem=4G", "cpu-power=250") - c.Assert(err, jc.ErrorIsNil) - - cons, err := s.State.EnvironConstraints() - c.Assert(err, jc.ErrorIsNil) - c.Assert(cons, gc.DeepEquals, constraints.Value{ - CpuPower: uint64p(250), - Mem: uint64p(4096), - }) -} === removed file 'src/github.com/juju/juju/cmd/juju/expose.go' --- src/github.com/juju/juju/cmd/juju/expose.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/expose.go 1970-01-01 00:00:00 +0000 @@ -1,53 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "errors" - - "github.com/juju/cmd" - - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/cmd/juju/block" -) - -// ExposeCommand is responsible exposing services. -type ExposeCommand struct { - envcmd.EnvCommandBase - ServiceName string -} - -var jujuExposeHelp = ` -Adjusts firewall rules and similar security mechanisms of the provider, to -allow the service to be accessed on its public address. - -` - -func (c *ExposeCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "expose", - Args: "", - Purpose: "expose a service", - Doc: jujuExposeHelp, - } -} - -func (c *ExposeCommand) Init(args []string) error { - if len(args) == 0 { - return errors.New("no service name specified") - } - c.ServiceName = args[0] - return cmd.CheckEmpty(args[1:]) -} - -// Run changes the juju-managed firewall to expose any -// ports that were also explicitly marked by units as open. -func (c *ExposeCommand) Run(_ *cmd.Context) error { - client, err := c.NewAPIClient() - if err != nil { - return err - } - defer client.Close() - return block.ProcessBlockedError(client.ServiceExpose(c.ServiceName), block.BlockChange) -} === removed file 'src/github.com/juju/juju/cmd/juju/expose_test.go' --- src/github.com/juju/juju/cmd/juju/expose_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/expose_test.go 1970-01-01 00:00:00 +0000 @@ -1,70 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - "gopkg.in/juju/charm.v5" - - "github.com/juju/juju/cmd/envcmd" - jujutesting "github.com/juju/juju/juju/testing" - "github.com/juju/juju/testcharms" - "github.com/juju/juju/testing" -) - -type ExposeSuite struct { - jujutesting.RepoSuite - CmdBlockHelper -} - -func (s *ExposeSuite) SetUpTest(c *gc.C) { - s.RepoSuite.SetUpTest(c) - s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) - c.Assert(s.CmdBlockHelper, gc.NotNil) - s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) -} - -var _ = gc.Suite(&ExposeSuite{}) - -func runExpose(c *gc.C, args ...string) error { - _, err := testing.RunCommand(c, envcmd.Wrap(&ExposeCommand{}), args...) - return err -} - -func (s *ExposeSuite) assertExposed(c *gc.C, service string) { - svc, err := s.State.Service(service) - c.Assert(err, jc.ErrorIsNil) - exposed := svc.IsExposed() - c.Assert(exposed, jc.IsTrue) -} - -func (s *ExposeSuite) TestExpose(c *gc.C) { - testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") - err := runDeploy(c, "local:dummy", "some-service-name") - c.Assert(err, jc.ErrorIsNil) - curl := charm.MustParseURL("local:trusty/dummy-1") - s.AssertService(c, "some-service-name", curl, 1, 0) - - err = runExpose(c, "some-service-name") - c.Assert(err, jc.ErrorIsNil) - s.assertExposed(c, "some-service-name") - - err = runExpose(c, "nonexistent-service") - c.Assert(err, gc.ErrorMatches, `service "nonexistent-service" not found`) -} - -func (s *ExposeSuite) TestBlockExpose(c *gc.C) { - testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") - err := runDeploy(c, "local:dummy", "some-service-name") - c.Assert(err, jc.ErrorIsNil) - curl := charm.MustParseURL("local:trusty/dummy-1") - s.AssertService(c, "some-service-name", curl, 1, 0) - - // Block operation - s.BlockAllChanges(c, "TestBlockExpose") - - err = runExpose(c, "some-service-name") - s.AssertBlocked(c, err, ".*TestBlockExpose.*") -} === removed file 'src/github.com/juju/juju/cmd/juju/flags.go' --- src/github.com/juju/juju/cmd/juju/flags.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/flags.go 1970-01-01 00:00:00 +0000 @@ -1,43 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - "strings" - - "github.com/juju/errors" - - "github.com/juju/juju/storage" -) - -type storageFlag struct { - stores *map[string]storage.Constraints -} - -// Set implements gnuflag.Value.Set. -func (f storageFlag) Set(s string) error { - fields := strings.SplitN(s, "=", 2) - if len(fields) < 2 { - return errors.New("expected =") - } - cons, err := storage.ParseConstraints(fields[1]) - if err != nil { - return errors.Annotate(err, "cannot parse disk constraints") - } - if *f.stores == nil { - *f.stores = make(map[string]storage.Constraints) - } - (*f.stores)[fields[0]] = cons - return nil -} - -// Set implements gnuflag.Value.String. -func (f storageFlag) String() string { - strs := make([]string, 0, len(*f.stores)) - for store, cons := range *f.stores { - strs = append(strs, fmt.Sprintf("%s=%v", store, cons)) - } - return strings.Join(strs, " ") -} === removed file 'src/github.com/juju/juju/cmd/juju/get.go' --- src/github.com/juju/juju/cmd/juju/get.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/get.go 1970-01-01 00:00:00 +0000 @@ -1,92 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "errors" - - "github.com/juju/cmd" - "launchpad.net/gnuflag" - - "github.com/juju/juju/cmd/envcmd" -) - -// GetCommand retrieves the configuration of a service. -type GetCommand struct { - envcmd.EnvCommandBase - ServiceName string - out cmd.Output -} - -const getDoc = ` -The command output includes the service and charm names, a detailed list of all config -settings for , including the setting name, whether it uses the default value -or not ("default: true"), description (if set), type, and current value. Example: - -$ juju get wordpress - -charm: wordpress -service: wordpress -settings: - engine: - default: true - description: 'Currently two ...' - type: string - value: nginx - tuning: - description: "This is the tuning level..." - type: string - value: optimized - -NOTE: In the example above the descriptions and most other settings were omitted for -brevity. The "engine" setting was left at its default value ("nginx"), while the -"tuning" setting was set to "optimized" (the default value is "single"). -` - -func (c *GetCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "get", - Args: "", - Purpose: "get service configuration options", - Doc: getDoc, - } -} - -func (c *GetCommand) SetFlags(f *gnuflag.FlagSet) { - // TODO(dfc) add json formatting ? - c.out.AddFlags(f, "yaml", map[string]cmd.Formatter{ - "yaml": cmd.FormatYaml, - }) -} - -func (c *GetCommand) Init(args []string) error { - // TODO(dfc) add --schema-only - if len(args) == 0 { - return errors.New("no service name specified") - } - c.ServiceName = args[0] - return cmd.CheckEmpty(args[1:]) -} - -// Run fetches the configuration of the service and formats -// the result as a YAML string. -func (c *GetCommand) Run(ctx *cmd.Context) error { - client, err := c.NewAPIClient() - if err != nil { - return err - } - defer client.Close() - - results, err := client.ServiceGet(c.ServiceName) - if err != nil { - return err - } - - resultsMap := map[string]interface{}{ - "service": results.Service, - "charm": results.Charm, - "settings": results.Config, - } - return c.out.Write(ctx, resultsMap) -} === removed file 'src/github.com/juju/juju/cmd/juju/get_test.go' --- src/github.com/juju/juju/cmd/juju/get_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/get_test.go 1970-01-01 00:00:00 +0000 @@ -1,89 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "bytes" - - "github.com/juju/cmd" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - "gopkg.in/juju/charm.v5" - goyaml "gopkg.in/yaml.v1" - - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/juju/testing" - coretesting "github.com/juju/juju/testing" -) - -type GetSuite struct { - testing.JujuConnSuite -} - -var _ = gc.Suite(&GetSuite{}) - -var getTests = []struct { - service string - expected map[string]interface{} -}{ - { - "dummy-service", - map[string]interface{}{ - "service": "dummy-service", - "charm": "dummy", - "settings": map[string]interface{}{ - "title": map[string]interface{}{ - "description": "A descriptive title used for the service.", - "type": "string", - "value": "Nearly There", - }, - "skill-level": map[string]interface{}{ - "description": "A number indicating skill.", - "type": "int", - "default": true, - }, - "username": map[string]interface{}{ - "description": "The name of the initial account (given admin permissions).", - "type": "string", - "value": "admin001", - "default": true, - }, - "outlook": map[string]interface{}{ - "description": "No default outlook.", - "type": "string", - "default": true, - }, - }, - }, - }, - - // TODO(dfc) add additional services (need more charms) - // TODO(dfc) add set tests -} - -func (s *GetSuite) TestGetConfig(c *gc.C) { - sch := s.AddTestingCharm(c, "dummy") - svc := s.AddTestingService(c, "dummy-service", sch) - err := svc.UpdateConfigSettings(charm.Settings{"title": "Nearly There"}) - c.Assert(err, jc.ErrorIsNil) - for _, t := range getTests { - ctx := coretesting.Context(c) - code := cmd.Main(envcmd.Wrap(&GetCommand{}), ctx, []string{t.service}) - c.Check(code, gc.Equals, 0) - c.Assert(ctx.Stderr.(*bytes.Buffer).String(), gc.Equals, "") - // round trip via goyaml to avoid being sucked into a quagmire of - // map[interface{}]interface{} vs map[string]interface{}. This is - // also required if we add json support to this command. - buf, err := goyaml.Marshal(t.expected) - c.Assert(err, jc.ErrorIsNil) - expected := make(map[string]interface{}) - err = goyaml.Unmarshal(buf, &expected) - c.Assert(err, jc.ErrorIsNil) - - actual := make(map[string]interface{}) - err = goyaml.Unmarshal(ctx.Stdout.(*bytes.Buffer).Bytes(), &actual) - c.Assert(err, jc.ErrorIsNil) - c.Assert(actual, gc.DeepEquals, expected) - } -} === removed file 'src/github.com/juju/juju/cmd/juju/help_topics.go' --- src/github.com/juju/juju/cmd/juju/help_topics.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/help_topics.go 1970-01-01 00:00:00 +0000 @@ -1,616 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -const helpBasics = ` -Juju -- devops distilled -https://juju.ubuntu.com/ - -Juju provides easy, intelligent service orchestration on top of environments -such as Amazon EC2, HP Cloud, OpenStack, MaaS, or your own local machine. - -Basic commands: - juju init generate boilerplate configuration for juju environments - juju bootstrap start up an environment from scratch - - juju deploy deploy a new service - juju add-relation add a relation between two services - juju expose expose a service - - juju help bootstrap more help on e.g. bootstrap command - juju help commands list all commands - juju help glossary glossary of terms - juju help topics list all help topics - -Provider information: - juju help azure-provider use on Windows Azure - juju help ec2-provider use on Amazon EC2 - juju help hpcloud-provider use on HP Cloud - juju help local-provider use on this computer - juju help openstack-provider use on OpenStack -` - -const helpProviderStart = ` -Start by generating a generic configuration file for Juju, using the command: - - juju init - -This will create the '~/.juju/' directory (or $JUJU_HOME, if set) if it doesn't -already exist and generate a file, 'environments.yaml' in that directory. -` -const helpProviderEnd = ` -See Also: - - juju help init - juju help bootstrap - -` - -const helpLocalProvider = ` -The local provider is a Linux-only Juju environment that uses LXC containers as -a virtual cloud on the local machine. Because of this, lxc and mongodb are -required for the local provider to work. All of these dependencies are tracked -in the 'juju-local' package. You can install that with: - - sudo apt-get update - sudo apt-get install juju-local - -After that you might get error for SSH authorised/public key not found. ERROR -SSH authorised/public key not found. - - ssh-keygen -t rsa - -Now you need to tell Juju to use the local provider and then bootstrap: - - juju switch local - juju bootstrap - -The first time this runs it might take a bit, as it's doing a netinstall for -the container, it's around a 300 megabyte download. Subsequent bootstraps -should be much quicker. You'll be asked for your 'sudo' password, which is -needed because only root can create LXC containers. When you need to destroy -the environment, do 'juju destroy-environment local' and you could be asked -for your 'sudo' password again. - -You deploy charms from the charm store using the following commands: - - juju deploy mysql - juju deploy wordpress - juju add-relation wordpress mysql - -For Ubuntu deployments, the local provider will prefer to use lxc-clone to create -the machines for the trusty OS series and later. -A 'template' container is created with the name - juju--template -where is the OS series, for example 'juju-trusty-template'. -You can override the use of clone by specifying - lxc-clone: true -or - lxc-clone: false -in the configuration for your local provider. If you have the main container -directory mounted on a btrfs partition, then the clone will be using btrfs -snapshots to create the containers. This means that clones use up much -less disk space. If you do not have btrfs, lxc will attempt to use aufs -(an overlay type filesystem). You can explicitly ask Juju to create -full containers and not overlays by specifying the following in the provider -configuration: - lxc-clone-aufs: false - - -References: - - http://askubuntu.com/questions/65359/how-do-i-configure-juju-for-local-usage - https://juju.ubuntu.com/docs/getting-started.html -` - -const helpOpenstackProvider = ` -Here's an example OpenStack configuration: - - sample_openstack: - type: openstack - - # Specifies whether the use of 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. - # use-floating-ip: false - - # Specifies whether new machine instances should have the "default" - # Openstack security group assigned. - # use-default-secgroup: false - - # Usually set via the env variable OS_AUTH_URL, but can be specified here - # auth-url: https://yourkeystoneurl:443/v2.0/ - - # The following are used for userpass authentication (the default) - # auth-mode: userpass - - # Usually set via the env variable OS_USERNAME, but can be specified here - # username: - - # Usually set via the env variable OS_PASSWORD, but can be specified here - # password: - - # Usually set via the env variable OS_TENANT_NAME, but can be specified here - # tenant-name: - - # Usually set via the env variable OS_REGION_NAME, but can be specified here - # region: - -If you have set the described OS_* environment variables, you only need "type:". -References: - - http://juju.ubuntu.com/docs/provider-configuration-openstack.html - http://askubuntu.com/questions/132411/how-can-i-configure-juju-for-deployment-on-openstack - -Placement directives: - - OpenStack environments support the following placement directives for use - with "juju bootstrap" and "juju add-machine": - - zone= - The "zone" placement directive instructs the OpenStack provider to - allocate a machine in the specified availability zone. If the zone - does not exist, or a machine cannot be allocated within it, then - the machine addition will fail. - -Other OpenStack Based Clouds: - -This answer is for generic OpenStack support, if you're using an OpenStack-based -provider check these questions out for provider-specific information: - - https://juju.ubuntu.com/docs/config-hpcloud.html - -` - -const helpEC2Provider = ` -Configuring the EC2 environment requires telling Juju about your AWS access key -and secret key. To do this, you can either set the 'AWS_ACCESS_KEY_ID' and -'AWS_SECRET_ACCESS_KEY' environment variables[1] (as usual for other EC2 tools) -or you can add access-key and secret-key options to your environments.yaml. -These are already in place in the generated config, you just need to uncomment -them out. For example: - - sample_ec2: - type: ec2 - # access-key: YOUR-ACCESS-KEY-GOES-HERE - # secret-key: YOUR-SECRET-KEY-GOES-HERE - -See the EC2 provider documentation[2] for more options. - -Note If you already have an AWS account, you can determine your access key by -visiting your account page[3], clicking "Security Credentials" and then clicking -"Access Credentials". You'll be taken to a table that lists your access keys and -has a "show" link for each access key that will reveal the associated secret -key. - -And that's it, you're ready to go! - -Placement directives: - - EC2 environments support the following placement directives for use with - "juju bootstrap" and "juju add-machine": - - zone= - The "zone" placement directive instructs the EC2 provider to - allocate a machine in the specified availability zone. If the zone - does not exist, or a machine cannot be allocated within it, then - the machine addition will fail. - -References: - - [1]: http://askubuntu.com/questions/730/how-do-i-set-environment-variables - [2]: https://juju.ubuntu.com/docs/provider-configuration-ec2.html - [3]: http://aws.amazon.com/account - -More information: - - https://juju.ubuntu.com/docs/getting-started.html - https://juju.ubuntu.com/docs/provider-configuration-ec2.html - http://askubuntu.com/questions/225513/how-do-i-configure-juju-to-use-amazon-web-services-aws -` - -const helpHPCloud = ` -HP Cloud is an Openstack cloud provider. To deploy to it, use an openstack -environment type for Juju, which would look something like this: - - sample_hpcloud: - type: openstack - tenant-name: "juju-project1" - auth-url: https://region-a.geo-1.identity.hpcloudsvc.com:35357/v2.0/ - auth-mode: userpass - username: "xxxyour-hpcloud-usernamexxx" - password: "xxxpasswordxxx" - region: az-1.region-a.geo-1 - -See the online help for more information: - - https://juju.ubuntu.com/docs/config-hpcloud.html -` - -const helpAzureProvider = ` -A generic Windows Azure environment looks like this: - - sample_azure: - type: azure - - # Location for instances, e.g. West US, North Europe. - location: West US - - # http://msdn.microsoft.com/en-us/library/windowsazure - # Windows Azure Management info. - management-subscription-id: 886413e1-3b8a-5382-9b90-0c9aee199e5d - management-certificate-path: /home/me/azure.pem - - # Windows Azure Storage info. - storage-account-name: juju0useast0 - - # Override OS image selection with a fixed image for all deployments. - # Most useful for developers. - # force-image-name: b39f27a8b8c64d52b05eac6a62ebad85__Ubuntu-13_10-amd64-server-DEVELOPMENT-20130713-Juju_ALPHA-en-us-30GB - - # image-stream chooses a simplestreams stream from which to select - # OS images, for example daily or released images (or any other stream - # available on simplestreams). - # - # image-stream: "released" - - # agent-stream chooses a simplestreams stream from which to select tools, - # for example released or proposed tools (or any other stream available - # on simplestreams). - # - # agent-stream: "released" - -This is the environments.yaml configuration file needed to run on Windows Azure. -You will need to set the management-subscription-id, management-certificate- -path, and storage-account-name. - -Note: Other than location, the defaults are recommended, but can be updated to -your preference. - -See the online help for more information: - - https://juju.ubuntu.com/docs/config-azure.html -` - -const helpMAASProvider = ` -A generic MAAS environment looks like this: - - sample_maas: - type: maas - maas-server: 'http://:80/MAAS' - maas-oauth: 'MAAS-API-KEY' - -The API key can be obtained from the preferences page in the MAAS web UI. - -Placement directives: - - MAAS environments support the following placement directives for use with - "juju bootstrap" and "juju add-machine": - - zone= - The "zone" placement directive instructs the MAAS provider to - allocate a machine in the specified availability zone. If the zone - does not exist, or a machine cannot be allocated within it, then - the machine addition will fail. - - - If the placement directive does not contain an "=" symbol, then - it is assumed to be the hostname of a node in MAAS. MAAS will attempt - to acquire that node and will fail if it cannot. - -See the online help for more information: - - https://juju.ubuntu.com/docs/config-maas.html -` - -const helpConstraints = ` -Constraints constrain the possible instances that may be started by juju -commands. They are usually passed as a flag to commands that provision a new -machine (such as bootstrap, deploy, and add-machine). - -Each constraint defines a minimum acceptable value for a characteristic of a -machine. Juju will provision the least expensive machine that fulfills all the -constraints specified. Note that these values are the minimum, and the actual -machine used may exceed these specifications if one that exactly matches does -not exist. - -If a constraint is defined that cannot be fulfilled by any machine in the -environment, no machine will be provisioned, and an error will be printed in the -machine's entry in juju status. - -Constraint defaults can be set on an environment or on specific services by -using the set-constraints command (see juju help set-constraints). Constraints -set on the environment or on a service can be viewed by using the get- -constraints command. In addition, you can specify constraints when executing a -command by using the --constraints flag (for commands that support it). - -Constraints specified on the environment and service will be combined to -determine the full list of constraints on the machine(s) to be provisioned by -the command. Service-specific constraints will override environment-specific -constraints, which override the juju default constraints. - -Constraints are specified as key value pairs separated by an equals sign, with -multiple constraints delimited by a space. - -Constraint Types: - -arch - Arch defines the CPU architecture that the machine must have. Currently - recognized architectures: - amd64 (default) - i386 - arm - -cpu-cores - Cpu-cores is a whole number that defines the number of effective cores the - machine must have available. - -mem - Mem is a float with an optional suffix that defines the minimum amount of RAM - that the machine must have. The value is rounded up to the next whole - megabyte. The default units are megabytes, but you can use a size suffix to - use other units: - - M megabytes (default) - G gigabytes (1024 megabytes) - T terabytes (1024 gigabytes) - P petabytes (1024 terabytes) - -root-disk - Root-Disk is a float that defines the amount of space in megabytes that must - be available in the machine's root partition. For providers that have - configurable root disk sizes (such as EC2) an instance with the specified - amount of disk space in the root partition may be requested. Root disk size - defaults to megabytes and may be specified in the same manner as the mem - constraint. - -container - Container defines that the machine must be a container of the specified type. - A container of that type may be created by juju to fulfill the request. - Currently supported containers: - none - (default) no container - lxc - an lxc container - kvm - a kvm container - -cpu-power - Cpu-power is a whole number that defines the speed of the machine's CPU, - where 100 CpuPower is considered to be equivalent to 1 Amazon ECU (or, - roughly, a single 2007-era Xeon). Cpu-power is currently only supported by - the Amazon EC2 environment. - -tags - Tags defines the list of tags that the machine must have applied to it. - Multiple tags must be delimited by a comma. Both positive and negative - tags constraints are supported, the latter have a "^" prefix. Tags are - currently only supported by the MaaS environment. - -networks - Networks defines the list of networks to ensure are available or not on the - machine. Both positive and negative network constraints can be specified, the - later have a "^" prefix to the name. Multiple networks must be delimited by a - comma. Not supported on all providers. Example: networks=storage,db,^logging - specifies to select machines with "storage" and "db" networks but not "logging" - network. Positive network constraints do not imply the networks will be enabled, - use the --networks argument for that, just that they could be enabled. - -instance-type - Instance-type is the provider-specific name of a type of machine to deploy, - for example m1.small on EC2 or A4 on Azure. Specifying this constraint may - conflict with other constraints depending on the provider (since the instance - type my determine things like memory size etc.) - -Example: - - juju add-machine --constraints "arch=amd64 mem=8G tags=foo,^bar" - -See Also: - juju help set-constraints - juju help get-constraints - juju help deploy - juju help add-unit - juju help add-machine - juju help bootstrap -` - -const helpPlacement = ` -Placement directives provide users with a means of providing instruction -to the cloud provider on how to allocate a machine. For example, the MAAS -provider can be directed to acquire a particular node by specifying its -hostname. - -See provider-specific documentation for details of placement directives -supported by that provider. - -Examples: - - # Bootstrap using an instance in the "us-east-1a" EC2 availability zone. - juju bootstrap --to zone=us-east-1a - - # Acquire the node "host01.maas" and add it to Juju. - juju add-machine host01.maas - -See also: - juju help add-machine - juju help bootstrap -` - -const helpGlossary = ` -Bootstrap - To boostrap an environment means initializing it so that Services may be - deployed on it. - -Charm - A Charm provides the definition of the service, including its metadata, - dependencies to other services, packages necessary, as well as the logic for - management of the application. It is the layer that integrates an external - application component like Postgres or WordPress into Juju. A Juju Service may - generally be seen as the composition of its Juju Charm and the upstream - application (traditionally made available through its package). - -Charm URL - A Charm URL is a resource locator for a charm, with the following format and - restrictions: - - :[~/]/[-] - - schema must be either "cs", for a charm from the Juju charm store, or "local", - for a charm from a local repository. - - user is only valid in charm store URLs, and allows you to source charms from - individual users (rather than from the main charm store); it must be a valid - Launchpad user name. - - collection denotes a charm's purpose and status, and is derived from the - Ubuntu series targeted by its contained charms: examples include "precise", - "quantal", "oneiric-universe". - - name is just the name of the charm; it must start and end with lowercase - (ascii) letters, and can otherwise contain any combination of lowercase - letters, digits, and "-"s. - - revision, if specified, points to a specific revision of the charm pointed to - by the rest of the URL. It must be a non-negative integer. - -Endpoint - The combination of a service name and a relation name. - -Environment - An Environment is a configured location where Services can be deployed onto. - An Environment typically has a name, which can usually be omitted when there's - a single Environment configured, or when a default is explicitly defined. - Depending on the type of Environment, it may have to be bootstrapped before - interactions with it may take place (e.g. EC2). The local environment - configuration is defined in the ~/.juju/environments.yaml file. - -Machine Agent - Software which runs inside each machine that is part of an Environment, and is - able to handle the needs of deploying and managing Service Units in this - machine. - -Placement Directive - A provider-specific string that directs the provisioner on how to allocate a - machine instance. - -Provisioning Agent - Software responsible for automatically allocating and terminating machines in - an Environment, as necessary for the requested configuration. - -Relation - Relations are the way in which Juju enables Services to communicate to each - other, and the way in which the topology of Services is assembled. The Charm - defines which Relations a given Service may establish, and what kind of - interface these Relations require. - - In many cases, the establishment of a Relation will result into an actual TCP - connection being created between the Service Units, but that's not necessarily - the case. Relations may also be established to inform Services of - configuration parameters, to request monitoring information, or any other - details which the Charm author has chosen to make available. - -Repository - A location where multiple charms are stored. Repositories may be as simple as - a directory structure on a local disk, or as complex as a rich smart server - supporting remote searching and so on. - -Service - Juju operates in terms of services. A service is any application (or set of - applications) that is integrated into the framework as an individual component - which should generally be joined with other components to perform a more - complex goal. - - As an example, WordPress could be deployed as a service and, to perform its - tasks properly, might communicate with a database service and a load balancer - service. - -Service Configuration - There are many different settings in a Juju deployment, but the term Service - Configuration refers to the settings which a user can define to customize the - behavior of a Service. - - The behavior of a Service when its Service Configuration changes is entirely - defined by its Charm. - -Service Unit - A running instance of a given Juju Service. Simple Services may be deployed - with a single Service Unit, but it is possible for an individual Service to - have multiple Service Units running in independent machines. All Service Units - for a given Service will share the same Charm, the same relations, and the - same user-provided configuration. - - For instance, one may deploy a single MongoDB Service, and specify that it - should run 3 Units, so that the replica set is resilient to failures. - Internally, even though the replica set shares the same user-provided - configuration, each Unit may be performing different roles within the replica - set, as defined by the Charm. - -Service Unit Agent - Software which manages all the lifecycle of a single Service Unit. - -` - -const helpLogging = ` -Juju has logging available for both client and server components. Most -users' exposure to the logging mechanism is through either the 'debug-log' -command, or through the log file stored on the bootstrap node at -/var/log/juju/all-machines.log. - -All the agents have their own log files on the individual machines. So -for the bootstrap node, there is the machine agent log file at -/var/log/juju/machine-0.log. When a unit is deployed on a machine, -a unit agent is started. This agent also logs to /var/log/juju and the -name of the log file is based on the id of the unit, so for wordpress/0 -the log file is unit-wordpress-0.log. - -Juju uses rsyslog to forward the content of all the log files on the machine -back to the bootstrap node, and they are accumulated into the all-machines.log -file. Each line is prefixed with the source agent tag (also the same as -the filename without the extension). - -Juju has a hierarchical logging system internally, and as a user you can -control how much information is logged out. - -Output from the charm hook execution comes under the log name "unit". -By default Juju makes sure that this information is logged out at -the DEBUG level. If you explicitly specify a value for unit, then -this is used instead. - -Juju internal logging comes under the log name "juju". Different areas -of the codebase have different anmes. For example: - providers are under juju.provider - workers are under juju.worker - database parts are under juju.state - -All the agents are started with all logging set to DEBUG. Which means you -see all the internal juju logging statements until the logging worker starts -and updates the logging configuration to be what is stored for the environment. - -You can set the logging levels using a number of different mechanisms. - -environments.yaml - - all environments support 'logging-config' as a key - - logging-config: ... -environment variable - - export JUJU_LOGGING_CONFIG='...' -setting the logging-config at bootstrap time - - juju bootstrap --logging-config='...' -juju set-environment logging-config='...' - -Configuration values are separated by semicolons. - -Examples: - - juju set-environment logging-config "juju=WARNING; unit=INFO" - -Developers may well like: - - export JUJU_LOGGING_CONFIG='juju=INFO; juju.current.work.area=TRACE' - -Valid logging levels: - CRITICAL - ERROR - WARNING - INFO - DEBUG - TRACE -` === removed file 'src/github.com/juju/juju/cmd/juju/helptool.go' --- src/github.com/juju/juju/cmd/juju/helptool.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/helptool.go 1970-01-01 00:00:00 +0000 @@ -1,140 +0,0 @@ -// Copyright 2013, 2014 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - "time" - - "github.com/juju/cmd" - "gopkg.in/juju/charm.v5" - "launchpad.net/gnuflag" - - "github.com/juju/juju/network" - "github.com/juju/juju/storage" - "github.com/juju/juju/worker/uniter/runner/jujuc" -) - -// dummyHookContext implements jujuc.Context, -// as expected by jujuc.NewCommand. -type dummyHookContext struct{ jujuc.Context } - -func (dummyHookContext) AddMetrics(_, _ string, _ time.Time) error { - return nil -} -func (dummyHookContext) UnitName() string { - return "" -} -func (dummyHookContext) PublicAddress() (string, bool) { - return "", false -} -func (dummyHookContext) PrivateAddress() (string, bool) { - return "", false -} -func (dummyHookContext) AvailabilityZone() (string, bool) { - return "", false -} -func (dummyHookContext) OpenPort(protocol string, port int) error { - return nil -} -func (dummyHookContext) ClosePort(protocol string, port int) error { - return nil -} -func (dummyHookContext) OpenedPorts() []network.PortRange { - return nil -} -func (dummyHookContext) ConfigSettings() (charm.Settings, error) { - return charm.NewConfig().DefaultSettings(), nil -} -func (dummyHookContext) HookRelation() (jujuc.ContextRelation, bool) { - return nil, false -} -func (dummyHookContext) RemoteUnitName() (string, bool) { - return "", false -} -func (dummyHookContext) Relation(id int) (jujuc.ContextRelation, bool) { - return nil, false -} -func (dummyHookContext) RelationIds() []int { - return []int{} -} - -func (dummyHookContext) RequestReboot(prio jujuc.RebootPriority) error { - return nil -} - -func (dummyHookContext) HookStorageInstance() (*storage.StorageInstance, bool) { - return nil, false -} - -func (dummyHookContext) StorageInstance(id string) (*storage.StorageInstance, bool) { - return nil, false -} - -func (dummyHookContext) OwnerTag() string { - return "" -} - -func (dummyHookContext) UnitStatus() (*jujuc.StatusInfo, error) { - return &jujuc.StatusInfo{}, nil -} - -func (dummyHookContext) SetStatus(jujuc.StatusInfo) error { - return nil -} - -type HelpToolCommand struct { - cmd.CommandBase - tool string -} - -func (t *HelpToolCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "help-tool", - Args: "[tool]", - Purpose: "show help on a juju charm tool", - } -} - -func (t *HelpToolCommand) Init(args []string) error { - tool, err := cmd.ZeroOrOneArgs(args) - if err == nil { - t.tool = tool - } - return err -} - -func (c *HelpToolCommand) Run(ctx *cmd.Context) error { - var hookctx dummyHookContext - if c.tool == "" { - // Ripped from SuperCommand. We could Run() a SuperCommand - // with "help commands", but then the implicit "help" command - // shows up. - names := jujuc.CommandNames() - cmds := make([]cmd.Command, 0, len(names)) - longest := 0 - for _, name := range names { - if c, err := jujuc.NewCommand(hookctx, name); err == nil { - if len(name) > longest { - longest = len(name) - } - cmds = append(cmds, c) - } - } - for _, c := range cmds { - info := c.Info() - fmt.Fprintf(ctx.Stdout, "%-*s %s\n", longest, info.Name, info.Purpose) - } - } else { - c, err := jujuc.NewCommand(hookctx, c.tool) - if err != nil { - return err - } - info := c.Info() - f := gnuflag.NewFlagSet(info.Name, gnuflag.ContinueOnError) - c.SetFlags(f) - ctx.Stdout.Write(info.Help(f)) - } - return nil -} === removed file 'src/github.com/juju/juju/cmd/juju/helptool_test.go' --- src/github.com/juju/juju/cmd/juju/helptool_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/helptool_test.go 1970-01-01 00:00:00 +0000 @@ -1,58 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "runtime" - "strings" - - gc "gopkg.in/check.v1" - - "github.com/juju/juju/testing" - "github.com/juju/juju/worker/uniter/runner/jujuc" -) - -type HelpToolSuite struct { - testing.FakeJujuHomeSuite -} - -var _ = gc.Suite(&HelpToolSuite{}) - -func (suite *HelpToolSuite) TestHelpToolHelp(c *gc.C) { - output := badrun(c, 0, "help", "help-tool") - c.Assert(output, gc.Equals, `usage: juju help-tool [tool] -purpose: show help on a juju charm tool -`) -} - -func (suite *HelpToolSuite) TestHelpTool(c *gc.C) { - expectedNames := jujuc.CommandNames() - output := badrun(c, 0, "help-tool") - lines := strings.Split(strings.TrimSpace(output), "\n") - for i, line := range lines { - lines[i] = strings.Fields(line)[0] - } - if runtime.GOOS == "windows" { - for i, command := range lines { - lines[i] = command + ".exe" - } - } - c.Assert(lines, gc.DeepEquals, expectedNames) -} - -func (suite *HelpToolSuite) TestHelpToolName(c *gc.C) { - var output string - if runtime.GOOS == "windows" { - output = badrun(c, 0, "help-tool", "relation-get.exe") - } else { - output = badrun(c, 0, "help-tool", "relation-get") - } - expectedHelp := `usage: relation-get \[options\] -purpose: get relation settings - -options: -(.|\n)* -relation-get prints the value(.|\n)*` - c.Assert(output, gc.Matches, expectedHelp) -} === added directory 'src/github.com/juju/juju/cmd/juju/helptopics' === added file 'src/github.com/juju/juju/cmd/juju/helptopics/basics.go' --- src/github.com/juju/juju/cmd/juju/helptopics/basics.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/helptopics/basics.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,33 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package helptopics + +const Basics = ` +Juju -- devops distilled +https://juju.ubuntu.com/ + +Juju provides easy, intelligent service orchestration on top of environments +such as Amazon EC2, HP Cloud, OpenStack, MaaS, or your own local machine. + +Basic commands: + juju init generate boilerplate configuration for juju environments + juju bootstrap start up an environment from scratch + + juju deploy deploy a new service + juju add-relation add a relation between two services + juju expose expose a service + + juju help juju what is juju? + juju help bootstrap more help on e.g. bootstrap command + juju help commands list all commands + juju help glossary glossary of terms + juju help topics list all help topics + +Provider information: + juju help azure-provider use on Windows Azure + juju help ec2-provider use on Amazon EC2 + juju help hpcloud-provider use on HP Cloud + juju help local-provider use on this computer + juju help openstack-provider use on OpenStack +` === added file 'src/github.com/juju/juju/cmd/juju/helptopics/constraints.go' --- src/github.com/juju/juju/cmd/juju/helptopics/constraints.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/helptopics/constraints.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,115 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package helptopics + +const Constraints = ` +Constraints constrain the possible instances that may be started by juju +commands. They are usually passed as a flag to commands that provision a new +machine (such as bootstrap, deploy, and add-machine). + +Each constraint defines a minimum acceptable value for a characteristic of a +machine. Juju will provision the least expensive machine that fulfills all the +constraints specified. Note that these values are the minimum, and the actual +machine used may exceed these specifications if one that exactly matches does +not exist. + +If a constraint is defined that cannot be fulfilled by any machine in the +environment, no machine will be provisioned, and an error will be printed in the +machine's entry in juju status. + +Constraint defaults can be set on an environment or on specific services by +using the set-constraints command (see juju help set-constraints). Constraints +set on the environment or on a service can be viewed by using the get- +constraints command. In addition, you can specify constraints when executing a +command by using the --constraints flag (for commands that support it). + +Constraints specified on the environment and service will be combined to +determine the full list of constraints on the machine(s) to be provisioned by +the command. Service-specific constraints will override environment-specific +constraints, which override the juju default constraints. + +Constraints are specified as key value pairs separated by an equals sign, with +multiple constraints delimited by a space. + +Constraint Types: + +arch + Arch defines the CPU architecture that the machine must have. Currently + recognized architectures: + amd64 (default) + i386 + arm + +cpu-cores + Cpu-cores is a whole number that defines the number of effective cores the + machine must have available. + +mem + Mem is a float with an optional suffix that defines the minimum amount of RAM + that the machine must have. The value is rounded up to the next whole + megabyte. The default units are megabytes, but you can use a size suffix to + use other units: + + M megabytes (default) + G gigabytes (1024 megabytes) + T terabytes (1024 gigabytes) + P petabytes (1024 terabytes) + +root-disk + Root-Disk is a float that defines the amount of space in megabytes that must + be available in the machine's root partition. For providers that have + configurable root disk sizes (such as EC2) an instance with the specified + amount of disk space in the root partition may be requested. Root disk size + defaults to megabytes and may be specified in the same manner as the mem + constraint. + +container + Container defines that the machine must be a container of the specified type. + A container of that type may be created by juju to fulfill the request. + Currently supported containers: + none - (default) no container + lxc - an lxc container + kvm - a kvm container + +cpu-power + Cpu-power is a whole number that defines the speed of the machine's CPU, + where 100 CpuPower is considered to be equivalent to 1 Amazon ECU (or, + roughly, a single 2007-era Xeon). Cpu-power is currently only supported by + the Amazon EC2 environment. + +tags + Tags defines the list of tags that the machine must have applied to it. + Multiple tags must be delimited by a comma. Both positive and negative + tags constraints are supported, the latter have a "^" prefix. Tags are + currently only supported by the MaaS environment. + +spaces + Spaces constraint allows specifying a list of Juju network space names a unit + or machine needs access to. Both positive and negative (prefixed with "^") + spaces can be in the list, separated by commas. + + Example: spaces=storage,db,^logging,^public (meaning, select machines connected + to the storage and db spaces, but NOT to logging or public spaces). + + EC2 is the only provider supporting spaces constraints. Support for other + providers is planned for future releases. + +instance-type + Instance-type is the provider-specific name of a type of machine to deploy, + for example m1.small on EC2 or A4 on Azure. Specifying this constraint may + conflict with other constraints depending on the provider (since the instance + type my determine things like memory size etc.) + + Example: + + juju add-machine --constraints "arch=amd64 mem=8G tags=foo,^bar" + +See Also: + juju help set-constraints + juju help get-constraints + juju help deploy + juju help add-unit + juju help add-machine + juju help bootstrap +` === added file 'src/github.com/juju/juju/cmd/juju/helptopics/glossary.go' --- src/github.com/juju/juju/cmd/juju/helptopics/glossary.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/helptopics/glossary.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,135 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package helptopics + +const Glossary = ` +Bootstrap + To boostrap an environment means initializing it so that Services may be + deployed on it. + +Charm + A Charm provides the definition of the service, including its metadata, + dependencies to other services, packages necessary, as well as the logic for + management of the application. It is the layer that integrates an external + application component like Postgres or WordPress into Juju. A Juju Service may + generally be seen as the composition of its Juju Charm and the upstream + application (traditionally made available through its package). + +Charm URL + A Charm URL is a resource locator for a charm, with the following format and + restrictions: + + :[~/]/[-] + + schema must be either "cs", for a charm from the Juju charm store, or "local", + for a charm from a local repository. + + user is only valid in charm store URLs, and allows you to source charms from + individual users (rather than from the main charm store); it must be a valid + Launchpad user name. + + collection denotes a charm's purpose and status, and is derived from the + Ubuntu series targeted by its contained charms: examples include "precise", + "quantal", "oneiric-universe". + + name is just the name of the charm; it must start and end with lowercase + (ascii) letters, and can otherwise contain any combination of lowercase + letters, digits, and "-"s. + + revision, if specified, points to a specific revision of the charm pointed to + by the rest of the URL. It must be a non-negative integer. + +Endpoint + The combination of a service name and a relation name. + +Environment + An Environment is a configured location where Services can be deployed onto. + An Environment typically has a name, which can usually be omitted when there's + a single Environment configured, or when a default is explicitly defined. + Depending on the type of Environment, it may have to be bootstrapped before + interactions with it may take place (e.g. EC2). The local environment + configuration is defined in the ~/.juju/environments.yaml file. + +Machine Agent + Software which runs inside each machine that is part of an Environment, and is + able to handle the needs of deploying and managing Service Units in this + machine. + +Placement Directive + A provider-specific string that directs the provisioner on how to allocate a + machine instance. + +Provisioning Agent + Software responsible for automatically allocating and terminating machines in + an Environment, as necessary for the requested configuration. + +Relation + Relations are the way in which Juju enables Services to communicate to each + other, and the way in which the topology of Services is assembled. The Charm + defines which Relations a given Service may establish, and what kind of + interface these Relations require. + + In many cases, the establishment of a Relation will result into an actual TCP + connection being created between the Service Units, but that's not necessarily + the case. Relations may also be established to inform Services of + configuration parameters, to request monitoring information, or any other + details which the Charm author has chosen to make available. + +Repository + A location where multiple charms are stored. Repositories may be as simple as + a directory structure on a local disk, or as complex as a rich smart server + supporting remote searching and so on. + +Service + Juju operates in terms of services. A service is any application (or set of + applications) that is integrated into the framework as an individual component + which should generally be joined with other components to perform a more + complex goal. + + As an example, WordPress could be deployed as a service and, to perform its + tasks properly, might communicate with a database service and a load balancer + service. + +Service Configuration + There are many different settings in a Juju deployment, but the term Service + Configuration refers to the settings which a user can define to customize the + behavior of a Service. + + The behavior of a Service when its Service Configuration changes is entirely + defined by its Charm. + +Service Unit + A running instance of a given Juju Service. Simple Services may be deployed + with a single Service Unit, but it is possible for an individual Service to + have multiple Service Units running in independent machines. All Service Units + for a given Service will share the same Charm, the same relations, and the + same user-provided configuration. + + For instance, one may deploy a single MongoDB Service, and specify that it + should run 3 Units, so that the replica set is resilient to failures. + Internally, even though the replica set shares the same user-provided + configuration, each Unit may be performing different roles within the replica + set, as defined by the Charm. + +Service Unit Agent + Software which manages all the lifecycle of a single Service Unit. + +Subnet + A broadcast address range identified by a CIDR like 10.1.2.0/24, or 2001:db8::/32. + +Space + A collection of subnets which must be routable between each other without + firewalls. A subnet can be in one and only one space. Connections between + spaces instead are assumed to go through firewalls. + + Spaces and their subnets can span multiples zones, if supported by the + cloud provider. + +Zone + Zones, also known as Availability Zones, are running on physically distinct, + independent infrastructure. They are engineered to be highly reliable. + Common points of failures like generators and cooling equipment are not shared + across Zones. Additionally, they are physically separate, such that even extremely + uncommon disasters would only affect a single Zone. +` === added file 'src/github.com/juju/juju/cmd/juju/helptopics/juju.go' --- src/github.com/juju/juju/cmd/juju/helptopics/juju.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/helptopics/juju.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,50 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package helptopics + +const Juju = ` + +What is Juju? + +Juju is a state-of-the-art, open source, universal model for service oriented +architecture and service oriented deployments. Juju allows you to deploy, +configure, manage, maintain, and scale cloud services quickly and efficiently +on public clouds, as well as on physical servers, OpenStack, and containers. +You can use Juju from the command line or through its beautiful GUI. + +What is service modelling? + +In modern environments, services are rarely deployed in isolation. Even simple +applications may require several actual services in order to function - like a +database and a web server for example. For deploying a more complex system, +e.g. OpenStack, many more services need to be installed, configured and +connected to each other. Juju's service modelling provides tools to express +the intent of how to deploy such services and to subsequently scale and manage +them. + +At the lowest level, traditional configuration management tools like Chef and +Puppet, or even general scripting languages such as Python or bash, automate +the configuration of machines to a particular specification. With Juju, you +create a model of the relationships between services that make up your +solution and you have a mapping of the parts of that model to machines. Juju +then applies the necessary configuration management scripts to each machine in +the model. + +Application-specific knowledge such as dependencies, scale-out practices, +operational events like backups and updates, and integration options with +other pieces of software are encapsulated in Juju's 'charms'. This knowledge +can then be shared between team members, reused everywhere from laptops to +virtual machines and cloud, and shared with other organisations. + +The charm defines everything you all collaboratively know about deploying that +particular service brilliantly. All you have to do is use any available charm +(or write your own), and the corresponding service will be deployed in +seconds, on any cloud or server or virtual machine. + +See Also: + juju help juju-systems + juju help bootstrap + juju help topics + https://jujucharms.com/docs +` === added file 'src/github.com/juju/juju/cmd/juju/helptopics/logging.go' --- src/github.com/juju/juju/cmd/juju/helptopics/logging.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/helptopics/logging.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,70 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package helptopics + +const Logging = ` +Juju has logging available for both client and server components. Most +users' exposure to the logging mechanism is through either the 'debug-log' +command, or through the log file stored on the bootstrap node at +/var/log/juju/all-machines.log. + +All the agents have their own log files on the individual machines. So +for the bootstrap node, there is the machine agent log file at +/var/log/juju/machine-0.log. When a unit is deployed on a machine, +a unit agent is started. This agent also logs to /var/log/juju and the +name of the log file is based on the id of the unit, so for wordpress/0 +the log file is unit-wordpress-0.log. + +Juju uses rsyslog to forward the content of all the log files on the machine +back to the bootstrap node, and they are accumulated into the all-machines.log +file. Each line is prefixed with the source agent tag (also the same as +the filename without the extension). + +Juju has a hierarchical logging system internally, and as a user you can +control how much information is logged out. + +Output from the charm hook execution comes under the log name "unit". +By default Juju makes sure that this information is logged out at +the DEBUG level. If you explicitly specify a value for unit, then +this is used instead. + +Juju internal logging comes under the log name "juju". Different areas +of the codebase have different anmes. For example: + providers are under juju.provider + workers are under juju.worker + database parts are under juju.state + +All the agents are started with all logging set to DEBUG. Which means you +see all the internal juju logging statements until the logging worker starts +and updates the logging configuration to be what is stored for the environment. + +You can set the logging levels using a number of different mechanisms. + +environments.yaml + - all environments support 'logging-config' as a key + - logging-config: ... +environment variable + - export JUJU_LOGGING_CONFIG='...' +setting the logging-config at bootstrap time + - juju bootstrap --logging-config='...' +juju set-environment logging-config='...' + +Configuration values are separated by semicolons. + +Examples: + + juju set-environment logging-config "juju=WARNING; unit=INFO" + +Developers may well like: + + export JUJU_LOGGING_CONFIG='juju=INFO; juju.current.work.area=TRACE' + +Valid logging levels: + CRITICAL + ERROR + WARNING + INFO + DEBUG + TRACE +` === added file 'src/github.com/juju/juju/cmd/juju/helptopics/networking.go' --- src/github.com/juju/juju/cmd/juju/helptopics/networking.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/helptopics/networking.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,87 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package helptopics + +const Networking = ` +Juju provides a set of features allowing the users to have better and +finer-grained control over the networking aspects of the environment +and service deployments in particular. Not all cloud providers support +these enhanced networking features yet, in fact they are currently +supported on AWS only. Support for MaaS and OpenStack is planed and +will be available in future releases of Juju. + +Juju network spaces (or just "spaces") represent sets of disjunct +subnets available for running cloud instances, which may span one +or more availability zones ("zones"). Subnets can be part of one +and only one space. All subnets within a space are considered "equal" +in terms of access control, firewall rules, and routing. Communication +between spaces on the other hand (e.g. between instances started in +subnets part of different spaces) will be subject to access restrictions +and isolation. + +Having multiple subnets spanning different zones within the same space +allows Juju to perform automatic distribution of units of a service +across zones inside the same space. This allows for high-availability +for services and spreading the instances evenly across subnets and zones. + +As an example, consider an environment divided into three segments with +distinct security requirements: + +- The "dmz" space for publicly-accessible services (e.g. HAProxy) providing + access to the CMS application behind it. +- The "cms" space for content-management applications accessible via the "dmz" + space only. + -The "database" space for backend database services, which should be accessible + only by the applications. + +HAProxy is deployed inside the "dmz" space, it is accessible from the Internet +and proxies HTTP requests to one or more Joomla units in the "cms" space. +The backend MySQL for Joomla is running in the "database" space. All subnets +within the "cms" and "database" spaces provide no access from outside the +environment for security reasons. Using spaces for deployments like this allows +Juju to have the necessary information about how to configure the firewall and +access control rules. In this case, instances in "dmz" can only communicate +with instances in "apps", which in turn are the only ones allowed to access +instances in "database". + +Please note, Juju does not yet enforce those security restrictions, but having +spaces and subnets available makes it possible to implement those restrictions +and access control in a future release. + +Due to the ability of spaces to span multiple zones services can be distributed +across these zones. This allows high available setup for services within the +environment. + +Spaces are created like this: + +$ juju space create [ ... ] [--private|--public] + +They can be listed in various formats using the "list" subcommand. See +also "juju space help" for more information. + +Existing subnets can be added to the environment using + +$ juju subnet add | [ ...] + +Like spaces they can be listed by the subcommand "list". See +also "juju space help" for more information. + +The commands "add-machine" and "deploy" allow the specification of a +spaces constraint for the selection of a matching instance. It is done by +adding: + +--constraints spaces=,,^ + +The constraint controls which subnet the new instance will be started in. +This instance has to have distinct IP addresses on any subnet of each allowed +space in the list and none of the subnets associated with one of the disallowed +spaces which are prefixed with a caret ("^"). + +For more information, see "juju help constraints". + +Please note, Juju supports the described syntax but currently ignores all but +the first allowed space in the list. This behavior will change in a future release. +Also, only the EC2 provider supports spaces as described, with support for MaaS +and OpenStack coming soon. +` === added file 'src/github.com/juju/juju/cmd/juju/helptopics/placement.go' --- src/github.com/juju/juju/cmd/juju/helptopics/placement.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/helptopics/placement.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,26 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package helptopics + +const Placement = ` +Placement directives provide users with a means of providing instruction +to the cloud provider on how to allocate a machine. For example, the MAAS +provider can be directed to acquire a particular node by specifying its +hostname. + +See provider-specific documentation for details of placement directives +supported by that provider. + +Examples: + + # Bootstrap using an instance in the "us-east-1a" EC2 availability zone. + juju bootstrap --to zone=us-east-1a + + # Acquire the node "host01.maas" and add it to Juju. + juju add-machine host01.maas + +See also: + juju help add-machine + juju help bootstrap +` === added file 'src/github.com/juju/juju/cmd/juju/helptopics/providers.go' --- src/github.com/juju/juju/cmd/juju/helptopics/providers.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/helptopics/providers.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,286 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package helptopics + +const ( + LocalProvider = helpProviderStart + helpLocalProvider + helpProviderEnd + OpenstackProvider = helpProviderStart + helpOpenstackProvider + helpProviderEnd + EC2Provider = helpProviderStart + helpEC2Provider + helpProviderEnd + HPCloud = helpProviderStart + helpHPCloud + helpProviderEnd + AzureProvider = helpProviderStart + helpAzureProvider + helpProviderEnd + MAASProvider = helpProviderStart + helpMAASProvider + helpProviderEnd +) + +const helpProviderStart = ` +Start by generating a generic configuration file for Juju, using the command: + + juju init + +This will create the '~/.juju/' directory (or $JUJU_HOME, if set) if it doesn't +already exist and generate a file, 'environments.yaml' in that directory. +` +const helpProviderEnd = ` +See Also: + + juju help init + juju help bootstrap + +` + +const helpLocalProvider = ` +The local provider is a Linux-only Juju environment that uses LXC containers as +a virtual cloud on the local machine. Because of this, lxc and mongodb are +required for the local provider to work. All of these dependencies are tracked +in the 'juju-local' package. You can install that with: + + sudo apt-get update + sudo apt-get install juju-local + +After that you might get error for SSH authorised/public key not found. ERROR +SSH authorised/public key not found. + + ssh-keygen -t rsa + +Now you need to tell Juju to use the local provider and then bootstrap: + + juju switch local + juju bootstrap + +The first time this runs it might take a bit, as it's doing a netinstall for +the container, it's around a 300 megabyte download. Subsequent bootstraps +should be much quicker. You'll be asked for your 'sudo' password, which is +needed because only root can create LXC containers. When you need to destroy +the environment, do 'juju destroy-environment local' and you could be asked +for your 'sudo' password again. + +You deploy charms from the charm store using the following commands: + + juju deploy mysql + juju deploy wordpress + juju add-relation wordpress mysql + +For Ubuntu deployments, the local provider will prefer to use lxc-clone to create +the machines for the trusty OS series and later. +A 'template' container is created with the name + juju--template +where is the OS series, for example 'juju-trusty-template'. +You can override the use of clone by specifying + lxc-clone: true +or + lxc-clone: false +in the configuration for your local provider. If you have the main container +directory mounted on a btrfs partition, then the clone will be using btrfs +snapshots to create the containers. This means that clones use up much +less disk space. If you do not have btrfs, lxc will attempt to use aufs +(an overlay type filesystem). You can explicitly ask Juju to create +full containers and not overlays by specifying the following in the provider +configuration: + lxc-clone-aufs: false + + +References: + + http://askubuntu.com/questions/65359/how-do-i-configure-juju-for-local-usage + https://juju.ubuntu.com/docs/getting-started.html +` + +const helpOpenstackProvider = ` +Here's an example OpenStack configuration: + + sample_openstack: + type: openstack + + # Specifies whether the use of 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. + # use-floating-ip: false + + # Specifies whether new machine instances should have the "default" + # Openstack security group assigned. + # use-default-secgroup: false + + # Usually set via the env variable OS_AUTH_URL, but can be specified here + # auth-url: https://yourkeystoneurl:443/v2.0/ + + # The following are used for userpass authentication (the default) + # auth-mode: userpass + + # Usually set via the env variable OS_USERNAME, but can be specified here + # username: + + # Usually set via the env variable OS_PASSWORD, but can be specified here + # password: + + # Usually set via the env variable OS_TENANT_NAME, but can be specified here + # tenant-name: + + # Usually set via the env variable OS_REGION_NAME, but can be specified here + # region: + +If you have set the described OS_* environment variables, you only need "type:". +References: + + http://juju.ubuntu.com/docs/provider-configuration-openstack.html + http://askubuntu.com/questions/132411/how-can-i-configure-juju-for-deployment-on-openstack + +Placement directives: + + OpenStack environments support the following placement directives for use + with "juju bootstrap" and "juju add-machine": + + zone= + The "zone" placement directive instructs the OpenStack provider to + allocate a machine in the specified availability zone. If the zone + does not exist, or a machine cannot be allocated within it, then + the machine addition will fail. + +Other OpenStack Based Clouds: + +This answer is for generic OpenStack support, if you're using an OpenStack-based +provider check these questions out for provider-specific information: + + https://juju.ubuntu.com/docs/config-hpcloud.html + +` + +const helpEC2Provider = ` +Configuring the EC2 environment requires telling Juju about your AWS access key +and secret key. To do this, you can either set the 'AWS_ACCESS_KEY_ID' and +'AWS_SECRET_ACCESS_KEY' environment variables[1] (as usual for other EC2 tools) +or you can add access-key and secret-key options to your environments.yaml. +These are already in place in the generated config, you just need to uncomment +them out. For example: + + sample_ec2: + type: ec2 + # access-key: YOUR-ACCESS-KEY-GOES-HERE + # secret-key: YOUR-SECRET-KEY-GOES-HERE + +See the EC2 provider documentation[2] for more options. + +Note If you already have an AWS account, you can determine your access key by +visiting your account page[3], clicking "Security Credentials" and then clicking +"Access Credentials". You'll be taken to a table that lists your access keys and +has a "show" link for each access key that will reveal the associated secret +key. + +And that's it, you're ready to go! + +Placement directives: + + EC2 environments support the following placement directives for use with + "juju bootstrap" and "juju add-machine": + + zone= + The "zone" placement directive instructs the EC2 provider to + allocate a machine in the specified availability zone. If the zone + does not exist, or a machine cannot be allocated within it, then + the machine addition will fail. + +References: + + [1]: http://askubuntu.com/questions/730/how-do-i-set-environment-variables + [2]: https://juju.ubuntu.com/docs/provider-configuration-ec2.html + [3]: http://aws.amazon.com/account + +More information: + + https://juju.ubuntu.com/docs/getting-started.html + https://juju.ubuntu.com/docs/provider-configuration-ec2.html + http://askubuntu.com/questions/225513/how-do-i-configure-juju-to-use-amazon-web-services-aws +` + +const helpHPCloud = ` +HP Cloud is an Openstack cloud provider. To deploy to it, use an openstack +environment type for Juju, which would look something like this: + + sample_hpcloud: + type: openstack + tenant-name: "juju-project1" + auth-url: https://region-a.geo-1.identity.hpcloudsvc.com:35357/v2.0/ + auth-mode: userpass + username: "xxxyour-hpcloud-usernamexxx" + password: "xxxpasswordxxx" + region: az-1.region-a.geo-1 + +See the online help for more information: + + https://juju.ubuntu.com/docs/config-hpcloud.html +` + +const helpAzureProvider = ` +A generic Windows Azure environment looks like this: + + sample_azure: + type: azure + + # Location for instances, e.g. West US, North Europe. + location: West US + + # http://msdn.microsoft.com/en-us/library/windowsazure + # Windows Azure Management info. + management-subscription-id: 886413e1-3b8a-5382-9b90-0c9aee199e5d + management-certificate-path: /home/me/azure.pem + + # Windows Azure Storage info. + storage-account-name: juju0useast0 + + # Override OS image selection with a fixed image for all deployments. + # Most useful for developers. + # force-image-name: b39f27a8b8c64d52b05eac6a62ebad85__Ubuntu-13_10-amd64-server-DEVELOPMENT-20130713-Juju_ALPHA-en-us-30GB + + # image-stream chooses a simplestreams stream from which to select + # OS images, for example daily or released images (or any other stream + # available on simplestreams). + # + # image-stream: "released" + + # agent-stream chooses a simplestreams stream from which to select tools, + # for example released or proposed tools (or any other stream available + # on simplestreams). + # + # agent-stream: "released" + +This is the environments.yaml configuration file needed to run on Windows Azure. +You will need to set the management-subscription-id, management-certificate- +path, and storage-account-name. + +Note: Other than location, the defaults are recommended, but can be updated to +your preference. + +See the online help for more information: + + https://juju.ubuntu.com/docs/config-azure.html +` + +const helpMAASProvider = ` +A generic MAAS environment looks like this: + + sample_maas: + type: maas + maas-server: 'http://:80/MAAS' + maas-oauth: 'MAAS-API-KEY' + +The API key can be obtained from the preferences page in the MAAS web UI. + +Placement directives: + + MAAS environments support the following placement directives for use with + "juju bootstrap" and "juju add-machine": + + zone= + The "zone" placement directive instructs the MAAS provider to + allocate a machine in the specified availability zone. If the zone + does not exist, or a machine cannot be allocated within it, then + the machine addition will fail. + + + If the placement directive does not contain an "=" symbol, then + it is assumed to be the hostname of a node in MAAS. MAAS will attempt + to acquire that node and will fail if it cannot. + +See the online help for more information: + + https://juju.ubuntu.com/docs/config-maas.html +` === added file 'src/github.com/juju/juju/cmd/juju/helptopics/spaces.go' --- src/github.com/juju/juju/cmd/juju/helptopics/spaces.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/helptopics/spaces.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,155 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package helptopics + +const Spaces = ` +Juju provides a set of features allowing the users to have better and +finer-grained control over the networking aspects of the environment +and service deployments in particular. Not all cloud providers support +these enhanced networking features yet, in fact they are currently +supported on AWS only. Support for MaaS and OpenStack is planed and +will be available in future releases of Juju. + +Juju network spaces (or just "spaces") represent sets of disjoint +subnets available for running cloud instances, which may span one +or more availability zones ("zones"). Any given subnet can be part of +one and only one space. All subnets within a space are considered "equal" +in terms of access control, firewall rules, and routing. Communication +between spaces on the other hand (e.g. between instances started in +subnets part of different spaces) will be subject to access restrictions +and isolation. + +Having multiple subnets spanning different zones within the same space +allows Juju to perform automatic distribution of units of a service +across zones inside the same space. This allows for high-availability +for services and spreading the instances evenly across subnets and zones. + +As an example, consider an environment divided into three segments with +distinct security requirements: + +- The "dmz" space for publicly-accessible services (e.g. HAProxy) providing + access to the CMS application behind it. +- The "cms" space for content-management applications accessible via the "dmz" + space only. + -The "database" space for backend database services, which should be accessible + only by the applications. + +HAProxy is deployed inside the "dmz" space, it is accessible from the Internet +and proxies HTTP requests to one or more Joomla units in the "cms" space. +The backend MySQL for Joomla is running in the "database" space. All subnets +within the "cms" and "database" spaces provide no access from outside the +environment for security reasons. Using spaces for deployments like this allows +Juju to have the necessary information about how to configure the firewall and +access control rules. In this case, instances in "dmz" can only communicate +with instances in "apps", which in turn are the only ones allowed to access +instances in "database". + +Please note, Juju does not yet enforce those security restrictions, but having +spaces and subnets available makes it possible to implement those restrictions +and access control in a future release. + +Due to the ability of spaces to span multiple zones services can be distributed +across these zones. This allows high available setup for services within the +environment. + +Spaces are created like this: + +$ juju space create [ ... ] [--private|--public] + +They can be listed in various formats using the "list" subcommand. See +also "juju space help" for more information. Other space subcommands are +"list", "rename", and "remove". + +Existing subnets can be added using: + +$ juju subnet add | [ ...] + +Like spaces they can be listed by the subcommand "list". See +also "juju subnet help" for more information. + +The commands "add-machine" and "deploy" allow the specification of a +spaces constraint for the selection of a matching instance. It is done by +adding: + +--constraints spaces=,,^ + +The spaces constraint allows to select an instance for the new machine or unit, +connected to one or more existing spaces. Both positive and negative entries are +accepted, the latter prefixed by "^", in a comma-delimited list. For example, +given the following: + +--constraints spaces=db,^storage,^dmz,internal, + +Juju will provision instances connected to (with IP addresses on) one of the subnets +of both db and internal spaces, and NOT connected to either the storage or dmz spaces. + +For more information regarding constraints in general, see "juju help constraints". + +Let's model the following deployment in Juju on AWS: + +- DMZ space (with 2 subnets, one in each zone), hosting 2 + units of the haproxy service, which is exposed and provides + access to the CMS application behind it. +- CMS space (also with 2 subnets, one per zone), hosting 2 + units of mediawiki, accessible only via haproxy (not exposed). +- Database (again, 2 subnets, one per zone), hosting 2 units of + mysql, providing the database backend for mediawiki. +- We also assume the used AWS account has a default VPC for the + chosen region (in the example we're using eu-central-1 region). + +First, we need to create additional subnets within the default VPC, +using the AWS Web Console, and enable the "automatic public IP address" +attribute on each subnet: + +- 172.31.50.0/24, in zone "eu-central-1a" (for space "database") +- 172.31.51.0/24, in zone "eu-central-1b" (for space "database") +- 172.31.100.0/24, in zone "eu-central-1a" (for space "cms") +- 172.31.110.0/24, in zone "eu-central-1b" (for space "cms") + +We also assume the default VPC already has 2 default subnets (one per +zone), configured like this: + +- 172.31.0.0/20, in zone "eu-central-1a" (we'll use it for the "dmz" space) +- 172.31.16.0/20, in zone "eu-central-1b"(also for the "dmz" space) + +Once the default VPC has those subnets, we can bootstrap as usual: + +$ juju bootstrap + +After that, we can create the 3 spaces and add the subnets we +created to each one. These steps will be automated, and the subnet +creation will be possible directly from Juju in a future release. + +$ juju space create dmz +$ juju space create cms +$ juju space create database +$ juju subnet add 172.31.0.0/20 dmz +$ juju subnet add 172.31.16.0/20 dmz +$ juju subnet add 172.31.50.0/24 database +$ juju subnet add 172.31.51.0/24 database +$ juju subnet add 172.31.100.0/24 cms +$ juju subnet add 172.31.110.0/24 cms + +Now we can deploy the services into their respective spaces, +relate them and expose haproxy: + +$ juju deploy haproxy -n 2 --constraints spaces=dmz +$ juju deploy mediawiki -n 2 --constraints spaces=cms +$ juju deploy mysql -n 2 --constraints spaces=database +$ juju add-relation haproxy mediawiki +$ juju add-relation mediawiki mysql +$ juju expose haproxy + +Once all the units are up, you will be able to get the public +IP address of one of the haproxy units (from $ juju status), and +open it in a browser, seeing the mediawiki page. + +In an upcoming release, Juju will provide much better visibility +of which services and units run in which spaces/subnets. + +Please note, Juju supports the described syntax but currently ignores +all but the first allowed space in the list. This behavior will change +in a future release. Also, only the EC2 provider supports spaces as +described, with support for MaaS and OpenStack coming soon. +` === added file 'src/github.com/juju/juju/cmd/juju/helptopics/systems.go' --- src/github.com/juju/juju/cmd/juju/helptopics/systems.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/helptopics/systems.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,115 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package helptopics + +const JujuSystems = ` + +In order to use the multiple environment features of JES, you need to enable +a development feature flag: + + export JUJU_DEV_FEATURE_FLAGS=jes + +This should be the default behaviour with Juju 1.26. + +A Juju Environment System (JES), also sometimes shortened to 'juju system', +describes the environment that runs and manages the Juju API servers and the +underlying database. + +This initial environment is also called the system environment, and is what is +created when the bootstrap command is called. This system environment is a +normal Juju environment that just happens to have machines that manage Juju. + +In order to keep a clean separation of concerns, it is considered best +practice to create additional environments for real workload deployment. + +Services can still be deployed to the system environment, but it is generally +expected that these services are more for management and monitoring purposes, +like landscape and nagios. + +When creating a Juju system that is going to be used by more than one person, +it is good practice to create users for each individual that will be accessing +the environments. + +Users are managed within the Juju system using the 'juju user' command. This +allows the creation, listing, and disabling of users. When a juju system is +initially bootstrapped, there is only one user. Additional users are created +as follows: + + $ juju user add bob "Bob Brown" + user "Bob Brown (bob)" added + server file written to /current/working/directory/bob.server + +This command will create a local file "bob.server". The name of the file is +customisable using the --output option on the command. This 'server file' +should then be sent to Bob. Bob can then use this file to login to the Juju +system. + +The system file contains everything that Juju needs to connect to the API +server of the Juju system. It has the network address, server certificate, +username and a randomly generated password. + +Juju needs to have a name for the system when Bob calls the login command. +This is used to identify the system by name for other commands, like switch. + +When Bob logs in to the system, a different random password is generated and +cached locally. This does mean that this particular server file is not usable +a second time. If Bob does not want to change the password, he can use the +--keep-password flag. + + $ juju system login --server=bob.server staging + cached connection details as system "staging" + -> staging (system) + +Bob can then list all the environments within that system that he has access +to: + + $ juju system environments + NAME OWNER LAST CONNECTION + +The list could well be empty. Bob can create an environment to use: + + $ juju system create-environment test + created environment "test" + staging (system) -> test + +When the environment has been created, it becomes the current environment. A +new environment has no machines, and no services. + + $ juju status + environment: test + machines: {} + services: {} + +Bob wants to collaborate with Mary on this environment. A user for Mary needs +to exist in the system before Bob is able to share the environment with her. + + $ juju environment share mary + ERROR could not share environment: user "mary" does not exist locally: user "mary" not found + +Bob gets the system administrator to add a user for Mary, and then shares the +environment with Mary. + + $ juju environment share mary + $ juju environment users + NAME DATE CREATED LAST CONNECTION + bob@local 5 minutes ago just now + mary@local 57 seconds ago never connected + +When Mary has used her credentials to connect to the juju system, she can see +Bob's environment. + + $ juju system environments + NAME OWNER LAST CONNECTION + test bob@local never connected + +Mary can use this environment. + + $ juju system use-environment test + mary-server (system) -> bob-test + +The local name for the environment is by default 'owner-name', so since this +environment is owned by 'bob@local' and called test, for mary the environment +is called 'bob-test'. + +` === added file 'src/github.com/juju/juju/cmd/juju/helptopics/users.go' --- src/github.com/juju/juju/cmd/juju/helptopics/users.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/helptopics/users.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,35 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package helptopics + +const Users = ` + +Juju has understanding of two different types of users: +local users, those stored in the database along side the environments and +entities in those environments; and remote users, those whose authenticiation +is managed by an external service and reserved for future use. + +When a Juju System is bootstrapped, an initial user is created when the intial +environment is created. This user is considered the administrator for the Juju +System. This user is the only user able to create other users until Juju has +full fine grained role based permissions. + +All user managment functionality is managed though the 'juju user' collection +of commands. + +The primary user commands are used by the admin users to create users and +disable or reenable login access. + +The change-password command can be used by any user to change their own +password, or, for admins, the command can change another user's password and +generate a new credentials file for them. + +The credentials command gives any use the ability to export the credentails +they are using to access an environment to a file that they can use elsewhere +to login to the same Juju System. + +See Also: + juju help system + juju help user +` === removed file 'src/github.com/juju/juju/cmd/juju/init.go' --- src/github.com/juju/juju/cmd/juju/init.go 2014-08-20 15:00:12 +0000 +++ src/github.com/juju/juju/cmd/juju/init.go 1970-01-01 00:00:00 +0000 @@ -1,63 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - - "github.com/juju/cmd" - "launchpad.net/gnuflag" - - "github.com/juju/juju/environs" -) - -// InitCommand is used to write out a boilerplate environments.yaml file. -type InitCommand struct { - cmd.CommandBase - WriteFile bool - Show bool -} - -func (c *InitCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "init", - Purpose: "generate boilerplate configuration for juju environments", - Aliases: []string{"generate-config"}, - } -} - -func (c *InitCommand) SetFlags(f *gnuflag.FlagSet) { - f.BoolVar(&c.WriteFile, "f", false, "force overwriting environments.yaml file even if it exists (ignored if --show flag specified)") - f.BoolVar(&c.Show, "show", false, "print the generated configuration data to stdout instead of writing it to a file") -} - -var errJujuEnvExists = fmt.Errorf(`A juju environment configuration already exists. - -Use -f to overwrite the existing environments.yaml. -`) - -// Run checks to see if there is already an environments.yaml file. In one does not exist already, -// a boilerplate version is created so that the user can edit it to get started. -func (c *InitCommand) Run(context *cmd.Context) error { - out := context.Stdout - config := environs.BoilerplateConfig() - if c.Show { - fmt.Fprint(out, config) - return nil - } - _, err := environs.ReadEnvirons("") - if err == nil && !c.WriteFile { - return errJujuEnvExists - } - if err != nil && !environs.IsNoEnv(err) { - return err - } - filename, err := environs.WriteEnvirons("", config) - if err != nil { - return fmt.Errorf("A boilerplate environment configuration file could not be created: %s", err.Error()) - } - fmt.Fprintf(out, "A boilerplate environment configuration file has been written to %s.\n", filename) - fmt.Fprint(out, "Edit the file to configure your juju environment and run bootstrap.\n") - return nil -} === removed file 'src/github.com/juju/juju/cmd/juju/init_test.go' --- src/github.com/juju/juju/cmd/juju/init_test.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/init_test.go 1970-01-01 00:00:00 +0000 @@ -1,103 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "bytes" - "io/ioutil" - "os" - "strings" - - "github.com/juju/cmd" - gitjujutesting "github.com/juju/testing" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/testing" -) - -type InitSuite struct { - testing.FakeJujuHomeSuite -} - -var _ = gc.Suite(&InitSuite{}) - -// The environments.yaml is created by default if it -// does not already exist. -func (*InitSuite) TestBoilerPlateEnvironment(c *gc.C) { - envPath := gitjujutesting.HomePath(".juju", "environments.yaml") - err := os.Remove(envPath) - c.Assert(err, jc.ErrorIsNil) - ctx := testing.Context(c) - code := cmd.Main(&InitCommand{}, ctx, nil) - c.Check(code, gc.Equals, 0) - outStr := ctx.Stdout.(*bytes.Buffer).String() - strippedOut := strings.Replace(outStr, "\n", "", -1) - c.Check(strippedOut, gc.Matches, ".*A boilerplate environment configuration file has been written.*") - environpath := gitjujutesting.HomePath(".juju", "environments.yaml") - data, err := ioutil.ReadFile(environpath) - c.Assert(err, jc.ErrorIsNil) - strippedData := strings.Replace(string(data), "\n", "", -1) - c.Assert(strippedData, gc.Matches, ".*# This is the Juju config file, which you can use.*") -} - -// The boilerplate is sent to stdout with --show, and the environments.yaml -// is not created. -func (*InitSuite) TestBoilerPlatePrinted(c *gc.C) { - envPath := gitjujutesting.HomePath(".juju", "environments.yaml") - err := os.Remove(envPath) - c.Assert(err, jc.ErrorIsNil) - ctx := testing.Context(c) - code := cmd.Main(&InitCommand{}, ctx, []string{"--show"}) - c.Check(code, gc.Equals, 0) - outStr := ctx.Stdout.(*bytes.Buffer).String() - strippedOut := strings.Replace(outStr, "\n", "", -1) - c.Check(strippedOut, gc.Matches, ".*# This is the Juju config file, which you can use.*") - environpath := gitjujutesting.HomePath(".juju", "environments.yaml") - _, err = ioutil.ReadFile(environpath) - c.Assert(err, gc.NotNil) -} - -const existingEnv = ` -environments: - test: - type: dummy - state-server: false - authorized-keys: i-am-a-key -` - -// An existing environments.yaml will not be overwritten without -// the explicit -f option. -func (*InitSuite) TestExistingEnvironmentNotOverwritten(c *gc.C) { - testing.WriteEnvironments(c, existingEnv) - - ctx := testing.Context(c) - code := cmd.Main(&InitCommand{}, ctx, nil) - c.Check(code, gc.Equals, 1) - errOut := ctx.Stderr.(*bytes.Buffer).String() - strippedOut := strings.Replace(errOut, "\n", "", -1) - c.Check(strippedOut, gc.Matches, ".*A juju environment configuration already exists.*") - environpath := gitjujutesting.HomePath(".juju", "environments.yaml") - data, err := ioutil.ReadFile(environpath) - c.Assert(err, jc.ErrorIsNil) - c.Assert(string(data), gc.Equals, existingEnv) -} - -// An existing environments.yaml will be overwritten when -f is -// given explicitly. -func (*InitSuite) TestExistingEnvironmentOverwritten(c *gc.C) { - testing.WriteEnvironments(c, existingEnv) - - ctx := testing.Context(c) - code := cmd.Main(&InitCommand{}, ctx, []string{"-f"}) - c.Check(code, gc.Equals, 0) - stdOut := ctx.Stdout.(*bytes.Buffer).String() - strippedOut := strings.Replace(stdOut, "\n", "", -1) - c.Check(strippedOut, gc.Matches, ".*A boilerplate environment configuration file has been written.*") - environpath := gitjujutesting.HomePath(".juju", "environments.yaml") - data, err := ioutil.ReadFile(environpath) - c.Assert(err, jc.ErrorIsNil) - strippedData := strings.Replace(string(data), "\n", "", -1) - c.Assert(strippedData, gc.Matches, ".*# This is the Juju config file, which you can use.*") -} === modified file 'src/github.com/juju/juju/cmd/juju/machine/add.go' --- src/github.com/juju/juju/cmd/juju/machine/add.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/machine/add.go 2015-10-23 18:29:32 +0000 @@ -57,10 +57,10 @@ the same network as the API server. It is possible to override or augment constraints by passing provider-specific -"placement directives" with "--to"; these give the provider additional +"placement directives" as an argument; these give the provider additional information about how to allocate the machine. For example, one can direct the -MAAS provider to acquire a particular node by specifying its hostname with -"--to". For more information on placement directives, see "juju help placement". +MAAS provider to acquire a particular node by specifying its hostname. +For more information on placement directives, see "juju help placement". Examples: juju machine add (starts a new machine) @@ -70,7 +70,8 @@ juju machine add lxc:4 (starts a new lxc container on machine 4) juju machine add --constraints mem=8G (starts a machine with at least 8GB RAM) juju machine add ssh:user@10.10.0.3 (manually provisions a machine with ssh) - juju machine add zone=us-east-1a + juju machine add zone=us-east-1a (start a machine in zone us-east-1a on AWS) + juju machine add maas2.name (acquire machine maas2.name on MAAS) See Also: juju help constraints @@ -208,7 +209,7 @@ var config *config.Config if defaultStore, err := configstore.Default(); err != nil { return err - } else if config, err = c.Config(defaultStore); err != nil { + } else if config, err = c.Config(defaultStore, client); err != nil { return err } === removed file 'src/github.com/juju/juju/cmd/juju/machine_test.go' --- src/github.com/juju/juju/cmd/juju/machine_test.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/machine_test.go 1970-01-01 00:00:00 +0000 @@ -1,60 +0,0 @@ -// Copyright 2014 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "github.com/juju/cmd" - 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/testing" -) - -// MachineSuite tests the connectivity of all the machine subcommands. These -// tests go from the command line, api client, api server, db. The db changes -// are then checked. Only one test for each command is done here to check -// connectivity. Exhaustive unit tests are at each layer. -type MachineSuite struct { - jujutesting.JujuConnSuite -} - -var _ = gc.Suite(&MachineSuite{}) - -func (s *MachineSuite) RunMachineCommand(c *gc.C, commands ...string) (*cmd.Context, error) { - args := []string{"machine"} - args = append(args, commands...) - context := testing.Context(c) - juju := NewJujuCommand(context) - if err := testing.InitCommand(juju, args); err != nil { - return context, err - } - return context, juju.Run(context) -} - -func (s *MachineSuite) TestMachineAdd(c *gc.C) { - machines, err := s.State.AllMachines() - c.Assert(err, jc.ErrorIsNil) - count := len(machines) - - ctx, err := s.RunMachineCommand(c, "add") - c.Assert(testing.Stderr(ctx), jc.Contains, `created machine`) - - machines, err = s.State.AllMachines() - c.Assert(err, jc.ErrorIsNil) - c.Assert(machines, gc.HasLen, count+1) -} - -func (s *MachineSuite) TestMachineRemove(c *gc.C) { - machine := s.Factory.MakeMachine(c, nil) - - ctx, err := s.RunMachineCommand(c, "remove", machine.Id()) - c.Assert(testing.Stdout(ctx), gc.Equals, "") - - err = machine.Refresh() - c.Assert(err, jc.ErrorIsNil) - - c.Assert(machine.Life(), gc.Equals, state.Dying) -} === modified file 'src/github.com/juju/juju/cmd/juju/main.go' --- src/github.com/juju/juju/cmd/juju/main.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/main.go 2015-10-23 18:29:32 +0000 @@ -4,262 +4,19 @@ package main import ( - "fmt" "os" - "github.com/juju/cmd" - "github.com/juju/loggo" - "github.com/juju/utils/featureflag" - - jujucmd "github.com/juju/juju/cmd" - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/cmd/juju/action" - "github.com/juju/juju/cmd/juju/backups" - "github.com/juju/juju/cmd/juju/block" - "github.com/juju/juju/cmd/juju/cachedimages" - "github.com/juju/juju/cmd/juju/common" - "github.com/juju/juju/cmd/juju/environment" - "github.com/juju/juju/cmd/juju/machine" - "github.com/juju/juju/cmd/juju/service" - "github.com/juju/juju/cmd/juju/storage" - "github.com/juju/juju/cmd/juju/user" - "github.com/juju/juju/environs" - "github.com/juju/juju/juju" - "github.com/juju/juju/juju/osenv" + "github.com/juju/juju/cmd/juju/commands" + components "github.com/juju/juju/component/all" // Import the providers. _ "github.com/juju/juju/provider/all" - "github.com/juju/juju/version" + "github.com/juju/juju/utils" ) func init() { - featureflag.SetFlagsFromEnvironment(osenv.JujuFeatureFlagEnvKey) -} - -var logger = loggo.GetLogger("juju.cmd.juju") - -var jujuDoc = ` -juju provides easy, intelligent service orchestration on top of cloud -infrastructure providers such as Amazon EC2, HP Cloud, MaaS, OpenStack, Windows -Azure, or your local machine. - -https://juju.ubuntu.com/ -` - -var x = []byte("\x96\x8c\x99\x8a\x9c\x94\x96\x91\x98\xdf\x9e\x92\x9e\x85\x96\x91\x98\xf5") - -// Main registers subcommands for the juju executable, and hands over control -// to the cmd package. This function is not redundant with main, because it -// provides an entry point for testing with arbitrary command line arguments. -func Main(args []string) { - ctx, err := cmd.DefaultContext() - if err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(2) - } - if err = juju.InitJujuHome(); err != nil { - fmt.Fprintf(os.Stderr, "error: %s\n", err) - os.Exit(2) - } - for i := range x { - x[i] ^= 255 - } - if len(args) == 2 && args[1] == string(x[0:2]) { - os.Stdout.Write(x[2:]) - os.Exit(0) - } - jcmd := NewJujuCommand(ctx) - os.Exit(cmd.Main(jcmd, ctx, args[1:])) -} - -func NewJujuCommand(ctx *cmd.Context) cmd.Command { - jcmd := jujucmd.NewSuperCommand(cmd.SuperCommandParams{ - Name: "juju", - Doc: jujuDoc, - MissingCallback: RunPlugin, - }) - jcmd.AddHelpTopic("basics", "Basic commands", helpBasics) - jcmd.AddHelpTopic("local-provider", "How to configure a local (LXC) provider", - helpProviderStart+helpLocalProvider+helpProviderEnd) - jcmd.AddHelpTopic("openstack-provider", "How to configure an OpenStack provider", - helpProviderStart+helpOpenstackProvider+helpProviderEnd, "openstack") - jcmd.AddHelpTopic("ec2-provider", "How to configure an Amazon EC2 provider", - helpProviderStart+helpEC2Provider+helpProviderEnd, "ec2", "aws", "amazon") - jcmd.AddHelpTopic("hpcloud-provider", "How to configure an HP Cloud provider", - helpProviderStart+helpHPCloud+helpProviderEnd, "hpcloud", "hp-cloud") - jcmd.AddHelpTopic("azure-provider", "How to configure a Windows Azure provider", - helpProviderStart+helpAzureProvider+helpProviderEnd, "azure") - jcmd.AddHelpTopic("maas-provider", "How to configure a MAAS provider", - helpProviderStart+helpMAASProvider+helpProviderEnd, "maas") - jcmd.AddHelpTopic("constraints", "How to use commands with constraints", helpConstraints) - jcmd.AddHelpTopic("placement", "How to use placement directives", helpPlacement) - jcmd.AddHelpTopic("glossary", "Glossary of terms", helpGlossary) - jcmd.AddHelpTopic("logging", "How Juju handles logging", helpLogging) - - jcmd.AddHelpTopicCallback("plugins", "Show Juju plugins", PluginHelpTopic) - - registerCommands(jcmd, ctx) - return jcmd -} - -type commandRegistry interface { - Register(cmd.Command) - RegisterSuperAlias(name, super, forName string, check cmd.DeprecationCheck) - RegisterDeprecated(subcmd cmd.Command, check cmd.DeprecationCheck) -} - -// registerCommands registers commands in the specified registry. -// EnvironCommands must be wrapped with an envCmdWrapper. -func registerCommands(r commandRegistry, ctx *cmd.Context) { - wrapEnvCommand := func(c envcmd.EnvironCommand) cmd.Command { - return envCmdWrapper{envcmd.Wrap(c), ctx} - } - - // Creation commands. - r.Register(wrapEnvCommand(&BootstrapCommand{})) - r.Register(wrapEnvCommand(&DeployCommand{})) - r.Register(wrapEnvCommand(&AddRelationCommand{})) - - // Destruction commands. - r.Register(wrapEnvCommand(&RemoveRelationCommand{})) - r.Register(wrapEnvCommand(&RemoveServiceCommand{})) - r.Register(wrapEnvCommand(&RemoveUnitCommand{})) - r.Register(&DestroyEnvironmentCommand{}) - - // Reporting commands. - r.Register(wrapEnvCommand(&StatusCommand{})) - r.Register(&SwitchCommand{}) - r.Register(wrapEnvCommand(&EndpointCommand{})) - r.Register(wrapEnvCommand(&APIInfoCommand{})) - r.Register(wrapEnvCommand(&StatusHistoryCommand{})) - - // Error resolution and debugging commands. - r.Register(wrapEnvCommand(&RunCommand{})) - r.Register(wrapEnvCommand(&SCPCommand{})) - r.Register(wrapEnvCommand(&SSHCommand{})) - r.Register(wrapEnvCommand(&ResolvedCommand{})) - r.Register(wrapEnvCommand(&DebugLogCommand{})) - r.Register(wrapEnvCommand(&DebugHooksCommand{})) - - // Configuration commands. - r.Register(&InitCommand{}) - r.RegisterDeprecated(wrapEnvCommand(&common.GetConstraintsCommand{}), - twoDotOhDeprecation("environment get-constraints or service get-constraints")) - r.RegisterDeprecated(wrapEnvCommand(&common.SetConstraintsCommand{}), - twoDotOhDeprecation("environment set-constraints or service set-constraints")) - r.Register(wrapEnvCommand(&ExposeCommand{})) - r.Register(wrapEnvCommand(&SyncToolsCommand{})) - r.Register(wrapEnvCommand(&UnexposeCommand{})) - r.Register(wrapEnvCommand(&UpgradeJujuCommand{})) - r.Register(wrapEnvCommand(&UpgradeCharmCommand{})) - - // Charm publishing commands. - r.Register(wrapEnvCommand(&PublishCommand{})) - - // Charm tool commands. - r.Register(&HelpToolCommand{}) - - // Manage backups. - r.Register(backups.NewCommand()) - - // Manage authorized ssh keys. - r.Register(NewAuthorizedKeysCommand()) - - // Manage users and access - r.Register(user.NewSuperCommand()) - - // Manage cached images - r.Register(cachedimages.NewSuperCommand()) - - // Manage machines - r.Register(machine.NewSuperCommand()) - r.RegisterSuperAlias("add-machine", "machine", "add", twoDotOhDeprecation("machine add")) - r.RegisterSuperAlias("remove-machine", "machine", "remove", twoDotOhDeprecation("machine remove")) - r.RegisterSuperAlias("destroy-machine", "machine", "remove", twoDotOhDeprecation("machine remove")) - r.RegisterSuperAlias("terminate-machine", "machine", "remove", twoDotOhDeprecation("machine remove")) - - // Mangage environment - r.Register(environment.NewSuperCommand()) - r.RegisterSuperAlias("get-environment", "environment", "get", twoDotOhDeprecation("environment get")) - r.RegisterSuperAlias("get-env", "environment", "get", twoDotOhDeprecation("environment get")) - r.RegisterSuperAlias("set-environment", "environment", "set", twoDotOhDeprecation("environment set")) - r.RegisterSuperAlias("set-env", "environment", "set", twoDotOhDeprecation("environment set")) - r.RegisterSuperAlias("unset-environment", "environment", "unset", twoDotOhDeprecation("environment unset")) - r.RegisterSuperAlias("unset-env", "environment", "unset", twoDotOhDeprecation("environment unset")) - r.RegisterSuperAlias("retry-provisioning", "environment", "retry-provisioning", twoDotOhDeprecation("environment retry-provisioning")) - - // Manage and control actions - r.Register(action.NewSuperCommand()) - - // Manage state server availability - r.Register(wrapEnvCommand(&EnsureAvailabilityCommand{})) - - // Manage and control services - r.Register(service.NewSuperCommand()) - r.RegisterSuperAlias("add-unit", "service", "add-unit", twoDotOhDeprecation("service add-unit")) - r.RegisterSuperAlias("get", "service", "get", twoDotOhDeprecation("service get")) - r.RegisterSuperAlias("set", "service", "set", twoDotOhDeprecation("service set")) - r.RegisterSuperAlias("unset", "service", "unset", twoDotOhDeprecation("service unset")) - - // Operation protection commands - r.Register(block.NewSuperBlockCommand()) - r.Register(wrapEnvCommand(&block.UnblockCommand{})) - - // Manage storage - r.Register(storage.NewSuperCommand()) -} - -// envCmdWrapper is a struct that wraps an environment command and lets us handle -// errors returned from Init before they're returned to the main function. -type envCmdWrapper struct { - cmd.Command - ctx *cmd.Context -} - -func (w envCmdWrapper) Init(args []string) error { - err := w.Command.Init(args) - if environs.IsNoEnv(err) { - fmt.Fprintln(w.ctx.Stderr, "No juju environment configuration file exists.") - fmt.Fprintln(w.ctx.Stderr, err) - fmt.Fprintln(w.ctx.Stderr, "Please create a configuration by running:") - fmt.Fprintln(w.ctx.Stderr, " juju init") - fmt.Fprintln(w.ctx.Stderr, "then edit the file to configure your juju environment.") - fmt.Fprintln(w.ctx.Stderr, "You can then re-run the command.") - return cmd.ErrSilent - } - return err + utils.Must(components.RegisterForClient()) } func main() { - Main(os.Args) -} - -type versionDeprecation struct { - replacement string - deprecate version.Number - obsolete version.Number -} - -// Deprecated implements cmd.DeprecationCheck. -// If the current version is after the deprecate version number, -// the command is deprecated and the replacement should be used. -func (v *versionDeprecation) Deprecated() (bool, string) { - if version.Current.Number.Compare(v.deprecate) > 0 { - return true, v.replacement - } - return false, "" -} - -// Obsolete implements cmd.DeprecationCheck. -// If the current version is after the obsolete version number, -// the command is obsolete and shouldn't be registered. -func (v *versionDeprecation) Obsolete() bool { - return version.Current.Number.Compare(v.obsolete) > 0 -} - -func twoDotOhDeprecation(replacement string) cmd.DeprecationCheck { - return &versionDeprecation{ - replacement: replacement, - deprecate: version.MustParse("2.0-00"), - obsolete: version.MustParse("3.0-00"), - } + commands.Main(os.Args) } === removed file 'src/github.com/juju/juju/cmd/juju/main_test.go' --- src/github.com/juju/juju/cmd/juju/main_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/main_test.go 1970-01-01 00:00:00 +0000 @@ -1,497 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "io/ioutil" - "os" - "path/filepath" - "runtime" - "strings" - - "github.com/juju/cmd" - jc "github.com/juju/testing/checkers" - "github.com/juju/utils/featureflag" - "github.com/juju/utils/set" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/cmd/juju/block" - "github.com/juju/juju/cmd/juju/service" - cmdtesting "github.com/juju/juju/cmd/testing" - "github.com/juju/juju/juju/osenv" - _ "github.com/juju/juju/provider/dummy" - "github.com/juju/juju/testing" - "github.com/juju/juju/version" -) - -type MainSuite struct { - testing.FakeJujuHomeSuite -} - -var _ = gc.Suite(&MainSuite{}) - -func deployHelpText() string { - return cmdtesting.HelpText(envcmd.Wrap(&DeployCommand{}), "juju deploy") -} - -func setHelpText() string { - return cmdtesting.HelpText(envcmd.Wrap(&service.SetCommand{}), "juju service set") -} - -func syncToolsHelpText() string { - return cmdtesting.HelpText(envcmd.Wrap(&SyncToolsCommand{}), "juju sync-tools") -} - -func blockHelpText() string { - return cmdtesting.HelpText(block.NewSuperBlockCommand(), "juju block") -} - -func (s *MainSuite) TestRunMain(c *gc.C) { - // The test array structure needs to be inline here as some of the - // expected values below use deployHelpText(). This constructs the deploy - // command and runs gets the help for it. When the deploy command is - // setting the flags (which is needed for the help text) it is accessing - // osenv.JujuHome(), which panics if SetJujuHome has not been called. - // The FakeHome from testing does this. - for i, t := range []struct { - summary string - args []string - code int - out string - }{{ - summary: "no params shows help", - args: []string{}, - code: 0, - out: strings.TrimLeft(helpBasics, "\n"), - }, { - summary: "juju help is the same as juju", - args: []string{"help"}, - code: 0, - out: strings.TrimLeft(helpBasics, "\n"), - }, { - summary: "juju --help works too", - args: []string{"--help"}, - code: 0, - out: strings.TrimLeft(helpBasics, "\n"), - }, { - summary: "juju help basics is the same as juju", - args: []string{"help", "basics"}, - code: 0, - out: strings.TrimLeft(helpBasics, "\n"), - }, { - summary: "juju help foo doesn't exist", - args: []string{"help", "foo"}, - code: 1, - out: "ERROR unknown command or topic for foo\n", - }, { - summary: "juju help deploy shows the default help without global options", - args: []string{"help", "deploy"}, - code: 0, - out: deployHelpText(), - }, { - summary: "juju --help deploy shows the same help as 'help deploy'", - args: []string{"--help", "deploy"}, - code: 0, - out: deployHelpText(), - }, { - summary: "juju deploy --help shows the same help as 'help deploy'", - args: []string{"deploy", "--help"}, - code: 0, - out: deployHelpText(), - }, { - summary: "juju help set shows the default help without global options", - args: []string{"help", "set"}, - code: 0, - out: setHelpText(), - }, { - summary: "juju --help set shows the same help as 'help set'", - args: []string{"--help", "set"}, - code: 0, - out: setHelpText(), - }, { - summary: "juju set --help shows the same help as 'help set'", - args: []string{"set", "--help"}, - code: 0, - out: setHelpText(), - }, { - summary: "unknown command", - args: []string{"discombobulate"}, - code: 1, - out: "ERROR unrecognized command: juju discombobulate\n", - }, { - summary: "unknown option before command", - args: []string{"--cheese", "bootstrap"}, - code: 2, - out: "error: flag provided but not defined: --cheese\n", - }, { - summary: "unknown option after command", - args: []string{"bootstrap", "--cheese"}, - code: 2, - out: "error: flag provided but not defined: --cheese\n", - }, { - summary: "known option, but specified before command", - args: []string{"--environment", "blah", "bootstrap"}, - code: 2, - out: "error: flag provided but not defined: --environment\n", - }, { - summary: "juju sync-tools registered properly", - args: []string{"sync-tools", "--help"}, - code: 0, - out: syncToolsHelpText(), - }, { - summary: "check version command registered properly", - args: []string{"version"}, - code: 0, - out: version.Current.String() + "\n", - }, { - summary: "check block command registered properly", - args: []string{"block", "-h"}, - code: 0, - out: blockHelpText(), - }, { - summary: "check unblock command registered properly", - args: []string{"unblock"}, - code: 0, - out: "error: must specify one of [destroy-environment | remove-object | all-changes] to unblock\n", - }, - } { - c.Logf("test %d: %s", i, t.summary) - out := badrun(c, t.code, t.args...) - c.Assert(out, gc.Equals, t.out) - } -} - -func (s *MainSuite) TestActualRunJujuArgOrder(c *gc.C) { - //TODO(bogdanteleaga): cannot read the env file because of some suite - //problems. The juju home, when calling something from the command line is - //not the same as in the test suite. - if runtime.GOOS == "windows" { - c.Skip("bug 1403084: cannot read env file on windows because of suite problems") - } - logpath := filepath.Join(c.MkDir(), "log") - tests := [][]string{ - {"--log-file", logpath, "--debug", "env"}, // global flags before - {"env", "--log-file", logpath, "--debug"}, // after - {"--log-file", logpath, "env", "--debug"}, // mixed - } - for i, test := range tests { - c.Logf("test %d: %v", i, test) - badrun(c, 0, test...) - content, err := ioutil.ReadFile(logpath) - c.Assert(err, jc.ErrorIsNil) - c.Assert(string(content), gc.Matches, "(.|\n)*running juju(.|\n)*command finished(.|\n)*") - err = os.Remove(logpath) - c.Assert(err, jc.ErrorIsNil) - } -} - -var commandNames = []string{ - "action", - "add-machine", - "add-relation", - "add-unit", - "api-endpoints", - "api-info", - "authorised-keys", // alias for authorized-keys - "authorized-keys", - "backups", - "block", - "bootstrap", - "cached-images", - "debug-hooks", - "debug-log", - "deploy", - "destroy-environment", - "destroy-machine", - "destroy-relation", - "destroy-service", - "destroy-unit", - "ensure-availability", - "env", // alias for switch - "environment", - "expose", - "generate-config", // alias for init - "get", - "get-constraints", - "get-env", // alias for get-environment - "get-environment", - "help", - "help-tool", - "init", - "machine", - "publish", - "remove-machine", // alias for destroy-machine - "remove-relation", // alias for destroy-relation - "remove-service", // alias for destroy-service - "remove-unit", // alias for destroy-unit - "resolved", - "retry-provisioning", - "run", - "scp", - "service", - "set", - "set-constraints", - "set-env", // alias for set-environment - "set-environment", - "ssh", - "stat", // alias for status - "status", - "status-history", - "storage", - "switch", - "sync-tools", - "terminate-machine", // alias for destroy-machine - "unblock", - "unexpose", - "unset", - "unset-env", // alias for unset-environment - "unset-environment", - "upgrade-charm", - "upgrade-juju", - "user", - "version", -} - -func (s *MainSuite) TestHelpCommands(c *gc.C) { - defer osenv.SetJujuHome(osenv.SetJujuHome(c.MkDir())) - - // Check that we have correctly registered all the commands - // by checking the help output. - // First check default commands, and then check commands that are - // activated by feature flags. - - // Here we can add feature flags for any commands we want to hide by default. - devFeatures := []string{} - - // remove features behind dev_flag for the first test - // since they are not enabled. - cmdSet := set.NewStrings(commandNames...) - for _, feature := range devFeatures { - cmdSet.Remove(feature) - } - - // 1. Default Commands. Disable all features. - setFeatureFlags("") - c.Assert(getHelpCommandNames(c), jc.SameContents, cmdSet.Values()) - - // 2. Enable development features, and test again. - setFeatureFlags(strings.Join(devFeatures, ",")) - c.Assert(getHelpCommandNames(c), jc.SameContents, commandNames) -} - -func getHelpCommandNames(c *gc.C) []string { - out := badrun(c, 0, "help", "commands") - lines := strings.Split(out, "\n") - var names []string - for _, line := range lines { - f := strings.Fields(line) - if len(f) == 0 { - continue - } - names = append(names, f[0]) - } - return names -} - -func setFeatureFlags(flags string) { - if err := os.Setenv(osenv.JujuFeatureFlagEnvKey, flags); err != nil { - panic(err) - } - featureflag.SetFlagsFromEnvironment(osenv.JujuFeatureFlagEnvKey) -} - -var topicNames = []string{ - "azure-provider", - "basics", - "commands", - "constraints", - "ec2-provider", - "global-options", - "glossary", - "hpcloud-provider", - "local-provider", - "logging", - "maas-provider", - "openstack-provider", - "placement", - "plugins", - "topics", -} - -func (s *MainSuite) TestHelpTopics(c *gc.C) { - // Check that we have correctly registered all the topics - // by checking the help output. - defer osenv.SetJujuHome(osenv.SetJujuHome(c.MkDir())) - out := badrun(c, 0, "help", "topics") - lines := strings.Split(out, "\n") - var names []string - for _, line := range lines { - f := strings.Fields(line) - if len(f) == 0 { - continue - } - names = append(names, f[0]) - } - // The names should be output in alphabetical order, so don't sort. - c.Assert(names, gc.DeepEquals, topicNames) -} - -var globalFlags = []string{ - "--debug .*", - "--description .*", - "-h, --help .*", - "--log-file .*", - "--logging-config .*", - "-q, --quiet .*", - "--show-log .*", - "-v, --verbose .*", -} - -func (s *MainSuite) TestHelpGlobalOptions(c *gc.C) { - // Check that we have correctly registered all the topics - // by checking the help output. - defer osenv.SetJujuHome(osenv.SetJujuHome(c.MkDir())) - out := badrun(c, 0, "help", "global-options") - c.Assert(out, gc.Matches, `Global Options - -These options may be used with any command, and may appear in front of any -command\.(.|\n)*`) - lines := strings.Split(out, "\n") - var flags []string - for _, line := range lines { - f := strings.Fields(line) - if len(f) == 0 || line[0] != '-' { - continue - } - flags = append(flags, line) - } - c.Assert(len(flags), gc.Equals, len(globalFlags)) - for i, line := range flags { - c.Assert(line, gc.Matches, globalFlags[i]) - } -} - -type commands []cmd.Command - -func (r *commands) Register(c cmd.Command) { - *r = append(*r, c) -} - -func (r *commands) RegisterDeprecated(c cmd.Command, check cmd.DeprecationCheck) { - if !check.Obsolete() { - *r = append(*r, c) - } -} - -func (r *commands) RegisterSuperAlias(name, super, forName string, check cmd.DeprecationCheck) { - // Do nothing. -} - -func (s *MainSuite) TestEnvironCommands(c *gc.C) { - var commands commands - registerCommands(&commands, testing.Context(c)) - // There should not be any EnvironCommands registered. - // EnvironCommands must be wrapped using envcmd.Wrap. - for _, cmd := range commands { - c.Logf("%v", cmd.Info().Name) - c.Check(cmd, gc.Not(gc.FitsTypeOf), envcmd.EnvironCommand(&BootstrapCommand{})) - } -} - -func (s *MainSuite) TestAllCommandsPurposeDocCapitalization(c *gc.C) { - // Verify each command that: - // - the Purpose field is not empty and begins with a lowercase - // letter, and, - // - if set, the Doc field either begins with the name of the - // command or and uppercase letter. - // - // The first makes Purpose a required documentation. Also, makes - // both "help commands"'s output and "help "'s header more - // uniform. The second makes the Doc content either start like a - // sentence, or start godoc-like by using the command's name in - // lowercase. - var commands commands - registerCommands(&commands, testing.Context(c)) - for _, cmd := range commands { - info := cmd.Info() - c.Logf("%v", info.Name) - purpose := strings.TrimSpace(info.Purpose) - doc := strings.TrimSpace(info.Doc) - comment := func(message string) interface{} { - return gc.Commentf("command %q %s", info.Name, message) - } - - c.Check(purpose, gc.Not(gc.Equals), "", comment("has empty Purpose")) - if purpose != "" { - prefix := string(purpose[0]) - c.Check(prefix, gc.Equals, strings.ToLower(prefix), - comment("expected lowercase first-letter Purpose"), - ) - } - if doc != "" && !strings.HasPrefix(doc, info.Name) { - prefix := string(doc[0]) - c.Check(prefix, gc.Equals, strings.ToUpper(prefix), - comment("expected uppercase first-letter Doc"), - ) - } - } -} - -func (s *MainSuite) TestTwoDotOhDeprecation(c *gc.C) { - check := twoDotOhDeprecation("the replacement") - - // first check pre-2.0 - s.PatchValue(&version.Current.Number, version.MustParse("1.26.4")) - deprecated, replacement := check.Deprecated() - c.Check(deprecated, jc.IsFalse) - c.Check(replacement, gc.Equals, "") - c.Check(check.Obsolete(), jc.IsFalse) - - s.PatchValue(&version.Current.Number, version.MustParse("2.0-alpha1")) - deprecated, replacement = check.Deprecated() - c.Check(deprecated, jc.IsTrue) - c.Check(replacement, gc.Equals, "the replacement") - c.Check(check.Obsolete(), jc.IsFalse) - - s.PatchValue(&version.Current.Number, version.MustParse("3.0-alpha1")) - deprecated, replacement = check.Deprecated() - c.Check(deprecated, jc.IsTrue) - c.Check(replacement, gc.Equals, "the replacement") - c.Check(check.Obsolete(), jc.IsTrue) -} - -// obsoleteCommandNames is the list of commands that are deprecated in -// 2.0, and obsolete in 3.0 -var obsoleteCommandNames = []string{ - "add-machine", - "destroy-machine", - "get-constraints", - "get-env", - "get-environment", - "remove-machine", - "retry-provisioning", - "set-constraints", - "set-env", - "set-environment", - "terminate-machine", - "unset-env", - "unset-environment", -} - -func (s *MainSuite) TestObsoleteRegistration(c *gc.C) { - var commands commands - s.PatchValue(&version.Current.Number, version.MustParse("3.0-alpha1")) - registerCommands(&commands, testing.Context(c)) - - cmdSet := set.NewStrings(obsoleteCommandNames...) - registeredCmdSet := set.NewStrings() - for _, cmd := range commands { - registeredCmdSet.Add(cmd.Info().Name) - } - - intersection := registeredCmdSet.Intersection(cmdSet) - c.Logf("Registered obsolete commands: %s", intersection.Values()) - c.Assert(intersection.IsEmpty(), gc.Equals, true) -} === removed file 'src/github.com/juju/juju/cmd/juju/package_test.go' --- src/github.com/juju/juju/cmd/juju/package_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/package_test.go 1970-01-01 00:00:00 +0000 @@ -1,30 +0,0 @@ -// Copyright 2014 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -// TODO(dimitern): bug http://pad.lv/1425569 -// Disabled until we have time to fix these tests on i386 properly. -// -// +build !386 - -import ( - "flag" - stdtesting "testing" - - cmdtesting "github.com/juju/juju/cmd/testing" - _ "github.com/juju/juju/provider/dummy" - "github.com/juju/juju/testing" -) - -func TestPackage(t *stdtesting.T) { - testing.MgoTestPackage(t) -} - -// Reentrancy point for testing (something as close as possible to) the juju -// tool itself. -func TestRunMain(t *stdtesting.T) { - if *cmdtesting.FlagRunMain { - Main(flag.Args()) - } -} === removed file 'src/github.com/juju/juju/cmd/juju/plugin.go' --- src/github.com/juju/juju/cmd/juju/plugin.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/plugin.go 1970-01-01 00:00:00 +0000 @@ -1,215 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "bytes" - "fmt" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "sort" - "strings" - "syscall" - - "github.com/juju/cmd" - "launchpad.net/gnuflag" - - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/juju/osenv" -) - -const JujuPluginPrefix = "juju-" - -// This is a very rudimentary method used to extract common Juju -// arguments from the full list passed to the plugin. Currently, -// there is only one such argument: -e env -// If more than just -e is required, the method can be improved then. -func extractJujuArgs(args []string) []string { - var jujuArgs []string - nrArgs := len(args) - for nextArg := 0; nextArg < nrArgs; { - arg := args[nextArg] - nextArg++ - if arg != "-e" { - continue - } - jujuArgs = append(jujuArgs, arg) - if nextArg < nrArgs { - jujuArgs = append(jujuArgs, args[nextArg]) - nextArg++ - } - } - return jujuArgs -} - -func RunPlugin(ctx *cmd.Context, subcommand string, args []string) error { - cmdName := JujuPluginPrefix + subcommand - plugin := envcmd.Wrap(&PluginCommand{name: cmdName}) - - // We process common flags supported by Juju commands. - // To do this, we extract only those supported flags from the - // argument list to avoid confusing flags.Parse(). - flags := gnuflag.NewFlagSet(cmdName, gnuflag.ContinueOnError) - flags.SetOutput(ioutil.Discard) - plugin.SetFlags(flags) - jujuArgs := extractJujuArgs(args) - if err := flags.Parse(false, jujuArgs); err != nil { - return err - } - if err := plugin.Init(args); err != nil { - return err - } - err := plugin.Run(ctx) - _, execError := err.(*exec.Error) - // exec.Error results are for when the executable isn't found, in - // those cases, drop through. - if !execError { - return err - } - return &cmd.UnrecognizedCommand{Name: subcommand} -} - -type PluginCommand struct { - envcmd.EnvCommandBase - name string - args []string -} - -// Info is just a stub so that PluginCommand implements cmd.Command. -// Since this is never actually called, we can happily return nil. -func (*PluginCommand) Info() *cmd.Info { - return nil -} - -func (c *PluginCommand) Init(args []string) error { - c.args = args - return nil -} - -func (c *PluginCommand) Run(ctx *cmd.Context) error { - command := exec.Command(c.name, c.args...) - command.Env = append(os.Environ(), []string{ - osenv.JujuHomeEnvKey + "=" + osenv.JujuHome(), - osenv.JujuEnvEnvKey + "=" + c.ConnectionName()}..., - ) - - // Now hook up stdin, stdout, stderr - command.Stdin = ctx.Stdin - command.Stdout = ctx.Stdout - command.Stderr = ctx.Stderr - // And run it! - err := command.Run() - - if exitError, ok := err.(*exec.ExitError); ok && exitError != nil { - status := exitError.ProcessState.Sys().(syscall.WaitStatus) - if status.Exited() { - return cmd.NewRcPassthroughError(status.ExitStatus()) - } - } - return err -} - -type PluginDescription struct { - name string - description string -} - -const PluginTopicText = `Juju Plugins - -Plugins are implemented as stand-alone executable files somewhere in the user's PATH. -The executable command must be of the format juju-. - -` - -func PluginHelpTopic() string { - output := &bytes.Buffer{} - fmt.Fprintf(output, PluginTopicText) - - existingPlugins := GetPluginDescriptions() - - if len(existingPlugins) == 0 { - fmt.Fprintf(output, "No plugins found.\n") - } else { - longest := 0 - for _, plugin := range existingPlugins { - if len(plugin.name) > longest { - longest = len(plugin.name) - } - } - for _, plugin := range existingPlugins { - fmt.Fprintf(output, "%-*s %s\n", longest, plugin.name, plugin.description) - } - } - - return output.String() -} - -// GetPluginDescriptions runs each plugin with "--description". The calls to -// the plugins are run in parallel, so the function should only take as long -// as the longest call. -func GetPluginDescriptions() []PluginDescription { - plugins := findPlugins() - results := []PluginDescription{} - if len(plugins) == 0 { - return results - } - // create a channel with enough backing for each plugin - description := make(chan PluginDescription, len(plugins)) - - // exec the command, and wait only for the timeout before killing the process - for _, plugin := range plugins { - go func(plugin string) { - result := PluginDescription{name: plugin} - defer func() { - description <- result - }() - desccmd := exec.Command(plugin, "--description") - output, err := desccmd.CombinedOutput() - - if err == nil { - // trim to only get the first line - result.description = strings.SplitN(string(output), "\n", 2)[0] - } else { - result.description = fmt.Sprintf("error occurred running '%s --description'", plugin) - logger.Errorf("'%s --description': %s", plugin, err) - } - }(plugin) - } - resultMap := map[string]PluginDescription{} - // gather the results at the end - for _ = range plugins { - result := <-description - resultMap[result.name] = result - } - // plugins array is already sorted, use this to get the results in order - for _, plugin := range plugins { - // Strip the 'juju-' off the start of the plugin name in the results - result := resultMap[plugin] - result.name = result.name[len(JujuPluginPrefix):] - results = append(results, result) - } - return results -} - -// findPlugins searches the current PATH for executable files that start with -// JujuPluginPrefix. -func findPlugins() []string { - path := os.Getenv("PATH") - plugins := []string{} - for _, name := range filepath.SplitList(path) { - entries, err := ioutil.ReadDir(name) - if err != nil { - continue - } - for _, entry := range entries { - if strings.HasPrefix(entry.Name(), JujuPluginPrefix) && (entry.Mode()&0111) != 0 { - plugins = append(plugins, entry.Name()) - } - } - } - sort.Strings(plugins) - return plugins -} === removed file 'src/github.com/juju/juju/cmd/juju/plugin_test.go' --- src/github.com/juju/juju/cmd/juju/plugin_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/plugin_test.go 1970-01-01 00:00:00 +0000 @@ -1,246 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "bytes" - "fmt" - "io/ioutil" - "os" - "runtime" - "text/template" - "time" - - "github.com/juju/cmd" - gitjujutesting "github.com/juju/testing" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/testing" -) - -type PluginSuite struct { - testing.FakeJujuHomeSuite - oldPath string -} - -var _ = gc.Suite(&PluginSuite{}) - -func (suite *PluginSuite) SetUpTest(c *gc.C) { - //TODO(bogdanteleaga): Fix bash tests - if runtime.GOOS == "windows" { - c.Skip("bug 1403084: tests use bash scrips, will be rewritten for windows") - } - suite.FakeJujuHomeSuite.SetUpTest(c) - suite.oldPath = os.Getenv("PATH") - os.Setenv("PATH", "/bin:"+gitjujutesting.HomePath()) -} - -func (suite *PluginSuite) TearDownTest(c *gc.C) { - os.Setenv("PATH", suite.oldPath) - suite.FakeJujuHomeSuite.TearDownTest(c) -} - -func (*PluginSuite) TestFindPlugins(c *gc.C) { - plugins := findPlugins() - c.Assert(plugins, gc.DeepEquals, []string{}) -} - -func (suite *PluginSuite) TestFindPluginsOrder(c *gc.C) { - suite.makePlugin("foo", 0744) - suite.makePlugin("bar", 0654) - suite.makePlugin("baz", 0645) - plugins := findPlugins() - c.Assert(plugins, gc.DeepEquals, []string{"juju-bar", "juju-baz", "juju-foo"}) -} - -func (suite *PluginSuite) TestFindPluginsIgnoreNotExec(c *gc.C) { - suite.makePlugin("foo", 0644) - suite.makePlugin("bar", 0666) - plugins := findPlugins() - c.Assert(plugins, gc.DeepEquals, []string{}) -} - -func (suite *PluginSuite) TestRunPluginExising(c *gc.C) { - suite.makePlugin("foo", 0755) - ctx := testing.Context(c) - err := RunPlugin(ctx, "foo", []string{"some params"}) - c.Assert(err, jc.ErrorIsNil) - c.Assert(testing.Stdout(ctx), gc.Equals, "foo some params\n") - c.Assert(testing.Stderr(ctx), gc.Equals, "") -} - -func (suite *PluginSuite) TestRunPluginWithFailing(c *gc.C) { - suite.makeFailingPlugin("foo", 2) - ctx := testing.Context(c) - err := RunPlugin(ctx, "foo", []string{"some params"}) - c.Assert(err, gc.ErrorMatches, "subprocess encountered error code 2") - c.Assert(err, jc.Satisfies, cmd.IsRcPassthroughError) - c.Assert(testing.Stdout(ctx), gc.Equals, "failing\n") - c.Assert(testing.Stderr(ctx), gc.Equals, "") -} - -func (suite *PluginSuite) TestGatherDescriptionsInParallel(c *gc.C) { - // Make plugins that will deadlock if we don't start them in parallel. - // Each plugin depends on another one being started before they will - // complete. They make a full loop, so no sequential ordering will ever - // succeed. - suite.makeFullPlugin(PluginParams{Name: "foo", Creates: "foo", DependsOn: "bar"}) - suite.makeFullPlugin(PluginParams{Name: "bar", Creates: "bar", DependsOn: "baz"}) - suite.makeFullPlugin(PluginParams{Name: "baz", Creates: "baz", DependsOn: "error"}) - suite.makeFullPlugin(PluginParams{Name: "error", ExitStatus: 1, Creates: "error", DependsOn: "foo"}) - - // If the code was wrong, GetPluginDescriptions would deadlock, - // so timeout after a short while - resultChan := make(chan []PluginDescription) - go func() { - resultChan <- GetPluginDescriptions() - }() - // 10 seconds is arbitrary but should always be generously long. Test - // actually only takes about 15ms in practice. But 10s allows for system hiccups, etc. - waitTime := 10 * time.Second - var results []PluginDescription - select { - case results = <-resultChan: - break - case <-time.After(waitTime): - c.Fatalf("took longer than %fs to complete.", waitTime.Seconds()) - } - - c.Assert(results, gc.HasLen, 4) - c.Assert(results[0].name, gc.Equals, "bar") - c.Assert(results[0].description, gc.Equals, "bar description") - c.Assert(results[1].name, gc.Equals, "baz") - c.Assert(results[1].description, gc.Equals, "baz description") - c.Assert(results[2].name, gc.Equals, "error") - c.Assert(results[2].description, gc.Equals, "error occurred running 'juju-error --description'") - c.Assert(results[3].name, gc.Equals, "foo") - c.Assert(results[3].description, gc.Equals, "foo description") -} - -func (suite *PluginSuite) TestHelpPluginsWithNoPlugins(c *gc.C) { - output := badrun(c, 0, "help", "plugins") - c.Assert(output, jc.HasPrefix, PluginTopicText) - c.Assert(output, jc.HasSuffix, "\n\nNo plugins found.\n") -} - -func (suite *PluginSuite) TestHelpPluginsWithPlugins(c *gc.C) { - suite.makeFullPlugin(PluginParams{Name: "foo"}) - suite.makeFullPlugin(PluginParams{Name: "bar"}) - output := badrun(c, 0, "help", "plugins") - c.Assert(output, jc.HasPrefix, PluginTopicText) - expectedPlugins := ` - -bar bar description -foo foo description -` - c.Assert(output, jc.HasSuffix, expectedPlugins) -} - -func (suite *PluginSuite) TestHelpPluginName(c *gc.C) { - suite.makeFullPlugin(PluginParams{Name: "foo"}) - output := badrun(c, 0, "help", "foo") - expectedHelp := `foo longer help - -something useful -` - c.Assert(output, gc.Matches, expectedHelp) -} - -func (suite *PluginSuite) TestHelpPluginNameNotAPlugin(c *gc.C) { - output := badrun(c, 0, "help", "foo") - expectedHelp := "ERROR unknown command or topic for foo\n" - c.Assert(output, gc.Matches, expectedHelp) -} - -func (suite *PluginSuite) TestHelpAsArg(c *gc.C) { - suite.makeFullPlugin(PluginParams{Name: "foo"}) - output := badrun(c, 0, "foo", "--help") - expectedHelp := `foo longer help - -something useful -` - c.Assert(output, gc.Matches, expectedHelp) -} - -func (suite *PluginSuite) TestDebugAsArg(c *gc.C) { - suite.makeFullPlugin(PluginParams{Name: "foo"}) - output := badrun(c, 0, "foo", "--debug") - expectedDebug := "some debug\n" - c.Assert(output, gc.Matches, expectedDebug) -} - -func (suite *PluginSuite) TestJujuEnvVars(c *gc.C) { - suite.makeFullPlugin(PluginParams{Name: "foo"}) - output := badrun(c, 0, "foo", "-e", "myenv", "-p", "pluginarg") - expectedDebug := `foo -e myenv -p pluginarg\n.*env is: myenv\n.*home is: .*\.juju\n` - c.Assert(output, gc.Matches, expectedDebug) -} - -func (suite *PluginSuite) makePlugin(name string, perm os.FileMode) { - content := fmt.Sprintf("#!/bin/bash --norc\necho %s $*", name) - filename := gitjujutesting.HomePath(JujuPluginPrefix + name) - ioutil.WriteFile(filename, []byte(content), perm) -} - -func (suite *PluginSuite) makeFailingPlugin(name string, exitStatus int) { - content := fmt.Sprintf("#!/bin/bash --norc\necho failing\nexit %d", exitStatus) - filename := gitjujutesting.HomePath(JujuPluginPrefix + name) - ioutil.WriteFile(filename, []byte(content), 0755) -} - -type PluginParams struct { - Name string - ExitStatus int - Creates string - DependsOn string -} - -const pluginTemplate = `#!/bin/bash --norc - -if [ "$1" = "--description" ]; then - if [ -n "{{.Creates}}" ]; then - touch "{{.Creates}}" - fi - if [ -n "{{.DependsOn}}" ]; then - # Sleep 10ms while waiting to allow other stuff to do work - while [ ! -e "{{.DependsOn}}" ]; do sleep 0.010; done - fi - echo "{{.Name}} description" - exit {{.ExitStatus}} -fi - -if [ "$1" = "--help" ]; then - echo "{{.Name}} longer help" - echo "" - echo "something useful" - exit {{.ExitStatus}} -fi - -if [ "$1" = "--debug" ]; then - echo "some debug" - exit {{.ExitStatus}} -fi - -echo {{.Name}} $* -echo "env is: " $JUJU_ENV -echo "home is: " $JUJU_HOME -exit {{.ExitStatus}} -` - -func (suite *PluginSuite) makeFullPlugin(params PluginParams) { - // Create a new template and parse the plugin into it. - t := template.Must(template.New("plugin").Parse(pluginTemplate)) - content := &bytes.Buffer{} - filename := gitjujutesting.HomePath("juju-" + params.Name) - // Create the files in the temp dirs, so we don't pollute the working space - if params.Creates != "" { - params.Creates = gitjujutesting.HomePath(params.Creates) - } - if params.DependsOn != "" { - params.DependsOn = gitjujutesting.HomePath(params.DependsOn) - } - t.Execute(content, params) - ioutil.WriteFile(filename, content.Bytes(), 0755) -} === removed file 'src/github.com/juju/juju/cmd/juju/publish.go' --- src/github.com/juju/juju/cmd/juju/publish.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/publish.go 1970-01-01 00:00:00 +0000 @@ -1,190 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - "os" - "strings" - "time" - - "github.com/juju/cmd" - "gopkg.in/juju/charm.v5" - "gopkg.in/juju/charm.v5/charmrepo" - "launchpad.net/gnuflag" - - "github.com/juju/juju/bzr" - "github.com/juju/juju/cmd/envcmd" -) - -type PublishCommand struct { - envcmd.EnvCommandBase - 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 == "" { - if err == nil { - 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.InferURL(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.Reference(), "/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 -} === removed file 'src/github.com/juju/juju/cmd/juju/publish_nix_test.go' --- src/github.com/juju/juju/cmd/juju/publish_nix_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/publish_nix_test.go 1970-01-01 00:00:00 +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 main - -var bzrHomeFile = ".bazaar/bazaar.conf" === removed file 'src/github.com/juju/juju/cmd/juju/publish_test.go' --- src/github.com/juju/juju/cmd/juju/publish_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/publish_test.go 1970-01-01 00:00:00 +0000 @@ -1,403 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - "os" - - "github.com/juju/cmd" - gitjujutesting "github.com/juju/testing" - jc "github.com/juju/testing/checkers" - "github.com/juju/utils" - gc "gopkg.in/check.v1" - "gopkg.in/juju/charm.v5/charmrepo" - - "github.com/juju/juju/bzr" - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/testing" -) - -// Sadly, this is a very slow test suite, heavily dominated by calls to bzr. - -type PublishSuite struct { - testing.FakeJujuHomeSuite - 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, envcmd.Wrap(&PublishCommand{}), args, s.dir) -} - -const pollDelay = testing.ShortWait - -func (s *PublishSuite) SetUpSuite(c *gc.C) { - s.FakeJujuHomeSuite.SetUpSuite(c) - s.HTTPSuite.SetUpSuite(c) - - s.oldBaseURL = charmrepo.LegacyStore.BaseURL - charmrepo.LegacyStore.BaseURL = s.URL("") -} - -func (s *PublishSuite) TearDownSuite(c *gc.C) { - s.FakeJujuHomeSuite.TearDownSuite(c) - s.HTTPSuite.TearDownSuite(c) - - charmrepo.LegacyStore.BaseURL = s.oldBaseURL -} - -func (s *PublishSuite) SetUpTest(c *gc.C) { - s.FakeJujuHomeSuite.SetUpTest(c) - s.HTTPSuite.SetUpTest(c) - s.PatchEnvironment("BZR_HOME", utils.Home()) - s.FakeJujuHomeSuite.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.FakeJujuHomeSuite.TearDownTest(c) -} - -func (s *PublishSuite) TestNoBranch(c *gc.C) { - dir := c.MkDir() - _, err := testing.RunCommandInDir(c, envcmd.Wrap(&PublishCommand{}), []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, envcmd.Wrap(&PublishCommand{}), []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) TestMissingSeries(c *gc.C) { - _, err := s.runPublish(c, "cs:wordpress") - c.Assert(err, gc.ErrorMatches, `cannot infer charm URL for "cs:wordpress": charm url series is not resolved`) -} - -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) TestInferURL(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, envcmd.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, envcmd.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, envcmd.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, envcmd.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") - } -} === removed file 'src/github.com/juju/juju/cmd/juju/publish_windows_test.go' --- src/github.com/juju/juju/cmd/juju/publish_windows_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/publish_windows_test.go 1970-01-01 00:00:00 +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 main - -var bzrHomeFile = "Bazaar/2.0/bazaar.conf" === removed file 'src/github.com/juju/juju/cmd/juju/removerelation.go' --- src/github.com/juju/juju/cmd/juju/removerelation.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/removerelation.go 1970-01-01 00:00:00 +0000 @@ -1,45 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - - "github.com/juju/cmd" - - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/cmd/juju/block" -) - -// RemoveRelationCommand causes an existing service relation to be shut down. -type RemoveRelationCommand struct { - envcmd.EnvCommandBase - Endpoints []string -} - -func (c *RemoveRelationCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "remove-relation", - Args: "[:] [:]", - Purpose: "remove a relation between two services", - Aliases: []string{"destroy-relation"}, - } -} - -func (c *RemoveRelationCommand) Init(args []string) error { - if len(args) != 2 { - return fmt.Errorf("a relation must involve two services") - } - c.Endpoints = args - return nil -} - -func (c *RemoveRelationCommand) Run(_ *cmd.Context) error { - client, err := c.NewAPIClient() - if err != nil { - return err - } - defer client.Close() - return block.ProcessBlockedError(client.DestroyRelation(c.Endpoints...), block.BlockRemove) -} === removed file 'src/github.com/juju/juju/cmd/juju/removerelation_test.go' --- src/github.com/juju/juju/cmd/juju/removerelation_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/removerelation_test.go 1970-01-01 00:00:00 +0000 @@ -1,71 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/cmd/envcmd" - jujutesting "github.com/juju/juju/juju/testing" - "github.com/juju/juju/testcharms" - "github.com/juju/juju/testing" -) - -type RemoveRelationSuite struct { - jujutesting.RepoSuite - CmdBlockHelper -} - -func (s *RemoveRelationSuite) SetUpTest(c *gc.C) { - s.RepoSuite.SetUpTest(c) - s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) - c.Assert(s.CmdBlockHelper, gc.NotNil) - s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) -} - -var _ = gc.Suite(&RemoveRelationSuite{}) - -func runRemoveRelation(c *gc.C, args ...string) error { - _, err := testing.RunCommand(c, envcmd.Wrap(&RemoveRelationCommand{}), args...) - return err -} - -func (s *RemoveRelationSuite) setupRelationForRemove(c *gc.C) { - testcharms.Repo.CharmArchivePath(s.SeriesPath, "riak") - err := runDeploy(c, "local:riak", "riak") - c.Assert(err, jc.ErrorIsNil) - testcharms.Repo.CharmArchivePath(s.SeriesPath, "logging") - err = runDeploy(c, "local:logging", "logging") - c.Assert(err, jc.ErrorIsNil) - runAddRelation(c, "riak", "logging") -} - -func (s *RemoveRelationSuite) TestRemoveRelation(c *gc.C) { - s.setupRelationForRemove(c) - - // Destroy a relation that exists. - err := runRemoveRelation(c, "logging", "riak") - c.Assert(err, jc.ErrorIsNil) - - // Destroy a relation that used to exist. - err = runRemoveRelation(c, "riak", "logging") - c.Assert(err, gc.ErrorMatches, `relation "logging:info riak:juju-info" not found`) - - // Invalid removes. - err = runRemoveRelation(c, "ping", "pong") - c.Assert(err, gc.ErrorMatches, `service "ping" not found`) - err = runRemoveRelation(c, "riak") - c.Assert(err, gc.ErrorMatches, `a relation must involve two services`) -} - -func (s *RemoveRelationSuite) TestBlockRemoveRelation(c *gc.C) { - s.setupRelationForRemove(c) - - // block operation - s.BlockRemoveObject(c, "TestBlockRemoveRelation") - // Destroy a relation that exists. - err := runRemoveRelation(c, "logging", "riak") - s.AssertBlocked(c, err, ".*TestBlockRemoveRelation.*") -} === removed file 'src/github.com/juju/juju/cmd/juju/removeservice.go' --- src/github.com/juju/juju/cmd/juju/removeservice.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/removeservice.go 1970-01-01 00:00:00 +0000 @@ -1,60 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - - "github.com/juju/cmd" - "github.com/juju/names" - - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/cmd/juju/block" -) - -// RemoveServiceCommand causes an existing service to be destroyed. -type RemoveServiceCommand struct { - envcmd.EnvCommandBase - ServiceName string -} - -const removeServiceDoc = ` -Removing a service will remove all its units and relations. - -If this is the only service running, the machine on which -the service is hosted will also be destroyed, if possible. -The machine will be destroyed if: -- it is not a state server -- it is not hosting any Juju managed containers -` - -func (c *RemoveServiceCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "remove-service", - Args: "", - Purpose: "remove a service from the environment", - Doc: removeServiceDoc, - Aliases: []string{"destroy-service"}, - } -} - -func (c *RemoveServiceCommand) Init(args []string) error { - if len(args) == 0 { - return fmt.Errorf("no service specified") - } - if !names.IsValidService(args[0]) { - return fmt.Errorf("invalid service name %q", args[0]) - } - c.ServiceName, args = args[0], args[1:] - return cmd.CheckEmpty(args) -} - -func (c *RemoveServiceCommand) Run(_ *cmd.Context) error { - client, err := c.NewAPIClient() - if err != nil { - return err - } - defer client.Close() - return block.ProcessBlockedError(client.ServiceDestroy(c.ServiceName), block.BlockRemove) -} === removed file 'src/github.com/juju/juju/cmd/juju/removeservice_test.go' --- src/github.com/juju/juju/cmd/juju/removeservice_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/removeservice_test.go 1970-01-01 00:00:00 +0000 @@ -1,77 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/cmd/envcmd" - jujutesting "github.com/juju/juju/juju/testing" - "github.com/juju/juju/state" - "github.com/juju/juju/testcharms" - "github.com/juju/juju/testing" -) - -type RemoveServiceSuite struct { - jujutesting.RepoSuite - CmdBlockHelper -} - -var _ = gc.Suite(&RemoveServiceSuite{}) - -func (s *RemoveServiceSuite) SetUpTest(c *gc.C) { - s.RepoSuite.SetUpTest(c) - s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) - c.Assert(s.CmdBlockHelper, gc.NotNil) - s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) -} - -func runRemoveService(c *gc.C, args ...string) error { - _, err := testing.RunCommand(c, envcmd.Wrap(&RemoveServiceCommand{}), args...) - return err -} - -func (s *RemoveServiceSuite) setupTestService(c *gc.C) { - // Destroy a service that exists. - testcharms.Repo.CharmArchivePath(s.SeriesPath, "riak") - err := runDeploy(c, "local:riak", "riak") - c.Assert(err, jc.ErrorIsNil) -} - -func (s *RemoveServiceSuite) TestSuccess(c *gc.C) { - s.setupTestService(c) - err := runRemoveService(c, "riak") - c.Assert(err, jc.ErrorIsNil) - riak, err := s.State.Service("riak") - c.Assert(err, jc.ErrorIsNil) - c.Assert(riak.Life(), gc.Equals, state.Dying) -} - -func (s *RemoveServiceSuite) TestBlockRemoveService(c *gc.C) { - s.setupTestService(c) - - // block operation - s.BlockRemoveObject(c, "TestBlockRemoveService") - err := runRemoveService(c, "riak") - s.AssertBlocked(c, err, ".*TestBlockRemoveService.*") - riak, err := s.State.Service("riak") - c.Assert(err, jc.ErrorIsNil) - c.Assert(riak.Life(), gc.Equals, state.Alive) -} - -func (s *RemoveServiceSuite) TestFailure(c *gc.C) { - // Destroy a service that does not exist. - err := runRemoveService(c, "gargleblaster") - c.Assert(err, gc.ErrorMatches, `service "gargleblaster" not found`) -} - -func (s *RemoveServiceSuite) TestInvalidArgs(c *gc.C) { - err := runRemoveService(c) - c.Assert(err, gc.ErrorMatches, `no service specified`) - err = runRemoveService(c, "ping", "pong") - c.Assert(err, gc.ErrorMatches, `unrecognized args: \["pong"\]`) - err = runRemoveService(c, "invalid:name") - c.Assert(err, gc.ErrorMatches, `invalid service name "invalid:name"`) -} === removed file 'src/github.com/juju/juju/cmd/juju/removeunit.go' --- src/github.com/juju/juju/cmd/juju/removeunit.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/removeunit.go 1970-01-01 00:00:00 +0000 @@ -1,64 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - - "github.com/juju/cmd" - "github.com/juju/names" - - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/cmd/juju/block" -) - -// RemoveUnitCommand is responsible for destroying service units. -type RemoveUnitCommand struct { - envcmd.EnvCommandBase - UnitNames []string -} - -const removeUnitDoc = ` -Remove service units from the environment. - -If this is the only unit running, the machine on which -the unit is hosted will also be destroyed, if possible. -The machine will be destroyed if: -- it is not a state server -- it is not hosting any Juju managed containers -` - -func (c *RemoveUnitCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "remove-unit", - Args: " [...]", - Purpose: "remove service units from the environment", - Doc: removeUnitDoc, - Aliases: []string{"destroy-unit"}, - } -} - -func (c *RemoveUnitCommand) Init(args []string) error { - c.UnitNames = args - if len(c.UnitNames) == 0 { - return fmt.Errorf("no units specified") - } - for _, name := range c.UnitNames { - if !names.IsValidUnit(name) { - return fmt.Errorf("invalid unit name %q", name) - } - } - return nil -} - -// Run connects to the environment specified on the command line and destroys -// units therein. -func (c *RemoveUnitCommand) Run(_ *cmd.Context) error { - client, err := c.NewAPIClient() - if err != nil { - return err - } - defer client.Close() - return block.ProcessBlockedError(client.DestroyServiceUnits(c.UnitNames...), block.BlockRemove) -} === removed file 'src/github.com/juju/juju/cmd/juju/removeunit_test.go' --- src/github.com/juju/juju/cmd/juju/removeunit_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/removeunit_test.go 1970-01-01 00:00:00 +0000 @@ -1,67 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - "gopkg.in/juju/charm.v5" - - "github.com/juju/juju/cmd/envcmd" - jujutesting "github.com/juju/juju/juju/testing" - "github.com/juju/juju/state" - "github.com/juju/juju/testcharms" - "github.com/juju/juju/testing" -) - -type RemoveUnitSuite struct { - jujutesting.RepoSuite - CmdBlockHelper -} - -func (s *RemoveUnitSuite) SetUpTest(c *gc.C) { - s.RepoSuite.SetUpTest(c) - s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) - c.Assert(s.CmdBlockHelper, gc.NotNil) - s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) -} - -var _ = gc.Suite(&RemoveUnitSuite{}) - -func runRemoveUnit(c *gc.C, args ...string) error { - _, err := testing.RunCommand(c, envcmd.Wrap(&RemoveUnitCommand{}), args...) - return err -} - -func (s *RemoveUnitSuite) setupUnitForRemove(c *gc.C) *state.Service { - testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") - err := runDeploy(c, "-n", "2", "local:dummy", "dummy") - c.Assert(err, jc.ErrorIsNil) - curl := charm.MustParseURL(fmt.Sprintf("local:%s/dummy-1", testing.FakeDefaultSeries)) - svc, _ := s.AssertService(c, "dummy", curl, 2, 0) - return svc -} - -func (s *RemoveUnitSuite) TestRemoveUnit(c *gc.C) { - svc := s.setupUnitForRemove(c) - - err := runRemoveUnit(c, "dummy/0", "dummy/1", "dummy/2", "sillybilly/17") - c.Assert(err, gc.ErrorMatches, `some units were not destroyed: unit "dummy/2" does not exist; unit "sillybilly/17" does not exist`) - units, err := svc.AllUnits() - c.Assert(err, jc.ErrorIsNil) - for _, u := range units { - c.Assert(u.Life(), gc.Equals, state.Dying) - } -} -func (s *RemoveUnitSuite) TestBlockRemoveUnit(c *gc.C) { - svc := s.setupUnitForRemove(c) - - // block operation - s.BlockRemoveObject(c, "TestBlockRemoveUnit") - err := runRemoveUnit(c, "dummy/0", "dummy/1") - s.AssertBlocked(c, err, ".*TestBlockRemoveUnit.*") - c.Assert(svc.Life(), gc.Equals, state.Alive) -} === removed file 'src/github.com/juju/juju/cmd/juju/resolved.go' --- src/github.com/juju/juju/cmd/juju/resolved.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/resolved.go 1970-01-01 00:00:00 +0000 @@ -1,57 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - - "github.com/juju/cmd" - "github.com/juju/names" - "launchpad.net/gnuflag" - - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/cmd/juju/block" -) - -// ResolvedCommand marks a unit in an error state as ready to continue. -type ResolvedCommand struct { - envcmd.EnvCommandBase - UnitName string - Retry bool -} - -func (c *ResolvedCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "resolved", - Args: "", - Purpose: "marks unit errors resolved", - } -} - -func (c *ResolvedCommand) SetFlags(f *gnuflag.FlagSet) { - f.BoolVar(&c.Retry, "r", false, "re-execute failed hooks") - f.BoolVar(&c.Retry, "retry", false, "") -} - -func (c *ResolvedCommand) Init(args []string) error { - if len(args) > 0 { - c.UnitName = args[0] - if !names.IsValidUnit(c.UnitName) { - return fmt.Errorf("invalid unit name %q", c.UnitName) - } - args = args[1:] - } else { - return fmt.Errorf("no unit specified") - } - return cmd.CheckEmpty(args) -} - -func (c *ResolvedCommand) Run(_ *cmd.Context) error { - client, err := c.NewAPIClient() - if err != nil { - return err - } - defer client.Close() - return block.ProcessBlockedError(client.Resolved(c.UnitName, c.Retry), block.BlockChange) -} === removed file 'src/github.com/juju/juju/cmd/juju/resolved_test.go' --- src/github.com/juju/juju/cmd/juju/resolved_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/resolved_test.go 1970-01-01 00:00:00 +0000 @@ -1,128 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/cmd/envcmd" - jujutesting "github.com/juju/juju/juju/testing" - "github.com/juju/juju/state" - "github.com/juju/juju/testcharms" - "github.com/juju/juju/testing" -) - -type ResolvedSuite struct { - jujutesting.RepoSuite - CmdBlockHelper -} - -func (s *ResolvedSuite) SetUpTest(c *gc.C) { - s.RepoSuite.SetUpTest(c) - s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) - c.Assert(s.CmdBlockHelper, gc.NotNil) - s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) -} - -var _ = gc.Suite(&ResolvedSuite{}) - -func runResolved(c *gc.C, args []string) error { - _, err := testing.RunCommand(c, envcmd.Wrap(&ResolvedCommand{}), args...) - return err -} - -var resolvedTests = []struct { - args []string - err string - unit string - mode state.ResolvedMode -}{ - { - err: `no unit specified`, - }, { - args: []string{"jeremy-fisher"}, - err: `invalid unit name "jeremy-fisher"`, - }, { - args: []string{"jeremy-fisher/99"}, - err: `unit "jeremy-fisher/99" not found`, - }, { - args: []string{"dummy/0"}, - err: `unit "dummy/0" is not in an error state`, - unit: "dummy/0", - mode: state.ResolvedNone, - }, { - args: []string{"dummy/1", "--retry"}, - err: `unit "dummy/1" is not in an error state`, - unit: "dummy/1", - mode: state.ResolvedNone, - }, { - args: []string{"dummy/2"}, - unit: "dummy/2", - mode: state.ResolvedNoHooks, - }, { - args: []string{"dummy/2", "--retry"}, - err: `cannot set resolved mode for unit "dummy/2": already resolved`, - unit: "dummy/2", - mode: state.ResolvedNoHooks, - }, { - args: []string{"dummy/3", "--retry"}, - unit: "dummy/3", - mode: state.ResolvedRetryHooks, - }, { - args: []string{"dummy/3"}, - err: `cannot set resolved mode for unit "dummy/3": already resolved`, - unit: "dummy/3", - mode: state.ResolvedRetryHooks, - }, { - args: []string{"dummy/4", "roflcopter"}, - err: `unrecognized args: \["roflcopter"\]`, - }, -} - -func (s *ResolvedSuite) TestResolved(c *gc.C) { - testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") - err := runDeploy(c, "-n", "5", "local:dummy", "dummy") - c.Assert(err, jc.ErrorIsNil) - - for _, name := range []string{"dummy/2", "dummy/3", "dummy/4"} { - u, err := s.State.Unit(name) - c.Assert(err, jc.ErrorIsNil) - err = u.SetAgentStatus(state.StatusError, "lol borken", nil) - c.Assert(err, jc.ErrorIsNil) - } - - for i, t := range resolvedTests { - c.Logf("test %d: %v", i, t.args) - err := runResolved(c, t.args) - if t.err != "" { - c.Assert(err, gc.ErrorMatches, t.err) - } else { - c.Assert(err, jc.ErrorIsNil) - } - if t.unit != "" { - unit, err := s.State.Unit(t.unit) - c.Assert(err, jc.ErrorIsNil) - c.Assert(unit.Resolved(), gc.Equals, t.mode) - } - } -} - -func (s *ResolvedSuite) TestBlockResolved(c *gc.C) { - testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") - err := runDeploy(c, "-n", "5", "local:dummy", "dummy") - c.Assert(err, jc.ErrorIsNil) - - for _, name := range []string{"dummy/2", "dummy/3", "dummy/4"} { - u, err := s.State.Unit(name) - c.Assert(err, jc.ErrorIsNil) - err = u.SetAgentStatus(state.StatusError, "lol borken", nil) - c.Assert(err, jc.ErrorIsNil) - } - - // Block operation - s.BlockAllChanges(c, "TestBlockResolved") - err = runResolved(c, []string{"dummy/2"}) - s.AssertBlocked(c, err, ".*TestBlockResolved.*") -} === removed file 'src/github.com/juju/juju/cmd/juju/run.go' --- src/github.com/juju/juju/cmd/juju/run.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/run.go 1970-01-01 00:00:00 +0000 @@ -1,232 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "encoding/base64" - "fmt" - "strings" - "time" - "unicode/utf8" - - "github.com/juju/cmd" - "github.com/juju/names" - "launchpad.net/gnuflag" - - "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/cmd/juju/block" -) - -// RunCommand is responsible for running arbitrary commands on remote machines. -type RunCommand struct { - envcmd.EnvCommandBase - out cmd.Output - all bool - timeout time.Duration - machines []string - services []string - units []string - commands string -} - -const runDoc = ` -Run the commands on the specified targets. - -Targets are specified using either machine ids, service names or unit -names. At least one target specifier is needed. - -Multiple values can be set for --machine, --service, and --unit by using -comma separated values. - -If the target is a machine, the command is run as the "ubuntu" user on -the remote machine. - -If the target is a service, the command is run on all units for that -service. For example, if there was a service "mysql" and that service -had two units, "mysql/0" and "mysql/1", then - --service mysql -is equivalent to - --unit mysql/0,mysql/1 - -Commands run for services or units are executed in a 'hook context' for -the unit. - ---all is provided as a simple way to run the command on all the machines -in the environment. If you specify --all you cannot provide additional -targets. - -` - -func (c *RunCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "run", - Args: "", - Purpose: "run the commands on the remote targets specified", - Doc: runDoc, - } -} - -func (c *RunCommand) SetFlags(f *gnuflag.FlagSet) { - c.out.AddFlags(f, "smart", cmd.DefaultFormatters) - f.BoolVar(&c.all, "all", false, "run the commands on all the machines") - f.DurationVar(&c.timeout, "timeout", 5*time.Minute, "how long to wait before the remote command is considered to have failed") - f.Var(cmd.NewStringsValue(nil, &c.machines), "machine", "one or more machine ids") - f.Var(cmd.NewStringsValue(nil, &c.services), "service", "one or more service names") - f.Var(cmd.NewStringsValue(nil, &c.units), "unit", "one or more unit ids") -} - -func (c *RunCommand) Init(args []string) error { - if len(args) == 0 { - return fmt.Errorf("no commands specified") - } - c.commands, args = args[0], args[1:] - - if c.all { - if len(c.machines) != 0 { - return fmt.Errorf("You cannot specify --all and individual machines") - } - if len(c.services) != 0 { - return fmt.Errorf("You cannot specify --all and individual services") - } - if len(c.units) != 0 { - return fmt.Errorf("You cannot specify --all and individual units") - } - } else { - if len(c.machines) == 0 && len(c.services) == 0 && len(c.units) == 0 { - return fmt.Errorf("You must specify a target, either through --all, --machine, --service or --unit") - } - } - - var nameErrors []string - for _, machineId := range c.machines { - if !names.IsValidMachine(machineId) { - nameErrors = append(nameErrors, fmt.Sprintf(" %q is not a valid machine id", machineId)) - } - } - for _, service := range c.services { - if !names.IsValidService(service) { - nameErrors = append(nameErrors, fmt.Sprintf(" %q is not a valid service name", service)) - } - } - for _, unit := range c.units { - if !names.IsValidUnit(unit) { - nameErrors = append(nameErrors, fmt.Sprintf(" %q is not a valid unit name", unit)) - } - } - if len(nameErrors) > 0 { - return fmt.Errorf("The following run targets are not valid:\n%s", - strings.Join(nameErrors, "\n")) - } - - return cmd.CheckEmpty(args) -} - -func encodeBytes(input []byte) (value string, encoding string) { - if utf8.Valid(input) { - value = string(input) - encoding = "utf8" - } else { - value = base64.StdEncoding.EncodeToString(input) - encoding = "base64" - } - return value, encoding -} - -func storeOutput(values map[string]interface{}, key string, input []byte) { - value, encoding := encodeBytes(input) - values[key] = value - if encoding != "utf8" { - values[key+".encoding"] = encoding - } -} - -// ConvertRunResults takes the results from the api and creates a map -// suitable for format converstion to YAML or JSON. -func ConvertRunResults(runResults []params.RunResult) interface{} { - var results = make([]interface{}, len(runResults)) - - for i, result := range runResults { - // We always want to have a string for stdout, but only show stderr, - // code and error if they are there. - values := make(map[string]interface{}) - values["MachineId"] = result.MachineId - if result.UnitId != "" { - values["UnitId"] = result.UnitId - - } - storeOutput(values, "Stdout", result.Stdout) - if len(result.Stderr) > 0 { - storeOutput(values, "Stderr", result.Stderr) - } - if result.Code != 0 { - values["ReturnCode"] = result.Code - } - if result.Error != "" { - values["Error"] = result.Error - } - results[i] = values - } - - return results -} - -func (c *RunCommand) Run(ctx *cmd.Context) error { - client, err := getRunAPIClient(c) - if err != nil { - return err - } - defer client.Close() - - var runResults []params.RunResult - if c.all { - runResults, err = client.RunOnAllMachines(c.commands, c.timeout) - } else { - params := params.RunParams{ - Commands: c.commands, - Timeout: c.timeout, - Machines: c.machines, - Services: c.services, - Units: c.units, - } - runResults, err = client.Run(params) - } - - if err != nil { - return block.ProcessBlockedError(err, block.BlockChange) - } - - // If we are just dealing with one result, AND we are using the smart - // format, then pretend we were running it locally. - if len(runResults) == 1 && c.out.Name() == "smart" { - result := runResults[0] - ctx.Stdout.Write(result.Stdout) - ctx.Stderr.Write(result.Stderr) - if result.Error != "" { - // Convert the error string back into an error object. - return fmt.Errorf("%s", result.Error) - } - if result.Code != 0 { - return cmd.NewRcPassthroughError(result.Code) - } - return nil - } - - c.out.Write(ctx, ConvertRunResults(runResults)) - return nil -} - -// In order to be able to easily mock out the API side for testing, -// the API client is got using a function. - -type RunClient interface { - Close() error - RunOnAllMachines(commands string, timeout time.Duration) ([]params.RunResult, error) - Run(run params.RunParams) ([]params.RunResult, error) -} - -// Here we need the signature to be correct for the interface. -var getRunAPIClient = func(c *RunCommand) (RunClient, error) { - return c.NewAPIClient() -} === removed file 'src/github.com/juju/juju/cmd/juju/run_test.go' --- src/github.com/juju/juju/cmd/juju/run_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/run_test.go 1970-01-01 00:00:00 +0000 @@ -1,486 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - "sort" - "strings" - "time" - - "github.com/juju/cmd" - jc "github.com/juju/testing/checkers" - "github.com/juju/utils/exec" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/apiserver/common" - "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/testing" -) - -type RunSuite struct { - testing.FakeJujuHomeSuite -} - -var _ = gc.Suite(&RunSuite{}) - -func (*RunSuite) TestTargetArgParsing(c *gc.C) { - for i, test := range []struct { - message string - args []string - all bool - machines []string - units []string - services []string - commands string - errMatch string - }{{ - message: "no args", - errMatch: "no commands specified", - }, { - message: "no target", - args: []string{"sudo reboot"}, - errMatch: "You must specify a target, either through --all, --machine, --service or --unit", - }, { - message: "too many args", - args: []string{"--all", "sudo reboot", "oops"}, - errMatch: `unrecognized args: \["oops"\]`, - }, { - message: "command to all machines", - args: []string{"--all", "sudo reboot"}, - all: true, - commands: "sudo reboot", - }, { - message: "all and defined machines", - args: []string{"--all", "--machine=1,2", "sudo reboot"}, - errMatch: `You cannot specify --all and individual machines`, - }, { - message: "command to machines 1, 2, and 1/kvm/0", - args: []string{"--machine=1,2,1/kvm/0", "sudo reboot"}, - commands: "sudo reboot", - machines: []string{"1", "2", "1/kvm/0"}, - }, { - message: "bad machine names", - args: []string{"--machine=foo,machine-2", "sudo reboot"}, - errMatch: "" + - "The following run targets are not valid:\n" + - " \"foo\" is not a valid machine id\n" + - " \"machine-2\" is not a valid machine id", - }, { - message: "all and defined services", - args: []string{"--all", "--service=wordpress,mysql", "sudo reboot"}, - errMatch: `You cannot specify --all and individual services`, - }, { - message: "command to services wordpress and mysql", - args: []string{"--service=wordpress,mysql", "sudo reboot"}, - commands: "sudo reboot", - services: []string{"wordpress", "mysql"}, - }, { - message: "bad service names", - args: []string{"--service", "foo,2,foo/0", "sudo reboot"}, - errMatch: "" + - "The following run targets are not valid:\n" + - " \"2\" is not a valid service name\n" + - " \"foo/0\" is not a valid service name", - }, { - message: "all and defined units", - args: []string{"--all", "--unit=wordpress/0,mysql/1", "sudo reboot"}, - errMatch: `You cannot specify --all and individual units`, - }, { - message: "command to valid units", - args: []string{"--unit=wordpress/0,wordpress/1,mysql/0", "sudo reboot"}, - commands: "sudo reboot", - units: []string{"wordpress/0", "wordpress/1", "mysql/0"}, - }, { - message: "bad unit names", - args: []string{"--unit", "foo,2,foo/0", "sudo reboot"}, - errMatch: "" + - "The following run targets are not valid:\n" + - " \"foo\" is not a valid unit name\n" + - " \"2\" is not a valid unit name", - }, { - message: "command to mixed valid targets", - args: []string{"--machine=0", "--unit=wordpress/0,wordpress/1", "--service=mysql", "sudo reboot"}, - commands: "sudo reboot", - machines: []string{"0"}, - services: []string{"mysql"}, - units: []string{"wordpress/0", "wordpress/1"}, - }} { - c.Log(fmt.Sprintf("%v: %s", i, test.message)) - runCmd := &RunCommand{} - testing.TestInit(c, envcmd.Wrap(runCmd), test.args, test.errMatch) - if test.errMatch == "" { - c.Check(runCmd.all, gc.Equals, test.all) - c.Check(runCmd.machines, gc.DeepEquals, test.machines) - c.Check(runCmd.services, gc.DeepEquals, test.services) - c.Check(runCmd.units, gc.DeepEquals, test.units) - c.Check(runCmd.commands, gc.Equals, test.commands) - } - } -} - -func (*RunSuite) TestTimeoutArgParsing(c *gc.C) { - for i, test := range []struct { - message string - args []string - errMatch string - timeout time.Duration - }{{ - message: "default time", - args: []string{"--all", "sudo reboot"}, - timeout: 5 * time.Minute, - }, { - message: "invalid time", - args: []string{"--timeout=foo", "--all", "sudo reboot"}, - errMatch: `invalid value "foo" for flag --timeout: time: invalid duration foo`, - }, { - message: "two hours", - args: []string{"--timeout=2h", "--all", "sudo reboot"}, - timeout: 2 * time.Hour, - }, { - message: "3 minutes 30 seconds", - args: []string{"--timeout=3m30s", "--all", "sudo reboot"}, - timeout: (3 * time.Minute) + (30 * time.Second), - }} { - c.Log(fmt.Sprintf("%v: %s", i, test.message)) - runCmd := &RunCommand{} - testing.TestInit(c, envcmd.Wrap(runCmd), test.args, test.errMatch) - if test.errMatch == "" { - c.Check(runCmd.timeout, gc.Equals, test.timeout) - } - } -} - -func (s *RunSuite) TestConvertRunResults(c *gc.C) { - for i, test := range []struct { - message string - results []params.RunResult - expected interface{} - }{{ - message: "empty", - expected: []interface{}{}, - }, { - message: "minimum is machine id and stdout", - results: []params.RunResult{ - makeRunResult(mockResponse{machineId: "1"}), - }, - expected: []interface{}{ - map[string]interface{}{ - "MachineId": "1", - "Stdout": "", - }}, - }, { - message: "other fields are copied if there", - results: []params.RunResult{ - makeRunResult(mockResponse{ - machineId: "1", - stdout: "stdout", - stderr: "stderr", - code: 42, - unitId: "unit/0", - error: "error", - }), - }, - expected: []interface{}{ - map[string]interface{}{ - "MachineId": "1", - "Stdout": "stdout", - "Stderr": "stderr", - "ReturnCode": 42, - "UnitId": "unit/0", - "Error": "error", - }}, - }, { - message: "stdout and stderr are base64 encoded if not valid utf8", - results: []params.RunResult{ - { - ExecResponse: exec.ExecResponse{ - Stdout: []byte{0xff}, - Stderr: []byte{0xfe}, - }, - MachineId: "jake", - }, - }, - expected: []interface{}{ - map[string]interface{}{ - "MachineId": "jake", - "Stdout": "/w==", - "Stdout.encoding": "base64", - "Stderr": "/g==", - "Stderr.encoding": "base64", - }}, - }, { - message: "more than one", - results: []params.RunResult{ - makeRunResult(mockResponse{machineId: "1"}), - makeRunResult(mockResponse{machineId: "2"}), - makeRunResult(mockResponse{machineId: "3"}), - }, - expected: []interface{}{ - map[string]interface{}{ - "MachineId": "1", - "Stdout": "", - }, - map[string]interface{}{ - "MachineId": "2", - "Stdout": "", - }, - map[string]interface{}{ - "MachineId": "3", - "Stdout": "", - }, - }, - }} { - c.Log(fmt.Sprintf("%v: %s", i, test.message)) - result := ConvertRunResults(test.results) - c.Check(result, jc.DeepEquals, test.expected) - } -} - -func (s *RunSuite) TestRunForMachineAndUnit(c *gc.C) { - mock := s.setupMockAPI() - machineResponse := mockResponse{ - stdout: "megatron\n", - machineId: "0", - } - unitResponse := mockResponse{ - stdout: "bumblebee", - machineId: "1", - unitId: "unit/0", - } - mock.setResponse("0", machineResponse) - mock.setResponse("unit/0", unitResponse) - - unformatted := ConvertRunResults([]params.RunResult{ - makeRunResult(machineResponse), - makeRunResult(unitResponse), - }) - - jsonFormatted, err := cmd.FormatJson(unformatted) - c.Assert(err, jc.ErrorIsNil) - - context, err := testing.RunCommand(c, envcmd.Wrap(&RunCommand{}), - "--format=json", "--machine=0", "--unit=unit/0", "hostname", - ) - c.Assert(err, jc.ErrorIsNil) - - c.Check(testing.Stdout(context), gc.Equals, string(jsonFormatted)+"\n") -} - -func (s *RunSuite) TestBlockRunForMachineAndUnit(c *gc.C) { - mock := s.setupMockAPI() - // Block operation - mock.block = true - _, err := testing.RunCommand(c, envcmd.Wrap(&RunCommand{}), - "--format=json", "--machine=0", "--unit=unit/0", "hostname", - "-e blah", - ) - c.Assert(err, gc.ErrorMatches, cmd.ErrSilent.Error()) - // msg is logged - stripped := strings.Replace(c.GetTestLog(), "\n", "", -1) - c.Check(stripped, gc.Matches, ".*To unblock changes.*") -} - -func (s *RunSuite) TestAllMachines(c *gc.C) { - mock := s.setupMockAPI() - mock.setMachinesAlive("0", "1") - response0 := mockResponse{ - stdout: "megatron\n", - machineId: "0", - } - response1 := mockResponse{ - error: "command timed out", - machineId: "1", - } - mock.setResponse("0", response0) - - unformatted := ConvertRunResults([]params.RunResult{ - makeRunResult(response0), - makeRunResult(response1), - }) - - jsonFormatted, err := cmd.FormatJson(unformatted) - c.Assert(err, jc.ErrorIsNil) - - context, err := testing.RunCommand(c, &RunCommand{}, "--format=json", "--all", "hostname") - c.Assert(err, jc.ErrorIsNil) - - c.Check(testing.Stdout(context), gc.Equals, string(jsonFormatted)+"\n") -} - -func (s *RunSuite) TestBlockAllMachines(c *gc.C) { - mock := s.setupMockAPI() - // Block operation - mock.block = true - _, err := testing.RunCommand(c, &RunCommand{}, "--format=json", "--all", "hostname") - c.Assert(err, gc.ErrorMatches, cmd.ErrSilent.Error()) - // msg is logged - stripped := strings.Replace(c.GetTestLog(), "\n", "", -1) - c.Check(stripped, gc.Matches, ".*To unblock changes.*") -} - -func (s *RunSuite) TestSingleResponse(c *gc.C) { - mock := s.setupMockAPI() - mock.setMachinesAlive("0") - mockResponse := mockResponse{ - stdout: "stdout\n", - stderr: "stderr\n", - code: 42, - machineId: "0", - } - mock.setResponse("0", mockResponse) - unformatted := ConvertRunResults([]params.RunResult{ - makeRunResult(mockResponse)}) - yamlFormatted, err := cmd.FormatYaml(unformatted) - c.Assert(err, jc.ErrorIsNil) - jsonFormatted, err := cmd.FormatJson(unformatted) - c.Assert(err, jc.ErrorIsNil) - - for i, test := range []struct { - message string - format string - stdout string - stderr string - errorMatch string - }{{ - message: "smart (default)", - stdout: "stdout\n", - stderr: "stderr\n", - errorMatch: "subprocess encountered error code 42", - }, { - message: "yaml output", - format: "yaml", - stdout: string(yamlFormatted) + "\n", - }, { - message: "json output", - format: "json", - stdout: string(jsonFormatted) + "\n", - }} { - c.Log(fmt.Sprintf("%v: %s", i, test.message)) - args := []string{} - if test.format != "" { - args = append(args, "--format", test.format) - } - args = append(args, "--all", "ignored") - context, err := testing.RunCommand(c, envcmd.Wrap(&RunCommand{}), args...) - if test.errorMatch != "" { - c.Check(err, gc.ErrorMatches, test.errorMatch) - } else { - c.Check(err, jc.ErrorIsNil) - } - c.Check(testing.Stdout(context), gc.Equals, test.stdout) - c.Check(testing.Stderr(context), gc.Equals, test.stderr) - } -} - -func (s *RunSuite) setupMockAPI() *mockRunAPI { - mock := &mockRunAPI{} - s.PatchValue(&getRunAPIClient, func(_ *RunCommand) (RunClient, error) { - return mock, nil - }) - return mock -} - -type mockRunAPI struct { - stdout string - stderr string - code int - // machines, services, units - machines map[string]bool - responses map[string]params.RunResult - block bool -} - -type mockResponse struct { - stdout string - stderr string - code int - error string - machineId string - unitId string -} - -var _ RunClient = (*mockRunAPI)(nil) - -func (m *mockRunAPI) setMachinesAlive(ids ...string) { - if m.machines == nil { - m.machines = make(map[string]bool) - } - for _, id := range ids { - m.machines[id] = true - } -} - -func makeRunResult(mock mockResponse) params.RunResult { - return params.RunResult{ - ExecResponse: exec.ExecResponse{ - Stdout: []byte(mock.stdout), - Stderr: []byte(mock.stderr), - Code: mock.code, - }, - MachineId: mock.machineId, - UnitId: mock.unitId, - Error: mock.error, - } -} - -func (m *mockRunAPI) setResponse(id string, mock mockResponse) { - if m.responses == nil { - m.responses = make(map[string]params.RunResult) - } - m.responses[id] = makeRunResult(mock) -} - -func (*mockRunAPI) Close() error { - return nil -} - -func (m *mockRunAPI) RunOnAllMachines(commands string, timeout time.Duration) ([]params.RunResult, error) { - var result []params.RunResult - - if m.block { - return result, common.ErrOperationBlocked("The operation has been blocked.") - } - sortedMachineIds := make([]string, 0, len(m.machines)) - for machineId := range m.machines { - sortedMachineIds = append(sortedMachineIds, machineId) - } - sort.Strings(sortedMachineIds) - - for _, machineId := range sortedMachineIds { - response, found := m.responses[machineId] - if !found { - // Consider this a timeout - response = params.RunResult{MachineId: machineId, Error: "command timed out"} - } - result = append(result, response) - } - - return result, nil -} - -func (m *mockRunAPI) Run(runParams params.RunParams) ([]params.RunResult, error) { - var result []params.RunResult - - if m.block { - return result, common.ErrOperationBlocked("The operation has been blocked.") - } - // Just add in ids that match in order. - for _, id := range runParams.Machines { - response, found := m.responses[id] - if found { - result = append(result, response) - } - } - // mock ignores services - for _, id := range runParams.Units { - response, found := m.responses[id] - if found { - result = append(result, response) - } - } - - return result, nil -} === removed file 'src/github.com/juju/juju/cmd/juju/scp.go' --- src/github.com/juju/juju/cmd/juju/scp.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/scp.go 1970-01-01 00:00:00 +0000 @@ -1,112 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - "net" - "strings" - - "github.com/juju/cmd" - - "github.com/juju/juju/utils/ssh" -) - -// SCPCommand is responsible for launching a scp command to copy files to/from remote machine(s) -type SCPCommand struct { - SSHCommon -} - -const scpDoc = ` -Launch an scp command to copy files. Each argument ... -is either local file path or remote locations of the form [@]:, -where can be either a machine id as listed by "juju status" in the -"machines" section or a unit name as listed in the "services" section. If a -username is not specified, the username "ubuntu" will be used. - -To pass additional flags to "scp", separate "juju scp" from the options with -"--" to prevent Juju from attempting to interpret the flags. This is only -supported if the scp command can be found in the system PATH. Please refer to -the man page of scp(1) for the supported extra arguments. - -Examples: - -Copy a single file from machine 2 to the local machine: - - juju scp 2:/var/log/syslog . - -Copy 2 files from two units to the local backup/ directory, passing -v -to scp as an extra argument: - - juju scp -- -v ubuntu/0:/path/file1 ubuntu/1:/path/file2 backup/ - -Recursively copy the directory /var/log/mongodb/ on the first mongodb -server to the local directory remote-logs: - - juju scp -- -r mongodb/0:/var/log/mongodb/ remote-logs/ - -Copy a local file to the second apache unit of the environment "testing": - - juju scp -e testing foo.txt apache2/1: -` - -func (c *SCPCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "scp", - Args: " ... [scp-option...]", - Purpose: "launch a scp command to copy files to/from remote machine(s)", - Doc: scpDoc, - } -} - -func (c *SCPCommand) Init(args []string) error { - if len(args) < 2 { - return fmt.Errorf("at least two arguments required") - } - c.Args = args - return nil -} - -// expandArgs takes a list of arguments and looks for ones in the form of -// 0:some/path or service/0:some/path, and translates them into -// ubuntu@machine:some/path so they can be passed as arguments to scp, and pass -// the rest verbatim on to scp -func expandArgs(args []string, userHostFromTarget func(string) (string, string, error)) ([]string, error) { - outArgs := make([]string, len(args)) - for i, arg := range args { - v := strings.SplitN(arg, ":", 2) - if strings.HasPrefix(arg, "-") || len(v) <= 1 { - // Can't be an interesting target, so just pass it along - outArgs[i] = arg - continue - } - user, host, err := userHostFromTarget(v[0]) - if err != nil { - return nil, err - } - outArgs[i] = user + "@" + net.JoinHostPort(host, v[1]) - } - return outArgs, nil -} - -// Run resolves c.Target to a machine, or host of a unit and -// forks ssh with c.Args, if provided. -func (c *SCPCommand) Run(ctx *cmd.Context) error { - var err error - c.apiClient, err = c.initAPIClient() - if err != nil { - return err - } - defer c.apiClient.Close() - - options, err := c.getSSHOptions(false) - if err != nil { - return err - } - args, err := expandArgs(c.Args, c.userHostFromTarget) - if err != nil { - return err - } - return ssh.Copy(args, options) -} === removed file 'src/github.com/juju/juju/cmd/juju/scp_unix_test.go' --- src/github.com/juju/juju/cmd/juju/scp_unix_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/scp_unix_test.go 1970-01-01 00:00:00 +0000 @@ -1,244 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -// +build !windows - -package main - -import ( - "bytes" - "fmt" - "io/ioutil" - "path/filepath" - "strings" - - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - "gopkg.in/juju/charm.v5" - - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/network" - "github.com/juju/juju/testcharms" - coretesting "github.com/juju/juju/testing" -) - -var _ = gc.Suite(&SCPSuite{}) -var _ = gc.Suite(&expandArgsSuite{}) - -type SCPSuite struct { - SSHCommonSuite -} - -type expandArgsSuite struct{} - -var scpTests = []struct { - about string - args []string - result string - proxy bool - error string -}{ - { - about: "scp from machine 0 to current dir", - args: []string{"0:foo", "."}, - result: commonArgsNoProxy + "ubuntu@dummyenv-0.dns:foo .\n", - }, { - about: "scp from machine 0 to current dir with extra args", - args: []string{"0:foo", ".", "-rv", "-o", "SomeOption"}, - result: commonArgsNoProxy + "ubuntu@dummyenv-0.dns:foo . -rv -o SomeOption\n", - }, { - about: "scp from current dir to machine 0", - args: []string{"foo", "0:"}, - result: commonArgsNoProxy + "foo ubuntu@dummyenv-0.dns:\n", - }, { - about: "scp from current dir to machine 0 with extra args", - args: []string{"foo", "0:", "-r", "-v"}, - result: commonArgsNoProxy + "foo ubuntu@dummyenv-0.dns: -r -v\n", - }, { - about: "scp from machine 0 to unit mysql/0", - args: []string{"0:foo", "mysql/0:/foo"}, - result: commonArgsNoProxy + "ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n", - }, { - about: "scp from machine 0 to unit mysql/0 and extra args", - args: []string{"0:foo", "mysql/0:/foo", "-q"}, - result: commonArgsNoProxy + "ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo -q\n", - }, { - about: "scp from machine 0 to unit mysql/0 and extra args before", - args: []string{"-q", "-r", "0:foo", "mysql/0:/foo"}, - result: commonArgsNoProxy + "-q -r ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n", - }, { - about: "scp two local files to unit mysql/0", - args: []string{"file1", "file2", "mysql/0:/foo/"}, - result: commonArgsNoProxy + "file1 file2 ubuntu@dummyenv-0.dns:/foo/\n", - }, { - about: "scp from unit mongodb/1 to unit mongodb/0 and multiple extra args", - args: []string{"mongodb/1:foo", "mongodb/0:", "-r", "-v", "-q", "-l5"}, - result: commonArgsNoProxy + "ubuntu@dummyenv-2.dns:foo ubuntu@dummyenv-1.dns: -r -v -q -l5\n", - }, { - about: "scp works with IPv6 addresses", - args: []string{"ipv6-svc/0:foo", "bar"}, - result: commonArgsNoProxy + `ubuntu@[2001:db8::1]:foo bar` + "\n", - }, { - about: "scp from machine 0 to unit mysql/0 with proxy", - args: []string{"0:foo", "mysql/0:/foo"}, - result: commonArgs + "ubuntu@dummyenv-0.internal:foo ubuntu@dummyenv-0.internal:/foo\n", - proxy: true, - }, { - args: []string{"0:foo", ".", "-rv", "-o", "SomeOption"}, - result: commonArgsNoProxy + "ubuntu@dummyenv-0.dns:foo . -rv -o SomeOption\n", - }, { - args: []string{"foo", "0:", "-r", "-v"}, - result: commonArgsNoProxy + "foo ubuntu@dummyenv-0.dns: -r -v\n", - }, { - args: []string{"mongodb/1:foo", "mongodb/0:", "-r", "-v", "-q", "-l5"}, - result: commonArgsNoProxy + "ubuntu@dummyenv-2.dns:foo ubuntu@dummyenv-1.dns: -r -v -q -l5\n", - }, { - about: "scp from unit mongodb/1 to unit mongodb/0 with a --", - args: []string{"--", "-r", "-v", "mongodb/1:foo", "mongodb/0:", "-q", "-l5"}, - result: commonArgsNoProxy + "-- -r -v ubuntu@dummyenv-2.dns:foo ubuntu@dummyenv-1.dns: -q -l5\n", - }, { - about: "scp from unit mongodb/1 to current dir as 'mongo' user", - args: []string{"mongo@mongodb/1:foo", "."}, - result: commonArgsNoProxy + "mongo@dummyenv-2.dns:foo .\n", - }, { - about: "scp with no such machine", - args: []string{"5:foo", "bar"}, - error: "machine 5 not found", - }, -} - -func (s *SCPSuite) TestSCPCommand(c *gc.C) { - m := s.makeMachines(4, c, true) - ch := testcharms.Repo.CharmDir("dummy") - curl := charm.MustParseURL( - fmt.Sprintf("local:quantal/%s-%d", ch.Meta().Name, ch.Revision()), - ) - dummyCharm, err := s.State.AddCharm(ch, curl, "dummy-path", "dummy-1-sha256") - c.Assert(err, jc.ErrorIsNil) - srv := s.AddTestingService(c, "mysql", dummyCharm) - s.addUnit(srv, m[0], c) - - srv = s.AddTestingService(c, "mongodb", dummyCharm) - s.addUnit(srv, m[1], c) - s.addUnit(srv, m[2], c) - srv = s.AddTestingService(c, "ipv6-svc", dummyCharm) - s.addUnit(srv, m[3], c) - // Simulate machine 3 has a public IPv6 address. - ipv6Addr := network.NewScopedAddress("2001:db8::1", network.ScopePublic) - err = m[3].SetProviderAddresses(ipv6Addr) - c.Assert(err, jc.ErrorIsNil) - - for i, t := range scpTests { - c.Logf("test %d: %s -> %s\n", i, t.about, t.args) - ctx := coretesting.Context(c) - scpcmd := &SCPCommand{} - scpcmd.proxy = t.proxy - - err := envcmd.Wrap(scpcmd).Init(t.args) - c.Check(err, jc.ErrorIsNil) - err = scpcmd.Run(ctx) - if t.error != "" { - c.Check(err, gc.ErrorMatches, t.error) - c.Check(t.result, gc.Equals, "") - } else { - c.Check(err, jc.ErrorIsNil) - // we suppress stdout from scp - c.Check(ctx.Stderr.(*bytes.Buffer).String(), gc.Equals, "") - c.Check(ctx.Stdout.(*bytes.Buffer).String(), gc.Equals, "") - data, err := ioutil.ReadFile(filepath.Join(s.bin, "scp.args")) - c.Check(err, jc.ErrorIsNil) - actual := string(data) - if t.proxy { - actual = strings.Replace(actual, ".dns", ".internal", 2) - } - c.Check(actual, gc.Equals, t.result) - } - } -} - -type userHost struct { - user string - host string -} - -var userHostsFromTargets = map[string]userHost{ - "0": {"ubuntu", "dummyenv-0.dns"}, - "mysql/0": {"ubuntu", "dummyenv-0.dns"}, - "mongodb/0": {"ubuntu", "dummyenv-1.dns"}, - "mongodb/1": {"ubuntu", "dummyenv-2.dns"}, - "mongo@mongodb/1": {"mongo", "dummyenv-2.dns"}, - "ipv6-svc/0": {"ubuntu", "2001:db8::1"}, -} - -func dummyHostsFromTarget(target string) (string, string, error) { - if res, ok := userHostsFromTargets[target]; ok { - return res.user, res.host, nil - } - return "ubuntu", target, nil -} - -func (s *expandArgsSuite) TestSCPExpandArgs(c *gc.C) { - for i, t := range scpTests { - if t.error != "" { - // We are just running a focused set of tests on - // expandArgs, we aren't implementing the full - // userHostsFromTargets to actually trigger errors - continue - } - c.Logf("test %d: %s -> %s\n", i, t.about, t.args) - // expandArgs doesn't add the commonArgs prefix, so strip it - // off, along with the trailing '\n' - var argString string - if t.proxy { - c.Check(strings.HasPrefix(t.result, commonArgs), jc.IsTrue) - argString = t.result[len(commonArgs):] - } else { - c.Check(strings.HasPrefix(t.result, commonArgsNoProxy), jc.IsTrue) - argString = t.result[len(commonArgsNoProxy):] - } - c.Check(strings.HasSuffix(argString, "\n"), jc.IsTrue) - argString = argString[:len(argString)-1] - args := strings.Split(argString, " ") - expanded, err := expandArgs(t.args, func(target string) (string, string, error) { - if res, ok := userHostsFromTargets[target]; ok { - if t.proxy { - res.host = strings.Replace(res.host, ".dns", ".internal", 1) - } - return res.user, res.host, nil - } - return "ubuntu", target, nil - }) - c.Check(err, jc.ErrorIsNil) - c.Check(expanded, gc.DeepEquals, args) - } -} - -var expandTests = []struct { - about string - args []string - result []string -}{ - { - "don't expand params that start with '-'", - []string{"-0:stuff", "0:foo", "."}, - []string{"-0:stuff", "ubuntu@dummyenv-0.dns:foo", "."}, - }, -} - -func (s *expandArgsSuite) TestExpandArgs(c *gc.C) { - for i, t := range expandTests { - c.Logf("test %d: %s -> %s\n", i, t.about, t.args) - expanded, err := expandArgs(t.args, dummyHostsFromTarget) - c.Check(err, jc.ErrorIsNil) - c.Check(expanded, gc.DeepEquals, t.result) - } -} - -func (s *expandArgsSuite) TestExpandArgsPropagatesErrors(c *gc.C) { - erroringUserHostFromTargets := func(string) (string, string, error) { - return "", "", fmt.Errorf("this is my error") - } - expanded, err := expandArgs([]string{"foo:1", "bar"}, erroringUserHostFromTargets) - c.Assert(err, gc.ErrorMatches, "this is my error") - c.Check(expanded, gc.IsNil) -} === modified file 'src/github.com/juju/juju/cmd/juju/service/addunit.go' --- src/github.com/juju/juju/cmd/juju/service/addunit.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/service/addunit.go 2015-10-23 18:29:32 +0000 @@ -4,55 +4,86 @@ package service import ( - "errors" - "fmt" "regexp" + "strings" "github.com/juju/cmd" + "github.com/juju/errors" "github.com/juju/names" "launchpad.net/gnuflag" + "github.com/juju/juju/apiserver/params" "github.com/juju/juju/cmd/envcmd" "github.com/juju/juju/cmd/juju/block" "github.com/juju/juju/environs/config" + "github.com/juju/juju/instance" "github.com/juju/juju/provider" ) // UnitCommandBase provides support for commands which deploy units. It handles the parsing // and validation of --to and --num-units arguments. type UnitCommandBase struct { - ToMachineSpec string - NumUnits int + // PlacementSpec is the raw string command arg value used to specify placement directives. + PlacementSpec string + // Placement is the result of parsing the PlacementSpec arg value. + Placement []*instance.Placement + NumUnits int } func (c *UnitCommandBase) SetFlags(f *gnuflag.FlagSet) { f.IntVar(&c.NumUnits, "num-units", 1, "") - f.StringVar(&c.ToMachineSpec, "to", "", "the machine or container to deploy the unit in, bypasses constraints") + f.StringVar(&c.PlacementSpec, "to", "", "the machine, container or placement directive to deploy the unit in, bypasses constraints") } func (c *UnitCommandBase) Init(args []string) error { if c.NumUnits < 1 { return errors.New("--num-units must be a positive integer") } - if c.ToMachineSpec != "" { - if c.NumUnits > 1 { - return errors.New("cannot use --num-units > 1 with --to") - } - if !IsMachineOrNewContainer(c.ToMachineSpec) { - return fmt.Errorf("invalid --to parameter %q", c.ToMachineSpec) - } - + if c.PlacementSpec != "" { + // Older Juju versions just accept a single machine or container. + if IsMachineOrNewContainer(c.PlacementSpec) { + return nil + } + // Newer Juju versions accept a comma separated list of placement directives. + placementSpecs := strings.Split(c.PlacementSpec, ",") + c.Placement = make([]*instance.Placement, len(placementSpecs)) + for i, spec := range placementSpecs { + placement, err := parsePlacement(spec) + if err != nil { + return errors.Errorf("invalid --to parameter %q", spec) + } + c.Placement[i] = placement + } + } + if len(c.Placement) > c.NumUnits { + logger.Warningf("%d unit(s) will be deployed, extra placement directives will be ignored", c.NumUnits) } return nil } +func parsePlacement(spec string) (*instance.Placement, error) { + placement, err := instance.ParsePlacement(spec) + if err == instance.ErrPlacementScopeMissing { + spec = "env-uuid" + ":" + spec + placement, err = instance.ParsePlacement(spec) + } + if err != nil { + return nil, errors.Errorf("invalid --to parameter %q", spec) + } + return placement, nil +} + // TODO(anastasiamac) 2014-10-20 Bug#1383116 // This exists to provide more context to the user about // why they cannot allocate units to machine 0. Remove // this when the local provider's machine 0 is a container. // TODO(cherylj) Unexport CheckProvider once deploy is moved under service func (c *UnitCommandBase) CheckProvider(conf *config.Config) error { - if conf.Type() == provider.Local && c.ToMachineSpec == "0" { + isMachineZero := c.PlacementSpec == "0" + for _, p := range c.Placement { + isMachineZero = isMachineZero || (p.Scope == instance.MachineScope && p.Directive == "0") + } + if conf.Type() == provider.Local && isMachineZero { return errors.New("machine 0 is the state server for a local environment and cannot host units") } return nil @@ -125,7 +156,9 @@ // that the service add-unit command calls. type ServiceAddUnitAPI interface { Close() error + EnvironmentUUID() string AddServiceUnits(service string, numUnits int, machineSpec string) ([]string, error) + AddServiceUnitsWithPlacement(service string, numUnits int, placement []*instance.Placement) ([]string, error) EnvironmentGet() (map[string]interface{}, error) } @@ -154,7 +187,28 @@ return err } - _, err = apiclient.AddServiceUnits(c.ServiceName, c.NumUnits, c.ToMachineSpec) + for i, p := range c.Placement { + if p.Scope == "env-uuid" { + p.Scope = apiclient.EnvironmentUUID() + } + c.Placement[i] = p + } + if len(c.Placement) > 0 { + _, err = apiclient.AddServiceUnitsWithPlacement(c.ServiceName, c.NumUnits, c.Placement) + if err == nil { + return nil + } + if !params.IsCodeNotImplemented(err) { + return block.ProcessBlockedError(err, block.BlockChange) + } + } + if c.PlacementSpec != "" && !IsMachineOrNewContainer(c.PlacementSpec) { + return errors.Errorf("unsupported --to parameter %q", c.PlacementSpec) + } + if c.PlacementSpec != "" && c.NumUnits > 1 { + return errors.New("this version of Juju does not support --num-units > 1 with --to") + } + _, err = apiclient.AddServiceUnits(c.ServiceName, c.NumUnits, c.PlacementSpec) return block.ProcessBlockedError(err, block.BlockChange) } === modified file 'src/github.com/juju/juju/cmd/juju/service/addunit_test.go' --- src/github.com/juju/juju/cmd/juju/service/addunit_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/service/addunit_test.go 2015-10-23 18:29:32 +0000 @@ -11,9 +11,11 @@ gc "gopkg.in/check.v1" "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/params" "github.com/juju/juju/cmd/envcmd" "github.com/juju/juju/cmd/juju/service" "github.com/juju/juju/environs/config" + "github.com/juju/juju/instance" "github.com/juju/juju/testing" ) @@ -27,13 +29,19 @@ service string numUnits int machineSpec string + placement []*instance.Placement err error + newAPI bool } func (f *fakeServiceAddUnitAPI) Close() error { return nil } +func (f *fakeServiceAddUnitAPI) EnvironmentUUID() string { + return "fake-uuid" +} + func (f *fakeServiceAddUnitAPI) AddServiceUnits(service string, numUnits int, machineSpec string) ([]string, error) { if f.err != nil { return nil, f.err @@ -50,6 +58,19 @@ return nil, nil } +func (f *fakeServiceAddUnitAPI) AddServiceUnitsWithPlacement(service string, numUnits int, placement []*instance.Placement) ([]string, error) { + if !f.newAPI { + return nil, ¶ms.Error{Code: params.CodeNotImplemented} + } + if service != f.service { + return nil, errors.NotFoundf("service %q", service) + } + + f.numUnits += numUnits + f.placement = placement + return nil, nil +} + func (f *fakeServiceAddUnitAPI) EnvironmentGet() (map[string]interface{}, error) { cfg, err := config.New(config.UseDefaults, map[string]interface{}{ "type": f.envType, @@ -80,11 +101,8 @@ args: []string{}, err: `no service specified`, }, { - args: []string{"some-service-name", "--to", "bigglesplop"}, - err: `invalid --to parameter "bigglesplop"`, - }, { - args: []string{"some-service-name", "-n", "2", "--to", "123"}, - err: `cannot use --num-units > 1 with --to`, + args: []string{"some-service-name", "--to", "1,#:foo"}, + err: `invalid --to parameter "#:foo"`, }, } @@ -101,6 +119,24 @@ return err } +func (s *AddUnitSuite) TestInvalidToParamWithOlderServer(c *gc.C) { + err := s.runAddUnit(c, "some-service-name") + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.fake.numUnits, gc.Equals, 2) + + err = s.runAddUnit(c, "--to", "bigglesplop", "some-service-name") + c.Assert(err, gc.ErrorMatches, `unsupported --to parameter "bigglesplop"`) +} + +func (s *AddUnitSuite) TestUnsupportedNumUnitsWithOlderServer(c *gc.C) { + err := s.runAddUnit(c, "some-service-name") + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.fake.numUnits, gc.Equals, 2) + + err = s.runAddUnit(c, "-n", "2", "--to", "123", "some-service-name") + c.Assert(err, gc.ErrorMatches, `this version of Juju does not support --num-units > 1 with --to`) +} + func (s *AddUnitSuite) TestAddUnit(c *gc.C) { err := s.runAddUnit(c, "some-service-name") c.Assert(err, jc.ErrorIsNil) @@ -111,6 +147,23 @@ c.Assert(s.fake.numUnits, gc.Equals, 4) } +func (s *AddUnitSuite) TestAddUnitWithPlacement(c *gc.C) { + s.fake.newAPI = true + err := s.runAddUnit(c, "some-service-name") + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.fake.numUnits, gc.Equals, 2) + + err = s.runAddUnit(c, "--num-units", "2", "--to", "123,lxc:1,1/lxc/2,foo", "some-service-name") + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.fake.numUnits, gc.Equals, 4) + c.Assert(s.fake.placement, jc.DeepEquals, []*instance.Placement{ + {"#", "123"}, + {"lxc", "1"}, + {"#", "1/lxc/2"}, + {"fake-uuid", "foo"}, + }) +} + func (s *AddUnitSuite) TestBlockAddUnit(c *gc.C) { // Block operation s.fake.err = common.ErrOperationBlocked("TestBlockAddUnit") @@ -130,6 +183,8 @@ s.fake.envType = "local" err := s.runAddUnit(c, "some-service-name", "--to", "0") c.Assert(err, gc.ErrorMatches, "machine 0 is the state server for a local environment and cannot host units") + err = s.runAddUnit(c, "some-service-name", "--to", "1,#:0") + c.Assert(err, gc.ErrorMatches, "machine 0 is the state server for a local environment and cannot host units") } func (s *AddUnitSuite) TestForceMachine(c *gc.C) { === modified file 'src/github.com/juju/juju/cmd/juju/service/constraints.go' --- src/github.com/juju/juju/cmd/juju/service/constraints.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/service/constraints.go 2015-10-23 18:29:32 +0000 @@ -26,9 +26,9 @@ See Also: juju help constraints - juju service help set-constraints + juju help service set-constraints juju help deploy - juju machine help add + juju help machine add juju help add-unit ` @@ -49,9 +49,9 @@ See Also: juju help constraints - juju service help get-constraints + juju help service get-constraints juju help deploy - juju machine help add + juju help machine add juju help add-unit ` @@ -79,7 +79,7 @@ return fmt.Errorf("invalid service name %q", args[0]) } - c.ServiceName, args = args[0], args[1:] + c.ServiceName = args[0] return nil } === modified file 'src/github.com/juju/juju/cmd/juju/service/unset.go' --- src/github.com/juju/juju/cmd/juju/service/unset.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/service/unset.go 2015-10-23 18:29:32 +0000 @@ -23,7 +23,7 @@ const unsetDoc = ` Set one or more configuration options for the specified service to their -default. See also the set commmand to set one or more configuration options for +default. See also the set command to set one or more configuration options for a specified service. ` === added directory 'src/github.com/juju/juju/cmd/juju/space' === added file 'src/github.com/juju/juju/cmd/juju/space/create.go' --- src/github.com/juju/juju/cmd/juju/space/create.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/space/create.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,69 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package space + +import ( + "fmt" + "strings" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/utils/set" +) + +// CreateCommand calls the API to create a new network space. +type CreateCommand struct { + SpaceCommandBase + Name string + CIDRs set.Strings +} + +const createCommandDoc = ` +Creates a new space with the given name and associates the given +(optional) list of existing subnet CIDRs with it.` + +// Info is defined on the cmd.Command interface. +func (c *CreateCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "create", + Args: " [ ...]", + Purpose: "create a new network space", + Doc: strings.TrimSpace(createCommandDoc), + } +} + +// Init is defined on the cmd.Command interface. It checks the +// arguments for sanity and sets up the command to run. +func (c *CreateCommand) Init(args []string) error { + var err error + c.Name, c.CIDRs, err = ParseNameAndCIDRs(args, true) + return errors.Trace(err) +} + +// Run implements Command.Run. +func (c *CreateCommand) Run(ctx *cmd.Context) error { + return c.RunWithAPI(ctx, func(api SpaceAPI, ctx *cmd.Context) error { + // Prepare a nicer message and proper arguments to use in case + // there are not CIDRs given. + var subnetIds []string + msgSuffix := "no subnets" + if !c.CIDRs.IsEmpty() { + subnetIds = c.CIDRs.SortedValues() + msgSuffix = fmt.Sprintf("subnets %s", strings.Join(subnetIds, ", ")) + } + + // Create the new space. + // TODO(dimitern): Accept --public|--private and pass it here. + err := api.CreateSpace(c.Name, subnetIds, true) + if err != nil { + if errors.IsNotSupported(err) { + ctx.Infof("cannot create space %q: %v", c.Name, err) + } + return errors.Annotatef(err, "cannot create space %q", c.Name) + } + + ctx.Infof("created space %q with %s", c.Name, msgSuffix) + return nil + }) +} === added file 'src/github.com/juju/juju/cmd/juju/space/create_test.go' --- src/github.com/juju/juju/cmd/juju/space/create_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/space/create_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,84 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package space_test + +import ( + "github.com/juju/errors" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/juju/space" +) + +type CreateSuite struct { + BaseSpaceSuite +} + +var _ = gc.Suite(&CreateSuite{}) + +func (s *CreateSuite) SetUpTest(c *gc.C) { + s.BaseSpaceSuite.SetUpTest(c) + s.command = space.NewCreateCommand(s.api) + c.Assert(s.command, gc.NotNil) +} + +func (s *CreateSuite) TestRunWithoutSubnetsSucceeds(c *gc.C) { + s.AssertRunSucceeds(c, + `created space "myspace" with no subnets\n`, + "", // no stdout, just stderr + "myspace", + ) + + s.api.CheckCallNames(c, "CreateSpace", "Close") + s.api.CheckCall(c, 0, "CreateSpace", "myspace", []string(nil), true) +} + +func (s *CreateSuite) TestRunWithSubnetsSucceeds(c *gc.C) { + s.AssertRunSucceeds(c, + `created space "myspace" with subnets 10.1.2.0/24, 4.3.2.0/28\n`, + "", // no stdout, just stderr + "myspace", "10.1.2.0/24", "4.3.2.0/28", + ) + + s.api.CheckCallNames(c, "CreateSpace", "Close") + s.api.CheckCall(c, + 0, "CreateSpace", + "myspace", s.Strings("10.1.2.0/24", "4.3.2.0/28"), true, + ) +} + +func (s *CreateSuite) TestRunWhenSpacesNotSupported(c *gc.C) { + s.api.SetErrors(errors.NewNotSupported(nil, "spaces not supported")) + + err := s.AssertRunSpacesNotSupported(c, + `cannot create space "foo": spaces not supported`, + "foo", "10.1.2.0/24", + ) + c.Assert(err, jc.Satisfies, errors.IsNotSupported) + + s.api.CheckCallNames(c, "CreateSpace", "Close") + s.api.CheckCall(c, 0, "CreateSpace", "foo", s.Strings("10.1.2.0/24"), true) +} + +func (s *CreateSuite) TestRunWhenSpacesAPIFails(c *gc.C) { + s.api.SetErrors(errors.New("API error")) + + s.AssertRunFails(c, + `cannot create space "foo": API error`, + "foo", "10.1.2.0/24", + ) + + s.api.CheckCallNames(c, "CreateSpace", "Close") + s.api.CheckCall(c, 0, "CreateSpace", "foo", s.Strings("10.1.2.0/24"), true) +} + +func (s *CreateSuite) TestRunAPIConnectFails(c *gc.C) { + s.command = space.NewCreateCommand(nil) + s.AssertRunFails(c, + "cannot connect to the API server: no environment specified", + "myspace", "10.20.30.0/24", + ) + // No API calls recoreded. + s.api.CheckCallNames(c) +} === added file 'src/github.com/juju/juju/cmd/juju/space/export_test.go' --- src/github.com/juju/juju/cmd/juju/space/export_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/space/export_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,38 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package space + +func NewCreateCommand(api SpaceAPI) *CreateCommand { + createCmd := &CreateCommand{} + createCmd.api = api + return createCmd +} + +func NewRemoveCommand(api SpaceAPI) *RemoveCommand { + removeCmd := &RemoveCommand{} + removeCmd.api = api + return removeCmd +} + +func NewUpdateCommand(api SpaceAPI) *UpdateCommand { + updateCmd := &UpdateCommand{} + updateCmd.api = api + return updateCmd +} + +func NewRenameCommand(api SpaceAPI) *RenameCommand { + renameCmd := &RenameCommand{} + renameCmd.api = api + return renameCmd +} + +func NewListCommand(api SpaceAPI) *ListCommand { + listCmd := &ListCommand{} + listCmd.api = api + return listCmd +} + +func ListFormat(cmd *ListCommand) string { + return cmd.out.Name() +} === added file 'src/github.com/juju/juju/cmd/juju/space/list.go' --- src/github.com/juju/juju/cmd/juju/space/list.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/space/list.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,185 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package space + +import ( + "encoding/json" + "fmt" + "net" + "strings" + + "launchpad.net/gnuflag" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/juju/apiserver/params" +) + +// ListCommand displays a list of all spaces known to Juju. +type ListCommand struct { + SpaceCommandBase + Short bool + out cmd.Output +} + +const listCommandDoc = ` +Displays all defined spaces. If --short is not given both spaces and +their subnets are displayed, otherwise just a list of spaces. The +--format argument has the same semantics as in other CLI commands - +"yaml" is the default. The --output argument allows the command +output to be redirected to a file. ` + +// Info is defined on the cmd.Command interface. +func (c *ListCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "list", + Args: "[--short] [--format yaml|json] [--output ]", + Purpose: "list spaces known to Juju, including associated subnets", + Doc: strings.TrimSpace(listCommandDoc), + } +} + +// SetFlags is defined on the cmd.Command interface. +func (c *ListCommand) SetFlags(f *gnuflag.FlagSet) { + c.SpaceCommandBase.SetFlags(f) + c.out.AddFlags(f, "yaml", map[string]cmd.Formatter{ + "yaml": cmd.FormatYaml, + "json": cmd.FormatJson, + }) + + f.BoolVar(&c.Short, "short", false, "only display spaces.") +} + +// Init is defined on the cmd.Command interface. It checks the +// arguments for sanity and sets up the command to run. +func (c *ListCommand) Init(args []string) error { + // No arguments are accepted, just flags. + if err := cmd.CheckEmpty(args); err != nil { + return errors.Trace(err) + } + + return nil +} + +// Run implements Command.Run. +func (c *ListCommand) Run(ctx *cmd.Context) error { + return c.RunWithAPI(ctx, func(api SpaceAPI, ctx *cmd.Context) error { + spaces, err := api.ListSpaces() + if err != nil { + if errors.IsNotSupported(err) { + ctx.Infof("cannot list spaces: %v", err) + } + return errors.Annotate(err, "cannot list spaces") + } + if len(spaces) == 0 { + ctx.Infof("no spaces to display") + return c.out.Write(ctx, nil) + } + + if c.Short { + result := formattedShortList{} + for _, space := range spaces { + result.Spaces = append(result.Spaces, space.Name) + } + return c.out.Write(ctx, result) + } + // Construct the output list for displaying with the chosen + // format. + result := formattedList{ + Spaces: make(map[string]map[string]formattedSubnet), + } + + for _, space := range spaces { + result.Spaces[space.Name] = make(map[string]formattedSubnet) + for _, subnet := range space.Subnets { + subResult := formattedSubnet{ + Type: typeUnknown, + ProviderId: subnet.ProviderId, + Zones: subnet.Zones, + } + // Display correct status according to the life cycle value. + // + // TODO(dimitern): Do this on the apiserver side, also + // do the same for params.Space, so in case of an + // error it can be displayed. + switch subnet.Life { + case params.Alive: + subResult.Status = statusInUse + case params.Dying, params.Dead: + subResult.Status = statusTerminating + } + + // Use the CIDR to determine the subnet type. + // TODO(dimitern): Do this on the apiserver side. + if ip, _, err := net.ParseCIDR(subnet.CIDR); err != nil { + // This should never happen as subnets will be + // validated before saving in state. + msg := fmt.Sprintf("error: invalid subnet CIDR: %s", subnet.CIDR) + subResult.Status = msg + } else if ip.To4() != nil { + subResult.Type = typeIPv4 + } else if ip.To16() != nil { + subResult.Type = typeIPv6 + } + result.Spaces[space.Name][subnet.CIDR] = subResult + } + } + return c.out.Write(ctx, result) + }) +} + +const ( + typeUnknown = "unknown" + typeIPv4 = "ipv4" + typeIPv6 = "ipv6" + + statusInUse = "in-use" + statusTerminating = "terminating" +) + +// TODO(dimitern): Display space attributes along with subnets (state +// or error,public,?) + +type formattedList struct { + Spaces map[string]map[string]formattedSubnet `json:"spaces" yaml:"spaces"` +} + +type formattedShortList struct { + Spaces []string `json:"spaces" yaml:"spaces"` +} + +// A goyaml bug means we can't declare these types locally to the +// GetYAML methods. +type formattedListNoMethods formattedList + +// MarshalJSON is defined on json.Marshaller. +func (l formattedList) MarshalJSON() ([]byte, error) { + return json.Marshal(formattedListNoMethods(l)) +} + +// GetYAML is defined on yaml.Getter. +func (l formattedList) GetYAML() (tag string, value interface{}) { + return "", formattedListNoMethods(l) +} + +type formattedSubnet struct { + Type string `json:"type" yaml:"type"` + ProviderId string `json:"provider-id,omitempty" yaml:"provider-id,omitempty"` + Status string `json:"status,omitempty" yaml:"status,omitempty"` + Zones []string `json:"zones" yaml:"zones"` +} + +// A goyaml bug means we can't declare these types locally to the +// GetYAML methods. +type formattedSubnetNoMethods formattedSubnet + +// MarshalJSON is defined on json.Marshaller. +func (s formattedSubnet) MarshalJSON() ([]byte, error) { + return json.Marshal(formattedSubnetNoMethods(s)) +} + +// GetYAML is defined on yaml.Getter. +func (s formattedSubnet) GetYAML() (tag string, value interface{}) { + return "", formattedSubnetNoMethods(s) +} === added file 'src/github.com/juju/juju/cmd/juju/space/list_test.go' --- src/github.com/juju/juju/cmd/juju/space/list_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/space/list_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,292 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package space_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + + "regexp" + + "github.com/juju/errors" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/juju/space" + coretesting "github.com/juju/juju/testing" +) + +type ListSuite struct { + BaseSpaceSuite +} + +var _ = gc.Suite(&ListSuite{}) + +func (s *ListSuite) SetUpTest(c *gc.C) { + s.BaseSpaceSuite.SetUpTest(c) + s.command = space.NewListCommand(s.api) + c.Assert(s.command, gc.NotNil) +} + +func (s *ListSuite) TestInit(c *gc.C) { + for i, test := range []struct { + about string + args []string + expectShort bool + expectFormat string + expectErr string + }{{ + about: "unrecognized arguments", + args: s.Strings("foo"), + expectErr: `unrecognized args: \["foo"\]`, + expectFormat: "yaml", + }, { + about: "invalid format", + args: s.Strings("--format", "foo"), + expectErr: `invalid value "foo" for flag --format: unknown format "foo"`, + expectFormat: "yaml", + }, { + about: "invalid format (value is case-sensitive)", + args: s.Strings("--format", "JSON"), + expectErr: `invalid value "JSON" for flag --format: unknown format "JSON"`, + expectFormat: "yaml", + }, { + about: "json format", + args: s.Strings("--format", "json"), + expectFormat: "json", + }, { + about: "yaml format", + args: s.Strings("--format", "yaml"), + expectFormat: "yaml", + }, { + // --output and -o are tested separately in TestOutputFormats. + about: "both --output and -o specified (latter overrides former)", + args: s.Strings("--output", "foo", "-o", "bar"), + expectFormat: "yaml", + }} { + c.Logf("test #%d: %s", i, test.about) + // Create a new instance of the subcommand for each test, but + // since we're not running the command no need to use + // envcmd.Wrap(). + command := space.NewListCommand(s.api) + err := coretesting.InitCommand(command, test.args) + if test.expectErr != "" { + c.Check(err, gc.ErrorMatches, test.expectErr) + } else { + c.Check(err, jc.ErrorIsNil) + } + c.Check(space.ListFormat(command), gc.Equals, test.expectFormat) + c.Check(command.Short, gc.Equals, test.expectShort) + + // No API calls should be recorded at this stage. + s.api.CheckCallNames(c) + } +} + +func (s *ListSuite) TestOutputFormats(c *gc.C) { + outDir := c.MkDir() + expectedYAML := ` +spaces: + space1: + 2001:db8::/32: + type: ipv6 + provider-id: subnet-public + status: terminating + zones: + - zone2 + invalid: + type: unknown + provider-id: no-such + status: 'error: invalid subnet CIDR: invalid' + zones: + - zone1 + space2: + 4.3.2.0/28: + type: ipv4 + provider-id: vlan-42 + status: terminating + zones: + - zone1 + 10.1.2.0/24: + type: ipv4 + provider-id: subnet-private + status: in-use + zones: + - zone1 + - zone2 +`[1:] + unwrap := regexp.MustCompile(`[\s+\n]`) + expectedJSON := unwrap.ReplaceAllLiteralString(` +{ + "spaces": { + "space1": { + "2001:db8::/32": { + "type": "ipv6", + "provider-id": "subnet-public", + "status": "terminating", + "zones": ["zone2"] + }, + "invalid": { + "type": "unknown", + "provider-id": "no-such", + "status": "error: invalid subnet CIDR: invalid", + "zones": ["zone1"] + } + }, + "space2": { + "10.1.2.0/24": { + "type": "ipv4", + "provider-id": "subnet-private", + "status": "in-use", + "zones": ["zone1","zone2"] + }, + "4.3.2.0/28": { + "type": "ipv4", + "provider-id": "vlan-42", + "status": "terminating", + "zones": ["zone1"] + } + } + } +} +`, "") + "\n" + // Work around the big unwrap hammer above. + expectedJSON = strings.Replace( + expectedJSON, + "error:invalidsubnetCIDR:invalid", + "error: invalid subnet CIDR: invalid", + 1, + ) + expectedShortYAML := ` +spaces: +- space1 +- space2 +`[1:] + + expectedShortJSON := unwrap.ReplaceAllLiteralString(` +{ + "spaces": [ + "space1", + "space2" + ] +} +`, "") + "\n" + + assertAPICalls := func() { + // Verify the API calls and reset the recorded calls. + s.api.CheckCallNames(c, "ListSpaces", "Close") + s.api.ResetCalls() + } + makeArgs := func(format string, short bool, extraArgs ...string) []string { + args := s.Strings(extraArgs...) + if format != "" { + args = append(args, "--format", format) + } + if short == true { + args = append(args, "--short") + } + return args + } + assertOutput := func(format, expected string, short bool) { + outFile := filepath.Join(outDir, "output") + c.Assert(outFile, jc.DoesNotExist) + defer os.Remove(outFile) + // Check -o works. + var args []string + args = makeArgs(format, short, "-o", outFile) + s.AssertRunSucceeds(c, "", "", args...) + assertAPICalls() + + data, err := ioutil.ReadFile(outFile) + c.Assert(err, jc.ErrorIsNil) + c.Assert(string(data), gc.Equals, expected) + + // Check the last output argument takes precedence when both + // -o and --output are given (and also that --output works the + // same as -o). + outFile1 := filepath.Join(outDir, "output1") + c.Assert(outFile1, jc.DoesNotExist) + defer os.Remove(outFile1) + outFile2 := filepath.Join(outDir, "output2") + c.Assert(outFile2, jc.DoesNotExist) + defer os.Remove(outFile2) + // Write something in outFile2 to verify its contents are + // overwritten. + err = ioutil.WriteFile(outFile2, []byte("some contents"), 0644) + c.Assert(err, jc.ErrorIsNil) + + args = makeArgs(format, short, "-o", outFile1, "--output", outFile2) + s.AssertRunSucceeds(c, "", "", args...) + // Check only the last output file was used, and the output + // file was overwritten. + c.Assert(outFile1, jc.DoesNotExist) + data, err = ioutil.ReadFile(outFile2) + c.Assert(err, jc.ErrorIsNil) + c.Assert(string(data), gc.Equals, expected) + assertAPICalls() + + // Finally, check without --output. + args = makeArgs(format, short) + s.AssertRunSucceeds(c, "", expected, args...) + assertAPICalls() + } + + for i, test := range []struct { + format string + expected string + short bool + }{ + {"", expectedYAML, false}, // default format is YAML + {"yaml", expectedYAML, false}, + {"json", expectedJSON, false}, + {"", expectedShortYAML, true}, // default format is YAML + {"yaml", expectedShortYAML, true}, + {"json", expectedShortJSON, true}, + } { + c.Logf("test #%d: format %q, short %v", i, test.format, test.short) + assertOutput(test.format, test.expected, test.short) + } +} + +func (s *ListSuite) TestRunWhenNoSpacesExistSucceeds(c *gc.C) { + s.api.Spaces = s.api.Spaces[0:0] + + s.AssertRunSucceeds(c, + `no spaces to display\n`, + "", // empty stdout. + ) + + s.api.CheckCallNames(c, "ListSpaces", "Close") + s.api.CheckCall(c, 0, "ListSpaces") +} + +func (s *ListSuite) TestRunWhenSpacesNotSupported(c *gc.C) { + s.api.SetErrors(errors.NewNotSupported(nil, "spaces not supported")) + + err := s.AssertRunSpacesNotSupported(c, "cannot list spaces: spaces not supported") + c.Assert(err, jc.Satisfies, errors.IsNotSupported) + + s.api.CheckCallNames(c, "ListSpaces", "Close") + s.api.CheckCall(c, 0, "ListSpaces") +} + +func (s *ListSuite) TestRunWhenSpacesAPIFails(c *gc.C) { + s.api.SetErrors(errors.New("boom")) + + s.AssertRunFails(c, "cannot list spaces: boom") + + s.api.CheckCallNames(c, "ListSpaces", "Close") + s.api.CheckCall(c, 0, "ListSpaces") +} + +func (s *ListSuite) TestRunAPIConnectFails(c *gc.C) { + s.command = space.NewListCommand(nil) + s.AssertRunFails(c, + "cannot connect to the API server: no environment specified", + ) + // No API calls recoreded. + s.api.CheckCallNames(c) +} === added file 'src/github.com/juju/juju/cmd/juju/space/package_test.go' --- src/github.com/juju/juju/cmd/juju/space/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/space/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,250 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package space_test + +import ( + "net" + "regexp" + stdtesting "testing" + + "github.com/juju/cmd" + "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + "github.com/juju/utils/featureflag" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/juju/space" + "github.com/juju/juju/feature" + coretesting "github.com/juju/juju/testing" +) + +func TestPackage(t *stdtesting.T) { + gc.TestingT(t) +} + +// BaseSpaceSuite is used for embedding in other suites. +type BaseSpaceSuite struct { + coretesting.FakeJujuHomeSuite + coretesting.BaseSuite + + superCmd cmd.Command + command cmd.Command + api *StubAPI +} + +var _ = gc.Suite(&BaseSpaceSuite{}) + +func (s *BaseSpaceSuite) SetUpTest(c *gc.C) { + // If any post-MVP command suite enabled the flag, keep it. + hasFeatureFlag := featureflag.Enabled(feature.PostNetCLIMVP) + + s.BaseSuite.SetUpTest(c) + s.FakeJujuHomeSuite.SetUpTest(c) + + if hasFeatureFlag { + s.BaseSuite.SetFeatureFlags(feature.PostNetCLIMVP) + } + + s.superCmd = space.NewSuperCommand() + c.Assert(s.superCmd, gc.NotNil) + + s.api = NewStubAPI() + c.Assert(s.api, gc.NotNil) + + // All subcommand suites embedding this one should initialize + // s.command immediately after calling this method! +} + +// RunSuperCommand executes the super command passing any args and +// returning the stdout and stderr output as strings, as well as any +// error. If s.command is set, the subcommand's name will be passed as +// first argument. +func (s *BaseSpaceSuite) RunSuperCommand(c *gc.C, args ...string) (string, string, error) { + if s.command != nil { + args = append([]string{s.command.Info().Name}, args...) + } + ctx, err := coretesting.RunCommand(c, s.superCmd, args...) + if ctx != nil { + return coretesting.Stdout(ctx), coretesting.Stderr(ctx), err + } + return "", "", err +} + +// RunSubCommand executes the s.command subcommand passing any args +// and returning the stdout and stderr output as strings, as well as +// any error. +func (s *BaseSpaceSuite) RunSubCommand(c *gc.C, args ...string) (string, string, error) { + if s.command == nil { + panic("subcommand is nil") + } + ctx, err := coretesting.RunCommand(c, s.command, args...) + if ctx != nil { + return coretesting.Stdout(ctx), coretesting.Stderr(ctx), err + } + return "", "", err +} + +// AssertRunSpacesNotSupported is a shortcut for calling RunSubCommand with the +// passed args then asserting the output is empty and the error is the +// spaces not supported, finally returning the error. +func (s *BaseSpaceSuite) AssertRunSpacesNotSupported(c *gc.C, expectErr string, args ...string) error { + stdout, stderr, err := s.RunSubCommand(c, args...) + c.Assert(err, gc.ErrorMatches, expectErr) + c.Assert(stdout, gc.Equals, "") + c.Assert(stderr, gc.Equals, expectErr+"\n") + return err +} + +// AssertRunFails is a shortcut for calling RunSubCommand with the +// passed args then asserting the output is empty and the error is as +// expected, finally returning the error. +func (s *BaseSpaceSuite) AssertRunFails(c *gc.C, expectErr string, args ...string) error { + stdout, stderr, err := s.RunSubCommand(c, args...) + c.Assert(err, gc.ErrorMatches, expectErr) + c.Assert(stdout, gc.Equals, "") + c.Assert(stderr, gc.Equals, "") + return err +} + +// AssertRunSucceeds is a shortcut for calling RunSuperCommand with +// the passed args then asserting the stderr output matches +// expectStderr, stdout is equal to expectStdout, and the error is +// nil. +func (s *BaseSpaceSuite) AssertRunSucceeds(c *gc.C, expectStderr, expectStdout string, args ...string) { + stdout, stderr, err := s.RunSubCommand(c, args...) + c.Assert(err, jc.ErrorIsNil) + c.Assert(stdout, gc.Equals, expectStdout) + c.Assert(stderr, gc.Matches, expectStderr) +} + +// TestHelp runs the command with --help as argument and verifies the +// output. +func (s *BaseSpaceSuite) TestHelp(c *gc.C) { + stderr, stdout, err := s.RunSuperCommand(c, "--help") + c.Assert(err, jc.ErrorIsNil) + c.Check(stdout, gc.Equals, "") + c.Check(stderr, gc.Not(gc.Equals), "") + + // If s.command is set, use it instead of s.superCmd. + cmdInfo := s.superCmd.Info() + var expected string + if s.command != nil { + // Subcommands embed EnvCommandBase + cmdInfo = s.command.Info() + expected = "(?sm).*^usage: juju space " + + regexp.QuoteMeta(cmdInfo.Name) + + `( \[options\])? ` + regexp.QuoteMeta(cmdInfo.Args) + ".+" + } else { + expected = "(?sm).*^usage: juju space" + + `( \[options\])? ` + regexp.QuoteMeta(cmdInfo.Args) + ".+" + } + c.Check(cmdInfo, gc.NotNil) + c.Check(stderr, gc.Matches, expected) + + expected = "(?sm).*^purpose: " + regexp.QuoteMeta(cmdInfo.Purpose) + "$.*" + c.Check(stderr, gc.Matches, expected) + + expected = "(?sm).*^" + regexp.QuoteMeta(cmdInfo.Doc) + "$.*" + c.Check(stderr, gc.Matches, expected) +} + +// Strings is makes tests taking a slice of strings slightly easier to +// write: e.g. s.Strings("foo", "bar") vs. []string{"foo", "bar"}. +func (s *BaseSpaceSuite) Strings(values ...string) []string { + return values +} + +// StubAPI defines a testing stub for the SpaceAPI interface. +type StubAPI struct { + *testing.Stub + + Spaces []params.Space + Subnets []params.Subnet +} + +var _ space.SpaceAPI = (*StubAPI)(nil) + +// NewStubAPI creates a StubAPI suitable for passing to +// space.New*Command(). +func NewStubAPI() *StubAPI { + subnets := []params.Subnet{{ + // IPv6 subnet. + CIDR: "2001:db8::/32", + ProviderId: "subnet-public", + Life: params.Dying, + SpaceTag: "space-space1", + Zones: []string{"zone2"}, + }, { + // Invalid subnet (just for 100% coverage, otherwise it can't happen). + CIDR: "invalid", + ProviderId: "no-such", + SpaceTag: "space-space1", + Zones: []string{"zone1"}, + }, { + // IPv4 subnet. + CIDR: "10.1.2.0/24", + ProviderId: "subnet-private", + Life: params.Alive, + SpaceTag: "space-space2", + Zones: []string{"zone1", "zone2"}, + StaticRangeLowIP: net.ParseIP("10.1.2.10"), + StaticRangeHighIP: net.ParseIP("10.1.2.200"), + }, { + // IPv4 VLAN subnet. + CIDR: "4.3.2.0/28", + Life: params.Dead, + ProviderId: "vlan-42", + SpaceTag: "space-space2", + Zones: []string{"zone1"}, + VLANTag: 42, + StaticRangeLowIP: net.ParseIP("4.3.2.2"), + StaticRangeHighIP: net.ParseIP("4.3.2.4"), + }} + spaces := []params.Space{{ + Name: "space1", + Subnets: append([]params.Subnet{}, subnets[:2]...), + }, { + Name: "space2", + Subnets: append([]params.Subnet{}, subnets[2:]...), + }} + return &StubAPI{ + Stub: &testing.Stub{}, + Spaces: spaces, + Subnets: subnets, + } +} + +func (sa *StubAPI) Close() error { + sa.MethodCall(sa, "Close") + return sa.NextErr() +} + +func (sa *StubAPI) ListSpaces() ([]params.Space, error) { + sa.MethodCall(sa, "ListSpaces") + if err := sa.NextErr(); err != nil { + return nil, err + } + return sa.Spaces, nil +} + +func (sa *StubAPI) CreateSpace(name string, subnetIds []string, public bool) error { + sa.MethodCall(sa, "CreateSpace", name, subnetIds, public) + return sa.NextErr() +} + +func (sa *StubAPI) RemoveSpace(name string) error { + sa.MethodCall(sa, "RemoveSpace", name) + return sa.NextErr() +} + +func (sa *StubAPI) UpdateSpace(name string, subnetIds []string) error { + sa.MethodCall(sa, "UpdateSpace", name, subnetIds) + return sa.NextErr() +} + +func (sa *StubAPI) RenameSpace(name, newName string) error { + sa.MethodCall(sa, "RenameSpace", name, newName) + return sa.NextErr() +} === added file 'src/github.com/juju/juju/cmd/juju/space/remove.go' --- src/github.com/juju/juju/cmd/juju/space/remove.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/space/remove.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,64 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package space + +import ( + "strings" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/names" +) + +// RemoveCommand calls the API to remove an existing network space. +type RemoveCommand struct { + SpaceCommandBase + Name string +} + +const removeCommandDoc = ` +Removes an existing Juju network space with the given name. Any subnets +associated with the space will be transfered to the default space. +` + +// Info is defined on the cmd.Command interface. +func (c *RemoveCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "remove", + Args: "", + Purpose: "remove a network space", + Doc: strings.TrimSpace(removeCommandDoc), + } +} + +// Init is defined on the cmd.Command interface. It checks the +// arguments for sanity and sets up the command to run. +func (c *RemoveCommand) Init(args []string) (err error) { + defer errors.DeferredAnnotatef(&err, "invalid arguments specified") + + // Validate given name. + if len(args) == 0 { + return errors.New("space name is required") + } + givenName := args[0] + if !names.IsValidSpace(givenName) { + return errors.Errorf("%q is not a valid space name", givenName) + } + c.Name = givenName + + return cmd.CheckEmpty(args[1:]) +} + +// Run implements Command.Run. +func (c *RemoveCommand) Run(ctx *cmd.Context) error { + return c.RunWithAPI(ctx, func(api SpaceAPI, ctx *cmd.Context) error { + // Remove the space. + err := api.RemoveSpace(c.Name) + if err != nil { + return errors.Annotatef(err, "cannot remove space %q", c.Name) + } + ctx.Infof("removed space %q", c.Name) + return nil + }) +} === added file 'src/github.com/juju/juju/cmd/juju/space/remove_test.go' --- src/github.com/juju/juju/cmd/juju/space/remove_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/space/remove_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,101 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package space_test + +import ( + "github.com/juju/errors" + "github.com/juju/juju/feature" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/juju/space" + coretesting "github.com/juju/juju/testing" +) + +type RemoveSuite struct { + BaseSpaceSuite +} + +var _ = gc.Suite(&RemoveSuite{}) + +func (s *RemoveSuite) SetUpTest(c *gc.C) { + s.BaseSuite.SetFeatureFlags(feature.PostNetCLIMVP) + s.BaseSpaceSuite.SetUpTest(c) + s.command = space.NewRemoveCommand(s.api) + c.Assert(s.command, gc.NotNil) +} + +func (s *RemoveSuite) TestInit(c *gc.C) { + for i, test := range []struct { + about string + args []string + expectName string + expectErr string + }{{ + about: "no arguments", + expectErr: "space name is required", + }, { + about: "invalid space name", + args: s.Strings("%inv$alid", "new-name"), + expectErr: `"%inv\$alid" is not a valid space name`, + }, { + about: "multiple space names aren't allowed", + args: s.Strings("a-space", "another-space"), + expectErr: `unrecognized args: \["another-space"\]`, + expectName: "a-space", + }, { + about: "delete a valid space name", + args: s.Strings("myspace"), + expectName: "myspace", + }} { + c.Logf("test #%d: %s", i, test.about) + // Create a new instance of the subcommand for each test, but + // since we're not running the command no need to use + // envcmd.Wrap(). + command := space.NewRemoveCommand(s.api) + err := coretesting.InitCommand(command, test.args) + if test.expectErr != "" { + prefixedErr := "invalid arguments specified: " + test.expectErr + c.Check(err, gc.ErrorMatches, prefixedErr) + } else { + c.Check(err, jc.ErrorIsNil) + } + c.Check(command.Name, gc.Equals, test.expectName) + // No API calls should be recorded at this stage. + s.api.CheckCallNames(c) + } +} + +func (s *RemoveSuite) TestRunWithValidSpaceSucceeds(c *gc.C) { + s.AssertRunSucceeds(c, + `removed space "myspace"\n`, + "", // no stdout, just stderr + "myspace", + ) + + s.api.CheckCallNames(c, "RemoveSpace", "Close") + s.api.CheckCall(c, 0, "RemoveSpace", "myspace") +} + +func (s *RemoveSuite) TestRunWhenSpacesAPIFails(c *gc.C) { + s.api.SetErrors(errors.New("boom")) + + s.AssertRunFails(c, + `cannot remove space "myspace": boom`, + "myspace", + ) + + s.api.CheckCallNames(c, "RemoveSpace", "Close") + s.api.CheckCall(c, 0, "RemoveSpace", "myspace") +} + +func (s *RemoveSuite) TestRunAPIConnectFails(c *gc.C) { + s.command = space.NewRemoveCommand(nil) + s.AssertRunFails(c, + "cannot connect to the API server: no environment specified", + "myname", // Drop the args once RunWitnAPI is called internally. + ) + // No API calls recoreded. + s.api.CheckCallNames(c) +} === added file 'src/github.com/juju/juju/cmd/juju/space/rename.go' --- src/github.com/juju/juju/cmd/juju/space/rename.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/space/rename.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,79 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package space + +import ( + "strings" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/names" + "launchpad.net/gnuflag" +) + +// RenameCommand calls the API to rename an existing network space. +type RenameCommand struct { + SpaceCommandBase + Name string + NewName string +} + +const RenameCommandDoc = ` +Renames an existing space from "old-name" to "new-name". Does not change the +associated subnets and "new-name" must not match another existing space. +` + +func (c *RenameCommand) SetFlags(f *gnuflag.FlagSet) { + c.SpaceCommandBase.SetFlags(f) + f.StringVar(&c.NewName, "rename", "", "the new name for the network space") +} + +// Info is defined on the cmd.Command interface. +func (c *RenameCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "rename", + Args: " ", + Purpose: "rename a network space", + Doc: strings.TrimSpace(RenameCommandDoc), + } +} + +// Init is defined on the cmd.Command interface. It checks the +// arguments for sanity and sets up the command to run. +func (c *RenameCommand) Init(args []string) (err error) { + defer errors.DeferredAnnotatef(&err, "invalid arguments specified") + + switch len(args) { + case 0: + return errors.New("old-name is required") + case 1: + return errors.New("new-name is required") + } + for _, name := range args { + if !names.IsValidSpace(name) { + return errors.Errorf("%q is not a valid space name", name) + } + } + c.Name = args[0] + c.NewName = args[1] + + if c.Name == c.NewName { + return errors.New("old-name and new-name are the same") + } + + return cmd.CheckEmpty(args[2:]) +} + +// Run implements Command.Run. +func (c *RenameCommand) Run(ctx *cmd.Context) error { + return c.RunWithAPI(ctx, func(api SpaceAPI, ctx *cmd.Context) error { + err := api.RenameSpace(c.Name, c.NewName) + if err != nil { + return errors.Annotatef(err, "cannot rename space %q", c.Name) + } + + ctx.Infof("renamed space %q to %q", c.Name, c.NewName) + return nil + }) +} === added file 'src/github.com/juju/juju/cmd/juju/space/rename_test.go' --- src/github.com/juju/juju/cmd/juju/space/rename_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/space/rename_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,127 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package space_test + +import ( + "github.com/juju/errors" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/juju/space" + "github.com/juju/juju/feature" + coretesting "github.com/juju/juju/testing" +) + +type RenameSuite struct { + BaseSpaceSuite +} + +var _ = gc.Suite(&RenameSuite{}) + +func (s *RenameSuite) SetUpTest(c *gc.C) { + s.BaseSuite.SetFeatureFlags(feature.PostNetCLIMVP) + s.BaseSpaceSuite.SetUpTest(c) + s.command = space.NewRenameCommand(s.api) + c.Assert(s.command, gc.NotNil) +} + +func (s *RenameSuite) TestInit(c *gc.C) { + for i, test := range []struct { + about string + args []string + expectName string + expectNewName string + expectErr string + }{{ + about: "no arguments", + expectErr: "old-name is required", + }, { + about: "no new name", + args: s.Strings("a-space"), + expectErr: "new-name is required", + }, { + about: "invalid space name - with invalid characters", + args: s.Strings("%inv$alid", "new-name"), + expectErr: `"%inv\$alid" is not a valid space name`, + }, { + about: "invalid space name - using underscores", + args: s.Strings("42_space", "new-name"), + expectErr: `"42_space" is not a valid space name`, + }, { + about: "valid space name with invalid new name", + args: s.Strings("a-space", "inv#alid"), + expectErr: `"inv#alid" is not a valid space name`, + }, { + about: "valid space name with CIDR as new name", + args: s.Strings("a-space", "1.2.3.4/24"), + expectErr: `"1.2.3.4/24" is not a valid space name`, + }, { + about: "more than two arguments", + args: s.Strings("a-space", "another-space", "rubbish"), + expectErr: `unrecognized args: \["rubbish"\]`, + expectName: "a-space", + expectNewName: "another-space", + }, { + about: "old and new names are the same", + args: s.Strings("a-space", "a-space"), + expectName: "a-space", + expectNewName: "a-space", + expectErr: "old-name and new-name are the same", + }, { + about: "all ok", + args: s.Strings("a-space", "another-space"), + expectName: "a-space", + expectNewName: "another-space", + }} { + c.Logf("test #%d: %s", i, test.about) + // Create a new instance of the subcommand for each test, but + // since we're not running the command no need to use + // envcmd.Wrap(). + command := space.NewRenameCommand(s.api) // surely can use s.command?? + err := coretesting.InitCommand(command, test.args) + if test.expectErr != "" { + prefixedErr := "invalid arguments specified: " + test.expectErr + c.Check(err, gc.ErrorMatches, prefixedErr) + } else { + c.Check(err, jc.ErrorIsNil) + } + c.Check(command.Name, gc.Equals, test.expectName) + c.Check(command.NewName, gc.Equals, test.expectNewName) + // No API calls should be recorded at this stage. + s.api.CheckCallNames(c) + } +} + +func (s *RenameSuite) TestRunWithValidNamesSucceeds(c *gc.C) { + s.AssertRunSucceeds(c, + `renamed space "a-space" to "another-space"\n`, + "", // no stdout, just stderr + "a-space", "another-space", + ) + + s.api.CheckCallNames(c, "RenameSpace", "Close") + s.api.CheckCall(c, 0, "RenameSpace", "a-space", "another-space") +} + +func (s *RenameSuite) TestRunWhenSpacesAPIFails(c *gc.C) { + s.api.SetErrors(errors.New("boom")) + + s.AssertRunFails(c, + `cannot rename space "foo": boom`, + "foo", "bar", + ) + + s.api.CheckCallNames(c, "RenameSpace", "Close") + s.api.CheckCall(c, 0, "RenameSpace", "foo", "bar") +} + +func (s *RenameSuite) TestRunAPIConnectFails(c *gc.C) { + s.command = space.NewRenameCommand(nil) + s.AssertRunFails(c, + "cannot connect to the API server: no environment specified", + "myname", "newname", // Drop the args once RunWitnAPI is called internally. + ) + // No API calls recoreded. + s.api.CheckCallNames(c) +} === added file 'src/github.com/juju/juju/cmd/juju/space/space.go' --- src/github.com/juju/juju/cmd/juju/space/space.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/space/space.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,231 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package space + +import ( + "io" + "net" + "strings" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/loggo" + "github.com/juju/names" + "github.com/juju/utils/featureflag" + "github.com/juju/utils/set" + + "github.com/juju/juju/api" + "github.com/juju/juju/api/spaces" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/feature" +) + +// SpaceAPI defines the necessary API methods needed by the space +// subcommands. +type SpaceAPI interface { + io.Closer + + // ListSpaces returns all Juju network spaces and their subnets. + ListSpaces() ([]params.Space, error) + + // CreateSpace creates a new Juju network space, associating the + // specified subnets with it (optional; can be empty), setting the + // space and subnets access to public or private. + CreateSpace(name string, subnetIds []string, public bool) error + + // TODO(dimitern): All of the following api methods should take + // names.SpaceTag instead of name, the only exceptions are + // CreateSpace, and RenameSpace as the named space doesn't exist + // yet. + + // RemoveSpace removes an existing Juju network space, transferring + // any associated subnets to the default space. + RemoveSpace(name string) error + + // UpdateSpace changes the associated subnets for an existing space with + // the given name. The list of subnets must contain at least one entry. + UpdateSpace(name string, subnetIds []string) error + + // RenameSpace changes the name of the space. + RenameSpace(name, newName string) error +} + +var logger = loggo.GetLogger("juju.cmd.juju.space") + +const commandDoc = ` +"juju space" provides commands to manage Juju network spaces. + +A space is a security subdivision of a network. + +In practice, a space is a collection of related subnets that have no +firewalls between each other, and that have the same ingress and +egress policies. Common examples in company networks are “the dmz” or +“the pci compliant space”. The name of the space suggests that it is a +logical network area which has some specific security characteristics +- hence the “common ingress and egress policy” definition. + +All of the addresses in all the subnets in a given space are assumed +to be equally able to connect to one another, and all of them are +assumed to go through the same firewalls (or through the same firewall +rules) for connections into or out of the space. For allocation +purposes, then, putting a service on any address in a space is equally +secure - all the addresses in the space have the same firewall rules +applied to them. + +Users create spaces to describe relevant areas of their network (i.e. +DMZ, internal, etc.). + +Spaces can be specified via constraints when deploying a service +and/or at add-relation time. Since all subnets in a space are +considered equal, placement of services in a space means placement on +any of the subnets in that space. A machine bound to a space could be +on any one of the subnets, and routable to any other machine in the +space because any subnet in the space can access any other in the same +space. + +Initially, there is one space (named "default") which always exists +and "contains" all subnets not associated with another space. However, +since the spaces are defined on the cloud substrate (e.g. using tags +in EC2), there could be pre-existing spaces that get discovered after +bootstrapping a new environment using shared credentials (multiple +users or roles, same substrate). ` + +// NewSuperCommand creates the "space" supercommand and registers the +// subcommands that it supports. +func NewSuperCommand() cmd.Command { + spaceCmd := cmd.NewSuperCommand(cmd.SuperCommandParams{ + Name: "space", + Doc: strings.TrimSpace(commandDoc), + UsagePrefix: "juju", + Purpose: "manage network spaces", + }) + spaceCmd.Register(envcmd.Wrap(&CreateCommand{})) + spaceCmd.Register(envcmd.Wrap(&ListCommand{})) + if featureflag.Enabled(feature.PostNetCLIMVP) { + // The following commands are not part of the MVP. + spaceCmd.Register(envcmd.Wrap(&RemoveCommand{})) + spaceCmd.Register(envcmd.Wrap(&UpdateCommand{})) + spaceCmd.Register(envcmd.Wrap(&RenameCommand{})) + } + + return spaceCmd +} + +// SpaceCommandBase is the base type embedded into all space +// subcommands. +type SpaceCommandBase struct { + envcmd.EnvCommandBase + api SpaceAPI +} + +// ParseNameAndCIDRs verifies the input args and returns any errors, +// like missing/invalid name or CIDRs (validated when given, but it's +// an error for CIDRs to be empty if cidrsOptional is false). +func ParseNameAndCIDRs(args []string, cidrsOptional bool) ( + name string, CIDRs set.Strings, err error, +) { + defer errors.DeferredAnnotatef(&err, "invalid arguments specified") + + if len(args) == 0 { + return "", nil, errors.New("space name is required") + } + name, err = CheckName(args[0]) + if err != nil { + return name, nil, errors.Trace(err) + } + + CIDRs, err = CheckCIDRs(args[1:], cidrsOptional) + return name, CIDRs, err +} + +// CheckName checks whether name is a valid space name. +func CheckName(name string) (string, error) { + // Validate given name. + if !names.IsValidSpace(name) { + return "", errors.Errorf("%q is not a valid space name", name) + } + return name, nil +} + +// CheckCIDRs parses the list of strings as CIDRs, checking for +// correct formatting, no duplication and no overlaps. Returns error +// if no CIDRs are provided, unless cidrsOptional is true. +func CheckCIDRs(args []string, cidrsOptional bool) (set.Strings, error) { + // Validate any given CIDRs. + CIDRs := set.NewStrings() + for _, arg := range args { + _, ipNet, err := net.ParseCIDR(arg) + if err != nil { + logger.Debugf("cannot parse %q: %v", arg, err) + return CIDRs, errors.Errorf("%q is not a valid CIDR", arg) + } + cidr := ipNet.String() + if CIDRs.Contains(cidr) { + if cidr == arg { + return CIDRs, errors.Errorf("duplicate subnet %q specified", cidr) + } + return CIDRs, errors.Errorf("subnet %q overlaps with %q", arg, cidr) + } + CIDRs.Add(cidr) + } + + if CIDRs.IsEmpty() && !cidrsOptional { + return CIDRs, errors.New("CIDRs required but not provided") + } + + return CIDRs, nil +} + +// mvpAPIShim forwards SpaceAPI methods to the real API facade for +// implemented methods only. Tested with a feature test only. +type mvpAPIShim struct { + SpaceAPI + + apiState api.Connection + facade *spaces.API +} + +func (m *mvpAPIShim) Close() error { + return m.apiState.Close() +} + +func (m *mvpAPIShim) CreateSpace(name string, subnetIds []string, public bool) error { + return m.facade.CreateSpace(name, subnetIds, public) +} + +func (m *mvpAPIShim) ListSpaces() ([]params.Space, error) { + return m.facade.ListSpaces() +} + +// NewAPI returns a SpaceAPI for the root api endpoint that the +// environment command returns. +func (c *SpaceCommandBase) NewAPI() (SpaceAPI, error) { + if c.api != nil { + // Already created. + return c.api, nil + } + root, err := c.NewAPIRoot() + if err != nil { + return nil, errors.Trace(err) + } + + // This is tested with a feature test. + shim := &mvpAPIShim{ + apiState: root, + facade: spaces.NewAPI(root), + } + return shim, nil +} + +type RunOnAPI func(api SpaceAPI, ctx *cmd.Context) error + +func (c *SpaceCommandBase) RunWithAPI(ctx *cmd.Context, toRun RunOnAPI) error { + api, err := c.NewAPI() + if err != nil { + return errors.Annotate(err, "cannot connect to the API server") + } + defer api.Close() + return toRun(api, ctx) +} === added file 'src/github.com/juju/juju/cmd/juju/space/space_test.go' --- src/github.com/juju/juju/cmd/juju/space/space_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/space/space_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,136 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package space_test + +import ( + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/juju/space" + "github.com/juju/juju/feature" + coretesting "github.com/juju/juju/testing" +) + +var mvpSubcommandNames = []string{ + "create", + "list", + "help", +} + +var postMVPSubcommandNames = []string{ + "remove", + "update", + "rename", +} + +type SpaceCommandSuite struct { + BaseSpaceSuite +} + +var _ = gc.Suite(&SpaceCommandSuite{}) + +func (s *SpaceCommandSuite) TestHelpSubcommandsMVP(c *gc.C) { + s.BaseSuite.SetFeatureFlags() + s.BaseSpaceSuite.SetUpTest(c) // looks evil, but works fine + + ctx, err := coretesting.RunCommand(c, s.superCmd, "--help") + c.Assert(err, jc.ErrorIsNil) + + namesFound := coretesting.ExtractCommandsFromHelpOutput(ctx) + c.Assert(namesFound, jc.SameContents, mvpSubcommandNames) +} + +func (s *SpaceCommandSuite) TestHelpSubcommandsPostMVP(c *gc.C) { + s.BaseSuite.SetFeatureFlags(feature.PostNetCLIMVP) + s.BaseSpaceSuite.SetUpTest(c) // looks evil, but works fine + + ctx, err := coretesting.RunCommand(c, s.superCmd, "--help") + c.Assert(err, jc.ErrorIsNil) + + namesFound := coretesting.ExtractCommandsFromHelpOutput(ctx) + allSubcommandNames := append(mvpSubcommandNames, postMVPSubcommandNames...) + c.Assert(namesFound, jc.SameContents, allSubcommandNames) +} + +func (s *SpaceCommandSuite) TestInit(c *gc.C) { + for i, test := range []struct { + about string + args []string + cidrsOptional bool + + expectName string + expectCIDRs []string + expectErr string + }{{ + about: "no arguments", + expectErr: "space name is required", + }, { + about: "invalid space name - with invalid characters", + args: s.Strings("%inv#alid"), + expectErr: `"%inv#alid" is not a valid space name`, + }, { + about: "valid space name with invalid CIDR", + args: s.Strings("space-name", "noCIDR"), + expectName: "space-name", + expectErr: `"noCIDR" is not a valid CIDR`, + }, { + about: "valid space with one valid and one invalid CIDR (CIDRs required)", + args: s.Strings("space-name", "10.1.0.0/16", "nonsense"), + cidrsOptional: false, + expectName: "space-name", + expectCIDRs: s.Strings("10.1.0.0/16"), + expectErr: `"nonsense" is not a valid CIDR`, + }, { + about: "valid space with one valid and one invalid CIDR (CIDRs optional)", + args: s.Strings("space-name", "10.1.0.0/16", "nonsense"), + expectName: "space-name", + cidrsOptional: true, + expectCIDRs: s.Strings("10.1.0.0/16"), + expectErr: `"nonsense" is not a valid CIDR`, + }, { + about: "valid space with valid but overlapping CIDRs", + args: s.Strings("space-name", "10.1.0.0/16", "10.1.0.1/16"), + expectName: "space-name", + expectCIDRs: s.Strings("10.1.0.0/16"), + expectErr: `subnet "10.1.0.1/16" overlaps with "10.1.0.0/16"`, + }, { + about: "valid space with valid but duplicated CIDRs", + args: s.Strings("space-name", "10.10.0.0/24", "10.10.0.0/24"), + expectName: "space-name", + expectCIDRs: s.Strings("10.10.0.0/24"), + expectErr: `duplicate subnet "10.10.0.0/24" specified`, + }, { + about: "valid space name with no other arguments (CIDRs required)", + args: s.Strings("space-name"), + cidrsOptional: false, + expectName: "space-name", + expectErr: "CIDRs required but not provided", + expectCIDRs: s.Strings(), + }, { + about: "valid space name with no other arguments (CIDRs optional)", + args: s.Strings("space-name"), + cidrsOptional: true, + expectName: "space-name", + expectCIDRs: s.Strings(), + }, { + about: "all ok - CIDRs updated", + args: s.Strings("space-name", "10.10.0.0/24", "2001:db8::1/32"), + expectName: "space-name", + expectCIDRs: s.Strings("10.10.0.0/24", "2001:db8::/32"), + }} { + c.Logf("test #%d: %s", i, test.about) + // Create a new instance of the subcommand for each test, but + // since we're not running the command no need to use + // envcmd.Wrap(). + name, CIDRs, err := space.ParseNameAndCIDRs(test.args, test.cidrsOptional) + if test.expectErr != "" { + prefixedErr := "invalid arguments specified: " + test.expectErr + c.Check(err, gc.ErrorMatches, prefixedErr) + } else { + c.Check(err, jc.ErrorIsNil) + } + c.Check(name, gc.Equals, test.expectName) + c.Check(CIDRs.SortedValues(), jc.DeepEquals, test.expectCIDRs) + } +} === added file 'src/github.com/juju/juju/cmd/juju/space/update.go' --- src/github.com/juju/juju/cmd/juju/space/update.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/space/update.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,62 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package space + +import ( + "strings" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/utils/set" + "launchpad.net/gnuflag" +) + +// UpdateCommand calls the API to update an existing network space. +type UpdateCommand struct { + SpaceCommandBase + Name string + CIDRs set.Strings +} + +const updateCommandDoc = ` +Replaces the list of associated subnets of the space. Since subnets +can only be part of a single space, all specified subnets (using their +CIDRs) "leave" their current space and "enter" the one we're updating. +` + +func (c *UpdateCommand) SetFlags(f *gnuflag.FlagSet) { + c.SpaceCommandBase.SetFlags(f) +} + +// Info is defined on the cmd.Command interface. +func (c *UpdateCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "update", + Args: " [ ...]", + Purpose: "update a network space's CIDRs", + Doc: strings.TrimSpace(updateCommandDoc), + } +} + +// Init is defined on the cmd.Command interface. It checks the +// arguments for sanity and sets up the command to run. +func (c *UpdateCommand) Init(args []string) error { + var err error + c.Name, c.CIDRs, err = ParseNameAndCIDRs(args, false) + return errors.Trace(err) +} + +// Run implements Command.Run. +func (c *UpdateCommand) Run(ctx *cmd.Context) error { + return c.RunWithAPI(ctx, func(api SpaceAPI, ctx *cmd.Context) error { + // Update the space. + err := api.UpdateSpace(c.Name, c.CIDRs.SortedValues()) + if err != nil { + return errors.Annotatef(err, "cannot update space %q", c.Name) + } + + ctx.Infof("updated space %q: changed subnets to %s", c.Name, strings.Join(c.CIDRs.SortedValues(), ", ")) + return nil + }) +} === added file 'src/github.com/juju/juju/cmd/juju/space/update_test.go' --- src/github.com/juju/juju/cmd/juju/space/update_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/space/update_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,61 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package space_test + +import ( + "github.com/juju/errors" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/juju/space" + "github.com/juju/juju/feature" +) + +type UpdateSuite struct { + BaseSpaceSuite +} + +var _ = gc.Suite(&UpdateSuite{}) + +func (s *UpdateSuite) SetUpTest(c *gc.C) { + s.BaseSuite.SetFeatureFlags(feature.PostNetCLIMVP) + s.BaseSpaceSuite.SetUpTest(c) + s.command = space.NewUpdateCommand(s.api) + c.Assert(s.command, gc.NotNil) +} + +func (s *UpdateSuite) TestRunWithSubnetsSucceeds(c *gc.C) { + s.AssertRunSucceeds(c, + `updated space "myspace": changed subnets to 10.1.2.0/24, 4.3.2.0/28\n`, + "", // no stdout, just stderr + "myspace", "10.1.2.0/24", "4.3.2.0/28", + ) + + s.api.CheckCallNames(c, "UpdateSpace", "Close") + s.api.CheckCall(c, + 0, "UpdateSpace", + "myspace", s.Strings("10.1.2.0/24", "4.3.2.0/28"), + ) +} + +func (s *UpdateSuite) TestRunWhenSpacesAPIFails(c *gc.C) { + s.api.SetErrors(errors.New("boom")) + + s.AssertRunFails(c, + `cannot update space "foo": boom`, + "foo", "10.1.2.0/24", + ) + + s.api.CheckCallNames(c, "UpdateSpace", "Close") + s.api.CheckCall(c, 0, "UpdateSpace", "foo", s.Strings("10.1.2.0/24")) +} + +func (s *UpdateSuite) TestRunAPIConnectFails(c *gc.C) { + s.command = space.NewUpdateCommand(nil) + s.AssertRunFails(c, + "cannot connect to the API server: no environment specified", + "myname", "10.0.0.0/8", // Drop the args once RunWitnAPI is called internally. + ) + // No API calls recoreded. + s.api.CheckCallNames(c) +} === removed file 'src/github.com/juju/juju/cmd/juju/ssh.go' --- src/github.com/juju/juju/cmd/juju/ssh.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/ssh.go 1970-01-01 00:00:00 +0000 @@ -1,269 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - "net" - "os" - "os/exec" - "strings" - "time" - - "github.com/juju/cmd" - "github.com/juju/names" - "github.com/juju/utils" - "launchpad.net/gnuflag" - - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/environs/config" - "github.com/juju/juju/utils/ssh" -) - -// SSHCommand is responsible for launching a ssh shell on a given unit or machine. -type SSHCommand struct { - SSHCommon -} - -// SSHCommon provides common methods for SSHCommand, SCPCommand and DebugHooksCommand. -type SSHCommon struct { - envcmd.EnvCommandBase - proxy bool - pty bool - Target string - Args []string - apiClient sshAPIClient - apiAddr string -} - -func (c *SSHCommon) SetFlags(f *gnuflag.FlagSet) { - f.BoolVar(&c.proxy, "proxy", true, "proxy through the API server") - f.BoolVar(&c.pty, "pty", true, "enable pseudo-tty allocation") -} - -// setProxyCommand sets the proxy command option. -func (c *SSHCommon) setProxyCommand(options *ssh.Options) error { - apiServerHost, _, err := net.SplitHostPort(c.apiAddr) - if err != nil { - return fmt.Errorf("failed to get proxy address: %v", err) - } - juju, err := getJujuExecutable() - if err != nil { - return fmt.Errorf("failed to get juju executable path: %v", err) - } - options.SetProxyCommand(juju, "ssh", "--proxy=false", "--pty=false", apiServerHost, "nc", "%h", "%p") - return nil -} - -const sshDoc = ` -Launch an ssh shell on the machine identified by the parameter. - can be either a machine id as listed by "juju status" in the -"machines" section or a unit name as listed in the "services" section. -Any extra parameters are passsed as extra parameters to the ssh command. - -Examples: - -Connect to machine 0: - - juju ssh 0 - -Connect to machine 1 and run 'uname -a': - - juju ssh 1 uname -a - -Connect to the first mysql unit: - - juju ssh mysql/0 - -Connect to the first mysql unit and run 'ls -la /var/log/juju': - - juju ssh mysql/0 ls -la /var/log/juju - -Connect to the first jenkins unit as the user jenkins: - - juju ssh jenkins@jenkins/0 -` - -func (c *SSHCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "ssh", - Args: " [...]", - Purpose: "launch an ssh shell on a given unit or machine", - Doc: sshDoc, - } -} - -func (c *SSHCommand) Init(args []string) error { - if len(args) == 0 { - return fmt.Errorf("no target name specified") - } - c.Target, c.Args = args[0], args[1:] - return nil -} - -// getJujuExecutable returns the path to the juju -// executable, or an error if it could not be found. -var getJujuExecutable = func() (string, error) { - return exec.LookPath(os.Args[0]) -} - -// getSSHOptions configures and returns SSH options and proxy settings. -func (c *SSHCommon) getSSHOptions(enablePty bool) (*ssh.Options, error) { - var options ssh.Options - - // TODO(waigani) do not save fingerprint only until this bug is addressed: - // lp:892552. Also see lp:1334481. - options.SetKnownHostsFile("/dev/null") - if enablePty { - options.EnablePTY() - } - var err error - if c.proxy, err = c.proxySSH(); err != nil { - return nil, err - } else if c.proxy { - if err := c.setProxyCommand(&options); err != nil { - return nil, err - } - } - return &options, nil -} - -// Run resolves c.Target to a machine, to the address of a i -// machine or unit forks ssh passing any arguments provided. -func (c *SSHCommand) Run(ctx *cmd.Context) error { - if c.apiClient == nil { - // If the apClient is not already opened and it is opened - // by ensureAPIClient, then close it when we're done. - defer func() { - if c.apiClient != nil { - c.apiClient.Close() - c.apiClient = nil - } - }() - } - options, err := c.getSSHOptions(c.pty) - if err != nil { - return err - } - - user, host, err := c.userHostFromTarget(c.Target) - if err != nil { - return err - } - cmd := ssh.Command(user+"@"+host, c.Args, options) - cmd.Stdin = ctx.Stdin - cmd.Stdout = ctx.Stdout - cmd.Stderr = ctx.Stderr - return cmd.Run() -} - -// proxySSH returns true iff both c.proxy and -// the proxy-ssh environment configuration -// are true. -func (c *SSHCommon) proxySSH() (bool, error) { - if !c.proxy { - return false, nil - } - if _, err := c.ensureAPIClient(); err != nil { - return false, err - } - var cfg *config.Config - attrs, err := c.apiClient.EnvironmentGet() - if err == nil { - cfg, err = config.New(config.NoDefaults, attrs) - } - if err != nil { - return false, err - } - logger.Debugf("proxy-ssh is %v", cfg.ProxySSH()) - return cfg.ProxySSH(), nil -} - -func (c *SSHCommon) ensureAPIClient() (sshAPIClient, error) { - if c.apiClient != nil { - return c.apiClient, nil - } - return c.initAPIClient() -} - -// initAPIClient initialises the API connection. -// It is the caller's responsibility to close the connection. -func (c *SSHCommon) initAPIClient() (sshAPIClient, error) { - st, err := c.NewAPIRoot() - if err != nil { - return nil, err - } - c.apiClient = st.Client() - c.apiAddr = st.Addr() - return c.apiClient, nil -} - -type sshAPIClient interface { - EnvironmentGet() (map[string]interface{}, error) - PublicAddress(target string) (string, error) - PrivateAddress(target string) (string, error) - ServiceCharmRelations(service string) ([]string, error) - Close() error -} - -// attemptStarter is an interface corresponding to utils.AttemptStrategy -type attemptStarter interface { - Start() attempt -} - -type attempt interface { - Next() bool -} - -type attemptStrategy utils.AttemptStrategy - -func (s attemptStrategy) Start() attempt { - return utils.AttemptStrategy(s).Start() -} - -var sshHostFromTargetAttemptStrategy attemptStarter = attemptStrategy{ - Total: 5 * time.Second, - Delay: 500 * time.Millisecond, -} - -func (c *SSHCommon) userHostFromTarget(target string) (user, host string, err error) { - if i := strings.IndexRune(target, '@'); i != -1 { - user = target[:i] - target = target[i+1:] - } else { - user = "ubuntu" - } - - // If the target is neither a machine nor a unit, - // assume it's a hostname and try it directly. - if !names.IsValidMachine(target) && !names.IsValidUnit(target) { - return user, target, nil - } - - // A target may not initially have an address (e.g. the - // address updater hasn't yet run), so we must do this in - // a loop. - if _, err := c.ensureAPIClient(); err != nil { - return "", "", err - } - for a := sshHostFromTargetAttemptStrategy.Start(); a.Next(); { - var addr string - if c.proxy { - addr, err = c.apiClient.PrivateAddress(target) - } else { - addr, err = c.apiClient.PublicAddress(target) - } - if err == nil { - return user, addr, nil - } - } - return "", "", err -} - -// AllowInterspersedFlags for ssh/scp is set to false so that -// flags after the unit name are passed through to ssh, for eg. -// `juju ssh -v service-name/0 uname -a`. -func (c *SSHCommon) AllowInterspersedFlags() bool { - return false -} === removed file 'src/github.com/juju/juju/cmd/juju/ssh_test.go' --- src/github.com/juju/juju/cmd/juju/ssh_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/ssh_test.go 1970-01-01 00:00:00 +0000 @@ -1,257 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "bytes" - "fmt" - "os" - "path/filepath" - "reflect" - "strings" - - "github.com/juju/cmd" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - "gopkg.in/juju/charm.v5" - - "github.com/juju/juju/apiserver" - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/juju/testing" - "github.com/juju/juju/network" - "github.com/juju/juju/state" - "github.com/juju/juju/testcharms" - coretesting "github.com/juju/juju/testing" - "github.com/juju/juju/utils/ssh" -) - -var _ = gc.Suite(&SSHSuite{}) - -type SSHSuite struct { - SSHCommonSuite -} - -type SSHCommonSuite struct { - testing.JujuConnSuite - bin string -} - -func (s *SSHCommonSuite) SetUpTest(c *gc.C) { - s.JujuConnSuite.SetUpTest(c) - s.PatchValue(&getJujuExecutable, func() (string, error) { return "juju", nil }) - - s.bin = c.MkDir() - s.PatchEnvPathPrepend(s.bin) - for _, name := range patchedCommands { - f, err := os.OpenFile(filepath.Join(s.bin, name), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0777) - c.Assert(err, jc.ErrorIsNil) - _, err = f.Write([]byte(fakecommand)) - c.Assert(err, jc.ErrorIsNil) - err = f.Close() - c.Assert(err, jc.ErrorIsNil) - } - client, _ := ssh.NewOpenSSHClient() - s.PatchValue(&ssh.DefaultClient, client) -} - -const ( - noProxy = `-o StrictHostKeyChecking no -o PasswordAuthentication no -o ServerAliveInterval 30 ` - args = `-o StrictHostKeyChecking no -o ProxyCommand juju ssh --proxy=false --pty=false localhost nc %h %p -o PasswordAuthentication no -o ServerAliveInterval 30 ` - commonArgsNoProxy = noProxy + `-o UserKnownHostsFile /dev/null ` - commonArgs = args + `-o UserKnownHostsFile /dev/null ` - sshArgs = args + `-t -t -o UserKnownHostsFile /dev/null ` - sshArgsNoProxy = noProxy + `-t -t -o UserKnownHostsFile /dev/null ` -) - -var sshTests = []struct { - about string - args []string - result string -}{ - { - "connect to machine 0", - []string{"ssh", "0"}, - sshArgs + "ubuntu@dummyenv-0.internal", - }, - { - "connect to machine 0 and pass extra arguments", - []string{"ssh", "0", "uname", "-a"}, - sshArgs + "ubuntu@dummyenv-0.internal uname -a", - }, - { - "connect to unit mysql/0", - []string{"ssh", "mysql/0"}, - sshArgs + "ubuntu@dummyenv-0.internal", - }, - { - "connect to unit mongodb/1 as the mongo user", - []string{"ssh", "mongo@mongodb/1"}, - sshArgs + "mongo@dummyenv-2.internal", - }, - { - "connect to unit mongodb/1 and pass extra arguments", - []string{"ssh", "mongodb/1", "ls", "/"}, - sshArgs + "ubuntu@dummyenv-2.internal ls /", - }, - { - "connect to unit mysql/0 without proxy", - []string{"ssh", "--proxy=false", "mysql/0"}, - sshArgsNoProxy + "ubuntu@dummyenv-0.dns", - }, -} - -func (s *SSHSuite) TestSSHCommand(c *gc.C) { - m := s.makeMachines(3, c, true) - ch := testcharms.Repo.CharmDir("dummy") - curl := charm.MustParseURL( - fmt.Sprintf("local:quantal/%s-%d", ch.Meta().Name, ch.Revision()), - ) - dummy, err := s.State.AddCharm(ch, curl, "dummy-path", "dummy-1-sha256") - c.Assert(err, jc.ErrorIsNil) - srv := s.AddTestingService(c, "mysql", dummy) - s.addUnit(srv, m[0], c) - - srv = s.AddTestingService(c, "mongodb", dummy) - s.addUnit(srv, m[1], c) - s.addUnit(srv, m[2], c) - - for i, t := range sshTests { - c.Logf("test %d: %s -> %s", i, t.about, t.args) - ctx := coretesting.Context(c) - jujucmd := cmd.NewSuperCommand(cmd.SuperCommandParams{}) - jujucmd.Register(envcmd.Wrap(&SSHCommand{})) - - code := cmd.Main(jujucmd, ctx, t.args) - c.Check(code, gc.Equals, 0) - c.Check(ctx.Stderr.(*bytes.Buffer).String(), gc.Equals, "") - c.Check(strings.TrimRight(ctx.Stdout.(*bytes.Buffer).String(), "\r\n"), gc.Equals, t.result) - } -} - -func (s *SSHSuite) TestSSHCommandEnvironProxySSH(c *gc.C) { - s.makeMachines(1, c, true) - // Setting proxy-ssh=false in the environment overrides --proxy. - err := s.State.UpdateEnvironConfig(map[string]interface{}{"proxy-ssh": false}, nil, nil) - c.Assert(err, jc.ErrorIsNil) - ctx := coretesting.Context(c) - jujucmd := cmd.NewSuperCommand(cmd.SuperCommandParams{}) - jujucmd.Register(envcmd.Wrap(&SSHCommand{})) - code := cmd.Main(jujucmd, ctx, []string{"ssh", "0"}) - c.Check(code, gc.Equals, 0) - c.Check(ctx.Stderr.(*bytes.Buffer).String(), gc.Equals, "") - c.Check(strings.TrimRight(ctx.Stdout.(*bytes.Buffer).String(), "\r\n"), gc.Equals, sshArgsNoProxy+"ubuntu@dummyenv-0.dns") -} - -func (s *SSHSuite) TestSSHWillWorkInUpgrade(c *gc.C) { - // Check the API client interface used by "juju ssh" against what - // the API server will allow during upgrades. Ensure that the API - // server will allow all required API calls to support SSH. - type concrete struct { - sshAPIClient - } - t := reflect.TypeOf(concrete{}) - for i := 0; i < t.NumMethod(); i++ { - name := t.Method(i).Name - - // Close isn't an API method and ServiceCharmRelations is not - // relevant to "juju ssh". - if name == "Close" || name == "ServiceCharmRelations" { - continue - } - c.Logf("checking %q", name) - c.Check(apiserver.IsMethodAllowedDuringUpgrade("Client", name), jc.IsTrue) - } -} - -type callbackAttemptStarter struct { - next func() bool -} - -func (s *callbackAttemptStarter) Start() attempt { - return callbackAttempt{next: s.next} -} - -type callbackAttempt struct { - next func() bool -} - -func (a callbackAttempt) Next() bool { - return a.next() -} - -func (s *SSHSuite) TestSSHCommandHostAddressRetry(c *gc.C) { - s.testSSHCommandHostAddressRetry(c, false) -} - -func (s *SSHSuite) TestSSHCommandHostAddressRetryProxy(c *gc.C) { - s.testSSHCommandHostAddressRetry(c, true) -} - -func (s *SSHSuite) testSSHCommandHostAddressRetry(c *gc.C, proxy bool) { - m := s.makeMachines(1, c, false) - ctx := coretesting.Context(c) - - var called int - next := func() bool { - called++ - return called < 2 - } - attemptStarter := &callbackAttemptStarter{next: next} - s.PatchValue(&sshHostFromTargetAttemptStrategy, attemptStarter) - - // Ensure that the ssh command waits for a public address, or the attempt - // strategy's Done method returns false. - args := []string{"--proxy=" + fmt.Sprint(proxy), "0"} - code := cmd.Main(envcmd.Wrap(&SSHCommand{}), ctx, args) - c.Check(code, gc.Equals, 1) - c.Assert(called, gc.Equals, 2) - called = 0 - attemptStarter.next = func() bool { - called++ - if called > 1 { - s.setAddresses(m[0], c) - } - return true - } - code = cmd.Main(envcmd.Wrap(&SSHCommand{}), ctx, args) - c.Check(code, gc.Equals, 0) - c.Assert(called, gc.Equals, 2) -} - -func (s *SSHCommonSuite) setAddresses(m *state.Machine, c *gc.C) { - addrPub := network.NewScopedAddress( - fmt.Sprintf("dummyenv-%s.dns", m.Id()), - network.ScopePublic, - ) - addrPriv := network.NewScopedAddress( - fmt.Sprintf("dummyenv-%s.internal", m.Id()), - network.ScopeCloudLocal, - ) - err := m.SetProviderAddresses(addrPub, addrPriv) - c.Assert(err, jc.ErrorIsNil) -} - -func (s *SSHCommonSuite) makeMachines(n int, c *gc.C, setAddresses bool) []*state.Machine { - var machines = make([]*state.Machine, n) - for i := 0; i < n; i++ { - m, err := s.State.AddMachine("quantal", state.JobHostUnits) - c.Assert(err, jc.ErrorIsNil) - if setAddresses { - s.setAddresses(m, c) - } - // must set an instance id as the ssh command uses that as a signal the - // machine has been provisioned - inst, md := testing.AssertStartInstance(c, s.Environ, m.Id()) - c.Assert(m.SetProvisioned(inst.Id(), "fake_nonce", md), gc.IsNil) - machines[i] = m - } - return machines -} - -func (s *SSHCommonSuite) addUnit(srv *state.Service, m *state.Machine, c *gc.C) { - u, err := srv.AddUnit() - c.Assert(err, jc.ErrorIsNil) - err = u.AssignToMachine(m) - c.Assert(err, jc.ErrorIsNil) -} === removed file 'src/github.com/juju/juju/cmd/juju/ssh_unix_test.go' --- src/github.com/juju/juju/cmd/juju/ssh_unix_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/ssh_unix_test.go 1970-01-01 00:00:00 +0000 @@ -1,16 +0,0 @@ -// Copyright 2014 Canonical Ltd. -// Copyright 2014 Cloudbase Solutions SRL -// Licensed under the AGPLv3, see LICENCE file for details. - -// +build !windows - -package main - -// Commands to patch -var patchedCommands = []string{"ssh", "scp"} - -// fakecommand outputs its arguments to stdout for verification -var fakecommand = `#!/bin/bash - -echo "$@" | tee $0.args -` === removed file 'src/github.com/juju/juju/cmd/juju/ssh_windows_test.go' --- src/github.com/juju/juju/cmd/juju/ssh_windows_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/ssh_windows_test.go 1970-01-01 00:00:00 +0000 @@ -1,23 +0,0 @@ -// Copyright 2014 Canonical Ltd. -// Copyright 2014 Cloudbase Solutions SRL -// Licensed under the AGPLv3, see LICENCE file for details. - -// +build windows - -package main - -// Commands to patch -var patchedCommands = []string{"scp.cmd", "ssh.cmd"} - -// fakecommand outputs its arguments to stdout for verification -var fakecommand = `@echo off -setlocal enabledelayedexpansion -set list=%1 -set argCount=0 -for %%x in (%*) do ( -set /A argCount+=1 -set "argVec[!argCount!]=%%~x" -) -for /L %%i in (2,1,%argCount%) do set list=!list! !argVec[%%i]! -echo %list% -` === added directory 'src/github.com/juju/juju/cmd/juju/status' === removed file 'src/github.com/juju/juju/cmd/juju/status.go' --- src/github.com/juju/juju/cmd/juju/status.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/status.go 1970-01-01 00:00:00 +0000 @@ -1,583 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "encoding/json" - "fmt" - "os" - "strconv" - - "github.com/juju/cmd" - "github.com/juju/errors" - "launchpad.net/gnuflag" - - "github.com/juju/juju/api" - "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/instance" - "github.com/juju/juju/juju/osenv" - "github.com/juju/juju/network" - "github.com/juju/juju/state/multiwatcher" -) - -type StatusCommand struct { - envcmd.EnvCommandBase - out cmd.Output - patterns []string - isoTime bool -} - -var statusDoc = ` -This command will report on the runtime state of various system entities. - -There are a number of ways to format the status output: - -- {short|line|oneline}: List units and their subordinates. For each - unit, the IP address and agent status are listed. -- summary: Displays the subnet(s) and port(s) the environment utilizes. - Also displays aggregate information about: - - MACHINES: total #, and # in each state. - - UNITS: total #, and # in each state. - - SERVICES: total #, and # exposed of each service. -- tabular: Displays information in a tabular format in these sections: - - Machines: ID, STATE, VERSION, DNS, INS-ID, SERIES, HARDWARE - - Services: NAME, EXPOSED, CHARM - - Units: ID, STATE, VERSION, MACHINE, PORTS, PUBLIC-ADDRESS - - Also displays subordinate units. -- yaml (DEFAULT): Displays information on machines, services, and units - in the yaml format. - -Service or unit names may be specified to filter the status to only those -services and units that match, along with the related machines, services -and units. If a subordinate unit is matched, then its principal unit will -be displayed. If a principal unit is matched, then all of its subordinates -will be displayed. - -Wildcards ('*') may be specified in service/unit names to match any sequence -of characters. For example, 'nova-*' will match any service whose name begins -with 'nova-': 'nova-compute', 'nova-volume', etc. -` - -func (c *StatusCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "status", - Args: "[pattern ...]", - Purpose: "output status information about an environment", - Doc: statusDoc, - Aliases: []string{"stat"}, - } -} - -func (c *StatusCommand) SetFlags(f *gnuflag.FlagSet) { - f.BoolVar(&c.isoTime, "utc", false, "display time as UTC in RFC3339 format") - - defaultFormat := "yaml" - if c.CompatVersion() > 1 { - defaultFormat = "tabular" - } - c.out.AddFlags(f, defaultFormat, map[string]cmd.Formatter{ - "yaml": cmd.FormatYaml, - "json": cmd.FormatJson, - "short": FormatOneline, - "oneline": FormatOneline, - "line": FormatOneline, - "tabular": FormatTabular, - "summary": FormatSummary, - }) -} - -func (c *StatusCommand) Init(args []string) error { - c.patterns = args - // If use of ISO time not specified on command line, - // check env var. - if !c.isoTime { - var err error - envVarValue := os.Getenv(osenv.JujuStatusIsoTimeEnvKey) - if envVarValue != "" { - if c.isoTime, err = strconv.ParseBool(envVarValue); err != nil { - return errors.Annotatef(err, "invalid %s env var, expected true|false", osenv.JujuStatusIsoTimeEnvKey) - } - } - } - return nil -} - -var connectionError = `Unable to connect to environment %q. -Please check your credentials or use 'juju bootstrap' to create a new environment. - -Error details: -%v -` - -type statusAPI interface { - Status(patterns []string) (*api.Status, error) - Close() error -} - -var newApiClientForStatus = func(c *StatusCommand) (statusAPI, error) { - return c.NewAPIClient() -} - -func (c *StatusCommand) Run(ctx *cmd.Context) error { - - apiclient, err := newApiClientForStatus(c) - if err != nil { - return fmt.Errorf(connectionError, c.ConnectionName(), err) - } - defer apiclient.Close() - - status, err := apiclient.Status(c.patterns) - if err != nil { - if status == nil { - // Status call completely failed, there is nothing to report - return err - } - // Display any error, but continue to print status if some was returned - fmt.Fprintf(ctx.Stderr, "%v\n", err) - } else if status == nil { - return errors.Errorf("unable to obtain the current status") - } - - result := newStatusFormatter(status, c.CompatVersion(), c.isoTime).format() - return c.out.Write(ctx, result) -} - -type formattedStatus struct { - Environment string `json:"environment"` - AvailableVersion string `json:"available-version,omitempty" yaml:"available-version,omitempty"` - Machines map[string]machineStatus `json:"machines"` - Services map[string]serviceStatus `json:"services"` - Networks map[string]networkStatus `json:"networks,omitempty" yaml:",omitempty"` -} - -type errorStatus struct { - StatusError string `json:"status-error" yaml:"status-error"` -} - -type machineStatus struct { - Err error `json:"-" yaml:",omitempty"` - AgentState params.Status `json:"agent-state,omitempty" yaml:"agent-state,omitempty"` - AgentStateInfo string `json:"agent-state-info,omitempty" yaml:"agent-state-info,omitempty"` - AgentVersion string `json:"agent-version,omitempty" yaml:"agent-version,omitempty"` - DNSName string `json:"dns-name,omitempty" yaml:"dns-name,omitempty"` - InstanceId instance.Id `json:"instance-id,omitempty" yaml:"instance-id,omitempty"` - InstanceState string `json:"instance-state,omitempty" yaml:"instance-state,omitempty"` - Life string `json:"life,omitempty" yaml:"life,omitempty"` - Series string `json:"series,omitempty" yaml:"series,omitempty"` - Id string `json:"-" yaml:"-"` - Containers map[string]machineStatus `json:"containers,omitempty" yaml:"containers,omitempty"` - Hardware string `json:"hardware,omitempty" yaml:"hardware,omitempty"` - HAStatus string `json:"state-server-member-status,omitempty" yaml:"state-server-member-status,omitempty"` -} - -// A goyaml bug means we can't declare these types -// locally to the GetYAML methods. -type machineStatusNoMarshal machineStatus - -func (s machineStatus) MarshalJSON() ([]byte, error) { - if s.Err != nil { - return json.Marshal(errorStatus{s.Err.Error()}) - } - return json.Marshal(machineStatusNoMarshal(s)) -} - -func (s machineStatus) GetYAML() (tag string, value interface{}) { - if s.Err != nil { - return "", errorStatus{s.Err.Error()} - } - // TODO(rog) rename mNoMethods to noMethods (and also in - // the other GetYAML methods) when people are using the non-buggy - // goyaml version. // TODO(jw4) however verify that gccgo does not - // complain about symbol already defined. - type mNoMethods machineStatus - return "", mNoMethods(s) -} - -type serviceStatus struct { - Err error `json:"-" yaml:",omitempty"` - Charm string `json:"charm" yaml:"charm"` - CanUpgradeTo string `json:"can-upgrade-to,omitempty" yaml:"can-upgrade-to,omitempty"` - Exposed bool `json:"exposed" yaml:"exposed"` - Life string `json:"life,omitempty" yaml:"life,omitempty"` - StatusInfo statusInfoContents `json:"service-status,omitempty" yaml:"service-status,omitempty"` - Relations map[string][]string `json:"relations,omitempty" yaml:"relations,omitempty"` - Networks map[string][]string `json:"networks,omitempty" yaml:"networks,omitempty"` - SubordinateTo []string `json:"subordinate-to,omitempty" yaml:"subordinate-to,omitempty"` - Units map[string]unitStatus `json:"units,omitempty" yaml:"units,omitempty"` -} - -type serviceStatusNoMarshal serviceStatus - -func (s serviceStatus) MarshalJSON() ([]byte, error) { - if s.Err != nil { - return json.Marshal(errorStatus{s.Err.Error()}) - } - type ssNoMethods serviceStatus - return json.Marshal(ssNoMethods(s)) -} - -func (s serviceStatus) GetYAML() (tag string, value interface{}) { - if s.Err != nil { - return "", errorStatus{s.Err.Error()} - } - type ssNoMethods serviceStatus - return "", ssNoMethods(s) -} - -type unitStatus struct { - // New Juju Health Status fields. - WorkloadStatusInfo statusInfoContents `json:"workload-status,omitempty" yaml:"workload-status,omitempty"` - AgentStatusInfo statusInfoContents `json:"agent-status,omitempty" yaml:"agent-status,omitempty"` - - // Legacy status fields, to be removed in Juju 2.0 - AgentState params.Status `json:"agent-state,omitempty" yaml:"agent-state,omitempty"` - AgentStateInfo string `json:"agent-state-info,omitempty" yaml:"agent-state-info,omitempty"` - Err error `json:"-" yaml:",omitempty"` - AgentVersion string `json:"agent-version,omitempty" yaml:"agent-version,omitempty"` - Life string `json:"life,omitempty" yaml:"life,omitempty"` - - Charm string `json:"upgrading-from,omitempty" yaml:"upgrading-from,omitempty"` - Machine string `json:"machine,omitempty" yaml:"machine,omitempty"` - OpenedPorts []string `json:"open-ports,omitempty" yaml:"open-ports,omitempty"` - PublicAddress string `json:"public-address,omitempty" yaml:"public-address,omitempty"` - Subordinates map[string]unitStatus `json:"subordinates,omitempty" yaml:"subordinates,omitempty"` -} - -type statusInfoContents struct { - Err error `json:"-" yaml:",omitempty"` - Current params.Status `json:"current,omitempty" yaml:"current,omitempty"` - Message string `json:"message,omitempty" yaml:"message,omitempty"` - Since string `json:"since,omitempty" yaml:"since,omitempty"` - Version string `json:"version,omitempty" yaml:"version,omitempty"` -} - -type statusInfoContentsNoMarshal statusInfoContents - -func (s statusInfoContents) MarshalJSON() ([]byte, error) { - if s.Err != nil { - return json.Marshal(errorStatus{s.Err.Error()}) - } - return json.Marshal(statusInfoContentsNoMarshal(s)) -} - -func (s statusInfoContents) GetYAML() (tag string, value interface{}) { - if s.Err != nil { - return "", errorStatus{s.Err.Error()} - } - type sicNoMethods statusInfoContents - return "", sicNoMethods(s) -} - -type unitStatusNoMarshal unitStatus - -func (s unitStatus) MarshalJSON() ([]byte, error) { - if s.Err != nil { - return json.Marshal(errorStatus{s.Err.Error()}) - } - return json.Marshal(unitStatusNoMarshal(s)) -} - -func (s unitStatus) GetYAML() (tag string, value interface{}) { - if s.Err != nil { - return "", errorStatus{s.Err.Error()} - } - type usNoMethods unitStatus - return "", usNoMethods(s) -} - -type networkStatus struct { - Err error `json:"-" yaml:",omitempty"` - ProviderId network.Id `json:"provider-id" yaml:"provider-id"` - CIDR string `json:"cidr,omitempty" yaml:"cidr,omitempty"` - VLANTag int `json:"vlan-tag,omitempty" yaml:"vlan-tag,omitempty"` -} - -type networkStatusNoMarshal networkStatus - -func (n networkStatus) MarshalJSON() ([]byte, error) { - if n.Err != nil { - return json.Marshal(errorStatus{n.Err.Error()}) - } - type nNoMethods networkStatus - return json.Marshal(nNoMethods(n)) -} - -func (n networkStatus) GetYAML() (tag string, value interface{}) { - if n.Err != nil { - return "", errorStatus{n.Err.Error()} - } - type nNoMethods networkStatus - return "", nNoMethods(n) -} - -type statusFormatter struct { - status *api.Status - relations map[int]api.RelationStatus - isoTime bool - compatVersion int -} - -func newStatusFormatter(status *api.Status, compatVersion int, isoTime bool) *statusFormatter { - sf := statusFormatter{ - status: status, - relations: make(map[int]api.RelationStatus), - compatVersion: compatVersion, - isoTime: isoTime, - } - for _, relation := range status.Relations { - sf.relations[relation.Id] = relation - } - return &sf -} - -func (sf *statusFormatter) format() formattedStatus { - if sf.status == nil { - return formattedStatus{} - } - out := formattedStatus{ - Environment: sf.status.EnvironmentName, - AvailableVersion: sf.status.AvailableVersion, - Machines: make(map[string]machineStatus), - Services: make(map[string]serviceStatus), - } - for k, m := range sf.status.Machines { - out.Machines[k] = sf.formatMachine(m) - } - for sn, s := range sf.status.Services { - out.Services[sn] = sf.formatService(sn, s) - } - for k, n := range sf.status.Networks { - if out.Networks == nil { - out.Networks = make(map[string]networkStatus) - } - out.Networks[k] = sf.formatNetwork(n) - } - return out -} - -func (sf *statusFormatter) formatMachine(machine api.MachineStatus) machineStatus { - var out machineStatus - - if machine.Agent.Status == "" { - // Older server - // TODO: this will go away at some point (v1.21?). - out = machineStatus{ - AgentState: machine.AgentState, - AgentStateInfo: machine.AgentStateInfo, - AgentVersion: machine.AgentVersion, - Life: machine.Life, - Err: machine.Err, - DNSName: machine.DNSName, - InstanceId: machine.InstanceId, - InstanceState: machine.InstanceState, - Series: machine.Series, - Id: machine.Id, - Containers: make(map[string]machineStatus), - Hardware: machine.Hardware, - } - } else { - // New server - agent := machine.Agent - out = machineStatus{ - AgentState: machine.AgentState, - AgentStateInfo: adjustInfoIfMachineAgentDown(machine.AgentState, agent.Status, agent.Info), - AgentVersion: agent.Version, - Life: agent.Life, - Err: agent.Err, - DNSName: machine.DNSName, - InstanceId: machine.InstanceId, - InstanceState: machine.InstanceState, - Series: machine.Series, - Id: machine.Id, - Containers: make(map[string]machineStatus), - Hardware: machine.Hardware, - } - } - - for k, m := range machine.Containers { - out.Containers[k] = sf.formatMachine(m) - } - - for _, job := range machine.Jobs { - if job == multiwatcher.JobManageEnviron { - out.HAStatus = makeHAStatus(machine.HasVote, machine.WantsVote) - break - } - } - return out -} - -func (sf *statusFormatter) formatService(name string, service api.ServiceStatus) serviceStatus { - out := serviceStatus{ - Err: service.Err, - Charm: service.Charm, - Exposed: service.Exposed, - Life: service.Life, - Relations: service.Relations, - Networks: make(map[string][]string), - CanUpgradeTo: service.CanUpgradeTo, - SubordinateTo: service.SubordinateTo, - Units: make(map[string]unitStatus), - StatusInfo: sf.getServiceStatusInfo(service), - } - if len(service.Networks.Enabled) > 0 { - out.Networks["enabled"] = service.Networks.Enabled - } - if len(service.Networks.Disabled) > 0 { - out.Networks["disabled"] = service.Networks.Disabled - } - for k, m := range service.Units { - out.Units[k] = sf.formatUnit(m, name) - } - return out -} - -func (sf *statusFormatter) getServiceStatusInfo(service api.ServiceStatus) statusInfoContents { - info := statusInfoContents{ - Err: service.Status.Err, - Current: service.Status.Status, - Message: service.Status.Info, - Version: service.Status.Version, - } - if service.Status.Since != nil { - info.Since = formatStatusTime(service.Status.Since, sf.isoTime) - } - return info -} - -func (sf *statusFormatter) formatUnit(unit api.UnitStatus, serviceName string) unitStatus { - // TODO(Wallyworld) - this should be server side but we still need to support older servers. - sf.updateUnitStatusInfo(&unit, serviceName) - - out := unitStatus{ - WorkloadStatusInfo: sf.getWorkloadStatusInfo(unit), - AgentStatusInfo: sf.getAgentStatusInfo(unit), - Machine: unit.Machine, - OpenedPorts: unit.OpenedPorts, - PublicAddress: unit.PublicAddress, - Charm: unit.Charm, - Subordinates: make(map[string]unitStatus), - } - - // These legacy fields will be dropped for Juju 2.0. - if sf.compatVersion < 2 || out.AgentStatusInfo.Current == "" { - out.Err = unit.Err - out.AgentState = unit.AgentState - out.AgentStateInfo = unit.AgentStateInfo - out.Life = unit.Life - out.AgentVersion = unit.AgentVersion - } - - for k, m := range unit.Subordinates { - out.Subordinates[k] = sf.formatUnit(m, serviceName) - } - return out -} - -func (sf *statusFormatter) getWorkloadStatusInfo(unit api.UnitStatus) statusInfoContents { - info := statusInfoContents{ - Err: unit.Workload.Err, - Current: unit.Workload.Status, - Message: unit.Workload.Info, - Version: unit.Workload.Version, - } - if unit.Workload.Since != nil { - info.Since = formatStatusTime(unit.Workload.Since, sf.isoTime) - } - return info -} - -func (sf *statusFormatter) getAgentStatusInfo(unit api.UnitStatus) statusInfoContents { - info := statusInfoContents{ - Err: unit.UnitAgent.Err, - Current: unit.UnitAgent.Status, - Message: unit.UnitAgent.Info, - Version: unit.UnitAgent.Version, - } - if unit.UnitAgent.Since != nil { - info.Since = formatStatusTime(unit.UnitAgent.Since, sf.isoTime) - } - return info -} - -func (sf *statusFormatter) updateUnitStatusInfo(unit *api.UnitStatus, serviceName string) { - // This logic has no business here but can't be moved until Juju 2.0. - statusInfo := unit.Workload.Info - if unit.Workload.Status == "" { - // Old server that doesn't support this field and others. - // Just use the info string as-is. - statusInfo = unit.AgentStateInfo - } - if unit.Workload.Status == params.StatusError { - if relation, ok := sf.relations[getRelationIdFromData(unit)]; ok { - // Append the details of the other endpoint on to the status info string. - if ep, ok := findOtherEndpoint(relation.Endpoints, serviceName); ok { - unit.Workload.Info = statusInfo + " for " + ep.String() - unit.AgentStateInfo = unit.Workload.Info - } - } - } -} - -func (sf *statusFormatter) formatNetwork(network api.NetworkStatus) networkStatus { - return networkStatus{ - Err: network.Err, - ProviderId: network.ProviderId, - CIDR: network.CIDR, - VLANTag: network.VLANTag, - } -} - -func makeHAStatus(hasVote, wantsVote bool) string { - var s string - switch { - case hasVote && wantsVote: - s = "has-vote" - case hasVote && !wantsVote: - s = "removing-vote" - case !hasVote && wantsVote: - s = "adding-vote" - case !hasVote && !wantsVote: - s = "no-vote" - } - return s -} - -func getRelationIdFromData(unit *api.UnitStatus) int { - if relationId_, ok := unit.Workload.Data["relation-id"]; ok { - if relationId, ok := relationId_.(float64); ok { - return int(relationId) - } else { - logger.Infof("relation-id found status data but was unexpected "+ - "type: %q. Status output may be lacking some detail.", relationId_) - } - } - return -1 -} - -// findOtherEndpoint searches the provided endpoints for an endpoint -// that *doesn't* match serviceName. The returned bool indicates if -// such an endpoint was found. -func findOtherEndpoint(endpoints []api.EndpointStatus, serviceName string) (api.EndpointStatus, bool) { - for _, endpoint := range endpoints { - if endpoint.ServiceName != serviceName { - return endpoint, true - } - } - return api.EndpointStatus{}, false -} - -// adjustInfoIfMachineAgentDown modifies the agent status info string if the -// agent is down. The original status and info is included in -// parentheses. -func adjustInfoIfMachineAgentDown(status, origStatus params.Status, info string) string { - if status == params.StatusDown { - if info == "" { - return fmt.Sprintf("(%s)", origStatus) - } - return fmt.Sprintf("(%s: %s)", origStatus, info) - } - return info -} === added file 'src/github.com/juju/juju/cmd/juju/status/formatted.go' --- src/github.com/juju/juju/cmd/juju/status/formatted.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/status/formatted.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,190 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package status + +import ( + "encoding/json" + + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/instance" + "github.com/juju/juju/network" +) + +type formattedStatus struct { + Environment string `json:"environment"` + EnvironmentStatus *environmentStatus `json:"environment-status,omitempty" yaml:"environment-status,omitempty"` + Machines map[string]machineStatus `json:"machines"` + Services map[string]serviceStatus `json:"services"` + Networks map[string]networkStatus `json:"networks,omitempty" yaml:",omitempty"` +} + +type errorStatus struct { + StatusError string `json:"status-error" yaml:"status-error"` +} + +type environmentStatus struct { + AvailableVersion string `json:"upgrade-available,omitempty" yaml:"upgrade-available,omitempty"` +} + +type machineStatus struct { + Err error `json:"-" yaml:",omitempty"` + AgentState params.Status `json:"agent-state,omitempty" yaml:"agent-state,omitempty"` + AgentStateInfo string `json:"agent-state-info,omitempty" yaml:"agent-state-info,omitempty"` + AgentVersion string `json:"agent-version,omitempty" yaml:"agent-version,omitempty"` + DNSName string `json:"dns-name,omitempty" yaml:"dns-name,omitempty"` + InstanceId instance.Id `json:"instance-id,omitempty" yaml:"instance-id,omitempty"` + InstanceState string `json:"instance-state,omitempty" yaml:"instance-state,omitempty"` + Life string `json:"life,omitempty" yaml:"life,omitempty"` + Series string `json:"series,omitempty" yaml:"series,omitempty"` + Id string `json:"-" yaml:"-"` + Containers map[string]machineStatus `json:"containers,omitempty" yaml:"containers,omitempty"` + Hardware string `json:"hardware,omitempty" yaml:"hardware,omitempty"` + HAStatus string `json:"state-server-member-status,omitempty" yaml:"state-server-member-status,omitempty"` +} + +// A goyaml bug means we can't declare these types +// locally to the GetYAML methods. +type machineStatusNoMarshal machineStatus + +func (s machineStatus) MarshalJSON() ([]byte, error) { + if s.Err != nil { + return json.Marshal(errorStatus{s.Err.Error()}) + } + return json.Marshal(machineStatusNoMarshal(s)) +} + +func (s machineStatus) GetYAML() (tag string, value interface{}) { + if s.Err != nil { + return "", errorStatus{s.Err.Error()} + } + // TODO(rog) rename mNoMethods to noMethods (and also in + // the other GetYAML methods) when people are using the non-buggy + // goyaml version. // TODO(jw4) however verify that gccgo does not + // complain about symbol already defined. + type mNoMethods machineStatus + return "", mNoMethods(s) +} + +type serviceStatus struct { + Err error `json:"-" yaml:",omitempty"` + Charm string `json:"charm" yaml:"charm"` + CanUpgradeTo string `json:"can-upgrade-to,omitempty" yaml:"can-upgrade-to,omitempty"` + Exposed bool `json:"exposed" yaml:"exposed"` + Life string `json:"life,omitempty" yaml:"life,omitempty"` + StatusInfo statusInfoContents `json:"service-status,omitempty" yaml:"service-status,omitempty"` + Relations map[string][]string `json:"relations,omitempty" yaml:"relations,omitempty"` + Networks map[string][]string `json:"networks,omitempty" yaml:"networks,omitempty"` + SubordinateTo []string `json:"subordinate-to,omitempty" yaml:"subordinate-to,omitempty"` + Units map[string]unitStatus `json:"units,omitempty" yaml:"units,omitempty"` +} + +type serviceStatusNoMarshal serviceStatus + +func (s serviceStatus) MarshalJSON() ([]byte, error) { + if s.Err != nil { + return json.Marshal(errorStatus{s.Err.Error()}) + } + type ssNoMethods serviceStatus + return json.Marshal(ssNoMethods(s)) +} + +func (s serviceStatus) GetYAML() (tag string, value interface{}) { + if s.Err != nil { + return "", errorStatus{s.Err.Error()} + } + type ssNoMethods serviceStatus + return "", ssNoMethods(s) +} + +type meterStatus struct { + Color string `json:"color,omitempty" yaml:"color,omitempty"` + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +type unitStatus struct { + // New Juju Health Status fields. + WorkloadStatusInfo statusInfoContents `json:"workload-status,omitempty" yaml:"workload-status,omitempty"` + AgentStatusInfo statusInfoContents `json:"agent-status,omitempty" yaml:"agent-status,omitempty"` + MeterStatus *meterStatus `json:"meter-status,omitempty" yaml:"meter-status,omitempty"` + + // Legacy status fields, to be removed in Juju 2.0 + AgentState params.Status `json:"agent-state,omitempty" yaml:"agent-state,omitempty"` + AgentStateInfo string `json:"agent-state-info,omitempty" yaml:"agent-state-info,omitempty"` + Err error `json:"-" yaml:",omitempty"` + AgentVersion string `json:"agent-version,omitempty" yaml:"agent-version,omitempty"` + Life string `json:"life,omitempty" yaml:"life,omitempty"` + + Charm string `json:"upgrading-from,omitempty" yaml:"upgrading-from,omitempty"` + Machine string `json:"machine,omitempty" yaml:"machine,omitempty"` + OpenedPorts []string `json:"open-ports,omitempty" yaml:"open-ports,omitempty"` + PublicAddress string `json:"public-address,omitempty" yaml:"public-address,omitempty"` + Subordinates map[string]unitStatus `json:"subordinates,omitempty" yaml:"subordinates,omitempty"` +} + +type statusInfoContents struct { + Err error `json:"-" yaml:",omitempty"` + Current params.Status `json:"current,omitempty" yaml:"current,omitempty"` + Message string `json:"message,omitempty" yaml:"message,omitempty"` + Since string `json:"since,omitempty" yaml:"since,omitempty"` + Version string `json:"version,omitempty" yaml:"version,omitempty"` +} + +type statusInfoContentsNoMarshal statusInfoContents + +func (s statusInfoContents) MarshalJSON() ([]byte, error) { + if s.Err != nil { + return json.Marshal(errorStatus{s.Err.Error()}) + } + return json.Marshal(statusInfoContentsNoMarshal(s)) +} + +func (s statusInfoContents) GetYAML() (tag string, value interface{}) { + if s.Err != nil { + return "", errorStatus{s.Err.Error()} + } + type sicNoMethods statusInfoContents + return "", sicNoMethods(s) +} + +type unitStatusNoMarshal unitStatus + +func (s unitStatus) MarshalJSON() ([]byte, error) { + if s.Err != nil { + return json.Marshal(errorStatus{s.Err.Error()}) + } + return json.Marshal(unitStatusNoMarshal(s)) +} + +func (s unitStatus) GetYAML() (tag string, value interface{}) { + if s.Err != nil { + return "", errorStatus{s.Err.Error()} + } + type usNoMethods unitStatus + return "", usNoMethods(s) +} + +type networkStatus struct { + Err error `json:"-" yaml:",omitempty"` + ProviderId network.Id `json:"provider-id" yaml:"provider-id"` + CIDR string `json:"cidr,omitempty" yaml:"cidr,omitempty"` + VLANTag int `json:"vlan-tag,omitempty" yaml:"vlan-tag,omitempty"` +} + +type networkStatusNoMarshal networkStatus + +func (n networkStatus) MarshalJSON() ([]byte, error) { + if n.Err != nil { + return json.Marshal(errorStatus{n.Err.Error()}) + } + type nNoMethods networkStatus + return json.Marshal(nNoMethods(n)) +} + +func (n networkStatus) GetYAML() (tag string, value interface{}) { + if n.Err != nil { + return "", errorStatus{n.Err.Error()} + } + type nNoMethods networkStatus + return "", nNoMethods(n) +} === added file 'src/github.com/juju/juju/cmd/juju/status/formatter.go' --- src/github.com/juju/juju/cmd/juju/status/formatter.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/status/formatter.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,311 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package status + +import ( + "fmt" + + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/juju/common" + "github.com/juju/juju/state/multiwatcher" +) + +type statusFormatter struct { + status *params.FullStatus + relations map[int]params.RelationStatus + isoTime bool + compatVersion int +} + +func newStatusFormatter(status *params.FullStatus, compatVersion int, isoTime bool) *statusFormatter { + sf := statusFormatter{ + status: status, + relations: make(map[int]params.RelationStatus), + compatVersion: compatVersion, + isoTime: isoTime, + } + for _, relation := range status.Relations { + sf.relations[relation.Id] = relation + } + return &sf +} + +func (sf *statusFormatter) format() formattedStatus { + if sf.status == nil { + return formattedStatus{} + } + out := formattedStatus{ + Environment: sf.status.EnvironmentName, + Machines: make(map[string]machineStatus), + Services: make(map[string]serviceStatus), + } + if sf.status.AvailableVersion != "" { + out.EnvironmentStatus = &environmentStatus{ + AvailableVersion: sf.status.AvailableVersion, + } + } + + for k, m := range sf.status.Machines { + out.Machines[k] = sf.formatMachine(m) + } + for sn, s := range sf.status.Services { + out.Services[sn] = sf.formatService(sn, s) + } + for k, n := range sf.status.Networks { + if out.Networks == nil { + out.Networks = make(map[string]networkStatus) + } + out.Networks[k] = sf.formatNetwork(n) + } + return out +} + +func (sf *statusFormatter) formatMachine(machine params.MachineStatus) machineStatus { + var out machineStatus + + if machine.Agent.Status == "" { + // Older server + // TODO: this will go away at some point (v1.21?). + out = machineStatus{ + AgentState: machine.AgentState, + AgentStateInfo: machine.AgentStateInfo, + AgentVersion: machine.AgentVersion, + Life: machine.Life, + Err: machine.Err, + DNSName: machine.DNSName, + InstanceId: machine.InstanceId, + InstanceState: machine.InstanceState, + Series: machine.Series, + Id: machine.Id, + Containers: make(map[string]machineStatus), + Hardware: machine.Hardware, + } + } else { + // New server + agent := machine.Agent + out = machineStatus{ + AgentState: agent.Status, + AgentStateInfo: adjustInfoIfMachineAgentDown(machine.AgentState, agent.Status, agent.Info), + AgentVersion: agent.Version, + Life: agent.Life, + Err: agent.Err, + DNSName: machine.DNSName, + InstanceId: machine.InstanceId, + InstanceState: machine.InstanceState, + Series: machine.Series, + Id: machine.Id, + Containers: make(map[string]machineStatus), + Hardware: machine.Hardware, + } + } + + for k, m := range machine.Containers { + out.Containers[k] = sf.formatMachine(m) + } + + for _, job := range machine.Jobs { + if job == multiwatcher.JobManageEnviron { + out.HAStatus = makeHAStatus(machine.HasVote, machine.WantsVote) + break + } + } + return out +} + +func (sf *statusFormatter) formatService(name string, service params.ServiceStatus) serviceStatus { + out := serviceStatus{ + Err: service.Err, + Charm: service.Charm, + Exposed: service.Exposed, + Life: service.Life, + Relations: service.Relations, + Networks: make(map[string][]string), + CanUpgradeTo: service.CanUpgradeTo, + SubordinateTo: service.SubordinateTo, + Units: make(map[string]unitStatus), + StatusInfo: sf.getServiceStatusInfo(service), + } + if len(service.Networks.Enabled) > 0 { + out.Networks["enabled"] = service.Networks.Enabled + } + if len(service.Networks.Disabled) > 0 { + out.Networks["disabled"] = service.Networks.Disabled + } + for k, m := range service.Units { + out.Units[k] = sf.formatUnit(unitFormatInfo{ + unit: m, + unitName: k, + serviceName: name, + meterStatuses: service.MeterStatuses, + }) + } + return out +} + +func (sf *statusFormatter) getServiceStatusInfo(service params.ServiceStatus) statusInfoContents { + info := statusInfoContents{ + Err: service.Status.Err, + Current: service.Status.Status, + Message: service.Status.Info, + Version: service.Status.Version, + } + if service.Status.Since != nil { + info.Since = common.FormatTime(service.Status.Since, sf.isoTime) + } + return info +} + +type unitFormatInfo struct { + unit params.UnitStatus + unitName string + serviceName string + meterStatuses map[string]params.MeterStatus +} + +func (sf *statusFormatter) formatUnit(info unitFormatInfo) unitStatus { + // TODO(Wallyworld) - this should be server side but we still need to support older servers. + sf.updateUnitStatusInfo(&info.unit, info.serviceName) + + out := unitStatus{ + WorkloadStatusInfo: sf.getWorkloadStatusInfo(info.unit), + AgentStatusInfo: sf.getAgentStatusInfo(info.unit), + Machine: info.unit.Machine, + OpenedPorts: info.unit.OpenedPorts, + PublicAddress: info.unit.PublicAddress, + Charm: info.unit.Charm, + Subordinates: make(map[string]unitStatus), + } + + if ms, ok := info.meterStatuses[info.unitName]; ok { + out.MeterStatus = &meterStatus{ + Color: ms.Color, + Message: ms.Message, + } + } + + // These legacy fields will be dropped for Juju 2.0. + if sf.compatVersion < 2 || out.AgentStatusInfo.Current == "" { + out.Err = info.unit.Err + out.AgentState = info.unit.AgentState + out.AgentStateInfo = info.unit.AgentStateInfo + out.Life = info.unit.Life + out.AgentVersion = info.unit.AgentVersion + } + + for k, m := range info.unit.Subordinates { + out.Subordinates[k] = sf.formatUnit(unitFormatInfo{ + unit: m, + unitName: k, + serviceName: info.serviceName, + meterStatuses: info.meterStatuses, + }) + } + return out +} + +func (sf *statusFormatter) getWorkloadStatusInfo(unit params.UnitStatus) statusInfoContents { + info := statusInfoContents{ + Err: unit.Workload.Err, + Current: unit.Workload.Status, + Message: unit.Workload.Info, + Version: unit.Workload.Version, + } + if unit.Workload.Since != nil { + info.Since = common.FormatTime(unit.Workload.Since, sf.isoTime) + } + return info +} + +func (sf *statusFormatter) getAgentStatusInfo(unit params.UnitStatus) statusInfoContents { + info := statusInfoContents{ + Err: unit.UnitAgent.Err, + Current: unit.UnitAgent.Status, + Message: unit.UnitAgent.Info, + Version: unit.UnitAgent.Version, + } + if unit.UnitAgent.Since != nil { + info.Since = common.FormatTime(unit.UnitAgent.Since, sf.isoTime) + } + return info +} + +func (sf *statusFormatter) updateUnitStatusInfo(unit *params.UnitStatus, serviceName string) { + // This logic has no business here but can't be moved until Juju 2.0. + statusInfo := unit.Workload.Info + if unit.Workload.Status == "" { + // Old server that doesn't support this field and others. + // Just use the info string as-is. + statusInfo = unit.AgentStateInfo + } + if unit.Workload.Status == params.StatusError { + if relation, ok := sf.relations[getRelationIdFromData(unit)]; ok { + // Append the details of the other endpoint on to the status info string. + if ep, ok := findOtherEndpoint(relation.Endpoints, serviceName); ok { + unit.Workload.Info = statusInfo + " for " + ep.String() + unit.AgentStateInfo = unit.Workload.Info + } + } + } +} + +func (sf *statusFormatter) formatNetwork(network params.NetworkStatus) networkStatus { + return networkStatus{ + Err: network.Err, + ProviderId: network.ProviderId, + CIDR: network.CIDR, + VLANTag: network.VLANTag, + } +} + +func makeHAStatus(hasVote, wantsVote bool) string { + var s string + switch { + case hasVote && wantsVote: + s = "has-vote" + case hasVote && !wantsVote: + s = "removing-vote" + case !hasVote && wantsVote: + s = "adding-vote" + case !hasVote && !wantsVote: + s = "no-vote" + } + return s +} + +func getRelationIdFromData(unit *params.UnitStatus) int { + if relationId_, ok := unit.Workload.Data["relation-id"]; ok { + if relationId, ok := relationId_.(float64); ok { + return int(relationId) + } else { + logger.Infof("relation-id found status data but was unexpected "+ + "type: %q. Status output may be lacking some detail.", relationId_) + } + } + return -1 +} + +// findOtherEndpoint searches the provided endpoints for an endpoint +// that *doesn't* match serviceName. The returned bool indicates if +// such an endpoint was found. +func findOtherEndpoint(endpoints []params.EndpointStatus, serviceName string) (params.EndpointStatus, bool) { + for _, endpoint := range endpoints { + if endpoint.ServiceName != serviceName { + return endpoint, true + } + } + return params.EndpointStatus{}, false +} + +// adjustInfoIfMachineAgentDown modifies the agent status info string if the +// agent is down. The original status and info is included in +// parentheses. +func adjustInfoIfMachineAgentDown(status, origStatus params.Status, info string) string { + if status == params.StatusDown { + if info == "" { + return fmt.Sprintf("(%s)", origStatus) + } + return fmt.Sprintf("(%s: %s)", origStatus, info) + } + return info +} === added file 'src/github.com/juju/juju/cmd/juju/status/history.go' --- src/github.com/juju/juju/cmd/juju/status/history.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/status/history.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,119 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package status + +import ( + "fmt" + "os" + "strconv" + + "github.com/juju/cmd" + "github.com/juju/errors" + "launchpad.net/gnuflag" + + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/common" + "github.com/juju/juju/juju/osenv" +) + +type StatusHistoryCommand struct { + envcmd.EnvCommandBase + out cmd.Output + outputContent string + backlogSize int + isoTime bool + unitName string +} + +var statusHistoryDoc = ` +This command will report the history of status changes for +a given unit. +The statuses for the unit workload and/or agent are available. +-type supports: + agent: will show statuses for the unit's agent + workload: will show statuses for the unit's workload + combined: will show agent and workload statuses combined + and sorted by time of occurrence. +` + +func (c *StatusHistoryCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "status-history", + Args: "[-n N] ", + Purpose: "output past statuses for a unit", + Doc: statusHistoryDoc, + } +} + +func (c *StatusHistoryCommand) SetFlags(f *gnuflag.FlagSet) { + f.StringVar(&c.outputContent, "type", "combined", "type of statuses to be displayed [agent|workload|combined].") + f.IntVar(&c.backlogSize, "n", 20, "size of logs backlog.") + f.BoolVar(&c.isoTime, "utc", false, "display time as UTC in RFC3339 format") +} + +func (c *StatusHistoryCommand) Init(args []string) error { + switch { + case len(args) > 1: + return errors.Errorf("unexpected arguments after unit name.") + case len(args) == 0: + return errors.Errorf("unit name is missing.") + default: + c.unitName = args[0] + } + // If use of ISO time not specified on command line, + // check env var. + if !c.isoTime { + var err error + envVarValue := os.Getenv(osenv.JujuStatusIsoTimeEnvKey) + if envVarValue != "" { + if c.isoTime, err = strconv.ParseBool(envVarValue); err != nil { + return errors.Annotatef(err, "invalid %s env var, expected true|false", osenv.JujuStatusIsoTimeEnvKey) + } + } + } + kind := params.HistoryKind(c.outputContent) + switch kind { + case params.KindCombined, params.KindAgent, params.KindWorkload: + return nil + + } + return errors.Errorf("unexpected status type %q", c.outputContent) +} + +func (c *StatusHistoryCommand) Run(ctx *cmd.Context) error { + apiclient, err := c.NewAPIClient() + if err != nil { + return fmt.Errorf(connectionError, c.ConnectionName(), err) + } + defer apiclient.Close() + var statuses *params.UnitStatusHistory + kind := params.HistoryKind(c.outputContent) + statuses, err = apiclient.UnitStatusHistory(kind, c.unitName, c.backlogSize) + if err != nil { + if len(statuses.Statuses) == 0 { + return errors.Trace(err) + } + // Display any error, but continue to print status if some was returned + fmt.Fprintf(ctx.Stderr, "%v\n", err) + } else if len(statuses.Statuses) == 0 { + return errors.Errorf("no status history available") + } + table := [][]string{{"TIME", "TYPE", "STATUS", "MESSAGE"}} + lengths := []int{1, 1, 1, 1} + for _, v := range statuses.Statuses { + fields := []string{common.FormatTime(v.Since, c.isoTime), string(v.Kind), string(v.Status), v.Info} + for k, v := range fields { + if len(v) > lengths[k] { + lengths[k] = len(v) + } + } + table = append(table, fields) + } + f := fmt.Sprintf("%%-%ds\t%%-%ds\t%%-%ds\t%%-%ds\n", lengths[0], lengths[1], lengths[2], lengths[3]) + for _, v := range table { + fmt.Printf(f, v[0], v[1], v[2], v[3]) + } + return nil +} === added file 'src/github.com/juju/juju/cmd/juju/status/output_oneline.go' --- src/github.com/juju/juju/cmd/juju/status/output_oneline.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/status/output_oneline.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,74 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package status + +import ( + "bytes" + "fmt" + "strings" + + "github.com/juju/errors" + "github.com/juju/juju/cmd/juju/common" +) + +// FormatOneline returns a brief list of units and their subordinates. +// Subordinates will be indented 2 spaces and listed under their +// superiors. +func FormatOneline(value interface{}) ([]byte, error) { + return formatOneline(value, func(out *bytes.Buffer, format, uName string, u unitStatus, level int) { + fmt.Fprintf(out, format, + uName, + u.PublicAddress, + u.AgentState, + ) + }) +} + +// FormatOnelineV2 returns a brief list of units and their subordinates. +// Subordinates will be indented 2 spaces and listed under their +// superiors. This format works with version 2 of the CLI. +func FormatOnelineV2(value interface{}) ([]byte, error) { + return formatOneline(value, func(out *bytes.Buffer, format, uName string, u unitStatus, level int) { + status := fmt.Sprintf( + "agent:%s, workload:%s", + u.AgentStatusInfo.Current, + u.WorkloadStatusInfo.Current, + ) + fmt.Fprintf(out, format, + uName, + u.PublicAddress, + status, + ) + }) +} + +type onelinePrintf func(out *bytes.Buffer, format, uName string, u unitStatus, level int) + +func formatOneline(value interface{}, printf onelinePrintf) ([]byte, error) { + fs, valueConverted := value.(formattedStatus) + if !valueConverted { + return nil, errors.Errorf("expected value of type %T, got %T", fs, value) + } + var out bytes.Buffer + + pprint := func(uName string, u unitStatus, level int) { + var fmtPorts string + if len(u.OpenedPorts) > 0 { + fmtPorts = fmt.Sprintf(" %s", strings.Join(u.OpenedPorts, ", ")) + } + format := indent("\n", level*2, "- %s: %s (%v)"+fmtPorts) + printf(&out, format, uName, u, level) + } + + for _, svcName := range common.SortStringsNaturally(stringKeysFromMap(fs.Services)) { + svc := fs.Services[svcName] + for _, uName := range common.SortStringsNaturally(stringKeysFromMap(svc.Units)) { + unit := svc.Units[uName] + pprint(uName, unit, 0) + recurseUnits(unit, 1, pprint) + } + } + + return out.Bytes(), nil +} === added file 'src/github.com/juju/juju/cmd/juju/status/output_summary.go' --- src/github.com/juju/juju/cmd/juju/status/output_summary.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/status/output_summary.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,190 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package status + +import ( + "bytes" + "fmt" + "net" + "strings" + "text/tabwriter" + + "github.com/juju/errors" + "github.com/juju/utils/set" + + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/juju/common" +) + +// FormatSummary returns a summary of the current environment +// including the following information: +// - Headers: +// - All subnets the environment occupies. +// - All ports the environment utilizes. +// - Sections: +// - Machines: Displays total #, and then the # in each state. +// - Units: Displays total #, and then # in each state. +// - Services: Displays total #, their names, and how many of each +// are exposed. +func FormatSummary(value interface{}) ([]byte, error) { + fs, valueConverted := value.(formattedStatus) + if !valueConverted { + return nil, errors.Errorf("expected value of type %T, got %T", fs, value) + } + + f := newSummaryFormatter() + stateToMachine := f.aggregateMachineStates(fs.Machines) + svcExposure := f.aggregateServiceAndUnitStates(fs.Services) + p := f.delimitValuesWithTabs + + // Print everything out + p("Running on subnets:", strings.Join(f.netStrings, ", ")) + p("Utilizing ports:", f.portsInColumnsOf(3)) + f.tw.Flush() + + // Right align summary information + f.tw.Init(&f.out, 0, 2, 1, ' ', tabwriter.AlignRight) + p("# MACHINES:", fmt.Sprintf("(%d)", len(fs.Machines))) + f.printStateToCount(stateToMachine) + p(" ") + + p("# UNITS:", fmt.Sprintf("(%d)", f.numUnits)) + f.printStateToCount(f.stateToUnit) + p(" ") + + p("# SERVICES:", fmt.Sprintf(" (%d)", len(fs.Services))) + for _, svcName := range common.SortStringsNaturally(stringKeysFromMap(svcExposure)) { + s := svcExposure[svcName] + p(svcName, fmt.Sprintf("%d/%d\texposed", s[true], s[true]+s[false])) + } + f.tw.Flush() + + return f.out.Bytes(), nil +} + +func newSummaryFormatter() *summaryFormatter { + f := &summaryFormatter{ + ipAddrs: make([]net.IPNet, 0), + netStrings: make([]string, 0), + openPorts: set.NewStrings(), + stateToUnit: make(map[params.Status]int), + } + f.tw = tabwriter.NewWriter(&f.out, 0, 1, 1, ' ', 0) + return f +} + +type summaryFormatter struct { + ipAddrs []net.IPNet + netStrings []string + numUnits int + openPorts set.Strings + // status -> count + stateToUnit map[params.Status]int + tw *tabwriter.Writer + out bytes.Buffer +} + +func (f *summaryFormatter) delimitValuesWithTabs(values ...string) { + for _, v := range values { + fmt.Fprintf(f.tw, "%s\t", v) + } + fmt.Fprintln(f.tw) +} + +func (f *summaryFormatter) portsInColumnsOf(col int) string { + + var b bytes.Buffer + for i, p := range f.openPorts.SortedValues() { + if i != 0 && i%col == 0 { + fmt.Fprintf(&b, "\n\t") + } + fmt.Fprintf(&b, "%s, ", p) + } + // Elide the last delimiter + portList := b.String() + if len(portList) >= 2 { + return portList[:b.Len()-2] + } + return portList +} + +func (f *summaryFormatter) trackUnit(name string, status unitStatus, indentLevel int) { + f.resolveAndTrackIp(status.PublicAddress) + + for _, p := range status.OpenedPorts { + if p != "" { + f.openPorts.Add(p) + } + } + f.numUnits++ + f.stateToUnit[status.AgentState]++ +} + +func (f *summaryFormatter) printStateToCount(m map[params.Status]int) { + for _, status := range common.SortStringsNaturally(stringKeysFromMap(m)) { + numInStatus := m[params.Status(status)] + f.delimitValuesWithTabs(status+":", fmt.Sprintf(" %d ", numInStatus)) + } +} + +func (f *summaryFormatter) trackIp(ip net.IP) { + for _, net := range f.ipAddrs { + if net.Contains(ip) { + return + } + } + + ipNet := net.IPNet{ip, ip.DefaultMask()} + f.ipAddrs = append(f.ipAddrs, ipNet) + f.netStrings = append(f.netStrings, ipNet.String()) +} + +func (f *summaryFormatter) resolveAndTrackIp(publicDns string) { + // TODO(katco-): We may be able to utilize upcoming work which will expose these addresses outright. + ip, err := net.ResolveIPAddr("ip4", publicDns) + if err != nil { + logger.Warningf( + "unable to resolve %s to an IP address. Status may be incorrect: %v", + publicDns, + err, + ) + return + } + f.trackIp(ip.IP) +} + +func (f *summaryFormatter) aggregateMachineStates(machines map[string]machineStatus) map[params.Status]int { + stateToMachine := make(map[params.Status]int) + for _, name := range common.SortStringsNaturally(stringKeysFromMap(machines)) { + m := machines[name] + f.resolveAndTrackIp(m.DNSName) + + if agentState := m.AgentState; agentState == "" { + agentState = params.StatusPending + } else { + stateToMachine[agentState]++ + } + } + return stateToMachine +} + +func (f *summaryFormatter) aggregateServiceAndUnitStates(services map[string]serviceStatus) map[string]map[bool]int { + svcExposure := make(map[string]map[bool]int) + for _, name := range common.SortStringsNaturally(stringKeysFromMap(services)) { + s := services[name] + // Grab unit states + for _, un := range common.SortStringsNaturally(stringKeysFromMap(s.Units)) { + u := s.Units[un] + f.trackUnit(un, u, 0) + recurseUnits(u, 1, f.trackUnit) + } + + if _, ok := svcExposure[name]; !ok { + svcExposure[name] = make(map[bool]int) + } + + svcExposure[name][s.Exposed]++ + } + return svcExposure +} === added file 'src/github.com/juju/juju/cmd/juju/status/output_tabular.go' --- src/github.com/juju/juju/cmd/juju/status/output_tabular.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/status/output_tabular.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,141 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package status + +import ( + "bytes" + "fmt" + "regexp" + "strings" + "text/tabwriter" + + "github.com/juju/errors" + "gopkg.in/juju/charm.v5/hooks" + + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/juju/common" +) + +// FormatTabular returns a tabular summary of machines, services, and +// units. Any subordinate items are indented by two spaces beneath +// their superior. +func FormatTabular(value interface{}) ([]byte, error) { + fs, valueConverted := value.(formattedStatus) + if !valueConverted { + return nil, errors.Errorf("expected value of type %T, got %T", fs, value) + } + var out bytes.Buffer + // To format things into columns. + tw := tabwriter.NewWriter(&out, 0, 1, 1, ' ', 0) + p := func(values ...interface{}) { + for _, v := range values { + fmt.Fprintf(tw, "%s\t", v) + } + fmt.Fprintln(tw) + } + + if envStatus := fs.EnvironmentStatus; envStatus != nil { + p("[Environment]") + if envStatus.AvailableVersion != "" { + p("UPGRADE-AVAILABLE") + p(envStatus.AvailableVersion) + } + p() + tw.Flush() + } + + units := make(map[string]unitStatus) + p("[Services]") + p("NAME\tSTATUS\tEXPOSED\tCHARM") + for _, svcName := range common.SortStringsNaturally(stringKeysFromMap(fs.Services)) { + svc := fs.Services[svcName] + for un, u := range svc.Units { + units[un] = u + } + p(svcName, svc.StatusInfo.Current, fmt.Sprintf("%t", svc.Exposed), svc.Charm) + } + tw.Flush() + + pUnit := func(name string, u unitStatus, level int) { + message := u.WorkloadStatusInfo.Message + agentDoing := agentDoing(u.AgentStatusInfo) + if agentDoing != "" { + message = fmt.Sprintf("(%s) %s", agentDoing, message) + } + p( + indent("", level*2, name), + u.WorkloadStatusInfo.Current, + u.AgentStatusInfo.Current, + u.AgentStatusInfo.Version, + u.Machine, + strings.Join(u.OpenedPorts, ","), + u.PublicAddress, + message, + ) + } + + // See if we have new or old data; that determines what data we can display. + newStatus := false + for _, u := range units { + if u.AgentStatusInfo.Current != "" { + newStatus = true + break + } + } + var header []string + if newStatus { + header = []string{"ID", "WORKLOAD-STATE", "AGENT-STATE", "VERSION", "MACHINE", "PORTS", "PUBLIC-ADDRESS", "MESSAGE"} + } else { + header = []string{"ID", "STATE", "VERSION", "MACHINE", "PORTS", "PUBLIC-ADDRESS"} + } + + p("\n[Units]") + p(strings.Join(header, "\t")) + for _, name := range common.SortStringsNaturally(stringKeysFromMap(units)) { + u := units[name] + pUnit(name, u, 0) + const indentationLevel = 1 + recurseUnits(u, indentationLevel, pUnit) + } + tw.Flush() + + p("\n[Machines]") + p("ID\tSTATE\tVERSION\tDNS\tINS-ID\tSERIES\tHARDWARE") + for _, name := range common.SortStringsNaturally(stringKeysFromMap(fs.Machines)) { + m := fs.Machines[name] + p(m.Id, m.AgentState, m.AgentVersion, m.DNSName, m.InstanceId, m.Series, m.Hardware) + } + tw.Flush() + + return out.Bytes(), nil +} + +// agentDoing returns what hook or action, if any, +// the agent is currently executing. +// The hook name or action is extracted from the agent message. +func agentDoing(status statusInfoContents) string { + if status.Current != params.StatusExecuting { + return "" + } + // First see if we can determine a hook name. + var hookNames []string + for _, h := range hooks.UnitHooks() { + hookNames = append(hookNames, string(h)) + } + for _, h := range hooks.RelationHooks() { + hookNames = append(hookNames, string(h)) + } + hookExp := regexp.MustCompile(fmt.Sprintf(`running (?P%s?) hook`, strings.Join(hookNames, "|"))) + match := hookExp.FindStringSubmatch(status.Message) + if len(match) > 0 { + return match[1] + } + // Now try for an action name. + actionExp := regexp.MustCompile(`running action (?P.*)`) + match = actionExp.FindStringSubmatch(status.Message) + if len(match) > 0 { + return match[1] + } + return "" +} === added file 'src/github.com/juju/juju/cmd/juju/status/package_test.go' --- src/github.com/juju/juju/cmd/juju/status/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/status/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,14 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package status + +import ( + stdtesting "testing" + + "github.com/juju/juju/testing" +) + +func TestPackage(t *stdtesting.T) { + testing.MgoTestPackage(t) +} === added file 'src/github.com/juju/juju/cmd/juju/status/status.go' --- src/github.com/juju/juju/cmd/juju/status/status.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/status/status.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,147 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package status + +import ( + "fmt" + "os" + "strconv" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/loggo" + "launchpad.net/gnuflag" + + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/juju/osenv" +) + +var logger = loggo.GetLogger("juju.cmd.juju.status") + +type StatusCommand struct { + envcmd.EnvCommandBase + out cmd.Output + patterns []string + isoTime bool +} + +var statusDoc = ` +This command will report on the runtime state of various system entities. + +There are a number of ways to format the status output: + +- {short|line|oneline}: List units and their subordinates. For each + unit, the IP address and agent status are listed. +- summary: Displays the subnet(s) and port(s) the environment utilises. + Also displays aggregate information about: + - MACHINES: total #, and # in each state. + - UNITS: total #, and # in each state. + - SERVICES: total #, and # exposed of each service. +- tabular: Displays information in a tabular format in these sections: + - Machines: ID, STATE, VERSION, DNS, INS-ID, SERIES, HARDWARE + - Services: NAME, EXPOSED, CHARM + - Units: ID, STATE, VERSION, MACHINE, PORTS, PUBLIC-ADDRESS + - Also displays subordinate units. +- yaml (DEFAULT): Displays information on machines, services, and units + in the yaml format. + +Service or unit names may be specified to filter the status to only those +services and units that match, along with the related machines, services +and units. If a subordinate unit is matched, then its principal unit will +be displayed. If a principal unit is matched, then all of its subordinates +will be displayed. + +Wildcards ('*') may be specified in service/unit names to match any sequence +of characters. For example, 'nova-*' will match any service whose name begins +with 'nova-': 'nova-compute', 'nova-volume', etc. +` + +func (c *StatusCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "status", + Args: "[pattern ...]", + Purpose: "output status information about an environment", + Doc: statusDoc, + Aliases: []string{"stat"}, + } +} + +func (c *StatusCommand) SetFlags(f *gnuflag.FlagSet) { + f.BoolVar(&c.isoTime, "utc", false, "display time as UTC in RFC3339 format") + + oneLineFormatter := FormatOneline + defaultFormat := "yaml" + if c.CompatVersion() > 1 { + defaultFormat = "tabular" + oneLineFormatter = FormatOnelineV2 + } + + c.out.AddFlags(f, defaultFormat, map[string]cmd.Formatter{ + "yaml": cmd.FormatYaml, + "json": cmd.FormatJson, + "short": oneLineFormatter, + "oneline": oneLineFormatter, + "line": oneLineFormatter, + "tabular": FormatTabular, + "summary": FormatSummary, + }) +} + +func (c *StatusCommand) Init(args []string) error { + c.patterns = args + // If use of ISO time not specified on command line, + // check env var. + if !c.isoTime { + var err error + envVarValue := os.Getenv(osenv.JujuStatusIsoTimeEnvKey) + if envVarValue != "" { + if c.isoTime, err = strconv.ParseBool(envVarValue); err != nil { + return errors.Annotatef(err, "invalid %s env var, expected true|false", osenv.JujuStatusIsoTimeEnvKey) + } + } + } + return nil +} + +var connectionError = `Unable to connect to environment %q. +Please check your credentials or use 'juju bootstrap' to create a new environment. + +Error details: +%v +` + +type statusAPI interface { + Status(patterns []string) (*params.FullStatus, error) + Close() error +} + +var newApiClientForStatus = func(c *StatusCommand) (statusAPI, error) { + return c.NewAPIClient() +} + +func (c *StatusCommand) Run(ctx *cmd.Context) error { + + apiclient, err := newApiClientForStatus(c) + if err != nil { + return errors.Errorf(connectionError, c.ConnectionName(), err) + } + defer apiclient.Close() + + status, err := apiclient.Status(c.patterns) + if err != nil { + if status == nil { + // Status call completely failed, there is nothing to report + return err + } + // Display any error, but continue to print status if some was returned + fmt.Fprintf(ctx.Stderr, "%v\n", err) + } else if status == nil { + return errors.Errorf("unable to obtain the current status") + } + + formatter := newStatusFormatter(status, c.CompatVersion(), c.isoTime) + formatted := formatter.format() + return c.out.Write(ctx, formatted) +} === added file 'src/github.com/juju/juju/cmd/juju/status/status_test.go' --- src/github.com/juju/juju/cmd/juju/status/status_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/status/status_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,3752 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package status + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "regexp" + "strings" + "time" + + "github.com/juju/cmd" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/juju/charm.v5" + goyaml "gopkg.in/yaml.v1" + + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/constraints" + "github.com/juju/juju/environs" + "github.com/juju/juju/instance" + "github.com/juju/juju/juju/osenv" + "github.com/juju/juju/juju/testing" + "github.com/juju/juju/network" + "github.com/juju/juju/state" + "github.com/juju/juju/state/multiwatcher" + "github.com/juju/juju/state/presence" + "github.com/juju/juju/testcharms" + coretesting "github.com/juju/juju/testing" + "github.com/juju/juju/version" +) + +func nextVersion() version.Number { + ver := version.Current.Number + ver.Patch++ + return ver +} + +func runStatus(c *gc.C, args ...string) (code int, stdout, stderr []byte) { + ctx := coretesting.Context(c) + code = cmd.Main(envcmd.Wrap(&StatusCommand{}), ctx, args) + stdout = ctx.Stdout.(*bytes.Buffer).Bytes() + stderr = ctx.Stderr.(*bytes.Buffer).Bytes() + return +} + +type StatusSuite struct { + testing.JujuConnSuite +} + +var _ = gc.Suite(&StatusSuite{}) + +type M map[string]interface{} + +type L []interface{} + +type testCase struct { + summary string + steps []stepper +} + +func test(summary string, steps ...stepper) testCase { + return testCase{summary, steps} +} + +type stepper interface { + step(c *gc.C, ctx *context) +} + +// +// context +// + +func newContext(c *gc.C, st *state.State, env environs.Environ, adminUserTag string) *context { + // We make changes in the API server's state so that + // our changes to presence are immediately noticed + // in the status. + return &context{ + st: st, + env: env, + charms: make(map[string]*state.Charm), + pingers: make(map[string]*presence.Pinger), + adminUserTag: adminUserTag, + } +} + +type context struct { + st *state.State + env environs.Environ + charms map[string]*state.Charm + pingers map[string]*presence.Pinger + adminUserTag string // A string repr of the tag. + expectIsoTime bool +} + +func (ctx *context) reset(c *gc.C) { + for _, up := range ctx.pingers { + err := up.Kill() + c.Check(err, jc.ErrorIsNil) + } +} + +func (ctx *context) run(c *gc.C, steps []stepper) { + for i, s := range steps { + c.Logf("step %d", i) + c.Logf("%#v", s) + s.step(c, ctx) + } +} + +func (ctx *context) setAgentPresence(c *gc.C, p presence.Presencer) *presence.Pinger { + pinger, err := p.SetAgentPresence() + c.Assert(err, jc.ErrorIsNil) + ctx.st.StartSync() + err = p.WaitAgentPresence(coretesting.LongWait) + c.Assert(err, jc.ErrorIsNil) + agentPresence, err := p.AgentPresence() + c.Assert(err, jc.ErrorIsNil) + c.Assert(agentPresence, jc.IsTrue) + return pinger +} + +func (s *StatusSuite) newContext(c *gc.C) *context { + st := s.Environ.(testing.GetStater).GetStateInAPIServer() + + // We make changes in the API server's state so that + // our changes to presence are immediately noticed + // in the status. + return newContext(c, st, s.Environ, s.AdminUserTag(c).String()) +} + +func (s *StatusSuite) resetContext(c *gc.C, ctx *context) { + ctx.reset(c) + s.JujuConnSuite.Reset(c) +} + +// shortcuts for expected output. +var ( + machine0 = M{ + "agent-state": "started", + "dns-name": "dummyenv-0.dns", + "instance-id": "dummyenv-0", + "series": "quantal", + "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", + "state-server-member-status": "adding-vote", + } + machine1 = M{ + "agent-state": "started", + "dns-name": "dummyenv-1.dns", + "instance-id": "dummyenv-1", + "series": "quantal", + "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", + } + machine2 = M{ + "agent-state": "started", + "dns-name": "dummyenv-2.dns", + "instance-id": "dummyenv-2", + "series": "quantal", + "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", + } + machine3 = M{ + "agent-state": "started", + "dns-name": "dummyenv-3.dns", + "instance-id": "dummyenv-3", + "series": "quantal", + "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", + } + machine4 = M{ + "agent-state": "started", + "dns-name": "dummyenv-4.dns", + "instance-id": "dummyenv-4", + "series": "quantal", + "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", + } + machine1WithContainers = M{ + "agent-state": "started", + "containers": M{ + "1/lxc/0": M{ + "agent-state": "started", + "containers": M{ + "1/lxc/0/lxc/0": M{ + "agent-state": "started", + "dns-name": "dummyenv-3.dns", + "instance-id": "dummyenv-3", + "series": "quantal", + }, + }, + "dns-name": "dummyenv-2.dns", + "instance-id": "dummyenv-2", + "series": "quantal", + }, + "1/lxc/1": M{ + "agent-state": "pending", + "instance-id": "pending", + "series": "quantal", + }, + }, + "dns-name": "dummyenv-1.dns", + "instance-id": "dummyenv-1", + "series": "quantal", + "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", + } + machine1WithContainersScoped = M{ + "agent-state": "started", + "containers": M{ + "1/lxc/0": M{ + "agent-state": "started", + "dns-name": "dummyenv-2.dns", + "instance-id": "dummyenv-2", + "series": "quantal", + }, + }, + "dns-name": "dummyenv-1.dns", + "instance-id": "dummyenv-1", + "series": "quantal", + "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", + } + unexposedService = M{ + "service-status": M{ + "current": "unknown", + "message": "Waiting for agent initialization to finish", + "since": "01 Apr 15 01:23+10:00", + }, + "charm": "cs:quantal/dummy-1", + "exposed": false, + } + exposedService = M{ + "service-status": M{ + "current": "unknown", + "message": "Waiting for agent initialization to finish", + "since": "01 Apr 15 01:23+10:00", + }, + "charm": "cs:quantal/dummy-1", + "exposed": true, + } +) + +type outputFormat struct { + name string + marshal func(v interface{}) ([]byte, error) + unmarshal func(data []byte, v interface{}) error +} + +// statusFormats list all output formats that can be marshalled as structured data, +// supported by status command. +var statusFormats = []outputFormat{ + {"yaml", goyaml.Marshal, goyaml.Unmarshal}, + {"json", json.Marshal, json.Unmarshal}, +} + +var machineCons = constraints.MustParse("cpu-cores=2 mem=8G root-disk=8G") + +var statusTests = []testCase{ + // Status tests + test( // 0 + "bootstrap and starting a single instance", + + addMachine{machineId: "0", job: state.JobManageEnviron}, + expect{ + "simulate juju bootstrap by adding machine/0 to the state", + M{ + "environment": "dummyenv", + "machines": M{ + "0": M{ + "agent-state": "pending", + "instance-id": "pending", + "series": "quantal", + "state-server-member-status": "adding-vote", + }, + }, + "services": M{}, + }, + }, + + startAliveMachine{"0"}, + setAddresses{"0", []network.Address{ + network.NewAddress("10.0.0.1"), + network.NewScopedAddress("dummyenv-0.dns", network.ScopePublic), + }}, + expect{ + "simulate the PA starting an instance in response to the state change", + M{ + "environment": "dummyenv", + "machines": M{ + "0": M{ + "agent-state": "pending", + "dns-name": "dummyenv-0.dns", + "instance-id": "dummyenv-0", + "series": "quantal", + "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", + "state-server-member-status": "adding-vote", + }, + }, + "services": M{}, + }, + }, + + setMachineStatus{"0", state.StatusStarted, ""}, + expect{ + "simulate the MA started and set the machine status", + M{ + "environment": "dummyenv", + "machines": M{ + "0": machine0, + }, + "services": M{}, + }, + }, + + setTools{"0", version.MustParseBinary("1.2.3-trusty-ppc")}, + expect{ + "simulate the MA setting the version", + M{ + "environment": "dummyenv", + "machines": M{ + "0": M{ + "dns-name": "dummyenv-0.dns", + "instance-id": "dummyenv-0", + "agent-version": "1.2.3", + "agent-state": "started", + "series": "quantal", + "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", + "state-server-member-status": "adding-vote", + }, + }, + "services": M{}, + }, + }, + ), + test( // 1 + "instance with different hardware characteristics", + addMachine{machineId: "0", cons: machineCons, job: state.JobManageEnviron}, + setAddresses{"0", []network.Address{ + network.NewAddress("10.0.0.1"), + network.NewScopedAddress("dummyenv-0.dns", network.ScopePublic), + }}, + startAliveMachine{"0"}, + setMachineStatus{"0", state.StatusStarted, ""}, + expect{ + "machine 0 has specific hardware characteristics", + M{ + "environment": "dummyenv", + "machines": M{ + "0": M{ + "agent-state": "started", + "dns-name": "dummyenv-0.dns", + "instance-id": "dummyenv-0", + "series": "quantal", + "hardware": "arch=amd64 cpu-cores=2 mem=8192M root-disk=8192M", + "state-server-member-status": "adding-vote", + }, + }, + "services": M{}, + }, + }, + ), + test( // 2 + "instance without addresses", + addMachine{machineId: "0", cons: machineCons, job: state.JobManageEnviron}, + startAliveMachine{"0"}, + setMachineStatus{"0", state.StatusStarted, ""}, + expect{ + "machine 0 has no dns-name", + M{ + "environment": "dummyenv", + "machines": M{ + "0": M{ + "agent-state": "started", + "instance-id": "dummyenv-0", + "series": "quantal", + "hardware": "arch=amd64 cpu-cores=2 mem=8192M root-disk=8192M", + "state-server-member-status": "adding-vote", + }, + }, + "services": M{}, + }, + }, + ), + test( // 3 + "test pending and missing machines", + addMachine{machineId: "0", job: state.JobManageEnviron}, + expect{ + "machine 0 reports pending", + M{ + "environment": "dummyenv", + "machines": M{ + "0": M{ + "agent-state": "pending", + "instance-id": "pending", + "series": "quantal", + "state-server-member-status": "adding-vote", + }, + }, + "services": M{}, + }, + }, + + startMissingMachine{"0"}, + expect{ + "machine 0 reports missing", + M{ + "environment": "dummyenv", + "machines": M{ + "0": M{ + "instance-state": "missing", + "instance-id": "i-missing", + "agent-state": "pending", + "series": "quantal", + "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", + "state-server-member-status": "adding-vote", + }, + }, + "services": M{}, + }, + }, + ), + test( // 4 + "add two services and expose one, then add 2 more machines and some units", + // step 0 + addMachine{machineId: "0", job: state.JobManageEnviron}, + setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, + startAliveMachine{"0"}, + setMachineStatus{"0", state.StatusStarted, ""}, + addCharm{"dummy"}, + addService{name: "dummy-service", charm: "dummy"}, + addService{name: "exposed-service", charm: "dummy"}, + expect{ + "no services exposed yet", + M{ + "environment": "dummyenv", + "machines": M{ + "0": machine0, + }, + "services": M{ + "dummy-service": unexposedService, + "exposed-service": unexposedService, + }, + }, + }, + + // step 8 + setServiceExposed{"exposed-service", true}, + expect{ + "one exposed service", + M{ + "environment": "dummyenv", + "machines": M{ + "0": machine0, + }, + "services": M{ + "dummy-service": unexposedService, + "exposed-service": exposedService, + }, + }, + }, + + // step 10 + addMachine{machineId: "1", job: state.JobHostUnits}, + setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, + startAliveMachine{"1"}, + setMachineStatus{"1", state.StatusStarted, ""}, + addMachine{machineId: "2", job: state.JobHostUnits}, + setAddresses{"2", network.NewAddresses("dummyenv-2.dns")}, + startAliveMachine{"2"}, + setMachineStatus{"2", state.StatusStarted, ""}, + expect{ + "two more machines added", + M{ + "environment": "dummyenv", + "machines": M{ + "0": machine0, + "1": machine1, + "2": machine2, + }, + "services": M{ + "dummy-service": unexposedService, + "exposed-service": exposedService, + }, + }, + }, + + // step 19 + addAliveUnit{"dummy-service", "1"}, + addAliveUnit{"exposed-service", "2"}, + setAgentStatus{"exposed-service/0", state.StatusError, "You Require More Vespene Gas", nil}, + // Open multiple ports with different protocols, + // ensure they're sorted on protocol, then number. + openUnitPort{"exposed-service/0", "udp", 10}, + openUnitPort{"exposed-service/0", "udp", 2}, + openUnitPort{"exposed-service/0", "tcp", 3}, + openUnitPort{"exposed-service/0", "tcp", 2}, + // Simulate some status with no info, while the agent is down. + // Status used to be down, we no longer support said state. + // now is one of: pending, started, error. + setUnitStatus{"dummy-service/0", state.StatusTerminated, "", nil}, + setAgentStatus{"dummy-service/0", state.StatusIdle, "", nil}, + + // dummy-service/0 used to expect "agent-state-info": "(started)", + // which is populated as the previous state by adjustInfoIfAgentDown + // but sice it no longer is down it no longer applies. + expect{ + "add two units, one alive (in error state), one started", + M{ + "environment": "dummyenv", + "machines": M{ + "0": machine0, + "1": machine1, + "2": machine2, + }, + "services": M{ + "exposed-service": M{ + "charm": "cs:quantal/dummy-1", + "exposed": true, + "service-status": M{ + "current": "error", + "message": "You Require More Vespene Gas", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "exposed-service/0": M{ + "machine": "2", + "agent-state": "error", + "agent-state-info": "You Require More Vespene Gas", + "workload-status": M{ + "current": "error", + "message": "You Require More Vespene Gas", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "open-ports": L{ + "2/tcp", "3/tcp", "2/udp", "10/udp", + }, + "public-address": "dummyenv-2.dns", + }, + }, + }, + "dummy-service": M{ + "charm": "cs:quantal/dummy-1", + "exposed": false, + "service-status": M{ + "current": "terminated", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "dummy-service/0": M{ + "machine": "1", + "agent-state": "stopped", + "workload-status": M{ + "current": "terminated", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-1.dns", + }, + }, + }, + }, + }, + }, + + // step 29 + addMachine{machineId: "3", job: state.JobHostUnits}, + startMachine{"3"}, + // Simulate some status with info, while the agent is down. + setAddresses{"3", network.NewAddresses("dummyenv-3.dns")}, + setMachineStatus{"3", state.StatusStopped, "Really?"}, + addMachine{machineId: "4", job: state.JobHostUnits}, + setAddresses{"4", network.NewAddresses("dummyenv-4.dns")}, + startAliveMachine{"4"}, + setMachineStatus{"4", state.StatusError, "Beware the red toys"}, + ensureDyingUnit{"dummy-service/0"}, + addMachine{machineId: "5", job: state.JobHostUnits}, + ensureDeadMachine{"5"}, + expect{ + "add three more machine, one with a dead agent, one in error state and one dead itself; also one dying unit", + M{ + "environment": "dummyenv", + "machines": M{ + "0": machine0, + "1": machine1, + "2": machine2, + "3": M{ + "dns-name": "dummyenv-3.dns", + "instance-id": "dummyenv-3", + "agent-state": "stopped", + "agent-state-info": "(stopped: Really?)", + "series": "quantal", + "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", + }, + "4": M{ + "dns-name": "dummyenv-4.dns", + "instance-id": "dummyenv-4", + "agent-state": "error", + "agent-state-info": "Beware the red toys", + "series": "quantal", + "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", + }, + "5": M{ + "agent-state": "pending", + "life": "dead", + "instance-id": "pending", + "series": "quantal", + }, + }, + "services": M{ + "exposed-service": M{ + "charm": "cs:quantal/dummy-1", + "exposed": true, + "service-status": M{ + "current": "error", + "message": "You Require More Vespene Gas", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "exposed-service/0": M{ + "machine": "2", + "agent-state": "error", + "agent-state-info": "You Require More Vespene Gas", + "workload-status": M{ + "current": "error", + "message": "You Require More Vespene Gas", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "open-ports": L{ + "2/tcp", "3/tcp", "2/udp", "10/udp", + }, + "public-address": "dummyenv-2.dns", + }, + }, + }, + "dummy-service": M{ + "charm": "cs:quantal/dummy-1", + "exposed": false, + "service-status": M{ + "current": "terminated", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "dummy-service/0": M{ + "machine": "1", + "agent-state": "stopped", + "life": "dying", + "workload-status": M{ + "current": "terminated", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-1.dns", + }, + }, + }, + }, + }, + }, + + // step 41 + scopedExpect{ + "scope status on dummy-service/0 unit", + []string{"dummy-service/0"}, + M{ + "environment": "dummyenv", + "machines": M{ + "1": machine1, + }, + "services": M{ + "dummy-service": M{ + "charm": "cs:quantal/dummy-1", + "exposed": false, + "service-status": M{ + "current": "terminated", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "dummy-service/0": M{ + "machine": "1", + "life": "dying", + "agent-state": "stopped", + "workload-status": M{ + "current": "terminated", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-1.dns", + }, + }, + }, + }, + }, + }, + scopedExpect{ + "scope status on exposed-service service", + []string{"exposed-service"}, + M{ + "environment": "dummyenv", + "machines": M{ + "2": machine2, + }, + "services": M{ + "exposed-service": M{ + "charm": "cs:quantal/dummy-1", + "exposed": true, + "service-status": M{ + "current": "error", + "message": "You Require More Vespene Gas", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "exposed-service/0": M{ + "machine": "2", + "agent-state": "error", + "agent-state-info": "You Require More Vespene Gas", + "workload-status": M{ + "current": "error", + "message": "You Require More Vespene Gas", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "open-ports": L{ + "2/tcp", "3/tcp", "2/udp", "10/udp", + }, + "public-address": "dummyenv-2.dns", + }, + }, + }, + }, + }, + }, + scopedExpect{ + "scope status on service pattern", + []string{"d*-service"}, + M{ + "environment": "dummyenv", + "machines": M{ + "1": machine1, + }, + "services": M{ + "dummy-service": M{ + "charm": "cs:quantal/dummy-1", + "exposed": false, + "service-status": M{ + "current": "terminated", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "dummy-service/0": M{ + "machine": "1", + "life": "dying", + "agent-state": "stopped", + "workload-status": M{ + "current": "terminated", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-1.dns", + }, + }, + }, + }, + }, + }, + scopedExpect{ + "scope status on unit pattern", + []string{"e*posed-service/*"}, + M{ + "environment": "dummyenv", + "machines": M{ + "2": machine2, + }, + "services": M{ + "exposed-service": M{ + "charm": "cs:quantal/dummy-1", + "exposed": true, + "service-status": M{ + "current": "error", + "message": "You Require More Vespene Gas", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "exposed-service/0": M{ + "machine": "2", + "agent-state": "error", + "agent-state-info": "You Require More Vespene Gas", + "workload-status": M{ + "current": "error", + "message": "You Require More Vespene Gas", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "open-ports": L{ + "2/tcp", "3/tcp", "2/udp", "10/udp", + }, + "public-address": "dummyenv-2.dns", + }, + }, + }, + }, + }, + }, + scopedExpect{ + "scope status on combination of service and unit patterns", + []string{"exposed-service", "dummy-service", "e*posed-service/*", "dummy-service/*"}, + M{ + "environment": "dummyenv", + "machines": M{ + "1": machine1, + "2": machine2, + }, + "services": M{ + "dummy-service": M{ + "charm": "cs:quantal/dummy-1", + "exposed": false, + "service-status": M{ + "current": "terminated", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "dummy-service/0": M{ + "machine": "1", + "life": "dying", + "agent-state": "stopped", + "workload-status": M{ + "current": "terminated", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-1.dns", + }, + }, + }, + "exposed-service": M{ + "charm": "cs:quantal/dummy-1", + "exposed": true, + "service-status": M{ + "current": "error", + "message": "You Require More Vespene Gas", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "exposed-service/0": M{ + "machine": "2", + "agent-state": "error", + "agent-state-info": "You Require More Vespene Gas", + "workload-status": M{ + "current": "error", + "message": "You Require More Vespene Gas", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "open-ports": L{ + "2/tcp", "3/tcp", "2/udp", "10/udp", + }, + "public-address": "dummyenv-2.dns", + }, + }, + }, + }, + }, + }, + ), + test( // 5 + "a unit with a hook relation error", + addMachine{machineId: "0", job: state.JobManageEnviron}, + setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, + startAliveMachine{"0"}, + setMachineStatus{"0", state.StatusStarted, ""}, + + addMachine{machineId: "1", job: state.JobHostUnits}, + setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, + startAliveMachine{"1"}, + setMachineStatus{"1", state.StatusStarted, ""}, + + addCharm{"wordpress"}, + addService{name: "wordpress", charm: "wordpress"}, + addAliveUnit{"wordpress", "1"}, + + addCharm{"mysql"}, + addService{name: "mysql", charm: "mysql"}, + addAliveUnit{"mysql", "1"}, + + relateServices{"wordpress", "mysql"}, + + setAgentStatus{"wordpress/0", state.StatusError, + "hook failed: some-relation-changed", + map[string]interface{}{"relation-id": 0}}, + + expect{ + "a unit with a hook relation error", + M{ + "environment": "dummyenv", + "machines": M{ + "0": machine0, + "1": machine1, + }, + "services": M{ + "wordpress": M{ + "charm": "cs:quantal/wordpress-3", + "exposed": false, + "relations": M{ + "db": L{"mysql"}, + }, + "service-status": M{ + "current": "error", + "message": "hook failed: some-relation-changed", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "wordpress/0": M{ + "machine": "1", + "agent-state": "error", + "agent-state-info": "hook failed: some-relation-changed for mysql:server", + "workload-status": M{ + "current": "error", + "message": "hook failed: some-relation-changed for mysql:server", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-1.dns", + }, + }, + }, + "mysql": M{ + "charm": "cs:quantal/mysql-1", + "exposed": false, + "relations": M{ + "server": L{"wordpress"}, + }, + "service-status": M{ + "current": "unknown", + "message": "Waiting for agent initialization to finish", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "mysql/0": M{ + "machine": "1", + "agent-state": "pending", + "workload-status": M{ + "current": "unknown", + "message": "Waiting for agent initialization to finish", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "allocating", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-1.dns", + }, + }, + }, + }, + }, + }, + ), + test( // 6 + "a unit with a hook relation error when the agent is down", + addMachine{machineId: "0", job: state.JobManageEnviron}, + setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, + startAliveMachine{"0"}, + setMachineStatus{"0", state.StatusStarted, ""}, + + addMachine{machineId: "1", job: state.JobHostUnits}, + setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, + startAliveMachine{"1"}, + setMachineStatus{"1", state.StatusStarted, ""}, + + addCharm{"wordpress"}, + addService{name: "wordpress", charm: "wordpress"}, + addAliveUnit{"wordpress", "1"}, + + addCharm{"mysql"}, + addService{name: "mysql", charm: "mysql"}, + addAliveUnit{"mysql", "1"}, + + relateServices{"wordpress", "mysql"}, + + setAgentStatus{"wordpress/0", state.StatusError, + "hook failed: some-relation-changed", + map[string]interface{}{"relation-id": 0}}, + + expect{ + "a unit with a hook relation error when the agent is down", + M{ + "environment": "dummyenv", + "machines": M{ + "0": machine0, + "1": machine1, + }, + "services": M{ + "wordpress": M{ + "charm": "cs:quantal/wordpress-3", + "exposed": false, + "relations": M{ + "db": L{"mysql"}, + }, + "service-status": M{ + "current": "error", + "message": "hook failed: some-relation-changed", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "wordpress/0": M{ + "machine": "1", + "agent-state": "error", + "agent-state-info": "hook failed: some-relation-changed for mysql:server", + "workload-status": M{ + "current": "error", + "message": "hook failed: some-relation-changed for mysql:server", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-1.dns", + }, + }, + }, + "mysql": M{ + "charm": "cs:quantal/mysql-1", + "exposed": false, + "relations": M{ + "server": L{"wordpress"}, + }, + "service-status": M{ + "current": "unknown", + "message": "Waiting for agent initialization to finish", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "mysql/0": M{ + "machine": "1", + "agent-state": "pending", + "workload-status": M{ + "current": "unknown", + "message": "Waiting for agent initialization to finish", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "allocating", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-1.dns", + }, + }, + }, + }, + }, + }, + ), + test( // 7 + "add a dying service", + addCharm{"dummy"}, + addService{name: "dummy-service", charm: "dummy"}, + addMachine{machineId: "0", job: state.JobHostUnits}, + addAliveUnit{"dummy-service", "0"}, + ensureDyingService{"dummy-service"}, + expect{ + "service shows life==dying", + M{ + "environment": "dummyenv", + "machines": M{ + "0": M{ + "agent-state": "pending", + "instance-id": "pending", + "series": "quantal", + }, + }, + "services": M{ + "dummy-service": M{ + "charm": "cs:quantal/dummy-1", + "exposed": false, + "life": "dying", + "service-status": M{ + "current": "unknown", + "message": "Waiting for agent initialization to finish", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "dummy-service/0": M{ + "machine": "0", + "agent-state": "pending", + "workload-status": M{ + "current": "unknown", + "message": "Waiting for agent initialization to finish", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "allocating", + "since": "01 Apr 15 01:23+10:00", + }, + }, + }, + }, + }, + }, + }, + ), + test( // 8 + "a unit where the agent is down shows as lost", + addCharm{"dummy"}, + addService{name: "dummy-service", charm: "dummy"}, + addMachine{machineId: "0", job: state.JobHostUnits}, + startAliveMachine{"0"}, + setMachineStatus{"0", state.StatusStarted, ""}, + addUnit{"dummy-service", "0"}, + setAgentStatus{"dummy-service/0", state.StatusIdle, "", nil}, + setUnitStatus{"dummy-service/0", state.StatusActive, "", nil}, + expect{ + "unit shows that agent is lost", + M{ + "environment": "dummyenv", + "machines": M{ + "0": M{ + "agent-state": "started", + "instance-id": "dummyenv-0", + "series": "quantal", + "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", + }, + }, + "services": M{ + "dummy-service": M{ + "charm": "cs:quantal/dummy-1", + "exposed": false, + "service-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "dummy-service/0": M{ + "machine": "0", + "agent-state": "started", + "workload-status": M{ + "current": "unknown", + "message": "agent is lost, sorry! See 'juju status-history dummy-service/0'", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "lost", + "message": "agent is not communicating with the server", + "since": "01 Apr 15 01:23+10:00", + }, + }, + }, + }, + }, + }, + }, + ), + + // Relation tests + test( // 9 + "complex scenario with multiple related services", + addMachine{machineId: "0", job: state.JobManageEnviron}, + setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, + startAliveMachine{"0"}, + setMachineStatus{"0", state.StatusStarted, ""}, + addCharm{"wordpress"}, + addCharm{"mysql"}, + addCharm{"varnish"}, + + addService{name: "project", charm: "wordpress"}, + setServiceExposed{"project", true}, + addMachine{machineId: "1", job: state.JobHostUnits}, + setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, + startAliveMachine{"1"}, + setMachineStatus{"1", state.StatusStarted, ""}, + addAliveUnit{"project", "1"}, + setAgentStatus{"project/0", state.StatusIdle, "", nil}, + setUnitStatus{"project/0", state.StatusActive, "", nil}, + + addService{name: "mysql", charm: "mysql"}, + setServiceExposed{"mysql", true}, + addMachine{machineId: "2", job: state.JobHostUnits}, + setAddresses{"2", network.NewAddresses("dummyenv-2.dns")}, + startAliveMachine{"2"}, + setMachineStatus{"2", state.StatusStarted, ""}, + addAliveUnit{"mysql", "2"}, + setAgentStatus{"mysql/0", state.StatusIdle, "", nil}, + setUnitStatus{"mysql/0", state.StatusActive, "", nil}, + + addService{name: "varnish", charm: "varnish"}, + setServiceExposed{"varnish", true}, + addMachine{machineId: "3", job: state.JobHostUnits}, + setAddresses{"3", network.NewAddresses("dummyenv-3.dns")}, + startAliveMachine{"3"}, + setMachineStatus{"3", state.StatusStarted, ""}, + addAliveUnit{"varnish", "3"}, + + addService{name: "private", charm: "wordpress"}, + setServiceExposed{"private", true}, + addMachine{machineId: "4", job: state.JobHostUnits}, + setAddresses{"4", network.NewAddresses("dummyenv-4.dns")}, + startAliveMachine{"4"}, + setMachineStatus{"4", state.StatusStarted, ""}, + addAliveUnit{"private", "4"}, + + relateServices{"project", "mysql"}, + relateServices{"project", "varnish"}, + relateServices{"private", "mysql"}, + + expect{ + "multiples services with relations between some of them", + M{ + "environment": "dummyenv", + "machines": M{ + "0": machine0, + "1": machine1, + "2": machine2, + "3": machine3, + "4": machine4, + }, + "services": M{ + "project": M{ + "charm": "cs:quantal/wordpress-3", + "exposed": true, + "service-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "project/0": M{ + "machine": "1", + "agent-state": "started", + "workload-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-1.dns", + }, + }, + "relations": M{ + "db": L{"mysql"}, + "cache": L{"varnish"}, + }, + }, + "mysql": M{ + "charm": "cs:quantal/mysql-1", + "exposed": true, + "service-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "mysql/0": M{ + "machine": "2", + "agent-state": "started", + "workload-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-2.dns", + }, + }, + "relations": M{ + "server": L{"private", "project"}, + }, + }, + "varnish": M{ + "charm": "cs:quantal/varnish-1", + "exposed": true, + "service-status": M{ + "current": "unknown", + "message": "Waiting for agent initialization to finish", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "varnish/0": M{ + "machine": "3", + "agent-state": "pending", + "workload-status": M{ + "current": "unknown", + "message": "Waiting for agent initialization to finish", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "allocating", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-3.dns", + }, + }, + "relations": M{ + "webcache": L{"project"}, + }, + }, + "private": M{ + "charm": "cs:quantal/wordpress-3", + "exposed": true, + "service-status": M{ + "current": "unknown", + "message": "Waiting for agent initialization to finish", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "private/0": M{ + "machine": "4", + "agent-state": "pending", + "workload-status": M{ + "current": "unknown", + "message": "Waiting for agent initialization to finish", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "allocating", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-4.dns", + }, + }, + "relations": M{ + "db": L{"mysql"}, + }, + }, + }, + }, + }, + ), + test( // 10 + "simple peer scenario", + addMachine{machineId: "0", job: state.JobManageEnviron}, + setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, + startAliveMachine{"0"}, + setMachineStatus{"0", state.StatusStarted, ""}, + addCharm{"riak"}, + addCharm{"wordpress"}, + + addService{name: "riak", charm: "riak"}, + setServiceExposed{"riak", true}, + addMachine{machineId: "1", job: state.JobHostUnits}, + setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, + startAliveMachine{"1"}, + setMachineStatus{"1", state.StatusStarted, ""}, + addAliveUnit{"riak", "1"}, + setAgentStatus{"riak/0", state.StatusIdle, "", nil}, + setUnitStatus{"riak/0", state.StatusActive, "", nil}, + addMachine{machineId: "2", job: state.JobHostUnits}, + setAddresses{"2", network.NewAddresses("dummyenv-2.dns")}, + startAliveMachine{"2"}, + setMachineStatus{"2", state.StatusStarted, ""}, + addAliveUnit{"riak", "2"}, + setAgentStatus{"riak/1", state.StatusIdle, "", nil}, + setUnitStatus{"riak/1", state.StatusActive, "", nil}, + addMachine{machineId: "3", job: state.JobHostUnits}, + setAddresses{"3", network.NewAddresses("dummyenv-3.dns")}, + startAliveMachine{"3"}, + setMachineStatus{"3", state.StatusStarted, ""}, + addAliveUnit{"riak", "3"}, + setAgentStatus{"riak/2", state.StatusIdle, "", nil}, + setUnitStatus{"riak/2", state.StatusActive, "", nil}, + + expect{ + "multiples related peer units", + M{ + "environment": "dummyenv", + "machines": M{ + "0": machine0, + "1": machine1, + "2": machine2, + "3": machine3, + }, + "services": M{ + "riak": M{ + "charm": "cs:quantal/riak-7", + "exposed": true, + "service-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "riak/0": M{ + "machine": "1", + "agent-state": "started", + "workload-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-1.dns", + }, + "riak/1": M{ + "machine": "2", + "agent-state": "started", + "workload-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-2.dns", + }, + "riak/2": M{ + "machine": "3", + "agent-state": "started", + "workload-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-3.dns", + }, + }, + "relations": M{ + "ring": L{"riak"}, + }, + }, + }, + }, + }, + ), + + // Subordinate tests + test( // 11 + "one service with one subordinate service", + addMachine{machineId: "0", job: state.JobManageEnviron}, + setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, + startAliveMachine{"0"}, + setMachineStatus{"0", state.StatusStarted, ""}, + addCharm{"wordpress"}, + addCharm{"mysql"}, + addCharm{"logging"}, + + addService{name: "wordpress", charm: "wordpress"}, + setServiceExposed{"wordpress", true}, + addMachine{machineId: "1", job: state.JobHostUnits}, + setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, + startAliveMachine{"1"}, + setMachineStatus{"1", state.StatusStarted, ""}, + addAliveUnit{"wordpress", "1"}, + setAgentStatus{"wordpress/0", state.StatusIdle, "", nil}, + setUnitStatus{"wordpress/0", state.StatusActive, "", nil}, + + addService{name: "mysql", charm: "mysql"}, + setServiceExposed{"mysql", true}, + addMachine{machineId: "2", job: state.JobHostUnits}, + setAddresses{"2", network.NewAddresses("dummyenv-2.dns")}, + startAliveMachine{"2"}, + setMachineStatus{"2", state.StatusStarted, ""}, + addAliveUnit{"mysql", "2"}, + setAgentStatus{"mysql/0", state.StatusIdle, "", nil}, + setUnitStatus{"mysql/0", state.StatusActive, "", nil}, + + addService{name: "logging", charm: "logging"}, + setServiceExposed{"logging", true}, + + relateServices{"wordpress", "mysql"}, + relateServices{"wordpress", "logging"}, + relateServices{"mysql", "logging"}, + + addSubordinate{"wordpress/0", "logging"}, + addSubordinate{"mysql/0", "logging"}, + + setUnitsAlive{"logging"}, + setAgentStatus{"logging/0", state.StatusIdle, "", nil}, + setUnitStatus{"logging/0", state.StatusActive, "", nil}, + setAgentStatus{"logging/1", state.StatusError, "somehow lost in all those logs", nil}, + + expect{ + "multiples related peer units", + M{ + "environment": "dummyenv", + "machines": M{ + "0": machine0, + "1": machine1, + "2": machine2, + }, + "services": M{ + "wordpress": M{ + "charm": "cs:quantal/wordpress-3", + "exposed": true, + "service-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "wordpress/0": M{ + "machine": "1", + "agent-state": "started", + "workload-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "subordinates": M{ + "logging/0": M{ + "agent-state": "started", + "workload-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-1.dns", + }, + }, + "public-address": "dummyenv-1.dns", + }, + }, + "relations": M{ + "db": L{"mysql"}, + "logging-dir": L{"logging"}, + }, + }, + "mysql": M{ + "charm": "cs:quantal/mysql-1", + "exposed": true, + "service-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "mysql/0": M{ + "machine": "2", + "agent-state": "started", + "workload-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "subordinates": M{ + "logging/1": M{ + "agent-state": "error", + "agent-state-info": "somehow lost in all those logs", + "workload-status": M{ + "current": "error", + "message": "somehow lost in all those logs", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-2.dns", + }, + }, + "public-address": "dummyenv-2.dns", + }, + }, + "relations": M{ + "server": L{"wordpress"}, + "juju-info": L{"logging"}, + }, + }, + "logging": M{ + "charm": "cs:quantal/logging-1", + "exposed": true, + "service-status": M{}, + "relations": M{ + "logging-directory": L{"wordpress"}, + "info": L{"mysql"}, + }, + "subordinate-to": L{"mysql", "wordpress"}, + }, + }, + }, + }, + + // scoped on 'logging' + scopedExpect{ + "subordinates scoped on logging", + []string{"logging"}, + M{ + "environment": "dummyenv", + "machines": M{ + "1": machine1, + "2": machine2, + }, + "services": M{ + "wordpress": M{ + "charm": "cs:quantal/wordpress-3", + "exposed": true, + "service-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "wordpress/0": M{ + "machine": "1", + "agent-state": "started", + "workload-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "subordinates": M{ + "logging/0": M{ + "agent-state": "started", + "workload-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-1.dns", + }, + }, + "public-address": "dummyenv-1.dns", + }, + }, + "relations": M{ + "db": L{"mysql"}, + "logging-dir": L{"logging"}, + }, + }, + "mysql": M{ + "charm": "cs:quantal/mysql-1", + "exposed": true, + "service-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "mysql/0": M{ + "machine": "2", + "agent-state": "started", + "workload-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "subordinates": M{ + "logging/1": M{ + "agent-state": "error", + "workload-status": M{ + "current": "error", + "message": "somehow lost in all those logs", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-state-info": "somehow lost in all those logs", + "public-address": "dummyenv-2.dns", + }, + }, + "public-address": "dummyenv-2.dns", + }, + }, + "relations": M{ + "server": L{"wordpress"}, + "juju-info": L{"logging"}, + }, + }, + "logging": M{ + "charm": "cs:quantal/logging-1", + "exposed": true, + "service-status": M{}, + "relations": M{ + "logging-directory": L{"wordpress"}, + "info": L{"mysql"}, + }, + "subordinate-to": L{"mysql", "wordpress"}, + }, + }, + }, + }, + + // scoped on wordpress/0 + scopedExpect{ + "subordinates scoped on logging", + []string{"wordpress/0"}, + M{ + "environment": "dummyenv", + "machines": M{ + "1": machine1, + }, + "services": M{ + "wordpress": M{ + "charm": "cs:quantal/wordpress-3", + "exposed": true, + "service-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "wordpress/0": M{ + "machine": "1", + "agent-state": "started", + "workload-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "subordinates": M{ + "logging/0": M{ + "agent-state": "started", + "workload-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-1.dns", + }, + }, + "public-address": "dummyenv-1.dns", + }, + }, + "relations": M{ + "db": L{"mysql"}, + "logging-dir": L{"logging"}, + }, + }, + "logging": M{ + "charm": "cs:quantal/logging-1", + "exposed": true, + "service-status": M{}, + "relations": M{ + "logging-directory": L{"wordpress"}, + "info": L{"mysql"}, + }, + "subordinate-to": L{"mysql", "wordpress"}, + }, + }, + }, + }, + ), + test( // 12 + "machines with containers", + // step 0 + addMachine{machineId: "0", job: state.JobManageEnviron}, + setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, + startAliveMachine{"0"}, + setMachineStatus{"0", state.StatusStarted, ""}, + addCharm{"mysql"}, + addService{name: "mysql", charm: "mysql"}, + setServiceExposed{"mysql", true}, + + // step 7 + addMachine{machineId: "1", job: state.JobHostUnits}, + setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, + startAliveMachine{"1"}, + setMachineStatus{"1", state.StatusStarted, ""}, + addAliveUnit{"mysql", "1"}, + setAgentStatus{"mysql/0", state.StatusIdle, "", nil}, + setUnitStatus{"mysql/0", state.StatusActive, "", nil}, + + // step 14: A container on machine 1. + addContainer{"1", "1/lxc/0", state.JobHostUnits}, + setAddresses{"1/lxc/0", network.NewAddresses("dummyenv-2.dns")}, + startAliveMachine{"1/lxc/0"}, + setMachineStatus{"1/lxc/0", state.StatusStarted, ""}, + addAliveUnit{"mysql", "1/lxc/0"}, + setAgentStatus{"mysql/1", state.StatusIdle, "", nil}, + setUnitStatus{"mysql/1", state.StatusActive, "", nil}, + addContainer{"1", "1/lxc/1", state.JobHostUnits}, + + // step 22: A nested container. + addContainer{"1/lxc/0", "1/lxc/0/lxc/0", state.JobHostUnits}, + setAddresses{"1/lxc/0/lxc/0", network.NewAddresses("dummyenv-3.dns")}, + startAliveMachine{"1/lxc/0/lxc/0"}, + setMachineStatus{"1/lxc/0/lxc/0", state.StatusStarted, ""}, + + expect{ + "machines with nested containers", + M{ + "environment": "dummyenv", + "machines": M{ + "0": machine0, + "1": machine1WithContainers, + }, + "services": M{ + "mysql": M{ + "charm": "cs:quantal/mysql-1", + "exposed": true, + "service-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "mysql/0": M{ + "machine": "1", + "agent-state": "started", + "workload-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-1.dns", + }, + "mysql/1": M{ + "machine": "1/lxc/0", + "agent-state": "started", + "workload-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-2.dns", + }, + }, + }, + }, + }, + }, + + // step 27: once again, with a scope on mysql/1 + scopedExpect{ + "machines with nested containers", + []string{"mysql/1"}, + M{ + "environment": "dummyenv", + "machines": M{ + "1": M{ + "agent-state": "started", + "containers": M{ + "1/lxc/0": M{ + "agent-state": "started", + "dns-name": "dummyenv-2.dns", + "instance-id": "dummyenv-2", + "series": "quantal", + }, + }, + "dns-name": "dummyenv-1.dns", + "instance-id": "dummyenv-1", + "series": "quantal", + "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", + }, + }, + "services": M{ + "mysql": M{ + "charm": "cs:quantal/mysql-1", + "exposed": true, + "service-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "mysql/1": M{ + "machine": "1/lxc/0", + "agent-state": "started", + "workload-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-2.dns", + }, + }, + }, + }, + }, + }, + ), + test( // 13 + "service with out of date charm", + addMachine{machineId: "0", job: state.JobManageEnviron}, + setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, + startAliveMachine{"0"}, + setMachineStatus{"0", state.StatusStarted, ""}, + addMachine{machineId: "1", job: state.JobHostUnits}, + setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, + startAliveMachine{"1"}, + setMachineStatus{"1", state.StatusStarted, ""}, + addCharm{"mysql"}, + addService{name: "mysql", charm: "mysql"}, + setServiceExposed{"mysql", true}, + addCharmPlaceholder{"mysql", 23}, + addAliveUnit{"mysql", "1"}, + + expect{ + "services and units with correct charm status", + M{ + "environment": "dummyenv", + "machines": M{ + "0": machine0, + "1": machine1, + }, + "services": M{ + "mysql": M{ + "charm": "cs:quantal/mysql-1", + "can-upgrade-to": "cs:quantal/mysql-23", + "exposed": true, + "service-status": M{ + "current": "unknown", + "message": "Waiting for agent initialization to finish", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "mysql/0": M{ + "machine": "1", + "agent-state": "pending", + "workload-status": M{ + "current": "unknown", + "message": "Waiting for agent initialization to finish", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "allocating", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-1.dns", + }, + }, + }, + }, + }, + }, + ), + test( // 14 + "unit with out of date charm", + addMachine{machineId: "0", job: state.JobManageEnviron}, + setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, + startAliveMachine{"0"}, + setMachineStatus{"0", state.StatusStarted, ""}, + addMachine{machineId: "1", job: state.JobHostUnits}, + setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, + startAliveMachine{"1"}, + setMachineStatus{"1", state.StatusStarted, ""}, + addCharm{"mysql"}, + addService{name: "mysql", charm: "mysql"}, + setServiceExposed{"mysql", true}, + addAliveUnit{"mysql", "1"}, + setUnitCharmURL{"mysql/0", "cs:quantal/mysql-1"}, + addCharmWithRevision{addCharm{"mysql"}, "local", 1}, + setServiceCharm{"mysql", "local:quantal/mysql-1"}, + + expect{ + "services and units with correct charm status", + M{ + "environment": "dummyenv", + "machines": M{ + "0": machine0, + "1": machine1, + }, + "services": M{ + "mysql": M{ + "charm": "local:quantal/mysql-1", + "exposed": true, + "service-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "mysql/0": M{ + "machine": "1", + "agent-state": "started", + "workload-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "upgrading-from": "cs:quantal/mysql-1", + "public-address": "dummyenv-1.dns", + }, + }, + }, + }, + }, + }, + ), + test( // 15 + "service and unit with out of date charms", + addMachine{machineId: "0", job: state.JobManageEnviron}, + setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, + startAliveMachine{"0"}, + setMachineStatus{"0", state.StatusStarted, ""}, + addMachine{machineId: "1", job: state.JobHostUnits}, + setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, + startAliveMachine{"1"}, + setMachineStatus{"1", state.StatusStarted, ""}, + addCharm{"mysql"}, + addService{name: "mysql", charm: "mysql"}, + setServiceExposed{"mysql", true}, + addAliveUnit{"mysql", "1"}, + setUnitCharmURL{"mysql/0", "cs:quantal/mysql-1"}, + addCharmWithRevision{addCharm{"mysql"}, "cs", 2}, + setServiceCharm{"mysql", "cs:quantal/mysql-2"}, + addCharmPlaceholder{"mysql", 23}, + + expect{ + "services and units with correct charm status", + M{ + "environment": "dummyenv", + "machines": M{ + "0": machine0, + "1": machine1, + }, + "services": M{ + "mysql": M{ + "charm": "cs:quantal/mysql-2", + "can-upgrade-to": "cs:quantal/mysql-23", + "exposed": true, + "service-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "mysql/0": M{ + "machine": "1", + "agent-state": "started", + "workload-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "upgrading-from": "cs:quantal/mysql-1", + "public-address": "dummyenv-1.dns", + }, + }, + }, + }, + }, + }, + ), + test( // 16 + "service with local charm not shown as out of date", + addMachine{machineId: "0", job: state.JobManageEnviron}, + setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, + startAliveMachine{"0"}, + setMachineStatus{"0", state.StatusStarted, ""}, + addMachine{machineId: "1", job: state.JobHostUnits}, + setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, + startAliveMachine{"1"}, + setMachineStatus{"1", state.StatusStarted, ""}, + addCharm{"mysql"}, + addService{name: "mysql", charm: "mysql"}, + setServiceExposed{"mysql", true}, + addAliveUnit{"mysql", "1"}, + setUnitCharmURL{"mysql/0", "cs:quantal/mysql-1"}, + addCharmWithRevision{addCharm{"mysql"}, "local", 1}, + setServiceCharm{"mysql", "local:quantal/mysql-1"}, + addCharmPlaceholder{"mysql", 23}, + + expect{ + "services and units with correct charm status", + M{ + "environment": "dummyenv", + "machines": M{ + "0": machine0, + "1": machine1, + }, + "services": M{ + "mysql": M{ + "charm": "local:quantal/mysql-1", + "exposed": true, + "service-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "mysql/0": M{ + "machine": "1", + "agent-state": "started", + "workload-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "upgrading-from": "cs:quantal/mysql-1", + "public-address": "dummyenv-1.dns", + }, + }, + }, + }, + }, + }, + ), + test( // 17 + "deploy two services; set meter statuses on one", + addMachine{machineId: "0", job: state.JobManageEnviron}, + setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, + startAliveMachine{"0"}, + setMachineStatus{"0", state.StatusStarted, ""}, + + addMachine{machineId: "1", job: state.JobHostUnits}, + setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, + startAliveMachine{"1"}, + setMachineStatus{"1", state.StatusStarted, ""}, + + addMachine{machineId: "2", job: state.JobHostUnits}, + setAddresses{"2", network.NewAddresses("dummyenv-2.dns")}, + startAliveMachine{"2"}, + setMachineStatus{"2", state.StatusStarted, ""}, + + addMachine{machineId: "3", job: state.JobHostUnits}, + setAddresses{"3", network.NewAddresses("dummyenv-3.dns")}, + startAliveMachine{"3"}, + setMachineStatus{"3", state.StatusStarted, ""}, + + addMachine{machineId: "4", job: state.JobHostUnits}, + setAddresses{"4", network.NewAddresses("dummyenv-4.dns")}, + startAliveMachine{"4"}, + setMachineStatus{"4", state.StatusStarted, ""}, + + addCharm{"mysql"}, + addService{name: "mysql", charm: "mysql"}, + setServiceExposed{"mysql", true}, + + addService{name: "servicewithmeterstatus", charm: "mysql"}, + + addAliveUnit{"mysql", "1"}, + addAliveUnit{"servicewithmeterstatus", "2"}, + addAliveUnit{"servicewithmeterstatus", "3"}, + addAliveUnit{"servicewithmeterstatus", "4"}, + + setServiceExposed{"mysql", true}, + + setAgentStatus{"mysql/0", state.StatusIdle, "", nil}, + setUnitStatus{"mysql/0", state.StatusActive, "", nil}, + setAgentStatus{"servicewithmeterstatus/0", state.StatusIdle, "", nil}, + setUnitStatus{"servicewithmeterstatus/0", state.StatusActive, "", nil}, + setAgentStatus{"servicewithmeterstatus/1", state.StatusIdle, "", nil}, + setUnitStatus{"servicewithmeterstatus/1", state.StatusActive, "", nil}, + setAgentStatus{"servicewithmeterstatus/2", state.StatusIdle, "", nil}, + setUnitStatus{"servicewithmeterstatus/2", state.StatusActive, "", nil}, + + setUnitMeterStatus{"servicewithmeterstatus/1", "GREEN", "test green status"}, + setUnitMeterStatus{"servicewithmeterstatus/2", "RED", "test red status"}, + + expect{ + "simulate just the two services and a bootstrap node", + M{ + "environment": "dummyenv", + "machines": M{ + "0": machine0, + "1": machine1, + "2": machine2, + "3": machine3, + "4": machine4, + }, + "services": M{ + "mysql": M{ + "charm": "cs:quantal/mysql-1", + "exposed": true, + "service-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "mysql/0": M{ + "machine": "1", + "agent-state": "started", + "workload-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-1.dns", + }, + }, + }, + + "servicewithmeterstatus": M{ + "charm": "cs:quantal/mysql-1", + "exposed": false, + "service-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "servicewithmeterstatus/0": M{ + "machine": "2", + "agent-state": "started", + "workload-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-2.dns", + }, + "servicewithmeterstatus/1": M{ + "machine": "3", + "agent-state": "started", + "workload-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "meter-status": M{ + "color": "green", + "message": "test green status", + }, + "public-address": "dummyenv-3.dns", + }, + "servicewithmeterstatus/2": M{ + "machine": "4", + "agent-state": "started", + "workload-status": M{ + "current": "active", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "idle", + "since": "01 Apr 15 01:23+10:00", + }, + "meter-status": M{ + "color": "red", + "message": "test red status", + }, + "public-address": "dummyenv-4.dns", + }, + }, + }, + }, + }, + }, + ), + test( // 18 + "upgrade available", + setToolsUpgradeAvailable{}, + expect{ + "upgrade availability should be shown in environment-status", + M{ + "environment": "dummyenv", + "environment-status": M{ + "upgrade-available": nextVersion().String(), + }, + "machines": M{}, + "services": M{}, + }, + }, + ), +} + +// TODO(dfc) test failing components by destructively mutating the state under the hood + +type addMachine struct { + machineId string + cons constraints.Value + job state.MachineJob +} + +func (am addMachine) step(c *gc.C, ctx *context) { + m, err := ctx.st.AddOneMachine(state.MachineTemplate{ + Series: "quantal", + Constraints: am.cons, + Jobs: []state.MachineJob{am.job}, + }) + c.Assert(err, jc.ErrorIsNil) + c.Assert(m.Id(), gc.Equals, am.machineId) +} + +type addNetwork struct { + name string + providerId network.Id + cidr string + vlanTag int +} + +func (an addNetwork) step(c *gc.C, ctx *context) { + n, err := ctx.st.AddNetwork(state.NetworkInfo{ + Name: an.name, + ProviderId: an.providerId, + CIDR: an.cidr, + VLANTag: an.vlanTag, + }) + c.Assert(err, jc.ErrorIsNil) + c.Assert(n.Name(), gc.Equals, an.name) +} + +type addContainer struct { + parentId string + machineId string + job state.MachineJob +} + +func (ac addContainer) step(c *gc.C, ctx *context) { + template := state.MachineTemplate{ + Series: "quantal", + Jobs: []state.MachineJob{ac.job}, + } + m, err := ctx.st.AddMachineInsideMachine(template, ac.parentId, instance.LXC) + c.Assert(err, jc.ErrorIsNil) + c.Assert(m.Id(), gc.Equals, ac.machineId) +} + +type startMachine struct { + machineId string +} + +func (sm startMachine) step(c *gc.C, ctx *context) { + m, err := ctx.st.Machine(sm.machineId) + c.Assert(err, jc.ErrorIsNil) + cons, err := m.Constraints() + c.Assert(err, jc.ErrorIsNil) + inst, hc := testing.AssertStartInstanceWithConstraints(c, ctx.env, m.Id(), cons) + err = m.SetProvisioned(inst.Id(), "fake_nonce", hc) + c.Assert(err, jc.ErrorIsNil) +} + +type startMissingMachine struct { + machineId string +} + +func (sm startMissingMachine) step(c *gc.C, ctx *context) { + m, err := ctx.st.Machine(sm.machineId) + c.Assert(err, jc.ErrorIsNil) + cons, err := m.Constraints() + c.Assert(err, jc.ErrorIsNil) + _, hc := testing.AssertStartInstanceWithConstraints(c, ctx.env, m.Id(), cons) + err = m.SetProvisioned("i-missing", "fake_nonce", hc) + c.Assert(err, jc.ErrorIsNil) + err = m.SetInstanceStatus("missing") + c.Assert(err, jc.ErrorIsNil) +} + +type startAliveMachine struct { + machineId string +} + +func (sam startAliveMachine) step(c *gc.C, ctx *context) { + m, err := ctx.st.Machine(sam.machineId) + c.Assert(err, jc.ErrorIsNil) + pinger := ctx.setAgentPresence(c, m) + cons, err := m.Constraints() + c.Assert(err, jc.ErrorIsNil) + inst, hc := testing.AssertStartInstanceWithConstraints(c, ctx.env, m.Id(), cons) + err = m.SetProvisioned(inst.Id(), "fake_nonce", hc) + c.Assert(err, jc.ErrorIsNil) + ctx.pingers[m.Id()] = pinger +} + +type setAddresses struct { + machineId string + addresses []network.Address +} + +func (sa setAddresses) step(c *gc.C, ctx *context) { + m, err := ctx.st.Machine(sa.machineId) + c.Assert(err, jc.ErrorIsNil) + err = m.SetProviderAddresses(sa.addresses...) + c.Assert(err, jc.ErrorIsNil) +} + +type setTools struct { + machineId string + version version.Binary +} + +func (st setTools) step(c *gc.C, ctx *context) { + m, err := ctx.st.Machine(st.machineId) + c.Assert(err, jc.ErrorIsNil) + err = m.SetAgentVersion(st.version) + c.Assert(err, jc.ErrorIsNil) +} + +type setUnitTools struct { + unitName string + version version.Binary +} + +func (st setUnitTools) step(c *gc.C, ctx *context) { + m, err := ctx.st.Unit(st.unitName) + c.Assert(err, jc.ErrorIsNil) + err = m.SetAgentVersion(st.version) + c.Assert(err, jc.ErrorIsNil) +} + +type addCharm struct { + name string +} + +func (ac addCharm) addCharmStep(c *gc.C, ctx *context, scheme string, rev int) { + ch := testcharms.Repo.CharmDir(ac.name) + name := ch.Meta().Name + curl := charm.MustParseURL(fmt.Sprintf("%s:quantal/%s-%d", scheme, name, rev)) + dummy, err := ctx.st.AddCharm(ch, curl, "dummy-path", fmt.Sprintf("%s-%d-sha256", name, rev)) + c.Assert(err, jc.ErrorIsNil) + ctx.charms[ac.name] = dummy +} + +func (ac addCharm) step(c *gc.C, ctx *context) { + ch := testcharms.Repo.CharmDir(ac.name) + ac.addCharmStep(c, ctx, "cs", ch.Revision()) +} + +type addCharmWithRevision struct { + addCharm + scheme string + rev int +} + +func (ac addCharmWithRevision) step(c *gc.C, ctx *context) { + ac.addCharmStep(c, ctx, ac.scheme, ac.rev) +} + +type addService struct { + name string + charm string + networks []string + cons constraints.Value +} + +func (as addService) step(c *gc.C, ctx *context) { + ch, ok := ctx.charms[as.charm] + c.Assert(ok, jc.IsTrue) + svc, err := ctx.st.AddService(as.name, ctx.adminUserTag, ch, as.networks, nil, nil) + c.Assert(err, jc.ErrorIsNil) + if svc.IsPrincipal() { + err = svc.SetConstraints(as.cons) + c.Assert(err, jc.ErrorIsNil) + } +} + +type setServiceExposed struct { + name string + exposed bool +} + +func (sse setServiceExposed) step(c *gc.C, ctx *context) { + s, err := ctx.st.Service(sse.name) + c.Assert(err, jc.ErrorIsNil) + err = s.ClearExposed() + c.Assert(err, jc.ErrorIsNil) + if sse.exposed { + err = s.SetExposed() + c.Assert(err, jc.ErrorIsNil) + } +} + +type setServiceCharm struct { + name string + charm string +} + +func (ssc setServiceCharm) step(c *gc.C, ctx *context) { + ch, err := ctx.st.Charm(charm.MustParseURL(ssc.charm)) + c.Assert(err, jc.ErrorIsNil) + s, err := ctx.st.Service(ssc.name) + c.Assert(err, jc.ErrorIsNil) + err = s.SetCharm(ch, false) + c.Assert(err, jc.ErrorIsNil) +} + +type addCharmPlaceholder struct { + name string + rev int +} + +func (ac addCharmPlaceholder) step(c *gc.C, ctx *context) { + ch := testcharms.Repo.CharmDir(ac.name) + name := ch.Meta().Name + curl := charm.MustParseURL(fmt.Sprintf("cs:quantal/%s-%d", name, ac.rev)) + err := ctx.st.AddStoreCharmPlaceholder(curl) + c.Assert(err, jc.ErrorIsNil) +} + +type addUnit struct { + serviceName string + machineId string +} + +func (au addUnit) step(c *gc.C, ctx *context) { + s, err := ctx.st.Service(au.serviceName) + c.Assert(err, jc.ErrorIsNil) + u, err := s.AddUnit() + c.Assert(err, jc.ErrorIsNil) + m, err := ctx.st.Machine(au.machineId) + c.Assert(err, jc.ErrorIsNil) + err = u.AssignToMachine(m) + c.Assert(err, jc.ErrorIsNil) +} + +type addAliveUnit struct { + serviceName string + machineId string +} + +func (aau addAliveUnit) step(c *gc.C, ctx *context) { + s, err := ctx.st.Service(aau.serviceName) + c.Assert(err, jc.ErrorIsNil) + u, err := s.AddUnit() + c.Assert(err, jc.ErrorIsNil) + pinger := ctx.setAgentPresence(c, u) + m, err := ctx.st.Machine(aau.machineId) + c.Assert(err, jc.ErrorIsNil) + err = u.AssignToMachine(m) + c.Assert(err, jc.ErrorIsNil) + ctx.pingers[u.Name()] = pinger +} + +type setUnitsAlive struct { + serviceName string +} + +func (sua setUnitsAlive) step(c *gc.C, ctx *context) { + s, err := ctx.st.Service(sua.serviceName) + c.Assert(err, jc.ErrorIsNil) + us, err := s.AllUnits() + c.Assert(err, jc.ErrorIsNil) + for _, u := range us { + ctx.pingers[u.Name()] = ctx.setAgentPresence(c, u) + } +} + +type setUnitMeterStatus struct { + unitName string + color string + message string +} + +func (s setUnitMeterStatus) step(c *gc.C, ctx *context) { + u, err := ctx.st.Unit(s.unitName) + c.Assert(err, jc.ErrorIsNil) + err = u.SetMeterStatus(s.color, s.message) + c.Assert(err, jc.ErrorIsNil) +} + +type setUnitStatus struct { + unitName string + status state.Status + statusInfo string + statusData map[string]interface{} +} + +func (sus setUnitStatus) step(c *gc.C, ctx *context) { + u, err := ctx.st.Unit(sus.unitName) + c.Assert(err, jc.ErrorIsNil) + err = u.SetStatus(sus.status, sus.statusInfo, sus.statusData) + c.Assert(err, jc.ErrorIsNil) +} + +type setAgentStatus struct { + unitName string + status state.Status + statusInfo string + statusData map[string]interface{} +} + +func (sus setAgentStatus) step(c *gc.C, ctx *context) { + u, err := ctx.st.Unit(sus.unitName) + c.Assert(err, jc.ErrorIsNil) + err = u.SetAgentStatus(sus.status, sus.statusInfo, sus.statusData) + c.Assert(err, jc.ErrorIsNil) +} + +type setUnitCharmURL struct { + unitName string + charm string +} + +func (uc setUnitCharmURL) step(c *gc.C, ctx *context) { + u, err := ctx.st.Unit(uc.unitName) + c.Assert(err, jc.ErrorIsNil) + curl := charm.MustParseURL(uc.charm) + err = u.SetCharmURL(curl) + c.Assert(err, jc.ErrorIsNil) + err = u.SetStatus(state.StatusActive, "", nil) + c.Assert(err, jc.ErrorIsNil) + err = u.SetAgentStatus(state.StatusIdle, "", nil) + c.Assert(err, jc.ErrorIsNil) + +} + +type openUnitPort struct { + unitName string + protocol string + number int +} + +func (oup openUnitPort) step(c *gc.C, ctx *context) { + u, err := ctx.st.Unit(oup.unitName) + c.Assert(err, jc.ErrorIsNil) + err = u.OpenPort(oup.protocol, oup.number) + c.Assert(err, jc.ErrorIsNil) +} + +type ensureDyingUnit struct { + unitName string +} + +func (e ensureDyingUnit) step(c *gc.C, ctx *context) { + u, err := ctx.st.Unit(e.unitName) + c.Assert(err, jc.ErrorIsNil) + err = u.Destroy() + c.Assert(err, jc.ErrorIsNil) + c.Assert(u.Life(), gc.Equals, state.Dying) +} + +type ensureDyingService struct { + serviceName string +} + +func (e ensureDyingService) step(c *gc.C, ctx *context) { + svc, err := ctx.st.Service(e.serviceName) + c.Assert(err, jc.ErrorIsNil) + err = svc.Destroy() + c.Assert(err, jc.ErrorIsNil) + err = svc.Refresh() + c.Assert(err, jc.ErrorIsNil) + c.Assert(svc.Life(), gc.Equals, state.Dying) +} + +type ensureDeadMachine struct { + machineId string +} + +func (e ensureDeadMachine) step(c *gc.C, ctx *context) { + m, err := ctx.st.Machine(e.machineId) + c.Assert(err, jc.ErrorIsNil) + err = m.EnsureDead() + c.Assert(err, jc.ErrorIsNil) + c.Assert(m.Life(), gc.Equals, state.Dead) +} + +type setMachineStatus struct { + machineId string + status state.Status + statusInfo string +} + +func (sms setMachineStatus) step(c *gc.C, ctx *context) { + m, err := ctx.st.Machine(sms.machineId) + c.Assert(err, jc.ErrorIsNil) + err = m.SetStatus(sms.status, sms.statusInfo, nil) + c.Assert(err, jc.ErrorIsNil) +} + +type relateServices struct { + ep1, ep2 string +} + +func (rs relateServices) step(c *gc.C, ctx *context) { + eps, err := ctx.st.InferEndpoints(rs.ep1, rs.ep2) + c.Assert(err, jc.ErrorIsNil) + _, err = ctx.st.AddRelation(eps...) + c.Assert(err, jc.ErrorIsNil) +} + +type addSubordinate struct { + prinUnit string + subService string +} + +func (as addSubordinate) step(c *gc.C, ctx *context) { + u, err := ctx.st.Unit(as.prinUnit) + c.Assert(err, jc.ErrorIsNil) + eps, err := ctx.st.InferEndpoints(u.ServiceName(), as.subService) + c.Assert(err, jc.ErrorIsNil) + rel, err := ctx.st.EndpointsRelation(eps...) + c.Assert(err, jc.ErrorIsNil) + ru, err := rel.Unit(u) + c.Assert(err, jc.ErrorIsNil) + err = ru.EnterScope(nil) + c.Assert(err, jc.ErrorIsNil) +} + +type scopedExpect struct { + what string + scope []string + output M +} + +type expect struct { + what string + output M +} + +// substituteFakeTime replaces all "since" values +// in actual status output with a known fake value. +func substituteFakeSinceTime(c *gc.C, in []byte, expectIsoTime bool) []byte { + // This regexp will work for yaml and json. + exp := regexp.MustCompile(`(?P"?since"?:\ ?)(?P"?)(?P[^("|\n)]*)*"?`) + // Before the substritution is done, check that the timestamp produced + // by status is in the correct format. + if matches := exp.FindStringSubmatch(string(in)); matches != nil { + for i, name := range exp.SubexpNames() { + if name != "timestamp" { + continue + } + timeFormat := "02 Jan 2006 15:04:05Z07:00" + if expectIsoTime { + timeFormat = "2006-01-02 15:04:05Z" + } + _, err := time.Parse(timeFormat, matches[i]) + c.Assert(err, jc.ErrorIsNil) + } + } + + out := exp.ReplaceAllString(string(in), `$since$quote$quote`) + // Substitute a made up time used in our expected output. + out = strings.Replace(out, "", "01 Apr 15 01:23+10:00", -1) + return []byte(out) +} + +func (e scopedExpect) step(c *gc.C, ctx *context) { + c.Logf("\nexpect: %s %s\n", e.what, strings.Join(e.scope, " ")) + + // Now execute the command for each format. + for _, format := range statusFormats { + c.Logf("format %q", format.name) + // Run command with the required format. + args := []string{"--format", format.name} + if ctx.expectIsoTime { + args = append(args, "--utc") + } + args = append(args, e.scope...) + c.Logf("running status %s", strings.Join(args, " ")) + code, stdout, stderr := runStatus(c, args...) + c.Assert(code, gc.Equals, 0) + if !c.Check(stderr, gc.HasLen, 0) { + c.Fatalf("status failed: %s", string(stderr)) + } + + // Prepare the output in the same format. + buf, err := format.marshal(e.output) + c.Assert(err, jc.ErrorIsNil) + expected := make(M) + err = format.unmarshal(buf, &expected) + c.Assert(err, jc.ErrorIsNil) + + // Check the output is as expected. + actual := make(M) + out := substituteFakeSinceTime(c, stdout, ctx.expectIsoTime) + err = format.unmarshal(out, &actual) + c.Assert(err, jc.ErrorIsNil) + c.Assert(actual, jc.DeepEquals, expected) + } +} + +func (e expect) step(c *gc.C, ctx *context) { + scopedExpect{e.what, nil, e.output}.step(c, ctx) +} + +type setToolsUpgradeAvailable struct{} + +func (ua setToolsUpgradeAvailable) step(c *gc.C, ctx *context) { + env, err := ctx.st.Environment() + c.Assert(err, jc.ErrorIsNil) + err = env.UpdateLatestToolsVersion(nextVersion()) + c.Assert(err, jc.ErrorIsNil) +} + +func (s *StatusSuite) TestStatusAllFormats(c *gc.C) { + for i, t := range statusTests { + c.Logf("test %d: %s", i, t.summary) + func(t testCase) { + // Prepare context and run all steps to setup. + ctx := s.newContext(c) + defer s.resetContext(c, ctx) + ctx.run(c, t.steps) + }(t) + } +} + +type fakeApiClient struct { + statusReturn *params.FullStatus + patternsUsed []string + closeCalled bool +} + +func newFakeApiClient(statusReturn *params.FullStatus) fakeApiClient { + return fakeApiClient{ + statusReturn: statusReturn, + } +} + +func (a *fakeApiClient) Status(patterns []string) (*params.FullStatus, error) { + a.patternsUsed = patterns + return a.statusReturn, nil +} + +func (a *fakeApiClient) Close() error { + a.closeCalled = true + return nil +} + +// Check that the client works with an older server which doesn't +// return the top level Relations field nor the unit and machine level +// Agent field (they were introduced at the same time). +func (s *StatusSuite) TestStatusWithPreRelationsServer(c *gc.C) { + // Construct an older style status response + client := newFakeApiClient(¶ms.FullStatus{ + EnvironmentName: "dummyenv", + Machines: map[string]params.MachineStatus{ + "0": { + // Agent field intentionally not set + Id: "0", + InstanceId: instance.Id("dummyenv-0"), + AgentState: "down", + AgentStateInfo: "(started)", + Series: "quantal", + Containers: map[string]params.MachineStatus{}, + Jobs: []multiwatcher.MachineJob{multiwatcher.JobManageEnviron}, + HasVote: false, + WantsVote: true, + }, + "1": { + // Agent field intentionally not set + Id: "1", + InstanceId: instance.Id("dummyenv-1"), + AgentState: "started", + AgentStateInfo: "hello", + Series: "quantal", + Containers: map[string]params.MachineStatus{}, + Jobs: []multiwatcher.MachineJob{multiwatcher.JobHostUnits}, + HasVote: false, + WantsVote: false, + }, + }, + Services: map[string]params.ServiceStatus{ + "mysql": { + Charm: "local:quantal/mysql-1", + Relations: map[string][]string{ + "server": {"wordpress"}, + }, + Units: map[string]params.UnitStatus{ + "mysql/0": { + // Agent field intentionally not set + Machine: "1", + AgentState: "allocating", + }, + }, + }, + "wordpress": { + Charm: "local:quantal/wordpress-3", + Relations: map[string][]string{ + "db": {"mysql"}, + }, + Units: map[string]params.UnitStatus{ + "wordpress/0": { + // Agent field intentionally not set + AgentState: "error", + AgentStateInfo: "blam", + Machine: "1", + }, + }, + }, + }, + Networks: map[string]params.NetworkStatus{}, + // Relations field intentionally not set + }) + s.PatchValue(&newApiClientForStatus, func(_ *StatusCommand) (statusAPI, error) { + return &client, nil + }) + + expected := expect{ + "sane output with an older client that doesn't return Agent or Relations fields", + M{ + "environment": "dummyenv", + "machines": M{ + "0": M{ + "agent-state": "down", + "agent-state-info": "(started)", + "instance-id": "dummyenv-0", + "series": "quantal", + "state-server-member-status": "adding-vote", + }, + "1": M{ + "agent-state": "started", + "agent-state-info": "hello", + "instance-id": "dummyenv-1", + "series": "quantal", + }, + }, + "services": M{ + "mysql": M{ + "charm": "local:quantal/mysql-1", + "exposed": false, + "relations": M{ + "server": L{"wordpress"}, + }, + "service-status": M{}, + "units": M{ + "mysql/0": M{ + "machine": "1", + "agent-state": "allocating", + "workload-status": M{}, + "agent-status": M{}, + }, + }, + }, + "wordpress": M{ + "charm": "local:quantal/wordpress-3", + "exposed": false, + "relations": M{ + "db": L{"mysql"}, + }, + "service-status": M{}, + "units": M{ + "wordpress/0": M{ + "machine": "1", + "agent-state": "error", + "agent-state-info": "blam", + "workload-status": M{}, + "agent-status": M{}, + }, + }, + }, + }, + }, + } + ctx := s.newContext(c) + defer s.resetContext(c, ctx) + ctx.run(c, []stepper{expected}) +} + +func (s *StatusSuite) TestStatusWithFormatSummary(c *gc.C) { + ctx := s.newContext(c) + defer s.resetContext(c, ctx) + steps := []stepper{ + addMachine{machineId: "0", job: state.JobManageEnviron}, + setAddresses{"0", network.NewAddresses("localhost")}, + startAliveMachine{"0"}, + setMachineStatus{"0", state.StatusStarted, ""}, + addCharm{"wordpress"}, + addCharm{"mysql"}, + addCharm{"logging"}, + addService{name: "wordpress", charm: "wordpress"}, + setServiceExposed{"wordpress", true}, + addMachine{machineId: "1", job: state.JobHostUnits}, + setAddresses{"1", network.NewAddresses("localhost")}, + startAliveMachine{"1"}, + setMachineStatus{"1", state.StatusStarted, ""}, + addAliveUnit{"wordpress", "1"}, + setAgentStatus{"wordpress/0", state.StatusIdle, "", nil}, + setUnitStatus{"wordpress/0", state.StatusActive, "", nil}, + addService{name: "mysql", charm: "mysql"}, + setServiceExposed{"mysql", true}, + addMachine{machineId: "2", job: state.JobHostUnits}, + setAddresses{"2", network.NewAddresses("10.0.0.1")}, + startAliveMachine{"2"}, + setMachineStatus{"2", state.StatusStarted, ""}, + addAliveUnit{"mysql", "2"}, + setAgentStatus{"mysql/0", state.StatusIdle, "", nil}, + setUnitStatus{"mysql/0", state.StatusActive, "", nil}, + addService{name: "logging", charm: "logging"}, + setServiceExposed{"logging", true}, + relateServices{"wordpress", "mysql"}, + relateServices{"wordpress", "logging"}, + relateServices{"mysql", "logging"}, + addSubordinate{"wordpress/0", "logging"}, + addSubordinate{"mysql/0", "logging"}, + setUnitsAlive{"logging"}, + setAgentStatus{"logging/0", state.StatusIdle, "", nil}, + setUnitStatus{"logging/0", state.StatusActive, "", nil}, + setAgentStatus{"logging/1", state.StatusError, "somehow lost in all those logs", nil}, + } + for _, s := range steps { + s.step(c, ctx) + } + code, stdout, stderr := runStatus(c, "--format", "summary") + c.Check(code, gc.Equals, 0) + c.Check(string(stderr), gc.Equals, "") + c.Assert(string(stdout), gc.Equals, ` +Running on subnets: 127.0.0.1/8, 10.0.0.1/8 +Utilizing ports: + # MACHINES: (3) + started: 3 + + # UNITS: (4) + error: 1 + started: 3 + + # SERVICES: (3) + logging 1/1 exposed + mysql 1/1 exposed + wordpress 1/1 exposed + +`[1:]) +} +func (s *StatusSuite) TestStatusWithFormatOneline(c *gc.C) { + ctx := s.newContext(c) + defer s.resetContext(c, ctx) + steps := []stepper{ + addMachine{machineId: "0", job: state.JobManageEnviron}, + setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, + startAliveMachine{"0"}, + setMachineStatus{"0", state.StatusStarted, ""}, + addCharm{"wordpress"}, + addCharm{"mysql"}, + addCharm{"logging"}, + + addService{name: "wordpress", charm: "wordpress"}, + setServiceExposed{"wordpress", true}, + addMachine{machineId: "1", job: state.JobHostUnits}, + setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, + startAliveMachine{"1"}, + setMachineStatus{"1", state.StatusStarted, ""}, + addAliveUnit{"wordpress", "1"}, + setAgentStatus{"wordpress/0", state.StatusIdle, "", nil}, + setUnitStatus{"wordpress/0", state.StatusActive, "", nil}, + + addService{name: "mysql", charm: "mysql"}, + setServiceExposed{"mysql", true}, + addMachine{machineId: "2", job: state.JobHostUnits}, + setAddresses{"2", network.NewAddresses("dummyenv-2.dns")}, + startAliveMachine{"2"}, + setMachineStatus{"2", state.StatusStarted, ""}, + addAliveUnit{"mysql", "2"}, + setAgentStatus{"mysql/0", state.StatusIdle, "", nil}, + setUnitStatus{"mysql/0", state.StatusActive, "", nil}, + + addService{name: "logging", charm: "logging"}, + setServiceExposed{"logging", true}, + + relateServices{"wordpress", "mysql"}, + relateServices{"wordpress", "logging"}, + relateServices{"mysql", "logging"}, + + addSubordinate{"wordpress/0", "logging"}, + addSubordinate{"mysql/0", "logging"}, + + setUnitsAlive{"logging"}, + setAgentStatus{"logging/0", state.StatusIdle, "", nil}, + setUnitStatus{"logging/0", state.StatusActive, "", nil}, + setAgentStatus{"logging/1", state.StatusError, "somehow lost in all those logs", nil}, + } + + ctx.run(c, steps) + + const expectedV1 = ` +- mysql/0: dummyenv-2.dns (started) + - logging/1: dummyenv-2.dns (error) +- wordpress/0: dummyenv-1.dns (started) + - logging/0: dummyenv-1.dns (started) +` + assertOneLineStatus(c, expectedV1) + + const expectedV2 = ` +- mysql/0: dummyenv-2.dns (agent:idle, workload:active) + - logging/1: dummyenv-2.dns (agent:idle, workload:error) +- wordpress/0: dummyenv-1.dns (agent:idle, workload:active) + - logging/0: dummyenv-1.dns (agent:idle, workload:active) +` + s.PatchEnvironment(osenv.JujuCLIVersion, "2") + assertOneLineStatus(c, expectedV2) +} + +func assertOneLineStatus(c *gc.C, expected string) { + code, stdout, stderr := runStatus(c, "--format", "oneline") + c.Check(code, gc.Equals, 0) + c.Check(string(stderr), gc.Equals, "") + c.Assert(string(stdout), gc.Equals, expected) + + c.Log(`Check that "short" is an alias for oneline.`) + code, stdout, stderr = runStatus(c, "--format", "short") + c.Check(code, gc.Equals, 0) + c.Check(string(stderr), gc.Equals, "") + c.Assert(string(stdout), gc.Equals, expected) + + c.Log(`Check that "line" is an alias for oneline.`) + code, stdout, stderr = runStatus(c, "--format", "line") + c.Check(code, gc.Equals, 0) + c.Check(string(stderr), gc.Equals, "") + c.Assert(string(stdout), gc.Equals, expected) +} + +func (s *StatusSuite) prepareTabularData(c *gc.C) *context { + ctx := s.newContext(c) + steps := []stepper{ + setToolsUpgradeAvailable{}, + addMachine{machineId: "0", job: state.JobManageEnviron}, + setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, + startAliveMachine{"0"}, + setMachineStatus{"0", state.StatusStarted, ""}, + addCharm{"wordpress"}, + addCharm{"mysql"}, + addCharm{"logging"}, + addService{name: "wordpress", charm: "wordpress"}, + setServiceExposed{"wordpress", true}, + addMachine{machineId: "1", job: state.JobHostUnits}, + setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, + startAliveMachine{"1"}, + setMachineStatus{"1", state.StatusStarted, ""}, + addAliveUnit{"wordpress", "1"}, + setAgentStatus{"wordpress/0", state.StatusIdle, "", nil}, + setUnitStatus{"wordpress/0", state.StatusActive, "", nil}, + setUnitTools{"wordpress/0", version.MustParseBinary("1.2.3-trusty-ppc")}, + addService{name: "mysql", charm: "mysql"}, + setServiceExposed{"mysql", true}, + addMachine{machineId: "2", job: state.JobHostUnits}, + setAddresses{"2", network.NewAddresses("dummyenv-2.dns")}, + startAliveMachine{"2"}, + setMachineStatus{"2", state.StatusStarted, ""}, + addAliveUnit{"mysql", "2"}, + setAgentStatus{"mysql/0", state.StatusIdle, "", nil}, + setUnitStatus{ + "mysql/0", + state.StatusMaintenance, + "installing all the things", nil}, + setUnitTools{"mysql/0", version.MustParseBinary("1.2.3-trusty-ppc")}, + addService{name: "logging", charm: "logging"}, + setServiceExposed{"logging", true}, + relateServices{"wordpress", "mysql"}, + relateServices{"wordpress", "logging"}, + relateServices{"mysql", "logging"}, + addSubordinate{"wordpress/0", "logging"}, + addSubordinate{"mysql/0", "logging"}, + setUnitsAlive{"logging"}, + setAgentStatus{"logging/0", state.StatusIdle, "", nil}, + setUnitStatus{"logging/0", state.StatusActive, "", nil}, + setAgentStatus{"logging/1", state.StatusError, "somehow lost in all those logs", nil}, + } + for _, s := range steps { + s.step(c, ctx) + } + return ctx +} + +func (s *StatusSuite) testStatusWithFormatTabular(c *gc.C, useFeatureFlag bool) { + ctx := s.prepareTabularData(c) + defer s.resetContext(c, ctx) + var args []string + if !useFeatureFlag { + args = []string{"--format", "tabular"} + } + code, stdout, stderr := runStatus(c, args...) + c.Check(code, gc.Equals, 0) + c.Check(string(stderr), gc.Equals, "") + const expected = ` +[Environment] +UPGRADE-AVAILABLE +%s + +[Services] +NAME STATUS EXPOSED CHARM +logging true cs:quantal/logging-1 +mysql maintenance true cs:quantal/mysql-1 +wordpress active true cs:quantal/wordpress-3 + +[Units] +ID WORKLOAD-STATE AGENT-STATE VERSION MACHINE PORTS PUBLIC-ADDRESS MESSAGE +mysql/0 maintenance idle 1.2.3 2 dummyenv-2.dns installing all the things + logging/1 error idle dummyenv-2.dns somehow lost in all those logs +wordpress/0 active idle 1.2.3 1 dummyenv-1.dns + logging/0 active idle dummyenv-1.dns + +[Machines] +ID STATE VERSION DNS INS-ID SERIES HARDWARE +0 started dummyenv-0.dns dummyenv-0 quantal arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M +1 started dummyenv-1.dns dummyenv-1 quantal arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M +2 started dummyenv-2.dns dummyenv-2 quantal arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M + +` + c.Assert(string(stdout), gc.Equals, fmt.Sprintf(expected[1:], nextVersion())) +} + +func (s *StatusSuite) TestStatusV2(c *gc.C) { + s.PatchValue(&version.Current, version.MustParseBinary("1.25.0-trusty-amd64")) + s.PatchEnvironment(osenv.JujuCLIVersion, "2") + s.testStatusWithFormatTabular(c, true) +} + +func (s *StatusSuite) TestStatusWithFormatTabular(c *gc.C) { + s.PatchValue(&version.Current, version.MustParseBinary("1.25.0-trusty-amd64")) + s.testStatusWithFormatTabular(c, false) +} + +func (s *StatusSuite) TestFormatTabularHookActionName(c *gc.C) { + status := formattedStatus{ + Services: map[string]serviceStatus{ + "foo": serviceStatus{ + Units: map[string]unitStatus{ + "foo/0": unitStatus{ + AgentStatusInfo: statusInfoContents{ + Current: params.StatusExecuting, + Message: "running config-changed hook", + }, + WorkloadStatusInfo: statusInfoContents{ + Current: params.StatusMaintenance, + Message: "doing some work", + }, + }, + "foo/1": unitStatus{ + AgentStatusInfo: statusInfoContents{ + Current: params.StatusExecuting, + Message: "running action backup database", + }, + WorkloadStatusInfo: statusInfoContents{ + Current: params.StatusMaintenance, + Message: "doing some work", + }, + }, + }, + }, + }, + } + out, err := FormatTabular(status) + c.Assert(err, jc.ErrorIsNil) + c.Assert(string(out), gc.Equals, ` +[Services] +NAME STATUS EXPOSED CHARM +foo false + +[Units] +ID WORKLOAD-STATE AGENT-STATE VERSION MACHINE PORTS PUBLIC-ADDRESS MESSAGE +foo/0 maintenance executing (config-changed) doing some work +foo/1 maintenance executing (backup database) doing some work + +[Machines] +ID STATE VERSION DNS INS-ID SERIES HARDWARE +`[1:]) +} + +func (s *StatusSuite) TestStatusWithNilStatusApi(c *gc.C) { + ctx := s.newContext(c) + defer s.resetContext(c, ctx) + steps := []stepper{ + addMachine{machineId: "0", job: state.JobManageEnviron}, + setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, + startAliveMachine{"0"}, + setMachineStatus{"0", state.StatusStarted, ""}, + } + + for _, s := range steps { + s.step(c, ctx) + } + + client := fakeApiClient{} + var status = client.Status + s.PatchValue(&status, func(_ []string) (*params.FullStatus, error) { + return nil, nil + }) + s.PatchValue(&newApiClientForStatus, func(_ *StatusCommand) (statusAPI, error) { + return &client, nil + }) + + code, _, stderr := runStatus(c, "--format", "tabular") + c.Check(code, gc.Equals, 1) + c.Check(string(stderr), gc.Equals, "error: unable to obtain the current status\n") +} + +// +// Filtering Feature +// + +func (s *StatusSuite) FilteringTestSetup(c *gc.C) *context { + ctx := s.newContext(c) + + steps := []stepper{ + // Given a machine is started + // And the machine's ID is "0" + // And the machine's job is to manage the environment + addMachine{machineId: "0", job: state.JobManageEnviron}, + startAliveMachine{"0"}, + setMachineStatus{"0", state.StatusStarted, ""}, + // And the machine's address is "dummyenv-0.dns" + setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, + // And the "wordpress" charm is available + addCharm{"wordpress"}, + addService{name: "wordpress", charm: "wordpress"}, + // And the "mysql" charm is available + addCharm{"mysql"}, + addService{name: "mysql", charm: "mysql"}, + // And the "logging" charm is available + addCharm{"logging"}, + // And a machine is started + // And the machine's ID is "1" + // And the machine's job is to host units + addMachine{machineId: "1", job: state.JobHostUnits}, + startAliveMachine{"1"}, + setMachineStatus{"1", state.StatusStarted, ""}, + // And the machine's address is "dummyenv-1.dns" + setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, + // And a unit of "wordpress" is deployed to machine "1" + addAliveUnit{"wordpress", "1"}, + // And the unit is started + setAgentStatus{"wordpress/0", state.StatusIdle, "", nil}, + setUnitStatus{"wordpress/0", state.StatusActive, "", nil}, + // And a machine is started + + // And the machine's ID is "2" + // And the machine's job is to host units + addMachine{machineId: "2", job: state.JobHostUnits}, + startAliveMachine{"2"}, + setMachineStatus{"2", state.StatusStarted, ""}, + // And the machine's address is "dummyenv-2.dns" + setAddresses{"2", network.NewAddresses("dummyenv-2.dns")}, + // And a unit of "mysql" is deployed to machine "2" + addAliveUnit{"mysql", "2"}, + // And the unit is started + setAgentStatus{"mysql/0", state.StatusIdle, "", nil}, + setUnitStatus{"mysql/0", state.StatusActive, "", nil}, + // And the "logging" service is added + addService{name: "logging", charm: "logging"}, + // And the service is exposed + setServiceExposed{"logging", true}, + // And the "wordpress" service is related to the "mysql" service + relateServices{"wordpress", "mysql"}, + // And the "wordpress" service is related to the "logging" service + relateServices{"wordpress", "logging"}, + // And the "mysql" service is related to the "logging" service + relateServices{"mysql", "logging"}, + // And the "logging" service is a subordinate to unit 0 of the "wordpress" service + addSubordinate{"wordpress/0", "logging"}, + setAgentStatus{"logging/0", state.StatusIdle, "", nil}, + setUnitStatus{"logging/0", state.StatusActive, "", nil}, + // And the "logging" service is a subordinate to unit 0 of the "mysql" service + addSubordinate{"mysql/0", "logging"}, + setAgentStatus{"logging/1", state.StatusIdle, "", nil}, + setUnitStatus{"logging/1", state.StatusActive, "", nil}, + setUnitsAlive{"logging"}, + } + + ctx.run(c, steps) + return ctx +} + +// Scenario: One unit is in an errored state and user filters to started +func (s *StatusSuite) TestFilterToStarted(c *gc.C) { + ctx := s.FilteringTestSetup(c) + defer s.resetContext(c, ctx) + + // Given unit 1 of the "logging" service has an error + setAgentStatus{"logging/1", state.StatusError, "mock error", nil}.step(c, ctx) + // And unit 0 of the "mysql" service has an error + setAgentStatus{"mysql/0", state.StatusError, "mock error", nil}.step(c, ctx) + // When I run juju status --format oneline started + _, stdout, stderr := runStatus(c, "--format", "oneline", "started") + c.Assert(string(stderr), gc.Equals, "") + // Then I should receive output prefixed with: + const expected = ` + +- wordpress/0: dummyenv-1.dns (started) + - logging/0: dummyenv-1.dns (started) +` + c.Assert(string(stdout), gc.Equals, expected[1:]) +} + +// Scenario: One unit is in an errored state and user filters to errored +func (s *StatusSuite) TestFilterToErrored(c *gc.C) { + ctx := s.FilteringTestSetup(c) + defer s.resetContext(c, ctx) + + // Given unit 1 of the "logging" service has an error + setAgentStatus{"logging/1", state.StatusError, "mock error", nil}.step(c, ctx) + // When I run juju status --format oneline error + _, stdout, stderr := runStatus(c, "--format", "oneline", "error") + c.Assert(stderr, gc.IsNil) + // Then I should receive output prefixed with: + const expected = ` + +- mysql/0: dummyenv-2.dns (started) + - logging/1: dummyenv-2.dns (error) +` + c.Assert(string(stdout), gc.Equals, expected[1:]) +} + +// Scenario: User filters to mysql service +func (s *StatusSuite) TestFilterToService(c *gc.C) { + ctx := s.FilteringTestSetup(c) + defer s.resetContext(c, ctx) + + // When I run juju status --format oneline error + _, stdout, stderr := runStatus(c, "--format", "oneline", "mysql") + c.Assert(stderr, gc.IsNil) + // Then I should receive output prefixed with: + const expected = ` + +- mysql/0: dummyenv-2.dns (started) + - logging/1: dummyenv-2.dns (started) +` + + c.Assert(string(stdout), gc.Equals, expected[1:]) +} + +// Scenario: User filters to exposed services +func (s *StatusSuite) TestFilterToExposedService(c *gc.C) { + ctx := s.FilteringTestSetup(c) + defer s.resetContext(c, ctx) + + // Given unit 1 of the "mysql" service is exposed + setServiceExposed{"mysql", true}.step(c, ctx) + // And the logging service is not exposed + setServiceExposed{"logging", false}.step(c, ctx) + // And the wordpress service is not exposed + setServiceExposed{"wordpress", false}.step(c, ctx) + // When I run juju status --format oneline exposed + _, stdout, stderr := runStatus(c, "--format", "oneline", "exposed") + c.Assert(stderr, gc.IsNil) + // Then I should receive output prefixed with: + const expected = ` + +- mysql/0: dummyenv-2.dns (started) + - logging/1: dummyenv-2.dns (started) +` + c.Assert(string(stdout), gc.Equals, expected[1:]) +} + +// Scenario: User filters to non-exposed services +func (s *StatusSuite) TestFilterToNotExposedService(c *gc.C) { + ctx := s.FilteringTestSetup(c) + defer s.resetContext(c, ctx) + + setServiceExposed{"mysql", true}.step(c, ctx) + // When I run juju status --format oneline not exposed + _, stdout, stderr := runStatus(c, "--format", "oneline", "not", "exposed") + c.Assert(stderr, gc.IsNil) + // Then I should receive output prefixed with: + const expected = ` + +- wordpress/0: dummyenv-1.dns (started) + - logging/0: dummyenv-1.dns (started) +` + c.Assert(string(stdout), gc.Equals, expected[1:]) +} + +// Scenario: Filtering on Subnets +func (s *StatusSuite) TestFilterOnSubnet(c *gc.C) { + ctx := s.FilteringTestSetup(c) + defer s.resetContext(c, ctx) + + // Given the address for machine "1" is "localhost" + setAddresses{"1", network.NewAddresses("localhost")}.step(c, ctx) + // And the address for machine "2" is "10.0.0.1" + setAddresses{"2", network.NewAddresses("10.0.0.1")}.step(c, ctx) + // When I run juju status --format oneline 127.0.0.1 + _, stdout, stderr := runStatus(c, "--format", "oneline", "127.0.0.1") + c.Assert(stderr, gc.IsNil) + // Then I should receive output prefixed with: + const expected = ` + +- wordpress/0: localhost (started) + - logging/0: localhost (started) +` + c.Assert(string(stdout), gc.Equals, expected[1:]) +} + +// Scenario: Filtering on Ports +func (s *StatusSuite) TestFilterOnPorts(c *gc.C) { + ctx := s.FilteringTestSetup(c) + defer s.resetContext(c, ctx) + + // Given the address for machine "1" is "localhost" + setAddresses{"1", network.NewAddresses("localhost")}.step(c, ctx) + // And the address for machine "2" is "10.0.0.1" + setAddresses{"2", network.NewAddresses("10.0.0.1")}.step(c, ctx) + openUnitPort{"wordpress/0", "tcp", 80}.step(c, ctx) + // When I run juju status --format oneline 80/tcp + _, stdout, stderr := runStatus(c, "--format", "oneline", "80/tcp") + c.Assert(stderr, gc.IsNil) + // Then I should receive output prefixed with: + const expected = ` + +- wordpress/0: localhost (started) 80/tcp + - logging/0: localhost (started) +` + c.Assert(string(stdout), gc.Equals, expected[1:]) +} + +// Scenario: User filters out a parent, but not its subordinate +func (s *StatusSuite) TestFilterParentButNotSubordinate(c *gc.C) { + ctx := s.FilteringTestSetup(c) + defer s.resetContext(c, ctx) + + // When I run juju status --format oneline 80/tcp + _, stdout, stderr := runStatus(c, "--format", "oneline", "logging") + c.Assert(stderr, gc.IsNil) + // Then I should receive output prefixed with: + const expected = ` + +- mysql/0: dummyenv-2.dns (started) + - logging/1: dummyenv-2.dns (started) +- wordpress/0: dummyenv-1.dns (started) + - logging/0: dummyenv-1.dns (started) +` + c.Assert(string(stdout), gc.Equals, expected[1:]) +} + +// Scenario: User filters out a subordinate, but not its parent +func (s *StatusSuite) TestFilterSubordinateButNotParent(c *gc.C) { + ctx := s.FilteringTestSetup(c) + defer s.resetContext(c, ctx) + + // Given the wordpress service is exposed + setServiceExposed{"wordpress", true}.step(c, ctx) + // When I run juju status --format oneline not exposed + _, stdout, stderr := runStatus(c, "--format", "oneline", "not", "exposed") + c.Assert(stderr, gc.IsNil) + // Then I should receive output prefixed with: + const expected = ` + +- mysql/0: dummyenv-2.dns (started) + - logging/1: dummyenv-2.dns (started) +` + c.Assert(string(stdout), gc.Equals, expected[1:]) +} + +func (s *StatusSuite) TestFilterMultipleHomogenousPatterns(c *gc.C) { + ctx := s.FilteringTestSetup(c) + defer s.resetContext(c, ctx) + + _, stdout, stderr := runStatus(c, "--format", "oneline", "wordpress/0", "mysql/0") + c.Assert(stderr, gc.IsNil) + // Then I should receive output prefixed with: + const expected = ` + +- mysql/0: dummyenv-2.dns (started) + - logging/1: dummyenv-2.dns (started) +- wordpress/0: dummyenv-1.dns (started) + - logging/0: dummyenv-1.dns (started) +` + c.Assert(string(stdout), gc.Equals, expected[1:]) +} + +func (s *StatusSuite) TestFilterMultipleHeterogenousPatterns(c *gc.C) { + ctx := s.FilteringTestSetup(c) + defer s.resetContext(c, ctx) + + _, stdout, stderr := runStatus(c, "--format", "oneline", "wordpress/0", "started") + c.Assert(stderr, gc.IsNil) + // Then I should receive output prefixed with: + const expected = ` + +- mysql/0: dummyenv-2.dns (started) + - logging/1: dummyenv-2.dns (started) +- wordpress/0: dummyenv-1.dns (started) + - logging/0: dummyenv-1.dns (started) +` + c.Assert(string(stdout), gc.Equals, expected[1:]) +} + +// TestSummaryStatusWithUnresolvableDns is result of bug# 1410320. +func (s *StatusSuite) TestSummaryStatusWithUnresolvableDns(c *gc.C) { + formatter := &summaryFormatter{} + formatter.resolveAndTrackIp("invalidDns") + // Test should not panic. +} + +func initStatusCommand(args ...string) (*StatusCommand, error) { + com := &StatusCommand{} + return com, coretesting.InitCommand(envcmd.Wrap(com), args) +} + +var statusInitTests = []struct { + args []string + envVar string + isoTime bool + err string +}{ + { + isoTime: false, + }, { + args: []string{"--utc"}, + isoTime: true, + }, { + envVar: "true", + isoTime: true, + }, { + envVar: "foo", + err: "invalid JUJU_STATUS_ISO_TIME env var, expected true|false.*", + }, +} + +func (*StatusSuite) TestStatusCommandInit(c *gc.C) { + defer os.Setenv(osenv.JujuStatusIsoTimeEnvKey, os.Getenv(osenv.JujuStatusIsoTimeEnvKey)) + + for i, t := range statusInitTests { + c.Logf("test %d", i) + os.Setenv(osenv.JujuStatusIsoTimeEnvKey, t.envVar) + com, err := initStatusCommand(t.args...) + if t.err != "" { + c.Check(err, gc.ErrorMatches, t.err) + } else { + c.Check(err, jc.ErrorIsNil) + } + c.Check(com.isoTime, gc.DeepEquals, t.isoTime) + } +} + +var statusTimeTest = test( + "status generates timestamps as UTC in ISO format", + addMachine{machineId: "0", job: state.JobManageEnviron}, + setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, + startAliveMachine{"0"}, + setMachineStatus{"0", state.StatusStarted, ""}, + addCharm{"dummy"}, + addService{name: "dummy-service", charm: "dummy"}, + + addMachine{machineId: "1", job: state.JobHostUnits}, + startAliveMachine{"1"}, + setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, + setMachineStatus{"1", state.StatusStarted, ""}, + + addAliveUnit{"dummy-service", "1"}, + expect{ + "add two units, one alive (in error state), one started", + M{ + "environment": "dummyenv", + "machines": M{ + "0": machine0, + "1": machine1, + }, + "services": M{ + "dummy-service": M{ + "charm": "cs:quantal/dummy-1", + "exposed": false, + "service-status": M{ + "current": "unknown", + "message": "Waiting for agent initialization to finish", + "since": "01 Apr 15 01:23+10:00", + }, + "units": M{ + "dummy-service/0": M{ + "machine": "1", + "agent-state": "pending", + "workload-status": M{ + "current": "unknown", + "message": "Waiting for agent initialization to finish", + "since": "01 Apr 15 01:23+10:00", + }, + "agent-status": M{ + "current": "allocating", + "since": "01 Apr 15 01:23+10:00", + }, + "public-address": "dummyenv-1.dns", + }, + }, + }, + }, + }, + }, +) + +func (s *StatusSuite) TestIsoTimeFormat(c *gc.C) { + func(t testCase) { + // Prepare context and run all steps to setup. + ctx := s.newContext(c) + ctx.expectIsoTime = true + defer s.resetContext(c, ctx) + ctx.run(c, t.steps) + }(statusTimeTest) +} + +func (s *StatusSuite) TestFormatProvisioningError(c *gc.C) { + status := ¶ms.FullStatus{ + Machines: map[string]params.MachineStatus{ + "1": params.MachineStatus{ + Agent: params.AgentStatus{ + Status: "error", + Info: "", + }, + AgentState: "", + AgentStateInfo: "", + InstanceId: "pending", + InstanceState: "", + Series: "trusty", + Id: "1", + Jobs: []multiwatcher.MachineJob{"JobHostUnits"}, + }, + }, + } + formatter := newStatusFormatter(status, 0, true) + formatted := formatter.format() + + c.Check(formatted, jc.DeepEquals, formattedStatus{ + Machines: map[string]machineStatus{ + "1": machineStatus{ + AgentState: "error", + AgentStateInfo: "", + InstanceId: "pending", + Series: "trusty", + Id: "1", + Containers: map[string]machineStatus{}, + }, + }, + Services: map[string]serviceStatus{}, + }) +} === added file 'src/github.com/juju/juju/cmd/juju/status/utils.go' --- src/github.com/juju/juju/cmd/juju/status/utils.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/status/utils.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,38 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package status + +import ( + "fmt" + "reflect" + + "github.com/juju/juju/cmd/juju/common" +) + +// stringKeysFromMap takes a map with keys which are strings and returns +// only the keys. +func stringKeysFromMap(m interface{}) (keys []string) { + for _, k := range reflect.ValueOf(m).MapKeys() { + keys = append(keys, k.String()) + } + return +} + +// recurseUnits calls the given recurseMap function on the given unit +// and its subordinates (recursively defined on the given unit). +func recurseUnits(u unitStatus, il int, recurseMap func(string, unitStatus, int)) { + if len(u.Subordinates) == 0 { + return + } + for _, uName := range common.SortStringsNaturally(stringKeysFromMap(u.Subordinates)) { + unit := u.Subordinates[uName] + recurseMap(uName, unit, il) + recurseUnits(unit, il+1, recurseMap) + } +} + +// indent prepends a format string with the given number of spaces. +func indent(prepend string, level int, append string) string { + return fmt.Sprintf("%s%*s%s", prepend, level, "", append) +} === removed file 'src/github.com/juju/juju/cmd/juju/status_formatters.go' --- src/github.com/juju/juju/cmd/juju/status_formatters.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/status_formatters.go 1970-01-01 00:00:00 +0000 @@ -1,383 +0,0 @@ -// Copyright 2014 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "bytes" - "fmt" - "net" - "reflect" - "regexp" - "sort" - "strings" - "text/tabwriter" - - "github.com/juju/errors" - "github.com/juju/utils/set" - "gopkg.in/juju/charm.v5/hooks" - - "github.com/juju/juju/apiserver/params" -) - -// FormatOneline returns a brief list of units and their subordinates. -// Subordinates will be indented 2 spaces and listed under their -// superiors. -func FormatOneline(value interface{}) ([]byte, error) { - fs, valueConverted := value.(formattedStatus) - if !valueConverted { - return nil, errors.Errorf("expected value of type %T, got %T", fs, value) - } - var out bytes.Buffer - - pprint := func(uName string, u unitStatus, level int) { - var fmtPorts string - if len(u.OpenedPorts) > 0 { - fmtPorts = fmt.Sprintf(" %s", strings.Join(u.OpenedPorts, ", ")) - } - fmt.Fprintf(&out, indent("\n", level*2, "- %s: %s (%v)%v"), - uName, - u.PublicAddress, - u.AgentState, - fmtPorts, - ) - } - - for _, svcName := range sortStringsNaturally(stringKeysFromMap(fs.Services)) { - svc := fs.Services[svcName] - for _, uName := range sortStringsNaturally(stringKeysFromMap(svc.Units)) { - unit := svc.Units[uName] - pprint(uName, unit, 0) - recurseUnits(unit, 1, pprint) - } - } - if fs.AvailableVersion != "" { - fmt.Fprintf(&out, "\n- new available version: %q", fs.AvailableVersion) - } - return out.Bytes(), nil -} - -// agentDoing returns what hook or action, if any, -// the agent is currently executing. -// The hook name or action is extracted from the agent message. -func agentDoing(status statusInfoContents) string { - if status.Current != params.StatusExecuting { - return "" - } - // First see if we can determine a hook name. - var hookNames []string - for _, h := range hooks.UnitHooks() { - hookNames = append(hookNames, string(h)) - } - for _, h := range hooks.RelationHooks() { - hookNames = append(hookNames, string(h)) - } - hookExp := regexp.MustCompile(fmt.Sprintf(`running (?P%s?) hook`, strings.Join(hookNames, "|"))) - match := hookExp.FindStringSubmatch(status.Message) - if len(match) > 0 { - return match[1] - } - // Now try for an action name. - actionExp := regexp.MustCompile(`running action (?P.*)`) - match = actionExp.FindStringSubmatch(status.Message) - if len(match) > 0 { - return match[1] - } - return "" -} - -// FormatTabular returns a tabular summary of machines, services, and -// units. Any subordinate items are indented by two spaces beneath -// their superior. -func FormatTabular(value interface{}) ([]byte, error) { - fs, valueConverted := value.(formattedStatus) - if !valueConverted { - return nil, errors.Errorf("expected value of type %T, got %T", fs, value) - } - var out bytes.Buffer - // To format things into columns. - tw := tabwriter.NewWriter(&out, 0, 1, 1, ' ', 0) - p := func(values ...interface{}) { - for _, v := range values { - fmt.Fprintf(tw, "%s\t", v) - } - fmt.Fprintln(tw) - } - - units := make(map[string]unitStatus) - p("[Services]") - p("NAME\tSTATUS\tEXPOSED\tCHARM") - for _, svcName := range sortStringsNaturally(stringKeysFromMap(fs.Services)) { - svc := fs.Services[svcName] - for un, u := range svc.Units { - units[un] = u - } - p(svcName, svc.StatusInfo.Current, fmt.Sprintf("%t", svc.Exposed), svc.Charm) - } - tw.Flush() - - pUnit := func(name string, u unitStatus, level int) { - message := u.WorkloadStatusInfo.Message - agentDoing := agentDoing(u.AgentStatusInfo) - if agentDoing != "" { - message = fmt.Sprintf("(%s) %s", agentDoing, message) - } - p( - indent("", level*2, name), - u.WorkloadStatusInfo.Current, - u.AgentStatusInfo.Current, - u.AgentStatusInfo.Version, - u.Machine, - strings.Join(u.OpenedPorts, ","), - u.PublicAddress, - message, - ) - } - - // See if we have new or old data; that determines what data we can display. - newStatus := false - for _, u := range units { - if u.AgentStatusInfo.Current != "" { - newStatus = true - break - } - } - var header []string - if newStatus { - header = []string{"ID", "WORKLOAD-STATE", "AGENT-STATE", "VERSION", "MACHINE", "PORTS", "PUBLIC-ADDRESS", "MESSAGE"} - } else { - header = []string{"ID", "STATE", "VERSION", "MACHINE", "PORTS", "PUBLIC-ADDRESS"} - } - - p("\n[Units]") - p(strings.Join(header, "\t")) - for _, name := range sortStringsNaturally(stringKeysFromMap(units)) { - u := units[name] - pUnit(name, u, 0) - const indentationLevel = 1 - recurseUnits(u, indentationLevel, pUnit) - } - tw.Flush() - - p("\n[Machines]") - p("ID\tSTATE\tVERSION\tDNS\tINS-ID\tSERIES\tHARDWARE") - for _, name := range sortStringsNaturally(stringKeysFromMap(fs.Machines)) { - m := fs.Machines[name] - p(m.Id, m.AgentState, m.AgentVersion, m.DNSName, m.InstanceId, m.Series, m.Hardware) - } - tw.Flush() - - if fs.AvailableVersion != "" { - p("\n[Juju]") - p("UPGRADE-AVAILABLE") - p(fs.AvailableVersion) - } - tw.Flush() - - return out.Bytes(), nil -} - -// FormatSummary returns a summary of the current environment -// including the following information: -// - Headers: -// - All subnets the environment occupies. -// - All ports the environment utilizes. -// - Sections: -// - Machines: Displays total #, and then the # in each state. -// - Units: Displays total #, and then # in each state. -// - Services: Displays total #, their names, and how many of each -// are exposed. -func FormatSummary(value interface{}) ([]byte, error) { - fs, valueConverted := value.(formattedStatus) - if !valueConverted { - return nil, errors.Errorf("expected value of type %T, got %T", fs, value) - } - - f := newSummaryFormatter() - stateToMachine := f.aggregateMachineStates(fs.Machines) - svcExposure := f.aggregateServiceAndUnitStates(fs.Services) - p := f.delimitValuesWithTabs - - // Print everything out - p("Running on subnets:", strings.Join(f.netStrings, ", ")) - p("Utilizing ports:", f.portsInColumnsOf(3)) - f.tw.Flush() - - // Right align summary information - f.tw.Init(&f.out, 0, 2, 1, ' ', tabwriter.AlignRight) - p("# MACHINES:", fmt.Sprintf("(%d)", len(fs.Machines))) - f.printStateToCount(stateToMachine) - p(" ") - - p("# UNITS:", fmt.Sprintf("(%d)", f.numUnits)) - f.printStateToCount(f.stateToUnit) - p(" ") - - p("# SERVICES:", fmt.Sprintf(" (%d)", len(fs.Services))) - for _, svcName := range sortStringsNaturally(stringKeysFromMap(svcExposure)) { - s := svcExposure[svcName] - p(svcName, fmt.Sprintf("%d/%d\texposed", s[true], s[true]+s[false])) - } - f.tw.Flush() - - return f.out.Bytes(), nil -} - -func newSummaryFormatter() *summaryFormatter { - f := &summaryFormatter{ - ipAddrs: make([]net.IPNet, 0), - netStrings: make([]string, 0), - openPorts: set.NewStrings(), - stateToUnit: make(map[params.Status]int), - } - f.tw = tabwriter.NewWriter(&f.out, 0, 1, 1, ' ', 0) - return f -} - -type summaryFormatter struct { - ipAddrs []net.IPNet - netStrings []string - numUnits int - openPorts set.Strings - // status -> count - stateToUnit map[params.Status]int - tw *tabwriter.Writer - out bytes.Buffer -} - -func (f *summaryFormatter) delimitValuesWithTabs(values ...string) { - for _, v := range values { - fmt.Fprintf(f.tw, "%s\t", v) - } - fmt.Fprintln(f.tw) -} - -func (f *summaryFormatter) portsInColumnsOf(col int) string { - - var b bytes.Buffer - for i, p := range f.openPorts.SortedValues() { - if i != 0 && i%col == 0 { - fmt.Fprintf(&b, "\n\t") - } - fmt.Fprintf(&b, "%s, ", p) - } - // Elide the last delimiter - portList := b.String() - if len(portList) >= 2 { - return portList[:b.Len()-2] - } - return portList -} - -func (f *summaryFormatter) trackUnit(name string, status unitStatus, indentLevel int) { - f.resolveAndTrackIp(status.PublicAddress) - - for _, p := range status.OpenedPorts { - if p != "" { - f.openPorts.Add(p) - } - } - f.numUnits++ - f.stateToUnit[status.AgentState]++ -} - -func (f *summaryFormatter) printStateToCount(m map[params.Status]int) { - for _, status := range sortStringsNaturally(stringKeysFromMap(m)) { - numInStatus := m[params.Status(status)] - f.delimitValuesWithTabs(status+":", fmt.Sprintf(" %d ", numInStatus)) - } -} - -func (f *summaryFormatter) trackIp(ip net.IP) { - for _, net := range f.ipAddrs { - if net.Contains(ip) { - return - } - } - - ipNet := net.IPNet{ip, ip.DefaultMask()} - f.ipAddrs = append(f.ipAddrs, ipNet) - f.netStrings = append(f.netStrings, ipNet.String()) -} - -func (f *summaryFormatter) resolveAndTrackIp(publicDns string) { - // TODO(katco-): We may be able to utilize upcoming work which will expose these addresses outright. - ip, err := net.ResolveIPAddr("ip4", publicDns) - if err != nil { - logger.Warningf( - "unable to resolve %s to an IP address. Status may be incorrect: %v", - publicDns, - err, - ) - return - } - f.trackIp(ip.IP) -} - -func (f *summaryFormatter) aggregateMachineStates(machines map[string]machineStatus) map[params.Status]int { - stateToMachine := make(map[params.Status]int) - for _, name := range sortStringsNaturally(stringKeysFromMap(machines)) { - m := machines[name] - f.resolveAndTrackIp(m.DNSName) - - if agentState := m.AgentState; agentState == "" { - agentState = params.StatusPending - } else { - stateToMachine[agentState]++ - } - } - return stateToMachine -} - -func (f *summaryFormatter) aggregateServiceAndUnitStates(services map[string]serviceStatus) map[string]map[bool]int { - svcExposure := make(map[string]map[bool]int) - for _, name := range sortStringsNaturally(stringKeysFromMap(services)) { - s := services[name] - // Grab unit states - for _, un := range sortStringsNaturally(stringKeysFromMap(s.Units)) { - u := s.Units[un] - f.trackUnit(un, u, 0) - recurseUnits(u, 1, f.trackUnit) - } - - if _, ok := svcExposure[name]; !ok { - svcExposure[name] = make(map[bool]int) - } - - svcExposure[name][s.Exposed]++ - } - return svcExposure -} - -// sortStringsNaturally is syntactic sugar so we can do sorts in one line. -func sortStringsNaturally(s []string) []string { - sort.Sort(naturally(s)) - return s -} - -// stringKeysFromMap takes a map with keys which are strings and returns -// only the keys. -func stringKeysFromMap(m interface{}) (keys []string) { - for _, k := range reflect.ValueOf(m).MapKeys() { - keys = append(keys, k.String()) - } - return -} - -// recurseUnits calls the given recurseMap function on the given unit -// and its subordinates (recursively defined on the given unit). -func recurseUnits(u unitStatus, il int, recurseMap func(string, unitStatus, int)) { - if len(u.Subordinates) == 0 { - return - } - for _, uName := range sortStringsNaturally(stringKeysFromMap(u.Subordinates)) { - unit := u.Subordinates[uName] - recurseMap(uName, unit, il) - recurseUnits(unit, il+1, recurseMap) - } -} - -// indent prepends a format string with the given number of spaces. -func indent(prepend string, level int, append string) string { - return fmt.Sprintf("%s%*s%s", prepend, level, "", append) -} === removed file 'src/github.com/juju/juju/cmd/juju/status_test.go' --- src/github.com/juju/juju/cmd/juju/status_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/status_test.go 1970-01-01 00:00:00 +0000 @@ -1,3643 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "bytes" - "encoding/json" - "fmt" - "os" - "regexp" - "strings" - "time" - - "github.com/juju/cmd" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - "gopkg.in/juju/charm.v5" - goyaml "gopkg.in/yaml.v1" - - "github.com/juju/juju/api" - "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/constraints" - "github.com/juju/juju/environs" - "github.com/juju/juju/instance" - "github.com/juju/juju/juju/osenv" - "github.com/juju/juju/juju/testing" - "github.com/juju/juju/network" - "github.com/juju/juju/state" - "github.com/juju/juju/state/multiwatcher" - "github.com/juju/juju/state/presence" - "github.com/juju/juju/testcharms" - coretesting "github.com/juju/juju/testing" - "github.com/juju/juju/version" -) - -func defineNextVersion() string { - ver := version.Current.Number - ver.Patch++ - return ver.String() -} - -var nextVersion = defineNextVersion() - -func runStatus(c *gc.C, args ...string) (code int, stdout, stderr []byte) { - ctx := coretesting.Context(c) - code = cmd.Main(envcmd.Wrap(&StatusCommand{}), ctx, args) - stdout = ctx.Stdout.(*bytes.Buffer).Bytes() - stderr = ctx.Stderr.(*bytes.Buffer).Bytes() - return -} - -type StatusSuite struct { - testing.JujuConnSuite -} - -var _ = gc.Suite(&StatusSuite{}) - -type M map[string]interface{} - -type L []interface{} - -type testCase struct { - summary string - steps []stepper -} - -func test(summary string, steps ...stepper) testCase { - return testCase{summary, steps} -} - -type stepper interface { - step(c *gc.C, ctx *context) -} - -// -// context -// - -func newContext(c *gc.C, st *state.State, env environs.Environ, adminUserTag string) *context { - // We make changes in the API server's state so that - // our changes to presence are immediately noticed - // in the status. - return &context{ - st: st, - env: env, - charms: make(map[string]*state.Charm), - pingers: make(map[string]*presence.Pinger), - adminUserTag: adminUserTag, - } -} - -type context struct { - st *state.State - env environs.Environ - charms map[string]*state.Charm - pingers map[string]*presence.Pinger - adminUserTag string // A string repr of the tag. - expectIsoTime bool -} - -func (ctx *context) reset(c *gc.C) { - for _, up := range ctx.pingers { - err := up.Kill() - c.Check(err, jc.ErrorIsNil) - } -} - -func (ctx *context) run(c *gc.C, steps []stepper) { - for i, s := range steps { - c.Logf("step %d", i) - c.Logf("%#v", s) - s.step(c, ctx) - } -} - -func (ctx *context) setAgentPresence(c *gc.C, p presence.Presencer) *presence.Pinger { - pinger, err := p.SetAgentPresence() - c.Assert(err, jc.ErrorIsNil) - ctx.st.StartSync() - err = p.WaitAgentPresence(coretesting.LongWait) - c.Assert(err, jc.ErrorIsNil) - agentPresence, err := p.AgentPresence() - c.Assert(err, jc.ErrorIsNil) - c.Assert(agentPresence, jc.IsTrue) - return pinger -} - -func (s *StatusSuite) newContext(c *gc.C) *context { - st := s.Environ.(testing.GetStater).GetStateInAPIServer() - - // We need to have a new version available to test it outputs - // correctly. - env, err := st.Environment() - c.Check(err, jc.ErrorIsNil) - ver := version.Current.Number - ver.Patch++ - err = env.UpdateLatestToolsVersion(ver) - c.Check(err, jc.ErrorIsNil) - - // We make changes in the API server's state so that - // our changes to presence are immediately noticed - // in the status. - return newContext(c, st, s.Environ, s.AdminUserTag(c).String()) -} - -func (s *StatusSuite) resetContext(c *gc.C, ctx *context) { - ctx.reset(c) - s.JujuConnSuite.Reset(c) -} - -// shortcuts for expected output. -var ( - machine0 = M{ - "agent-state": "started", - "dns-name": "dummyenv-0.dns", - "instance-id": "dummyenv-0", - "series": "quantal", - "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", - "state-server-member-status": "adding-vote", - } - machine1 = M{ - "agent-state": "started", - "dns-name": "dummyenv-1.dns", - "instance-id": "dummyenv-1", - "series": "quantal", - "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", - } - machine2 = M{ - "agent-state": "started", - "dns-name": "dummyenv-2.dns", - "instance-id": "dummyenv-2", - "series": "quantal", - "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", - } - machine3 = M{ - "agent-state": "started", - "dns-name": "dummyenv-3.dns", - "instance-id": "dummyenv-3", - "series": "quantal", - "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", - } - machine4 = M{ - "agent-state": "started", - "dns-name": "dummyenv-4.dns", - "instance-id": "dummyenv-4", - "series": "quantal", - "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", - } - machine1WithContainers = M{ - "agent-state": "started", - "containers": M{ - "1/lxc/0": M{ - "agent-state": "started", - "containers": M{ - "1/lxc/0/lxc/0": M{ - "agent-state": "started", - "dns-name": "dummyenv-3.dns", - "instance-id": "dummyenv-3", - "series": "quantal", - }, - }, - "dns-name": "dummyenv-2.dns", - "instance-id": "dummyenv-2", - "series": "quantal", - }, - "1/lxc/1": M{ - "instance-id": "pending", - "series": "quantal", - }, - }, - "dns-name": "dummyenv-1.dns", - "instance-id": "dummyenv-1", - "series": "quantal", - "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", - } - machine1WithContainersScoped = M{ - "agent-state": "started", - "containers": M{ - "1/lxc/0": M{ - "agent-state": "started", - "dns-name": "dummyenv-2.dns", - "instance-id": "dummyenv-2", - "series": "quantal", - }, - }, - "dns-name": "dummyenv-1.dns", - "instance-id": "dummyenv-1", - "series": "quantal", - "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", - } - unexposedService = M{ - "service-status": M{ - "current": "unknown", - "message": "Waiting for agent initialization to finish", - "since": "01 Apr 15 01:23+10:00", - }, - "charm": "cs:quantal/dummy-1", - "exposed": false, - } - exposedService = M{ - "service-status": M{ - "current": "unknown", - "message": "Waiting for agent initialization to finish", - "since": "01 Apr 15 01:23+10:00", - }, - "charm": "cs:quantal/dummy-1", - "exposed": true, - } -) - -type outputFormat struct { - name string - marshal func(v interface{}) ([]byte, error) - unmarshal func(data []byte, v interface{}) error -} - -// statusFormats list all output formats that can be marshalled as structured data, -// supported by status command. -var statusFormats = []outputFormat{ - {"yaml", goyaml.Marshal, goyaml.Unmarshal}, - {"json", json.Marshal, json.Unmarshal}, -} - -var machineCons = constraints.MustParse("cpu-cores=2 mem=8G root-disk=8G") - -var statusTests = []testCase{ - // Status tests - test( - "bootstrap and starting a single instance", - - addMachine{machineId: "0", job: state.JobManageEnviron}, - expect{ - "simulate juju bootstrap by adding machine/0 to the state", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": M{ - "instance-id": "pending", - "series": "quantal", - "state-server-member-status": "adding-vote", - }, - }, - "services": M{}, - }, - }, - - startAliveMachine{"0"}, - setAddresses{"0", []network.Address{ - network.NewAddress("10.0.0.1"), - network.NewScopedAddress("dummyenv-0.dns", network.ScopePublic), - }}, - expect{ - "simulate the PA starting an instance in response to the state change", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": M{ - "agent-state": "pending", - "dns-name": "dummyenv-0.dns", - "instance-id": "dummyenv-0", - "series": "quantal", - "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", - "state-server-member-status": "adding-vote", - }, - }, - "services": M{}, - }, - }, - - setMachineStatus{"0", state.StatusStarted, ""}, - expect{ - "simulate the MA started and set the machine status", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": machine0, - }, - "services": M{}, - }, - }, - - setTools{"0", version.MustParseBinary("1.2.3-trusty-ppc")}, - expect{ - "simulate the MA setting the version", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": M{ - "dns-name": "dummyenv-0.dns", - "instance-id": "dummyenv-0", - "agent-version": "1.2.3", - "agent-state": "started", - "series": "quantal", - "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", - "state-server-member-status": "adding-vote", - }, - }, - "services": M{}, - }, - }, - ), test( - "deploy two services and two networks", - addMachine{machineId: "0", job: state.JobManageEnviron}, - startAliveMachine{"0"}, - setMachineStatus{"0", state.StatusStarted, ""}, - setAddresses{"0", []network.Address{ - network.NewAddress("10.0.0.1"), - network.NewScopedAddress("dummyenv-0.dns", network.ScopePublic), - }}, - addCharm{"dummy"}, - addService{ - name: "networks-service", - charm: "dummy", - networks: []string{"net1", "net2"}, - cons: constraints.MustParse("networks=foo,bar,^no,^good"), - }, - addService{ - name: "no-networks-service", - charm: "dummy", - cons: constraints.MustParse("networks=^mynet"), - }, - addNetwork{ - name: "net1", - providerId: network.Id("provider-net1"), - cidr: "0.1.2.0/24", - vlanTag: 0, - }, - addNetwork{ - name: "net2", - providerId: network.Id("provider-vlan42"), - cidr: "0.42.1.0/24", - vlanTag: 42, - }, - - expect{ - "simulate just the two services and a bootstrap node", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": machine0, - }, - "services": M{ - "networks-service": M{ - "service-status": M{ - "current": "unknown", - "message": "Waiting for agent initialization to finish", - "since": "01 Apr 15 01:23+10:00", - }, - "charm": "cs:quantal/dummy-1", - "exposed": false, - "networks": M{ - "enabled": L{"net1", "net2"}, - "disabled": L{"foo", "bar", "no", "good"}, - }, - }, - "no-networks-service": M{ - "service-status": M{ - "current": "unknown", - "message": "Waiting for agent initialization to finish", - "since": "01 Apr 15 01:23+10:00", - }, - "charm": "cs:quantal/dummy-1", - "exposed": false, - "networks": M{ - "disabled": L{"mynet"}, - }, - }, - }, - "networks": M{ - "net1": M{ - "provider-id": "provider-net1", - "cidr": "0.1.2.0/24", - }, - "net2": M{ - "provider-id": "provider-vlan42", - "cidr": "0.42.1.0/24", - "vlan-tag": 42, - }, - }, - }, - }, - ), test( - "instance with different hardware characteristics", - addMachine{machineId: "0", cons: machineCons, job: state.JobManageEnviron}, - setAddresses{"0", []network.Address{ - network.NewAddress("10.0.0.1"), - network.NewScopedAddress("dummyenv-0.dns", network.ScopePublic), - }}, - startAliveMachine{"0"}, - setMachineStatus{"0", state.StatusStarted, ""}, - expect{ - "machine 0 has specific hardware characteristics", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": M{ - "agent-state": "started", - "dns-name": "dummyenv-0.dns", - "instance-id": "dummyenv-0", - "series": "quantal", - "hardware": "arch=amd64 cpu-cores=2 mem=8192M root-disk=8192M", - "state-server-member-status": "adding-vote", - }, - }, - "services": M{}, - }, - }, - ), test( - "instance without addresses", - addMachine{machineId: "0", cons: machineCons, job: state.JobManageEnviron}, - startAliveMachine{"0"}, - setMachineStatus{"0", state.StatusStarted, ""}, - expect{ - "machine 0 has no dns-name", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": M{ - "agent-state": "started", - "instance-id": "dummyenv-0", - "series": "quantal", - "hardware": "arch=amd64 cpu-cores=2 mem=8192M root-disk=8192M", - "state-server-member-status": "adding-vote", - }, - }, - "services": M{}, - }, - }, - ), test( - "test pending and missing machines", - addMachine{machineId: "0", job: state.JobManageEnviron}, - expect{ - "machine 0 reports pending", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": M{ - "instance-id": "pending", - "series": "quantal", - "state-server-member-status": "adding-vote", - }, - }, - "services": M{}, - }, - }, - - startMissingMachine{"0"}, - expect{ - "machine 0 reports missing", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": M{ - "instance-state": "missing", - "instance-id": "i-missing", - "agent-state": "pending", - "series": "quantal", - "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", - "state-server-member-status": "adding-vote", - }, - }, - "services": M{}, - }, - }, - ), test( - "add two services and expose one, then add 2 more machines and some units", - addMachine{machineId: "0", job: state.JobManageEnviron}, - setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, - startAliveMachine{"0"}, - setMachineStatus{"0", state.StatusStarted, ""}, - addCharm{"dummy"}, - addService{name: "dummy-service", charm: "dummy"}, - addService{name: "exposed-service", charm: "dummy"}, - expect{ - "no services exposed yet", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": machine0, - }, - "services": M{ - "dummy-service": unexposedService, - "exposed-service": unexposedService, - }, - }, - }, - - setServiceExposed{"exposed-service", true}, - expect{ - "one exposed service", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": machine0, - }, - "services": M{ - "dummy-service": unexposedService, - "exposed-service": exposedService, - }, - }, - }, - - addMachine{machineId: "1", job: state.JobHostUnits}, - setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, - startAliveMachine{"1"}, - setMachineStatus{"1", state.StatusStarted, ""}, - addMachine{machineId: "2", job: state.JobHostUnits}, - setAddresses{"2", network.NewAddresses("dummyenv-2.dns")}, - startAliveMachine{"2"}, - setMachineStatus{"2", state.StatusStarted, ""}, - expect{ - "two more machines added", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": machine0, - "1": machine1, - "2": machine2, - }, - "services": M{ - "dummy-service": unexposedService, - "exposed-service": exposedService, - }, - }, - }, - - addAliveUnit{"dummy-service", "1"}, - addAliveUnit{"exposed-service", "2"}, - setAgentStatus{"exposed-service/0", state.StatusError, "You Require More Vespene Gas", nil}, - // Open multiple ports with different protocols, - // ensure they're sorted on protocol, then number. - openUnitPort{"exposed-service/0", "udp", 10}, - openUnitPort{"exposed-service/0", "udp", 2}, - openUnitPort{"exposed-service/0", "tcp", 3}, - openUnitPort{"exposed-service/0", "tcp", 2}, - // Simulate some status with no info, while the agent is down. - // Status used to be down, we no longer support said state. - // now is one of: pending, started, error. - setUnitStatus{"dummy-service/0", state.StatusTerminated, "", nil}, - setAgentStatus{"dummy-service/0", state.StatusIdle, "", nil}, - - // dummy-service/0 used to expect "agent-state-info": "(started)", - // which is populated as the previous state by adjustInfoIfAgentDown - // but sice it no longer is down it no longer applies. - expect{ - "add two units, one alive (in error state), one started", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": machine0, - "1": machine1, - "2": machine2, - }, - "services": M{ - "exposed-service": M{ - "charm": "cs:quantal/dummy-1", - "exposed": true, - "service-status": M{ - "current": "error", - "message": "You Require More Vespene Gas", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "exposed-service/0": M{ - "machine": "2", - "agent-state": "error", - "agent-state-info": "You Require More Vespene Gas", - "workload-status": M{ - "current": "error", - "message": "You Require More Vespene Gas", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "open-ports": L{ - "2/tcp", "3/tcp", "2/udp", "10/udp", - }, - "public-address": "dummyenv-2.dns", - }, - }, - }, - "dummy-service": M{ - "charm": "cs:quantal/dummy-1", - "exposed": false, - "service-status": M{ - "current": "terminated", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "dummy-service/0": M{ - "machine": "1", - "agent-state": "stopped", - "workload-status": M{ - "current": "terminated", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "public-address": "dummyenv-1.dns", - }, - }, - }, - }, - }, - }, - - addMachine{machineId: "3", job: state.JobHostUnits}, - startMachine{"3"}, - // Simulate some status with info, while the agent is down. - setAddresses{"3", network.NewAddresses("dummyenv-3.dns")}, - setMachineStatus{"3", state.StatusStopped, "Really?"}, - addMachine{machineId: "4", job: state.JobHostUnits}, - setAddresses{"4", network.NewAddresses("dummyenv-4.dns")}, - startAliveMachine{"4"}, - setMachineStatus{"4", state.StatusError, "Beware the red toys"}, - ensureDyingUnit{"dummy-service/0"}, - addMachine{machineId: "5", job: state.JobHostUnits}, - ensureDeadMachine{"5"}, - expect{ - "add three more machine, one with a dead agent, one in error state and one dead itself; also one dying unit", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": machine0, - "1": machine1, - "2": machine2, - "3": M{ - "dns-name": "dummyenv-3.dns", - "instance-id": "dummyenv-3", - "agent-state": "down", - "agent-state-info": "(stopped: Really?)", - "series": "quantal", - "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", - }, - "4": M{ - "dns-name": "dummyenv-4.dns", - "instance-id": "dummyenv-4", - "agent-state": "error", - "agent-state-info": "Beware the red toys", - "series": "quantal", - "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", - }, - "5": M{ - "life": "dead", - "instance-id": "pending", - "series": "quantal", - }, - }, - "services": M{ - "exposed-service": M{ - "charm": "cs:quantal/dummy-1", - "exposed": true, - "service-status": M{ - "current": "error", - "message": "You Require More Vespene Gas", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "exposed-service/0": M{ - "machine": "2", - "agent-state": "error", - "agent-state-info": "You Require More Vespene Gas", - "workload-status": M{ - "current": "error", - "message": "You Require More Vespene Gas", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "open-ports": L{ - "2/tcp", "3/tcp", "2/udp", "10/udp", - }, - "public-address": "dummyenv-2.dns", - }, - }, - }, - "dummy-service": M{ - "charm": "cs:quantal/dummy-1", - "exposed": false, - "service-status": M{ - "current": "terminated", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "dummy-service/0": M{ - "machine": "1", - "agent-state": "stopped", - "life": "dying", - "workload-status": M{ - "current": "terminated", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "public-address": "dummyenv-1.dns", - }, - }, - }, - }, - }, - }, - - scopedExpect{ - "scope status on dummy-service/0 unit", - []string{"dummy-service/0"}, - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "1": machine1, - }, - "services": M{ - "dummy-service": M{ - "charm": "cs:quantal/dummy-1", - "exposed": false, - "service-status": M{ - "current": "terminated", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "dummy-service/0": M{ - "machine": "1", - "life": "dying", - "agent-state": "stopped", - "workload-status": M{ - "current": "terminated", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "public-address": "dummyenv-1.dns", - }, - }, - }, - }, - }, - }, - scopedExpect{ - "scope status on exposed-service service", - []string{"exposed-service"}, - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "2": machine2, - }, - "services": M{ - "exposed-service": M{ - "charm": "cs:quantal/dummy-1", - "exposed": true, - "service-status": M{ - "current": "error", - "message": "You Require More Vespene Gas", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "exposed-service/0": M{ - "machine": "2", - "agent-state": "error", - "agent-state-info": "You Require More Vespene Gas", - "workload-status": M{ - "current": "error", - "message": "You Require More Vespene Gas", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "open-ports": L{ - "2/tcp", "3/tcp", "2/udp", "10/udp", - }, - "public-address": "dummyenv-2.dns", - }, - }, - }, - }, - }, - }, - scopedExpect{ - "scope status on service pattern", - []string{"d*-service"}, - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "1": machine1, - }, - "services": M{ - "dummy-service": M{ - "charm": "cs:quantal/dummy-1", - "exposed": false, - "service-status": M{ - "current": "terminated", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "dummy-service/0": M{ - "machine": "1", - "life": "dying", - "agent-state": "stopped", - "workload-status": M{ - "current": "terminated", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "public-address": "dummyenv-1.dns", - }, - }, - }, - }, - }, - }, - scopedExpect{ - "scope status on unit pattern", - []string{"e*posed-service/*"}, - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "2": machine2, - }, - "services": M{ - "exposed-service": M{ - "charm": "cs:quantal/dummy-1", - "exposed": true, - "service-status": M{ - "current": "error", - "message": "You Require More Vespene Gas", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "exposed-service/0": M{ - "machine": "2", - "agent-state": "error", - "agent-state-info": "You Require More Vespene Gas", - "workload-status": M{ - "current": "error", - "message": "You Require More Vespene Gas", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "open-ports": L{ - "2/tcp", "3/tcp", "2/udp", "10/udp", - }, - "public-address": "dummyenv-2.dns", - }, - }, - }, - }, - }, - }, - scopedExpect{ - "scope status on combination of service and unit patterns", - []string{"exposed-service", "dummy-service", "e*posed-service/*", "dummy-service/*"}, - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "1": machine1, - "2": machine2, - }, - "services": M{ - "dummy-service": M{ - "charm": "cs:quantal/dummy-1", - "exposed": false, - "service-status": M{ - "current": "terminated", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "dummy-service/0": M{ - "machine": "1", - "life": "dying", - "agent-state": "stopped", - "workload-status": M{ - "current": "terminated", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "public-address": "dummyenv-1.dns", - }, - }, - }, - "exposed-service": M{ - "charm": "cs:quantal/dummy-1", - "exposed": true, - "service-status": M{ - "current": "error", - "message": "You Require More Vespene Gas", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "exposed-service/0": M{ - "machine": "2", - "agent-state": "error", - "agent-state-info": "You Require More Vespene Gas", - "workload-status": M{ - "current": "error", - "message": "You Require More Vespene Gas", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "open-ports": L{ - "2/tcp", "3/tcp", "2/udp", "10/udp", - }, - "public-address": "dummyenv-2.dns", - }, - }, - }, - }, - }, - }, - ), test( - "a unit with a hook relation error", - addMachine{machineId: "0", job: state.JobManageEnviron}, - setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, - startAliveMachine{"0"}, - setMachineStatus{"0", state.StatusStarted, ""}, - - addMachine{machineId: "1", job: state.JobHostUnits}, - setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, - startAliveMachine{"1"}, - setMachineStatus{"1", state.StatusStarted, ""}, - - addCharm{"wordpress"}, - addService{name: "wordpress", charm: "wordpress"}, - addAliveUnit{"wordpress", "1"}, - - addCharm{"mysql"}, - addService{name: "mysql", charm: "mysql"}, - addAliveUnit{"mysql", "1"}, - - relateServices{"wordpress", "mysql"}, - - setAgentStatus{"wordpress/0", state.StatusError, - "hook failed: some-relation-changed", - map[string]interface{}{"relation-id": 0}}, - - expect{ - "a unit with a hook relation error", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": machine0, - "1": machine1, - }, - "services": M{ - "wordpress": M{ - "charm": "cs:quantal/wordpress-3", - "exposed": false, - "relations": M{ - "db": L{"mysql"}, - }, - "service-status": M{ - "current": "error", - "message": "hook failed: some-relation-changed", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "wordpress/0": M{ - "machine": "1", - "agent-state": "error", - "agent-state-info": "hook failed: some-relation-changed for mysql:server", - "workload-status": M{ - "current": "error", - "message": "hook failed: some-relation-changed for mysql:server", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "public-address": "dummyenv-1.dns", - }, - }, - }, - "mysql": M{ - "charm": "cs:quantal/mysql-1", - "exposed": false, - "relations": M{ - "server": L{"wordpress"}, - }, - "service-status": M{ - "current": "unknown", - "message": "Waiting for agent initialization to finish", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "mysql/0": M{ - "machine": "1", - "agent-state": "pending", - "workload-status": M{ - "current": "unknown", - "message": "Waiting for agent initialization to finish", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "allocating", - "since": "01 Apr 15 01:23+10:00", - }, - "public-address": "dummyenv-1.dns", - }, - }, - }, - }, - }, - }, - ), test( - "a unit with a hook relation error when the agent is down", - addMachine{machineId: "0", job: state.JobManageEnviron}, - setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, - startAliveMachine{"0"}, - setMachineStatus{"0", state.StatusStarted, ""}, - - addMachine{machineId: "1", job: state.JobHostUnits}, - setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, - startAliveMachine{"1"}, - setMachineStatus{"1", state.StatusStarted, ""}, - - addCharm{"wordpress"}, - addService{name: "wordpress", charm: "wordpress"}, - addAliveUnit{"wordpress", "1"}, - - addCharm{"mysql"}, - addService{name: "mysql", charm: "mysql"}, - addAliveUnit{"mysql", "1"}, - - relateServices{"wordpress", "mysql"}, - - setAgentStatus{"wordpress/0", state.StatusError, - "hook failed: some-relation-changed", - map[string]interface{}{"relation-id": 0}}, - - expect{ - "a unit with a hook relation error when the agent is down", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": machine0, - "1": machine1, - }, - "services": M{ - "wordpress": M{ - "charm": "cs:quantal/wordpress-3", - "exposed": false, - "relations": M{ - "db": L{"mysql"}, - }, - "service-status": M{ - "current": "error", - "message": "hook failed: some-relation-changed", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "wordpress/0": M{ - "machine": "1", - "agent-state": "error", - "agent-state-info": "hook failed: some-relation-changed for mysql:server", - "workload-status": M{ - "current": "error", - "message": "hook failed: some-relation-changed for mysql:server", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "public-address": "dummyenv-1.dns", - }, - }, - }, - "mysql": M{ - "charm": "cs:quantal/mysql-1", - "exposed": false, - "relations": M{ - "server": L{"wordpress"}, - }, - "service-status": M{ - "current": "unknown", - "message": "Waiting for agent initialization to finish", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "mysql/0": M{ - "machine": "1", - "agent-state": "pending", - "workload-status": M{ - "current": "unknown", - "message": "Waiting for agent initialization to finish", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "allocating", - "since": "01 Apr 15 01:23+10:00", - }, - "public-address": "dummyenv-1.dns", - }, - }, - }, - }, - }, - }, - ), test( - "add a dying service", - addCharm{"dummy"}, - addService{name: "dummy-service", charm: "dummy"}, - addMachine{machineId: "0", job: state.JobHostUnits}, - addAliveUnit{"dummy-service", "0"}, - ensureDyingService{"dummy-service"}, - expect{ - "service shows life==dying", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": M{ - "instance-id": "pending", - "series": "quantal", - }, - }, - "services": M{ - "dummy-service": M{ - "charm": "cs:quantal/dummy-1", - "exposed": false, - "life": "dying", - "service-status": M{ - "current": "unknown", - "message": "Waiting for agent initialization to finish", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "dummy-service/0": M{ - "machine": "0", - "agent-state": "pending", - "workload-status": M{ - "current": "unknown", - "message": "Waiting for agent initialization to finish", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "allocating", - "since": "01 Apr 15 01:23+10:00", - }, - }, - }, - }, - }, - }, - }, - ), test( - "a unit where the agent is down shows as lost", - addCharm{"dummy"}, - addService{name: "dummy-service", charm: "dummy"}, - addMachine{machineId: "0", job: state.JobHostUnits}, - startAliveMachine{"0"}, - setMachineStatus{"0", state.StatusStarted, ""}, - addUnit{"dummy-service", "0"}, - setAgentStatus{"dummy-service/0", state.StatusIdle, "", nil}, - setUnitStatus{"dummy-service/0", state.StatusActive, "", nil}, - expect{ - "unit shows that agent is lost", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": M{ - "agent-state": "started", - "instance-id": "dummyenv-0", - "series": "quantal", - "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", - }, - }, - "services": M{ - "dummy-service": M{ - "charm": "cs:quantal/dummy-1", - "exposed": false, - "service-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "dummy-service/0": M{ - "machine": "0", - "agent-state": "started", - "workload-status": M{ - "current": "unknown", - "message": "agent is lost, sorry! See 'juju status-history dummy-service/0'", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "lost", - "message": "agent is not communicating with the server", - "since": "01 Apr 15 01:23+10:00", - }, - }, - }, - }, - }, - }, - }, - ), - - // Relation tests - test( - "complex scenario with multiple related services", - addMachine{machineId: "0", job: state.JobManageEnviron}, - setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, - startAliveMachine{"0"}, - setMachineStatus{"0", state.StatusStarted, ""}, - addCharm{"wordpress"}, - addCharm{"mysql"}, - addCharm{"varnish"}, - - addService{name: "project", charm: "wordpress"}, - setServiceExposed{"project", true}, - addMachine{machineId: "1", job: state.JobHostUnits}, - setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, - startAliveMachine{"1"}, - setMachineStatus{"1", state.StatusStarted, ""}, - addAliveUnit{"project", "1"}, - setAgentStatus{"project/0", state.StatusIdle, "", nil}, - setUnitStatus{"project/0", state.StatusActive, "", nil}, - - addService{name: "mysql", charm: "mysql"}, - setServiceExposed{"mysql", true}, - addMachine{machineId: "2", job: state.JobHostUnits}, - setAddresses{"2", network.NewAddresses("dummyenv-2.dns")}, - startAliveMachine{"2"}, - setMachineStatus{"2", state.StatusStarted, ""}, - addAliveUnit{"mysql", "2"}, - setAgentStatus{"mysql/0", state.StatusIdle, "", nil}, - setUnitStatus{"mysql/0", state.StatusActive, "", nil}, - - addService{name: "varnish", charm: "varnish"}, - setServiceExposed{"varnish", true}, - addMachine{machineId: "3", job: state.JobHostUnits}, - setAddresses{"3", network.NewAddresses("dummyenv-3.dns")}, - startAliveMachine{"3"}, - setMachineStatus{"3", state.StatusStarted, ""}, - addAliveUnit{"varnish", "3"}, - - addService{name: "private", charm: "wordpress"}, - setServiceExposed{"private", true}, - addMachine{machineId: "4", job: state.JobHostUnits}, - setAddresses{"4", network.NewAddresses("dummyenv-4.dns")}, - startAliveMachine{"4"}, - setMachineStatus{"4", state.StatusStarted, ""}, - addAliveUnit{"private", "4"}, - - relateServices{"project", "mysql"}, - relateServices{"project", "varnish"}, - relateServices{"private", "mysql"}, - - expect{ - "multiples services with relations between some of them", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": machine0, - "1": machine1, - "2": machine2, - "3": machine3, - "4": machine4, - }, - "services": M{ - "project": M{ - "charm": "cs:quantal/wordpress-3", - "exposed": true, - "service-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "project/0": M{ - "machine": "1", - "agent-state": "started", - "workload-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "public-address": "dummyenv-1.dns", - }, - }, - "relations": M{ - "db": L{"mysql"}, - "cache": L{"varnish"}, - }, - }, - "mysql": M{ - "charm": "cs:quantal/mysql-1", - "exposed": true, - "service-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "mysql/0": M{ - "machine": "2", - "agent-state": "started", - "workload-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "public-address": "dummyenv-2.dns", - }, - }, - "relations": M{ - "server": L{"private", "project"}, - }, - }, - "varnish": M{ - "charm": "cs:quantal/varnish-1", - "exposed": true, - "service-status": M{ - "current": "unknown", - "message": "Waiting for agent initialization to finish", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "varnish/0": M{ - "machine": "3", - "agent-state": "pending", - "workload-status": M{ - "current": "unknown", - "message": "Waiting for agent initialization to finish", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "allocating", - "since": "01 Apr 15 01:23+10:00", - }, - "public-address": "dummyenv-3.dns", - }, - }, - "relations": M{ - "webcache": L{"project"}, - }, - }, - "private": M{ - "charm": "cs:quantal/wordpress-3", - "exposed": true, - "service-status": M{ - "current": "unknown", - "message": "Waiting for agent initialization to finish", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "private/0": M{ - "machine": "4", - "agent-state": "pending", - "workload-status": M{ - "current": "unknown", - "message": "Waiting for agent initialization to finish", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "allocating", - "since": "01 Apr 15 01:23+10:00", - }, - "public-address": "dummyenv-4.dns", - }, - }, - "relations": M{ - "db": L{"mysql"}, - }, - }, - }, - }, - }, - ), test( - "simple peer scenario", - addMachine{machineId: "0", job: state.JobManageEnviron}, - setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, - startAliveMachine{"0"}, - setMachineStatus{"0", state.StatusStarted, ""}, - addCharm{"riak"}, - addCharm{"wordpress"}, - - addService{name: "riak", charm: "riak"}, - setServiceExposed{"riak", true}, - addMachine{machineId: "1", job: state.JobHostUnits}, - setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, - startAliveMachine{"1"}, - setMachineStatus{"1", state.StatusStarted, ""}, - addAliveUnit{"riak", "1"}, - setAgentStatus{"riak/0", state.StatusIdle, "", nil}, - setUnitStatus{"riak/0", state.StatusActive, "", nil}, - addMachine{machineId: "2", job: state.JobHostUnits}, - setAddresses{"2", network.NewAddresses("dummyenv-2.dns")}, - startAliveMachine{"2"}, - setMachineStatus{"2", state.StatusStarted, ""}, - addAliveUnit{"riak", "2"}, - setAgentStatus{"riak/1", state.StatusIdle, "", nil}, - setUnitStatus{"riak/1", state.StatusActive, "", nil}, - addMachine{machineId: "3", job: state.JobHostUnits}, - setAddresses{"3", network.NewAddresses("dummyenv-3.dns")}, - startAliveMachine{"3"}, - setMachineStatus{"3", state.StatusStarted, ""}, - addAliveUnit{"riak", "3"}, - setAgentStatus{"riak/2", state.StatusIdle, "", nil}, - setUnitStatus{"riak/2", state.StatusActive, "", nil}, - - expect{ - "multiples related peer units", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": machine0, - "1": machine1, - "2": machine2, - "3": machine3, - }, - "services": M{ - "riak": M{ - "charm": "cs:quantal/riak-7", - "exposed": true, - "service-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "riak/0": M{ - "machine": "1", - "agent-state": "started", - "workload-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "public-address": "dummyenv-1.dns", - }, - "riak/1": M{ - "machine": "2", - "agent-state": "started", - "workload-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "public-address": "dummyenv-2.dns", - }, - "riak/2": M{ - "machine": "3", - "agent-state": "started", - "workload-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "public-address": "dummyenv-3.dns", - }, - }, - "relations": M{ - "ring": L{"riak"}, - }, - }, - }, - }, - }, - ), - - // Subordinate tests - test( - "one service with one subordinate service", - addMachine{machineId: "0", job: state.JobManageEnviron}, - setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, - startAliveMachine{"0"}, - setMachineStatus{"0", state.StatusStarted, ""}, - addCharm{"wordpress"}, - addCharm{"mysql"}, - addCharm{"logging"}, - - addService{name: "wordpress", charm: "wordpress"}, - setServiceExposed{"wordpress", true}, - addMachine{machineId: "1", job: state.JobHostUnits}, - setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, - startAliveMachine{"1"}, - setMachineStatus{"1", state.StatusStarted, ""}, - addAliveUnit{"wordpress", "1"}, - setAgentStatus{"wordpress/0", state.StatusIdle, "", nil}, - setUnitStatus{"wordpress/0", state.StatusActive, "", nil}, - - addService{name: "mysql", charm: "mysql"}, - setServiceExposed{"mysql", true}, - addMachine{machineId: "2", job: state.JobHostUnits}, - setAddresses{"2", network.NewAddresses("dummyenv-2.dns")}, - startAliveMachine{"2"}, - setMachineStatus{"2", state.StatusStarted, ""}, - addAliveUnit{"mysql", "2"}, - setAgentStatus{"mysql/0", state.StatusIdle, "", nil}, - setUnitStatus{"mysql/0", state.StatusActive, "", nil}, - - addService{name: "logging", charm: "logging"}, - setServiceExposed{"logging", true}, - - relateServices{"wordpress", "mysql"}, - relateServices{"wordpress", "logging"}, - relateServices{"mysql", "logging"}, - - addSubordinate{"wordpress/0", "logging"}, - addSubordinate{"mysql/0", "logging"}, - - setUnitsAlive{"logging"}, - setAgentStatus{"logging/0", state.StatusIdle, "", nil}, - setUnitStatus{"logging/0", state.StatusActive, "", nil}, - setAgentStatus{"logging/1", state.StatusError, "somehow lost in all those logs", nil}, - - expect{ - "multiples related peer units", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": machine0, - "1": machine1, - "2": machine2, - }, - "services": M{ - "wordpress": M{ - "charm": "cs:quantal/wordpress-3", - "exposed": true, - "service-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "wordpress/0": M{ - "machine": "1", - "agent-state": "started", - "workload-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "subordinates": M{ - "logging/0": M{ - "agent-state": "started", - "workload-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "public-address": "dummyenv-1.dns", - }, - }, - "public-address": "dummyenv-1.dns", - }, - }, - "relations": M{ - "db": L{"mysql"}, - "logging-dir": L{"logging"}, - }, - }, - "mysql": M{ - "charm": "cs:quantal/mysql-1", - "exposed": true, - "service-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "mysql/0": M{ - "machine": "2", - "agent-state": "started", - "workload-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "subordinates": M{ - "logging/1": M{ - "agent-state": "error", - "agent-state-info": "somehow lost in all those logs", - "workload-status": M{ - "current": "error", - "message": "somehow lost in all those logs", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "public-address": "dummyenv-2.dns", - }, - }, - "public-address": "dummyenv-2.dns", - }, - }, - "relations": M{ - "server": L{"wordpress"}, - "juju-info": L{"logging"}, - }, - }, - "logging": M{ - "charm": "cs:quantal/logging-1", - "exposed": true, - "service-status": M{}, - "relations": M{ - "logging-directory": L{"wordpress"}, - "info": L{"mysql"}, - }, - "subordinate-to": L{"mysql", "wordpress"}, - }, - }, - }, - }, - - // scoped on 'logging' - scopedExpect{ - "subordinates scoped on logging", - []string{"logging"}, - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "1": machine1, - "2": machine2, - }, - "services": M{ - "wordpress": M{ - "charm": "cs:quantal/wordpress-3", - "exposed": true, - "service-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "wordpress/0": M{ - "machine": "1", - "agent-state": "started", - "workload-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "subordinates": M{ - "logging/0": M{ - "agent-state": "started", - "workload-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "public-address": "dummyenv-1.dns", - }, - }, - "public-address": "dummyenv-1.dns", - }, - }, - "relations": M{ - "db": L{"mysql"}, - "logging-dir": L{"logging"}, - }, - }, - "mysql": M{ - "charm": "cs:quantal/mysql-1", - "exposed": true, - "service-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "mysql/0": M{ - "machine": "2", - "agent-state": "started", - "workload-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "subordinates": M{ - "logging/1": M{ - "agent-state": "error", - "workload-status": M{ - "current": "error", - "message": "somehow lost in all those logs", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-state-info": "somehow lost in all those logs", - "public-address": "dummyenv-2.dns", - }, - }, - "public-address": "dummyenv-2.dns", - }, - }, - "relations": M{ - "server": L{"wordpress"}, - "juju-info": L{"logging"}, - }, - }, - "logging": M{ - "charm": "cs:quantal/logging-1", - "exposed": true, - "service-status": M{}, - "relations": M{ - "logging-directory": L{"wordpress"}, - "info": L{"mysql"}, - }, - "subordinate-to": L{"mysql", "wordpress"}, - }, - }, - }, - }, - - // scoped on wordpress/0 - scopedExpect{ - "subordinates scoped on logging", - []string{"wordpress/0"}, - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "1": machine1, - }, - "services": M{ - "wordpress": M{ - "charm": "cs:quantal/wordpress-3", - "exposed": true, - "service-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "wordpress/0": M{ - "machine": "1", - "agent-state": "started", - "workload-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "subordinates": M{ - "logging/0": M{ - "agent-state": "started", - "workload-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "public-address": "dummyenv-1.dns", - }, - }, - "public-address": "dummyenv-1.dns", - }, - }, - "relations": M{ - "db": L{"mysql"}, - "logging-dir": L{"logging"}, - }, - }, - "logging": M{ - "charm": "cs:quantal/logging-1", - "exposed": true, - "service-status": M{}, - "relations": M{ - "logging-directory": L{"wordpress"}, - "info": L{"mysql"}, - }, - "subordinate-to": L{"mysql", "wordpress"}, - }, - }, - }, - }, - ), - test( - "machines with containers", - addMachine{machineId: "0", job: state.JobManageEnviron}, - setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, - startAliveMachine{"0"}, - setMachineStatus{"0", state.StatusStarted, ""}, - addCharm{"mysql"}, - addService{name: "mysql", charm: "mysql"}, - setServiceExposed{"mysql", true}, - - addMachine{machineId: "1", job: state.JobHostUnits}, - setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, - startAliveMachine{"1"}, - setMachineStatus{"1", state.StatusStarted, ""}, - addAliveUnit{"mysql", "1"}, - setAgentStatus{"mysql/0", state.StatusIdle, "", nil}, - setUnitStatus{"mysql/0", state.StatusActive, "", nil}, - - // A container on machine 1. - addContainer{"1", "1/lxc/0", state.JobHostUnits}, - setAddresses{"1/lxc/0", network.NewAddresses("dummyenv-2.dns")}, - startAliveMachine{"1/lxc/0"}, - setMachineStatus{"1/lxc/0", state.StatusStarted, ""}, - addAliveUnit{"mysql", "1/lxc/0"}, - setAgentStatus{"mysql/1", state.StatusIdle, "", nil}, - setUnitStatus{"mysql/1", state.StatusActive, "", nil}, - addContainer{"1", "1/lxc/1", state.JobHostUnits}, - - // A nested container. - addContainer{"1/lxc/0", "1/lxc/0/lxc/0", state.JobHostUnits}, - setAddresses{"1/lxc/0/lxc/0", network.NewAddresses("dummyenv-3.dns")}, - startAliveMachine{"1/lxc/0/lxc/0"}, - setMachineStatus{"1/lxc/0/lxc/0", state.StatusStarted, ""}, - - expect{ - "machines with nested containers", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": machine0, - "1": machine1WithContainers, - }, - "services": M{ - "mysql": M{ - "charm": "cs:quantal/mysql-1", - "exposed": true, - "service-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "mysql/0": M{ - "machine": "1", - "agent-state": "started", - "workload-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "public-address": "dummyenv-1.dns", - }, - "mysql/1": M{ - "machine": "1/lxc/0", - "agent-state": "started", - "workload-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "public-address": "dummyenv-2.dns", - }, - }, - }, - }, - }, - }, - - // once again, with a scope on mysql/1 - scopedExpect{ - "machines with nested containers", - []string{"mysql/1"}, - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "1": M{ - "agent-state": "started", - "containers": M{ - "1/lxc/0": M{ - "agent-state": "started", - "dns-name": "dummyenv-2.dns", - "instance-id": "dummyenv-2", - "series": "quantal", - }, - }, - "dns-name": "dummyenv-1.dns", - "instance-id": "dummyenv-1", - "series": "quantal", - "hardware": "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M", - }, - }, - "services": M{ - "mysql": M{ - "charm": "cs:quantal/mysql-1", - "exposed": true, - "service-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "mysql/1": M{ - "machine": "1/lxc/0", - "agent-state": "started", - "workload-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "public-address": "dummyenv-2.dns", - }, - }, - }, - }, - }, - }, - ), test( - "service with out of date charm", - addMachine{machineId: "0", job: state.JobManageEnviron}, - setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, - startAliveMachine{"0"}, - setMachineStatus{"0", state.StatusStarted, ""}, - addMachine{machineId: "1", job: state.JobHostUnits}, - setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, - startAliveMachine{"1"}, - setMachineStatus{"1", state.StatusStarted, ""}, - addCharm{"mysql"}, - addService{name: "mysql", charm: "mysql"}, - setServiceExposed{"mysql", true}, - addCharmPlaceholder{"mysql", 23}, - addAliveUnit{"mysql", "1"}, - - expect{ - "services and units with correct charm status", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": machine0, - "1": machine1, - }, - "services": M{ - "mysql": M{ - "charm": "cs:quantal/mysql-1", - "can-upgrade-to": "cs:quantal/mysql-23", - "exposed": true, - "service-status": M{ - "current": "unknown", - "message": "Waiting for agent initialization to finish", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "mysql/0": M{ - "machine": "1", - "agent-state": "pending", - "workload-status": M{ - "current": "unknown", - "message": "Waiting for agent initialization to finish", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "allocating", - "since": "01 Apr 15 01:23+10:00", - }, - "public-address": "dummyenv-1.dns", - }, - }, - }, - }, - }, - }, - ), test( - "unit with out of date charm", - addMachine{machineId: "0", job: state.JobManageEnviron}, - setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, - startAliveMachine{"0"}, - setMachineStatus{"0", state.StatusStarted, ""}, - addMachine{machineId: "1", job: state.JobHostUnits}, - setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, - startAliveMachine{"1"}, - setMachineStatus{"1", state.StatusStarted, ""}, - addCharm{"mysql"}, - addService{name: "mysql", charm: "mysql"}, - setServiceExposed{"mysql", true}, - addAliveUnit{"mysql", "1"}, - setUnitCharmURL{"mysql/0", "cs:quantal/mysql-1"}, - addCharmWithRevision{addCharm{"mysql"}, "local", 1}, - setServiceCharm{"mysql", "local:quantal/mysql-1"}, - - expect{ - "services and units with correct charm status", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": machine0, - "1": machine1, - }, - "services": M{ - "mysql": M{ - "charm": "local:quantal/mysql-1", - "exposed": true, - "service-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "mysql/0": M{ - "machine": "1", - "agent-state": "started", - "workload-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "upgrading-from": "cs:quantal/mysql-1", - "public-address": "dummyenv-1.dns", - }, - }, - }, - }, - }, - }, - ), test( - "service and unit with out of date charms", - addMachine{machineId: "0", job: state.JobManageEnviron}, - setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, - startAliveMachine{"0"}, - setMachineStatus{"0", state.StatusStarted, ""}, - addMachine{machineId: "1", job: state.JobHostUnits}, - setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, - startAliveMachine{"1"}, - setMachineStatus{"1", state.StatusStarted, ""}, - addCharm{"mysql"}, - addService{name: "mysql", charm: "mysql"}, - setServiceExposed{"mysql", true}, - addAliveUnit{"mysql", "1"}, - setUnitCharmURL{"mysql/0", "cs:quantal/mysql-1"}, - addCharmWithRevision{addCharm{"mysql"}, "cs", 2}, - setServiceCharm{"mysql", "cs:quantal/mysql-2"}, - addCharmPlaceholder{"mysql", 23}, - - expect{ - "services and units with correct charm status", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": machine0, - "1": machine1, - }, - "services": M{ - "mysql": M{ - "charm": "cs:quantal/mysql-2", - "can-upgrade-to": "cs:quantal/mysql-23", - "exposed": true, - "service-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "mysql/0": M{ - "machine": "1", - "agent-state": "started", - "workload-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "upgrading-from": "cs:quantal/mysql-1", - "public-address": "dummyenv-1.dns", - }, - }, - }, - }, - }, - }, - ), test( - "service with local charm not shown as out of date", - addMachine{machineId: "0", job: state.JobManageEnviron}, - setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, - startAliveMachine{"0"}, - setMachineStatus{"0", state.StatusStarted, ""}, - addMachine{machineId: "1", job: state.JobHostUnits}, - setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, - startAliveMachine{"1"}, - setMachineStatus{"1", state.StatusStarted, ""}, - addCharm{"mysql"}, - addService{name: "mysql", charm: "mysql"}, - setServiceExposed{"mysql", true}, - addAliveUnit{"mysql", "1"}, - setUnitCharmURL{"mysql/0", "cs:quantal/mysql-1"}, - addCharmWithRevision{addCharm{"mysql"}, "local", 1}, - setServiceCharm{"mysql", "local:quantal/mysql-1"}, - addCharmPlaceholder{"mysql", 23}, - - expect{ - "services and units with correct charm status", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": machine0, - "1": machine1, - }, - "services": M{ - "mysql": M{ - "charm": "local:quantal/mysql-1", - "exposed": true, - "service-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "mysql/0": M{ - "machine": "1", - "agent-state": "started", - "workload-status": M{ - "current": "active", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "idle", - "since": "01 Apr 15 01:23+10:00", - }, - "upgrading-from": "cs:quantal/mysql-1", - "public-address": "dummyenv-1.dns", - }, - }, - }, - }, - }, - }, - ), -} - -// TODO(dfc) test failing components by destructively mutating the state under the hood - -type addMachine struct { - machineId string - cons constraints.Value - job state.MachineJob -} - -func (am addMachine) step(c *gc.C, ctx *context) { - m, err := ctx.st.AddOneMachine(state.MachineTemplate{ - Series: "quantal", - Constraints: am.cons, - Jobs: []state.MachineJob{am.job}, - }) - c.Assert(err, jc.ErrorIsNil) - c.Assert(m.Id(), gc.Equals, am.machineId) -} - -type addNetwork struct { - name string - providerId network.Id - cidr string - vlanTag int -} - -func (an addNetwork) step(c *gc.C, ctx *context) { - n, err := ctx.st.AddNetwork(state.NetworkInfo{ - Name: an.name, - ProviderId: an.providerId, - CIDR: an.cidr, - VLANTag: an.vlanTag, - }) - c.Assert(err, jc.ErrorIsNil) - c.Assert(n.Name(), gc.Equals, an.name) -} - -type addContainer struct { - parentId string - machineId string - job state.MachineJob -} - -func (ac addContainer) step(c *gc.C, ctx *context) { - template := state.MachineTemplate{ - Series: "quantal", - Jobs: []state.MachineJob{ac.job}, - } - m, err := ctx.st.AddMachineInsideMachine(template, ac.parentId, instance.LXC) - c.Assert(err, jc.ErrorIsNil) - c.Assert(m.Id(), gc.Equals, ac.machineId) -} - -type startMachine struct { - machineId string -} - -func (sm startMachine) step(c *gc.C, ctx *context) { - m, err := ctx.st.Machine(sm.machineId) - c.Assert(err, jc.ErrorIsNil) - cons, err := m.Constraints() - c.Assert(err, jc.ErrorIsNil) - inst, hc := testing.AssertStartInstanceWithConstraints(c, ctx.env, m.Id(), cons) - err = m.SetProvisioned(inst.Id(), "fake_nonce", hc) - c.Assert(err, jc.ErrorIsNil) -} - -type startMissingMachine struct { - machineId string -} - -func (sm startMissingMachine) step(c *gc.C, ctx *context) { - m, err := ctx.st.Machine(sm.machineId) - c.Assert(err, jc.ErrorIsNil) - cons, err := m.Constraints() - c.Assert(err, jc.ErrorIsNil) - _, hc := testing.AssertStartInstanceWithConstraints(c, ctx.env, m.Id(), cons) - err = m.SetProvisioned("i-missing", "fake_nonce", hc) - c.Assert(err, jc.ErrorIsNil) - err = m.SetInstanceStatus("missing") - c.Assert(err, jc.ErrorIsNil) -} - -type startAliveMachine struct { - machineId string -} - -func (sam startAliveMachine) step(c *gc.C, ctx *context) { - m, err := ctx.st.Machine(sam.machineId) - c.Assert(err, jc.ErrorIsNil) - pinger := ctx.setAgentPresence(c, m) - cons, err := m.Constraints() - c.Assert(err, jc.ErrorIsNil) - inst, hc := testing.AssertStartInstanceWithConstraints(c, ctx.env, m.Id(), cons) - err = m.SetProvisioned(inst.Id(), "fake_nonce", hc) - c.Assert(err, jc.ErrorIsNil) - ctx.pingers[m.Id()] = pinger -} - -type setAddresses struct { - machineId string - addresses []network.Address -} - -func (sa setAddresses) step(c *gc.C, ctx *context) { - m, err := ctx.st.Machine(sa.machineId) - c.Assert(err, jc.ErrorIsNil) - err = m.SetProviderAddresses(sa.addresses...) - c.Assert(err, jc.ErrorIsNil) -} - -type setTools struct { - machineId string - version version.Binary -} - -func (st setTools) step(c *gc.C, ctx *context) { - m, err := ctx.st.Machine(st.machineId) - c.Assert(err, jc.ErrorIsNil) - err = m.SetAgentVersion(st.version) - c.Assert(err, jc.ErrorIsNil) -} - -type setUnitTools struct { - unitName string - version version.Binary -} - -func (st setUnitTools) step(c *gc.C, ctx *context) { - m, err := ctx.st.Unit(st.unitName) - c.Assert(err, jc.ErrorIsNil) - err = m.SetAgentVersion(st.version) - c.Assert(err, jc.ErrorIsNil) -} - -type addCharm struct { - name string -} - -func (ac addCharm) addCharmStep(c *gc.C, ctx *context, scheme string, rev int) { - ch := testcharms.Repo.CharmDir(ac.name) - name := ch.Meta().Name - curl := charm.MustParseURL(fmt.Sprintf("%s:quantal/%s-%d", scheme, name, rev)) - dummy, err := ctx.st.AddCharm(ch, curl, "dummy-path", fmt.Sprintf("%s-%d-sha256", name, rev)) - c.Assert(err, jc.ErrorIsNil) - ctx.charms[ac.name] = dummy -} - -func (ac addCharm) step(c *gc.C, ctx *context) { - ch := testcharms.Repo.CharmDir(ac.name) - ac.addCharmStep(c, ctx, "cs", ch.Revision()) -} - -type addCharmWithRevision struct { - addCharm - scheme string - rev int -} - -func (ac addCharmWithRevision) step(c *gc.C, ctx *context) { - ac.addCharmStep(c, ctx, ac.scheme, ac.rev) -} - -type addService struct { - name string - charm string - networks []string - cons constraints.Value -} - -func (as addService) step(c *gc.C, ctx *context) { - ch, ok := ctx.charms[as.charm] - c.Assert(ok, jc.IsTrue) - svc, err := ctx.st.AddService(as.name, ctx.adminUserTag, ch, as.networks, nil, nil) - c.Assert(err, jc.ErrorIsNil) - if svc.IsPrincipal() { - err = svc.SetConstraints(as.cons) - c.Assert(err, jc.ErrorIsNil) - } -} - -type setServiceExposed struct { - name string - exposed bool -} - -func (sse setServiceExposed) step(c *gc.C, ctx *context) { - s, err := ctx.st.Service(sse.name) - c.Assert(err, jc.ErrorIsNil) - err = s.ClearExposed() - c.Assert(err, jc.ErrorIsNil) - if sse.exposed { - err = s.SetExposed() - c.Assert(err, jc.ErrorIsNil) - } -} - -type setServiceCharm struct { - name string - charm string -} - -func (ssc setServiceCharm) step(c *gc.C, ctx *context) { - ch, err := ctx.st.Charm(charm.MustParseURL(ssc.charm)) - c.Assert(err, jc.ErrorIsNil) - s, err := ctx.st.Service(ssc.name) - c.Assert(err, jc.ErrorIsNil) - err = s.SetCharm(ch, false) - c.Assert(err, jc.ErrorIsNil) -} - -type addCharmPlaceholder struct { - name string - rev int -} - -func (ac addCharmPlaceholder) step(c *gc.C, ctx *context) { - ch := testcharms.Repo.CharmDir(ac.name) - name := ch.Meta().Name - curl := charm.MustParseURL(fmt.Sprintf("cs:quantal/%s-%d", name, ac.rev)) - err := ctx.st.AddStoreCharmPlaceholder(curl) - c.Assert(err, jc.ErrorIsNil) -} - -type addUnit struct { - serviceName string - machineId string -} - -func (au addUnit) step(c *gc.C, ctx *context) { - s, err := ctx.st.Service(au.serviceName) - c.Assert(err, jc.ErrorIsNil) - u, err := s.AddUnit() - c.Assert(err, jc.ErrorIsNil) - m, err := ctx.st.Machine(au.machineId) - c.Assert(err, jc.ErrorIsNil) - err = u.AssignToMachine(m) - c.Assert(err, jc.ErrorIsNil) -} - -type addAliveUnit struct { - serviceName string - machineId string -} - -func (aau addAliveUnit) step(c *gc.C, ctx *context) { - s, err := ctx.st.Service(aau.serviceName) - c.Assert(err, jc.ErrorIsNil) - u, err := s.AddUnit() - c.Assert(err, jc.ErrorIsNil) - pinger := ctx.setAgentPresence(c, u) - m, err := ctx.st.Machine(aau.machineId) - c.Assert(err, jc.ErrorIsNil) - err = u.AssignToMachine(m) - c.Assert(err, jc.ErrorIsNil) - ctx.pingers[u.Name()] = pinger -} - -type setUnitsAlive struct { - serviceName string -} - -func (sua setUnitsAlive) step(c *gc.C, ctx *context) { - s, err := ctx.st.Service(sua.serviceName) - c.Assert(err, jc.ErrorIsNil) - us, err := s.AllUnits() - c.Assert(err, jc.ErrorIsNil) - for _, u := range us { - ctx.pingers[u.Name()] = ctx.setAgentPresence(c, u) - } -} - -type setUnitStatus struct { - unitName string - status state.Status - statusInfo string - statusData map[string]interface{} -} - -func (sus setUnitStatus) step(c *gc.C, ctx *context) { - u, err := ctx.st.Unit(sus.unitName) - c.Assert(err, jc.ErrorIsNil) - err = u.SetStatus(sus.status, sus.statusInfo, sus.statusData) - c.Assert(err, jc.ErrorIsNil) -} - -type setAgentStatus struct { - unitName string - status state.Status - statusInfo string - statusData map[string]interface{} -} - -func (sus setAgentStatus) step(c *gc.C, ctx *context) { - u, err := ctx.st.Unit(sus.unitName) - c.Assert(err, jc.ErrorIsNil) - err = u.SetAgentStatus(sus.status, sus.statusInfo, sus.statusData) - c.Assert(err, jc.ErrorIsNil) -} - -type setUnitCharmURL struct { - unitName string - charm string -} - -func (uc setUnitCharmURL) step(c *gc.C, ctx *context) { - u, err := ctx.st.Unit(uc.unitName) - c.Assert(err, jc.ErrorIsNil) - curl := charm.MustParseURL(uc.charm) - err = u.SetCharmURL(curl) - c.Assert(err, jc.ErrorIsNil) - err = u.SetStatus(state.StatusActive, "", nil) - c.Assert(err, jc.ErrorIsNil) - err = u.SetAgentStatus(state.StatusIdle, "", nil) - c.Assert(err, jc.ErrorIsNil) - -} - -type openUnitPort struct { - unitName string - protocol string - number int -} - -func (oup openUnitPort) step(c *gc.C, ctx *context) { - u, err := ctx.st.Unit(oup.unitName) - c.Assert(err, jc.ErrorIsNil) - err = u.OpenPort(oup.protocol, oup.number) - c.Assert(err, jc.ErrorIsNil) -} - -type ensureDyingUnit struct { - unitName string -} - -func (e ensureDyingUnit) step(c *gc.C, ctx *context) { - u, err := ctx.st.Unit(e.unitName) - c.Assert(err, jc.ErrorIsNil) - err = u.Destroy() - c.Assert(err, jc.ErrorIsNil) - c.Assert(u.Life(), gc.Equals, state.Dying) -} - -type ensureDyingService struct { - serviceName string -} - -func (e ensureDyingService) step(c *gc.C, ctx *context) { - svc, err := ctx.st.Service(e.serviceName) - c.Assert(err, jc.ErrorIsNil) - err = svc.Destroy() - c.Assert(err, jc.ErrorIsNil) - err = svc.Refresh() - c.Assert(err, jc.ErrorIsNil) - c.Assert(svc.Life(), gc.Equals, state.Dying) -} - -type ensureDeadMachine struct { - machineId string -} - -func (e ensureDeadMachine) step(c *gc.C, ctx *context) { - m, err := ctx.st.Machine(e.machineId) - c.Assert(err, jc.ErrorIsNil) - err = m.EnsureDead() - c.Assert(err, jc.ErrorIsNil) - c.Assert(m.Life(), gc.Equals, state.Dead) -} - -type setMachineStatus struct { - machineId string - status state.Status - statusInfo string -} - -func (sms setMachineStatus) step(c *gc.C, ctx *context) { - m, err := ctx.st.Machine(sms.machineId) - c.Assert(err, jc.ErrorIsNil) - err = m.SetStatus(sms.status, sms.statusInfo, nil) - c.Assert(err, jc.ErrorIsNil) -} - -type relateServices struct { - ep1, ep2 string -} - -func (rs relateServices) step(c *gc.C, ctx *context) { - eps, err := ctx.st.InferEndpoints(rs.ep1, rs.ep2) - c.Assert(err, jc.ErrorIsNil) - _, err = ctx.st.AddRelation(eps...) - c.Assert(err, jc.ErrorIsNil) -} - -type addSubordinate struct { - prinUnit string - subService string -} - -func (as addSubordinate) step(c *gc.C, ctx *context) { - u, err := ctx.st.Unit(as.prinUnit) - c.Assert(err, jc.ErrorIsNil) - eps, err := ctx.st.InferEndpoints(u.ServiceName(), as.subService) - c.Assert(err, jc.ErrorIsNil) - rel, err := ctx.st.EndpointsRelation(eps...) - c.Assert(err, jc.ErrorIsNil) - ru, err := rel.Unit(u) - c.Assert(err, jc.ErrorIsNil) - err = ru.EnterScope(nil) - c.Assert(err, jc.ErrorIsNil) -} - -type scopedExpect struct { - what string - scope []string - output M -} - -type expect struct { - what string - output M -} - -// substituteFakeTime replaces all "since" values -// in actual status output with a known fake value. -func substituteFakeSinceTime(c *gc.C, in []byte, expectIsoTime bool) []byte { - // This regexp will work for yaml and json. - exp := regexp.MustCompile(`(?P"?since"?:\ ?)(?P"?)(?P[^("|\n)]*)*"?`) - // Before the substritution is done, check that the timestamp produced - // by status is in the correct format. - if matches := exp.FindStringSubmatch(string(in)); matches != nil { - for i, name := range exp.SubexpNames() { - if name != "timestamp" { - continue - } - timeFormat := "02 Jan 2006 15:04:05Z07:00" - if expectIsoTime { - timeFormat = "2006-01-02 15:04:05Z" - } - _, err := time.Parse(timeFormat, matches[i]) - c.Assert(err, jc.ErrorIsNil) - } - } - - out := exp.ReplaceAllString(string(in), `$since$quote$quote`) - // Substitute a made up time used in our expected output. - out = strings.Replace(out, "", "01 Apr 15 01:23+10:00", -1) - return []byte(out) -} - -func (e scopedExpect) step(c *gc.C, ctx *context) { - c.Logf("\nexpect: %s %s\n", e.what, strings.Join(e.scope, " ")) - - // Now execute the command for each format. - for _, format := range statusFormats { - c.Logf("format %q", format.name) - // Run command with the required format. - args := []string{"--format", format.name} - if ctx.expectIsoTime { - args = append(args, "--utc") - } - args = append(args, e.scope...) - c.Logf("running status %s", strings.Join(args, " ")) - code, stdout, stderr := runStatus(c, args...) - c.Assert(code, gc.Equals, 0) - if !c.Check(stderr, gc.HasLen, 0) { - c.Fatalf("status failed: %s", string(stderr)) - } - - // Prepare the output in the same format. - buf, err := format.marshal(e.output) - c.Assert(err, jc.ErrorIsNil) - expected := make(M) - err = format.unmarshal(buf, &expected) - c.Assert(err, jc.ErrorIsNil) - - // Check the output is as expected. - actual := make(M) - out := substituteFakeSinceTime(c, stdout, ctx.expectIsoTime) - err = format.unmarshal(out, &actual) - c.Assert(err, jc.ErrorIsNil) - c.Assert(actual, jc.DeepEquals, expected) - } -} - -func (e expect) step(c *gc.C, ctx *context) { - scopedExpect{e.what, nil, e.output}.step(c, ctx) -} - -func (s *StatusSuite) TestStatusAllFormats(c *gc.C) { - for i, t := range statusTests { - c.Logf("test %d: %s", i, t.summary) - func(t testCase) { - // Prepare context and run all steps to setup. - ctx := s.newContext(c) - defer s.resetContext(c, ctx) - ctx.run(c, t.steps) - }(t) - } -} - -type fakeApiClient struct { - statusReturn *api.Status - patternsUsed []string - closeCalled bool -} - -func newFakeApiClient(statusReturn *api.Status) fakeApiClient { - return fakeApiClient{ - statusReturn: statusReturn, - } -} - -func (a *fakeApiClient) Status(patterns []string) (*api.Status, error) { - a.patternsUsed = patterns - return a.statusReturn, nil -} - -func (a *fakeApiClient) Close() error { - a.closeCalled = true - return nil -} - -// Check that the client works with an older server which doesn't -// return the top level Relations field nor the unit and machine level -// Agent field (they were introduced at the same time). -func (s *StatusSuite) TestStatusWithPreRelationsServer(c *gc.C) { - // Construct an older style status response - client := newFakeApiClient(&api.Status{ - EnvironmentName: "dummyenv", - Machines: map[string]api.MachineStatus{ - "0": { - // Agent field intentionally not set - Id: "0", - InstanceId: instance.Id("dummyenv-0"), - AgentState: "down", - AgentStateInfo: "(started)", - Series: "quantal", - Containers: map[string]api.MachineStatus{}, - Jobs: []multiwatcher.MachineJob{multiwatcher.JobManageEnviron}, - HasVote: false, - WantsVote: true, - }, - "1": { - // Agent field intentionally not set - Id: "1", - InstanceId: instance.Id("dummyenv-1"), - AgentState: "started", - AgentStateInfo: "hello", - Series: "quantal", - Containers: map[string]api.MachineStatus{}, - Jobs: []multiwatcher.MachineJob{multiwatcher.JobHostUnits}, - HasVote: false, - WantsVote: false, - }, - }, - Services: map[string]api.ServiceStatus{ - "mysql": { - Charm: "local:quantal/mysql-1", - Relations: map[string][]string{ - "server": {"wordpress"}, - }, - Units: map[string]api.UnitStatus{ - "mysql/0": { - // Agent field intentionally not set - Machine: "1", - AgentState: "allocating", - }, - }, - }, - "wordpress": { - Charm: "local:quantal/wordpress-3", - Relations: map[string][]string{ - "db": {"mysql"}, - }, - Units: map[string]api.UnitStatus{ - "wordpress/0": { - // Agent field intentionally not set - AgentState: "error", - AgentStateInfo: "blam", - Machine: "1", - }, - }, - }, - }, - Networks: map[string]api.NetworkStatus{}, - // Relations field intentionally not set - }) - s.PatchValue(&newApiClientForStatus, func(_ *StatusCommand) (statusAPI, error) { - return &client, nil - }) - - expected := expect{ - "sane output with an older client that doesn't return Agent or Relations fields", - M{ - "environment": "dummyenv", - "machines": M{ - "0": M{ - "agent-state": "down", - "agent-state-info": "(started)", - "instance-id": "dummyenv-0", - "series": "quantal", - "state-server-member-status": "adding-vote", - }, - "1": M{ - "agent-state": "started", - "agent-state-info": "hello", - "instance-id": "dummyenv-1", - "series": "quantal", - }, - }, - "services": M{ - "mysql": M{ - "charm": "local:quantal/mysql-1", - "exposed": false, - "relations": M{ - "server": L{"wordpress"}, - }, - "service-status": M{}, - "units": M{ - "mysql/0": M{ - "machine": "1", - "agent-state": "allocating", - "workload-status": M{}, - "agent-status": M{}, - }, - }, - }, - "wordpress": M{ - "charm": "local:quantal/wordpress-3", - "exposed": false, - "relations": M{ - "db": L{"mysql"}, - }, - "service-status": M{}, - "units": M{ - "wordpress/0": M{ - "machine": "1", - "agent-state": "error", - "agent-state-info": "blam", - "workload-status": M{}, - "agent-status": M{}, - }, - }, - }, - }, - }, - } - ctx := s.newContext(c) - defer s.resetContext(c, ctx) - ctx.run(c, []stepper{expected}) -} - -func (s *StatusSuite) TestStatusWithFormatSummary(c *gc.C) { - ctx := s.newContext(c) - defer s.resetContext(c, ctx) - steps := []stepper{ - addMachine{machineId: "0", job: state.JobManageEnviron}, - setAddresses{"0", network.NewAddresses("localhost")}, - startAliveMachine{"0"}, - setMachineStatus{"0", state.StatusStarted, ""}, - addCharm{"wordpress"}, - addCharm{"mysql"}, - addCharm{"logging"}, - addService{name: "wordpress", charm: "wordpress"}, - setServiceExposed{"wordpress", true}, - addMachine{machineId: "1", job: state.JobHostUnits}, - setAddresses{"1", network.NewAddresses("localhost")}, - startAliveMachine{"1"}, - setMachineStatus{"1", state.StatusStarted, ""}, - addAliveUnit{"wordpress", "1"}, - setAgentStatus{"wordpress/0", state.StatusIdle, "", nil}, - setUnitStatus{"wordpress/0", state.StatusActive, "", nil}, - addService{name: "mysql", charm: "mysql"}, - setServiceExposed{"mysql", true}, - addMachine{machineId: "2", job: state.JobHostUnits}, - setAddresses{"2", network.NewAddresses("10.0.0.1")}, - startAliveMachine{"2"}, - setMachineStatus{"2", state.StatusStarted, ""}, - addAliveUnit{"mysql", "2"}, - setAgentStatus{"mysql/0", state.StatusIdle, "", nil}, - setUnitStatus{"mysql/0", state.StatusActive, "", nil}, - addService{name: "logging", charm: "logging"}, - setServiceExposed{"logging", true}, - relateServices{"wordpress", "mysql"}, - relateServices{"wordpress", "logging"}, - relateServices{"mysql", "logging"}, - addSubordinate{"wordpress/0", "logging"}, - addSubordinate{"mysql/0", "logging"}, - setUnitsAlive{"logging"}, - setAgentStatus{"logging/0", state.StatusIdle, "", nil}, - setUnitStatus{"logging/0", state.StatusActive, "", nil}, - setAgentStatus{"logging/1", state.StatusError, "somehow lost in all those logs", nil}, - } - for _, s := range steps { - s.step(c, ctx) - } - code, stdout, stderr := runStatus(c, "--format", "summary") - c.Check(code, gc.Equals, 0) - c.Check(string(stderr), gc.Equals, "") - c.Assert( - string(stdout), - gc.Equals, - "Running on subnets: 127.0.0.1/8, 10.0.0.1/8 \n"+ - "Utilizing ports: \n"+ - " # MACHINES: (3)\n"+ - " started: 3 \n"+ - " \n"+ - " # UNITS: (4)\n"+ - " error: 1 \n"+ - " started: 3 \n"+ - " \n"+ - " # SERVICES: (3)\n"+ - " logging 1/1 exposed\n"+ - " mysql 1/1 exposed\n"+ - " wordpress 1/1 exposed\n"+ - "\n", - ) -} -func (s *StatusSuite) TestStatusWithFormatOneline(c *gc.C) { - ctx := s.newContext(c) - defer s.resetContext(c, ctx) - steps := []stepper{ - addMachine{machineId: "0", job: state.JobManageEnviron}, - setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, - startAliveMachine{"0"}, - setMachineStatus{"0", state.StatusStarted, ""}, - addCharm{"wordpress"}, - addCharm{"mysql"}, - addCharm{"logging"}, - - addService{name: "wordpress", charm: "wordpress"}, - setServiceExposed{"wordpress", true}, - addMachine{machineId: "1", job: state.JobHostUnits}, - setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, - startAliveMachine{"1"}, - setMachineStatus{"1", state.StatusStarted, ""}, - addAliveUnit{"wordpress", "1"}, - setAgentStatus{"wordpress/0", state.StatusIdle, "", nil}, - setUnitStatus{"wordpress/0", state.StatusActive, "", nil}, - - addService{name: "mysql", charm: "mysql"}, - setServiceExposed{"mysql", true}, - addMachine{machineId: "2", job: state.JobHostUnits}, - setAddresses{"2", network.NewAddresses("dummyenv-2.dns")}, - startAliveMachine{"2"}, - setMachineStatus{"2", state.StatusStarted, ""}, - addAliveUnit{"mysql", "2"}, - setAgentStatus{"mysql/0", state.StatusIdle, "", nil}, - setUnitStatus{"mysql/0", state.StatusActive, "", nil}, - - addService{name: "logging", charm: "logging"}, - setServiceExposed{"logging", true}, - - relateServices{"wordpress", "mysql"}, - relateServices{"wordpress", "logging"}, - relateServices{"mysql", "logging"}, - - addSubordinate{"wordpress/0", "logging"}, - addSubordinate{"mysql/0", "logging"}, - - setUnitsAlive{"logging"}, - setAgentStatus{"logging/0", state.StatusIdle, "", nil}, - setUnitStatus{"logging/0", state.StatusActive, "", nil}, - setAgentStatus{"logging/1", state.StatusError, "somehow lost in all those logs", nil}, - } - - ctx.run(c, steps) - - var expected = ` -- mysql/0: dummyenv-2.dns (started) - - logging/1: dummyenv-2.dns (error) -- wordpress/0: dummyenv-1.dns (started) - - logging/0: dummyenv-1.dns (started) -- new available version: "1.24.7" -` - - code, stdout, stderr := runStatus(c, "--format", "oneline") - c.Check(code, gc.Equals, 0) - c.Check(string(stderr), gc.Equals, "") - c.Assert(string(stdout), gc.Equals, expected) - - c.Log(`Check that "short" is an alias for oneline.`) - code, stdout, stderr = runStatus(c, "--format", "short") - c.Check(code, gc.Equals, 0) - c.Check(string(stderr), gc.Equals, "") - c.Assert(string(stdout), gc.Equals, expected) - - c.Log(`Check that "line" is an alias for oneline.`) - code, stdout, stderr = runStatus(c, "--format", "line") - c.Check(code, gc.Equals, 0) - c.Check(string(stderr), gc.Equals, "") - c.Assert(string(stdout), gc.Equals, expected) -} -func (s *StatusSuite) prepareTabularData(c *gc.C) *context { - ctx := s.newContext(c) - steps := []stepper{ - addMachine{machineId: "0", job: state.JobManageEnviron}, - setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, - startAliveMachine{"0"}, - setMachineStatus{"0", state.StatusStarted, ""}, - addCharm{"wordpress"}, - addCharm{"mysql"}, - addCharm{"logging"}, - addService{name: "wordpress", charm: "wordpress"}, - setServiceExposed{"wordpress", true}, - addMachine{machineId: "1", job: state.JobHostUnits}, - setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, - startAliveMachine{"1"}, - setMachineStatus{"1", state.StatusStarted, ""}, - addAliveUnit{"wordpress", "1"}, - setAgentStatus{"wordpress/0", state.StatusIdle, "", nil}, - setUnitStatus{"wordpress/0", state.StatusActive, "", nil}, - setUnitTools{"wordpress/0", version.MustParseBinary("1.2.3-trusty-ppc")}, - addService{name: "mysql", charm: "mysql"}, - setServiceExposed{"mysql", true}, - addMachine{machineId: "2", job: state.JobHostUnits}, - setAddresses{"2", network.NewAddresses("dummyenv-2.dns")}, - startAliveMachine{"2"}, - setMachineStatus{"2", state.StatusStarted, ""}, - addAliveUnit{"mysql", "2"}, - setAgentStatus{"mysql/0", state.StatusIdle, "", nil}, - setUnitStatus{ - "mysql/0", - state.StatusMaintenance, - "installing all the things", nil}, - setUnitTools{"mysql/0", version.MustParseBinary("1.2.3-trusty-ppc")}, - addService{name: "logging", charm: "logging"}, - setServiceExposed{"logging", true}, - relateServices{"wordpress", "mysql"}, - relateServices{"wordpress", "logging"}, - relateServices{"mysql", "logging"}, - addSubordinate{"wordpress/0", "logging"}, - addSubordinate{"mysql/0", "logging"}, - setUnitsAlive{"logging"}, - setAgentStatus{"logging/0", state.StatusIdle, "", nil}, - setUnitStatus{"logging/0", state.StatusActive, "", nil}, - setAgentStatus{"logging/1", state.StatusError, "somehow lost in all those logs", nil}, - } - for _, s := range steps { - s.step(c, ctx) - } - return ctx -} - -func (s *StatusSuite) testStatusWithFormatTabular(c *gc.C, useFeatureFlag bool) { - ctx := s.prepareTabularData(c) - defer s.resetContext(c, ctx) - var args []string - if !useFeatureFlag { - args = []string{"--format", "tabular"} - } - code, stdout, stderr := runStatus(c, args...) - c.Check(code, gc.Equals, 0) - c.Check(string(stderr), gc.Equals, "") - c.Assert( - string(stdout), - gc.Equals, - "[Services] \n"+ - "NAME STATUS EXPOSED CHARM \n"+ - "logging true cs:quantal/logging-1 \n"+ - "mysql maintenance true cs:quantal/mysql-1 \n"+ - "wordpress active true cs:quantal/wordpress-3 \n"+ - "\n"+ - "[Units] \n"+ - "ID WORKLOAD-STATE AGENT-STATE VERSION MACHINE PORTS PUBLIC-ADDRESS MESSAGE \n"+ - "mysql/0 maintenance idle 1.2.3 2 dummyenv-2.dns installing all the things \n"+ - " logging/1 error idle dummyenv-2.dns somehow lost in all those logs \n"+ - "wordpress/0 active idle 1.2.3 1 dummyenv-1.dns \n"+ - " logging/0 active idle dummyenv-1.dns \n"+ - "\n"+ - "[Machines] \n"+ - "ID STATE VERSION DNS INS-ID SERIES HARDWARE \n"+ - "0 started dummyenv-0.dns dummyenv-0 quantal arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M \n"+ - "1 started dummyenv-1.dns dummyenv-1 quantal arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M \n"+ - "2 started dummyenv-2.dns dummyenv-2 quantal arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M \n"+ - "\n"+ - "[Juju] \n"+ - "UPGRADE-AVAILABLE \n"+ - "1.24.7 \n"+ - "\n", - ) -} - -func (s *StatusSuite) TestStatusV2(c *gc.C) { - s.PatchEnvironment(osenv.JujuCLIVersion, "2") - s.testStatusWithFormatTabular(c, true) -} - -func (s *StatusSuite) TestStatusWithFormatTabular(c *gc.C) { - s.testStatusWithFormatTabular(c, false) -} - -func (s *StatusSuite) TestFormatTabularHookActionName(c *gc.C) { - status := formattedStatus{ - Services: map[string]serviceStatus{ - "foo": serviceStatus{ - Units: map[string]unitStatus{ - "foo/0": unitStatus{ - AgentStatusInfo: statusInfoContents{ - Current: params.StatusExecuting, - Message: "running config-changed hook", - }, - WorkloadStatusInfo: statusInfoContents{ - Current: params.StatusMaintenance, - Message: "doing some work", - }, - }, - "foo/1": unitStatus{ - AgentStatusInfo: statusInfoContents{ - Current: params.StatusExecuting, - Message: "running action backup database", - }, - WorkloadStatusInfo: statusInfoContents{ - Current: params.StatusMaintenance, - Message: "doing some work", - }, - }, - }, - }, - }, - } - out, err := FormatTabular(status) - c.Assert(err, jc.ErrorIsNil) - c.Assert( - string(out), - gc.Equals, - "[Services] \n"+ - "NAME STATUS EXPOSED CHARM \n"+ - "foo false \n"+ - "\n"+ - "[Units] \n"+ - "ID WORKLOAD-STATE AGENT-STATE VERSION MACHINE PORTS PUBLIC-ADDRESS MESSAGE \n"+ - "foo/0 maintenance executing (config-changed) doing some work \n"+ - "foo/1 maintenance executing (backup database) doing some work \n"+ - "\n"+ - "[Machines] \n"+ - "ID STATE VERSION DNS INS-ID SERIES HARDWARE \n", - ) -} - -func (s *StatusSuite) TestStatusWithNilStatusApi(c *gc.C) { - ctx := s.newContext(c) - defer s.resetContext(c, ctx) - steps := []stepper{ - addMachine{machineId: "0", job: state.JobManageEnviron}, - setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, - startAliveMachine{"0"}, - setMachineStatus{"0", state.StatusStarted, ""}, - } - - for _, s := range steps { - s.step(c, ctx) - } - - client := fakeApiClient{} - var status = client.Status - s.PatchValue(&status, func(_ []string) (*api.Status, error) { - return nil, nil - }) - s.PatchValue(&newApiClientForStatus, func(_ *StatusCommand) (statusAPI, error) { - return &client, nil - }) - - code, _, stderr := runStatus(c, "--format", "tabular") - c.Check(code, gc.Equals, 1) - c.Check(string(stderr), gc.Equals, "error: unable to obtain the current status\n") -} - -// -// Filtering Feature -// - -func (s *StatusSuite) FilteringTestSetup(c *gc.C) *context { - ctx := s.newContext(c) - - steps := []stepper{ - // Given a machine is started - // And the machine's ID is "0" - // And the machine's job is to manage the environment - addMachine{machineId: "0", job: state.JobManageEnviron}, - startAliveMachine{"0"}, - setMachineStatus{"0", state.StatusStarted, ""}, - // And the machine's address is "dummyenv-0.dns" - setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, - // And the "wordpress" charm is available - addCharm{"wordpress"}, - addService{name: "wordpress", charm: "wordpress"}, - // And the "mysql" charm is available - addCharm{"mysql"}, - addService{name: "mysql", charm: "mysql"}, - // And the "logging" charm is available - addCharm{"logging"}, - // And a machine is started - // And the machine's ID is "1" - // And the machine's job is to host units - addMachine{machineId: "1", job: state.JobHostUnits}, - startAliveMachine{"1"}, - setMachineStatus{"1", state.StatusStarted, ""}, - // And the machine's address is "dummyenv-1.dns" - setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, - // And a unit of "wordpress" is deployed to machine "1" - addAliveUnit{"wordpress", "1"}, - // And the unit is started - setAgentStatus{"wordpress/0", state.StatusIdle, "", nil}, - setUnitStatus{"wordpress/0", state.StatusActive, "", nil}, - // And a machine is started - - // And the machine's ID is "2" - // And the machine's job is to host units - addMachine{machineId: "2", job: state.JobHostUnits}, - startAliveMachine{"2"}, - setMachineStatus{"2", state.StatusStarted, ""}, - // And the machine's address is "dummyenv-2.dns" - setAddresses{"2", network.NewAddresses("dummyenv-2.dns")}, - // And a unit of "mysql" is deployed to machine "2" - addAliveUnit{"mysql", "2"}, - // And the unit is started - setAgentStatus{"mysql/0", state.StatusIdle, "", nil}, - setUnitStatus{"mysql/0", state.StatusActive, "", nil}, - // And the "logging" service is added - addService{name: "logging", charm: "logging"}, - // And the service is exposed - setServiceExposed{"logging", true}, - // And the "wordpress" service is related to the "mysql" service - relateServices{"wordpress", "mysql"}, - // And the "wordpress" service is related to the "logging" service - relateServices{"wordpress", "logging"}, - // And the "mysql" service is related to the "logging" service - relateServices{"mysql", "logging"}, - // And the "logging" service is a subordinate to unit 0 of the "wordpress" service - addSubordinate{"wordpress/0", "logging"}, - setAgentStatus{"logging/0", state.StatusIdle, "", nil}, - setUnitStatus{"logging/0", state.StatusActive, "", nil}, - // And the "logging" service is a subordinate to unit 0 of the "mysql" service - addSubordinate{"mysql/0", "logging"}, - setAgentStatus{"logging/1", state.StatusIdle, "", nil}, - setUnitStatus{"logging/1", state.StatusActive, "", nil}, - setUnitsAlive{"logging"}, - } - - ctx.run(c, steps) - return ctx -} - -// Scenario: One unit is in an errored state and user filters to started -func (s *StatusSuite) TestFilterToStarted(c *gc.C) { - ctx := s.FilteringTestSetup(c) - defer s.resetContext(c, ctx) - - // Given unit 1 of the "logging" service has an error - setAgentStatus{"logging/1", state.StatusError, "mock error", nil}.step(c, ctx) - // And unit 0 of the "mysql" service has an error - setAgentStatus{"mysql/0", state.StatusError, "mock error", nil}.step(c, ctx) - // When I run juju status --format oneline started - _, stdout, stderr := runStatus(c, "--format", "oneline", "started") - c.Assert(string(stderr), gc.Equals, "") - // Then I should receive output prefixed with: - var expected = ` - -- wordpress/0: dummyenv-1.dns (started) - - logging/0: dummyenv-1.dns (started) -- new available version: "` + nextVersion + `" -` - - c.Assert(string(stdout), gc.Equals, expected[1:]) -} - -// Scenario: One unit is in an errored state and user filters to errored -func (s *StatusSuite) TestFilterToErrored(c *gc.C) { - ctx := s.FilteringTestSetup(c) - defer s.resetContext(c, ctx) - - // Given unit 1 of the "logging" service has an error - setAgentStatus{"logging/1", state.StatusError, "mock error", nil}.step(c, ctx) - // When I run juju status --format oneline error - _, stdout, stderr := runStatus(c, "--format", "oneline", "error") - c.Assert(stderr, gc.IsNil) - // Then I should receive output prefixed with: - var expected = ` - -- mysql/0: dummyenv-2.dns (started) - - logging/1: dummyenv-2.dns (error) -- new available version: "` + nextVersion + `" -` - - c.Assert(string(stdout), gc.Equals, expected[1:]) -} - -// Scenario: User filters to mysql service -func (s *StatusSuite) TestFilterToService(c *gc.C) { - ctx := s.FilteringTestSetup(c) - defer s.resetContext(c, ctx) - - // When I run juju status --format oneline error - _, stdout, stderr := runStatus(c, "--format", "oneline", "mysql") - c.Assert(stderr, gc.IsNil) - // Then I should receive output prefixed with: - var expected = ` - -- mysql/0: dummyenv-2.dns (started) - - logging/1: dummyenv-2.dns (started) -- new available version: "` + nextVersion + `" -` - - c.Assert(string(stdout), gc.Equals, expected[1:]) -} - -// Scenario: User filters to exposed services -func (s *StatusSuite) TestFilterToExposedService(c *gc.C) { - ctx := s.FilteringTestSetup(c) - defer s.resetContext(c, ctx) - - // Given unit 1 of the "mysql" service is exposed - setServiceExposed{"mysql", true}.step(c, ctx) - // And the logging service is not exposed - setServiceExposed{"logging", false}.step(c, ctx) - // And the wordpress service is not exposed - setServiceExposed{"wordpress", false}.step(c, ctx) - // When I run juju status --format oneline exposed - _, stdout, stderr := runStatus(c, "--format", "oneline", "exposed") - c.Assert(stderr, gc.IsNil) - // Then I should receive output prefixed with: - var expected = ` - -- mysql/0: dummyenv-2.dns (started) - - logging/1: dummyenv-2.dns (started) -- new available version: "` + nextVersion + `" -` - - c.Assert(string(stdout), gc.Equals, expected[1:]) -} - -// Scenario: User filters to non-exposed services -func (s *StatusSuite) TestFilterToNotExposedService(c *gc.C) { - ctx := s.FilteringTestSetup(c) - defer s.resetContext(c, ctx) - - setServiceExposed{"mysql", true}.step(c, ctx) - // When I run juju status --format oneline not exposed - _, stdout, stderr := runStatus(c, "--format", "oneline", "not", "exposed") - c.Assert(stderr, gc.IsNil) - // Then I should receive output prefixed with: - var expected = ` - -- wordpress/0: dummyenv-1.dns (started) - - logging/0: dummyenv-1.dns (started) -- new available version: "` + nextVersion + `" -` - - c.Assert(string(stdout), gc.Equals, expected[1:]) -} - -// Scenario: Filtering on Subnets -func (s *StatusSuite) TestFilterOnSubnet(c *gc.C) { - ctx := s.FilteringTestSetup(c) - defer s.resetContext(c, ctx) - - // Given the address for machine "1" is "localhost" - setAddresses{"1", network.NewAddresses("localhost")}.step(c, ctx) - // And the address for machine "2" is "10.0.0.1" - setAddresses{"2", network.NewAddresses("10.0.0.1")}.step(c, ctx) - // When I run juju status --format oneline 127.0.0.1 - _, stdout, stderr := runStatus(c, "--format", "oneline", "127.0.0.1") - c.Assert(stderr, gc.IsNil) - // Then I should receive output prefixed with: - var expected = ` - -- wordpress/0: localhost (started) - - logging/0: localhost (started) -- new available version: "` + nextVersion + `" -` - - c.Assert(string(stdout), gc.Equals, expected[1:]) -} - -// Scenario: Filtering on Ports -func (s *StatusSuite) TestFilterOnPorts(c *gc.C) { - ctx := s.FilteringTestSetup(c) - defer s.resetContext(c, ctx) - - // Given the address for machine "1" is "localhost" - setAddresses{"1", network.NewAddresses("localhost")}.step(c, ctx) - // And the address for machine "2" is "10.0.0.1" - setAddresses{"2", network.NewAddresses("10.0.0.1")}.step(c, ctx) - openUnitPort{"wordpress/0", "tcp", 80}.step(c, ctx) - // When I run juju status --format oneline 80/tcp - _, stdout, stderr := runStatus(c, "--format", "oneline", "80/tcp") - c.Assert(stderr, gc.IsNil) - // Then I should receive output prefixed with: - var expected = ` - -- wordpress/0: localhost (started) 80/tcp - - logging/0: localhost (started) -- new available version: "` + nextVersion + `" -` - - c.Assert(string(stdout), gc.Equals, expected[1:]) -} - -// Scenario: User filters out a parent, but not its subordinate -func (s *StatusSuite) TestFilterParentButNotSubordinate(c *gc.C) { - ctx := s.FilteringTestSetup(c) - defer s.resetContext(c, ctx) - - // When I run juju status --format oneline 80/tcp - _, stdout, stderr := runStatus(c, "--format", "oneline", "logging") - c.Assert(stderr, gc.IsNil) - // Then I should receive output prefixed with: - var expected = ` - -- mysql/0: dummyenv-2.dns (started) - - logging/1: dummyenv-2.dns (started) -- wordpress/0: dummyenv-1.dns (started) - - logging/0: dummyenv-1.dns (started) -- new available version: "` + nextVersion + `" -` - - c.Assert(string(stdout), gc.Equals, expected[1:]) -} - -// Scenario: User filters out a subordinate, but not its parent -func (s *StatusSuite) TestFilterSubordinateButNotParent(c *gc.C) { - ctx := s.FilteringTestSetup(c) - defer s.resetContext(c, ctx) - - // Given the wordpress service is exposed - setServiceExposed{"wordpress", true}.step(c, ctx) - // When I run juju status --format oneline not exposed - _, stdout, stderr := runStatus(c, "--format", "oneline", "not", "exposed") - c.Assert(stderr, gc.IsNil) - // Then I should receive output prefixed with: - var expected = ` - -- mysql/0: dummyenv-2.dns (started) - - logging/1: dummyenv-2.dns (started) -- new available version: "` + nextVersion + `" -` - - c.Assert(string(stdout), gc.Equals, expected[1:]) -} - -func (s *StatusSuite) TestFilterMultipleHomogenousPatterns(c *gc.C) { - ctx := s.FilteringTestSetup(c) - defer s.resetContext(c, ctx) - - _, stdout, stderr := runStatus(c, "--format", "oneline", "wordpress/0", "mysql/0") - c.Assert(stderr, gc.IsNil) - // Then I should receive output prefixed with: - var expected = ` - -- mysql/0: dummyenv-2.dns (started) - - logging/1: dummyenv-2.dns (started) -- wordpress/0: dummyenv-1.dns (started) - - logging/0: dummyenv-1.dns (started) -- new available version: "` + nextVersion + `" -` - - c.Assert(string(stdout), gc.Equals, expected[1:]) -} - -func (s *StatusSuite) TestFilterMultipleHeterogenousPatterns(c *gc.C) { - ctx := s.FilteringTestSetup(c) - defer s.resetContext(c, ctx) - - _, stdout, stderr := runStatus(c, "--format", "oneline", "wordpress/0", "started") - c.Assert(stderr, gc.IsNil) - // Then I should receive output prefixed with: - var expected = ` - -- mysql/0: dummyenv-2.dns (started) - - logging/1: dummyenv-2.dns (started) -- wordpress/0: dummyenv-1.dns (started) - - logging/0: dummyenv-1.dns (started) -- new available version: "` + nextVersion + `" -` - - c.Assert(string(stdout), gc.Equals, expected[1:]) -} - -// TestSummaryStatusWithUnresolvableDns is result of bug# 1410320. -func (s *StatusSuite) TestSummaryStatusWithUnresolvableDns(c *gc.C) { - formatter := &summaryFormatter{} - formatter.resolveAndTrackIp("invalidDns") - // Test should not panic. -} - -func initStatusCommand(args ...string) (*StatusCommand, error) { - com := &StatusCommand{} - return com, coretesting.InitCommand(envcmd.Wrap(com), args) -} - -var statusInitTests = []struct { - args []string - envVar string - isoTime bool - err string -}{ - { - isoTime: false, - }, { - args: []string{"--utc"}, - isoTime: true, - }, { - envVar: "true", - isoTime: true, - }, { - envVar: "foo", - err: "invalid JUJU_STATUS_ISO_TIME env var, expected true|false.*", - }, -} - -func (*StatusSuite) TestStatusCommandInit(c *gc.C) { - defer os.Setenv(osenv.JujuStatusIsoTimeEnvKey, os.Getenv(osenv.JujuStatusIsoTimeEnvKey)) - - for i, t := range statusInitTests { - c.Logf("test %d", i) - os.Setenv(osenv.JujuStatusIsoTimeEnvKey, t.envVar) - com, err := initStatusCommand(t.args...) - if t.err != "" { - c.Check(err, gc.ErrorMatches, t.err) - } else { - c.Check(err, jc.ErrorIsNil) - } - c.Check(com.isoTime, gc.DeepEquals, t.isoTime) - } -} - -var statusTimeTest = test( - "status generates timestamps as UTC in ISO format", - addMachine{machineId: "0", job: state.JobManageEnviron}, - setAddresses{"0", network.NewAddresses("dummyenv-0.dns")}, - startAliveMachine{"0"}, - setMachineStatus{"0", state.StatusStarted, ""}, - addCharm{"dummy"}, - addService{name: "dummy-service", charm: "dummy"}, - - addMachine{machineId: "1", job: state.JobHostUnits}, - startAliveMachine{"1"}, - setAddresses{"1", network.NewAddresses("dummyenv-1.dns")}, - setMachineStatus{"1", state.StatusStarted, ""}, - - addAliveUnit{"dummy-service", "1"}, - expect{ - "add two units, one alive (in error state), one started", - M{ - "environment": "dummyenv", - "available-version": nextVersion, - "machines": M{ - "0": machine0, - "1": machine1, - }, - "services": M{ - "dummy-service": M{ - "charm": "cs:quantal/dummy-1", - "exposed": false, - "service-status": M{ - "current": "unknown", - "message": "Waiting for agent initialization to finish", - "since": "01 Apr 15 01:23+10:00", - }, - "units": M{ - "dummy-service/0": M{ - "machine": "1", - "agent-state": "pending", - "workload-status": M{ - "current": "unknown", - "message": "Waiting for agent initialization to finish", - "since": "01 Apr 15 01:23+10:00", - }, - "agent-status": M{ - "current": "allocating", - "since": "01 Apr 15 01:23+10:00", - }, - "public-address": "dummyenv-1.dns", - }, - }, - }, - }, - }, - }, -) - -func (s *StatusSuite) TestIsoTimeFormat(c *gc.C) { - func(t testCase) { - // Prepare context and run all steps to setup. - ctx := s.newContext(c) - ctx.expectIsoTime = true - defer s.resetContext(c, ctx) - ctx.run(c, t.steps) - }(statusTimeTest) -} === removed file 'src/github.com/juju/juju/cmd/juju/statushistory.go' --- src/github.com/juju/juju/cmd/juju/statushistory.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/statushistory.go 1970-01-01 00:00:00 +0000 @@ -1,119 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - "os" - "strconv" - - "github.com/juju/cmd" - "github.com/juju/errors" - - "github.com/juju/juju/api" - "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/juju/osenv" - "launchpad.net/gnuflag" -) - -type StatusHistoryCommand struct { - envcmd.EnvCommandBase - out cmd.Output - outputContent string - backlogSize int - isoTime bool - unitName string -} - -var statusHistoryDoc = ` -This command will report the history of status changes for -a given unit. -The statuses for the unit workload and/or agent are available. --type supports: - agent: will show statuses for the unit's agent - workload: will show statuses for the unit's workload - combined: will show agent and workload statuses combined - and sorted by time of occurence. -` - -func (c *StatusHistoryCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "status-history", - Args: "[-n N] ", - Purpose: "output past statuses for a unit", - Doc: statusHistoryDoc, - } -} - -func (c *StatusHistoryCommand) SetFlags(f *gnuflag.FlagSet) { - f.StringVar(&c.outputContent, "type", "combined", "type of statuses to be displayed [agent|workload|combined].") - f.IntVar(&c.backlogSize, "n", 20, "size of logs backlog.") - f.BoolVar(&c.isoTime, "utc", false, "display time as UTC in RFC3339 format") -} - -func (c *StatusHistoryCommand) Init(args []string) error { - switch { - case len(args) > 1: - return errors.Errorf("unexpected arguments after unit name.") - case len(args) == 0: - return errors.Errorf("unit name is missing.") - default: - c.unitName = args[0] - } - // If use of ISO time not specified on command line, - // check env var. - if !c.isoTime { - var err error - envVarValue := os.Getenv(osenv.JujuStatusIsoTimeEnvKey) - if envVarValue != "" { - if c.isoTime, err = strconv.ParseBool(envVarValue); err != nil { - return errors.Annotatef(err, "invalid %s env var, expected true|false", osenv.JujuStatusIsoTimeEnvKey) - } - } - } - kind := params.HistoryKind(c.outputContent) - switch kind { - case params.KindCombined, params.KindAgent, params.KindWorkload: - return nil - - } - return errors.Errorf("unexpected status type %q", c.outputContent) -} - -func (c *StatusHistoryCommand) Run(ctx *cmd.Context) error { - apiclient, err := c.NewAPIClient() - if err != nil { - return fmt.Errorf(connectionError, c.ConnectionName(), err) - } - defer apiclient.Close() - var statuses *api.UnitStatusHistory - kind := params.HistoryKind(c.outputContent) - statuses, err = apiclient.UnitStatusHistory(kind, c.unitName, c.backlogSize) - if err != nil { - if len(statuses.Statuses) == 0 { - return errors.Trace(err) - } - // Display any error, but continue to print status if some was returned - fmt.Fprintf(ctx.Stderr, "%v\n", err) - } else if len(statuses.Statuses) == 0 { - return errors.Errorf("no status history available") - } - table := [][]string{{"TIME", "TYPE", "STATUS", "MESSAGE"}} - lengths := []int{1, 1, 1, 1} - for _, v := range statuses.Statuses { - fields := []string{formatStatusTime(v.Since, c.isoTime), string(v.Kind), string(v.Status), v.Info} - for k, v := range fields { - if len(v) > lengths[k] { - lengths[k] = len(v) - } - } - table = append(table, fields) - } - f := fmt.Sprintf("%%-%ds\t%%-%ds\t%%-%ds\t%%-%ds\n", lengths[0], lengths[1], lengths[2], lengths[3]) - for _, v := range table { - fmt.Printf(f, v[0], v[1], v[2], v[3]) - } - return nil -} === removed file 'src/github.com/juju/juju/cmd/juju/statusnaturalsort.go' --- src/github.com/juju/juju/cmd/juju/statusnaturalsort.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/statusnaturalsort.go 1970-01-01 00:00:00 +0000 @@ -1,50 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - "strconv" - "strings" - "unicode" -) - -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 -} === removed file 'src/github.com/juju/juju/cmd/juju/statusnaturalsort_test.go' --- src/github.com/juju/juju/cmd/juju/statusnaturalsort_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/statusnaturalsort_test.go 1970-01-01 00:00:00 +0000 @@ -1,113 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -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) -} === modified file 'src/github.com/juju/juju/cmd/juju/storage/help_test.go' --- src/github.com/juju/juju/cmd/juju/storage/help_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/storage/help_test.go 2015-10-23 18:29:32 +0000 @@ -10,14 +10,13 @@ jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" - "github.com/juju/juju/cmd/juju/storage" "github.com/juju/juju/testing" ) type HelpStorageSuite struct { testing.FakeJujuHomeSuite - command *storage.Command + command cmd.Command } func (s *HelpStorageSuite) SetUpTest(c *gc.C) { @@ -32,9 +31,11 @@ ctx, err := testing.RunCommand(c, s.command, "--help") c.Assert(err, jc.ErrorIsNil) - expected := "(?sm).*^purpose: " + s.command.Purpose + "$.*" + super, ok := s.command.(*cmd.SuperCommand) + c.Assert(ok, jc.IsTrue) + expected := "(?sm).*^purpose: " + super.Purpose + "$.*" c.Check(testing.Stdout(ctx), gc.Matches, expected) - expected = "(?sm).*^" + s.command.Doc + "$.*" + expected = "(?sm).*^" + super.Doc + "$.*" c.Check(testing.Stdout(ctx), gc.Matches, expected) s.checkHelpCommands(c, ctx, expectedNames) === modified file 'src/github.com/juju/juju/cmd/juju/storage/list.go' --- src/github.com/juju/juju/cmd/juju/storage/list.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/storage/list.go 2015-10-23 18:29:32 +0000 @@ -69,20 +69,31 @@ // filter out valid output, if any var valid []params.StorageDetails for _, one := range found { - if one.Error == nil { - valid = append(valid, one.StorageDetails) + if one.Error != nil { + fmt.Fprintf(ctx.Stderr, "%v\n", one.Error) continue } - // display individual error - fmt.Fprintf(ctx.Stderr, "%v\n", one.Error) + if one.Result != nil { + valid = append(valid, *one.Result) + } else { + details := storageDetailsFromLegacy(one.Legacy) + valid = append(valid, details) + } } if len(valid) == 0 { return nil } - output, err := formatStorageDetails(valid) + details, err := formatStorageDetails(valid) if err != nil { return err } + var output interface{} + switch c.out.Name() { + case "yaml", "json": + output = map[string]map[string]StorageInfo{"storage": details} + default: + output = details + } return c.out.Write(ctx, output) } @@ -93,7 +104,7 @@ // StorageAPI defines the API methods that the storage commands use. type StorageListAPI interface { Close() error - List() ([]params.StorageInfo, error) + List() ([]params.StorageDetailsResult, error) } func (c *ListCommand) getStorageListAPI() (StorageListAPI, error) { === modified file 'src/github.com/juju/juju/cmd/juju/storage/list_test.go' --- src/github.com/juju/juju/cmd/juju/storage/list_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/storage/list_test.go 2015-10-23 18:29:32 +0000 @@ -42,13 +42,12 @@ nil, // Default format is tabular ` -[Storage] -UNIT ID LOCATION STATUS PERSISTENT -postgresql/0 db-dir/1100 pending false -transcode/0 db-dir/1000 pending true -transcode/0 db-dir/1100 pending false -transcode/0 shared-fs/0 pending false -transcode/1 shared-fs/0 pending false +\[Storage\] +UNIT ID LOCATION STATUS MESSAGE +postgresql/0 db-dir/1100 hither attached +transcode/0 db-dir/1000 pending +transcode/0 shared-fs/0 there attached +transcode/1 shared-fs/0 here attached `[1:], "", @@ -60,61 +59,59 @@ c, []string{"--format", "yaml"}, ` -postgresql/0: - db-dir/1100: - storage: db-dir - kind: filesystem - status: pending - persistent: false -transcode/0: +storage: db-dir/1000: - storage: db-dir kind: block - status: pending - persistent: true + status: + current: pending + since: .* + persistent: false + attachments: + units: + transcode/0: {} db-dir/1100: - storage: db-dir + kind: block + status: + current: attached + since: .* + persistent: true + attachments: + units: + postgresql/0: + location: hither + shared-fs/0: kind: filesystem - status: pending - persistent: false - shared-fs/0: - storage: shared-fs - kind: unknown - status: pending - persistent: false -transcode/1: - shared-fs/0: - storage: shared-fs - kind: unknown - status: pending - persistent: false + status: + current: attached + since: .* + persistent: true + attachments: + units: + transcode/0: + location: there + transcode/1: + location: here `[1:], "", ) } func (s *ListSuite) TestListOwnerStorageIdSort(c *gc.C) { - s.mockAPI.lexicalChaos = true + s.mockAPI.includeErrors = true s.assertValidList( c, nil, // Default format is tabular ` -[Storage] -UNIT ID LOCATION STATUS PERSISTENT -postgresql/0 db-dir/1100 pending false -transcode/0 db-dir/1000 pending true -transcode/0 db-dir/1100 pending false -transcode/0 shared-fs/0 pending false -transcode/0 shared-fs/5 pending false -transcode/1 db-dir/1000 pending true -transcode/1 shared-fs/0 pending false +\[Storage\] +UNIT ID LOCATION STATUS MESSAGE +postgresql/0 db-dir/1100 hither attached +transcode/0 db-dir/1000 pending +transcode/0 shared-fs/0 there attached +transcode/1 shared-fs/0 here attached `[1:], - ` -error for storage-db-dir-1010 -error for test storage-db-dir-1010 -`[1:], + "error for storage-db-dir-1010\n", ) } @@ -123,160 +120,77 @@ c.Assert(err, jc.ErrorIsNil) obtainedErr := testing.Stderr(context) - c.Assert(obtainedErr, gc.Equals, expectedErr) + c.Assert(obtainedErr, gc.Matches, expectedErr) obtainedValid := testing.Stdout(context) - c.Assert(obtainedValid, gc.Equals, expectedValid) + c.Assert(obtainedValid, gc.Matches, expectedValid) } type mockListAPI struct { - lexicalChaos bool + includeErrors bool } func (s mockListAPI) Close() error { return nil } -func (s mockListAPI) List() ([]params.StorageInfo, error) { - result := []params.StorageInfo{} - result = append(result, getTestAttachments(s.lexicalChaos)...) - result = append(result, getTestInstances(s.lexicalChaos)...) - return result, nil -} - -func getTestAttachments(chaos bool) []params.StorageInfo { - results := []params.StorageInfo{{ - params.StorageDetails{ +func (s mockListAPI) List() ([]params.StorageDetailsResult, error) { + // postgresql/0 has "db-dir/1100" + // transcode/1 has "db-dir/1000" + // transcode/0 and transcode/1 share "shared-fs/0" + // + // there is also a storage instance "db-dir/1010" which + // returns an error when listed. + results := []params.StorageDetailsResult{{ + Legacy: params.LegacyStorageDetails{ + StorageTag: "storage-db-dir-1000", + OwnerTag: "unit-transcode-0", + UnitTag: "unit-transcode-0", + Kind: params.StorageKindBlock, + Status: "pending", + }, + }, { + Result: ¶ms.StorageDetails{ + StorageTag: "storage-db-dir-1100", + OwnerTag: "unit-postgresql-0", + Kind: params.StorageKindBlock, + Status: params.EntityStatus{ + Status: params.StatusAttached, + Since: &epoch, + }, + Persistent: true, + Attachments: map[string]params.StorageAttachmentDetails{ + "unit-postgresql-0": params.StorageAttachmentDetails{ + Location: "hither", + }, + }, + }, + }, { + Result: ¶ms.StorageDetails{ StorageTag: "storage-shared-fs-0", OwnerTag: "service-transcode", - UnitTag: "unit-transcode-0", - Kind: params.StorageKindBlock, - Location: "here", - Status: "attached", - }, nil}, { - params.StorageDetails{ - StorageTag: "storage-db-dir-1000", - OwnerTag: "unit-transcode-0", - UnitTag: "unit-transcode-0", - Kind: params.StorageKindUnknown, - Location: "there", - Status: "provisioned", + Kind: params.StorageKindFilesystem, + Status: params.EntityStatus{ + Status: params.StatusAttached, + Since: &epoch, + }, Persistent: true, - }, nil}} - - if chaos { - last := params.StorageInfo{ - params.StorageDetails{ - StorageTag: "storage-shared-fs-5", - OwnerTag: "service-transcode", - UnitTag: "unit-transcode-0", - Kind: params.StorageKindUnknown, - Location: "nowhere", - Status: "pending", - }, nil} - second := params.StorageInfo{ - params.StorageDetails{ - StorageTag: "storage-db-dir-1010", - OwnerTag: "unit-transcode-1", - UnitTag: "unit-transcode-1", - Kind: params.StorageKindBlock, - Location: "", - Status: "pending", - }, ¶ms.Error{Message: "error for storage-db-dir-1010"}} - first := params.StorageInfo{ - params.StorageDetails{ - StorageTag: "storage-db-dir-1000", - OwnerTag: "unit-transcode-1", - UnitTag: "unit-transcode-1", - Kind: params.StorageKindFilesystem, - Status: "attached", - Persistent: true, - }, nil} - results = append(results, last) - results = append(results, second) - results = append(results, first) - } - return results -} - -func getTestInstances(chaos bool) []params.StorageInfo { - - results := []params.StorageInfo{ - { - params.StorageDetails{ - StorageTag: "storage-shared-fs-0", - OwnerTag: "service-transcode", - UnitTag: "unit-transcode-0", - Kind: params.StorageKindUnknown, - Status: "pending", - }, nil}, - { - params.StorageDetails{ - StorageTag: "storage-shared-fs-0", - OwnerTag: "service-transcode", - UnitTag: "unit-transcode-1", - Kind: params.StorageKindUnknown, - Status: "pending", - }, nil}, - { - params.StorageDetails{ - StorageTag: "storage-db-dir-1100", - UnitTag: "unit-postgresql-0", - Kind: params.StorageKindFilesystem, - Status: "pending", - }, nil}, - { - params.StorageDetails{ - StorageTag: "storage-db-dir-1100", - UnitTag: "unit-transcode-0", - Kind: params.StorageKindFilesystem, - Status: "pending", - }, nil}, - { - params.StorageDetails{ - StorageTag: "storage-db-dir-1000", - OwnerTag: "unit-transcode-0", - UnitTag: "unit-transcode-0", - Kind: params.StorageKindBlock, - Status: "pending", - Persistent: true, - }, nil}} - - if chaos { - last := params.StorageInfo{ - params.StorageDetails{ - StorageTag: "storage-shared-fs-5", - OwnerTag: "service-transcode", - UnitTag: "unit-transcode-0", - Kind: params.StorageKindUnknown, - Status: "pending", - }, nil} - second := params.StorageInfo{ - params.StorageDetails{ - StorageTag: "storage-db-dir-1010", - UnitTag: "unit-transcode-1", - Kind: params.StorageKindBlock, - Status: "pending", - }, ¶ms.Error{Message: "error for test storage-db-dir-1010"}} - first := params.StorageInfo{ - params.StorageDetails{ - StorageTag: "storage-db-dir-1000", - UnitTag: "unit-transcode-1", - Kind: params.StorageKindFilesystem, - Status: "pending", - Persistent: true, - }, nil} - zero := params.StorageInfo{ - params.StorageDetails{ - StorageTag: "storage-db-dir-1100", - UnitTag: "unit-postgresql-0", - Kind: params.StorageKindFilesystem, - Status: "pending", - }, nil} - results = append(results, last) - results = append(results, second) - results = append(results, zero) - results = append(results, first) - } - return results + Attachments: map[string]params.StorageAttachmentDetails{ + "unit-transcode-0": params.StorageAttachmentDetails{ + Location: "there", + }, + "unit-transcode-1": params.StorageAttachmentDetails{ + Location: "here", + }, + }, + }, + }} + if s.includeErrors { + results = append(results, params.StorageDetailsResult{ + Error: ¶ms.Error{ + Message: "error for storage-db-dir-1010", + }, + }) + } + return results, nil } === modified file 'src/github.com/juju/juju/cmd/juju/storage/listformatters.go' --- src/github.com/juju/juju/cmd/juju/storage/listformatters.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/storage/listformatters.go 2015-10-23 18:29:32 +0000 @@ -16,7 +16,7 @@ // formatListTabular returns a tabular summary of storage instances. func formatListTabular(value interface{}) ([]byte, error) { - storageInfo, ok := value.(map[string]map[string]StorageInfo) + storageInfo, ok := value.(map[string]StorageInfo) if !ok { return nil, errors.Errorf("expected value of type %T, got %T", storageInfo, value) } @@ -30,27 +30,60 @@ fmt.Fprintln(tw) } p("[Storage]") - p("UNIT\tID\tLOCATION\tSTATUS\tPERSISTENT") + p("UNIT\tID\tLOCATION\tSTATUS\tMESSAGE") + + byUnit := make(map[string]map[string]storageAttachmentInfo) + for storageId, storageInfo := range storageInfo { + if storageInfo.Attachments == nil { + byStorage := byUnit[""] + if byStorage == nil { + byStorage = make(map[string]storageAttachmentInfo) + byUnit[""] = byStorage + } + byStorage[storageId] = storageAttachmentInfo{ + storageId: storageId, + kind: storageInfo.Kind, + persistent: storageInfo.Persistent, + status: storageInfo.Status, + } + continue + } + for unitId, a := range storageInfo.Attachments.Units { + byStorage := byUnit[unitId] + if byStorage == nil { + byStorage = make(map[string]storageAttachmentInfo) + byUnit[unitId] = byStorage + } + byStorage[storageId] = storageAttachmentInfo{ + storageId: storageId, + unitId: unitId, + kind: storageInfo.Kind, + persistent: storageInfo.Persistent, + location: a.Location, + status: storageInfo.Status, + } + } + } // First sort by units units := make([]string, 0, len(storageInfo)) - for order := range storageInfo { - units = append(units, order) + for unit := range byUnit { + units = append(units, unit) } - sort.Strings(bySuffixNaturally(units)) + sort.Strings(slashSeparatedIds(units)) + for _, unit := range units { - all := storageInfo[unit] - // Then sort by storage ids - storageIds := make([]string, 0, len(all)) - for anId := range all { - storageIds = append(storageIds, anId) + byStorage := byUnit[unit] + storageIds := make([]string, 0, len(byStorage)) + for storageId := range byStorage { + storageIds = append(storageIds, storageId) } - sort.Strings(bySuffixNaturally(storageIds)) + sort.Strings(slashSeparatedIds(storageIds)) for _, storageId := range storageIds { - info := all[storageId] - p(unit, storageId, info.Location, info.Status, info.Persistent) + info := byStorage[storageId] + p(info.unitId, info.storageId, info.location, info.status.Current, info.status.Message) } } tw.Flush() @@ -58,42 +91,85 @@ return out.Bytes(), nil } -type bySuffixNaturally []string - -func (s bySuffixNaturally) Len() int { +type storageAttachmentInfo struct { + storageId string + unitId string + kind string + persistent bool + location string + status EntityStatus +} + +type slashSeparatedIds []string + +func (s slashSeparatedIds) Len() int { return len(s) } -func (s bySuffixNaturally) Swap(a, b int) { +func (s slashSeparatedIds) Swap(a, b int) { s[a], s[b] = s[b], s[a] } -func (s bySuffixNaturally) Less(a, b int) bool { - sa := strings.SplitN(s[a], "/", 2) - sb := strings.SplitN(s[b], "/", 2) +func (s slashSeparatedIds) Less(a, b int) bool { + return compareSlashSeparated(s[a], s[b]) == -1 +} + +// compareSlashSeparated compares a with b, first the string before +// "/", and then the integer or string after. Empty strings are sorted +// after all others. +func compareSlashSeparated(a, b string) int { + switch { + case a == "" && b == "": + return 0 + case a == "": + return 1 + case b == "": + return -1 + } + + sa := strings.SplitN(a, "/", 2) + sb := strings.SplitN(b, "/", 2) if sa[0] < sb[0] { - return true - } - altReturn := sa[0] == sb[0] && sa[1] < sb[1] + return -1 + } + if sa[0] > sb[0] { + return 1 + } getInt := func(suffix string) (bool, int) { num, err := strconv.Atoi(suffix) if err != nil { - // It's possible that we are not looking at numeric suffix - logger.Infof("parsing a non-numeric %v: %v", suffix, err) return false, 0 } - fmt.Printf("parsing a non-numeric %v: %v", suffix, err) return true, num } naIsNumeric, na := getInt(sa[1]) if !naIsNumeric { - return altReturn + return compareStrings(sa[1], sb[1]) } nbIsNumeric, nb := getInt(sb[1]) if !nbIsNumeric { - return altReturn - } - return sa[0] == sb[0] && na < nb + return compareStrings(sa[1], sb[1]) + } + + switch { + case na < nb: + return -1 + case na == nb: + return 0 + } + return 1 +} + +// compareStrings does what strings.Compare does, but without using +// strings.Compare as it does not exist in Go 1.2. +func compareStrings(a, b string) int { + if a == b { + return 0 + } + if a < b { + return -1 + } + return 1 } === modified file 'src/github.com/juju/juju/cmd/juju/storage/package_test.go' --- src/github.com/juju/juju/cmd/juju/storage/package_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/storage/package_test.go 2015-10-23 18:29:32 +0000 @@ -7,6 +7,7 @@ "os" "testing" + "github.com/juju/cmd" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" @@ -23,13 +24,13 @@ type BaseStorageSuite struct { jujutesting.FakeJujuHomeSuite - command *storage.Command + command cmd.Command } func (s *BaseStorageSuite) SetUpTest(c *gc.C) { s.FakeJujuHomeSuite.SetUpTest(c) - s.command = storage.NewSuperCommand().(*storage.Command) + s.command = storage.NewSuperCommand() } func (s *BaseStorageSuite) TearDownTest(c *gc.C) { === modified file 'src/github.com/juju/juju/cmd/juju/storage/pool.go' --- src/github.com/juju/juju/cmd/juju/storage/pool.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/storage/pool.go 2015-10-23 18:29:32 +0000 @@ -21,16 +21,15 @@ // NewPoolSuperCommand creates the storage pool super subcommand and // registers the subcommands that it supports. func NewPoolSuperCommand() cmd.Command { - poolcmd := Command{ - SuperCommand: *jujucmd.NewSubSuperCommand(cmd.SuperCommandParams{ - Name: "pool", - Doc: poolCmdDoc, - UsagePrefix: "juju storage", - Purpose: poolCmdPurpose, - })} + poolcmd := jujucmd.NewSubSuperCommand(cmd.SuperCommandParams{ + Name: "pool", + Doc: poolCmdDoc, + UsagePrefix: "juju storage", + Purpose: poolCmdPurpose, + }) poolcmd.Register(envcmd.Wrap(&PoolListCommand{})) poolcmd.Register(envcmd.Wrap(&PoolCreateCommand{})) - return &poolcmd + return poolcmd } // PoolCommandBase is a helper base structure for pool commands. === modified file 'src/github.com/juju/juju/cmd/juju/storage/pool_test.go' --- src/github.com/juju/juju/cmd/juju/storage/pool_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/storage/pool_test.go 2015-10-23 18:29:32 +0000 @@ -22,6 +22,6 @@ var _ = gc.Suite(&poolSuite{}) func (s *poolSuite) TestPoolHelp(c *gc.C) { - s.command = storage.NewPoolSuperCommand().(*storage.Command) + s.command = storage.NewPoolSuperCommand() s.assertHelp(c, expectedPoolCommmandNames) } === modified file 'src/github.com/juju/juju/cmd/juju/storage/show.go' --- src/github.com/juju/juju/cmd/juju/storage/show.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/storage/show.go 2015-10-23 18:29:32 +0000 @@ -71,11 +71,30 @@ if err != nil { return err } - found, err := api.Show(tags) + + results, err := api.Show(tags) if err != nil { return err } - output, err := formatStorageDetails(found) + + var errs params.ErrorResults + var valid []params.StorageDetails + for _, result := range results { + if result.Error != nil { + errs.Results = append(errs.Results, params.ErrorResult{result.Error}) + continue + } + if result.Result != nil { + valid = append(valid, *result.Result) + } else { + valid = append(valid, storageDetailsFromLegacy(result.Legacy)) + } + } + if len(errs.Results) > 0 { + return errs.Combine() + } + + output, err := formatStorageDetails(valid) if err != nil { return err } @@ -100,7 +119,7 @@ // StorageAPI defines the API methods that the storage commands use. type StorageShowAPI interface { Close() error - Show(tags []names.StorageTag) ([]params.StorageDetails, error) + Show(tags []names.StorageTag) ([]params.StorageDetailsResult, error) } func (c *ShowCommand) getStorageShowAPI() (StorageShowAPI, error) { === modified file 'src/github.com/juju/juju/cmd/juju/storage/show_test.go' --- src/github.com/juju/juju/cmd/juju/storage/show_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/storage/show_test.go 2015-10-23 18:29:32 +0000 @@ -5,6 +5,7 @@ import ( "strings" + "time" "github.com/juju/cmd" "github.com/juju/names" @@ -18,6 +19,11 @@ "github.com/juju/juju/testing" ) +// epoch is the time we use for "since" in statuses. The time +// is always shown as a local time, so we override the local +// location to be UTC+8. +var epoch = time.Unix(0, 0) + type ShowSuite struct { SubStorageSuite mockAPI *mockShowAPI @@ -32,6 +38,7 @@ s.PatchValue(storage.GetStorageShowAPI, func(c *storage.ShowCommand) (storage.StorageShowAPI, error) { return s.mockAPI, nil }) + s.PatchValue(&time.Local, time.FixedZone("Australia/Perth", 3600*8)) } @@ -56,19 +63,20 @@ []string{"shared-fs/0"}, // Default format is yaml ` -postgresql/0: - shared-fs/0: - storage: shared-fs - kind: block - status: pending - persistent: false -transcode/0: - shared-fs/0: - storage: shared-fs - kind: filesystem - status: attached - persistent: false - location: a location +shared-fs/0: + kind: filesystem + status: + current: attached + since: 01 Jan 1970 08:00:00\+08:00 + persistent: true + attachments: + units: + transcode/0: + machine: \"1\" + location: a location + transcode/1: + machine: \"2\" + location: b location `[1:], ) } @@ -82,7 +90,7 @@ s.assertValidShow( c, []string{"shared-fs/0", "--format", "json"}, - `{"postgresql/0":{"shared-fs/0":{"storage":"shared-fs","kind":"block","status":"pending","persistent":false}},"transcode/0":{"shared-fs/0":{"storage":"shared-fs","kind":"filesystem","status":"attached","persistent":false,"location":"a location"}}} + `{"shared-fs/0":{"kind":"filesystem","status":{"current":"attached","since":"01 Jan 1970 08:00:00\+08:00"},"persistent":true,"attachments":{"units":{"transcode/0":{"machine":"1","location":"a location"},"transcode/1":{"machine":"2","location":"b location"}}}}} `, ) } @@ -92,24 +100,29 @@ c, []string{"shared-fs/0", "db-dir/1000"}, ` -postgresql/0: - db-dir/1000: - storage: db-dir - kind: block - status: pending - persistent: true - shared-fs/0: - storage: shared-fs - kind: block - status: pending - persistent: false -transcode/0: - shared-fs/0: - storage: shared-fs - kind: filesystem - status: attached - persistent: false - location: a location +db-dir/1000: + kind: block + status: + current: pending + since: .* + persistent: true + attachments: + units: + postgresql/0: {} +shared-fs/0: + kind: filesystem + status: + current: attached + since: 01 Jan 1970 08:00:00\+08:00 + persistent: true + attachments: + units: + transcode/0: + machine: \"1\" + location: a location + transcode/1: + machine: \"2\" + location: b location `[1:], ) } @@ -119,7 +132,7 @@ c.Assert(err, jc.ErrorIsNil) obtained := testing.Stdout(context) - c.Assert(obtained, gc.Equals, expected) + c.Assert(obtained, gc.Matches, expected) } type mockShowAPI struct { @@ -130,32 +143,43 @@ return nil } -func (s mockShowAPI) Show(tags []names.StorageTag) ([]params.StorageDetails, error) { +func (s mockShowAPI) Show(tags []names.StorageTag) ([]params.StorageDetailsResult, error) { if s.noMatch { return nil, nil } - all := make([]params.StorageDetails, len(tags)) + all := make([]params.StorageDetailsResult, len(tags)) for i, tag := range tags { - all[i] = params.StorageDetails{ - StorageTag: tag.String(), - UnitTag: "unit-postgresql-0", - Kind: params.StorageKindBlock, - Status: "pending", - } - if i == 1 { - all[i].Persistent = true - } - } - for _, tag := range tags { if strings.Contains(tag.String(), "shared") { - all = append(all, params.StorageDetails{ + all[i].Result = ¶ms.StorageDetails{ StorageTag: tag.String(), - OwnerTag: "unit-transcode-0", - UnitTag: "unit-transcode-0", + OwnerTag: "service-transcode", Kind: params.StorageKindFilesystem, - Location: "a location", - Status: "attached", - }) + Status: params.EntityStatus{ + Status: "attached", + Since: &epoch, + }, + Persistent: true, + Attachments: map[string]params.StorageAttachmentDetails{ + "unit-transcode-0": params.StorageAttachmentDetails{ + MachineTag: "machine-1", + Location: "a location", + }, + "unit-transcode-1": params.StorageAttachmentDetails{ + MachineTag: "machine-2", + Location: "b location", + }, + }, + } + } else { + all[i].Legacy = params.LegacyStorageDetails{ + StorageTag: tag.String(), + UnitTag: "unit-postgresql-0", + Kind: params.StorageKindBlock, + Status: "pending", + } + if i == 1 { + all[i].Legacy.Persistent = true + } } } return all, nil === modified file 'src/github.com/juju/juju/cmd/juju/storage/storage.go' --- src/github.com/juju/juju/cmd/juju/storage/storage.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/storage/storage.go 2015-10-23 18:29:32 +0000 @@ -4,6 +4,8 @@ package storage import ( + "time" + "github.com/juju/cmd" "github.com/juju/errors" "github.com/juju/loggo" @@ -12,6 +14,7 @@ "github.com/juju/juju/api/storage" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/common" ) var logger = loggo.GetLogger("juju.cmd.juju.storage") @@ -23,28 +26,22 @@ const storageCmdPurpose = "manage storage instances" -// Command is the top-level command wrapping all storage functionality. -type Command struct { - cmd.SuperCommand -} - // NewSuperCommand creates the storage supercommand and // registers the subcommands that it supports. func NewSuperCommand() cmd.Command { - storagecmd := Command{ - SuperCommand: *cmd.NewSuperCommand( - cmd.SuperCommandParams{ - Name: "storage", - Doc: storageCmdDoc, - UsagePrefix: "juju", - Purpose: storageCmdPurpose, - })} + storagecmd := cmd.NewSuperCommand( + cmd.SuperCommandParams{ + Name: "storage", + Doc: storageCmdDoc, + UsagePrefix: "juju", + Purpose: storageCmdPurpose, + }) storagecmd.Register(envcmd.Wrap(&ShowCommand{})) storagecmd.Register(envcmd.Wrap(&ListCommand{})) storagecmd.Register(envcmd.Wrap(&AddCommand{})) storagecmd.Register(NewPoolSuperCommand()) storagecmd.Register(NewVolumeSuperCommand()) - return &storagecmd + return storagecmd } // StorageCommandBase is a helper base structure that has a method to get the @@ -65,48 +62,115 @@ // StorageInfo defines the serialization behaviour of the storage information. type StorageInfo struct { - StorageName string `yaml:"storage" json:"storage"` - Kind string `yaml:"kind" json:"kind"` - Status string `yaml:"status,omitempty" json:"status,omitempty"` - Persistent bool `yaml:"persistent" json:"persistent"` - Location string `yaml:"location,omitempty" json:"location,omitempty"` -} - -// formatStorageDetails takes a set of StorageDetail and creates a -// mapping keyed on unit and storage id. -func formatStorageDetails(storages []params.StorageDetails) (map[string]map[string]StorageInfo, error) { + Kind string `yaml:"kind" json:"kind"` + Status EntityStatus `yaml:"status" json:"status"` + Persistent bool `yaml:"persistent" json:"persistent"` + Attachments *StorageAttachments `yaml:"attachments" json:"attachments"` +} + +// StorageAttachments contains details about all attachments to a storage +// instance. +type StorageAttachments struct { + // Units is a mapping from unit ID to unit storage attachment details. + Units map[string]UnitStorageAttachment `yaml:"units" json:"units"` +} + +// UnitStorageAttachment contains details of a unit storage attachment. +type UnitStorageAttachment struct { + // MachineId is the ID of the machine that the unit is assigned to. + // + // This is omitempty to cater for legacy results, where the machine + // information is not available. + MachineId string `yaml:"machine,omitempty" json:"machine,omitempty"` + + // Location is the location of the storage attachment. + Location string `yaml:"location,omitempty" json:"location,omitempty"` + + // TODO(axw) per-unit status when we have it in state. +} + +// formatStorageDetails takes a set of StorageDetail and +// creates a mapping from storage ID to storage details. +func formatStorageDetails(storages []params.StorageDetails) (map[string]StorageInfo, error) { if len(storages) == 0 { return nil, nil } - output := make(map[string]map[string]StorageInfo) - for _, one := range storages { - storageTag, err := names.ParseStorageTag(one.StorageTag) - if err != nil { - return nil, errors.Annotate(err, "invalid storage tag") - } - unitTag, err := names.ParseTag(one.UnitTag) - if err != nil { - return nil, errors.Annotate(err, "invalid unit tag") - } - - storageName, err := names.StorageName(storageTag.Id()) - if err != nil { - panic(err) // impossible - } - si := StorageInfo{ - StorageName: storageName, - Kind: one.Kind.String(), - Status: one.Status, - Location: one.Location, - Persistent: one.Persistent, - } - unit := unitTag.Id() - unitColl, ok := output[unit] - if !ok { - unitColl = map[string]StorageInfo{} - output[unit] = unitColl - } - unitColl[storageTag.Id()] = si + output := make(map[string]StorageInfo) + for _, details := range storages { + storageTag, storageInfo, err := createStorageInfo(details) + if err != nil { + return nil, errors.Trace(err) + } + output[storageTag.Id()] = storageInfo } return output, nil } + +func createStorageInfo(details params.StorageDetails) (names.StorageTag, StorageInfo, error) { + storageTag, err := names.ParseStorageTag(details.StorageTag) + if err != nil { + return names.StorageTag{}, StorageInfo{}, errors.Trace(err) + } + + info := StorageInfo{ + Kind: details.Kind.String(), + Status: EntityStatus{ + details.Status.Status, + details.Status.Info, + // TODO(axw) we should support formatting as ISO time + common.FormatTime(details.Status.Since, false), + }, + Persistent: details.Persistent, + } + + if len(details.Attachments) > 0 { + unitStorageAttachments := make(map[string]UnitStorageAttachment) + for unitTagString, attachmentDetails := range details.Attachments { + unitTag, err := names.ParseUnitTag(unitTagString) + if err != nil { + return names.StorageTag{}, StorageInfo{}, errors.Trace(err) + } + var machineId string + if attachmentDetails.MachineTag != "" { + machineTag, err := names.ParseMachineTag(attachmentDetails.MachineTag) + if err != nil { + return names.StorageTag{}, StorageInfo{}, errors.Trace(err) + } + machineId = machineTag.Id() + } + unitStorageAttachments[unitTag.Id()] = UnitStorageAttachment{ + machineId, + attachmentDetails.Location, + } + } + info.Attachments = &StorageAttachments{unitStorageAttachments} + } + + return storageTag, info, nil +} + +func storageDetailsFromLegacy(legacy params.LegacyStorageDetails) params.StorageDetails { + nowUTC := time.Now().UTC() + details := params.StorageDetails{ + legacy.StorageTag, + legacy.OwnerTag, + legacy.Kind, + params.EntityStatus{ + Status: params.Status(legacy.Status), + Since: &nowUTC, + }, + legacy.Persistent, + nil, + } + if legacy.UnitTag != "" { + details.Attachments = map[string]params.StorageAttachmentDetails{ + legacy.UnitTag: params.StorageAttachmentDetails{ + legacy.StorageTag, + legacy.UnitTag, + "", // machine is unknown in legacy + legacy.Location, + }, + } + } + return details +} === modified file 'src/github.com/juju/juju/cmd/juju/storage/storage_test.go' --- src/github.com/juju/juju/cmd/juju/storage/storage_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/storage/storage_test.go 2015-10-23 18:29:32 +0000 @@ -25,6 +25,6 @@ var _ = gc.Suite(&storageSuite{}) func (s *storageSuite) TestHelp(c *gc.C) { - s.command = storage.NewSuperCommand().(*storage.Command) + s.command = storage.NewSuperCommand() s.assertHelp(c, expectedSubCommmandNames) } === modified file 'src/github.com/juju/juju/cmd/juju/storage/volume.go' --- src/github.com/juju/juju/cmd/juju/storage/volume.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/storage/volume.go 2015-10-23 18:29:32 +0000 @@ -11,6 +11,7 @@ "github.com/juju/juju/apiserver/params" jujucmd "github.com/juju/juju/cmd" "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/common" ) const volumeCmdDoc = ` @@ -23,15 +24,14 @@ // NewVolumeSuperCommand creates the storage volume super subcommand and // registers the subcommands that it supports. func NewVolumeSuperCommand() cmd.Command { - poolcmd := Command{ - SuperCommand: *jujucmd.NewSubSuperCommand(cmd.SuperCommandParams{ - Name: "volume", - Doc: volumeCmdDoc, - UsagePrefix: "juju storage", - Purpose: volumeCmdPurpose, - })} + poolcmd := jujucmd.NewSubSuperCommand(cmd.SuperCommandParams{ + Name: "volume", + Doc: volumeCmdDoc, + UsagePrefix: "juju storage", + Purpose: volumeCmdPurpose, + }) poolcmd.Register(envcmd.Wrap(&VolumeListCommand{})) - return &poolcmd + return poolcmd } // VolumeCommandBase is a helper base structure for volume commands. @@ -42,10 +42,17 @@ // VolumeInfo defines the serialization behaviour for storage volume. type VolumeInfo struct { // from params.Volume. This is provider-supplied unique volume id. - VolumeId string `yaml:"id" json:"id"` + ProviderVolumeId string `yaml:"provider-id,omitempty" json:"provider-id,omitempty"` + + // Storage is the ID of the storage instance that the volume is + // assigned to, if any. + Storage string `yaml:"storage,omitempty" json:"storage,omitempty"` + + // Attachments is the set of entities attached to the volume. + Attachments *VolumeAttachments `yaml:"attachments,omitempty" json:"attachments,omitempty"` // from params.Volume - HardwareId string `yaml:"hardwareid" json:"hardwareid"` + HardwareId string `yaml:"hardware-id,omitempty" json:"hardware-id,omitempty"` // from params.Volume Size uint64 `yaml:"size" json:"size"` @@ -53,38 +60,42 @@ // from params.Volume Persistent bool `yaml:"persistent" json:"persistent"` - // from params.VolumeAttachments + Status EntityStatus `yaml:"status,omitempty" json:"status,omitempty"` +} + +type EntityStatus struct { + Current params.Status `json:"current,omitempty" yaml:"current,omitempty"` + Message string `json:"message,omitempty" yaml:"message,omitempty"` + Since string `json:"since,omitempty" yaml:"since,omitempty"` +} + +type VolumeAttachments struct { + Machines map[string]MachineVolumeAttachment `yaml:"machines,omitempty" json:"machines,omitempty"` + Units map[string]UnitStorageAttachment `yaml:"units,omitempty" json:"units,omitempty"` +} + +type MachineVolumeAttachment struct { DeviceName string `yaml:"device,omitempty" json:"device,omitempty"` - - // from params.VolumeAttachments - ReadOnly bool `yaml:"read-only" json:"read-only"` - - // from params.Volume. This is juju volume id. - Volume string `yaml:"volume,omitempty" json:"volume,omitempty"` + DeviceLink string `yaml:"device-link,omitempty" json:"device-link,omitempty"` + BusAddress string `yaml:"bus-address,omitempty" json:"bus-address,omitempty"` + ReadOnly bool `yaml:"read-only" json:"read-only"` + // TODO(axw) add machine volume attachment status when we have it } // convertToVolumeInfo returns map of maps with volume info -// keyed first on machine_id and then on volume_id. -func convertToVolumeInfo(all []params.VolumeItem) (map[string]map[string]map[string]VolumeInfo, error) { - result := map[string]map[string]map[string]VolumeInfo{} +// keyed first on machine ID and then on volume ID. +func convertToVolumeInfo(all []params.VolumeDetailsResult) (map[string]VolumeInfo, error) { + result := make(map[string]VolumeInfo) for _, one := range all { - if err := convertVolumeItem(one, result); err != nil { + volumeTag, info, err := createVolumeInfo(one) + if err != nil { return nil, errors.Trace(err) } + result[volumeTag.Id()] = info } return result, nil } -func convertVolumeItem(item params.VolumeItem, all map[string]map[string]map[string]VolumeInfo) error { - if len(item.Attachments) != 0 { - // add info for volume attachments - return convertVolumeAttachments(item, all) - } - unattached, unit, storage := createInfo(item.Volume) - addOneToAll("unattached", unit, storage, unattached, all) - return nil -} - var idFromTag = func(s string) (string, error) { tag, err := names.ParseTag(s) if err != nil { @@ -93,50 +104,104 @@ return tag.Id(), nil } -func convertVolumeAttachments(item params.VolumeItem, all map[string]map[string]map[string]VolumeInfo) error { - for _, one := range item.Attachments { - machine, err := idFromTag(one.MachineTag) +func createVolumeInfo(result params.VolumeDetailsResult) (names.VolumeTag, VolumeInfo, error) { + details := result.Details + if details == nil { + details = volumeDetailsFromLegacy(result) + } + + volumeTag, err := names.ParseVolumeTag(details.VolumeTag) + if err != nil { + return names.VolumeTag{}, VolumeInfo{}, errors.Trace(err) + } + + var info VolumeInfo + info.ProviderVolumeId = details.Info.VolumeId + info.HardwareId = details.Info.HardwareId + info.Size = details.Info.Size + info.Persistent = details.Info.Persistent + info.Status = EntityStatus{ + details.Status.Status, + details.Status.Info, + // TODO(axw) we should support formatting as ISO time + common.FormatTime(details.Status.Since, false), + } + + if len(details.MachineAttachments) > 0 { + machineAttachments := make(map[string]MachineVolumeAttachment) + for machineTag, attachment := range details.MachineAttachments { + machineId, err := idFromTag(machineTag) + if err != nil { + return names.VolumeTag{}, VolumeInfo{}, errors.Trace(err) + } + machineAttachments[machineId] = MachineVolumeAttachment{ + attachment.DeviceName, + attachment.DeviceLink, + attachment.BusAddress, + attachment.ReadOnly, + } + } + info.Attachments = &VolumeAttachments{ + Machines: machineAttachments, + } + } + + if details.Storage != nil { + storageTag, storageInfo, err := createStorageInfo(*details.Storage) if err != nil { - return errors.Trace(err) - } - info, unit, storage := createInfo(item.Volume) - info.DeviceName = one.Info.DeviceName - info.ReadOnly = one.Info.ReadOnly - - addOneToAll(machine, unit, storage, info, all) - } - return nil -} - -func addOneToAll(machineId, unitId, storageId string, item VolumeInfo, all map[string]map[string]map[string]VolumeInfo) { - machineVolumes, ok := all[machineId] - if !ok { - machineVolumes = map[string]map[string]VolumeInfo{} - all[machineId] = machineVolumes - } - unitVolumes, ok := machineVolumes[unitId] - if !ok { - unitVolumes = map[string]VolumeInfo{} - machineVolumes[unitId] = unitVolumes - } - unitVolumes[storageId] = item -} - -func createInfo(volume params.VolumeInstance) (info VolumeInfo, unit, storage string) { - info.VolumeId = volume.VolumeId - info.HardwareId = volume.HardwareId - info.Size = volume.Size - info.Persistent = volume.Persistent - - if v, err := idFromTag(volume.VolumeTag); err == nil { - info.Volume = v - } - var err error - if storage, err = idFromTag(volume.StorageTag); err != nil { - storage = "unassigned" - } - if unit, err = idFromTag(volume.UnitTag); err != nil { - unit = "unattached" - } - return + return names.VolumeTag{}, VolumeInfo{}, errors.Trace(err) + } + info.Storage = storageTag.Id() + if storageInfo.Attachments != nil { + info.Attachments.Units = storageInfo.Attachments.Units + } + } + + return volumeTag, info, nil +} + +// volumeDetailsFromLegacy converts from legacy data structures +// to params.VolumeDetails. This exists only for backwards- +// compatibility. Please think long and hard before changing it. +func volumeDetailsFromLegacy(result params.VolumeDetailsResult) *params.VolumeDetails { + details := ¶ms.VolumeDetails{ + VolumeTag: result.LegacyVolume.VolumeTag, + Status: result.LegacyVolume.Status, + } + details.Info.VolumeId = result.LegacyVolume.VolumeId + details.Info.HardwareId = result.LegacyVolume.HardwareId + details.Info.Size = result.LegacyVolume.Size + details.Info.Persistent = result.LegacyVolume.Persistent + if len(result.LegacyAttachments) > 0 { + attachments := make(map[string]params.VolumeAttachmentInfo) + for _, attachment := range result.LegacyAttachments { + attachments[attachment.MachineTag] = attachment.Info + } + details.MachineAttachments = attachments + } + if result.LegacyVolume.StorageTag != "" { + details.Storage = ¶ms.StorageDetails{ + StorageTag: result.LegacyVolume.StorageTag, + Status: details.Status, + } + if result.LegacyVolume.UnitTag != "" { + // Servers with legacy storage do not support shared + // storage, so there will only be one attachment, and + // the owner is always a unit. + details.Storage.OwnerTag = result.LegacyVolume.UnitTag + if len(result.LegacyAttachments) == 1 { + details.Storage.Attachments = map[string]params.StorageAttachmentDetails{ + result.LegacyVolume.UnitTag: params.StorageAttachmentDetails{ + StorageTag: result.LegacyVolume.StorageTag, + UnitTag: result.LegacyVolume.UnitTag, + MachineTag: result.LegacyAttachments[0].MachineTag, + // Don't set Location, because we can't infer that + // from the legacy volume details. + Location: "", + }, + } + } + } + } + return details } === modified file 'src/github.com/juju/juju/cmd/juju/storage/volume_test.go' --- src/github.com/juju/juju/cmd/juju/storage/volume_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/storage/volume_test.go 2015-10-23 18:29:32 +0000 @@ -21,6 +21,6 @@ var _ = gc.Suite(&volumeSuite{}) func (s *volumeSuite) TestVolumeHelp(c *gc.C) { - s.command = storage.NewVolumeSuperCommand().(*storage.Command) + s.command = storage.NewVolumeSuperCommand() s.assertHelp(c, expectedVolumeCommmandNames) } === modified file 'src/github.com/juju/juju/cmd/juju/storage/volumelist.go' --- src/github.com/juju/juju/cmd/juju/storage/volumelist.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/storage/volumelist.go 2015-10-23 18:29:32 +0000 @@ -71,7 +71,7 @@ return err } // filter out valid output, if any - var valid []params.VolumeItem + var valid []params.VolumeDetailsResult for _, one := range found { if one.Error == nil { valid = append(valid, one) @@ -83,10 +83,19 @@ if len(valid) == 0 { return nil } - output, err := convertToVolumeInfo(valid) + + info, err := convertToVolumeInfo(valid) if err != nil { return err } + + var output interface{} + switch c.out.Name() { + case "json", "yaml": + output = map[string]map[string]VolumeInfo{"volumes": info} + default: + output = info + } return c.out.Write(ctx, output) } @@ -95,7 +104,7 @@ // VolumeListAPI defines the API methods that the volume list command use. type VolumeListAPI interface { Close() error - ListVolumes(machines []string) ([]params.VolumeItem, error) + ListVolumes(machines []string) ([]params.VolumeDetailsResult, error) } func (c *VolumeListCommand) getVolumeListAPI() (VolumeListAPI, error) { === modified file 'src/github.com/juju/juju/cmd/juju/storage/volumelist_test.go' --- src/github.com/juju/juju/cmd/juju/storage/volumelist_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/storage/volumelist_test.go 2015-10-23 18:29:32 +0000 @@ -4,19 +4,15 @@ package storage_test import ( - "bytes" "encoding/json" + "time" "github.com/juju/cmd" "github.com/juju/errors" - "github.com/juju/names" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" goyaml "gopkg.in/yaml.v1" - "fmt" - - "github.com/juju/juju/apiserver/common" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/cmd/envcmd" "github.com/juju/juju/cmd/juju/storage" @@ -33,7 +29,7 @@ func (s *volumeListSuite) SetUpTest(c *gc.C) { s.SubStorageSuite.SetUpTest(c) - s.mockAPI = &mockVolumeListAPI{fillDeviceName: true, addErrItem: true} + s.mockAPI = &mockVolumeListAPI{} s.PatchValue(storage.GetVolumeListAPI, func(c *storage.VolumeListCommand) (storage.VolumeListAPI, error) { return s.mockAPI, nil @@ -41,47 +37,46 @@ } func (s *volumeListSuite) TestVolumeListEmpty(c *gc.C) { - s.mockAPI.listEmpty = true + s.mockAPI.listVolumes = func([]string) ([]params.VolumeDetailsResult, error) { + return nil, nil + } s.assertValidList( c, []string{"--format", "yaml"}, "", - "", ) } func (s *volumeListSuite) TestVolumeListError(c *gc.C) { - s.mockAPI.errOut = "just my luck" - + s.mockAPI.listVolumes = func([]string) ([]params.VolumeDetailsResult, error) { + return nil, errors.New("just my luck") + } context, err := runVolumeList(c, "--format", "yaml") - c.Assert(errors.Cause(err), gc.ErrorMatches, s.mockAPI.errOut) + c.Assert(errors.Cause(err), gc.ErrorMatches, "just my luck") s.assertUserFacingOutput(c, context, "", "") } -func (s *volumeListSuite) TestVolumeListAll(c *gc.C) { - s.mockAPI.listAll = true - s.assertUnmarshalledOutput( +func (s *volumeListSuite) TestVolumeListArgs(c *gc.C) { + var called bool + expectedArgs := []string{"a", "b", "c"} + s.mockAPI.listVolumes = func(arg []string) ([]params.VolumeDetailsResult, error) { + c.Assert(arg, jc.DeepEquals, expectedArgs) + called = true + return nil, nil + } + s.assertValidList( c, - goyaml.Unmarshal, - // mock will ignore any value here, as listAll flag above has precedence + append([]string{"--format", "yaml"}, expectedArgs...), "", - "--format", "yaml") + ) + c.Assert(called, jc.IsTrue) } func (s *volumeListSuite) TestVolumeListYaml(c *gc.C) { s.assertUnmarshalledOutput( c, goyaml.Unmarshal, - "2", - "--format", "yaml") -} - -func (s *volumeListSuite) TestVolumeListYamlNoDeviceName(c *gc.C) { - s.mockAPI.fillDeviceName = false - s.assertUnmarshalledOutput( - c, - goyaml.Unmarshal, - "2", + "", // no error "--format", "yaml") } @@ -89,134 +84,93 @@ s.assertUnmarshalledOutput( c, json.Unmarshal, - "2", + "", // no error "--format", "json") } +func (s *volumeListSuite) TestVolumeListWithErrorResults(c *gc.C) { + s.mockAPI.listVolumes = func([]string) ([]params.VolumeDetailsResult, error) { + results, _ := mockVolumeListAPI{}.ListVolumes(nil) + results = append(results, params.VolumeDetailsResult{ + Error: ¶ms.Error{Message: "bad"}, + }) + results = append(results, params.VolumeDetailsResult{ + Error: ¶ms.Error{Message: "ness"}, + }) + return results, nil + } + // we should see the error in stderr, but it should not + // otherwise affect the rendering of valid results. + s.assertUnmarshalledOutput(c, json.Unmarshal, "bad\nness\n", "--format", "json") + s.assertUnmarshalledOutput(c, goyaml.Unmarshal, "bad\nness\n", "--format", "yaml") +} + +var expectedVolumeListTabular = ` +MACHINE UNIT STORAGE ID PROVIDER-ID DEVICE SIZE STATE MESSAGE +0 abc/0 db-dir/1000 0 provider-supplied-volume-0 sda 1.0GiB destroying +0 abc/0 db-dir/1001 0/0 provider-supplied-volume-0-0 loop0 512MiB attached +0 transcode/0 shared-fs/0 4 provider-supplied-volume-4 xvdf2 1.0GiB attached +0 1 provider-supplied-volume-1 2.0GiB attaching failed to attach, will retry +1 transcode/1 shared-fs/0 4 provider-supplied-volume-4 xvdf3 1.0GiB attached +1 2 provider-supplied-volume-2 xvdf1 3.0MiB attached +1 3 42MiB pending + +`[1:] + func (s *volumeListSuite) TestVolumeListTabular(c *gc.C) { - s.assertValidList( - c, - []string{"2"}, - // Default format is tabular - ` -MACHINE UNIT STORAGE DEVICE VOLUME ID SIZE -2 postgresql/0 shared-fs/0 testdevice 0/1 provider-supplied-0/1 1.0GiB -2 unattached shared-fs/0 testdevice 0/abc/0/88 provider-supplied-0/abc/0/88 1.0GiB - -`[1:], - ` -volume item error -`[1:], - ) -} - -func (s *volumeListSuite) TestVolumeListTabularSort(c *gc.C) { - s.assertValidList( - c, - []string{"2", "3"}, - // Default format is tabular - ` -MACHINE UNIT STORAGE DEVICE VOLUME ID SIZE -2 postgresql/0 shared-fs/0 testdevice 0/1 provider-supplied-0/1 1.0GiB -2 unattached shared-fs/0 testdevice 0/abc/0/88 provider-supplied-0/abc/0/88 1.0GiB -3 postgresql/0 shared-fs/0 testdevice 0/1 provider-supplied-0/1 1.0GiB -3 unattached shared-fs/0 testdevice 0/abc/0/88 provider-supplied-0/abc/0/88 1.0GiB - -`[1:], - ` -volume item error -`[1:], - ) -} - -func (s *volumeListSuite) TestVolumeListTabularSortWithUnattached(c *gc.C) { - s.mockAPI.listAll = true - s.assertValidList( - c, - []string{"2", "3"}, - // Default format is tabular - ` -MACHINE UNIT STORAGE DEVICE VOLUME ID SIZE -25 postgresql/0 shared-fs/0 testdevice 0/1 provider-supplied-0/1 1.0GiB -25 unattached shared-fs/0 testdevice 0/abc/0/88 provider-supplied-0/abc/0/88 1.0GiB -42 postgresql/0 shared-fs/0 testdevice 0/1 provider-supplied-0/1 1.0GiB -42 unattached shared-fs/0 testdevice 0/abc/0/88 provider-supplied-0/abc/0/88 1.0GiB -unattached abc/0 db-dir/1000 3/4 provider-supplied-3/4 1.0GiB -unattached unattached unassigned 3/3 provider-supplied-3/3 1.0GiB - -`[1:], - ` -volume item error -`[1:], - ) -} - -func (s *volumeListSuite) assertUnmarshalledOutput(c *gc.C, unmarshall unmarshaller, machine string, args ...string) { - all := []string{machine} - context, err := runVolumeList(c, append(all, args...)...) - c.Assert(err, jc.ErrorIsNil) - var result map[string]map[string]map[string]storage.VolumeInfo - err = unmarshall(context.Stdout.(*bytes.Buffer).Bytes(), &result) - c.Assert(err, jc.ErrorIsNil) - expected := s.expect(c, []string{machine}) - // This comparison cannot rely on gc.DeepEquals as - // json.Unmarshal unmarshalls the number as a float64, - // rather than an int - s.assertSameVolumeInfos(c, result, expected) + s.assertValidList(c, []string{}, expectedVolumeListTabular) + + // Do it again, reversing the results returned by the API. + // We should get everything sorted in the appropriate order. + s.mockAPI.listVolumes = func([]string) ([]params.VolumeDetailsResult, error) { + results, _ := mockVolumeListAPI{}.ListVolumes(nil) + n := len(results) + for i := 0; i < n/2; i++ { + results[i], results[n-i-1] = results[n-i-1], results[i] + } + return results, nil + } + s.assertValidList(c, []string{}, expectedVolumeListTabular) +} + +func (s *volumeListSuite) assertUnmarshalledOutput(c *gc.C, unmarshal unmarshaller, expectedErr string, args ...string) { + context, err := runVolumeList(c, args...) + c.Assert(err, jc.ErrorIsNil) + + var result struct { + Volumes map[string]storage.VolumeInfo + } + err = unmarshal([]byte(testing.Stdout(context)), &result) + c.Assert(err, jc.ErrorIsNil) + + expected := s.expect(c, nil) + c.Assert(result.Volumes, jc.DeepEquals, expected) obtainedErr := testing.Stderr(context) - c.Assert(obtainedErr, gc.Equals, ` -volume item error -`[1:]) + c.Assert(obtainedErr, gc.Equals, expectedErr) } -func (s *volumeListSuite) expect(c *gc.C, machines []string) map[string]map[string]map[string]storage.VolumeInfo { - //no need for this element as we are building output on out stream not err - s.mockAPI.addErrItem = false +// expect returns the VolumeInfo mapping we should expect to unmarshal +// from rendered YAML or JSON. +func (s *volumeListSuite) expect(c *gc.C, machines []string) map[string]storage.VolumeInfo { all, err := s.mockAPI.ListVolumes(machines) c.Assert(err, jc.ErrorIsNil) - result, err := storage.ConvertToVolumeInfo(all) + + var valid []params.VolumeDetailsResult + for _, result := range all { + if result.Error == nil { + valid = append(valid, result) + } + } + result, err := storage.ConvertToVolumeInfo(valid) c.Assert(err, jc.ErrorIsNil) return result } -func (s *volumeListSuite) assertSameVolumeInfos(c *gc.C, one, two map[string]map[string]map[string]storage.VolumeInfo) { - c.Assert(len(one), gc.Equals, len(two)) - - propertyCompare := func(a, b interface{}) { - // As some types may have been unmarshalled incorrectly, for example - // int versus float64, compare values' string representations - c.Assert(fmt.Sprintf("%v", a), jc.DeepEquals, fmt.Sprintf("%v", b)) - - } - for machineKey, machineVolumes1 := range one { - machineVolumes2, ok := two[machineKey] - c.Assert(ok, jc.IsTrue) - // these are maps - c.Assert(len(machineVolumes1), gc.Equals, len(machineVolumes2)) - for unitKey, units1 := range machineVolumes1 { - units2, ok := machineVolumes2[unitKey] - c.Assert(ok, jc.IsTrue) - // these are maps - c.Assert(len(units1), gc.Equals, len(units2)) - for storageKey, info1 := range units1 { - info2, ok := units2[storageKey] - c.Assert(ok, jc.IsTrue) - propertyCompare(info1.VolumeId, info2.VolumeId) - propertyCompare(info1.HardwareId, info2.HardwareId) - propertyCompare(info1.Size, info2.Size) - propertyCompare(info1.Persistent, info2.Persistent) - propertyCompare(info1.DeviceName, info2.DeviceName) - propertyCompare(info1.ReadOnly, info2.ReadOnly) - } - } - } -} - -func (s *volumeListSuite) assertValidList(c *gc.C, args []string, expectedOut, expectedErr string) { +func (s *volumeListSuite) assertValidList(c *gc.C, args []string, expectedOut string) { context, err := runVolumeList(c, args...) c.Assert(err, jc.ErrorIsNil) - s.assertUserFacingOutput(c, context, expectedOut, expectedErr) + s.assertUserFacingOutput(c, context, expectedOut, "") } func runVolumeList(c *gc.C, args ...string) (*cmd.Context, error) { @@ -234,90 +188,167 @@ } type mockVolumeListAPI struct { - listAll, listEmpty, fillDeviceName, addErrItem bool - errOut string + listVolumes func([]string) ([]params.VolumeDetailsResult, error) } func (s mockVolumeListAPI) Close() error { return nil } -func (s mockVolumeListAPI) ListVolumes(machines []string) ([]params.VolumeItem, error) { - if s.errOut != "" { - return nil, errors.New(s.errOut) - } - if s.listEmpty { - return nil, nil - } - result := []params.VolumeItem{} - if s.addErrItem { - result = append(result, params.VolumeItem{ - Error: common.ServerError(errors.New("volume item error"))}) - } - if s.listAll { - machines = []string{"25", "42"} - //unattached - result = append(result, s.createTestVolumeItem("3/4", true, "db-dir/1000", "abc/0", nil)) - result = append(result, s.createTestVolumeItem("3/3", false, "", "", nil)) - } - result = append(result, s.createTestVolumeItem("0/1", true, "shared-fs/0", "postgresql/0", machines)) - result = append(result, s.createTestVolumeItem("0/abc/0/88", false, "shared-fs/0", "", machines)) - return result, nil -} - -func (s mockVolumeListAPI) createTestVolumeItem( - id string, - persistent bool, - storageid, unitid string, - machines []string, -) params.VolumeItem { - volume := s.createTestVolume(id, persistent, storageid, unitid) - - // Create unattached volume - if len(machines) == 0 { - return params.VolumeItem{Volume: volume} - } - - // Create volume attachments - attachments := make([]params.VolumeAttachment, len(machines)) - for i, machine := range machines { - attachments[i] = s.createTestAttachment(volume.VolumeTag, machine, i%2 == 0) - } - - return params.VolumeItem{ - Volume: volume, - Attachments: attachments, - } -} - -func (s mockVolumeListAPI) createTestVolume(id string, persistent bool, storageid, unitid string) params.VolumeInstance { - tag := names.NewVolumeTag(id) - result := params.VolumeInstance{ - VolumeTag: tag.String(), - VolumeId: "provider-supplied-" + tag.Id(), - HardwareId: "serial blah blah", - Persistent: persistent, - Size: uint64(1024), - } - if storageid != "" { - result.StorageTag = names.NewStorageTag(storageid).String() - } - if unitid != "" { - result.UnitTag = names.NewUnitTag(unitid).String() - } - return result -} - -func (s mockVolumeListAPI) createTestAttachment(volumeTag, machine string, readonly bool) params.VolumeAttachment { - result := params.VolumeAttachment{ - VolumeTag: volumeTag, - MachineTag: names.NewMachineTag(machine).String(), - Info: params.VolumeAttachmentInfo{ - ReadOnly: readonly, - }, - } - if s.fillDeviceName { - result.Info.DeviceName = "testdevice" - } - return result +func (s mockVolumeListAPI) ListVolumes(machines []string) ([]params.VolumeDetailsResult, error) { + if s.listVolumes != nil { + return s.listVolumes(machines) + } + results := []params.VolumeDetailsResult{{ + // volume 0/0 is attached to machine 0, assigned to + // storage db-dir/1001, which is attached to unit + // abc/0. + Details: ¶ms.VolumeDetails{ + VolumeTag: "volume-0-0", + Info: params.VolumeInfo{ + VolumeId: "provider-supplied-volume-0-0", + Size: 512, + }, + Status: createTestStatus(params.StatusAttached, ""), + MachineAttachments: map[string]params.VolumeAttachmentInfo{ + "machine-0": params.VolumeAttachmentInfo{ + DeviceName: "loop0", + }, + }, + Storage: ¶ms.StorageDetails{ + StorageTag: "storage-db-dir-1001", + OwnerTag: "unit-abc-0", + Kind: params.StorageKindBlock, + Status: createTestStatus(params.StatusAttached, ""), + Attachments: map[string]params.StorageAttachmentDetails{ + "unit-abc-0": params.StorageAttachmentDetails{ + StorageTag: "storage-db-dir-1001", + UnitTag: "unit-abc-0", + MachineTag: "machine-0", + Location: "/dev/loop0", + }, + }, + }, + }, + }, { + // volume 0 is attached to machine 0, assigned to + // storage db-dir/1000, which is attached to unit + // abc/0. + // + // Use Legacy and LegacyAttachment here to test + // backwards compatibility. + LegacyVolume: ¶ms.LegacyVolumeDetails{ + VolumeTag: "volume-0", + StorageTag: "storage-db-dir-1000", + UnitTag: "unit-abc-0", + VolumeId: "provider-supplied-volume-0", + Size: 1024, + Persistent: false, + Status: createTestStatus(params.StatusDestroying, ""), + }, + LegacyAttachments: []params.VolumeAttachment{{ + VolumeTag: "volume-0", + MachineTag: "machine-0", + Info: params.VolumeAttachmentInfo{ + DeviceName: "sda", + ReadOnly: true, + }, + }}, + }, { + // volume 1 is attaching to machine 0, but is not assigned + // to any storage. + Details: ¶ms.VolumeDetails{ + VolumeTag: "volume-1", + Info: params.VolumeInfo{ + VolumeId: "provider-supplied-volume-1", + HardwareId: "serial blah blah", + Persistent: true, + Size: 2048, + }, + Status: createTestStatus(params.StatusAttaching, "failed to attach, will retry"), + MachineAttachments: map[string]params.VolumeAttachmentInfo{ + "machine-0": params.VolumeAttachmentInfo{}, + }, + }, + }, { + // volume 3 is due to be attached to machine 1, but is not + // assigned to any storage and has not yet been provisioned. + Details: ¶ms.VolumeDetails{ + VolumeTag: "volume-3", + Info: params.VolumeInfo{ + Size: 42, + }, + Status: createTestStatus(params.StatusPending, ""), + MachineAttachments: map[string]params.VolumeAttachmentInfo{ + "machine-1": params.VolumeAttachmentInfo{}, + }, + }, + }, { + // volume 2 is due to be attached to machine 1, but is not + // assigned to any storage and has not yet been provisioned. + Details: ¶ms.VolumeDetails{ + VolumeTag: "volume-2", + Info: params.VolumeInfo{ + VolumeId: "provider-supplied-volume-2", + Size: 3, + }, + Status: createTestStatus(params.StatusAttached, ""), + MachineAttachments: map[string]params.VolumeAttachmentInfo{ + "machine-1": params.VolumeAttachmentInfo{ + DeviceName: "xvdf1", + }, + }, + }, + }, { + // volume 4 is attached to machines 0 and 1, and is assigned + // to shared storage. + Details: ¶ms.VolumeDetails{ + VolumeTag: "volume-4", + Info: params.VolumeInfo{ + VolumeId: "provider-supplied-volume-4", + Persistent: true, + Size: 1024, + }, + Status: createTestStatus(params.StatusAttached, ""), + MachineAttachments: map[string]params.VolumeAttachmentInfo{ + "machine-0": params.VolumeAttachmentInfo{ + DeviceName: "xvdf2", + ReadOnly: true, + }, + "machine-1": params.VolumeAttachmentInfo{ + DeviceName: "xvdf3", + ReadOnly: true, + }, + }, + Storage: ¶ms.StorageDetails{ + StorageTag: "storage-shared-fs-0", + OwnerTag: "service-transcode", + Kind: params.StorageKindBlock, + Status: createTestStatus(params.StatusAttached, ""), + Attachments: map[string]params.StorageAttachmentDetails{ + "unit-transcode-0": params.StorageAttachmentDetails{ + StorageTag: "storage-shared-fs-0", + UnitTag: "unit-transcode-0", + MachineTag: "machine-0", + Location: "/mnt/bits", + }, + "unit-transcode-1": params.StorageAttachmentDetails{ + StorageTag: "storage-shared-fs-0", + UnitTag: "unit-transcode-1", + MachineTag: "machine-1", + Location: "/mnt/pieces", + }, + }, + }, + }, + }} + return results, nil +} + +func createTestStatus(status params.Status, message string) params.EntityStatus { + return params.EntityStatus{ + Status: status, + Info: message, + Since: &time.Time{}, + } } === modified file 'src/github.com/juju/juju/cmd/juju/storage/volumelistformatters.go' --- src/github.com/juju/juju/cmd/juju/storage/volumelistformatters.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/storage/volumelistformatters.go 2015-10-23 18:29:32 +0000 @@ -3,24 +3,24 @@ import ( "bytes" "fmt" + "sort" "strings" "text/tabwriter" "github.com/dustin/go-humanize" "github.com/juju/errors" - "github.com/juju/utils/set" ) // formatVolumeListTabular returns a tabular summary of volume instances. func formatVolumeListTabular(value interface{}) ([]byte, error) { - infos, ok := value.(map[string]map[string]map[string]VolumeInfo) + infos, ok := value.(map[string]VolumeInfo) if !ok { return nil, errors.Errorf("expected value of type %T, got %T", infos, value) } return formatVolumeListTabularTyped(infos), nil } -func formatVolumeListTabularTyped(infos map[string]map[string]map[string]VolumeInfo) []byte { +func formatVolumeListTabularTyped(infos map[string]VolumeInfo) []byte { var out bytes.Buffer const ( // To format things into columns. @@ -35,42 +35,98 @@ print := func(values ...string) { fmt.Fprintln(tw, strings.Join(values, "\t")) } - print("MACHINE", "UNIT", "STORAGE", "DEVICE", "VOLUME", "ID", "SIZE") - - // 1. sort by machines - machines := set.NewStrings() - for machine := range infos { - if !machines.Contains(machine) { - machines.Add(machine) - } - } - for _, machine := range machines.SortedValues() { - machineUnits := infos[machine] - - // 2. sort by unit - units := set.NewStrings() - for unit := range machineUnits { - if !units.Contains(unit) { - units.Add(unit) - } - } - for _, unit := range units.SortedValues() { - unitStorages := machineUnits[unit] - - // 3.sort by storage - storages := set.NewStrings() - for storage := range unitStorages { - if !storages.Contains(storage) { - storages.Add(storage) + print("MACHINE", "UNIT", "STORAGE", "ID", "PROVIDER-ID", "DEVICE", "SIZE", "STATE", "MESSAGE") + + volumeAttachmentInfos := make(volumeAttachmentInfos, 0, len(infos)) + for volumeId, info := range infos { + volumeAttachmentInfo := volumeAttachmentInfo{ + VolumeId: volumeId, + VolumeInfo: info, + } + if info.Attachments == nil { + volumeAttachmentInfos = append(volumeAttachmentInfos, volumeAttachmentInfo) + continue + } + // Each unit attachment must have a corresponding volume + // attachment. Enumerate each of the volume attachments, + // and locate the corresponding unit attachment if any. + // Each volume attachment has at most one corresponding + // unit attachment. + for machineId, machineInfo := range info.Attachments.Machines { + volumeAttachmentInfo := volumeAttachmentInfo + volumeAttachmentInfo.MachineId = machineId + volumeAttachmentInfo.MachineVolumeAttachment = machineInfo + for unitId, unitInfo := range info.Attachments.Units { + if unitInfo.MachineId == machineId { + volumeAttachmentInfo.UnitId = unitId + volumeAttachmentInfo.UnitStorageAttachment = unitInfo + break } } - for _, storage := range storages.SortedValues() { - info := unitStorages[storage] - size := humanize.IBytes(info.Size * humanize.MiByte) - print(machine, unit, storage, info.DeviceName, info.Volume, info.VolumeId, size) - } - } - } + volumeAttachmentInfos = append(volumeAttachmentInfos, volumeAttachmentInfo) + } + } + sort.Sort(volumeAttachmentInfos) + + for _, info := range volumeAttachmentInfos { + var size string + if info.Size > 0 { + size = humanize.IBytes(info.Size * humanize.MiByte) + } + print( + info.MachineId, info.UnitId, info.Storage, + info.VolumeId, info.ProviderVolumeId, + info.DeviceName, size, + string(info.Status.Current), info.Status.Message, + ) + } + tw.Flush() return out.Bytes() } + +type volumeAttachmentInfo struct { + VolumeId string + VolumeInfo + + MachineId string + MachineVolumeAttachment + + UnitId string + UnitStorageAttachment +} + +type volumeAttachmentInfos []volumeAttachmentInfo + +func (v volumeAttachmentInfos) Len() int { + return len(v) +} + +func (v volumeAttachmentInfos) Swap(i, j int) { + v[i], v[j] = v[j], v[i] +} + +func (v volumeAttachmentInfos) Less(i, j int) bool { + switch compareStrings(v[i].MachineId, v[j].MachineId) { + case -1: + return true + case 1: + return false + } + + switch compareSlashSeparated(v[i].UnitId, v[j].UnitId) { + case -1: + return true + case 1: + return false + } + + switch compareSlashSeparated(v[i].Storage, v[j].Storage) { + case -1: + return true + case 1: + return false + } + + return v[i].VolumeId < v[j].VolumeId +} === added directory 'src/github.com/juju/juju/cmd/juju/subnet' === added file 'src/github.com/juju/juju/cmd/juju/subnet/add.go' --- src/github.com/juju/juju/cmd/juju/subnet/add.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/subnet/add.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,118 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package subnet + +import ( + "strings" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/juju/network" + "github.com/juju/names" +) + +// AddCommand calls the API to add an existing subnet to Juju. +type AddCommand struct { + SubnetCommandBase + + CIDR names.SubnetTag + RawCIDR string // before normalizing (e.g. 10.10.0.0/8 to 10.0.0.0/8) + ProviderId string + Space names.SpaceTag + Zones []string +} + +const addCommandDoc = ` +Adds an existing subnet to Juju, making it available for use. Unlike +"juju subnet create", this command does not create a new subnet, so it +is supported on a wider variety of clouds (where SDN features are not +available, e.g. MAAS). The subnet will be associated with the given +existing Juju network space. + +Subnets can be referenced by either their CIDR or ProviderId (if the +provider supports it). If CIDR is used an multiple subnets have the +same CIDR, an error will be returned, including the list of possible +provider IDs uniquely identifying each subnet. + +Any availablility zones associated with the added subnet are automatically +discovered using the cloud API (if supported). If this is not possible, +since any subnet needs to be part of at least one zone, specifying +zone(s) is required. +` + +// Info is defined on the cmd.Command interface. +func (c *AddCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "add", + Args: "| [ ...]", + Purpose: "add an existing subnet to Juju", + Doc: strings.TrimSpace(addCommandDoc), + } +} + +// Init is defined on the cmd.Command interface. It checks the +// arguments for sanity and sets up the command to run. +func (c *AddCommand) Init(args []string) (err error) { + defer errors.DeferredAnnotatef(&err, "invalid arguments specified") + + // Ensure we have 2 or more arguments. + switch len(args) { + case 0: + return errNoCIDROrID + case 1: + return errNoSpace + } + + // Try to validate first argument as a CIDR first. + c.RawCIDR = args[0] + c.CIDR, err = c.ValidateCIDR(args[0], false) + if err != nil { + // If it's not a CIDR it could be a ProviderId, so ignore the + // error. + c.ProviderId = args[0] + c.RawCIDR = "" + } + + // Validate the space name. + c.Space, err = c.ValidateSpace(args[1]) + if err != nil { + return err + } + + // Add any given zones. + for _, zone := range args[2:] { + c.Zones = append(c.Zones, zone) + } + return nil +} + +// Run implements Command.Run. +func (c *AddCommand) Run(ctx *cmd.Context) error { + return c.RunWithAPI(ctx, func(api SubnetAPI, ctx *cmd.Context) error { + if c.CIDR.Id() != "" && c.RawCIDR != c.CIDR.Id() { + ctx.Infof( + "WARNING: using CIDR %q instead of the incorrectly specified %q.", + c.CIDR.Id(), c.RawCIDR, + ) + } + + // Add the existing subnet. + err := api.AddSubnet(c.CIDR, network.Id(c.ProviderId), c.Space, c.Zones) + // TODO(dimitern): Change this once the API returns a concrete error. + if err != nil && strings.Contains(err.Error(), "multiple subnets with") { + // Special case: multiple subnets with the same CIDR exist + ctx.Infof("ERROR: %v.", err) + return nil + } else if err != nil { + return errors.Annotatef(err, "cannot add subnet") + } + + if c.ProviderId != "" { + ctx.Infof("added subnet with ProviderId %q in space %q", c.ProviderId, c.Space.Id()) + } else { + ctx.Infof("added subnet with CIDR %q in space %q", c.CIDR.Id(), c.Space.Id()) + } + return nil + }) +} === added file 'src/github.com/juju/juju/cmd/juju/subnet/add_test.go' --- src/github.com/juju/juju/cmd/juju/subnet/add_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/subnet/add_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,251 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package subnet_test + +import ( + "fmt" + "strings" + + "github.com/juju/errors" + "github.com/juju/names" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/juju/subnet" + "github.com/juju/juju/network" + coretesting "github.com/juju/juju/testing" +) + +type AddSuite struct { + BaseSubnetSuite +} + +var _ = gc.Suite(&AddSuite{}) + +func (s *AddSuite) SetUpTest(c *gc.C) { + s.BaseSubnetSuite.SetUpTest(c) + s.command = subnet.NewAddCommand(s.api) + c.Assert(s.command, gc.NotNil) +} + +func (s *AddSuite) TestInit(c *gc.C) { + for i, test := range []struct { + about string + args []string + expectCIDR string + expectRawCIDR string + expectProviderId string + expectSpace string + expectZones []string + expectErr string + }{{ + about: "no arguments", + expectErr: "either CIDR or provider ID is required", + }, { + about: "single argument - invalid CIDR: space name is required", + args: s.Strings("anything"), + expectErr: "space name is required", + }, { + about: "single argument - valid CIDR: space name is required", + args: s.Strings("10.0.0.0/8"), + expectErr: "space name is required", + }, { + about: "single argument - incorrect CIDR: space name is required", + args: s.Strings("10.10.0.0/8"), + expectErr: "space name is required", + }, { + about: "two arguments: an invalid CIDR is assumed to mean ProviderId", + args: s.Strings("foo", "bar"), + expectProviderId: "foo", + expectSpace: "bar", + }, { + about: "two arguments: an incorrectly specified CIDR is fixed", + args: s.Strings("10.10.0.0/8", "bar"), + expectCIDR: "10.0.0.0/8", + expectRawCIDR: "10.10.0.0/8", + expectSpace: "bar", + }, { + about: "more arguments parsed as zones", + args: s.Strings("10.0.0.0/8", "new-space", "zone1", "zone2"), + expectCIDR: "10.0.0.0/8", + expectRawCIDR: "10.0.0.0/8", + expectSpace: "new-space", + expectZones: s.Strings("zone1", "zone2"), + }, { + about: "CIDR and invalid space name, one zone", + args: s.Strings("10.10.0.0/24", "%inv$alid", "zone"), + expectCIDR: "10.10.0.0/24", + expectRawCIDR: "10.10.0.0/24", + expectErr: `"%inv\$alid" is not a valid space name`, + }, { + about: "incorrect CIDR and invalid space name, no zones", + args: s.Strings("10.10.0.0/8", "%inv$alid"), + expectCIDR: "10.0.0.0/8", + expectRawCIDR: "10.10.0.0/8", + expectErr: `"%inv\$alid" is not a valid space name`, + }, { + about: "ProviderId and invalid space name, two zones", + args: s.Strings("foo", "%inv$alid", "zone1", "zone2"), + expectProviderId: "foo", + expectErr: `"%inv\$alid" is not a valid space name`, + }} { + c.Logf("test #%d: %s", i, test.about) + // Create a new instance of the subcommand for each test, but + // since we're not running the command no need to use + // envcmd.Wrap(). + command := subnet.NewAddCommand(s.api) + err := coretesting.InitCommand(command, test.args) + if test.expectErr != "" { + prefixedErr := "invalid arguments specified: " + test.expectErr + c.Check(err, gc.ErrorMatches, prefixedErr) + } else { + c.Check(err, jc.ErrorIsNil) + c.Check(command.CIDR.Id(), gc.Equals, test.expectCIDR) + c.Check(command.RawCIDR, gc.Equals, test.expectRawCIDR) + c.Check(command.ProviderId, gc.Equals, test.expectProviderId) + c.Check(command.Space.Id(), gc.Equals, test.expectSpace) + c.Check(command.Zones, jc.DeepEquals, test.expectZones) + } + // No API calls should be recorded at this stage. + s.api.CheckCallNames(c) + } +} + +func (s *AddSuite) TestRunWithIPv4CIDRSucceeds(c *gc.C) { + s.AssertRunSucceeds(c, + `added subnet with CIDR "10.20.0.0/24" in space "myspace"\n`, + "", // empty stdout. + "10.20.0.0/24", "myspace", + ) + + s.api.CheckCallNames(c, "AddSubnet", "Close") + s.api.CheckCall(c, 0, "AddSubnet", + names.NewSubnetTag("10.20.0.0/24"), + network.Id(""), + names.NewSpaceTag("myspace"), + []string(nil), + ) +} + +func (s *AddSuite) TestRunWithIncorrectlyGivenCIDRSucceedsWithWarning(c *gc.C) { + expectStderr := strings.Join([]string{ + "(.|\n)*", + "WARNING: using CIDR \"10.0.0.0/8\" instead of ", + "the incorrectly specified \"10.10.0.0/8\".\n", + "added subnet with CIDR \"10.0.0.0/8\" in space \"myspace\"\n", + }, "") + + s.AssertRunSucceeds(c, + expectStderr, + "", // empty stdout. + "10.10.0.0/8", "myspace", + ) + + s.api.CheckCallNames(c, "AddSubnet", "Close") + s.api.CheckCall(c, 0, "AddSubnet", + names.NewSubnetTag("10.0.0.0/8"), + network.Id(""), + names.NewSpaceTag("myspace"), + []string(nil), + ) +} + +func (s *AddSuite) TestRunWithProviderIdSucceeds(c *gc.C) { + s.AssertRunSucceeds(c, + `added subnet with ProviderId "foo" in space "myspace"\n`, + "", // empty stdout. + "foo", "myspace", "zone1", "zone2", + ) + + s.api.CheckCallNames(c, "AddSubnet", "Close") + s.api.CheckCall(c, 0, "AddSubnet", + names.SubnetTag{}, + network.Id("foo"), + names.NewSpaceTag("myspace"), + s.Strings("zone1", "zone2"), + ) +} + +func (s *AddSuite) TestRunWithIPv6CIDRSucceeds(c *gc.C) { + s.AssertRunSucceeds(c, + `added subnet with CIDR "2001:db8::/32" in space "hyperspace"\n`, + "", // empty stdout. + "2001:db8::/32", "hyperspace", + ) + + s.api.CheckCallNames(c, "AddSubnet", "Close") + s.api.CheckCall(c, 0, "AddSubnet", + names.NewSubnetTag("2001:db8::/32"), + network.Id(""), + names.NewSpaceTag("hyperspace"), + []string(nil), + ) +} + +func (s *AddSuite) TestRunWithExistingSubnetFails(c *gc.C) { + s.api.SetErrors(errors.AlreadyExistsf("subnet %q", "10.10.0.0/24")) + + err := s.AssertRunFails(c, + `cannot add subnet: subnet "10.10.0.0/24" already exists`, + "10.10.0.0/24", "space", + ) + c.Assert(err, jc.Satisfies, errors.IsAlreadyExists) + + s.api.CheckCallNames(c, "AddSubnet", "Close") + s.api.CheckCall(c, 0, "AddSubnet", + names.NewSubnetTag("10.10.0.0/24"), + network.Id(""), + names.NewSpaceTag("space"), + []string(nil), + ) +} + +func (s *AddSuite) TestRunWithNonExistingSpaceFails(c *gc.C) { + s.api.SetErrors(errors.NotFoundf("space %q", "space")) + + err := s.AssertRunFails(c, + `cannot add subnet: space "space" not found`, + "10.10.0.0/24", "space", "zone1", "zone2", + ) + c.Assert(err, jc.Satisfies, errors.IsNotFound) + + s.api.CheckCallNames(c, "AddSubnet", "Close") + s.api.CheckCall(c, 0, "AddSubnet", + names.NewSubnetTag("10.10.0.0/24"), + network.Id(""), + names.NewSpaceTag("space"), + s.Strings("zone1", "zone2"), + ) +} + +func (s *AddSuite) TestRunWithAmbiguousCIDRDisplaysError(c *gc.C) { + apiError := errors.New(`multiple subnets with CIDR "10.10.0.0/24" `) + s.api.SetErrors(apiError) + + s.AssertRunSucceeds(c, + fmt.Sprintf("ERROR: %v.\n", apiError), + "", + "10.10.0.0/24", "space", "zone1", "zone2", + ) + + s.api.CheckCallNames(c, "AddSubnet", "Close") + s.api.CheckCall(c, 0, "AddSubnet", + names.NewSubnetTag("10.10.0.0/24"), + network.Id(""), + names.NewSpaceTag("space"), + s.Strings("zone1", "zone2"), + ) +} + +func (s *AddSuite) TestRunAPIConnectFails(c *gc.C) { + // TODO(dimitern): Change this once API is implemented. + s.command = subnet.NewAddCommand(nil) + s.AssertRunFails(c, + "cannot connect to the API server: no environment specified", + "10.10.0.0/24", "space", + ) + + // No API calls recoreded. + s.api.CheckCallNames(c) +} === added file 'src/github.com/juju/juju/cmd/juju/subnet/create.go' --- src/github.com/juju/juju/cmd/juju/subnet/create.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/subnet/create.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,178 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package subnet + +import ( + "strings" + + "launchpad.net/gnuflag" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/names" + "github.com/juju/utils/set" +) + +// CreateCommand calls the API to create a new subnet. +type CreateCommand struct { + SubnetCommandBase + + CIDR names.SubnetTag + Space names.SpaceTag + Zones set.Strings + IsPublic bool + IsPrivate bool + + flagSet *gnuflag.FlagSet +} + +const createCommandDoc = ` +Creates a new subnet with a given CIDR, associated with an existing Juju +network space, and attached to one or more availablility zones. Desired +access for the subnet can be specified using the mutually exclusive flags +--private and --public. + +When --private is specified (or no flags are given, as this is the default), +the created subnet will not allow access from outside the environment and +the available address range is only cloud-local. + +When --public is specified, the created subnet will support "shadow addresses" +(see "juju help glossary" for the full definition of the term). This means +all machines inside the subnet will have cloud-local addresses configured, +but there will also be a shadow address configured for each machine, so that +the machines can be accessed from outside the environment (similarly to the +automatic public IP addresses supported with AWS VPCs). + +This command is only supported on clouds which support creating new subnets +dynamically (i.e. Software Defined Networking or SDN). If you want to make +an existing subnet available for Juju to use, rather than creating a new +one, use the "juju subnet add" command. + +Some clouds allow a subnet to span multiple zones, but others do not. It is +an error to try creating a subnet spanning more than one zone if it is not +supported. +` + +// Info is defined on the cmd.Command interface. +func (c *CreateCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "create", + Args: " [ ...] [--public|--private]", + Purpose: "create a new subnet", + Doc: strings.TrimSpace(createCommandDoc), + } +} + +// SetFlags is defined on the cmd.Command interface. +func (c *CreateCommand) SetFlags(f *gnuflag.FlagSet) { + c.SubnetCommandBase.SetFlags(f) + f.BoolVar(&c.IsPublic, "public", false, "enable public access with shadow addresses") + f.BoolVar(&c.IsPrivate, "private", true, "disable public access with shadow addresses") + + // Because SetFlags is called before Parse, we cannot + // use f.Visit() here to check both flags were not + // specified at once. So we store the flag set and + // defer the check to Init(). + c.flagSet = f +} + +// Init is defined on the cmd.Command interface. It checks the +// arguments for sanity and sets up the command to run. +func (c *CreateCommand) Init(args []string) error { + // Ensure we have at least 3 arguments. + // TODO:(mfoord) we need to support VLANTag as an additional optional + // argument. + err := c.CheckNumArgs(args, []error{errNoCIDR, errNoSpace, errNoZones}) + if err != nil { + return err + } + + // Validate given CIDR. + c.CIDR, err = c.ValidateCIDR(args[0], true) + if err != nil { + return err + } + + // Validate the space name. + c.Space, err = c.ValidateSpace(args[1]) + if err != nil { + return err + } + + // Validate any given zones. + c.Zones = set.NewStrings() + for _, zone := range args[2:] { + if c.Zones.Contains(zone) { + return errors.Errorf("duplicate zone %q specified", zone) + } + c.Zones.Add(zone) + } + + // Ensure --public and --private are not both specified. + // TODO(dimitern): This is a really awkward way to handle + // mutually exclusive bool flags and needs to be factored + // out in a helper if another command needs to do it. + var publicSet, privateSet bool + c.flagSet.Visit(func(flag *gnuflag.Flag) { + switch flag.Name { + case "public": + publicSet = true + case "private": + privateSet = true + } + }) + switch { + case publicSet && privateSet: + return errors.Errorf("cannot specify both --public and --private") + case publicSet: + c.IsPrivate = false + case privateSet: + c.IsPublic = false + } + + return nil +} + +// Run implements Command.Run. +func (c *CreateCommand) Run(ctx *cmd.Context) error { + return c.RunWithAPI(ctx, func(api SubnetAPI, ctx *cmd.Context) error { + if !c.Zones.IsEmpty() { + // Fetch all zones to validate the given zones. + zones, err := api.AllZones() + if err != nil { + return errors.Annotate(err, "cannot fetch availability zones") + } + + // Find which of the given CIDRs match existing ones. + validZones := set.NewStrings() + for _, zone := range zones { + validZones.Add(zone) + } + diff := c.Zones.Difference(validZones) + + if !diff.IsEmpty() { + // Some given zones are missing. + zones := strings.Join(diff.SortedValues(), ", ") + return errors.Errorf("unknown zones specified: %s", zones) + } + } + + // Create the new subnet. + err := api.CreateSubnet(c.CIDR, c.Space, c.Zones.SortedValues(), c.IsPublic) + if err != nil { + return errors.Annotatef(err, "cannot create subnet %q", c.CIDR.Id()) + } + + zones := strings.Join(c.Zones.SortedValues(), ", ") + accessType := "private" + if c.IsPublic { + accessType = "public" + } + ctx.Infof( + "created a %s subnet %q in space %q with zones %s", + accessType, c.CIDR.Id(), c.Space.Id(), zones, + ) + return nil + }) +} === added file 'src/github.com/juju/juju/cmd/juju/subnet/create_test.go' --- src/github.com/juju/juju/cmd/juju/subnet/create_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/subnet/create_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,246 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package subnet_test + +import ( + "github.com/juju/errors" + "github.com/juju/names" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/juju/subnet" + "github.com/juju/juju/feature" + coretesting "github.com/juju/juju/testing" +) + +type CreateSuite struct { + BaseSubnetSuite +} + +var _ = gc.Suite(&CreateSuite{}) + +func (s *CreateSuite) SetUpTest(c *gc.C) { + s.BaseSuite.SetFeatureFlags(feature.PostNetCLIMVP) + s.BaseSubnetSuite.SetUpTest(c) + s.command = subnet.NewCreateCommand(s.api) + c.Assert(s.command, gc.NotNil) +} + +func (s *CreateSuite) TestInit(c *gc.C) { + for i, test := range []struct { + about string + args []string + expectCIDR string + expectSpace string + expectZones []string + expectPublic bool + expectPrivate bool + expectErr string + }{{ + about: "no arguments", + expectErr: "CIDR is required", + expectPrivate: true, + }, { + about: "only a subnet argument (invalid)", + args: s.Strings("foo"), + expectPrivate: true, + expectErr: "space name is required", + }, { + about: "no zone arguments (both CIDR and space are invalid)", + args: s.Strings("foo", "%invalid"), + expectPrivate: true, + expectErr: "at least one zone is required", + }, { + about: "invalid CIDR", + args: s.Strings("foo", "space", "zone"), + expectPrivate: true, + expectErr: `"foo" is not a valid CIDR`, + }, { + about: "incorrectly specified CIDR", + args: s.Strings("5.4.3.2/10", "space", "zone"), + expectPrivate: true, + expectErr: `"5.4.3.2/10" is not correctly specified, expected "5.0.0.0/10"`, + }, { + about: "invalid space name", + args: s.Strings("10.10.0.0/24", "%inv$alid", "zone"), + expectCIDR: "10.10.0.0/24", + expectPrivate: true, + expectErr: `"%inv\$alid" is not a valid space name`, + }, { + about: "duplicate zones specified", + args: s.Strings("10.10.0.0/24", "myspace", "zone1", "zone2", "zone1"), + expectCIDR: "10.10.0.0/24", + expectSpace: "myspace", + expectZones: s.Strings("zone1", "zone2"), + expectPrivate: true, + expectErr: `duplicate zone "zone1" specified`, + }, { + about: "both --public and --private specified", + args: s.Strings("10.1.0.0/16", "new-space", "zone", "--public", "--private"), + expectCIDR: "10.1.0.0/16", + expectSpace: "new-space", + expectZones: s.Strings("zone"), + expectErr: `cannot specify both --public and --private`, + expectPublic: true, + expectPrivate: true, + }, { + about: "--public specified", + args: s.Strings("10.1.0.0/16", "new-space", "zone", "--public"), + expectCIDR: "10.1.0.0/16", + expectSpace: "new-space", + expectZones: s.Strings("zone"), + expectPublic: true, + expectPrivate: false, + expectErr: "", + }, { + about: "--private explicitly specified", + args: s.Strings("10.1.0.0/16", "new-space", "zone", "--private"), + expectCIDR: "10.1.0.0/16", + expectSpace: "new-space", + expectZones: s.Strings("zone"), + expectPublic: false, + expectPrivate: true, + expectErr: "", + }, { + about: "--private specified out of order", + args: s.Strings("2001:db8::/32", "--private", "space", "zone"), + expectCIDR: "2001:db8::/32", + expectSpace: "space", + expectZones: s.Strings("zone"), + expectPublic: false, + expectPrivate: true, + expectErr: "", + }, { + about: "--public specified twice", + args: s.Strings("--public", "2001:db8::/32", "--public", "space", "zone"), + expectCIDR: "2001:db8::/32", + expectSpace: "space", + expectZones: s.Strings("zone"), + expectPublic: true, + expectPrivate: false, + expectErr: "", + }} { + c.Logf("test #%d: %s", i, test.about) + // Create a new instance of the subcommand for each test, but + // since we're not running the command no need to use + // envcmd.Wrap(). + command := subnet.NewCreateCommand(s.api) + err := coretesting.InitCommand(command, test.args) + if test.expectErr != "" { + c.Check(err, gc.ErrorMatches, test.expectErr) + } else { + c.Check(err, jc.ErrorIsNil) + c.Check(command.CIDR.Id(), gc.Equals, test.expectCIDR) + c.Check(command.Space.Id(), gc.Equals, test.expectSpace) + c.Check(command.Zones.SortedValues(), jc.DeepEquals, test.expectZones) + c.Check(command.IsPublic, gc.Equals, test.expectPublic) + c.Check(command.IsPrivate, gc.Equals, test.expectPrivate) + } + // No API calls should be recorded at this stage. + s.api.CheckCallNames(c) + } +} + +func (s *CreateSuite) TestRunOneZoneSucceeds(c *gc.C) { + s.AssertRunSucceeds(c, + `created a private subnet "10.20.0.0/24" in space "myspace" with zones zone1\n`, + "", // empty stdout. + "10.20.0.0/24", "myspace", "zone1", + ) + + s.api.CheckCallNames(c, "AllZones", "CreateSubnet", "Close") + s.api.CheckCall(c, 1, "CreateSubnet", + names.NewSubnetTag("10.20.0.0/24"), names.NewSpaceTag("myspace"), s.Strings("zone1"), false, + ) +} + +func (s *CreateSuite) TestRunWithPublicAndIPv6CIDRSucceeds(c *gc.C) { + s.AssertRunSucceeds(c, + `created a public subnet "2001:db8::/32" in space "space" with zones zone1\n`, + "", // empty stdout. + "2001:db8::/32", "space", "zone1", "--public", + ) + + s.api.CheckCallNames(c, "AllZones", "CreateSubnet", "Close") + s.api.CheckCall(c, 1, "CreateSubnet", + names.NewSubnetTag("2001:db8::/32"), names.NewSpaceTag("space"), s.Strings("zone1"), true, + ) +} + +func (s *CreateSuite) TestRunWithMultipleZonesSucceeds(c *gc.C) { + s.AssertRunSucceeds(c, + // The list of zones is sorted both when displayed and passed + // to CreateSubnet. + `created a private subnet "10.20.0.0/24" in space "foo" with zones zone1, zone2\n`, + "", // empty stdout. + "10.20.0.0/24", "foo", "zone2", "zone1", // unsorted zones + ) + + s.api.CheckCallNames(c, "AllZones", "CreateSubnet", "Close") + s.api.CheckCall(c, 1, "CreateSubnet", + names.NewSubnetTag("10.20.0.0/24"), names.NewSpaceTag("foo"), s.Strings("zone1", "zone2"), false, + ) +} + +func (s *CreateSuite) TestRunWithAllZonesErrorFails(c *gc.C) { + s.api.SetErrors(errors.New("boom")) + + s.AssertRunFails(c, + `cannot fetch availability zones: boom`, + "10.10.0.0/24", "space", "zone1", + ) + s.api.CheckCallNames(c, "AllZones", "Close") +} + +func (s *CreateSuite) TestRunWithExistingSubnetFails(c *gc.C) { + s.api.SetErrors(nil, errors.AlreadyExistsf("subnet %q", "10.10.0.0/24")) + + err := s.AssertRunFails(c, + `cannot create subnet "10.10.0.0/24": subnet "10.10.0.0/24" already exists`, + "10.10.0.0/24", "space", "zone1", + ) + c.Assert(err, jc.Satisfies, errors.IsAlreadyExists) + + s.api.CheckCallNames(c, "AllZones", "CreateSubnet", "Close") + s.api.CheckCall(c, 1, "CreateSubnet", + names.NewSubnetTag("10.10.0.0/24"), names.NewSpaceTag("space"), s.Strings("zone1"), false, + ) +} + +func (s *CreateSuite) TestRunWithNonExistingSpaceFails(c *gc.C) { + s.api.SetErrors(nil, errors.NotFoundf("space %q", "space")) + + err := s.AssertRunFails(c, + `cannot create subnet "10.10.0.0/24": space "space" not found`, + "10.10.0.0/24", "space", "zone1", + ) + c.Assert(err, jc.Satisfies, errors.IsNotFound) + + s.api.CheckCallNames(c, "AllZones", "CreateSubnet", "Close") + s.api.CheckCall(c, 1, "CreateSubnet", + names.NewSubnetTag("10.10.0.0/24"), names.NewSpaceTag("space"), s.Strings("zone1"), false, + ) +} + +func (s *CreateSuite) TestRunWithUnknownZonesFails(c *gc.C) { + s.AssertRunFails(c, + // The list of unknown zones is sorted. + "unknown zones specified: foo, no-zone", + "10.30.30.0/24", "space", "no-zone", "zone1", "foo", + ) + + s.api.CheckCallNames(c, "AllZones", "Close") +} + +func (s *CreateSuite) TestRunAPIConnectFails(c *gc.C) { + // TODO(dimitern): Change this once API is implemented. + s.command = subnet.NewCreateCommand(nil) + s.AssertRunFails(c, + "cannot connect to the API server: no environment specified", + "10.10.0.0/24", "space", "zone1", + ) + + // No API calls recoreded. + s.api.CheckCallNames(c) +} === added file 'src/github.com/juju/juju/cmd/juju/subnet/export_test.go' --- src/github.com/juju/juju/cmd/juju/subnet/export_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/subnet/export_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,32 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package subnet + +func NewCreateCommand(api SubnetAPI) *CreateCommand { + createCmd := &CreateCommand{} + createCmd.api = api + return createCmd +} + +func NewAddCommand(api SubnetAPI) *AddCommand { + addCmd := &AddCommand{} + addCmd.api = api + return addCmd +} + +func NewRemoveCommand(api SubnetAPI) *RemoveCommand { + removeCmd := &RemoveCommand{} + removeCmd.api = api + return removeCmd +} + +func NewListCommand(api SubnetAPI) *ListCommand { + listCmd := &ListCommand{} + listCmd.api = api + return listCmd +} + +func ListFormat(cmd *ListCommand) string { + return cmd.out.Name() +} === added file 'src/github.com/juju/juju/cmd/juju/subnet/list.go' --- src/github.com/juju/juju/cmd/juju/subnet/list.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/subnet/list.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,194 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package subnet + +import ( + "encoding/json" + "net" + "strings" + + "launchpad.net/gnuflag" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/juju/apiserver/params" + "github.com/juju/names" +) + +// ListCommand displays a list of all subnets known to Juju +type ListCommand struct { + SubnetCommandBase + + SpaceName string + ZoneName string + + spaceTag *names.SpaceTag + + out cmd.Output +} + +const listCommandDoc = ` +Displays a list of all subnets known to Juju. Results can be filtered +using the optional --space and/or --zone arguments to only display +subnets associated with a given network space and/or availability zone. + +Like with other Juju commands, the output and its format can be changed +using the --format and --output (or -o) optional arguments. Supported +output formats include "yaml" (default) and "json". To redirect the +output to a file, use --output. +` + +// Info is defined on the cmd.Command interface. +func (c *ListCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "list", + Args: "[--space ] [--zone ] [--format yaml|json] [--output ]", + Purpose: "list subnets known to Juju", + Doc: strings.TrimSpace(listCommandDoc), + } +} + +// SetFlags is defined on the cmd.Command interface. +func (c *ListCommand) SetFlags(f *gnuflag.FlagSet) { + c.SubnetCommandBase.SetFlags(f) + c.out.AddFlags(f, "yaml", map[string]cmd.Formatter{ + "yaml": cmd.FormatYaml, + "json": cmd.FormatJson, + }) + + f.StringVar(&c.SpaceName, "space", "", "filter results by space name") + f.StringVar(&c.ZoneName, "zone", "", "filter results by zone name") +} + +// Init is defined on the cmd.Command interface. It checks the +// arguments for sanity and sets up the command to run. +func (c *ListCommand) Init(args []string) error { + // No arguments are accepted, just flags. + err := cmd.CheckEmpty(args) + if err != nil { + return err + } + + // Validate space name, if given and store as tag. + c.spaceTag = nil + if c.SpaceName != "" { + tag, err := c.ValidateSpace(c.SpaceName) + if err != nil { + c.SpaceName = "" + return err + } + c.spaceTag = &tag + } + return nil +} + +// Run implements Command.Run. +func (c *ListCommand) Run(ctx *cmd.Context) error { + return c.RunWithAPI(ctx, func(api SubnetAPI, ctx *cmd.Context) error { + // Validate space and/or zone, if given to display a nicer error + // message. + // Get the list of subnets, filtering them as requested. + subnets, err := api.ListSubnets(c.spaceTag, c.ZoneName) + if err != nil { + return errors.Annotate(err, "cannot list subnets") + } + + // Display a nicer message in case no subnets were found. + if len(subnets) == 0 { + if c.SpaceName != "" || c.ZoneName != "" { + ctx.Infof("no subnets found matching requested criteria") + } else { + ctx.Infof("no subnets to display") + } + return nil + } + + // Construct the output list for displaying with the chosen + // format. + result := formattedList{ + Subnets: make(map[string]formattedSubnet), + } + for _, sub := range subnets { + subResult := formattedSubnet{ + ProviderId: sub.ProviderId, + Zones: sub.Zones, + } + + // Use the CIDR to determine the subnet type. + if ip, _, err := net.ParseCIDR(sub.CIDR); err != nil { + return errors.Errorf("subnet %q has invalid CIDR", sub.CIDR) + } else if ip.To4() != nil { + subResult.Type = typeIPv4 + } else if ip.To16() != nil { + subResult.Type = typeIPv6 + } + // Space must be valid, but verify anyway. + spaceTag, err := names.ParseSpaceTag(sub.SpaceTag) + if err != nil { + return errors.Annotatef(err, "subnet %q has invalid space", sub.CIDR) + } + subResult.Space = spaceTag.Id() + + // Display correct status according to the life cycle value. + switch sub.Life { + case params.Alive: + subResult.Status = statusInUse + case params.Dying, params.Dead: + subResult.Status = statusTerminating + } + + result.Subnets[sub.CIDR] = subResult + } + + return c.out.Write(ctx, result) + }) +} + +const ( + typeIPv4 = "ipv4" + typeIPv6 = "ipv6" + + statusInUse = "in-use" + statusTerminating = "terminating" +) + +type formattedList struct { + Subnets map[string]formattedSubnet `json:"subnets" yaml:"subnets"` +} + +// A goyaml bug means we can't declare these types locally to the +// GetYAML methods. +type formattedListNoMethods formattedList + +// MarshalJSON is defined on json.Marshaller. +func (l formattedList) MarshalJSON() ([]byte, error) { + return json.Marshal(formattedListNoMethods(l)) +} + +// GetYAML is defined on yaml.Getter. +func (l formattedList) GetYAML() (tag string, value interface{}) { + return "", formattedListNoMethods(l) +} + +type formattedSubnet struct { + Type string `json:"type" yaml:"type"` + ProviderId string `json:"provider-id,omitempty" yaml:"provider-id,omitempty"` + Status string `json:"status,omitempty" yaml:"status,omitempty"` + Space string `json:"space" yaml:"space"` + Zones []string `json:"zones" yaml:"zones"` +} + +// A goyaml bug means we can't declare these types locally to the +// GetYAML methods. +type formattedSubnetNoMethods formattedSubnet + +// MarshalJSON is defined on json.Marshaller. +func (s formattedSubnet) MarshalJSON() ([]byte, error) { + return json.Marshal(formattedSubnetNoMethods(s)) +} + +// GetYAML is defined on yaml.Getter. +func (s formattedSubnet) GetYAML() (tag string, value interface{}) { + return "", formattedSubnetNoMethods(s) +} === added file 'src/github.com/juju/juju/cmd/juju/subnet/list_test.go' --- src/github.com/juju/juju/cmd/juju/subnet/list_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/subnet/list_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,343 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package subnet_test + +import ( + "io/ioutil" + "os" + "path/filepath" + + "github.com/juju/errors" + "github.com/juju/names" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/juju/subnet" + coretesting "github.com/juju/juju/testing" +) + +type ListSuite struct { + BaseSubnetSuite +} + +var _ = gc.Suite(&ListSuite{}) + +func (s *ListSuite) SetUpTest(c *gc.C) { + s.BaseSubnetSuite.SetUpTest(c) + s.command = subnet.NewListCommand(s.api) + c.Assert(s.command, gc.NotNil) +} + +func (s *ListSuite) TestInit(c *gc.C) { + for i, test := range []struct { + about string + args []string + expectSpace string + expectZone string + expectFormat string + expectErr string + }{{ + about: "too many arguments", + args: s.Strings("foo", "bar"), + expectErr: `unrecognized args: \["foo" "bar"\]`, + expectFormat: "yaml", + }, { + about: "invalid space name", + args: s.Strings("--space", "%inv$alid"), + expectErr: `"%inv\$alid" is not a valid space name`, + expectFormat: "yaml", + }, { + about: "valid space name", + args: s.Strings("--space", "my-space"), + expectSpace: "my-space", + expectFormat: "yaml", + }, { + about: "both space and zone given", + args: s.Strings("--zone", "zone1", "--space", "my-space"), + expectSpace: "my-space", + expectZone: "zone1", + expectFormat: "yaml", + }, { + about: "invalid format", + args: s.Strings("--format", "foo"), + expectErr: `invalid value "foo" for flag --format: unknown format "foo"`, + expectFormat: "yaml", + }, { + about: "invalid format (value is case-sensitive)", + args: s.Strings("--format", "JSON"), + expectErr: `invalid value "JSON" for flag --format: unknown format "JSON"`, + expectFormat: "yaml", + }, { + about: "json format", + args: s.Strings("--format", "json"), + expectFormat: "json", + }, { + about: "yaml format", + args: s.Strings("--format", "yaml"), + expectFormat: "yaml", + }, { + // --output and -o are tested separately in TestOutputFormats. + about: "both --output and -o specified (latter overrides former)", + args: s.Strings("--output", "foo", "-o", "bar"), + expectFormat: "yaml", + }} { + c.Logf("test #%d: %s", i, test.about) + // Create a new instance of the subcommand for each test, but + // since we're not running the command no need to use + // envcmd.Wrap(). + command := subnet.NewListCommand(s.api) + err := coretesting.InitCommand(command, test.args) + if test.expectErr != "" { + c.Check(err, gc.ErrorMatches, test.expectErr) + } else { + c.Check(err, jc.ErrorIsNil) + } + c.Check(command.SpaceName, gc.Equals, test.expectSpace) + c.Check(command.ZoneName, gc.Equals, test.expectZone) + c.Check(subnet.ListFormat(command), gc.Equals, test.expectFormat) + + // No API calls should be recorded at this stage. + s.api.CheckCallNames(c) + } +} + +func (s *ListSuite) TestOutputFormats(c *gc.C) { + outDir := c.MkDir() + expectedYAML := ` +subnets: + 10.10.0.0/16: + type: ipv4 + status: terminating + space: vlan-42 + zones: + - zone1 + 10.20.0.0/24: + type: ipv4 + provider-id: subnet-foo + status: in-use + space: public + zones: + - zone1 + - zone2 + 2001:db8::/32: + type: ipv6 + provider-id: subnet-bar + status: terminating + space: dmz + zones: + - zone2 +`[1:] + expectedJSON := `{"subnets":{` + + `"10.10.0.0/16":{` + + `"type":"ipv4",` + + `"status":"terminating",` + + `"space":"vlan-42",` + + `"zones":["zone1"]},` + + + `"10.20.0.0/24":{` + + `"type":"ipv4",` + + `"provider-id":"subnet-foo",` + + `"status":"in-use",` + + `"space":"public",` + + `"zones":["zone1","zone2"]},` + + + `"2001:db8::/32":{` + + `"type":"ipv6",` + + `"provider-id":"subnet-bar",` + + `"status":"terminating",` + + `"space":"dmz",` + + `"zones":["zone2"]}}} +` + + assertAPICalls := func() { + // Verify the API calls and reset the recorded calls. + s.api.CheckCallNames(c, "ListSubnets", "Close") + s.api.ResetCalls() + } + makeArgs := func(format string, extraArgs ...string) []string { + args := s.Strings(extraArgs...) + if format != "" { + args = append(args, "--format", format) + } + return args + } + assertOutput := func(format, expected string) { + outFile := filepath.Join(outDir, "output") + c.Assert(outFile, jc.DoesNotExist) + defer os.Remove(outFile) + // Check -o works. + args := makeArgs(format, "-o", outFile) + s.AssertRunSucceeds(c, "", "", args...) + assertAPICalls() + + data, err := ioutil.ReadFile(outFile) + c.Assert(err, jc.ErrorIsNil) + c.Assert(string(data), gc.Equals, expected) + + // Check the last output argument takes precedence when both + // -o and --output are given (and also that --output works the + // same as -o). + outFile1 := filepath.Join(outDir, "output1") + c.Assert(outFile1, jc.DoesNotExist) + defer os.Remove(outFile1) + outFile2 := filepath.Join(outDir, "output2") + c.Assert(outFile2, jc.DoesNotExist) + defer os.Remove(outFile2) + // Write something in outFile2 to verify its contents are + // overwritten. + err = ioutil.WriteFile(outFile2, []byte("some contents"), 0644) + c.Assert(err, jc.ErrorIsNil) + + args = makeArgs(format, "-o", outFile1, "--output", outFile2) + s.AssertRunSucceeds(c, "", "", args...) + // Check only the last output file was used, and the output + // file was overwritten. + c.Assert(outFile1, jc.DoesNotExist) + data, err = ioutil.ReadFile(outFile2) + c.Assert(err, jc.ErrorIsNil) + c.Assert(string(data), gc.Equals, expected) + assertAPICalls() + + // Finally, check without --output. + args = makeArgs(format) + s.AssertRunSucceeds(c, "", expected, args...) + assertAPICalls() + } + + for i, test := range []struct { + format string + expected string + }{ + {"", expectedYAML}, // default format is YAML + {"yaml", expectedYAML}, + {"json", expectedJSON}, + } { + c.Logf("test #%d: format %q", i, test.format) + assertOutput(test.format, test.expected) + } +} + +func (s *ListSuite) TestRunWhenNoneMatchSucceeds(c *gc.C) { + // Simulate no subnets are using the "default" space. + s.api.Subnets = s.api.Subnets[0:0] + + s.AssertRunSucceeds(c, + `no subnets found matching requested criteria\n`, + "", // empty stdout. + "--space", "default", + ) + + s.api.CheckCallNames(c, "ListSubnets", "Close") + tag := names.NewSpaceTag("default") + s.api.CheckCall(c, 0, "ListSubnets", &tag, "") +} + +func (s *ListSuite) TestRunWhenNoSubnetsExistSucceeds(c *gc.C) { + s.api.Subnets = s.api.Subnets[0:0] + + s.AssertRunSucceeds(c, + `no subnets to display\n`, + "", // empty stdout. + ) + + s.api.CheckCallNames(c, "ListSubnets", "Close") + s.api.CheckCall(c, 0, "ListSubnets", nil, "") +} + +func (s *ListSuite) TestRunWithFilteringSucceeds(c *gc.C) { + // Simulate one subnet is using the "public" space or "zone1". + s.api.Subnets = s.api.Subnets[0:1] + + expected := ` +subnets: + 10.20.0.0/24: + type: ipv4 + provider-id: subnet-foo + status: in-use + space: public + zones: + - zone1 + - zone2 +`[1:] + + // Filter by space name first. + s.AssertRunSucceeds(c, + "", // empty stderr. + expected, + "--space", "public", + ) + + s.api.CheckCallNames(c, "ListSubnets", "Close") + tag := names.NewSpaceTag("public") + s.api.CheckCall(c, 0, "ListSubnets", &tag, "") + s.api.ResetCalls() + + // Now filter by zone. + s.AssertRunSucceeds(c, + "", // empty stderr. + expected, + "--zone", "zone1", + ) + + s.api.CheckCallNames(c, "ListSubnets", "Close") + s.api.CheckCall(c, 0, "ListSubnets", nil, "zone1") + s.api.ResetCalls() + + // Finally, filter by both space and zone. + s.AssertRunSucceeds(c, + "", // empty stderr. + expected, + "--zone", "zone1", "--space", "public", + ) + + s.api.CheckCallNames(c, "ListSubnets", "Close") + tag = names.NewSpaceTag("public") + s.api.CheckCall(c, 0, "ListSubnets", &tag, "zone1") +} + +func (s *ListSuite) TestRunWhenListSubnetFails(c *gc.C) { + s.api.SetErrors(errors.NotSupportedf("foo")) + + // Ensure the error cause is preserved. + err := s.AssertRunFails(c, "cannot list subnets: foo not supported") + c.Assert(err, jc.Satisfies, errors.IsNotSupported) + + s.api.CheckCallNames(c, "ListSubnets", "Close") + s.api.CheckCall(c, 0, "ListSubnets", nil, "") +} + +func (s *ListSuite) TestRunWhenASubnetHasInvalidCIDRFails(c *gc.C) { + // This cannot happen in practice, as CIDRs are validated before + // adding a subnet, but this test ensures 100% coverage. + s.api.Subnets = s.api.Subnets[0:1] + s.api.Subnets[0].CIDR = "invalid" + + s.AssertRunFails(c, `subnet "invalid" has invalid CIDR`) + + s.api.CheckCallNames(c, "ListSubnets", "Close") + s.api.CheckCall(c, 0, "ListSubnets", nil, "") +} + +func (s *ListSuite) TestRunWhenASubnetHasInvalidSpaceFails(c *gc.C) { + // This cannot happen in practice, as space names are validated + // before adding a subnet, but this test ensures 100% coverage. + s.api.Subnets = s.api.Subnets[0:1] + s.api.Subnets[0].SpaceTag = "foo" + + s.AssertRunFails(c, `subnet "10.20.0.0/24" has invalid space: "foo" is not a valid tag`) + + s.api.CheckCallNames(c, "ListSubnets", "Close") + s.api.CheckCall(c, 0, "ListSubnets", nil, "") +} + +func (s *ListSuite) TestRunAPIConnectFails(c *gc.C) { + // TODO(dimitern): Change this once API is implemented. + s.command = subnet.NewListCommand(nil) + s.AssertRunFails(c, + "cannot connect to the API server: no environment specified", + ) + + // No API calls recoreded. + s.api.CheckCallNames(c) +} === added file 'src/github.com/juju/juju/cmd/juju/subnet/package_test.go' --- src/github.com/juju/juju/cmd/juju/subnet/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/subnet/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,251 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package subnet_test + +import ( + "net" + "regexp" + stdtesting "testing" + + "github.com/juju/cmd" + "github.com/juju/names" + "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + "github.com/juju/utils/featureflag" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/juju/subnet" + "github.com/juju/juju/feature" + "github.com/juju/juju/network" + coretesting "github.com/juju/juju/testing" +) + +func TestPackage(t *stdtesting.T) { + gc.TestingT(t) +} + +// BaseSubnetSuite is used for embedding in other suites. +type BaseSubnetSuite struct { + coretesting.FakeJujuHomeSuite + coretesting.BaseSuite + + superCmd cmd.Command + command cmd.Command + api *StubAPI +} + +var _ = gc.Suite(&BaseSubnetSuite{}) + +func (s *BaseSubnetSuite) SetUpTest(c *gc.C) { + // If any post-MVP command suite enabled the flag, keep it. + hasFeatureFlag := featureflag.Enabled(feature.PostNetCLIMVP) + + s.BaseSuite.SetUpTest(c) + s.FakeJujuHomeSuite.SetUpTest(c) + + if hasFeatureFlag { + s.BaseSuite.SetFeatureFlags(feature.PostNetCLIMVP) + } + + s.superCmd = subnet.NewSuperCommand() + c.Assert(s.superCmd, gc.NotNil) + + s.api = NewStubAPI() + c.Assert(s.api, gc.NotNil) + + // All subcommand suites embedding this one should initialize + // s.command immediately after calling this method! +} + +// RunSuperCommand executes the super command passing any args and +// returning the stdout and stderr output as strings, as well as any +// error. If s.command is set, the subcommand's name will be passed as +// first argument. +func (s *BaseSubnetSuite) RunSuperCommand(c *gc.C, args ...string) (string, string, error) { + if s.command != nil { + args = append([]string{s.command.Info().Name}, args...) + } + ctx, err := coretesting.RunCommand(c, s.superCmd, args...) + if ctx != nil { + return coretesting.Stdout(ctx), coretesting.Stderr(ctx), err + } + return "", "", err +} + +// RunSubCommand executes the s.command subcommand passing any args +// and returning the stdout and stderr output as strings, as well as +// any error. +func (s *BaseSubnetSuite) RunSubCommand(c *gc.C, args ...string) (string, string, error) { + if s.command == nil { + panic("subcommand is nil") + } + ctx, err := coretesting.RunCommand(c, s.command, args...) + if ctx != nil { + return coretesting.Stdout(ctx), coretesting.Stderr(ctx), err + } + return "", "", err +} + +// AssertRunFails is a shortcut for calling RunSubCommand with the +// passed args then asserting the output is empty and the error is as +// expected, finally returning the error. +func (s *BaseSubnetSuite) AssertRunFails(c *gc.C, expectErr string, args ...string) error { + stdout, stderr, err := s.RunSubCommand(c, args...) + c.Assert(err, gc.ErrorMatches, expectErr) + c.Assert(stdout, gc.Equals, "") + c.Assert(stderr, gc.Equals, "") + return err +} + +// AssertRunSucceeds is a shortcut for calling RunSuperCommand with +// the passed args then asserting the stderr output matches +// expectStderr, stdout is equal to expectStdout, and the error is +// nil. +func (s *BaseSubnetSuite) AssertRunSucceeds(c *gc.C, expectStderr, expectStdout string, args ...string) { + stdout, stderr, err := s.RunSubCommand(c, args...) + c.Assert(err, jc.ErrorIsNil) + c.Assert(stdout, gc.Equals, expectStdout) + c.Assert(stderr, gc.Matches, expectStderr) +} + +// TestHelp runs the command with --help as argument and verifies the +// output. +func (s *BaseSubnetSuite) TestHelp(c *gc.C) { + stderr, stdout, err := s.RunSuperCommand(c, "--help") + c.Assert(err, jc.ErrorIsNil) + c.Check(stdout, gc.Equals, "") + c.Check(stderr, gc.Not(gc.Equals), "") + + // If s.command is set, use it instead of s.superCmd. + cmdInfo := s.superCmd.Info() + var expected string + if s.command != nil { + // Subcommands embed EnvCommandBase and have an extra + // "[options]" prepended before the args. + cmdInfo = s.command.Info() + expected = "(?sm).*^usage: juju subnet " + + regexp.QuoteMeta(cmdInfo.Name) + + `( \[options\])? ` + regexp.QuoteMeta(cmdInfo.Args) + ".+" + } else { + expected = "(?sm).*^usage: juju subnet" + + `( \[options\])? ` + regexp.QuoteMeta(cmdInfo.Args) + ".+" + } + c.Check(cmdInfo, gc.NotNil) + c.Check(stderr, gc.Matches, expected) + + expected = "(?sm).*^purpose: " + regexp.QuoteMeta(cmdInfo.Purpose) + "$.*" + c.Check(stderr, gc.Matches, expected) + + expected = "(?sm).*^" + regexp.QuoteMeta(cmdInfo.Doc) + "$.*" + c.Check(stderr, gc.Matches, expected) +} + +// Strings makes tests taking a slice of strings slightly easier to +// write: e.g. s.Strings("foo", "bar") vs. []string{"foo", "bar"}. +func (s *BaseSubnetSuite) Strings(values ...string) []string { + return values +} + +// StubAPI defines a testing stub for the SubnetAPI interface. +type StubAPI struct { + *testing.Stub + + Subnets []params.Subnet + Spaces []names.Tag + Zones []string +} + +var _ subnet.SubnetAPI = (*StubAPI)(nil) + +// NewStubAPI creates a StubAPI suitable for passing to +// subnet.New*Command(). +func NewStubAPI() *StubAPI { + subnets := []params.Subnet{{ + // IPv4 subnet. + CIDR: "10.20.0.0/24", + ProviderId: "subnet-foo", + Life: params.Alive, + SpaceTag: "space-public", + Zones: []string{"zone1", "zone2"}, + StaticRangeLowIP: net.ParseIP("10.20.0.10"), + StaticRangeHighIP: net.ParseIP("10.20.0.100"), + }, { + // IPv6 subnet. + CIDR: "2001:db8::/32", + ProviderId: "subnet-bar", + Life: params.Dying, + SpaceTag: "space-dmz", + Zones: []string{"zone2"}, + }, { + // IPv4 VLAN subnet. + CIDR: "10.10.0.0/16", + Life: params.Dead, + SpaceTag: "space-vlan-42", + Zones: []string{"zone1"}, + VLANTag: 42, + }} + return &StubAPI{ + Stub: &testing.Stub{}, + Zones: []string{"zone1", "zone2"}, + Subnets: subnets, + Spaces: []names.Tag{ + names.NewSpaceTag("default"), + names.NewSpaceTag("public"), + names.NewSpaceTag("dmz"), + names.NewSpaceTag("vlan-42"), + }, + } +} + +func (sa *StubAPI) Close() error { + sa.MethodCall(sa, "Close") + return sa.NextErr() +} + +func (sa *StubAPI) AllZones() ([]string, error) { + sa.MethodCall(sa, "AllZones") + if err := sa.NextErr(); err != nil { + return nil, err + } + return sa.Zones, nil +} + +func (sa *StubAPI) AllSpaces() ([]names.Tag, error) { + sa.MethodCall(sa, "AllSpaces") + if err := sa.NextErr(); err != nil { + return nil, err + } + return sa.Spaces, nil +} + +func (sa *StubAPI) CreateSubnet(subnetCIDR names.SubnetTag, spaceTag names.SpaceTag, zones []string, isPublic bool) error { + sa.MethodCall(sa, "CreateSubnet", subnetCIDR, spaceTag, zones, isPublic) + return sa.NextErr() +} + +func (sa *StubAPI) AddSubnet(cidr names.SubnetTag, id network.Id, spaceTag names.SpaceTag, zones []string) error { + sa.MethodCall(sa, "AddSubnet", cidr, id, spaceTag, zones) + return sa.NextErr() +} + +func (sa *StubAPI) RemoveSubnet(subnetCIDR names.SubnetTag) error { + sa.MethodCall(sa, "RemoveSubnet", subnetCIDR) + return sa.NextErr() +} + +func (sa *StubAPI) ListSubnets(withSpace *names.SpaceTag, withZone string) ([]params.Subnet, error) { + if withSpace == nil { + // Due to the way CheckCall works (using jc.DeepEquals + // internally), we need to pass an explicit nil here, rather + // than a pointer to a names.SpaceTag pointing to nil. + sa.MethodCall(sa, "ListSubnets", nil, withZone) + } else { + sa.MethodCall(sa, "ListSubnets", withSpace, withZone) + } + if err := sa.NextErr(); err != nil { + return nil, err + } + return sa.Subnets, nil +} === added file 'src/github.com/juju/juju/cmd/juju/subnet/remove.go' --- src/github.com/juju/juju/cmd/juju/subnet/remove.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/subnet/remove.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,75 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package subnet + +import ( + "strings" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/names" +) + +// RemoveCommand calls the API to remove an existing, unused subnet +// from Juju. +type RemoveCommand struct { + SubnetCommandBase + + CIDR names.SubnetTag +} + +const removeCommandDoc = ` +Marks an existing subnet for removal. Depending on what features the +cloud infrastructure supports, this command will either delete the +subnet using the cloud API (if supported, e.g. in Amazon VPC) or just +remove the subnet entity from Juju's database (with non-SDN substrates, +e.g. MAAS). In other words "remove" acts like the opposite of "create" +(if supported) or "add" (if "create" is not supported). + +If any machines are still using the subnet, it cannot be removed and +an error is returned instead. If the subnet is not in use, it will be +marked for removal, but it will not be removed from the Juju database +until all related entites are cleaned up (e.g. allocated addresses). +` + +// Info is defined on the cmd.Command interface. +func (c *RemoveCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "remove", + Args: "", + Purpose: "remove an existing subnet from Juju", + Doc: strings.TrimSpace(removeCommandDoc), + } +} + +// Init is defined on the cmd.Command interface. It checks the +// arguments for sanity and sets up the command to run. +func (c *RemoveCommand) Init(args []string) error { + // Ensure we have exactly 1 argument. + err := c.CheckNumArgs(args, []error{errNoCIDR}) + if err != nil { + return err + } + + // Validate given CIDR. + c.CIDR, err = c.ValidateCIDR(args[0], true) + if err != nil { + return err + } + + return cmd.CheckEmpty(args[1:]) +} + +// Run implements Command.Run. +func (c *RemoveCommand) Run(ctx *cmd.Context) error { + return c.RunWithAPI(ctx, func(api SubnetAPI, ctx *cmd.Context) error { + // Try removing the subnet. + if err := api.RemoveSubnet(c.CIDR); err != nil { + return errors.Annotatef(err, "cannot remove subnet %q", c.CIDR.Id()) + } + + ctx.Infof("marked subnet %q for removal", c.CIDR.Id()) + return nil + }) +} === added file 'src/github.com/juju/juju/cmd/juju/subnet/remove_test.go' --- src/github.com/juju/juju/cmd/juju/subnet/remove_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/subnet/remove_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,128 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package subnet_test + +import ( + "github.com/juju/errors" + "github.com/juju/names" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/juju/subnet" + "github.com/juju/juju/feature" + coretesting "github.com/juju/juju/testing" +) + +type RemoveSuite struct { + BaseSubnetSuite +} + +var _ = gc.Suite(&RemoveSuite{}) + +func (s *RemoveSuite) SetUpTest(c *gc.C) { + s.BaseSuite.SetFeatureFlags(feature.PostNetCLIMVP) + s.BaseSubnetSuite.SetUpTest(c) + s.command = subnet.NewRemoveCommand(s.api) + c.Assert(s.command, gc.NotNil) +} + +func (s *RemoveSuite) TestInit(c *gc.C) { + for i, test := range []struct { + about string + args []string + expectCIDR string + expectErr string + }{{ + about: "no arguments", + expectErr: "CIDR is required", + }, { + about: "an invalid CIDR", + args: s.Strings("foo"), + expectErr: `"foo" is not a valid CIDR`, + }, { + about: "too many arguments (first is valid)", + args: s.Strings("10.0.0.0/8", "bar", "baz"), + expectCIDR: "10.0.0.0/8", + expectErr: `unrecognized args: \["bar" "baz"\]`, + }, { + about: "incorrectly specified CIDR", + args: s.Strings("5.4.3.2/10"), + expectErr: `"5.4.3.2/10" is not correctly specified, expected "5.0.0.0/10"`, + }} { + c.Logf("test #%d: %s", i, test.about) + // Create a new instance of the subcommand for each test, but + // since we're not running the command no need to use + // envcmd.Wrap(). + command := subnet.NewRemoveCommand(s.api) + err := coretesting.InitCommand(command, test.args) + if test.expectErr != "" { + c.Check(err, gc.ErrorMatches, test.expectErr) + } else { + c.Check(err, jc.ErrorIsNil) + c.Check(command.CIDR, gc.Equals, test.expectCIDR) + } + + // No API calls should be recorded at this stage. + s.api.CheckCallNames(c) + } +} + +func (s *RemoveSuite) TestRunWithIPv4CIDRSucceeds(c *gc.C) { + s.AssertRunSucceeds(c, + `marked subnet "10.20.0.0/16" for removal\n`, + "", // empty stdout. + "10.20.0.0/16", + ) + + s.api.CheckCallNames(c, "RemoveSubnet", "Close") + s.api.CheckCall(c, 0, "RemoveSubnet", names.NewSubnetTag("10.20.0.0/16")) +} + +func (s *RemoveSuite) TestRunWithIPv6CIDRSucceeds(c *gc.C) { + s.AssertRunSucceeds(c, + `marked subnet "2001:db8::/32" for removal\n`, + "", // empty stdout. + "2001:db8::/32", + ) + + s.api.CheckCallNames(c, "RemoveSubnet", "Close") + s.api.CheckCall(c, 0, "RemoveSubnet", names.NewSubnetTag("2001:db8::/32")) +} + +func (s *RemoveSuite) TestRunWithNonExistingSubnetFails(c *gc.C) { + s.api.SetErrors(errors.NotFoundf("subnet %q", "10.10.0.0/24")) + + err := s.AssertRunFails(c, + `cannot remove subnet "10.10.0.0/24": subnet "10.10.0.0/24" not found`, + "10.10.0.0/24", + ) + c.Assert(err, jc.Satisfies, errors.IsNotFound) + + s.api.CheckCallNames(c, "RemoveSubnet", "Close") + s.api.CheckCall(c, 0, "RemoveSubnet", names.NewSubnetTag("10.10.0.0/24")) +} + +func (s *RemoveSuite) TestRunWithSubnetInUseFails(c *gc.C) { + s.api.SetErrors(errors.Errorf("subnet %q is still in use", "10.10.0.0/24")) + + s.AssertRunFails(c, + `cannot remove subnet "10.10.0.0/24": subnet "10.10.0.0/24" is still in use`, + "10.10.0.0/24", + ) + + s.api.CheckCallNames(c, "RemoveSubnet", "Close") + s.api.CheckCall(c, 0, "RemoveSubnet", names.NewSubnetTag("10.10.0.0/24")) +} + +func (s *RemoveSuite) TestRunAPIConnectFails(c *gc.C) { + // TODO(dimitern): Change this once API is implemented. + s.command = subnet.NewRemoveCommand(nil) + s.AssertRunFails(c, + "cannot connect to the API server: no environment specified", + "10.10.0.0/24", + ) + + // No API calls recoreded. + s.api.CheckCallNames(c) +} === added file 'src/github.com/juju/juju/cmd/juju/subnet/subnet.go' --- src/github.com/juju/juju/cmd/juju/subnet/subnet.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/subnet/subnet.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,193 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package subnet + +import ( + "io" + "net" + "strings" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/loggo" + "github.com/juju/names" + "github.com/juju/utils/featureflag" + + "github.com/juju/juju/api" + "github.com/juju/juju/api/subnets" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/feature" + "github.com/juju/juju/network" +) + +// SubnetAPI defines the necessary API methods needed by the subnet +// subcommands. +type SubnetAPI interface { + io.Closer + + // AddSubnet adds an existing subnet to Juju. + AddSubnet(cidr names.SubnetTag, id network.Id, spaceTag names.SpaceTag, zones []string) error + + // ListSubnets returns information about subnets known to Juju, + // optionally filtered by space and/or zone (both can be empty). + ListSubnets(withSpace *names.SpaceTag, withZone string) ([]params.Subnet, error) + + // AllZones returns all availability zones known to Juju. + AllZones() ([]string, error) + + // AllSpaces returns all Juju network spaces. + AllSpaces() ([]names.Tag, error) + + // CreateSubnet creates a new Juju subnet. + CreateSubnet(subnetCIDR names.SubnetTag, spaceTag names.SpaceTag, zones []string, isPublic bool) error + + // RemoveSubnet marks an existing subnet as no longer used, which + // will cause it to get removed at some point after all its + // related entites are cleaned up. It will fail if the subnet is + // still in use by any machines. + RemoveSubnet(subnetCIDR names.SubnetTag) error +} + +// mvpAPIShim forwards SubnetAPI methods to the real API facade for +// implemented methods only. Tested with a feature test only. +type mvpAPIShim struct { + SubnetAPI + + apiState api.Connection + facade *subnets.API +} + +func (m *mvpAPIShim) Close() error { + return m.apiState.Close() +} + +func (m *mvpAPIShim) AddSubnet(cidr names.SubnetTag, id network.Id, spaceTag names.SpaceTag, zones []string) error { + return m.facade.AddSubnet(cidr, id, spaceTag, zones) +} + +func (m *mvpAPIShim) ListSubnets(withSpace *names.SpaceTag, withZone string) ([]params.Subnet, error) { + return m.facade.ListSubnets(withSpace, withZone) +} + +var logger = loggo.GetLogger("juju.cmd.juju.subnet") + +const commandDoc = ` +"juju subnet" provides commands to manage Juju subnets. In Juju, a +subnet is a logical address range, a subdivision of a network, defined +by the subnet's Classless Inter-Domain Routing (CIDR) range, like +10.10.0.0/24 or 2001:db8::/32. Alternatively, subnets can be +identified uniquely by their provider-specific identifier +(ProviderId), if the provider supports that. Subnets have two kinds of +supported access: "public" (using shadow addresses) or "private" +(using cloud-local addresses, this is the default). For more +information about subnets and shadow addresses, please refer to Juju's +glossary help topics ("juju help glossary"). ` + +// NewSuperCommand creates the "subnet" supercommand and registers the +// subcommands that it supports. +func NewSuperCommand() cmd.Command { + subnetCmd := cmd.NewSuperCommand(cmd.SuperCommandParams{ + Name: "subnet", + Doc: strings.TrimSpace(commandDoc), + UsagePrefix: "juju", + Purpose: "manage subnets", + }) + subnetCmd.Register(envcmd.Wrap(&AddCommand{})) + subnetCmd.Register(envcmd.Wrap(&ListCommand{})) + if featureflag.Enabled(feature.PostNetCLIMVP) { + // The following commands are not part of the MVP. + subnetCmd.Register(envcmd.Wrap(&CreateCommand{})) + subnetCmd.Register(envcmd.Wrap(&RemoveCommand{})) + } + + return subnetCmd +} + +// SubnetCommandBase is the base type embedded into all subnet +// subcommands. +type SubnetCommandBase struct { + envcmd.EnvCommandBase + api SubnetAPI +} + +// NewAPI returns a SubnetAPI for the root api endpoint that the +// environment command returns. +func (c *SubnetCommandBase) NewAPI() (SubnetAPI, error) { + if c.api != nil { + // Already created. + return c.api, nil + } + root, err := c.NewAPIRoot() + if err != nil { + return nil, errors.Trace(err) + } + + // This is tested with a feature test. + shim := &mvpAPIShim{ + apiState: root, + facade: subnets.NewAPI(root), + } + return shim, nil +} + +type RunOnAPI func(api SubnetAPI, ctx *cmd.Context) error + +func (c *SubnetCommandBase) RunWithAPI(ctx *cmd.Context, toRun RunOnAPI) error { + api, err := c.NewAPI() + if err != nil { + return errors.Annotate(err, "cannot connect to the API server") + } + defer api.Close() + return toRun(api, ctx) +} + +// Common errors shared between subcommands. +var ( + errNoCIDR = errors.New("CIDR is required") + errNoCIDROrID = errors.New("either CIDR or provider ID is required") + errNoSpace = errors.New("space name is required") + errNoZones = errors.New("at least one zone is required") +) + +// CheckNumArgs is a helper used to validate the number of arguments +// passed to Init(). If the number of arguments is X, errors[X] (if +// set) will be returned, otherwise no error occurs. +func (s *SubnetCommandBase) CheckNumArgs(args []string, errors []error) error { + for num, err := range errors { + if len(args) == num { + return err + } + } + return nil +} + +// ValidateCIDR parses given and returns an error if it's not valid. +// If the CIDR is incorrectly specified (e.g. 10.10.10.0/16 instead of +// 10.10.0.0/16) and strict is false, the correctly parsed CIDR in the +// expected format is returned instead without an error. Otherwise, +// when strict is true and given is incorrectly formatted, an error +// will be returned. +func (s *SubnetCommandBase) ValidateCIDR(given string, strict bool) (names.SubnetTag, error) { + _, ipNet, err := net.ParseCIDR(given) + if err != nil { + logger.Debugf("cannot parse CIDR %q: %v", given, err) + return names.SubnetTag{}, errors.Errorf("%q is not a valid CIDR", given) + } + if strict && given != ipNet.String() { + expected := ipNet.String() + return names.SubnetTag{}, errors.Errorf("%q is not correctly specified, expected %q", given, expected) + } + // Already validated, so shouldn't error here. + return names.NewSubnetTag(ipNet.String()), nil +} + +// ValidateSpace parses given and returns an error if it's not a valid +// space name, otherwise returns the parsed tag and no error. +func (s *SubnetCommandBase) ValidateSpace(given string) (names.SpaceTag, error) { + if !names.IsValidSpace(given) { + return names.SpaceTag{}, errors.Errorf("%q is not a valid space name", given) + } + return names.NewSpaceTag(given), nil +} === added file 'src/github.com/juju/juju/cmd/juju/subnet/subnet_test.go' --- src/github.com/juju/juju/cmd/juju/subnet/subnet_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/subnet/subnet_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,220 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package subnet_test + +import ( + "errors" + + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/juju/subnet" + "github.com/juju/juju/feature" + coretesting "github.com/juju/juju/testing" +) + +var mvpSubcommandNames = []string{ + "add", + "list", + "help", +} + +var postMVPSubcommandNames = []string{ + "create", + "remove", +} + +type SubnetCommandSuite struct { + BaseSubnetSuite +} + +var _ = gc.Suite(&SubnetCommandSuite{}) + +func (s *SubnetCommandSuite) TestHelpSubcommandsMVP(c *gc.C) { + s.BaseSuite.SetFeatureFlags() + s.BaseSubnetSuite.SetUpTest(c) // looks evil, but works fine + + ctx, err := coretesting.RunCommand(c, s.superCmd, "--help") + c.Assert(err, jc.ErrorIsNil) + + namesFound := coretesting.ExtractCommandsFromHelpOutput(ctx) + c.Assert(namesFound, jc.SameContents, mvpSubcommandNames) +} + +func (s *SubnetCommandSuite) TestHelpSubcommandsPostMVP(c *gc.C) { + s.BaseSuite.SetFeatureFlags(feature.PostNetCLIMVP) + s.BaseSubnetSuite.SetUpTest(c) // looks evil, but works fine + + ctx, err := coretesting.RunCommand(c, s.superCmd, "--help") + c.Assert(err, jc.ErrorIsNil) + + namesFound := coretesting.ExtractCommandsFromHelpOutput(ctx) + allSubcommandNames := append(mvpSubcommandNames, postMVPSubcommandNames...) + c.Assert(namesFound, jc.SameContents, allSubcommandNames) +} + +type SubnetCommandBaseSuite struct { + coretesting.BaseSuite + + baseCmd *subnet.SubnetCommandBase +} + +func (s *SubnetCommandBaseSuite) SetUpTest(c *gc.C) { + s.BaseSuite.SetUpTest(c) + s.baseCmd = &subnet.SubnetCommandBase{} +} + +var _ = gc.Suite(&SubnetCommandBaseSuite{}) + +func (s *SubnetCommandBaseSuite) TestCheckNumArgs(c *gc.C) { + threeErrors := []error{ + errors.New("first"), + errors.New("second"), + errors.New("third"), + } + twoErrors := threeErrors[:2] + oneError := threeErrors[:1] + threeArgs := []string{"foo", "bar", "baz"} + twoArgs := threeArgs[:2] + oneArg := threeArgs[:1] + + for i, errs := range [][]error{nil, oneError, twoErrors, threeErrors} { + for j, args := range [][]string{nil, oneArg, twoArgs, threeArgs} { + expectErr := "" + if i > j { + // Returned error is always the one with index equal + // to len(args), if it exists. + expectErr = errs[j].Error() + } + + c.Logf("test #%d: args: %v, errors: %v -> %q", i*4+j, args, errs, expectErr) + err := s.baseCmd.CheckNumArgs(args, errs) + if expectErr != "" { + c.Check(err, gc.ErrorMatches, expectErr) + } else { + c.Check(err, jc.ErrorIsNil) + } + } + } +} + +func (s *SubnetCommandBaseSuite) TestValidateCIDR(c *gc.C) { + // We only validate the subset of accepted CIDR formats which we + // need to support. + for i, test := range []struct { + about string + input string + strict bool + output string + expectErr string + }{{ + about: "valid IPv4 CIDR, strict=false", + input: "10.0.5.0/24", + strict: false, + output: "10.0.5.0/24", + }, { + about: "valid IPv4 CIDR, struct=true", + input: "10.0.5.0/24", + strict: true, + output: "10.0.5.0/24", + }, { + about: "valid IPv6 CIDR, strict=false", + input: "2001:db8::/32", + strict: false, + output: "2001:db8::/32", + }, { + about: "valid IPv6 CIDR, strict=true", + input: "2001:db8::/32", + strict: true, + output: "2001:db8::/32", + }, { + about: "incorrectly specified IPv4 CIDR, strict=false", + input: "192.168.10.20/16", + strict: false, + output: "192.168.0.0/16", + }, { + about: "incorrectly specified IPv4 CIDR, strict=true", + input: "192.168.10.20/16", + strict: true, + expectErr: `"192.168.10.20/16" is not correctly specified, expected "192.168.0.0/16"`, + }, { + about: "incorrectly specified IPv6 CIDR, strict=false", + input: "2001:db8::2/48", + strict: false, + output: "2001:db8::/48", + }, { + about: "incorrectly specified IPv6 CIDR, strict=true", + input: "2001:db8::2/48", + strict: true, + expectErr: `"2001:db8::2/48" is not correctly specified, expected "2001:db8::/48"`, + }, { + about: "empty CIDR, strict=false", + input: "", + strict: false, + expectErr: `"" is not a valid CIDR`, + }, { + about: "empty CIDR, strict=true", + input: "", + strict: true, + expectErr: `"" is not a valid CIDR`, + }} { + c.Logf("test #%d: %s -> %s", i, test.about, test.expectErr) + validated, err := s.baseCmd.ValidateCIDR(test.input, test.strict) + if test.expectErr != "" { + c.Check(err, gc.ErrorMatches, test.expectErr) + } else { + c.Check(err, jc.ErrorIsNil) + } + c.Check(validated.Id(), gc.Equals, test.output) + } +} + +func (s *SubnetCommandBaseSuite) TestValidateSpace(c *gc.C) { + // We only validate a few more common invalid cases as + // names.IsValidSpace() is separately and more extensively tested. + for i, test := range []struct { + about string + input string + expectErr string + }{{ + about: "valid space - only lowercase letters", + input: "space", + }, { + about: "valid space - only numbers", + input: "42", + }, { + about: "valid space - only lowercase letters and numbers", + input: "over9000", + }, { + about: "valid space - with dashes", + input: "my-new-99space", + }, { + about: "invalid space - with symbols", + input: "%in$valid", + expectErr: `"%in\$valid" is not a valid space name`, + }, { + about: "invalid space - with underscores", + input: "42_foo", + expectErr: `"42_foo" is not a valid space name`, + }, { + about: "invalid space - with uppercase letters", + input: "Not-Good", + expectErr: `"Not-Good" is not a valid space name`, + }, { + about: "empty space name", + input: "", + expectErr: `"" is not a valid space name`, + }} { + c.Logf("test #%d: %s -> %s", i, test.about, test.expectErr) + validated, err := s.baseCmd.ValidateSpace(test.input) + if test.expectErr != "" { + c.Check(err, gc.ErrorMatches, test.expectErr) + c.Check(validated.Id(), gc.Equals, "") + } else { + c.Check(err, jc.ErrorIsNil) + // When the input is valid it should stay the same. + c.Check(validated.Id(), gc.Equals, test.input) + } + } +} === removed file 'src/github.com/juju/juju/cmd/juju/switch.go' --- src/github.com/juju/juju/cmd/juju/switch.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/switch.go 1970-01-01 00:00:00 +0000 @@ -1,137 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - "os" - - "github.com/juju/cmd" - "github.com/juju/errors" - "github.com/juju/utils/set" - "launchpad.net/gnuflag" - - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/environs" - "github.com/juju/juju/environs/configstore" -) - -type SwitchCommand struct { - cmd.CommandBase - EnvName string - List bool -} - -var switchDoc = ` -Show or change the default juju environment name. - -If no command line parameters are passed, switch will output the current -environment as defined by the file $JUJU_HOME/current-environment. - -If a command line parameter is passed in, that value will is stored in the -current environment file if it represents a valid environment name as -specified in the environments.yaml file. -` - -func (c *SwitchCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "switch", - Args: "[environment name]", - Purpose: "show or change the default juju environment name", - Doc: switchDoc, - Aliases: []string{"env"}, - } -} - -func (c *SwitchCommand) SetFlags(f *gnuflag.FlagSet) { - f.BoolVar(&c.List, "l", false, "list the environment names") - f.BoolVar(&c.List, "list", false, "") -} - -func (c *SwitchCommand) Init(args []string) (err error) { - c.EnvName, err = cmd.ZeroOrOneArgs(args) - return -} - -func getConfigstoreEnvironments() (set.Strings, error) { - store, err := configstore.Default() - if err != nil { - return nil, errors.Annotate(err, "failed to get config store") - } - other, err := store.List() - if err != nil { - return nil, errors.Annotate(err, "failed to list environments in config store") - } - return set.NewStrings(other...), nil -} - -func (c *SwitchCommand) Run(ctx *cmd.Context) error { - // Switch is an alternative way of dealing with environments than using - // the JUJU_ENV environment setting, and as such, doesn't play too well. - // If JUJU_ENV is set we should report that as the current environment, - // and not allow switching when it is set. - - // Passing through the empty string reads the default environments.yaml file. - environments, err := environs.ReadEnvirons("") - if err != nil { - return errors.Errorf("couldn't read the environment") - } - - names := set.NewStrings(environments.Names()...) - configEnvirons, err := getConfigstoreEnvironments() - if err != nil { - return err - } - names = names.Union(configEnvirons) - - if c.List { - // List all environments. - if c.EnvName != "" { - return errors.New("cannot switch and list at the same time") - } - for _, name := range names.SortedValues() { - fmt.Fprintf(ctx.Stdout, "%s\n", name) - } - return nil - } - - jujuEnv := os.Getenv("JUJU_ENV") - if jujuEnv != "" { - if c.EnvName == "" { - fmt.Fprintf(ctx.Stdout, "%s\n", jujuEnv) - return nil - } else { - return errors.Errorf("cannot switch when JUJU_ENV is overriding the environment (set to %q)", jujuEnv) - } - } - - currentEnv := envcmd.ReadCurrentEnvironment() - if currentEnv == "" { - currentEnv = environments.Default - } - - // Handle the different operation modes. - switch { - case c.EnvName == "" && currentEnv == "": - // Nothing specified and nothing to switch to. - return errors.New("no currently specified environment") - case c.EnvName == "": - // Simply print the current environment. - fmt.Fprintf(ctx.Stdout, "%s\n", currentEnv) - default: - // Switch the environment. - if !names.Contains(c.EnvName) { - return errors.Errorf("%q is not a name of an existing defined environment", c.EnvName) - } - if err := envcmd.WriteCurrentEnvironment(c.EnvName); err != nil { - return err - } - if currentEnv == "" { - fmt.Fprintf(ctx.Stdout, "-> %s\n", c.EnvName) - } else { - fmt.Fprintf(ctx.Stdout, "%s -> %s\n", currentEnv, c.EnvName) - } - } - return nil -} === removed file 'src/github.com/juju/juju/cmd/juju/switch_test.go' --- src/github.com/juju/juju/cmd/juju/switch_test.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/switch_test.go 1970-01-01 00:00:00 +0000 @@ -1,135 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "os" - - gitjujutesting "github.com/juju/testing" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/environs/configstore" - _ "github.com/juju/juju/juju" - "github.com/juju/juju/testing" -) - -type SwitchSimpleSuite struct { - testing.FakeJujuHomeSuite -} - -var _ = gc.Suite(&SwitchSimpleSuite{}) - -func (*SwitchSimpleSuite) TestNoEnvironment(c *gc.C) { - envPath := gitjujutesting.HomePath(".juju", "environments.yaml") - err := os.Remove(envPath) - c.Assert(err, jc.ErrorIsNil) - _, err = testing.RunCommand(c, &SwitchCommand{}) - c.Assert(err, gc.ErrorMatches, "couldn't read the environment") -} - -func (*SwitchSimpleSuite) TestNoDefault(c *gc.C) { - testing.WriteEnvironments(c, testing.MultipleEnvConfigNoDefault) - _, err := testing.RunCommand(c, &SwitchCommand{}) - c.Assert(err, gc.ErrorMatches, "no currently specified environment") -} - -func (*SwitchSimpleSuite) TestShowsDefault(c *gc.C) { - testing.WriteEnvironments(c, testing.MultipleEnvConfig) - context, err := testing.RunCommand(c, &SwitchCommand{}) - c.Assert(err, jc.ErrorIsNil) - c.Assert(testing.Stdout(context), gc.Equals, "erewhemos\n") -} - -func (s *SwitchSimpleSuite) TestCurrentEnvironmentHasPrecidence(c *gc.C) { - testing.WriteEnvironments(c, testing.MultipleEnvConfig) - s.FakeHomeSuite.Home.AddFiles(c, gitjujutesting.TestFile{".juju/current-environment", "fubar"}) - context, err := testing.RunCommand(c, &SwitchCommand{}) - c.Assert(err, jc.ErrorIsNil) - c.Assert(testing.Stdout(context), gc.Equals, "fubar\n") -} - -func (*SwitchSimpleSuite) TestShowsJujuEnv(c *gc.C) { - testing.WriteEnvironments(c, testing.MultipleEnvConfig) - os.Setenv("JUJU_ENV", "using-env") - context, err := testing.RunCommand(c, &SwitchCommand{}) - c.Assert(err, jc.ErrorIsNil) - c.Assert(testing.Stdout(context), gc.Equals, "using-env\n") -} - -func (s *SwitchSimpleSuite) TestJujuEnvOverCurrentEnvironment(c *gc.C) { - testing.WriteEnvironments(c, testing.MultipleEnvConfig) - s.FakeHomeSuite.Home.AddFiles(c, gitjujutesting.TestFile{".juju/current-environment", "fubar"}) - os.Setenv("JUJU_ENV", "using-env") - context, err := testing.RunCommand(c, &SwitchCommand{}) - c.Assert(err, jc.ErrorIsNil) - c.Assert(testing.Stdout(context), gc.Equals, "using-env\n") -} - -func (*SwitchSimpleSuite) TestSettingWritesFile(c *gc.C) { - testing.WriteEnvironments(c, testing.MultipleEnvConfig) - context, err := testing.RunCommand(c, &SwitchCommand{}, "erewhemos-2") - c.Assert(err, jc.ErrorIsNil) - c.Assert(testing.Stdout(context), gc.Equals, "erewhemos -> erewhemos-2\n") - c.Assert(envcmd.ReadCurrentEnvironment(), gc.Equals, "erewhemos-2") -} - -func (*SwitchSimpleSuite) TestSettingToUnknown(c *gc.C) { - testing.WriteEnvironments(c, testing.MultipleEnvConfig) - _, err := testing.RunCommand(c, &SwitchCommand{}, "unknown") - c.Assert(err, gc.ErrorMatches, `"unknown" is not a name of an existing defined environment`) -} - -func (*SwitchSimpleSuite) TestSettingWhenJujuEnvSet(c *gc.C) { - testing.WriteEnvironments(c, testing.MultipleEnvConfig) - os.Setenv("JUJU_ENV", "using-env") - _, err := testing.RunCommand(c, &SwitchCommand{}, "erewhemos-2") - c.Assert(err, gc.ErrorMatches, `cannot switch when JUJU_ENV is overriding the environment \(set to "using-env"\)`) -} - -const expectedEnvironments = `erewhemos -erewhemos-2 -` - -func (*SwitchSimpleSuite) TestListEnvironments(c *gc.C) { - testing.WriteEnvironments(c, testing.MultipleEnvConfig) - context, err := testing.RunCommand(c, &SwitchCommand{}, "--list") - c.Assert(err, jc.ErrorIsNil) - c.Assert(testing.Stdout(context), gc.Equals, expectedEnvironments) -} - -func (s *SwitchSimpleSuite) TestListEnvironmentsWithConfigstore(c *gc.C) { - memstore := configstore.NewMem() - s.PatchValue(&configstore.Default, func() (configstore.Storage, error) { - return memstore, nil - }) - info := memstore.CreateInfo("testing") - err := info.Write() - testing.WriteEnvironments(c, testing.MultipleEnvConfig) - context, err := testing.RunCommand(c, &SwitchCommand{}, "--list") - c.Assert(err, jc.ErrorIsNil) - expected := expectedEnvironments + "testing\n" - c.Assert(testing.Stdout(context), gc.Equals, expected) -} - -func (*SwitchSimpleSuite) TestListEnvironmentsOSJujuEnvSet(c *gc.C) { - testing.WriteEnvironments(c, testing.MultipleEnvConfig) - os.Setenv("JUJU_ENV", "using-env") - context, err := testing.RunCommand(c, &SwitchCommand{}, "--list") - c.Assert(err, jc.ErrorIsNil) - c.Assert(testing.Stdout(context), gc.Equals, expectedEnvironments) -} - -func (*SwitchSimpleSuite) TestListEnvironmentsAndChange(c *gc.C) { - testing.WriteEnvironments(c, testing.MultipleEnvConfig) - _, err := testing.RunCommand(c, &SwitchCommand{}, "--list", "erewhemos-2") - c.Assert(err, gc.ErrorMatches, "cannot switch and list at the same time") -} - -func (*SwitchSimpleSuite) TestTooManyParams(c *gc.C) { - testing.WriteEnvironments(c, testing.MultipleEnvConfig) - _, err := testing.RunCommand(c, &SwitchCommand{}, "foo", "bar") - c.Assert(err, gc.ErrorMatches, `unrecognized args: ."bar".`) -} === removed file 'src/github.com/juju/juju/cmd/juju/synctools.go' --- src/github.com/juju/juju/cmd/juju/synctools.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/synctools.go 1970-01-01 00:00:00 +0000 @@ -1,174 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "bytes" - "io" - - "github.com/juju/cmd" - "github.com/juju/loggo" - "launchpad.net/gnuflag" - - "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/cmd/juju/block" - "github.com/juju/juju/environs/filestorage" - "github.com/juju/juju/environs/sync" - envtools "github.com/juju/juju/environs/tools" - coretools "github.com/juju/juju/tools" - "github.com/juju/juju/version" -) - -var syncTools = sync.SyncTools - -// SyncToolsCommand copies all the tools from the us-east-1 bucket to the local -// bucket. -type SyncToolsCommand struct { - envcmd.EnvCommandBase - allVersions bool - versionStr string - majorVersion int - minorVersion int - dryRun bool - dev bool - public bool - source string - stream string - localDir string - destination string -} - -var _ cmd.Command = (*SyncToolsCommand)(nil) - -func (c *SyncToolsCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "sync-tools", - Purpose: "copy tools from the official tool store into a local environment", - Doc: ` -This copies the Juju tools tarball from the official tools store (located -at https://streams.canonical.com/juju) into your environment. -This is generally done when you want Juju to be able to run without having to -access the Internet. Alternatively you can specify a local directory as source. - -Sometimes this is because the environment does not have public access, -and sometimes you just want to avoid having to access data outside of -the local cloud. -`, - } -} - -func (c *SyncToolsCommand) SetFlags(f *gnuflag.FlagSet) { - f.BoolVar(&c.allVersions, "all", false, "copy all versions, not just the latest") - f.StringVar(&c.versionStr, "version", "", "copy a specific major[.minor] version") - f.BoolVar(&c.dryRun, "dry-run", false, "don't copy, just print what would be copied") - f.BoolVar(&c.dev, "dev", false, "consider development versions as well as released ones\n DEPRECATED: use --stream instead") - f.BoolVar(&c.public, "public", false, "tools are for a public cloud, so generate mirrors information") - f.StringVar(&c.source, "source", "", "local source directory") - f.StringVar(&c.stream, "stream", "", "simplestreams stream for which to sync metadata") - f.StringVar(&c.localDir, "local-dir", "", "local destination directory") - f.StringVar(&c.destination, "destination", "", "local destination directory") -} - -func (c *SyncToolsCommand) Init(args []string) error { - if c.destination != "" { - // Override localDir with destination as localDir now replaces destination - c.localDir = c.destination - logger.Warningf("Use of the --destination flag is deprecated in 1.18. Please use --local-dir instead.") - } - if c.versionStr != "" { - var err error - if c.majorVersion, c.minorVersion, err = version.ParseMajorMinor(c.versionStr); err != nil { - return err - } - } - if c.dev { - c.stream = envtools.TestingStream - } - return cmd.CheckEmpty(args) -} - -// syncToolsAPI provides an interface with a subset of the -// api.Client API. This exists to enable mocking. -type syncToolsAPI interface { - FindTools(majorVersion, minorVersion int, series, arch string) (params.FindToolsResult, error) - UploadTools(r io.Reader, v version.Binary, series ...string) (*coretools.Tools, error) - Close() error -} - -var getSyncToolsAPI = func(c *SyncToolsCommand) (syncToolsAPI, error) { - return c.NewAPIClient() -} - -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) - defer loggo.RemoveWriter("synctools") - - sctx := &sync.SyncContext{ - AllVersions: c.allVersions, - MajorVersion: c.majorVersion, - MinorVersion: c.minorVersion, - DryRun: c.dryRun, - Stream: c.stream, - Source: c.source, - } - - if c.localDir != "" { - stor, err := filestorage.NewFileStorageWriter(c.localDir) - if err != nil { - return err - } - writeMirrors := envtools.DoNotWriteMirrors - if c.public { - writeMirrors = envtools.WriteMirrors - } - sctx.TargetToolsFinder = sync.StorageToolsFinder{Storage: stor} - sctx.TargetToolsUploader = sync.StorageToolsUploader{ - Storage: stor, - WriteMetadata: true, - WriteMirrors: writeMirrors, - } - } else { - if c.public { - logger.Warningf("--public is ignored unless --local-dir is specified") - } - api, err := getSyncToolsAPI(c) - if err != nil { - return err - } - defer api.Close() - adapter := syncToolsAPIAdapter{api} - sctx.TargetToolsFinder = adapter - sctx.TargetToolsUploader = adapter - } - return block.ProcessBlockedError(syncTools(sctx), block.BlockChange) -} - -// syncToolsAPIAdapter implements sync.ToolsFinder and -// sync.ToolsUploader, adapting a syncToolsAPI. This -// enables the use of sync.SyncTools with the client -// API. -type syncToolsAPIAdapter struct { - syncToolsAPI -} - -func (s syncToolsAPIAdapter) FindTools(majorVersion int, stream string) (coretools.List, error) { - result, err := s.syncToolsAPI.FindTools(majorVersion, -1, "", "") - if err != nil { - return nil, err - } - if result.Error != nil { - if params.IsCodeNotFound(result.Error) { - return nil, coretools.ErrNoMatches - } - return nil, result.Error - } - return result.List, nil -} - -func (s syncToolsAPIAdapter) UploadTools(toolsDir, stream string, tools *coretools.Tools, data []byte) error { - _, err := s.syncToolsAPI.UploadTools(bytes.NewReader(data), tools.Version) - return err -} === removed file 'src/github.com/juju/juju/cmd/juju/synctools_test.go' --- src/github.com/juju/juju/cmd/juju/synctools_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/synctools_test.go 1970-01-01 00:00:00 +0000 @@ -1,292 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "io" - "io/ioutil" - "strings" - - "github.com/juju/cmd" - "github.com/juju/errors" - "github.com/juju/loggo" - jc "github.com/juju/testing/checkers" - "github.com/juju/utils" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/apiserver/common" - "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/environs/sync" - envtools "github.com/juju/juju/environs/tools" - coretesting "github.com/juju/juju/testing" - coretools "github.com/juju/juju/tools" - "github.com/juju/juju/version" -) - -type syncToolsSuite struct { - coretesting.BaseSuite - fakeSyncToolsAPI *fakeSyncToolsAPI -} - -var _ = gc.Suite(&syncToolsSuite{}) - -func (s *syncToolsSuite) SetUpTest(c *gc.C) { - s.BaseSuite.SetUpTest(c) - s.fakeSyncToolsAPI = &fakeSyncToolsAPI{} - s.PatchValue(&getSyncToolsAPI, func(c *SyncToolsCommand) (syncToolsAPI, error) { - return s.fakeSyncToolsAPI, nil - }) -} - -func (s *syncToolsSuite) Reset(c *gc.C) { - s.TearDownTest(c) - s.SetUpTest(c) -} - -func runSyncToolsCommand(c *gc.C, args ...string) (*cmd.Context, error) { - return coretesting.RunCommand(c, envcmd.Wrap(&SyncToolsCommand{}), args...) -} - -var syncToolsCommandTests = []struct { - description string - args []string - sctx *sync.SyncContext - public bool -}{ - { - description: "environment as only argument", - args: []string{"-e", "test-target"}, - sctx: &sync.SyncContext{}, - }, - { - description: "specifying also the synchronization source", - args: []string{"-e", "test-target", "--source", "/foo/bar"}, - sctx: &sync.SyncContext{ - Source: "/foo/bar", - }, - }, - { - description: "synchronize all version including development", - args: []string{"-e", "test-target", "--all", "--dev"}, - sctx: &sync.SyncContext{ - AllVersions: true, - Stream: "testing", - }, - }, - { - description: "just make a dry run", - args: []string{"-e", "test-target", "--dry-run"}, - sctx: &sync.SyncContext{ - DryRun: true, - }, - }, - { - description: "specified public (ignored by API)", - args: []string{"-e", "test-target", "--public"}, - sctx: &sync.SyncContext{}, - }, - { - description: "specify version", - args: []string{"-e", "test-target", "--version", "1.2"}, - sctx: &sync.SyncContext{ - MajorVersion: 1, - MinorVersion: 2, - }, - }, -} - -func (s *syncToolsSuite) TestSyncToolsCommand(c *gc.C) { - for i, test := range syncToolsCommandTests { - c.Logf("test %d: %s", i, test.description) - called := false - syncTools = func(sctx *sync.SyncContext) error { - c.Assert(sctx.AllVersions, gc.Equals, test.sctx.AllVersions) - c.Assert(sctx.MajorVersion, gc.Equals, test.sctx.MajorVersion) - c.Assert(sctx.MinorVersion, gc.Equals, test.sctx.MinorVersion) - c.Assert(sctx.DryRun, gc.Equals, test.sctx.DryRun) - c.Assert(sctx.Stream, gc.Equals, test.sctx.Stream) - c.Assert(sctx.Source, gc.Equals, test.sctx.Source) - - c.Assert(sctx.TargetToolsFinder, gc.FitsTypeOf, syncToolsAPIAdapter{}) - finder := sctx.TargetToolsFinder.(syncToolsAPIAdapter) - c.Assert(finder.syncToolsAPI, gc.Equals, s.fakeSyncToolsAPI) - - c.Assert(sctx.TargetToolsUploader, gc.FitsTypeOf, syncToolsAPIAdapter{}) - uploader := sctx.TargetToolsUploader.(syncToolsAPIAdapter) - c.Assert(uploader.syncToolsAPI, gc.Equals, s.fakeSyncToolsAPI) - - called = true - return nil - } - ctx, err := runSyncToolsCommand(c, test.args...) - c.Assert(err, jc.ErrorIsNil) - c.Assert(ctx, gc.NotNil) - c.Assert(called, jc.IsTrue) - s.Reset(c) - } -} - -func (s *syncToolsSuite) TestSyncToolsCommandTargetDirectory(c *gc.C) { - called := false - dir := c.MkDir() - syncTools = func(sctx *sync.SyncContext) error { - c.Assert(sctx.AllVersions, jc.IsFalse) - c.Assert(sctx.DryRun, jc.IsFalse) - c.Assert(sctx.Stream, gc.Equals, "proposed") - c.Assert(sctx.Source, gc.Equals, "") - c.Assert(sctx.TargetToolsUploader, gc.FitsTypeOf, sync.StorageToolsUploader{}) - uploader := sctx.TargetToolsUploader.(sync.StorageToolsUploader) - c.Assert(uploader.WriteMirrors, gc.Equals, envtools.DoNotWriteMirrors) - url, err := uploader.Storage.URL("") - c.Assert(err, jc.ErrorIsNil) - c.Assert(url, gc.Equals, utils.MakeFileURL(dir)) - called = true - return nil - } - ctx, err := runSyncToolsCommand(c, "-e", "test-target", "--local-dir", dir, "--stream", "proposed") - c.Assert(err, jc.ErrorIsNil) - c.Assert(ctx, gc.NotNil) - c.Assert(called, jc.IsTrue) -} - -func (s *syncToolsSuite) TestSyncToolsCommandTargetDirectoryPublic(c *gc.C) { - called := false - dir := c.MkDir() - syncTools = func(sctx *sync.SyncContext) error { - c.Assert(sctx.TargetToolsUploader, gc.FitsTypeOf, sync.StorageToolsUploader{}) - uploader := sctx.TargetToolsUploader.(sync.StorageToolsUploader) - c.Assert(uploader.WriteMirrors, gc.Equals, envtools.WriteMirrors) - called = true - return nil - } - ctx, err := runSyncToolsCommand(c, "-e", "test-target", "--local-dir", dir, "--public") - c.Assert(err, jc.ErrorIsNil) - c.Assert(ctx, gc.NotNil) - c.Assert(called, jc.IsTrue) -} - -func (s *syncToolsSuite) TestSyncToolsCommandDeprecatedDestination(c *gc.C) { - called := false - dir := c.MkDir() - syncTools = func(sctx *sync.SyncContext) error { - c.Assert(sctx.AllVersions, jc.IsFalse) - c.Assert(sctx.DryRun, jc.IsFalse) - c.Assert(sctx.Stream, gc.Equals, "released") - c.Assert(sctx.Source, gc.Equals, "") - c.Assert(sctx.TargetToolsUploader, gc.FitsTypeOf, sync.StorageToolsUploader{}) - uploader := sctx.TargetToolsUploader.(sync.StorageToolsUploader) - url, err := uploader.Storage.URL("") - c.Assert(err, jc.ErrorIsNil) - c.Assert(url, gc.Equals, utils.MakeFileURL(dir)) - called = true - return nil - } - // Register writer. - var tw loggo.TestWriter - c.Assert(loggo.RegisterWriter("deprecated-tester", &tw, loggo.DEBUG), gc.IsNil) - defer loggo.RemoveWriter("deprecated-tester") - // Add deprecated message to be checked. - messages := []jc.SimpleMessage{ - {loggo.WARNING, "Use of the --destination flag is deprecated in 1.18. Please use --local-dir instead."}, - } - // Run sync-tools command with --destination flag. - ctx, err := runSyncToolsCommand(c, "-e", "test-target", "--destination", dir, "--stream", "released") - c.Assert(err, jc.ErrorIsNil) - c.Assert(ctx, gc.NotNil) - c.Assert(called, jc.IsTrue) - // Check deprecated message was logged. - c.Check(tw.Log(), jc.LogMatches, messages) -} - -func (s *syncToolsSuite) TestAPIAdapterFindTools(c *gc.C) { - var called bool - result := coretools.List{&coretools.Tools{}} - fake := fakeSyncToolsAPI{ - findTools: func(majorVersion, minorVersion int, series, arch string) (params.FindToolsResult, error) { - called = true - c.Assert(majorVersion, gc.Equals, 2) - c.Assert(minorVersion, gc.Equals, -1) - c.Assert(series, gc.Equals, "") - c.Assert(arch, gc.Equals, "") - return params.FindToolsResult{List: result}, nil - }, - } - a := syncToolsAPIAdapter{&fake} - list, err := a.FindTools(2, "released") - c.Assert(err, jc.ErrorIsNil) - c.Assert(list, jc.SameContents, result) - c.Assert(called, jc.IsTrue) -} - -func (s *syncToolsSuite) TestAPIAdapterFindToolsNotFound(c *gc.C) { - fake := fakeSyncToolsAPI{ - findTools: func(majorVersion, minorVersion int, series, arch string) (params.FindToolsResult, error) { - err := common.ServerError(errors.NotFoundf("tools")) - return params.FindToolsResult{Error: err}, nil - }, - } - a := syncToolsAPIAdapter{&fake} - list, err := a.FindTools(1, "released") - c.Assert(err, gc.Equals, coretools.ErrNoMatches) - c.Assert(list, gc.HasLen, 0) -} - -func (s *syncToolsSuite) TestAPIAdapterFindToolsAPIError(c *gc.C) { - findToolsErr := common.ServerError(errors.NotFoundf("tools")) - fake := fakeSyncToolsAPI{ - findTools: func(majorVersion, minorVersion int, series, arch string) (params.FindToolsResult, error) { - return params.FindToolsResult{Error: findToolsErr}, findToolsErr - }, - } - a := syncToolsAPIAdapter{&fake} - list, err := a.FindTools(1, "released") - c.Assert(err, gc.Equals, findToolsErr) // error comes through untranslated - c.Assert(list, gc.HasLen, 0) -} - -func (s *syncToolsSuite) TestAPIAdapterUploadTools(c *gc.C) { - uploadToolsErr := errors.New("uh oh") - fake := fakeSyncToolsAPI{ - uploadTools: func(r io.Reader, v version.Binary, additionalSeries ...string) (*coretools.Tools, error) { - data, err := ioutil.ReadAll(r) - c.Assert(err, jc.ErrorIsNil) - c.Assert(string(data), gc.Equals, "abc") - c.Assert(v, gc.Equals, version.Current) - return nil, uploadToolsErr - }, - } - a := syncToolsAPIAdapter{&fake} - err := a.UploadTools("released", "released", &coretools.Tools{Version: version.Current}, []byte("abc")) - c.Assert(err, gc.Equals, uploadToolsErr) -} - -func (s *syncToolsSuite) TestAPIAdapterBlockUploadTools(c *gc.C) { - syncTools = func(sctx *sync.SyncContext) error { - // Block operation - return common.ErrOperationBlocked("TestAPIAdapterBlockUploadTools") - } - _, err := runSyncToolsCommand(c, "-e", "test-target", "--destination", c.MkDir(), "--stream", "released") - c.Assert(err, gc.ErrorMatches, cmd.ErrSilent.Error()) - // msg is logged - stripped := strings.Replace(c.GetTestLog(), "\n", "", -1) - c.Check(stripped, gc.Matches, ".*TestAPIAdapterBlockUploadTools.*") -} - -type fakeSyncToolsAPI struct { - findTools func(majorVersion, minorVersion int, series, arch string) (params.FindToolsResult, error) - uploadTools func(r io.Reader, v version.Binary, additionalSeries ...string) (*coretools.Tools, error) -} - -func (f *fakeSyncToolsAPI) FindTools(majorVersion, minorVersion int, series, arch string) (params.FindToolsResult, error) { - return f.findTools(majorVersion, minorVersion, series, arch) -} - -func (f *fakeSyncToolsAPI) UploadTools(r io.Reader, v version.Binary, additionalSeries ...string) (*coretools.Tools, error) { - return f.uploadTools(r, v, additionalSeries...) -} - -func (f *fakeSyncToolsAPI) Close() error { - return nil -} === added directory 'src/github.com/juju/juju/cmd/juju/system' === added file 'src/github.com/juju/juju/cmd/juju/system/createenvironment.go' --- src/github.com/juju/juju/cmd/juju/system/createenvironment.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/system/createenvironment.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,274 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package system + +import ( + "os" + "os/user" + "strings" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/names" + "github.com/juju/utils/keyvalues" + "gopkg.in/yaml.v1" + "launchpad.net/gnuflag" + + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/common" + "github.com/juju/juju/environs/config" + "github.com/juju/juju/environs/configstore" + localProvider "github.com/juju/juju/provider/local" +) + +// CreateEnvironmentCommand calls the API to create a new environment. +type CreateEnvironmentCommand struct { + envcmd.SysCommandBase + api CreateEnvironmentAPI + + name string + owner string + configFile cmd.FileVar + confValues map[string]string + configParser func(interface{}) (interface{}, error) +} + +const createEnvHelpDoc = ` +This command will create another environment within the current Juju +Environment Server. The provider has to match, and the environment config must +specify all the required configuration values for the provider. In the cases +of ‘ec2’ and ‘openstack’, the same environment variables are checked for the +access and secret keys. + +If configuration values are passed by both extra command line arguments and +the --config option, the command line args take priority. + +Examples: + + juju system create-environment new-env + + juju system create-environment new-env --config=aws-creds.yaml + +See Also: + juju help environment share +` + +func (c *CreateEnvironmentCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "create-environment", + Args: " [key=[value] ...]", + Purpose: "create an environment within the Juju Environment Server", + Doc: strings.TrimSpace(createEnvHelpDoc), + Aliases: []string{"create-env"}, + } +} + +func (c *CreateEnvironmentCommand) SetFlags(f *gnuflag.FlagSet) { + f.StringVar(&c.owner, "owner", "", "the owner of the new environment if not the current user") + f.Var(&c.configFile, "config", "path to yaml-formatted file containing environment config values") +} + +func (c *CreateEnvironmentCommand) Init(args []string) error { + if len(args) == 0 { + return errors.New("environment name is required") + } + c.name, args = args[0], args[1:] + + values, err := keyvalues.Parse(args, true) + if err != nil { + return err + } + c.confValues = values + + if c.owner != "" && !names.IsValidUser(c.owner) { + return errors.Errorf("%q is not a valid user", c.owner) + } + + if c.configParser == nil { + c.configParser = common.ConformYAML + } + + return nil +} + +type CreateEnvironmentAPI interface { + Close() error + ConfigSkeleton(provider, region string) (params.EnvironConfig, error) + CreateEnvironment(owner string, account, config map[string]interface{}) (params.Environment, error) +} + +func (c *CreateEnvironmentCommand) getAPI() (CreateEnvironmentAPI, error) { + if c.api != nil { + return c.api, nil + } + return c.NewEnvironmentManagerAPIClient() +} + +func (c *CreateEnvironmentCommand) Run(ctx *cmd.Context) (return_err error) { + client, err := c.getAPI() + if err != nil { + return err + } + defer client.Close() + + creds, err := c.ConnectionCredentials() + if err != nil { + return errors.Trace(err) + } + + creatingForSelf := true + envOwner := creds.User + if c.owner != "" { + owner := names.NewUserTag(c.owner) + user := names.NewUserTag(creds.User) + creatingForSelf = owner == user + envOwner = c.owner + } + + var info configstore.EnvironInfo + var endpoint configstore.APIEndpoint + if creatingForSelf { + logger.Debugf("create cache entry for %q", c.name) + // Create the configstore entry and write it to disk, as this will error + // if one with the same name already exists. + endpoint, err = c.ConnectionEndpoint() + if err != nil { + return errors.Trace(err) + } + + store, err := configstore.Default() + if err != nil { + return errors.Trace(err) + } + info = store.CreateInfo(c.name) + info.SetAPICredentials(creds) + endpoint.EnvironUUID = "" + if err := info.Write(); err != nil { + if errors.Cause(err) == configstore.ErrEnvironInfoAlreadyExists { + newErr := errors.AlreadyExistsf("environment %q", c.name) + return errors.Wrap(err, newErr) + } + return errors.Trace(err) + } + defer func() { + if return_err != nil { + logger.Debugf("error found, remove cache entry") + e := info.Destroy() + if e != nil { + logger.Errorf("could not remove environment file: %v", e) + } + } + }() + } else { + logger.Debugf("skipping cache entry for %q as owned %q", c.name, c.owner) + } + + serverSkeleton, err := client.ConfigSkeleton("", "") + if err != nil { + return errors.Trace(err) + } + + attrs, err := c.getConfigValues(ctx, serverSkeleton) + if err != nil { + return errors.Trace(err) + } + + // We pass nil through for the account details until we implement that bit. + env, err := client.CreateEnvironment(envOwner, nil, attrs) + if err != nil { + // cleanup configstore + return errors.Trace(err) + } + if creatingForSelf { + // update the cached details with the environment uuid + endpoint.EnvironUUID = env.UUID + info.SetAPIEndpoint(endpoint) + if err := info.Write(); err != nil { + return errors.Trace(err) + } + ctx.Infof("created environment %q", c.name) + return envcmd.SetCurrentEnvironment(ctx, c.name) + } else { + ctx.Infof("created environment %q for %q", c.name, c.owner) + } + + return nil +} + +func (c *CreateEnvironmentCommand) getConfigValues(ctx *cmd.Context, serverSkeleton params.EnvironConfig) (map[string]interface{}, error) { + // The reading of the config YAML is done in the Run + // method because the Read method requires the cmd Context + // for the current directory. + fileConfig := make(map[string]interface{}) + if c.configFile.Path != "" { + configYAML, err := c.configFile.Read(ctx) + if err != nil { + return nil, errors.Annotate(err, "unable to read config file") + } + + rawFileConfig := make(map[string]interface{}) + err = yaml.Unmarshal(configYAML, &rawFileConfig) + if err != nil { + return nil, errors.Annotate(err, "unable to parse config file") + } + + conformantConfig, err := c.configParser(rawFileConfig) + if err != nil { + return nil, errors.Annotate(err, "unable to parse config file") + } + betterConfig, ok := conformantConfig.(map[string]interface{}) + if !ok { + return nil, errors.New("config must contain a YAML map with string keys") + } + + fileConfig = betterConfig + } + + configValues := make(map[string]interface{}) + for key, value := range serverSkeleton { + configValues[key] = value + } + for key, value := range fileConfig { + configValues[key] = value + } + for key, value := range c.confValues { + configValues[key] = value + } + configValues["name"] = c.name + + if err := setConfigSpecialCaseDefaults(c.name, configValues); err != nil { + return nil, errors.Trace(err) + } + // TODO: allow version to be specified on the command line and add here. + cfg, err := config.New(config.UseDefaults, configValues) + if err != nil { + return nil, errors.Trace(err) + } + + return cfg.AllAttrs(), nil +} + +var userCurrent = user.Current + +func setConfigSpecialCaseDefaults(envName string, cfg map[string]interface{}) error { + // As a special case, the local provider's namespace value + // comes from the user's name and the environment name. + switch cfg["type"] { + case "local": + if _, ok := cfg[localProvider.NamespaceKey]; ok { + return nil + } + username := os.Getenv("USER") + if username == "" { + u, err := userCurrent() + if err != nil { + return errors.Annotatef(err, "failed to determine username for namespace") + } + username = u.Username + } + cfg[localProvider.NamespaceKey] = username + "-" + envName + } + return nil +} === added file 'src/github.com/juju/juju/cmd/juju/system/createenvironment_test.go' --- src/github.com/juju/juju/cmd/juju/system/createenvironment_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/system/createenvironment_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,439 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package system_test + +import ( + "io/ioutil" + "os" + "os/user" + + "github.com/juju/cmd" + "github.com/juju/errors" + jc "github.com/juju/testing/checkers" + "github.com/juju/utils" + gc "gopkg.in/check.v1" + "gopkg.in/yaml.v1" + + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/system" + "github.com/juju/juju/environs/configstore" + "github.com/juju/juju/feature" + "github.com/juju/juju/testing" +) + +type createSuite struct { + testing.FakeJujuHomeSuite + fake *fakeCreateClient + parser func(interface{}) (interface{}, error) + store configstore.Storage + serverUUID string + server configstore.EnvironInfo +} + +var _ = gc.Suite(&createSuite{}) + +func (s *createSuite) SetUpTest(c *gc.C) { + s.FakeJujuHomeSuite.SetUpTest(c) + s.SetFeatureFlags(feature.JES) + s.fake = &fakeCreateClient{} + s.parser = nil + store := configstore.Default + s.AddCleanup(func(*gc.C) { + configstore.Default = store + }) + s.store = configstore.NewMem() + configstore.Default = func() (configstore.Storage, error) { + return s.store, nil + } + // Set up the current environment, and write just enough info + // so we don't try to refresh + envName := "test-master" + s.serverUUID = "fake-server-uuid" + info := s.store.CreateInfo(envName) + info.SetAPIEndpoint(configstore.APIEndpoint{ + Addresses: []string{"localhost"}, + CACert: testing.CACert, + EnvironUUID: s.serverUUID, + ServerUUID: s.serverUUID, + }) + info.SetAPICredentials(configstore.APICredentials{User: "bob", Password: "sekrit"}) + err := info.Write() + c.Assert(err, jc.ErrorIsNil) + s.server = info + err = envcmd.WriteCurrentEnvironment(envName) + c.Assert(err, jc.ErrorIsNil) +} + +func (s *createSuite) run(c *gc.C, args ...string) (*cmd.Context, error) { + command := system.NewCreateEnvironmentCommand(s.fake, s.parser) + return testing.RunCommand(c, envcmd.WrapSystem(command), args...) +} + +func (s *createSuite) TestInit(c *gc.C) { + + for i, test := range []struct { + args []string + err string + name string + owner string + path string + values map[string]string + }{ + { + err: "environment name is required", + }, { + args: []string{"new-env"}, + name: "new-env", + }, { + args: []string{"new-env", "--owner", "foo"}, + name: "new-env", + owner: "foo", + }, { + args: []string{"new-env", "--owner", "not=valid"}, + err: `"not=valid" is not a valid user`, + }, { + args: []string{"new-env", "key=value", "key2=value2"}, + name: "new-env", + values: map[string]string{"key": "value", "key2": "value2"}, + }, { + args: []string{"new-env", "key=value", "key=value2"}, + err: `key "key" specified more than once`, + }, { + args: []string{"new-env", "another"}, + err: `expected "key=value", got "another"`, + }, { + args: []string{"new-env", "--config", "some-file"}, + name: "new-env", + path: "some-file", + }, + } { + c.Logf("test %d", i) + create := &system.CreateEnvironmentCommand{} + err := testing.InitCommand(create, test.args) + if test.err != "" { + c.Assert(err, gc.ErrorMatches, test.err) + continue + } + + c.Assert(err, jc.ErrorIsNil) + c.Assert(create.Name(), gc.Equals, test.name) + c.Assert(create.Owner(), gc.Equals, test.owner) + c.Assert(create.ConfigFile().Path, gc.Equals, test.path) + // The config value parse method returns an empty map + // if there were no values + if len(test.values) == 0 { + c.Assert(create.ConfValues(), gc.HasLen, 0) + } else { + c.Assert(create.ConfValues(), jc.DeepEquals, test.values) + } + } +} + +func (s *createSuite) TestCreateExistingName(c *gc.C) { + // Make a configstore entry with the same name. + info := s.store.CreateInfo("test") + err := info.Write() + c.Assert(err, jc.ErrorIsNil) + + _, err = s.run(c, "test") + c.Assert(err, gc.ErrorMatches, `environment "test" already exists`) +} + +func (s *createSuite) TestComandLineConfigPassedThrough(c *gc.C) { + _, err := s.run(c, "test", "account=magic", "cloud=special") + c.Assert(err, jc.ErrorIsNil) + + c.Assert(s.fake.config["account"], gc.Equals, "magic") + c.Assert(s.fake.config["cloud"], gc.Equals, "special") +} + +func (s *createSuite) TestConfigFileValuesPassedThrough(c *gc.C) { + config := map[string]string{ + "account": "magic", + "cloud": "9", + } + bytes, err := yaml.Marshal(config) + c.Assert(err, jc.ErrorIsNil) + file, err := ioutil.TempFile(c.MkDir(), "") + c.Assert(err, jc.ErrorIsNil) + file.Write(bytes) + file.Close() + + _, err = s.run(c, "test", "--config", file.Name()) + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.fake.config["account"], gc.Equals, "magic") + c.Assert(s.fake.config["cloud"], gc.Equals, "9") +} + +func (s *createSuite) TestConfigFileWithNestedMaps(c *gc.C) { + nestedConfig := map[string]interface{}{ + "account": "magic", + "cloud": "9", + } + config := map[string]interface{}{ + "foo": "bar", + "nested": nestedConfig, + } + + bytes, err := yaml.Marshal(config) + c.Assert(err, jc.ErrorIsNil) + file, err := ioutil.TempFile(c.MkDir(), "") + c.Assert(err, jc.ErrorIsNil) + file.Write(bytes) + file.Close() + + _, err = s.run(c, "test", "--config", file.Name()) + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.fake.config["foo"], gc.Equals, "bar") + c.Assert(s.fake.config["nested"], jc.DeepEquals, nestedConfig) +} + +func (s *createSuite) TestConfigFileFailsToConform(c *gc.C) { + nestedConfig := map[int]interface{}{ + 9: "9", + } + config := map[string]interface{}{ + "foo": "bar", + "nested": nestedConfig, + } + bytes, err := yaml.Marshal(config) + c.Assert(err, jc.ErrorIsNil) + file, err := ioutil.TempFile(c.MkDir(), "") + c.Assert(err, jc.ErrorIsNil) + file.Write(bytes) + file.Close() + + _, err = s.run(c, "test", "--config", file.Name()) + c.Assert(err, gc.ErrorMatches, `unable to parse config file: map keyed with non-string value`) +} + +func (s *createSuite) TestConfigFileFailsWithUnknownType(c *gc.C) { + config := map[string]interface{}{ + "account": "magic", + "cloud": "9", + } + + bytes, err := yaml.Marshal(config) + c.Assert(err, jc.ErrorIsNil) + file, err := ioutil.TempFile(c.MkDir(), "") + c.Assert(err, jc.ErrorIsNil) + file.Write(bytes) + file.Close() + + s.parser = func(interface{}) (interface{}, error) { return "not a map", nil } + _, err = s.run(c, "test", "--config", file.Name()) + c.Assert(err, gc.ErrorMatches, `config must contain a YAML map with string keys`) +} + +func (s *createSuite) TestConfigFileFormatError(c *gc.C) { + file, err := ioutil.TempFile(c.MkDir(), "") + c.Assert(err, jc.ErrorIsNil) + file.Write(([]byte)("not: valid: yaml")) + file.Close() + + _, err = s.run(c, "test", "--config", file.Name()) + c.Assert(err, gc.ErrorMatches, `unable to parse config file: YAML error: .*`) +} + +func (s *createSuite) TestConfigFileDoesntExist(c *gc.C) { + _, err := s.run(c, "test", "--config", "missing-file") + errMsg := ".*" + utils.NoSuchFileErrRegexp + c.Assert(err, gc.ErrorMatches, errMsg) +} + +func (s *createSuite) TestConfigValuePrecedence(c *gc.C) { + config := map[string]string{ + "account": "magic", + "cloud": "9", + } + bytes, err := yaml.Marshal(config) + c.Assert(err, jc.ErrorIsNil) + file, err := ioutil.TempFile(c.MkDir(), "") + c.Assert(err, jc.ErrorIsNil) + file.Write(bytes) + file.Close() + + _, err = s.run(c, "test", "--config", file.Name(), "account=magic", "cloud=special") + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.fake.config["account"], gc.Equals, "magic") + c.Assert(s.fake.config["cloud"], gc.Equals, "special") +} + +var setConfigSpecialCaseDefaultsTests = []struct { + about string + userEnvVar string + userCurrent func() (*user.User, error) + config map[string]interface{} + expectConfig map[string]interface{} + expectError string +}{{ + about: "use env var if available", + userEnvVar: "bob", + config: map[string]interface{}{ + "name": "envname", + "type": "local", + }, + expectConfig: map[string]interface{}{ + "name": "envname", + "type": "local", + "namespace": "bob-envname", + }, +}, { + about: "fall back to user.Current", + userCurrent: func() (*user.User, error) { + return &user.User{Username: "bob"}, nil + }, + config: map[string]interface{}{ + "name": "envname", + "type": "local", + }, + expectConfig: map[string]interface{}{ + "name": "envname", + "type": "local", + "namespace": "bob-envname", + }, +}, { + about: "other provider types unaffected", + userEnvVar: "bob", + config: map[string]interface{}{ + "name": "envname", + "type": "dummy", + }, + expectConfig: map[string]interface{}{ + "name": "envname", + "type": "dummy", + }, +}, { + about: "explicit namespace takes precedence", + userCurrent: func() (*user.User, error) { + return &user.User{Username: "bob"}, nil + }, + config: map[string]interface{}{ + "name": "envname", + "namespace": "something", + "type": "local", + }, + expectConfig: map[string]interface{}{ + "name": "envname", + "namespace": "something", + "type": "local", + }, +}, { + about: "user.Current returns error", + userCurrent: func() (*user.User, error) { + return nil, errors.New("an error") + }, + config: map[string]interface{}{ + "name": "envname", + "type": "local", + }, + expectError: "failed to determine username for namespace: an error", +}} + +func (s *createSuite) TestSetConfigSpecialCaseDefaults(c *gc.C) { + noUserCurrent := func() (*user.User, error) { + panic("should not be called") + } + s.PatchValue(system.UserCurrent, noUserCurrent) + // We test setConfigSpecialCaseDefaults independently + // because we can't use the local provider in the tests. + for i, test := range setConfigSpecialCaseDefaultsTests { + c.Logf("test %d: %s", i, test.about) + os.Setenv("USER", test.userEnvVar) + if test.userCurrent != nil { + *system.UserCurrent = test.userCurrent + } else { + *system.UserCurrent = noUserCurrent + } + err := system.SetConfigSpecialCaseDefaults(test.config["name"].(string), test.config) + if test.expectError != "" { + c.Assert(err, gc.ErrorMatches, test.expectError) + } else { + c.Assert(err, gc.IsNil) + c.Assert(test.config, jc.DeepEquals, test.expectConfig) + } + } + +} + +func (s *createSuite) TestCreateErrorRemoveConfigstoreInfo(c *gc.C) { + s.fake.err = errors.New("bah humbug") + + _, err := s.run(c, "test") + c.Assert(err, gc.ErrorMatches, "bah humbug") + + _, err = s.store.ReadInfo("test") + c.Assert(err, gc.ErrorMatches, `environment "test" not found`) +} + +func (s *createSuite) TestCreateStoresValues(c *gc.C) { + s.fake.env = params.Environment{ + Name: "test", + UUID: "fake-env-uuid", + OwnerTag: "ignored-for-now", + ServerUUID: s.serverUUID, + } + _, err := s.run(c, "test") + c.Assert(err, jc.ErrorIsNil) + + info, err := s.store.ReadInfo("test") + c.Assert(err, jc.ErrorIsNil) + // Stores the credentials of the original environment + c.Assert(info.APICredentials(), jc.DeepEquals, s.server.APICredentials()) + endpoint := info.APIEndpoint() + expected := s.server.APIEndpoint() + c.Assert(endpoint.Addresses, jc.DeepEquals, expected.Addresses) + c.Assert(endpoint.Hostnames, jc.DeepEquals, expected.Hostnames) + c.Assert(endpoint.ServerUUID, gc.Equals, expected.ServerUUID) + c.Assert(endpoint.CACert, gc.Equals, expected.CACert) + c.Assert(endpoint.EnvironUUID, gc.Equals, "fake-env-uuid") +} + +func (s *createSuite) TestNoEnvCacheOtherUser(c *gc.C) { + s.fake.env = params.Environment{ + Name: "test", + UUID: "fake-env-uuid", + OwnerTag: "ignored-for-now", + ServerUUID: s.serverUUID, + } + _, err := s.run(c, "test", "--owner", "zeus") + c.Assert(err, jc.ErrorIsNil) + + _, err = s.store.ReadInfo("test") + c.Assert(err, gc.ErrorMatches, `environment "test" not found`) +} + +// fakeCreateClient is used to mock out the behavior of the real +// CreateEnvironment command. +type fakeCreateClient struct { + owner string + account map[string]interface{} + config map[string]interface{} + err error + env params.Environment +} + +var _ system.CreateEnvironmentAPI = (*fakeCreateClient)(nil) + +func (*fakeCreateClient) Close() error { + return nil +} + +func (*fakeCreateClient) ConfigSkeleton(provider, region string) (params.EnvironConfig, error) { + return params.EnvironConfig{ + "type": "dummy", + "state-server": false, + }, nil +} +func (f *fakeCreateClient) CreateEnvironment(owner string, account, config map[string]interface{}) (params.Environment, error) { + var env params.Environment + if f.err != nil { + return env, f.err + } + f.owner = owner + f.account = account + f.config = config + return f.env, nil +} === added file 'src/github.com/juju/juju/cmd/juju/system/destroy.go' --- src/github.com/juju/juju/cmd/juju/system/destroy.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/system/destroy.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,327 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package system + +import ( + "bufio" + "bytes" + "fmt" + "io" + "strings" + "text/tabwriter" + + "github.com/juju/cmd" + "github.com/juju/errors" + "launchpad.net/gnuflag" + + "github.com/juju/juju/api/systemmanager" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/block" + "github.com/juju/juju/environs" + "github.com/juju/juju/environs/config" + "github.com/juju/juju/environs/configstore" + "github.com/juju/juju/juju" +) + +// DestroyCommand destroys the specified system. +type DestroyCommand struct { + DestroyCommandBase + destroyEnvs bool +} + +var destroyDoc = `Destroys the specified system` +var destroySysMsg = ` +WARNING! This command will destroy the %q system. +This includes all machines, services, data and other resources. + +Continue [y/N]? `[1:] + +// destroySystemAPI defines the methods on the system manager API endpoint +// that the destroy command calls. +type destroySystemAPI interface { + Close() error + EnvironmentConfig() (map[string]interface{}, error) + DestroySystem(destroyEnvs bool, ignoreBlocks bool) error + ListBlockedEnvironments() ([]params.EnvironmentBlockInfo, error) +} + +// destroyClientAPI defines the methods on the client API endpoint that the +// destroy command might call. +type destroyClientAPI interface { + Close() error + EnvironmentGet() (map[string]interface{}, error) + DestroyEnvironment() error +} + +// Info implements Command.Info. +func (c *DestroyCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "destroy", + Args: "", + Purpose: "terminate all machines and other associated resources for a system environment", + Doc: destroyDoc, + } +} + +// SetFlags implements Command.SetFlags. +func (c *DestroyCommand) SetFlags(f *gnuflag.FlagSet) { + f.BoolVar(&c.destroyEnvs, "destroy-all-environments", false, "destroy all hosted environments on the system") + c.DestroyCommandBase.SetFlags(f) +} + +func (c *DestroyCommand) getSystemAPI() (destroySystemAPI, error) { + if c.api != nil { + return c.api, c.apierr + } + root, err := juju.NewAPIFromName(c.systemName) + if err != nil { + return nil, errors.Trace(err) + } + + return systemmanager.NewClient(root), nil +} + +// Run implements Command.Run +func (c *DestroyCommand) Run(ctx *cmd.Context) error { + store, err := configstore.Default() + if err != nil { + return errors.Annotate(err, "cannot open system info storage") + } + + cfgInfo, err := store.ReadInfo(c.systemName) + if err != nil { + return errors.Annotate(err, "cannot read system info") + } + + // Verify that we're destroying a system + apiEndpoint := cfgInfo.APIEndpoint() + if apiEndpoint.ServerUUID != "" && apiEndpoint.EnvironUUID != apiEndpoint.ServerUUID { + return errors.Errorf("%q is not a system; use juju environment destroy to destroy it", c.systemName) + } + + if !c.assumeYes { + if err = confirmDestruction(ctx, c.systemName); err != nil { + return err + } + } + + // Attempt to connect to the API. If we can't, fail the destroy. Users will + // need to use the system kill command if we can't connect. + api, err := c.getSystemAPI() + if err != nil { + return c.ensureUserFriendlyErrorLog(errors.Annotate(err, "cannot connect to API"), ctx, nil) + } + defer api.Close() + + // Obtain bootstrap / system environ information + systemEnviron, err := c.getSystemEnviron(cfgInfo, api) + if err != nil { + return errors.Annotate(err, "cannot obtain bootstrap information") + } + + // Attempt to destroy the system. + err = api.DestroySystem(c.destroyEnvs, false) + if params.IsCodeNotImplemented(err) { + // Fall back to using the client endpoint to destroy the system, + // sending the info we were already able to collect. + return c.destroySystemViaClient(ctx, cfgInfo, systemEnviron, store) + } + if err != nil { + return c.ensureUserFriendlyErrorLog(errors.Annotate(err, "cannot destroy system"), ctx, api) + } + + return environs.Destroy(systemEnviron, store) +} + +// destroySystemViaClient attempts to destroy the system using the client +// endpoint for older juju systems which do not implement systemmanager.DestroySystem +func (c *DestroyCommand) destroySystemViaClient(ctx *cmd.Context, info configstore.EnvironInfo, systemEnviron environs.Environ, store configstore.Storage) error { + api, err := c.getClientAPI() + if err != nil { + return c.ensureUserFriendlyErrorLog(errors.Annotate(err, "cannot connect to API"), ctx, nil) + } + defer api.Close() + + err = api.DestroyEnvironment() + if err != nil { + return c.ensureUserFriendlyErrorLog(errors.Annotate(err, "cannot destroy system"), ctx, nil) + } + + return environs.Destroy(systemEnviron, store) +} + +// ensureUserFriendlyErrorLog ensures that error will be logged and displayed +// in a user-friendly manner with readable and digestable error message. +func (c *DestroyCommand) ensureUserFriendlyErrorLog(destroyErr error, ctx *cmd.Context, api destroySystemAPI) error { + if destroyErr == nil { + return nil + } + if params.IsCodeOperationBlocked(destroyErr) { + logger.Errorf(`there are blocks preventing system destruction +To remove all blocks in the system, please run: + + juju system remove-blocks + +`) + if api != nil { + envs, err := api.ListBlockedEnvironments() + var bytes []byte + if err == nil { + bytes, err = formatTabularBlockedEnvironments(envs) + } + + if err != nil { + logger.Errorf("Unable to list blocked environments: %s", err) + return cmd.ErrSilent + } + ctx.Infof(string(bytes)) + } + return cmd.ErrSilent + } + logger.Errorf(stdFailureMsg, c.systemName) + return destroyErr +} + +var stdFailureMsg = `failed to destroy system %q + +If the system is unusable, then you may run + + juju system kill + +to forcibly destroy the system. Upon doing so, review +your environment provider console for any resources that need +to be cleaned up. +` + +func formatTabularBlockedEnvironments(value interface{}) ([]byte, error) { + envs, ok := value.([]params.EnvironmentBlockInfo) + if !ok { + return nil, errors.Errorf("expected value of type %T, got %T", envs, 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) + fmt.Fprintf(tw, "NAME\tENVIRONMENT UUID\tOWNER\tBLOCKS\n") + for _, env := range envs { + fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", env.Name, env.UUID, env.OwnerTag, blocksToStr(env.Blocks)) + } + tw.Flush() + return out.Bytes(), nil +} + +func blocksToStr(blocks []string) string { + result := "" + sep := "" + for _, blk := range blocks { + result = result + sep + block.OperationFromType(blk) + sep = "," + } + + return result +} + +// DestroyCommandBase provides common attributes and methods that both the system +// destroy and system kill commands require. +type DestroyCommandBase struct { + envcmd.SysCommandBase + systemName string + assumeYes bool + + // The following fields are for mocking out + // api behavior for testing. + api destroySystemAPI + apierr error + clientapi destroyClientAPI +} + +func (c *DestroyCommandBase) getClientAPI() (destroyClientAPI, error) { + if c.clientapi != nil { + return c.clientapi, nil + } + root, err := juju.NewAPIFromName(c.systemName) + if err != nil { + return nil, errors.Trace(err) + } + return root.Client(), nil +} + +// SetFlags implements Command.SetFlags. +func (c *DestroyCommandBase) SetFlags(f *gnuflag.FlagSet) { + f.BoolVar(&c.assumeYes, "y", false, "Do not ask for confirmation") + f.BoolVar(&c.assumeYes, "yes", false, "") +} + +// Init implements Command.Init. +func (c *DestroyCommandBase) Init(args []string) error { + switch len(args) { + case 0: + return errors.New("no system specified") + case 1: + c.systemName = args[0] + return nil + default: + return cmd.CheckEmpty(args[1:]) + } +} + +// getSystemEnviron gets the bootstrap information required to destroy the environment +// by first checking the config store, then querying the API if the information is not +// in the store. +func (c *DestroyCommandBase) getSystemEnviron(info configstore.EnvironInfo, sysAPI destroySystemAPI) (_ environs.Environ, err error) { + bootstrapCfg := info.BootstrapConfig() + if bootstrapCfg == nil { + if sysAPI == nil { + return nil, errors.New("unable to get bootstrap information from API") + } + bootstrapCfg, err = sysAPI.EnvironmentConfig() + if params.IsCodeNotImplemented(err) { + // Fallback to the client API. Better to encapsulate the logic for + // old servers than worry about connecting twice. + client, err := c.getClientAPI() + if err != nil { + return nil, errors.Trace(err) + } + defer client.Close() + bootstrapCfg, err = client.EnvironmentGet() + if err != nil { + return nil, errors.Trace(err) + } + } else if err != nil { + return nil, errors.Trace(err) + } + } + + cfg, err := config.New(config.NoDefaults, bootstrapCfg) + if err != nil { + return nil, errors.Trace(err) + } + return environs.New(cfg) +} + +func confirmDestruction(ctx *cmd.Context, systemName string) error { + // Get confirmation from the user that they want to continue + fmt.Fprintf(ctx.Stdout, destroySysMsg, systemName) + + scanner := bufio.NewScanner(ctx.Stdin) + scanner.Scan() + err := scanner.Err() + if err != nil && err != io.EOF { + return errors.Annotate(err, "system destruction aborted") + } + answer := strings.ToLower(scanner.Text()) + if answer != "y" && answer != "yes" { + return errors.New("system destruction aborted") + } + + return nil +} === added file 'src/github.com/juju/juju/cmd/juju/system/destroy_test.go' --- src/github.com/juju/juju/cmd/juju/system/destroy_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/system/destroy_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,362 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package system_test + +import ( + "bytes" + "time" + + "github.com/juju/cmd" + "github.com/juju/errors" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/juju/system" + cmdtesting "github.com/juju/juju/cmd/testing" + "github.com/juju/juju/environs/config" + "github.com/juju/juju/environs/configstore" + _ "github.com/juju/juju/provider/dummy" + "github.com/juju/juju/testing" +) + +type DestroySuite struct { + testing.FakeJujuHomeSuite + api *fakeDestroyAPI + clientapi *fakeDestroyAPIClient + store configstore.Storage + apierror error +} + +var _ = gc.Suite(&DestroySuite{}) + +// fakeDestroyAPI mocks out the systemmanager API +type fakeDestroyAPI struct { + err error + env map[string]interface{} + destroyAll bool + ignoreBlocks bool + blocks []params.EnvironmentBlockInfo + blocksErr error +} + +func (f *fakeDestroyAPI) Close() error { return nil } + +func (f *fakeDestroyAPI) EnvironmentConfig() (map[string]interface{}, error) { + if f.err != nil { + return nil, f.err + } + return f.env, nil +} + +func (f *fakeDestroyAPI) DestroySystem(destroyAll bool, ignoreBlocks bool) error { + f.destroyAll = destroyAll + f.ignoreBlocks = ignoreBlocks + return f.err +} + +func (f *fakeDestroyAPI) ListBlockedEnvironments() ([]params.EnvironmentBlockInfo, error) { + return f.blocks, f.blocksErr +} + +// fakeDestroyAPIClient mocks out the client API +type fakeDestroyAPIClient struct { + err error + env map[string]interface{} + envgetcalled bool + destroycalled bool +} + +func (f *fakeDestroyAPIClient) Close() error { return nil } + +func (f *fakeDestroyAPIClient) EnvironmentGet() (map[string]interface{}, error) { + f.envgetcalled = true + if f.err != nil { + return nil, f.err + } + return f.env, nil +} + +func (f *fakeDestroyAPIClient) DestroyEnvironment() error { + f.destroycalled = true + return f.err +} + +func createBootstrapInfo(c *gc.C, name string) map[string]interface{} { + cfg, err := config.New(config.UseDefaults, map[string]interface{}{ + "type": "dummy", + "name": name, + "state-server": "true", + "state-id": "1", + }) + c.Assert(err, jc.ErrorIsNil) + return cfg.AllAttrs() +} + +func (s *DestroySuite) SetUpTest(c *gc.C) { + s.FakeJujuHomeSuite.SetUpTest(c) + s.clientapi = &fakeDestroyAPIClient{} + s.api = &fakeDestroyAPI{} + s.apierror = nil + + var err error + s.store, err = configstore.Default() + c.Assert(err, jc.ErrorIsNil) + + var envList = []struct { + name string + serverUUID string + envUUID string + bootstrapCfg map[string]interface{} + }{ + { + name: "test1", + serverUUID: "test1-uuid", + envUUID: "test1-uuid", + bootstrapCfg: createBootstrapInfo(c, "test1"), + }, { + name: "test2", + serverUUID: "test1-uuid", + envUUID: "test2-uuid", + }, { + name: "test3", + envUUID: "test3-uuid", + }, + } + for _, env := range envList { + info := s.store.CreateInfo(env.name) + info.SetAPIEndpoint(configstore.APIEndpoint{ + Addresses: []string{"localhost"}, + CACert: testing.CACert, + EnvironUUID: env.envUUID, + ServerUUID: env.serverUUID, + }) + + if env.bootstrapCfg != nil { + info.SetBootstrapConfig(env.bootstrapCfg) + } + err := info.Write() + c.Assert(err, jc.ErrorIsNil) + } +} + +func (s *DestroySuite) runDestroyCommand(c *gc.C, args ...string) (*cmd.Context, error) { + cmd := system.NewDestroyCommand(s.api, s.clientapi, s.apierror) + return testing.RunCommand(c, cmd, args...) +} + +func (s *DestroySuite) newDestroyCommand() *system.DestroyCommand { + return system.NewDestroyCommand(s.api, s.clientapi, s.apierror) +} + +func checkSystemExistsInStore(c *gc.C, name string, store configstore.Storage) { + _, err := store.ReadInfo(name) + c.Check(err, jc.ErrorIsNil) +} + +func checkSystemRemovedFromStore(c *gc.C, name string, store configstore.Storage) { + _, err := store.ReadInfo(name) + c.Check(err, jc.Satisfies, errors.IsNotFound) +} + +func (s *DestroySuite) TestDestroyNoSystemNameError(c *gc.C) { + _, err := s.runDestroyCommand(c) + c.Assert(err, gc.ErrorMatches, "no system specified") +} + +func (s *DestroySuite) TestDestroyBadFlags(c *gc.C) { + _, err := s.runDestroyCommand(c, "-n") + c.Assert(err, gc.ErrorMatches, "flag provided but not defined: -n") +} + +func (s *DestroySuite) TestDestroyUnknownArgument(c *gc.C) { + _, err := s.runDestroyCommand(c, "environment", "whoops") + c.Assert(err, gc.ErrorMatches, `unrecognized args: \["whoops"\]`) +} + +func (s *DestroySuite) TestDestroyUnknownSystem(c *gc.C) { + _, err := s.runDestroyCommand(c, "foo") + c.Assert(err, gc.ErrorMatches, `cannot read system info: environment "foo" not found`) +} + +func (s *DestroySuite) TestDestroyNonSystemEnvFails(c *gc.C) { + _, err := s.runDestroyCommand(c, "test2") + c.Assert(err, gc.ErrorMatches, "\"test2\" is not a system; use juju environment destroy to destroy it") +} + +func (s *DestroySuite) TestDestroySystemNotFoundNotRemovedFromStore(c *gc.C) { + s.apierror = errors.NotFoundf("test1") + _, err := s.runDestroyCommand(c, "test1", "-y") + c.Assert(err, gc.ErrorMatches, "cannot connect to API: test1 not found") + c.Check(c.GetTestLog(), jc.Contains, "If the system is unusable") + checkSystemExistsInStore(c, "test1", s.store) +} + +func (s *DestroySuite) TestDestroyCannotConnectToAPI(c *gc.C) { + s.apierror = errors.New("connection refused") + _, err := s.runDestroyCommand(c, "test1", "-y") + c.Assert(err, gc.ErrorMatches, "cannot connect to API: connection refused") + c.Check(c.GetTestLog(), jc.Contains, "If the system is unusable") + checkSystemExistsInStore(c, "test1", s.store) +} + +func (s *DestroySuite) TestDestroy(c *gc.C) { + _, err := s.runDestroyCommand(c, "test1", "-y") + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.api.ignoreBlocks, jc.IsFalse) + c.Assert(s.api.destroyAll, jc.IsFalse) + c.Assert(s.clientapi.destroycalled, jc.IsFalse) + checkSystemRemovedFromStore(c, "test1", s.store) +} + +func (s *DestroySuite) TestDestroyWithDestroyAllEnvsFlag(c *gc.C) { + _, err := s.runDestroyCommand(c, "test1", "-y", "--destroy-all-environments") + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.api.ignoreBlocks, jc.IsFalse) + c.Assert(s.api.destroyAll, jc.IsTrue) + checkSystemRemovedFromStore(c, "test1", s.store) +} + +func (s *DestroySuite) TestDestroyEnvironmentGetFails(c *gc.C) { + s.api.err = errors.NotFoundf(`system "test3"`) + _, err := s.runDestroyCommand(c, "test3", "-y") + c.Assert(err, gc.ErrorMatches, "cannot obtain bootstrap information: system \"test3\" not found") + checkSystemExistsInStore(c, "test3", s.store) +} + +func (s *DestroySuite) TestDestroyFallsBackToClient(c *gc.C) { + s.api.err = ¶ms.Error{"DestroyEnvironment", params.CodeNotImplemented} + _, err := s.runDestroyCommand(c, "test1", "-y") + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.clientapi.destroycalled, jc.IsTrue) + checkSystemRemovedFromStore(c, "test1", s.store) +} + +func (s *DestroySuite) TestEnvironmentGetFallsBackToClient(c *gc.C) { + s.api.err = ¶ms.Error{"EnvironmentGet", params.CodeNotImplemented} + s.clientapi.env = createBootstrapInfo(c, "test3") + _, err := s.runDestroyCommand(c, "test3", "-y") + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.clientapi.envgetcalled, jc.IsTrue) + c.Assert(s.clientapi.destroycalled, jc.IsTrue) + checkSystemRemovedFromStore(c, "test3", s.store) +} + +func (s *DestroySuite) TestFailedDestroyEnvironment(c *gc.C) { + s.api.err = errors.New("permission denied") + _, err := s.runDestroyCommand(c, "test1", "-y") + c.Assert(err, gc.ErrorMatches, "cannot destroy system: permission denied") + c.Assert(s.api.ignoreBlocks, jc.IsFalse) + c.Assert(s.api.destroyAll, jc.IsFalse) + checkSystemExistsInStore(c, "test1", s.store) +} + +func (s *DestroySuite) resetSystem(c *gc.C) { + info := s.store.CreateInfo("test1") + info.SetAPIEndpoint(configstore.APIEndpoint{ + Addresses: []string{"localhost"}, + CACert: testing.CACert, + EnvironUUID: "test1-uuid", + ServerUUID: "test1-uuid", + }) + info.SetBootstrapConfig(createBootstrapInfo(c, "test1")) + err := info.Write() + c.Assert(err, jc.ErrorIsNil) +} + +func (s *DestroySuite) TestDestroyCommandConfirmation(c *gc.C) { + var stdin, stdout bytes.Buffer + ctx, err := cmd.DefaultContext() + c.Assert(err, jc.ErrorIsNil) + ctx.Stdout = &stdout + ctx.Stdin = &stdin + + // Ensure confirmation is requested if "-y" is not specified. + stdin.WriteString("n") + _, errc := cmdtesting.RunCommand(ctx, s.newDestroyCommand(), "test1") + select { + case err := <-errc: + c.Check(err, gc.ErrorMatches, "system destruction aborted") + case <-time.After(testing.LongWait): + c.Fatalf("command took too long") + } + c.Check(testing.Stdout(ctx), gc.Matches, "WARNING!.*test1(.|\n)*") + checkSystemExistsInStore(c, "test1", s.store) + + // EOF on stdin: equivalent to answering no. + stdin.Reset() + stdout.Reset() + _, errc = cmdtesting.RunCommand(ctx, s.newDestroyCommand(), "test1") + select { + case err := <-errc: + c.Check(err, gc.ErrorMatches, "system destruction aborted") + case <-time.After(testing.LongWait): + c.Fatalf("command took too long") + } + c.Check(testing.Stdout(ctx), gc.Matches, "WARNING!.*test1(.|\n)*") + checkSystemExistsInStore(c, "test1", s.store) + + for _, answer := range []string{"y", "Y", "yes", "YES"} { + stdin.Reset() + stdout.Reset() + stdin.WriteString(answer) + _, errc = cmdtesting.RunCommand(ctx, s.newDestroyCommand(), "test1") + select { + case err := <-errc: + c.Check(err, jc.ErrorIsNil) + case <-time.After(testing.LongWait): + c.Fatalf("command took too long") + } + checkSystemRemovedFromStore(c, "test1", s.store) + + // Add the test1 system back into the store for the next test + s.resetSystem(c) + } +} + +func (s *DestroySuite) TestBlockedDestroy(c *gc.C) { + s.api.err = ¶ms.Error{Code: params.CodeOperationBlocked} + s.runDestroyCommand(c, "test1", "-y") + testLog := c.GetTestLog() + c.Check(testLog, jc.Contains, "To remove all blocks in the system, please run:") + c.Check(testLog, jc.Contains, "juju system remove-blocks") +} + +func (s *DestroySuite) TestDestroyListBlocksError(c *gc.C) { + s.api.err = ¶ms.Error{Code: params.CodeOperationBlocked} + s.api.blocksErr = errors.New("unexpected api error") + s.runDestroyCommand(c, "test1", "-y") + testLog := c.GetTestLog() + c.Check(testLog, jc.Contains, "To remove all blocks in the system, please run:") + c.Check(testLog, jc.Contains, "juju system remove-blocks") + c.Check(testLog, jc.Contains, "Unable to list blocked environments: unexpected api error") +} + +func (s *DestroySuite) TestDestroyReturnsBlocks(c *gc.C) { + s.api.err = ¶ms.Error{Code: params.CodeOperationBlocked} + s.api.blocks = []params.EnvironmentBlockInfo{ + params.EnvironmentBlockInfo{ + Name: "test1", + UUID: "test1-uuid", + OwnerTag: "cheryl@local", + Blocks: []string{ + "BlockDestroy", + }, + }, + params.EnvironmentBlockInfo{ + Name: "test2", + UUID: "test2-uuid", + OwnerTag: "bob@local", + Blocks: []string{ + "BlockDestroy", + "BlockChange", + }, + }, + } + ctx, _ := s.runDestroyCommand(c, "test1", "-y", "--destroy-all-environments") + c.Assert(testing.Stderr(ctx), gc.Equals, ""+ + "NAME ENVIRONMENT UUID OWNER BLOCKS\n"+ + "test1 test1-uuid cheryl@local destroy-environment\n"+ + "test2 test2-uuid bob@local destroy-environment,all-changes\n") +} === added file 'src/github.com/juju/juju/cmd/juju/system/environments.go' --- src/github.com/juju/juju/cmd/juju/system/environments.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/system/environments.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,198 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package system + +import ( + "bytes" + "fmt" + "text/tabwriter" + "time" + + "github.com/juju/cmd" + "github.com/juju/errors" + "launchpad.net/gnuflag" + + "github.com/juju/juju/api/base" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/user" + "github.com/juju/juju/environs/configstore" +) + +// EnvironmentsCommand returns the list of all the environments the +// current user can access on the current system. +type EnvironmentsCommand struct { + envcmd.SysCommandBase + out cmd.Output + all bool + user string + listUUID bool + exactTime bool + envAPI EnvironmentsEnvAPI + sysAPI EnvironmentsSysAPI + userCreds *configstore.APICredentials +} + +var envsDoc = ` +List all the environments the user can access on the current system. + +The environments listed here are either environments you have created +yourself, or environments which have been shared with you. + +See Also: + juju help juju-systems + juju help systems + juju help environment users + juju help environment share + juju help environment unshare +` + +// EnvironmentsEnvAPI defines the methods on the environment manager API that +// the environments command calls. +type EnvironmentsEnvAPI interface { + Close() error + ListEnvironments(user string) ([]base.UserEnvironment, error) +} + +// EnvironmentsSysAPI defines the methods on the system manager API that the +// environments command calls. +type EnvironmentsSysAPI interface { + Close() error + AllEnvironments() ([]base.UserEnvironment, error) +} + +// Info implements Command.Info +func (c *EnvironmentsCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "environments", + Purpose: "list all environments the user can access on the current system", + Doc: envsDoc, + } +} + +func (c *EnvironmentsCommand) getEnvAPI() (EnvironmentsEnvAPI, error) { + if c.envAPI != nil { + return c.envAPI, nil + } + return c.NewEnvironmentManagerAPIClient() +} + +func (c *EnvironmentsCommand) getSysAPI() (EnvironmentsSysAPI, error) { + if c.sysAPI != nil { + return c.sysAPI, nil + } + return c.NewSystemManagerAPIClient() +} + +func (c *EnvironmentsCommand) getConnectionCredentials() (configstore.APICredentials, error) { + if c.userCreds != nil { + return *c.userCreds, nil + } + return c.ConnectionCredentials() +} + +// SetFlags implements Command.SetFlags. +func (c *EnvironmentsCommand) SetFlags(f *gnuflag.FlagSet) { + f.StringVar(&c.user, "user", "", "the user to list environments for (administrative users only)") + f.BoolVar(&c.all, "all", false, "show all environments (administrative users only)") + f.BoolVar(&c.listUUID, "uuid", false, "display UUID for environments") + f.BoolVar(&c.exactTime, "exact-time", false, "use full timestamp precision") + c.out.AddFlags(f, "tabular", map[string]cmd.Formatter{ + "yaml": cmd.FormatYaml, + "json": cmd.FormatJson, + "tabular": c.formatTabular, + }) +} + +// Local structure that controls the output structure. +type UserEnvironment struct { + Name string `json:"name"` + UUID string `json:"env-uuid" yaml:"env-uuid"` + Owner string `json:"owner"` + LastConnection string `json:"last-connection" yaml:"last-connection"` +} + +// Run implements Command.Run +func (c *EnvironmentsCommand) Run(ctx *cmd.Context) error { + if c.user == "" { + creds, err := c.getConnectionCredentials() + if err != nil { + return errors.Trace(err) + } + c.user = creds.User + } + + var envs []base.UserEnvironment + var err error + if c.all { + envs, err = c.getAllEnvironments() + } else { + envs, err = c.getUserEnvironments() + } + if err != nil { + return errors.Annotate(err, "cannot list environments") + } + + output := make([]UserEnvironment, len(envs)) + now := time.Now() + for i, env := range envs { + output[i] = UserEnvironment{ + Name: env.Name, + UUID: env.UUID, + Owner: env.Owner, + LastConnection: user.LastConnection(env.LastConnection, now, c.exactTime), + } + } + + return c.out.Write(ctx, output) +} + +func (c *EnvironmentsCommand) getAllEnvironments() ([]base.UserEnvironment, error) { + client, err := c.getSysAPI() + if err != nil { + return nil, errors.Trace(err) + } + defer client.Close() + return client.AllEnvironments() +} + +func (c *EnvironmentsCommand) getUserEnvironments() ([]base.UserEnvironment, error) { + client, err := c.getEnvAPI() + if err != nil { + return nil, errors.Trace(err) + } + defer client.Close() + return client.ListEnvironments(c.user) +} + +// formatTabular takes an interface{} to adhere to the cmd.Formatter interface +func (c *EnvironmentsCommand) formatTabular(value interface{}) ([]byte, error) { + envs, ok := value.([]UserEnvironment) + if !ok { + return nil, errors.Errorf("expected value of type %T, got %T", envs, 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) + fmt.Fprintf(tw, "NAME") + if c.listUUID { + fmt.Fprintf(tw, "\tENVIRONMENT UUID") + } + fmt.Fprintf(tw, "\tOWNER\tLAST CONNECTION\n") + for _, env := range envs { + fmt.Fprintf(tw, "%s", env.Name) + if c.listUUID { + fmt.Fprintf(tw, "\t%s", env.UUID) + } + fmt.Fprintf(tw, "\t%s\t%s\n", env.Owner, env.LastConnection) + } + tw.Flush() + return out.Bytes(), nil +} === added file 'src/github.com/juju/juju/cmd/juju/system/environments_test.go' --- src/github.com/juju/juju/cmd/juju/system/environments_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/system/environments_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,142 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package system_test + +import ( + "time" + + "github.com/juju/cmd" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/api/base" + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/system" + "github.com/juju/juju/environs/configstore" + "github.com/juju/juju/testing" +) + +type EnvironmentsSuite struct { + testing.FakeJujuHomeSuite + api *fakeEnvMgrAPIClient + creds *configstore.APICredentials +} + +var _ = gc.Suite(&EnvironmentsSuite{}) + +type fakeEnvMgrAPIClient struct { + err error + user string + envs []base.UserEnvironment + all bool +} + +func (f *fakeEnvMgrAPIClient) Close() error { + return nil +} + +func (f *fakeEnvMgrAPIClient) ListEnvironments(user string) ([]base.UserEnvironment, error) { + if f.err != nil { + return nil, f.err + } + + f.user = user + return f.envs, nil +} + +func (f *fakeEnvMgrAPIClient) AllEnvironments() ([]base.UserEnvironment, error) { + if f.err != nil { + return nil, f.err + } + f.all = true + return f.envs, nil +} + +func (s *EnvironmentsSuite) SetUpTest(c *gc.C) { + s.FakeJujuHomeSuite.SetUpTest(c) + + err := envcmd.WriteCurrentSystem("fake") + c.Assert(err, jc.ErrorIsNil) + + last1 := time.Date(2015, 3, 20, 0, 0, 0, 0, time.UTC) + last2 := time.Date(2015, 3, 1, 0, 0, 0, 0, time.UTC) + + envs := []base.UserEnvironment{ + { + Name: "test-env1", + Owner: "user-admin@local", + UUID: "test-env1-UUID", + LastConnection: &last1, + }, { + Name: "test-env2", + Owner: "user-admin@local", + UUID: "test-env2-UUID", + LastConnection: &last2, + }, { + Name: "test-env3", + Owner: "user-admin@local", + UUID: "test-env3-UUID", + }, + } + s.api = &fakeEnvMgrAPIClient{envs: envs} + s.creds = &configstore.APICredentials{User: "admin@local", Password: "password"} +} + +func (s *EnvironmentsSuite) newCommand() cmd.Command { + command := system.NewEnvironmentsCommand(s.api, s.api, s.creds) + return envcmd.WrapSystem(command) +} + +func (s *EnvironmentsSuite) checkSuccess(c *gc.C, user string, args ...string) { + context, err := testing.RunCommand(c, s.newCommand(), args...) + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.api.user, gc.Equals, user) + c.Assert(testing.Stdout(context), gc.Equals, ""+ + "NAME OWNER LAST CONNECTION\n"+ + "test-env1 user-admin@local 2015-03-20\n"+ + "test-env2 user-admin@local 2015-03-01\n"+ + "test-env3 user-admin@local never connected\n"+ + "\n") +} + +func (s *EnvironmentsSuite) TestEnvironments(c *gc.C) { + s.checkSuccess(c, "admin@local") + s.checkSuccess(c, "bob", "--user", "bob") +} + +func (s *EnvironmentsSuite) TestAllEnvironments(c *gc.C) { + context, err := testing.RunCommand(c, s.newCommand(), "--all") + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.api.all, jc.IsTrue) + c.Assert(testing.Stdout(context), gc.Equals, ""+ + "NAME OWNER LAST CONNECTION\n"+ + "test-env1 user-admin@local 2015-03-20\n"+ + "test-env2 user-admin@local 2015-03-01\n"+ + "test-env3 user-admin@local never connected\n"+ + "\n") +} + +func (s *EnvironmentsSuite) TestEnvironmentsUUID(c *gc.C) { + context, err := testing.RunCommand(c, s.newCommand(), "--uuid") + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.api.user, gc.Equals, "admin@local") + c.Assert(testing.Stdout(context), gc.Equals, ""+ + "NAME ENVIRONMENT UUID OWNER LAST CONNECTION\n"+ + "test-env1 test-env1-UUID user-admin@local 2015-03-20\n"+ + "test-env2 test-env2-UUID user-admin@local 2015-03-01\n"+ + "test-env3 test-env3-UUID user-admin@local never connected\n"+ + "\n") +} + +func (s *EnvironmentsSuite) TestUnrecognizedArg(c *gc.C) { + _, err := testing.RunCommand(c, s.newCommand(), "whoops") + c.Assert(err, gc.ErrorMatches, `unrecognized args: \["whoops"\]`) +} + +func (s *EnvironmentsSuite) TestEnvironmentsError(c *gc.C) { + s.api.err = common.ErrPerm + _, err := testing.RunCommand(c, s.newCommand()) + c.Assert(err, gc.ErrorMatches, "cannot list environments: permission denied") +} === added file 'src/github.com/juju/juju/cmd/juju/system/export_test.go' --- src/github.com/juju/juju/cmd/juju/system/export_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/system/export_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,122 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package system + +import ( + "github.com/juju/cmd" + + "github.com/juju/juju/api" + "github.com/juju/juju/environs/configstore" +) + +var ( + SetConfigSpecialCaseDefaults = setConfigSpecialCaseDefaults + UserCurrent = &userCurrent +) + +// NewListCommand returns a ListCommand with the configstore provided as specified. +func NewListCommand(cfgStore configstore.Storage) *ListCommand { + return &ListCommand{ + cfgStore: cfgStore, + } +} + +// NewCreateEnvironmentCommand returns a CreateEnvironmentCommand with the api provided as specified. +func NewCreateEnvironmentCommand(api CreateEnvironmentAPI, parser func(interface{}) (interface{}, error)) *CreateEnvironmentCommand { + return &CreateEnvironmentCommand{ + api: api, + configParser: parser, + } +} + +// NewEnvironmentsCommand returns a EnvironmentsCommand with the API and userCreds +// provided as specified. +func NewEnvironmentsCommand(envAPI EnvironmentsEnvAPI, sysAPI EnvironmentsSysAPI, userCreds *configstore.APICredentials) *EnvironmentsCommand { + return &EnvironmentsCommand{ + envAPI: envAPI, + sysAPI: sysAPI, + userCreds: userCreds, + } +} + +// NewLoginCommand returns a LoginCommand with the function used to open +// the API connection mocked out. +func NewLoginCommand(apiOpen api.OpenFunc, getUserManager GetUserManagerFunc) *LoginCommand { + return &LoginCommand{ + apiOpen: apiOpen, + getUserManager: getUserManager, + } +} + +// NewUseEnvironmentCommand returns a UseEnvironmentCommand with the API and +// userCreds provided as specified. +func NewUseEnvironmentCommand(api UseEnvironmentAPI, userCreds *configstore.APICredentials, endpoint *configstore.APIEndpoint) *UseEnvironmentCommand { + return &UseEnvironmentCommand{ + api: api, + userCreds: userCreds, + endpoint: endpoint, + } +} + +// NewRemoveBlocksCommand returns a RemoveBlocksCommand with the function used +// to open the API connection mocked out. +func NewRemoveBlocksCommand(api removeBlocksAPI) *RemoveBlocksCommand { + return &RemoveBlocksCommand{ + api: api, + } +} + +// Name makes the private name attribute accessible for tests. +func (c *CreateEnvironmentCommand) Name() string { + return c.name +} + +// Owner makes the private name attribute accessible for tests. +func (c *CreateEnvironmentCommand) Owner() string { + return c.owner +} + +// ConfigFile makes the private configFile attribute accessible for tests. +func (c *CreateEnvironmentCommand) ConfigFile() cmd.FileVar { + return c.configFile +} + +// ConfValues makes the private confValues attribute accessible for tests. +func (c *CreateEnvironmentCommand) ConfValues() map[string]string { + return c.confValues +} + +// NewDestroyCommand returns a DestroyCommand with the systemmanager and client +// endpoints mocked out. +func NewDestroyCommand(api destroySystemAPI, clientapi destroyClientAPI, apierr error) *DestroyCommand { + return &DestroyCommand{ + DestroyCommandBase: DestroyCommandBase{ + api: api, + clientapi: clientapi, + apierr: apierr, + }, + } +} + +// NewKillCommand returns a KillCommand with the systemmanager and client +// endpoints mocked out. +func NewKillCommand(api destroySystemAPI, clientapi destroyClientAPI, apierr error, dialFunc func(string) (api.Connection, error)) *KillCommand { + return &KillCommand{ + DestroyCommandBase{ + api: api, + clientapi: clientapi, + apierr: apierr, + }, + dialFunc, + } +} + +// NewListBlocksCommand returns a ListBlocksCommand with the systemmanager +// endpoint mocked out. +func NewListBlocksCommand(api listBlocksAPI, apierr error) *ListBlocksCommand { + return &ListBlocksCommand{ + api: api, + apierr: apierr, + } +} === added file 'src/github.com/juju/juju/cmd/juju/system/kill.go' --- src/github.com/juju/juju/cmd/juju/system/kill.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/system/kill.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,184 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package system + +import ( + "time" + + "github.com/juju/cmd" + "github.com/juju/errors" + "launchpad.net/gnuflag" + + "github.com/juju/juju/api" + "github.com/juju/juju/api/systemmanager" + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/environs" + "github.com/juju/juju/environs/configstore" +) + +var ( + apiTimeout = 10 * time.Second + ErrConnTimedOut = errors.New("connection to state server timed out") +) + +const killDoc = ` +Forcibly destroy the specified system. If the API server is accessible, +this command will attempt to destroy the state server environment and all +hosted environments and their resources. + +If the API server is unreachable, the machines of the state server environment +will be destroyed through the cloud provisioner. If there are additional +machines, including machines within hosted environments, these machines +will not be destroyed and will never be reconnected to the Juju system being +destroyed. +` + +// KillCommand kills the specified system. +type KillCommand struct { + DestroyCommandBase + // TODO (cherylj) If timeouts for dialing the API are added to new or + // existing commands later, the dialer should be pulled into a common + // base and made to be an interface rather than a function. + apiDialerFunc func(string) (api.Connection, error) +} + +// Info implements Command.Info. +func (c *KillCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "kill", + Args: "", + Purpose: "forcibly terminate all machines and other associated resources for a system environment", + Doc: killDoc, + } +} + +// SetFlags implements Command.SetFlags. +func (c *KillCommand) SetFlags(f *gnuflag.FlagSet) { + f.BoolVar(&c.assumeYes, "y", false, "Do not ask for confirmation") + f.BoolVar(&c.assumeYes, "yes", false, "") +} + +// Init implements Command.Init. +func (c *KillCommand) Init(args []string) error { + if c.apiDialerFunc == nil { + // This should never happen, but check here instead of panicking later. + return errors.New("no api dialer specified") + } + + return c.DestroyCommandBase.Init(args) +} + +func (c *KillCommand) getSystemAPI(info configstore.EnvironInfo) (destroySystemAPI, error) { + if c.api != nil { + return c.api, c.apierr + } + + // Attempt to connect to the API with a short timeout + apic := make(chan api.Connection) + errc := make(chan error) + go func() { + api, dialErr := c.apiDialerFunc(c.systemName) + if dialErr != nil { + errc <- dialErr + return + } + apic <- api + }() + + var apiRoot api.Connection + select { + case err := <-errc: + return nil, err + case apiRoot = <-apic: + case <-time.After(apiTimeout): + return nil, ErrConnTimedOut + } + + return systemmanager.NewClient(apiRoot), nil +} + +// Run implements Command.Run +func (c *KillCommand) Run(ctx *cmd.Context) error { + store, err := configstore.Default() + if err != nil { + return errors.Annotate(err, "cannot open system info storage") + } + + cfgInfo, err := store.ReadInfo(c.systemName) + if err != nil { + return errors.Annotate(err, "cannot read system info") + } + + // Verify that we're destroying a system + apiEndpoint := cfgInfo.APIEndpoint() + if apiEndpoint.ServerUUID != "" && apiEndpoint.EnvironUUID != apiEndpoint.ServerUUID { + return errors.Errorf("%q is not a system; use juju environment destroy to destroy it", c.systemName) + } + + if !c.assumeYes { + if err = confirmDestruction(ctx, c.systemName); err != nil { + return err + } + } + + // Attempt to connect to the API. + api, err := c.getSystemAPI(cfgInfo) + switch { + case err == nil: + defer api.Close() + case errors.Cause(err) == common.ErrPerm: + return errors.Annotate(err, "cannot destroy system") + default: + if err != ErrConnTimedOut { + logger.Debugf("unable to open api: %s", err) + } + ctx.Infof("Unable to open API: %s\n", err) + api = nil + } + + // Obtain bootstrap / system environ information + systemEnviron, err := c.getSystemEnviron(cfgInfo, api) + if err != nil { + return errors.Annotate(err, "cannot obtain bootstrap information") + } + + // If we were unable to connect to the API, just destroy the system through + // the environs interface. + if api == nil { + return environs.Destroy(systemEnviron, store) + } + + // Attempt to destroy the system with destroyEnvs and ignoreBlocks = true + err = api.DestroySystem(true, true) + if params.IsCodeNotImplemented(err) { + // Fall back to using the client endpoint to destroy the system, + // sending the info we were already able to collect. + return c.killSystemViaClient(ctx, cfgInfo, systemEnviron, store) + } + + if err != nil { + ctx.Infof("Unable to destroy system through the API: %s. Destroying through provider.", err) + } + + return environs.Destroy(systemEnviron, store) +} + +// killSystemViaClient attempts to kill the system using the client +// endpoint for older juju systems which do not implement systemmanager.DestroySystem +func (c *KillCommand) killSystemViaClient(ctx *cmd.Context, info configstore.EnvironInfo, systemEnviron environs.Environ, store configstore.Storage) error { + api, err := c.getClientAPI() + if err != nil { + defer api.Close() + } + + if api != nil { + err = api.DestroyEnvironment() + if err != nil { + ctx.Infof("Unable to destroy system through the API: %s. Destroying through provider.", err) + } + } + + return environs.Destroy(systemEnviron, store) +} === added file 'src/github.com/juju/juju/cmd/juju/system/kill_test.go' --- src/github.com/juju/juju/cmd/juju/system/kill_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/system/kill_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,194 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package system_test + +import ( + "bytes" + "time" + + "github.com/juju/cmd" + "github.com/juju/errors" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/api" + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/juju/system" + cmdtesting "github.com/juju/juju/cmd/testing" + "github.com/juju/juju/juju" + _ "github.com/juju/juju/provider/dummy" + "github.com/juju/juju/testing" +) + +type KillSuite struct { + DestroySuite +} + +var _ = gc.Suite(&KillSuite{}) + +func (s *KillSuite) SetUpTest(c *gc.C) { + s.DestroySuite.SetUpTest(c) +} + +func (s *KillSuite) runKillCommand(c *gc.C, args ...string) (*cmd.Context, error) { + cmd := system.NewKillCommand(s.api, s.clientapi, s.apierror, juju.NewAPIFromName) + return testing.RunCommand(c, cmd, args...) +} + +func (s *KillSuite) newKillCommand() *system.KillCommand { + return system.NewKillCommand(s.api, s.clientapi, s.apierror, juju.NewAPIFromName) +} + +func (s *KillSuite) TestKillNoSystemNameError(c *gc.C) { + _, err := s.runKillCommand(c) + c.Assert(err, gc.ErrorMatches, "no system specified") +} + +func (s *KillSuite) TestKillBadFlags(c *gc.C) { + _, err := s.runKillCommand(c, "-n") + c.Assert(err, gc.ErrorMatches, "flag provided but not defined: -n") +} + +func (s *KillSuite) TestKillUnknownArgument(c *gc.C) { + _, err := s.runKillCommand(c, "environment", "whoops") + c.Assert(err, gc.ErrorMatches, `unrecognized args: \["whoops"\]`) +} + +func (s *KillSuite) TestKillNoDialer(c *gc.C) { + cmd := system.NewKillCommand(nil, nil, nil, nil) + _, err := testing.RunCommand(c, cmd, "test1", "-y") + c.Assert(err, gc.ErrorMatches, "no api dialer specified") +} + +func (s *KillSuite) TestKillUnknownSystem(c *gc.C) { + _, err := s.runKillCommand(c, "foo") + c.Assert(err, gc.ErrorMatches, `cannot read system info: environment "foo" not found`) +} + +func (s *KillSuite) TestKillNonSystemEnvFails(c *gc.C) { + _, err := s.runKillCommand(c, "test2") + c.Assert(err, gc.ErrorMatches, "\"test2\" is not a system; use juju environment destroy to destroy it") +} + +func (s *KillSuite) TestKillCannotConnectToAPISucceeds(c *gc.C) { + s.apierror = errors.New("connection refused") + ctx, err := s.runKillCommand(c, "test1", "-y") + c.Assert(err, jc.ErrorIsNil) + c.Check(testing.Stderr(ctx), jc.Contains, "Unable to open API: connection refused") + checkSystemRemovedFromStore(c, "test1", s.store) + + // Check that we didn't call the API + c.Assert(s.api.ignoreBlocks, jc.IsFalse) +} + +func (s *KillSuite) TestKillWithAPIConnection(c *gc.C) { + _, err := s.runKillCommand(c, "test1", "-y") + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.api.ignoreBlocks, jc.IsTrue) + c.Assert(s.api.destroyAll, jc.IsTrue) + c.Assert(s.clientapi.destroycalled, jc.IsFalse) + checkSystemRemovedFromStore(c, "test1", s.store) +} + +func (s *KillSuite) TestKillEnvironmentGetFailsWithoutAPIConnection(c *gc.C) { + s.apierror = errors.New("connection refused") + s.api.err = errors.NotFoundf(`system "test3"`) + _, err := s.runKillCommand(c, "test3", "-y") + c.Assert(err, gc.ErrorMatches, "cannot obtain bootstrap information: unable to get bootstrap information from API") + checkSystemExistsInStore(c, "test3", s.store) +} + +func (s *KillSuite) TestKillEnvironmentGetFailsWithAPIConnection(c *gc.C) { + s.api.err = errors.NotFoundf(`system "test3"`) + _, err := s.runKillCommand(c, "test3", "-y") + c.Assert(err, gc.ErrorMatches, "cannot obtain bootstrap information: system \"test3\" not found") + checkSystemExistsInStore(c, "test3", s.store) +} + +func (s *KillSuite) TestKillFallsBackToClient(c *gc.C) { + s.api.err = ¶ms.Error{"DestroySystem", params.CodeNotImplemented} + _, err := s.runKillCommand(c, "test1", "-y") + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.clientapi.destroycalled, jc.IsTrue) + checkSystemRemovedFromStore(c, "test1", s.store) +} + +func (s *KillSuite) TestClientKillDestroysSystemWithAPIError(c *gc.C) { + s.api.err = ¶ms.Error{"DestroySystem", params.CodeNotImplemented} + s.clientapi.err = errors.New("some destroy error") + ctx, err := s.runKillCommand(c, "test1", "-y") + c.Assert(err, jc.ErrorIsNil) + c.Check(testing.Stderr(ctx), jc.Contains, "Unable to destroy system through the API: some destroy error. Destroying through provider.") + c.Assert(s.clientapi.destroycalled, jc.IsTrue) + checkSystemRemovedFromStore(c, "test1", s.store) +} + +func (s *KillSuite) TestKillDestroysSystemWithAPIError(c *gc.C) { + s.api.err = errors.New("some destroy error") + ctx, err := s.runKillCommand(c, "test1", "-y") + c.Assert(err, jc.ErrorIsNil) + c.Check(testing.Stderr(ctx), jc.Contains, "Unable to destroy system through the API: some destroy error. Destroying through provider.") + c.Assert(s.api.ignoreBlocks, jc.IsTrue) + c.Assert(s.api.destroyAll, jc.IsTrue) + checkSystemRemovedFromStore(c, "test1", s.store) +} + +func (s *KillSuite) TestKillCommandConfirmation(c *gc.C) { + var stdin, stdout bytes.Buffer + ctx, err := cmd.DefaultContext() + c.Assert(err, jc.ErrorIsNil) + ctx.Stdout = &stdout + ctx.Stdin = &stdin + + // Ensure confirmation is requested if "-y" is not specified. + stdin.WriteString("n") + _, errc := cmdtesting.RunCommand(ctx, s.newKillCommand(), "test1") + select { + case err := <-errc: + c.Check(err, gc.ErrorMatches, "system destruction aborted") + case <-time.After(testing.LongWait): + c.Fatalf("command took too long") + } + c.Check(testing.Stdout(ctx), gc.Matches, "WARNING!.*test1(.|\n)*") + checkSystemExistsInStore(c, "test1", s.store) +} + +func (s *KillSuite) TestKillAPIPermErrFails(c *gc.C) { + testDialer := func(sysName string) (api.Connection, error) { + return nil, common.ErrPerm + } + + cmd := system.NewKillCommand(nil, nil, nil, testDialer) + _, err := testing.RunCommand(c, cmd, "test1", "-y") + c.Assert(err, gc.ErrorMatches, "cannot destroy system: permission denied") + c.Assert(s.api.ignoreBlocks, jc.IsFalse) + checkSystemExistsInStore(c, "test1", s.store) +} + +func (s *KillSuite) TestKillEarlyAPIConnectionTimeout(c *gc.C) { + stop := make(chan struct{}) + defer close(stop) + testDialer := func(sysName string) (api.Connection, error) { + <-stop + return nil, errors.New("kill command waited too long") + } + + done := make(chan struct{}) + go func() { + defer close(done) + cmd := system.NewKillCommand(nil, nil, nil, testDialer) + ctx, err := testing.RunCommand(c, cmd, "test1", "-y") + c.Check(err, jc.ErrorIsNil) + c.Check(testing.Stderr(ctx), jc.Contains, "Unable to open API: connection to state server timed out") + c.Check(s.api.ignoreBlocks, jc.IsFalse) + c.Check(s.api.destroyAll, jc.IsFalse) + checkSystemRemovedFromStore(c, "test1", s.store) + }() + select { + case <-done: + case <-time.After(1 * time.Minute): + c.Fatalf("Kill command waited too long to open the API") + } +} === added file 'src/github.com/juju/juju/cmd/juju/system/list.go' --- src/github.com/juju/juju/cmd/juju/system/list.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/system/list.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,72 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package system + +import ( + "fmt" + "sort" + + "github.com/juju/cmd" + "github.com/juju/errors" + + "github.com/juju/juju/environs/configstore" +) + +// ListCommand returns the list of all systems the user is +// currently logged in to on the current machine. +type ListCommand struct { + cmd.CommandBase + cfgStore configstore.Storage +} + +var listDoc = ` +List all the Juju systems logged in to on the current machine. + +A system refers to a Juju Environment System (JES) that runs and manages the +Juju API server and the underlying database used by Juju. A system may manage +multiple environments. + +See Also: + juju help juju-systems + juju help system environments + juju help system create-environment + juju help system use-environment +` + +// Info implements Command.Info +func (c *ListCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "list", + Purpose: "list all systems logged in to on the current machine", + Doc: listDoc, + } +} + +func (c *ListCommand) getConfigstore() (configstore.Storage, error) { + if c.cfgStore != nil { + return c.cfgStore, nil + } + return configstore.Default() +} + +// Run implements Command.Run +func (c *ListCommand) Run(ctx *cmd.Context) error { + store, err := c.getConfigstore() + + if err != nil { + return errors.Annotate(err, "failed to get config store") + } + + list, err := store.ListSystems() + if err != nil { + return errors.Annotate(err, "failed to list systems in config store") + } + + sort.Strings(list) + for _, name := range list { + fmt.Fprintf(ctx.Stdout, "%s\n", name) + } + + return nil +} === added file 'src/github.com/juju/juju/cmd/juju/system/list_test.go' --- src/github.com/juju/juju/cmd/juju/system/list_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/system/list_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,95 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package system_test + +import ( + "github.com/juju/errors" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/juju/system" + "github.com/juju/juju/environs/configstore" + "github.com/juju/juju/feature" + "github.com/juju/juju/testing" +) + +type ListSuite struct { + testing.FakeJujuHomeSuite + store configstore.Storage +} + +var _ = gc.Suite(&ListSuite{}) + +type errorStore struct { + err error +} + +func (errorStore) CreateInfo(envName string) configstore.EnvironInfo { + panic("CreateInfo not implemented") +} + +func (errorStore) List() ([]string, error) { + panic("List not implemented") +} + +func (e errorStore) ListSystems() ([]string, error) { + return nil, e.err +} + +func (errorStore) ReadInfo(envName string) (configstore.EnvironInfo, error) { + panic("ReadInfo not implemented") +} + +func (s *ListSuite) SetUpTest(c *gc.C) { + s.FakeJujuHomeSuite.SetUpTest(c) + s.SetFeatureFlags(feature.JES) + s.store = configstore.NewMem() + + var envList = []struct { + name string + serverUUID string + envUUID string + }{ + { + name: "test1", + serverUUID: "test1-uuid", + envUUID: "test1-uuid", + }, { + name: "test2", + serverUUID: "test1-uuid", + envUUID: "test2-uuid", + }, { + name: "test3", + envUUID: "test3-uuid", + }, + } + for _, env := range envList { + info := s.store.CreateInfo(env.name) + info.SetAPIEndpoint(configstore.APIEndpoint{ + Addresses: []string{"localhost"}, + CACert: testing.CACert, + EnvironUUID: env.envUUID, + ServerUUID: env.serverUUID, + }) + err := info.Write() + c.Assert(err, jc.ErrorIsNil) + } +} + +func (s *ListSuite) TestSystemList(c *gc.C) { + context, err := testing.RunCommand(c, system.NewListCommand(s.store)) + c.Assert(err, jc.ErrorIsNil) + c.Assert(testing.Stdout(context), gc.Equals, "test1\ntest3\n") +} + +func (s *ListSuite) TestUnrecognizedArg(c *gc.C) { + _, err := testing.RunCommand(c, system.NewListCommand(s.store), "whoops") + c.Assert(err, gc.ErrorMatches, `unrecognized args: \["whoops"\]`) +} + +func (s *ListSuite) TestListSystemsError(c *gc.C) { + s.store = errorStore{err: errors.New("cannot read info")} + _, err := testing.RunCommand(c, system.NewListCommand(s.store)) + c.Assert(err, gc.ErrorMatches, "failed to list systems in config store: cannot read info") +} === added file 'src/github.com/juju/juju/cmd/juju/system/listblocks.go' --- src/github.com/juju/juju/cmd/juju/system/listblocks.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/system/listblocks.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,71 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package system + +import ( + "github.com/juju/cmd" + "github.com/juju/errors" + "launchpad.net/gnuflag" + + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/envcmd" +) + +// ListBlocksCommand lists all blocks for environments within the system. +type ListBlocksCommand struct { + envcmd.SysCommandBase + out cmd.Output + api listBlocksAPI + apierr error +} + +var listBlocksDoc = `List all blocks for environments within the specified system` + +// listBlocksAPI defines the methods on the system manager API endpoint +// that the list-blocks command calls. +type listBlocksAPI interface { + Close() error + ListBlockedEnvironments() ([]params.EnvironmentBlockInfo, error) +} + +// Info implements Command.Info. +func (c *ListBlocksCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "list-blocks", + Purpose: "list all blocks within the system", + Doc: listBlocksDoc, + } +} + +// SetFlags implements Command.SetFlags. +func (c *ListBlocksCommand) SetFlags(f *gnuflag.FlagSet) { + c.out.AddFlags(f, "tabular", map[string]cmd.Formatter{ + "yaml": cmd.FormatYaml, + "json": cmd.FormatJson, + "tabular": formatTabularBlockedEnvironments, + }) +} + +func (c *ListBlocksCommand) getAPI() (listBlocksAPI, error) { + if c.api != nil { + return c.api, c.apierr + } + return c.NewSystemManagerAPIClient() +} + +// Run implements Command.Run +func (c *ListBlocksCommand) Run(ctx *cmd.Context) error { + api, err := c.getAPI() + if err != nil { + return errors.Annotate(err, "cannot connect to the API") + } + defer api.Close() + + envs, err := api.ListBlockedEnvironments() + if err != nil { + logger.Errorf("Unable to list blocked environments: %s", err) + return err + } + return c.out.Write(ctx, envs) +} === added file 'src/github.com/juju/juju/cmd/juju/system/listblocks_test.go' --- src/github.com/juju/juju/cmd/juju/system/listblocks_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/system/listblocks_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,118 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package system_test + +import ( + "github.com/juju/cmd" + "github.com/juju/errors" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/apiserver/params" + "github.com/juju/juju/cmd/juju/system" + _ "github.com/juju/juju/provider/dummy" + "github.com/juju/juju/testing" +) + +type ListBlocksSuite struct { + testing.FakeJujuHomeSuite + api *fakeListBlocksAPI + apierror error +} + +var _ = gc.Suite(&ListBlocksSuite{}) + +// fakeListBlocksAPI mocks out the systemmanager API +type fakeListBlocksAPI struct { + err error + blocks []params.EnvironmentBlockInfo +} + +func (f *fakeListBlocksAPI) Close() error { return nil } + +func (f *fakeListBlocksAPI) ListBlockedEnvironments() ([]params.EnvironmentBlockInfo, error) { + return f.blocks, f.err +} + +func (s *ListBlocksSuite) SetUpTest(c *gc.C) { + s.FakeJujuHomeSuite.SetUpTest(c) + s.apierror = nil + s.api = &fakeListBlocksAPI{ + blocks: []params.EnvironmentBlockInfo{ + params.EnvironmentBlockInfo{ + Name: "test1", + UUID: "test1-uuid", + OwnerTag: "cheryl@local", + Blocks: []string{ + "BlockDestroy", + }, + }, + params.EnvironmentBlockInfo{ + Name: "test2", + UUID: "test2-uuid", + OwnerTag: "bob@local", + Blocks: []string{ + "BlockDestroy", + "BlockChange", + }, + }, + }, + } +} + +func (s *ListBlocksSuite) runListBlocksCommand(c *gc.C, args ...string) (*cmd.Context, error) { + cmd := system.NewListBlocksCommand(s.api, s.apierror) + return testing.RunCommand(c, cmd, args...) +} + +func (s *ListBlocksSuite) TestListBlocksCannotConnectToAPI(c *gc.C) { + s.apierror = errors.New("connection refused") + _, err := s.runListBlocksCommand(c) + c.Assert(err, gc.ErrorMatches, "cannot connect to the API: connection refused") +} + +func (s *ListBlocksSuite) TestListBlocksError(c *gc.C) { + s.api.err = errors.New("unexpected api error") + s.runListBlocksCommand(c) + testLog := c.GetTestLog() + c.Check(testLog, jc.Contains, "Unable to list blocked environments: unexpected api error") +} + +func (s *ListBlocksSuite) TestListBlocksTabular(c *gc.C) { + ctx, err := s.runListBlocksCommand(c) + c.Check(err, jc.ErrorIsNil) + c.Check(testing.Stdout(ctx), gc.Equals, ""+ + "NAME ENVIRONMENT UUID OWNER BLOCKS\n"+ + "test1 test1-uuid cheryl@local destroy-environment\n"+ + "test2 test2-uuid bob@local destroy-environment,all-changes\n"+ + "\n") +} + +func (s *ListBlocksSuite) TestListBlocksJSON(c *gc.C) { + ctx, err := s.runListBlocksCommand(c, "--format", "json") + c.Check(err, jc.ErrorIsNil) + c.Check(testing.Stdout(ctx), gc.Equals, "["+ + `{"name":"test1","env-uuid":"test1-uuid","owner-tag":"cheryl@local",`+ + `"blocks":["BlockDestroy"]},`+ + `{"name":"test2","env-uuid":"test2-uuid","owner-tag":"bob@local",`+ + `"blocks":["BlockDestroy","BlockChange"]}`+ + "]\n") +} + +func (s *ListBlocksSuite) TestListBlocksYAML(c *gc.C) { + ctx, err := s.runListBlocksCommand(c, "--format", "yaml") + c.Check(err, jc.ErrorIsNil) + c.Check(testing.Stdout(ctx), gc.Equals, ""+ + "- name: test1\n"+ + " uuid: test1-uuid\n"+ + " ownertag: cheryl@local\n"+ + " blocks:\n"+ + " - BlockDestroy\n"+ + "- name: test2\n"+ + " uuid: test2-uuid\n"+ + " ownertag: bob@local\n"+ + " blocks:\n"+ + " - BlockDestroy\n"+ + " - BlockChange\n") +} === added file 'src/github.com/juju/juju/cmd/juju/system/login.go' --- src/github.com/juju/juju/cmd/juju/system/login.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/system/login.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,254 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package system + +import ( + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/juju/api/usermanager" + "github.com/juju/names" + "github.com/juju/utils" + goyaml "gopkg.in/yaml.v1" + "launchpad.net/gnuflag" + + "github.com/juju/juju/api" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/environs/configstore" + "github.com/juju/juju/juju" + "github.com/juju/juju/network" +) + +// GetUserManagerFunc defines a function that takes an api connection +// and returns the (locally defined) UserManager interface. +type GetUserManagerFunc func(conn api.Connection) (UserManager, error) + +// LoginCommand logs in to a Juju system and caches the connection +// information. +type LoginCommand struct { + cmd.CommandBase + apiOpen api.OpenFunc + getUserManager GetUserManagerFunc + // TODO (thumper): when we support local cert definitions + // allow the use to specify the user and server address. + // user string + // address string + Server cmd.FileVar + Name string + KeepPassword bool +} + +var loginDoc = ` +login connects to a juju system and caches the information that juju +needs to connect to the api server in the $(JUJU_HOME)/environments directory. + +In order to login to a system, you need to have a user already created for you +in that system. The way that this occurs is for an existing user on the system +to create you as a user. This will generate a file that contains the +information needed to connect. + +If you have been sent one of these server files, you can login by doing the +following: + + # if you have saved the server file as ~/erica.server + juju system login --server=~/erica.server test-system + +A new strong random password is generated to replace the password defined in +the server file. The 'test-system' will also become the current system that +the juju command will talk to by default. + +If you have used the 'api-info' command to generate a copy of your current +credentials for a system, you should use the --keep-password option as it will +mean that you will still be able to connect to the api server from the +computer where you ran api-info. + +See Also: + juju help system environments + juju help system use-environment + juju help system create-environment + juju help user add + juju help switch +` + +// Info implements Command.Info +func (c *LoginCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "login", + // TODO(thumper): support user and address options + // Args: " [[:]]" + Args: "", + Purpose: "login to a Juju System", + Doc: loginDoc, + } +} + +// SetFlags implements Command.SetFlags. +func (c *LoginCommand) SetFlags(f *gnuflag.FlagSet) { + f.Var(&c.Server, "server", "path to yaml-formatted server file") + f.BoolVar(&c.KeepPassword, "keep-password", false, "do not generate a new random password") +} + +// SetFlags implements Command.Init. +func (c *LoginCommand) Init(args []string) error { + if c.apiOpen == nil { + c.apiOpen = apiOpen + } + if c.getUserManager == nil { + c.getUserManager = getUserManager + } + if len(args) == 0 { + return errors.New("no name specified") + } + + c.Name, args = args[0], args[1:] + return cmd.CheckEmpty(args) +} + +// Run implements Command.Run +func (c *LoginCommand) Run(ctx *cmd.Context) error { + // TODO(thumper): as we support the user and address + // change this check here. + if c.Server.Path == "" { + return errors.New("no server file specified") + } + + serverYAML, err := c.Server.Read(ctx) + if err != nil { + return errors.Trace(err) + } + + var serverDetails envcmd.ServerFile + if err := goyaml.Unmarshal(serverYAML, &serverDetails); err != nil { + return errors.Trace(err) + } + + // Construct the api.Info struct from the provided values + // and attempt to connect to the remote server before we do anything else. + if !names.IsValidUser(serverDetails.Username) { + return errors.Errorf("%q is not a valid username", serverDetails.Username) + } + + userTag := names.NewUserTag(serverDetails.Username) + if userTag.Provider() != names.LocalProvider { + // Remove users do not have their passwords stored in Juju + // so we never attempt to change them. + c.KeepPassword = true + } + + info := api.Info{ + Addrs: serverDetails.Addresses, + CACert: serverDetails.CACert, + Tag: userTag, + Password: serverDetails.Password, + } + + apiState, err := c.apiOpen(&info, api.DefaultDialOpts()) + if err != nil { + return errors.Trace(err) + } + defer apiState.Close() + + // If we get to here, the credentials supplied were sufficient to connect + // to the Juju System and login. Now we cache the details. + serverInfo, err := c.cacheConnectionInfo(serverDetails, apiState) + if err != nil { + return errors.Trace(err) + } + ctx.Infof("cached connection details as system %q", c.Name) + + // If we get to here, we have been able to connect to the API server, and + // also have been able to write the cached information. Now we can change + // the user's password to a new randomly generated strong password, and + // update the cached information knowing that the likelihood of failure is + // minimal. + if !c.KeepPassword { + if err := c.updatePassword(ctx, apiState, userTag, serverInfo); err != nil { + return errors.Trace(err) + } + } + + return errors.Trace(envcmd.SetCurrentSystem(ctx, c.Name)) +} + +func (c *LoginCommand) cacheConnectionInfo(serverDetails envcmd.ServerFile, apiState api.Connection) (configstore.EnvironInfo, error) { + store, err := configstore.Default() + if err != nil { + return nil, errors.Trace(err) + } + serverInfo := store.CreateInfo(c.Name) + + serverTag, err := apiState.ServerTag() + if err != nil { + return nil, errors.Wrap(err, errors.New("juju system too old to support login")) + } + + connectedAddresses, err := network.ParseHostPorts(apiState.Addr()) + if err != nil { + // Should never happen, since we've just connected with it. + return nil, errors.Annotatef(err, "invalid API address %q", apiState.Addr()) + } + addressConnectedTo := connectedAddresses[0] + + addrs, hosts, changed := juju.PrepareEndpointsForCaching(serverInfo, apiState.APIHostPorts(), addressConnectedTo) + if !changed { + logger.Infof("api addresses: %v", apiState.APIHostPorts()) + logger.Infof("address connected to: %v", addressConnectedTo) + return nil, errors.New("no addresses returned from prepare for caching") + } + + serverInfo.SetAPICredentials( + configstore.APICredentials{ + User: serverDetails.Username, + Password: serverDetails.Password, + }) + + serverInfo.SetAPIEndpoint(configstore.APIEndpoint{ + Addresses: addrs, + Hostnames: hosts, + CACert: serverDetails.CACert, + ServerUUID: serverTag.Id(), + }) + + if err = serverInfo.Write(); err != nil { + return nil, errors.Trace(err) + } + return serverInfo, nil +} + +func (c *LoginCommand) updatePassword(ctx *cmd.Context, conn api.Connection, userTag names.UserTag, serverInfo configstore.EnvironInfo) error { + password, err := utils.RandomPassword() + if err != nil { + return errors.Annotate(err, "failed to generate random password") + } + + userManager, err := c.getUserManager(conn) + if err != nil { + return errors.Trace(err) + } + if err := userManager.SetPassword(userTag.Name(), password); err != nil { + errors.Trace(err) + } + ctx.Infof("password updated\n") + creds := serverInfo.APICredentials() + creds.Password = password + serverInfo.SetAPICredentials(creds) + if err = serverInfo.Write(); err != nil { + return errors.Trace(err) + } + return nil +} + +func apiOpen(info *api.Info, opts api.DialOpts) (api.Connection, error) { + return api.Open(info, opts) +} + +// UserManager defines the calls that the Login command makes to the user +// manager client. It is returned by a helper function that is overridden in +// tests. +type UserManager interface { + SetPassword(username, password string) error +} + +func getUserManager(conn api.Connection) (UserManager, error) { + return usermanager.NewClient(conn), nil +} === added file 'src/github.com/juju/juju/cmd/juju/system/login_test.go' --- src/github.com/juju/juju/cmd/juju/system/login_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/system/login_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,240 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package system_test + +import ( + "io/ioutil" + "os" + "path/filepath" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/names" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/api" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/system" + "github.com/juju/juju/environs/configstore" + "github.com/juju/juju/network" + "github.com/juju/juju/testing" +) + +type LoginSuite struct { + testing.FakeJujuHomeSuite + apiConnection *mockAPIConnection + openError error + store configstore.Storage + username string +} + +var _ = gc.Suite(&LoginSuite{}) + +func (s *LoginSuite) SetUpTest(c *gc.C) { + s.FakeJujuHomeSuite.SetUpTest(c) + s.store = configstore.NewMem() + s.PatchValue(&configstore.Default, func() (configstore.Storage, error) { + return s.store, nil + }) + s.openError = nil + s.apiConnection = &mockAPIConnection{ + serverTag: testing.EnvironmentTag, + addr: "192.168.2.1:1234", + } + s.username = "valid-user" +} + +func (s *LoginSuite) apiOpen(info *api.Info, opts api.DialOpts) (api.Connection, error) { + if s.openError != nil { + return nil, s.openError + } + s.apiConnection.info = info + return s.apiConnection, nil +} + +func (s *LoginSuite) getUserManager(conn api.Connection) (system.UserManager, error) { + return s.apiConnection, nil +} + +func (s *LoginSuite) run(c *gc.C, args ...string) (*cmd.Context, error) { + command := system.NewLoginCommand(s.apiOpen, s.getUserManager) + return testing.RunCommand(c, command, args...) +} + +func (s *LoginSuite) runServerFile(c *gc.C, args ...string) (*cmd.Context, error) { + serverFilePath := filepath.Join(c.MkDir(), "server.yaml") + content := ` +addresses: ["192.168.2.1:1234", "192.168.2.2:1234"] +ca-cert: a-cert +username: ` + s.username + ` +password: sekrit +` + err := ioutil.WriteFile(serverFilePath, []byte(content), 0644) + c.Assert(err, jc.ErrorIsNil) + allArgs := []string{"foo", "--server", serverFilePath} + allArgs = append(allArgs, args...) + return s.run(c, allArgs...) +} + +func (s *LoginSuite) TestInit(c *gc.C) { + loginCommand := system.NewLoginCommand(nil, nil) + + err := testing.InitCommand(loginCommand, []string{}) + c.Assert(err, gc.ErrorMatches, "no name specified") + + err = testing.InitCommand(loginCommand, []string{"foo"}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(loginCommand.Name, gc.Equals, "foo") + + err = testing.InitCommand(loginCommand, []string{"foo", "bar"}) + c.Assert(err, gc.ErrorMatches, `unrecognized args: \["bar"\]`) +} + +func (s *LoginSuite) TestNoSpecifiedServerFile(c *gc.C) { + _, err := s.run(c, "foo") + c.Assert(err, gc.ErrorMatches, "no server file specified") +} + +func (s *LoginSuite) TestMissingServerFile(c *gc.C) { + serverFilePath := filepath.Join(c.MkDir(), "server.yaml") + _, err := s.run(c, "foo", "--server", serverFilePath) + c.Assert(errors.Cause(err), jc.Satisfies, os.IsNotExist) +} + +func (s *LoginSuite) TestBadServerFile(c *gc.C) { + serverFilePath := filepath.Join(c.MkDir(), "server.yaml") + err := ioutil.WriteFile(serverFilePath, []byte("&^%$#@"), 0644) + c.Assert(err, jc.ErrorIsNil) + + _, err = s.run(c, "foo", "--server", serverFilePath) + c.Assert(err, gc.ErrorMatches, "YAML error: did not find expected alphabetic or numeric character") +} + +func (s *LoginSuite) TestBadUser(c *gc.C) { + serverFilePath := filepath.Join(c.MkDir(), "server.yaml") + content := ` +username: omg@not@valid +` + err := ioutil.WriteFile(serverFilePath, []byte(content), 0644) + c.Assert(err, jc.ErrorIsNil) + + _, err = s.run(c, "foo", "--server", serverFilePath) + c.Assert(err, gc.ErrorMatches, `"omg@not@valid" is not a valid username`) +} + +func (s *LoginSuite) TestAPIOpenError(c *gc.C) { + s.openError = errors.New("open failed") + _, err := s.runServerFile(c) + c.Assert(err, gc.ErrorMatches, `open failed`) +} + +func (s *LoginSuite) TestOldServerNoServerUUID(c *gc.C) { + s.apiConnection.serverTag = names.EnvironTag{} + _, err := s.runServerFile(c) + c.Assert(err, gc.ErrorMatches, `juju system too old to support login`) +} + +func (s *LoginSuite) TestWritesConfig(c *gc.C) { + ctx, err := s.runServerFile(c) + c.Assert(err, jc.ErrorIsNil) + + info, err := s.store.ReadInfo("foo") + c.Assert(err, jc.ErrorIsNil) + creds := info.APICredentials() + c.Assert(creds.User, gc.Equals, "valid-user") + // Make sure that the password was changed, and that the new + // value was not "sekrit". + c.Assert(creds.Password, gc.Not(gc.Equals), "sekrit") + c.Assert(creds.Password, gc.Equals, s.apiConnection.password) + endpoint := info.APIEndpoint() + c.Assert(endpoint.CACert, gc.Equals, "a-cert") + c.Assert(endpoint.EnvironUUID, gc.Equals, "") + c.Assert(endpoint.ServerUUID, gc.Equals, testing.EnvironmentTag.Id()) + c.Assert(endpoint.Addresses, jc.DeepEquals, []string{"192.168.2.1:1234"}) + c.Assert(endpoint.Hostnames, jc.DeepEquals, []string{"192.168.2.1:1234"}) + + c.Assert(testing.Stderr(ctx), jc.Contains, "cached connection details as system \"foo\"\n") + c.Assert(testing.Stderr(ctx), jc.Contains, "password updated\n") +} + +func (s *LoginSuite) TestKeepPassword(c *gc.C) { + _, err := s.runServerFile(c, "--keep-password") + c.Assert(err, jc.ErrorIsNil) + + info, err := s.store.ReadInfo("foo") + c.Assert(err, jc.ErrorIsNil) + creds := info.APICredentials() + c.Assert(creds.User, gc.Equals, "valid-user") + c.Assert(creds.Password, gc.Equals, "sekrit") +} + +func (s *LoginSuite) TestRemoteUsersKeepPassword(c *gc.C) { + s.username = "user@remote" + _, err := s.runServerFile(c) + c.Assert(err, jc.ErrorIsNil) + + info, err := s.store.ReadInfo("foo") + c.Assert(err, jc.ErrorIsNil) + creds := info.APICredentials() + c.Assert(creds.User, gc.Equals, "user@remote") + c.Assert(creds.Password, gc.Equals, "sekrit") +} + +func (s *LoginSuite) TestConnectsUsingServerFileInfo(c *gc.C) { + s.username = "valid-user@local" + _, err := s.runServerFile(c) + c.Assert(err, jc.ErrorIsNil) + + info := s.apiConnection.info + c.Assert(info.Addrs, jc.DeepEquals, []string{"192.168.2.1:1234", "192.168.2.2:1234"}) + c.Assert(info.CACert, gc.Equals, "a-cert") + c.Assert(info.EnvironTag.Id(), gc.Equals, "") + c.Assert(info.Tag.Id(), gc.Equals, "valid-user@local") + c.Assert(info.Password, gc.Equals, "sekrit") + c.Assert(info.Nonce, gc.Equals, "") +} + +func (s *LoginSuite) TestWritesCurrentSystem(c *gc.C) { + _, err := s.runServerFile(c) + c.Assert(err, jc.ErrorIsNil) + currentSystem, err := envcmd.ReadCurrentSystem() + c.Assert(err, jc.ErrorIsNil) + c.Assert(currentSystem, gc.Equals, "foo") +} + +type mockAPIConnection struct { + api.Connection + info *api.Info + addr string + apiHostPorts [][]network.HostPort + serverTag names.EnvironTag + username string + password string +} + +func (*mockAPIConnection) Close() error { + return nil +} + +func (m *mockAPIConnection) Addr() string { + return m.addr +} + +func (m *mockAPIConnection) APIHostPorts() [][]network.HostPort { + return m.apiHostPorts +} + +func (m *mockAPIConnection) ServerTag() (names.EnvironTag, error) { + if m.serverTag.Id() == "" { + return m.serverTag, errors.New("no server tag") + } + return m.serverTag, nil +} + +func (m *mockAPIConnection) SetPassword(username, password string) error { + m.username = username + m.password = password + return nil +} === added file 'src/github.com/juju/juju/cmd/juju/system/package_test.go' --- src/github.com/juju/juju/cmd/juju/system/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/system/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,14 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package system_test + +import ( + "testing" + + gc "gopkg.in/check.v1" +) + +func TestPackage(t *testing.T) { + gc.TestingT(t) +} === added file 'src/github.com/juju/juju/cmd/juju/system/removeblocks.go' --- src/github.com/juju/juju/cmd/juju/system/removeblocks.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/system/removeblocks.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,60 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package system + +import ( + "github.com/juju/cmd" + "github.com/juju/errors" + + "github.com/juju/juju/cmd/envcmd" +) + +// RemoveBlocksCommand returns the list of all systems the user is +// currently logged in to on the current machine. +type RemoveBlocksCommand struct { + envcmd.SysCommandBase + api removeBlocksAPI +} + +type removeBlocksAPI interface { + Close() error + RemoveBlocks() error +} + +var removeBlocksDoc = ` +Remove all blocks in the Juju system. + +A system administrator is able to remove all the blocks that have been added +in a Juju system. + +See Also: + juju help block + juju help unblock +` + +// Info implements Command.Info +func (c *RemoveBlocksCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "remove-blocks", + Purpose: "remove all blocks in the Juju system", + Doc: removeBlocksDoc, + } +} + +func (c *RemoveBlocksCommand) getAPI() (removeBlocksAPI, error) { + if c.api != nil { + return c.api, nil + } + return c.NewSystemManagerAPIClient() +} + +// Run implements Command.Run +func (c *RemoveBlocksCommand) Run(ctx *cmd.Context) error { + client, err := c.getAPI() + if err != nil { + return errors.Trace(err) + } + defer client.Close() + return errors.Annotatef(client.RemoveBlocks(), "cannot remove blocks") +} === added file 'src/github.com/juju/juju/cmd/juju/system/removeblocks_test.go' --- src/github.com/juju/juju/cmd/juju/system/removeblocks_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/system/removeblocks_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,68 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package system_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/envcmd" + "github.com/juju/juju/cmd/juju/system" + "github.com/juju/juju/testing" +) + +type removeBlocksSuite struct { + testing.FakeJujuHomeSuite + api *fakeRemoveBlocksAPI +} + +var _ = gc.Suite(&removeBlocksSuite{}) + +func (s *removeBlocksSuite) SetUpTest(c *gc.C) { + s.FakeJujuHomeSuite.SetUpTest(c) + + err := envcmd.WriteCurrentSystem("fake") + c.Assert(err, jc.ErrorIsNil) + + s.api = &fakeRemoveBlocksAPI{} +} + +func (s *removeBlocksSuite) newCommand() cmd.Command { + command := system.NewRemoveBlocksCommand(s.api) + return envcmd.WrapSystem(command) +} + +func (s *removeBlocksSuite) TestRemove(c *gc.C) { + _, err := testing.RunCommand(c, s.newCommand()) + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.api.called, jc.IsTrue) +} + +func (s *removeBlocksSuite) TestUnrecognizedArg(c *gc.C) { + _, err := testing.RunCommand(c, s.newCommand(), "whoops") + c.Assert(err, gc.ErrorMatches, `unrecognized args: \["whoops"\]`) + c.Assert(s.api.called, jc.IsFalse) +} + +func (s *removeBlocksSuite) TestEnvironmentsError(c *gc.C) { + s.api.err = common.ErrPerm + _, err := testing.RunCommand(c, s.newCommand()) + c.Assert(err, gc.ErrorMatches, "cannot remove blocks: permission denied") +} + +type fakeRemoveBlocksAPI struct { + err error + called bool +} + +func (f *fakeRemoveBlocksAPI) Close() error { + return nil +} + +func (f *fakeRemoveBlocksAPI) RemoveBlocks() error { + f.called = true + return f.err +} === added file 'src/github.com/juju/juju/cmd/juju/system/system.go' --- src/github.com/juju/juju/cmd/juju/system/system.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/system/system.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,54 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package system + +import ( + "github.com/juju/cmd" + "github.com/juju/loggo" + + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/juju" +) + +var logger = loggo.GetLogger("juju.cmd.juju.system") + +const commandDoc = ` + +A Juju system is a Juju environment that runs the API servers, and manages the +underlying database used by Juju. The initial environment that is created when +bootstrapping is called a "system". + +The "juju system" command provides the commands to create, use, and destroy +environments running withing a Juju system. + +System commands also allow the user to connect to an existing system using the +"login" command, and to use an environment that already exists in the current +system through the "use-environment" command. + +see also: + juju help juju-systems +` + +// NewSuperCommand creates the system supercommand and registers the +// subcommands that it supports. +func NewSuperCommand() cmd.Command { + systemCmd := cmd.NewSuperCommand(cmd.SuperCommandParams{ + Name: "system", + Doc: commandDoc, + UsagePrefix: "juju", + Purpose: "manage systems", + }) + + systemCmd.Register(&ListCommand{}) + systemCmd.Register(&LoginCommand{}) + systemCmd.Register(&DestroyCommand{}) + systemCmd.Register(&KillCommand{apiDialerFunc: juju.NewAPIFromName}) + systemCmd.Register(envcmd.WrapSystem(&ListBlocksCommand{})) + systemCmd.Register(envcmd.WrapSystem(&EnvironmentsCommand{})) + systemCmd.Register(envcmd.WrapSystem(&CreateEnvironmentCommand{})) + systemCmd.Register(envcmd.WrapSystem(&RemoveBlocksCommand{})) + systemCmd.Register(envcmd.WrapSystem(&UseEnvironmentCommand{})) + + return systemCmd +} === added file 'src/github.com/juju/juju/cmd/juju/system/system_test.go' --- src/github.com/juju/juju/cmd/juju/system/system_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/system/system_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,44 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package system_test + +import ( + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/juju/system" + "github.com/juju/juju/testing" + + // Bring in the dummy provider definition. + _ "github.com/juju/juju/provider/dummy" +) + +type SystemCommandSuite struct { + testing.BaseSuite +} + +var _ = gc.Suite(&SystemCommandSuite{}) + +var expectedCommmandNames = []string{ + "create-env", // alias for create-environment + "create-environment", + "destroy", + "environments", + "help", + "kill", + "list", + "list-blocks", + "login", + "remove-blocks", + "use-env", // alias for use-environment + "use-environment", +} + +func (s *SystemCommandSuite) TestHelp(c *gc.C) { + // Check the help output + ctx, err := testing.RunCommand(c, system.NewSuperCommand(), "--help") + c.Assert(err, jc.ErrorIsNil) + namesFound := testing.ExtractCommandsFromHelpOutput(ctx) + c.Assert(namesFound, gc.DeepEquals, expectedCommmandNames) +} === added file 'src/github.com/juju/juju/cmd/juju/system/useenvironment.go' --- src/github.com/juju/juju/cmd/juju/system/useenvironment.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/system/useenvironment.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,298 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package system + +import ( + "strings" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/names" + "launchpad.net/gnuflag" + + "github.com/juju/juju/api" + "github.com/juju/juju/api/base" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/environs/configstore" +) + +// UseEnvironmentCommand returns the list of all the environments the +// current user can access on the current system. +type UseEnvironmentCommand struct { + envcmd.SysCommandBase + + apiOpen api.OpenFunc + api UseEnvironmentAPI + userCreds *configstore.APICredentials + endpoint *configstore.APIEndpoint + + LocalName string + Owner string + EnvName string + EnvUUID string +} + +// UseEnvironmentAPI defines the methods on the environment manager API that +// the use environment command calls. +type UseEnvironmentAPI interface { + Close() error + ListEnvironments(user string) ([]base.UserEnvironment, error) +} + +var useEnvDoc = ` +use-environment caches the necessary information about the specified +environment on the current machine. This allows you to switch between +environments. + +By default, the local names for the environment are based on the name that the +owner of the environment gave it when they created it. If you are the owner +of the environment, then the local name is just the name of the environment. +If you are not the owner, the name is prefixed by the name of the owner and a +dash. + +If there is just one environment called "test" in the current system that you +have access to, then you can just specify the name. + + $ juju system use-environment test + +If however there are multiple enviornments called "test" that are owned + + $ juju system use-environment test + Multiple environments matched name "test": + cb4b94e8-29bb-44ae-820c-adac21194395, owned by bob@local + ae673c19-73ef-437f-8224-4842a1772bdf, owned by mary@local + Please specify either the environment UUID or the owner to disambiguate. + ERROR multiple environments matched + +You can specify either the environment UUID like this: + + $ juju system use-environment cb4b94e8-29bb-44ae-820c-adac21194395 + +Or, specify the owner: + + $ juju system use-environment mary@local/test + +Since '@local' is the default for users, this can be shortened to: + + $ juju system use-environment mary/test + + +See Also: + juju help juju-systems + juju help system create-environment + juju help environment share + juju help environment unshare + juju help switch + juju help user add +` + +// Info implements Command.Info +func (c *UseEnvironmentCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "use-environment", + Purpose: "use an environment that you have access to on this machine", + Doc: useEnvDoc, + Aliases: []string{"use-env"}, + } +} + +func (c *UseEnvironmentCommand) getAPI() (UseEnvironmentAPI, error) { + if c.api != nil { + return c.api, nil + } + return c.NewEnvironmentManagerAPIClient() +} + +func (c *UseEnvironmentCommand) getConnectionCredentials() (configstore.APICredentials, error) { + if c.userCreds != nil { + return *c.userCreds, nil + } + return c.ConnectionCredentials() +} + +func (c *UseEnvironmentCommand) getConnectionEndpoint() (configstore.APIEndpoint, error) { + if c.endpoint != nil { + return *c.endpoint, nil + } + return c.ConnectionEndpoint() +} + +// SetFlags implements Command.SetFlags. +func (c *UseEnvironmentCommand) SetFlags(f *gnuflag.FlagSet) { + f.StringVar(&c.LocalName, "name", "", "the local name for this environment") +} + +// SetFlags implements Command.Init. +func (c *UseEnvironmentCommand) Init(args []string) error { + if c.apiOpen == nil { + c.apiOpen = apiOpen + } + if len(args) == 0 || strings.TrimSpace(args[0]) == "" { + return errors.New("no environment supplied") + } + + name, args := args[0], args[1:] + + // First check to see if an owner has been specified. + bits := strings.SplitN(name, "/", 2) + switch len(bits) { + case 1: + // No user specified + c.EnvName = bits[0] + case 2: + owner := bits[0] + if names.IsValidUser(owner) { + c.Owner = owner + } else { + return errors.Errorf("%q is not a valid user", owner) + } + c.EnvName = bits[1] + } + + // Environment names can generally be anything, but we take a good + // stab at trying to determine if the user has speicifed a UUID + // instead of a name. For now, we only accept a properly formatted UUID, + // which means one with dashes in the right place. + if names.IsValidEnvironment(c.EnvName) { + c.EnvUUID, c.EnvName = c.EnvName, "" + } + + return cmd.CheckEmpty(args) +} + +// Run implements Command.Run +func (c *UseEnvironmentCommand) Run(ctx *cmd.Context) error { + client, err := c.getAPI() + if err != nil { + return errors.Trace(err) + } + defer client.Close() + + creds, err := c.getConnectionCredentials() + if err != nil { + return errors.Trace(err) + } + endpoint, err := c.getConnectionEndpoint() + if err != nil { + return errors.Trace(err) + } + + username := names.NewUserTag(creds.User).Username() + + env, err := c.findMatchingEnvironment(ctx, client, creds) + if err != nil { + return errors.Trace(err) + } + + if c.LocalName == "" { + if env.Owner == username { + c.LocalName = env.Name + } else { + envOwner := names.NewUserTag(env.Owner) + c.LocalName = envOwner.Name() + "-" + env.Name + } + } + + // Check with the store to see if we have an environment with that name. + store, err := configstore.Default() + if err != nil { + return errors.Trace(err) + } + + existing, err := store.ReadInfo(c.LocalName) + if err == nil { + // We have an existing environment with the same name. If it is the + // same environment with the same user, then this is fine, and we just + // change the current envrionment. + endpoint := existing.APIEndpoint() + existingCreds := existing.APICredentials() + // Need to make sure we check the username of the credentials, + // not just matching tags. + existingUsername := names.NewUserTag(existingCreds.User).Username() + if endpoint.EnvironUUID == env.UUID && existingUsername == username { + ctx.Infof("You already have environment details for %q cached locally.", c.LocalName) + return envcmd.SetCurrentEnvironment(ctx, c.LocalName) + } + ctx.Infof("You have an existing environment called %q, use --name to specify a different local name.", c.LocalName) + return errors.New("existing environment") + } + + info := store.CreateInfo(c.LocalName) + if err := c.updateCachedInfo(info, env.UUID, creds, endpoint); err != nil { + return errors.Annotatef(err, "failed to cache environment details") + } + + return envcmd.SetCurrentEnvironment(ctx, c.LocalName) +} + +func (c *UseEnvironmentCommand) updateCachedInfo(info configstore.EnvironInfo, envUUID string, creds configstore.APICredentials, endpoint configstore.APIEndpoint) error { + info.SetAPICredentials(creds) + // Specify the environment UUID. The server UUID will be the same as the + // endpoint that we have just connected to, as will be the CACert, addresses + // and hostnames. + endpoint.EnvironUUID = envUUID + info.SetAPIEndpoint(endpoint) + return errors.Trace(info.Write()) +} + +func (c *UseEnvironmentCommand) findMatchingEnvironment(ctx *cmd.Context, client UseEnvironmentAPI, creds configstore.APICredentials) (base.UserEnvironment, error) { + + var empty base.UserEnvironment + + envs, err := client.ListEnvironments(creds.User) + if err != nil { + return empty, errors.Annotate(err, "cannot list environments") + } + + var owner string + if c.Owner != "" { + // The username always contains the provider aspect of the user. + owner = names.NewUserTag(c.Owner).Username() + } + + // If we have a UUID, we warn if the owner is different, but accept it. + // We also trust that the environment UUIDs are unique + if c.EnvUUID != "" { + for _, env := range envs { + if env.UUID == c.EnvUUID { + if owner != "" && env.Owner != owner { + ctx.Infof("Specified environment owned by %s, not %s", env.Owner, owner) + } + return env, nil + } + } + return empty, errors.NotFoundf("matching environment") + } + + var matches []base.UserEnvironment + for _, env := range envs { + match := env.Name == c.EnvName + if match && owner != "" { + match = env.Owner == owner + } + if match { + matches = append(matches, env) + } + } + + // If there is only one match, that's the one. + switch len(matches) { + case 0: + return empty, errors.NotFoundf("matching environment") + case 1: + return matches[0], nil + } + + // We are going to return an error, but tell the user what the matches + // were so they can make an informed decision. We are also going to assume + // here that the resulting environment list has only one matching name for + // each user. There are tests creating environments that enforce this. + ctx.Infof("Multiple environments matched name %q:", c.EnvName) + for _, env := range matches { + ctx.Infof(" %s, owned by %s", env.UUID, env.Owner) + } + ctx.Infof("Please specify either the environment UUID or the owner to disambiguate.") + + return empty, errors.New("multiple environments matched") +} === added file 'src/github.com/juju/juju/cmd/juju/system/useenvironment_test.go' --- src/github.com/juju/juju/cmd/juju/system/useenvironment_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/system/useenvironment_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,283 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package system_test + +import ( + "strings" + + "github.com/juju/cmd" + jc "github.com/juju/testing/checkers" + "github.com/juju/utils" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/api/base" + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/system" + "github.com/juju/juju/environs/configstore" + "github.com/juju/juju/testing" +) + +const ( + serverUUID = "0dbfe161-de6c-47ad-9283-5e3ea64e1dd3" + env1UUID = "ebf03329-cdad-44a5-9f10-fe318efda3ce" + env2UUID = "b366cdd5-82da-49a1-ac18-001f26bb59a3" + env3UUID = "fd0f57a3-eb94-4095-9ab0-d1f6042f942a" + env4UUID = "1e45141b-85cb-4a0a-96ef-0aa6bbeac45a" +) + +type UseEnvironmentSuite struct { + testing.FakeJujuHomeSuite + api *fakeEnvMgrAPIClient + creds configstore.APICredentials + endpoint configstore.APIEndpoint +} + +var _ = gc.Suite(&UseEnvironmentSuite{}) + +func (s *UseEnvironmentSuite) SetUpTest(c *gc.C) { + s.FakeJujuHomeSuite.SetUpTest(c) + + err := envcmd.WriteCurrentSystem("fake") + c.Assert(err, jc.ErrorIsNil) + + envs := []base.UserEnvironment{{ + Name: "unique", + Owner: "tester@local", + UUID: "some-uuid", + }, { + Name: "test", + Owner: "tester@local", + UUID: env1UUID, + }, { + Name: "test", + Owner: "bob@local", + UUID: env2UUID, + }, { + Name: "other", + Owner: "bob@local", + UUID: env3UUID, + }, { + Name: "other", + Owner: "bob@remote", + UUID: env4UUID, + }} + s.api = &fakeEnvMgrAPIClient{envs: envs} + s.creds = configstore.APICredentials{User: "tester", Password: "password"} + s.endpoint = configstore.APIEndpoint{ + Addresses: []string{"127.0.0.1:12345"}, + Hostnames: []string{"localhost:12345"}, + CACert: testing.CACert, + ServerUUID: serverUUID, + } +} + +func (s *UseEnvironmentSuite) run(c *gc.C, args ...string) (*cmd.Context, error) { + command := system.NewUseEnvironmentCommand(s.api, &s.creds, &s.endpoint) + return testing.RunCommand(c, envcmd.WrapSystem(command), args...) +} + +func (s *UseEnvironmentSuite) TestInit(c *gc.C) { + for i, test := range []struct { + args []string + errorString string + localName string + owner string + envName string + envUUID string + }{{ + errorString: "no environment supplied", + }, { + args: []string{""}, + errorString: "no environment supplied", + }, { + args: []string{"env-name"}, + envName: "env-name", + }, { + args: []string{"env-name", "--name", "foo"}, + localName: "foo", + envName: "env-name", + }, { + args: []string{"user/foobar"}, + envName: "foobar", + owner: "user", + }, { + args: []string{"user@local/foobar"}, + envName: "foobar", + owner: "user@local", + }, { + args: []string{"user@remote/foobar"}, + envName: "foobar", + owner: "user@remote", + }, { + args: []string{"+user+name/foobar"}, + errorString: `"\+user\+name" is not a valid user`, + }, { + args: []string{env1UUID}, + envUUID: env1UUID, + }, { + args: []string{"user/" + env1UUID}, + owner: "user", + envUUID: env1UUID, + }} { + c.Logf("test %d", i) + command := &system.UseEnvironmentCommand{} + err := testing.InitCommand(command, test.args) + if test.errorString == "" { + c.Check(command.LocalName, gc.Equals, test.localName) + c.Check(command.EnvName, gc.Equals, test.envName) + c.Check(command.EnvUUID, gc.Equals, test.envUUID) + c.Check(command.Owner, gc.Equals, test.owner) + } else { + c.Check(err, gc.ErrorMatches, test.errorString) + } + } +} + +func (s *UseEnvironmentSuite) TestEnvironmentsError(c *gc.C) { + s.api.err = common.ErrPerm + _, err := s.run(c, "ignored-but-needed") + c.Assert(err, gc.ErrorMatches, "cannot list environments: permission denied") +} + +func (s *UseEnvironmentSuite) TestNameNotFound(c *gc.C) { + _, err := s.run(c, "missing") + c.Assert(err, gc.ErrorMatches, "matching environment not found") +} + +func (s *UseEnvironmentSuite) TestUUID(c *gc.C) { + _, err := s.run(c, env3UUID) + c.Assert(err, gc.IsNil) + + s.assertCurrentEnvironment(c, "bob-other", env3UUID) +} + +func (s *UseEnvironmentSuite) TestUUIDCorrectOwner(c *gc.C) { + _, err := s.run(c, "bob/"+env3UUID) + c.Assert(err, gc.IsNil) + + s.assertCurrentEnvironment(c, "bob-other", env3UUID) +} + +func (s *UseEnvironmentSuite) TestUUIDWrongOwner(c *gc.C) { + ctx, err := s.run(c, "charles/"+env3UUID) + c.Assert(err, gc.IsNil) + expected := "Specified environment owned by bob@local, not charles@local" + c.Assert(testing.Stderr(ctx), jc.Contains, expected) + + s.assertCurrentEnvironment(c, "bob-other", env3UUID) +} + +func (s *UseEnvironmentSuite) TestUniqueName(c *gc.C) { + _, err := s.run(c, "unique") + c.Assert(err, gc.IsNil) + + s.assertCurrentEnvironment(c, "unique", "some-uuid") +} + +func (s *UseEnvironmentSuite) TestMultipleNameMatches(c *gc.C) { + ctx, err := s.run(c, "test") + c.Assert(err, gc.ErrorMatches, "multiple environments matched") + + message := strings.TrimSpace(testing.Stderr(ctx)) + lines := strings.Split(message, "\n") + c.Assert(lines, gc.HasLen, 4) + c.Assert(lines[0], gc.Equals, `Multiple environments matched name "test":`) + c.Assert(lines[1], gc.Equals, " "+env1UUID+", owned by tester@local") + c.Assert(lines[2], gc.Equals, " "+env2UUID+", owned by bob@local") + c.Assert(lines[3], gc.Equals, `Please specify either the environment UUID or the owner to disambiguate.`) +} + +func (s *UseEnvironmentSuite) TestUserOwnerOfEnvironment(c *gc.C) { + _, err := s.run(c, "tester/test") + c.Assert(err, gc.IsNil) + + s.assertCurrentEnvironment(c, "test", env1UUID) +} + +func (s *UseEnvironmentSuite) TestOtherUsersEnvironment(c *gc.C) { + _, err := s.run(c, "bob/test") + c.Assert(err, gc.IsNil) + + s.assertCurrentEnvironment(c, "bob-test", env2UUID) +} + +func (s *UseEnvironmentSuite) TestRemoteUsersEnvironmentName(c *gc.C) { + _, err := s.run(c, "bob@remote/other") + c.Assert(err, gc.IsNil) + + s.assertCurrentEnvironment(c, "bob-other", env4UUID) +} + +func (s *UseEnvironmentSuite) TestDisambiguateWrongOwner(c *gc.C) { + _, err := s.run(c, "wrong/test") + c.Assert(err, gc.ErrorMatches, "matching environment not found") +} + +func (s *UseEnvironmentSuite) TestUseEnvAlreadyExisting(c *gc.C) { + s.makeLocalEnvironment(c, "unique", "", "") + ctx, err := s.run(c, "unique") + c.Assert(err, gc.ErrorMatches, "existing environment") + expected := `You have an existing environment called "unique", use --name to specify a different local name.` + c.Assert(testing.Stderr(ctx), jc.Contains, expected) +} + +func (s *UseEnvironmentSuite) TestUseEnvAlreadyExistingSameEnv(c *gc.C) { + s.makeLocalEnvironment(c, "unique", "some-uuid", "tester") + ctx, err := s.run(c, "unique") + c.Assert(err, gc.IsNil) + + message := strings.TrimSpace(testing.Stderr(ctx)) + lines := strings.Split(message, "\n") + c.Assert(lines, gc.HasLen, 2) + + expected := `You already have environment details for "unique" cached locally.` + c.Assert(lines[0], gc.Equals, expected) + c.Assert(lines[1], gc.Equals, `fake (system) -> unique`) + + current, err := envcmd.ReadCurrentEnvironment() + c.Assert(err, gc.IsNil) + c.Assert(current, gc.Equals, "unique") +} + +func (s *UseEnvironmentSuite) assertCurrentEnvironment(c *gc.C, name, uuid string) { + current, err := envcmd.ReadCurrentEnvironment() + c.Assert(err, gc.IsNil) + c.Assert(current, gc.Equals, name) + + store, err := configstore.Default() + c.Assert(err, gc.IsNil) + + info, err := store.ReadInfo(name) + c.Assert(err, gc.IsNil) + c.Assert(info.APIEndpoint(), jc.DeepEquals, configstore.APIEndpoint{ + Addresses: []string{"127.0.0.1:12345"}, + Hostnames: []string{"localhost:12345"}, + CACert: testing.CACert, + EnvironUUID: uuid, + ServerUUID: serverUUID, + }) + c.Assert(info.APICredentials(), jc.DeepEquals, s.creds) +} + +func (s *UseEnvironmentSuite) makeLocalEnvironment(c *gc.C, name, uuid, owner string) { + store, err := configstore.Default() + c.Assert(err, gc.IsNil) + + if uuid == "" { + uuid = utils.MustNewUUID().String() + } + if owner == "" { + owner = "random@person" + } + info := store.CreateInfo(name) + info.SetAPIEndpoint(configstore.APIEndpoint{ + EnvironUUID: uuid, + }) + info.SetAPICredentials(configstore.APICredentials{ + User: owner, + }) + err = info.Write() + c.Assert(err, gc.IsNil) +} === removed file 'src/github.com/juju/juju/cmd/juju/unexpose.go' --- src/github.com/juju/juju/cmd/juju/unexpose.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/unexpose.go 1970-01-01 00:00:00 +0000 @@ -1,46 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "errors" - - "github.com/juju/cmd" - - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/cmd/juju/block" -) - -// UnexposeCommand is responsible exposing services. -type UnexposeCommand struct { - envcmd.EnvCommandBase - ServiceName string -} - -func (c *UnexposeCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "unexpose", - Args: "", - Purpose: "unexpose a service", - } -} - -func (c *UnexposeCommand) Init(args []string) error { - if len(args) == 0 { - return errors.New("no service name specified") - } - c.ServiceName = args[0] - return cmd.CheckEmpty(args[1:]) -} - -// Run changes the juju-managed firewall to hide any -// ports that were also explicitly marked by units as closed. -func (c *UnexposeCommand) Run(_ *cmd.Context) error { - client, err := c.NewAPIClient() - if err != nil { - return err - } - defer client.Close() - return block.ProcessBlockedError(client.ServiceUnexpose(c.ServiceName), block.BlockChange) -} === removed file 'src/github.com/juju/juju/cmd/juju/unexpose_test.go' --- src/github.com/juju/juju/cmd/juju/unexpose_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/unexpose_test.go 1970-01-01 00:00:00 +0000 @@ -1,73 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - "gopkg.in/juju/charm.v5" - - "github.com/juju/juju/cmd/envcmd" - jujutesting "github.com/juju/juju/juju/testing" - "github.com/juju/juju/testcharms" - "github.com/juju/juju/testing" -) - -type UnexposeSuite struct { - jujutesting.RepoSuite - CmdBlockHelper -} - -func (s *UnexposeSuite) SetUpTest(c *gc.C) { - s.RepoSuite.SetUpTest(c) - s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) - c.Assert(s.CmdBlockHelper, gc.NotNil) - s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) -} - -var _ = gc.Suite(&UnexposeSuite{}) - -func runUnexpose(c *gc.C, args ...string) error { - _, err := testing.RunCommand(c, envcmd.Wrap(&UnexposeCommand{}), args...) - return err -} - -func (s *UnexposeSuite) assertExposed(c *gc.C, service string, expected bool) { - svc, err := s.State.Service(service) - c.Assert(err, jc.ErrorIsNil) - actual := svc.IsExposed() - c.Assert(actual, gc.Equals, expected) -} - -func (s *UnexposeSuite) TestUnexpose(c *gc.C) { - testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") - err := runDeploy(c, "local:dummy", "some-service-name") - c.Assert(err, jc.ErrorIsNil) - curl := charm.MustParseURL("local:trusty/dummy-1") - s.AssertService(c, "some-service-name", curl, 1, 0) - - err = runExpose(c, "some-service-name") - c.Assert(err, jc.ErrorIsNil) - s.assertExposed(c, "some-service-name", true) - - err = runUnexpose(c, "some-service-name") - c.Assert(err, jc.ErrorIsNil) - s.assertExposed(c, "some-service-name", false) - - err = runUnexpose(c, "nonexistent-service") - c.Assert(err, gc.ErrorMatches, `service "nonexistent-service" not found`) -} - -func (s *UnexposeSuite) TestBlockUnexpose(c *gc.C) { - testcharms.Repo.CharmArchivePath(s.SeriesPath, "dummy") - err := runDeploy(c, "local:dummy", "some-service-name") - c.Assert(err, jc.ErrorIsNil) - curl := charm.MustParseURL("local:trusty/dummy-1") - s.AssertService(c, "some-service-name", curl, 1, 0) - - // Block operation - s.BlockAllChanges(c, "TestBlockUnexpose") - err = runExpose(c, "some-service-name") - s.AssertBlocked(c, err, ".*TestBlockUnexpose.*") -} === removed file 'src/github.com/juju/juju/cmd/juju/upgradecharm.go' --- src/github.com/juju/juju/cmd/juju/upgradecharm.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/upgradecharm.go 1970-01-01 00:00:00 +0000 @@ -1,162 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - "os" - - "github.com/juju/cmd" - "github.com/juju/errors" - "github.com/juju/names" - "gopkg.in/juju/charm.v5" - "launchpad.net/gnuflag" - - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/cmd/juju/block" - "github.com/juju/juju/cmd/juju/service" -) - -// UpgradeCharm is responsible for upgrading a service's charm. -type UpgradeCharmCommand struct { - envcmd.EnvCommandBase - ServiceName string - Force bool - RepoPath string // defaults to JUJU_REPOSITORY - SwitchURL string - Revision int // defaults to -1 (latest) -} - -const upgradeCharmDoc = ` -When no flags are set, the service's charm will be upgraded to the latest -revision available in the repository from which it was originally deployed. An -explicit revision can be chosen with the --revision flag. - -If the charm came from a local repository, its path will be assumed to be -$JUJU_REPOSITORY unless overridden by --repository. - -The local repository behaviour is tuned specifically to the workflow of a charm -author working on a single client machine; use of local repositories from -multiple clients is not supported and may lead to confusing behaviour. Each -local charm gets uploaded with the revision specified in the charm, if possible, -otherwise it gets a unique revision (highest in state + 1). - -The --switch flag allows you to replace the charm with an entirely different -one. The new charm's URL and revision are inferred as they would be when running -a deploy command. - -Please note that --switch is dangerous, because juju only has limited -information with which to determine compatibility; the operation will succeed, -regardless of potential havoc, so long as the following conditions hold: - -- The new charm must declare all relations that the service is currently -participating in. -- All config settings shared by the old and new charms must -have the same types. - -The new charm may add new relations and configuration settings. - ---switch and --revision are mutually exclusive. To specify a given revision -number with --switch, give it in the charm URL, for instance "cs:wordpress-5" -would specify revision number 5 of the wordpress charm. - -Use of the --force flag is not generally recommended; units upgraded while in an -error state will not have upgrade-charm hooks executed, and may cause unexpected -behavior. -` - -func (c *UpgradeCharmCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "upgrade-charm", - Args: "", - Purpose: "upgrade a service's charm", - Doc: upgradeCharmDoc, - } -} - -func (c *UpgradeCharmCommand) SetFlags(f *gnuflag.FlagSet) { - f.BoolVar(&c.Force, "force", false, "upgrade all units immediately, even if in error state") - f.StringVar(&c.RepoPath, "repository", os.Getenv("JUJU_REPOSITORY"), "local charm repository path") - f.StringVar(&c.SwitchURL, "switch", "", "crossgrade to a different charm") - f.IntVar(&c.Revision, "revision", -1, "explicit revision of current charm") -} - -func (c *UpgradeCharmCommand) Init(args []string) error { - switch len(args) { - case 1: - if !names.IsValidService(args[0]) { - return fmt.Errorf("invalid service name %q", args[0]) - } - c.ServiceName = args[0] - case 0: - return fmt.Errorf("no service specified") - default: - return cmd.CheckEmpty(args[1:]) - } - if c.SwitchURL != "" && c.Revision != -1 { - return fmt.Errorf("--switch and --revision are mutually exclusive") - } - return nil -} - -// Run connects to the specified environment and starts the charm -// upgrade process. -func (c *UpgradeCharmCommand) Run(ctx *cmd.Context) error { - client, err := c.NewAPIClient() - if err != nil { - return err - } - defer client.Close() - oldURL, err := client.ServiceGetCharmURL(c.ServiceName) - if err != nil { - return err - } - - conf, err := service.GetClientConfig(client) - if err != nil { - return errors.Trace(err) - } - - var newRef *charm.Reference - if c.SwitchURL != "" { - newRef, err = charm.ParseReference(c.SwitchURL) - if err != nil { - return err - } - } else { - // No new URL specified, but revision might have been. - newRef = oldURL.WithRevision(c.Revision).Reference() - } - - csClient, err := newCharmStoreClient() - if err != nil { - return errors.Trace(err) - } - defer csClient.jar.Save() - newURL, repo, err := resolveCharmURL(newRef.String(), csClient.params, ctx.AbsPath(c.RepoPath), conf) - if err != nil { - return errors.Trace(err) - } - - // If no explicit revision was set with either SwitchURL - // or Revision flags, discover the latest. - if *newURL == *oldURL { - if newRef.Revision != -1 { - return fmt.Errorf("already running specified charm %q", newURL) - } - if newURL.Schema == "cs" { - // No point in trying to upgrade a charm store charm when - // we just determined that's the latest revision - // available. - return fmt.Errorf("already running latest charm %q", newURL) - } - } - - addedURL, err := addCharmViaAPI(client, ctx, newURL, repo, csClient) - if err != nil { - return block.ProcessBlockedError(err, block.BlockChange) - } - - return block.ProcessBlockedError(client.ServiceSetCharm(c.ServiceName, addedURL.String(), c.Force), block.BlockChange) -} === removed file 'src/github.com/juju/juju/cmd/juju/upgradecharm_test.go' --- src/github.com/juju/juju/cmd/juju/upgradecharm_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/upgradecharm_test.go 1970-01-01 00:00:00 +0000 @@ -1,335 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "bytes" - "io/ioutil" - "os" - "path" - - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - "gopkg.in/juju/charm.v5" - "gopkg.in/juju/charm.v5/charmrepo" - "gopkg.in/juju/charmstore.v4" - "gopkg.in/juju/charmstore.v4/charmstoretesting" - - "github.com/juju/juju/cmd/envcmd" - jujutesting "github.com/juju/juju/juju/testing" - "github.com/juju/juju/state" - "github.com/juju/juju/testcharms" - "github.com/juju/juju/testing" -) - -type UpgradeCharmErrorsSuite struct { - jujutesting.RepoSuite - srv *charmstoretesting.Server -} - -func (s *UpgradeCharmErrorsSuite) SetUpTest(c *gc.C) { - s.RepoSuite.SetUpTest(c) - s.srv = charmstoretesting.OpenServer(c, s.Session, charmstore.ServerParams{}) - s.PatchValue(&charmrepo.CacheDir, c.MkDir()) - original := newCharmStoreClient - s.PatchValue(&newCharmStoreClient, func() (*csClient, error) { - csclient, err := original() - c.Assert(err, jc.ErrorIsNil) - csclient.params.URL = s.srv.URL() - return csclient, nil - }) -} - -func (s *UpgradeCharmErrorsSuite) TearDownTest(c *gc.C) { - s.srv.Close() - s.RepoSuite.TearDownTest(c) -} - -var _ = gc.Suite(&UpgradeCharmErrorsSuite{}) - -func runUpgradeCharm(c *gc.C, args ...string) error { - _, err := testing.RunCommand(c, envcmd.Wrap(&UpgradeCharmCommand{}), args...) - return err -} - -func (s *UpgradeCharmErrorsSuite) TestInvalidArgs(c *gc.C) { - err := runUpgradeCharm(c) - c.Assert(err, gc.ErrorMatches, "no service specified") - err = runUpgradeCharm(c, "invalid:name") - c.Assert(err, gc.ErrorMatches, `invalid service name "invalid:name"`) - err = runUpgradeCharm(c, "foo", "bar") - c.Assert(err, gc.ErrorMatches, `unrecognized args: \["bar"\]`) -} - -func (s *UpgradeCharmErrorsSuite) TestWithInvalidRepository(c *gc.C) { - testcharms.Repo.ClonedDirPath(s.SeriesPath, "riak") - err := runDeploy(c, "local:riak", "riak") - c.Assert(err, jc.ErrorIsNil) - - err = runUpgradeCharm(c, "riak", "--repository=blah") - c.Assert(err, gc.ErrorMatches, `no repository found at ".*blah"`) - // Reset JUJU_REPOSITORY explicitly, because repoSuite.SetUpTest - // overwrites it (TearDownTest will revert it again). - os.Setenv("JUJU_REPOSITORY", "") - err = runUpgradeCharm(c, "riak", "--repository=") - c.Assert(err, gc.ErrorMatches, `charm not found in ".*": local:trusty/riak`) -} - -func (s *UpgradeCharmErrorsSuite) TestInvalidService(c *gc.C) { - err := runUpgradeCharm(c, "phony") - c.Assert(err, gc.ErrorMatches, `service "phony" not found`) -} - -func (s *UpgradeCharmErrorsSuite) deployService(c *gc.C) { - testcharms.Repo.ClonedDirPath(s.SeriesPath, "riak") - err := runDeploy(c, "local:riak", "riak") - c.Assert(err, jc.ErrorIsNil) -} - -func (s *UpgradeCharmErrorsSuite) TestInvalidSwitchURL(c *gc.C) { - s.deployService(c) - err := runUpgradeCharm(c, "riak", "--switch=blah") - c.Assert(err, gc.ErrorMatches, `cannot resolve charm URL "cs:trusty/blah": charm not found`) - err = runUpgradeCharm(c, "riak", "--switch=cs:missing/one") - c.Assert(err, gc.ErrorMatches, `cannot resolve charm URL "cs:missing/one": charm not found`) - // TODO(dimitern): add tests with incompatible charms -} - -func (s *UpgradeCharmErrorsSuite) TestSwitchAndRevisionFails(c *gc.C) { - s.deployService(c) - err := runUpgradeCharm(c, "riak", "--switch=riak", "--revision=2") - c.Assert(err, gc.ErrorMatches, "--switch and --revision are mutually exclusive") -} - -func (s *UpgradeCharmErrorsSuite) TestInvalidRevision(c *gc.C) { - s.deployService(c) - err := runUpgradeCharm(c, "riak", "--revision=blah") - c.Assert(err, gc.ErrorMatches, `invalid value "blah" for flag --revision: strconv.ParseInt: parsing "blah": invalid syntax`) -} - -type UpgradeCharmSuccessSuite struct { - jujutesting.RepoSuite - CmdBlockHelper - path string - riak *state.Service -} - -var _ = gc.Suite(&UpgradeCharmSuccessSuite{}) - -func (s *UpgradeCharmSuccessSuite) SetUpTest(c *gc.C) { - s.RepoSuite.SetUpTest(c) - s.path = testcharms.Repo.ClonedDirPath(s.SeriesPath, "riak") - err := runDeploy(c, "local:riak", "riak") - c.Assert(err, jc.ErrorIsNil) - s.riak, err = s.State.Service("riak") - c.Assert(err, jc.ErrorIsNil) - ch, forced, err := s.riak.Charm() - c.Assert(err, jc.ErrorIsNil) - c.Assert(ch.Revision(), gc.Equals, 7) - c.Assert(forced, jc.IsFalse) - - s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) - c.Assert(s.CmdBlockHelper, gc.NotNil) - s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) -} - -func (s *UpgradeCharmSuccessSuite) assertUpgraded(c *gc.C, revision int, forced bool) *charm.URL { - err := s.riak.Refresh() - c.Assert(err, jc.ErrorIsNil) - ch, force, err := s.riak.Charm() - c.Assert(err, jc.ErrorIsNil) - c.Assert(ch.Revision(), gc.Equals, revision) - c.Assert(force, gc.Equals, forced) - s.AssertCharmUploaded(c, ch.URL()) - return ch.URL() -} - -func (s *UpgradeCharmSuccessSuite) assertLocalRevision(c *gc.C, revision int, path string) { - dir, err := charm.ReadCharmDir(path) - c.Assert(err, jc.ErrorIsNil) - c.Assert(dir.Revision(), gc.Equals, revision) -} - -func (s *UpgradeCharmSuccessSuite) TestLocalRevisionUnchanged(c *gc.C) { - err := runUpgradeCharm(c, "riak") - c.Assert(err, jc.ErrorIsNil) - s.assertUpgraded(c, 8, false) - // Even though the remote revision is bumped, the local one should - // be unchanged. - s.assertLocalRevision(c, 7, s.path) -} - -func (s *UpgradeCharmSuccessSuite) TestBlockUpgradeCharm(c *gc.C) { - // Block operation - s.BlockAllChanges(c, "TestBlockUpgradeCharm") - err := runUpgradeCharm(c, "riak") - s.AssertBlocked(c, err, ".*TestBlockUpgradeCharm.*") -} - -func (s *UpgradeCharmSuccessSuite) TestRespectsLocalRevisionWhenPossible(c *gc.C) { - dir, err := charm.ReadCharmDir(s.path) - c.Assert(err, jc.ErrorIsNil) - err = dir.SetDiskRevision(42) - c.Assert(err, jc.ErrorIsNil) - - err = runUpgradeCharm(c, "riak") - c.Assert(err, jc.ErrorIsNil) - s.assertUpgraded(c, 42, false) - s.assertLocalRevision(c, 42, s.path) -} - -func (s *UpgradeCharmSuccessSuite) TestUpgradesWithBundle(c *gc.C) { - dir, err := charm.ReadCharmDir(s.path) - c.Assert(err, jc.ErrorIsNil) - dir.SetRevision(42) - buf := &bytes.Buffer{} - err = dir.ArchiveTo(buf) - c.Assert(err, jc.ErrorIsNil) - bundlePath := path.Join(s.SeriesPath, "riak.charm") - err = ioutil.WriteFile(bundlePath, buf.Bytes(), 0644) - c.Assert(err, jc.ErrorIsNil) - - err = runUpgradeCharm(c, "riak") - c.Assert(err, jc.ErrorIsNil) - s.assertUpgraded(c, 42, false) - s.assertLocalRevision(c, 7, s.path) -} - -func (s *UpgradeCharmSuccessSuite) TestBlockUpgradesWithBundle(c *gc.C) { - dir, err := charm.ReadCharmDir(s.path) - c.Assert(err, jc.ErrorIsNil) - dir.SetRevision(42) - buf := &bytes.Buffer{} - err = dir.ArchiveTo(buf) - c.Assert(err, jc.ErrorIsNil) - bundlePath := path.Join(s.SeriesPath, "riak.charm") - err = ioutil.WriteFile(bundlePath, buf.Bytes(), 0644) - c.Assert(err, jc.ErrorIsNil) - - // Block operation - s.BlockAllChanges(c, "TestBlockUpgradesWithBundle") - err = runUpgradeCharm(c, "riak") - s.AssertBlocked(c, err, ".*TestBlockUpgradesWithBundle.*") -} - -func (s *UpgradeCharmSuccessSuite) TestForcedUpgrade(c *gc.C) { - err := runUpgradeCharm(c, "riak", "--force") - c.Assert(err, jc.ErrorIsNil) - s.assertUpgraded(c, 8, true) - // Local revision is not changed. - s.assertLocalRevision(c, 7, s.path) -} - -func (s *UpgradeCharmSuccessSuite) TestBlockForcedUpgrade(c *gc.C) { - // Block operation - s.BlockAllChanges(c, "TestBlockForcedUpgrade") - err := runUpgradeCharm(c, "riak", "--force") - c.Assert(err, jc.ErrorIsNil) - s.assertUpgraded(c, 8, true) - // Local revision is not changed. - s.assertLocalRevision(c, 7, s.path) -} - -var myriakMeta = []byte(` -name: myriak -summary: "K/V storage engine" -description: "Scalable K/V Store in Erlang with Clocks :-)" -provides: - endpoint: - interface: http - admin: - interface: http -peers: - ring: - interface: riak -`) - -func (s *UpgradeCharmSuccessSuite) TestSwitch(c *gc.C) { - myriakPath := testcharms.Repo.RenamedClonedDirPath(s.SeriesPath, "riak", "myriak") - err := ioutil.WriteFile(path.Join(myriakPath, "metadata.yaml"), myriakMeta, 0644) - c.Assert(err, jc.ErrorIsNil) - - // Test with local repo and no explicit revsion. - err = runUpgradeCharm(c, "riak", "--switch=local:myriak") - c.Assert(err, jc.ErrorIsNil) - curl := s.assertUpgraded(c, 7, false) - c.Assert(curl.String(), gc.Equals, "local:trusty/myriak-7") - s.assertLocalRevision(c, 7, myriakPath) - - // Now try the same with explicit revision - should fail. - err = runUpgradeCharm(c, "riak", "--switch=local:myriak-7") - c.Assert(err, gc.ErrorMatches, `already running specified charm "local:trusty/myriak-7"`) - - // Change the revision to 42 and upgrade to it with explicit revision. - err = ioutil.WriteFile(path.Join(myriakPath, "revision"), []byte("42"), 0644) - c.Assert(err, jc.ErrorIsNil) - err = runUpgradeCharm(c, "riak", "--switch=local:myriak-42") - c.Assert(err, jc.ErrorIsNil) - curl = s.assertUpgraded(c, 42, false) - c.Assert(curl.String(), gc.Equals, "local:trusty/myriak-42") - s.assertLocalRevision(c, 42, myriakPath) -} - -type UpgradeCharmCharmStoreSuite struct { - charmStoreSuite -} - -var _ = gc.Suite(&UpgradeCharmCharmStoreSuite{}) - -var upgradeCharmAuthorizationTests = []struct { - about string - uploadURL string - switchURL string - readPermUser string - expectError string -}{{ - about: "public charm, success", - uploadURL: "cs:~bob/trusty/wordpress1-10", - switchURL: "cs:~bob/trusty/wordpress1", -}, { - about: "public charm, fully resolved, success", - uploadURL: "cs:~bob/trusty/wordpress2-10", - switchURL: "cs:~bob/trusty/wordpress2-10", -}, { - about: "non-public charm, success", - uploadURL: "cs:~bob/trusty/wordpress3-10", - switchURL: "cs:~bob/trusty/wordpress3", - readPermUser: clientUserName, -}, { - about: "non-public charm, fully resolved, success", - uploadURL: "cs:~bob/trusty/wordpress4-10", - switchURL: "cs:~bob/trusty/wordpress4-10", - readPermUser: clientUserName, -}, { - about: "non-public charm, access denied", - uploadURL: "cs:~bob/trusty/wordpress5-10", - switchURL: "cs:~bob/trusty/wordpress5", - readPermUser: "bob", - expectError: `cannot resolve charm URL "cs:~bob/trusty/wordpress5": cannot get "/~bob/trusty/wordpress5/meta/any\?include=id": unauthorized: access denied for user "client-username"`, -}, { - about: "non-public charm, fully resolved, access denied", - uploadURL: "cs:~bob/trusty/wordpress6-47", - switchURL: "cs:~bob/trusty/wordpress6-47", - readPermUser: "bob", - expectError: `cannot retrieve charm "cs:~bob/trusty/wordpress6-47": cannot get archive: unauthorized: access denied for user "client-username"`, -}} - -func (s *UpgradeCharmCharmStoreSuite) TestUpgradeCharmAuthorization(c *gc.C) { - s.uploadCharm(c, "cs:~other/trusty/wordpress-0", "wordpress") - err := runDeploy(c, "cs:~other/trusty/wordpress-0") - c.Assert(err, jc.ErrorIsNil) - for i, test := range upgradeCharmAuthorizationTests { - c.Logf("test %d: %s", i, test.about) - url, _ := s.uploadCharm(c, test.uploadURL, "wordpress") - if test.readPermUser != "" { - s.changeReadPerm(c, url, test.readPermUser) - } - err := runUpgradeCharm(c, "wordpress", "--switch", test.switchURL) - if test.expectError != "" { - c.Assert(err, gc.ErrorMatches, test.expectError) - continue - } - c.Assert(err, jc.ErrorIsNil) - } -} === removed file 'src/github.com/juju/juju/cmd/juju/upgradejuju.go' --- src/github.com/juju/juju/cmd/juju/upgradejuju.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/upgradejuju.go 1970-01-01 00:00:00 +0000 @@ -1,418 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "bufio" - stderrors "errors" - "fmt" - "io" - "os" - "path" - "strings" - - "github.com/juju/cmd" - "github.com/juju/errors" - "launchpad.net/gnuflag" - - "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/cmd/juju/block" - "github.com/juju/juju/environs/config" - "github.com/juju/juju/environs/sync" - coretools "github.com/juju/juju/tools" - "github.com/juju/juju/version" -) - -// UpgradeJujuCommand upgrades the agents in a juju installation. -type UpgradeJujuCommand struct { - envcmd.EnvCommandBase - vers string - Version version.Number - UploadTools bool - DryRun bool - ResetPrevious bool - AssumeYes bool - Series []string -} - -var upgradeJujuDoc = ` -The upgrade-juju command upgrades a running environment by setting a version -number for all juju agents to run. By default, it chooses the most recent -supported version compatible with the command-line tools version. - -A development version is defined to be any version with an odd minor -version or a nonzero build component (for example version 2.1.1, 3.3.0 -and 2.0.0.1 are development versions; 2.0.3 and 3.4.1 are not). A -development version may be chosen in two cases: - - - when the current agent version is a development one and there is - a more recent version available with the same major.minor numbers; - - when an explicit --version major.minor is given (e.g. --version 1.17, - or 1.17.2, but not just 1) - -For development use, the --upload-tools flag specifies that the juju tools will -packaged (or compiled locally, if no jujud binaries exists, for which you will -need the golang packages installed) and uploaded before the version is set. -Currently the tools will be uploaded as if they had the version of the current -juju tool, unless specified otherwise by the --version flag. - -When run without arguments. upgrade-juju will try to upgrade to the -following versions, in order of preference, depending on the current -value of the environment's agent-version setting: - - - The highest patch.build version of the *next* stable major.minor version. - - The highest patch.build version of the *current* major.minor version. - -Both of these depend on tools availability, which some situations (no -outgoing internet access) and provider types (such as maas) require that -you manage yourself; see the documentation for "sync-tools". - -The upgrade-juju command will abort if an upgrade is already in -progress. It will also abort if a previous upgrade was partially -completed - this can happen if one of the state servers in a high -availability environment failed to upgrade. If a failed upgrade has -been resolved, the --reset-previous-upgrade flag can be used to reset -the environment's upgrade tracking state, allowing further upgrades.` - -func (c *UpgradeJujuCommand) Info() *cmd.Info { - return &cmd.Info{ - Name: "upgrade-juju", - Purpose: "upgrade the tools in a juju environment", - Doc: upgradeJujuDoc, - } -} - -func (c *UpgradeJujuCommand) SetFlags(f *gnuflag.FlagSet) { - f.StringVar(&c.vers, "version", "", "upgrade to specific version") - f.BoolVar(&c.UploadTools, "upload-tools", false, "upload local version of tools") - f.BoolVar(&c.DryRun, "dry-run", false, "don't change anything, just report what would change") - f.BoolVar(&c.ResetPrevious, "reset-previous-upgrade", false, "clear the previous (incomplete) upgrade status (use with care)") - f.BoolVar(&c.AssumeYes, "y", false, "answer 'yes' to confirmation prompts") - f.BoolVar(&c.AssumeYes, "yes", false, "") - f.Var(newSeriesValue(nil, &c.Series), "series", "upload tools for supplied comma-separated series list (OBSOLETE)") -} - -func (c *UpgradeJujuCommand) Init(args []string) error { - if c.vers != "" { - vers, err := version.Parse(c.vers) - if err != nil { - return err - } - if vers.Major != version.Current.Major { - return fmt.Errorf("cannot upgrade to version incompatible with CLI") - } - if c.UploadTools && vers.Build != 0 { - // TODO(fwereade): when we start taking versions from actual built - // code, we should disable --version when used with --upload-tools. - // For now, it's the only way to experiment with version upgrade - // 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") - } - c.Version = vers - } - if len(c.Series) > 0 && !c.UploadTools { - return fmt.Errorf("--series requires --upload-tools") - } - return cmd.CheckEmpty(args) -} - -var errUpToDate = stderrors.New("no upgrades available") - -func formatTools(tools coretools.List) string { - formatted := make([]string, len(tools)) - for i, tools := range tools { - formatted[i] = fmt.Sprintf(" %s", tools.Version.String()) - } - return strings.Join(formatted, "\n") -} - -type upgradeJujuAPI interface { - EnvironmentGet() (map[string]interface{}, error) - FindTools(majorVersion, minorVersion int, series, arch string) (result params.FindToolsResult, err error) - UploadTools(r io.Reader, vers version.Binary, additionalSeries ...string) (*coretools.Tools, error) - AbortCurrentUpgrade() error - SetEnvironAgentVersion(version version.Number) error - Close() error -} - -var getUpgradeJujuAPI = func(c *UpgradeJujuCommand) (upgradeJujuAPI, error) { - return c.NewAPIClient() -} - -// Run changes the version proposed for the juju envtools. -func (c *UpgradeJujuCommand) Run(ctx *cmd.Context) (err error) { - if len(c.Series) > 0 { - fmt.Fprintln(ctx.Stderr, "Use of --series is obsolete. --upload-tools now expands to all supported series of the same operating system.") - } - - client, err := getUpgradeJujuAPI(c) - if err != nil { - return err - } - defer client.Close() - defer func() { - if err == errUpToDate { - ctx.Infof(err.Error()) - err = nil - } - }() - - // Determine the version to upgrade to, uploading tools if necessary. - attrs, err := client.EnvironmentGet() - if err != nil { - return err - } - cfg, err := config.New(config.NoDefaults, attrs) - if err != nil { - return err - } - context, err := c.initVersions(client, cfg) - if err != nil { - return err - } - if c.UploadTools && !c.DryRun { - if err := context.uploadTools(); err != nil { - return block.ProcessBlockedError(err, block.BlockChange) - } - } - if err := context.validate(); err != nil { - return err - } - // TODO(fwereade): this list may be incomplete, pending envtools.Upload change. - ctx.Infof("available tools:\n%s", formatTools(context.tools)) - ctx.Infof("best version:\n %s", context.chosen) - if c.DryRun { - ctx.Infof("upgrade to this version by running\n juju upgrade-juju --version=\"%s\"\n", context.chosen) - } else { - if c.ResetPrevious { - if ok, err := c.confirmResetPreviousUpgrade(ctx); !ok || err != nil { - const message = "previous upgrade not reset and no new upgrade triggered" - if err != nil { - return errors.Annotate(err, message) - } - return errors.New(message) - } - if err := client.AbortCurrentUpgrade(); err != nil { - return block.ProcessBlockedError(err, block.BlockChange) - } - } - if err := client.SetEnvironAgentVersion(context.chosen); err != nil { - if params.IsCodeUpgradeInProgress(err) { - return errors.Errorf("%s\n\n"+ - "Please wait for the upgrade to complete or if there was a problem with\n"+ - "the last upgrade that has been resolved, consider running the\n"+ - "upgrade-juju command with the --reset-previous-upgrade flag.", err, - ) - } else { - return block.ProcessBlockedError(err, block.BlockChange) - } - } - logger.Infof("started upgrade to %s", context.chosen) - } - return nil -} - -const resetPreviousUpgradeMessage = ` -WARNING! using --reset-previous-upgrade when an upgrade is in progress -will cause the upgrade to fail. Only use this option to clear an -incomplete upgrade where the root cause has been resolved. - -Continue [y/N]? ` - -func (c *UpgradeJujuCommand) confirmResetPreviousUpgrade(ctx *cmd.Context) (bool, error) { - if c.AssumeYes { - return true, nil - } - fmt.Fprintf(ctx.Stdout, resetPreviousUpgradeMessage) - scanner := bufio.NewScanner(ctx.Stdin) - scanner.Scan() - err := scanner.Err() - if err != nil && err != io.EOF { - return false, err - } - answer := strings.ToLower(scanner.Text()) - return answer == "y" || answer == "yes", nil -} - -// initVersions collects state relevant to an upgrade decision. The returned -// agent and client versions, and the list of currently available tools, will -// always be accurate; the chosen version, and the flag indicating development -// mode, may remain blank until uploadTools or validate is called. -func (c *UpgradeJujuCommand) initVersions(client upgradeJujuAPI, cfg *config.Config) (*upgradeContext, error) { - agent, ok := cfg.AgentVersion() - if !ok { - // Can't happen. In theory. - return nil, fmt.Errorf("incomplete environment configuration") - } - if c.Version == agent { - return nil, errUpToDate - } - clientVersion := version.Current.Number - findResult, err := client.FindTools(clientVersion.Major, -1, "", "") - if err != nil { - return nil, err - } - err = findResult.Error - if findResult.Error != nil { - if !params.IsCodeNotFound(err) { - return nil, err - } - if !c.UploadTools { - // No tools found and we shouldn't upload any, so if we are not asking for a - // major upgrade, pretend there is no more recent version available. - if c.Version == version.Zero && agent.Major == clientVersion.Major { - return nil, errUpToDate - } - return nil, err - } - } - return &upgradeContext{ - agent: agent, - client: clientVersion, - chosen: c.Version, - tools: findResult.List, - apiClient: client, - config: cfg, - }, nil -} - -// upgradeContext holds the version information for making upgrade decisions. -type upgradeContext struct { - agent version.Number - client version.Number - chosen version.Number - tools coretools.List - config *config.Config - apiClient upgradeJujuAPI -} - -// uploadTools compiles jujud from $GOPATH and uploads it into the supplied -// storage. If no version has been explicitly chosen, the version number -// reported by the built tools will be based on the client version number. -// In any case, the version number reported will have a build component higher -// than that of any otherwise-matching available envtools. -// uploadTools resets the chosen version and replaces the available tools -// with the ones just uploaded. -func (context *upgradeContext) uploadTools() (err error) { - // TODO(fwereade): this is kinda crack: we should not assume that - // version.Current matches whatever source happens to be built. The - // ideal would be: - // 1) compile jujud from $GOPATH into some build dir - // 2) get actual version with `jujud version` - // 3) check actual version for compatibility with CLI tools - // 4) generate unique build version with reference to available tools - // 5) force-version that unique version into the dir directly - // 6) archive and upload the build dir - // ...but there's no way we have time for that now. In the meantime, - // considering the use cases, this should work well enough; but it - // won't detect an incompatible major-version change, which is a shame. - if context.chosen == version.Zero { - context.chosen = context.client - } - context.chosen = uploadVersion(context.chosen, context.tools) - - builtTools, err := sync.BuildToolsTarball(&context.chosen, "upgrade") - if err != nil { - return err - } - defer os.RemoveAll(builtTools.Dir) - - var uploaded *coretools.Tools - toolsPath := path.Join(builtTools.Dir, builtTools.StorageName) - logger.Infof("uploading tools %v (%dkB) to Juju state server", builtTools.Version, (builtTools.Size+512)/1024) - f, err := os.Open(toolsPath) - if err != nil { - return err - } - defer f.Close() - additionalSeries := version.OSSupportedSeries(builtTools.Version.OS) - uploaded, err = context.apiClient.UploadTools(f, builtTools.Version, additionalSeries...) - if err != nil { - return err - } - context.tools = coretools.List{uploaded} - return nil -} - -// validate chooses an upgrade version, if one has not already been chosen, -// and ensures the tools list contains no entries that do not have that version. -// If validate returns no error, the environment agent-version can be set to -// the value of the chosen field. -func (context *upgradeContext) validate() (err error) { - if context.chosen == version.Zero { - // No explicitly specified version, so find the version to which we - // need to upgrade. If the CLI and agent major versions match, we find - // next available stable release to upgrade to by incrementing the - // minor version, starting from the current agent version and doing - // major.minor+1.patch=0. If the CLI has a greater major version, - // we just use the CLI version as is. - nextVersion := context.agent - if nextVersion.Major == context.client.Major { - nextVersion.Minor += 1 - nextVersion.Patch = 0 - } else { - nextVersion = context.client - } - - newestNextStable, found := context.tools.NewestCompatible(nextVersion) - if found { - logger.Debugf("found a more recent stable version %s", newestNextStable) - context.chosen = newestNextStable - } else { - newestCurrent, found := context.tools.NewestCompatible(context.agent) - if found { - logger.Debugf("found more recent current version %s", newestCurrent) - context.chosen = newestCurrent - } else { - if context.agent.Major != context.client.Major { - return fmt.Errorf("no compatible tools available") - } else { - return fmt.Errorf("no more recent supported versions available") - } - } - } - } else { - // If not completely specified already, pick a single tools version. - filter := coretools.Filter{Number: context.chosen} - if context.tools, err = context.tools.Match(filter); err != nil { - return err - } - context.chosen, context.tools = context.tools.Newest() - } - if context.chosen == context.agent { - return errUpToDate - } - - // Disallow major.minor version downgrades. - if context.chosen.Major < context.agent.Major || - context.chosen.Major == context.agent.Major && context.chosen.Minor < context.agent.Minor { - // TODO(fwereade): I'm a bit concerned about old agent/CLI tools even - // *connecting* to environments with higher agent-versions; but ofc they - // have to connect in order to discover they shouldn't. However, once - // 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("cannot change version from %s to %s", context.agent, context.chosen) - } - - return nil -} - -// uploadVersion returns a copy of the supplied version with a build number -// higher than any of the supplied tools that share its major, minor and patch. -func uploadVersion(vers version.Number, existing coretools.List) version.Number { - vers.Build++ - for _, t := range existing { - if t.Version.Major != vers.Major || t.Version.Minor != vers.Minor || t.Version.Patch != vers.Patch { - continue - } - if t.Version.Build >= vers.Build { - vers.Build = t.Version.Build + 1 - } - } - return vers -} === removed file 'src/github.com/juju/juju/cmd/juju/upgradejuju_test.go' --- src/github.com/juju/juju/cmd/juju/upgradejuju_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/upgradejuju_test.go 1970-01-01 00:00:00 +0000 @@ -1,711 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "archive/tar" - "bytes" - "compress/gzip" - "io" - "io/ioutil" - "strings" - - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/apiserver/common" - "github.com/juju/juju/apiserver/params" - apiservertesting "github.com/juju/juju/apiserver/testing" - "github.com/juju/juju/cmd/envcmd" - "github.com/juju/juju/environs/config" - "github.com/juju/juju/environs/filestorage" - "github.com/juju/juju/environs/sync" - envtesting "github.com/juju/juju/environs/testing" - "github.com/juju/juju/environs/tools" - toolstesting "github.com/juju/juju/environs/tools/testing" - jujutesting "github.com/juju/juju/juju/testing" - "github.com/juju/juju/network" - _ "github.com/juju/juju/provider/dummy" - "github.com/juju/juju/state" - coretesting "github.com/juju/juju/testing" - coretools "github.com/juju/juju/tools" - "github.com/juju/juju/version" -) - -type UpgradeJujuSuite struct { - jujutesting.JujuConnSuite - - resources *common.Resources - authoriser apiservertesting.FakeAuthorizer - - toolsDir string - CmdBlockHelper -} - -func (s *UpgradeJujuSuite) SetUpTest(c *gc.C) { - s.JujuConnSuite.SetUpTest(c) - s.resources = common.NewResources() - s.authoriser = apiservertesting.FakeAuthorizer{ - Tag: s.AdminUserTag(c), - } - - s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) - c.Assert(s.CmdBlockHelper, gc.NotNil) - s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) -} - -var _ = gc.Suite(&UpgradeJujuSuite{}) - -var upgradeJujuTests = []struct { - about string - tools []string - currentVersion string - agentVersion string - - args []string - expectInitErr string - expectErr string - expectVersion string - expectUploaded []string -}{{ - about: "unwanted extra argument", - currentVersion: "1.0.0-quantal-amd64", - args: []string{"foo"}, - expectInitErr: "unrecognized args:.*", -}, { - about: "removed arg --dev specified", - currentVersion: "1.0.0-quantal-amd64", - args: []string{"--dev"}, - expectInitErr: "flag provided but not defined: --dev", -}, { - about: "invalid --version value", - currentVersion: "1.0.0-quantal-amd64", - args: []string{"--version", "invalid-version"}, - expectInitErr: "invalid version .*", -}, { - about: "just major version, no minor specified", - currentVersion: "4.2.0-quantal-amd64", - args: []string{"--version", "4"}, - expectInitErr: `invalid version "4"`, -}, { - about: "major version upgrade to incompatible version", - currentVersion: "2.0.0-quantal-amd64", - args: []string{"--version", "5.2.0"}, - expectInitErr: "cannot upgrade to version incompatible with CLI", -}, { - about: "major version downgrade to incompatible version", - currentVersion: "4.2.0-quantal-amd64", - args: []string{"--version", "3.2.0"}, - expectInitErr: "cannot upgrade to version incompatible with CLI", -}, { - about: "invalid --series", - currentVersion: "4.2.0-quantal-amd64", - args: []string{"--series", "precise&quantal"}, - expectInitErr: `invalid value "precise&quantal" for flag --series: .*`, -}, { - about: "--series without --upload-tools", - currentVersion: "4.2.0-quantal-amd64", - args: []string{"--series", "precise,quantal"}, - expectInitErr: "--series requires --upload-tools", -}, { - about: "--upload-tools with inappropriate version 1", - currentVersion: "4.2.0-quantal-amd64", - args: []string{"--upload-tools", "--version", "3.1.0"}, - expectInitErr: "cannot upgrade to version incompatible with CLI", -}, { - about: "--upload-tools with inappropriate version 2", - currentVersion: "3.2.7-quantal-amd64", - args: []string{"--upload-tools", "--version", "3.2.8.4"}, - expectInitErr: "cannot specify build number when uploading tools", -}, { - about: "latest supported stable release", - tools: []string{"2.1.0-quantal-amd64", "2.1.2-quantal-i386", "2.1.3-quantal-amd64", "2.1-dev1-quantal-amd64"}, - currentVersion: "2.0.0-quantal-amd64", - agentVersion: "2.0.0", - expectVersion: "2.1.3", -}, { - about: "latest current release", - tools: []string{"2.0.5-quantal-amd64", "2.0.1-quantal-i386", "2.3.3-quantal-amd64"}, - currentVersion: "2.0.0-quantal-amd64", - agentVersion: "2.0.0", - expectVersion: "2.0.5", -}, { - about: "latest current release matching CLI, major version", - tools: []string{"3.2.0-quantal-amd64"}, - currentVersion: "3.2.0-quantal-amd64", - agentVersion: "2.8.2", - expectVersion: "3.2.0", -}, { - about: "latest current release matching CLI, major version, no matching major tools", - tools: []string{"2.8.2-quantal-amd64"}, - currentVersion: "3.2.0-quantal-amd64", - agentVersion: "2.8.2", - expectErr: "no matching tools available", -}, { - about: "latest current release matching CLI, major version, no matching tools", - tools: []string{"3.3.0-quantal-amd64"}, - currentVersion: "3.2.0-quantal-amd64", - agentVersion: "2.8.2", - expectErr: "no compatible tools available", -}, { - about: "no next supported available", - tools: []string{"2.2.0-quantal-amd64", "2.2.5-quantal-i386", "2.3.3-quantal-amd64", "2.1-dev1-quantal-amd64"}, - currentVersion: "2.0.0-quantal-amd64", - agentVersion: "2.0.0", - expectErr: "no more recent supported versions available", -}, { - about: "latest supported stable, when client is dev", - tools: []string{"2.1-dev1-quantal-amd64", "2.1.0-quantal-amd64", "2.3-dev0-quantal-amd64", "3.0.1-quantal-amd64"}, - currentVersion: "2.1-dev0-quantal-amd64", - agentVersion: "2.0.0", - expectVersion: "2.1.0", -}, { - about: "latest current, when agent is dev", - tools: []string{"2.1-dev1-quantal-amd64", "2.2.0-quantal-amd64", "2.3-dev0-quantal-amd64", "3.0.1-quantal-amd64"}, - currentVersion: "2.0.0-quantal-amd64", - agentVersion: "2.1-dev0", - expectVersion: "2.2.0", -}, { - about: "specified version", - tools: []string{"2.3-dev0-quantal-amd64"}, - currentVersion: "2.0.0-quantal-amd64", - agentVersion: "2.0.0", - args: []string{"--version", "2.3-dev0"}, - expectVersion: "2.3-dev0", -}, { - about: "specified major version", - tools: []string{"3.2.0-quantal-amd64"}, - currentVersion: "3.2.0-quantal-amd64", - agentVersion: "2.8.2", - args: []string{"--version", "3.2.0"}, - expectVersion: "3.2.0", -}, { - about: "specified version missing, but already set", - currentVersion: "3.0.0-quantal-amd64", - agentVersion: "3.0.0", - args: []string{"--version", "3.0.0"}, - expectVersion: "3.0.0", -}, { - about: "specified version, no tools", - currentVersion: "3.0.0-quantal-amd64", - agentVersion: "3.0.0", - args: []string{"--version", "3.2.0"}, - expectErr: "no tools available", -}, { - about: "specified version, no matching major version", - tools: []string{"4.2.0-quantal-amd64"}, - currentVersion: "3.0.0-quantal-amd64", - agentVersion: "3.0.0", - args: []string{"--version", "3.2.0"}, - expectErr: "no matching tools available", -}, { - about: "specified version, no matching minor version", - tools: []string{"3.4.0-quantal-amd64"}, - currentVersion: "3.0.0-quantal-amd64", - agentVersion: "3.0.0", - args: []string{"--version", "3.2.0"}, - expectErr: "no matching tools available", -}, { - about: "specified version, no matching patch version", - tools: []string{"3.2.5-quantal-amd64"}, - currentVersion: "3.0.0-quantal-amd64", - agentVersion: "3.0.0", - args: []string{"--version", "3.2.0"}, - expectErr: "no matching tools available", -}, { - about: "specified version, no matching build version", - tools: []string{"3.2.0.2-quantal-amd64"}, - currentVersion: "3.0.0-quantal-amd64", - agentVersion: "3.0.0", - args: []string{"--version", "3.2.0"}, - expectErr: "no matching tools available", -}, { - about: "major version downgrade to incompatible version", - tools: []string{"3.2.0-quantal-amd64"}, - currentVersion: "3.2.0-quantal-amd64", - agentVersion: "4.2.0", - args: []string{"--version", "3.2.0"}, - expectErr: "cannot change version from 4.2.0 to 3.2.0", -}, { - about: "minor version downgrade to incompatible version", - tools: []string{"3.2.0-quantal-amd64"}, - currentVersion: "3.2.0-quantal-amd64", - agentVersion: "3.3-dev0", - args: []string{"--version", "3.2.0"}, - expectErr: "cannot change version from 3.3-dev0 to 3.2.0", -}, { - about: "nothing available", - currentVersion: "2.0.0-quantal-amd64", - agentVersion: "2.0.0", - expectVersion: "2.0.0", -}, { - about: "nothing available 2", - currentVersion: "2.0.0-quantal-amd64", - tools: []string{"3.2.0-quantal-amd64"}, - agentVersion: "2.0.0", - expectVersion: "2.0.0", -}, { - about: "upload with default series", - currentVersion: "2.2.0-quantal-amd64", - agentVersion: "2.0.0", - args: []string{"--upload-tools"}, - expectVersion: "2.2.0.1", - expectUploaded: []string{"2.2.0.1-quantal-amd64", "2.2.0.1-%LTS%-amd64", "2.2.0.1-raring-amd64"}, -}, { - about: "upload with explicit version", - currentVersion: "2.2.0-quantal-amd64", - agentVersion: "2.0.0", - args: []string{"--upload-tools", "--version", "2.7.3"}, - expectVersion: "2.7.3.1", - expectUploaded: []string{"2.7.3.1-quantal-amd64", "2.7.3.1-%LTS%-amd64", "2.7.3.1-raring-amd64"}, -}, { - about: "upload with explicit series", - currentVersion: "2.2.0-quantal-amd64", - agentVersion: "2.0.0", - args: []string{"--upload-tools", "--series", "raring"}, - expectVersion: "2.2.0.1", - expectUploaded: []string{"2.2.0.1-quantal-amd64", "2.2.0.1-raring-amd64"}, -}, { - about: "upload dev version, currently on release version", - currentVersion: "2.1.0-quantal-amd64", - agentVersion: "2.0.0", - args: []string{"--upload-tools"}, - expectVersion: "2.1.0.1", - expectUploaded: []string{"2.1.0.1-quantal-amd64", "2.1.0.1-%LTS%-amd64", "2.1.0.1-raring-amd64"}, -}, { - about: "upload bumps version when necessary", - tools: []string{"2.4.6-quantal-amd64", "2.4.8-quantal-amd64"}, - currentVersion: "2.4.6-quantal-amd64", - agentVersion: "2.4.0", - args: []string{"--upload-tools"}, - expectVersion: "2.4.6.1", - expectUploaded: []string{"2.4.6.1-quantal-amd64", "2.4.6.1-%LTS%-amd64", "2.4.6.1-raring-amd64"}, -}, { - about: "upload re-bumps version when necessary", - tools: []string{"2.4.6-quantal-amd64", "2.4.6.2-saucy-i386", "2.4.8-quantal-amd64"}, - currentVersion: "2.4.6-quantal-amd64", - agentVersion: "2.4.6.2", - args: []string{"--upload-tools"}, - expectVersion: "2.4.6.3", - expectUploaded: []string{"2.4.6.3-quantal-amd64", "2.4.6.3-%LTS%-amd64", "2.4.6.3-raring-amd64"}, -}, { - about: "upload with explicit version bumps when necessary", - currentVersion: "2.2.0-quantal-amd64", - tools: []string{"2.7.3.1-quantal-amd64"}, - agentVersion: "2.0.0", - args: []string{"--upload-tools", "--version", "2.7.3"}, - expectVersion: "2.7.3.2", - expectUploaded: []string{"2.7.3.2-quantal-amd64", "2.7.3.2-%LTS%-amd64", "2.7.3.2-raring-amd64"}, -}, { - about: "latest supported stable release", - tools: []string{"1.21.3-quantal-amd64", "1.22.1-quantal-amd64"}, - currentVersion: "1.22.1-quantal-amd64", - agentVersion: "1.20.14", - expectVersion: "1.21.3", -}} - -func (s *UpgradeJujuSuite) TestUpgradeJuju(c *gc.C) { - oldVersion := version.Current - defer func() { - version.Current = oldVersion - }() - - for i, test := range upgradeJujuTests { - c.Logf("\ntest %d: %s", i, test.about) - s.Reset(c) - tools.DefaultBaseURL = "" - - // Set up apparent CLI version and initialize the command. - version.Current = version.MustParseBinary(test.currentVersion) - com := &UpgradeJujuCommand{} - if err := coretesting.InitCommand(envcmd.Wrap(com), test.args); err != nil { - if test.expectInitErr != "" { - c.Check(err, gc.ErrorMatches, test.expectInitErr) - } else { - c.Check(err, jc.ErrorIsNil) - } - continue - } - - // Set up state and environ, and run the command. - toolsDir := c.MkDir() - updateAttrs := map[string]interface{}{ - "agent-version": test.agentVersion, - "agent-metadata-url": "file://" + toolsDir + "/tools", - } - err := s.State.UpdateEnvironConfig(updateAttrs, nil, nil) - c.Assert(err, jc.ErrorIsNil) - versions := make([]version.Binary, len(test.tools)) - for i, v := range test.tools { - versions[i] = version.MustParseBinary(v) - } - if len(versions) > 0 { - stor, err := filestorage.NewFileStorageWriter(toolsDir) - c.Assert(err, jc.ErrorIsNil) - envtesting.MustUploadFakeToolsVersions(stor, s.Environ.Config().AgentStream(), versions...) - } - - err = com.Run(coretesting.Context(c)) - if test.expectErr != "" { - c.Check(err, gc.ErrorMatches, test.expectErr) - continue - } else if !c.Check(err, jc.ErrorIsNil) { - continue - } - - // Check expected changes to environ/state. - cfg, err := s.State.EnvironConfig() - c.Check(err, jc.ErrorIsNil) - agentVersion, ok := cfg.AgentVersion() - c.Check(ok, jc.IsTrue) - c.Check(agentVersion, gc.Equals, version.MustParse(test.expectVersion)) - - for _, uploaded := range test.expectUploaded { - // Substitute latest LTS for placeholder in expected series for uploaded tools - uploaded = strings.Replace(uploaded, "%LTS%", config.LatestLtsSeries(), 1) - vers := version.MustParseBinary(uploaded) - s.checkToolsUploaded(c, vers, agentVersion) - } - } -} - -func (s *UpgradeJujuSuite) checkToolsUploaded(c *gc.C, vers version.Binary, agentVersion version.Number) { - storage, err := s.State.ToolsStorage() - c.Assert(err, jc.ErrorIsNil) - defer storage.Close() - _, r, err := storage.Tools(vers) - if !c.Check(err, jc.ErrorIsNil) { - return - } - data, err := ioutil.ReadAll(r) - r.Close() - c.Check(err, jc.ErrorIsNil) - expectContent := version.Current - expectContent.Number = agentVersion - checkToolsContent(c, data, "jujud contents "+expectContent.String()) -} - -func checkToolsContent(c *gc.C, data []byte, uploaded string) { - zr, err := gzip.NewReader(bytes.NewReader(data)) - c.Check(err, jc.ErrorIsNil) - defer zr.Close() - tr := tar.NewReader(zr) - found := false - for { - hdr, err := tr.Next() - if err == io.EOF { - break - } - c.Check(err, jc.ErrorIsNil) - if strings.ContainsAny(hdr.Name, "/\\") { - c.Fail() - } - if hdr.Typeflag != tar.TypeReg { - c.Fail() - } - content, err := ioutil.ReadAll(tr) - c.Check(err, jc.ErrorIsNil) - c.Check(string(content), gc.Equals, uploaded) - found = true - } - c.Check(found, jc.IsTrue) -} - -// JujuConnSuite very helpfully uploads some default -// tools to the environment's storage. We don't want -// 'em there; but we do want a consistent default-series -// in the environment state. -func (s *UpgradeJujuSuite) Reset(c *gc.C) { - s.JujuConnSuite.Reset(c) - envtesting.RemoveTools(c, s.DefaultToolsStorage, s.Environ.Config().AgentStream()) - updateAttrs := map[string]interface{}{ - "default-series": "raring", - "agent-version": "1.2.3", - } - err := s.State.UpdateEnvironConfig(updateAttrs, nil, nil) - c.Assert(err, jc.ErrorIsNil) - s.PatchValue(&sync.BuildToolsTarball, toolstesting.GetMockBuildTools(c)) - - // Set API host ports so FindTools works. - hostPorts := [][]network.HostPort{ - network.NewHostPorts(1234, "0.1.2.3"), - } - err = s.State.SetAPIHostPorts(hostPorts) - c.Assert(err, jc.ErrorIsNil) - - s.CmdBlockHelper = NewCmdBlockHelper(s.APIState) - c.Assert(s.CmdBlockHelper, gc.NotNil) - s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() }) -} - -func (s *UpgradeJujuSuite) TestUpgradeJujuWithRealUpload(c *gc.C) { - s.Reset(c) - cmd := envcmd.Wrap(&UpgradeJujuCommand{}) - _, err := coretesting.RunCommand(c, cmd, "--upload-tools") - c.Assert(err, jc.ErrorIsNil) - vers := version.Current - vers.Build = 1 - s.checkToolsUploaded(c, vers, vers.Number) -} - -func (s *UpgradeJujuSuite) TestBlockUpgradeJujuWithRealUpload(c *gc.C) { - s.Reset(c) - cmd := envcmd.Wrap(&UpgradeJujuCommand{}) - // Block operation - s.BlockAllChanges(c, "TestBlockUpgradeJujuWithRealUpload") - _, err := coretesting.RunCommand(c, cmd, "--upload-tools") - s.AssertBlocked(c, err, ".*TestBlockUpgradeJujuWithRealUpload.*") -} - -type DryRunTest struct { - about string - cmdArgs []string - tools []string - currentVersion string - agentVersion string - expectedCmdOutput string -} - -func (s *UpgradeJujuSuite) TestUpgradeDryRun(c *gc.C) { - tests := []DryRunTest{ - { - about: "dry run outputs and doesn't change anything when uploading tools", - cmdArgs: []string{"--upload-tools", "--dry-run"}, - tools: []string{"2.1.0-quantal-amd64", "2.1.2-quantal-i386", "2.1.3-quantal-amd64", "2.1-dev1-quantal-amd64", "2.2.3-quantal-amd64"}, - currentVersion: "2.0.0-quantal-amd64", - agentVersion: "2.0.0", - expectedCmdOutput: `available tools: - 2.1-dev1-quantal-amd64 - 2.1.0-quantal-amd64 - 2.1.2-quantal-i386 - 2.1.3-quantal-amd64 - 2.2.3-quantal-amd64 -best version: - 2.1.3 -upgrade to this version by running - juju upgrade-juju --version="2.1.3" -`, - }, - { - about: "dry run outputs and doesn't change anything", - cmdArgs: []string{"--dry-run"}, - tools: []string{"2.1.0-quantal-amd64", "2.1.2-quantal-i386", "2.1.3-quantal-amd64", "2.1-dev1-quantal-amd64", "2.2.3-quantal-amd64"}, - currentVersion: "2.0.0-quantal-amd64", - agentVersion: "2.0.0", - expectedCmdOutput: `available tools: - 2.1-dev1-quantal-amd64 - 2.1.0-quantal-amd64 - 2.1.2-quantal-i386 - 2.1.3-quantal-amd64 - 2.2.3-quantal-amd64 -best version: - 2.1.3 -upgrade to this version by running - juju upgrade-juju --version="2.1.3" -`, - }, - } - - for i, test := range tests { - c.Logf("\ntest %d: %s", i, test.about) - s.Reset(c) - tools.DefaultBaseURL = "" - - s.PatchValue(&version.Current, version.MustParseBinary(test.currentVersion)) - com := &UpgradeJujuCommand{} - err := coretesting.InitCommand(envcmd.Wrap(com), test.cmdArgs) - c.Assert(err, jc.ErrorIsNil) - toolsDir := c.MkDir() - updateAttrs := map[string]interface{}{ - "agent-version": test.agentVersion, - "agent-metadata-url": "file://" + toolsDir + "/tools", - } - - err = s.State.UpdateEnvironConfig(updateAttrs, nil, nil) - c.Assert(err, jc.ErrorIsNil) - versions := make([]version.Binary, len(test.tools)) - for i, v := range test.tools { - versions[i] = version.MustParseBinary(v) - } - if len(versions) > 0 { - stor, err := filestorage.NewFileStorageWriter(toolsDir) - c.Assert(err, jc.ErrorIsNil) - envtesting.MustUploadFakeToolsVersions(stor, s.Environ.Config().AgentStream(), versions...) - } - - ctx := coretesting.Context(c) - err = com.Run(ctx) - c.Assert(err, jc.ErrorIsNil) - - // Check agent version doesn't change - cfg, err := s.State.EnvironConfig() - c.Assert(err, jc.ErrorIsNil) - agentVer, ok := cfg.AgentVersion() - c.Assert(ok, jc.IsTrue) - c.Assert(agentVer, gc.Equals, version.MustParse(test.agentVersion)) - output := coretesting.Stderr(ctx) - c.Assert(output, gc.Equals, test.expectedCmdOutput) - } -} - -func (s *UpgradeJujuSuite) TestUpgradeInProgress(c *gc.C) { - fakeAPI := NewFakeUpgradeJujuAPI(c, s.State) - fakeAPI.setVersionErr = ¶ms.Error{ - Message: "a message from the server about the problem", - Code: params.CodeUpgradeInProgress, - } - fakeAPI.patch(s) - cmd := &UpgradeJujuCommand{} - err := coretesting.InitCommand(envcmd.Wrap(cmd), []string{}) - c.Assert(err, jc.ErrorIsNil) - - err = cmd.Run(coretesting.Context(c)) - c.Assert(err, gc.ErrorMatches, "a message from the server about the problem\n"+ - "\n"+ - "Please wait for the upgrade to complete or if there was a problem with\n"+ - "the last upgrade that has been resolved, consider running the\n"+ - "upgrade-juju command with the --reset-previous-upgrade flag.", - ) -} - -func (s *UpgradeJujuSuite) TestBlockUpgradeInProgress(c *gc.C) { - fakeAPI := NewFakeUpgradeJujuAPI(c, s.State) - fakeAPI.setVersionErr = common.ErrOperationBlocked("The operation has been blocked.") - fakeAPI.patch(s) - cmd := &UpgradeJujuCommand{} - err := coretesting.InitCommand(envcmd.Wrap(cmd), []string{}) - c.Assert(err, jc.ErrorIsNil) - - // Block operation - s.BlockAllChanges(c, "TestBlockUpgradeInProgress") - err = cmd.Run(coretesting.Context(c)) - s.AssertBlocked(c, err, ".*To unblock changes.*") -} - -func (s *UpgradeJujuSuite) TestResetPreviousUpgrade(c *gc.C) { - fakeAPI := NewFakeUpgradeJujuAPI(c, s.State) - fakeAPI.patch(s) - - ctx := coretesting.Context(c) - var stdin bytes.Buffer - ctx.Stdin = &stdin - - run := func(answer string, expect bool, args ...string) { - stdin.Reset() - if answer != "" { - stdin.WriteString(answer) - } - - fakeAPI.reset() - - cmd := &UpgradeJujuCommand{} - err := coretesting.InitCommand(envcmd.Wrap(cmd), - append([]string{"--reset-previous-upgrade"}, args...)) - c.Assert(err, jc.ErrorIsNil) - err = cmd.Run(ctx) - if expect { - c.Assert(err, jc.ErrorIsNil) - } else { - c.Assert(err, gc.ErrorMatches, "previous upgrade not reset and no new upgrade triggered") - } - - c.Assert(fakeAPI.abortCurrentUpgradeCalled, gc.Equals, expect) - expectedVersion := version.Number{} - if expect { - expectedVersion = fakeAPI.nextVersion.Number - } - c.Assert(fakeAPI.setVersionCalledWith, gc.Equals, expectedVersion) - } - - const expectUpgrade = true - const expectNoUpgrade = false - - // EOF on stdin - equivalent to answering no. - run("", expectNoUpgrade) - - // -y on command line - no confirmation required - run("", expectUpgrade, "-y") - - // --yes on command line - no confirmation required - run("", expectUpgrade, "--yes") - - // various ways of saying "yes" to the prompt - for _, answer := range []string{"y", "Y", "yes", "YES"} { - run(answer, expectUpgrade) - } - - // various ways of saying "no" to the prompt - for _, answer := range []string{"n", "N", "no", "foo"} { - run(answer, expectNoUpgrade) - } -} - -func NewFakeUpgradeJujuAPI(c *gc.C, st *state.State) *fakeUpgradeJujuAPI { - nextVersion := version.Current - nextVersion.Minor++ - return &fakeUpgradeJujuAPI{ - c: c, - st: st, - nextVersion: nextVersion, - } -} - -type fakeUpgradeJujuAPI struct { - c *gc.C - st *state.State - nextVersion version.Binary - setVersionErr error - abortCurrentUpgradeCalled bool - setVersionCalledWith version.Number -} - -func (a *fakeUpgradeJujuAPI) reset() { - a.setVersionErr = nil - a.abortCurrentUpgradeCalled = false - a.setVersionCalledWith = version.Number{} -} - -func (a *fakeUpgradeJujuAPI) patch(s *UpgradeJujuSuite) { - s.PatchValue(&getUpgradeJujuAPI, func(*UpgradeJujuCommand) (upgradeJujuAPI, error) { - return a, nil - }) -} - -func (a *fakeUpgradeJujuAPI) EnvironmentGet() (map[string]interface{}, error) { - config, err := a.st.EnvironConfig() - if err != nil { - return make(map[string]interface{}), err - } - return config.AllAttrs(), nil -} - -func (a *fakeUpgradeJujuAPI) FindTools(majorVersion, minorVersion int, series, arch string) ( - result params.FindToolsResult, err error, -) { - tools := toolstesting.MakeTools(a.c, a.c.MkDir(), "released", []string{a.nextVersion.String()}) - return params.FindToolsResult{ - List: tools, - Error: nil, - }, nil -} - -func (a *fakeUpgradeJujuAPI) UploadTools(r io.Reader, vers version.Binary, additionalSeries ...string) ( - *coretools.Tools, error, -) { - panic("not implemented") -} - -func (a *fakeUpgradeJujuAPI) AbortCurrentUpgrade() error { - a.abortCurrentUpgradeCalled = true - return nil -} - -func (a *fakeUpgradeJujuAPI) SetEnvironAgentVersion(v version.Number) error { - a.setVersionCalledWith = v - return a.setVersionErr -} - -func (a *fakeUpgradeJujuAPI) Close() error { - return nil -} === modified file 'src/github.com/juju/juju/cmd/juju/user/add.go' --- src/github.com/juju/juju/cmd/juju/user/add.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/user/add.go 2015-10-23 18:29:32 +0000 @@ -5,46 +5,46 @@ import ( "fmt" - "os" - "strings" "github.com/juju/cmd" "github.com/juju/errors" "github.com/juju/names" + "github.com/juju/utils" "launchpad.net/gnuflag" "github.com/juju/juju/cmd/juju/block" - "github.com/juju/juju/environs/configstore" ) const userAddCommandDoc = ` Add users to an existing environment. The user information is stored within an existing environment, and will be -lost when the environent is destroyed. An environment file (.jenv) will be -written out in the current directory. You can control the name and location -of this file using the --output option. +lost when the environent is destroyed. A server file will be written out in +the current directory. You can control the name and location of this file +using the --output option. Examples: - # Add user "foobar". You will be prompted to enter a password. - juju user add foobar - - # Add user "foobar" with a strong random password is generated. - juju user add foobar --generate + # Add user "foobar" with a strong random password is generated. + juju user add foobar See Also: - juju user change-password + juju help user change-password ` +// AddUserAPI defines the usermanager API methods that the add command uses. +type AddUserAPI interface { + AddUser(username, displayName, password string) (names.UserTag, error) + Close() error +} + // AddCommand adds new users into a Juju Server. type AddCommand struct { UserCommandBase + api AddUserAPI User string DisplayName string - Password string OutPath string - Generate bool } // Info implements Command.Info. @@ -59,7 +59,6 @@ // SetFlags implements Command.SetFlags. func (c *AddCommand) SetFlags(f *gnuflag.FlagSet) { - f.BoolVar(&c.Generate, "generate", false, "generate a new strong password") f.StringVar(&c.OutPath, "o", "", "specify the environment file for new user") f.StringVar(&c.OutPath, "output", "", "") } @@ -73,126 +72,39 @@ if len(args) > 0 { c.DisplayName, args = args[0], args[1:] } + if c.OutPath == "" { + c.OutPath = c.User + ".server" + } return cmd.CheckEmpty(args) } -// AddUserAPI defines the usermanager API methods that the add command uses. -type AddUserAPI interface { - AddUser(username, displayName, password string) (names.UserTag, error) - Close() error -} - -// ShareEnvironmentAPI defines the client API methods that the add command uses. -type ShareEnvironmentAPI interface { - ShareEnvironment(users ...names.UserTag) error - Close() error -} - -func (c *AddCommand) getAddUserAPI() (AddUserAPI, error) { - return c.NewUserManagerClient() -} - -func (c *AddCommand) getShareEnvAPI() (ShareEnvironmentAPI, error) { - return c.NewAPIClient() -} - -var ( - getAddUserAPI = (*AddCommand).getAddUserAPI - getShareEnvAPI = (*AddCommand).getShareEnvAPI -) - // Run implements Command.Run. func (c *AddCommand) Run(ctx *cmd.Context) error { - client, err := getAddUserAPI(c) - if err != nil { - return err - } - defer client.Close() - - if !c.Generate { - ctx.Infof("To generate a random strong password, use the --generate flag.") - } - - shareClient, err := getShareEnvAPI(c) - if err != nil { - return err - } - defer shareClient.Close() - - c.Password, err = c.generateOrReadPassword(ctx, c.Generate) - if err != nil { - return errors.Trace(err) - } - - tag, err := client.AddUser(c.User, c.DisplayName, c.Password) - if err != nil { + if c.api == nil { + api, err := c.NewUserManagerAPIClient() + if err != nil { + return errors.Trace(err) + } + c.api = api + defer c.api.Close() + } + + password, err := utils.RandomPassword() + if err != nil { + return errors.Annotate(err, "failed to generate random password") + } + randomPasswordNotify(password) + + if _, err := c.api.AddUser(c.User, c.DisplayName, password); err != nil { return block.ProcessBlockedError(err, block.BlockChange) } - // Until we have multiple environments stored in a state server - // it makes no sense at all to create a user and not have that user - // able to log in and use the one and only environment. - // So we share the existing environment with the user here and now. - err = shareClient.ShareEnvironment(tag) - if err != nil { - return err - } - user := c.User + displayName := c.User if c.DisplayName != "" { - user = fmt.Sprintf("%s (%s)", c.DisplayName, user) - } - - fmt.Fprintf(ctx.Stdout, "user %q added\n", user) - if c.OutPath == "" { - c.OutPath = c.User + ".jenv" - } - - outPath := normaliseJenvPath(ctx, c.OutPath) - err = generateUserJenv(c.ConnectionName(), c.User, c.Password, outPath) - if err == nil { - fmt.Fprintf(ctx.Stdout, "environment file written to %s\n", outPath) - } - - return err -} - -func normaliseJenvPath(ctx *cmd.Context, outPath string) string { - if !strings.HasSuffix(outPath, ".jenv") { - outPath = outPath + ".jenv" - } - return ctx.AbsPath(outPath) -} - -func generateUserJenv(envName, user, password, outPath string) error { - store, err := configstore.Default() - if err != nil { - return errors.Trace(err) - } - storeInfo, err := store.ReadInfo(envName) - if err != nil { - return errors.Trace(err) - } - endpoint := storeInfo.APIEndpoint() - outputInfo := configstore.EnvironInfoData{ - User: user, - Password: password, - EnvironUUID: endpoint.EnvironUUID, - StateServers: endpoint.Addresses, - CACert: endpoint.CACert, - } - yaml, err := cmd.FormatYaml(outputInfo) - if err != nil { - return errors.Trace(err) - } - - outFile, err := os.Create(outPath) - if err != nil { - return errors.Trace(err) - } - defer outFile.Close() - outFile.Write(yaml) - if err != nil { - return errors.Trace(err) - } - return nil + displayName = fmt.Sprintf("%s (%s)", c.DisplayName, c.User) + } + + ctx.Infof("user %q added", displayName) + + return writeServerFile(c, ctx, c.User, password, c.OutPath) } === modified file 'src/github.com/juju/juju/cmd/juju/user/add_test.go' --- src/github.com/juju/juju/cmd/juju/user/add_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/user/add_test.go 2015-10-23 18:29:32 +0000 @@ -4,8 +4,6 @@ package user_test import ( - "io/ioutil" - "path/filepath" "strings" "github.com/juju/cmd" @@ -24,7 +22,9 @@ // This suite provides basic tests for the "user add" command type UserAddCommandSuite struct { BaseSuite - mockAPI *mockAddUserAPI + mockAPI *mockAddUserAPI + randomPassword string + serverFilename string } var _ = gc.Suite(&UserAddCommandSuite{}) @@ -32,16 +32,19 @@ func (s *UserAddCommandSuite) SetUpTest(c *gc.C) { s.BaseSuite.SetUpTest(c) s.mockAPI = &mockAddUserAPI{} - s.PatchValue(user.GetAddUserAPI, func(c *user.AddCommand) (user.AddUserAPI, error) { - return s.mockAPI, nil + s.randomPassword = "" + s.serverFilename = "" + s.PatchValue(user.RandomPasswordNotify, func(pwd string) { + s.randomPassword = pwd }) - s.PatchValue(user.GetShareEnvAPI, func(c *user.AddCommand) (user.ShareEnvironmentAPI, error) { - return s.mockAPI, nil + s.PatchValue(user.ServerFileNotify, func(filename string) { + s.serverFilename = filename }) } -func newUserAddCommand() cmd.Command { - return envcmd.Wrap(&user.AddCommand{}) +func (s *UserAddCommandSuite) run(c *gc.C, args ...string) (*cmd.Context, error) { + addCommand := envcmd.WrapSystem(user.NewAddCommand(s.mockAPI)) + return testing.RunCommand(c, addCommand, args...) } func (s *UserAddCommandSuite) TestInit(c *gc.C) { @@ -50,26 +53,23 @@ user string displayname string outPath string - generate bool errorString string }{ { errorString: "no username supplied", }, { - args: []string{"foobar"}, - user: "foobar", + args: []string{"foobar"}, + user: "foobar", + outPath: "foobar.server", }, { args: []string{"foobar", "Foo Bar"}, user: "foobar", displayname: "Foo Bar", + outPath: "foobar.server", }, { args: []string{"foobar", "Foo Bar", "extra"}, errorString: `unrecognized args: \["extra"\]`, }, { - args: []string{"foobar", "--generate"}, - user: "foobar", - generate: true, - }, { args: []string{"foobar", "--output", "somefile"}, user: "foobar", outPath: "somefile", @@ -86,114 +86,55 @@ c.Check(addUserCmd.User, gc.Equals, test.user) c.Check(addUserCmd.DisplayName, gc.Equals, test.displayname) c.Check(addUserCmd.OutPath, gc.Equals, test.outPath) - c.Check(addUserCmd.Generate, gc.Equals, test.generate) } else { c.Check(err, gc.ErrorMatches, test.errorString) } } } -// serializedCACert adjusts the testing.CACert for the test below. -func serializedCACert() string { - parts := strings.Split(testing.CACert, "\n") - for i, part := range parts { - parts[i] = strings.TrimSpace(part) - } - return strings.Join(parts[:len(parts)-1], "\n") -} - -func assertJENVContents(c *gc.C, filename, username, password string) { - raw, err := ioutil.ReadFile(filename) +func (s *UserAddCommandSuite) TestRandomPassword(c *gc.C) { + _, err := s.run(c, "foobar") c.Assert(err, jc.ErrorIsNil) - expected := map[string]interface{}{ - "user": username, - "password": password, - "state-servers": []interface{}{"127.0.0.1:12345"}, - "ca-cert": serializedCACert(), - "environ-uuid": "env-uuid", - } - c.Assert(string(raw), jc.YAMLEquals, expected) -} - -func (s *UserAddCommandSuite) AssertJENVContents(c *gc.C, filename string) { - assertJENVContents(c, filename, s.mockAPI.username, s.mockAPI.password) -} - -func (s *UserAddCommandSuite) TestAddUserJustUsername(c *gc.C) { - context, err := testing.RunCommand(c, newUserAddCommand(), "foobar") + c.Assert(s.randomPassword, gc.HasLen, 24) +} + +func (s *UserAddCommandSuite) TestUsername(c *gc.C) { + context, err := s.run(c, "foobar") c.Assert(err, jc.ErrorIsNil) c.Assert(s.mockAPI.username, gc.Equals, "foobar") c.Assert(s.mockAPI.displayname, gc.Equals, "") - c.Assert(s.mockAPI.password, gc.Equals, "sekrit") expected := ` -password: -type password again: user "foobar" added -environment file written to .*foobar.jenv +server file written to .*foobar.server `[1:] - c.Assert(testing.Stdout(context), gc.Matches, expected) - c.Assert(testing.Stderr(context), gc.Equals, "To generate a random strong password, use the --generate flag.\n") - s.AssertJENVContents(c, context.AbsPath("foobar.jenv")) + c.Assert(testing.Stderr(context), gc.Matches, expected) + s.assertServerFileMatches(c, s.serverFilename, "foobar", s.randomPassword) } -func (s *UserAddCommandSuite) TestAddUserUsernameAndDisplayname(c *gc.C) { - context, err := testing.RunCommand(c, newUserAddCommand(), "foobar", "Foo Bar") +func (s *UserAddCommandSuite) TestUsernameAndDisplayname(c *gc.C) { + context, err := s.run(c, "foobar", "Foo Bar") c.Assert(err, jc.ErrorIsNil) c.Assert(s.mockAPI.username, gc.Equals, "foobar") c.Assert(s.mockAPI.displayname, gc.Equals, "Foo Bar") expected := `user "Foo Bar (foobar)" added` - c.Assert(testing.Stdout(context), jc.Contains, expected) - s.AssertJENVContents(c, context.AbsPath("foobar.jenv")) + c.Assert(testing.Stderr(context), jc.Contains, expected) + s.assertServerFileMatches(c, s.serverFilename, "foobar", s.randomPassword) } func (s *UserAddCommandSuite) TestBlockAddUser(c *gc.C) { // Block operation s.mockAPI.blocked = true - _, err := testing.RunCommand(c, newUserAddCommand(), "foobar", "Foo Bar") + _, err := s.run(c, "foobar", "Foo Bar") c.Assert(err, gc.ErrorMatches, cmd.ErrSilent.Error()) // msg is logged stripped := strings.Replace(c.GetTestLog(), "\n", "", -1) c.Check(stripped, gc.Matches, ".*To unblock changes.*") } -func (s *UserAddCommandSuite) TestGeneratePassword(c *gc.C) { - context, err := testing.RunCommand(c, newUserAddCommand(), "foobar", "--generate") - c.Assert(err, jc.ErrorIsNil) - c.Assert(s.mockAPI.username, gc.Equals, "foobar") - c.Assert(s.mockAPI.password, gc.Not(gc.Equals), "sekrit") - c.Assert(s.mockAPI.password, gc.HasLen, 24) - expected := ` -user "foobar" added -environment file written to .*foobar.jenv -`[1:] - c.Assert(testing.Stdout(context), gc.Matches, expected) - c.Assert(testing.Stderr(context), gc.Equals, "") - s.AssertJENVContents(c, context.AbsPath("foobar.jenv")) -} - func (s *UserAddCommandSuite) TestAddUserErrorResponse(c *gc.C) { s.mockAPI.failMessage = "failed to create user, chaos ensues" - context, err := testing.RunCommand(c, newUserAddCommand(), "foobar", "--generate") - c.Assert(err, gc.ErrorMatches, "failed to create user, chaos ensues") - c.Assert(s.mockAPI.username, gc.Equals, "foobar") - c.Assert(s.mockAPI.displayname, gc.Equals, "") - c.Assert(testing.Stdout(context), gc.Equals, "") -} - -func (s *UserAddCommandSuite) TestJenvOutput(c *gc.C) { - outputName := filepath.Join(c.MkDir(), "output") - context, err := testing.RunCommand(c, newUserAddCommand(), - "foobar", "--output", outputName) - c.Assert(err, jc.ErrorIsNil) - s.AssertJENVContents(c, context.AbsPath(outputName+".jenv")) -} - -func (s *UserAddCommandSuite) TestJenvOutputWithSuffix(c *gc.C) { - outputName := filepath.Join(c.MkDir(), "output.jenv") - context, err := testing.RunCommand(c, newUserAddCommand(), - "foobar", "--output", outputName) - c.Assert(err, jc.ErrorIsNil) - s.AssertJENVContents(c, context.AbsPath(outputName)) + _, err := s.run(c, "foobar") + c.Assert(err, gc.ErrorMatches, s.mockAPI.failMessage) } type mockAddUserAPI struct { @@ -221,14 +162,6 @@ return names.UserTag{}, errors.New(m.failMessage) } -func (m *mockAddUserAPI) ShareEnvironment(users ...names.UserTag) error { - if m.shareFailMsg != "" { - return errors.New(m.shareFailMsg) - } - m.sharedUsers = users - return nil -} - func (*mockAddUserAPI) Close() error { return nil } === modified file 'src/github.com/juju/juju/cmd/juju/user/change_password.go' --- src/github.com/juju/juju/cmd/juju/user/change_password.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/user/change_password.go 2015-10-23 18:29:32 +0000 @@ -8,12 +8,17 @@ "github.com/juju/cmd" "github.com/juju/errors" + "github.com/juju/utils" + "github.com/juju/utils/readpass" "launchpad.net/gnuflag" "github.com/juju/juju/cmd/juju/block" "github.com/juju/juju/environs/configstore" ) +// randomPasswordNotify is called when a random password is generated. +var randomPasswordNotify = func(string) {} + const userChangePasswordDoc = ` Change the password for the user you are currently logged in as, or as an admin, change the password for another user. @@ -25,15 +30,16 @@ # Change the password to a random strong password. juju user change-password --generate - # Change the password for bob - juju user change-password bob --generate + # Change the password for bob, this always uses a random password + juju user change-password bob ` // ChangePasswordCommand changes the password for a user. type ChangePasswordCommand struct { UserCommandBase - Password string + api ChangePasswordAPI + writer EnvironInfoCredsWriter Generate bool OutPath string User string @@ -63,6 +69,12 @@ if c.User == "" && c.OutPath != "" { return errors.New("output is only a valid option when changing another user's password") } + if c.User != "" { + c.Generate = true + if c.OutPath == "" { + c.OutPath = c.User + ".server" + } + } return err } @@ -77,80 +89,64 @@ // are used to change the password. type EnvironInfoCredsWriter interface { Write() error + APICredentials() configstore.APICredentials SetAPICredentials(creds configstore.APICredentials) - Location() string -} - -func (c *ChangePasswordCommand) getChangePasswordAPI() (ChangePasswordAPI, error) { - return c.NewUserManagerClient() -} - -func (c *ChangePasswordCommand) getEnvironInfoWriter() (EnvironInfoCredsWriter, error) { - return c.ConnectionWriter() -} - -func (c *ChangePasswordCommand) getConnectionCredentials() (configstore.APICredentials, error) { - return c.ConnectionCredentials() -} - -var ( - getChangePasswordAPI = (*ChangePasswordCommand).getChangePasswordAPI - getEnvironInfoWriter = (*ChangePasswordCommand).getEnvironInfoWriter - getConnectionCredentials = (*ChangePasswordCommand).getConnectionCredentials -) +} // Run implements Command.Run. func (c *ChangePasswordCommand) Run(ctx *cmd.Context) error { - var err error + if c.api == nil { + api, err := c.NewUserManagerAPIClient() + if err != nil { + return errors.Trace(err) + } + c.api = api + defer c.api.Close() + } - c.Password, err = c.generateOrReadPassword(ctx, c.Generate) + password, err := c.generateOrReadPassword(ctx, c.Generate) if err != nil { return errors.Trace(err) } - var credsWriter EnvironInfoCredsWriter + var writer EnvironInfoCredsWriter + var creds configstore.APICredentials if c.User == "" { // We get the creds writer before changing the password just to // minimise the things that could go wrong after changing the password // in the server. - credsWriter, err = getEnvironInfoWriter(c) - if err != nil { - return errors.Trace(err) + if c.writer == nil { + writer, err = c.ConnectionInfo() + if err != nil { + return errors.Trace(err) + } + } else { + writer = c.writer } - creds, err = getConnectionCredentials(c) - if err != nil { - return errors.Trace(err) - } + creds = writer.APICredentials() } else { creds.User = c.User } - client, err := getChangePasswordAPI(c) - if err != nil { - return err - } - defer client.Close() - oldPassword := creds.Password - creds.Password = c.Password - err = client.SetPassword(creds.User, c.Password) - if err != nil { + creds.Password = password + if err = c.api.SetPassword(creds.User, password); err != nil { return block.ProcessBlockedError(err, block.BlockChange) } if c.User != "" { - return c.writeEnvironmentFile(ctx) + return writeServerFile(c, ctx, c.User, password, c.OutPath) } - credsWriter.SetAPICredentials(creds) - if err := credsWriter.Write(); err != nil { - logger.Errorf("updating the environments file failed, reverting to original password") - setErr := client.SetPassword(creds.User, oldPassword) + writer.SetAPICredentials(creds) + if err := writer.Write(); err != nil { + logger.Errorf("updating the cached credentials failed, reverting to original password") + setErr := c.api.SetPassword(creds.User, oldPassword) if setErr != nil { - logger.Errorf("failed to set password back, you will need to edit your environments file by hand to specify the password: %q", c.Password) + logger.Errorf("failed to set password back, you will need to edit your environments file by hand to specify the password: %q", password) return errors.Annotate(setErr, "failed to set password back") } return errors.Annotate(err, "failed to write new password to environments file") @@ -159,15 +155,35 @@ return nil } -func (c *ChangePasswordCommand) writeEnvironmentFile(ctx *cmd.Context) error { - outPath := c.OutPath - if outPath == "" { - outPath = c.User + ".jenv" - } - outPath = normaliseJenvPath(ctx, outPath) - if err := generateUserJenv(c.ConnectionName(), c.User, c.Password, outPath); err != nil { - return err - } - fmt.Fprintf(ctx.Stdout, "environment file written to %s\n", outPath) - return nil +var readPassword = readpass.ReadPassword + +func (*ChangePasswordCommand) generateOrReadPassword(ctx *cmd.Context, generate bool) (string, error) { + if generate { + password, err := utils.RandomPassword() + if err != nil { + return "", errors.Annotate(err, "failed to generate random password") + } + randomPasswordNotify(password) + return password, nil + } + + // Don't add the carriage returns before readPassword, but add + // them directly after the readPassword so any errors are output + // on their own lines. + fmt.Fprint(ctx.Stdout, "password: ") + password, err := readPassword() + fmt.Fprint(ctx.Stdout, "\n") + if err != nil { + return "", errors.Trace(err) + } + fmt.Fprint(ctx.Stdout, "type password again: ") + verify, err := readPassword() + fmt.Fprint(ctx.Stdout, "\n") + if err != nil { + return "", errors.Trace(err) + } + if password != verify { + return "", errors.New("Passwords do not match") + } + return password, nil } === modified file 'src/github.com/juju/juju/cmd/juju/user/change_password_test.go' --- src/github.com/juju/juju/cmd/juju/user/change_password_test.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/user/change_password_test.go 2015-10-23 18:29:32 +0000 @@ -21,6 +21,8 @@ BaseSuite mockAPI *mockChangePasswordAPI mockEnvironInfo *mockEnvironInfo + randomPassword string + serverFilename string } var _ = gc.Suite(&ChangePasswordCommandSuite{}) @@ -31,19 +33,19 @@ s.mockEnvironInfo = &mockEnvironInfo{ creds: configstore.APICredentials{"user-name", "password"}, } - s.PatchValue(user.GetChangePasswordAPI, func(c *user.ChangePasswordCommand) (user.ChangePasswordAPI, error) { - return s.mockAPI, nil - }) - s.PatchValue(user.GetEnvironInfoWriter, func(c *user.ChangePasswordCommand) (user.EnvironInfoCredsWriter, error) { - return s.mockEnvironInfo, nil - }) - s.PatchValue(user.GetConnectionCredentials, func(c *user.ChangePasswordCommand) (configstore.APICredentials, error) { - return s.mockEnvironInfo.creds, nil + s.randomPassword = "" + s.serverFilename = "" + s.PatchValue(user.RandomPasswordNotify, func(pwd string) { + s.randomPassword = pwd + }) + s.PatchValue(user.ServerFileNotify, func(filename string) { + s.serverFilename = filename }) } -func newUserChangePassword() cmd.Command { - return envcmd.Wrap(&user.ChangePasswordCommand{}) +func (s *ChangePasswordCommandSuite) run(c *gc.C, args ...string) (*cmd.Context, error) { + changePasswordCommand := envcmd.WrapSystem(user.NewChangePasswordCommand(s.mockAPI, s.mockEnvironInfo)) + return testing.RunCommand(c, changePasswordCommand, args...) } func (s *ChangePasswordCommandSuite) TestInit(c *gc.C) { @@ -60,32 +62,29 @@ args: []string{"--generate"}, generate: true, }, { + args: []string{"foobar"}, + user: "foobar", + generate: true, + outPath: "foobar.server", + }, { + args: []string{"foobar", "--generate"}, + user: "foobar", + generate: true, + outPath: "foobar.server", + }, { + args: []string{"foobar", "--output", "somefile"}, + user: "foobar", + generate: true, + outPath: "somefile", + }, { args: []string{"--foobar"}, errorString: "flag provided but not defined: --foobar", }, { - args: []string{"foobar"}, - user: "foobar", - }, { args: []string{"foobar", "extra"}, errorString: `unrecognized args: \["extra"\]`, }, { args: []string{"--output", "somefile"}, errorString: "output is only a valid option when changing another user's password", - }, { - args: []string{"-o", "somefile"}, - errorString: "output is only a valid option when changing another user's password", - }, { - args: []string{"foobar", "--generate"}, - user: "foobar", - generate: true, - }, { - args: []string{"foobar", "--output", "somefile"}, - user: "foobar", - outPath: "somefile", - }, { - args: []string{"foobar", "-o", "somefile"}, - user: "foobar", - outPath: "somefile", }, } { c.Logf("test %d", i) @@ -101,40 +100,40 @@ } } -func (s *ChangePasswordCommandSuite) TestFailedToReadInfo(c *gc.C) { - s.PatchValue(user.GetEnvironInfoWriter, func(c *user.ChangePasswordCommand) (user.EnvironInfoCredsWriter, error) { - return s.mockEnvironInfo, errors.New("something failed") - }) - _, err := testing.RunCommand(c, newUserChangePassword(), "--generate") - c.Assert(err, gc.ErrorMatches, "something failed") +func (s *ChangePasswordCommandSuite) assertRandomPassword(c *gc.C) { + c.Assert(s.mockAPI.password, gc.Equals, s.randomPassword) + c.Assert(s.mockAPI.password, gc.HasLen, 24) +} + +func (s *ChangePasswordCommandSuite) assertPasswordFromReadPass(c *gc.C) { + c.Assert(s.mockAPI.password, gc.Equals, "sekrit") } func (s *ChangePasswordCommandSuite) TestChangePassword(c *gc.C) { - context, err := testing.RunCommand(c, newUserChangePassword()) + context, err := s.run(c) c.Assert(err, jc.ErrorIsNil) c.Assert(s.mockAPI.username, gc.Equals, "user-name") - c.Assert(s.mockAPI.password, gc.Equals, "sekrit") + s.assertPasswordFromReadPass(c) expected := ` -password: -type password again: +password: +type password again: `[1:] c.Assert(testing.Stdout(context), gc.Equals, expected) c.Assert(testing.Stderr(context), gc.Equals, "Your password has been updated.\n") } func (s *ChangePasswordCommandSuite) TestChangePasswordGenerate(c *gc.C) { - context, err := testing.RunCommand(c, newUserChangePassword(), "--generate") + context, err := s.run(c, "--generate") c.Assert(err, jc.ErrorIsNil) c.Assert(s.mockAPI.username, gc.Equals, "user-name") - c.Assert(s.mockAPI.password, gc.Not(gc.Equals), "sekrit") - c.Assert(s.mockAPI.password, gc.HasLen, 24) + s.assertRandomPassword(c) c.Assert(testing.Stderr(context), gc.Equals, "Your password has been updated.\n") } func (s *ChangePasswordCommandSuite) TestChangePasswordFail(c *gc.C) { s.mockAPI.failMessage = "failed to do something" s.mockAPI.failOps = []bool{true, false} - _, err := testing.RunCommand(c, newUserChangePassword(), "--generate") + _, err := s.run(c, "--generate") c.Assert(err, gc.ErrorMatches, "failed to do something") c.Assert(s.mockAPI.username, gc.Equals, "") } @@ -143,7 +142,7 @@ func (s *ChangePasswordCommandSuite) TestRevertPasswordAfterFailedWrite(c *gc.C) { // Fail to Write the new jenv file s.mockEnvironInfo.failMessage = "failed to write" - _, err := testing.RunCommand(c, newUserChangePassword(), "--generate") + _, err := s.run(c, "--generate") c.Assert(err, gc.ErrorMatches, "failed to write new password to environments file: failed to write") // Last api call was to set the password back to the original. c.Assert(s.mockAPI.password, gc.Equals, "password") @@ -154,43 +153,33 @@ s.mockAPI.failMessage = "failed to do something" s.mockEnvironInfo.failMessage = "failed to write" s.mockAPI.failOps = []bool{false, true} - _, err := testing.RunCommand(c, newUserChangePassword(), "--generate") + _, err := s.run(c, "--generate") c.Assert(err, gc.ErrorMatches, "failed to set password back: failed to do something") } func (s *ChangePasswordCommandSuite) TestChangeOthersPassword(c *gc.C) { // The checks for user existence and admin rights are tested // at the apiserver level. - context, err := testing.RunCommand(c, newUserChangePassword(), "other") + context, err := s.run(c, "other") c.Assert(err, jc.ErrorIsNil) c.Assert(s.mockAPI.username, gc.Equals, "other") - c.Assert(s.mockAPI.password, gc.Equals, "sekrit") - filename := context.AbsPath("other.jenv") + s.assertRandomPassword(c) + s.assertServerFileMatches(c, s.serverFilename, "other", s.randomPassword) expected := ` -password: -type password again: -environment file written to `[1:] + filename + "\n" - c.Assert(testing.Stdout(context), gc.Equals, expected) - c.Assert(testing.Stderr(context), gc.Equals, "") - assertJENVContents(c, context.AbsPath("other.jenv"), "other", "sekrit") - +server file written to .*other.server +`[1:] + c.Assert(testing.Stderr(context), gc.Matches, expected) } func (s *ChangePasswordCommandSuite) TestChangeOthersPasswordWithFile(c *gc.C) { // The checks for user existence and admin rights are tested // at the apiserver level. - filename := filepath.Join(c.MkDir(), "test.jenv") - context, err := testing.RunCommand(c, newUserChangePassword(), "other", "-o", filename) + filename := filepath.Join(c.MkDir(), "test.result") + _, err := s.run(c, "other", "-o", filename) c.Assert(err, jc.ErrorIsNil) - c.Assert(s.mockAPI.username, gc.Equals, "other") - c.Assert(s.mockAPI.password, gc.Equals, "sekrit") - expected := ` -password: -type password again: -environment file written to `[1:] + filename + "\n" - c.Assert(testing.Stdout(context), gc.Equals, expected) - c.Assert(testing.Stderr(context), gc.Equals, "") - assertJENVContents(c, filename, "other", "sekrit") + s.assertRandomPassword(c) + c.Assert(filepath.Base(s.serverFilename), gc.Equals, "test.result") + s.assertServerFileMatches(c, s.serverFilename, "other", s.randomPassword) } type mockEnvironInfo struct { @@ -213,10 +202,6 @@ return m.creds } -func (m *mockEnvironInfo) Location() string { - return "location" -} - type mockChangePasswordAPI struct { failMessage string currentOp int === added file 'src/github.com/juju/juju/cmd/juju/user/common.go' --- src/github.com/juju/juju/cmd/juju/user/common.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/user/common.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,52 @@ +// Copyright 2012-2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package user + +import ( + "io/ioutil" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/names" + + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/environs/configstore" +) + +// serverFileNotify is called with the absolute path of the written server +// file. +var serverFileNotify = func(string) {} + +// EndpointProvider defines the method used by the writeServerFile +// in order to get the addresses and ca-cert of the juju server. +type EndpointProvider interface { + ConnectionEndpoint() (configstore.APIEndpoint, error) +} + +func writeServerFile(endpointProvider EndpointProvider, ctx *cmd.Context, username, password, outPath string) error { + outPath = ctx.AbsPath(outPath) + endpoint, err := endpointProvider.ConnectionEndpoint() + if err != nil { + return errors.Trace(err) + } + if !names.IsValidUser(username) { + return errors.Errorf("%q is not a valid username", username) + } + outputInfo := envcmd.ServerFile{ + Username: username, + Password: password, + Addresses: endpoint.Addresses, + CACert: endpoint.CACert, + } + yaml, err := cmd.FormatYaml(outputInfo) + if err != nil { + return errors.Trace(err) + } + if err := ioutil.WriteFile(outPath, yaml, 0644); err != nil { + return errors.Trace(err) + } + serverFileNotify(outPath) + ctx.Infof("server file written to %s", outPath) + return nil +} === added file 'src/github.com/juju/juju/cmd/juju/user/common_test.go' --- src/github.com/juju/juju/cmd/juju/user/common_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/user/common_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,60 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package user_test + +import ( + "path/filepath" + + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/juju/user" + "github.com/juju/juju/environs/configstore" + "github.com/juju/juju/testing" +) + +type CommonSuite struct { + BaseSuite + serverFilename string +} + +var _ = gc.Suite(&CommonSuite{}) + +func (s *CommonSuite) SetUpTest(c *gc.C) { + s.BaseSuite.SetUpTest(c) + s.serverFilename = "" + s.PatchValue(user.ServerFileNotify, func(filename string) { + s.serverFilename = filename + }) +} + +// ConnectionEndpoint so this suite implements the EndpointProvider interface. +func (s *CommonSuite) ConnectionEndpoint() (configstore.APIEndpoint, error) { + return configstore.APIEndpoint{ + // NOTE: the content here is the same as t + Addresses: []string{"127.0.0.1:12345"}, + CACert: testing.CACert, + }, nil +} + +func (s *CommonSuite) TestAbsolutePath(c *gc.C) { + ctx := testing.Context(c) + err := user.WriteServerFile(s, ctx, "username", "password", "outfile.blah") + c.Assert(err, jc.ErrorIsNil) + c.Assert(filepath.IsAbs(s.serverFilename), jc.IsTrue) + c.Assert(s.serverFilename, gc.Equals, filepath.Join(ctx.Dir, "outfile.blah")) +} + +func (s *CommonSuite) TestFileContent(c *gc.C) { + ctx := testing.Context(c) + err := user.WriteServerFile(s, ctx, "username", "password", "outfile.blah") + c.Assert(err, jc.ErrorIsNil) + s.assertServerFileMatches(c, s.serverFilename, "username", "password") +} + +func (s *CommonSuite) TestWriteServerFileBadUser(c *gc.C) { + ctx := testing.Context(c) + err := user.WriteServerFile(s, ctx, "bad user", "password", "outfile.blah") + c.Assert(err, gc.ErrorMatches, `"bad user" is not a valid username`) +} === added file 'src/github.com/juju/juju/cmd/juju/user/credentials.go' --- src/github.com/juju/juju/cmd/juju/user/credentials.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/user/credentials.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,72 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package user + +import ( + "fmt" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/names" + "launchpad.net/gnuflag" +) + +const userCredentialsDoc = ` +Writes out the current user and credentials to a file that can be used +with 'juju system login' to allow the user to access the same environments +as the same user from another machine. + +Examples: + + $ juju user credentials --output staging.creds + + # copy the staging.creds file to another machine + + $ juju system login staging --server staging.creds --keep-password + + +See Also: + juju system login +` + +// CredentialsCommand changes the password for a user. +type CredentialsCommand struct { + UserCommandBase + OutPath string +} + +// Info implements Command.Info. +func (c *CredentialsCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "credentials", + Purpose: "save the credentials and server details to a file", + Doc: userCredentialsDoc, + } +} + +// SetFlags implements Command.SetFlags. +func (c *CredentialsCommand) SetFlags(f *gnuflag.FlagSet) { + f.StringVar(&c.OutPath, "o", "", "specifies the path of the generated file") + f.StringVar(&c.OutPath, "output", "", "") +} + +// Run implements Command.Run. +func (c *CredentialsCommand) Run(ctx *cmd.Context) error { + creds, err := c.ConnectionCredentials() + if err != nil { + return errors.Trace(err) + } + + filename := c.OutPath + if filename == "" { + // The reason for the dance though the newUserTag + // is to strip off the optional provider. + // user -> user + // user@local -> user + // user@remote -> user + name := names.NewUserTag(creds.User).Name() + filename = fmt.Sprintf("%s.server", name) + } + return writeServerFile(c, ctx, creds.User, creds.Password, filename) +} === added file 'src/github.com/juju/juju/cmd/juju/user/credentials_test.go' --- src/github.com/juju/juju/cmd/juju/user/credentials_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/juju/user/credentials_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,86 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package user_test + +import ( + "github.com/juju/cmd" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/user" + "github.com/juju/juju/testing" +) + +type CredentialsCommandSuite struct { + BaseSuite + serverFilename string +} + +var _ = gc.Suite(&CredentialsCommandSuite{}) + +func (s *CredentialsCommandSuite) SetUpTest(c *gc.C) { + s.BaseSuite.SetUpTest(c) + s.serverFilename = "" + s.PatchValue(user.ServerFileNotify, func(filename string) { + s.serverFilename = filename + }) +} + +func (s *CredentialsCommandSuite) run(c *gc.C, args ...string) (*cmd.Context, error) { + command := envcmd.WrapSystem(&user.CredentialsCommand{}) + return testing.RunCommand(c, command, args...) +} + +func (s *CredentialsCommandSuite) TestInit(c *gc.C) { + for i, test := range []struct { + args []string + outPath string + errorString string + }{ + { + // no args is fine + }, { + args: []string{"--output=foo.bar"}, + outPath: "foo.bar", + }, { + args: []string{"-o", "foo.bar"}, + outPath: "foo.bar", + }, { + args: []string{"foobar"}, + errorString: `unrecognized args: \["foobar"\]`, + }, + } { + c.Logf("test %d", i) + command := &user.CredentialsCommand{} + err := testing.InitCommand(command, test.args) + if test.errorString == "" { + c.Check(command.OutPath, gc.Equals, test.outPath) + } else { + c.Check(err, gc.ErrorMatches, test.errorString) + } + } +} + +func (s *CredentialsCommandSuite) TestNoArgs(c *gc.C) { + context, err := s.run(c) + c.Assert(err, jc.ErrorIsNil) + // User and password are set in BaseSuite.SetUpTest. + s.assertServerFileMatches(c, s.serverFilename, "user-test", "password") + expected := ` +server file written to .*user-test.server +`[1:] + c.Assert(testing.Stderr(context), gc.Matches, expected) +} + +func (s *CredentialsCommandSuite) TestFilename(c *gc.C) { + context, err := s.run(c, "--output=testing.creds") + c.Assert(err, jc.ErrorIsNil) + // User and password are set in BaseSuite.SetUpTest. + s.assertServerFileMatches(c, s.serverFilename, "user-test", "password") + expected := ` +server file written to .*testing.creds +`[1:] + c.Assert(testing.Stderr(context), gc.Matches, expected) +} === modified file 'src/github.com/juju/juju/cmd/juju/user/disenable.go' --- src/github.com/juju/juju/cmd/juju/user/disenable.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/user/disenable.go 2015-10-23 18:29:32 +0000 @@ -19,7 +19,7 @@ juju user disable foobar See Also: - juju enable + juju help user enable ` const enableUserDoc = ` @@ -31,13 +31,14 @@ juju user enable foobar See Also: - juju disable + juju help user disable ` // DisenableUserBase common code for enable/disable user commands type DisenableUserBase struct { UserCommandBase - user string + api DisenableUserAPI + User string } // DisableCommand disables users. @@ -75,10 +76,19 @@ if len(args) == 0 { return errors.New("no username supplied") } - c.user = args[0] + // TODO(thumper): support multiple users in one command, + // and also verify that the values are valid user names. + c.User = args[0] return cmd.CheckEmpty(args[1:]) } +// Username is here entirely for testing purposes to allow both the +// DisableCommand and EnableCommand to support a common interface that is able +// to ask for the command line supplied username. +func (c *DisenableUserBase) Username() string { + return c.User +} + // DisenableUserAPI defines the API methods that the disable and enable // commands use. type DisenableUserAPI interface { @@ -88,37 +98,43 @@ } func (c *DisenableUserBase) getDisableUserAPI() (DisenableUserAPI, error) { - return c.NewUserManagerClient() + return c.NewUserManagerAPIClient() } var getDisableUserAPI = (*DisenableUserBase).getDisableUserAPI -// Info implements Command.Run. +// Run implements Command.Run. func (c *DisableCommand) Run(ctx *cmd.Context) error { - client, err := getDisableUserAPI(&c.DisenableUserBase) - if err != nil { - return err + if c.api == nil { + api, err := c.NewUserManagerAPIClient() + if err != nil { + return errors.Trace(err) + } + c.api = api + defer c.api.Close() } - defer client.Close() - err = client.DisableUser(c.user) - if err != nil { + + if err := c.api.DisableUser(c.User); err != nil { return block.ProcessBlockedError(err, block.BlockChange) } - ctx.Infof("User %q disabled", c.user) + ctx.Infof("User %q disabled", c.User) return nil } -// Info implements Command.Run. +// Run implements Command.Run. func (c *EnableCommand) Run(ctx *cmd.Context) error { - client, err := getDisableUserAPI(&c.DisenableUserBase) - if err != nil { - return err + if c.api == nil { + api, err := c.NewUserManagerAPIClient() + if err != nil { + return errors.Trace(err) + } + c.api = api + defer c.api.Close() } - defer client.Close() - err = client.EnableUser(c.user) - if err != nil { + + if err := c.api.EnableUser(c.User); err != nil { return block.ProcessBlockedError(err, block.BlockChange) } - ctx.Infof("User %q enabled", c.user) + ctx.Infof("User %q enabled", c.User) return nil } === modified file 'src/github.com/juju/juju/cmd/juju/user/disenable_test.go' --- src/github.com/juju/juju/cmd/juju/user/disenable_test.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/user/disenable_test.go 2015-10-23 18:29:32 +0000 @@ -15,32 +15,22 @@ type DisableUserSuite struct { BaseSuite - mock mockDisableUserAPI + mock *mockDisenableUserAPI } var _ = gc.Suite(&DisableUserSuite{}) +func (s *DisableUserSuite) SetUpTest(c *gc.C) { + s.BaseSuite.SetUpTest(c) + s.mock = &mockDisenableUserAPI{} +} + type disenableCommand interface { cmd.Command - username() string -} - -func (s *DisableUserSuite) disableUserCommand() cmd.Command { - return envcmd.Wrap(&user.DisableCommand{}) -} - -func (s *DisableUserSuite) enableUserCommand() cmd.Command { - return envcmd.Wrap(&user.EnableCommand{}) -} - -func (s *DisableUserSuite) SetUpTest(c *gc.C) { - s.BaseSuite.SetUpTest(c) - s.PatchValue(user.GetDisableUserAPI, func(*user.DisenableUserBase) (user.DisenableUserAPI, error) { - return &s.mock, nil - }) -} - -func (s *DisableUserSuite) testInit(c *gc.C, command user.DisenableCommand) { + Username() string +} + +func (s *DisableUserSuite) testInit(c *gc.C, command disenableCommand) { for i, test := range []struct { args []string errMatch string @@ -75,35 +65,37 @@ func (s *DisableUserSuite) TestDisable(c *gc.C) { username := "testing" - _, err := testing.RunCommand(c, s.disableUserCommand(), username) + disableCommand := envcmd.WrapSystem(user.NewDisableCommand(s.mock)) + _, err := testing.RunCommand(c, disableCommand, username) c.Assert(err, jc.ErrorIsNil) c.Assert(s.mock.disable, gc.Equals, username) } func (s *DisableUserSuite) TestEnable(c *gc.C) { username := "testing" - _, err := testing.RunCommand(c, s.enableUserCommand(), username) + enableCommand := envcmd.WrapSystem(user.NewEnableCommand(s.mock)) + _, err := testing.RunCommand(c, enableCommand, username) c.Assert(err, jc.ErrorIsNil) c.Assert(s.mock.enable, gc.Equals, username) } -type mockDisableUserAPI struct { +type mockDisenableUserAPI struct { enable string disable string } -var _ user.DisenableUserAPI = (*mockDisableUserAPI)(nil) +var _ user.DisenableUserAPI = (*mockDisenableUserAPI)(nil) -func (m *mockDisableUserAPI) Close() error { +func (m *mockDisenableUserAPI) Close() error { return nil } -func (m *mockDisableUserAPI) EnableUser(username string) error { +func (m *mockDisenableUserAPI) EnableUser(username string) error { m.enable = username return nil } -func (m *mockDisableUserAPI) DisableUser(username string) error { +func (m *mockDisenableUserAPI) DisableUser(username string) error { m.disable = username return nil } === modified file 'src/github.com/juju/juju/cmd/juju/user/export_test.go' --- src/github.com/juju/juju/cmd/juju/user/export_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/user/export_test.go 2015-10-23 18:29:32 +0000 @@ -3,41 +3,48 @@ package user -import ( - "github.com/juju/cmd" -) - -var ( - ReadPassword = &readPassword - // add - GetAddUserAPI = &getAddUserAPI - GetShareEnvAPI = &getShareEnvAPI - // change password - GetChangePasswordAPI = &getChangePasswordAPI - GetEnvironInfoWriter = &getEnvironInfoWriter - GetConnectionCredentials = &getConnectionCredentials - // disable and enable - GetDisableUserAPI = &getDisableUserAPI -) - -// DisenableCommand is used for testing both Disable and Enable user commands. -type DisenableCommand interface { - cmd.Command - Username() string -} - -func (c *DisableCommand) Username() string { - return c.user -} - -func (c *EnableCommand) Username() string { - return c.user -} - -var ( - _ DisenableCommand = (*DisableCommand)(nil) - _ DisenableCommand = (*EnableCommand)(nil) -) +var ( + RandomPasswordNotify = &randomPasswordNotify + ReadPassword = &readPassword + ServerFileNotify = &serverFileNotify + WriteServerFile = writeServerFile +) + +// NewAddCommand returns an AddCommand with the api provided as specified. +func NewAddCommand(api AddUserAPI) *AddCommand { + return &AddCommand{ + api: api, + } +} + +// NewChangePasswordCommand returns a ChangePasswordCommand with the api +// and writer provided as specified. +func NewChangePasswordCommand(api ChangePasswordAPI, writer EnvironInfoCredsWriter) *ChangePasswordCommand { + return &ChangePasswordCommand{ + api: api, + writer: writer, + } +} + +// NewDisableCommand returns a DisableCommand with the api provided as +// specified. +func NewDisableCommand(api DisenableUserAPI) *DisableCommand { + return &DisableCommand{ + DisenableUserBase{ + api: api, + }, + } +} + +// NewEnableCommand returns a EnableCommand with the api provided as +// specified. +func NewEnableCommand(api DisenableUserAPI) *EnableCommand { + return &EnableCommand{ + DisenableUserBase{ + api: api, + }, + } +} // NewInfoCommand returns an InfoCommand with the api provided as specified. func NewInfoCommand(api UserInfoAPI) *InfoCommand { === modified file 'src/github.com/juju/juju/cmd/juju/user/info.go' --- src/github.com/juju/juju/cmd/juju/user/info.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/user/info.go 2015-10-23 18:29:32 +0000 @@ -107,7 +107,7 @@ if c.api != nil { return c.api, nil } - return c.NewUserManagerClient() + return c.NewUserManagerAPIClient() } // Run implements Command.Run. @@ -144,24 +144,16 @@ var now = time.Now() for _, info := range users { outInfo := UserInfo{ - Username: info.Username, - DisplayName: info.DisplayName, - Disabled: info.Disabled, + Username: info.Username, + DisplayName: info.DisplayName, + Disabled: info.Disabled, + LastConnection: LastConnection(info.LastConnection, now, c.exactTime), } if c.exactTime { outInfo.DateCreated = info.DateCreated.String() } else { outInfo.DateCreated = UserFriendlyDuration(info.DateCreated, now) } - if info.LastConnection != nil { - if c.exactTime { - outInfo.LastConnection = info.LastConnection.String() - } else { - outInfo.LastConnection = UserFriendlyDuration(*info.LastConnection, now) - } - } else { - outInfo.LastConnection = "never connected" - } output = append(output, outInfo) } @@ -169,6 +161,19 @@ return output } +// LastConnection turns the *time.Time returned from the API server +// into a user facing string with either exact time or a user friendly +// string based on the args. +func LastConnection(connectionTime *time.Time, now time.Time, exact bool) string { + if connectionTime == nil { + return "never connected" + } + if exact { + return connectionTime.String() + } + return UserFriendlyDuration(*connectionTime, now) +} + // UserFriendlyDuration translates a time in the past into a user // friendly string representation relative to the "now" time argument. func UserFriendlyDuration(when, now time.Time) string { === modified file 'src/github.com/juju/juju/cmd/juju/user/info_test.go' --- src/github.com/juju/juju/cmd/juju/user/info_test.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/user/info_test.go 2015-10-23 18:29:32 +0000 @@ -36,7 +36,7 @@ ) func newUserInfoCommand() cmd.Command { - return envcmd.Wrap(user.NewInfoCommand(&fakeUserInfoAPI{})) + return envcmd.WrapSystem(user.NewInfoCommand(&fakeUserInfoAPI{})) } type fakeUserInfoAPI struct{} === modified file 'src/github.com/juju/juju/cmd/juju/user/list.go' --- src/github.com/juju/juju/cmd/juju/user/list.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/user/list.go 2015-10-23 18:29:32 +0000 @@ -19,7 +19,7 @@ List all the current users in the Juju server. See Also: - juju user info + juju help user info ` // ListCommand shows all the users in the Juju server. === modified file 'src/github.com/juju/juju/cmd/juju/user/list_test.go' --- src/github.com/juju/juju/cmd/juju/user/list_test.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/user/list_test.go 2015-10-23 18:29:32 +0000 @@ -27,7 +27,7 @@ var _ = gc.Suite(&UserListCommandSuite{}) func newUserListCommand() cmd.Command { - return envcmd.Wrap(user.NewListCommand(&fakeUserListAPI{})) + return envcmd.WrapSystem(user.NewListCommand(&fakeUserListAPI{})) } type fakeUserListAPI struct{} === modified file 'src/github.com/juju/juju/cmd/juju/user/user.go' --- src/github.com/juju/juju/cmd/juju/user/user.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/user/user.go 2015-10-23 18:29:32 +0000 @@ -4,15 +4,9 @@ package user import ( - "fmt" - "github.com/juju/cmd" - "github.com/juju/errors" "github.com/juju/loggo" - "github.com/juju/utils" - "github.com/juju/utils/readpass" - "github.com/juju/juju/api/usermanager" "github.com/juju/juju/cmd/envcmd" ) @@ -21,6 +15,9 @@ const userCommandDoc = ` "juju user" is used to manage the user accounts and access control in the Juju environment. + +See Also: + juju help users ` const userCommandPurpose = "manage user accounts and access control" @@ -34,54 +31,18 @@ UsagePrefix: "juju", Purpose: userCommandPurpose, }) - usercmd.Register(envcmd.Wrap(&AddCommand{})) - usercmd.Register(envcmd.Wrap(&ChangePasswordCommand{})) - usercmd.Register(envcmd.Wrap(&InfoCommand{})) - usercmd.Register(envcmd.Wrap(&DisableCommand{})) - usercmd.Register(envcmd.Wrap(&EnableCommand{})) - usercmd.Register(envcmd.Wrap(&ListCommand{})) + usercmd.Register(envcmd.WrapSystem(&AddCommand{})) + usercmd.Register(envcmd.WrapSystem(&ChangePasswordCommand{})) + usercmd.Register(envcmd.WrapSystem(&CredentialsCommand{})) + usercmd.Register(envcmd.WrapSystem(&InfoCommand{})) + usercmd.Register(envcmd.WrapSystem(&DisableCommand{})) + usercmd.Register(envcmd.WrapSystem(&EnableCommand{})) + usercmd.Register(envcmd.WrapSystem(&ListCommand{})) return usercmd } // UserCommandBase is a helper base structure that has a method to get the // user manager client. type UserCommandBase struct { - envcmd.EnvCommandBase -} - -// NewUserManagerClient returns a usermanager client for the root api endpoint -// that the environment command returns. -func (c *UserCommandBase) NewUserManagerClient() (*usermanager.Client, error) { - root, err := c.NewAPIRoot() - if err != nil { - return nil, err - } - return usermanager.NewClient(root), nil -} - -var readPassword = readpass.ReadPassword - -func (*UserCommandBase) generateOrReadPassword(ctx *cmd.Context, generate bool) (string, error) { - if generate { - password, err := utils.RandomPassword() - if err != nil { - return "", errors.Annotate(err, "failed to generate random password") - } - return password, nil - } - - fmt.Fprintln(ctx.Stdout, "password:") - password, err := readPassword() - if err != nil { - return "", errors.Trace(err) - } - fmt.Fprintln(ctx.Stdout, "type password again:") - verify, err := readPassword() - if err != nil { - return "", errors.Trace(err) - } - if password != verify { - return "", errors.New("Passwords do not match") - } - return password, nil + envcmd.SysCommandBase } === modified file 'src/github.com/juju/juju/cmd/juju/user/user_test.go' --- src/github.com/juju/juju/cmd/juju/user/user_test.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/juju/user/user_test.go 2015-10-23 18:29:32 +0000 @@ -4,11 +4,14 @@ package user_test import ( + "io/ioutil" "os" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" + goyaml "gopkg.in/yaml.v1" + "github.com/juju/juju/cmd/envcmd" "github.com/juju/juju/cmd/juju/user" "github.com/juju/juju/environs/configstore" "github.com/juju/juju/juju/osenv" @@ -24,6 +27,7 @@ var expectedUserCommmandNames = []string{ "add", "change-password", + "credentials", "disable", "enable", "help", @@ -40,11 +44,11 @@ } type BaseSuite struct { - testing.BaseSuite + testing.FakeJujuHomeSuite } func (s *BaseSuite) SetUpTest(c *gc.C) { - s.BaseSuite.SetUpTest(c) + s.FakeJujuHomeSuite.SetUpTest(c) memstore := configstore.NewMem() s.PatchValue(&configstore.Default, func() (configstore.Storage, error) { return memstore, nil @@ -67,4 +71,19 @@ s.PatchValue(user.ReadPassword, func() (string, error) { return "sekrit", nil }) + err = envcmd.WriteCurrentSystem("testing") + c.Assert(err, jc.ErrorIsNil) +} + +func (s *BaseSuite) assertServerFileMatches(c *gc.C, serverfile, username, password string) { + yaml, err := ioutil.ReadFile(serverfile) + c.Assert(err, jc.ErrorIsNil) + var content envcmd.ServerFile + err = goyaml.Unmarshal(yaml, &content) + c.Assert(err, jc.ErrorIsNil) + + c.Assert(content.Username, gc.Equals, username) + c.Assert(content.Password, gc.Equals, password) + c.Assert(content.CACert, gc.Equals, testing.CACert) + c.Assert(content.Addresses, jc.DeepEquals, []string{"127.0.0.1:12345"}) } === removed file 'src/github.com/juju/juju/cmd/juju/user_test.go' --- src/github.com/juju/juju/cmd/juju/user_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/juju/user_test.go 1970-01-01 00:00:00 +0000 @@ -1,97 +0,0 @@ -// Copyright 2014 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - - "github.com/juju/cmd" - "github.com/juju/names" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - jujutesting "github.com/juju/juju/juju/testing" - "github.com/juju/juju/testing" - "github.com/juju/juju/testing/factory" -) - -// UserSuite tests the connectivity of all the user subcommands. These tests -// go from the command line, api client, api server, db. The db changes are -// then checked. Only one test for each command is done here to check -// connectivity. Exhaustive tests are at each layer. -type UserSuite struct { - jujutesting.JujuConnSuite -} - -var _ = gc.Suite(&UserSuite{}) - -func (s *UserSuite) RunUserCommand(c *gc.C, commands ...string) (*cmd.Context, error) { - args := []string{"user"} - args = append(args, commands...) - context := testing.Context(c) - juju := NewJujuCommand(context) - if err := testing.InitCommand(juju, args); err != nil { - return context, err - } - return context, juju.Run(context) -} - -func (s *UserSuite) TestUserAdd(c *gc.C) { - ctx, err := s.RunUserCommand(c, "add", "test", "--generate") - c.Assert(err, jc.ErrorIsNil) - c.Assert(testing.Stdout(ctx), jc.HasPrefix, `user "test" added`) - user, err := s.State.User(names.NewLocalUserTag("test")) - c.Assert(err, jc.ErrorIsNil) - c.Assert(user.IsDisabled(), jc.IsFalse) -} - -func (s *UserSuite) TestUserChangePassword(c *gc.C) { - user, err := s.State.User(s.AdminUserTag(c)) - c.Assert(err, jc.ErrorIsNil) - c.Assert(user.PasswordValid("dummy-secret"), jc.IsTrue) - _, err = s.RunUserCommand(c, "change-password", "--generate") - c.Assert(err, jc.ErrorIsNil) - user.Refresh() - c.Assert(err, jc.ErrorIsNil) - c.Assert(user.PasswordValid("dummy-secret"), jc.IsFalse) -} - -func (s *UserSuite) TestUserInfo(c *gc.C) { - user, err := s.State.User(s.AdminUserTag(c)) - c.Assert(err, jc.ErrorIsNil) - c.Assert(user.PasswordValid("dummy-secret"), jc.IsTrue) - ctx, err := s.RunUserCommand(c, "info") - c.Assert(err, jc.ErrorIsNil) - c.Assert(testing.Stdout(ctx), jc.Contains, "user-name: dummy-admin") -} - -func (s *UserSuite) TestUserDisable(c *gc.C) { - user := s.Factory.MakeUser(c, &factory.UserParams{Name: "barbara"}) - _, err := s.RunUserCommand(c, "disable", "barbara") - c.Assert(err, jc.ErrorIsNil) - user.Refresh() - c.Assert(err, jc.ErrorIsNil) - c.Assert(user.IsDisabled(), jc.IsTrue) -} - -func (s *UserSuite) TestUserEnable(c *gc.C) { - user := s.Factory.MakeUser(c, &factory.UserParams{Name: "barbara", Disabled: true}) - _, err := s.RunUserCommand(c, "enable", "barbara") - c.Assert(err, jc.ErrorIsNil) - user.Refresh() - c.Assert(err, jc.ErrorIsNil) - c.Assert(user.IsDisabled(), jc.IsFalse) -} - -func (s *UserSuite) TestUserList(c *gc.C) { - ctx, err := s.RunUserCommand(c, "list") - c.Assert(err, jc.ErrorIsNil) - periodPattern := `(just now|\d+ \S+ ago)` - expected := fmt.Sprintf(` -NAME\s+DISPLAY NAME\s+DATE CREATED\s+LAST CONNECTION -dummy-admin\s+dummy-admin\s+%s\s+%s - -`[1:], periodPattern, periodPattern) - c.Assert(testing.Stdout(ctx), gc.Matches, expected) -} === modified file 'src/github.com/juju/juju/cmd/jujud/agent/agent.go' --- src/github.com/juju/juju/cmd/jujud/agent/agent.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/jujud/agent/agent.go 2015-10-23 18:29:32 +0000 @@ -8,64 +8,40 @@ import ( "sync" - "time" "github.com/juju/cmd" "github.com/juju/errors" "github.com/juju/names" - "github.com/juju/utils" "launchpad.net/gnuflag" "github.com/juju/juju/agent" - "github.com/juju/juju/api" - apiagent "github.com/juju/juju/api/agent" - "github.com/juju/juju/apiserver/params" "github.com/juju/juju/cmd/jujud/util" - "github.com/juju/juju/network" - "github.com/juju/juju/state" - "github.com/juju/juju/version" - "github.com/juju/juju/worker" -) - -var ( - apiOpen = openAPIForAgent - - checkProvisionedStrategy = utils.AttemptStrategy{ - Total: 1 * time.Minute, - Delay: 5 * time.Second, - } -) - -// openAPIForAgent exists to handle the edge case that exists -// when an environment is jumping several versions and doesn't -// yet have the environment UUID cached in the agent config. -// This happens only the first time an agent tries to connect -// after an upgrade. If there is no environment UUID set, then -// use login version 1. -func openAPIForAgent(info *api.Info, opts api.DialOpts) (*api.State, error) { - if info.EnvironTag.Id() == "" { - return api.OpenWithVersion(info, opts, 1) - } - return api.Open(info, opts) -} - +) + +// AgentConf is a terribly confused interface. +// +// Parts of it are a mixin for cmd.Command implementations; others are a mixin +// for agent.Agent implementations; others bridge the two. We should be aiming +// to separate the cmd responsibilities from the agent responsibilities. type AgentConf interface { + // AddFlags injects common agent flags into f. AddFlags(f *gnuflag.FlagSet) + // CheckArgs reports whether the given args are valid for this agent. CheckArgs(args []string) error + + // DataDir returns the directory where this agent should store its data. + DataDir() string + // ReadConfig reads the agent's config from its config file. ReadConfig(tag string) error + + // CurrentConfig returns the agent config for this agent. + CurrentConfig() agent.Config + // ChangeConfig modifies this configuration using the given mutator. ChangeConfig(change agent.ConfigMutator) error - // CurrentConfig returns the agent config for this agent. - CurrentConfig() agent.Config - // SetAPIHostPorts satisfies worker/apiaddressupdater/APIAddressSetter. - SetAPIHostPorts(servers [][]network.HostPort) error - // SetStateServingInfo satisfies worker/certupdater/SetStateServingInfo. - SetStateServingInfo(info params.StateServingInfo) error - // DataDir returns the directory where this agent should store its data. - DataDir() string } // NewAgentConf returns a new value that satisfies AgentConf @@ -137,169 +113,3 @@ defer ch.mu.Unlock() return ch._config.Clone() } - -// SetAPIHostPorts satisfies worker/apiaddressupdater/APIAddressSetter. -func (a *agentConf) SetAPIHostPorts(servers [][]network.HostPort) error { - return a.ChangeConfig(func(c agent.ConfigSetter) error { - c.SetAPIHostPorts(servers) - return nil - }) -} - -// SetStateServingInfo satisfies worker/certupdater/SetStateServingInfo. -func (a *agentConf) SetStateServingInfo(info params.StateServingInfo) error { - return a.ChangeConfig(func(c agent.ConfigSetter) error { - c.SetStateServingInfo(info) - return nil - }) -} - -type Agent interface { - Tag() names.Tag - ChangeConfig(agent.ConfigMutator) error -} - -// The AgentState interface is implemented by state types -// that represent running agents. -type AgentState interface { - // SetAgentVersion sets the tools version that the agent is - // currently running. - SetAgentVersion(v version.Binary) error - Tag() string - Life() state.Life -} - -// isleep waits for the given duration or until it receives a value on -// stop. It returns whether the full duration was slept without being -// stopped. -func isleep(d time.Duration, stop <-chan struct{}) bool { - select { - case <-stop: - return false - case <-time.After(d): - } - return true -} - -type configChanger func(c *agent.Config) - -// OpenAPIState opens the API using the given information. The agent's -// password is changed if the fallback password was used to connect to -// the API. -func OpenAPIState(agentConfig agent.Config, a Agent) (_ *api.State, _ *apiagent.Entity, outErr error) { - info := agentConfig.APIInfo() - st, usedOldPassword, err := openAPIStateUsingInfo(info, a, agentConfig.OldPassword()) - if err != nil { - return nil, nil, err - } - defer func() { - if outErr != nil && st != nil { - st.Close() - } - }() - - entity, err := st.Agent().Entity(a.Tag()) - if err == nil && entity.Life() == params.Dead { - logger.Errorf("agent terminating - entity %q is dead", a.Tag()) - return nil, nil, worker.ErrTerminateAgent - } - if params.IsCodeUnauthorized(err) { - logger.Errorf("agent terminating due to error returned during entity lookup: %v", err) - return nil, nil, worker.ErrTerminateAgent - } - if err != nil { - return nil, nil, err - } - - if !usedOldPassword { - // Call set password with the current password. If we've recently - // become a state server, this will fix up our credentials in mongo. - if err := entity.SetPassword(info.Password); err != nil { - return nil, nil, errors.Annotate(err, "can't reset agent password") - } - } else { - // We succeeded in connecting with the fallback - // password, so we need to create a new password - // for the future. - newPassword, err := utils.RandomPassword() - if err != nil { - return nil, nil, err - } - err = setAgentPassword(newPassword, info.Password, a, entity) - if err != nil { - return nil, nil, err - } - - // Reconnect to the API with the new password. - st.Close() - info.Password = newPassword - st, err = apiOpen(info, api.DialOpts{}) - if err != nil { - return nil, nil, err - } - } - - return st, entity, err -} - -func setAgentPassword(newPw, oldPw string, a Agent, entity *apiagent.Entity) error { - // Change the configuration *before* setting the entity - // password, so that we avoid the possibility that - // we might successfully change the entity's - // password but fail to write the configuration, - // thus locking us out completely. - if err := a.ChangeConfig(func(c agent.ConfigSetter) error { - c.SetPassword(newPw) - c.SetOldPassword(oldPw) - return nil - }); err != nil { - return err - } - return entity.SetPassword(newPw) -} - -// OpenAPIStateUsingInfo opens the API using the given API -// information, and returns the opened state and the api entity with -// the given tag. -func OpenAPIStateUsingInfo(info *api.Info, a Agent, oldPassword string) (*api.State, error) { - st, _, err := openAPIStateUsingInfo(info, a, oldPassword) - return st, err -} - -func openAPIStateUsingInfo(info *api.Info, a Agent, oldPassword string) (*api.State, bool, error) { - // We let the API dial fail immediately because the - // runner's loop outside the caller of openAPIState will - // keep on retrying. If we block for ages here, - // then the worker that's calling this cannot - // be interrupted. - st, err := apiOpen(info, api.DialOpts{}) - usedOldPassword := false - if params.IsCodeUnauthorized(err) { - // We've perhaps used the wrong password, so - // try again with the fallback password. - infoCopy := *info - info = &infoCopy - info.Password = oldPassword - usedOldPassword = true - st, err = apiOpen(info, api.DialOpts{}) - } - // The provisioner may take some time to record the agent's - // machine instance ID, so wait until it does so. - if params.IsCodeNotProvisioned(err) { - for a := checkProvisionedStrategy.Start(); a.Next(); { - st, err = apiOpen(info, api.DialOpts{}) - if !params.IsCodeNotProvisioned(err) { - break - } - } - } - if err != nil { - if params.IsCodeNotProvisioned(err) || params.IsCodeUnauthorized(err) { - logger.Errorf("agent terminating due to error returned during API open: %v", err) - return nil, false, worker.ErrTerminateAgent - } - return nil, false, err - } - - return st, usedOldPassword, nil -} === modified file 'src/github.com/juju/juju/cmd/jujud/agent/agent_test.go' --- src/github.com/juju/juju/cmd/jujud/agent/agent_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/jujud/agent/agent_test.go 2015-10-23 18:29:32 +0000 @@ -11,12 +11,10 @@ "github.com/juju/names" gitjujutesting "github.com/juju/testing" jc "github.com/juju/testing/checkers" - "github.com/juju/utils" gc "gopkg.in/check.v1" "github.com/juju/juju/agent" agenttools "github.com/juju/juju/agent/tools" - "github.com/juju/juju/api" apienvironment "github.com/juju/juju/api/environment" "github.com/juju/juju/apiserver/params" agenttesting "github.com/juju/juju/cmd/jujud/agent/testing" @@ -26,8 +24,6 @@ "github.com/juju/juju/juju/paths" "github.com/juju/juju/mongo" "github.com/juju/juju/network" - "github.com/juju/juju/state" - "github.com/juju/juju/state/multiwatcher" coretesting "github.com/juju/juju/testing" coretools "github.com/juju/juju/tools" "github.com/juju/juju/version" @@ -35,75 +31,6 @@ "github.com/juju/juju/worker/proxyupdater" ) -var ( - _ = gc.Suite(&apiOpenSuite{}) -) - -type apiOpenSuite struct{ coretesting.BaseSuite } - -func (s *apiOpenSuite) SetUpTest(c *gc.C) { - s.PatchValue(&checkProvisionedStrategy, utils.AttemptStrategy{}) -} - -func (s *apiOpenSuite) TestOpenAPIStateReplaceErrors(c *gc.C) { - type replaceErrors struct { - openErr error - replaceErr error - } - var apiError error - s.PatchValue(&apiOpen, func(info *api.Info, opts api.DialOpts) (*api.State, error) { - return nil, apiError - }) - errReplacePairs := []replaceErrors{{ - fmt.Errorf("blah"), nil, - }, { - openErr: ¶ms.Error{Code: params.CodeNotProvisioned}, - replaceErr: worker.ErrTerminateAgent, - }, { - openErr: ¶ms.Error{Code: params.CodeUnauthorized}, - replaceErr: worker.ErrTerminateAgent, - }} - for i, test := range errReplacePairs { - c.Logf("test %d", i) - apiError = test.openErr - _, _, err := OpenAPIState(fakeAPIOpenConfig{}, nil) - if test.replaceErr == nil { - c.Check(err, gc.Equals, test.openErr) - } else { - c.Check(err, gc.Equals, test.replaceErr) - } - } -} - -func (s *apiOpenSuite) TestOpenAPIStateWaitsProvisioned(c *gc.C) { - s.PatchValue(&checkProvisionedStrategy.Min, 5) - var called int - s.PatchValue(&apiOpen, func(info *api.Info, opts api.DialOpts) (*api.State, error) { - called++ - if called == checkProvisionedStrategy.Min-1 { - return nil, ¶ms.Error{Code: params.CodeUnauthorized} - } - return nil, ¶ms.Error{Code: params.CodeNotProvisioned} - }) - _, _, err := OpenAPIState(fakeAPIOpenConfig{}, nil) - c.Assert(err, gc.Equals, worker.ErrTerminateAgent) - c.Assert(called, gc.Equals, checkProvisionedStrategy.Min-1) -} - -func (s *apiOpenSuite) TestOpenAPIStateWaitsProvisionedGivesUp(c *gc.C) { - s.PatchValue(&checkProvisionedStrategy.Min, 5) - var called int - s.PatchValue(&apiOpen, func(info *api.Info, opts api.DialOpts) (*api.State, error) { - called++ - return nil, ¶ms.Error{Code: params.CodeNotProvisioned} - }) - _, _, err := OpenAPIState(fakeAPIOpenConfig{}, nil) - c.Assert(err, gc.Equals, worker.ErrTerminateAgent) - // +1 because we always attempt at least once outside the attempt strategy - // (twice if the API server initially returns CodeUnauthorized.) - c.Assert(called, gc.Equals, checkProvisionedStrategy.Min+1) -} - type acCreator func() (cmd.Command, AgentConf) // CheckAgentCommand is a utility function for verifying that common agent @@ -205,36 +132,6 @@ return conf, agentTools } -func (s *AgentSuite) RunTestOpenAPIState(c *gc.C, ent state.AgentEntity, agentCmd Agent, initialPassword string) { - conf, err := agent.ReadConfig(agent.ConfigPath(s.DataDir(), ent.Tag())) - c.Assert(err, jc.ErrorIsNil) - - conf.SetPassword("") - err = conf.Write() - c.Assert(err, jc.ErrorIsNil) - - // Check that it starts initially and changes the password - assertOpen := func(conf agent.Config) { - st, gotEnt, err := OpenAPIState(conf, agentCmd) - c.Assert(err, jc.ErrorIsNil) - c.Assert(st, gc.NotNil) - st.Close() - c.Assert(gotEnt.Tag(), gc.Equals, ent.Tag().String()) - } - assertOpen(conf) - - // Check that the initial password is no longer valid. - err = ent.Refresh() - c.Assert(err, jc.ErrorIsNil) - c.Assert(ent.PasswordValid(initialPassword), jc.IsFalse) - - // Read the configuration and check that we can connect with it. - conf, err = agent.ReadConfig(agent.ConfigPath(conf.DataDir(), conf.Tag())) - c.Assert(err, gc.IsNil) - // Check we can open the API with the new configuration. - assertOpen(conf) -} - // writeStateAgentConfig creates and writes a state agent config. func writeStateAgentConfig( c *gc.C, stateInfo *mongo.MongoInfo, dataDir string, tag names.Tag, @@ -265,9 +162,3 @@ c.Assert(conf.Write(), gc.IsNil) return conf } - -type fakeAPIOpenConfig struct{ agent.Config } - -func (fakeAPIOpenConfig) APIInfo() *api.Info { return &api.Info{} } -func (fakeAPIOpenConfig) OldPassword() string { return "old" } -func (fakeAPIOpenConfig) Jobs() []multiwatcher.MachineJob { return []multiwatcher.MachineJob{} } === added file 'src/github.com/juju/juju/cmd/jujud/agent/common_test.go' --- src/github.com/juju/juju/cmd/jujud/agent/common_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/jujud/agent/common_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,69 @@ +package agent + +import ( + "fmt" + "path/filepath" + "time" + + "github.com/juju/cmd" + "github.com/juju/names" + + "github.com/juju/juju/agent" + coretesting "github.com/juju/juju/testing" + "github.com/juju/juju/worker" +) + +// This file contains bits of test infrastructure that are shared by +// the unit and machine agent tests. + +type runner interface { + Run(*cmd.Context) error + Stop() error +} + +// runWithTimeout runs an agent and waits +// for it to complete within a reasonable time. +func runWithTimeout(r runner) error { + done := make(chan error) + go func() { + done <- r.Run(nil) + }() + select { + case err := <-done: + return err + case <-time.After(coretesting.LongWait): + } + err := r.Stop() + return fmt.Errorf("timed out waiting for agent to finish; stop error: %v", err) +} + +func newDummyWorker() worker.Worker { + return worker.NewSimpleWorker(func(stop <-chan struct{}) error { + <-stop + return nil + }) +} + +type FakeConfig struct { + agent.Config +} + +func (FakeConfig) LogDir() string { + return filepath.FromSlash("/var/log/juju/") +} + +func (FakeConfig) Tag() names.Tag { + return names.NewMachineTag("42") +} + +type FakeAgentConfig struct { + AgentConf +} + +func (FakeAgentConfig) ReadConfig(string) error { return nil } + +func (FakeAgentConfig) CurrentConfig() agent.Config { + return FakeConfig{} +} + +func (FakeAgentConfig) CheckArgs([]string) error { return nil } === modified file 'src/github.com/juju/juju/cmd/jujud/agent/machine.go' --- src/github.com/juju/juju/cmd/jujud/agent/machine.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/jujud/agent/machine.go 2015-10-23 18:29:32 +0000 @@ -20,6 +20,7 @@ "github.com/juju/names" "github.com/juju/replicaset" "github.com/juju/utils" + "github.com/juju/utils/clock" "github.com/juju/utils/featureflag" "github.com/juju/utils/set" "github.com/juju/utils/symlink" @@ -44,6 +45,7 @@ "github.com/juju/juju/container" "github.com/juju/juju/container/kvm" "github.com/juju/juju/container/lxc" + "github.com/juju/juju/container/lxc/lxcutils" "github.com/juju/juju/environs" "github.com/juju/juju/environs/config" "github.com/juju/juju/feature" @@ -58,11 +60,12 @@ "github.com/juju/juju/state" "github.com/juju/juju/state/multiwatcher" statestorage "github.com/juju/juju/state/storage" - coretools "github.com/juju/juju/tools" + "github.com/juju/juju/storage/looputil" "github.com/juju/juju/version" "github.com/juju/juju/worker" "github.com/juju/juju/worker/addresser" "github.com/juju/juju/worker/apiaddressupdater" + "github.com/juju/juju/worker/apicaller" "github.com/juju/juju/worker/authenticationworker" "github.com/juju/juju/worker/certupdater" "github.com/juju/juju/worker/charmrevisionworker" @@ -73,9 +76,11 @@ "github.com/juju/juju/worker/diskmanager" "github.com/juju/juju/worker/envworkermanager" "github.com/juju/juju/worker/firewaller" + "github.com/juju/juju/worker/gate" "github.com/juju/juju/worker/instancepoller" "github.com/juju/juju/worker/localstorage" workerlogger "github.com/juju/juju/worker/logger" + "github.com/juju/juju/worker/logsender" "github.com/juju/juju/worker/machiner" "github.com/juju/juju/worker/metricworker" "github.com/juju/juju/worker/minunitsworker" @@ -115,9 +120,12 @@ newDiskManager = diskmanager.NewWorker newStorageWorker = storageprovisioner.NewStorageProvisioner newCertificateUpdater = certupdater.NewCertificateUpdater + newResumer = resumer.NewResumer + newInstancePoller = instancepoller.NewWorker + newCleaner = cleaner.NewCleaner + newAddresser = addresser.NewWorker reportOpenedState = func(io.Closer) {} reportOpenedAPI = func(io.Closer) {} - reportClosedAPI = func(io.Closer) {} getMetricAPI = metricAPI ) @@ -255,15 +263,17 @@ // MachineAgent given a machineId. func MachineAgentFactoryFn( agentConfWriter AgentConfigWriter, - apiAddressSetter apiaddressupdater.APIAddressSetter, + bufferedLogs logsender.LogRecordCh, + loopDeviceManager looputil.LoopDeviceManager, ) func(string) *MachineAgent { return func(machineId string) *MachineAgent { return NewMachineAgent( machineId, agentConfWriter, - apiAddressSetter, + bufferedLogs, NewUpgradeWorkerContext(), worker.NewRunner(cmdutil.IsFatal, cmdutil.MoreImportant), + loopDeviceManager, ) } } @@ -272,44 +282,41 @@ func NewMachineAgent( machineId string, agentConfWriter AgentConfigWriter, - apiAddressSetter apiaddressupdater.APIAddressSetter, + bufferedLogs logsender.LogRecordCh, upgradeWorkerContext *upgradeWorkerContext, runner worker.Runner, + loopDeviceManager looputil.LoopDeviceManager, ) *MachineAgent { - return &MachineAgent{ machineId: machineId, AgentConfigWriter: agentConfWriter, - apiAddressSetter: apiAddressSetter, + bufferedLogs: bufferedLogs, + upgradeWorkerContext: upgradeWorkerContext, workersStarted: make(chan struct{}), - upgradeWorkerContext: upgradeWorkerContext, runner: runner, initialAgentUpgradeCheckComplete: make(chan struct{}), + loopDeviceManager: loopDeviceManager, } } -// APIStateUpgrader defines the methods on the Upgrader that -// agents call. -type APIStateUpgrader interface { - SetVersion(string, version.Binary) error -} - // MachineAgent is responsible for tying together all functionality -// needed to orchestarte a Jujud instance which controls a machine. +// needed to orchestrate a Jujud instance which controls a machine. type MachineAgent struct { AgentConfigWriter tomb tomb.Tomb machineId string previousAgentVersion version.Number - apiAddressSetter apiaddressupdater.APIAddressSetter runner worker.Runner + bufferedLogs logsender.LogRecordCh configChangedVal voyeur.Value upgradeWorkerContext *upgradeWorkerContext - restoreMode bool - restoring bool workersStarted chan struct{} + // XXX(fwereade): these smell strongly of goroutine-unsafeness. + restoreMode bool + restoring bool + // Used to signal that the upgrade worker will not // reboot the agent on startup because there are no // longer any immediately pending agent upgrades. @@ -319,14 +326,7 @@ mongoInitMutex sync.Mutex mongoInitialized bool - apiStateUpgrader APIStateUpgrader -} - -func (a *MachineAgent) getUpgrader(st *api.State) APIStateUpgrader { - if a.apiStateUpgrader != nil { - return a.apiStateUpgrader - } - return st.Upgrader() + loopDeviceManager looputil.LoopDeviceManager } // IsRestorePreparing returns bool representing if we are in restore mode @@ -395,7 +395,7 @@ if !update { return nil } - // Write a new certificate to the mongp pem and agent config files. + // Write a new certificate to the mongo pem and agent config files. si.Cert, si.PrivateKey, err = cert.NewDefaultServer(agentConfig.CACert(), si.CAPrivateKey, dnsNames.Values()) if err != nil { return err @@ -429,6 +429,7 @@ if err := a.upgradeCertificateDNSNames(); err != nil { return errors.Annotate(err, "error upgrading server certificate") } + agentConfig := a.CurrentConfig() if err := a.upgradeWorkerContext.InitializeUsingAgent(a); err != nil { @@ -436,6 +437,7 @@ } a.configChangedVal.Set(struct{}{}) a.previousAgentVersion = agentConfig.UpgradedToVersion() + network.InitializeFromConfig(agentConfig) charmrepo.CacheDir = filepath.Join(agentConfig.DataDir(), "charmcache") if err := a.createJujuRun(agentConfig.DataDir()); err != nil { @@ -444,8 +446,13 @@ a.runner.StartWorker("api", a.APIWorker) a.runner.StartWorker("statestarter", a.newStateStarterWorker) a.runner.StartWorker("termination", func() (worker.Worker, error) { - return terminationworker.NewWorker(), nil + return startTerminationWorker( + agentConfig.DataDir(), + terminationworker.NewWorker, + os.Stat, + ), nil }) + // At this point, all workers will have been configured to start close(a.workersStarted) err := a.runner.Wait() @@ -464,13 +471,40 @@ return err } +// startTerminationWorker starts a new termination worker that will cause +// the machine agent to uninstall if the uninstall-agent file is present. +func startTerminationWorker( + dataDir string, + newTerminationWorker func(func() error) worker.Worker, + statFile func(string) (os.FileInfo, error), +) worker.Worker { + uninstallFile := filepath.Join(dataDir, agent.UninstallAgentFile) + terminationError := func() error { + // If the uninstall file exists, then the termination + // signal should cause the agent to uninstall; otherwise + // it should just restart the workers. + if _, err := statFile(uninstallFile); err == nil { + return worker.ErrTerminateAgent + } + logger.Debugf( + "uninstall file %q does not exist", + uninstallFile, + ) + return &cmdutil.FatalError{fmt.Sprintf( + "%q signal received", + terminationworker.TerminationSignal, + )} + } + return newTerminationWorker(terminationError) +} + func (a *MachineAgent) executeRebootOrShutdown(action params.RebootAction) error { agentCfg := a.CurrentConfig() // At this stage, all API connections would have been closed // We need to reopen the API to clear the reboot flag after // scheduling the reboot. It may be cleaner to do this in the reboot // worker, before returning the ErrRebootMachine. - st, _, err := OpenAPIState(agentCfg, a) + st, _, err := apicaller.OpenAPIState(a) if err != nil { logger.Infof("Reboot: Error connecting to state") return errors.Trace(err) @@ -538,9 +572,9 @@ a.restoring = false } -// newrestorestatewatcherworker will return a worker or err if there is a failure, -// the worker takes care of watching the state of restoreInfo doc and put the -// agent in the different restore modes. +// newRestoreStateWatcherWorker will return a worker or err if there +// is a failure, the worker takes care of watching the state of +// restoreInfo doc and put the agent in the different restore modes. func (a *MachineAgent) newRestoreStateWatcherWorker(st *state.State) (worker.Worker, error) { rWorker := func(stopch <-chan struct{}) error { return a.restoreStateWatcher(st, stopch) @@ -631,22 +665,27 @@ // APIWorker returns a Worker that connects to the API and starts any // workers that need an API connection. func (a *MachineAgent) APIWorker() (_ worker.Worker, err error) { - agentConfig := a.CurrentConfig() - st, entity, err := OpenAPIState(agentConfig, a) + st, entity, err := apicaller.OpenAPIState(a) if err != nil { return nil, err } reportOpenedAPI(st) defer func() { + // TODO(fwereade): this is not properly tested. Old tests were evil + // (dependent on injecting an error in a patched-out upgrader API + // that shouldn't even be used at this level)... so I just deleted + // them. Not a major worry: this whole method will become redundant + // when we switch to the dependency engine (and specifically use + // worker/apicaller to connect). if err != nil { - st.Close() - reportClosedAPI(st) + if err := st.Close(); err != nil { + logger.Errorf("while closing API: %v", err) + } } }() - // Refresh the configuration, since it may have been updated after opening state. - agentConfig = a.CurrentConfig() + agentConfig := a.CurrentConfig() for _, job := range entity.Jobs() { if job.NeedsState() { info, err := st.Agent().StateServingInfo() @@ -665,14 +704,6 @@ } } - // Before starting any workers, ensure we record the Juju version this machine - // agent is running. - currentTools := &coretools.Tools{Version: version.Current} - apiStateUpgrader := a.getUpgrader(st) - if err := apiStateUpgrader.SetVersion(agentConfig.Tag().String(), currentTools.Version); err != nil { - return nil, errors.Annotate(err, "cannot set machine agent version") - } - runner := newConnRunner(st) // Run the agent upgrader and the upgrade-steps worker without waiting for @@ -689,7 +720,7 @@ } func (a *MachineAgent) postUpgradeAPIWorker( - st *api.State, + st api.Connection, agentConfig agent.Config, entity *apiagent.Entity, ) (worker.Worker, error) { @@ -702,12 +733,8 @@ } } - rsyslogMode := rsyslog.RsyslogModeForwarding - if isEnvironManager { - rsyslogMode = rsyslog.RsyslogModeAccumulate - } - runner := newConnRunner(st) + // TODO(fwereade): this is *still* a hideous layering violation, but at least // it's confined to jujud rather than extending into the worker itself. // Start this worker first to try and get proxy settings in place @@ -717,6 +744,21 @@ return proxyupdater.New(st.Environment(), writeSystemFiles), nil }) + if isEnvironManager { + runner.StartWorker("resumer", func() (worker.Worker, error) { + // The action of resumer is so subtle that it is not tested, + // because we can't figure out how to do so without + // brutalising the transaction log. + return newResumer(st.Resumer()), nil + }) + } + + if feature.IsDbLogEnabled() { + runner.StartWorker("logsender", func() (worker.Worker, error) { + return logsender.New(a.bufferedLogs, gate.AlreadyUnlocked{}, a), nil + }) + } + envConfig, err := st.Environment().EnvironConfig() if err != nil { return nil, fmt.Errorf("cannot read environment config: %v", err) @@ -726,7 +768,8 @@ logger.Infof("machine addresses not used, only addresses from provider") } runner.StartWorker("machiner", func() (worker.Worker, error) { - return newMachiner(st.Machiner(), agentConfig, ignoreMachineAddresses), nil + accessor := machiner.APIMachineAccessor{st.Machiner()} + return newMachiner(accessor, agentConfig, ignoreMachineAddresses), nil }) runner.StartWorker("reboot", func() (worker.Worker, error) { reboot, err := st.Reboot() @@ -740,15 +783,23 @@ return rebootworker.NewReboot(reboot, agentConfig, lock) }) runner.StartWorker("apiaddressupdater", func() (worker.Worker, error) { - return apiaddressupdater.NewAPIAddressUpdater(st.Machiner(), a.apiAddressSetter), nil + addressUpdater := agent.APIHostPortsSetter{a} + return apiaddressupdater.NewAPIAddressUpdater(st.Machiner(), addressUpdater), nil }) runner.StartWorker("logger", func() (worker.Worker, error) { return workerlogger.NewLogger(st.Logger(), agentConfig), nil }) - runner.StartWorker("rsyslog", func() (worker.Worker, error) { - return cmdutil.NewRsyslogConfigWorker(st.Rsyslog(), agentConfig, rsyslogMode) - }) + if !featureflag.Enabled(feature.DisableRsyslog) { + rsyslogMode := rsyslog.RsyslogModeForwarding + if isEnvironManager { + rsyslogMode = rsyslog.RsyslogModeAccumulate + } + + runner.StartWorker("rsyslog", func() (worker.Worker, error) { + return cmdutil.NewRsyslogConfigWorker(st.Rsyslog(), agentConfig, rsyslogMode) + }) + } if !isEnvironManager { runner.StartWorker("stateconverter", func() (worker.Worker, error) { @@ -767,7 +818,10 @@ scope := agentConfig.Tag() api := st.StorageProvisioner(scope) storageDir := filepath.Join(agentConfig.DataDir(), "storage") - return newStorageWorker(scope, storageDir, api, api, api, api, api), nil + return newStorageWorker( + scope, storageDir, api, api, api, api, api, api, + clock.WallClock, + ), nil }) // Check if the network management is disabled. @@ -848,7 +902,7 @@ } func (a *MachineAgent) upgradeStepsWorkerStarter( - st *api.State, + st api.Connection, jobs []multiwatcher.MachineJob, ) func() (worker.Worker, error) { return func() (worker.Worker, error) { @@ -882,7 +936,7 @@ // setupContainerSupport determines what containers can be run on this machine and // initialises suitable infrastructure to support such containers. -func (a *MachineAgent) setupContainerSupport(runner worker.Runner, st *api.State, entity *apiagent.Entity, agentConfig agent.Config) error { +func (a *MachineAgent) setupContainerSupport(runner worker.Runner, st api.Connection, entity *apiagent.Entity, agentConfig agent.Config) error { var supportedContainers []instance.ContainerType // LXC containers are only supported on bare metal and fully virtualized linux systems // Nested LXC containers and Windows machines cannot run LXC containers @@ -910,7 +964,7 @@ // and a suitable provisioner is started. func (a *MachineAgent) updateSupportedContainers( runner worker.Runner, - st *api.State, + st api.Connection, machineTag string, containers []instance.ContainerType, agentConfig agent.Config, @@ -951,7 +1005,17 @@ // use an image URL getter if there's a private key. var imageURLGetter container.ImageURLGetter if agentConfig.Value(agent.AllowsSecureConnection) == "true" { - imageURLGetter = container.NewImageURLGetter(st.Addr(), envUUID.Id(), []byte(agentConfig.CACert())) + cfg, err := pr.EnvironConfig() + 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{ + st.Addr(), envUUID.Id(), []byte(agentConfig.CACert()), + cfg.CloudImageBaseURL(), container.ImageDownloadURL, + }) } params := provisioner.ContainerSetupParams{ Runner: runner, @@ -1044,7 +1108,7 @@ return newCertificateUpdater(m, agentConfig, st, st, stateServingSetter), nil }) - if featureflag.Enabled(feature.DbLog) { + if feature.IsDbLogEnabled() { a.startWorkerAfterUpgrade(singularRunner, "dblogpruner", func() (worker.Worker, error) { return dblogpruner.New(st, dblogpruner.NewLogPruneParams()), nil }) @@ -1053,13 +1117,6 @@ return statushistorypruner.New(st, statushistorypruner.NewHistoryPrunerParams()), nil }) - a.startWorkerAfterUpgrade(singularRunner, "resumer", func() (worker.Worker, error) { - // The action of resumer is so subtle that it is not tested, - // because we can't figure out how to do so without brutalising - // the transaction log. - return resumer.NewResumer(st), nil - }) - a.startWorkerAfterUpgrade(singularRunner, "txnpruner", func() (worker.Worker, error) { return txnpruner.New(st, time.Hour*2), nil }) @@ -1070,7 +1127,18 @@ logger.Warningf("ignoring unknown job %q", job) } } - return cmdutil.NewCloseWorker(logger, runner, st), nil + return cmdutil.NewCloseWorker(logger, runner, stateWorkerCloser{st}), nil +} + +type stateWorkerCloser struct { + stateCloser io.Closer +} + +func (s stateWorkerCloser) Close() error { + // This state-dependent data source will be useless once state is closed - + // un-register it before closing state. + unregisterSimplestreamsDataSource() + return s.stateCloser.Close() } // startEnvWorkers starts state server workers that need to run per @@ -1078,16 +1146,19 @@ func (a *MachineAgent) startEnvWorkers( ssSt envworkermanager.InitialState, st *state.State, -) (runner worker.Runner, err error) { +) (_ worker.Worker, err error) { envUUID := st.EnvironUUID() defer errors.DeferredAnnotatef(&err, "failed to start workers for env %s", envUUID) logger.Infof("starting workers for env %s", envUUID) // Establish API connection for this environment. agentConfig := a.CurrentConfig() - apiInfo := agentConfig.APIInfo() + apiInfo, ok := agentConfig.APIInfo() + if !ok { + return nil, errors.New("API info not available") + } apiInfo.EnvironTag = st.EnvironTag() - apiSt, err := OpenAPIStateUsingInfo(apiInfo, a, agentConfig.OldPassword()) + apiSt, err := apicaller.OpenAPIStateUsingInfo(apiInfo, agentConfig.OldPassword()) if err != nil { return nil, errors.Trace(err) } @@ -1095,7 +1166,7 @@ // Create a runner for workers specific to this // environment. Either the State or API connection failing will be // considered fatal, killing the runner and all its workers. - runner = newConnRunner(st, apiSt) + runner := newConnRunner(st, apiSt) defer func() { if err != nil && runner != nil { runner.Kill() @@ -1130,18 +1201,9 @@ // Start workers that depend on a *state.State. // TODO(fwereade): 2015-04-21 THIS SHALL NOT PASS // Seriously, these should all be using the API. - runner.StartWorker("instancepoller", func() (worker.Worker, error) { - return instancepoller.NewWorker(st), nil - }) - singularRunner.StartWorker("cleaner", func() (worker.Worker, error) { - return cleaner.NewCleaner(st), nil - }) singularRunner.StartWorker("minunitsworker", func() (worker.Worker, error) { return minunitsworker.NewMinUnitsWorker(st), nil }) - singularRunner.StartWorker("addresserworker", func() (worker.Worker, error) { - return addresser.NewWorker(st) - }) // Start workers that use an API connection. singularRunner.StartWorker("environ-provisioner", func() (worker.Worker, error) { @@ -1150,7 +1212,10 @@ singularRunner.StartWorker("environ-storageprovisioner", func() (worker.Worker, error) { scope := st.EnvironTag() api := apiSt.StorageProvisioner(scope) - return newStorageWorker(scope, "", api, api, api, api, api), nil + return newStorageWorker( + scope, "", api, api, api, api, api, api, + clock.WallClock, + ), nil }) singularRunner.StartWorker("charm-revision-updater", func() (worker.Worker, error) { return charmrevisionworker.NewRevisionUpdateWorker(apiSt.CharmRevisionUpdater()), nil @@ -1158,6 +1223,15 @@ runner.StartWorker("metricmanagerworker", func() (worker.Worker, error) { return metricworker.NewMetricsManager(getMetricAPI(apiSt)) }) + singularRunner.StartWorker("instancepoller", func() (worker.Worker, error) { + return newInstancePoller(apiSt.InstancePoller()), nil + }) + singularRunner.StartWorker("cleaner", func() (worker.Worker, error) { + return newCleaner(apiSt.Cleaner()), nil + }) + singularRunner.StartWorker("addresserworker", func() (worker.Worker, error) { + return newAddresser(apiSt.Addresser()) + }) // TODO(axw) 2013-09-24 bug #1229506 // Make another job to enable the firewaller. Not all @@ -1180,7 +1254,7 @@ var getFirewallMode = _getFirewallMode -func _getFirewallMode(apiSt *api.State) (string, error) { +func _getFirewallMode(apiSt api.Connection) (string, error) { envConfig, err := apiSt.Environment().EnvironConfig() if err != nil { return "", errors.Annotate(err, "cannot read environment config") @@ -1589,6 +1663,7 @@ } } logger.Debugf("upgrades done, starting worker %q", name) + // Upgrades are done, start the worker. worker, err := start() if err != nil { @@ -1611,7 +1686,7 @@ }) } -func (a *MachineAgent) setMachineStatus(apiState *api.State, status params.Status, info string) error { +func (a *MachineAgent) setMachineStatus(apiState api.Connection, status params.Status, info string) error { tag := a.Tag().(names.MachineTag) machine, err := apiState.Machiner().Machine(tag) if err != nil { @@ -1658,11 +1733,28 @@ errors = append(errors, fmt.Errorf("cannot remove service %q: %v", agentServiceName, err)) } } + // Remove the juju-run symlink. if err := os.Remove(JujuRun); err != nil && !os.IsNotExist(err) { errors = append(errors, err) } + insideLXC, err := lxcutils.RunningInsideLXC() + if err != nil { + errors = append(errors, err) + } else if insideLXC { + // We're running inside LXC, so loop devices may leak. Detach + // any loop devices that are backed by files on this machine. + // + // It is necessary to do this here as well as in container/lxc, + // as container/lxc needs to check in the container's rootfs + // to see if the loop device is attached to the container; that + // will fail if the data-dir is removed first. + if err := a.loopDeviceManager.DetachLoopDevices("/", agentConfig.DataDir()); err != nil { + errors = append(errors, err) + } + } + namespace := agentConfig.Value(agent.Namespace) if err := mongo.RemoveService(namespace); err != nil { errors = append(errors, fmt.Errorf("cannot stop/remove mongo service with namespace %q: %v", namespace, err)) @@ -1708,7 +1800,7 @@ return c.session.Ping() } -func metricAPI(st *api.State) metricsmanager.MetricsManagerClient { +func metricAPI(st api.Connection) metricsmanager.MetricsManagerClient { return metricsmanager.NewClient(st) } === modified file 'src/github.com/juju/juju/cmd/jujud/agent/machine_test.go' --- src/github.com/juju/juju/cmd/jujud/agent/machine_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/jujud/agent/machine_test.go 2015-10-23 18:29:32 +0000 @@ -4,7 +4,6 @@ package agent import ( - "fmt" "io" "io/ioutil" "os" @@ -12,6 +11,7 @@ "reflect" "runtime" "strings" + "sync/atomic" "testing" "time" @@ -20,6 +20,7 @@ "github.com/juju/names" gitjujutesting "github.com/juju/testing" jc "github.com/juju/testing/checkers" + "github.com/juju/utils/clock" "github.com/juju/utils/proxy" "github.com/juju/utils/set" "github.com/juju/utils/symlink" @@ -30,10 +31,11 @@ "github.com/juju/juju/agent" "github.com/juju/juju/api" + apiaddresser "github.com/juju/juju/api/addresser" apideployer "github.com/juju/juju/api/deployer" apienvironment "github.com/juju/juju/api/environment" apifirewaller "github.com/juju/juju/api/firewaller" - apimachiner "github.com/juju/juju/api/machiner" + apiinstancepoller "github.com/juju/juju/api/instancepoller" apimetricsmanager "github.com/juju/juju/api/metricsmanager" apinetworker "github.com/juju/juju/api/networker" apirsyslog "github.com/juju/juju/api/rsyslog" @@ -63,16 +65,19 @@ sshtesting "github.com/juju/juju/utils/ssh/testing" "github.com/juju/juju/version" "github.com/juju/juju/worker" - "github.com/juju/juju/worker/apiaddressupdater" + "github.com/juju/juju/worker/addresser" + "github.com/juju/juju/worker/apicaller" "github.com/juju/juju/worker/authenticationworker" "github.com/juju/juju/worker/certupdater" "github.com/juju/juju/worker/deployer" "github.com/juju/juju/worker/diskmanager" "github.com/juju/juju/worker/instancepoller" + "github.com/juju/juju/worker/logsender" "github.com/juju/juju/worker/machiner" "github.com/juju/juju/worker/networker" "github.com/juju/juju/worker/peergrouper" "github.com/juju/juju/worker/proxyupdater" + "github.com/juju/juju/worker/resumer" "github.com/juju/juju/worker/rsyslog" "github.com/juju/juju/worker/singular" "github.com/juju/juju/worker/storageprovisioner" @@ -115,6 +120,7 @@ } func (s *commonMachineSuite) SetUpTest(c *gc.C) { + s.AgentSuite.PatchValue(&version.Current.Number, coretesting.FakeVersionNumber) s.AgentSuite.SetUpTest(c) s.TestSuite.SetUpTest(c) s.AgentSuite.PatchValue(&charmrepo.CacheDir, c.MkDir()) @@ -213,12 +219,13 @@ // newAgent returns a new MachineAgent instance func (s *commonMachineSuite) newAgent(c *gc.C, m *state.Machine) *MachineAgent { - confDir := filepath.Join(s.RootDir, "/etc/juju") - s.PatchValue(&agent.DefaultConfDir, confDir) - agentConf := agentConf{dataDir: s.DataDir()} agentConf.ReadConfig(names.NewMachineTag(m.Id()).String()) - machineAgentFactory := MachineAgentFactoryFn(&agentConf, &agentConf) + logsCh, err := logsender.InstallBufferedLogWriter(1024) + c.Assert(err, jc.ErrorIsNil) + machineAgentFactory := MachineAgentFactoryFn( + &agentConf, logsCh, &mockLoopDeviceManager{}, + ) return machineAgentFactory(m.Id()) } @@ -227,7 +234,9 @@ agentConf := agentConf{dataDir: s.DataDir()} a := NewMachineAgentCmd( nil, - MachineAgentFactoryFn(&agentConf, &agentConf), + MachineAgentFactoryFn( + &agentConf, nil, &mockLoopDeviceManager{}, + ), &agentConf, &agentConf, ) @@ -250,6 +259,7 @@ "addresserworker", "environ-provisioner", "charm-revision-updater", + "instancepoller", "firewaller", } @@ -258,7 +268,7 @@ func (s *MachineSuite) SetUpTest(c *gc.C) { s.commonMachineSuite.SetUpTest(c) s.metricAPI = newMockMetricAPI() - s.PatchValue(&getMetricAPI, func(_ *api.State) apimetricsmanager.MetricsManagerClient { + s.PatchValue(&getMetricAPI, func(_ api.Connection) apimetricsmanager.MetricsManagerClient { return s.metricAPI }) s.AddCleanup(func(*gc.C) { s.metricAPI.Stop() }) @@ -293,32 +303,6 @@ c.Assert(err, gc.ErrorMatches, "some error") } -type FakeConfig struct { - agent.Config -} - -func (FakeConfig) LogDir() string { - return filepath.FromSlash("/var/log/juju/") -} - -func (FakeConfig) Tag() names.Tag { - return names.NewMachineTag("42") -} - -type FakeAgentConfig struct { - AgentConfigWriter - apiaddressupdater.APIAddressSetter - AgentInitializer -} - -func (FakeAgentConfig) ReadConfig(string) error { return nil } - -func (FakeAgentConfig) CurrentConfig() agent.Config { - return FakeConfig{} -} - -func (FakeAgentConfig) CheckArgs([]string) error { return nil } - func (s *MachineSuite) TestUseLumberjack(c *gc.C) { ctx, err := cmd.DefaultContext() c.Assert(err, gc.IsNil) @@ -327,7 +311,9 @@ a := NewMachineAgentCmd( ctx, - MachineAgentFactoryFn(agentConf, agentConf), + MachineAgentFactoryFn( + agentConf, nil, &mockLoopDeviceManager{}, + ), agentConf, agentConf, ) @@ -353,7 +339,10 @@ a := NewMachineAgentCmd( ctx, - MachineAgentFactoryFn(agentConf, agentConf), + MachineAgentFactoryFn( + agentConf, nil, + &mockLoopDeviceManager{}, + ), agentConf, agentConf, ) @@ -422,7 +411,7 @@ c.Assert(err, jc.ErrorIsNil) case <-time.After(watcher.Period * 5 / 4): // TODO(rog) Fix this so it doesn't wait for so long. - // https://bugs.github.com/juju/juju/+bug/1163983 + // https://bugs.launchpad.net/juju-core/+bug/1163983 c.Fatalf("timed out waiting for agent to terminate") } err = m.Refresh() @@ -538,12 +527,10 @@ // See state server runners start r0 := s.singularRecord.nextRunner(c) - r0.waitForWorker(c, "resumer") r0.waitForWorker(c, "txnpruner") r1 := s.singularRecord.nextRunner(c) - lastWorker := perEnvSingularWorkers[len(perEnvSingularWorkers)-1] - r1.waitForWorker(c, lastWorker) + r1.waitForWorkers(c, perEnvSingularWorkers) // Check that the provisioner and firewaller are alive by doing // a rudimentary check that it responds to state changes. @@ -588,10 +575,65 @@ } } +func (s *MachineSuite) TestManageEnvironRunsResumer(c *gc.C) { + started := make(chan struct{}) + s.AgentSuite.PatchValue(&newResumer, func(st resumer.TransactionResumer) *resumer.Resumer { + close(started) + return resumer.NewResumer(st) + }) + + m, _, _ := s.primeAgent(c, version.Current, state.JobManageEnviron) + a := s.newAgent(c, m) + defer a.Stop() + go func() { + c.Check(a.Run(nil), jc.ErrorIsNil) + }() + + // Wait for the worker that starts before the resumer to start. + _ = s.singularRecord.nextRunner(c) + r := s.singularRecord.nextRunner(c) + r.waitForWorker(c, "charm-revision-updater") + + // Now make sure the resumer starts. + select { + case <-started: + case <-time.After(coretesting.LongWait): + c.Fatalf("resumer worker not started as expected") + } +} + +func (s *MachineSuite) TestManageEnvironStartsInstancePoller(c *gc.C) { + started := make(chan struct{}) + s.AgentSuite.PatchValue(&newInstancePoller, func(st *apiinstancepoller.API) worker.Worker { + close(started) + return instancepoller.NewWorker(st) + }) + + m, _, _ := s.primeAgent(c, version.Current, state.JobManageEnviron) + a := s.newAgent(c, m) + defer a.Stop() + go func() { + c.Check(a.Run(nil), jc.ErrorIsNil) + }() + + // Wait for the worker that starts before the instancepoller to + // start. + _ = s.singularRecord.nextRunner(c) + r := s.singularRecord.nextRunner(c) + r.waitForWorker(c, "charm-revision-updater") + + // Now make sure the resumer starts. + select { + case <-started: + case <-time.After(coretesting.LongWait): + c.Fatalf("instancepoller worker not started as expected") + } +} + const startWorkerWait = 250 * time.Millisecond func (s *MachineSuite) TestManageEnvironDoesNotRunFirewallerWhenModeIsNone(c *gc.C) { - s.PatchValue(&getFirewallMode, func(*api.State) (string, error) { + s.PatchValue(&getFirewallMode, func(api.Connection) (string, error) { return config.FwNone, nil }) started := make(chan struct{}) @@ -625,7 +667,11 @@ usefulVersion := version.Current usefulVersion.Series = "quantal" // to match the charm created below envtesting.AssertUploadFakeToolsVersions( - c, s.DefaultToolsStorage, s.Environ.Config().AgentStream(), s.Environ.Config().AgentStream(), usefulVersion) + c, s.DefaultToolsStorage, + s.Environ.Config().AgentStream(), + s.Environ.Config().AgentStream(), + usefulVersion, + ) m, _, _ := s.primeAgent(c, version.Current, state.JobManageEnviron) a := s.newAgent(c, m) defer a.Stop() @@ -684,8 +730,56 @@ } } +func (s *MachineSuite) testAddresserNewWorkerResult(c *gc.C, expectFinished bool) { + // TODO(dimitern): Fix this in a follow-up. + c.Skip("Test temporarily disabled as flaky - see bug lp:1488576") + + started := make(chan struct{}) + s.PatchValue(&newAddresser, func(api *apiaddresser.API) (worker.Worker, error) { + close(started) + w, err := addresser.NewWorker(api) + c.Check(err, jc.ErrorIsNil) + if expectFinished { + // When the address-allocation feature flag is disabled. + c.Check(w, gc.FitsTypeOf, worker.FinishedWorker{}) + } else { + // When the address-allocation feature flag is enabled. + c.Check(w, gc.Not(gc.FitsTypeOf), worker.FinishedWorker{}) + } + return w, err + }) + + m, _, _ := s.primeAgent(c, version.Current, state.JobManageEnviron) + a := s.newAgent(c, m) + defer a.Stop() + go func() { + c.Check(a.Run(nil), jc.ErrorIsNil) + }() + + // Wait for the worker that starts before the addresser to start. + _ = s.singularRecord.nextRunner(c) + r := s.singularRecord.nextRunner(c) + r.waitForWorker(c, "cleaner") + + select { + case <-started: + case <-time.After(coretesting.LongWait): + c.Fatalf("timed out waiting for addresser to start") + } +} + +func (s *MachineSuite) TestAddresserWorkerDoesNotStopWhenAddressDeallocationSupported(c *gc.C) { + s.SetFeatureFlags(feature.AddressAllocation) + s.testAddresserNewWorkerResult(c, false) +} + +func (s *MachineSuite) TestAddresserWorkerStopsWhenAddressDeallocationNotSupported(c *gc.C) { + s.SetFeatureFlags() + s.testAddresserNewWorkerResult(c, true) +} + func (s *MachineSuite) TestManageEnvironRunsDbLogPrunerIfFeatureFlagEnabled(c *gc.C) { - s.SetFeatureFlags(feature.DbLog) + s.SetFeatureFlags("db-log") m, _, _ := s.primeAgent(c, version.Current, state.JobManageEnviron) a := s.newAgent(c, m) @@ -702,10 +796,10 @@ defer func() { c.Check(a.Stop(), jc.ErrorIsNil) }() go func() { c.Check(a.Run(nil), jc.ErrorIsNil) }() - // Wait for the resumer to be started. This is started just after + // Wait for the txnpruner to be started. This is started just after // dblogpruner would be started. runner := s.singularRecord.nextRunner(c) - started := set.NewStrings(runner.waitForWorker(c, "resumer")...) + started := set.NewStrings(runner.waitForWorker(c, "txnpruner")...) c.Assert(started.Contains("dblogpruner"), jc.IsFalse) } @@ -857,10 +951,10 @@ func (s *MachineSuite) assertJobWithAPI( c *gc.C, job state.MachineJob, - test func(agent.Config, *api.State), + test func(agent.Config, api.Connection), ) { s.assertAgentOpensState(c, &reportOpenedAPI, job, func(cfg agent.Config, st interface{}) { - test(cfg, st.(*api.State)) + test(cfg, st.(api.Connection)) }) } @@ -922,7 +1016,9 @@ func (s *MachineSuite) TestManageEnvironServesAPI(c *gc.C) { s.assertJobWithState(c, state.JobManageEnviron, func(conf agent.Config, agentState *state.State) { - st, err := api.Open(conf.APIInfo(), fastDialOpts) + apiInfo, ok := conf.APIInfo() + c.Assert(ok, jc.IsTrue) + st, err := api.Open(apiInfo, fastDialOpts) c.Assert(err, jc.ErrorIsNil) defer st.Close() m, err := st.Machiner().Machine(conf.Tag().(names.MachineTag)) @@ -1093,25 +1189,99 @@ } } -func (s *MachineSuite) TestOpenStateFailsForJobHostUnitsButOpenAPIWorks(c *gc.C) { - m, _, _ := s.primeAgent(c, version.Current, state.JobHostUnits) - a := s.newAgent(c, m) - s.RunTestOpenAPIState(c, m, a, initialMachinePassword) - s.assertJobWithAPI(c, state.JobHostUnits, func(conf agent.Config, st *api.State) { +func (s *MachineSuite) TestOpenStateFailsForJobHostUnits(c *gc.C) { + s.assertJobWithAPI(c, state.JobHostUnits, func(conf agent.Config, st api.Connection) { + s.AssertCannotOpenState(c, conf.Tag(), conf.DataDir()) + }) +} + +func (s *MachineSuite) TestOpenStateFailsForJobManageNetworking(c *gc.C) { + s.assertJobWithAPI(c, state.JobManageNetworking, func(conf agent.Config, st api.Connection) { s.AssertCannotOpenState(c, conf.Tag(), conf.DataDir()) }) } func (s *MachineSuite) TestOpenStateWorksForJobManageEnviron(c *gc.C) { - s.assertJobWithAPI(c, state.JobManageEnviron, func(conf agent.Config, st *api.State) { + s.assertJobWithAPI(c, state.JobManageEnviron, func(conf agent.Config, st api.Connection) { s.AssertCanOpenState(c, conf.Tag(), conf.DataDir()) }) } +func (s *MachineSuite) TestOpenAPIStateWorksForJobHostUnits(c *gc.C) { + machine, conf, _ := s.primeAgent(c, version.Current, state.JobHostUnits) + s.runOpenAPISTateTest(c, machine, conf) +} + +func (s *MachineSuite) TestOpenAPIStateWorksForJobManageNetworking(c *gc.C) { + machine, conf, _ := s.primeAgent(c, version.Current, state.JobManageNetworking) + s.runOpenAPISTateTest(c, machine, conf) +} + +func (s *MachineSuite) TestOpenAPIStateWorksForJobManageEnviron(c *gc.C) { + machine, conf, _ := s.primeAgent(c, version.Current, state.JobManageEnviron) + s.runOpenAPISTateTest(c, machine, conf) +} + +func (s *MachineSuite) runOpenAPISTateTest(c *gc.C, machine *state.Machine, conf agent.Config) { + configPath := agent.ConfigPath(conf.DataDir(), conf.Tag()) + + // Set a failing password... + confW, err := agent.ReadConfig(configPath) + c.Assert(err, jc.ErrorIsNil) + confW.SetPassword("not-set-on-state-server") + + // ...and also make sure the api info points to the testing api + // server (and not, as for JobManageEnviron machines, to the port + // chosen for the agent's own API server to run on. This is usually + // sane, but inconvenient here because we're not running the full + // agent and so the configured API server is not actually there). + apiInfo := s.APIInfo(c) + hostPorts, err := network.ParseHostPorts(apiInfo.Addrs...) + c.Assert(err, jc.ErrorIsNil) + confW.SetAPIHostPorts([][]network.HostPort{hostPorts}) + err = confW.Write() + c.Assert(err, jc.ErrorIsNil) + + // Check that it successfully connects with the conf's old password. + assertOpen := func() { + tagString := conf.Tag().String() + agent := NewAgentConf(conf.DataDir()) + err := agent.ReadConfig(tagString) + c.Assert(err, jc.ErrorIsNil) + st, gotEntity, err := apicaller.OpenAPIState(agent) + c.Assert(err, jc.ErrorIsNil) + c.Assert(st, gc.NotNil) + st.Close() + c.Assert(gotEntity.Tag(), gc.Equals, tagString) + } + assertOpen() + + // Check that the initial password is no longer valid. + assertPassword := func(password string, valid bool) { + err := machine.Refresh() + c.Assert(err, jc.ErrorIsNil) + c.Check(machine.PasswordValid(password), gc.Equals, valid) + } + assertPassword(initialMachinePassword, false) + + // Read the configuration and check that we can connect with it. + confR, err := agent.ReadConfig(configPath) + c.Assert(err, gc.IsNil) + apiInfo, ok := confR.APIInfo() + c.Assert(ok, jc.IsTrue) + newPassword := apiInfo.Password + assertPassword(newPassword, true) + + // Double-check that we can open a fresh connection with the stored + // conf ... and that the password hasn't been changed again. + assertOpen() + assertPassword(newPassword, true) +} + func (s *MachineSuite) TestMachineAgentSymlinkJujuRun(c *gc.C) { _, err := os.Stat(JujuRun) c.Assert(err, jc.Satisfies, os.IsNotExist) - s.assertJobWithAPI(c, state.JobManageEnviron, func(conf agent.Config, st *api.State) { + s.assertJobWithAPI(c, state.JobManageEnviron, func(conf agent.Config, st api.Connection) { // juju-run should have been created _, err := os.Stat(JujuRun) c.Assert(err, jc.ErrorIsNil) @@ -1128,7 +1298,7 @@ c.Assert(err, jc.ErrorIsNil) _, err = os.Stat(JujuRun) c.Assert(err, jc.Satisfies, os.IsNotExist) - s.assertJobWithAPI(c, state.JobManageEnviron, func(conf agent.Config, st *api.State) { + s.assertJobWithAPI(c, state.JobManageEnviron, func(conf agent.Config, st api.Connection) { // juju-run should have been recreated _, err := os.Stat(JujuRun) c.Assert(err, jc.ErrorIsNil) @@ -1180,7 +1350,7 @@ s.AgentSuite.PatchValue(&proxyupdater.New, mockNew) s.primeAgent(c, version.Current, state.JobHostUnits) - s.assertJobWithAPI(c, state.JobHostUnits, func(conf agent.Config, st *api.State) { + s.assertJobWithAPI(c, state.JobHostUnits, func(conf agent.Config, st api.Connection) { for { select { case <-time.After(coretesting.LongWait): @@ -1222,7 +1392,7 @@ created <- mode return newDummyWorker(), nil }) - s.assertJobWithAPI(c, job, func(conf agent.Config, st *api.State) { + s.assertJobWithAPI(c, job, func(conf agent.Config, st api.Connection) { select { case <-time.After(coretesting.LongWait): c.Fatalf("timeout while waiting for rsyslog worker to be created") @@ -1259,12 +1429,7 @@ } func (s *MachineSuite) TestMachineAgentRunsDiskManagerWorker(c *gc.C) { - // Start the machine agent. - m, _, _ := s.primeAgent(c, version.Current, state.JobHostUnits) - a := s.newAgent(c, m) - go func() { c.Check(a.Run(nil), jc.ErrorIsNil) }() - defer func() { c.Check(a.Stop(), jc.ErrorIsNil) }() - + // Patch out the worker func before starting the agent. started := make(chan struct{}) newWorker := func(diskmanager.ListBlockDevicesFunc, diskmanager.BlockDeviceSetter) worker.Worker { close(started) @@ -1272,6 +1437,12 @@ } s.PatchValue(&newDiskManager, newWorker) + // Start the machine agent. + m, _, _ := s.primeAgent(c, version.Current, 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 worker to be started. select { case <-started: @@ -1307,11 +1478,7 @@ } func (s *MachineSuite) TestMachineAgentRunsMachineStorageWorker(c *gc.C) { - // Start the machine agent. m, _, _ := s.primeAgent(c, version.Current, state.JobHostUnits) - a := s.newAgent(c, m) - go func() { c.Check(a.Run(nil), jc.ErrorIsNil) }() - defer func() { c.Check(a.Stop(), jc.ErrorIsNil) }() started := make(chan struct{}) newWorker := func( @@ -1322,6 +1489,8 @@ _ storageprovisioner.LifecycleManager, _ storageprovisioner.EnvironAccessor, _ storageprovisioner.MachineAccessor, + _ storageprovisioner.StatusSetter, + _ clock.Clock, ) worker.Worker { c.Check(scope, gc.Equals, m.Tag()) // storageDir is not empty for machine scoped storage provisioners @@ -1331,6 +1500,11 @@ } s.PatchValue(&newStorageWorker, newWorker) + // Start the machine agent. + a := s.newAgent(c, m) + go func() { c.Check(a.Run(nil), jc.ErrorIsNil) }() + defer func() { c.Check(a.Stop(), jc.ErrorIsNil) }() + // Wait for worker to be started. select { case <-started: @@ -1340,15 +1514,9 @@ } func (s *MachineSuite) TestMachineAgentRunsEnvironStorageWorker(c *gc.C) { - // Start the machine agent. m, _, _ := s.primeAgent(c, version.Current, state.JobManageEnviron) - a := s.newAgent(c, m) - go func() { c.Check(a.Run(nil), jc.ErrorIsNil) }() - defer func() { c.Check(a.Stop(), jc.ErrorIsNil) }() - machineWorkerStarted := false - environWorkerStarted := false - numWorkers := 0 + var numWorkers, machineWorkers, environWorkers uint32 started := make(chan struct{}) newWorker := func( scope names.Tag, @@ -1358,29 +1526,36 @@ _ storageprovisioner.LifecycleManager, _ storageprovisioner.EnvironAccessor, _ storageprovisioner.MachineAccessor, + _ storageprovisioner.StatusSetter, + _ clock.Clock, ) worker.Worker { // storageDir is empty for environ storage provisioners if storageDir == "" { c.Check(scope, gc.Equals, s.State.EnvironTag()) - environWorkerStarted = true - numWorkers = numWorkers + 1 + c.Check(atomic.AddUint32(&environWorkers, 1), gc.Equals, uint32(1)) + atomic.AddUint32(&numWorkers, 1) } if storageDir != "" { c.Check(scope, gc.Equals, m.Tag()) - machineWorkerStarted = true - numWorkers = numWorkers + 1 + c.Check(atomic.AddUint32(&machineWorkers, 1), gc.Equals, uint32(1)) + atomic.AddUint32(&numWorkers, 1) } - if environWorkerStarted && machineWorkerStarted { + if atomic.LoadUint32(&environWorkers) == 1 && atomic.LoadUint32(&machineWorkers) == 1 { close(started) } return worker.NewNoOpWorker() } s.PatchValue(&newStorageWorker, newWorker) + // Start the machine agent. + a := s.newAgent(c, m) + go func() { c.Check(a.Run(nil), jc.ErrorIsNil) }() + defer func() { c.Check(a.Stop(), jc.ErrorIsNil) }() + // Wait for worker to be started. select { case <-started: - c.Assert(numWorkers, gc.Equals, 2) + c.Assert(atomic.LoadUint32(&numWorkers), gc.Equals, uint32(2)) case <-time.After(coretesting.LongWait): c.Fatalf("timeout while waiting for storage worker to start") } @@ -1587,7 +1762,7 @@ for _, expectedIgnoreValue := range []bool{true, false} { ignoreAddressCh := make(chan bool, 1) s.AgentSuite.PatchValue(&newMachiner, func( - st *apimachiner.State, + accessor machiner.MachineAccessor, conf agent.Config, ignoreMachineAddresses bool, ) worker.Worker { @@ -1595,7 +1770,7 @@ case ignoreAddressCh <- ignoreMachineAddresses: default: } - return machiner.NewMachiner(st, conf, ignoreMachineAddresses) + return machiner.NewMachiner(accessor, conf, ignoreMachineAddresses) }) attrs := coretesting.Attrs{"ignore-machine-addresses": expectedIgnoreValue} @@ -1709,52 +1884,20 @@ c.Assert(err, gc.ErrorMatches, "not in restore mode, cannot begin restoration") c.Assert(a.IsRestoreRunning(), jc.IsFalse) } -func (s *MachineSuite) TestMachineAgentAPIWorkerErrorClosesAPI(c *gc.C) { - // Start the machine agent. - m, _, _ := s.primeAgent(c, version.Current, state.JobHostUnits) - a := s.newAgent(c, m) - a.apiStateUpgrader = &machineAgentUpgrader{} - - closedAPI := make(chan io.Closer, 1) - s.AgentSuite.PatchValue(&reportClosedAPI, func(st io.Closer) { - select { - case closedAPI <- st: - close(closedAPI) - default: - } - }) - - worker, err := a.APIWorker() - - select { - case closed := <-closedAPI: - c.Assert(closed, gc.NotNil) - case <-time.After(coretesting.LongWait): - c.Fatalf("API not opened") - } - - c.Assert(worker, gc.IsNil) - c.Assert(err, gc.ErrorMatches, "cannot set machine agent version: test failure") - c.Assert(a.isAgentUpgradePending(), jc.IsTrue) -} - -type machineAgentUpgrader struct{} - -func (m *machineAgentUpgrader) SetVersion(s string, v version.Binary) error { - return errors.New("test failure") -} func (s *MachineSuite) TestNewEnvironmentStartsNewWorkers(c *gc.C) { - _, expectedWorkers, closer := s.setUpNewEnvironment(c) + _, closer := s.setUpNewEnvironment(c) + defer closer() + expectedWorkers, closer := s.setUpAgent(c) defer closer() r1 := s.singularRecord.nextRunner(c) workers := r1.waitForWorker(c, "firewaller") - c.Assert(workers, jc.DeepEquals, expectedWorkers) + c.Assert(workers, jc.SameContents, expectedWorkers) } func (s *MachineSuite) TestNewStorageWorkerIsScopedToNewEnviron(c *gc.C) { - st, _, closer := s.setUpNewEnvironment(c) + st, closer := s.setUpNewEnvironment(c) defer closer() // Check that newStorageWorker is called and the environ tag is scoped to @@ -1768,16 +1911,24 @@ _ storageprovisioner.LifecycleManager, _ storageprovisioner.EnvironAccessor, _ storageprovisioner.MachineAccessor, + _ storageprovisioner.StatusSetter, + _ clock.Clock, ) worker.Worker { // storageDir is empty for environ storage provisioners if storageDir == "" { - c.Check(scope, gc.Equals, st.EnvironTag()) - close(started) + // If this is the worker for the new environment, + // close the channel. + if scope == st.EnvironTag() { + close(started) + } } return worker.NewNoOpWorker() } s.PatchValue(&newStorageWorker, newWorker) + _, closer = s.setUpAgent(c) + defer closer() + // Wait for newStorageWorker to be started. select { case <-started: @@ -1786,7 +1937,20 @@ } } -func (s *MachineSuite) setUpNewEnvironment(c *gc.C) (newSt *state.State, expectedWorkers []string, closer func()) { +func (s *MachineSuite) setUpNewEnvironment(c *gc.C) (newSt *state.State, closer func()) { + // Create a new environment, tests can now watch if workers start for it. + newSt = s.Factory.MakeEnvironment(c, &factory.EnvParams{ + ConfigAttrs: map[string]interface{}{ + "state-server": false, + }, + Prepare: true, + }) + return newSt, func() { + newSt.Close() + } +} + +func (s *MachineSuite) setUpAgent(c *gc.C) (expectedWorkers []string, closer func()) { expectedWorkers = make([]string, 0, len(perEnvSingularWorkers)+1) for _, w := range perEnvSingularWorkers { expectedWorkers = append(expectedWorkers, w) @@ -1806,17 +1970,9 @@ // firewaller is the last worker started for a new environment. r0 := s.singularRecord.nextRunner(c) workers := r0.waitForWorker(c, "firewaller") - c.Assert(workers, jc.DeepEquals, expectedWorkers) + c.Assert(workers, jc.SameContents, expectedWorkers) - // Create a new environment, tests can now watch if workers start for it. - newSt = s.Factory.MakeEnvironment(c, &factory.EnvParams{ - ConfigAttrs: map[string]interface{}{ - "state-server": false, - }, - Prepare: true, - }) - return newSt, expectedWorkers, func() { - newSt.Close() + return expectedWorkers, func() { c.Check(a.Stop(), jc.ErrorIsNil) } } @@ -2033,6 +2189,37 @@ } } +type machineAgentTerminationSuite struct { + coretesting.BaseSuite +} + +var _ = gc.Suite(&machineAgentTerminationSuite{}) + +func (*machineAgentTerminationSuite) TestStartTerminationWorker(c *gc.C) { + var stub gitjujutesting.Stub + statFile := func(path string) (os.FileInfo, error) { + stub.AddCall("Stat", path) + return nil, stub.NextErr() + } + var errorFunction func() error + newTerminationWorker := func(f func() error) worker.Worker { + errorFunction = f + return nil + } + startTerminationWorker("data-dir", newTerminationWorker, statFile) + c.Assert(errorFunction, gc.NotNil) + + stub.SetErrors(os.ErrNotExist, nil) + errorResult := errorFunction() + c.Assert(errorResult, gc.FitsTypeOf, (*cmdutil.FatalError)(nil)) + c.Assert(errorResult, gc.ErrorMatches, `"[aA]borted" signal received`) + stub.CheckCall(c, 0, "Stat", filepath.Join("data-dir", "uninstall-agent")) + + // No error returned from Stat == uninstall-agent exists. + c.Assert(errorFunction(), gc.Equals, worker.ErrTerminateAgent) + stub.CheckCall(c, 1, "Stat", filepath.Join("data-dir", "uninstall-agent")) +} + type mockAgentConfig struct { agent.Config providerType string @@ -2113,11 +2300,29 @@ } } -func newDummyWorker() worker.Worker { - return worker.NewSimpleWorker(func(stop <-chan struct{}) error { - <-stop - return nil - }) +// waitForWorkers waits for a given worker to be started, returning all +// workers started while waiting. +func (r *fakeSingularRunner) waitForWorkers(c *gc.C, targets []string) []string { + var seen []string + seenTargets := make(map[string]bool) + numSeenTargets := 0 + timeout := time.After(coretesting.LongWait) + for { + select { + case workerName := <-r.startC: + if seenTargets[workerName] == true { + c.Fatal("worker started twice: " + workerName) + } + seenTargets[workerName] = true + numSeenTargets++ + seen = append(seen, workerName) + if numSeenTargets == len(targets) { + return seen + } + case <-timeout: + c.Fatalf("timed out waiting for %v", targets) + } + } } type mockMetricAPI struct { @@ -2189,23 +2394,13 @@ return f.Name() } -type runner interface { - Run(*cmd.Context) error - Stop() error +type mockLoopDeviceManager struct { + detachLoopDevicesArgRootfs string + detachLoopDevicesArgPrefix string } -// runWithTimeout runs an agent and waits -// for it to complete within a reasonable time. -func runWithTimeout(r runner) error { - done := make(chan error) - go func() { - done <- r.Run(nil) - }() - select { - case err := <-done: - return err - case <-time.After(coretesting.LongWait): - } - err := r.Stop() - return fmt.Errorf("timed out waiting for agent to finish; stop error: %v", err) +func (m *mockLoopDeviceManager) DetachLoopDevices(rootfs, prefix string) error { + m.detachLoopDevicesArgRootfs = rootfs + m.detachLoopDevicesArgPrefix = prefix + return nil } === modified file 'src/github.com/juju/juju/cmd/jujud/agent/simplestreams.go' --- src/github.com/juju/juju/cmd/jujud/agent/simplestreams.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/jujud/agent/simplestreams.go 2015-10-23 18:29:32 +0000 @@ -16,6 +16,11 @@ "github.com/juju/juju/state/storage" ) +const ( + storageDataSourceId = "environment storage" + storageDataSourceDescription = storageDataSourceId +) + // environmentStorageDataSource is a simplestreams.DataSource that // retrieves simplestreams metadata from environment storage. type environmentStorageDataSource struct { @@ -30,7 +35,7 @@ // Description is defined in simplestreams.DataSource. func (d environmentStorageDataSource) Description() string { - return "environment storage" + return storageDataSourceDescription } // Fetch is defined in simplestreams.DataSource. @@ -63,7 +68,12 @@ // registerSimplestreamsDataSource registers a environmentStorageDataSource. func registerSimplestreamsDataSource(stor storage.Storage) { ds := NewEnvironmentStorageDataSource(stor) - environs.RegisterUserImageDataSourceFunc(ds.Description(), func(environs.Environ) (simplestreams.DataSource, error) { + environs.RegisterUserImageDataSourceFunc(storageDataSourceId, func(environs.Environ) (simplestreams.DataSource, error) { return ds, nil }) } + +// unregisterSimplestreamsDataSource de-registers an environmentStorageDataSource. +func unregisterSimplestreamsDataSource() { + environs.UnregisterImageDataSourceFunc(storageDataSourceId) +} === added directory 'src/github.com/juju/juju/cmd/jujud/agent/unit' === added file 'src/github.com/juju/juju/cmd/jujud/agent/unit.go' --- src/github.com/juju/juju/cmd/jujud/agent/unit.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/jujud/agent/unit.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,192 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package agent + +import ( + "fmt" + "runtime" + "time" + + "github.com/juju/cmd" + "github.com/juju/loggo" + "github.com/juju/names" + "github.com/juju/utils/featureflag" + "gopkg.in/natefinch/lumberjack.v2" + "launchpad.net/gnuflag" + "launchpad.net/tomb" + + "github.com/juju/juju/agent" + "github.com/juju/juju/cmd/jujud/agent/unit" + cmdutil "github.com/juju/juju/cmd/jujud/util" + "github.com/juju/juju/network" + "github.com/juju/juju/version" + "github.com/juju/juju/worker" + "github.com/juju/juju/worker/dependency" + "github.com/juju/juju/worker/logsender" + "github.com/juju/juju/worker/uniter" +) + +var ( + agentLogger = loggo.GetLogger("juju.jujud") +) + +// UnitAgent is a cmd.Command responsible for running a unit agent. +type UnitAgent struct { + cmd.CommandBase + tomb tomb.Tomb + AgentConf + UnitName string + runner worker.Runner + bufferedLogs logsender.LogRecordCh + setupLogging func(agent.Config) error + logToStdErr bool + ctx *cmd.Context + + // Used to signal that the upgrade worker will not + // reboot the agent on startup because there are no + // longer any immediately pending agent upgrades. + // Channel used as a selectable bool (closed means true). + initialAgentUpgradeCheckComplete chan struct{} +} + +// NewUnitAgent creates a new UnitAgent value properly initialized. +func NewUnitAgent(ctx *cmd.Context, bufferedLogs logsender.LogRecordCh) *UnitAgent { + return &UnitAgent{ + AgentConf: NewAgentConf(""), + ctx: ctx, + initialAgentUpgradeCheckComplete: make(chan struct{}), + bufferedLogs: bufferedLogs, + } +} + +// Info returns usage information for the command. +func (a *UnitAgent) Info() *cmd.Info { + return &cmd.Info{ + Name: "unit", + Purpose: "run a juju unit agent", + } +} + +func (a *UnitAgent) SetFlags(f *gnuflag.FlagSet) { + a.AgentConf.AddFlags(f) + f.StringVar(&a.UnitName, "unit-name", "", "name of the unit to run") + f.BoolVar(&a.logToStdErr, "log-to-stderr", false, "whether to log to standard error instead of log files") +} + +// Init initializes the command for running. +func (a *UnitAgent) Init(args []string) error { + if a.UnitName == "" { + return cmdutil.RequiredError("unit-name") + } + if !names.IsValidUnit(a.UnitName) { + return fmt.Errorf(`--unit-name option expects "/" argument`) + } + if err := a.AgentConf.CheckArgs(args); err != nil { + return err + } + a.runner = worker.NewRunner(cmdutil.IsFatal, cmdutil.MoreImportant) + + if !a.logToStdErr { + if err := a.ReadConfig(a.Tag().String()); err != nil { + return err + } + agentConfig := a.CurrentConfig() + + // the writer in ctx.stderr gets set as the loggo writer in github.com/juju/cmd/logging.go + a.ctx.Stderr = &lumberjack.Logger{ + Filename: agent.LogFilename(agentConfig), + MaxSize: 300, // megabytes + MaxBackups: 2, + } + + } + + return nil +} + +// Stop stops the unit agent. +func (a *UnitAgent) Stop() error { + a.runner.Kill() + return a.tomb.Wait() +} + +// Run runs a unit agent. +func (a *UnitAgent) Run(ctx *cmd.Context) error { + defer a.tomb.Done() + if err := a.ReadConfig(a.Tag().String()); err != nil { + return err + } + agentConfig := a.CurrentConfig() + + agentLogger.Infof("unit agent %v start (%s [%s])", a.Tag().String(), version.Current, runtime.Compiler) + if flags := featureflag.String(); flags != "" { + logger.Warningf("developer feature flags enabled: %s", flags) + } + network.InitializeFromConfig(agentConfig) + + // Sometimes there are upgrade steps that are needed for each unit. + // There are plans afoot to unify the unit and machine agents. When + // this happens, there will be a simple helper function for the upgrade + // steps to run something for each unit on the machine. Until then, we + // need to have the uniter do it, as the overhead of getting a full + // upgrade process in the unit agent out weights the current benefits. + // So.. since the upgrade steps are all idempotent, we will just call + // the upgrade steps when we start the uniter. To be clear, these + // should move back to the upgrade package when we do unify the agents. + runUpgrades(agentConfig.Tag(), agentConfig.DataDir()) + + a.runner.StartWorker("api", a.APIWorkers) + err := cmdutil.AgentDone(logger, a.runner.Wait()) + a.tomb.Kill(err) + return err +} + +// runUpgrades is a temporary fix to deal with upgrade steps that need +// to be run for each unit. This function cannot fail. Errors in the +// upgrade steps are logged, but the uniter will attempt to continue. +// Worst case, we are no worse off than we are today, best case, things +// actually work properly. Only simple upgrade steps that don't use the API +// are available now. If we need really complex steps using the API, there +// should be significant steps to unify the agents first. +func runUpgrades(tag names.Tag, dataDir string) { + unitTag, ok := tag.(names.UnitTag) + if !ok { + logger.Errorf("unit agent tag not a unit tag: %v", tag) + return + } + if err := uniter.AddStoppedFieldToUniterState(unitTag, dataDir); err != nil { + logger.Errorf("Upgrade step failed - add Stopped field to uniter state: %v", err) + } +} + +// APIWorkers returns a dependency.Engine running the unit agent's responsibilities. +func (a *UnitAgent) APIWorkers() (worker.Worker, error) { + manifolds := unit.Manifolds(unit.ManifoldsConfig{ + Agent: agent.APIHostPortsSetter{a}, + LogSource: a.bufferedLogs, + LeadershipGuarantee: 30 * time.Second, + }) + + config := dependency.EngineConfig{ + IsFatal: cmdutil.IsFatal, + MoreImportant: cmdutil.MoreImportantError, + ErrorDelay: 3 * time.Second, + BounceDelay: 10 * time.Millisecond, + } + engine, err := dependency.NewEngine(config) + if err != nil { + return nil, err + } + if err := dependency.Install(engine, manifolds); err != nil { + if err := worker.Stop(engine); err != nil { + logger.Errorf("while stopping engine with bad manifolds: %v", err) + } + return nil, err + } + return engine, nil +} + +func (a *UnitAgent) Tag() names.Tag { + return names.NewUnitTag(a.UnitName) +} === added file 'src/github.com/juju/juju/cmd/jujud/agent/unit/manifolds.go' --- src/github.com/juju/juju/cmd/jujud/agent/unit/manifolds.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/jujud/agent/unit/manifolds.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,169 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package unit + +import ( + "time" + + coreagent "github.com/juju/juju/agent" + "github.com/juju/juju/worker/agent" + "github.com/juju/juju/worker/apiaddressupdater" + "github.com/juju/juju/worker/apicaller" + "github.com/juju/juju/worker/dependency" + "github.com/juju/juju/worker/gate" + "github.com/juju/juju/worker/leadership" + "github.com/juju/juju/worker/logger" + "github.com/juju/juju/worker/logsender" + "github.com/juju/juju/worker/machinelock" + "github.com/juju/juju/worker/proxyupdater" + "github.com/juju/juju/worker/rsyslog" + "github.com/juju/juju/worker/uniter" + "github.com/juju/juju/worker/upgrader" +) + +// ManifoldsConfig allows specialisation of the result of Manifolds. +type ManifoldsConfig struct { + + // Agent contains the agent that will be wrapped and made available to + // its dependencies via a dependency.Engine. + Agent coreagent.Agent + + // LogSource will be read from by the logsender component. + LogSource logsender.LogRecordCh + + // LeadershipGuarantee controls the behaviour of the leadership tracker. + LeadershipGuarantee time.Duration +} + +// Manifolds returns a set of co-configured manifolds covering the various +// responsibilities of a standalone unit agent. It also accepts the logSource +// argument because we haven't figured out how to thread all the logging bits +// through a dependency engine yet. +// +// Thou Shalt Not Use String Literals In This Function. Or Else. +func Manifolds(config ManifoldsConfig) dependency.Manifolds { + return dependency.Manifolds{ + + // The agent manifold references the enclosing agent, and is the + // foundation stone on which most other manifolds ultimately depend. + // (Currently, that is "all manifolds", but consider a shared clock.) + AgentName: agent.Manifold(config.Agent), + + // The machine lock manifold is a thin concurrent wrapper around an + // FSLock in an agreed location. We expect it to be replaced with an + // in-memory lock when the unit agent moves into the machine agent. + MachineLockName: machinelock.Manifold(machinelock.ManifoldConfig{ + AgentName: AgentName, + }), + + // The api caller is a thin concurrent wrapper around a connection + // to some API server. It's used by many other manifolds, which all + // select their own desired facades. It will be interesting to see + // how this works when we consolidate the agents; might be best to + // handle the auth changes server-side..? + APICallerName: apicaller.Manifold(apicaller.ManifoldConfig{ + AgentName: AgentName, + APIInfoGateName: APIInfoGateName, + }), + + // This manifold is used to coordinate between the api caller and the + // log sender, which share the API credentials that the API caller may + // update. To avoid surprising races, the log sender waits for the api + // caller to unblock this, indicating that any password dance has been + // completed and the log-sender can now connect without confusion. + APIInfoGateName: gate.Manifold(), + + // The log sender is a leaf worker that sends log messages to some + // API server, when configured so to do. We should only need one of + // these in a consolidated agent. + LogSenderName: logsender.Manifold(logsender.ManifoldConfig{ + AgentName: AgentName, + APIInfoGateName: APIInfoGateName, + LogSource: config.LogSource, + }), + + // The rsyslog config updater is a leaf worker that causes rsyslog + // to send messages to the state servers. We should only need one + // of these in a consolidated agent. + RsyslogConfigUpdaterName: rsyslog.Manifold(rsyslog.ManifoldConfig{ + AgentName: AgentName, + APICallerName: APICallerName, + }), + + // The logging config updater is a leaf worker that indirectly + // 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: logger.Manifold(logger.ManifoldConfig{ + AgentName: AgentName, + APICallerName: APICallerName, + }), + + // 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. + APIAdddressUpdaterName: apiaddressupdater.Manifold(apiaddressupdater.ManifoldConfig{ + AgentName: AgentName, + APICallerName: APICallerName, + }), + + // The proxy config updater is a leaf worker that sets http/https/apt/etc + // proxy settings. + // TODO(fwereade): timing of this is suspicious. There was superstitious + // 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{ + APICallerName: APICallerName, + }), + + // The upgrader is a leaf worker that returns a specific error type + // recognised by the unit agent, causing other workers to be stopped + // and the agent to be restarted running the new tools. We should only + // need one of these in a consolidated agent, but we'll need to be + // careful about behavioural differences, and interactions with the + // upgrade-steps worker. + UpgraderName: upgrader.Manifold(upgrader.ManifoldConfig{ + AgentName: AgentName, + APICallerName: APICallerName, + }), + + // 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{ + AgentName: AgentName, + APICallerName: APICallerName, + LeadershipGuarantee: config.LeadershipGuarantee, + }), + + // 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{ + AgentName: AgentName, + APICallerName: APICallerName, + LeadershipTrackerName: LeadershipTrackerName, + MachineLockName: MachineLockName, + }), + } +} + +const ( + AgentName = "agent" + APIAdddressUpdaterName = "api-address-updater" + APICallerName = "api-caller" + APIInfoGateName = "api-info-gate" + LeadershipTrackerName = "leadership-tracker" + LoggingConfigUpdaterName = "logging-config-updater" + LogSenderName = "log-sender" + MachineLockName = "machine-lock" + ProxyConfigUpdaterName = "proxy-config-updater" + RsyslogConfigUpdaterName = "rsyslog-config-updater" + UniterName = "uniter" + UpgraderName = "upgrader" +) === added file 'src/github.com/juju/juju/cmd/jujud/agent/unit/manifolds_test.go' --- src/github.com/juju/juju/cmd/jujud/agent/unit/manifolds_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/jujud/agent/unit/manifolds_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,75 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package unit_test + +import ( + gc "gopkg.in/check.v1" + + "github.com/juju/juju/agent" + "github.com/juju/juju/cmd/jujud/agent/unit" + "github.com/juju/juju/testing" +) + +type ManifoldsSuite struct { + testing.BaseSuite +} + +var _ = gc.Suite(&ManifoldsSuite{}) + +func (s *ManifoldsSuite) TestStartFuncs(c *gc.C) { + manifolds := unit.Manifolds(unit.ManifoldsConfig{ + Agent: fakeAgent{}, + }) + + for name, manifold := range manifolds { + c.Logf("checking %q manifold", name) + c.Check(manifold.Start, gc.NotNil) + } +} + +// TODO(cmars) 2015/08/10: rework this into builtin Engine cycle checker. +func (s *ManifoldsSuite) TestAcyclic(c *gc.C) { + manifolds := unit.Manifolds(unit.ManifoldsConfig{ + Agent: fakeAgent{}, + }) + count := len(manifolds) + + // Set of vars for depth-first topological sort of manifolds. (Note that, + // because we've already got outgoing links stored conveniently, we're + // actually checking the transpose of the dependency graph. Cycles will + // still be cycles in either direction, though.) + done := make(map[string]bool) + doing := make(map[string]bool) + sorted := make([]string, 0, count) + + // Stupid _-suffix malarkey allows recursion. Seems cleaner to keep these + // considerations inside this func than to embody the algorithm in a type. + visit := func(node string) {} + visit_ := func(node string) { + if doing[node] { + c.Fatalf("cycle detected at %q (considering: %v)", node, doing) + } + if !done[node] { + doing[node] = true + for _, input := range manifolds[node].Inputs { + visit(input) + } + done[node] = true + doing[node] = false + sorted = append(sorted, node) + } + } + visit = visit_ + + // Actually sort them, or fail if we find a cycle. + for node := range manifolds { + visit(node) + } + c.Logf("got: %v", sorted) + c.Check(sorted, gc.HasLen, count) // Final sanity check. +} + +type fakeAgent struct { + agent.Agent +} === added file 'src/github.com/juju/juju/cmd/jujud/agent/unit/package_test.go' --- src/github.com/juju/juju/cmd/jujud/agent/unit/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/jujud/agent/unit/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,14 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package unit_test + +import ( + "testing" + + gc "gopkg.in/check.v1" +) + +func TestPackage(t *testing.T) { + gc.TestingT(t) +} === added file 'src/github.com/juju/juju/cmd/jujud/agent/unit_test.go' --- src/github.com/juju/juju/cmd/jujud/agent/unit_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/jujud/agent/unit_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,452 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package agent + +import ( + "encoding/json" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "time" + + "github.com/juju/cmd" + "github.com/juju/names" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/natefinch/lumberjack.v2" + + "github.com/juju/juju/agent" + agenttools "github.com/juju/juju/agent/tools" + apirsyslog "github.com/juju/juju/api/rsyslog" + agenttesting "github.com/juju/juju/cmd/jujud/agent/testing" + envtesting "github.com/juju/juju/environs/testing" + jujutesting "github.com/juju/juju/juju/testing" + "github.com/juju/juju/network" + "github.com/juju/juju/state" + coretesting "github.com/juju/juju/testing" + "github.com/juju/juju/tools" + "github.com/juju/juju/version" + "github.com/juju/juju/worker" + "github.com/juju/juju/worker/apicaller" + "github.com/juju/juju/worker/rsyslog" + "github.com/juju/juju/worker/upgrader" +) + +type UnitSuite struct { + coretesting.GitSuite + agenttesting.AgentSuite +} + +var _ = gc.Suite(&UnitSuite{}) + +func (s *UnitSuite) SetUpSuite(c *gc.C) { + s.GitSuite.SetUpSuite(c) + s.AgentSuite.SetUpSuite(c) +} + +func (s *UnitSuite) TearDownSuite(c *gc.C) { + s.AgentSuite.TearDownSuite(c) + s.GitSuite.TearDownSuite(c) +} + +func (s *UnitSuite) SetUpTest(c *gc.C) { + s.GitSuite.SetUpTest(c) + s.AgentSuite.SetUpTest(c) +} + +func (s *UnitSuite) TearDownTest(c *gc.C) { + s.AgentSuite.TearDownTest(c) + s.GitSuite.TearDownTest(c) +} + +const initialUnitPassword = "unit-password-1234567890" + +// 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.AddStateServerMachine(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, id) + err = machine.SetProvisioned(inst.Id(), agent.BootstrapNonce, md) + c.Assert(err, jc.ErrorIsNil) + conf, tools := s.PrimeAgent(c, unit.Tag(), initialUnitPassword, version.Current) + return machine, unit, conf, tools +} + +func (s *UnitSuite) newAgent(c *gc.C, unit *state.Unit) *UnitAgent { + a := NewUnitAgent(nil, nil) + s.InitAgent(c, a, "--unit-name", unit.Name(), "--log-to-stderr=true") + err := a.ReadConfig(unit.Tag().String()) + c.Assert(err, jc.ErrorIsNil) + return a +} + +func (s *UnitSuite) TestParseSuccess(c *gc.C) { + a := NewUnitAgent(nil, nil) + err := coretesting.InitCommand(a, []string{ + "--data-dir", "jd", + "--unit-name", "w0rd-pre55/1", + "--log-to-stderr", + }) + + c.Assert(err, gc.IsNil) + c.Check(a.AgentConf.DataDir(), gc.Equals, "jd") + c.Check(a.UnitName, gc.Equals, "w0rd-pre55/1") +} + +func (s *UnitSuite) TestParseMissing(c *gc.C) { + uc := NewUnitAgent(nil, nil) + err := coretesting.InitCommand(uc, []string{ + "--data-dir", "jc", + }) + + c.Assert(err, gc.ErrorMatches, "--unit-name option must be set") +} + +func (s *UnitSuite) TestParseNonsense(c *gc.C) { + for _, args := range [][]string{ + {"--unit-name", "wordpress"}, + {"--unit-name", "wordpress/seventeen"}, + {"--unit-name", "wordpress/-32"}, + {"--unit-name", "wordpress/wild/9"}, + {"--unit-name", "20/20"}, + } { + err := coretesting.InitCommand(NewUnitAgent(nil, nil), append(args, "--data-dir", "jc")) + c.Check(err, gc.ErrorMatches, `--unit-name option expects "/" argument`) + } +} + +func (s *UnitSuite) TestParseUnknown(c *gc.C) { + err := coretesting.InitCommand(NewUnitAgent(nil, nil), []string{ + "--unit-name", "wordpress/1", + "thundering typhoons", + }) + c.Check(err, gc.ErrorMatches, `unrecognized args: \["thundering typhoons"\]`) +} + +func waitForUnitActive(stateConn *state.State, unit *state.Unit, c *gc.C) { + timeout := time.After(5 * time.Second) + + for { + select { + case <-timeout: + c.Fatalf("no activity detected") + case <-time.After(coretesting.ShortWait): + err := unit.Refresh() + c.Assert(err, jc.ErrorIsNil) + statusInfo, err := unit.Status() + c.Assert(err, jc.ErrorIsNil) + switch statusInfo.Status { + case state.StatusMaintenance, state.StatusWaiting, state.StatusBlocked: + c.Logf("waiting...") + continue + case state.StatusActive: + c.Logf("active!") + return + case state.StatusUnknown: + // Active units may have a status of unknown if they have + // started but not run status-set. + c.Logf("unknown but active!") + return + default: + c.Fatalf("unexpected status %s %s %v", statusInfo.Status, statusInfo.Message, statusInfo.Data) + } + statusInfo, err = unit.AgentStatus() + c.Assert(err, jc.ErrorIsNil) + switch statusInfo.Status { + case state.StatusAllocating, state.StatusExecuting, state.StatusRebooting, state.StatusIdle: + c.Logf("waiting...") + continue + case state.StatusError: + stateConn.StartSync() + c.Logf("unit is still down") + default: + c.Fatalf("unexpected status %s %s %v", statusInfo.Status, statusInfo.Message, statusInfo.Data) + } + } + } +} + +func (s *UnitSuite) TestRunStop(c *gc.C) { + _, unit, _, _ := s.primeAgent(c) + a := s.newAgent(c, unit) + go func() { c.Check(a.Run(nil), gc.IsNil) }() + defer func() { c.Check(a.Stop(), gc.IsNil) }() + waitForUnitActive(s.State, unit, c) +} + +func (s *UnitSuite) TestUpgrade(c *gc.C) { + machine, unit, _, currentTools := s.primeAgent(c) + agent := s.newAgent(c, unit) + newVers := version.Current + newVers.Patch++ + envtesting.AssertUploadFakeToolsVersions( + c, s.DefaultToolsStorage, s.Environ.Config().AgentStream(), s.Environ.Config().AgentStream(), newVers) + + // The machine agent downloads the tools; fake this by + // creating downloaded-tools.txt in data-dir/tools/. + toolsDir := agenttools.SharedToolsDir(s.DataDir(), newVers) + err := os.MkdirAll(toolsDir, 0755) + c.Assert(err, jc.ErrorIsNil) + toolsPath := filepath.Join(toolsDir, "downloaded-tools.txt") + testTools := tools.Tools{Version: newVers, URL: "http://testing.invalid/tools"} + data, err := json.Marshal(testTools) + c.Assert(err, jc.ErrorIsNil) + err = ioutil.WriteFile(toolsPath, data, 0644) + c.Assert(err, jc.ErrorIsNil) + + // Set the machine agent version to trigger an upgrade. + err = machine.SetAgentVersion(newVers) + c.Assert(err, jc.ErrorIsNil) + err = runWithTimeout(agent) + envtesting.CheckUpgraderReadyError(c, err, &upgrader.UpgradeReadyError{ + AgentName: unit.Tag().String(), + OldTools: currentTools.Version, + NewTools: newVers, + DataDir: s.DataDir(), + }) +} + +func (s *UnitSuite) TestUpgradeFailsWithoutTools(c *gc.C) { + machine, unit, _, _ := s.primeAgent(c) + agent := s.newAgent(c, unit) + newVers := version.Current + newVers.Patch++ + err := machine.SetAgentVersion(newVers) + c.Assert(err, jc.ErrorIsNil) + err = runWithTimeout(agent) + c.Assert(err, gc.ErrorMatches, "timed out waiting for agent to finish.*") +} + +func (s *UnitSuite) TestWithDeadUnit(c *gc.C) { + _, unit, _, _ := s.primeAgent(c) + err := unit.EnsureDead() + c.Assert(err, jc.ErrorIsNil) + a := s.newAgent(c, unit) + err = runWithTimeout(a) + c.Assert(err, jc.ErrorIsNil) + + // try again when the unit has been removed. + err = unit.Remove() + c.Assert(err, jc.ErrorIsNil) + a = s.newAgent(c, unit) + err = runWithTimeout(a) + c.Assert(err, jc.ErrorIsNil) +} + +func (s *UnitSuite) TestOpenAPIState(c *gc.C) { + _, unit, conf, _ := s.primeAgent(c) + configPath := agent.ConfigPath(conf.DataDir(), conf.Tag()) + + // Set an invalid password (but the old initial password will still work). + // This test is a sort of unsophisticated simulation of what might happen + // if a previous cycle had picked, and locally recorded, a new password; + // but failed to set it on the state server. Would be better to test that + // code path explicitly in future, but this suffices for now. + confW, err := agent.ReadConfig(configPath) + c.Assert(err, gc.IsNil) + confW.SetPassword("nonsense-borken") + err = confW.Write() + c.Assert(err, jc.ErrorIsNil) + + // Check that it successfully connects (with the conf's old password). + assertOpen := func() { + agent := NewAgentConf(conf.DataDir()) + err := agent.ReadConfig(conf.Tag().String()) + c.Assert(err, jc.ErrorIsNil) + st, gotEntity, err := apicaller.OpenAPIState(agent) + c.Assert(err, jc.ErrorIsNil) + c.Assert(st, gc.NotNil) + st.Close() + c.Assert(gotEntity.Tag(), gc.Equals, unit.Tag().String()) + } + assertOpen() + + // Check that the old password has been invalidated. + assertPassword := func(password string, valid bool) { + err := unit.Refresh() + c.Assert(err, jc.ErrorIsNil) + c.Check(unit.PasswordValid(password), gc.Equals, valid) + } + assertPassword(initialUnitPassword, false) + + // Read the stored password and check it's valid. + confR, err := agent.ReadConfig(configPath) + c.Assert(err, gc.IsNil) + apiInfo, ok := confR.APIInfo() + c.Assert(ok, jc.IsTrue) + newPassword := apiInfo.Password + assertPassword(newPassword, true) + + // Double-check that we can open a fresh connection with the stored + // conf ... and that the password hasn't been changed again. + assertOpen() + assertPassword(newPassword, true) +} + +func (s *UnitSuite) TestOpenAPIStateWithBadCredsTerminates(c *gc.C) { + conf, _ := s.PrimeAgent(c, names.NewUnitTag("missing/0"), "no-password", version.Current) + + _, _, err := apicaller.OpenAPIState(fakeConfAgent{conf: conf}) + c.Assert(err, gc.Equals, worker.ErrTerminateAgent) +} + +func (s *UnitSuite) TestOpenAPIStateWithDeadEntityTerminates(c *gc.C) { + _, unit, conf, _ := s.primeAgent(c) + err := unit.EnsureDead() + c.Assert(err, jc.ErrorIsNil) + + _, _, err = apicaller.OpenAPIState(fakeConfAgent{conf: conf}) + c.Assert(err, gc.Equals, worker.ErrTerminateAgent) +} + +type fakeConfAgent struct { + agent.Agent + conf agent.Config +} + +func (f fakeConfAgent) CurrentConfig() agent.Config { + return f.conf +} + +func (s *UnitSuite) TestOpenStateFails(c *gc.C) { + // Start a unit agent and make sure it doesn't set a mongo password + // we can use to connect to state with. + _, unit, conf, _ := s.primeAgent(c) + a := s.newAgent(c, unit) + go func() { c.Check(a.Run(nil), gc.IsNil) }() + defer func() { c.Check(a.Stop(), gc.IsNil) }() + waitForUnitActive(s.State, unit, c) + + s.AssertCannotOpenState(c, conf.Tag(), conf.DataDir()) +} + +func (s *UnitSuite) TestRsyslogConfigWorker(c *gc.C) { + created := make(chan rsyslog.RsyslogMode, 1) + s.PatchValue(&rsyslog.NewRsyslogConfigWorker, func(_ *apirsyslog.State, mode rsyslog.RsyslogMode, _ names.Tag, _ string, _ []string, _ string) (worker.Worker, error) { + created <- mode + return newDummyWorker(), nil + }) + + _, unit, _, _ := s.primeAgent(c) + a := s.newAgent(c, unit) + go func() { c.Check(a.Run(nil), gc.IsNil) }() + defer func() { c.Check(a.Stop(), gc.IsNil) }() + + select { + case <-time.After(coretesting.LongWait): + c.Fatalf("timeout while waiting for rsyslog worker to be created") + case mode := <-created: + c.Assert(mode, gc.Equals, rsyslog.RsyslogModeForwarding) + } +} + +func (s *UnitSuite) TestAgentSetsToolsVersion(c *gc.C) { + _, unit, _, _ := s.primeAgent(c) + vers := version.Current + vers.Minor = version.Current.Minor + 1 + err := unit.SetAgentVersion(vers) + c.Assert(err, jc.ErrorIsNil) + + a := s.newAgent(c, unit) + go func() { c.Check(a.Run(nil), gc.IsNil) }() + defer func() { c.Check(a.Stop(), gc.IsNil) }() + + timeout := time.After(coretesting.LongWait) + for done := false; !done; { + select { + case <-timeout: + c.Fatalf("timeout while waiting for agent version to be set") + case <-time.After(coretesting.ShortWait): + err := unit.Refresh() + c.Assert(err, jc.ErrorIsNil) + agentTools, err := unit.AgentTools() + c.Assert(err, jc.ErrorIsNil) + if agentTools.Version.Minor != version.Current.Minor { + continue + } + c.Assert(agentTools.Version, gc.DeepEquals, version.Current) + done = true + } + } +} + +func (s *UnitSuite) TestUnitAgentRunsAPIAddressUpdaterWorker(c *gc.C) { + _, unit, _, _ := s.primeAgent(c) + a := s.newAgent(c, unit) + go func() { c.Check(a.Run(nil), gc.IsNil) }() + defer func() { c.Check(a.Stop(), gc.IsNil) }() + + // Update the API addresses. + updatedServers := [][]network.HostPort{ + network.NewHostPorts(1234, "localhost"), + } + err := s.BackingState.SetAPIHostPorts(updatedServers) + c.Assert(err, jc.ErrorIsNil) + + // Wait for config to be updated. + s.BackingState.StartSync() + for attempt := coretesting.LongAttempt.Start(); attempt.Next(); { + addrs, err := a.CurrentConfig().APIAddresses() + c.Assert(err, jc.ErrorIsNil) + if reflect.DeepEqual(addrs, []string{"localhost:1234"}) { + return + } + } + c.Fatalf("timeout while waiting for agent config to change") +} + +func (s *UnitSuite) TestUseLumberjack(c *gc.C) { + ctx, err := cmd.DefaultContext() + c.Assert(err, gc.IsNil) + + a := UnitAgent{ + AgentConf: FakeAgentConfig{}, + ctx: ctx, + UnitName: "mysql/25", + } + + err = a.Init(nil) + c.Assert(err, gc.IsNil) + + l, ok := ctx.Stderr.(*lumberjack.Logger) + c.Assert(ok, jc.IsTrue) + c.Check(l.MaxAge, gc.Equals, 0) + c.Check(l.MaxBackups, gc.Equals, 2) + c.Check(l.Filename, gc.Equals, filepath.FromSlash("/var/log/juju/machine-42.log")) + c.Check(l.MaxSize, gc.Equals, 300) +} + +func (s *UnitSuite) TestDontUseLumberjack(c *gc.C) { + ctx, err := cmd.DefaultContext() + c.Assert(err, gc.IsNil) + + a := UnitAgent{ + AgentConf: FakeAgentConfig{}, + ctx: ctx, + UnitName: "mysql/25", + + // this is what would get set by the CLI flags to tell us not to log to + // the file. + logToStdErr: true, + } + + err = a.Init(nil) + c.Assert(err, gc.IsNil) + + _, ok := ctx.Stderr.(*lumberjack.Logger) + c.Assert(ok, jc.IsFalse) +} === modified file 'src/github.com/juju/juju/cmd/jujud/agent/upgrade.go' --- src/github.com/juju/juju/cmd/jujud/agent/upgrade.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/jujud/agent/upgrade.go 2015-10-23 18:29:32 +0000 @@ -25,7 +25,7 @@ type upgradingMachineAgent interface { ensureMongoServer(agent.Config) error - setMachineStatus(*api.State, params.Status, string) error + setMachineStatus(api.Connection, params.Status, string) error CurrentConfig() agent.Config ChangeConfig(agent.ConfigMutator) error Dying() <-chan struct{} @@ -66,7 +66,7 @@ tag names.MachineTag machineId string isMaster bool - apiState *api.State + apiState api.Connection jobs []multiwatcher.MachineJob agentConfig agent.Config isStateServer bool @@ -97,7 +97,7 @@ func (c *upgradeWorkerContext) Worker( agent upgradingMachineAgent, - apiState *api.State, + apiState api.Connection, jobs []multiwatcher.MachineJob, ) worker.Worker { c.agent = agent @@ -179,6 +179,10 @@ stor := storage.NewStorage(c.st.EnvironUUID(), c.st.MongoSession()) registerSimplestreamsDataSource(stor) + + // This state-dependent data source will be useless + // once state is closed in previous defer - un-register it. + defer unregisterSimplestreamsDataSource() } if err := c.runUpgrades(); err != nil { // Only return an error from the worker if the connection to @@ -277,6 +281,7 @@ func (c *upgradeWorkerContext) waitForOtherStateServers(info *state.UpgradeInfo) error { watcher := info.Watch() + defer watcher.Stop() maxWait := getUpgradeStartTimeout(c.isMaster) timeout := time.After(maxWait) === modified file 'src/github.com/juju/juju/cmd/jujud/agent/upgrade_test.go' --- src/github.com/juju/juju/cmd/jujud/agent/upgrade_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/jujud/agent/upgrade_test.go 2015-10-23 18:29:32 +0000 @@ -62,7 +62,7 @@ const fails = true const succeeds = false -func (s *UpgradeSuite) setAptCmds(cmd *exec.Cmd) []*exec.Cmd { +func (s *UpgradeSuite) setAptCmds(cmd *exec.Cmd) { s.aptMutex.Lock() defer s.aptMutex.Unlock() if cmd == nil { @@ -70,7 +70,6 @@ } else { s.aptCmds = append(s.aptCmds, cmd) } - return s.aptCmds } func (s *UpgradeSuite) getAptCmds() []*exec.Cmd { @@ -82,8 +81,10 @@ func (s *UpgradeSuite) SetUpTest(c *gc.C) { s.commonMachineSuite.SetUpTest(c) + // clear s.aptCmds + s.setAptCmds(nil) + // Capture all apt commands. - s.aptCmds = nil aptCmds := s.AgentSuite.HookCommandOutput(&pacman.CommandOutput, nil, nil) go func() { for cmd := range aptCmds { @@ -901,7 +902,8 @@ } func (s *UpgradeSuite) attemptRestrictedAPIAsUser(c *gc.C, conf agent.Config) error { - info := conf.APIInfo() + info, ok := conf.APIInfo() + c.Assert(ok, jc.IsTrue) info.Tag = s.AdminUserTag(c) info.Password = "dummy-secret" info.Nonce = "" @@ -911,7 +913,7 @@ defer apiState.Close() // this call should always work - var result api.Status + var result params.FullStatus err = apiState.APICall("Client", 0, "", "FullStatus", nil, &result) c.Assert(err, jc.ErrorIsNil) @@ -920,9 +922,12 @@ } func canLoginToAPIAsMachine(c *gc.C, fromConf, toConf agent.Config) bool { - info := fromConf.APIInfo() - info.Addrs = toConf.APIInfo().Addrs - apiState, err := api.Open(info, upgradeTestDialOpts) + fromInfo, ok := fromConf.APIInfo() + c.Assert(ok, jc.IsTrue) + toInfo, ok := toConf.APIInfo() + c.Assert(ok, jc.IsTrue) + fromInfo.Addrs = toInfo.Addrs + apiState, err := api.Open(fromInfo, upgradeTestDialOpts) if apiState != nil { apiState.Close() } @@ -1000,7 +1005,7 @@ Info string } -func (a *fakeUpgradingMachineAgent) setMachineStatus(_ *api.State, status params.Status, info string) error { +func (a *fakeUpgradingMachineAgent) setMachineStatus(_ api.Connection, status params.Status, info string) error { // Record setMachineStatus calls for later inspection. a.MachineStatusCalls = append(a.MachineStatusCalls, MachineStatusCall{status, info}) return nil === modified file 'src/github.com/juju/juju/cmd/jujud/bootstrap_test.go' --- src/github.com/juju/juju/cmd/jujud/bootstrap_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/jujud/bootstrap_test.go 2015-10-23 18:29:32 +0000 @@ -86,6 +86,7 @@ s.BaseSuite.SetUpSuite(c) s.MgoSuite.SetUpSuite(c) + s.PatchValue(&version.Current.Number, testing.FakeVersionNumber) s.makeTestEnv(c) } @@ -667,7 +668,8 @@ addresses, err := inst.Addresses() c.Assert(err, jc.ErrorIsNil) - s.bootstrapName = network.SelectPublicAddress(addresses) + addr, _ := network.SelectPublicAddress(addresses) + s.bootstrapName = addr.Value s.envcfg = env.Config() s.b64yamlEnvcfg = b64yaml(s.envcfg.AllAttrs()).encode() } === modified file 'src/github.com/juju/juju/cmd/jujud/main.go' --- src/github.com/juju/juju/cmd/jujud/main.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/jujud/main.go 2015-10-23 18:29:32 +0000 @@ -17,20 +17,22 @@ "github.com/juju/errors" "github.com/juju/loggo" "github.com/juju/utils/exec" - "github.com/juju/utils/featureflag" jujucmd "github.com/juju/juju/cmd" agentcmd "github.com/juju/juju/cmd/jujud/agent" + components "github.com/juju/juju/component/all" "github.com/juju/juju/juju/names" - "github.com/juju/juju/juju/osenv" "github.com/juju/juju/juju/sockets" + "github.com/juju/juju/storage/looputil" // Import the providers. _ "github.com/juju/juju/provider/all" + "github.com/juju/juju/utils" + "github.com/juju/juju/worker/logsender" "github.com/juju/juju/worker/uniter/runner/jujuc" ) func init() { - featureflag.SetFlagsFromEnvironment(osenv.JujuFeatureFlagEnvKey) + utils.Must(components.RegisterForServer()) } var jujudDoc = ` @@ -122,23 +124,34 @@ // Main registers subcommands for the jujud executable, and hands over control // to the cmd package. func jujuDMain(args []string, ctx *cmd.Context) (code int, err error) { + // Assuming an average of 200 bytes per log message, use up to + // 200MB for the log buffer. + logCh, err := logsender.InstallBufferedLogWriter(1048576) + if err != nil { + return 1, errors.Trace(err) + } + jujud := jujucmd.NewSuperCommand(cmd.SuperCommandParams{ Name: "jujud", Doc: jujudDoc, }) - jujud.Log.Factory = &writerFactory{} + + jujud.Log.NewWriter = func(target io.Writer) loggo.Writer { + return &jujudWriter{target: target} + } + jujud.Register(NewBootstrapCommand()) // TODO(katco-): AgentConf type is doing too much. The - // MachineAgent type has called out the seperate concerns; the - // AgentConf should be split up to follow suite. + // MachineAgent type has called out the separate concerns; the + // AgentConf should be split up to follow suit. agentConf := agentcmd.NewAgentConf("") - machineAgentFactory := agentcmd.MachineAgentFactoryFn(agentConf, agentConf) + machineAgentFactory := agentcmd.MachineAgentFactoryFn( + agentConf, logCh, looputil.NewLoopDeviceManager(), + ) jujud.Register(agentcmd.NewMachineAgentCmd(ctx, machineAgentFactory, agentConf, agentConf)) - a := NewUnitAgent() - a.ctx = ctx - jujud.Register(a) + jujud.Register(agentcmd.NewUnitAgent(ctx, logCh)) code = cmd.Main(jujud, ctx, args[1:]) return code, nil @@ -186,20 +199,12 @@ return code } -type writerFactory struct{} - -func (*writerFactory) NewWriter(target io.Writer) loggo.Writer { - return &jujudWriter{target: target} -} - type jujudWriter struct { target io.Writer unitFormatter simpleFormatter defaultFormatter loggo.DefaultFormatter } -var _ loggo.Writer = (*jujudWriter)(nil) - 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)) === modified file 'src/github.com/juju/juju/cmd/jujud/main_nix.go' --- src/github.com/juju/juju/cmd/jujud/main_nix.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/jujud/main_nix.go 2015-10-23 18:29:32 +0000 @@ -1,14 +1,22 @@ // Copyright 2014 Canonical Ltd. // Copyright 2014 Cloudbase Solutions // Licensed under the AGPLv3, see LICENCE file for details. + // +build !windows package main import ( "os" + + "github.com/juju/juju/juju/osenv" + "github.com/juju/utils/featureflag" ) +func init() { + featureflag.SetFlagsFromEnvironment(osenv.JujuFeatureFlagEnvKey) +} + func main() { MainWrapper(os.Args) } === modified file 'src/github.com/juju/juju/cmd/jujud/main_windows.go' --- src/github.com/juju/juju/cmd/jujud/main_windows.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/jujud/main_windows.go 2015-10-23 18:29:32 +0000 @@ -10,11 +10,17 @@ "path/filepath" "github.com/gabriel-samfira/sys/windows/svc" + "github.com/juju/utils/featureflag" "github.com/juju/juju/cmd/service" "github.com/juju/juju/juju/names" + "github.com/juju/juju/juju/osenv" ) +func init() { + featureflag.SetFlagsFromRegistry(osenv.JujuRegistryKey, osenv.JujuFeatureFlagEnvKey) +} + func main() { isInteractive, err := svc.IsAnInteractiveSession() if err != nil { === modified file 'src/github.com/juju/juju/cmd/jujud/reboot/reboot.go' --- src/github.com/juju/juju/cmd/jujud/reboot/reboot.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/jujud/reboot/reboot.go 2015-10-23 18:29:32 +0000 @@ -43,12 +43,12 @@ // once all containers have shut down, or a timeout is reached type Reboot struct { acfg agent.Config - apistate *api.State + apistate api.Connection tag names.MachineTag st *reboot.State } -func NewRebootWaiter(apistate *api.State, acfg agent.Config) (*Reboot, error) { +func NewRebootWaiter(apistate api.Connection, acfg agent.Config) (*Reboot, error) { rebootState, err := apistate.Reboot() if err != nil { return nil, errors.Trace(err) === modified file 'src/github.com/juju/juju/cmd/jujud/reboot/reboot_test.go' --- src/github.com/juju/juju/cmd/jujud/reboot/reboot_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/jujud/reboot/reboot_test.go 2015-10-23 18:29:32 +0000 @@ -28,7 +28,7 @@ acfg agent.Config mgoInst testing.MgoInstance - st *api.State + st api.Connection tmpDir string rebootScriptName string === modified file 'src/github.com/juju/juju/cmd/jujud/reboot/reboot_windows.go' --- src/github.com/juju/juju/cmd/jujud/reboot/reboot_windows.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/jujud/reboot/reboot_windows.go 2015-10-23 18:29:32 +0000 @@ -15,7 +15,7 @@ if action == params.ShouldDoNothing { return nil } - args := []string{"shutdown.exe"} + args := []string{"shutdown.exe", "-f"} switch action { case params.ShouldReboot: args = append(args, "-r") === modified file 'src/github.com/juju/juju/cmd/jujud/reboot/reboot_windows_test.go' --- src/github.com/juju/juju/cmd/jujud/reboot/reboot_windows_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/jujud/reboot/reboot_windows_test.go 2015-10-23 18:29:32 +0000 @@ -16,6 +16,7 @@ func (s *RebootSuite) rebootCommandParams(c *gc.C) []string { return []string{ + "-f", "-r", "-t", rebootTime, @@ -24,6 +25,7 @@ func (s *RebootSuite) shutdownCommandParams(c *gc.C) []string { return []string{ + "-f", "-s", "-t", rebootTime, === modified file 'src/github.com/juju/juju/cmd/jujud/run_test.go' --- src/github.com/juju/juju/cmd/jujud/run_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/jujud/run_test.go 2015-10-23 18:29:32 +0000 @@ -206,7 +206,7 @@ c.Assert(err, jc.ErrorIsNil) _, err = testing.RunCommand(c, &RunCommand{}, "foo/1", "bar") - c.Assert(err, gc.ErrorMatches, `dial unix .*/run.socket: `+utils.NoSuchFileErrRegexp) + c.Assert(err, gc.ErrorMatches, `dial unix .*/run.socket:.*`+utils.NoSuchFileErrRegexp) } func (s *RunTestSuite) TestRunning(c *gc.C) { === removed file 'src/github.com/juju/juju/cmd/jujud/unit.go' --- src/github.com/juju/juju/cmd/jujud/unit.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/jujud/unit.go 1970-01-01 00:00:00 +0000 @@ -1,236 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "fmt" - "io" - "runtime" - - "github.com/juju/cmd" - "github.com/juju/errors" - "github.com/juju/loggo" - "github.com/juju/names" - "github.com/juju/utils/featureflag" - "gopkg.in/natefinch/lumberjack.v2" - "launchpad.net/gnuflag" - "launchpad.net/tomb" - - "github.com/juju/juju/agent" - "github.com/juju/juju/api" - "github.com/juju/juju/api/leadership" - agentcmd "github.com/juju/juju/cmd/jujud/agent" - cmdutil "github.com/juju/juju/cmd/jujud/util" - "github.com/juju/juju/network" - "github.com/juju/juju/tools" - "github.com/juju/juju/version" - "github.com/juju/juju/worker" - "github.com/juju/juju/worker/apiaddressupdater" - workerlogger "github.com/juju/juju/worker/logger" - "github.com/juju/juju/worker/proxyupdater" - "github.com/juju/juju/worker/rsyslog" - "github.com/juju/juju/worker/uniter" - "github.com/juju/juju/worker/upgrader" -) - -var ( - agentLogger = loggo.GetLogger("juju.jujud") - reportClosedAPI = func(io.Closer) {} -) - -// UnitAgent is a cmd.Command responsible for running a unit agent. -type UnitAgent struct { - cmd.CommandBase - tomb tomb.Tomb - agentcmd.AgentConf - UnitName string - runner worker.Runner - setupLogging func(agent.Config) error - logToStdErr bool - ctx *cmd.Context - apiStateUpgrader agentcmd.APIStateUpgrader - - // Used to signal that the upgrade worker will not - // reboot the agent on startup because there are no - // longer any immediately pending agent upgrades. - // Channel used as a selectable bool (closed means true). - initialAgentUpgradeCheckComplete chan struct{} -} - -// NewUnitAgent creates a new UnitAgent value properly initialized. -func NewUnitAgent() *UnitAgent { - return &UnitAgent{ - AgentConf: agentcmd.NewAgentConf(""), - initialAgentUpgradeCheckComplete: make(chan struct{}), - } -} - -func (a *UnitAgent) getUpgrader(st *api.State) agentcmd.APIStateUpgrader { - if a.apiStateUpgrader != nil { - return a.apiStateUpgrader - } - return st.Upgrader() -} - -// Info returns usage information for the command. -func (a *UnitAgent) Info() *cmd.Info { - return &cmd.Info{ - Name: "unit", - Purpose: "run a juju unit agent", - } -} - -func (a *UnitAgent) SetFlags(f *gnuflag.FlagSet) { - a.AgentConf.AddFlags(f) - f.StringVar(&a.UnitName, "unit-name", "", "name of the unit to run") - f.BoolVar(&a.logToStdErr, "log-to-stderr", false, "whether to log to standard error instead of log files") -} - -// Init initializes the command for running. -func (a *UnitAgent) Init(args []string) error { - if a.UnitName == "" { - return cmdutil.RequiredError("unit-name") - } - if !names.IsValidUnit(a.UnitName) { - return fmt.Errorf(`--unit-name option expects "/" argument`) - } - if err := a.AgentConf.CheckArgs(args); err != nil { - return err - } - a.runner = worker.NewRunner(cmdutil.IsFatal, cmdutil.MoreImportant) - - if !a.logToStdErr { - if err := a.ReadConfig(a.Tag().String()); err != nil { - return err - } - agentConfig := a.CurrentConfig() - - // the writer in ctx.stderr gets set as the loggo writer in github.com/juju/cmd/logging.go - a.ctx.Stderr = &lumberjack.Logger{ - Filename: agent.LogFilename(agentConfig), - MaxSize: 300, // megabytes - MaxBackups: 2, - } - - } - - return nil -} - -// Stop stops the unit agent. -func (a *UnitAgent) Stop() error { - a.runner.Kill() - return a.tomb.Wait() -} - -// Run runs a unit agent. -func (a *UnitAgent) Run(ctx *cmd.Context) error { - defer a.tomb.Done() - if err := a.ReadConfig(a.Tag().String()); err != nil { - return err - } - agentConfig := a.CurrentConfig() - - agentLogger.Infof("unit agent %v start (%s [%s])", a.Tag().String(), version.Current, runtime.Compiler) - if flags := featureflag.String(); flags != "" { - logger.Warningf("developer feature flags enabled: %s", flags) - } - - network.InitializeFromConfig(agentConfig) - a.runner.StartWorker("api", a.APIWorkers) - err := cmdutil.AgentDone(logger, a.runner.Wait()) - a.tomb.Kill(err) - return err -} - -func (a *UnitAgent) APIWorkers() (_ worker.Worker, err error) { - agentConfig := a.CurrentConfig() - dataDir := agentConfig.DataDir() - machineLock, err := cmdutil.HookExecutionLock(dataDir) - if err != nil { - return nil, err - } - st, entity, err := agentcmd.OpenAPIState(agentConfig, a) - if err != nil { - return nil, err - } - unitTag, err := names.ParseUnitTag(entity.Tag()) - if err != nil { - return nil, errors.Trace(err) - } - // Ensure that the environment uuid is stored in the agent config. - // Luckily the API has it recorded for us after we connect. - if agentConfig.Environment().Id() == "" { - err := a.ChangeConfig(func(setter agent.ConfigSetter) error { - environTag, err := st.EnvironTag() - if err != nil { - return errors.Annotate(err, "no environment uuid set on api") - } - - return setter.Migrate(agent.MigrateParams{ - Environment: environTag, - }) - }) - if err != nil { - logger.Warningf("unable to save environment uuid: %v", err) - // Not really fatal, just annoying. - } - } - - defer func() { - if err != nil { - st.Close() - reportClosedAPI(st) - } - }() - - // Before starting any workers, ensure we record the Juju version this unit - // agent is running. - currentTools := &tools.Tools{Version: version.Current} - apiStateUpgrader := a.getUpgrader(st) - if err := apiStateUpgrader.SetVersion(agentConfig.Tag().String(), currentTools.Version); err != nil { - return nil, errors.Annotate(err, "cannot set unit agent version") - } - - runner := worker.NewRunner(cmdutil.ConnectionIsFatal(logger, st), cmdutil.MoreImportant) - // start proxyupdater first to ensure proxy settings are correct - runner.StartWorker("proxyupdater", func() (worker.Worker, error) { - return proxyupdater.New(st.Environment(), false), nil - }) - runner.StartWorker("upgrader", func() (worker.Worker, error) { - return upgrader.NewAgentUpgrader( - st.Upgrader(), - agentConfig, - agentConfig.UpgradedToVersion(), - func() bool { return false }, - a.initialAgentUpgradeCheckComplete, - ), nil - }) - runner.StartWorker("logger", func() (worker.Worker, error) { - return workerlogger.NewLogger(st.Logger(), agentConfig), nil - }) - runner.StartWorker("uniter", func() (worker.Worker, error) { - uniterFacade, err := st.Uniter() - if err != nil { - return nil, errors.Trace(err) - } - return uniter.NewUniter(uniterFacade, unitTag, leadership.NewClient(st), dataDir, machineLock, nil), nil - }) - - runner.StartWorker("apiaddressupdater", func() (worker.Worker, error) { - uniterFacade, err := st.Uniter() - if err != nil { - return nil, errors.Trace(err) - } - return apiaddressupdater.NewAPIAddressUpdater(uniterFacade, a), nil - }) - runner.StartWorker("rsyslog", func() (worker.Worker, error) { - return cmdutil.NewRsyslogConfigWorker(st.Rsyslog(), agentConfig, rsyslog.RsyslogModeForwarding) - }) - return cmdutil.NewCloseWorker(logger, runner, st), nil -} - -func (a *UnitAgent) Tag() names.Tag { - return names.NewUnitTag(a.UnitName) -} === removed file 'src/github.com/juju/juju/cmd/jujud/unit_test.go' --- src/github.com/juju/juju/cmd/jujud/unit_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/jujud/unit_test.go 1970-01-01 00:00:00 +0000 @@ -1,528 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package main - -import ( - "encoding/json" - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "reflect" - "time" - - "github.com/juju/cmd" - "github.com/juju/errors" - "github.com/juju/names" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - "gopkg.in/natefinch/lumberjack.v2" - - "github.com/juju/juju/agent" - agenttools "github.com/juju/juju/agent/tools" - apirsyslog "github.com/juju/juju/api/rsyslog" - agentcmd "github.com/juju/juju/cmd/jujud/agent" - agenttesting "github.com/juju/juju/cmd/jujud/agent/testing" - cmdutil "github.com/juju/juju/cmd/jujud/util" - envtesting "github.com/juju/juju/environs/testing" - jujutesting "github.com/juju/juju/juju/testing" - "github.com/juju/juju/network" - "github.com/juju/juju/state" - coretesting "github.com/juju/juju/testing" - "github.com/juju/juju/tools" - "github.com/juju/juju/version" - "github.com/juju/juju/worker" - "github.com/juju/juju/worker/rsyslog" - "github.com/juju/juju/worker/upgrader" -) - -type UnitSuite struct { - coretesting.GitSuite - agenttesting.AgentSuite -} - -var _ = gc.Suite(&UnitSuite{}) - -func (s *UnitSuite) SetUpSuite(c *gc.C) { - s.GitSuite.SetUpSuite(c) - s.AgentSuite.SetUpSuite(c) -} - -func (s *UnitSuite) TearDownSuite(c *gc.C) { - s.AgentSuite.TearDownSuite(c) - s.GitSuite.TearDownSuite(c) -} - -func (s *UnitSuite) SetUpTest(c *gc.C) { - s.GitSuite.SetUpTest(c) - s.AgentSuite.SetUpTest(c) -} - -func (s *UnitSuite) TearDownTest(c *gc.C) { - s.AgentSuite.TearDownTest(c) - s.GitSuite.TearDownTest(c) -} - -const initialUnitPassword = "unit-password-1234567890" - -// 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.AddStateServerMachine(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, id) - err = machine.SetProvisioned(inst.Id(), agent.BootstrapNonce, md) - c.Assert(err, jc.ErrorIsNil) - conf, tools := s.PrimeAgent(c, unit.Tag(), initialUnitPassword, version.Current) - return machine, unit, conf, tools -} - -func (s *UnitSuite) newAgent(c *gc.C, unit *state.Unit) *UnitAgent { - a := NewUnitAgent() - s.InitAgent(c, a, "--unit-name", unit.Name(), "--log-to-stderr=true") - err := a.ReadConfig(unit.Tag().String()) - c.Assert(err, jc.ErrorIsNil) - return a -} - -func (s *UnitSuite) TestParseSuccess(c *gc.C) { - a := NewUnitAgent() - err := coretesting.InitCommand(a, []string{ - "--data-dir", "jd", - "--unit-name", "w0rd-pre55/1", - "--log-to-stderr", - }) - - c.Assert(err, gc.IsNil) - c.Check(a.AgentConf.DataDir(), gc.Equals, "jd") - c.Check(a.UnitName, gc.Equals, "w0rd-pre55/1") -} - -func (s *UnitSuite) TestParseMissing(c *gc.C) { - uc := NewUnitAgent() - err := coretesting.InitCommand(uc, []string{ - "--data-dir", "jc", - }) - - c.Assert(err, gc.ErrorMatches, "--unit-name option must be set") -} - -func (s *UnitSuite) TestParseNonsense(c *gc.C) { - for _, args := range [][]string{ - {"--unit-name", "wordpress"}, - {"--unit-name", "wordpress/seventeen"}, - {"--unit-name", "wordpress/-32"}, - {"--unit-name", "wordpress/wild/9"}, - {"--unit-name", "20/20"}, - } { - err := coretesting.InitCommand(NewUnitAgent(), append(args, "--data-dir", "jc")) - c.Check(err, gc.ErrorMatches, `--unit-name option expects "/" argument`) - } -} - -func (s *UnitSuite) TestParseUnknown(c *gc.C) { - err := coretesting.InitCommand(NewUnitAgent(), []string{ - "--unit-name", "wordpress/1", - "thundering typhoons", - }) - c.Check(err, gc.ErrorMatches, `unrecognized args: \["thundering typhoons"\]`) -} - -func waitForUnitActive(stateConn *state.State, unit *state.Unit, c *gc.C) { - timeout := time.After(5 * time.Second) - - for { - select { - case <-timeout: - c.Fatalf("no activity detected") - case <-time.After(coretesting.ShortWait): - err := unit.Refresh() - c.Assert(err, jc.ErrorIsNil) - statusInfo, err := unit.Status() - c.Assert(err, jc.ErrorIsNil) - switch statusInfo.Status { - case state.StatusMaintenance, state.StatusWaiting, state.StatusBlocked: - c.Logf("waiting...") - continue - case state.StatusActive: - c.Logf("active!") - return - case state.StatusUnknown: - // Active units may have a status of unknown if they have - // started but not run status-set. - c.Logf("unknown but active!") - return - default: - c.Fatalf("unexpected status %s %s %v", statusInfo.Status, statusInfo.Message, statusInfo.Data) - } - statusInfo, err = unit.AgentStatus() - c.Assert(err, jc.ErrorIsNil) - switch statusInfo.Status { - case state.StatusAllocating, state.StatusExecuting, state.StatusRebooting, state.StatusIdle: - c.Logf("waiting...") - continue - case state.StatusError: - stateConn.StartSync() - c.Logf("unit is still down") - default: - c.Fatalf("unexpected status %s %s %v", statusInfo.Status, statusInfo.Message, statusInfo.Data) - } - } - } -} - -func (s *UnitSuite) TestRunStop(c *gc.C) { - _, unit, _, _ := s.primeAgent(c) - a := s.newAgent(c, unit) - go func() { c.Check(a.Run(nil), gc.IsNil) }() - defer func() { c.Check(a.Stop(), gc.IsNil) }() - waitForUnitActive(s.State, unit, c) -} - -func (s *UnitSuite) TestUpgrade(c *gc.C) { - machine, unit, _, currentTools := s.primeAgent(c) - agent := s.newAgent(c, unit) - newVers := version.Current - newVers.Patch++ - envtesting.AssertUploadFakeToolsVersions( - c, s.DefaultToolsStorage, s.Environ.Config().AgentStream(), s.Environ.Config().AgentStream(), newVers) - - // The machine agent downloads the tools; fake this by - // creating downloaded-tools.txt in data-dir/tools/. - toolsDir := agenttools.SharedToolsDir(s.DataDir(), newVers) - err := os.MkdirAll(toolsDir, 0755) - c.Assert(err, jc.ErrorIsNil) - toolsPath := filepath.Join(toolsDir, "downloaded-tools.txt") - testTools := tools.Tools{Version: newVers, URL: "http://testing.invalid/tools"} - data, err := json.Marshal(testTools) - c.Assert(err, jc.ErrorIsNil) - err = ioutil.WriteFile(toolsPath, data, 0644) - c.Assert(err, jc.ErrorIsNil) - - // Set the machine agent version to trigger an upgrade. - err = machine.SetAgentVersion(newVers) - c.Assert(err, jc.ErrorIsNil) - err = runWithTimeout(agent) - envtesting.CheckUpgraderReadyError(c, err, &upgrader.UpgradeReadyError{ - AgentName: unit.Tag().String(), - OldTools: currentTools.Version, - NewTools: newVers, - DataDir: s.DataDir(), - }) -} - -func (s *UnitSuite) TestUpgradeFailsWithoutTools(c *gc.C) { - machine, unit, _, _ := s.primeAgent(c) - agent := s.newAgent(c, unit) - newVers := version.Current - newVers.Patch++ - err := machine.SetAgentVersion(newVers) - c.Assert(err, jc.ErrorIsNil) - err = runWithTimeout(agent) - c.Assert(err, gc.ErrorMatches, "timed out waiting for agent to finish.*") -} - -func (s *UnitSuite) TestWithDeadUnit(c *gc.C) { - _, unit, _, _ := s.primeAgent(c) - err := unit.EnsureDead() - c.Assert(err, jc.ErrorIsNil) - a := s.newAgent(c, unit) - err = runWithTimeout(a) - c.Assert(err, jc.ErrorIsNil) - - // try again when the unit has been removed. - err = unit.Remove() - c.Assert(err, jc.ErrorIsNil) - a = s.newAgent(c, unit) - err = runWithTimeout(a) - c.Assert(err, jc.ErrorIsNil) -} - -func (s *UnitSuite) TestOpenAPIState(c *gc.C) { - _, unit, _, _ := s.primeAgent(c) - s.RunTestOpenAPIState(c, unit, s.newAgent(c, unit), initialUnitPassword) -} - -func (s *UnitSuite) RunTestOpenAPIState(c *gc.C, ent state.AgentEntity, agentCmd agentcmd.Agent, initialPassword string) { - conf, err := agent.ReadConfig(agent.ConfigPath(s.DataDir(), ent.Tag())) - c.Assert(err, jc.ErrorIsNil) - - conf.SetPassword("") - err = conf.Write() - c.Assert(err, jc.ErrorIsNil) - - // Check that it starts initially and changes the password - assertOpen := func(conf agent.Config) { - st, gotEnt, err := agentcmd.OpenAPIState(conf, agentCmd) - c.Assert(err, jc.ErrorIsNil) - c.Assert(st, gc.NotNil) - st.Close() - c.Assert(gotEnt.Tag(), gc.Equals, ent.Tag().String()) - } - assertOpen(conf) - - // Check that the initial password is no longer valid. - err = ent.Refresh() - c.Assert(err, jc.ErrorIsNil) - c.Assert(ent.PasswordValid(initialPassword), jc.IsFalse) - - // Read the configuration and check that we can connect with it. - conf, err = agent.ReadConfig(agent.ConfigPath(conf.DataDir(), conf.Tag())) - //conf = refreshConfig(c, conf) - c.Assert(err, gc.IsNil) - // Check we can open the API with the new configuration. - assertOpen(conf) -} - -func (s *UnitSuite) TestOpenAPIStateWithBadCredsTerminates(c *gc.C) { - conf, _ := s.PrimeAgent(c, names.NewUnitTag("missing/0"), "no-password", version.Current) - _, _, err := agentcmd.OpenAPIState(conf, nil) - c.Assert(err, gc.Equals, worker.ErrTerminateAgent) -} - -type fakeUnitAgent struct { - unitName string -} - -func (f *fakeUnitAgent) Tag() names.Tag { - return names.NewUnitTag(f.unitName) -} - -func (f *fakeUnitAgent) ChangeConfig(agent.ConfigMutator) error { - panic("fakeUnitAgent.ChangeConfig called unexpectedly") -} - -func (s *UnitSuite) TestOpenAPIStateWithDeadEntityTerminates(c *gc.C) { - _, unit, conf, _ := s.primeAgent(c) - err := unit.EnsureDead() - c.Assert(err, jc.ErrorIsNil) - _, _, err = agentcmd.OpenAPIState(conf, &fakeUnitAgent{"wordpress/0"}) - c.Assert(err, gc.Equals, worker.ErrTerminateAgent) -} - -func (s *UnitSuite) TestOpenStateFails(c *gc.C) { - // Start a unit agent and make sure it doesn't set a mongo password - // we can use to connect to state with. - _, unit, conf, _ := s.primeAgent(c) - a := s.newAgent(c, unit) - go func() { c.Check(a.Run(nil), gc.IsNil) }() - defer func() { c.Check(a.Stop(), gc.IsNil) }() - waitForUnitActive(s.State, unit, c) - - s.AssertCannotOpenState(c, conf.Tag(), conf.DataDir()) -} - -func (s *UnitSuite) TestRsyslogConfigWorker(c *gc.C) { - created := make(chan rsyslog.RsyslogMode, 1) - s.PatchValue(&cmdutil.NewRsyslogConfigWorker, func(_ *apirsyslog.State, _ agent.Config, mode rsyslog.RsyslogMode) (worker.Worker, error) { - created <- mode - return newDummyWorker(), nil - }) - - _, unit, _, _ := s.primeAgent(c) - a := s.newAgent(c, unit) - go func() { c.Check(a.Run(nil), gc.IsNil) }() - defer func() { c.Check(a.Stop(), gc.IsNil) }() - - select { - case <-time.After(coretesting.LongWait): - c.Fatalf("timeout while waiting for rsyslog worker to be created") - case mode := <-created: - c.Assert(mode, gc.Equals, rsyslog.RsyslogModeForwarding) - } -} - -func (s *UnitSuite) TestAgentSetsToolsVersion(c *gc.C) { - _, unit, _, _ := s.primeAgent(c) - vers := version.Current - vers.Minor = version.Current.Minor + 1 - err := unit.SetAgentVersion(vers) - c.Assert(err, jc.ErrorIsNil) - - a := s.newAgent(c, unit) - go func() { c.Check(a.Run(nil), gc.IsNil) }() - defer func() { c.Check(a.Stop(), gc.IsNil) }() - - timeout := time.After(coretesting.LongWait) - for done := false; !done; { - select { - case <-timeout: - c.Fatalf("timeout while waiting for agent version to be set") - case <-time.After(coretesting.ShortWait): - err := unit.Refresh() - c.Assert(err, jc.ErrorIsNil) - agentTools, err := unit.AgentTools() - c.Assert(err, jc.ErrorIsNil) - if agentTools.Version.Minor != version.Current.Minor { - continue - } - c.Assert(agentTools.Version, gc.DeepEquals, version.Current) - done = true - } - } -} - -func (s *UnitSuite) TestUnitAgentRunsAPIAddressUpdaterWorker(c *gc.C) { - _, unit, _, _ := s.primeAgent(c) - a := s.newAgent(c, unit) - go func() { c.Check(a.Run(nil), gc.IsNil) }() - defer func() { c.Check(a.Stop(), gc.IsNil) }() - - // Update the API addresses. - updatedServers := [][]network.HostPort{ - network.NewHostPorts(1234, "localhost"), - } - err := s.BackingState.SetAPIHostPorts(updatedServers) - c.Assert(err, jc.ErrorIsNil) - - // Wait for config to be updated. - s.BackingState.StartSync() - for attempt := coretesting.LongAttempt.Start(); attempt.Next(); { - addrs, err := a.CurrentConfig().APIAddresses() - c.Assert(err, jc.ErrorIsNil) - if reflect.DeepEqual(addrs, []string{"localhost:1234"}) { - return - } - } - c.Fatalf("timeout while waiting for agent config to change") -} - -func (s *UnitSuite) TestUnitAgentAPIWorkerErrorClosesAPI(c *gc.C) { - _, unit, _, _ := s.primeAgent(c) - a := s.newAgent(c, unit) - a.apiStateUpgrader = &unitAgentUpgrader{} - - closedAPI := make(chan io.Closer, 1) - s.AgentSuite.PatchValue(&reportClosedAPI, func(st io.Closer) { - select { - case closedAPI <- st: - close(closedAPI) - default: - } - }) - - worker, err := a.APIWorkers() - - select { - case closed := <-closedAPI: - c.Assert(closed, gc.NotNil) - case <-time.After(coretesting.LongWait): - c.Fatalf("API not opened") - } - - c.Assert(worker, gc.IsNil) - c.Assert(err, gc.ErrorMatches, "cannot set unit agent version: test failure") -} - -type unitAgentUpgrader struct{} - -func (u *unitAgentUpgrader) SetVersion(s string, v version.Binary) error { - return errors.New("test failure") -} - -type runner interface { - Run(*cmd.Context) error - Stop() error -} - -// runWithTimeout runs an agent and waits -// for it to complete within a reasonable time. -func runWithTimeout(r runner) error { - done := make(chan error) - go func() { - done <- r.Run(nil) - }() - select { - case err := <-done: - return err - case <-time.After(coretesting.LongWait): - } - err := r.Stop() - return fmt.Errorf("timed out waiting for agent to finish; stop error: %v", err) -} - -func newDummyWorker() worker.Worker { - return worker.NewSimpleWorker(func(stop <-chan struct{}) error { - <-stop - return nil - }) -} - -type FakeConfig struct { - agent.Config -} - -func (FakeConfig) LogDir() string { - return filepath.FromSlash("/var/log/juju/") -} - -func (FakeConfig) Tag() names.Tag { - return names.NewMachineTag("42") -} - -type FakeAgentConfig struct { - agentcmd.AgentConf -} - -func (FakeAgentConfig) ReadConfig(string) error { return nil } - -func (FakeAgentConfig) CurrentConfig() agent.Config { - return FakeConfig{} -} - -func (FakeAgentConfig) CheckArgs([]string) error { return nil } - -func (s *UnitSuite) TestUseLumberjack(c *gc.C) { - ctx, err := cmd.DefaultContext() - c.Assert(err, gc.IsNil) - - a := UnitAgent{ - AgentConf: FakeAgentConfig{}, - ctx: ctx, - UnitName: "mysql/25", - } - - err = a.Init(nil) - c.Assert(err, gc.IsNil) - - l, ok := ctx.Stderr.(*lumberjack.Logger) - c.Assert(ok, jc.IsTrue) - c.Check(l.MaxAge, gc.Equals, 0) - c.Check(l.MaxBackups, gc.Equals, 2) - c.Check(l.Filename, gc.Equals, filepath.FromSlash("/var/log/juju/machine-42.log")) - c.Check(l.MaxSize, gc.Equals, 300) -} - -func (s *UnitSuite) TestDontUseLumberjack(c *gc.C) { - ctx, err := cmd.DefaultContext() - c.Assert(err, gc.IsNil) - - a := UnitAgent{ - AgentConf: FakeAgentConfig{}, - ctx: ctx, - UnitName: "mysql/25", - - // this is what would get set by the CLI flags to tell us not to log to - // the file. - logToStdErr: true, - } - - err = a.Init(nil) - c.Assert(err, gc.IsNil) - - _, ok := ctx.Stderr.(*lumberjack.Logger) - c.Assert(ok, jc.IsFalse) -} === added file 'src/github.com/juju/juju/cmd/jujud/util/package_test.go' --- src/github.com/juju/juju/cmd/jujud/util/package_test.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/cmd/jujud/util/package_test.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,14 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package util + +import ( + "testing" + + gc "gopkg.in/check.v1" +) + +func Test(t *testing.T) { + gc.TestingT(t) +} === modified file 'src/github.com/juju/juju/cmd/jujud/util/util.go' --- src/github.com/juju/juju/cmd/jujud/util/util.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/jujud/util/util.go 2015-10-23 18:29:32 +0000 @@ -26,6 +26,7 @@ ) var ( + logger = loggo.GetLogger("juju.cmd.jujud.util") DataDir = paths.MustSucceed(paths.DataDir(version.Current.Series)) EnsureMongoServer = mongo.EnsureServer ) @@ -89,6 +90,14 @@ return importance(err0) > importance(err1) } +// MoreImportantError returns the most important error +func MoreImportantError(err0, err1 error) error { + if importance(err0) > importance(err1) { + return err0 + } + return err1 +} + // AgentDone processes the error returned by // an exiting agent. func AgentDone(logger loggo.Logger, err error) error { @@ -237,8 +246,7 @@ if err != nil { return nil, err } - confDir := agent.DefaultConfDir - return rsyslog.NewRsyslogConfigWorker(st, mode, tag, namespace, addrs, confDir) + return rsyslog.NewRsyslogConfigWorker(st, mode, tag, namespace, addrs, agent.DefaultConfDir) } // ParamsStateServingInfoToStateStateServingInfo converts a === modified file 'src/github.com/juju/juju/cmd/jujud/util/util_test.go' --- src/github.com/juju/juju/cmd/jujud/util/util_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/jujud/util/util_test.go 2015-10-23 18:29:32 +0000 @@ -5,10 +5,8 @@ import ( stderrors "errors" - "testing" "github.com/juju/errors" - "github.com/juju/loggo" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" @@ -19,18 +17,13 @@ ) var ( - _ = gc.Suite(&toolSuite{}) - logger = loggo.GetLogger("juju.cmd.jujud") + _ = gc.Suite(&toolSuite{}) ) type toolSuite struct { coretesting.BaseSuite } -func Test(t *testing.T) { - gc.TestingT(t) -} - func (*toolSuite) TestErrorImportance(c *gc.C) { errorImportanceTests := []error{ === modified file 'src/github.com/juju/juju/cmd/plugins/juju-metadata/imagemetadata.go' --- src/github.com/juju/juju/cmd/plugins/juju-metadata/imagemetadata.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/plugins/juju-metadata/imagemetadata.go 2015-10-23 18:29:32 +0000 @@ -28,7 +28,7 @@ } func (c *ImageMetadataCommandBase) prepare(context *cmd.Context, store configstore.Storage) (environs.Environ, error) { - cfg, err := c.Config(store) + cfg, err := c.Config(store, nil) if err != nil { return nil, errors.Annotate(err, "could not get config from store") } === modified file 'src/github.com/juju/juju/cmd/plugins/juju-metadata/metadataplugin_test.go' --- src/github.com/juju/juju/cmd/plugins/juju-metadata/metadataplugin_test.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/cmd/plugins/juju-metadata/metadataplugin_test.go 2015-10-23 18:29:32 +0000 @@ -65,14 +65,12 @@ // Check that we have correctly registered all the sub commands // by checking the help output. out := badrun(c, 0, "--help") - lines := strings.Split(out, "\n") + c.Log(out) var names []string - for _, line := range lines { - f := strings.Fields(line) - if len(f) == 0 || !strings.HasPrefix(line, " ") { - continue - } - names = append(names, f[0]) + commandHelp := strings.SplitAfter(out, "commands:")[1] + commandHelp = strings.TrimSpace(commandHelp) + for _, line := range strings.Split(commandHelp, "\n") { + names = append(names, strings.TrimSpace(strings.Split(line, " - ")[0])) } // The names should be output in alphabetical order, so don't sort. c.Assert(names, gc.DeepEquals, metadataCommandNames) === modified file 'src/github.com/juju/juju/cmd/plugins/juju-metadata/validateimagemetadata_test.go' --- src/github.com/juju/juju/cmd/plugins/juju-metadata/validateimagemetadata_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/plugins/juju-metadata/validateimagemetadata_test.go 2015-10-23 18:29:32 +0000 @@ -109,6 +109,11 @@ coretesting.WriteEnvironments(c, metadataTestEnvConfig) s.PatchEnvironment("AWS_ACCESS_KEY_ID", "access") s.PatchEnvironment("AWS_SECRET_ACCESS_KEY", "secret") + // All of the following are recognized as fallbacks by goamz. + s.PatchEnvironment("AWS_ACCESS_KEY", "") + s.PatchEnvironment("AWS_SECRET_KEY", "") + s.PatchEnvironment("EC2_ACCESS_KEY", "") + s.PatchEnvironment("EC2_SECRET_KEY", "") } func (s *ValidateImageMetadataSuite) setupEc2LocalMetadata(c *gc.C, region, stream string) { === modified file 'src/github.com/juju/juju/cmd/plugins/juju-metadata/validatetoolsmetadata_test.go' --- src/github.com/juju/juju/cmd/plugins/juju-metadata/validatetoolsmetadata_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/plugins/juju-metadata/validatetoolsmetadata_test.go 2015-10-23 18:29:32 +0000 @@ -93,8 +93,14 @@ s.FakeJujuHomeSuite.SetUpTest(c) coretesting.WriteEnvironments(c, metadataTestEnvConfig) s.metadataDir = c.MkDir() + s.PatchEnvironment("AWS_ACCESS_KEY_ID", "access") s.PatchEnvironment("AWS_SECRET_ACCESS_KEY", "secret") + // All of the following are recognized as fallbacks by goamz. + s.PatchEnvironment("AWS_ACCESS_KEY", "") + s.PatchEnvironment("AWS_SECRET_KEY", "") + s.PatchEnvironment("EC2_ACCESS_KEY", "") + s.PatchEnvironment("EC2_SECRET_KEY", "") } func (s *ValidateToolsMetadataSuite) setupEc2LocalMetadata(c *gc.C, region string) { @@ -119,6 +125,8 @@ } func (s *ValidateToolsMetadataSuite) TestEc2LocalMetadataUsingIncompleteEnvironment(c *gc.C) { + // We already unset the other fallbacks recognized by goamz in + // SetUpTest(). s.PatchEnvironment("AWS_ACCESS_KEY_ID", "") s.PatchEnvironment("AWS_SECRET_ACCESS_KEY", "") s.setupEc2LocalMetadata(c, "us-east-1") === modified file 'src/github.com/juju/juju/cmd/plugins/juju-restore/restore.go' --- src/github.com/juju/juju/cmd/plugins/juju-restore/restore.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/cmd/plugins/juju-restore/restore.go 2015-10-23 18:29:32 +0000 @@ -232,7 +232,7 @@ if err != nil { return err } - cfg, err := c.Config(store) + cfg, err := c.Config(store, nil) if err != nil { return err } @@ -241,7 +241,7 @@ return errors.Annotate(err, "cannot re-bootstrap environment") } progress("connecting to newly bootstrapped instance") - var apiState *api.State + var apiState api.Connection // The state server backend may not be ready to accept logins so we retry. // We'll do up to 8 retries over 2 minutes to give the server time to come up. // Typically we expect only 1 retry will be needed. @@ -350,7 +350,7 @@ return env, nil } -func restoreBootstrapMachine(st *api.State, backupFile string, agentConf agentConfig) (addr string, err error) { +func restoreBootstrapMachine(st api.Connection, backupFile string, agentConf agentConfig) (addr string, err error) { client := st.Client() addr, err = client.PublicAddress("0") if err != nil { @@ -550,7 +550,7 @@ // updateAllMachines finds all machines and resets the stored state address // in each of them. The address does not include the port. -func updateAllMachines(apiState *api.State, stateAddr string) ([]restoreResult, error) { +func updateAllMachines(apiState api.Connection, stateAddr string) ([]restoreResult, error) { client := apiState.Client() status, err := client.Status(nil) if err != nil { === added directory 'src/github.com/juju/juju/component' === added directory 'src/github.com/juju/juju/component/all' === added file 'src/github.com/juju/juju/component/all/all.go' --- src/github.com/juju/juju/component/all/all.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/component/all/all.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,67 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +// The all package facilitates the registration of Juju components into +// the relevant machinery. It is intended as the one place in Juju where +// the components (horizontal design layers) and the machinery +// (vertical/architectural layers) intersect. This approach helps +// alleviate interdependence between the components and the machinery. +// +// This is done in an independent package to avoid circular imports. +package all + +import ( + "github.com/juju/errors" + "github.com/juju/utils/set" +) + +type component interface { + registerForServer() error + registerForClient() error +} + +var components = []component{ + &workloads{}, +} + +// RegisterForServer registers all the parts of the components with the +// Juju machinery for use as a server (e.g. jujud, jujuc). +func RegisterForServer() error { + for _, c := range components { + if err := c.registerForServer(); err != nil { + return errors.Trace(err) + } + } + return nil +} + +// RegisterForServer registers all the parts of the components with the +// Juju machinery for use as a client (e.g. juju). +func RegisterForClient() error { + for _, c := range components { + if err := c.registerForClient(); err != nil { + return errors.Trace(err) + } + } + return nil +} + +// registered tracks which parts of each component have been registered. +var registered = map[string]set.Strings{} + +// markRegistered helps components track which things they've +// registered. If the part has already been registered then false is +// returned, indicating that marking failed. This way components can +// ensure a part is registered only once. +func markRegistered(component, part string) bool { + parts, ok := registered[component] + if !ok { + parts = set.NewStrings() + registered[component] = parts + } + if parts.Contains(part) { + return false + } + parts.Add(part) + return true +} === added file 'src/github.com/juju/juju/component/all/workload.go' --- src/github.com/juju/juju/component/all/workload.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/component/all/workload.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,234 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package all + +import ( + "reflect" + + "github.com/juju/cmd" + "github.com/juju/errors" + "github.com/juju/names" + + "github.com/juju/juju/api/base" + "github.com/juju/juju/apiserver/common" + "github.com/juju/juju/cmd/envcmd" + "github.com/juju/juju/cmd/juju/commands" + "github.com/juju/juju/state" + "github.com/juju/juju/worker/uniter/runner" + "github.com/juju/juju/worker/uniter/runner/jujuc" + "github.com/juju/juju/workload" + "github.com/juju/juju/workload/api/client" + "github.com/juju/juju/workload/api/server" + "github.com/juju/juju/workload/context" + "github.com/juju/juju/workload/persistence" + workloadstate "github.com/juju/juju/workload/state" + "github.com/juju/juju/workload/status" +) + +const workloadsHookContextFacade = workload.ComponentName + "-hook-context" + +type workloads struct{} + +func (c workloads) registerForServer() error { + c.registerState() + c.registerPublicFacade() + + c.registerHookContext() + + return nil +} + +func (c workloads) registerForClient() error { + c.registerPublicCommands() + return nil +} + +func (workloads) newPublicFacade(st *state.State, resources *common.Resources, authorizer common.Authorizer) (*server.PublicAPI, error) { + up, err := st.EnvPayloads() + if err != nil { + return nil, errors.Trace(err) + } + return server.NewPublicAPI(up), nil +} + +func (c workloads) registerPublicFacade() { + common.RegisterStandardFacade( + workload.ComponentName, + 0, + c.newPublicFacade, + ) +} + +type facadeCaller struct { + base.FacadeCaller + closeFunc func() error +} + +func (c facadeCaller) Close() error { + return c.closeFunc() +} + +func (workloads) newListAPIClient(cmd *status.ListCommand) (status.ListAPI, error) { + apiCaller, err := cmd.NewAPIRoot() + if err != nil { + return nil, errors.Trace(err) + } + caller := base.NewFacadeCallerForVersion(apiCaller, workload.ComponentName, 0) + + listAPI := client.NewPublicClient(&facadeCaller{ + FacadeCaller: caller, + closeFunc: apiCaller.Close, + }) + return listAPI, nil +} + +func (c workloads) registerPublicCommands() { + if !markRegistered(workload.ComponentName, "public-commands") { + return + } + + commands.RegisterEnvCommand(func() envcmd.EnvironCommand { + return status.NewListCommand(c.newListAPIClient) + }) +} + +func (c workloads) registerHookContext() { + if !markRegistered(workload.ComponentName, "hook-context") { + return + } + + runner.RegisterComponentFunc(workload.ComponentName, + func(config runner.ComponentConfig) (jujuc.ContextComponent, error) { + hctxClient := c.newHookContextAPIClient(config.APICaller) + // TODO(ericsnow) Pass the unit's tag through to the component? + component, err := context.NewContextAPI(hctxClient, config.DataDir) + if err != nil { + return nil, errors.Trace(err) + } + return component, nil + }, + ) + + c.registerHookContextCommands() + c.registerHookContextFacade() +} + +func (workloads) newHookContextAPIClient(caller base.APICaller) context.APIClient { + facadeCaller := base.NewFacadeCallerForVersion(caller, workloadsHookContextFacade, 0) + return client.NewHookContextClient(facadeCaller) +} + +func (workloads) newHookContextFacade(st *state.State, unit *state.Unit) (interface{}, error) { + up, err := st.UnitWorkloads(unit) + if err != nil { + return nil, errors.Trace(err) + } + return server.NewHookContextAPI(up), nil +} + +func (c workloads) registerHookContextFacade() { + common.RegisterHookContextFacade( + workloadsHookContextFacade, + 0, + c.newHookContextFacade, + reflect.TypeOf(&server.HookContextAPI{}), + ) +} + +type workloadsHookContext struct { + jujuc.Context +} + +// Component implements context.HookContext. +func (c workloadsHookContext) Component(name string) (context.Component, error) { + found, err := c.Context.Component(name) + if err != nil { + return nil, errors.Trace(err) + } + compCtx, ok := found.(context.Component) + if !ok && found != nil { + return nil, errors.Errorf("wrong component context type registered: %T", found) + } + return compCtx, nil +} + +func (workloads) registerHookContextCommands() { + if !markRegistered(workload.ComponentName, "hook-context-commands") { + return + } + + jujuc.RegisterCommand(context.RegisterCmdName, func(ctx jujuc.Context) (cmd.Command, error) { + compCtx := workloadsHookContext{ctx} + cmd, err := context.NewRegisterCmd(compCtx) + if err != nil { + return nil, errors.Trace(err) + } + return cmd, nil + }) + + jujuc.RegisterCommand(context.StatusSetCmdName, func(ctx jujuc.Context) (cmd.Command, error) { + compCtx := workloadsHookContext{ctx} + cmd, err := context.NewStatusSetCmd(compCtx) + if err != nil { + return nil, errors.Trace(err) + } + return cmd, nil + }) + + jujuc.RegisterCommand(context.UnregisterCmdName, func(ctx jujuc.Context) (cmd.Command, error) { + compCtx := workloadsHookContext{ctx} + cmd, err := context.NewUnregisterCmd(compCtx) + if err != nil { + return nil, errors.Trace(err) + } + return cmd, nil + }) +} + +func (workloads) registerState() { + // TODO(ericsnow) Use a more general registration mechanism. + //state.RegisterMultiEnvCollections(persistence.Collections...) + + newUnitWorkloads := func(persist state.Persistence, unit names.UnitTag) (state.UnitWorkloads, error) { + return workloadstate.NewUnitWorkloads(persist, unit), nil + } + state.SetWorkloadsComponent(newUnitWorkloads) + + newEnvPayloads := func(persist state.Persistence, listMachines func() ([]string, error), listUnits func(string) ([]names.UnitTag, error)) (state.EnvPayloads, error) { + unitListFuncs := func() ([]func() ([]workload.Info, string, string, error), error) { + machines, err := listMachines() + if err != nil { + return nil, errors.Trace(err) + } + + var funcs []func() ([]workload.Info, string, string, error) + for i := range machines { + machine := machines[i] + units, err := listUnits(machine) + if err != nil { + return nil, errors.Trace(err) + } + + for i := range units { + unit := units[i] + machine := machine + workloadsPersist := persistence.NewPersistence(persist, unit) + funcs = append(funcs, func() ([]workload.Info, string, string, error) { + workloads, err := workloadsPersist.ListAll() + if err != nil { + return nil, "", "", errors.Trace(err) + } + return workloads, machine, unit.String(), nil + }) + } + } + return funcs, nil + } + envPayloads := workloadstate.EnvPayloads{ + UnitListFuncs: unitListFuncs, + } + return envPayloads, nil + } + state.SetPayloadsComponent(newEnvPayloads) +} === modified file 'src/github.com/juju/juju/constraints/constraints.go' --- src/github.com/juju/juju/constraints/constraints.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/constraints/constraints.go 2015-10-23 18:29:32 +0000 @@ -29,6 +29,7 @@ Tags = "tags" InstanceType = "instance-type" Networks = "networks" + Spaces = "spaces" ) // Value describes a user's requirements of the hardware on which units @@ -73,10 +74,19 @@ // be used. Only valid for clouds which support instance types. InstanceType *string `json:"instance-type,omitempty" yaml:"instance-type,omitempty"` - // Networks, if not nil, holds a list of juju network names that - // should be available (or not) on the machine. Positive and - // negative values are accepted, and the difference is the latter - // have a "^" prefix to the name. + // Spaces, if not nil, holds a list of juju network spaces that + // should be available (or not) on the machine. Positive and + // negative values are accepted, and the difference is the latter + // have a "^" prefix to the name. + Spaces *[]string `json:"spaces,omitempty" yaml:"spaces,omitempty"` + + // Networks, if not nil, holds a list of juju networks that + // should be available (or not) on the machine. Positive and + // negative values are accepted, and the difference is the latter + // have a "^" prefix to the name. + // + // TODO(dimitern): Drop this as soon as spaces can be used for + // deployments instead. Networks *[]string `json:"networks,omitempty" yaml:"networks,omitempty"` } @@ -115,35 +125,68 @@ return v.InstanceType != nil && *v.InstanceType != "" } -// extractNetworks returns the list of networks to include or exclude -// (without the "^" prefixes). -func (v *Value) extractNetworks() (include, exclude []string) { - if v.Networks == nil { - return nil, nil - } - for _, name := range *v.Networks { - if strings.HasPrefix(name, "^") { - exclude = append(exclude, strings.TrimPrefix(name, "^")) - } else { - include = append(include, name) +// extractItems returns the list of entries in the given field which +// are either positive (included) or negative (!included; with prefix +// "^"). +func (v *Value) extractItems(field []string, included bool) []string { + var items []string + for _, name := range field { + prefixed := strings.HasPrefix(name, "^") + if prefixed && !included { + // has prefix and we want negatives. + items = append(items, strings.TrimPrefix(name, "^")) + } else if !prefixed && included { + // no prefix and we want positives. + items = append(items, name) } } - return include, exclude -} + return items +} + +// IncludeSpaces returns a list of spaces to include when starting a +// machine, if specified. +func (v *Value) IncludeSpaces() []string { + if v.Spaces == nil { + return nil + } + return v.extractItems(*v.Spaces, true) +} + +// ExcludeSpaces returns a list of spaces to exclude when starting a +// machine, if specified. They are given in the spaces constraint with +// a "^" prefix to the name, which is stripped before returning. +func (v *Value) ExcludeSpaces() []string { + if v.Spaces == nil { + return nil + } + return v.extractItems(*v.Spaces, false) +} + +// HaveSpaces returns whether any spaces constraints were specified. +func (v *Value) HaveSpaces() bool { + return v.Spaces != nil && len(*v.Spaces) > 0 +} + +// TODO(dimitern): Drop the following 3 methods once spaces can be +// used as deployment constraints. // IncludeNetworks returns a list of networks to include when starting // a machine, if specified. func (v *Value) IncludeNetworks() []string { - include, _ := v.extractNetworks() - return include + if v.Networks == nil { + return nil + } + return v.extractItems(*v.Networks, true) } // ExcludeNetworks returns a list of networks to exclude when starting // a machine, if specified. They are given in the networks constraint // with a "^" prefix to the name, which is stripped before returning. func (v *Value) ExcludeNetworks() []string { - _, exclude := v.extractNetworks() - return exclude + if v.Networks == nil { + return nil + } + return v.extractItems(*v.Networks, false) } // HaveNetworks returns whether any network constraints were specified. @@ -187,6 +230,10 @@ s := strings.Join(*v.Tags, ",") strs = append(strs, "tags="+s) } + if v.Spaces != nil { + s := strings.Join(*v.Spaces, ",") + strs = append(strs, "spaces="+s) + } if v.Networks != nil { s := strings.Join(*v.Networks, ",") strs = append(strs, "networks="+s) @@ -194,6 +241,49 @@ return strings.Join(strs, " ") } +// GoString allows printing a constraints.Value nicely with the fmt +// package, especially when nested inside other types. +func (v Value) GoString() string { + var values []string + if v.Arch != nil { + values = append(values, fmt.Sprintf("Arch: %q", *v.Arch)) + } + if v.CpuCores != nil { + values = append(values, fmt.Sprintf("CpuCores: %v", *v.CpuCores)) + } + if v.CpuPower != nil { + values = append(values, fmt.Sprintf("CpuPower: %v", *v.CpuPower)) + } + if v.Mem != nil { + values = append(values, fmt.Sprintf("Mem: %v", *v.Mem)) + } + if v.RootDisk != nil { + values = append(values, fmt.Sprintf("RootDisk: %v", *v.RootDisk)) + } + if v.InstanceType != nil { + values = append(values, fmt.Sprintf("InstanceType: %q", *v.InstanceType)) + } + if v.Container != nil { + values = append(values, fmt.Sprintf("Container: %q", *v.Container)) + } + if v.Tags != nil && *v.Tags != nil { + values = append(values, fmt.Sprintf("Tags: %q", *v.Tags)) + } else if v.Tags != nil { + values = append(values, "Tags: (*[]string)(nil)") + } + if v.Spaces != nil && *v.Spaces != nil { + values = append(values, fmt.Sprintf("Spaces: %q", *v.Spaces)) + } else if v.Spaces != nil { + values = append(values, "Spaces: (*[]string)(nil)") + } + if v.Networks != nil && *v.Networks != nil { + values = append(values, fmt.Sprintf("Networks: %q", *v.Networks)) + } else if v.Networks != nil { + values = append(values, "Networks: (*[]string)(nil)") + } + return fmt.Sprintf("{%s}", strings.Join(values, ", ")) +} + func uintStr(i uint64) string { if i == 0 { return "" @@ -296,7 +386,7 @@ for _, tag := range attrTags { val, ok := result.fieldFromTag(tag) if !ok { - return Value{}, fmt.Errorf("unknown constraint %q", tag) + return Value{}, errors.Errorf("unknown constraint %q", tag) } val.Set(reflect.Zero(val.Type())) } @@ -307,7 +397,7 @@ func (v *Value) setRaw(raw string) error { eq := strings.Index(raw, "=") if eq <= 0 { - return fmt.Errorf("malformed constraint %q", raw) + return errors.Errorf("malformed constraint %q", raw) } name, str := raw[:eq], raw[eq+1:] var err error @@ -328,10 +418,12 @@ err = v.setTags(str) case InstanceType: err = v.setInstanceType(str) + case Spaces: + err = v.setSpaces(str) case Networks: err = v.setNetworks(str) default: - return fmt.Errorf("unknown constraint %q", name) + return errors.Errorf("unknown constraint %q", name) } if err != nil { return errors.Annotatef(err, "bad %q constraint", name) @@ -370,11 +462,25 @@ v.RootDisk, err = parseUint64(vstr) case Tags: v.Tags, err = parseYamlStrings("tags", val) + case Spaces: + var spaces *[]string + spaces, err = parseYamlStrings("spaces", val) + if err != nil { + return false + } + err = v.validateSpaces(spaces) + if err == nil { + v.Spaces = spaces + } case Networks: var networks *[]string networks, err = parseYamlStrings("networks", val) + if err != nil { + return false + } + err = v.validateNetworks(networks) if err == nil { - err = v.validateNetworks(networks) + v.Networks = networks } default: return false @@ -388,7 +494,7 @@ func (v *Value) setContainer(str string) error { if v.Container != nil { - return fmt.Errorf("already set") + return errors.Errorf("already set") } if str == "" { ctype := instance.ContainerType("") @@ -410,10 +516,10 @@ func (v *Value) setArch(str string) error { if v.Arch != nil { - return fmt.Errorf("already set") + return errors.Errorf("already set") } if str != "" && !arch.IsSupportedArch(str) { - return fmt.Errorf("%q not recognized", str) + return errors.Errorf("%q not recognized", str) } v.Arch = &str return nil @@ -421,7 +527,7 @@ func (v *Value) setCpuCores(str string) (err error) { if v.CpuCores != nil { - return fmt.Errorf("already set") + return errors.Errorf("already set") } v.CpuCores, err = parseUint64(str) return @@ -429,7 +535,7 @@ func (v *Value) setCpuPower(str string) (err error) { if v.CpuPower != nil { - return fmt.Errorf("already set") + return errors.Errorf("already set") } v.CpuPower, err = parseUint64(str) return @@ -437,7 +543,7 @@ func (v *Value) setInstanceType(str string) error { if v.InstanceType != nil { - return fmt.Errorf("already set") + return errors.Errorf("already set") } v.InstanceType = &str return nil @@ -445,7 +551,7 @@ func (v *Value) setMem(str string) (err error) { if v.Mem != nil { - return fmt.Errorf("already set") + return errors.Errorf("already set") } v.Mem, err = parseSize(str) return @@ -453,7 +559,7 @@ func (v *Value) setRootDisk(str string) (err error) { if v.RootDisk != nil { - return fmt.Errorf("already set") + return errors.Errorf("already set") } v.RootDisk, err = parseSize(str) return @@ -461,20 +567,46 @@ func (v *Value) setTags(str string) error { if v.Tags != nil { - return fmt.Errorf("already set") + return errors.Errorf("already set") } v.Tags = parseCommaDelimited(str) return nil } +func (v *Value) setSpaces(str string) error { + if v.Spaces != nil { + return errors.Errorf("already set") + } + spaces := parseCommaDelimited(str) + if err := v.validateSpaces(spaces); err != nil { + return err + } + v.Spaces = spaces + return nil +} + +func (v *Value) validateSpaces(spaces *[]string) error { + if spaces == nil { + return nil + } + for _, name := range *spaces { + space := strings.TrimPrefix(name, "^") + if !names.IsValidSpace(space) { + return errors.Errorf("%q is not a valid space name", space) + } + } + return nil +} + func (v *Value) setNetworks(str string) error { if v.Networks != nil { - return fmt.Errorf("already set") + return errors.Errorf("already set") } networks := parseCommaDelimited(str) if err := v.validateNetworks(networks); err != nil { return err } + v.Networks = networks return nil } @@ -482,15 +614,12 @@ if networks == nil { return nil } - for _, netName := range *networks { - if strings.HasPrefix(netName, "^") { - netName = strings.TrimPrefix(netName, "^") - } + for _, name := range *networks { + netName := strings.TrimPrefix(name, "^") if !names.IsValidNetwork(netName) { - return fmt.Errorf("%q is not a valid network name", netName) + return errors.Errorf("%q is not a valid network name", netName) } } - v.Networks = networks return nil } @@ -498,7 +627,7 @@ var value uint64 if str != "" { if val, err := strconv.ParseUint(str, 10, 64); err != nil { - return nil, fmt.Errorf("must be a non-negative integer") + return nil, errors.Errorf("must be a non-negative integer") } else { value = uint64(val) } @@ -516,7 +645,7 @@ } val, err := strconv.ParseFloat(str, 64) if err != nil || val < 0 { - return nil, fmt.Errorf("must be a non-negative float with optional M/G/T/P suffix") + return nil, errors.Errorf("must be a non-negative float with optional M/G/T/P suffix") } val *= mult value = uint64(math.Ceil(val)) @@ -525,7 +654,7 @@ } // parseCommaDelimited returns the items in the value s. We expect the -// tags to be comma delimited strings. It is used for tags and networks. +// items to be comma delimited strings. func parseCommaDelimited(s string) *[]string { if s == "" { return &[]string{} @@ -537,13 +666,13 @@ func parseYamlStrings(entityName string, val interface{}) (*[]string, error) { ifcs, ok := val.([]interface{}) if !ok { - return nil, fmt.Errorf("unexpected type passed to %s: %T", entityName, val) + return nil, errors.Errorf("unexpected type passed to %s: %T", entityName, val) } items := make([]string, len(ifcs)) for n, ifc := range ifcs { s, ok := ifc.(string) if !ok { - return nil, fmt.Errorf("unexpected type passed as in %s: %T", entityName, ifc) + return nil, errors.Errorf("unexpected type passed as in %s: %T", entityName, ifc) } items[n] = s } === modified file 'src/github.com/juju/juju/constraints/constraints_test.go' --- src/github.com/juju/juju/constraints/constraints_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/constraints/constraints_test.go 2015-10-23 18:29:32 +0000 @@ -265,6 +265,24 @@ args: []string{"tags="}, }, + // spaces + { + summary: "single space", + args: []string{"spaces=space1"}, + }, { + summary: "multiple spaces - positive", + args: []string{"spaces=space1,space2"}, + }, { + summary: "multiple spaces - negative", + args: []string{"spaces=^dmz,^public"}, + }, { + summary: "multiple spaces - positive and negative", + args: []string{"spaces=admin,^area52,dmz,^public"}, + }, { + summary: "no spaces", + args: []string{"spaces="}, + }, + // networks { summary: "single network", @@ -273,6 +291,9 @@ summary: "multiple networks - positive", args: []string{"networks=net1,net2"}, }, { + summary: "multiple networks - negative", + args: []string{"networks=^net1,^net2"}, + }, { summary: "multiple networks - positive and negative", args: []string{"networks=net1,^net2,net3,^net4"}, }, { @@ -294,16 +315,20 @@ summary: "kitchen sink together", args: []string{ "root-disk=8G mem=2T arch=i386 cpu-cores=4096 cpu-power=9001 container=lxc " + - "tags=foo,bar networks=net1,^net2 instance-type=foo"}, + "tags=foo,bar spaces=space1,^space2 networks=net,^net2 instance-type=foo"}, }, { summary: "kitchen sink separately", args: []string{ "root-disk=8G", "mem=2T", "cpu-cores=4096", "cpu-power=9001", "arch=armhf", - "container=lxc", "tags=foo,bar", "networks=net1,^net2", "instance-type=foo"}, + "container=lxc", "tags=foo,bar", "spaces=space1,^space2", "networks=net1,^net2", + "instance-type=foo"}, }, } func (s *ConstraintsSuite) TestParseConstraints(c *gc.C) { + // TODO(dimitern): This test is inadequate and needs to check for + // more than just the reparsed output of String() matches the + // expected. for i, t := range parseConstraintsTests { c.Logf("test %d: %s", i, t.summary) cons0, err := constraints.Parse(t.args...) @@ -322,7 +347,9 @@ func (s *ConstraintsSuite) TestMerge(c *gc.C) { con1 := constraints.MustParse("arch=amd64 mem=4G") con2 := constraints.MustParse("cpu-cores=42") - con3 := constraints.MustParse("root-disk=8G container=lxc networks=net1,^net2") + con3 := constraints.MustParse( + "root-disk=8G container=lxc spaces=space1,^space2 networks=net1,^net2", + ) merged, err := constraints.Merge(con1, con2) c.Assert(err, jc.ErrorIsNil) c.Assert(merged, jc.DeepEquals, constraints.MustParse("arch=amd64 mem=4G cpu-cores=42")) @@ -331,7 +358,10 @@ c.Assert(merged, jc.DeepEquals, con1) merged, err = constraints.Merge(con1, con2, con3) c.Assert(err, jc.ErrorIsNil) - c.Assert(merged, jc.DeepEquals, constraints.MustParse("arch=amd64 mem=4G cpu-cores=42 root-disk=8G container=lxc networks=net1,^net2")) + c.Assert(merged, jc.DeepEquals, constraints. + MustParse( + "arch=amd64 mem=4G cpu-cores=42 root-disk=8G container=lxc spaces=space1,^space2 networks=net1,^net2"), + ) merged, err = constraints.Merge() c.Assert(err, jc.ErrorIsNil) c.Assert(merged, jc.DeepEquals, constraints.Value{}) @@ -346,20 +376,41 @@ c.Assert(merged, jc.DeepEquals, constraints.Value{}) } -func (s *ConstraintsSuite) TestParseMissingTagsAndNetworks(c *gc.C) { +func (s *ConstraintsSuite) TestParseMissingTagsSpacesAndNetworks(c *gc.C) { con := constraints.MustParse("arch=amd64 mem=4G cpu-cores=1 root-disk=8G") c.Check(con.Tags, gc.IsNil) + c.Check(con.Spaces, gc.IsNil) c.Check(con.Networks, gc.IsNil) } -func (s *ConstraintsSuite) TestParseNoTagsNoNetworks(c *gc.C) { - con := constraints.MustParse("arch=amd64 mem=4G cpu-cores=1 root-disk=8G tags= networks=") +func (s *ConstraintsSuite) TestParseNoTagsNoSpacesNoNetworks(c *gc.C) { + con := constraints.MustParse( + "arch=amd64 mem=4G cpu-cores=1 root-disk=8G tags= spaces= networks=", + ) c.Assert(con.Tags, gc.Not(gc.IsNil)) - c.Assert(con.Networks, gc.Not(gc.IsNil)) + c.Assert(con.Spaces, gc.Not(gc.IsNil)) c.Check(*con.Tags, gc.HasLen, 0) + c.Check(*con.Spaces, gc.HasLen, 0) c.Check(*con.Networks, gc.HasLen, 0) } +func (s *ConstraintsSuite) TestIncludeExcludeAndHaveSpaces(c *gc.C) { + con := constraints.MustParse("spaces=space1,^space2,space3,^space4") + c.Assert(con.Spaces, gc.Not(gc.IsNil)) + c.Check(*con.Spaces, gc.HasLen, 4) + c.Check(con.IncludeSpaces(), jc.SameContents, []string{"space1", "space3"}) + c.Check(con.ExcludeSpaces(), jc.SameContents, []string{"space2", "space4"}) + c.Check(con.HaveSpaces(), jc.IsTrue) + c.Check(con.HaveNetworks(), jc.IsFalse) + con = constraints.MustParse("mem=4G") + c.Check(con.HaveSpaces(), jc.IsFalse) + con = constraints.MustParse("mem=4G spaces=space-foo,^space-bar") + c.Check(con.IncludeSpaces(), jc.SameContents, []string{"space-foo"}) + c.Check(con.ExcludeSpaces(), jc.SameContents, []string{"space-bar"}) + c.Check(con.HaveSpaces(), jc.IsTrue) + c.Check(con.HaveNetworks(), jc.IsFalse) +} + func (s *ConstraintsSuite) TestIncludeExcludeAndHaveNetworks(c *gc.C) { con := constraints.MustParse("networks=net1,^net2,net3,^net4") c.Assert(con.Networks, gc.Not(gc.IsNil)) @@ -367,10 +418,30 @@ c.Check(con.IncludeNetworks(), jc.SameContents, []string{"net1", "net3"}) c.Check(con.ExcludeNetworks(), jc.SameContents, []string{"net2", "net4"}) c.Check(con.HaveNetworks(), jc.IsTrue) + c.Check(con.HaveSpaces(), jc.IsFalse) con = constraints.MustParse("mem=4G") c.Check(con.HaveNetworks(), jc.IsFalse) - con = constraints.MustParse("mem=4G networks=^net1,^net2") + con = constraints.MustParse("mem=4G networks=net-foo,^net-bar") + c.Check(con.IncludeNetworks(), jc.SameContents, []string{"net-foo"}) + c.Check(con.ExcludeNetworks(), jc.SameContents, []string{"net-bar"}) c.Check(con.HaveNetworks(), jc.IsTrue) + c.Check(con.HaveSpaces(), jc.IsFalse) +} + +func (s *ConstraintsSuite) TestInvalidSpaces(c *gc.C) { + invalidNames := []string{ + "%$pace", "^foo#2", "+", "tcp:ip", + "^^myspace", "^^^^^^^^", "space^x", + "&-foo", "space/3", "^bar=4", "&#!", + } + for _, name := range invalidNames { + con, err := constraints.Parse("spaces=" + name) + expectName := strings.TrimPrefix(name, "^") + expectErr := fmt.Sprintf(`bad "spaces" constraint: %q is not a valid space name`, expectName) + c.Check(err, gc.NotNil) + c.Check(err.Error(), gc.Equals, expectErr) + c.Check(con, jc.DeepEquals, constraints.Value{}) + } } func (s *ConstraintsSuite) TestInvalidNetworks(c *gc.C) { @@ -398,6 +469,8 @@ c.Check(&con, jc.Satisfies, constraints.IsEmpty) con = constraints.MustParse("tags=") c.Check(&con, gc.Not(jc.Satisfies), constraints.IsEmpty) + con = constraints.MustParse("spaces=") + c.Check(&con, gc.Not(jc.Satisfies), constraints.IsEmpty) con = constraints.MustParse("networks=") c.Check(&con, gc.Not(jc.Satisfies), constraints.IsEmpty) con = constraints.MustParse("mem=") @@ -456,6 +529,9 @@ {"Tags1", constraints.Value{Tags: nil}}, {"Tags2", constraints.Value{Tags: &[]string{}}}, {"Tags3", constraints.Value{Tags: &[]string{"foo", "bar"}}}, + {"Spaces1", constraints.Value{Spaces: nil}}, + {"Spaces2", constraints.Value{Spaces: &[]string{}}}, + {"Spaces3", constraints.Value{Spaces: &[]string{"space1", "^space2"}}}, {"Networks1", constraints.Value{Networks: nil}}, {"Networks2", constraints.Value{Networks: &[]string{}}}, {"Networks3", constraints.Value{Networks: &[]string{"net1", "^net2"}}}, @@ -469,6 +545,7 @@ Mem: uint64p(18000000000), RootDisk: uint64p(24000000000), Tags: &[]string{"foo", "bar"}, + Spaces: &[]string{"space1", "^space2"}, Networks: &[]string{"net1", "^net2"}, InstanceType: strp("foo"), }}, @@ -548,7 +625,7 @@ c.Check(cons.HasInstanceType(), jc.IsTrue) } -const initialWithoutCons = "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 networks=net1,^net2 tags=foo container=lxc instance-type=bar" +const initialWithoutCons = "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 spaces=space1,^space2 networks=net1,^net2 tags=foo container=lxc instance-type=bar" var withoutTests = []struct { initial string @@ -557,43 +634,47 @@ }{{ initial: initialWithoutCons, without: []string{"root-disk"}, - final: "mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 tags=foo networks=net1,^net2 container=lxc instance-type=bar", + final: "mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 tags=foo spaces=space1,^space2 networks=net1,^net2 container=lxc instance-type=bar", }, { initial: initialWithoutCons, without: []string{"mem"}, - final: "root-disk=8G arch=amd64 cpu-power=1000 cpu-cores=4 tags=foo networks=net1,^net2 container=lxc instance-type=bar", + final: "root-disk=8G arch=amd64 cpu-power=1000 cpu-cores=4 tags=foo spaces=space1,^space2 networks=net1,^net2 container=lxc instance-type=bar", }, { initial: initialWithoutCons, without: []string{"arch"}, - final: "root-disk=8G mem=4G cpu-power=1000 cpu-cores=4 tags=foo networks=net1,^net2 container=lxc instance-type=bar", + final: "root-disk=8G mem=4G cpu-power=1000 cpu-cores=4 tags=foo spaces=space1,^space2 networks=net1,^net2 container=lxc instance-type=bar", }, { initial: initialWithoutCons, without: []string{"cpu-power"}, - final: "root-disk=8G mem=4G arch=amd64 cpu-cores=4 tags=foo networks=net1,^net2 container=lxc instance-type=bar", + final: "root-disk=8G mem=4G arch=amd64 cpu-cores=4 tags=foo spaces=space1,^space2 networks=net1,^net2 container=lxc instance-type=bar", }, { initial: initialWithoutCons, without: []string{"cpu-cores"}, - final: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 tags=foo networks=net1,^net2 container=lxc instance-type=bar", + final: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 tags=foo spaces=space1,^space2 networks=net1,^net2 container=lxc instance-type=bar", }, { initial: initialWithoutCons, without: []string{"tags"}, - final: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 networks=net1,^net2 container=lxc instance-type=bar", + final: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 spaces=space1,^space2 networks=net1,^net2 container=lxc instance-type=bar", +}, { + initial: initialWithoutCons, + without: []string{"spaces"}, + final: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 tags=foo networks=net1,^net2 container=lxc instance-type=bar", }, { initial: initialWithoutCons, without: []string{"networks"}, - final: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 tags=foo container=lxc instance-type=bar", + final: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 tags=foo spaces=space1,^space2 container=lxc instance-type=bar", }, { initial: initialWithoutCons, without: []string{"container"}, - final: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 tags=foo networks=net1,^net2 instance-type=bar", + final: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 tags=foo spaces=space1,^space2 networks=net1,^net2 instance-type=bar", }, { initial: initialWithoutCons, without: []string{"instance-type"}, - final: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 tags=foo networks=net1,^net2 container=lxc", + final: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 tags=foo spaces=space1,^space2 container=lxc networks=net1,^net2", }, { initial: initialWithoutCons, without: []string{"root-disk", "mem", "arch"}, - final: "cpu-power=1000 cpu-cores=4 tags=foo networks=net1,^net2 container=lxc instance-type=bar", + final: "cpu-power=1000 cpu-cores=4 tags=foo spaces=space1,^space2 networks=net1,^net2 container=lxc instance-type=bar", }} func (s *ConstraintsSuite) TestWithout(c *gc.C) { @@ -615,7 +696,7 @@ func (s *ConstraintsSuite) TestAttributesWithValues(c *gc.C) { for i, consStr := range []string{ "", - "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 instance-type=foo tags=foo,bar networks=net1,^net2", + "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4 instance-type=foo tags=foo,bar spaces=space1,^space2", } { c.Logf("test %d", i) cons := constraints.MustParse(consStr) @@ -654,6 +735,11 @@ } else { assertMissing("tags") } + if cons.Spaces != nil { + c.Check(obtained["spaces"], gc.DeepEquals, *cons.Spaces) + } else { + assertMissing("spaces") + } if cons.Networks != nil { c.Check(obtained["networks"], gc.DeepEquals, *cons.Networks) } else { @@ -673,13 +759,18 @@ expected []string }{ { + cons: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 spaces=space1,^space2 cpu-cores=4", + attrs: []string{"root-disk", "tags", "mem", "spaces"}, + expected: []string{"root-disk", "mem", "spaces"}, + }, + { cons: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 networks=net1,^net2 cpu-cores=4", attrs: []string{"root-disk", "tags", "mem", "networks"}, expected: []string{"root-disk", "mem", "networks"}, }, { cons: "root-disk=8G mem=4G arch=amd64 cpu-power=1000 cpu-cores=4", - attrs: []string{"tags", "networks"}, + attrs: []string{"tags", "spaces", "networks"}, expected: []string{}, }, } === modified file 'src/github.com/juju/juju/container/factory/factory.go' --- src/github.com/juju/juju/container/factory/factory.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/container/factory/factory.go 2015-10-23 18:29:32 +0000 @@ -12,6 +12,7 @@ "github.com/juju/juju/container/kvm" "github.com/juju/juju/container/lxc" "github.com/juju/juju/instance" + "github.com/juju/juju/storage/looputil" ) // NewContainerManager creates the appropriate container.Manager for the @@ -20,7 +21,7 @@ ) (container.Manager, error) { switch forType { case instance.LXC: - return lxc.NewContainerManager(conf, imageURLGetter) + return lxc.NewContainerManager(conf, imageURLGetter, looputil.NewLoopDeviceManager()) case instance.KVM: return kvm.NewContainerManager(conf) } === modified file 'src/github.com/juju/juju/container/image.go' --- src/github.com/juju/juju/container/image.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/container/image.go 2015-10-23 18:29:32 +0000 @@ -5,6 +5,7 @@ import ( "fmt" + "os" "os/exec" "path" "strings" @@ -26,44 +27,46 @@ CACert() []byte } +type ImageURLGetterConfig struct { + ServerRoot string + EnvUUID string + CACert []byte + CloudimgBaseUrl string + ImageDownloadFunc func(kind instance.ContainerType, series, arch, cloudimgBaseUrl string) (string, error) +} + type imageURLGetter struct { - serverRoot string - envuuid string - caCert []byte + config ImageURLGetterConfig } // NewImageURLGetter returns an ImageURLGetter for the specified state // server address and environment UUID. -func NewImageURLGetter(serverRoot, envuuid string, caCert []byte) ImageURLGetter { - return &imageURLGetter{ - serverRoot, - envuuid, - caCert, - } +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 := ImageDownloadURL(kind, series, arch) + imageURL, err := ug.config.ImageDownloadFunc(kind, series, arch, 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/environment/%s/images/%v/%s/%s/%s", ug.serverRoot, ug.envuuid, kind, series, arch, imageFilename, + "https://%s/environment/%s/images/%v/%s/%s/%s", ug.config.ServerRoot, ug.config.EnvUUID, kind, series, arch, imageFilename, ) return imageUrl, nil } // CACert is specified on the NewImageURLGetter interface. func (ug *imageURLGetter) CACert() []byte { - return ug.caCert + 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 string) (string, error) { +func ImageDownloadURL(kind instance.ContainerType, series, arch, cloudimgBaseUrl string) (string, error) { // TODO - we currently only need to support LXC images - kind is ignored. if kind != instance.LXC { return "", errors.Errorf("unsupported container type: %v", kind) @@ -72,11 +75,17 @@ // 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, "released", 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 } === modified file 'src/github.com/juju/juju/container/image_test.go' --- src/github.com/juju/juju/container/image_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/container/image_test.go 2015-10-23 18:29:32 +0000 @@ -26,20 +26,48 @@ } func (s *imageURLSuite) TestImageURL(c *gc.C) { - imageURLGetter := container.NewImageURLGetter("host:port", "12345", []byte("cert")) - imageURL, err := imageURLGetter.ImageURL(instance.LXC, "trusty", "amd64") - c.Assert(err, gc.IsNil) - c.Assert(imageURL, gc.Equals, "https://host:port/environment/12345/images/lxc/trusty/amd64/trusty-released-amd64-root.tar.gz") - c.Assert(imageURLGetter.CACert(), gc.DeepEquals, []byte("cert")) + imageURLGetter := container.NewImageURLGetter( + container.ImageURLGetterConfig{ + "host:port", "12345", []byte("cert"), "", + container.ImageDownloadURL, + }) + imageURL, err := imageURLGetter.ImageURL(instance.LXC, "trusty", "amd64") + c.Assert(err, gc.IsNil) + c.Assert(imageURL, gc.Equals, "https://host:port/environment/12345/images/lxc/trusty/amd64/trusty-released-amd64-root.tar.gz") + 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, cloudimgBaseUrl string) (string, error) { + calledBaseURL = cloudimgBaseUrl + return "omg://wat/trusty-released-amd64-root.tar.gz", nil + } + imageURLGetter := container.NewImageURLGetter( + container.ImageURLGetterConfig{ + "host:port", "12345", []byte("cert"), baseURL, mockFunc, + }) + imageURL, err := imageURLGetter.ImageURL(instance.LXC, "trusty", "amd64") + c.Assert(err, gc.IsNil) + c.Assert(imageURL, gc.Equals, "https://host:port/environment/12345/images/lxc/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.LXC, "trusty", "amd64") + imageDownloadURL, err := container.ImageDownloadURL(instance.LXC, "trusty", "amd64", "") 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.LXC, "trusty", "amd64", "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") + _, err := container.ImageDownloadURL(instance.KVM, "trusty", "amd64", "") c.Assert(err, gc.ErrorMatches, "unsupported container .*") } === modified file 'src/github.com/juju/juju/container/kvm/container.go' --- src/github.com/juju/juju/container/kvm/container.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/container/kvm/container.go 2015-10-23 18:29:32 +0000 @@ -9,6 +9,7 @@ "github.com/juju/errors" "github.com/juju/juju/container" + "github.com/juju/juju/network" ) type kvmContainer struct { @@ -33,9 +34,11 @@ return err } var bridge string + var interfaces []network.InterfaceInfo if params.Network != nil { if params.Network.NetworkType == container.BridgeNetwork { bridge = params.Network.Device + interfaces = params.Network.Interfaces } else { err := errors.New("Non-bridge network devices not yet supported") logger.Infof(err.Error()) @@ -52,6 +55,7 @@ Memory: params.Memory, CpuCores: params.CpuCores, RootDisk: params.RootDisk, + Interfaces: interfaces, }); err != nil { return err } === modified file 'src/github.com/juju/juju/container/kvm/export_test.go' --- src/github.com/juju/juju/container/kvm/export_test.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/container/kvm/export_test.go 2015-10-23 18:29:32 +0000 @@ -3,10 +3,12 @@ // This file exports internal package implementations so that tests // can utilize them to mock behavior. -var KVMPath = &kvmPath +var ( + KVMPath = &kvmPath -// Used to export the parameters used to call Start on the KVM Container -var TestStartParams = &startParams + // Used to export the parameters used to call Start on the KVM Container + TestStartParams = &startParams +) func NewEmptyKvmContainer() *kvmContainer { return &kvmContainer{} === modified file 'src/github.com/juju/juju/container/kvm/kvm.go' --- src/github.com/juju/juju/container/kvm/kvm.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/container/kvm/kvm.go 2015-10-23 18:29:32 +0000 @@ -20,7 +20,7 @@ "github.com/juju/juju/container" "github.com/juju/juju/environs/imagemetadata" "github.com/juju/juju/instance" - "github.com/juju/juju/version" + "github.com/juju/juju/juju/arch" ) var ( @@ -136,7 +136,7 @@ } // Create the container. startParams = ParseConstraintsToStartParams(instanceConfig.Constraints) - startParams.Arch = version.Current.Arch + startParams.Arch = arch.HostArch() startParams.Series = series startParams.Network = networkConfig startParams.UserDataFile = userDataFilename === modified file 'src/github.com/juju/juju/container/kvm/kvm_test.go' --- src/github.com/juju/juju/container/kvm/kvm_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/container/kvm/kvm_test.go 2015-10-23 18:29:32 +0000 @@ -21,9 +21,10 @@ containertesting "github.com/juju/juju/container/testing" "github.com/juju/juju/environs/config" "github.com/juju/juju/instance" + "github.com/juju/juju/juju/arch" + "github.com/juju/juju/network" "github.com/juju/juju/provider/dummy" coretesting "github.com/juju/juju/testing" - "github.com/juju/juju/version" ) type KVMSuite struct { @@ -66,7 +67,7 @@ network := container.BridgeNetworkConfig("testbr0", 0, nil) c.Assert(kvmContainer.Start(kvm.StartParams{ Series: "quantal", - Arch: version.Current.Arch, + Arch: arch.HostArch(), UserDataFile: "userdata.txt", Network: network}), gc.IsNil) return kvmContainer @@ -101,6 +102,60 @@ containertesting.AssertCloudInit(c, cloudInitFilename) } +func (s *KVMSuite) TestWriteTemplate(c *gc.C) { + params := kvm.CreateMachineParams{ + Hostname: "foo-bar", + NetworkBridge: "br0", + Interfaces: []network.InterfaceInfo{ + {MACAddress: "00:16:3e:20:b0:11"}, + }, + } + tempDir := c.MkDir() + + templatePath := filepath.Join(tempDir, "kvm.xml") + err := kvm.WriteTemplate(templatePath, params) + c.Assert(err, jc.ErrorIsNil) + templateBytes, err := ioutil.ReadFile(templatePath) + c.Assert(err, jc.ErrorIsNil) + + template := string(templateBytes) + + c.Assert(template, jc.Contains, "foo-bar") + c.Assert(template, jc.Contains, "") + c.Assert(template, jc.Contains, "") + c.Assert(strings.Count(string(template), ""), gc.Equals, 1) +} + +func (s *KVMSuite) TestCreateMachineUsesTemplate(c *gc.C) { + const uvtKvmBinName = "uvt-kvm" + testing.PatchExecutableAsEchoArgs(c, s, uvtKvmBinName) + + tempDir := c.MkDir() + params := kvm.CreateMachineParams{ + Hostname: "foo-bar", + NetworkBridge: "br0", + Interfaces: []network.InterfaceInfo{ + {MACAddress: "00:16:3e:20:b0:11"}, + }, + UserDataFile: filepath.Join(tempDir, "something"), + } + + err := kvm.CreateMachine(params) + c.Assert(err, jc.ErrorIsNil) + + expectedArgs := []string{ + "create", + "--log-console-output", + "--user-data", + filepath.Join(tempDir, "something"), + "--template", + filepath.Join(tempDir, "kvm-template.xml"), + "foo-bar", + } + + testing.AssertEchoArgs(c, uvtKvmBinName, expectedArgs...) +} + func (s *KVMSuite) TestDestroyContainer(c *gc.C) { instance := containertesting.CreateContainer(c, s.manager, "1/lxc/0") === modified file 'src/github.com/juju/juju/container/kvm/libvirt.go' --- src/github.com/juju/juju/container/kvm/libvirt.go 2015-03-26 15:54:39 +0000 +++ src/github.com/juju/juju/container/kvm/libvirt.go 2015-10-23 18:29:32 +0000 @@ -16,9 +16,12 @@ import ( "fmt" + "path/filepath" "regexp" "strings" + "github.com/juju/errors" + "github.com/juju/juju/network" "github.com/juju/utils" ) @@ -65,6 +68,7 @@ Memory uint64 CpuCores uint64 RootDisk uint64 + Interfaces []network.InterfaceInfo } // CreateMachine creates a virtual machine and starts it. @@ -79,9 +83,6 @@ if params.UserDataFile != "" { args = append(args, "--user-data", params.UserDataFile) } - if params.NetworkBridge != "" { - args = append(args, "--bridge", params.NetworkBridge) - } if params.Memory != 0 { args = append(args, "--memory", fmt.Sprint(params.Memory)) } @@ -91,7 +92,22 @@ if params.RootDisk != 0 { args = append(args, "--disk", fmt.Sprint(params.RootDisk)) } - // TODO add memory, cpu and disk prior to hostname + if params.NetworkBridge != "" { + if len(params.Interfaces) != 0 { + templateDir := filepath.Dir(params.UserDataFile) + + templatePath := filepath.Join(templateDir, "kvm-template.xml") + err := WriteTemplate(templatePath, params) + if err != nil { + return errors.Trace(err) + } + + args = append(args, "--template", templatePath) + } else { + args = append(args, "--bridge", params.NetworkBridge) + } + } + args = append(args, params.Hostname) if params.Series != "" { args = append(args, fmt.Sprintf("release=%s", params.Series)) === modified file 'src/github.com/juju/juju/container/kvm/live_test.go' --- src/github.com/juju/juju/container/kvm/live_test.go 2015-09-22 15:27:01 +0000 +++ src/github.com/juju/juju/container/kvm/live_test.go 2015-10-23 18:29:32 +0000 @@ -18,6 +18,7 @@ "github.com/juju/juju/environs/config" "github.com/juju/juju/environs/imagemetadata" "github.com/juju/juju/instance" + "github.com/juju/juju/juju/arch" jujutesting "github.com/juju/juju/juju/testing" coretesting "github.com/juju/juju/testing" "github.com/juju/juju/tools" @@ -100,7 +101,7 @@ inst, hardware, err := manager.CreateContainer(instanceConfig, "precise", network, nil) c.Assert(err, jc.ErrorIsNil) c.Assert(hardware, gc.NotNil) - expected := fmt.Sprintf("arch=%s cpu-cores=1 mem=512M root-disk=8192M", version.Current.Arch) + expected := fmt.Sprintf("arch=%s cpu-cores=1 mem=512M root-disk=8192M", arch.HostArch()) c.Assert(hardware.String(), gc.Equals, expected) return inst } === added file 'src/github.com/juju/juju/container/kvm/template.go' --- src/github.com/juju/juju/container/kvm/template.go 1970-01-01 00:00:00 +0000 +++ src/github.com/juju/juju/container/kvm/template.go 2015-10-23 18:29:32 +0000 @@ -0,0 +1,78 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package kvm + +import ( + "bytes" + "os" + "text/template" + + "github.com/juju/errors" +) + +var kvmTemplate = ` + + {{.Hostname}} + 1 + + hvm + + + + + + + + +
+ + + + + + + + + + + + + +