From 1453b62bedde61932218c361bfef5d7657003743 Mon Sep 17 00:00:00 2001 From: Robert Sander Date: Mon, 2 Dec 2024 08:59:28 +0100 Subject: [PATCH] adds support for multiple countries and notification rules --- holidays/bin/holidays.py | 355 +++++++++++++++++++++++++----------- holidays/holidays-2.1.3.mkp | Bin 4546 -> 0 bytes holidays/holidays-2.2.0.mkp | Bin 0 -> 5704 bytes 3 files changed, 244 insertions(+), 111 deletions(-) delete mode 100644 holidays/holidays-2.1.3.mkp create mode 100644 holidays/holidays-2.2.0.mkp diff --git a/holidays/bin/holidays.py b/holidays/bin/holidays.py index 88ad48e0..5c30878c 100755 --- a/holidays/bin/holidays.py +++ b/holidays/bin/holidays.py @@ -8,42 +8,62 @@ import sys import os -import argparse +import argparse # type: ignore import checkmkapi import requests import json -from datetime import date -from pprint import pprint +from datetime import date # type: ignore +from pprint import pprint # type: ignore -apifeiertage = 'https://get.api-feiertage.de/' +apifeiertage = 'https://openholidaysapi.org/' +created_by = f"created by {__file__}" # # defaults # -countries = ["de"] +countries = ["de", "at", "lu"] default_country = "de" -states = { - 'de': { - 'bb': 'Brandenburg', - 'be': 'Berlin', - 'bw': 'Baden-Württemberg', - 'by': 'Bayern', - 'hb': 'Bremen', - 'hh': 'Hamburg', - 'he': 'Hessen', - 'mv': 'Mecklenburg-Vorpommern', - 'ni': 'Niedersachsen', - 'nw': 'Nordrhein-Westfalen', - 'rp': 'Rheinland-Pfalz', - 'sl': 'Saarland', - 'sn': 'Sachsen', - 'st': 'Sachsen-Anhalt', - 'sh': 'Schleswig-Holstein', - 'th': 'Thüringen', - }, -} -always = [{'day': 'all', 'time_ranges': [{'start': '00:00', 'end': '24:00'}]}] +language = "DE" + +def convert_name(name): + umlaute = { + 'ä': 'ae', + 'ö': 'oe', + 'ü': 'ue', + 'Ä': 'Ae', + 'Ö': 'Oe', + 'Ü': 'Ue', + 'ß': 'ss', + } + for umlaut, ersatz in umlaute.items(): + name = name.replace(umlaut, ersatz) + return name + +states = {} +states_orig = {} +state_choices = set() +for country in countries: + states[country] = {} + states_orig[country] = {} + resp = requests.get(apifeiertage + 'Subdivisions', params = {"countryIsoCode": country.upper()}) + for region in resp.json(): + use_region = False + for category in region.get("category", []): + if category.get("language") == "EN" and category.get("text") in ["state", "federal state"]: + use_region = True + break + if use_region: + longname = region.get("shortName") + name = longname.lower() + for name_info in region.get("name", []): + if name_info.get("language") == language: + longname = name_info.get("text", longname) + name_convert = convert_name(name) + states[country][name] = {"name": longname, "ascii": name_convert} + states_orig[country][name_convert] = name + state_choices.add(name) +always = [{'day': 'all', 'time_ranges': [{'start': '00:00', 'end': '24:00'}]}] parser = argparse.ArgumentParser() parser.add_argument('-s', '--url', help='URL to Check_MK site') @@ -58,13 +78,15 @@ delete_timeperiod.add_argument('name', help='name of timeperiod') delete_old = subparsers.add_parser('delete_old', help='delete old holiday timeperiods') delete_old.set_defaults(func='delete_old') -dump_timeperiods = subparsers.add_parser('dump', help='dump timeperiods') +dump_timeperiods = subparsers.add_parser('dump_timeperiods', help='dump timeperiods') dump_timeperiods.set_defaults(func='dump_timeperiods') -add_holidays = subparsers.add_parser('add_holidays', help='add timeperiod from api-feiertage.de') +dump_regions = subparsers.add_parser('dump_regions', help='show avaliable regions') +dump_regions.set_defaults(func='dump_regions') +add_holidays = subparsers.add_parser('add_holidays', help='add timeperiod from %s' % apifeiertage) 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('-s', '--state', choices=list(state_choices), 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') @@ -72,8 +94,8 @@ 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', 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_region.add_argument('-s', '--state', choices=list(state_choices), help='Bundesland') +add_auto_holidays = subparsers.add_parser('add_auto_holidays', help='add timeperiods from %s for all regions' % apifeiertage) 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') @@ -96,7 +118,7 @@ def exclude_in_timeperiod(name, exclude_in_tp): cmk.edit_timeperiod(exclude_in_tp, etag, exclude=exclude) 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 = {} + params = {"languageIsoCode": language, "countryIsoCode": country.upper()} name = config['timeperiods']['holidays']['name'] + '_' alias = config['timeperiods']['holidays']['title'] + ' ' year = None @@ -105,29 +127,29 @@ def add_holiday_timeperiod(country=default_country, state=None, all_states=False elif set_year: year = set_year if year: - params['years'] = year + params['validFrom'] = f"{year}-01-01" + params['validTo'] = f"{year}-12-31" name += year - alias += year + alias += year name += "_" + country alias += " " + country.upper() if all_states: - params['all_states'] = "true" - name += '_bundeseinheitlich' - alias += ' bundeseinheitlich' + name += '_national' + alias += ' national' elif state: - params['states'] = state - name += '_%s' % state - alias += ' %s' % states[country][state] + params['subdivisionCode'] = "%s-%s" % (country.upper(), state.upper()) + name += '_%s' % states[country][state]["ascii"] + alias += ' %s' % states[country][state]["name"] if args.debug: pprint(params) - if not params: + if 'validFrom' not in params and 'subdivisonCode' not in params: print('Please give at least a year or a state.\n') add_holidays.print_help() sys.exit(1) - resp = requests.get(apifeiertage, params=params) + resp = requests.get(apifeiertage + "PublicHolidays", params=params) if resp.content: try: data = resp.json() @@ -137,34 +159,47 @@ def add_holiday_timeperiod(country=default_country, state=None, all_states=False data = {} if resp.status_code >= 400: sys.stderr.write("%r\n" % data) - - if data.get('feiertage'): - exceptions = [] - for feiertag in data['feiertage']: + + if args.debug: + print("data = ") + pprint(data) + + exceptions = [] + for feiertag in data: + if all_states: + if feiertag["nationwide"]: + exceptions.append({ + 'date': feiertag['startDate'], + 'time_ranges': [ { 'start': '00:00:00', 'end': '24:00:00' } ] + }) + else: exceptions.append({ - 'date': feiertag['date'], + 'date': feiertag['startDate'], 'time_ranges': [ { 'start': '00:00:00', 'end': '24:00:00' } ] }) - if args.debug: - pprint(name) - pprint(alias) - pprint(exceptions) + if args.debug: + print("name =") + pprint(name) + print("alias =") + pprint(alias) + print("exceptions =") + pprint(exceptions) - if exceptions: - tp, etag = cmk.create_timeperiod(name, alias, [], exceptions=exceptions) + if exceptions: + tp, etag = cmk.create_timeperiod(name, alias, [], exceptions=exceptions) - if args.debug: - pprint(tp) + if args.debug: + pprint(tp) - if exclude_in_default: - exclude_in_timeperiod(name, config['timeperiods']['workhours']['name'] + "_" + country + "_" + state) + if exclude_in_default: + if state: + exclude_in_timeperiod(name, config['timeperiods']['workhours']['name'] + "_" + country + "_" + states[country][state]["ascii"]) + else: + exclude_in_timeperiod(name, config['timeperiods']['workhours']['name'] + "_" + country) - if exclude_in: - exclude_in_timeperiod(name, exclude_in) - else: - print('Error: %s' % data.get('additional_note')) - sys.exit(1) + if exclude_in: + exclude_in_timeperiod(name, exclude_in) args = parser.parse_args() if 'func' not in args: @@ -172,6 +207,8 @@ def add_holiday_timeperiod(country=default_country, state=None, all_states=False sys.exit(1) if args.debug: pprint(args) + pprint(states) + pprint(states_orig) config = json.load(open(args.config_file)) @@ -183,6 +220,10 @@ def add_holiday_timeperiod(country=default_country, state=None, all_states=False if args.func == 'dump_timeperiods': pprint(cmk.get_timeperiods()[0]) +if args.func == 'dump_regions': + pprint(states) + pprint(states_orig) + if args.func == 'delete_timeperiod': cmk.delete_timeperiod(args.name, '*') cmk.activate() @@ -228,69 +269,154 @@ def add_holiday_timeperiod(country=default_country, state=None, all_states=False cmk.activate() if args.func == "add_region": - workhoursname = '%s_%s_%s' % ( config['timeperiods']['workhours']['name'], - args.country, - args.state ) - tp, etag = cmk.create_timeperiod(workhoursname, - '%s %s %s' % ( config['timeperiods']['workhours']['title'], - args.country.upper(), - states[args.country][args.state] ), + if not args.state: + if states[args.country]: + print(f"State needed for country {args.country}") + sys.exit(1) + + tp_workhours_id = '%s_%s' % ( config['timeperiods']['workhours']['name'], + args.country) + tp_workhours_title = '%s %s' % ( config['timeperiods']['workhours']['title'], + args.country.upper()) + tp_oncall_id = '%s_%s' % ( config['timeperiods']['oncall']['name'], + args.country) + tp_oncall_title = '%s %s' % ( config['timeperiods']['oncall']['title'], + args.country.upper()) + tag = { + 'id': "%s_%s" % (config['taggroup']['name'], args.country), + 'title': "%s %s" % (config['taggroup']['title'], args.country.upper()) + } + else: + tp_workhours_id = '%s_%s_%s' % ( config['timeperiods']['workhours']['name'], + args.country, + states[args.country][args.state]["ascii"] ) + tp_workhours_title = '%s %s %s' % ( config['timeperiods']['workhours']['title'], + args.country.upper(), + states[args.country][args.state]["name"] ) + tp_oncall_id = '%s_%s_%s' % ( config['timeperiods']['oncall']['name'], + args.country, + states[args.country][args.state]["ascii"] ) + tp_oncall_title = '%s %s %s' % ( config['timeperiods']['oncall']['title'], + args.country.upper(), + states[args.country][args.state]["name"] ) + tag = { + 'id': "%s_%s_%s" % (config['taggroup']['name'], args.country, states[args.country][args.state]["ascii"]), + 'title': "%s %s %s" % (config['taggroup']['title'], args.country.upper(), states[args.country][args.state]["name"]) + } + + + tp, etag = cmk.create_timeperiod(tp_workhours_id, + tp_workhours_title, config['workdays']) if args.debug: + print("created time period") pprint(tp) - - tp, etag = cmk.create_timeperiod('%s_%s_%s' % ( config['timeperiods']['oncall']['name'], - args.country, - args.state ), - '%s %s %s' % ( config['timeperiods']['oncall']['title'], - args.country.upper(), - states[args.country][args.state] ), + + tp, etag = cmk.create_timeperiod(tp_oncall_id, + tp_oncall_title, always, - exclude=[workhoursname]) + exclude=[tp_workhours_id]) if args.debug: + print("created time period") 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']) except: pass - if tg: - tags = tg['extensions']['tags'] - - tags.append({'id': "%s_%s_%s" % (config['taggroup']['name'], args.country, args.state), - 'title': "%s %s %s" % (config['taggroup']['title'], args.country.upper(), states[args.country][args.state])}) - - cmk.edit_host_tag_group(config['taggroup']['name'], etag, tags=tags) - - else: + if not tg: tg, etag = cmk.create_host_tag_group( config['taggroup']['name'], config['taggroup']['title'], [ {'title': config['taggroup']['empty_title']}, - {'id': "%s_%s_%s" % (config['taggroup']['name'], - args.country, - args.state), - 'title': "%s %s %s" % (config['taggroup']['title'], - args.country.upper(), - states[args.country][args.state])} + tag, ], topic = config['taggroup'].get('topic'), - help = config['taggroup'].get('help') + help = config['taggroup'].get('help', created_by), ) + else: + tags = tg['extensions']['tags'] - # nrs, etag = cmk.get_notification_rules() + tags.append(tag) - # if args.debug: - # pprint(nrs) + cmk.edit_host_tag_group(config['taggroup']['name'], etag, tags=tags) + + rule_config = { + 'conditions': { + 'match_host_tags': { + 'state': 'enabled', + 'value': [{ + 'operator': 'is', + 'tag_group_id': config['taggroup']['name'], + 'tag_id': tag['id'], + 'tag_type': 'tag_group', + }], + }, + 'match_only_during_time_period': { + 'state': 'enabled', + 'value': tp_oncall_id, + }, + 'event_console_alerts': {'state': 'disabled'}, + 'match_check_types': {'state': 'disabled'}, + 'match_contact_groups': {'state': 'disabled'}, + 'match_exclude_hosts': {'state': 'disabled'}, + 'match_exclude_service_groups': {'state': 'disabled'}, + 'match_exclude_service_groups_regex': {'state': 'disabled'}, + 'match_exclude_services': {'state': 'disabled'}, + 'match_folder': {'state': 'disabled'}, + 'match_host_event_type': {'state': 'disabled'}, + 'match_host_groups': {'state': 'disabled'}, + 'match_host_labels': {'state': 'disabled'}, + 'match_hosts': {'state': 'disabled'}, + 'match_notification_comment': {'state': 'disabled'}, + 'match_plugin_output': {'state': 'disabled'}, + 'match_service_event_type': {'state': 'disabled'}, + 'match_service_groups': {'state': 'disabled'}, + 'match_service_groups_regex': {'state': 'disabled'}, + 'match_service_labels': {'state': 'disabled'}, + 'match_service_levels': {'state': 'disabled'}, + 'match_services': {'state': 'disabled'}, + 'match_sites': {'state': 'disabled'}, + 'restrict_to_notification_numbers': {'state': 'disabled'}, + 'throttle_periodic_notifications': {'state': 'disabled'}, + }, + 'contact_selection': { + 'all_contacts_of_the_notified_object': {'state': 'disabled'}, + 'restrict_by_contact_groups': {'state': 'disabled'}, + 'all_users': {'state': 'disabled'}, + 'all_users_with_an_email_address': {'state': 'disabled'}, + 'explicit_email_addresses': {'state': 'disabled'}, + 'members_of_contact_groups': {'state': 'disabled'}, + 'restrict_by_custom_macros': {'state': 'disabled'}, + 'the_following_users': {'state': 'disabled'}, + }, + 'notification_method': { + 'notification_bulking': {'state': 'disabled'}, + }, + 'rule_properties': { + 'allow_users_to_deactivate': {'state': 'disabled'}, + 'comment': created_by, + 'description': tp_oncall_title, + 'do_not_apply_this_rule': {'state': 'disabled'}, + 'documentation_url': '', + }, + } + + for key1, value1 in config["notification_rule_config"].items(): + if key1 in rule_config: + for key2, value2 in value1.items(): + rule_config[key1][key2] = value2 + else: + rule_config[key1] = value1 + + nr, etag = cmk.create_notification_rule(rule_config) + if args.debug: + print("created notification rule:") + pprint(nr) - # for nr in nrs['value']: - - cmk.activate() if args.func == 'add_auto_holidays': @@ -300,34 +426,41 @@ def add_holiday_timeperiod(country=default_country, state=None, all_states=False 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) + try: + _, country, state = tp['id'].split('_') + add_holiday_timeperiod(country=country, state=states_orig[country][state], current_year=args.current_year, set_year=args.year) + except ValueError: + _, country = tp['id'].split('_') + add_holiday_timeperiod(country=country, state=None, current_year=args.current_year, set_year=args.year) changes = True if changes: cmk.activate() if args.func == "cleanup": - tps, etag = cmk.get_timeperiods() - - if args.debug: - pprint(tps) + nrs, etag = cmk.get_all_notification_rules() + for nr in nrs['value']: + if nr['extensions']['rule_config']['rule_properties']['comment'] == created_by: + if args.debug: + print(f"removing notification rule {nr['id']}") + cmk.delete_notification_rule(nr['id']) changes = False - + + tps, etag = cmk.get_timeperiods() for typ in ['oncall', 'workhours', 'holidays']: # order is important for tp in tps['value']: if tp['id'].startswith(config['timeperiods'][typ]['name']): if args.debug: - print(f"removing {tp['id']}") + print(f"removing time period {tp['id']}") cmk.delete_timeperiod(tp['id'], '*') changes = True try: cmk.delete_host_tag_group(config['taggroup']['name']) - if args.debug: - print(f"remove host tag group {config['taggroup']['name']}") changes = True + if args.debug: + print(f"removed host tag group {config['taggroup']['name']}") except: pass diff --git a/holidays/holidays-2.1.3.mkp b/holidays/holidays-2.1.3.mkp deleted file mode 100644 index 40f58a43fc1ca0dbdcd96afd1c3394f3a54d5e7a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4546 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 diff --git a/holidays/holidays-2.2.0.mkp b/holidays/holidays-2.2.0.mkp new file mode 100644 index 0000000000000000000000000000000000000000..55b3c22875f84afd8e517fa03e2edc50642883d7 GIT binary patch literal 5704 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 cjy`8}%$IQg|D?