1# Copyright The Cloud Custodian Authors.
2# SPDX-License-Identifier: Apache-2.0
3
4from datetime import datetime, timedelta
5
6from c7n.utils import type_schema
7from c7n.filters import Filter, FilterValidationError
8from c7n.filters.offhours import Time
9
10DEFAULT_TAG = "custodian_status"
11
12
13class LabelActionFilter(Filter):
14 """Filter resources for label specified future action
15
16 Filters resources by a 'custodian_status' label which specifies a future
17 date for an action.
18
19 The filter parses the label values looking for an 'op@date'
20 string. The date is parsed and compared to do today's date, the
21 filter succeeds if today's date is gte to the target date.
22
23 The optional 'skew' parameter provides for incrementing today's
24 date a number of days into the future. An example use case might
25 be sending a final notice email a few days before terminating an
26 instance, or snapshotting a volume prior to deletion.
27
28 The optional 'skew_hours' parameter provides for incrementing the current
29 time a number of hours into the future.
30
31 Optionally, the 'tz' parameter can get used to specify the timezone
32 in which to interpret the clock (default value is 'utc')
33
34 :example:
35
36 .. code-block :: yaml
37
38 policies:
39 - name: vm-stop-marked
40 resource: gcp.instance
41 filters:
42 - type: marked-for-op
43 # The default label used is custodian_status
44 # but that is configurable
45 label: custodian_status
46 op: stop
47 # Another optional label is skew
48 tz: utc
49
50
51 """
52 schema = type_schema(
53 'marked-for-op',
54 label={'type': 'string'},
55 tz={'type': 'string'},
56 skew={'type': 'number', 'minimum': 0},
57 skew_hours={'type': 'number', 'minimum': 0},
58 op={'type': 'string'})
59
60 def validate(self):
61 op = self.data.get('op')
62 if self.manager and op not in self.manager.action_registry.keys():
63 raise FilterValidationError(
64 "Invalid marked-for-op op:%s in %s" % (op, self.manager.data))
65
66 tz = Time.get_tz(self.data.get('tz', 'utc'))
67 if not tz:
68 raise FilterValidationError(
69 "Invalid timezone specified '%s' in %s" % (
70 self.data.get('tz'), self.manager.data))
71 self.valid_actions = sorted(
72 self.manager.action_registry.keys(), key=lambda k: len(k), reverse=True)
73 return self
74
75 def process(self, resources, event=None):
76 self.label = self.data.get('label', DEFAULT_TAG)
77 self.op = self.data.get('op', 'stop')
78 self.skew = self.data.get('skew', 0)
79 self.skew_hours = self.data.get('skew_hours', 0)
80 self.tz = Time.get_tz(self.data.get('tz', 'utc'))
81 return super(LabelActionFilter, self).process(resources, event)
82
83 def parse(self, v: str):
84 remainder, action_date = v.rsplit('-', 1)
85 found = False
86 msg = ""
87 for a in self.valid_actions:
88 if remainder.endswith(a):
89 found = a
90 msg = remainder[:-len(a) - 1]
91 break
92 return msg, found, action_date
93
94 def __call__(self, i):
95 v = i.get('labels', {}).get(self.label, None)
96
97 if v is None:
98 return False
99 if '-' not in v or '_' not in v:
100 return False
101
102 _, action, action_date_str = self.parse(v)
103
104 if action != self.op or not action:
105 return False
106
107 try:
108 action_date = datetime.strptime(action_date_str, '%Y_%m_%d__%H_%M')
109 except Exception:
110 self.log.error("could not parse label:%s value:%s on %s" % (
111 self.label, v, i['name']))
112 return False
113
114 # current_date must match timezones with the parsed date string
115 if action_date.tzinfo:
116 action_date = action_date.astimezone(self.tz)
117 current_date = datetime.now(tz=self.tz)
118 else:
119 current_date = datetime.now()
120
121 return current_date >= (action_date - timedelta(days=self.skew, hours=self.skew_hours))