root/branch/umitweb-ng/umitCore/Scheduler.py @ 4770

Revision 4770, 22.9 kB (checked in by rcarvalho, 4 years ago)

Merged revisions 4208-4214 via svnmerge from
http://svn.umitproject.org/svnroot/umit/trunk

........

r4208 | gpolo | 2009-02-27 15:54:02 -0300 (Sex, 27 Fev 2009) | 1 line


Fixed ticket #213. HOME under Windows is now set to the user's local appdata.

........

r4209 | gpolo | 2009-02-27 15:55:22 -0300 (Sex, 27 Fev 2009) | 1 line


Adjusted to work with win32com.shell imports.

........

r4210 | gpolo | 2009-02-27 15:58:49 -0300 (Sex, 27 Fev 2009) | 1 line


Fixed ticket #199: "Umit may hang while staring".

........

r4211 | gpolo | 2009-02-27 20:09:20 -0300 (Sex, 27 Fev 2009) | 1 line


Generate a service class string from the service path if given. This helps installing the service inside Umit in case umit_scheduler.py moves to somewhere else (like to bin/).

........

r4212 | gpolo | 2009-02-27 20:21:42 -0300 (Sex, 27 Fev 2009) | 1 line


Setting service path

........

r4213 | gpolo | 2009-02-28 18:17:47 -0300 (Sáb, 28 Fev 2009) | 1 line


Relayout

........

r4214 | gpolo | 2009-02-28 18:45:29 -0300 (Sáb, 28 Fev 2009) | 3 lines


Moved umitDB/sql to share/umit/sql, updated installers. This also fixes
ticket #220.

........

