Repository: cloud-custodian/cloud-custodian

Issue:
govcloud compatibility
using `--region us-gov-west-1` with any resource type outputs:

```
c7n.policies:WARNING policy:test-policy resources:ec2 not available in region:us-gov-west-1
Empty policy file(s).  Nothing to do.
```

The same policy runs successfully in a non-GovCloud region, using non-GC AWS keys.



Discussion:

Resolving patch:
diff --git a/c7n/commands.py b/c7n/commands.py
--- a/c7n/commands.py
+++ b/c7n/commands.py
@@ -229,13 +229,14 @@ def report(options, policies):
 
     resources = set([p.resource_type for p in policies])
     if len(resources) > 1:
-        log.error('Error: Report subcommand can accept multiple policies, but they must '
-                  'all be for the same resource.')
+        log.error('Error: Report subcommand can accept multiple policies, '
+                  'but they must all be for the same resource.')
         sys.exit(1)
 
     delta = timedelta(days=options.days)
     begin_date = datetime.now() - delta
-    do_report(policies, begin_date, options, sys.stdout, raw_output_fh=options.raw)
+    do_report(
+        policies, begin_date, options, sys.stdout, raw_output_fh=options.raw)
 
 
 @policy_command
diff --git a/c7n/policy.py b/c7n/policy.py
--- a/c7n/policy.py
+++ b/c7n/policy.py
@@ -61,7 +61,9 @@ def load(options, path, format='yaml', validate=True, vars=None):
         from c7n.schema import validate
         errors = validate(data)
         if errors:
-            raise Exception("Failed to validate on policy %s \n %s" % (errors[1], errors[0]))
+            raise Exception(
+                "Failed to validate on policy %s \n %s" % (
+                    errors[1], errors[0]))
 
     collection = PolicyCollection.from_data(data, options)
     if validate:
@@ -70,6 +72,35 @@ def load(options, path, format='yaml', validate=True, vars=None):
     return collection
 
 
+def get_service_region_map(regions, resource_types):
+    # we're not interacting with the apis just using the sdk meta information.
+    session = boto3.Session(
+        region_name='us-east-1',
+        aws_access_key_id='never',
+        aws_secret_access_key='found')
+
+    resource_service_map = {r: resources.get(r).resource_type.service
+                            for r in resource_types if r != 'account'}
+    # support for govcloud and china, we only utilize these regions if they
+    # are explicitly passed in on the cli.
+    partition_regions = {}
+    for p in ('aws-cn', 'aws-us-gov'):
+        for r in session.get_available_regions('s3', partition_name=p):
+            partition_regions[r] = p
+
+    partitions = ['aws']
+    for r in regions:
+        if r in partition_regions:
+            partitions.append(partition_regions[r])
+
+    service_region_map = {}
+    for s in set(itertools.chain(resource_service_map.values())):
+        for partition in partitions:
+            service_region_map.setdefault(s, []).extend(
+                session.get_available_regions(s, partition_name=partition))
+    return service_region_map, resource_service_map
+
+
 class PolicyCollection(object):
 
     log = logging.getLogger('c7n.policies')
@@ -80,7 +111,8 @@ def __init__(self, policies, options):
 
     @classmethod
     def from_data(cls, data, options):
