github.com/rogpeppe/juju@v0.0.0-20140613142852-6337964b789e/cmd/jujud/agent_test.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package main
     5  
     6  import (
     7  	stderrors "errors"
     8  	"fmt"
     9  	"net"
    10  	"strconv"
    11  	"time"
    12  
    13  	"github.com/juju/cmd"
    14  	gitjujutesting "github.com/juju/testing"
    15  	jc "github.com/juju/testing/checkers"
    16  	gc "launchpad.net/gocheck"
    17  
    18  	"github.com/juju/juju/agent"
    19  	agenttools "github.com/juju/juju/agent/tools"
    20  	"github.com/juju/juju/environs"
    21  	envtesting "github.com/juju/juju/environs/testing"
    22  	envtools "github.com/juju/juju/environs/tools"
    23  	"github.com/juju/juju/juju/testing"
    24  	"github.com/juju/juju/mongo"
    25  	"github.com/juju/juju/network"
    26  	"github.com/juju/juju/state"
    27  	"github.com/juju/juju/state/api"
    28  	"github.com/juju/juju/state/api/params"
    29  	coretesting "github.com/juju/juju/testing"
    30  	coretools "github.com/juju/juju/tools"
    31  	"github.com/juju/juju/version"
    32  	"github.com/juju/juju/worker"
    33  	"github.com/juju/juju/worker/upgrader"
    34  )
    35  
    36  var _ = gc.Suite(&toolSuite{})
    37  
    38  type toolSuite struct {
    39  	coretesting.BaseSuite
    40  }
    41  
    42  var errorImportanceTests = []error{
    43  	nil,
    44  	stderrors.New("foo"),
    45  	&upgrader.UpgradeReadyError{},
    46  	worker.ErrTerminateAgent,
    47  }
    48  
    49  func (*toolSuite) TestErrorImportance(c *gc.C) {
    50  	for i, err0 := range errorImportanceTests {
    51  		for j, err1 := range errorImportanceTests {
    52  			c.Assert(moreImportant(err0, err1), gc.Equals, i > j)
    53  		}
    54  	}
    55  }
    56  
    57  var isFatalTests = []struct {
    58  	err     error
    59  	isFatal bool
    60  }{{
    61  	err:     worker.ErrTerminateAgent,
    62  	isFatal: true,
    63  }, {
    64  	err:     &upgrader.UpgradeReadyError{},
    65  	isFatal: true,
    66  }, {
    67  	err: &params.Error{
    68  		Message: "blah",
    69  		Code:    params.CodeNotProvisioned,
    70  	},
    71  	isFatal: false,
    72  }, {
    73  	err:     &fatalError{"some fatal error"},
    74  	isFatal: true,
    75  }, {
    76  	err:     stderrors.New("foo"),
    77  	isFatal: false,
    78  }, {
    79  	err: &params.Error{
    80  		Message: "blah",
    81  		Code:    params.CodeNotFound,
    82  	},
    83  	isFatal: false,
    84  }}
    85  
    86  func (*toolSuite) TestIsFatal(c *gc.C) {
    87  	for i, test := range isFatalTests {
    88  		c.Logf("test %d: %s", i, test.err)
    89  		c.Assert(isFatal(test.err), gc.Equals, test.isFatal)
    90  	}
    91  }
    92  
    93  type apiOpenSuite struct {
    94  	coretesting.BaseSuite
    95  }
    96  
    97  type fakeAPIOpenConfig struct {
    98  	agent.Config
    99  }
   100  
   101  func (fakeAPIOpenConfig) APIInfo() *api.Info {
   102  	return &api.Info{}
   103  }
   104  
   105  func (fakeAPIOpenConfig) OldPassword() string {
   106  	return "old"
   107  }
   108  
   109  func (fakeAPIOpenConfig) Jobs() []params.MachineJob {
   110  	return []params.MachineJob{}
   111  }
   112  
   113  var _ = gc.Suite(&apiOpenSuite{})
   114  
   115  func (s *apiOpenSuite) TestOpenAPIStateReplaceErrors(c *gc.C) {
   116  	type replaceErrors struct {
   117  		openErr    error
   118  		replaceErr error
   119  	}
   120  	var apiError error
   121  	s.PatchValue(&apiOpen, func(info *api.Info, opts api.DialOpts) (*api.State, error) {
   122  		return nil, apiError
   123  	})
   124  	errReplacePairs := []replaceErrors{{
   125  		fmt.Errorf("blah"), nil,
   126  	}, {
   127  		openErr:    &params.Error{Code: params.CodeNotProvisioned},
   128  		replaceErr: worker.ErrTerminateAgent,
   129  	}, {
   130  		openErr:    &params.Error{Code: params.CodeUnauthorized},
   131  		replaceErr: worker.ErrTerminateAgent,
   132  	}}
   133  	for i, test := range errReplacePairs {
   134  		c.Logf("test %d", i)
   135  		apiError = test.openErr
   136  		_, _, err := openAPIState(fakeAPIOpenConfig{}, nil)
   137  		if test.replaceErr == nil {
   138  			c.Check(err, gc.Equals, test.openErr)
   139  		} else {
   140  			c.Check(err, gc.Equals, test.replaceErr)
   141  		}
   142  	}
   143  }
   144  
   145  type testPinger func() error
   146  
   147  func (f testPinger) Ping() error {
   148  	return f()
   149  }
   150  
   151  func (s *toolSuite) TestConnectionIsFatal(c *gc.C) {
   152  	var (
   153  		errPinger testPinger = func() error {
   154  			return stderrors.New("ping error")
   155  		}
   156  		okPinger testPinger = func() error {
   157  			return nil
   158  		}
   159  	)
   160  	for i, pinger := range []testPinger{errPinger, okPinger} {
   161  		for j, test := range isFatalTests {
   162  			c.Logf("test %d.%d: %s", i, j, test.err)
   163  			fatal := connectionIsFatal(pinger)(test.err)
   164  			if test.isFatal {
   165  				c.Check(fatal, jc.IsTrue)
   166  			} else {
   167  				c.Check(fatal, gc.Equals, i == 0)
   168  			}
   169  		}
   170  	}
   171  }
   172  
   173  func mkTools(s string) *coretools.Tools {
   174  	return &coretools.Tools{
   175  		Version: version.MustParseBinary(s + "-foo-bar"),
   176  	}
   177  }
   178  
   179  type acCreator func() (cmd.Command, *AgentConf)
   180  
   181  // CheckAgentCommand is a utility function for verifying that common agent
   182  // options are handled by a Command; it returns an instance of that
   183  // command pre-parsed, with any mandatory flags added.
   184  func CheckAgentCommand(c *gc.C, create acCreator, args []string) cmd.Command {
   185  	com, conf := create()
   186  	err := coretesting.InitCommand(com, args)
   187  	c.Assert(conf.dataDir, gc.Equals, "/var/lib/juju")
   188  	badArgs := append(args, "--data-dir", "")
   189  	com, conf = create()
   190  	err = coretesting.InitCommand(com, badArgs)
   191  	c.Assert(err, gc.ErrorMatches, "--data-dir option must be set")
   192  
   193  	args = append(args, "--data-dir", "jd")
   194  	com, conf = create()
   195  	c.Assert(coretesting.InitCommand(com, args), gc.IsNil)
   196  	c.Assert(conf.dataDir, gc.Equals, "jd")
   197  	return com
   198  }
   199  
   200  // ParseAgentCommand is a utility function that inserts the always-required args
   201  // before parsing an agent command and returning the result.
   202  func ParseAgentCommand(ac cmd.Command, args []string) error {
   203  	common := []string{
   204  		"--data-dir", "jd",
   205  	}
   206  	return coretesting.InitCommand(ac, append(common, args...))
   207  }
   208  
   209  type runner interface {
   210  	Run(*cmd.Context) error
   211  	Stop() error
   212  }
   213  
   214  // runWithTimeout runs an agent and waits
   215  // for it to complete within a reasonable time.
   216  func runWithTimeout(r runner) error {
   217  	done := make(chan error)
   218  	go func() {
   219  		done <- r.Run(nil)
   220  	}()
   221  	select {
   222  	case err := <-done:
   223  		return err
   224  	case <-time.After(coretesting.LongWait):
   225  	}
   226  	err := r.Stop()
   227  	return fmt.Errorf("timed out waiting for agent to finish; stop error: %v", err)
   228  }
   229  
   230  // agentSuite is a fixture to be used by agent test suites.
   231  type agentSuite struct {
   232  	oldRestartDelay time.Duration
   233  	testing.JujuConnSuite
   234  }
   235  
   236  func (s *agentSuite) SetUpSuite(c *gc.C) {
   237  	s.JujuConnSuite.SetUpSuite(c)
   238  
   239  	s.oldRestartDelay = worker.RestartDelay
   240  	// We could use testing.ShortWait, but this thrashes quite
   241  	// a bit when some tests are restarting every 50ms for 10 seconds,
   242  	// so use a slightly more friendly delay.
   243  	worker.RestartDelay = 250 * time.Millisecond
   244  	s.PatchValue(&ensureMongoServer, func(string, string, params.StateServingInfo) error {
   245  		return nil
   246  	})
   247  }
   248  
   249  func (s *agentSuite) TearDownSuite(c *gc.C) {
   250  	s.JujuConnSuite.TearDownSuite(c)
   251  	worker.RestartDelay = s.oldRestartDelay
   252  }
   253  
   254  // primeAgent writes the configuration file and tools with version vers
   255  // for an agent with the given entity name.  It returns the agent's
   256  // configuration and the current tools.
   257  func (s *agentSuite) primeAgent(c *gc.C, tag, password string, vers version.Binary) (agent.ConfigSetterWriter, *coretools.Tools) {
   258  	stor := s.Conn.Environ.Storage()
   259  	agentTools := envtesting.PrimeTools(c, stor, s.DataDir(), vers)
   260  	err := envtools.MergeAndWriteMetadata(stor, coretools.List{agentTools}, envtools.DoNotWriteMirrors)
   261  	c.Assert(err, gc.IsNil)
   262  	tools1, err := agenttools.ChangeAgentTools(s.DataDir(), tag, vers)
   263  	c.Assert(err, gc.IsNil)
   264  	c.Assert(tools1, gc.DeepEquals, agentTools)
   265  
   266  	stateInfo := s.StateInfo(c)
   267  	apiInfo := s.APIInfo(c)
   268  	conf, err := agent.NewAgentConfig(
   269  		agent.AgentConfigParams{
   270  			DataDir:           s.DataDir(),
   271  			Tag:               tag,
   272  			UpgradedToVersion: vers.Number,
   273  			Password:          password,
   274  			Nonce:             state.BootstrapNonce,
   275  			StateAddresses:    stateInfo.Addrs,
   276  			APIAddresses:      apiInfo.Addrs,
   277  			CACert:            stateInfo.CACert,
   278  		})
   279  	conf.SetPassword(password)
   280  	c.Assert(conf.Write(), gc.IsNil)
   281  	s.primeAPIHostPorts(c)
   282  	return conf, agentTools
   283  }
   284  
   285  func (s *agentSuite) primeAPIHostPorts(c *gc.C) {
   286  	apiInfo := s.APIInfo(c)
   287  
   288  	c.Assert(apiInfo.Addrs, gc.HasLen, 1)
   289  	hostPort, err := parseHostPort(apiInfo.Addrs[0])
   290  	c.Assert(err, gc.IsNil)
   291  
   292  	err = s.State.SetAPIHostPorts([][]network.HostPort{{hostPort}})
   293  	c.Assert(err, gc.IsNil)
   294  }
   295  
   296  func parseHostPort(s string) (network.HostPort, error) {
   297  	addr, port, err := net.SplitHostPort(s)
   298  	if err != nil {
   299  		return network.HostPort{}, err
   300  	}
   301  	portNum, err := strconv.Atoi(port)
   302  	if err != nil {
   303  		return network.HostPort{}, fmt.Errorf("bad port number %q", port)
   304  	}
   305  	addrs := network.NewAddresses(addr)
   306  	hostPorts := network.AddressesWithPort(addrs, portNum)
   307  	return hostPorts[0], nil
   308  }
   309  
   310  // writeStateAgentConfig creates and writes a state agent config.
   311  func writeStateAgentConfig(c *gc.C, stateInfo *state.Info, dataDir, tag, password string, vers version.Binary) agent.ConfigSetterWriter {
   312  	port := gitjujutesting.FindTCPPort()
   313  	apiAddr := []string{fmt.Sprintf("localhost:%d", port)}
   314  	conf, err := agent.NewStateMachineConfig(
   315  		agent.AgentConfigParams{
   316  			DataDir:           dataDir,
   317  			Tag:               tag,
   318  			UpgradedToVersion: vers.Number,
   319  			Password:          password,
   320  			Nonce:             state.BootstrapNonce,
   321  			StateAddresses:    stateInfo.Addrs,
   322  			APIAddresses:      apiAddr,
   323  			CACert:            stateInfo.CACert,
   324  		},
   325  		params.StateServingInfo{
   326  			Cert:       coretesting.ServerCert,
   327  			PrivateKey: coretesting.ServerKey,
   328  			StatePort:  gitjujutesting.MgoServer.Port(),
   329  			APIPort:    port,
   330  		})
   331  	c.Assert(err, gc.IsNil)
   332  	conf.SetPassword(password)
   333  	c.Assert(conf.Write(), gc.IsNil)
   334  	return conf
   335  }
   336  
   337  // primeStateAgent writes the configuration file and tools with version vers
   338  // for an agent with the given entity name.  It returns the agent's configuration
   339  // and the current tools.
   340  func (s *agentSuite) primeStateAgent(
   341  	c *gc.C, tag, password string, vers version.Binary) (agent.ConfigSetterWriter, *coretools.Tools) {
   342  
   343  	agentTools := envtesting.PrimeTools(c, s.Conn.Environ.Storage(), s.DataDir(), vers)
   344  	tools1, err := agenttools.ChangeAgentTools(s.DataDir(), tag, vers)
   345  	c.Assert(err, gc.IsNil)
   346  	c.Assert(tools1, gc.DeepEquals, agentTools)
   347  
   348  	stateInfo := s.StateInfo(c)
   349  	conf := writeStateAgentConfig(c, stateInfo, s.DataDir(), tag, password, vers)
   350  	s.primeAPIHostPorts(c)
   351  	return conf, agentTools
   352  }
   353  
   354  // initAgent initialises the given agent command with additional
   355  // arguments as provided.
   356  func (s *agentSuite) initAgent(c *gc.C, a cmd.Command, args ...string) {
   357  	args = append([]string{"--data-dir", s.DataDir()}, args...)
   358  	err := coretesting.InitCommand(a, args)
   359  	c.Assert(err, gc.IsNil)
   360  }
   361  
   362  func (s *agentSuite) testOpenAPIState(c *gc.C, ent state.AgentEntity, agentCmd Agent, initialPassword string) {
   363  	conf, err := agent.ReadConfig(agent.ConfigPath(s.DataDir(), ent.Tag()))
   364  	c.Assert(err, gc.IsNil)
   365  
   366  	conf.SetPassword("")
   367  	err = conf.Write()
   368  	c.Assert(err, gc.IsNil)
   369  
   370  	// Check that it starts initially and changes the password
   371  	assertOpen := func(conf agent.Config) {
   372  		st, gotEnt, err := openAPIState(conf, agentCmd)
   373  		c.Assert(err, gc.IsNil)
   374  		c.Assert(st, gc.NotNil)
   375  		st.Close()
   376  		c.Assert(gotEnt.Tag(), gc.Equals, ent.Tag())
   377  	}
   378  	assertOpen(conf)
   379  
   380  	// Check that the initial password is no longer valid.
   381  	err = ent.Refresh()
   382  	c.Assert(err, gc.IsNil)
   383  	c.Assert(ent.PasswordValid(initialPassword), gc.Equals, false)
   384  
   385  	// Read the configuration and check that we can connect with it.
   386  	conf = refreshConfig(c, conf)
   387  	// Check we can open the API with the new configuration.
   388  	assertOpen(conf)
   389  }
   390  
   391  type errorAPIOpener struct {
   392  	err error
   393  }
   394  
   395  func (e *errorAPIOpener) OpenAPI(_ api.DialOpts) (*api.State, string, error) {
   396  	return nil, "", e.err
   397  }
   398  
   399  func (s *agentSuite) assertCanOpenState(c *gc.C, tag, dataDir string) {
   400  	config, err := agent.ReadConfig(agent.ConfigPath(dataDir, tag))
   401  	c.Assert(err, gc.IsNil)
   402  	info, ok := config.StateInfo()
   403  	c.Assert(ok, jc.IsTrue)
   404  	st, err := state.Open(info, mongo.DialOpts{}, environs.NewStatePolicy())
   405  	c.Assert(err, gc.IsNil)
   406  	st.Close()
   407  }
   408  
   409  func (s *agentSuite) assertCannotOpenState(c *gc.C, tag, dataDir string) {
   410  	config, err := agent.ReadConfig(agent.ConfigPath(dataDir, tag))
   411  	c.Assert(err, gc.IsNil)
   412  	_, ok := config.StateInfo()
   413  	c.Assert(ok, jc.IsFalse)
   414  }
   415  
   416  func refreshConfig(c *gc.C, config agent.Config) agent.ConfigSetterWriter {
   417  	config1, err := agent.ReadConfig(agent.ConfigPath(config.DataDir(), config.Tag()))
   418  	c.Assert(err, gc.IsNil)
   419  	return config1
   420  }