github.com/altoros/juju-vmware@v0.0.0-20150312064031-f19ae857ccca/cmd/juju/status_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  	"bytes"
     8  	"encoding/json"
     9  	"fmt"
    10  	"strings"
    11  
    12  	"github.com/juju/cmd"
    13  	jc "github.com/juju/testing/checkers"
    14  	gc "gopkg.in/check.v1"
    15  	"gopkg.in/juju/charm.v4"
    16  	goyaml "gopkg.in/yaml.v1"
    17  
    18  	"github.com/juju/juju/api"
    19  	"github.com/juju/juju/cmd/envcmd"
    20  	"github.com/juju/juju/constraints"
    21  	"github.com/juju/juju/environs"
    22  	"github.com/juju/juju/instance"
    23  	"github.com/juju/juju/juju/testing"
    24  	"github.com/juju/juju/network"
    25  	"github.com/juju/juju/state"
    26  	"github.com/juju/juju/state/multiwatcher"
    27  	"github.com/juju/juju/state/presence"
    28  	"github.com/juju/juju/testcharms"
    29  	coretesting "github.com/juju/juju/testing"
    30  	"github.com/juju/juju/version"
    31  )
    32  
    33  func runStatus(c *gc.C, args ...string) (code int, stdout, stderr []byte) {
    34  	ctx := coretesting.Context(c)
    35  	code = cmd.Main(envcmd.Wrap(&StatusCommand{}), ctx, args)
    36  	stdout = ctx.Stdout.(*bytes.Buffer).Bytes()
    37  	stderr = ctx.Stderr.(*bytes.Buffer).Bytes()
    38  	return
    39  }
    40  
    41  type StatusSuite struct {
    42  	testing.JujuConnSuite
    43  }
    44  
    45  var _ = gc.Suite(&StatusSuite{})
    46  
    47  type M map[string]interface{}
    48  
    49  type L []interface{}
    50  
    51  type testCase struct {
    52  	summary string
    53  	steps   []stepper
    54  }
    55  
    56  func test(summary string, steps ...stepper) testCase {
    57  	return testCase{summary, steps}
    58  }
    59  
    60  type stepper interface {
    61  	step(c *gc.C, ctx *context)
    62  }
    63  
    64  //
    65  // context
    66  //
    67  
    68  func newContext(c *gc.C, st *state.State, env environs.Environ, adminUserTag string) *context {
    69  	// We make changes in the API server's state so that
    70  	// our changes to presence are immediately noticed
    71  	// in the status.
    72  	return &context{
    73  		st:           st,
    74  		env:          env,
    75  		charms:       make(map[string]*state.Charm),
    76  		pingers:      make(map[string]*presence.Pinger),
    77  		adminUserTag: adminUserTag,
    78  	}
    79  }
    80  
    81  type context struct {
    82  	st           *state.State
    83  	env          environs.Environ
    84  	charms       map[string]*state.Charm
    85  	pingers      map[string]*presence.Pinger
    86  	adminUserTag string // A string repr of the tag.
    87  }
    88  
    89  func (ctx *context) reset(c *gc.C) {
    90  	for _, up := range ctx.pingers {
    91  		err := up.Kill()
    92  		c.Check(err, jc.ErrorIsNil)
    93  	}
    94  }
    95  
    96  func (ctx *context) run(c *gc.C, steps []stepper) {
    97  	for i, s := range steps {
    98  		c.Logf("step %d", i)
    99  		c.Logf("%#v", s)
   100  		s.step(c, ctx)
   101  	}
   102  }
   103  
   104  func (ctx *context) setAgentPresence(c *gc.C, p presence.Presencer) *presence.Pinger {
   105  	pinger, err := p.SetAgentPresence()
   106  	c.Assert(err, jc.ErrorIsNil)
   107  	ctx.st.StartSync()
   108  	err = p.WaitAgentPresence(coretesting.LongWait)
   109  	c.Assert(err, jc.ErrorIsNil)
   110  	agentPresence, err := p.AgentPresence()
   111  	c.Assert(err, jc.ErrorIsNil)
   112  	c.Assert(agentPresence, jc.IsTrue)
   113  	return pinger
   114  }
   115  
   116  func (s *StatusSuite) newContext(c *gc.C) *context {
   117  	st := s.Environ.(testing.GetStater).GetStateInAPIServer()
   118  	// We make changes in the API server's state so that
   119  	// our changes to presence are immediately noticed
   120  	// in the status.
   121  	return newContext(c, st, s.Environ, s.AdminUserTag(c).String())
   122  }
   123  
   124  func (s *StatusSuite) resetContext(c *gc.C, ctx *context) {
   125  	ctx.reset(c)
   126  	s.JujuConnSuite.Reset(c)
   127  }
   128  
   129  // shortcuts for expected output.
   130  var (
   131  	machine0 = M{
   132  		"agent-state":                "started",
   133  		"dns-name":                   "dummyenv-0.dns",
   134  		"instance-id":                "dummyenv-0",
   135  		"series":                     "quantal",
   136  		"hardware":                   "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M",
   137  		"state-server-member-status": "adding-vote",
   138  	}
   139  	machine1 = M{
   140  		"agent-state": "started",
   141  		"dns-name":    "dummyenv-1.dns",
   142  		"instance-id": "dummyenv-1",
   143  		"series":      "quantal",
   144  		"hardware":    "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M",
   145  	}
   146  	machine2 = M{
   147  		"agent-state": "started",
   148  		"dns-name":    "dummyenv-2.dns",
   149  		"instance-id": "dummyenv-2",
   150  		"series":      "quantal",
   151  		"hardware":    "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M",
   152  	}
   153  	machine3 = M{
   154  		"agent-state": "started",
   155  		"dns-name":    "dummyenv-3.dns",
   156  		"instance-id": "dummyenv-3",
   157  		"series":      "quantal",
   158  		"hardware":    "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M",
   159  	}
   160  	machine4 = M{
   161  		"agent-state": "started",
   162  		"dns-name":    "dummyenv-4.dns",
   163  		"instance-id": "dummyenv-4",
   164  		"series":      "quantal",
   165  		"hardware":    "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M",
   166  	}
   167  	machine1WithContainers = M{
   168  		"agent-state": "started",
   169  		"containers": M{
   170  			"1/lxc/0": M{
   171  				"agent-state": "started",
   172  				"containers": M{
   173  					"1/lxc/0/lxc/0": M{
   174  						"agent-state": "started",
   175  						"dns-name":    "dummyenv-3.dns",
   176  						"instance-id": "dummyenv-3",
   177  						"series":      "quantal",
   178  					},
   179  				},
   180  				"dns-name":    "dummyenv-2.dns",
   181  				"instance-id": "dummyenv-2",
   182  				"series":      "quantal",
   183  			},
   184  			"1/lxc/1": M{
   185  				"instance-id": "pending",
   186  				"series":      "quantal",
   187  			},
   188  		},
   189  		"dns-name":    "dummyenv-1.dns",
   190  		"instance-id": "dummyenv-1",
   191  		"series":      "quantal",
   192  		"hardware":    "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M",
   193  	}
   194  	machine1WithContainersScoped = M{
   195  		"agent-state": "started",
   196  		"containers": M{
   197  			"1/lxc/0": M{
   198  				"agent-state": "started",
   199  				"dns-name":    "dummyenv-2.dns",
   200  				"instance-id": "dummyenv-2",
   201  				"series":      "quantal",
   202  			},
   203  		},
   204  		"dns-name":    "dummyenv-1.dns",
   205  		"instance-id": "dummyenv-1",
   206  		"series":      "quantal",
   207  		"hardware":    "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M",
   208  	}
   209  	unexposedService = M{
   210  		"charm":   "cs:quantal/dummy-1",
   211  		"exposed": false,
   212  	}
   213  	exposedService = M{
   214  		"charm":   "cs:quantal/dummy-1",
   215  		"exposed": true,
   216  	}
   217  )
   218  
   219  type outputFormat struct {
   220  	name      string
   221  	marshal   func(v interface{}) ([]byte, error)
   222  	unmarshal func(data []byte, v interface{}) error
   223  }
   224  
   225  // statusFormats list all output formats supported by status command.
   226  var statusFormats = []outputFormat{
   227  	{"yaml", goyaml.Marshal, goyaml.Unmarshal},
   228  	{"json", json.Marshal, json.Unmarshal},
   229  }
   230  
   231  var machineCons = constraints.MustParse("cpu-cores=2 mem=8G root-disk=8G")
   232  
   233  var statusTests = []testCase{
   234  	// Status tests
   235  	test(
   236  		"bootstrap and starting a single instance",
   237  
   238  		addMachine{machineId: "0", job: state.JobManageEnviron},
   239  		expect{
   240  			"simulate juju bootstrap by adding machine/0 to the state",
   241  			M{
   242  				"environment": "dummyenv",
   243  				"machines": M{
   244  					"0": M{
   245  						"instance-id":                "pending",
   246  						"series":                     "quantal",
   247  						"state-server-member-status": "adding-vote",
   248  					},
   249  				},
   250  				"services": M{},
   251  			},
   252  		},
   253  
   254  		startAliveMachine{"0"},
   255  		setAddresses{"0", []network.Address{
   256  			network.NewAddress("10.0.0.1", network.ScopeUnknown),
   257  			network.NewAddress("dummyenv-0.dns", network.ScopePublic),
   258  		}},
   259  		expect{
   260  			"simulate the PA starting an instance in response to the state change",
   261  			M{
   262  				"environment": "dummyenv",
   263  				"machines": M{
   264  					"0": M{
   265  						"agent-state":                "pending",
   266  						"dns-name":                   "dummyenv-0.dns",
   267  						"instance-id":                "dummyenv-0",
   268  						"series":                     "quantal",
   269  						"hardware":                   "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M",
   270  						"state-server-member-status": "adding-vote",
   271  					},
   272  				},
   273  				"services": M{},
   274  			},
   275  		},
   276  
   277  		setMachineStatus{"0", state.StatusStarted, ""},
   278  		expect{
   279  			"simulate the MA started and set the machine status",
   280  			M{
   281  				"environment": "dummyenv",
   282  				"machines": M{
   283  					"0": machine0,
   284  				},
   285  				"services": M{},
   286  			},
   287  		},
   288  
   289  		setTools{"0", version.MustParseBinary("1.2.3-trusty-ppc")},
   290  		expect{
   291  			"simulate the MA setting the version",
   292  			M{
   293  				"environment": "dummyenv",
   294  				"machines": M{
   295  					"0": M{
   296  						"dns-name":                   "dummyenv-0.dns",
   297  						"instance-id":                "dummyenv-0",
   298  						"agent-version":              "1.2.3",
   299  						"agent-state":                "started",
   300  						"series":                     "quantal",
   301  						"hardware":                   "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M",
   302  						"state-server-member-status": "adding-vote",
   303  					},
   304  				},
   305  				"services": M{},
   306  			},
   307  		},
   308  	), test(
   309  		"deploy two services and two networks",
   310  		addMachine{machineId: "0", job: state.JobManageEnviron},
   311  		startAliveMachine{"0"},
   312  		setMachineStatus{"0", state.StatusStarted, ""},
   313  		setAddresses{"0", []network.Address{
   314  			network.NewAddress("10.0.0.1", network.ScopeUnknown),
   315  			network.NewAddress("dummyenv-0.dns", network.ScopePublic),
   316  		}},
   317  		addCharm{"dummy"},
   318  		addService{
   319  			name:     "networks-service",
   320  			charm:    "dummy",
   321  			networks: []string{"net1", "net2"},
   322  			cons:     constraints.MustParse("networks=foo,bar,^no,^good"),
   323  		},
   324  		addService{
   325  			name:  "no-networks-service",
   326  			charm: "dummy",
   327  			cons:  constraints.MustParse("networks=^mynet"),
   328  		},
   329  		addNetwork{
   330  			name:       "net1",
   331  			providerId: network.Id("provider-net1"),
   332  			cidr:       "0.1.2.0/24",
   333  			vlanTag:    0,
   334  		},
   335  		addNetwork{
   336  			name:       "net2",
   337  			providerId: network.Id("provider-vlan42"),
   338  			cidr:       "0.42.1.0/24",
   339  			vlanTag:    42,
   340  		},
   341  
   342  		expect{
   343  			"simulate just the two services and a bootstrap node",
   344  			M{
   345  				"environment": "dummyenv",
   346  				"machines": M{
   347  					"0": machine0,
   348  				},
   349  				"services": M{
   350  					"networks-service": M{
   351  						"charm":   "cs:quantal/dummy-1",
   352  						"exposed": false,
   353  						"networks": M{
   354  							"enabled":  L{"net1", "net2"},
   355  							"disabled": L{"foo", "bar", "no", "good"},
   356  						},
   357  					},
   358  					"no-networks-service": M{
   359  						"charm":   "cs:quantal/dummy-1",
   360  						"exposed": false,
   361  						"networks": M{
   362  							"disabled": L{"mynet"},
   363  						},
   364  					},
   365  				},
   366  				"networks": M{
   367  					"net1": M{
   368  						"provider-id": "provider-net1",
   369  						"cidr":        "0.1.2.0/24",
   370  					},
   371  					"net2": M{
   372  						"provider-id": "provider-vlan42",
   373  						"cidr":        "0.42.1.0/24",
   374  						"vlan-tag":    42,
   375  					},
   376  				},
   377  			},
   378  		},
   379  	), test(
   380  		"instance with different hardware characteristics",
   381  		addMachine{machineId: "0", cons: machineCons, job: state.JobManageEnviron},
   382  		setAddresses{"0", []network.Address{
   383  			network.NewAddress("10.0.0.1", network.ScopeUnknown),
   384  			network.NewAddress("dummyenv-0.dns", network.ScopePublic),
   385  		}},
   386  		startAliveMachine{"0"},
   387  		setMachineStatus{"0", state.StatusStarted, ""},
   388  		expect{
   389  			"machine 0 has specific hardware characteristics",
   390  			M{
   391  				"environment": "dummyenv",
   392  				"machines": M{
   393  					"0": M{
   394  						"agent-state":                "started",
   395  						"dns-name":                   "dummyenv-0.dns",
   396  						"instance-id":                "dummyenv-0",
   397  						"series":                     "quantal",
   398  						"hardware":                   "arch=amd64 cpu-cores=2 mem=8192M root-disk=8192M",
   399  						"state-server-member-status": "adding-vote",
   400  					},
   401  				},
   402  				"services": M{},
   403  			},
   404  		},
   405  	), test(
   406  		"instance without addresses",
   407  		addMachine{machineId: "0", cons: machineCons, job: state.JobManageEnviron},
   408  		startAliveMachine{"0"},
   409  		setMachineStatus{"0", state.StatusStarted, ""},
   410  		expect{
   411  			"machine 0 has no dns-name",
   412  			M{
   413  				"environment": "dummyenv",
   414  				"machines": M{
   415  					"0": M{
   416  						"agent-state":                "started",
   417  						"instance-id":                "dummyenv-0",
   418  						"series":                     "quantal",
   419  						"hardware":                   "arch=amd64 cpu-cores=2 mem=8192M root-disk=8192M",
   420  						"state-server-member-status": "adding-vote",
   421  					},
   422  				},
   423  				"services": M{},
   424  			},
   425  		},
   426  	), test(
   427  		"test pending and missing machines",
   428  		addMachine{machineId: "0", job: state.JobManageEnviron},
   429  		expect{
   430  			"machine 0 reports pending",
   431  			M{
   432  				"environment": "dummyenv",
   433  				"machines": M{
   434  					"0": M{
   435  						"instance-id":                "pending",
   436  						"series":                     "quantal",
   437  						"state-server-member-status": "adding-vote",
   438  					},
   439  				},
   440  				"services": M{},
   441  			},
   442  		},
   443  
   444  		startMissingMachine{"0"},
   445  		expect{
   446  			"machine 0 reports missing",
   447  			M{
   448  				"environment": "dummyenv",
   449  				"machines": M{
   450  					"0": M{
   451  						"instance-state":             "missing",
   452  						"instance-id":                "i-missing",
   453  						"agent-state":                "pending",
   454  						"series":                     "quantal",
   455  						"hardware":                   "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M",
   456  						"state-server-member-status": "adding-vote",
   457  					},
   458  				},
   459  				"services": M{},
   460  			},
   461  		},
   462  	), test(
   463  		"add two services and expose one, then add 2 more machines and some units",
   464  		addMachine{machineId: "0", job: state.JobManageEnviron},
   465  		setAddresses{"0", []network.Address{network.NewAddress("dummyenv-0.dns", network.ScopeUnknown)}},
   466  		startAliveMachine{"0"},
   467  		setMachineStatus{"0", state.StatusStarted, ""},
   468  		addCharm{"dummy"},
   469  		addService{name: "dummy-service", charm: "dummy"},
   470  		addService{name: "exposed-service", charm: "dummy"},
   471  		expect{
   472  			"no services exposed yet",
   473  			M{
   474  				"environment": "dummyenv",
   475  				"machines": M{
   476  					"0": machine0,
   477  				},
   478  				"services": M{
   479  					"dummy-service":   unexposedService,
   480  					"exposed-service": unexposedService,
   481  				},
   482  			},
   483  		},
   484  
   485  		setServiceExposed{"exposed-service", true},
   486  		expect{
   487  			"one exposed service",
   488  			M{
   489  				"environment": "dummyenv",
   490  				"machines": M{
   491  					"0": machine0,
   492  				},
   493  				"services": M{
   494  					"dummy-service":   unexposedService,
   495  					"exposed-service": exposedService,
   496  				},
   497  			},
   498  		},
   499  
   500  		addMachine{machineId: "1", job: state.JobHostUnits},
   501  		setAddresses{"1", []network.Address{network.NewAddress("dummyenv-1.dns", network.ScopeUnknown)}},
   502  		startAliveMachine{"1"},
   503  		setMachineStatus{"1", state.StatusStarted, ""},
   504  		addMachine{machineId: "2", job: state.JobHostUnits},
   505  		setAddresses{"2", []network.Address{network.NewAddress("dummyenv-2.dns", network.ScopeUnknown)}},
   506  		startAliveMachine{"2"},
   507  		setMachineStatus{"2", state.StatusStarted, ""},
   508  		expect{
   509  			"two more machines added",
   510  			M{
   511  				"environment": "dummyenv",
   512  				"machines": M{
   513  					"0": machine0,
   514  					"1": machine1,
   515  					"2": machine2,
   516  				},
   517  				"services": M{
   518  					"dummy-service":   unexposedService,
   519  					"exposed-service": exposedService,
   520  				},
   521  			},
   522  		},
   523  
   524  		addUnit{"dummy-service", "1"},
   525  		addAliveUnit{"exposed-service", "2"},
   526  		setUnitStatus{"exposed-service/0", state.StatusError, "You Require More Vespene Gas", nil},
   527  		// Open multiple ports with different protocols,
   528  		// ensure they're sorted on protocol, then number.
   529  		openUnitPort{"exposed-service/0", "udp", 10},
   530  		openUnitPort{"exposed-service/0", "udp", 2},
   531  		openUnitPort{"exposed-service/0", "tcp", 3},
   532  		openUnitPort{"exposed-service/0", "tcp", 2},
   533  		// Simulate some status with no info, while the agent is down.
   534  		setUnitStatus{"dummy-service/0", state.StatusActive, "", nil},
   535  		expect{
   536  			"add two units, one alive (in error state), one down",
   537  			M{
   538  				"environment": "dummyenv",
   539  				"machines": M{
   540  					"0": machine0,
   541  					"1": machine1,
   542  					"2": machine2,
   543  				},
   544  				"services": M{
   545  					"exposed-service": M{
   546  						"charm":   "cs:quantal/dummy-1",
   547  						"exposed": true,
   548  						"units": M{
   549  							"exposed-service/0": M{
   550  								"machine":          "2",
   551  								"agent-state":      "error",
   552  								"agent-state-info": "You Require More Vespene Gas",
   553  								"open-ports": L{
   554  									"2/tcp", "3/tcp", "2/udp", "10/udp",
   555  								},
   556  								"public-address": "dummyenv-2.dns",
   557  							},
   558  						},
   559  					},
   560  					"dummy-service": M{
   561  						"charm":   "cs:quantal/dummy-1",
   562  						"exposed": false,
   563  						"units": M{
   564  							"dummy-service/0": M{
   565  								"machine":          "1",
   566  								"agent-state":      "down",
   567  								"agent-state-info": "(started)",
   568  								"public-address":   "dummyenv-1.dns",
   569  							},
   570  						},
   571  					},
   572  				},
   573  			},
   574  		},
   575  
   576  		addMachine{machineId: "3", job: state.JobHostUnits},
   577  		startMachine{"3"},
   578  		// Simulate some status with info, while the agent is down.
   579  		setAddresses{"3", []network.Address{network.NewAddress("dummyenv-3.dns", network.ScopeUnknown)}},
   580  		setMachineStatus{"3", state.StatusStopped, "Really?"},
   581  		addMachine{machineId: "4", job: state.JobHostUnits},
   582  		setAddresses{"4", []network.Address{network.NewAddress("dummyenv-4.dns", network.ScopeUnknown)}},
   583  		startAliveMachine{"4"},
   584  		setMachineStatus{"4", state.StatusError, "Beware the red toys"},
   585  		ensureDyingUnit{"dummy-service/0"},
   586  		addMachine{machineId: "5", job: state.JobHostUnits},
   587  		ensureDeadMachine{"5"},
   588  		expect{
   589  			"add three more machine, one with a dead agent, one in error state and one dead itself; also one dying unit",
   590  			M{
   591  				"environment": "dummyenv",
   592  				"machines": M{
   593  					"0": machine0,
   594  					"1": machine1,
   595  					"2": machine2,
   596  					"3": M{
   597  						"dns-name":         "dummyenv-3.dns",
   598  						"instance-id":      "dummyenv-3",
   599  						"agent-state":      "down",
   600  						"agent-state-info": "(stopped: Really?)",
   601  						"series":           "quantal",
   602  						"hardware":         "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M",
   603  					},
   604  					"4": M{
   605  						"dns-name":         "dummyenv-4.dns",
   606  						"instance-id":      "dummyenv-4",
   607  						"agent-state":      "error",
   608  						"agent-state-info": "Beware the red toys",
   609  						"series":           "quantal",
   610  						"hardware":         "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M",
   611  					},
   612  					"5": M{
   613  						"life":        "dead",
   614  						"instance-id": "pending",
   615  						"series":      "quantal",
   616  					},
   617  				},
   618  				"services": M{
   619  					"exposed-service": M{
   620  						"charm":   "cs:quantal/dummy-1",
   621  						"exposed": true,
   622  						"units": M{
   623  							"exposed-service/0": M{
   624  								"machine":          "2",
   625  								"agent-state":      "error",
   626  								"agent-state-info": "You Require More Vespene Gas",
   627  								"open-ports": L{
   628  									"2/tcp", "3/tcp", "2/udp", "10/udp",
   629  								},
   630  								"public-address": "dummyenv-2.dns",
   631  							},
   632  						},
   633  					},
   634  					"dummy-service": M{
   635  						"charm":   "cs:quantal/dummy-1",
   636  						"exposed": false,
   637  						"units": M{
   638  							"dummy-service/0": M{
   639  								"machine":          "1",
   640  								"life":             "dying",
   641  								"agent-state":      "down",
   642  								"agent-state-info": "(started)",
   643  								"public-address":   "dummyenv-1.dns",
   644  							},
   645  						},
   646  					},
   647  				},
   648  			},
   649  		},
   650  
   651  		scopedExpect{
   652  			"scope status on dummy-service/0 unit",
   653  			[]string{"dummy-service/0"},
   654  			M{
   655  				"environment": "dummyenv",
   656  				"machines": M{
   657  					"1": machine1,
   658  				},
   659  				"services": M{
   660  					"dummy-service": M{
   661  						"charm":   "cs:quantal/dummy-1",
   662  						"exposed": false,
   663  						"units": M{
   664  							"dummy-service/0": M{
   665  								"machine":          "1",
   666  								"life":             "dying",
   667  								"agent-state":      "down",
   668  								"agent-state-info": "(started)",
   669  								"public-address":   "dummyenv-1.dns",
   670  							},
   671  						},
   672  					},
   673  				},
   674  			},
   675  		},
   676  		scopedExpect{
   677  			"scope status on exposed-service service",
   678  			[]string{"exposed-service"},
   679  			M{
   680  				"environment": "dummyenv",
   681  				"machines": M{
   682  					"2": machine2,
   683  				},
   684  				"services": M{
   685  					"exposed-service": M{
   686  						"charm":   "cs:quantal/dummy-1",
   687  						"exposed": true,
   688  						"units": M{
   689  							"exposed-service/0": M{
   690  								"machine":          "2",
   691  								"agent-state":      "error",
   692  								"agent-state-info": "You Require More Vespene Gas",
   693  								"open-ports": L{
   694  									"2/tcp", "3/tcp", "2/udp", "10/udp",
   695  								},
   696  								"public-address": "dummyenv-2.dns",
   697  							},
   698  						},
   699  					},
   700  				},
   701  			},
   702  		},
   703  		scopedExpect{
   704  			"scope status on service pattern",
   705  			[]string{"d*-service"},
   706  			M{
   707  				"environment": "dummyenv",
   708  				"machines": M{
   709  					"1": machine1,
   710  				},
   711  				"services": M{
   712  					"dummy-service": M{
   713  						"charm":   "cs:quantal/dummy-1",
   714  						"exposed": false,
   715  						"units": M{
   716  							"dummy-service/0": M{
   717  								"machine":          "1",
   718  								"life":             "dying",
   719  								"agent-state":      "down",
   720  								"agent-state-info": "(started)",
   721  								"public-address":   "dummyenv-1.dns",
   722  							},
   723  						},
   724  					},
   725  				},
   726  			},
   727  		},
   728  		scopedExpect{
   729  			"scope status on unit pattern",
   730  			[]string{"e*posed-service/*"},
   731  			M{
   732  				"environment": "dummyenv",
   733  				"machines": M{
   734  					"2": machine2,
   735  				},
   736  				"services": M{
   737  					"exposed-service": M{
   738  						"charm":   "cs:quantal/dummy-1",
   739  						"exposed": true,
   740  						"units": M{
   741  							"exposed-service/0": M{
   742  								"machine":          "2",
   743  								"agent-state":      "error",
   744  								"agent-state-info": "You Require More Vespene Gas",
   745  								"open-ports": L{
   746  									"2/tcp", "3/tcp", "2/udp", "10/udp",
   747  								},
   748  								"public-address": "dummyenv-2.dns",
   749  							},
   750  						},
   751  					},
   752  				},
   753  			},
   754  		},
   755  		scopedExpect{
   756  			"scope status on combination of service and unit patterns",
   757  			[]string{"exposed-service", "dummy-service", "e*posed-service/*", "dummy-service/*"},
   758  			M{
   759  				"environment": "dummyenv",
   760  				"machines": M{
   761  					"1": machine1,
   762  					"2": machine2,
   763  				},
   764  				"services": M{
   765  					"dummy-service": M{
   766  						"charm":   "cs:quantal/dummy-1",
   767  						"exposed": false,
   768  						"units": M{
   769  							"dummy-service/0": M{
   770  								"machine":          "1",
   771  								"life":             "dying",
   772  								"agent-state":      "down",
   773  								"agent-state-info": "(started)",
   774  								"public-address":   "dummyenv-1.dns",
   775  							},
   776  						},
   777  					},
   778  					"exposed-service": M{
   779  						"charm":   "cs:quantal/dummy-1",
   780  						"exposed": true,
   781  						"units": M{
   782  							"exposed-service/0": M{
   783  								"machine":          "2",
   784  								"agent-state":      "error",
   785  								"agent-state-info": "You Require More Vespene Gas",
   786  								"open-ports": L{
   787  									"2/tcp", "3/tcp", "2/udp", "10/udp",
   788  								},
   789  								"public-address": "dummyenv-2.dns",
   790  							},
   791  						},
   792  					},
   793  				},
   794  			},
   795  		},
   796  	), test(
   797  		"a unit with a hook relation error",
   798  		addMachine{machineId: "0", job: state.JobManageEnviron},
   799  		setAddresses{"0", []network.Address{network.NewAddress("dummyenv-0.dns", network.ScopeUnknown)}},
   800  		startAliveMachine{"0"},
   801  		setMachineStatus{"0", state.StatusStarted, ""},
   802  
   803  		addMachine{machineId: "1", job: state.JobHostUnits},
   804  		setAddresses{"1", []network.Address{network.NewAddress("dummyenv-1.dns", network.ScopeUnknown)}},
   805  		startAliveMachine{"1"},
   806  		setMachineStatus{"1", state.StatusStarted, ""},
   807  
   808  		addCharm{"wordpress"},
   809  		addService{name: "wordpress", charm: "wordpress"},
   810  		addAliveUnit{"wordpress", "1"},
   811  
   812  		addCharm{"mysql"},
   813  		addService{name: "mysql", charm: "mysql"},
   814  		addAliveUnit{"mysql", "1"},
   815  
   816  		relateServices{"wordpress", "mysql"},
   817  
   818  		setUnitStatus{"wordpress/0", state.StatusError,
   819  			"hook failed: some-relation-changed",
   820  			map[string]interface{}{"relation-id": 0}},
   821  
   822  		expect{
   823  			"a unit with a hook relation error",
   824  			M{
   825  				"environment": "dummyenv",
   826  				"machines": M{
   827  					"0": machine0,
   828  					"1": machine1,
   829  				},
   830  				"services": M{
   831  					"wordpress": M{
   832  						"charm":   "cs:quantal/wordpress-3",
   833  						"exposed": false,
   834  						"relations": M{
   835  							"db": L{"mysql"},
   836  						},
   837  						"units": M{
   838  							"wordpress/0": M{
   839  								"machine":          "1",
   840  								"agent-state":      "error",
   841  								"agent-state-info": "hook failed: some-relation-changed for mysql:server",
   842  								"public-address":   "dummyenv-1.dns",
   843  							},
   844  						},
   845  					},
   846  					"mysql": M{
   847  						"charm":   "cs:quantal/mysql-1",
   848  						"exposed": false,
   849  						"relations": M{
   850  							"server": L{"wordpress"},
   851  						},
   852  						"units": M{
   853  							"mysql/0": M{
   854  								"machine":        "1",
   855  								"agent-state":    "allocating",
   856  								"public-address": "dummyenv-1.dns",
   857  							},
   858  						},
   859  					},
   860  				},
   861  			},
   862  		},
   863  	), test(
   864  		"a unit with a hook relation error when the agent is down",
   865  		addMachine{machineId: "0", job: state.JobManageEnviron},
   866  		setAddresses{"0", []network.Address{network.NewAddress("dummyenv-0.dns", network.ScopeUnknown)}},
   867  		startAliveMachine{"0"},
   868  		setMachineStatus{"0", state.StatusStarted, ""},
   869  
   870  		addMachine{machineId: "1", job: state.JobHostUnits},
   871  		setAddresses{"1", []network.Address{network.NewAddress("dummyenv-1.dns", network.ScopeUnknown)}},
   872  		startAliveMachine{"1"},
   873  		setMachineStatus{"1", state.StatusStarted, ""},
   874  
   875  		addCharm{"wordpress"},
   876  		addService{name: "wordpress", charm: "wordpress"},
   877  		addUnit{"wordpress", "1"},
   878  
   879  		addCharm{"mysql"},
   880  		addService{name: "mysql", charm: "mysql"},
   881  		addAliveUnit{"mysql", "1"},
   882  
   883  		relateServices{"wordpress", "mysql"},
   884  
   885  		setUnitStatus{"wordpress/0", state.StatusError,
   886  			"hook failed: some-relation-changed",
   887  			map[string]interface{}{"relation-id": 0}},
   888  
   889  		expect{
   890  			"a unit with a hook relation error when the agent is down",
   891  			M{
   892  				"environment": "dummyenv",
   893  				"machines": M{
   894  					"0": machine0,
   895  					"1": machine1,
   896  				},
   897  				"services": M{
   898  					"wordpress": M{
   899  						"charm":   "cs:quantal/wordpress-3",
   900  						"exposed": false,
   901  						"relations": M{
   902  							"db": L{"mysql"},
   903  						},
   904  						"units": M{
   905  							"wordpress/0": M{
   906  								"machine":          "1",
   907  								"agent-state":      "down",
   908  								"agent-state-info": "(error: hook failed: some-relation-changed for mysql:server)",
   909  								"public-address":   "dummyenv-1.dns",
   910  							},
   911  						},
   912  					},
   913  					"mysql": M{
   914  						"charm":   "cs:quantal/mysql-1",
   915  						"exposed": false,
   916  						"relations": M{
   917  							"server": L{"wordpress"},
   918  						},
   919  						"units": M{
   920  							"mysql/0": M{
   921  								"machine":        "1",
   922  								"agent-state":    "allocating",
   923  								"public-address": "dummyenv-1.dns",
   924  							},
   925  						},
   926  					},
   927  				},
   928  			},
   929  		},
   930  	), test(
   931  		"add a dying service",
   932  		addCharm{"dummy"},
   933  		addService{name: "dummy-service", charm: "dummy"},
   934  		addMachine{machineId: "0", job: state.JobHostUnits},
   935  		addUnit{"dummy-service", "0"},
   936  		ensureDyingService{"dummy-service"},
   937  		expect{
   938  			"service shows life==dying",
   939  			M{
   940  				"environment": "dummyenv",
   941  				"machines": M{
   942  					"0": M{
   943  						"instance-id": "pending",
   944  						"series":      "quantal",
   945  					},
   946  				},
   947  				"services": M{
   948  					"dummy-service": M{
   949  						"charm":   "cs:quantal/dummy-1",
   950  						"exposed": false,
   951  						"life":    "dying",
   952  						"units": M{
   953  							"dummy-service/0": M{
   954  								"machine":     "0",
   955  								"agent-state": "allocating",
   956  							},
   957  						},
   958  					},
   959  				},
   960  			},
   961  		},
   962  	),
   963  
   964  	// Relation tests
   965  	test(
   966  		"complex scenario with multiple related services",
   967  		addMachine{machineId: "0", job: state.JobManageEnviron},
   968  		setAddresses{"0", []network.Address{network.NewAddress("dummyenv-0.dns", network.ScopeUnknown)}},
   969  		startAliveMachine{"0"},
   970  		setMachineStatus{"0", state.StatusStarted, ""},
   971  		addCharm{"wordpress"},
   972  		addCharm{"mysql"},
   973  		addCharm{"varnish"},
   974  
   975  		addService{name: "project", charm: "wordpress"},
   976  		setServiceExposed{"project", true},
   977  		addMachine{machineId: "1", job: state.JobHostUnits},
   978  		setAddresses{"1", []network.Address{network.NewAddress("dummyenv-1.dns", network.ScopeUnknown)}},
   979  		startAliveMachine{"1"},
   980  		setMachineStatus{"1", state.StatusStarted, ""},
   981  		addAliveUnit{"project", "1"},
   982  		setUnitStatus{"project/0", state.StatusActive, "", nil},
   983  
   984  		addService{name: "mysql", charm: "mysql"},
   985  		setServiceExposed{"mysql", true},
   986  		addMachine{machineId: "2", job: state.JobHostUnits},
   987  		setAddresses{"2", []network.Address{network.NewAddress("dummyenv-2.dns", network.ScopeUnknown)}},
   988  		startAliveMachine{"2"},
   989  		setMachineStatus{"2", state.StatusStarted, ""},
   990  		addAliveUnit{"mysql", "2"},
   991  		setUnitStatus{"mysql/0", state.StatusActive, "", nil},
   992  
   993  		addService{name: "varnish", charm: "varnish"},
   994  		setServiceExposed{"varnish", true},
   995  		addMachine{machineId: "3", job: state.JobHostUnits},
   996  		setAddresses{"3", []network.Address{network.NewAddress("dummyenv-3.dns", network.ScopeUnknown)}},
   997  		startAliveMachine{"3"},
   998  		setMachineStatus{"3", state.StatusStarted, ""},
   999  		addUnit{"varnish", "3"},
  1000  
  1001  		addService{name: "private", charm: "wordpress"},
  1002  		setServiceExposed{"private", true},
  1003  		addMachine{machineId: "4", job: state.JobHostUnits},
  1004  		setAddresses{"4", []network.Address{network.NewAddress("dummyenv-4.dns", network.ScopeUnknown)}},
  1005  		startAliveMachine{"4"},
  1006  		setMachineStatus{"4", state.StatusStarted, ""},
  1007  		addUnit{"private", "4"},
  1008  
  1009  		relateServices{"project", "mysql"},
  1010  		relateServices{"project", "varnish"},
  1011  		relateServices{"private", "mysql"},
  1012  
  1013  		expect{
  1014  			"multiples services with relations between some of them",
  1015  			M{
  1016  				"environment": "dummyenv",
  1017  				"machines": M{
  1018  					"0": machine0,
  1019  					"1": machine1,
  1020  					"2": machine2,
  1021  					"3": machine3,
  1022  					"4": machine4,
  1023  				},
  1024  				"services": M{
  1025  					"project": M{
  1026  						"charm":   "cs:quantal/wordpress-3",
  1027  						"exposed": true,
  1028  						"units": M{
  1029  							"project/0": M{
  1030  								"machine":        "1",
  1031  								"agent-state":    "started",
  1032  								"public-address": "dummyenv-1.dns",
  1033  							},
  1034  						},
  1035  						"relations": M{
  1036  							"db":    L{"mysql"},
  1037  							"cache": L{"varnish"},
  1038  						},
  1039  					},
  1040  					"mysql": M{
  1041  						"charm":   "cs:quantal/mysql-1",
  1042  						"exposed": true,
  1043  						"units": M{
  1044  							"mysql/0": M{
  1045  								"machine":        "2",
  1046  								"agent-state":    "started",
  1047  								"public-address": "dummyenv-2.dns",
  1048  							},
  1049  						},
  1050  						"relations": M{
  1051  							"server": L{"private", "project"},
  1052  						},
  1053  					},
  1054  					"varnish": M{
  1055  						"charm":   "cs:quantal/varnish-1",
  1056  						"exposed": true,
  1057  						"units": M{
  1058  							"varnish/0": M{
  1059  								"machine":        "3",
  1060  								"agent-state":    "allocating",
  1061  								"public-address": "dummyenv-3.dns",
  1062  							},
  1063  						},
  1064  						"relations": M{
  1065  							"webcache": L{"project"},
  1066  						},
  1067  					},
  1068  					"private": M{
  1069  						"charm":   "cs:quantal/wordpress-3",
  1070  						"exposed": true,
  1071  						"units": M{
  1072  							"private/0": M{
  1073  								"machine":        "4",
  1074  								"agent-state":    "allocating",
  1075  								"public-address": "dummyenv-4.dns",
  1076  							},
  1077  						},
  1078  						"relations": M{
  1079  							"db": L{"mysql"},
  1080  						},
  1081  					},
  1082  				},
  1083  			},
  1084  		},
  1085  	), test(
  1086  		"simple peer scenario",
  1087  		addMachine{machineId: "0", job: state.JobManageEnviron},
  1088  		setAddresses{"0", []network.Address{network.NewAddress("dummyenv-0.dns", network.ScopeUnknown)}},
  1089  		startAliveMachine{"0"},
  1090  		setMachineStatus{"0", state.StatusStarted, ""},
  1091  		addCharm{"riak"},
  1092  		addCharm{"wordpress"},
  1093  
  1094  		addService{name: "riak", charm: "riak"},
  1095  		setServiceExposed{"riak", true},
  1096  		addMachine{machineId: "1", job: state.JobHostUnits},
  1097  		setAddresses{"1", []network.Address{network.NewAddress("dummyenv-1.dns", network.ScopeUnknown)}},
  1098  		startAliveMachine{"1"},
  1099  		setMachineStatus{"1", state.StatusStarted, ""},
  1100  		addAliveUnit{"riak", "1"},
  1101  		setUnitStatus{"riak/0", state.StatusActive, "", nil},
  1102  		addMachine{machineId: "2", job: state.JobHostUnits},
  1103  		setAddresses{"2", []network.Address{network.NewAddress("dummyenv-2.dns", network.ScopeUnknown)}},
  1104  		startAliveMachine{"2"},
  1105  		setMachineStatus{"2", state.StatusStarted, ""},
  1106  		addAliveUnit{"riak", "2"},
  1107  		setUnitStatus{"riak/1", state.StatusActive, "", nil},
  1108  		addMachine{machineId: "3", job: state.JobHostUnits},
  1109  		setAddresses{"3", []network.Address{network.NewAddress("dummyenv-3.dns", network.ScopeUnknown)}},
  1110  		startAliveMachine{"3"},
  1111  		setMachineStatus{"3", state.StatusStarted, ""},
  1112  		addAliveUnit{"riak", "3"},
  1113  		setUnitStatus{"riak/2", state.StatusActive, "", nil},
  1114  
  1115  		expect{
  1116  			"multiples related peer units",
  1117  			M{
  1118  				"environment": "dummyenv",
  1119  				"machines": M{
  1120  					"0": machine0,
  1121  					"1": machine1,
  1122  					"2": machine2,
  1123  					"3": machine3,
  1124  				},
  1125  				"services": M{
  1126  					"riak": M{
  1127  						"charm":   "cs:quantal/riak-7",
  1128  						"exposed": true,
  1129  						"units": M{
  1130  							"riak/0": M{
  1131  								"machine":        "1",
  1132  								"agent-state":    "started",
  1133  								"public-address": "dummyenv-1.dns",
  1134  							},
  1135  							"riak/1": M{
  1136  								"machine":        "2",
  1137  								"agent-state":    "started",
  1138  								"public-address": "dummyenv-2.dns",
  1139  							},
  1140  							"riak/2": M{
  1141  								"machine":        "3",
  1142  								"agent-state":    "started",
  1143  								"public-address": "dummyenv-3.dns",
  1144  							},
  1145  						},
  1146  						"relations": M{
  1147  							"ring": L{"riak"},
  1148  						},
  1149  					},
  1150  				},
  1151  			},
  1152  		},
  1153  	),
  1154  
  1155  	// Subordinate tests
  1156  	test(
  1157  		"one service with one subordinate service",
  1158  		addMachine{machineId: "0", job: state.JobManageEnviron},
  1159  		setAddresses{"0", []network.Address{network.NewAddress("dummyenv-0.dns", network.ScopeUnknown)}},
  1160  		startAliveMachine{"0"},
  1161  		setMachineStatus{"0", state.StatusStarted, ""},
  1162  		addCharm{"wordpress"},
  1163  		addCharm{"mysql"},
  1164  		addCharm{"logging"},
  1165  
  1166  		addService{name: "wordpress", charm: "wordpress"},
  1167  		setServiceExposed{"wordpress", true},
  1168  		addMachine{machineId: "1", job: state.JobHostUnits},
  1169  		setAddresses{"1", []network.Address{network.NewAddress("dummyenv-1.dns", network.ScopeUnknown)}},
  1170  		startAliveMachine{"1"},
  1171  		setMachineStatus{"1", state.StatusStarted, ""},
  1172  		addAliveUnit{"wordpress", "1"},
  1173  		setUnitStatus{"wordpress/0", state.StatusActive, "", nil},
  1174  
  1175  		addService{name: "mysql", charm: "mysql"},
  1176  		setServiceExposed{"mysql", true},
  1177  		addMachine{machineId: "2", job: state.JobHostUnits},
  1178  		setAddresses{"2", []network.Address{network.NewAddress("dummyenv-2.dns", network.ScopeUnknown)}},
  1179  		startAliveMachine{"2"},
  1180  		setMachineStatus{"2", state.StatusStarted, ""},
  1181  		addAliveUnit{"mysql", "2"},
  1182  		setUnitStatus{"mysql/0", state.StatusActive, "", nil},
  1183  
  1184  		addService{name: "logging", charm: "logging"},
  1185  		setServiceExposed{"logging", true},
  1186  
  1187  		relateServices{"wordpress", "mysql"},
  1188  		relateServices{"wordpress", "logging"},
  1189  		relateServices{"mysql", "logging"},
  1190  
  1191  		addSubordinate{"wordpress/0", "logging"},
  1192  		addSubordinate{"mysql/0", "logging"},
  1193  
  1194  		setUnitsAlive{"logging"},
  1195  		setUnitStatus{"logging/0", state.StatusActive, "", nil},
  1196  		setUnitStatus{"logging/1", state.StatusError, "somehow lost in all those logs", nil},
  1197  
  1198  		expect{
  1199  			"multiples related peer units",
  1200  			M{
  1201  				"environment": "dummyenv",
  1202  				"machines": M{
  1203  					"0": machine0,
  1204  					"1": machine1,
  1205  					"2": machine2,
  1206  				},
  1207  				"services": M{
  1208  					"wordpress": M{
  1209  						"charm":   "cs:quantal/wordpress-3",
  1210  						"exposed": true,
  1211  						"units": M{
  1212  							"wordpress/0": M{
  1213  								"machine":     "1",
  1214  								"agent-state": "started",
  1215  								"subordinates": M{
  1216  									"logging/0": M{
  1217  										"agent-state":    "started",
  1218  										"public-address": "dummyenv-1.dns",
  1219  									},
  1220  								},
  1221  								"public-address": "dummyenv-1.dns",
  1222  							},
  1223  						},
  1224  						"relations": M{
  1225  							"db":          L{"mysql"},
  1226  							"logging-dir": L{"logging"},
  1227  						},
  1228  					},
  1229  					"mysql": M{
  1230  						"charm":   "cs:quantal/mysql-1",
  1231  						"exposed": true,
  1232  						"units": M{
  1233  							"mysql/0": M{
  1234  								"machine":     "2",
  1235  								"agent-state": "started",
  1236  								"subordinates": M{
  1237  									"logging/1": M{
  1238  										"agent-state":      "error",
  1239  										"agent-state-info": "somehow lost in all those logs",
  1240  										"public-address":   "dummyenv-2.dns",
  1241  									},
  1242  								},
  1243  								"public-address": "dummyenv-2.dns",
  1244  							},
  1245  						},
  1246  						"relations": M{
  1247  							"server":    L{"wordpress"},
  1248  							"juju-info": L{"logging"},
  1249  						},
  1250  					},
  1251  					"logging": M{
  1252  						"charm":   "cs:quantal/logging-1",
  1253  						"exposed": true,
  1254  						"relations": M{
  1255  							"logging-directory": L{"wordpress"},
  1256  							"info":              L{"mysql"},
  1257  						},
  1258  						"subordinate-to": L{"mysql", "wordpress"},
  1259  					},
  1260  				},
  1261  			},
  1262  		},
  1263  
  1264  		// scoped on 'logging'
  1265  		scopedExpect{
  1266  			"subordinates scoped on logging",
  1267  			[]string{"logging"},
  1268  			M{
  1269  				"environment": "dummyenv",
  1270  				"machines": M{
  1271  					"1": machine1,
  1272  					"2": machine2,
  1273  				},
  1274  				"services": M{
  1275  					"wordpress": M{
  1276  						"charm":   "cs:quantal/wordpress-3",
  1277  						"exposed": true,
  1278  						"units": M{
  1279  							"wordpress/0": M{
  1280  								"machine":     "1",
  1281  								"agent-state": "started",
  1282  								"subordinates": M{
  1283  									"logging/0": M{
  1284  										"agent-state":    "started",
  1285  										"public-address": "dummyenv-1.dns",
  1286  									},
  1287  								},
  1288  								"public-address": "dummyenv-1.dns",
  1289  							},
  1290  						},
  1291  						"relations": M{
  1292  							"db":          L{"mysql"},
  1293  							"logging-dir": L{"logging"},
  1294  						},
  1295  					},
  1296  					"mysql": M{
  1297  						"charm":   "cs:quantal/mysql-1",
  1298  						"exposed": true,
  1299  						"units": M{
  1300  							"mysql/0": M{
  1301  								"machine":     "2",
  1302  								"agent-state": "started",
  1303  								"subordinates": M{
  1304  									"logging/1": M{
  1305  										"agent-state":      "error",
  1306  										"agent-state-info": "somehow lost in all those logs",
  1307  										"public-address":   "dummyenv-2.dns",
  1308  									},
  1309  								},
  1310  								"public-address": "dummyenv-2.dns",
  1311  							},
  1312  						},
  1313  						"relations": M{
  1314  							"server":    L{"wordpress"},
  1315  							"juju-info": L{"logging"},
  1316  						},
  1317  					},
  1318  					"logging": M{
  1319  						"charm":   "cs:quantal/logging-1",
  1320  						"exposed": true,
  1321  						"relations": M{
  1322  							"logging-directory": L{"wordpress"},
  1323  							"info":              L{"mysql"},
  1324  						},
  1325  						"subordinate-to": L{"mysql", "wordpress"},
  1326  					},
  1327  				},
  1328  			},
  1329  		},
  1330  
  1331  		// scoped on wordpress/0
  1332  		scopedExpect{
  1333  			"subordinates scoped on logging",
  1334  			[]string{"wordpress/0"},
  1335  			M{
  1336  				"environment": "dummyenv",
  1337  				"machines": M{
  1338  					"1": machine1,
  1339  				},
  1340  				"services": M{
  1341  					"wordpress": M{
  1342  						"charm":   "cs:quantal/wordpress-3",
  1343  						"exposed": true,
  1344  						"units": M{
  1345  							"wordpress/0": M{
  1346  								"machine":     "1",
  1347  								"agent-state": "started",
  1348  								"subordinates": M{
  1349  									"logging/0": M{
  1350  										"agent-state":    "started",
  1351  										"public-address": "dummyenv-1.dns",
  1352  									},
  1353  								},
  1354  								"public-address": "dummyenv-1.dns",
  1355  							},
  1356  						},
  1357  						"relations": M{
  1358  							"db":          L{"mysql"},
  1359  							"logging-dir": L{"logging"},
  1360  						},
  1361  					},
  1362  					"logging": M{
  1363  						"charm":   "cs:quantal/logging-1",
  1364  						"exposed": true,
  1365  						"relations": M{
  1366  							"logging-directory": L{"wordpress"},
  1367  							"info":              L{"mysql"},
  1368  						},
  1369  						"subordinate-to": L{"mysql", "wordpress"},
  1370  					},
  1371  				},
  1372  			},
  1373  		},
  1374  	),
  1375  	test(
  1376  		"machines with containers",
  1377  		addMachine{machineId: "0", job: state.JobManageEnviron},
  1378  		setAddresses{"0", []network.Address{network.NewAddress("dummyenv-0.dns", network.ScopeUnknown)}},
  1379  		startAliveMachine{"0"},
  1380  		setMachineStatus{"0", state.StatusStarted, ""},
  1381  		addCharm{"mysql"},
  1382  		addService{name: "mysql", charm: "mysql"},
  1383  		setServiceExposed{"mysql", true},
  1384  
  1385  		addMachine{machineId: "1", job: state.JobHostUnits},
  1386  		setAddresses{"1", []network.Address{network.NewAddress("dummyenv-1.dns", network.ScopeUnknown)}},
  1387  		startAliveMachine{"1"},
  1388  		setMachineStatus{"1", state.StatusStarted, ""},
  1389  		addAliveUnit{"mysql", "1"},
  1390  		setUnitStatus{"mysql/0", state.StatusActive, "", nil},
  1391  
  1392  		// A container on machine 1.
  1393  		addContainer{"1", "1/lxc/0", state.JobHostUnits},
  1394  		setAddresses{"1/lxc/0", []network.Address{network.NewAddress("dummyenv-2.dns", network.ScopeUnknown)}},
  1395  		startAliveMachine{"1/lxc/0"},
  1396  		setMachineStatus{"1/lxc/0", state.StatusStarted, ""},
  1397  		addAliveUnit{"mysql", "1/lxc/0"},
  1398  		setUnitStatus{"mysql/1", state.StatusActive, "", nil},
  1399  		addContainer{"1", "1/lxc/1", state.JobHostUnits},
  1400  
  1401  		// A nested container.
  1402  		addContainer{"1/lxc/0", "1/lxc/0/lxc/0", state.JobHostUnits},
  1403  		setAddresses{"1/lxc/0/lxc/0", []network.Address{network.NewAddress("dummyenv-3.dns", network.ScopeUnknown)}},
  1404  		startAliveMachine{"1/lxc/0/lxc/0"},
  1405  		setMachineStatus{"1/lxc/0/lxc/0", state.StatusStarted, ""},
  1406  
  1407  		expect{
  1408  			"machines with nested containers",
  1409  			M{
  1410  				"environment": "dummyenv",
  1411  				"machines": M{
  1412  					"0": machine0,
  1413  					"1": machine1WithContainers,
  1414  				},
  1415  				"services": M{
  1416  					"mysql": M{
  1417  						"charm":   "cs:quantal/mysql-1",
  1418  						"exposed": true,
  1419  						"units": M{
  1420  							"mysql/0": M{
  1421  								"machine":        "1",
  1422  								"agent-state":    "started",
  1423  								"public-address": "dummyenv-1.dns",
  1424  							},
  1425  							"mysql/1": M{
  1426  								"machine":        "1/lxc/0",
  1427  								"agent-state":    "started",
  1428  								"public-address": "dummyenv-2.dns",
  1429  							},
  1430  						},
  1431  					},
  1432  				},
  1433  			},
  1434  		},
  1435  
  1436  		// once again, with a scope on mysql/1
  1437  		scopedExpect{
  1438  			"machines with nested containers",
  1439  			[]string{"mysql/1"},
  1440  			M{
  1441  				"environment": "dummyenv",
  1442  				"machines": M{
  1443  					"1": M{
  1444  						"agent-state": "started",
  1445  						"containers": M{
  1446  							"1/lxc/0": M{
  1447  								"agent-state": "started",
  1448  								"dns-name":    "dummyenv-2.dns",
  1449  								"instance-id": "dummyenv-2",
  1450  								"series":      "quantal",
  1451  							},
  1452  						},
  1453  						"dns-name":    "dummyenv-1.dns",
  1454  						"instance-id": "dummyenv-1",
  1455  						"series":      "quantal",
  1456  						"hardware":    "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M",
  1457  					},
  1458  				},
  1459  				"services": M{
  1460  					"mysql": M{
  1461  						"charm":   "cs:quantal/mysql-1",
  1462  						"exposed": true,
  1463  						"units": M{
  1464  							"mysql/1": M{
  1465  								"machine":        "1/lxc/0",
  1466  								"agent-state":    "started",
  1467  								"public-address": "dummyenv-2.dns",
  1468  							},
  1469  						},
  1470  					},
  1471  				},
  1472  			},
  1473  		},
  1474  	), test(
  1475  		"service with out of date charm",
  1476  		addMachine{machineId: "0", job: state.JobManageEnviron},
  1477  		setAddresses{"0", []network.Address{network.NewAddress("dummyenv-0.dns", network.ScopeUnknown)}},
  1478  		startAliveMachine{"0"},
  1479  		setMachineStatus{"0", state.StatusStarted, ""},
  1480  		addMachine{machineId: "1", job: state.JobHostUnits},
  1481  		setAddresses{"1", []network.Address{network.NewAddress("dummyenv-1.dns", network.ScopeUnknown)}},
  1482  		startAliveMachine{"1"},
  1483  		setMachineStatus{"1", state.StatusStarted, ""},
  1484  		addCharm{"mysql"},
  1485  		addService{name: "mysql", charm: "mysql"},
  1486  		setServiceExposed{"mysql", true},
  1487  		addCharmPlaceholder{"mysql", 23},
  1488  		addAliveUnit{"mysql", "1"},
  1489  
  1490  		expect{
  1491  			"services and units with correct charm status",
  1492  			M{
  1493  				"environment": "dummyenv",
  1494  				"machines": M{
  1495  					"0": machine0,
  1496  					"1": machine1,
  1497  				},
  1498  				"services": M{
  1499  					"mysql": M{
  1500  						"charm":          "cs:quantal/mysql-1",
  1501  						"can-upgrade-to": "cs:quantal/mysql-23",
  1502  						"exposed":        true,
  1503  						"units": M{
  1504  							"mysql/0": M{
  1505  								"machine":        "1",
  1506  								"agent-state":    "allocating",
  1507  								"public-address": "dummyenv-1.dns",
  1508  							},
  1509  						},
  1510  					},
  1511  				},
  1512  			},
  1513  		},
  1514  	), test(
  1515  		"unit with out of date charm",
  1516  		addMachine{machineId: "0", job: state.JobManageEnviron},
  1517  		setAddresses{"0", []network.Address{network.NewAddress("dummyenv-0.dns", network.ScopeUnknown)}},
  1518  		startAliveMachine{"0"},
  1519  		setMachineStatus{"0", state.StatusStarted, ""},
  1520  		addMachine{machineId: "1", job: state.JobHostUnits},
  1521  		setAddresses{"1", []network.Address{network.NewAddress("dummyenv-1.dns", network.ScopeUnknown)}},
  1522  		startAliveMachine{"1"},
  1523  		setMachineStatus{"1", state.StatusStarted, ""},
  1524  		addCharm{"mysql"},
  1525  		addService{name: "mysql", charm: "mysql"},
  1526  		setServiceExposed{"mysql", true},
  1527  		addAliveUnit{"mysql", "1"},
  1528  		setUnitCharmURL{"mysql/0", "cs:quantal/mysql-1"},
  1529  		addCharmWithRevision{addCharm{"mysql"}, "local", 1},
  1530  		setServiceCharm{"mysql", "local:quantal/mysql-1"},
  1531  
  1532  		expect{
  1533  			"services and units with correct charm status",
  1534  			M{
  1535  				"environment": "dummyenv",
  1536  				"machines": M{
  1537  					"0": machine0,
  1538  					"1": machine1,
  1539  				},
  1540  				"services": M{
  1541  					"mysql": M{
  1542  						"charm":   "local:quantal/mysql-1",
  1543  						"exposed": true,
  1544  						"units": M{
  1545  							"mysql/0": M{
  1546  								"machine":        "1",
  1547  								"agent-state":    "started",
  1548  								"upgrading-from": "cs:quantal/mysql-1",
  1549  								"public-address": "dummyenv-1.dns",
  1550  							},
  1551  						},
  1552  					},
  1553  				},
  1554  			},
  1555  		},
  1556  	), test(
  1557  		"service and unit with out of date charms",
  1558  		addMachine{machineId: "0", job: state.JobManageEnviron},
  1559  		setAddresses{"0", []network.Address{network.NewAddress("dummyenv-0.dns", network.ScopeUnknown)}},
  1560  		startAliveMachine{"0"},
  1561  		setMachineStatus{"0", state.StatusStarted, ""},
  1562  		addMachine{machineId: "1", job: state.JobHostUnits},
  1563  		setAddresses{"1", []network.Address{network.NewAddress("dummyenv-1.dns", network.ScopeUnknown)}},
  1564  		startAliveMachine{"1"},
  1565  		setMachineStatus{"1", state.StatusStarted, ""},
  1566  		addCharm{"mysql"},
  1567  		addService{name: "mysql", charm: "mysql"},
  1568  		setServiceExposed{"mysql", true},
  1569  		addAliveUnit{"mysql", "1"},
  1570  		setUnitCharmURL{"mysql/0", "cs:quantal/mysql-1"},
  1571  		addCharmWithRevision{addCharm{"mysql"}, "cs", 2},
  1572  		setServiceCharm{"mysql", "cs:quantal/mysql-2"},
  1573  		addCharmPlaceholder{"mysql", 23},
  1574  
  1575  		expect{
  1576  			"services and units with correct charm status",
  1577  			M{
  1578  				"environment": "dummyenv",
  1579  				"machines": M{
  1580  					"0": machine0,
  1581  					"1": machine1,
  1582  				},
  1583  				"services": M{
  1584  					"mysql": M{
  1585  						"charm":          "cs:quantal/mysql-2",
  1586  						"can-upgrade-to": "cs:quantal/mysql-23",
  1587  						"exposed":        true,
  1588  						"units": M{
  1589  							"mysql/0": M{
  1590  								"machine":        "1",
  1591  								"agent-state":    "started",
  1592  								"upgrading-from": "cs:quantal/mysql-1",
  1593  								"public-address": "dummyenv-1.dns",
  1594  							},
  1595  						},
  1596  					},
  1597  				},
  1598  			},
  1599  		},
  1600  	), test(
  1601  		"service with local charm not shown as out of date",
  1602  		addMachine{machineId: "0", job: state.JobManageEnviron},
  1603  		setAddresses{"0", []network.Address{network.NewAddress("dummyenv-0.dns", network.ScopeUnknown)}},
  1604  		startAliveMachine{"0"},
  1605  		setMachineStatus{"0", state.StatusStarted, ""},
  1606  		addMachine{machineId: "1", job: state.JobHostUnits},
  1607  		setAddresses{"1", []network.Address{network.NewAddress("dummyenv-1.dns", network.ScopeUnknown)}},
  1608  		startAliveMachine{"1"},
  1609  		setMachineStatus{"1", state.StatusStarted, ""},
  1610  		addCharm{"mysql"},
  1611  		addService{name: "mysql", charm: "mysql"},
  1612  		setServiceExposed{"mysql", true},
  1613  		addAliveUnit{"mysql", "1"},
  1614  		setUnitCharmURL{"mysql/0", "cs:quantal/mysql-1"},
  1615  		addCharmWithRevision{addCharm{"mysql"}, "local", 1},
  1616  		setServiceCharm{"mysql", "local:quantal/mysql-1"},
  1617  		addCharmPlaceholder{"mysql", 23},
  1618  
  1619  		expect{
  1620  			"services and units with correct charm status",
  1621  			M{
  1622  				"environment": "dummyenv",
  1623  				"machines": M{
  1624  					"0": machine0,
  1625  					"1": machine1,
  1626  				},
  1627  				"services": M{
  1628  					"mysql": M{
  1629  						"charm":   "local:quantal/mysql-1",
  1630  						"exposed": true,
  1631  						"units": M{
  1632  							"mysql/0": M{
  1633  								"machine":        "1",
  1634  								"agent-state":    "started",
  1635  								"upgrading-from": "cs:quantal/mysql-1",
  1636  								"public-address": "dummyenv-1.dns",
  1637  							},
  1638  						},
  1639  					},
  1640  				},
  1641  			},
  1642  		},
  1643  	),
  1644  }
  1645  
  1646  // TODO(dfc) test failing components by destructively mutating the state under the hood
  1647  
  1648  type addMachine struct {
  1649  	machineId string
  1650  	cons      constraints.Value
  1651  	job       state.MachineJob
  1652  }
  1653  
  1654  func (am addMachine) step(c *gc.C, ctx *context) {
  1655  	m, err := ctx.st.AddOneMachine(state.MachineTemplate{
  1656  		Series:      "quantal",
  1657  		Constraints: am.cons,
  1658  		Jobs:        []state.MachineJob{am.job},
  1659  	})
  1660  	c.Assert(err, jc.ErrorIsNil)
  1661  	c.Assert(m.Id(), gc.Equals, am.machineId)
  1662  }
  1663  
  1664  type addNetwork struct {
  1665  	name       string
  1666  	providerId network.Id
  1667  	cidr       string
  1668  	vlanTag    int
  1669  }
  1670  
  1671  func (an addNetwork) step(c *gc.C, ctx *context) {
  1672  	n, err := ctx.st.AddNetwork(state.NetworkInfo{
  1673  		Name:       an.name,
  1674  		ProviderId: an.providerId,
  1675  		CIDR:       an.cidr,
  1676  		VLANTag:    an.vlanTag,
  1677  	})
  1678  	c.Assert(err, jc.ErrorIsNil)
  1679  	c.Assert(n.Name(), gc.Equals, an.name)
  1680  }
  1681  
  1682  type addContainer struct {
  1683  	parentId  string
  1684  	machineId string
  1685  	job       state.MachineJob
  1686  }
  1687  
  1688  func (ac addContainer) step(c *gc.C, ctx *context) {
  1689  	template := state.MachineTemplate{
  1690  		Series: "quantal",
  1691  		Jobs:   []state.MachineJob{ac.job},
  1692  	}
  1693  	m, err := ctx.st.AddMachineInsideMachine(template, ac.parentId, instance.LXC)
  1694  	c.Assert(err, jc.ErrorIsNil)
  1695  	c.Assert(m.Id(), gc.Equals, ac.machineId)
  1696  }
  1697  
  1698  type startMachine struct {
  1699  	machineId string
  1700  }
  1701  
  1702  func (sm startMachine) step(c *gc.C, ctx *context) {
  1703  	m, err := ctx.st.Machine(sm.machineId)
  1704  	c.Assert(err, jc.ErrorIsNil)
  1705  	cons, err := m.Constraints()
  1706  	c.Assert(err, jc.ErrorIsNil)
  1707  	inst, hc := testing.AssertStartInstanceWithConstraints(c, ctx.env, m.Id(), cons)
  1708  	err = m.SetProvisioned(inst.Id(), "fake_nonce", hc)
  1709  	c.Assert(err, jc.ErrorIsNil)
  1710  }
  1711  
  1712  type startMissingMachine struct {
  1713  	machineId string
  1714  }
  1715  
  1716  func (sm startMissingMachine) step(c *gc.C, ctx *context) {
  1717  	m, err := ctx.st.Machine(sm.machineId)
  1718  	c.Assert(err, jc.ErrorIsNil)
  1719  	cons, err := m.Constraints()
  1720  	c.Assert(err, jc.ErrorIsNil)
  1721  	_, hc := testing.AssertStartInstanceWithConstraints(c, ctx.env, m.Id(), cons)
  1722  	err = m.SetProvisioned("i-missing", "fake_nonce", hc)
  1723  	c.Assert(err, jc.ErrorIsNil)
  1724  	err = m.SetInstanceStatus("missing")
  1725  	c.Assert(err, jc.ErrorIsNil)
  1726  }
  1727  
  1728  type startAliveMachine struct {
  1729  	machineId string
  1730  }
  1731  
  1732  func (sam startAliveMachine) step(c *gc.C, ctx *context) {
  1733  	m, err := ctx.st.Machine(sam.machineId)
  1734  	c.Assert(err, jc.ErrorIsNil)
  1735  	pinger := ctx.setAgentPresence(c, m)
  1736  	cons, err := m.Constraints()
  1737  	c.Assert(err, jc.ErrorIsNil)
  1738  	inst, hc := testing.AssertStartInstanceWithConstraints(c, ctx.env, m.Id(), cons)
  1739  	err = m.SetProvisioned(inst.Id(), "fake_nonce", hc)
  1740  	c.Assert(err, jc.ErrorIsNil)
  1741  	ctx.pingers[m.Id()] = pinger
  1742  }
  1743  
  1744  type setAddresses struct {
  1745  	machineId string
  1746  	addresses []network.Address
  1747  }
  1748  
  1749  func (sa setAddresses) step(c *gc.C, ctx *context) {
  1750  	m, err := ctx.st.Machine(sa.machineId)
  1751  	c.Assert(err, jc.ErrorIsNil)
  1752  	err = m.SetAddresses(sa.addresses...)
  1753  	c.Assert(err, jc.ErrorIsNil)
  1754  }
  1755  
  1756  type setTools struct {
  1757  	machineId string
  1758  	version   version.Binary
  1759  }
  1760  
  1761  func (st setTools) step(c *gc.C, ctx *context) {
  1762  	m, err := ctx.st.Machine(st.machineId)
  1763  	c.Assert(err, jc.ErrorIsNil)
  1764  	err = m.SetAgentVersion(st.version)
  1765  	c.Assert(err, jc.ErrorIsNil)
  1766  }
  1767  
  1768  type addCharm struct {
  1769  	name string
  1770  }
  1771  
  1772  func (ac addCharm) addCharmStep(c *gc.C, ctx *context, scheme string, rev int) {
  1773  	ch := testcharms.Repo.CharmDir(ac.name)
  1774  	name := ch.Meta().Name
  1775  	curl := charm.MustParseURL(fmt.Sprintf("%s:quantal/%s-%d", scheme, name, rev))
  1776  	dummy, err := ctx.st.AddCharm(ch, curl, "dummy-path", fmt.Sprintf("%s-%d-sha256", name, rev))
  1777  	c.Assert(err, jc.ErrorIsNil)
  1778  	ctx.charms[ac.name] = dummy
  1779  }
  1780  
  1781  func (ac addCharm) step(c *gc.C, ctx *context) {
  1782  	ch := testcharms.Repo.CharmDir(ac.name)
  1783  	ac.addCharmStep(c, ctx, "cs", ch.Revision())
  1784  }
  1785  
  1786  type addCharmWithRevision struct {
  1787  	addCharm
  1788  	scheme string
  1789  	rev    int
  1790  }
  1791  
  1792  func (ac addCharmWithRevision) step(c *gc.C, ctx *context) {
  1793  	ac.addCharmStep(c, ctx, ac.scheme, ac.rev)
  1794  }
  1795  
  1796  type addService struct {
  1797  	name     string
  1798  	charm    string
  1799  	networks []string
  1800  	cons     constraints.Value
  1801  }
  1802  
  1803  func (as addService) step(c *gc.C, ctx *context) {
  1804  	ch, ok := ctx.charms[as.charm]
  1805  	c.Assert(ok, jc.IsTrue)
  1806  	svc, err := ctx.st.AddService(as.name, ctx.adminUserTag, ch, as.networks, nil)
  1807  	c.Assert(err, jc.ErrorIsNil)
  1808  	if svc.IsPrincipal() {
  1809  		err = svc.SetConstraints(as.cons)
  1810  		c.Assert(err, jc.ErrorIsNil)
  1811  	}
  1812  }
  1813  
  1814  type setServiceExposed struct {
  1815  	name    string
  1816  	exposed bool
  1817  }
  1818  
  1819  func (sse setServiceExposed) step(c *gc.C, ctx *context) {
  1820  	s, err := ctx.st.Service(sse.name)
  1821  	c.Assert(err, jc.ErrorIsNil)
  1822  	err = s.ClearExposed()
  1823  	c.Assert(err, jc.ErrorIsNil)
  1824  	if sse.exposed {
  1825  		err = s.SetExposed()
  1826  		c.Assert(err, jc.ErrorIsNil)
  1827  	}
  1828  }
  1829  
  1830  type setServiceCharm struct {
  1831  	name  string
  1832  	charm string
  1833  }
  1834  
  1835  func (ssc setServiceCharm) step(c *gc.C, ctx *context) {
  1836  	ch, err := ctx.st.Charm(charm.MustParseURL(ssc.charm))
  1837  	c.Assert(err, jc.ErrorIsNil)
  1838  	s, err := ctx.st.Service(ssc.name)
  1839  	c.Assert(err, jc.ErrorIsNil)
  1840  	err = s.SetCharm(ch, false)
  1841  	c.Assert(err, jc.ErrorIsNil)
  1842  }
  1843  
  1844  type addCharmPlaceholder struct {
  1845  	name string
  1846  	rev  int
  1847  }
  1848  
  1849  func (ac addCharmPlaceholder) step(c *gc.C, ctx *context) {
  1850  	ch := testcharms.Repo.CharmDir(ac.name)
  1851  	name := ch.Meta().Name
  1852  	curl := charm.MustParseURL(fmt.Sprintf("cs:quantal/%s-%d", name, ac.rev))
  1853  	err := ctx.st.AddStoreCharmPlaceholder(curl)
  1854  	c.Assert(err, jc.ErrorIsNil)
  1855  }
  1856  
  1857  type addUnit struct {
  1858  	serviceName string
  1859  	machineId   string
  1860  }
  1861  
  1862  func (au addUnit) step(c *gc.C, ctx *context) {
  1863  	s, err := ctx.st.Service(au.serviceName)
  1864  	c.Assert(err, jc.ErrorIsNil)
  1865  	u, err := s.AddUnit()
  1866  	c.Assert(err, jc.ErrorIsNil)
  1867  	m, err := ctx.st.Machine(au.machineId)
  1868  	c.Assert(err, jc.ErrorIsNil)
  1869  	err = u.AssignToMachine(m)
  1870  	c.Assert(err, jc.ErrorIsNil)
  1871  }
  1872  
  1873  type addAliveUnit struct {
  1874  	serviceName string
  1875  	machineId   string
  1876  }
  1877  
  1878  func (aau addAliveUnit) step(c *gc.C, ctx *context) {
  1879  	s, err := ctx.st.Service(aau.serviceName)
  1880  	c.Assert(err, jc.ErrorIsNil)
  1881  	u, err := s.AddUnit()
  1882  	c.Assert(err, jc.ErrorIsNil)
  1883  	pinger := ctx.setAgentPresence(c, u)
  1884  	m, err := ctx.st.Machine(aau.machineId)
  1885  	c.Assert(err, jc.ErrorIsNil)
  1886  	err = u.AssignToMachine(m)
  1887  	c.Assert(err, jc.ErrorIsNil)
  1888  	ctx.pingers[u.Name()] = pinger
  1889  }
  1890  
  1891  type setUnitsAlive struct {
  1892  	serviceName string
  1893  }
  1894  
  1895  func (sua setUnitsAlive) step(c *gc.C, ctx *context) {
  1896  	s, err := ctx.st.Service(sua.serviceName)
  1897  	c.Assert(err, jc.ErrorIsNil)
  1898  	us, err := s.AllUnits()
  1899  	c.Assert(err, jc.ErrorIsNil)
  1900  	for _, u := range us {
  1901  		ctx.pingers[u.Name()] = ctx.setAgentPresence(c, u)
  1902  	}
  1903  }
  1904  
  1905  type setUnitStatus struct {
  1906  	unitName   string
  1907  	status     state.Status
  1908  	statusInfo string
  1909  	statusData map[string]interface{}
  1910  }
  1911  
  1912  func (sus setUnitStatus) step(c *gc.C, ctx *context) {
  1913  	u, err := ctx.st.Unit(sus.unitName)
  1914  	c.Assert(err, jc.ErrorIsNil)
  1915  	err = u.SetStatus(sus.status, sus.statusInfo, sus.statusData)
  1916  	c.Assert(err, jc.ErrorIsNil)
  1917  }
  1918  
  1919  type setUnitCharmURL struct {
  1920  	unitName string
  1921  	charm    string
  1922  }
  1923  
  1924  func (uc setUnitCharmURL) step(c *gc.C, ctx *context) {
  1925  	u, err := ctx.st.Unit(uc.unitName)
  1926  	c.Assert(err, jc.ErrorIsNil)
  1927  	curl := charm.MustParseURL(uc.charm)
  1928  	err = u.SetCharmURL(curl)
  1929  	c.Assert(err, jc.ErrorIsNil)
  1930  	err = u.SetStatus(state.StatusActive, "", nil)
  1931  	c.Assert(err, jc.ErrorIsNil)
  1932  }
  1933  
  1934  type openUnitPort struct {
  1935  	unitName string
  1936  	protocol string
  1937  	number   int
  1938  }
  1939  
  1940  func (oup openUnitPort) step(c *gc.C, ctx *context) {
  1941  	u, err := ctx.st.Unit(oup.unitName)
  1942  	c.Assert(err, jc.ErrorIsNil)
  1943  	err = u.OpenPort(oup.protocol, oup.number)
  1944  	c.Assert(err, jc.ErrorIsNil)
  1945  }
  1946  
  1947  type ensureDyingUnit struct {
  1948  	unitName string
  1949  }
  1950  
  1951  func (e ensureDyingUnit) step(c *gc.C, ctx *context) {
  1952  	u, err := ctx.st.Unit(e.unitName)
  1953  	c.Assert(err, jc.ErrorIsNil)
  1954  	err = u.Destroy()
  1955  	c.Assert(err, jc.ErrorIsNil)
  1956  	c.Assert(u.Life(), gc.Equals, state.Dying)
  1957  }
  1958  
  1959  type ensureDyingService struct {
  1960  	serviceName string
  1961  }
  1962  
  1963  func (e ensureDyingService) step(c *gc.C, ctx *context) {
  1964  	svc, err := ctx.st.Service(e.serviceName)
  1965  	c.Assert(err, jc.ErrorIsNil)
  1966  	err = svc.Destroy()
  1967  	c.Assert(err, jc.ErrorIsNil)
  1968  	err = svc.Refresh()
  1969  	c.Assert(err, jc.ErrorIsNil)
  1970  	c.Assert(svc.Life(), gc.Equals, state.Dying)
  1971  }
  1972  
  1973  type ensureDeadMachine struct {
  1974  	machineId string
  1975  }
  1976  
  1977  func (e ensureDeadMachine) step(c *gc.C, ctx *context) {
  1978  	m, err := ctx.st.Machine(e.machineId)
  1979  	c.Assert(err, jc.ErrorIsNil)
  1980  	err = m.EnsureDead()
  1981  	c.Assert(err, jc.ErrorIsNil)
  1982  	c.Assert(m.Life(), gc.Equals, state.Dead)
  1983  }
  1984  
  1985  type setMachineStatus struct {
  1986  	machineId  string
  1987  	status     state.Status
  1988  	statusInfo string
  1989  }
  1990  
  1991  func (sms setMachineStatus) step(c *gc.C, ctx *context) {
  1992  	m, err := ctx.st.Machine(sms.machineId)
  1993  	c.Assert(err, jc.ErrorIsNil)
  1994  	err = m.SetStatus(sms.status, sms.statusInfo, nil)
  1995  	c.Assert(err, jc.ErrorIsNil)
  1996  }
  1997  
  1998  type relateServices struct {
  1999  	ep1, ep2 string
  2000  }
  2001  
  2002  func (rs relateServices) step(c *gc.C, ctx *context) {
  2003  	eps, err := ctx.st.InferEndpoints(rs.ep1, rs.ep2)
  2004  	c.Assert(err, jc.ErrorIsNil)
  2005  	_, err = ctx.st.AddRelation(eps...)
  2006  	c.Assert(err, jc.ErrorIsNil)
  2007  }
  2008  
  2009  type addSubordinate struct {
  2010  	prinUnit   string
  2011  	subService string
  2012  }
  2013  
  2014  func (as addSubordinate) step(c *gc.C, ctx *context) {
  2015  	u, err := ctx.st.Unit(as.prinUnit)
  2016  	c.Assert(err, jc.ErrorIsNil)
  2017  	eps, err := ctx.st.InferEndpoints(u.ServiceName(), as.subService)
  2018  	c.Assert(err, jc.ErrorIsNil)
  2019  	rel, err := ctx.st.EndpointsRelation(eps...)
  2020  	c.Assert(err, jc.ErrorIsNil)
  2021  	ru, err := rel.Unit(u)
  2022  	c.Assert(err, jc.ErrorIsNil)
  2023  	err = ru.EnterScope(nil)
  2024  	c.Assert(err, jc.ErrorIsNil)
  2025  }
  2026  
  2027  type scopedExpect struct {
  2028  	what   string
  2029  	scope  []string
  2030  	output M
  2031  }
  2032  
  2033  type expect struct {
  2034  	what   string
  2035  	output M
  2036  }
  2037  
  2038  func (e scopedExpect) step(c *gc.C, ctx *context) {
  2039  	c.Logf("\nexpect: %s %s\n", e.what, strings.Join(e.scope, " "))
  2040  
  2041  	// Now execute the command for each format.
  2042  	for _, format := range statusFormats {
  2043  		c.Logf("format %q", format.name)
  2044  		// Run command with the required format.
  2045  		args := append([]string{"--format", format.name}, e.scope...)
  2046  		c.Logf("running status %s", strings.Join(args, " "))
  2047  		code, stdout, stderr := runStatus(c, args...)
  2048  		c.Assert(code, gc.Equals, 0)
  2049  		if !c.Check(stderr, gc.HasLen, 0) {
  2050  			c.Fatalf("status failed: %s", string(stderr))
  2051  		}
  2052  
  2053  		// Prepare the output in the same format.
  2054  		buf, err := format.marshal(e.output)
  2055  		c.Assert(err, jc.ErrorIsNil)
  2056  		expected := make(M)
  2057  		err = format.unmarshal(buf, &expected)
  2058  		c.Assert(err, jc.ErrorIsNil)
  2059  
  2060  		// Check the output is as expected.
  2061  		actual := make(M)
  2062  		err = format.unmarshal(stdout, &actual)
  2063  		c.Assert(err, jc.ErrorIsNil)
  2064  		c.Assert(actual, jc.DeepEquals, expected)
  2065  	}
  2066  }
  2067  
  2068  func (e expect) step(c *gc.C, ctx *context) {
  2069  	scopedExpect{e.what, nil, e.output}.step(c, ctx)
  2070  }
  2071  
  2072  func (s *StatusSuite) TestStatusAllFormats(c *gc.C) {
  2073  	for i, t := range statusTests {
  2074  		c.Logf("test %d: %s", i, t.summary)
  2075  		func(t testCase) {
  2076  			// Prepare context and run all steps to setup.
  2077  			ctx := s.newContext(c)
  2078  			defer s.resetContext(c, ctx)
  2079  			ctx.run(c, t.steps)
  2080  		}(t)
  2081  	}
  2082  }
  2083  
  2084  type fakeApiClient struct {
  2085  	statusReturn *api.Status
  2086  	patternsUsed []string
  2087  	closeCalled  bool
  2088  }
  2089  
  2090  func newFakeApiClient(statusReturn *api.Status) fakeApiClient {
  2091  	return fakeApiClient{
  2092  		statusReturn: statusReturn,
  2093  	}
  2094  }
  2095  
  2096  func (a *fakeApiClient) Status(patterns []string) (*api.Status, error) {
  2097  	a.patternsUsed = patterns
  2098  	return a.statusReturn, nil
  2099  }
  2100  
  2101  func (a *fakeApiClient) Close() error {
  2102  	a.closeCalled = true
  2103  	return nil
  2104  }
  2105  
  2106  // Check that the client works with an older server which doesn't
  2107  // return the top level Relations field nor the unit and machine level
  2108  // Agent field (they were introduced at the same time).
  2109  func (s *StatusSuite) TestStatusWithPreRelationsServer(c *gc.C) {
  2110  	// Construct an older style status response
  2111  	client := newFakeApiClient(&api.Status{
  2112  		EnvironmentName: "dummyenv",
  2113  		Machines: map[string]api.MachineStatus{
  2114  			"0": {
  2115  				// Agent field intentionally not set
  2116  				Id:             "0",
  2117  				InstanceId:     instance.Id("dummyenv-0"),
  2118  				AgentState:     "down",
  2119  				AgentStateInfo: "(started)",
  2120  				Series:         "quantal",
  2121  				Containers:     map[string]api.MachineStatus{},
  2122  				Jobs:           []multiwatcher.MachineJob{multiwatcher.JobManageEnviron},
  2123  				HasVote:        false,
  2124  				WantsVote:      true,
  2125  			},
  2126  			"1": {
  2127  				// Agent field intentionally not set
  2128  				Id:             "1",
  2129  				InstanceId:     instance.Id("dummyenv-1"),
  2130  				AgentState:     "started",
  2131  				AgentStateInfo: "hello",
  2132  				Series:         "quantal",
  2133  				Containers:     map[string]api.MachineStatus{},
  2134  				Jobs:           []multiwatcher.MachineJob{multiwatcher.JobHostUnits},
  2135  				HasVote:        false,
  2136  				WantsVote:      false,
  2137  			},
  2138  		},
  2139  		Services: map[string]api.ServiceStatus{
  2140  			"mysql": {
  2141  				Charm: "local:quantal/mysql-1",
  2142  				Relations: map[string][]string{
  2143  					"server": {"wordpress"},
  2144  				},
  2145  				Units: map[string]api.UnitStatus{
  2146  					"mysql/0": {
  2147  						// Agent field intentionally not set
  2148  						Machine:    "1",
  2149  						AgentState: "allocating",
  2150  					},
  2151  				},
  2152  			},
  2153  			"wordpress": {
  2154  				Charm: "local:quantal/wordpress-3",
  2155  				Relations: map[string][]string{
  2156  					"db": {"mysql"},
  2157  				},
  2158  				Units: map[string]api.UnitStatus{
  2159  					"wordpress/0": {
  2160  						// Agent field intentionally not set
  2161  						AgentState:     "error",
  2162  						AgentStateInfo: "blam",
  2163  						Machine:        "1",
  2164  					},
  2165  				},
  2166  			},
  2167  		},
  2168  		Networks: map[string]api.NetworkStatus{},
  2169  		// Relations field intentionally not set
  2170  	})
  2171  	s.PatchValue(&newApiClientForStatus, func(_ *StatusCommand) (statusAPI, error) {
  2172  		return &client, nil
  2173  	})
  2174  
  2175  	expected := expect{
  2176  		"sane output with an older client that doesn't return Agent or Relations fields",
  2177  		M{
  2178  			"environment": "dummyenv",
  2179  			"machines": M{
  2180  				"0": M{
  2181  					"agent-state":                "down",
  2182  					"agent-state-info":           "(started)",
  2183  					"instance-id":                "dummyenv-0",
  2184  					"series":                     "quantal",
  2185  					"state-server-member-status": "adding-vote",
  2186  				},
  2187  				"1": M{
  2188  					"agent-state":      "started",
  2189  					"agent-state-info": "hello",
  2190  					"instance-id":      "dummyenv-1",
  2191  					"series":           "quantal",
  2192  				},
  2193  			},
  2194  			"services": M{
  2195  				"mysql": M{
  2196  					"charm":   "local:quantal/mysql-1",
  2197  					"exposed": false,
  2198  					"relations": M{
  2199  						"server": L{"wordpress"},
  2200  					},
  2201  					"units": M{
  2202  						"mysql/0": M{
  2203  							"machine":     "1",
  2204  							"agent-state": "allocating",
  2205  						},
  2206  					},
  2207  				},
  2208  				"wordpress": M{
  2209  					"charm":   "local:quantal/wordpress-3",
  2210  					"exposed": false,
  2211  					"relations": M{
  2212  						"db": L{"mysql"},
  2213  					},
  2214  					"units": M{
  2215  						"wordpress/0": M{
  2216  							"machine":          "1",
  2217  							"agent-state":      "error",
  2218  							"agent-state-info": "blam",
  2219  						},
  2220  					},
  2221  				},
  2222  			},
  2223  		},
  2224  	}
  2225  	ctx := s.newContext(c)
  2226  	defer s.resetContext(c, ctx)
  2227  	ctx.run(c, []stepper{expected})
  2228  }
  2229  
  2230  func (s *StatusSuite) TestStatusWithFormatSummary(c *gc.C) {
  2231  	ctx := s.newContext(c)
  2232  	defer s.resetContext(c, ctx)
  2233  	steps := []stepper{
  2234  		addMachine{machineId: "0", job: state.JobManageEnviron},
  2235  		setAddresses{"0", []network.Address{network.NewAddress("localhost", network.ScopeUnknown)}},
  2236  		startAliveMachine{"0"},
  2237  		setMachineStatus{"0", state.StatusStarted, ""},
  2238  		addCharm{"wordpress"},
  2239  		addCharm{"mysql"},
  2240  		addCharm{"logging"},
  2241  		addService{name: "wordpress", charm: "wordpress"},
  2242  		setServiceExposed{"wordpress", true},
  2243  		addMachine{machineId: "1", job: state.JobHostUnits},
  2244  		setAddresses{"1", []network.Address{network.NewAddress("localhost", network.ScopeUnknown)}},
  2245  		startAliveMachine{"1"},
  2246  		setMachineStatus{"1", state.StatusStarted, ""},
  2247  		addAliveUnit{"wordpress", "1"},
  2248  		setUnitStatus{"wordpress/0", state.StatusActive, "", nil},
  2249  		addService{name: "mysql", charm: "mysql"},
  2250  		setServiceExposed{"mysql", true},
  2251  		addMachine{machineId: "2", job: state.JobHostUnits},
  2252  		setAddresses{"2", []network.Address{network.NewAddress("10.0.0.1", network.ScopeUnknown)}},
  2253  		startAliveMachine{"2"},
  2254  		setMachineStatus{"2", state.StatusStarted, ""},
  2255  		addAliveUnit{"mysql", "2"},
  2256  		setUnitStatus{"mysql/0", state.StatusActive, "", nil},
  2257  		addService{name: "logging", charm: "logging"},
  2258  		setServiceExposed{"logging", true},
  2259  		relateServices{"wordpress", "mysql"},
  2260  		relateServices{"wordpress", "logging"},
  2261  		relateServices{"mysql", "logging"},
  2262  		addSubordinate{"wordpress/0", "logging"},
  2263  		addSubordinate{"mysql/0", "logging"},
  2264  		setUnitsAlive{"logging"},
  2265  		setUnitStatus{"logging/0", state.StatusActive, "", nil},
  2266  		setUnitStatus{"logging/1", state.StatusError, "somehow lost in all those logs", nil},
  2267  	}
  2268  	for _, s := range steps {
  2269  		s.step(c, ctx)
  2270  	}
  2271  	code, stdout, stderr := runStatus(c, "--format", "summary")
  2272  	c.Check(code, gc.Equals, 0)
  2273  	c.Check(string(stderr), gc.Equals, "")
  2274  	c.Assert(
  2275  		string(stdout),
  2276  		gc.Equals,
  2277  		"Running on subnets: 127.0.0.1/8, 10.0.0.1/8 \n"+
  2278  			"Utilizing ports:                            \n"+
  2279  			" # MACHINES: (3)\n"+
  2280  			"    started:  3 \n"+
  2281  			"            \n"+
  2282  			"    # UNITS: (4)\n"+
  2283  			"      error:  1 \n"+
  2284  			"    started:  3 \n"+
  2285  			"            \n"+
  2286  			" # SERVICES:  (3)\n"+
  2287  			"     logging  1/1 exposed\n"+
  2288  			"       mysql  1/1 exposed\n"+
  2289  			"   wordpress  1/1 exposed\n"+
  2290  			"\n",
  2291  	)
  2292  }
  2293  func (s *StatusSuite) TestStatusWithFormatOneline(c *gc.C) {
  2294  	ctx := s.newContext(c)
  2295  	defer s.resetContext(c, ctx)
  2296  	steps := []stepper{
  2297  		addMachine{machineId: "0", job: state.JobManageEnviron},
  2298  		setAddresses{"0", []network.Address{network.NewAddress("dummyenv-0.dns", network.ScopeUnknown)}},
  2299  		startAliveMachine{"0"},
  2300  		setMachineStatus{"0", state.StatusStarted, ""},
  2301  		addCharm{"wordpress"},
  2302  		addCharm{"mysql"},
  2303  		addCharm{"logging"},
  2304  
  2305  		addService{name: "wordpress", charm: "wordpress"},
  2306  		setServiceExposed{"wordpress", true},
  2307  		addMachine{machineId: "1", job: state.JobHostUnits},
  2308  		setAddresses{"1", []network.Address{network.NewAddress("dummyenv-1.dns", network.ScopeUnknown)}},
  2309  		startAliveMachine{"1"},
  2310  		setMachineStatus{"1", state.StatusStarted, ""},
  2311  		addAliveUnit{"wordpress", "1"},
  2312  		setUnitStatus{"wordpress/0", state.StatusActive, "", nil},
  2313  
  2314  		addService{name: "mysql", charm: "mysql"},
  2315  		setServiceExposed{"mysql", true},
  2316  		addMachine{machineId: "2", job: state.JobHostUnits},
  2317  		setAddresses{"2", []network.Address{network.NewAddress("dummyenv-2.dns", network.ScopeUnknown)}},
  2318  		startAliveMachine{"2"},
  2319  		setMachineStatus{"2", state.StatusStarted, ""},
  2320  		addAliveUnit{"mysql", "2"},
  2321  		setUnitStatus{"mysql/0", state.StatusActive, "", nil},
  2322  
  2323  		addService{name: "logging", charm: "logging"},
  2324  		setServiceExposed{"logging", true},
  2325  
  2326  		relateServices{"wordpress", "mysql"},
  2327  		relateServices{"wordpress", "logging"},
  2328  		relateServices{"mysql", "logging"},
  2329  
  2330  		addSubordinate{"wordpress/0", "logging"},
  2331  		addSubordinate{"mysql/0", "logging"},
  2332  
  2333  		setUnitsAlive{"logging"},
  2334  		setUnitStatus{"logging/0", state.StatusActive, "", nil},
  2335  		setUnitStatus{"logging/1", state.StatusError, "somehow lost in all those logs", nil},
  2336  	}
  2337  
  2338  	ctx.run(c, steps)
  2339  
  2340  	const expected = `
  2341  - mysql/0: dummyenv-2.dns (started)
  2342    - logging/1: dummyenv-2.dns (error)
  2343  - wordpress/0: dummyenv-1.dns (started)
  2344    - logging/0: dummyenv-1.dns (started)
  2345  `
  2346  
  2347  	code, stdout, stderr := runStatus(c, "--format", "oneline")
  2348  	c.Check(code, gc.Equals, 0)
  2349  	c.Check(string(stderr), gc.Equals, "")
  2350  	c.Assert(string(stdout), gc.Equals, expected)
  2351  
  2352  	c.Log(`Check that "short" is an alias for oneline.`)
  2353  	code, stdout, stderr = runStatus(c, "--format", "short")
  2354  	c.Check(code, gc.Equals, 0)
  2355  	c.Check(string(stderr), gc.Equals, "")
  2356  	c.Assert(string(stdout), gc.Equals, expected)
  2357  
  2358  	c.Log(`Check that "line" is an alias for oneline.`)
  2359  	code, stdout, stderr = runStatus(c, "--format", "line")
  2360  	c.Check(code, gc.Equals, 0)
  2361  	c.Check(string(stderr), gc.Equals, "")
  2362  	c.Assert(string(stdout), gc.Equals, expected)
  2363  }
  2364  func (s *StatusSuite) TestStatusWithFormatTabular(c *gc.C) {
  2365  	ctx := s.newContext(c)
  2366  	defer s.resetContext(c, ctx)
  2367  	steps := []stepper{
  2368  		addMachine{machineId: "0", job: state.JobManageEnviron},
  2369  		setAddresses{"0", []network.Address{network.NewAddress("dummyenv-0.dns", network.ScopeUnknown)}},
  2370  		startAliveMachine{"0"},
  2371  		setMachineStatus{"0", state.StatusStarted, ""},
  2372  		addCharm{"wordpress"},
  2373  		addCharm{"mysql"},
  2374  		addCharm{"logging"},
  2375  		addService{name: "wordpress", charm: "wordpress"},
  2376  		setServiceExposed{"wordpress", true},
  2377  		addMachine{machineId: "1", job: state.JobHostUnits},
  2378  		setAddresses{"1", []network.Address{network.NewAddress("dummyenv-1.dns", network.ScopeUnknown)}},
  2379  		startAliveMachine{"1"},
  2380  		setMachineStatus{"1", state.StatusStarted, ""},
  2381  		addAliveUnit{"wordpress", "1"},
  2382  		setUnitStatus{"wordpress/0", state.StatusActive, "", nil},
  2383  		addService{name: "mysql", charm: "mysql"},
  2384  		setServiceExposed{"mysql", true},
  2385  		addMachine{machineId: "2", job: state.JobHostUnits},
  2386  		setAddresses{"2", []network.Address{network.NewAddress("dummyenv-2.dns", network.ScopeUnknown)}},
  2387  		startAliveMachine{"2"},
  2388  		setMachineStatus{"2", state.StatusStarted, ""},
  2389  		addAliveUnit{"mysql", "2"},
  2390  		setUnitStatus{"mysql/0", state.StatusActive, "", nil},
  2391  		addService{name: "logging", charm: "logging"},
  2392  		setServiceExposed{"logging", true},
  2393  		relateServices{"wordpress", "mysql"},
  2394  		relateServices{"wordpress", "logging"},
  2395  		relateServices{"mysql", "logging"},
  2396  		addSubordinate{"wordpress/0", "logging"},
  2397  		addSubordinate{"mysql/0", "logging"},
  2398  		setUnitsAlive{"logging"},
  2399  		setUnitStatus{"logging/0", state.StatusActive, "", nil},
  2400  		setUnitStatus{"logging/1", state.StatusError, "somehow lost in all those logs", nil},
  2401  	}
  2402  	for _, s := range steps {
  2403  		s.step(c, ctx)
  2404  	}
  2405  	code, stdout, stderr := runStatus(c, "--format", "tabular")
  2406  	c.Check(code, gc.Equals, 0)
  2407  	c.Check(string(stderr), gc.Equals, "")
  2408  	c.Assert(
  2409  		string(stdout),
  2410  		gc.Equals,
  2411  		"[Machines] \n"+
  2412  			"ID         STATE   VERSION DNS            INS-ID     SERIES  HARDWARE                                         \n"+
  2413  			"0          started         dummyenv-0.dns dummyenv-0 quantal arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M \n"+
  2414  			"1          started         dummyenv-1.dns dummyenv-1 quantal arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M \n"+
  2415  			"2          started         dummyenv-2.dns dummyenv-2 quantal arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M \n"+
  2416  			"\n"+
  2417  			"[Services] \n"+
  2418  			"NAME       EXPOSED CHARM                  \n"+
  2419  			"logging    true    cs:quantal/logging-1   \n"+
  2420  			"mysql      true    cs:quantal/mysql-1     \n"+
  2421  			"wordpress  true    cs:quantal/wordpress-3 \n"+
  2422  			"\n"+
  2423  			"[Units]     \n"+
  2424  			"ID          STATE   VERSION MACHINE PORTS PUBLIC-ADDRESS \n"+
  2425  			"mysql/0     started         2             dummyenv-2.dns \n"+
  2426  			"  logging/1 error                         dummyenv-2.dns \n"+
  2427  			"wordpress/0 started         1             dummyenv-1.dns \n"+
  2428  			"  logging/0 started                       dummyenv-1.dns \n"+
  2429  			"\n",
  2430  	)
  2431  }
  2432  
  2433  func (s *StatusSuite) TestStatusWithNilStatusApi(c *gc.C) {
  2434  	ctx := s.newContext(c)
  2435  	defer s.resetContext(c, ctx)
  2436  	steps := []stepper{
  2437  		addMachine{machineId: "0", job: state.JobManageEnviron},
  2438  		setAddresses{"0", []network.Address{network.NewAddress("dummyenv-0.dns", network.ScopeUnknown)}},
  2439  		startAliveMachine{"0"},
  2440  		setMachineStatus{"0", state.StatusStarted, ""},
  2441  	}
  2442  
  2443  	for _, s := range steps {
  2444  		s.step(c, ctx)
  2445  	}
  2446  
  2447  	client := fakeApiClient{}
  2448  	var status = client.Status
  2449  	s.PatchValue(&status, func(_ []string) (*api.Status, error) {
  2450  		return nil, nil
  2451  	})
  2452  	s.PatchValue(&newApiClientForStatus, func(_ *StatusCommand) (statusAPI, error) {
  2453  		return &client, nil
  2454  	})
  2455  
  2456  	code, _, stderr := runStatus(c, "--format", "tabular")
  2457  	c.Check(code, gc.Equals, 1)
  2458  	c.Check(string(stderr), gc.Equals, "error: unable to obtain the current status\n")
  2459  }
  2460  
  2461  //
  2462  // Filtering Feature
  2463  //
  2464  
  2465  func (s *StatusSuite) FilteringTestSetup(c *gc.C) *context {
  2466  	ctx := s.newContext(c)
  2467  
  2468  	steps := []stepper{
  2469  		// Given a machine is started
  2470  		// And the machine's ID is "0"
  2471  		// And the machine's job is to manage the environment
  2472  		addMachine{machineId: "0", job: state.JobManageEnviron},
  2473  		startAliveMachine{"0"},
  2474  		setMachineStatus{"0", state.StatusStarted, ""},
  2475  		// And the machine's address is "dummyenv-0.dns"
  2476  		setAddresses{"0", []network.Address{network.NewAddress("dummyenv-0.dns", network.ScopeUnknown)}},
  2477  		// And the "wordpress" charm is available
  2478  		addCharm{"wordpress"},
  2479  		addService{name: "wordpress", charm: "wordpress"},
  2480  		// And the "mysql" charm is available
  2481  		addCharm{"mysql"},
  2482  		addService{name: "mysql", charm: "mysql"},
  2483  		// And the "logging" charm is available
  2484  		addCharm{"logging"},
  2485  		// And a machine is started
  2486  		// And the machine's ID is "1"
  2487  		// And the machine's job is to host units
  2488  		addMachine{machineId: "1", job: state.JobHostUnits},
  2489  		startAliveMachine{"1"},
  2490  		setMachineStatus{"1", state.StatusStarted, ""},
  2491  		// And the machine's address is "dummyenv-1.dns"
  2492  		setAddresses{"1", []network.Address{network.NewAddress("dummyenv-1.dns", network.ScopeUnknown)}},
  2493  		// And a unit of "wordpress" is deployed to machine "1"
  2494  		addAliveUnit{"wordpress", "1"},
  2495  		// And the unit is started
  2496  		setUnitStatus{"wordpress/0", state.StatusActive, "", nil},
  2497  		// And a machine is started
  2498  
  2499  		// And the machine's ID is "2"
  2500  		// And the machine's job is to host units
  2501  		addMachine{machineId: "2", job: state.JobHostUnits},
  2502  		startAliveMachine{"2"},
  2503  		setMachineStatus{"2", state.StatusStarted, ""},
  2504  		// And the machine's address is "dummyenv-2.dns"
  2505  		setAddresses{"2", []network.Address{network.NewAddress("dummyenv-2.dns", network.ScopeUnknown)}},
  2506  		// And a unit of "mysql" is deployed to machine "2"
  2507  		addAliveUnit{"mysql", "2"},
  2508  		// And the unit is started
  2509  		setUnitStatus{"mysql/0", state.StatusActive, "", nil},
  2510  		// And the "logging" service is added
  2511  		addService{name: "logging", charm: "logging"},
  2512  		// And the service is exposed
  2513  		setServiceExposed{"logging", true},
  2514  		// And the "wordpress" service is related to the "mysql" service
  2515  		relateServices{"wordpress", "mysql"},
  2516  		// And the "wordpress" service is related to the "logging" service
  2517  		relateServices{"wordpress", "logging"},
  2518  		// And the "mysql" service is related to the "logging" service
  2519  		relateServices{"mysql", "logging"},
  2520  		// And the "logging" service is a subordinate to unit 0 of the "wordpress" service
  2521  		addSubordinate{"wordpress/0", "logging"},
  2522  		setUnitStatus{"logging/0", state.StatusActive, "", nil},
  2523  		// And the "logging" service is a subordinate to unit 0 of the "mysql" service
  2524  		addSubordinate{"mysql/0", "logging"},
  2525  		setUnitStatus{"logging/1", state.StatusActive, "", nil},
  2526  		setUnitsAlive{"logging"},
  2527  	}
  2528  
  2529  	ctx.run(c, steps)
  2530  	return ctx
  2531  }
  2532  
  2533  // Scenario: One unit is in an errored state and user filters to started
  2534  func (s *StatusSuite) TestFilterToStarted(c *gc.C) {
  2535  	ctx := s.FilteringTestSetup(c)
  2536  	defer s.resetContext(c, ctx)
  2537  
  2538  	// Given unit 1 of the "logging" service has an error
  2539  	setUnitStatus{"logging/1", state.StatusError, "mock error", nil}.step(c, ctx)
  2540  	// And unit 0 of the "mysql" service has an error
  2541  	setUnitStatus{"mysql/0", state.StatusError, "mock error", nil}.step(c, ctx)
  2542  	// When I run juju status --format oneline started
  2543  	_, stdout, stderr := runStatus(c, "--format", "oneline", "started")
  2544  	c.Assert(string(stderr), gc.Equals, "")
  2545  	// Then I should receive output prefixed with:
  2546  	const expected = `
  2547  
  2548  - wordpress/0: dummyenv-1.dns (started)
  2549    - logging/0: dummyenv-1.dns (started)
  2550  `
  2551  
  2552  	c.Assert(string(stdout), gc.Equals, expected[1:])
  2553  }
  2554  
  2555  // Scenario: One unit is in an errored state and user filters to errored
  2556  func (s *StatusSuite) TestFilterToErrored(c *gc.C) {
  2557  	ctx := s.FilteringTestSetup(c)
  2558  	defer s.resetContext(c, ctx)
  2559  
  2560  	// Given unit 1 of the "logging" service has an error
  2561  	setUnitStatus{"logging/1", state.StatusError, "mock error", nil}.step(c, ctx)
  2562  	// When I run juju status --format oneline error
  2563  	_, stdout, stderr := runStatus(c, "--format", "oneline", "error")
  2564  	c.Assert(stderr, gc.IsNil)
  2565  	// Then I should receive output prefixed with:
  2566  	const expected = `
  2567  
  2568  - mysql/0: dummyenv-2.dns (started)
  2569    - logging/1: dummyenv-2.dns (error)
  2570  `
  2571  
  2572  	c.Assert(string(stdout), gc.Equals, expected[1:])
  2573  }
  2574  
  2575  // Scenario: User filters to mysql service
  2576  func (s *StatusSuite) TestFilterToService(c *gc.C) {
  2577  	ctx := s.FilteringTestSetup(c)
  2578  	defer s.resetContext(c, ctx)
  2579  
  2580  	// When I run juju status --format oneline error
  2581  	_, stdout, stderr := runStatus(c, "--format", "oneline", "mysql")
  2582  	c.Assert(stderr, gc.IsNil)
  2583  	// Then I should receive output prefixed with:
  2584  	const expected = `
  2585  
  2586  - mysql/0: dummyenv-2.dns (started)
  2587    - logging/1: dummyenv-2.dns (started)
  2588  `
  2589  
  2590  	c.Assert(string(stdout), gc.Equals, expected[1:])
  2591  }
  2592  
  2593  // Scenario: User filters to exposed services
  2594  func (s *StatusSuite) TestFilterToExposedService(c *gc.C) {
  2595  	ctx := s.FilteringTestSetup(c)
  2596  	defer s.resetContext(c, ctx)
  2597  
  2598  	// Given unit 1 of the "mysql" service is exposed
  2599  	setServiceExposed{"mysql", true}.step(c, ctx)
  2600  	// And the logging service is not exposed
  2601  	setServiceExposed{"logging", false}.step(c, ctx)
  2602  	// And the wordpress service is not exposed
  2603  	setServiceExposed{"wordpress", false}.step(c, ctx)
  2604  	// When I run juju status --format oneline exposed
  2605  	_, stdout, stderr := runStatus(c, "--format", "oneline", "exposed")
  2606  	c.Assert(stderr, gc.IsNil)
  2607  	// Then I should receive output prefixed with:
  2608  	const expected = `
  2609  
  2610  - mysql/0: dummyenv-2.dns (started)
  2611    - logging/1: dummyenv-2.dns (started)
  2612  `
  2613  
  2614  	c.Assert(string(stdout), gc.Equals, expected[1:])
  2615  }
  2616  
  2617  // Scenario: User filters to non-exposed services
  2618  func (s *StatusSuite) TestFilterToNotExposedService(c *gc.C) {
  2619  	ctx := s.FilteringTestSetup(c)
  2620  	defer s.resetContext(c, ctx)
  2621  
  2622  	setServiceExposed{"mysql", true}.step(c, ctx)
  2623  	// When I run juju status --format oneline not exposed
  2624  	_, stdout, stderr := runStatus(c, "--format", "oneline", "not", "exposed")
  2625  	c.Assert(stderr, gc.IsNil)
  2626  	// Then I should receive output prefixed with:
  2627  	const expected = `
  2628  
  2629  - wordpress/0: dummyenv-1.dns (started)
  2630    - logging/0: dummyenv-1.dns (started)
  2631  `
  2632  
  2633  	c.Assert(string(stdout), gc.Equals, expected[1:])
  2634  }
  2635  
  2636  // Scenario: Filtering on Subnets
  2637  func (s *StatusSuite) TestFilterOnSubnet(c *gc.C) {
  2638  	ctx := s.FilteringTestSetup(c)
  2639  	defer s.resetContext(c, ctx)
  2640  
  2641  	// Given the address for machine "1" is "localhost"
  2642  	setAddresses{"1", []network.Address{network.NewAddress("localhost", network.ScopeUnknown)}}.step(c, ctx)
  2643  	// And the address for machine "2" is "10.0.0.1"
  2644  	setAddresses{"2", []network.Address{network.NewAddress("10.0.0.1", network.ScopeUnknown)}}.step(c, ctx)
  2645  	// When I run juju status --format oneline 127.0.0.1
  2646  	_, stdout, stderr := runStatus(c, "--format", "oneline", "127.0.0.1")
  2647  	c.Assert(stderr, gc.IsNil)
  2648  	// Then I should receive output prefixed with:
  2649  	const expected = `
  2650  
  2651  - wordpress/0: localhost (started)
  2652    - logging/0: localhost (started)
  2653  `
  2654  
  2655  	c.Assert(string(stdout), gc.Equals, expected[1:])
  2656  }
  2657  
  2658  // Scenario: Filtering on Ports
  2659  func (s *StatusSuite) TestFilterOnPorts(c *gc.C) {
  2660  	ctx := s.FilteringTestSetup(c)
  2661  	defer s.resetContext(c, ctx)
  2662  
  2663  	// Given the address for machine "1" is "localhost"
  2664  	setAddresses{"1", []network.Address{network.NewAddress("localhost", network.ScopeUnknown)}}.step(c, ctx)
  2665  	// And the address for machine "2" is "10.0.0.1"
  2666  	setAddresses{"2", []network.Address{network.NewAddress("10.0.0.1", network.ScopeUnknown)}}.step(c, ctx)
  2667  	openUnitPort{"wordpress/0", "tcp", 80}.step(c, ctx)
  2668  	// When I run juju status --format oneline 80/tcp
  2669  	_, stdout, stderr := runStatus(c, "--format", "oneline", "80/tcp")
  2670  	c.Assert(stderr, gc.IsNil)
  2671  	// Then I should receive output prefixed with:
  2672  	const expected = `
  2673  
  2674  - wordpress/0: localhost (started) 80/tcp
  2675    - logging/0: localhost (started)
  2676  `
  2677  
  2678  	c.Assert(string(stdout), gc.Equals, expected[1:])
  2679  }
  2680  
  2681  // Scenario: User filters out a parent, but not its subordinate
  2682  func (s *StatusSuite) TestFilterParentButNotSubordinate(c *gc.C) {
  2683  	ctx := s.FilteringTestSetup(c)
  2684  	defer s.resetContext(c, ctx)
  2685  
  2686  	// When I run juju status --format oneline 80/tcp
  2687  	_, stdout, stderr := runStatus(c, "--format", "oneline", "logging")
  2688  	c.Assert(stderr, gc.IsNil)
  2689  	// Then I should receive output prefixed with:
  2690  	const expected = `
  2691  
  2692  - mysql/0: dummyenv-2.dns (started)
  2693    - logging/1: dummyenv-2.dns (started)
  2694  - wordpress/0: dummyenv-1.dns (started)
  2695    - logging/0: dummyenv-1.dns (started)
  2696  `
  2697  
  2698  	c.Assert(string(stdout), gc.Equals, expected[1:])
  2699  }
  2700  
  2701  // Scenario: User filters out a subordinate, but not its parent
  2702  func (s *StatusSuite) TestFilterSubordinateButNotParent(c *gc.C) {
  2703  	ctx := s.FilteringTestSetup(c)
  2704  	defer s.resetContext(c, ctx)
  2705  
  2706  	// Given the wordpress service is exposed
  2707  	setServiceExposed{"wordpress", true}.step(c, ctx)
  2708  	// When I run juju status --format oneline not exposed
  2709  	_, stdout, stderr := runStatus(c, "--format", "oneline", "not", "exposed")
  2710  	c.Assert(stderr, gc.IsNil)
  2711  	// Then I should receive output prefixed with:
  2712  	const expected = `
  2713  
  2714  - mysql/0: dummyenv-2.dns (started)
  2715    - logging/1: dummyenv-2.dns (started)
  2716  `
  2717  
  2718  	c.Assert(string(stdout), gc.Equals, expected[1:])
  2719  }
  2720  
  2721  func (s *StatusSuite) TestFilterMultipleHomogenousPatterns(c *gc.C) {
  2722  	ctx := s.FilteringTestSetup(c)
  2723  	defer s.resetContext(c, ctx)
  2724  
  2725  	_, stdout, stderr := runStatus(c, "--format", "oneline", "wordpress/0", "mysql/0")
  2726  	c.Assert(stderr, gc.IsNil)
  2727  	// Then I should receive output prefixed with:
  2728  	const expected = `
  2729  
  2730  - mysql/0: dummyenv-2.dns (started)
  2731    - logging/1: dummyenv-2.dns (started)
  2732  - wordpress/0: dummyenv-1.dns (started)
  2733    - logging/0: dummyenv-1.dns (started)
  2734  `
  2735  
  2736  	c.Assert(string(stdout), gc.Equals, expected[1:])
  2737  }
  2738  
  2739  func (s *StatusSuite) TestFilterMultipleHeterogenousPatterns(c *gc.C) {
  2740  	ctx := s.FilteringTestSetup(c)
  2741  	defer s.resetContext(c, ctx)
  2742  
  2743  	_, stdout, stderr := runStatus(c, "--format", "oneline", "wordpress/0", "started")
  2744  	c.Assert(stderr, gc.IsNil)
  2745  	// Then I should receive output prefixed with:
  2746  	const expected = `
  2747  
  2748  - mysql/0: dummyenv-2.dns (started)
  2749    - logging/1: dummyenv-2.dns (started)
  2750  - wordpress/0: dummyenv-1.dns (started)
  2751    - logging/0: dummyenv-1.dns (started)
  2752  `
  2753  
  2754  	c.Assert(string(stdout), gc.Equals, expected[1:])
  2755  }
  2756  
  2757  // TestSummaryStatusWithUnresolvableDns is result of bug# 1410320.
  2758  func (s *StatusSuite) TestSummaryStatusWithUnresolvableDns(c *gc.C) {
  2759  	formatter := &summaryFormatter{}
  2760  	formatter.resolveAndTrackIp("invalidDns")
  2761  	// Test should not panic.
  2762  }