commit cc8c4bd69565dc0c52938e653c8cbf9a937d56a7
Author: Teo Lisitza <teo@livefyre.com>
Date:   Fri May 20 16:10:28 2011 -0700

    Add clusters decorator

diff --git a/fabric/decorators.py b/fabric/decorators.py
index e68eeec..b0e8dcc 100644
--- a/fabric/decorators.py
+++ b/fabric/decorators.py
@@ -81,6 +81,35 @@ def roles(*role_list):
         return inner_decorator
     return attach_roles
 
+def clusters(*cluster_list):
+    """ 
+    Decorator defining which host or hosts to execute the wrapped function on.
+
+    For example, the following will ensure that, barring an override on the
+    command line, ``my_func`` will be run on ``staging``, ``production``.
+
+        @clusters('staging', 'production')
+        def my_func():
+            pass
+
+    `~lffabric.decorators.clusters` may be invoked with either an argument list
+    (``@clusters('host1')``, ``@clusters('host1', 'host2')``) or a single, iterable
+    argument (``@clusters(['host1', 'host2'])``).
+
+    Note that this decorator actually just sets the function's ``.clusters``
+    attribute, which is then read prior to executing the function.
+    """
+    def attach_clusters(func):
+        @wraps(func)
+        def inner_decorator(*args, **kwargs):
+            return func(*args, **kwargs)
+        _clusters = cluster_list
+        # Allow for single iterable argument as well as *args
+        if len(_clusters) == 1 and not isinstance(_clusters[0], StringTypes):
+            _clusters = _clusters[0]
+        inner_decorator.clusters = list(_clusters)
+        return inner_decorator
+    return attach_clusters
 
 def runs_once(func):
     """

commit 83295559052867911855a8e6bc21644a8b65bbeb
Author: Teo Lisitza <teo@livefyre.com>
Date:   Fri May 20 16:00:43 2011 -0700

    Add cluster support to Fabric

diff --git a/fabric/main.py b/fabric/main.py
index dfb435b..4b6cfc1 100644
--- a/fabric/main.py
+++ b/fabric/main.py
@@ -10,6 +10,7 @@ to individuals leveraging Fabric as a library, should be kept elsewhere.
 """
 
 from operator import add
+import operator
 from optparse import OptionParser
 import os
 import sys
@@ -312,13 +313,15 @@ def parse_arguments(arguments):
         kwargs = {}
         hosts = []
         roles = []
+        clusters = []
         if ':' in cmd:
             cmd, argstr = cmd.split(':', 1)
             for pair in _escape_split(',', argstr):
                 k, _, v = pair.partition('=')
                 if _:
                     # Catch, interpret host/hosts/role/roles kwargs
-                    if k in ['host', 'hosts', 'role', 'roles']:
+                    if k in ['host', 'hosts', 'role', 'roles',
+                             'cluster', 'clusters']:
                         if k == 'host':
                             hosts = [v.strip()]
                         elif k == 'hosts':
@@ -327,12 +330,16 @@ def parse_arguments(arguments):
                             roles = [v.strip()]
                         elif k == 'roles':
                             roles = [x.strip() for x in v.split(';')]
+                        elif k == 'cluster':
+                            clusters = [v.strip()]
+                        elif k == 'clusters':
+                            clusters = [x.strip() for x in v.split(';')]
                     # Otherwise, record as usual
                     else:
                         kwargs[k] = v
                 else:
                     args.append(k)
-        cmds.append((cmd, args, kwargs, hosts, roles))
+        cmds.append((cmd, args, kwargs, hosts, roles, clusters))
     return cmds
 
 
@@ -343,28 +350,57 @@ def parse_remainder(arguments):
     return ' '.join(arguments)
 
 
-def _merge(hosts, roles):
+def _merge(hosts, roles, clusters):
     """
     Merge given host and role lists into one list of deduped hosts.
     """
-    # Abort if any roles don't exist
-    bad_roles = [x for x in roles if x not in state.env.roledefs]
-    if bad_roles:
-        abort("The following specified roles do not exist:\n%s" % (
-            indent(bad_roles)
-        ))
-
-    # Look up roles, turn into flat list of hosts
     role_hosts = []
