services/device/device: allow specification of excluded states from glob filter

This cl introduces the option of prefixing the value of the instance-state and
installation-state flags by '!', which means that the states listed are to be
excluded (act as a blacklist rather than the default whilelist).

Change-Id: Ic013337164578e30cbf057a6fc736d502910761e
diff --git a/services/device/device/doc.go b/services/device/device/doc.go
index d25dc90..76fa353 100644
--- a/services/device/device/doc.go
+++ b/services/device/device/doc.go
@@ -262,11 +262,14 @@
  -installation-state=
    If non-empty, specifies allowed installation states (all others installations
    get filtered out). The value of the flag is a comma-separated list of values
-   from among: [Active Uninstalled].
+   from among: [Active Uninstalled]. If the value is prefixed by '!', the list
+   acts as a blacklist (all matching installations get filtered out).
  -instance-state=
    If non-empty, specifies allowed instance states (all other instances get
    filtered out). The value of the flag is a comma-separated list of values from
-   among: [Launching Running Dying NotRunning Updating Deleted].
+   among: [Launching Running Dying NotRunning Updating Deleted]. If the value is
+   prefixed by '!', the list acts as a blacklist (all matching instances get
+   filtered out).
  -only-installations=false
    If set, only consider installations.
  -only-instances=false
@@ -289,11 +292,14 @@
  -installation-state=
    If non-empty, specifies allowed installation states (all others installations
    get filtered out). The value of the flag is a comma-separated list of values
-   from among: [Active Uninstalled].
+   from among: [Active Uninstalled]. If the value is prefixed by '!', the list
+   acts as a blacklist (all matching installations get filtered out).
  -instance-state=
    If non-empty, specifies allowed instance states (all other instances get
    filtered out). The value of the flag is a comma-separated list of values from
-   among: [Launching Running Dying NotRunning Updating Deleted].
+   among: [Launching Running Dying NotRunning Updating Deleted]. If the value is
+   prefixed by '!', the list acts as a blacklist (all matching instances get
+   filtered out).
  -only-installations=false
    If set, only consider installations.
  -only-instances=false
@@ -316,11 +322,14 @@
  -installation-state=
    If non-empty, specifies allowed installation states (all others installations
    get filtered out). The value of the flag is a comma-separated list of values
-   from among: [Active Uninstalled].
+   from among: [Active Uninstalled]. If the value is prefixed by '!', the list
+   acts as a blacklist (all matching installations get filtered out).
  -instance-state=
    If non-empty, specifies allowed instance states (all other instances get
    filtered out). The value of the flag is a comma-separated list of values from
-   among: [Launching Running Dying NotRunning Updating Deleted].
+   among: [Launching Running Dying NotRunning Updating Deleted]. If the value is
+   prefixed by '!', the list acts as a blacklist (all matching instances get
+   filtered out).
  -only-installations=false
    If set, only consider installations.
  -only-instances=false
@@ -343,11 +352,14 @@
  -installation-state=
    If non-empty, specifies allowed installation states (all others installations
    get filtered out). The value of the flag is a comma-separated list of values
-   from among: [Active Uninstalled].
+   from among: [Active Uninstalled]. If the value is prefixed by '!', the list
+   acts as a blacklist (all matching installations get filtered out).
  -instance-state=
    If non-empty, specifies allowed instance states (all other instances get
    filtered out). The value of the flag is a comma-separated list of values from
-   among: [Launching Running Dying NotRunning Updating Deleted].
+   among: [Launching Running Dying NotRunning Updating Deleted]. If the value is
+   prefixed by '!', the list acts as a blacklist (all matching instances get
+   filtered out).
  -only-installations=false
    If set, only consider installations.
  -only-instances=false
@@ -458,11 +470,14 @@
  -installation-state=
    If non-empty, specifies allowed installation states (all others installations
    get filtered out). The value of the flag is a comma-separated list of values
