Merge lp:~axwalk/juju-core/lp1121916-juju-status-filters into lp:~go-bot/juju-core/trunk
- lp1121916-juju-status-filters
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Andrew Wilkins | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 1615 | ||||
Proposed branch: | lp:~axwalk/juju-core/lp1121916-juju-status-filters | ||||
Merge into: | lp:~go-bot/juju-core/trunk | ||||
Diff against target: |
949 lines (+670/-40) 9 files modified
charm/meta_test.go (+25/-21) cmd/juju/cmd_test.go (+3/-1) cmd/juju/status.go (+172/-14) cmd/juju/status_test.go (+402/-3) state/service_test.go (+13/-1) state/unit.go (+6/-0) state/unit_test.go (+30/-0) testing/repo/series/monitoring/metadata.yaml (+16/-0) testing/repo/series/wordpress/metadata.yaml (+3/-0) |
||||
To merge this branch: | bzr merge lp:~axwalk/juju-core/lp1121916-juju-status-filters | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+178004@code.launchpad.net |
Commit message
juju status: add service/unit filters
Any positional arguments after juju status are
now interpreted as filters for either service or
unit. If the filter includes a '/', then it is
considered a unit filter, else a service filter.
Service filters are transformed to unit filters
by appending '/*'.
A unit passes a filter if one of the following
conditions is true:
- Its name matches the pattern.
- The unit is a principal, and one of its
subordinates matches the pattern.
- The unit is a subordinate, and its principal
matches the pattern.
If all units of a service are filtered out, then
the service is filtered out; if all units of a
machine are filtered out, the machine is filtered
out (except to fill out the container hierarchy).
Unlike in pyjuju, this implementation allows
only the '*' wildcard in patterns. '?' and
character ranges are not permitted.
Partially fixes bug 1121916
Description of the change
juju status: add service/unit filters
Any positional arguments after juju status are
now interpreted as filters for either service or
unit. If the filter includes a '/', then it is
considered a unit filter, else a service filter.
Service filters are transformed to unit filters
by appending '/*'.
A unit passes a filter if one of the following
conditions is true:
- Its name matches the pattern.
- The unit is a principal, and one of its
subordinates matches the pattern.
- The unit is a subordinate, and its principal
matches the pattern.
If all units of a service are filtered out, then
the service is filtered out; if all units of a
machine are filtered out, the machine is filtered
out (except to fill out the container hierarchy).
Unlike in pyjuju, this implementation allows
only the '*' wildcard in patterns. '?' and
character ranges are not permitted.
Partially fixes bug 1121916
Andrew Wilkins (axwalk) wrote : | # |
Martin Packman (gz) wrote : | # |
Rietveld has screwed the diff, you'll need to run `lbox propose -cr -v`
again which should fix it.
One comment from looking at the changes on launchpad, this code ignores
a potential ErrBadPattern from path.Match() - seems like for user sanity
it might be nice to propogate that up to where they can see it. At the
least I'd like to see a test that has a borked pattern (unmatched [ or
similar should do) and asserts that things don't blow up.
Andrew Wilkins (axwalk) wrote : | # |
On 2013/08/01 15:39:18, gz wrote:
> Rietveld has screwed the diff, you'll need to run `lbox propose -cr
-v` again
> which should fix it.
Odd - the diffs show up fine to me. I've re-proposed anyway.
> One comment from looking at the changes on launchpad, this code
ignores a
> potential ErrBadPattern from path.Match() - seems like for user sanity
it might
> be nice to propogate that up to where they can see it. At the least
I'd like to
> see a test that has a borked pattern (unmatched [ or similar should
do) and
> asserts that things don't blow up.
Done. Go's path.Match function checks validity lazily, and I didn't
think it worthwhile expending effort to validate it ahead of time. Thus,
pattern validity will not be checked if it is never considered (i.e. a
preceding pattern matches). I've codified this in a test.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
William Reade (fwereade) wrote : | # |
Thanks, this is a great start, but we need to think about subordinates
in a bit more detail. Thoughts below.
https:/
File cmd/juju/status.go (right):
https:/
cmd/juju/
There's something here that demands a bit more thought.
* principal units have to(?) be included if the filter matches one of
their subordinates (because they're displayed in the principal's
"subordinates" field)
* subordinate units should be included if the filter matches their
principal (because otherwise we're stripping relevant info from the
requested unit)
* if a principal is included only because one of its subordinates is, we
probably shouldn't display its other subordinates, because they'renot
relevant to the user's request
* AssignedMachineId is expensive for subordinate units, because it
roundtrips to get the machine id from the principal
I *think* that if we tweak unit filtering as above (which remains open
to argument, but is I think consistent with the analogous behaviour for
machines) we can just skip all subordinate units here and still be
correct, and not waste any further time chatting to the database.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
Andrew Wilkins (axwalk) wrote : | # |
https:/
File cmd/juju/status.go (right):
https:/
cmd/juju/
On 2013/08/02 16:25:03, fwereade wrote:
> There's something here that demands a bit more thought.
> * principal units have to(?) be included if the filter matches one of
their
> subordinates (because they're displayed in the principal's
"subordinates" field)
> * subordinate units should be included if the filter matches their
principal
> (because otherwise we're stripping relevant info from the requested
unit)
> * if a principal is included only because one of its subordinates is,
we
> probably shouldn't display its other subordinates, because they'renot
relevant
> to the user's request
> * AssignedMachineId is expensive for subordinate units, because it
roundtrips to
> get the machine id from the principal
> I *think* that if we tweak unit filtering as above (which remains open
to
> argument, but is I think consistent with the analogous behaviour for
machines)
> we can just skip all subordinate units here and still be correct, and
not waste
> any further time chatting to the database.
Done. Your suggestions SGTM, so I've implemented them.
Roger Peppe (rogpeppe) wrote : | # |
I like the way this is going - I think the matching
semantics are nice and will be useful in practice
(although it might be nice to have a way of showing
info on a single unit, ignoring its machine and principal/subs - that
can easily be done later)
I have a few suggestions below.
https:/
File cmd/juju/status.go (right):
https:/
cmd/juju/
"scope" seems like a slightly odd name here.
"patterns" perhaps?
https:/
cmd/juju/
runtime state of various system entities."
I think we should add some documentation on the pattern-matching
behaviour here.
https:/
cmd/juju/
Args: "[pattern...]",
https:/
cmd/juju/
subordinateTo string) (bool, error) {
I'm not sure we need the subordinateTo argument here.
A unit already knows its own principal unit; we
could either use u.DeployerTag to find it out,
or implement a trivial Unit.PrincipalName
function in state.
https:/
cmd/juju/
we usually use a colon only for a following error.
perhaps:
return false, fmt.Errorf("pattern syntax error in %q", pattern)
?
https:/
cmd/juju/
*unitMatcher) {
It'd be nice to see a comment explaining the purpose of this function
here.
Rather than have the lazy initialisation, it might be
nicer to have something like this:
// newUnitMatcher returns a unitMatcher
// that matches units with the specified patterns.
func newUnitMatcher(
// matchesAll reports whether the unit matcher
// matches any unit.
func (um *unitMatcher) matchesAny() bool
Then in fetchAllService
um.matchesAny rather than using the somewhat
oblique um!=nil test (assuming I have the
intent correct)
Alternatively continue to use the ==nil test
and only call newUnitMatcher if len(patterns)>0.
https:/
cmd/juju/
I think we should try to avoid calling for subordinate units, because we
have all the information in the principal units.
Since we are guaranteed to have a principal unit if the unit matcher has
matched its subordinate, we could just do:
if unit.IsPrincipal() {
mid, err := unit.AssignedMa
// The only error we can get from AssignedMachineId
// on a principal unit is if there is no machine id
// assigned to the unit.
if err == nil {
}
}
You might want to move some or all the logic in this um!=nil
block in...
Andrew Wilkins (axwalk) wrote : | # |
https:/
File cmd/juju/status.go (right):
https:/
cmd/juju/
On 2013/08/05 14:23:02, rog wrote:
> "scope" seems like a slightly odd name here.
> "patterns" perhaps?
Done. I was following pyjuju, but I agree that it sounds slightly odd.
https:/
cmd/juju/
runtime state of various system entities."
On 2013/08/05 14:23:02, rog wrote:
> I think we should add some documentation on the pattern-matching
behaviour here.
Done.
https:/
cmd/juju/
subordinateTo string) (bool, error) {
On 2013/08/05 14:23:02, rog wrote:
> I'm not sure we need the subordinateTo argument here.
> A unit already knows its own principal unit; we
> could either use u.DeployerTag to find it out,
> or implement a trivial Unit.PrincipalName
> function in state.
Good point, I've added Unit.PrincipalName.
https:/
cmd/juju/
On 2013/08/05 14:23:02, rog wrote:
> we usually use a colon only for a following error.
> perhaps:
> return false, fmt.Errorf("pattern syntax error in %q", pattern)
> ?
Done.
https:/
cmd/juju/
*unitMatcher) {
On 2013/08/05 14:23:02, rog wrote:
> It'd be nice to see a comment explaining the purpose of this function
here.
> Rather than have the lazy initialisation, it might be
> nicer to have something like this:
> // newUnitMatcher returns a unitMatcher
> // that matches units with the specified patterns.
> func newUnitMatcher(
> // matchesAll reports whether the unit matcher
> // matches any unit.
> func (um *unitMatcher) matchesAny() bool
> Then in fetchAllService
> um.matchesAny rather than using the somewhat
> oblique um!=nil test (assuming I have the
> intent correct)
> Alternatively continue to use the ==nil test
> and only call newUnitMatcher if len(patterns)>0.
Done.
https:/
cmd/juju/
On 2013/08/05 14:23:02, rog wrote:
> I think we should try to avoid calling for subordinate units, because
we have
> all the information in the principal units.
> Since we are guaranteed to have a principal unit if the unit matcher
has matched
> its subordinate, we could just do:
> if unit.IsPrincipal() {
> mid, err := unit.AssignedMa
> // The only error we can get from AssignedMachineId
> // on a principal unit is if there is no machine id
> // assigned to the unit.
> if err == nil {
> machineIds.Add(mid)
> }
> }
> You might want to move some or all the logic in this um!=nil
> block ...
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
Roger Peppe (rogpeppe) wrote : | # |
LGTM with the second fix below, and one other passing thought.
https:/
File cmd/juju/status.go (right):
https:/
cmd/juju/
It's a pity that because path.Match is lazy
about checking pattern validity, we need to return an
error all the way back.
For consideration: validate the glob patterns
with a regexp, and then we can panic here instead,
and lose the error returns from matchString
and matchUnit (and provide more reliable errors
to the user too)
Something like:
^([^\\[
might be sufficient.
https:/
cmd/juju/
This still isn't quite right - it should only call AssignedMachineId if
the unit is a principal (that way it doesn't need to do a network call)
just:
if !unit.IsPrincipal() {
continue
}
should do the job.
William Reade (fwereade) wrote : | # |
LGTM, thanks -- sorry for the delay, it's a busy week.
https:/
File cmd/juju/status.go (right):
https:/
cmd/juju/
Maybe add a little bit more explaining which bits are filtered out (and
which bits are not).
https:/
cmd/juju/
s/n./n ./
?
https:/
cmd/juju/
lose the else?
https:/
File cmd/juju/
https:/
cmd/juju/
I'd prefer it if stderr and stdout were consistently typed...
https:/
cmd/juju/
mem=1024M",
Orthogonal... but this hardware data is not helpfully structured. Please
add a bug.
https:/
File testing/
https:/
testing/
doesn't anything test for endpoints of wordpress elsewhere? If not,
cool, I'm just a bit surprised :).
Andrew Wilkins (axwalk) wrote : | # |
On 2013/08/07 17:02:30, rog wrote:
> LGTM with the second fix below, and one other passing thought.
> https:/
> File cmd/juju/status.go (right):
https:/
> cmd/juju/
> It's a pity that because path.Match is lazy
> about checking pattern validity, we need to return an
> error all the way back.
> For consideration: validate the glob patterns
> with a regexp, and then we can panic here instead,
> and lose the error returns from matchString
> and matchUnit (and provide more reliable errors
> to the user too)
> Something like:
> ^([^\\[
> might be sufficient.
That would be better, but can we simplify this?
The use of path matching is overkill IMHO. I think the only real
use-case
for wildcards is to match services with a common prefix or suffix,
e.g. redis-*, which would match redis-master and redis-slave.
How about we simplify it to just allow a '*' wildcard, and nothing else?
Then we can just check that the pattern matches "[a-z0-9-*]". This will
also simplify the documentation.
If we find that people really want complex matching, we could later add
a
flag to interpret patterns as REs.
I went ahead and implemented this. If this was wrong, let me know and
I'll revert.
https:/
> cmd/juju/
> This still isn't quite right - it should only call AssignedMachineId
if the unit
> is a principal (that way it doesn't need to do a network call)
> just:
> if !unit.IsPrincipal() {
> continue
> }
> should do the job.
Sorry! I missed that from last time. Done.
Andrew Wilkins (axwalk) wrote : | # |
On 2013/08/07 17:06:53, fwereade wrote:
> LGTM, thanks -- sorry for the delay, it's a busy week.
No worries, thanks for the review.
> https:/
> File cmd/juju/status.go (right):
https:/
> cmd/juju/
> Maybe add a little bit more explaining which bits are filtered out
(and which
> bits are not).
Done.
https:/
> cmd/juju/
> s/n./n ./
> ?
Done.
https:/
> cmd/juju/
> lose the else?
Done.
https:/
> File cmd/juju/
https:/
> cmd/juju/
ctx.Stderr.
> I'd prefer it if stderr and stdout were consistently typed...
Done.
https:/
> cmd/juju/
mem=1024M",
> Orthogonal... but this hardware data is not helpfully structured.
Please add a
> bug.
Done: https:/
(I hope that's what you meant)
https:/
> File testing/
https:/
> testing/
> doesn't anything test for endpoints of wordpress elsewhere? If not,
cool, I'm
> just a bit surprised :).
Sorry, I;m not sure I understand your comment. There are tests for
subordinates relating to wordpress, but there was only a test with one
subordinate (logging). I needed a test involving two subordinates for
one principal, hence this addition.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
William Reade (fwereade) wrote : | # |
On 2013/08/08 02:46:27, axw1 wrote:
> On 2013/08/07 17:06:53, fwereade wrote:
https:/
> > cmd/juju/
> mem=1024M",
> > Orthogonal... but this hardware data is not helpfully structured.
Please add a
> > bug.
> Done: https:/
> (I hope that's what you meant)
Perfect, thanks.
https:/
> > testing/
> > doesn't anything test for endpoints of wordpress elsewhere? If not,
cool, I'm
> > just a bit surprised :).
> Sorry, I;m not sure I understand your comment. There are tests for
subordinates
> relating to wordpress, but there was only a test with one subordinate
(logging).
> I needed a test involving two subordinates for one principal, hence
this
> addition.
I was just expressing mild surprise that this change didn't cause any
other tests to fail. It's not a problem that they didn't :).
Unless there's anything specific you need me to check, my previous LGTM
can be assumed to stand and you can go ahead and merge.
Roger Peppe (rogpeppe) wrote : | # |
still LGTM with two final thoughts.
https:/
File cmd/juju/status.go (right):
https:/
cmd/juju/
"abcdefghijklmn
or:
var validPattern = regexp.
and use if !validPattern.
which would make it easier to add extra stuff if, for instance,
we started to allow unicode characters in service names.
https:/
File state/unit.go (right):
https:/
state/unit.go:96: var _ AgentEntity = (*Unit)(nil)
This has moved into interface.go now.
Go Bot (go-bot) wrote : | # |
The attempt to merge lp:~axwalk/juju-core/lp1121916-juju-status-filters into lp:juju-core failed. Below is the output from the failed tests.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
-------
FAIL: meta_test.go:329: MetaSuite.
meta_test.go:356:
c.Assert(hooks, DeepEquals, expectedHooks)
... obtained map[string]bool = map[string]
... expected map[string]bool = map[string]
OOPS: 76 passed, 1 FAILED
--- FAIL: Test (0.33 seconds)
FAIL
FAIL launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
? launchpad.
listing available tools
found 4 tools
found 4 recent tools (version 1.0.0)
listing target bucket
found 3 tools in target; 4 tools to be copied
copying 1.0.0-defaultse
copying tools/juju-
downloaded tools/juju-
download 0kB, uploading
copying 1.0.0-precise-amd64 from http://
copying tools/juju-
downloaded tools/juju-
download 0kB, uploading
copying 1.0.0-quantal-amd64 from http://
copying tools/juju-
downloaded tools/juju-
download 0kB, uploading
copying 1.0.0-quantal-
Go Bot (go-bot) wrote : | # |
The attempt to merge lp:~axwalk/juju-core/lp1121916-juju-status-filters into lp:juju-core failed. Below is the output from the failed tests.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
-------
FAIL: service_
[LOG] 9.66034 INFO juju state: opening state; mongo addresses: ["localhost:
[LOG] 9.66789 INFO juju state: connection established
[LOG] 9.69983 INFO juju state: initializing environment
service_
testing.
testing/
c.Fatalf(
... Error: watcher sent unexpected change: (_, true)
OOPS: 308 passed, 1 FAILED
--- FAIL: TestPackage (68.00 seconds)
FAIL
FAIL launchpad.
? lau...
Preview Diff
1 | === modified file 'charm/meta_test.go' |
2 | --- charm/meta_test.go 2013-07-09 10:32:23 +0000 |
3 | +++ charm/meta_test.go 2013-08-08 09:25:42 +0000 |
4 | @@ -331,27 +331,31 @@ |
5 | c.Assert(err, IsNil) |
6 | hooks := meta.Hooks() |
7 | expectedHooks := map[string]bool{ |
8 | - "install": true, |
9 | - "start": true, |
10 | - "config-changed": true, |
11 | - "upgrade-charm": true, |
12 | - "stop": true, |
13 | - "cache-relation-joined": true, |
14 | - "cache-relation-changed": true, |
15 | - "cache-relation-departed": true, |
16 | - "cache-relation-broken": true, |
17 | - "db-relation-joined": true, |
18 | - "db-relation-changed": true, |
19 | - "db-relation-departed": true, |
20 | - "db-relation-broken": true, |
21 | - "logging-dir-relation-joined": true, |
22 | - "logging-dir-relation-changed": true, |
23 | - "logging-dir-relation-departed": true, |
24 | - "logging-dir-relation-broken": true, |
25 | - "url-relation-joined": true, |
26 | - "url-relation-changed": true, |
27 | - "url-relation-departed": true, |
28 | - "url-relation-broken": true, |
29 | + "install": true, |
30 | + "start": true, |
31 | + "config-changed": true, |
32 | + "upgrade-charm": true, |
33 | + "stop": true, |
34 | + "cache-relation-joined": true, |
35 | + "cache-relation-changed": true, |
36 | + "cache-relation-departed": true, |
37 | + "cache-relation-broken": true, |
38 | + "db-relation-joined": true, |
39 | + "db-relation-changed": true, |
40 | + "db-relation-departed": true, |
41 | + "db-relation-broken": true, |
42 | + "logging-dir-relation-joined": true, |
43 | + "logging-dir-relation-changed": true, |
44 | + "logging-dir-relation-departed": true, |
45 | + "logging-dir-relation-broken": true, |
46 | + "monitoring-port-relation-joined": true, |
47 | + "monitoring-port-relation-changed": true, |
48 | + "monitoring-port-relation-departed": true, |
49 | + "monitoring-port-relation-broken": true, |
50 | + "url-relation-joined": true, |
51 | + "url-relation-changed": true, |
52 | + "url-relation-departed": true, |
53 | + "url-relation-broken": true, |
54 | } |
55 | c.Assert(hooks, DeepEquals, expectedHooks) |
56 | } |
57 | |
58 | === modified file 'cmd/juju/cmd_test.go' |
59 | --- cmd/juju/cmd_test.go 2013-08-06 05:22:59 +0000 |
60 | +++ cmd/juju/cmd_test.go 2013-08-08 09:25:42 +0000 |
61 | @@ -115,7 +115,9 @@ |
62 | assertConnName(c, com, "walthamstow") |
63 | |
64 | com, args = cmdFunc() |
65 | - testInit(c, com, append(args, "hotdog"), "unrecognized args.*") |
66 | + if _, ok := com.(*StatusCommand); !ok { |
67 | + testInit(c, com, append(args, "hotdog"), "unrecognized args.*") |
68 | + } |
69 | } |
70 | } |
71 | |
72 | |
73 | === modified file 'cmd/juju/status.go' |
74 | --- cmd/juju/status.go 2013-08-06 14:17:24 +0000 |
75 | +++ cmd/juju/status.go 2013-08-08 09:25:42 +0000 |
76 | @@ -6,6 +6,8 @@ |
77 | import ( |
78 | "encoding/json" |
79 | "fmt" |
80 | + "path" |
81 | + "regexp" |
82 | "strings" |
83 | |
84 | "launchpad.net/gnuflag" |
85 | @@ -24,14 +26,27 @@ |
86 | |
87 | type StatusCommand struct { |
88 | cmd.EnvCommandBase |
89 | - out cmd.Output |
90 | + out cmd.Output |
91 | + patterns []string |
92 | } |
93 | |
94 | -var statusDoc = "This command will report on the runtime state of various system entities." |
95 | +var statusDoc = ` |
96 | +This command will report on the runtime state of various system entities. |
97 | + |
98 | +Service or unit names may be specified to filter the status to only those |
99 | +services and units that match, along with the related machines, services |
100 | +and units. If a subordinate unit is matched, then its principal unit will |
101 | +be displayed. If a principal unit is matched, then all of its subordinates |
102 | +will be displayed. |
103 | + |
104 | +Wildcards ('*') may be specified in service/unit names to match any sequence |
105 | +of characters. For example, 'nova-*' will match any service whose name begins |
106 | +with 'nova-': 'nova-compute', 'nova-volume', etc.`[1:] |
107 | |
108 | func (c *StatusCommand) Info() *cmd.Info { |
109 | return &cmd.Info{ |
110 | Name: "status", |
111 | + Args: "[pattern ...]", |
112 | Purpose: "output status information about an environment", |
113 | Doc: statusDoc, |
114 | Aliases: []string{"stat"}, |
115 | @@ -46,6 +61,11 @@ |
116 | }) |
117 | } |
118 | |
119 | +func (c *StatusCommand) Init(args []string) error { |
120 | + c.patterns = args |
121 | + return nil |
122 | +} |
123 | + |
124 | type statusContext struct { |
125 | instances map[instance.Id]instance.Instance |
126 | machines map[string][]*state.Machine |
127 | @@ -53,6 +73,95 @@ |
128 | units map[string]map[string]*state.Unit |
129 | } |
130 | |
131 | +type unitMatcher struct { |
132 | + patterns []string |
133 | +} |
134 | + |
135 | +// matchesAny returns true if the unitMatcher will |
136 | +// match any unit, regardless of its attributes. |
137 | +func (m unitMatcher) matchesAny() bool { |
138 | + return len(m.patterns) == 0 |
139 | +} |
140 | + |
141 | +// matchUnit attempts to match a state.Unit to one of |
142 | +// a set of patterns, taking into account subordinate |
143 | +// relationships. |
144 | +func (m unitMatcher) matchUnit(u *state.Unit) bool { |
145 | + if m.matchesAny() { |
146 | + return true |
147 | + } |
148 | + |
149 | + // Keep the unit if: |
150 | + // (a) its name matches a pattern, or |
151 | + // (b) it's a principal and one of its subordinates matches, or |
152 | + // (c) it's a subordinate and its principal matches. |
153 | + // |
154 | + // Note: do *not* include a second subordinate if the principal is |
155 | + // only matched on account of a first subordinate matching. |
156 | + if m.matchString(u.Name()) { |
157 | + return true |
158 | + } |
159 | + if u.IsPrincipal() { |
160 | + for _, s := range u.SubordinateNames() { |
161 | + if m.matchString(s) { |
162 | + return true |
163 | + } |
164 | + } |
165 | + return false |
166 | + } |
167 | + principal, valid := u.PrincipalName() |
168 | + if !valid { |
169 | + panic("PrincipalName failed for subordinate unit") |
170 | + } |
171 | + return m.matchString(principal) |
172 | +} |
173 | + |
174 | +// matchString matches a string to one of the patterns in |
175 | +// the unit matcher, returning an error if a pattern with |
176 | +// invalid syntax is encountered. |
177 | +func (m unitMatcher) matchString(s string) bool { |
178 | + for _, pattern := range m.patterns { |
179 | + ok, err := path.Match(pattern, s) |
180 | + if err != nil { |
181 | + // We validate patterns, so should never get here. |
182 | + panic(fmt.Errorf("pattern syntax error in %q", pattern)) |
183 | + } else if ok { |
184 | + return true |
185 | + } |
186 | + } |
187 | + return false |
188 | +} |
189 | + |
190 | +// validPattern must match the parts of a unit or service name |
191 | +// pattern either side of the '/' for it to be valid. |
192 | +var validPattern = regexp.MustCompile("^[a-z0-9-*]+$") |
193 | + |
194 | +// newUnitMatcher returns a unitMatcher that matches units |
195 | +// with one of the specified patterns, or all units if no |
196 | +// patterns are specified. |
197 | +// |
198 | +// An error will be returned if any of the specified patterns |
199 | +// is invalid. Patterns are valid if they contain only |
200 | +// alpha-numeric characters, hyphens, or asterisks (and one |
201 | +// optional '/' to separate service/unit). |
202 | +func newUnitMatcher(patterns []string) (unitMatcher, error) { |
203 | + for i, pattern := range patterns { |
204 | + fields := strings.Split(pattern, "/") |
205 | + if len(fields) > 2 { |
206 | + return unitMatcher{}, fmt.Errorf("pattern %q contains too many '/' characters", pattern) |
207 | + } |
208 | + for _, f := range fields { |
209 | + if !validPattern.MatchString(f) { |
210 | + return unitMatcher{}, fmt.Errorf("pattern %q contains invalid characters", pattern) |
211 | + } |
212 | + } |
213 | + if len(fields) == 1 { |
214 | + patterns[i] += "/*" |
215 | + } |
216 | + } |
217 | + return unitMatcher{patterns}, nil |
218 | +} |
219 | + |
220 | func (c *StatusCommand) Run(ctx *cmd.Context) error { |
221 | conn, err := juju.NewConnFromName(c.EnvName) |
222 | if err != nil { |
223 | @@ -61,12 +170,26 @@ |
224 | defer conn.Close() |
225 | |
226 | var context statusContext |
227 | - if context.machines, err = fetchAllMachines(conn.State); err != nil { |
228 | - return err |
229 | - } |
230 | - if context.services, context.units, err = fetchAllServicesAndUnits(conn.State); err != nil { |
231 | - return err |
232 | - } |
233 | + unitMatcher, err := newUnitMatcher(c.patterns) |
234 | + if err != nil { |
235 | + return err |
236 | + } |
237 | + if context.services, context.units, err = fetchAllServicesAndUnits(conn.State, unitMatcher); err != nil { |
238 | + return err |
239 | + } |
240 | + |
241 | + // Filter machines by units in scope. |
242 | + var machineIds *set.Strings |
243 | + if !unitMatcher.matchesAny() { |
244 | + machineIds, err = fetchUnitMachineIds(context.units) |
245 | + if err != nil { |
246 | + return err |
247 | + } |
248 | + } |
249 | + if context.machines, err = fetchMachines(conn.State, machineIds); err != nil { |
250 | + return err |
251 | + } |
252 | + |
253 | context.instances, err = fetchAllInstances(conn.Environ) |
254 | if err != nil { |
255 | // We cannot see instances from the environment, but |
256 | @@ -98,9 +221,11 @@ |
257 | return m, nil |
258 | } |
259 | |
260 | -// fetchAllMachines returns a map from top level machine id to machines, where machines[0] is the host |
261 | +// fetchMachines returns a map from top level machine id to machines, where machines[0] is the host |
262 | // machine and machines[1..n] are any containers (including nested ones). |
263 | -func fetchAllMachines(st *state.State) (map[string][]*state.Machine, error) { |
264 | +// |
265 | +// If machineIds is non-nil, only machines whose IDs are in the set are returned. |
266 | +func fetchMachines(st *state.State, machineIds *set.Strings) (map[string][]*state.Machine, error) { |
267 | v := make(map[string][]*state.Machine) |
268 | machines, err := st.AllMachines() |
269 | if err != nil { |
270 | @@ -108,6 +233,9 @@ |
271 | } |
272 | // AllMachines gives us machines sorted by id. |
273 | for _, m := range machines { |
274 | + if machineIds != nil && !machineIds.Contains(m.Id()) { |
275 | + continue |
276 | + } |
277 | parentId, ok := m.ParentId() |
278 | if !ok { |
279 | // Only top level host machines go directly into the machine map. |
280 | @@ -127,7 +255,7 @@ |
281 | |
282 | // fetchAllServicesAndUnits returns a map from service name to service |
283 | // and a map from service name to unit name to unit. |
284 | -func fetchAllServicesAndUnits(st *state.State) (map[string]*state.Service, map[string]map[string]*state.Unit, error) { |
285 | +func fetchAllServicesAndUnits(st *state.State, unitMatcher unitMatcher) (map[string]*state.Service, map[string]map[string]*state.Unit, error) { |
286 | svcMap := make(map[string]*state.Service) |
287 | unitMap := make(map[string]map[string]*state.Unit) |
288 | services, err := st.AllServices() |
289 | @@ -135,20 +263,47 @@ |
290 | return nil, nil, err |
291 | } |
292 | for _, s := range services { |
293 | - svcMap[s.Name()] = s |
294 | units, err := s.AllUnits() |
295 | if err != nil { |
296 | return nil, nil, err |
297 | } |
298 | svcUnitMap := make(map[string]*state.Unit) |
299 | for _, u := range units { |
300 | + if !unitMatcher.matchUnit(u) { |
301 | + continue |
302 | + } |
303 | svcUnitMap[u.Name()] = u |
304 | } |
305 | - unitMap[s.Name()] = svcUnitMap |
306 | + if unitMatcher.matchesAny() || len(svcUnitMap) > 0 { |
307 | + unitMap[s.Name()] = svcUnitMap |
308 | + svcMap[s.Name()] = s |
309 | + } |
310 | } |
311 | return svcMap, unitMap, nil |
312 | } |
313 | |
314 | +// fetchUnitMachineIds returns a set of IDs for machines that |
315 | +// the specified units reside on, and those machines' ancestors. |
316 | +func fetchUnitMachineIds(units map[string]map[string]*state.Unit) (*set.Strings, error) { |
317 | + machineIds := new(set.Strings) |
318 | + for _, svcUnitMap := range units { |
319 | + for _, unit := range svcUnitMap { |
320 | + if !unit.IsPrincipal() { |
321 | + continue |
322 | + } |
323 | + mid, err := unit.AssignedMachineId() |
324 | + if err != nil { |
325 | + return nil, err |
326 | + } |
327 | + for mid != "" { |
328 | + machineIds.Add(mid) |
329 | + mid = state.ParentId(mid) |
330 | + } |
331 | + } |
332 | + } |
333 | + return machineIds, nil |
334 | +} |
335 | + |
336 | func (context *statusContext) processMachines() map[string]machineStatus { |
337 | machinesMap := make(map[string]machineStatus) |
338 | for id, machines := range context.machines { |
339 | @@ -275,7 +430,10 @@ |
340 | status.Subordinates = make(map[string]unitStatus) |
341 | for _, name := range subUnits { |
342 | subUnit := context.unitByName(name) |
343 | - status.Subordinates[name] = context.processUnit(subUnit) |
344 | + // subUnit may be nil if subordinate was filtered out. |
345 | + if subUnit != nil { |
346 | + status.Subordinates[name] = context.processUnit(subUnit) |
347 | + } |
348 | } |
349 | } |
350 | return |
351 | |
352 | === modified file 'cmd/juju/status_test.go' |
353 | --- cmd/juju/status_test.go 2013-08-06 14:17:24 +0000 |
354 | +++ cmd/juju/status_test.go 2013-08-08 09:25:42 +0000 |
355 | @@ -8,6 +8,7 @@ |
356 | "encoding/json" |
357 | "fmt" |
358 | "net/url" |
359 | + "strings" |
360 | |
361 | . "launchpad.net/gocheck" |
362 | "launchpad.net/goyaml" |
363 | @@ -153,6 +154,21 @@ |
364 | "series": "series", |
365 | "hardware": "arch=amd64 cpu-cores=1 mem=1024M", |
366 | } |
367 | + machine1WithContainersScoped = M{ |
368 | + "agent-state": "started", |
369 | + "containers": M{ |
370 | + "1/lxc/0": M{ |
371 | + "agent-state": "started", |
372 | + "dns-name": "dummyenv-2.dns", |
373 | + "instance-id": "dummyenv-2", |
374 | + "series": "series", |
375 | + }, |
376 | + }, |
377 | + "dns-name": "dummyenv-1.dns", |
378 | + "instance-id": "dummyenv-1", |
379 | + "series": "series", |
380 | + "hardware": "arch=amd64 cpu-cores=1 mem=1024M", |
381 | + } |
382 | unexposedService = M{ |
383 | "charm": "local:series/dummy-1", |
384 | "exposed": false, |
385 | @@ -497,6 +513,146 @@ |
386 | }, |
387 | }, |
388 | }, |
389 | + |
390 | + scopedExpect{ |
391 | + "scope status on dummy-service/0 unit", |
392 | + []string{"dummy-service/0"}, |
393 | + M{ |
394 | + "environment": "dummyenv", |
395 | + "machines": M{ |
396 | + "1": machine1, |
397 | + }, |
398 | + "services": M{ |
399 | + "dummy-service": M{ |
400 | + "charm": "local:series/dummy-1", |
401 | + "exposed": false, |
402 | + "units": M{ |
403 | + "dummy-service/0": M{ |
404 | + "machine": "1", |
405 | + "life": "dying", |
406 | + "agent-state": "down", |
407 | + "agent-state-info": "(started)", |
408 | + }, |
409 | + }, |
410 | + }, |
411 | + }, |
412 | + }, |
413 | + }, |
414 | + scopedExpect{ |
415 | + "scope status on exposed-service service", |
416 | + []string{"exposed-service"}, |
417 | + M{ |
418 | + "environment": "dummyenv", |
419 | + "machines": M{ |
420 | + "2": machine2, |
421 | + }, |
422 | + "services": M{ |
423 | + "exposed-service": M{ |
424 | + "charm": "local:series/dummy-1", |
425 | + "exposed": true, |
426 | + "units": M{ |
427 | + "exposed-service/0": M{ |
428 | + "machine": "2", |
429 | + "agent-state": "error", |
430 | + "agent-state-info": "You Require More Vespene Gas", |
431 | + "open-ports": L{ |
432 | + "2/tcp", "3/tcp", "2/udp", "10/udp", |
433 | + }, |
434 | + }, |
435 | + }, |
436 | + }, |
437 | + }, |
438 | + }, |
439 | + }, |
440 | + scopedExpect{ |
441 | + "scope status on service pattern", |
442 | + []string{"d*-service"}, |
443 | + M{ |
444 | + "environment": "dummyenv", |
445 | + "machines": M{ |
446 | + "1": machine1, |
447 | + }, |
448 | + "services": M{ |
449 | + "dummy-service": M{ |
450 | + "charm": "local:series/dummy-1", |
451 | + "exposed": false, |
452 | + "units": M{ |
453 | + "dummy-service/0": M{ |
454 | + "machine": "1", |
455 | + "life": "dying", |
456 | + "agent-state": "down", |
457 | + "agent-state-info": "(started)", |
458 | + }, |
459 | + }, |
460 | + }, |
461 | + }, |
462 | + }, |
463 | + }, |
464 | + scopedExpect{ |
465 | + "scope status on unit pattern", |
466 | + []string{"e*posed-service/*"}, |
467 | + M{ |
468 | + "environment": "dummyenv", |
469 | + "machines": M{ |
470 | + "2": machine2, |
471 | + }, |
472 | + "services": M{ |
473 | + "exposed-service": M{ |
474 | + "charm": "local:series/dummy-1", |
475 | + "exposed": true, |
476 | + "units": M{ |
477 | + "exposed-service/0": M{ |
478 | + "machine": "2", |
479 | + "agent-state": "error", |
480 | + "agent-state-info": "You Require More Vespene Gas", |
481 | + "open-ports": L{ |
482 | + "2/tcp", "3/tcp", "2/udp", "10/udp", |
483 | + }, |
484 | + }, |
485 | + }, |
486 | + }, |
487 | + }, |
488 | + }, |
489 | + }, |
490 | + scopedExpect{ |
491 | + "scope status on combination of service and unit patterns", |
492 | + []string{"exposed-service", "dummy-service", "e*posed-service/*", "dummy-service/*"}, |
493 | + M{ |
494 | + "environment": "dummyenv", |
495 | + "machines": M{ |
496 | + "1": machine1, |
497 | + "2": machine2, |
498 | + }, |
499 | + "services": M{ |
500 | + "dummy-service": M{ |
501 | + "charm": "local:series/dummy-1", |
502 | + "exposed": false, |
503 | + "units": M{ |
504 | + "dummy-service/0": M{ |
505 | + "machine": "1", |
506 | + "life": "dying", |
507 | + "agent-state": "down", |
508 | + "agent-state-info": "(started)", |
509 | + }, |
510 | + }, |
511 | + }, |
512 | + "exposed-service": M{ |
513 | + "charm": "local:series/dummy-1", |
514 | + "exposed": true, |
515 | + "units": M{ |
516 | + "exposed-service/0": M{ |
517 | + "machine": "2", |
518 | + "agent-state": "error", |
519 | + "agent-state-info": "You Require More Vespene Gas", |
520 | + "open-ports": L{ |
521 | + "2/tcp", "3/tcp", "2/udp", "10/udp", |
522 | + }, |
523 | + }, |
524 | + }, |
525 | + }, |
526 | + }, |
527 | + }, |
528 | + }, |
529 | ), |
530 | test( |
531 | "add a dying service", |
532 | @@ -808,6 +964,185 @@ |
533 | }, |
534 | }, |
535 | }, |
536 | + |
537 | + // scoped on 'logging' |
538 | + scopedExpect{ |
539 | + "subordinates scoped on logging", |
540 | + []string{"logging"}, |
541 | + M{ |
542 | + "environment": "dummyenv", |
543 | + "machines": M{ |
544 | + "1": machine1, |
545 | + "2": machine2, |
546 | + }, |
547 | + "services": M{ |
548 | + "wordpress": M{ |
549 | + "charm": "local:series/wordpress-3", |
550 | + "exposed": true, |
551 | + "units": M{ |
552 | + "wordpress/0": M{ |
553 | + "machine": "1", |
554 | + "agent-state": "started", |
555 | + "subordinates": M{ |
556 | + "logging/0": M{ |
557 | + "agent-state": "started", |
558 | + }, |
559 | + }, |
560 | + }, |
561 | + }, |
562 | + "relations": M{ |
563 | + "db": L{"mysql"}, |
564 | + "logging-dir": L{"logging"}, |
565 | + }, |
566 | + }, |
567 | + "mysql": M{ |
568 | + "charm": "local:series/mysql-1", |
569 | + "exposed": true, |
570 | + "units": M{ |
571 | + "mysql/0": M{ |
572 | + "machine": "2", |
573 | + "agent-state": "started", |
574 | + "subordinates": M{ |
575 | + "logging/1": M{ |
576 | + "agent-state": "error", |
577 | + "agent-state-info": "somehow lost in all those logs", |
578 | + }, |
579 | + }, |
580 | + }, |
581 | + }, |
582 | + "relations": M{ |
583 | + "server": L{"wordpress"}, |
584 | + "juju-info": L{"logging"}, |
585 | + }, |
586 | + }, |
587 | + "logging": M{ |
588 | + "charm": "local:series/logging-1", |
589 | + "exposed": true, |
590 | + "relations": M{ |
591 | + "logging-directory": L{"wordpress"}, |
592 | + "info": L{"mysql"}, |
593 | + }, |
594 | + "subordinate-to": L{"mysql", "wordpress"}, |
595 | + }, |
596 | + }, |
597 | + }, |
598 | + }, |
599 | + |
600 | + // scoped on wordpress/0 |
601 | + scopedExpect{ |
602 | + "subordinates scoped on logging", |
603 | + []string{"wordpress/0"}, |
604 | + M{ |
605 | + "environment": "dummyenv", |
606 | + "machines": M{ |
607 | + "1": machine1, |
608 | + }, |
609 | + "services": M{ |
610 | + "wordpress": M{ |
611 | + "charm": "local:series/wordpress-3", |
612 | + "exposed": true, |
613 | + "units": M{ |
614 | + "wordpress/0": M{ |
615 | + "machine": "1", |
616 | + "agent-state": "started", |
617 | + "subordinates": M{ |
618 | + "logging/0": M{ |
619 | + "agent-state": "started", |
620 | + }, |
621 | + }, |
622 | + }, |
623 | + }, |
624 | + "relations": M{ |
625 | + "db": L{"mysql"}, |
626 | + "logging-dir": L{"logging"}, |
627 | + }, |
628 | + }, |
629 | + "logging": M{ |
630 | + "charm": "local:series/logging-1", |
631 | + "exposed": true, |
632 | + "relations": M{ |
633 | + "logging-directory": L{"wordpress"}, |
634 | + "info": L{"mysql"}, |
635 | + }, |
636 | + "subordinate-to": L{"mysql", "wordpress"}, |
637 | + }, |
638 | + }, |
639 | + }, |
640 | + }, |
641 | + ), test( |
642 | + "one service with two subordinate services", |
643 | + addMachine{machineId: "0", job: state.JobManageEnviron}, |
644 | + startAliveMachine{"0"}, |
645 | + setMachineStatus{"0", params.StatusStarted, ""}, |
646 | + addCharm{"wordpress"}, |
647 | + addCharm{"logging"}, |
648 | + addCharm{"monitoring"}, |
649 | + |
650 | + addService{"wordpress", "wordpress"}, |
651 | + setServiceExposed{"wordpress", true}, |
652 | + addMachine{machineId: "1", job: state.JobHostUnits}, |
653 | + startAliveMachine{"1"}, |
654 | + setMachineStatus{"1", params.StatusStarted, ""}, |
655 | + addAliveUnit{"wordpress", "1"}, |
656 | + setUnitStatus{"wordpress/0", params.StatusStarted, ""}, |
657 | + |
658 | + addService{"logging", "logging"}, |
659 | + setServiceExposed{"logging", true}, |
660 | + addService{"monitoring", "monitoring"}, |
661 | + setServiceExposed{"monitoring", true}, |
662 | + |
663 | + relateServices{"wordpress", "logging"}, |
664 | + relateServices{"wordpress", "monitoring"}, |
665 | + |
666 | + addSubordinate{"wordpress/0", "logging"}, |
667 | + addSubordinate{"wordpress/0", "monitoring"}, |
668 | + |
669 | + setUnitsAlive{"logging"}, |
670 | + setUnitStatus{"logging/0", params.StatusStarted, ""}, |
671 | + |
672 | + setUnitsAlive{"monitoring"}, |
673 | + setUnitStatus{"monitoring/0", params.StatusStarted, ""}, |
674 | + |
675 | + // scoped on monitoring; make sure logging doesn't show up. |
676 | + scopedExpect{ |
677 | + "subordinates scoped on:", |
678 | + []string{"monitoring"}, |
679 | + M{ |
680 | + "environment": "dummyenv", |
681 | + "machines": M{ |
682 | + "1": machine1, |
683 | + }, |
684 | + "services": M{ |
685 | + "wordpress": M{ |
686 | + "charm": "local:series/wordpress-3", |
687 | + "exposed": true, |
688 | + "units": M{ |
689 | + "wordpress/0": M{ |
690 | + "machine": "1", |
691 | + "agent-state": "started", |
692 | + "subordinates": M{ |
693 | + "monitoring/0": M{ |
694 | + "agent-state": "started", |
695 | + }, |
696 | + }, |
697 | + }, |
698 | + }, |
699 | + "relations": M{ |
700 | + "logging-dir": L{"logging"}, |
701 | + "monitoring-port": L{"monitoring"}, |
702 | + }, |
703 | + }, |
704 | + "monitoring": M{ |
705 | + "charm": "local:series/monitoring-0", |
706 | + "exposed": true, |
707 | + "relations": M{ |
708 | + "monitoring-port": L{"wordpress"}, |
709 | + }, |
710 | + "subordinate-to": L{"wordpress"}, |
711 | + }, |
712 | + }, |
713 | + }, |
714 | + }, |
715 | ), test( |
716 | "machines with containers", |
717 | addMachine{machineId: "0", job: state.JobManageEnviron}, |
718 | @@ -862,6 +1197,30 @@ |
719 | }, |
720 | }, |
721 | }, |
722 | + |
723 | + // once again, with a scope on mysql/1 |
724 | + scopedExpect{ |
725 | + "machines with nested containers", |
726 | + []string{"mysql/1"}, |
727 | + M{ |
728 | + "environment": "dummyenv", |
729 | + "machines": M{ |
730 | + "1": machine1WithContainersScoped, |
731 | + }, |
732 | + "services": M{ |
733 | + "mysql": M{ |
734 | + "charm": "local:series/mysql-1", |
735 | + "exposed": true, |
736 | + "units": M{ |
737 | + "mysql/1": M{ |
738 | + "machine": "1/lxc/0", |
739 | + "agent-state": "started", |
740 | + }, |
741 | + }, |
742 | + }, |
743 | + }, |
744 | + }, |
745 | + }, |
746 | ), |
747 | } |
748 | |
749 | @@ -1175,19 +1534,26 @@ |
750 | c.Assert(err, IsNil) |
751 | } |
752 | |
753 | +type scopedExpect struct { |
754 | + what string |
755 | + scope []string |
756 | + output M |
757 | +} |
758 | + |
759 | type expect struct { |
760 | what string |
761 | output M |
762 | } |
763 | |
764 | -func (e expect) step(c *C, ctx *context) { |
765 | - c.Logf("expect: %s", e.what) |
766 | +func (e scopedExpect) step(c *C, ctx *context) { |
767 | + c.Logf("expect: %s %s", e.what, strings.Join(e.scope, " ")) |
768 | |
769 | // Now execute the command for each format. |
770 | for _, format := range statusFormats { |
771 | c.Logf("format %q", format.name) |
772 | // Run command with the required format. |
773 | - code, stdout, stderr := runStatus(c, "--format", format.name) |
774 | + args := append([]string{"--format", format.name}, e.scope...) |
775 | + code, stdout, stderr := runStatus(c, args...) |
776 | c.Assert(code, Equals, 0) |
777 | c.Assert(stderr, HasLen, 0) |
778 | |
779 | @@ -1206,6 +1572,10 @@ |
780 | } |
781 | } |
782 | |
783 | +func (e expect) step(c *C, ctx *context) { |
784 | + scopedExpect{e.what, nil, e.output}.step(c, ctx) |
785 | +} |
786 | + |
787 | func (s *StatusSuite) TestStatusAllFormats(c *C) { |
788 | for i, t := range statusTests { |
789 | c.Logf("test %d: %s", i, t.summary) |
790 | @@ -1217,3 +1587,32 @@ |
791 | }() |
792 | } |
793 | } |
794 | + |
795 | +func (s *StatusSuite) TestStatusFilterErrors(c *C) { |
796 | + steps := []stepper{ |
797 | + addMachine{machineId: "0", job: state.JobManageEnviron}, |
798 | + addMachine{machineId: "1", job: state.JobHostUnits}, |
799 | + addCharm{"mysql"}, |
800 | + addService{"mysql", "mysql"}, |
801 | + addAliveUnit{"mysql", "1"}, |
802 | + } |
803 | + ctx := s.newContext() |
804 | + defer s.resetContext(c, ctx) |
805 | + ctx.run(c, steps) |
806 | + |
807 | + // Status filters can only fail if the patterns are invalid. |
808 | + code, _, stderr := runStatus(c, "[*") |
809 | + c.Assert(code, Not(Equals), 0) |
810 | + c.Assert(string(stderr), Equals, `error: pattern "[*" contains invalid characters`+"\n") |
811 | + |
812 | + code, _, stderr = runStatus(c, "//") |
813 | + c.Assert(code, Not(Equals), 0) |
814 | + c.Assert(string(stderr), Equals, `error: pattern "//" contains too many '/' characters`+"\n") |
815 | + |
816 | + // Pattern validity is checked eagerly; if a bad pattern |
817 | + // proceeds a valid, matching pattern, then the bad pattern |
818 | + // will still cause an error. |
819 | + code, _, stderr = runStatus(c, "*", "[*") |
820 | + c.Assert(code, Not(Equals), 0) |
821 | + c.Assert(string(stderr), Equals, `error: pattern "[*" contains invalid characters`+"\n") |
822 | +} |
823 | |
824 | === modified file 'state/service_test.go' |
825 | --- state/service_test.go 2013-07-18 16:46:59 +0000 |
826 | +++ state/service_test.go 2013-08-08 09:25:42 +0000 |
827 | @@ -652,6 +652,18 @@ |
828 | }, |
829 | }) |
830 | |
831 | + mpEP, err := wordpress.Endpoint("monitoring-port") |
832 | + c.Assert(err, IsNil) |
833 | + c.Assert(mpEP, DeepEquals, state.Endpoint{ |
834 | + ServiceName: "wordpress", |
835 | + Relation: charm.Relation{ |
836 | + Interface: "monitoring", |
837 | + Name: "monitoring-port", |
838 | + Role: charm.RoleProvider, |
839 | + Scope: charm.ScopeContainer, |
840 | + }, |
841 | + }) |
842 | + |
843 | dbEP, err := wordpress.Endpoint("db") |
844 | c.Assert(err, IsNil) |
845 | c.Assert(dbEP, DeepEquals, state.Endpoint{ |
846 | @@ -681,7 +693,7 @@ |
847 | |
848 | eps, err := wordpress.Endpoints() |
849 | c.Assert(err, IsNil) |
850 | - c.Assert(eps, DeepEquals, []state.Endpoint{cacheEP, dbEP, jiEP, ldEP, urlEP}) |
851 | + c.Assert(eps, DeepEquals, []state.Endpoint{cacheEP, dbEP, jiEP, ldEP, mpEP, urlEP}) |
852 | } |
853 | |
854 | func (s *ServiceSuite) TestServiceRefresh(c *C) { |
855 | |
856 | === modified file 'state/unit.go' |
857 | --- state/unit.go 2013-08-06 17:08:26 +0000 |
858 | +++ state/unit.go 2013-08-08 09:25:42 +0000 |
859 | @@ -442,6 +442,12 @@ |
860 | return "", false |
861 | } |
862 | |
863 | +// PrincipalName returns the name of the unit's principal. |
864 | +// If the unit is not a subordinate, false is returned. |
865 | +func (u *Unit) PrincipalName() (string, bool) { |
866 | + return u.doc.Principal, u.doc.Principal != "" |
867 | +} |
868 | + |
869 | // PublicAddress returns the public address of the unit and whether it is valid. |
870 | func (u *Unit) PublicAddress() (string, bool) { |
871 | return u.doc.PublicAddress, u.doc.PublicAddress != "" |
872 | |
873 | === modified file 'state/unit_test.go' |
874 | --- state/unit_test.go 2013-07-31 14:19:29 +0000 |
875 | +++ state/unit_test.go 2013-08-08 09:25:42 +0000 |
876 | @@ -886,6 +886,36 @@ |
877 | c.Assert(err, IsNil) |
878 | } |
879 | |
880 | +func (s *UnitSuite) TestPrincipalName(c *C) { |
881 | + subCharm := s.AddTestingCharm(c, "logging") |
882 | + _, err := s.State.AddService("logging", subCharm) |
883 | + c.Assert(err, IsNil) |
884 | + eps, err := s.State.InferEndpoints([]string{"logging", "wordpress"}) |
885 | + c.Assert(err, IsNil) |
886 | + rel, err := s.State.AddRelation(eps...) |
887 | + c.Assert(err, IsNil) |
888 | + ru, err := rel.Unit(s.unit) |
889 | + c.Assert(err, IsNil) |
890 | + err = ru.EnterScope(nil) |
891 | + c.Assert(err, IsNil) |
892 | + |
893 | + err = s.unit.Refresh() |
894 | + c.Assert(err, IsNil) |
895 | + subordinates := s.unit.SubordinateNames() |
896 | + c.Assert(subordinates, DeepEquals, []string{"logging/0"}) |
897 | + |
898 | + su, err := s.State.Unit("logging/0") |
899 | + c.Assert(err, IsNil) |
900 | + principal, valid := su.PrincipalName() |
901 | + c.Assert(valid, Equals, true) |
902 | + c.Assert(principal, Equals, s.unit.Name()) |
903 | + |
904 | + // Calling PrincipalName on a principal unit yields "", false. |
905 | + principal, valid = s.unit.PrincipalName() |
906 | + c.Assert(valid, Equals, false) |
907 | + c.Assert(principal, Equals, "") |
908 | +} |
909 | + |
910 | func (s *UnitSuite) TestRemove(c *C) { |
911 | err := s.unit.Remove() |
912 | c.Assert(err, ErrorMatches, `cannot remove unit "wordpress/0": unit is not dead`) |
913 | |
914 | === added directory 'testing/repo/series/monitoring' |
915 | === added directory 'testing/repo/series/monitoring/hooks' |
916 | === added file 'testing/repo/series/monitoring/metadata.yaml' |
917 | --- testing/repo/series/monitoring/metadata.yaml 1970-01-01 00:00:00 +0000 |
918 | +++ testing/repo/series/monitoring/metadata.yaml 2013-08-08 09:25:42 +0000 |
919 | @@ -0,0 +1,16 @@ |
920 | +name: monitoring |
921 | +summary: "Subordinate monitoring test charm" |
922 | +description: | |
923 | + This is a longer description which |
924 | + potentially contains multiple lines. |
925 | +subordinate: true |
926 | +provides: |
927 | + monitoring-client: |
928 | + interface: monitoring |
929 | +requires: |
930 | + monitoring-port: |
931 | + interface: monitoring |
932 | + scope: container |
933 | + info: |
934 | + interface: juju-info |
935 | + scope: container |
936 | |
937 | === modified file 'testing/repo/series/wordpress/metadata.yaml' |
938 | --- testing/repo/series/wordpress/metadata.yaml 2012-10-30 16:14:26 +0000 |
939 | +++ testing/repo/series/wordpress/metadata.yaml 2013-08-08 09:25:42 +0000 |
940 | @@ -9,6 +9,9 @@ |
941 | logging-dir: |
942 | interface: logging |
943 | scope: container |
944 | + monitoring-port: |
945 | + interface: monitoring |
946 | + scope: container |
947 | requires: |
948 | db: |
949 | interface: mysql |
Reviewers: mp+178004_ code.launchpad. net,
Message:
Please take a look.
Description:
juju status: add service/unit filters
Any positional arguments after juju status are
now interpreted as filters for either service or
unit. If the filter includes a '/', then it is
considered a unit filter, else a service filter.
If all units of a service are filtered out, then
the service is filtered out; if all units of a
machine are filtered out, the machine is filtered
out (except to fill out the container hierarchy).
Filters are implemented using using shell file golang. org/pkg/ path/#Match
name pattern matching, like in pyjuju. See:
http://
Partially fixes bug 1121916
https:/ /code.launchpad .net/~axwalk/ juju-core/ lp1121916- juju-status- filters/ +merge/ 178004
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/12235043/
Affected files: juju/cmd_ test.go juju/status. go juju/status_ test.go
[revision details]
cmd/
cmd/
cmd/