-    for role in roles:
-        value = state.env.roledefs[role]
-        # Handle "lazy" roles (callables)
-        if callable(value):
-            value = value()
-        role_hosts += value
-    # Return deduped combo of hosts and role_hosts
-    return list(set(_clean_hosts(hosts + role_hosts)))
+    if roles:
+        # Abort if any roles don't exist
+        bad_roles = [x for x in roles if x not in state.env.roledefs]
+        if bad_roles:
+            abort("The following specified roles do not exist:\n%s" % (
+                indent(bad_roles)
+            ))
+
+        # Look up roles, turn into flat list of hosts
+        for role in roles:
+            value = state.env.roledefs[role]
+            # Handle "lazy" roles (callables)
+            if callable(value):
+                value = value()
+            role_hosts += value
+
+
+    cluster_hosts = []
+    if clusters:
+        # Abort if any clusters don't exist
+        bad_clusters = [x for x in clusters if x not in state.env.clusterdefs]
+        if bad_clusters:
+            abort("The following specified clusters do not exist:\n%s" % (
+                indent(bad_clusters)
+            ))
+
+        # Look up clusters, turn into flat list of hosts
+        for cluster in clusters:
+            value = state.env.clusterdefs[cluster]
+            # Handle "lazy" roles (callables)
+            if callable(value):
+                value = value()
+            cluster_hosts += value
+    
+    # Make clean sets
+    role_hosts = set(_clean_hosts(role_hosts))
+    cluster_hosts = set(_clean_hosts(cluster_hosts))
+    hosts = set(_clean_hosts(hosts))
+
+    # If only role or only cluster exist, don't intersect
+    if not operator.xor(bool(role_hosts), bool(cluster_hosts)):
+        found_hosts = role_hosts.intersection(cluster_hosts)
+    else:
+        found_hosts = role_hosts.union(cluster_hosts)
 
+    return list(hosts.union(found_hosts))
 
 def _clean_hosts(host_list):
     """
@@ -373,7 +409,7 @@ def _clean_hosts(host_list):
     return [host.strip() for host in host_list]
 
 
-def get_hosts(command, cli_hosts, cli_roles):
+def get_hosts(command, cli_hosts, cli_roles, cli_clusters):
     """
     Return the host list the given command should be using.
 
@@ -381,19 +417,22 @@ def get_hosts(command, cli_hosts, cli_roles):
     set.
     """
     # Command line per-command takes precedence over anything else.
-    if cli_hosts or cli_roles:
-        return _merge(cli_hosts, cli_roles)
+    if cli_roles or cli_hosts or cli_clusters:
+        return _merge(cli_hosts, cli_roles, cli_clusters)
+
     # Decorator-specific hosts/roles go next
     func_hosts = getattr(command, 'hosts', [])
     func_roles = getattr(command, 'roles', [])
-    if func_hosts or func_roles:
-        return _merge(func_hosts, func_roles)
+    func_clusters = getattr(command, 'clusters', [])
+
+    if func_roles or func_hosts or func_clusters:
+        return _merge(func_hosts, func_roles, func_clusters)
+
     # Finally, the env is checked (which might contain globally set lists from
     # the CLI or from module-level code). This will be the empty list if these
     # have not been set -- which is fine, this method should return an empty
     # list if no hosts have been set anywhere.
-    return _merge(state.env['hosts'], state.env['roles'])
-
+    return _merge(state.env['hosts'], state.env['roles'], state.env['clusters'])
 
 def update_output_levels(show, hide):
     """
@@ -431,7 +470,7 @@ def main():
             state.env[option.dest] = getattr(options, option.dest)
 
         # Handle --hosts, --roles (comma separated string => list)
-        for key in ['hosts', 'roles']:
+        for key in ['hosts', 'roles', 'clusters']:
             if key in state.env and isinstance(state.env[key], str):
                 state.env[key] = state.env[key].split(',')
 
@@ -525,14 +564,14 @@ def main():
             print("Commands to run: %s" % names)
 
         # At this point all commands must exist, so execute them in order.
-        for name, args, kwargs, cli_hosts, cli_roles in commands_to_run:
+        for name, args, kwargs, cli_hosts, cli_roles, cli_clusters in commands_to_run:
             # Get callable by itself
             command = commands[name]
             # Set current command name (used for some error messages)
             state.env.command = name
             # Set host list (also copy to env)
             state.env.all_hosts = hosts = get_hosts(
-                command, cli_hosts, cli_roles)
+                command, cli_hosts, cli_roles, cli_clusters)
             # If hosts found, execute the function on each host in turn
             for host in hosts:
                 # Preserve user
diff --git a/fabric/state.py b/fabric/state.py
index 343510c..2338d6d 100644
--- a/fabric/state.py
+++ b/fabric/state.py
@@ -144,6 +144,10 @@ env_options = [
         help="comma-separated list of roles to operate on"
     ),
 
+    make_option('-C', '--clusters',
+        default=[],
+        help='comma-separated list of clusters to operate on. Intersects with roles to only execute on role machines in said cluster.'),
+
     make_option('-i', 
         action='append',
         dest='key_filename',
@@ -221,6 +225,7 @@ env_options = [
 env = _AttributeDict({
     'again_prompt': 'Sorry, try again.',
     'all_hosts': [],
+    'clusterdefs': {},
     'combine_stderr': True,
     'command': None,
     'command_prefixes': [],
