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()