-        policies = [Policy(p, options, session_factory=cls.test_session_factory())
+        policies = [Policy(p, options,
+                           session_factory=cls.test_session_factory())
                     for p in data.get('policies', ())]
         return PolicyCollection(policies, options)
 
@@ -90,28 +122,25 @@ def __add__(self, other):
     def expand_regions(self, regions):
         """Return a set of policies targetted to the given regions.
 
-        Supports symbolic regions like 'all'. This will automatically filter out policies
-        if their being targetted to a region that does not support the service. Global
-        services will target a single region (us-east-1 if only all specified, else
-        first region in the list).
-        """
-        # we're not interacting with the apis just using the sdk meta information.
-        session = boto3.Session(
-            region_name='us-east-1',
-            aws_access_key_id='never',
-            aws_secret_access_key='found')
-        resource_service_map = {r: resources.get(r).resource_type.service
-                                for r in self.resource_types if r != 'account'}
-        service_region_map = {
-            s: session.get_available_regions(s) for s in set(
-                itertools.chain(resource_service_map.values()))}
+        Supports symbolic regions like 'all'. This will automatically
+        filter out policies if their being targetted to a region that
+        does not support the service. Global services will target a
+        single region (us-east-1 if only all specified, else first
+        region in the list).
 
+        Note for region partitions (govcloud and china) an explicit
+        region from the partition must be passed in.
+        """
         policies = []
+        service_region_map, resource_service_map = get_service_region_map(
+            regions, self.resource_types)
+
         for p in self.policies:
             available_regions = service_region_map.get(
                 resource_service_map.get(p.resource_type), ())
 
-            # its a global service/endpoint, use user provided region or us-east-1.
+            # its a global service/endpoint, use user provided region
+            # or us-east-1.
             if not available_regions and regions:
                 candidates = [r for r in regions if r != 'all']
                 candidate = candidates and candidates[0] or 'us-east-1'
@@ -123,7 +152,8 @@ def expand_regions(self, regions):
 
             for region in svc_regions:
                 if available_regions and region not in available_regions:
-                    level = 'all' in self.options.regions and logging.DEBUG or logging.WARNING
+                    level = ('all' in self.options.regions and
+                             logging.DEBUG or logging.WARNING)
                     self.log.log(
                         level, "policy:%s resources:%s not available in region:%s",
                         p.name, p.resource_type, region)
@@ -137,7 +167,8 @@ def expand_regions(self, regions):
                         self.options.output_dir.rstrip('/') + '/%s' % region)
 
                 policies.append(
-                    Policy(p.data, options_copy, session_factory=self.test_session_factory()))
+                    Policy(p.data, options_copy,
+                           session_factory=self.test_session_factory()))
         return PolicyCollection(policies, self.options)
 
     def filter(self, policy_name=None, resource_type=None):
@@ -283,7 +314,8 @@ def run(self, *args, **kw):
             elif (self.policy.max_resources is not None and
                   len(resources) > self.policy.max_resources):
                 msg = "policy %s matched %d resources max resources %s" % (
-                    self.policy.name, len(resources), self.policy.max_resources)
+                    self.policy.name, len(resources),
+                    self.policy.max_resources)
                 self.policy.log.warning(msg)
                 raise RuntimeError(msg)
 
diff --git a/c7n/resources/s3.py b/c7n/resources/s3.py
--- a/c7n/resources/s3.py
+++ b/c7n/resources/s3.py
@@ -690,6 +690,10 @@ def process(self, resources):
 class AttachLambdaEncrypt(BucketActionBase):
     """Action attaches lambda encryption policy to S3 bucket
 
+    supports attachment via lambda bucket notification or sns notification
+    to invoke lambda. a special topic value of `default` will utilize
+    an extant notification or create one matching the bucket name.
+
     :example:
 
         .. code-block: yaml
@@ -703,7 +707,9 @@ class AttachLambdaEncrypt(BucketActionBase):
                   - attach-encrypt
     """
     schema = type_schema(
-        'attach-encrypt', role={'type': 'string'}, topic={'type': 'string'})
+        'attach-encrypt',
+        role={'type': 'string'},
+        topic={'type': 'string'})
 
     permissions = (
         "s3:PutBucketNotification", "s3:GetBucketNotification",


Test patch:
diff --git a/tests/test_policy.py b/tests/test_policy.py
--- a/tests/test_policy.py
+++ b/tests/test_policy.py
@@ -140,6 +140,19 @@ def test_resource_permissions(self):
 
 class TestPolicyCollection(BaseTest):
 
+    def test_expand_partitions(self):
+        cfg = Config.empty(
+            regions=['us-gov-west-1', 'cn-north-1', 'us-west-2'])
+        original = policy.PolicyCollection.from_data(
+            {'policies': [
+                {'name': 'foo',
+                 'resource': 'ec2'}]},
+            cfg)
+        collection = original.expand_regions(cfg.regions)
+        self.assertEqual(
+            sorted([p.options.region for p in collection]),
+            ['cn-north-1', 'us-gov-west-1', 'us-west-2'])
+
     def test_policy_account_expand(self):
         original = policy.PolicyCollection.from_data(
             {'policies': [
@@ -149,7 +162,7 @@ def test_policy_account_expand(self):
 
         collection = original.expand_regions(['all'])
         self.assertEqual(len(collection), 1)
-        
+
     def test_policy_region_expand_global(self):
         original = policy.PolicyCollection.from_data(
             {'policies': [