From 2e8733a1b356833c660a61dd985bf98673136718 Mon Sep 17 00:00:00 2001 From: cmk-bonobo Date: Sat, 5 Apr 2025 11:34:44 +0200 Subject: [PATCH] initial commit source files --- hal9002/agent_based/hal9002_status.py | 80 +++++ hal9002/agent_based/hal9002_storages.py | 162 ++++++++++ hal9002/agent_based/hal9002_users.py | 107 +++++++ hal9002/checkman/hal9002_status | 12 + hal9002/checkman/hal9002_storages | 13 + hal9002/checkman/hal9002_users | 13 + hal9002/graphing/graph_hal9002.py | 97 ++++++ hal9002/libexec/agent_hal9002 | 349 +++++++++++++++++++++ hal9002/rulesets/rs_hal9002_params.py | 58 ++++ hal9002/rulesets/rs_hal9002_status.py | 48 +++ hal9002/rulesets/rs_hal9002_storages.py | 47 +++ hal9002/rulesets/rs_hal9002_users.py | 47 +++ hal9002/server_side_calls/agent_hal9002.py | 42 +++ 13 files changed, 1075 insertions(+) create mode 100644 hal9002/agent_based/hal9002_status.py create mode 100644 hal9002/agent_based/hal9002_storages.py create mode 100644 hal9002/agent_based/hal9002_users.py create mode 100644 hal9002/checkman/hal9002_status create mode 100644 hal9002/checkman/hal9002_storages create mode 100644 hal9002/checkman/hal9002_users create mode 100644 hal9002/graphing/graph_hal9002.py create mode 100755 hal9002/libexec/agent_hal9002 create mode 100644 hal9002/rulesets/rs_hal9002_params.py create mode 100644 hal9002/rulesets/rs_hal9002_status.py create mode 100644 hal9002/rulesets/rs_hal9002_storages.py create mode 100644 hal9002/rulesets/rs_hal9002_users.py create mode 100644 hal9002/server_side_calls/agent_hal9002.py diff --git a/hal9002/agent_based/hal9002_status.py b/hal9002/agent_based/hal9002_status.py new file mode 100644 index 0000000..43c40cc --- /dev/null +++ b/hal9002/agent_based/hal9002_status.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +# pylint: disable=missing-module-docstring, unused-argument, missing-function-docstring + +# import necessary elements from API version 2 +from cmk.agent_based.v2 import ( + AgentSection, + CheckPlugin, + Service, + Result, + State, +) + + +def parse_hal9002_status(string_table): + """the parse function""" + parsed_data = {} + # we only expect one line with four entries + line = string_table[0] + parsed_data["status"] = line[0] + parsed_data["version"] = line[1] + parsed_data["username"] = line[2] + parsed_data["ipaddress"] = line[3] + return parsed_data + + +def discover_hal9002_status(section): + """the discover function""" + yield Service() + + +def check_hal9002_status(params, section): + """the check function""" + # print(params["state_if_update_is_available"]) + status = section["status"] + version = section["version"] + username = section["username"] + ipaddress = section["ipaddress"] + # check what status is reported and set state accordingly + if status == "ok": + state = State.OK + elif status == "update available": + state = params["state_if_update_is_available"] + elif status == "storage down": + state = State.CRIT + else: + state = State.UNKNOWN + summary = f'Status is "{status}", Version is "{version}"' + details = f'User/IP used for access is "{username}/{ipaddress}"' + # no metrics, just a simple service + yield Result(state=state, summary=summary, details=details) + + +# create the new agent section, must begin with "agent_section_" +# and must be an instance of "AgentSection" +agent_section_hal9002_status = AgentSection( + # "name" must exactly match the section name within the agent output + name="hal9002_status", + # define the parse function, name is arbitrary, a good choice is to choose + # "parse_" as prefix and append the section name + parse_function=parse_hal9002_status, +) + +# create the new check plugin, must begin with "check_plugin_" +# and must be an instance of "CheckPlugin" +check_plugin_hal9002_status = CheckPlugin( + # "name" should be the same as the corresponding section within the agent output + name="hal9002_status", + service_name="HAL9002 Status", + # define the discovery function, name is arbitrary, a good choice is to choose + # "discover_" as prefix and append the section name + discovery_function=discover_hal9002_status, + # define the check function, name is arbitrary, a good choice is to choose + # "check_" as prefix and append the section name + check_function=check_hal9002_status, + # define the default parameters + check_default_parameters={"state_if_update_is_available": State.WARN}, + # connect to the ruleset where parameters can be defined + # must match the name of the ruleset exactly + check_ruleset_name="hal9002_status", +) diff --git a/hal9002/agent_based/hal9002_storages.py b/hal9002/agent_based/hal9002_storages.py new file mode 100644 index 0000000..ba073f5 --- /dev/null +++ b/hal9002/agent_based/hal9002_storages.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +# pylint: disable=too-many-locals +"""HAL9002 check for storages""" + +import time + +# import necessary elements from API version 2 +from cmk.agent_based.v2 import ( + AgentSection, + CheckPlugin, + Service, + Result, + State, + render, + get_value_store, + check_levels, +) + + +def parse_hal9002_storages(string_table): + """the parse function""" + parsed_data = {} + # convert the raw output of the agent section in an meaningful structure + # do type conversions and so on + parsed_data = {} + for line in string_table: + storageid = line[0] + realname = line[1] + upload = int(float(line[2])) + download = int(float(line[3])) + parsed_data[f"{storageid}"] = [realname, upload, download] + return parsed_data + + +def discover_hal9002_storages(section): + """the discover function""" + # since we have a service with item here we must create one service per item + for key in section: + yield Service(item=key) + + +def check_hal9002_storages(item, params, section): + """the check function""" + curr_time = time.time() # needed for calculating rates + attr = section.get(item) + if not attr: + # if a previously found storage does not exist anymore, create a meaningful result + yield Result( + state=State.UNKNOWN, + summary="Storage not found anymore, maybe it was deleted?", + ) + return + + # get all info of the storage + storage_id = item + realname = attr[0] + ul_bytes = attr[1] + dl_bytes = attr[2] + + # these are the keys for our value_store + vs_key_ul = f"hal9002_{storage_id}_ul" + vs_key_dl = f"hal9002_{storage_id}_dl" + vs_key_time = f"hal9002_{storage_id}_time" + + # we have to persist our upload/download values so that we can calculate rates + value_store = get_value_store() + # get the values from last run + ul_last = value_store.get(vs_key_ul, 0) + dl_last = value_store.get(vs_key_dl, 0) + time_last = value_store.get(vs_key_time, 0) + # store the new values + value_store[vs_key_ul] = ul_bytes + value_store[vs_key_dl] = dl_bytes + value_store[vs_key_time] = curr_time + + # now calculate the rates + time_delta = curr_time - time_last + # avoid dividing by null and getting negative rates + if time_delta <= 0: + # should not happen at all :-) + ul_rate = 1024.0 + dl_rate = 2048.0 + else: + ul_rate = (ul_bytes - ul_last) / time_delta + dl_rate = (dl_bytes - dl_last) / time_delta + + diff_ul = ul_bytes - ul_last + diff_dl = dl_bytes - dl_last + + # levels are defined as MBytes/s in our rule, so we have to adjust this here + # because we are getting Bytes/s from our rate calculation + level_type, levels = params["levels_hal_storages_rates"] + warn, crit = levels + warn = warn * (1024**2) + crit = crit * (1024**2) + levels = (level_type, (warn, crit)) + + # create graphs and results for upload/download rates + # this replaces all calls of yield Result|Metric + # metric_name must be used within the graphing templates + yield from check_levels( + value=ul_rate, + levels_upper=levels, + metric_name="hal_storage_upload_rate", + render_func=render.iobandwidth, + label=f"Transfer rates of {realname} - Up", + notice_only=False, + ) + yield from check_levels( + value=dl_rate, + levels_upper=levels, + metric_name="hal_storage_download_rate", + render_func=render.iobandwidth, + label="Down", + notice_only=False, + ) + # these appear only in the details section + yield from check_levels( + value=diff_ul, + metric_name="hal_storage_last_upload", + render_func=render.bytes, + label="Upload since last check: ", + notice_only=True, + ) + yield from check_levels( + value=diff_dl, + metric_name="hal_storage_last_download", + render_func=render.bytes, + label="Download since last check: ", + notice_only=True, + ) + + +# create the new agent section, must begin with "agent_section_" +# and must be an instance of "AgentSection" +agent_section_hal9002_storages = AgentSection( + # "name" must exactly match the section name within the agent output + name="hal9002_storages", + # define the parse function, name is arbitrary, a good choice is to choose + # "parse_" as prefix and append the section name + parse_function=parse_hal9002_storages, +) + +# create the new check plugin, must begin with "check_plugin_" +# and must be an instance of "CheckPlugin" +check_plugin_hal9002_storages = CheckPlugin( + # "name" should be the same as the corresponding section within the agent output + name="hal9002_storages", + # this is a service with item, so you have to include a place holder for the item id + service_name="HAL9002 storage %s", + # define the discovery function, name is arbitrary, a good choice is to choose + # "discover_" as prefix and append the section name + discovery_function=discover_hal9002_storages, + # define the check function, name is arbitrary, a good choice is to choose + # "check_" as prefix and append the section name + check_function=check_hal9002_storages, + # define the default parameters + check_default_parameters={"levels_hal_storages_rates": ("fixed", (200.0, 300.0))}, + # connect to the ruleset where parameters can be defined + # must match the name of the ruleset exactly + check_ruleset_name="hal9002_storages", +) diff --git a/hal9002/agent_based/hal9002_users.py b/hal9002/agent_based/hal9002_users.py new file mode 100644 index 0000000..928c13c --- /dev/null +++ b/hal9002/agent_based/hal9002_users.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +# pylint: disable=line-too-long +"""HAL9002 checks for users""" + +# import necessary elements from API version 2 +from cmk.agent_based.v2 import ( + AgentSection, + CheckPlugin, + Service, + Result, + State, + render, + check_levels, +) + + +def parse_hal9002_users(string_table): + """the parse function""" + parsed_data = {} + # convert the raw output of the agent section in an meaningful structure + # do type conversions and so on + parsed_data = {} + for line in string_table: + userid = line[0] + realname = line[1] + quota_used = float(line[2]) + quota_total = float(line[3]) + parsed_data[f"{userid}"] = [realname, quota_used, quota_total] + return parsed_data + + +def discover_hal9002_users(section): + """the discover function""" + # since we have a service with item here we must create one service per item + for key in section: + yield Service(item=key) + + +def check_hal9002_users(item, params, section): + """the check function""" + attr = section.get(item) + if not attr: + # if a previously found user does not exist anymore, create a meaningful result + yield Result( + state=State.UNKNOWN, summary="User not found anymore, maybe it was deleted?" + ) + return + # this refers to the definition of the parameters + # within the appropriate ruleset dictionary element + levels = params["quota_used_percent"] + + # get all info of the user + user_name = attr[0] + used_bytes = attr[1] + quota_max_bytes = attr[2] + # calculate percentage of quota used + quota_used_percent = used_bytes * 100 / quota_max_bytes + + # create graphs and results for used quota + # this replaces all calls of yield Result|Metric + # metric_name must be used within the graphing templates + yield from check_levels( + value=quota_used_percent, + levels_upper=levels, + metric_name="hal_user_quota_used_percent", + render_func=render.percent, + label=f"Used quota of {user_name}", + notice_only=False, + ) + yield from check_levels( + value=used_bytes, + metric_name="hal_user_used_bytes", + render_func=render.bytes, + label=f"Used from {render.disksize(quota_max_bytes)}", + notice_only=False, + ) + + +# create the new agent section, must begin with "agent_section_" +# and must be an instance of "AgentSection" +agent_section_hal9002_users = AgentSection( + # "name" must exactly match the section name within the agent output + name="hal9002_users", + # define the parse function, name is arbitrary, a good choice is to choose + # "parse_" as prefix and append the section name + parse_function=parse_hal9002_users, +) + +# create the new check plugin, must begin with "check_plugin_" +# and must be an instance of "CheckPlugin" +check_plugin_hal9002_users = CheckPlugin( + # "name" should be the same as the corresponding section within the agent output + name="hal9002_users", + # this is a service with item, so you have to include a place holder for the item id + service_name="HAL9002 user %s", + # define the discovery function, name is arbitrary, a good choice is to choose + # "discover_" as prefix and append the section name + discovery_function=discover_hal9002_users, + # define the check function, name is arbitrary, a good choice is to choose + # "check_" as prefix and append the section name + check_function=check_hal9002_users, + # define the default parameters + check_default_parameters={"quota_used_percent": ("fixed", (80.0, 90.0))}, + # connect to the ruleset where parameters can be defined + # must match the name of the ruleset exactly + check_ruleset_name="hal9002_users", +) diff --git a/hal9002/checkman/hal9002_status b/hal9002/checkman/hal9002_status new file mode 100644 index 0000000..03bf6a2 --- /dev/null +++ b/hal9002/checkman/hal9002_status @@ -0,0 +1,12 @@ +title: HAL9002: Overall Status +agents: linux +catalog: unsorted +license: GPL +distribution: check_mk +description: + Show info about HAL9002 storage systems, e.g. overall status and version + The check will raise WARN/CRIT if not "ok", dependig on the reported internal state +item: + none +inventory: + one service with overall information \ No newline at end of file diff --git a/hal9002/checkman/hal9002_storages b/hal9002/checkman/hal9002_storages new file mode 100644 index 0000000..ce0e1ec --- /dev/null +++ b/hal9002/checkman/hal9002_storages @@ -0,0 +1,13 @@ +title: HAL9002: Storage Upload/Download Rate +agents: linux +catalog: unsorted +license: GPL +distribution: check_mk +description: + Shows the upload/download rate of storages in bytes/s + The check will raise WARN/CRIT if rates are above the levels which can be defined + with the appropriate rule set. +item: + storageid +inventory: + one service is created for each storage \ No newline at end of file diff --git a/hal9002/checkman/hal9002_users b/hal9002/checkman/hal9002_users new file mode 100644 index 0000000..677b2d2 --- /dev/null +++ b/hal9002/checkman/hal9002_users @@ -0,0 +1,13 @@ +title: HAL9001: Quota Usage of Users +agents: linux +catalog: unsorted +license: GPL +distribution: check_mk +description: + Shows the usage of storage quota used by each user in percent of the maximum allowed quota + The check will raise WARN/CRIT if usage is above the levels which can be defined + with the apprppriate rule set. +item: + userid +inventory: + one service is created for each user \ No newline at end of file diff --git a/hal9002/graphing/graph_hal9002.py b/hal9002/graphing/graph_hal9002.py new file mode 100644 index 0000000..d381d9b --- /dev/null +++ b/hal9002/graphing/graph_hal9002.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +"""graphing and metrics definitions""" + +from cmk.graphing.v1 import Title +from cmk.graphing.v1.graphs import Graph, MinimalRange +from cmk.graphing.v1.metrics import Color, DecimalNotation, Metric, Unit, SINotation +from cmk.graphing.v1.perfometers import ( + Open, + Closed, + FocusRange, + Perfometer, + Bidirectional, +) + + +## Metrics +# metrics must begin with "metric_" and be an instance of "Metric" +metric_hal9002_user_quota_used = Metric( + # "name" must be exactly the "metric_name" within the check function + name="hal_user_quota_used_percent", + title=Title("Percentage of storage quota used"), + unit=Unit(DecimalNotation("%")), + color=Color.DARK_ORANGE, +) + +metric_hal9002_user_bytes_used = Metric( + # "name" must be exactly the "metric_name" within the check function + name="hal_user_used_bytes", + title=Title("Used storage"), + unit=Unit(SINotation("bytes")), + color=Color.DARK_CYAN, +) + +metric_hal9002_storage_upload_rate = Metric( + # "name" must be exactly the "metric_name" within the check function + name="hal_storage_upload_rate", + title=Title("Storage upload rate"), + unit=Unit(SINotation("bytes/s")), + color=Color.DARK_GREEN, +) + +metric_hal9002_storage_download_rate = Metric( + # "name" must be exactly the "metric_name" within the check function + name="hal_storage_download_rate", + title=Title("Storage download rate"), + unit=Unit(SINotation("bytes/s")), + color=Color.LIGHT_GREEN, +) + +metric_hal9002_storage_upload_last = Metric( + # "name" must be exactly the "metric_name" within the check function + name="hal_storage_last_upload", + title=Title("Upload since last check"), + unit=Unit(SINotation("bytes")), + color=Color.DARK_BLUE, +) + +metric_hal9002_storage_download_last = Metric( + # "name" must be exactly the "metric_name" within the check function + name="hal_storage_last_download", + title=Title("Download since last check"), + unit=Unit(SINotation("bytes")), + color=Color.LIGHT_BLUE, +) + +## Perfometers +# perfometers must begin with "perfometer_" and be an instance of "Perfometer" +perfometer_hal9002_users_quota_used = Perfometer( + name="hal9002_users_quota_used", + focus_range=FocusRange(Closed(0), Closed(100)), + # "segments" must be exactly the name of the metric + segments=["hal_user_quota_used_percent"], +) + +perfometer_hal9002_storage_rates = Bidirectional( + name="hal9002_storage_rates", + left=Perfometer( + name="hal9002_storage_download_rate", + focus_range=FocusRange(Open(0.0), Open(500.0)), + segments=["hal_storage_download_rate"], + ), + right=Perfometer( + name="hal9002_storage_upload_rate", + focus_range=FocusRange(Open(0.0), Open(500.0)), + segments=["hal_storage_upload_rate"], + ), +) + +## Combined Graphs +# combined graphs must begin with "graph_" and be an instance of "Graph" +graph_hal9002_storages_upload_download = Graph( + name="hal9002_storage_upload_download", + title=Title("Storage upload/download rate"), + # names here refer to the metric names above + simple_lines=["hal_storage_download_rate", "hal_storage_upload_rate"], + minimal_range=MinimalRange(0, 100), +) diff --git a/hal9002/libexec/agent_hal9002 b/hal9002/libexec/agent_hal9002 new file mode 100755 index 0000000..5525efb --- /dev/null +++ b/hal9002/libexec/agent_hal9002 @@ -0,0 +1,349 @@ +#!/usr/bin/env python3 +# pylint: disable=pointless-string-statement +"""HAL9002 special agent""" + +import sys +import os +import getopt +import random + +""" +Sample output + +<<>> +ok;SK-1st-Edition 1965-12-17;arthur;127.0.0.1 + + 1. Status, possible values: ok, update available, storage down (String) + 2. Version (String) + 3. Username used for login (String) + 4. IP used for access (String) + 5. Desired state, id an update is available (String) + +<<>> +user1;David Bowman;65536.0;262144.0 +user2;Frank Poole;16384.0;131072.0 +user3;Elena;32768.0;65536.0 + + 1. User ID (string) + 2. User Real Name (string) + 3. Quota Used (float in bytes, absolut) + 4. Quota Max (float in bytes, absolut) + +<<>> +storage1;DATA_Dullea;8192.0;262144.0 +storage2;DATA_Poole;2048.0;131072.0 + + 1. Storage ID (string) + 2. Storage Custom Name (string) + 3. Upload (float in bytes, counter) + 4. Download (float in bytes, counter) +""" + + +def show_usage() -> None: + """shows help for usage""" + sys.stderr.write( + """HAL9002 Special Agent +USAGE: agent_hal9002 -H [hostname] -u [username] -p [password] + agent_hal9002 -h +OPTIONS: + -H, --hostname Hostname (FQDN or IP) of HAL System + -u, --username Username + -p, --password Password + -s, --state-if-update-is-available State of service if an update is available (0/1/2/3) + -h, --help Show this help message and exit +""" + ) + + +SEP: str = "|" +TIMEFMT: str = "%Y-%m-%d %H:%M:%S" +FLOATFMT: str = "{:.4f}" + +# Parameters coming from CheckMK +opt_hostname: str = "" +opt_username: str = "" +opt_password: str = "" +opt_state_if_update_is_available: str = "" + +# Base values for randomly calculating the use of the storage system per user +base_usage: float = float(65536 * 8) # 0.5 MByte +max_quota: float = 1073741824.0 # 1 GByte + +# these dicts are used as starting points, feel free to extend and/or adjust them +# each user/storage results in an additional service +users_dict: dict[str, str] = { + "user1": { + "realname": "David Bowman", + "usage": base_usage * 2, + "quota_max": max_quota, + }, + "user2": { + "realname": "Frank Poole", + "usage": base_usage * 3, + "quota_max": max_quota / 2, + }, + "user3": {"realname": "Elena", "usage": base_usage * 4, "quota_max": max_quota * 2}, +} + +storages_dict: dict[str, str] = { + "storage1": { + "realname": "DATA_Dullea", + "download": float(1024**3), + "upload": float((1024**2) * 6), + }, + "storage2": { + "realname": "DATA_Poole", + "download": float(1024**3), + "upload": float((1024**2) * 4), + }, +} + +hal_status_list: list[str] = ["ok", "update available", "storage down"] +hal_version: str = "SK-1st-Edition 1965-12-17" + +short_options: str = "hH:u:p:s:" +long_options: list[str] = [ + "hostname=", + "username=", + "password=", + "state-if-update-is-available=", + "help", +] + + +def get_options() -> None: + """retrieves all command line options""" + global opt_hostname + global opt_username + global opt_password + global opt_state_if_update_is_available + + opts, _ = getopt.getopt(sys.argv[1:], short_options, long_options) + for opt, arg in opts: + if opt in ["-H", "--hostname"]: + opt_hostname = arg + if opt in ["-u", "--username"]: + opt_username = arg + elif opt in ["-p", "--password"]: + opt_password = arg + elif opt in ["-s", "--state-if-update-is-available"]: + opt_state_if_update_is_available = arg + elif opt in ["-h", "--help"]: + show_usage() + sys.exit(0) + + +def show_options() -> None: + """show the options from the command line""" + print(f"Hostname: {opt_hostname}") + print(f"Username: {opt_username}") + print(f"Password: {opt_password}") + print(f"State if update is available: {opt_state_if_update_is_available}") + + +def calculate_new_user_storage(current_storage: float) -> float: + """calculates new values for the user's storage""" + # let the chance that no change occured be at 90% + no_change: int = random.randint(1, 100) + if no_change > 10: + return float(current_storage) + else: + # create some variance, value of "amount" is interpreted in bytes + factor: int = random.randint(1, 4) + amount: int = random.randint(1024000, 4096000) + amount *= factor + # increase or decrease, give increase a better chance (60:40), that's more real :-) + plus_or_minus: int = random.randint(1, 100) + if plus_or_minus > 40: + new_storage: int = current_storage + amount + else: + new_storage: int = current_storage - amount + # avoid negative values + if new_storage < 0: + new_storage = 1024000 + return float(new_storage) + + +def calculate_new_storage_counters( + ul_bytes: float, dl_bytes: float +) -> tuple[float, float]: + """calculates new values for the user's storage counters""" + # let the chance that no change occured be at 2% + no_change: int = random.randint(1, 100) + if no_change > 98: + return float(ul_bytes), float(dl_bytes) + # create some variance, values of "amount_" is interpreted in bytes + factor_ul: int = random.randint(1, 7) + amount_ul: int = random.randint(20240000, 50960000) + amount_ul *= factor_ul + factor_dl: int = random.randint(1, 14) + amount_dl: int = random.randint(30240000, 60960000) + amount_dl *= factor_dl + # we are simulating counters, so only increasing makes sense + new_ul_bytes: int = ul_bytes + amount_ul + new_dl_bytes: int = dl_bytes + amount_dl + return float(new_ul_bytes), float(new_dl_bytes) + + +def do_cmk_hal_status_output( + status: str, + version: str, + username: str, + ipaddress: str, + state_if_update_is_available: str, +) -> None: + """prints out the section for status""" + print("<<>>") + print(f"{status};{version};{username};{ipaddress};{state_if_update_is_available}") + + +def do_cmk_hal_users_output(users: dict, hostname: str) -> None: + """prints out the section for users""" + print("<<>>") + current_storage: float = 0.0 + home_path: str = os.getenv("HOME") + tmp_path: str = f"{home_path}/tmp" + for user in users: + # we need a way to store the current value for storage usage between runs of the agent + help_file: str = f"{tmp_path}/hal9002_{hostname}_{user}_current_storage.txt" + if os.path.exists(help_file): + # help file exists, so get the content + with open(help_file, "r", encoding="utf-8") as file: + current_storage = float(file.read()) + else: + # help file does not exist, create it and store the start value in it + with open(help_file, "w", encoding="utf-8") as file: + file.write(str(users[user]["usage"])) + current_storage = users[user]["usage"] + realname: str = users[user]["realname"] + quota_max: str = users[user]["quota_max"] + # simulate changes in storage usage + new_storage: float = calculate_new_user_storage(current_storage) + # save the new value in help file + with open(help_file, "w", encoding="utf-8") as file: + file.write(str(new_storage)) + # create output + print(f"{user};{realname};{new_storage};{quota_max}") + + +def do_cmk_hal_storages_output(storages: dict, hostname: str) -> None: + """prints out the section for storages""" + print("<<>>") + current_dl_bytes: float = 0.0 + current_ul_bytes: float = 0.0 + new_dl_bytes: float = 0.0 + new_ul_bytes: float = 0.0 + home_path: str = os.getenv("HOME") + tmp_path: str = f"{home_path}/tmp" + for storage in storages: + # we need a way to store the current values for storage usage between runs of the agent + # so we can simulate counters for uploaded/downloaded bytes + help_file_ul: str = f"{tmp_path}/hal9002_{hostname}_{storage}_ul_bytes.txt" + help_file_dl: str = f"{tmp_path}/hal9002_{hostname}_{storage}_dl_bytes.txt" + if os.path.exists(help_file_ul): + # help file exists, so get the content + with open(help_file_ul, "r", encoding="utf-8") as file: + current_ul_bytes = float(file.read()) + else: + # help file does not exist, create it and store the start value in it + with open(help_file_ul, "w", encoding="utf-8") as file: + file.write(str(storages[storage]["upload"])) + current_ul_bytes = storages[storage]["upload"] + if os.path.exists(help_file_dl): + # help file exists, so get the content + with open(help_file_dl, "r", encoding="utf-8") as file: + current_dl_bytes = float(file.read()) + else: + # help file does not exist, create it and store the start value in it + with open(help_file_dl, "w", encoding="utf-8") as file: + file.write(str(storages[storage]["download"])) + current_dl_bytes = storages[storage]["download"] + realname: str = storages[storage]["realname"] + # simulate changes in storage usage + new_ul_bytes, new_dl_bytes = calculate_new_storage_counters( + current_ul_bytes, current_dl_bytes + ) + # save the new values in help files + with open(help_file_ul, "w", encoding="utf-8") as file: + file.write(str(new_ul_bytes)) + with open(help_file_dl, "w", encoding="utf-8") as file: + file.write(str(new_dl_bytes)) + # create output + print(f"{storage};{realname};{new_ul_bytes};{new_dl_bytes}") + + +def get_status() -> tuple[str, str]: + """returns status and version""" + # randomly set one of the three available states + hal_status: str = "" + status_index: int = random.randint(1, 100) + if status_index <= 5: + # should result in critical state + hal_status = hal_status_list[2] + elif status_index <= 15: + # should result in warning state + hal_status = hal_status_list[1] + else: + # should result in ok state + hal_status = hal_status_list[0] + return hal_status, hal_version + + +def do_login(_hostname: str, _username: str, _password: str) -> tuple[bool, int]: + """returns whetjer login was successful or not and a status code""" + # simulate the login to our HAL system + # give it a chance of 2% to fail to demonstrate an error from time to time + success: int = random.randint(1, 100) + if success > 2: + return True, 200 + return False, 401 + + +def main(): + """main function""" + success: bool = False + code: int = -1 + status: str = "no status" + version: str = "no version" + get_options() + # show_options() + # some checks + # keep in mind: the 1st line printed out to std.err will be shown as check result in CheckMK + if opt_hostname == "": + sys.stderr.write("No hostname given.\n") + show_usage() + sys.exit(1) + if opt_username == "": + sys.stderr.write("No username given.\n") + show_usage() + sys.exit(1) + if opt_password == "": + sys.stderr.write("No password given.\n") + show_usage() + sys.exit(1) + # if opt_state_if_update_is_available == "": + # sys.stderr.write("No state given when update is available.\n") + # show_usage() + # sys.exit(1) + success, code = do_login(opt_hostname, opt_username, opt_password) + if success: + status, version = get_status() + do_cmk_hal_status_output( + status, + version, + opt_username, + opt_hostname, + opt_state_if_update_is_available, + ) + do_cmk_hal_users_output(users_dict, opt_hostname) + do_cmk_hal_storages_output(storages_dict, opt_hostname) + else: + sys.stderr.write( + f"Login to {opt_hostname} with user {opt_username} failed with error code {code}\n" + ) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/hal9002/rulesets/rs_hal9002_params.py b/hal9002/rulesets/rs_hal9002_params.py new file mode 100644 index 0000000..843dd61 --- /dev/null +++ b/hal9002/rulesets/rs_hal9002_params.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# pylint: disable=line-too-long +"""defines the form for typing in all needed HAL9002 parameters""" + +from cmk.rulesets.v1 import Help, Title +from cmk.rulesets.v1.form_specs import ( + DictElement, + Dictionary, + migrate_to_password, + Password, + String, + validators, +) +from cmk.rulesets.v1.rule_specs import SpecialAgent, Topic + + +def _form_spec_special_agent_hal9002() -> Dictionary: + return Dictionary( + title=Title("HAL 9002 Server Information"), + help_text=Help("Checking HAL servers via API"), + elements={ + "hostname": DictElement( + required=True, + parameter_form=String( + title=Title("Hostname"), + help_text=Help( + "Hostname of HAL server (bare FQDN or IP), mandatory, eg. hal9002.yourdomain.tld" + ), + custom_validate=(validators.LengthInRange(min_value=1),), + ), + ), + "username": DictElement( + required=True, + parameter_form=String( + title=Title("Username"), + help_text=Help("Username with administrative rights, mandatory"), + custom_validate=(validators.LengthInRange(min_value=1),), + ), + ), + "password": DictElement( + required=True, + parameter_form=Password( + title=Title("Password"), + help_text=Help("Specify password, mandatory"), + custom_validate=(validators.LengthInRange(min_value=1),), + migrate=migrate_to_password, + ), + ), + }, + ) + + +rule_spec_hal9002 = SpecialAgent( + name="hal9002", + title=Title("HAL9002 connection parameters"), + topic=Topic.APPLICATIONS, + parameter_form=_form_spec_special_agent_hal9002, +) diff --git a/hal9002/rulesets/rs_hal9002_status.py b/hal9002/rulesets/rs_hal9002_status.py new file mode 100644 index 0000000..fc2fcf6 --- /dev/null +++ b/hal9002/rulesets/rs_hal9002_status.py @@ -0,0 +1,48 @@ +#!/user/bin/env python3 +"""general parameter form for HAL9002""" + +from cmk.rulesets.v1 import Help, Title +from cmk.rulesets.v1.form_specs import ( + DictElement, + Dictionary, + ServiceState, + DefaultValue, +) +from cmk.agent_based.v2 import ( + State, +) + +from cmk.rulesets.v1.rule_specs import CheckParameters, Topic, HostCondition + + +# function name should begin with an underscore to limit it's visibility +def _parameter_form(): + return Dictionary( + elements={ + "state_if_update_is_available": DictElement( + required=True, + parameter_form=ServiceState( + title=Title("State if update is available"), + help_text=Help( + "State of service if an update is available, mandatory" + ), + prefill=DefaultValue(value=State.WARN), + ), + ), + } + ) + + +# name must begin with "rule_spec_", should refer to the used check plugin +# must be an instance of "CheckParameters" +rule_spec_hal9002_status = CheckParameters( + # "name" should be the same as the check plugin + name="hal9002_status", + # the title is shown in the GUI + title=Title("HAL9002 general parameters"), + # this ruleset can be found under Setup|Service monitoring rules|Applications... + topic=Topic.APPLICATIONS, + # define the name of the function which creates the GUI elements + parameter_form=_parameter_form, + condition=HostCondition(), +) diff --git a/hal9002/rulesets/rs_hal9002_storages.py b/hal9002/rulesets/rs_hal9002_storages.py new file mode 100644 index 0000000..a0ace22 --- /dev/null +++ b/hal9002/rulesets/rs_hal9002_storages.py @@ -0,0 +1,47 @@ +#!/user/bin/env python3 +"""parameter form ruleset for HAL9002 storages""" + +from cmk.rulesets.v1 import Title +from cmk.rulesets.v1.form_specs import ( + DefaultValue, + DictElement, + Dictionary, + Float, + LevelDirection, + SimpleLevels, +) +from cmk.rulesets.v1.rule_specs import CheckParameters, HostAndItemCondition, Topic + + +# function name should begin with an underscore to limit it's visibility +def _parameter_form(): + return Dictionary( + elements={ + "levels_hal_storages_rates": DictElement( + parameter_form=SimpleLevels( + title=Title("Levels for storage up and download rate"), + form_spec_template=Float(unit_symbol="MBytes/s"), + level_direction=LevelDirection.UPPER, + prefill_fixed_levels=DefaultValue(value=(200.0, 300.0)), + ), + required=True, + ), + } + ) + + +# name must begin with "rule_spec_", should refer to the used check plugin +# must be an instance of "CheckParameters" +rule_spec_hal9002_storages = CheckParameters( + # "name" should be the same as the check plugin + name="hal9002_storages", + # the title is shown in the GUI + title=Title("HAL9002 levels for storage upload/download"), + # this ruleset can be found under Setup|Service monitoring rules|Applications... + topic=Topic.APPLICATIONS, + # define the name of the function which creates the GUI elements + parameter_form=_parameter_form, + # define the label in the GUI where you can restrict the + # settings to one or mor specific storages (item) + condition=HostAndItemCondition(item_title=Title("Storage ID")), +) diff --git a/hal9002/rulesets/rs_hal9002_users.py b/hal9002/rulesets/rs_hal9002_users.py new file mode 100644 index 0000000..b1d228d --- /dev/null +++ b/hal9002/rulesets/rs_hal9002_users.py @@ -0,0 +1,47 @@ +#!/user/bin/env python3 +"""parameter form ruleset for HAL9002 users""" + +from cmk.rulesets.v1 import Title +from cmk.rulesets.v1.form_specs import ( + DefaultValue, + DictElement, + Dictionary, + Float, + LevelDirection, + SimpleLevels, +) +from cmk.rulesets.v1.rule_specs import CheckParameters, HostAndItemCondition, Topic + + +# function name should begin with an underscore to limit it's visibility +def _parameter_form(): + return Dictionary( + elements={ + "quota_used_percent": DictElement( + parameter_form=SimpleLevels( + title=Title("Percentage thresholds for storage quota used"), + form_spec_template=Float(), + level_direction=LevelDirection.UPPER, + prefill_fixed_levels=DefaultValue(value=(80.0, 90.0)), + ), + required=True, + ), + } + ) + + +# name must begin with "rule_spec_", should refer to the used check plugin +# must be an instance of "CheckParameters" +rule_spec_hal9002_users = CheckParameters( + # "name" should be the same as the check plugin + name="hal9002_users", + # the title is shown in the GUI + title=Title("HAL9002 levels for user storage quota"), + # this ruleset can be found under Setup|Service monitoring rules|Applications... + topic=Topic.APPLICATIONS, + # define the name of the function which creates the GUI elements + parameter_form=_parameter_form, + # define the label in the GUI where you can restrict the + # settings to one or mor specific users (item) + condition=HostAndItemCondition(item_title=Title("User ID")), +) diff --git a/hal9002/server_side_calls/agent_hal9002.py b/hal9002/server_side_calls/agent_hal9002.py new file mode 100644 index 0000000..556775d --- /dev/null +++ b/hal9002/server_side_calls/agent_hal9002.py @@ -0,0 +1,42 @@ +"""HAL9002 parameter handling for the special agent""" + +from collections.abc import Iterable + +from pydantic import BaseModel + +from cmk.server_side_calls.v1 import ( + HostConfig, + Secret, + SpecialAgentCommand, + SpecialAgentConfig, +) + + +class Hal9002Params(BaseModel): + """defines all needed parameters for the special agent""" + + hostname: str | None = None + username: str | None = None + password: Secret | None = None + + +def agent_hal9002_arguments( + params: Hal9002Params, _host_config: HostConfig +) -> Iterable[SpecialAgentCommand]: + """replaces the argument_thingy from the old API""" + command_arguments: list[str | Secret] = [] + if params.hostname is not None: + command_arguments += ["--hostname", params.hostname] + if params.username is not None: + command_arguments += ["--username", params.username] + if params.password is not None: + command_arguments += ["--password", params.password.unsafe()] + yield SpecialAgentCommand(command_arguments=command_arguments) + + +special_agent_hal9002 = SpecialAgentConfig( + # name must be the filename of the executable for the special agent (without prefix) + name="hal9002", + parameter_parser=Hal9002Params.model_validate, + commands_function=agent_hal9002_arguments, +)