Line 
1# Copyright (C) 2007 Adriano Monteiro Marques
2#
3# Original author: Adriano Monteiro Marques
4# Now maintained and updated by: Guilherme Polo <ggpolo@gmail.com>
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 2 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
20"""
21Job scheduler
22"""
23
24import os
25import sys
26import time
27import signal
28import warnings
29import subprocess
30from datetime import datetime
31from ConfigParser import ConfigParser
32
33from umitCore.BGProcess import BGRunner
34from umitCore.Paths import Path
35from umitCore.UmitLogging import file_log
36from umitCore.I18N import _
37from umitCore.CronParser import CronParser
38from umitCore.NmapCommand import NmapCommand
39from umitCore.Email import Email
40from umitDB.XMLStore import XMLStore
41
42NT = os.name == 'nt'
43if NT:
44    import win32api
45    import win32event
46    import servicemanager
47else:
48    servicemanager = lambda: None
49    servicemanager.RunningAsService = lambda: False
50
51
52def format_asctime(date):
53    """Format a datetime.date almost like time.asctime, except this one
54    doesn't use ':' for the time so it can be used as a filename under
55    Windows."""
56    return datetime.strftime(date, '%a %b %d %Hh%Mm%Ss %Y')
57
58
59class Scheduler(object):
60    """
61    Schedules schemas to run.
62    """
63
64    def __init__(self, schemas_file, profiles_file):
65        self.schemas_file = schemas_file
66        self.profiles_file = profiles_file
67        self.schemas_stat = None
68        self.profiles_stat = None
69
70    def parse_schemas(self):
71        """
72        Parse schemas set in file.
73        """
74        self.schemas = [ ]
75
76        self.schema_parser = ConfigParser()
77        self.schema_parser.read(self.schemas_file)
78
79        for sec in self.schema_parser.sections():
80            self.schemas.append(SchedSchema(self.schema_parser, sec))
81
82   
83    def check_for_changes(self):
84        """
85        If schemas file or profiles file changed since last check,
86        reparse schemas.
87        """
88        try:
89            new_sstat = os.stat(self.schemas_file)
90            new_pstat = os.stat(self.profiles_file)
91        except OSError: # file was being saved (probably) when this got called
92            time.sleep(.1)
93            new_sstat = os.stat(self.schemas_file)
94            new_pstat = os.stat(self.profiles_file)
95
96        new_sstat = new_sstat.st_mtime
97        new_pstat = new_pstat.st_mtime
98
99        if new_sstat != self.schemas_stat or new_pstat != self.profiles_stat:
100            log.debug("Schemas file changed since last check")
101            self.schemas_stat = new_sstat
102            self.profiles_stat = new_pstat
103            self.parse_schemas()
104
105
106    def _get_schemas_stat(self):
107        """
108        Get latest os.stat for schemas file.
109        """
110        return self.__schemas_stat
111
112
113    def _set_schemas_stat(self, stat):
114        """
115        Set current os.stat for schemas file.
116        """
117        self.__schemas_stat = stat
118
119
120    def _get_profiles_stat(self):
121        """
122        Get latests os.stat for profiles file.
123        """
124        return self.__profiles_stat
125
126
127    def _set_profiles_stat(self, stat):
128        """
129        Set current os.stat for profiles file.
130        """
131        self.__profiles_stat = stat
132
133
134    def _get_schemas_file(self):
135        """
136        Get schemas file.
137        """
138        return self.__schemasf
139
140
141    def _set_schemas_file(self, sfile):
142        """
143        Set schemas file.
144        """
145        try:
146            os.stat(sfile)
147        except OSError, e:
148            log.error("%s" % e)
149            sys.exit(0)
150
151        self.__schemasf = sfile
152
153
154    def _get_profiles_file(self):
155        """
156        Get profiles file.
157        """
158        return self.__profilesf
159
160
161    def _set_profiles_file(self, pfile):
162        """
163        Set profiles file.
164        """
165        try:
166            os.stat(pfile)
167        except OSError, e:
168            log.error("%s" % e)
169            sys.exit(0)
170
171        self.__profilesf = pfile
172
173
174    # Properties
175    schemas_file = property(_get_schemas_file, _set_schemas_file)
176    profiles_file = property(_get_profiles_file, _set_profiles_file)
177    schemas_last_stat = property(_get_schemas_stat, _set_schemas_stat)
178    profiles_last_stat = property(_get_profiles_stat, _set_profiles_stat)
179
180
181class SchedSchema(object):
182    """
183    Parse scheduled schemas.
184    """
185
186    def __init__(self, schema_parser, schema_name):
187        self.profile_opts = {
188            # Task execution time
189            'hour':self._set_hour,
190            'minute':self._set_minute,
191            'month':self._set_month,
192            'day':self._set_day,
193            'weekday':self._set_weekday,
194        }
195
196        self.options = { 
197            # Task scheduling profile
198            'profile':self._set_profile,
199            # Task command
200            'command':self._set_command,
201            # Task options
202            'enabled':self._set_enabled,
203            'addtoinv':self._set_addtoinv,
204            'saveto':self._set_saveto,
205            'mailto':self._set_mailto,
206            'smtp':self._set_smtp
207        }
208
209        self.__schema_name = schema_name
210        self.schema_parser = schema_parser
211        self.last_check = None
212        self.cron_parser = CronParser()
213        self.profiles = Path.sched_profiles
214        self.setup_schema(self.schema_name)
215
216
217    def job_to_run(self):
218        """
219        Check if there is a scheduled job for now and if there is,
220        return True, otherwise False.
221        """
222        cur_time = time.localtime()
223
224        # check if we should check for job or not
225        if self.last_check and cur_time[1] == self.last_check[1] and \
226            cur_time[2] == self.last_check[2] and \
227            cur_time[3] == self.last_check[3] and \
228            cur_time[4] == self.last_check[4] and \
229            cur_time[6] == self.last_check[6]:
230
231            return False # too early to run a job again!
232
233        if cur_time[1] in self.month and cur_time[2] in self.day \
234            and cur_time[3] in self.hour and cur_time[4] in self.minute \
235            and cur_time[6] in self.weekday:
236           
237            self.last_check = cur_time
238            return True # there is a job to run!
239
240
241    def setup_schema(self, name):
242        """
243        Setup schema.
244        """
245        if self.schema_parser.has_section(name):
246            for opt in self.schema_parser.options(name):
247                if opt in self.options.keys():
248                    self.options[opt](self.schema_parser.get(name, opt))
249
250            self.load_profile()
251
252
253    def load_profile(self):
254        """
255        Load scheduling profile for current schema.
256        """
257        p_cfg = ConfigParser()
258        p_cfg.read(self.profiles)
259
260        if p_cfg.has_section(self.profile):
261            for opt in p_cfg.options(self.profile):
262                if opt in self.profile_opts.keys():
263                    self.profile_opts[opt](p_cfg.get(self.profile, opt))
264
265
266    def _get_schema_name(self):
267        """
268        Return schema name set.
269        """
270        return self.__schema_name
271
272
273    def _get_last_check(self):
274        """
275        Return when last check happened.
276        """
277        return self.__lcheck
278
279
280    def _set_last_check(self, when):
281        """
282        Set last check time.
283        """
284        self.__lcheck = when
285
286
287    # Schema Profile
288    def _get_profile(self):
289        return self.__profile
290
291
292    def _set_profile(self, profile):
293        """
294        Set scheduling profile for current schema.
295        """
296        self.__profile = profile
297
298
299    # Job time
300    def _get_month(self):
301        return self.__month
302
303
304    def _set_month(self, month):
305        """
306        Parse and set month.
307        """
308        self.__month = self.cron_parser.parse_month(month)
309
310
311    def _get_day(self): 
312        return self.__day
313
314
315    def _set_day(self, day):
316        """
317        Parse and set day.
318        """
319        self.__day = self.cron_parser.parse_day(day)
320
321
322    def _get_weekday(self): 
323        return self.__weekday
324
325
326    def _set_weekday(self, weekday):
327        """
328        Parse and set weekday.
329        """
330        self.__weekday = self.cron_parser.parse_weekday(weekday)
331
332
333    def _get_hour(self): 
334        return self.__hour
335
336
337    def _set_hour(self, hour):
338        """
339        Parse and set hour.
340        """
341        self.__hour = self.cron_parser.parse_hour(hour)
342
343
344    def _get_minute(self): 
345        return self.__minute
346
347
348    def _set_minute(self, minute):
349        """
350        Parse and set minute.
351        """
352        self.__minute = self.cron_parser.parse_minute(minute)
353
354
355    # Job Command
356    def _get_command(self):
357        """
358        Get job command.
359        """
360        return self.__command
361
362
363    def _set_command(self, command):
364        """
365        Set command for job.
366        """
367        self.__command = command
368
369
370    # Job Options
371    def _get_enabled(self):
372        """
373        Returns True if job should run, otherwise, False.
374        """
375        return self.__enabled
376
377
378    def _set_enabled(self, enable):
379        """
380        Set if job should run or not.
381        """
382        self.__enabled = enable
383
384
385    def _get_addtoinv(self):
386        """
387        Returns True if job result should be stored in Inventory, otherwise,
388        False.
389        """
390        return self.__addtoinv
391
392
393    def _set_addtoinv(self, add):
394        """
395        Sets job result to be added to Inventory, or not.
396        """
397        self.__addtoinv = add
398
399
400    def _get_saveto(self):
401        """
402        Get file that stores job results.
403        """
404        return self.__savefile
405
406
407    def _set_saveto(self, save_dir):
408        """
409        Set a file to store job results.
410        """
411        if save_dir:
412            self.__savefile = os.path.join(save_dir, self.schema_name + ".xml")
413        else:
414            self.__savefile = None
415       
416
417    def _get_mailto(self):
418        """
419        Get email set to receive job results.
420        """
421        return self.__mail
422
423
424    def _set_mailto(self, mail):
425        """
426        Set an email to receive job results.
427        """
428        self.__mail = mail
429
430   
431    def _get_smtp(self):
432        """
433        Get smtp profile to use for sending email.
434        """
435        return self.__smtp
436
437
438    def _set_smtp(self, smtp):
439        """
440        Set a smtp profile for sending email.
441        """
442        self.__smtp = smtp
443
444
445    # Properties
446    profile = property(_get_profile, _set_profile)
447    month = property(_get_month, _set_month)
448    day = property(_get_day, _set_day)
449    weekday = property(_get_weekday, _set_weekday)
450    hour = property(_get_hour, _set_hour)
451    minute = property(_get_minute, _set_minute)
452    command = property(_get_command, _set_command)
453    enabled = property(_get_enabled, _set_enabled)
454    addtoinv = property(_get_addtoinv, _set_addtoinv)
455    saveto = property(_get_saveto, _set_saveto)
456    mailto = property(_get_mailto, _set_mailto)
457    smtp = property(_get_smtp, _set_smtp)
458
459    last_check = property(_get_last_check, _set_last_check)
460    schema_name = property(_get_schema_name)
461
462
463class SchedulerControl(object):
464
465    def __init__(self, running_file=None, home_conf=None, verbose=False,
466            svc_class=None, svc_path=None):
467        if running_file is None or home_conf is None:
468            if home_conf is None:
469                home_conf = os.path.split(Path.get_umit_conf())[0]
470            running_file = os.path.join(home_conf, 'schedrunning')
471
472        self.svc_class = svc_class
473        self.svc_path = svc_path
474        self.running_file = running_file
475        self.home_conf = home_conf
476        self.verbose = verbose
477
478    def start(self, from_gui=False):
479        """Start scheduler."""
480        if NT:
481            bg = BGRunner(self.svc_class, self.svc_path)
482            from_gui = False
483        else:
484            if from_gui:
485                # Take care when running from gui
486                running_path = Path.get_running_path()
487                if running_path not in sys.path:
488                    sys.path.append(running_path)
489                starter = __import__('umit_scheduler')
490                subprocess.Popen([sys.executable, starter.__file__, 'start'])
491            else:
492                def post_init():
493                    return main('start', sys.path[0], self.home_conf)
494                bg = BGRunner(self.running_file, post_init)
495
496        if not from_gui:
497            err = bg.start()
498            if err:
499                return self._error(err)
500
501    def stop(self):
502        if NT:
503            bg = BGRunner(self.svc_class)
504        else:
505            bg = BGRunner(self.running_file)
506
507        err = bg.stop()
508        if err:
509            return self._error(err)
510
511    def running(self):
512        if NT:
513            bg = BGRunner(self.svc_class)
514        else:
515            bg = BGRunner(self.running_file)
516
517        return bg.running()
518
519    def cleanup(self, remove_service=False):
520        if NT:
521            bg = BGRunner(self.svc_class)
522            if remove_service:
523                err = bg.remove()
524                if err:
525                    return self._error(err)
526        else:
527            bg = BGRunner(self.running_file)
528
529        err = bg.cleanup()
530        if err:
531            return self._error(err)
532
533    def _error(self, error):
534        if self.verbose:
535            print error
536            return 1
537        else:
538            if NT:
539                return win32api.FormatMessage(error)
540            else:
541                return error
542
543
544def load_smtp_profile(smtp):
545    """
546    Load a smtp schema.
547    """
548    schema = ConfigParser()
549    schema.read(Path.smtp_schemas)
550   
551    if not schema.has_section(smtp):
552        raise Exception("Inconsistence in scheduling schemas within smtp \
553schemas, you tried to use the following smtp schema: %s, but it doesn't \
554exist." % smtp)
555 
556    smtp_dict = { 'auth': '',
557                  'user': '',
558                  'pass': '',
559                  'server': '',
560                  'port': '',
561                  'tls': '',
562                  'mailfrom': '',
563                }
564
565    for item in schema.items(smtp):
566        smtp_dict[item[0]] = item[1]
567
568    return smtp_dict
569
570
571def decide_output(ofile):
572    """
573    Choose a better or the same output path.
574    """
575    output = None
576    file_path = os.path.abspath(ofile)
577
578    if os.path.exists(os.path.split(file_path)[0]): # path exists at least
579        try: # try to open file in read mode
580            f = open(ofile, 'r')
581            f.close()
582            # if we are still here, file exists, this is bad ;/ (not so bad)
583            count = 1
584           
585            # file name without extension
586            cut = ofile.find('.xml')
587            orig = ofile[:cut]
588           
589            while os.path.isfile(ofile):
590                ofile = orig + "_%d.xml" % count # try appending extra extension
591               
592                count += 1
593           
594            warnings.warn("File will be saved as %s" % ofile, Warning)
595           
596        except IOError: # file didnt exist and path exists, this is good!
597            pass
598           
599        output = ofile
600       
601    return output
602
603
604def calc_next_time():
605    """
606    Calculate next time to check for changes and jobs.
607    """
608    tt = time.localtime(time.time() + 60)
609    tt = tt[:5] + (0,) + tt[6:]
610    return time.mktime(tt)
611
612
613running_scans = { }
614
615def run_scheduler(sched, winhndl=None):
616    """
617    Run scheduler forever.
618    """
619    next_time = calc_next_time()
620    scount = 0 # number of scans running
621
622    while 1: # run forever and ever ;)
623        current_time = time.time()
624
625        if current_time < next_time:
626            sleep_time = next_time - current_time + .1
627            if winhndl:
628                stopsignal = win32event.WaitForSingleObject(winhndl,
629                        int(sleep_time * 1000))
630                if stopsignal == win32event.WAIT_OBJECT_0:
631                    break
632            else:
633                time.sleep(sleep_time)
634
635        # check if time has changed by more than two minutes (clock changes)
636        if abs(time.time() - next_time) > 120:
637            # reset timer
638            next_time = calc_next_time()
639
640        sched.check_for_changes()
641
642        for schema in sched.schemas:
643            if schema.job_to_run():
644                if not int(schema.enabled): # schema disabled, neeexxt!
645                    continue
646
647                name = schema.schema_name
648                log.info("Scheduled schema to run: %s" % name)
649
650                if not schema.command:
651                    log.warning("No command set for schema %s, \
652skipping!" % name)
653                    continue
654
655                scan = NmapCommand(schema.command)
656                scan.run_scan()
657
658                running_scans[scount] = (scan, schema.saveto, schema.mailto,
659                                         name, schema.addtoinv, schema.smtp)
660                scount += 1 # more one scan running
661       
662        for running, opts in running_scans.items():
663            scan = opts[0]
664
665            try:
666                scan_state = scan.scan_state()
667            except Exception, err:
668                log.critical("Scheduled schema '%s' failed to run. \
669Reason: %s" % (schema.schema_name , err))
670                continue
671
672            if not scan_state: # scan finished
673                log.info("Scan finished: %s" % opts[3])
674           
675                if opts[1]: # save xml output
676                    saveto = decide_output(opts[1])
677                    f = open(saveto, 'w')
678                    f.write(open(scan.get_xml_output_file(), 'r').read())
679                    f.close()
680                    log.info("Scan output saved as: %s" % saveto)
681               
682                if opts[2]: # mail output
683                    recipients = opts[2].split(',')
684                    log.info("Scan output will be mailed to: %s" % recipients)
685
686                    smtp = load_smtp_profile(opts[5])
687
688                    auth = int(smtp['auth'])
689                    tls = int(smtp['tls'])
690                    mailfrom = smtp['mailfrom']
691                    user = smtp['user']
692                    passwd = smtp['pass']
693                    server = smtp['server']
694                    port = smtp['port']
695                    curr_time = format_asctime(datetime.now())
696                    orig_output = scan.get_xml_output_file()
697                    new_file_output = os.path.join(
698                            os.path.dirname(orig_output),
699                            "%s (%s).xml" % (curr_time, opts[3]))
700
701                    fd_wcont = open(new_file_output, 'w')
702                    for line in open(orig_output, 'r'):
703                        fd_wcont.write(line)
704                    fd_wcont.close()
705
706                    email = Email(mailfrom, recipients, server, None,
707                                  user, passwd, tls, port)
708                    email.sendmail(
709                            subject=(
710                                _("UMIT: Status Report for scheduled schema") +
711                                " %r " % opts[3]),
712                            msg=(
713                                _("There was a scheduled job that just "
714                                    "finished:") + (" %s\n" % curr_time) +
715                                _("Follows an attachment of the job output.")),
716                            attach=new_file_output)
717                    os.remove(new_file_output)
718
719                if int(opts[4]): # add to inventory
720                    log.info("Scan result is being added to Inventory "
721                            "%s" % opts[3])
722                    xmlstore = XMLStore(Path.umitdb_ng, False)
723                    try:
724                        xmlstore.store(scan.get_xml_output_file(),
725                                inventory=opts[3])
726                    finally:
727                        xmlstore.close() # close connection to the database
728                    log.info("Scan finished insertion on Inventory "
729                            "%s" % opts[3])
730
731                scan.close() # delete temporary files
732                scount -= 1 # one scan finished
733                del running_scans[running]
734           
735        next_time += 60
736
737
738def safe_shutdown(rec_signal, frame):
739    """
740    Remove temp files before quitting.
741    """
742    log.info("Scheduler finishing..")
743   
744    if running_scans:
745        scans_count = len(running_scans)
746        log.debug("%d scan%s to cleanup" % (scans_count,
747                                            (scans_count > 1) and 's' or ''))
748
749    for args in running_scans.values():
750        scan = args[0]
751        scan.close()
752
753    log.info("Scheduler finished sucessfully!")
754
755    if rec_signal is not None:
756        raise SystemExit
757
758
759def start(schema_file=None, profile_file=None, winhndl=None):
760    """
761    Start scheduler.
762    """
763    log.info("Scheduler starting..")
764
765    if not schema_file:
766        schema_file = Path.sched_schemas
767    if not profile_file:
768        profile_file = Path.sched_profiles
769
770    s = Scheduler(schema_file, profile_file)
771
772    log.info("Scheduler started, using schemas file %r and "
773            "scheduling file %r" % (schema_file, profile_file))
774
775    try:
776        run_scheduler(s, winhndl)
777    except KeyboardInterrupt:
778        # if we are on win32, we should be here in case a WM_CLOSE message
779        # was sent.
780        safe_shutdown(None, None)
781    else:
782        # run_scheduler should finish normally when running as a Windows
783        # Service
784        safe_shutdown(None, None)
785
786
787def main(cmd, base_path, home_conf, winhndl=None):
788    if base_path not in sys.path:
789        sys.path.insert(0, base_path)
790    Path.force_set_umit_conf(home_conf)
791    log = file_log(Path.sched_log)
792
793    globals()['log'] = log
794
795    # Trying to adjust signals when running as a windows service won't work
796    # since it needs to be adjusted while on the main thread.
797    if not servicemanager.RunningAsService():
798        if os.name == "posix":
799            signal.signal(signal.SIGHUP, safe_shutdown)
800        signal.signal(signal.SIGTERM, safe_shutdown)
801        signal.signal(signal.SIGINT, safe_shutdown)
802
803    cmds = {'start': start}
804    cmds[cmd](winhndl=winhndl)
Note: See TracBrowser for help on using the browser.