From 6c97fd7bb9ff8dec0fd36188500f691620e9bf93 Mon Sep 17 00:00:00 2001 From: Robert Sander Date: Thu, 7 Dec 2023 22:11:17 +0100 Subject: [PATCH] adds add_auto_holidays, config and cronjob --- holidays/bin/holidays.py | 225 +++++++++++++++++++---------------- holidays/etc/cron.d/holidays | 8 ++ holidays/etc/holidays | 33 +++++ holidays/holidays-2.0.0.mkp | Bin 4269 -> 0 bytes holidays/holidays-2.1.0.mkp | Bin 0 -> 4510 bytes 5 files changed, 166 insertions(+), 100 deletions(-) create mode 100644 holidays/etc/cron.d/holidays create mode 100644 holidays/etc/holidays delete mode 100644 holidays/holidays-2.0.0.mkp create mode 100644 holidays/holidays-2.1.0.mkp diff --git a/holidays/bin/holidays.py b/holidays/bin/holidays.py index db51631c..33487f08 100755 --- a/holidays/bin/holidays.py +++ b/holidays/bin/holidays.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +# -*- encoding: utf-8; py-indent-offset: 4 -*- # # (C) 2023 Heinlein Support GmbH - License: GNU General Public License v2 @@ -19,7 +20,8 @@ # # defaults # -country = "de" +countries = ["de"] +default_country = "de" states = { 'de': { 'bb': 'Brandenburg', @@ -42,26 +44,12 @@ } always = [{'day': 'all', 'time_ranges': [{'start': '00:00', 'end': '24:00'}]}] -def exclude_in(name, exclude_in_tp): - etp, etag = cmk.get_timeperiod(exclude_in_tp) - - if args.debug: - pprint(etp) - pprint(etag) - - exclude = etp['extensions']['exclude'] - exclude.append(name) - - if args.debug: - pprint(exclude) - - cmk.edit_timeperiod(exclude_in_tp, etag, exclude=exclude) parser = argparse.ArgumentParser() parser.add_argument('-s', '--url', help='URL to Check_MK site') parser.add_argument('-u', '--username', help='name of the automation user') parser.add_argument('-p', '--password', help='secret of the automation user') -parser.add_argument('-c', '--config-file', help='config file', +parser.add_argument('-c', '--config-file', help='config file (JSON)', default=os.path.join(os.environ.get('OMD_ROOT'), 'etc', 'holidays')) parser.add_argument('-D', '--debug', action='store_true') subparsers = parser.add_subparsers(title='available commands', help='call "subcommand --help" for more information') @@ -72,109 +60,71 @@ def exclude_in(name, exclude_in_tp): delete_old.set_defaults(func='delete_old') dump_timeperiods = subparsers.add_parser('dump', help='dump timeperiods') dump_timeperiods.set_defaults(func='dump_timeperiods') -create_timeperiod = subparsers.add_parser('create', help='create timeperiod from api-feiertage.de') -create_timeperiod.set_defaults(func='create_timeperiod') -create_timeperiod.add_argument('-a', '--all-states', action='store_true', help='Nur bundesweite Feiertage') -create_timeperiod.add_argument('-l', '--country', default=country, help='Land') -create_timeperiod.add_argument('-s', '--state', choices=states['de'].keys(), help='Bundesland') -create_timeperiod.add_argument('-y', '--year') -create_timeperiod.add_argument('-c', '--current-year', action='store_true') -create_timeperiod.add_argument('-e', '--exclude-in-default', action='store_true', help='Exclude in passender Standard-Timeperiod') -create_timeperiod.add_argument('-E', '--exclude-in', help='Exclude in anderer Timeperiod') -add_region = subparsers.add_parser('addregion', help='add a new region to the configuration (base timeperiods and tag)') +add_holidays = subparsers.add_parser('add_holidays', help='add timeperiod from api-feiertage.de') +add_holidays.set_defaults(func='add_holidays') +add_holidays.add_argument('-a', '--all-states', action='store_true', help='Nur bundesweite Feiertage') +add_holidays.add_argument('-l', '--country', choices=countries, default=default_country, help='Land (default=de)') +add_holidays.add_argument('-s', '--state', choices=states[default_country].keys(), help='Bundesland') +add_holidays.add_argument('-y', '--year') +add_holidays.add_argument('-Y', '--current-year', action='store_true') +add_holidays.add_argument('-e', '--exclude-in-default', action='store_true', help='Exclude in passender Standard-Timeperiod') +add_holidays.add_argument('-E', '--exclude-in', help='Exclude in anderer Timeperiod') +add_region = subparsers.add_parser('add_region', help='add a new region to the configuration (base timeperiods and tag)') add_region.set_defaults(func='add_region') -add_region.add_argument('-l', '--country', default=country, help='Land') +add_region.add_argument('-l', '--country', choices=countries, default=default_country, help='Land') add_region.add_argument('-s', '--state', choices=states['de'].keys(), help='Bundesland', required=True) +add_auto_holidays = subparsers.add_parser('add_auto_holidays', help='add timeperiods from api-feiertage.de for all regions') +add_auto_holidays.set_defaults(func='add_auto_holidays') +add_auto_holidays.add_argument('-y', '--year') +add_auto_holidays.add_argument('-Y', '--current-year', action='store_true') cleanup = subparsers.add_parser('cleanup', help='remove timeperiods and tag group. Use with caution!') cleanup.set_defaults(func='cleanup') -args = parser.parse_args() -if 'func' not in args: - parser.print_help() - sys.exit(1) -if args.debug: - pprint(args) - -config = json.load(open(args.config_file)) - -if args.debug: - pprint(config) - -cmk = checkmkapi.CMKRESTAPI(args.url, args.username, args.password) - -if args.func == 'dump_timeperiods': - pprint(cmk.get_timeperiods()[0]) - -if args.func == 'delete_timeperiod': - cmk.delete_timeperiod(args.name, '*') - cmk.activate() - -if args.func == 'delete_old': - tps, etag = cmk.get_timeperiods() - - to_delete = [] - thisyear = str(date.today().year) +def exclude_in_timeperiod(name, exclude_in_tp): + etp, etag = cmk.get_timeperiod(exclude_in_tp) if args.debug: - print(f"Removing each timeperiod starting with \"{config['timeperiods']['holidays']['name']}_\" up to but not including {thisyear}") + pprint(etp) + pprint(etag) - tps, etag = cmk.get_timeperiods() + exclude = etp['extensions']['exclude'] + exclude.append(name) - for tp in tps['value']: - if tp['id'].startswith(config['timeperiods']['holidays']['name'] + '_'): - year = tp['id'].split('_')[1] - if year < thisyear: - to_delete.append(tp['id']) + if args.debug: + pprint(exclude) - for tp in tps['value']: - excludes = tp['extensions'].get('exclude', []) - changes = False - for td in to_delete: - if td in excludes: - if args.debug: - print(f"Removing {td} from {tp['id']}") - excludes.remove(td) - changes = True - if changes: - te, etag = cmk.get_timeperiod(tp['id']) - cmk.edit_timeperiod(tp['id'], etag, exclude=excludes) - for td in to_delete: - if args.debug: - print(f"Removing {td}") - cmk.delete_timeperiod(td, '*') - if to_delete: - cmk.activate() + cmk.edit_timeperiod(exclude_in_tp, etag, exclude=exclude) -if args.func == 'create_timeperiod': +def add_holiday_timeperiod(country=default_country, state=None, all_states=False, current_year=False, set_year=None, exclude_in_default=True, exclude_in=None): params = {} name = config['timeperiods']['holidays']['name'] + '_' alias = config['timeperiods']['holidays']['title'] + ' ' year = None - if args.current_year: + if current_year: year = str(date.today().year) - elif args.year: - year = args.year + elif set_year: + year = set_year if year: params['years'] = year name += year alias += year - name += "_" + args.country - alias += " " + args.country.upper() - if args.all_states: + name += "_" + country + alias += " " + country.upper() + if all_states: params['all_states'] = "true" name += '_bundeseinheitlich' alias += ' bundeseinheitlich' - elif args.state: - params['states'] = args.state - name += '_%s' % args.state - alias += ' %s' % states[args.country][args.state] + elif state: + params['states'] = state + name += '_%s' % state + alias += ' %s' % states[country][state] if args.debug: pprint(params) if not params: print('Please give at least a year or a state.\n') - create_timeperiod.print_help() + add_holidays.print_help() sys.exit(1) resp = requests.get(apifeiertage, params=params) @@ -188,10 +138,7 @@ def exclude_in(name, exclude_in_tp): if resp.status_code >= 400: sys.stderr.write("%r\n" % data) - if not data.get('feiertage'): - print('Error: %s' % data.get('additional_note')) - sys.exit(1) - else: + if data.get('feiertage'): exceptions = [] for feiertag in data['feiertage']: exceptions.append({ @@ -210,13 +157,75 @@ def exclude_in(name, exclude_in_tp): if args.debug: pprint(tp) - if args.exclude_in_default: - exclude_in(name, config['timeperiods']['workhours']['name'] + "_" + args.country + "_" + args.state) + if exclude_in_default: + exclude_in_timeperiod(name, config['timeperiods']['workhours']['name'] + "_" + country + "_" + state) - if args.exclude_in: - exclude_in(name, args.exclude_in) + if exclude_in: + exclude_in_timeperiod(name, exclude_in) + else: + print('Error: %s' % data.get('additional_note')) + sys.exit(1) + +args = parser.parse_args() +if 'func' not in args: + parser.print_help() + sys.exit(1) +if args.debug: + pprint(args) + +config = json.load(open(args.config_file)) + +if args.debug: + pprint(config) + +cmk = checkmkapi.CMKRESTAPI(args.url, args.username, args.password) + +if args.func == 'dump_timeperiods': + pprint(cmk.get_timeperiods()[0]) + +if args.func == 'delete_timeperiod': + cmk.delete_timeperiod(args.name, '*') + cmk.activate() + +if args.func == 'delete_old': + tps, etag = cmk.get_timeperiods() + + to_delete = [] + thisyear = str(date.today().year) + + if args.debug: + print(f"Removing each timeperiod starting with \"{config['timeperiods']['holidays']['name']}_\" up to but not including {thisyear}") + + tps, etag = cmk.get_timeperiods() + + for tp in tps['value']: + if tp['id'].startswith(config['timeperiods']['holidays']['name'] + '_'): + year = tp['id'].split('_')[1] + if year < thisyear: + to_delete.append(tp['id']) + + for tp in tps['value']: + excludes = tp['extensions'].get('exclude', []) + changes = False + for td in to_delete: + if td in excludes: + if args.debug: + print(f"Removing {td} from {tp['id']}") + excludes.remove(td) + changes = True + if changes: + te, etag = cmk.get_timeperiod(tp['id']) + cmk.edit_timeperiod(tp['id'], etag, exclude=excludes) + for td in to_delete: + if args.debug: + print(f"Removing {td}") + cmk.delete_timeperiod(td, '*') + if to_delete: + cmk.activate() - cmk.activate() +if args.func == 'add_holidays': + add_holiday_timeperiod(args.country, args.state, args.all_states, args.current_year, args.year, args.exclude_in_default, args.exclude_in) + cmk.activate() if args.func == "add_region": workhoursname = '%s_%s_%s' % ( config['timeperiods']['workhours']['name'], @@ -241,6 +250,8 @@ def exclude_in(name, exclude_in_tp): if args.debug: pprint(tp) + add_holiday_timeperiod(country=args.country, state=args.state, current_year=True) + tg = None try: tg, etag = cmk.get_host_tag_group(config['taggroup']['name']) @@ -275,6 +286,19 @@ def exclude_in(name, exclude_in_tp): ) cmk.activate() +if args.func == 'add_auto_holidays': + tps, etag = cmk.get_timeperiods() + changes = False + for tp in tps['value']: + if tp['id'].startswith(config['timeperiods']['workhours']['name']): + if args.debug: + print(f"found {tp['id']}") + _, country, state = tp['id'].split('_') + add_holiday_timeperiod(country=country, state=state, current_year=args.current_year, set_year=args.year) + changes = True + if changes: + cmk.activate() + if args.func == "cleanup": tps, etag = cmk.get_timeperiods() @@ -294,7 +318,8 @@ def exclude_in(name, exclude_in_tp): try: cmk.delete_host_tag_group(config['taggroup']['name']) - print(f"remove host tag group {config['taggroup']['name']}") + if args.debug: + print(f"remove host tag group {config['taggroup']['name']}") changes = True except: pass diff --git a/holidays/etc/cron.d/holidays b/holidays/etc/cron.d/holidays new file mode 100644 index 00000000..87be723d --- /dev/null +++ b/holidays/etc/cron.d/holidays @@ -0,0 +1,8 @@ +# +# Remove old holiday timeperiods in February +# +1 1 12 2 * $OMD_ROOT/local/bin/holidays.py delete_old +# +# Automatically add new holiday timeperiods in November +# +1 1 12 11 * $OMD_ROOT/local/bin/holidays.py add_auto_holidays diff --git a/holidays/etc/holidays b/holidays/etc/holidays new file mode 100644 index 00000000..2784a0cf --- /dev/null +++ b/holidays/etc/holidays @@ -0,0 +1,33 @@ +{ + "comment": "Configuration for ~/local/bin/holidays.py", + + "workdays": [ + {"day": "monday", "time_ranges": [{"start": "09:00", "end": "18:00"}]}, + {"day": "tuesday", "time_ranges": [{"start": "09:00", "end": "18:00"}]}, + {"day": "wednesday", "time_ranges": [{"start": "09:00", "end": "18:00"}]}, + {"day": "thursday", "time_ranges": [{"start": "09:00", "end": "18:00"}]}, + {"day": "friday", "time_ranges": [{"start": "09:00", "end": "15:00"}]} + ], + + "taggroup": { + "name": "holidays", + "title": "Feiertage", + "empty_title": "Keine Feiertage", + "topic": "Notifications" + }, + + "timeperiods": { + "oncall": { + "name": "oncall", + "title": "Bereitschaft" + }, + "workhours": { + "name": "workhours", + "title": "Arbeitszeit" + }, + "holidays": { + "name": "holidays", + "title": "Feiertage" + } + } +} diff --git a/holidays/holidays-2.0.0.mkp b/holidays/holidays-2.0.0.mkp deleted file mode 100644 index 2ca76d42cd7a207bfa2dc302b30f25b3322fabe7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4269 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 P+)4QlOZf+U0C)fZP69_7 diff --git a/holidays/holidays-2.1.0.mkp b/holidays/holidays-2.1.0.mkp new file mode 100644 index 0000000000000000000000000000000000000000..c236c40f9ef622e1fb61788d4c3dddf7bf1a9172 GIT binary patch literal 4510 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 wn%1