-   from among: [Active Uninstalled].
+   from among: [Active Uninstalled]. If the value is prefixed by '!', the list
+   acts as a blacklist (all matching installations get filtered out).
  -instance-state=
    If non-empty, specifies allowed instance states (all other instances get
    filtered out). The value of the flag is a comma-separated list of values from
-   among: [Launching Running Dying NotRunning Updating Deleted].
+   among: [Launching Running Dying NotRunning Updating Deleted]. If the value is
+   prefixed by '!', the list acts as a blacklist (all matching instances get
+   filtered out).
  -only-installations=false
    If set, only consider installations.
  -only-instances=false
diff --git a/services/device/device/glob.go b/services/device/device/glob.go
index 711e76d..9f4ee8b 100644
--- a/services/device/device/glob.go
+++ b/services/device/device/glob.go
@@ -316,7 +316,10 @@
 	return results
 }
 
-type genericStateFlag map[genericState]bool
+type genericStateFlag struct {
+	set     map[genericState]bool
+	exclude bool
+}
 
 // genericState interface is meant to abstract device.InstanceState and
 // device.InstallationState.  We only make use of the String method, but we
@@ -327,22 +330,30 @@
 type genericState fmt.Stringer
 
 func (f *genericStateFlag) apply(state genericState) bool {
-	if len(*f) == 0 {
+	if len(f.set) == 0 {
 		return true
 	}
-	return (*f)[state]
+	return f.exclude != f.set[state]
 }
 
 func (f *genericStateFlag) String() string {
-	states := make([]string, 0, len(*f))
-	for s := range *f {
+	states := make([]string, 0, len(f.set))
+	for s := range f.set {
 		states = append(states, s.String())
 	}
 	sort.Strings(states)
-	return strings.Join(states, ",")
+	statesStr := strings.Join(states, ",")
+	if f.exclude {
+		return "!" + statesStr
+	}
+	return statesStr
 }
 
 func (f *genericStateFlag) fromString(s string, stateConstructor func(string) (genericState, error)) error {
+	if len(s) > 0 && s[0] == '!' {
+		f.exclude = true
+		s = s[1:]
+	}
 	states := strings.Split(s, ",")
 	for _, s := range states {
 		state, err := stateConstructor(s)
@@ -355,10 +366,10 @@
 }
 
 func (f *genericStateFlag) add(s genericState) {
-	if *f == nil {
-		*f = make(genericStateFlag)
+	if f.set == nil {
+		f.set = make(map[genericState]bool)
 	}
-	(*f)[s] = true
+	f.set[s] = true
 }
 
 type instanceStateFlag struct {
@@ -378,6 +389,12 @@
 	return
 }
 
+func ExcludeInstanceStates(states ...device.InstanceState) instanceStateFlag {
+	f := InstanceStates(states...)
+	f.exclude = true
+	return f
+}
+
 type installationStateFlag struct {
 	genericStateFlag
 }
@@ -395,6 +412,12 @@
 	return
 }
 
+func ExcludeInstallationStates(states ...device.InstallationState) installationStateFlag {
+	f := InstallationStates(states...)
+	f.exclude = true
+	return f
+}
+
 type parallelismFlag int
 
 const (
@@ -467,8 +490,8 @@
 }
 
 func defineGlobFlags(fs *flag.FlagSet, s *GlobSettings) {
-	fs.Var(&s.InstanceStateFilter, "instance-state", fmt.Sprintf("If non-empty, specifies allowed instance states (all other instances get filtered out). The value of the flag is a comma-separated list of values from among: %v.", device.InstanceStateAll))
-	fs.Var(&s.InstallationStateFilter, "installation-state", fmt.Sprintf("If non-empty, specifies allowed installation states (all others installations get filtered out). The value of the flag is a comma-separated list of values from among: %v.", device.InstallationStateAll))
+	fs.Var(&s.InstanceStateFilter, "instance-state", fmt.Sprintf("If non-empty, specifies allowed instance states (all other instances get filtered out). The value of the flag is a comma-separated list of values from among: %v. If the value is prefixed by '!', the list acts as a blacklist (all matching instances get filtered out).", device.InstanceStateAll))
+	fs.Var(&s.InstallationStateFilter, "installation-state", fmt.Sprintf("If non-empty, specifies allowed installation states (all others installations get filtered out). The value of the flag is a comma-separated list of values from among: %v. If the value is prefixed by '!', the list acts as a blacklist (all matching installations get filtered out).", device.InstallationStateAll))
 	fs.BoolVar(&s.OnlyInstances, "only-instances", false, "If set, only consider instances.")
 	fs.BoolVar(&s.OnlyInstallations, "only-installations", false, "If set, only consider installations.")
 	var parallelismValues []string
diff --git a/services/device/device/glob_test.go b/services/device/device/glob_test.go
index 084bd77..eceae7c 100644
--- a/services/device/device/glob_test.go
+++ b/services/device/device/glob_test.go
@@ -287,6 +287,39 @@
 			"",
 			"",
 		},
+		// Verifies "instance state" filter with more than 1 state.
+		{
+			simplePrintHandler,
+			allGlobResponses,
+			allStatusResponses,
+			cmd_device.GlobSettings{InstanceStateFilter: cmd_device.InstanceStates(device.InstanceStateUpdating, device.InstanceStateRunning)},
+			allGlobArgs,
+			joinLines(app2Out, app4Out, app7Out, app1Out, app3Out, app9Out, app6Out, app8Out),
+			"",
+			"",
+		},
+		// Verifies "instance state" filter with excluded state.
+		{
+			simplePrintHandler,
+			allGlobResponses,
+			allStatusResponses,
+			cmd_device.GlobSettings{InstanceStateFilter: cmd_device.ExcludeInstanceStates(device.InstanceStateUpdating)},
+			allGlobArgs,
+			joinLines(app2Out, app4Out, app7Out, app1Out, app5Out, app6Out, app8Out),
+			"",
+			"",
+		},
+		// Verifies "instance state" filter with more than 1 excluded state.
+		{
+			simplePrintHandler,
+			allGlobResponses,
+			allStatusResponses,
+			cmd_device.GlobSettings{InstanceStateFilter: cmd_device.ExcludeInstanceStates(device.InstanceStateUpdating, device.InstanceStateRunning)},
+			allGlobArgs,
+			joinLines(app2Out, app4Out, app7Out, app5Out, app6Out, app8Out),
+			"",
+			"",
+		},
 		// Verifies "installation state" filter.
 		{
 			simplePrintHandler,
@@ -298,6 +331,39 @@
 			"",
 			"",
 		},
+		// Verifies "installation state" filter with more than 1 state.
+		{
+			simplePrintHandler,
+			allGlobResponses,
+			allStatusResponses,
+			cmd_device.GlobSettings{InstallationStateFilter: cmd_device.InstallationStates(device.InstallationStateActive, device.InstallationStateUninstalled)},
+			allGlobArgs,
+			joinLines(app2Out, app4Out, app7Out, app1Out, app3Out, app5Out, app9Out, app6Out, app8Out),
+			"",
+			"",
+		},
+		// Verifies "installation state" filter with excluded state.
+		{
+			simplePrintHandler,
+			allGlobResponses,
+			allStatusResponses,
+			cmd_device.GlobSettings{InstallationStateFilter: cmd_device.ExcludeInstallationStates(device.InstallationStateActive)},
+			allGlobArgs,
+			joinLines(app2Out, app1Out, app3Out, app5Out, app9Out, app6Out, app8Out),
+			"",
+			"",
+		},
+		// Verifies "installation state" filter with more than 1 excluded state.
+		{
+			simplePrintHandler,
+			allGlobResponses,
+			allStatusResponses,
+			cmd_device.GlobSettings{InstallationStateFilter: cmd_device.ExcludeInstallationStates(device.InstallationStateActive, device.InstallationStateUninstalled)},
+			allGlobArgs,
+			joinLines(app1Out, app3Out, app5Out, app9Out, app6Out, app8Out),
+			"",
+			"",
+		},
 		// Verifies "installation state" filter + "only installations" filter.
 		{
 			simplePrintHandler,