Repository: cloud-custodian/cloud-custodian

Issue:
offhours with tz by itself goes to default
The merge of custom offhours schedules disrupted the ability of just tz specification alone in tag value.



Discussion:
Mainfests as a unit test failure at certain times:

```
(cloud-custodian) kit@lub1604x64-dev:~/wrk/src/github.com/ewbankkit/cloud-custodian$ make test
AWS_ACCESS_KEY_ID=foo AWS_SECRET_ACCESS_KEY=bar AWS_DEFAULT_REGION=us-east-1 ./bin/nosetests  --processes=-1 tests
.............................................................................................................................................................................................F..............................................................................................................................................................
======================================================================
FAIL: test_cant_parse_tz (tests.test_offhours.OffHoursFilterTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/kit/wrk/src/github.com/ewbankkit/cloud-custodian/tests/test_offhours.py", line 320, in test_cant_parse_tz
    self.assertEqual(OffHour({})(i), False)
AssertionError: True != False

----------------------------------------------------------------------
Ran 348 tests in 8.309s

FAILED (failures=1)
Makefile:22: recipe for target 'test' failed
make: *** [test] Error 1
```


Resolving patch:
diff --git a/c7n/filters/offhours.py b/c7n/filters/offhours.py
--- a/c7n/filters/offhours.py
+++ b/c7n/filters/offhours.py
@@ -266,8 +266,15 @@ def process_resource_schedule(self, i, value):
         rid = i[self.id_key]
         if self.parser.has_resource_schedule(value):
             schedule = self.parser.parse(value)
-        else:
+        elif self.parser.keys_are_valid(value):
+            # respect timezone from tag
+            raw_data = self.parser.raw_data(value)
+            if 'tz' in raw_data:
+                self.default_tz = raw_data['tz']
+                self.default_schedule = self.get_default_schedule()
             schedule = self.default_schedule
+        else:
+            schedule = None
 
         if schedule is None:
             log.warning(
@@ -418,6 +425,29 @@ def __init__(self, default_schedule):
         self.default_schedule = default_schedule
         self.cache = {}
 
+    @staticmethod
+    def raw_data(tag_value):
+        """convert the tag to a dictionary, taking values as is"""
+        data = {}
+
+        # parse components
+        pieces = tag_value.split(';')
+        for piece in pieces:
+            kv = piece.split('=')
+            # components must by key=value
+            if not len(kv) == 2:
+                continue
+            key, value = kv
+            data[key] = value
+        return data
+
+    def keys_are_valid(self, tag_value):
+        """test that provided tag keys are valid"""
+        for key in ScheduleParser.raw_data(tag_value):
+            if key not in ('on', 'off', 'tz'):
+                return False
+        return True
+
     def parse(self, tag_value):
         # check the cache
         if tag_value in self.cache:
@@ -425,6 +455,8 @@ def parse(self, tag_value):
 
         schedule = {}
 
+        if not self.keys_are_valid(tag_value):
+            return None
         # parse schedule components
         pieces = tag_value.split(';')
         for piece in pieces:
@@ -433,8 +465,6 @@ def parse(self, tag_value):
             if not len(kv) == 2:
                 return None
             key, value = kv
-            if key not in ('on', 'off', 'tz'):
-                return None
             if key != 'tz':
                 value = self.parse_resource_schedule(value)
             if value is None:
@@ -451,9 +481,8 @@ def parse(self, tag_value):
 
     @staticmethod
     def has_resource_schedule(tag_value):
-        if 'off=' in tag_value and 'on=' in tag_value:
-            return True
-        return False
+        raw_data = ScheduleParser.raw_data(tag_value)
+        return 'on' in raw_data and 'off' in raw_data
 
     def parse_resource_schedule(self, lexeme):
         parsed = []


Test patch:
diff --git a/tests/test_offhours.py b/tests/test_offhours.py
--- a/tests/test_offhours.py
+++ b/tests/test_offhours.py
@@ -288,14 +288,14 @@ def test_offhours_real_world_values(self):
         t = datetime.datetime.now(zoneinfo.gettz('America/New_York'))
         t = t.replace(year=2015, month=12, day=1, hour=19, minute=5)
         with mock_datetime_now(t, datetime):
-            for i in [
+            results = [OffHour({})(i) for i in [
                     instance(Tags=[
                         {'Key': 'maid_offhours', 'Value': ''}]),
                     instance(Tags=[
                         {'Key': 'maid_offhours', 'Value': '"Offhours tz=ET"'}]),
                     instance(Tags=[
-                        {'Key': 'maid_offhours', 'Value': 'Offhours tz=PT'}])]:
-                self.assertEqual(OffHour({})(i), True)
+                        {'Key': 'maid_offhours', 'Value': 'Offhours tz=PT'}])]]
+            self.assertEqual(results, [True, False, False])
 
     def test_offhours(self):
         t = datetime.datetime(year=2015, month=12, day=1, hour=19, minute=5,
@@ -384,6 +384,14 @@ def test_custom_bad_hours(self):
                                 'Value': 'off=(m-f,90);on=(m-f,7);tz=et'}])
             self.assertEqual(OffHour({})(i), False)
 
+    def test_tz_only(self):
+        t = datetime.datetime.now(zoneinfo.gettz('America/New_York'))
+        t = t.replace(year=2016, month=5, day=26, hour=7, minute=00)
+        with mock_datetime_now(t, datetime):
+            i = instance(Tags=[{'Key': 'maid_offhours',
+                                'Value': 'tz=est'}])
+            self.assertEqual(OnHour({})(i), True)
+
 
 class ScheduleParserTest(BaseTest):
     # table style test