Index: cmd/juju/status.go |
=== modified file 'cmd/juju/status.go' |
--- cmd/juju/status.go 2013-07-30 01:31:52 +0000 |
+++ cmd/juju/status.go 2013-08-08 02:57:02 +0000 |
@@ -6,6 +6,7 @@ |
import ( |
"encoding/json" |
"fmt" |
+ "path" |
"strings" |
"launchpad.net/gnuflag" |
@@ -24,14 +25,27 @@ |
type StatusCommand struct { |
cmd.EnvCommandBase |
- out cmd.Output |
+ out cmd.Output |
+ patterns []string |
} |
-var statusDoc = "This command will report on the runtime state of various system entities." |
+var statusDoc = ` |
+This command will report on the runtime state of various system entities. |
+ |
+Service or unit names may be specified to filter the status to only those |
+services and units that match, along with the related machines, services |
+and units. If a subordinate unit is matched, then its principal unit will |
+be displayed. If a principal unit is matched, then all of its subordinates |
+will be displayed. |
+ |
+Wildcards ('*') may be specified in service/unit names to match any sequence |
+of characters. For example, 'nova-*' will match any service whose name begins |
+with 'nova-': 'nova-compute', 'nova-volume', etc.`[1:] |
func (c *StatusCommand) Info() *cmd.Info { |
return &cmd.Info{ |
Name: "status", |
+ Args: "[pattern ...]", |
Purpose: "output status information about an environment", |
Doc: statusDoc, |
Aliases: []string{"stat"}, |
@@ -46,6 +60,11 @@ |
}) |
} |
+func (c *StatusCommand) Init(args []string) error { |
+ c.patterns = args |
+ return nil |
+} |
+ |
type statusContext struct { |
instances map[instance.Id]instance.Instance |
machines map[string][]*state.Machine |
@@ -53,6 +72,102 @@ |
units map[string]map[string]*state.Unit |
} |
+type unitMatcher struct { |
+ patterns []string |
+} |
+ |
+// matchesAny returns true if the unitMatcher will |
+// match any unit, regardless of its attributes. |
+func (m unitMatcher) matchesAny() bool { |
+ return len(m.patterns) == 0 |
+} |
+ |
+// matchUnit attempts to match a state.Unit to one of |
+// a set of patterns, taking into account subordinate |
+// relationships. |
+func (m unitMatcher) matchUnit(u *state.Unit) bool { |
+ if m.matchesAny() { |
+ return true |
+ } |
+ |
+ // Keep the unit if: |
+ // (a) its name matches a pattern, or |
+ // (b) it's a principal and one of its subordinates matches, or |
+ // (c) it's a subordinate and its principal matches. |
+ // |
+ // Note: do *not* include a second subordinate if the principal is |
+ // only matched on account of a first subordinate matching. |
+ if m.matchString(u.Name()) { |
+ return true |
+ } |
+ if u.IsPrincipal() { |
+ for _, s := range u.SubordinateNames() { |
+ if m.matchString(s) { |
+ return true |
+ } |
+ } |
+ return false |
+ } |
+ principal, valid := u.PrincipalName() |
+ if !valid { |
+ panic("PrincipalName failed for subordinate unit") |
+ } |
+ return m.matchString(principal) |
+} |
+ |
+// matchString matches a string to one of the patterns in |
+// the unit matcher, returning an error if a pattern with |
+// invalid syntax is encountered. |
+func (m unitMatcher) matchString(s string) bool { |
+ for _, pattern := range m.patterns { |
+ ok, err := path.Match(pattern, s) |
+ if err != nil { |
+ // We validate patterns, so should never get here. |
+ panic(fmt.Errorf("pattern syntax error in %q", pattern)) |
+ } else if ok { |
+ return true |
+ } |
+ } |
+ return false |
+} |
+ |
+// validatePattern checks if a string contains invalid |
+// pattern characters, and, if so, returns an error. |
+func validatePattern(s string) error { |
+ const valid = "abcdefghijklmnopqrstuvwxyz0123456789-*" |
rog
2013/08/08 07:52:43
or:
var validPattern = regexp.MustCompile("^[a-z0
|
+ for _, r := range s { |
+ if !strings.ContainsRune(valid, r) { |
+ return fmt.Errorf("pattern %q contains invalid character: %c", s, r) |
+ } |
+ } |
+ return nil |
+} |
+ |
+// newUnitMatcher returns a unitMatcher that matches units |
+// with one of the specified patterns, or all units if no |
+// patterns are specified. |
+// |
+// An error will be returned if any of the specified patterns |
+// is invalid. Patterns are valid if they contain only |
+// alpha-numeric characters, hyphens, or asterisks. |
+func newUnitMatcher(patterns []string) (unitMatcher, error) { |
+ for i, pattern := range patterns { |
+ fields := strings.Split(pattern, "/") |
+ if len(fields) > 2 { |
+ return unitMatcher{}, fmt.Errorf("pattern %q contains too many '/' characters", pattern) |
+ } |
+ for _, f := range fields { |
+ if err := validatePattern(f); err != nil { |
+ return unitMatcher{}, err |
+ } |
+ } |
+ if len(fields) == 1 { |
+ patterns[i] += "/*" |
+ } |
+ } |
+ return unitMatcher{patterns}, nil |
+} |
+ |
func (c *StatusCommand) Run(ctx *cmd.Context) error { |
conn, err := juju.NewConnFromName(c.EnvName) |
if err != nil { |
@@ -61,12 +176,26 @@ |
defer conn.Close() |
var context statusContext |
- if context.machines, err = fetchAllMachines(conn.State); err != nil { |
- return err |
- } |
- if context.services, context.units, err = fetchAllServicesAndUnits(conn.State); err != nil { |
- return err |
- } |
+ unitMatcher, err := newUnitMatcher(c.patterns) |
+ if err != nil { |
+ return err |
+ } |
+ if context.services, context.units, err = fetchAllServicesAndUnits(conn.State, unitMatcher); err != nil { |
+ return err |
+ } |
+ |
+ // Filter machines by units in scope. |
+ var machineIds *set.Strings |
+ if !unitMatcher.matchesAny() { |
+ machineIds, err = fetchUnitMachineIds(context.units) |
+ if err != nil { |
+ return err |
+ } |
+ } |
+ if context.machines, err = fetchMachines(conn.State, machineIds); err != nil { |
+ return err |
+ } |
+ |
context.instances, err = fetchAllInstances(conn.Environ) |
if err != nil { |
// We cannot see instances from the environment, but |
@@ -96,9 +225,11 @@ |
return m, nil |
} |
-// fetchAllMachines returns a map from top level machine id to machines, where machines[0] is the host |
+// fetchMachines returns a map from top level machine id to machines, where machines[0] is the host |
// machine and machines[1..n] are any containers (including nested ones). |
-func fetchAllMachines(st *state.State) (map[string][]*state.Machine, error) { |
+// |
+// If machineIds is non-nil, only machines whose IDs are in the set are returned. |
+func fetchMachines(st *state.State, machineIds *set.Strings) (map[string][]*state.Machine, error) { |
v := make(map[string][]*state.Machine) |
machines, err := st.AllMachines() |
if err != nil { |
@@ -106,6 +237,9 @@ |
} |
// AllMachines gives us machines sorted by id. |
for _, m := range machines { |
+ if machineIds != nil && !machineIds.Contains(m.Id()) { |
+ continue |
+ } |
parentId, ok := m.ParentId() |
if !ok { |
// Only top level host machines go directly into the machine map. |
@@ -125,7 +259,7 @@ |
// fetchAllServicesAndUnits returns a map from service name to service |
// and a map from service name to unit name to unit. |
-func fetchAllServicesAndUnits(st *state.State) (map[string]*state.Service, map[string]map[string]*state.Unit, error) { |
+func fetchAllServicesAndUnits(st *state.State, unitMatcher unitMatcher) (map[string]*state.Service, map[string]map[string]*state.Unit, error) { |
svcMap := make(map[string]*state.Service) |
unitMap := make(map[string]map[string]*state.Unit) |
services, err := st.AllServices() |
@@ -133,20 +267,47 @@ |
return nil, nil, err |
} |
for _, s := range services { |
- svcMap[s.Name()] = s |
units, err := s.AllUnits() |
if err != nil { |
return nil, nil, err |
} |
svcUnitMap := make(map[string]*state.Unit) |
for _, u := range units { |
+ if !unitMatcher.matchUnit(u) { |
+ continue |
+ } |
svcUnitMap[u.Name()] = u |
} |
- unitMap[s.Name()] = svcUnitMap |
+ if unitMatcher.matchesAny() || len(svcUnitMap) > 0 { |
+ unitMap[s.Name()] = svcUnitMap |
+ svcMap[s.Name()] = s |
+ } |
} |
return svcMap, unitMap, nil |
} |
+// fetchUnitMachineIds returns a set of IDs for machines that |
+// the specified units reside on, and those machines' ancestors. |
+func fetchUnitMachineIds(units map[string]map[string]*state.Unit) (*set.Strings, error) { |
+ machineIds := new(set.Strings) |
+ for _, svcUnitMap := range units { |
+ for _, unit := range svcUnitMap { |
+ if !unit.IsPrincipal() { |
+ continue |
+ } |
+ mid, err := unit.AssignedMachineId() |
+ if err != nil { |
+ return nil, err |
+ } |
+ for mid != "" { |
+ machineIds.Add(mid) |
+ mid = state.ParentId(mid) |
+ } |
+ } |
+ } |
+ return machineIds, nil |
+} |
+ |
func (context *statusContext) processMachines() map[string]machineStatus { |
machinesMap := make(map[string]machineStatus) |
for id, machines := range context.machines { |
@@ -273,7 +434,10 @@ |
status.Subordinates = make(map[string]unitStatus) |
for _, name := range subUnits { |
subUnit := context.unitByName(name) |
- status.Subordinates[name] = context.processUnit(subUnit) |
+ // subUnit may be nil if subordinate was filtered out. |
+ if subUnit != nil { |
+ status.Subordinates[name] = context.processUnit(subUnit) |
+ } |
} |
} |
return |