1#!/usr/bin/env python3
2
3"""Read jobcontrol a.k.a. scheduler files."""
4
5
6
7import logging
8import fnmatch
9import importlib
10from datetime import timedelta
11
12from chart.common.xml import load_xml
13from chart.common.xml import parsechildstr
14from chart.common.xml import parsechildstrs
15from chart.common.xml import parsechildint
16from chart.common.xml import parsechildbool
17from chart.common.xml import parsechildtimedelta
18from chart import settings
19from chart.common.exceptions import ConfigError
20from chart.backend.activity import Activity
21from chart.browse.make_url import make_url
22from chart.project import SID
23from chart.backend.activity import CallingConvention
24from chart.common.path import Path
25
26NULL_TIMEDELTA = timedelta()
27
28ELEM_RESPONSE = 'response'
29ELEM_ACTIVITY = 'activity'
30ELEM_ENABLED = 'enabled'
31ELEM_DIRECTORY = 'directory'
32ELEM_PATTERN = 'pattern'
33ELEM_PDU_DELAY_QUEUE = 'pdu-delay-queue'
34ELEM_TIMEOUT = 'timeout'
35ELEM_DAY = 'day'
36ELEM_HOUR = 'hour'
37ELEM_OFFSET = 'offset'
38ELEM_PARSER = 'parser'
39ELEM_MINUTE = 'minute'
40ELEM_DEPENDENCY = 'dependency'
41
42logger = logging.getLogger()
43
44
45class JobCreator:
46 """Interpret a job creator XML file."""
47
48 def __init__(self, name):
49 name = name.upper()
50 self.name = name
51 self.elem = load_xml(settings.SCHEDULE_DIR.child(name + '.xml'))
52
53 @property
54 def description(self):
55 """Description of this file."""
56 return parsechildstr(self.elem, 'description', None)
57
58 @property
59 def trigger(self):
60 """Return a dictionary describing this job creator."""
61 trigger_elem = self.elem.find('trigger')
62 if trigger_elem is None:
63 raise ConfigError('Job creator definition file has no trigger')
64
65 if len(trigger_elem.getchildren()) != 1:
66 raise ConfigError('Job creator trigger element must have 1 child element')
67
68 trigger_child = trigger_elem.getchildren()[0]
69 trigger_type = trigger_child.tag
70
71 if trigger_type == 'daily':
72 return {'type': 'daily',
73 'hour': parsechildint(trigger_child, 'hour'),
74 'duration': parsechildtimedelta(trigger_child, 'duration', NULL_TIMEDELTA),
75 'offset': parsechildtimedelta(trigger_child, 'offset', NULL_TIMEDELTA)}
76
77 elif trigger_type == 'hourly':
78 return {'type': 'hourly',
79 'minute': parsechildint(trigger_child, ELEM_MINUTE, 0)}
80
81 elif trigger_type == 'weekly':
82 days = {'monday': 0,
83 'tuesday': 1,
84 'wednesday': 2,
85 'thursday': 3,
86 'friday': 4,
87 'saturday': 5,
88 'sunday': 6}
89 day = parsechildstr(trigger_child, ELEM_DAY, '0')
90 if day.isdigit():
91 day = int(day)
92
93 else:
94 day = days[day.lower()]
95
96 return {'type': 'weekly',
97 'day': day,
98 'hour': parsechildint(trigger_child, ELEM_HOUR, 0),
99 'offset': parsechildtimedelta(trigger_child, ELEM_OFFSET, None)}
100
101 elif trigger_type == 'monthly':
102 return {'type': 'monthly',
103 'day': parsechildint(trigger_child, ELEM_DAY, 1),
104 'hour': parsechildint(trigger_child, ELEM_HOUR, 0),
105 'offset': parsechildtimedelta(trigger_child, ELEM_OFFSET, None)}
106
107 elif trigger_type == 'directory-monitor':
108 # don't use '-' chars here as it's difficult to read them from Django templates
109 monitor = Path(parsechildstr(trigger_child,
110 ELEM_DIRECTORY,
111 expand_settings=True,
112 expand_user=True))
113 parser_name = parsechildstr(trigger_child, ELEM_PARSER)
114
115 if parser_name is None or parser_name == '':
116 parser_fn = None
117
118 else:
119 module_name, _, fn_name = parser_name.rpartition('.')
120 module_obj = importlib.import_module(module_name, __package__)
121 parser_fn = getattr(module_obj, fn_name)
122
123 return {'type': 'directory-monitor',
124 'directory': monitor,
125 'pattern': parsechildstr(trigger_child, ELEM_PATTERN),
126 'pdu_delay_queue': parsechildint(trigger_child, ELEM_PDU_DELAY_QUEUE, None),
127 'timeout': parsechildtimedelta(trigger_child, ELEM_TIMEOUT, None),
128 'parser': parser_fn}
129
130 elif trigger_type == 'now':
131 return {'type': 'now',
132 'dependency': parsechildstr(trigger_child, ELEM_DEPENDENCY)}
133
134 else:
135 raise ConfigError('Unknown trigger type: {type}'.format(type=trigger_type))
136
137 def responses(self, # (dangerous default) pylint: disable=W0102
138 allowed_activities=None,
139 excluded_activities=[],
140 exclude_directory_monitoring=False):
141 """Yield a list of dictionaries describing the responses to this activity."""
142 # from chart.common.xml import xml_to_str
143 # logger.debug('seek responses in ' + self.name + ' ' + xml_to_str(self.elem))
144 # logger.debug('job creator allowed_activities ' + str(allowed_activities))
145 for r in self.elem.findall(ELEM_RESPONSE):
146 sid = SID.from_xml(r, wildcard=True)
147 # logger.debug('response sid ' + str(sid))
148 for a in parsechildstrs(r, ELEM_ACTIVITY):
149 if excluded_activities is not None and a in excluded_activities:
150 continue
151
152 activity = Activity(a)
153 if exclude_directory_monitoring and\
154 activity.convention is CallingConvention.FILENAME:
155 continue
156
157 if allowed_activities is not None:
158 allowed = False
159 for test in allowed_activities:
160 if fnmatch.fnmatch(a, test.upper()):
161 allowed = True
162 # logger.debug('match on ' + str(a))
163
164
165 # logger.debug('allowed is ' + str(allowed))
166 if not allowed:
167 # logger.debug('not allowed')
168 continue
169
170 # else:
171 # logger.debug('allowed')
172
173 yield {'sids': [] if sid is None else sid.expand(),
174 'activity': activity}
175
176 def grouped_responses(self):
177 """Return a list of responses split into groups as in the original XML file."""
178 pass
179
180 @property
181 def enabled(self):
182 """False if <enabled>false</enabled> is present.
183 Deactivates the schedule entry completely."""
184 return parsechildbool(self.elem, ELEM_ENABLED, True)
185
186 def is_time_based(self):
187 """Test if our trigger is time based."""
188 return self.trigger['type'] in ('daily', 'hourly', 'weekly', 'monthly')
189
190 def is_file_based(self):
191 """Test if our trigger file based."""
192 return self.trigger['type'] == 'directory-monitor'
193
194 def browse_source_url(self):
195 """Return a URL which will display our source file."""
196 return make_url(settings.SCHEDULE_DIR.joinpath(self.name + '.xml'))
197
198 @staticmethod
199 def all():
200 """Yield all job creators except disabled ones."""
201 for filename in sorted(settings.SCHEDULE_DIR.glob('*.xml')):
202 yield JobCreator(filename.stem)
203
204 @staticmethod
205 def all_time_based():
206 """Yield all time based job creators."""
207 # print('all time based')
208 for filename in sorted(settings.SCHEDULE_DIR.iterdir()):
209 if filename.suffix == '.xml':
210 candidate = JobCreator(filename.stem)
211 # print('trying ' + candidate.name)
212 if candidate.is_time_based():
213 yield candidate
214
215 @staticmethod
216 def all_file_based():
217 """Yield all file based job creators."""
218 for filename in sorted(settings.SCHEDULE_DIR.iterdir()):
219 if filename.suffix == '.xml':
220 candidate = JobCreator(filename.stem)
221 if candidate.is_file_based():
222 yield candidate
223
224
225def list_all():
226 """List all job creator files."""
227 print('All job creator files:')
228 for c in JobCreator.all():
229 limit = 90 - len(c.name)
230 description = c.description
231 if len(description) > limit:
232 description = description[:limit] + '...'
233
234 print(' {name}: {desc}'.format(name=c.name, desc=description))
235
236
237def info(name):
238 """Show info about a single job creator."""
239 c = JobCreator(name)
240 from chart.common.prettyprint import Table
241 t = Table()
242 t.append(('Name', c.name))
243 t.append(('Description', c.description))
244 t.append(('Trigger', ', '.join('{k}:{v}'.format(k=k, v=v) for k, v in c.trigger.items())))
245 for a in c.responses():
246 t.append(('Activity', ', '.join('{k}:{v}'.format(k=k, v=v) for k, v in a.items())))
247
248 t.write()
249
250
251def main():
252 """Command line entry point."""
253 from chart.common.args import ArgumentParser
254 parser = ArgumentParser()
255 parser.add_argument('--list', '-l',
256 action='store_true',
257 help='List job creator files')
258 parser.add_argument('--info', '-i',
259 help='Show info on a single job creator')
260 args = parser.parse_args()
261 if args.list:
262 list_all()
263 parser.exit()
264
265 elif args.info:
266 info(args.info)
267 parser.exit()
268
269 else:
270 parser.error('No actions specified')
271
272if __name__ == '__main__':
273 main()