Repository: cloud-custodian/cloud-custodian

Issue:
AWS resources with tag restrictions can't fully use OffHours
Hi, I'm setting up a shutdown schedule for RDS and know that RDS tags cannot have the characters needed in them. As a workaround, I am creating a custom policy for RDS with specific values set. The trouble I am facing is the policy I want to implement specifies days to shutdown "off=[(S,1)];on=[(M,6)];tz=it". 

Can the functionality of the Tag be added into the custodian policy.



Discussion:
sounds like being able to specify a  default tag value on offhour/onhour filters would suffice?
@kapilt  yes, being able to specify the default tag value would work, as long as you can still opt-in with the tag. 
Something like
```
 - name: stop-after-hours-rds
    resource: rds
    filters:
      - type: offhour
        opt-out: false
        tag: ShutdownSchedule
        tag-default-schedule: "off=[(S,1)];on=[(M,6)];tz=it".
    actions:
      - stop
```
Did this work? Dryrun produced this error.

Exception: Failed to validate on policy rds-ind-onhour-start
 Additional properties are not allowed ('tag-default-schedule' was unexpected)

Failed validating u'additionalProperties' in schema[11]:
    {u'additionalProperties': False,
     u'properties': {u'default_tz': {u'type': u'string'},
                     'onhour': {u'maximum': 23,
                                u'minimum': 0,
                                u'type': u'integer'},
                     u'opt-out': {u'type': u'boolean'},
                     u'skip-days': {u'items': {u'pattern': u'^[0-9]{4}-[0-9]{2}-[0-9]{2}',
                                               u'type': u'string'},
                                    u'type': u'array'},
                     u'skip-days-from': {u'additionalProperties': u'False',
                                         u'properties': {u'expr': {u'oneOf': [{u'type': u'integer'},
                                                                              {u'type': u'string'}]},
                                                         u'format': {u'enum': [u'csv',
                                                                               u'json',
                                                                               u'txt',
                                                                               u'csv2dict']},
                                                         u'url': {u'type': u'string'}},
                                         u'required': [u'url'],
                                         u'type': u'object'},
                     u'tag': {u'type': u'string'},
                     u'type': {u'enum': [u'onhour']},
                     u'weekends': {u'type': u'boolean'},
                     u'weekends-only': {u'type': u'boolean'}},
     u'required': [u'onhour', u'default_tz', u'type'],
     u'type': u'object'}

On instance:
    {'opt-out': False,
     'tag': 'StartUpSchedule',
     'tag-default-schedule': 'on=[(M-S,16)];tz=est',
     'type': 'onhour'}

@pricy this is talking about adding in a feature/code change, not what's there currently
Makes sense @kapilt . Unfortunately, I still can't get this workaround to work either.

https://github.com/cloud-custodian/cloud-custodian/issues/1310

Resolving patch:
diff --git a/c7n/filters/offhours.py b/c7n/filters/offhours.py
--- a/c7n/filters/offhours.py
+++ b/c7n/filters/offhours.py
@@ -29,6 +29,9 @@
  - **weekends-only**: default false, whether to turn the resource off only on
    the weekend
  - **default_tz**: which timezone to utilize when evaluating time **(REQUIRED)**
+ - **fallback-schedule**: If a resource doesn't support tagging or doesn't provide
+   a tag you can supply a default schedule that will be used. When the tag is provided
+   this will be ignored. See :ref:`ScheduleParser Time Specifications <scheduleparser-time-spec>`.
  - **tag**: which resource tag name to use for per-resource configuration
    (schedule and timezone overrides and opt-in/opt-out); default is
    ``maid_offhours``.
@@ -61,6 +64,7 @@
            onhour: 8
            offhour: 20
 
+
 Tag Based Configuration
 =======================
 
@@ -94,6 +98,9 @@
     supported by :py:class:`c7n.filters.offhours.ScheduleParser` as described
     in the next section.
 
+
+.. _scheduleparser-time-spec:
+
 ScheduleParser Time Specifications
 ----------------------------------
 
@@ -255,6 +262,7 @@ class Time(Filter):
         'properties': {
             'tag': {'type': 'string'},
             'default_tz': {'type': 'string'},
+            'fallback_schedule': {'type': 'string'},
             'weekends': {'type': 'boolean'},
             'weekends-only': {'type': 'boolean'},
             'opt-out': {'type': 'boolean'},
@@ -315,6 +323,7 @@ def __init__(self, data, manager=None):
         self.weekends_only = self.data.get('weekends-only', False)
         self.opt_out = self.data.get('opt-out', False)
         self.tag_key = self.data.get('tag', self.DEFAULT_TAG).lower()
+        self.fallback_schedule = self.data.get('fallback-schedule', None)
         self.default_schedule = self.get_default_schedule()
         self.parser = ScheduleParser(self.default_schedule)
 
@@ -438,12 +447,12 @@ def match(self, now, schedule):
     def get_tag_value(self, i):
         """Get the resource's tag value specifying its schedule."""
         # Look for the tag, Normalize tag key and tag value
-        found = False
+        found = self.fallback_schedule
         for t in i.get('Tags', ()):
             if t['Key'].lower() == self.tag_key:
                 found = t['Value']
                 break
-        if found is False:
+        if found in (False, None):
             return False
         # enforce utf8, or do translate tables via unicode ord mapping
         value = found.lower().encode('utf8').decode('utf8')


Test patch:
diff --git a/tests/test_offhours.py b/tests/test_offhours.py
--- a/tests/test_offhours.py
+++ b/tests/test_offhours.py
@@ -320,6 +320,17 @@ def test_offhours_get_value(self):
         self.assertTrue(off.parser.keys_are_valid(off.get_tag_value(i)))
         self.assertEqual(off.parser.raw_data(off.get_tag_value(i)), {"tz": "pt"})
 
+    def test_offhours_get_value_fallback(self):
+        sched = "off=[(S,1)];on=[(M,6)];tz=pst"
+        off = OffHour({"default_tz": "ct", "fallback-schedule": sched})
+        i = instance(Tags=[])
+        self.assertEqual(off.get_tag_value(i), sched.lower())
+        self.assertTrue(off.parser.has_resource_schedule(off.get_tag_value(i), "off"))
+        self.assertTrue(off.parser.has_resource_schedule(off.get_tag_value(i), "on"))
+        self.assertTrue(off.parser.keys_are_valid(off.get_tag_value(i)))
+        self.assertEqual(off.parser.raw_data(off.get_tag_value(i)),
+                        {'off': '[(s,1)]', 'on': '[(m,6)]', 'tz': 'pst'})
+
     def test_offhours(self):
         t = datetime.datetime(
             year=2015,