diff --git a/testlp.txt b/testlp.txt new file mode 100644 index 0000000..e1c07b5 --- /dev/null +++ b/testlp.txt @@ -0,0 +1,14 @@ +Repository | Name | Category | Severity | Interactive | Status | Summary +-------------------------------------------------------------+-----------------------------+-------------+-----------+-------------+--------+------------------------------------------------- +Update repository of openSUSE Backports | openSUSE-2024-254 | security | important | --- | needed | Security update for chromium, gn, rust-bindgen +Main Update Repository | openSUSE-2024-256 | recommended | moderate | --- | needed | Recommended update for yast2-theme +Update repository of openSUSE Backports | openSUSE-2024-258 | security | important | --- | needed | Security update for chromium +Update repository of openSUSE Backports | openSUSE-2024-267 | security | important | --- | needed | Security update for chromium +Update repository of openSUSE Backports | openSUSE-2024-278 | security | important | --- | needed | Security update for chromium +Update repository of openSUSE Backports | openSUSE-2024-287 | recommended | moderate | reboot | needed | Recommended update for cockpit, cockpit-machines +Update repository of openSUSE Backports | openSUSE-2024-302 | security | important | --- | needed | Security update for chromium +Update repository of openSUSE Backports | openSUSE-2024-311 | security | important | --- | needed | Security update for chromium +Update repository of openSUSE Backports | openSUSE-2024-314 | security | important | --- | needed | Security update for chromium +Update repository of openSUSE Backports | openSUSE-2024-327 | security | important | --- | needed | Security update for chromium +Update repository of openSUSE Backports | openSUSE-2024-335 | security | important | --- | needed | Security update for chromium +Update repository of openSUSE Backports | openSUSE-2024-337 | security | no | --- | needed | Security update for chromium diff --git a/testlu.txt b/testlu.txt new file mode 100644 index 0000000..4d5c347 --- /dev/null +++ b/testlu.txt @@ -0,0 +1,8 @@ +S | Repository | Name | Current Version | Available Version | Arch +--+---------------------------------------------------------------------------------------+----------------------------------+---------------------------------------+-----------------------------------------+------- +v | Update repository with updates from SUSE Linux Enterprise 15 | bash | 4.4-150400.25.22 | 4.4-150400.27.3.2 | x86_64 +v | Update repository with updates from SUSE Linux Enterprise 15 | bash-doc | 4.4-150400.25.22 | 4.4-150400.27.3.2 | noarch +v | Update repository with updates from SUSE Linux Enterprise 15 | bash-lang | 4.4-150400.25.22 | 4.4-150400.27.3.2 | noarch +v | Update repository with updates from SUSE Linux Enterprise 15 | bash-sh | 4.4-150400.25.22 | 4.4-150400.27.3.2 | x86_64 +v | Update repository with updates from SUSE Linux Enterprise 15 | binutils | 2.41-150100.7.46.1 | 2.43-150100.7.49.1 | x86_64 +v | Update repository with updates from SUSE Linux Enterprise 15 | bubblewrap | 0.8.0-150500.3.3.1 | 0.8.0-150500.3.6.1 | x86_64 diff --git a/zypper.py b/zypper.py new file mode 100755 index 0000000..e804e58 --- /dev/null +++ b/zypper.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python3 + +""" +Description: #TODO: short version + +#TODO: long version + +#TODO: example + + #TODO: code of the example + +#TODO: additional information + +Dependencies: #TODO: add if needed + +Authors: Gabriele Puliti + Bernd Shubert +""" + +import argparse +import subprocess +import os + +from collections.abc import Sequence + +def __print_package_info(package, fields, prefix, filters=None): + filters = filters or {} + check = all(package.get(k) == v for k, v in filters.items()) + + if check: + field_str = ",".join([f'{name}="{package[field]}"' for field, name in fields]) + print(f"{prefix}{{{field_str}}} 1") + +def __print_pending_data(data, all_info, fields_more, fields_less, prefix, filters=None): + if all_info: + fields = fields_more + else: + fields = fields_less + + if len(data) == 0: + field_str = ",".join([f'{name}=""' for _, name in fields]) + print(f"{prefix}{{{field_str}}} 0") + else: + for package in data: + __print_package_info(package, fields, prefix, filters) + +def print_pending_updates(data, all_info, filters=None): + fields_more = [("Repository", "repository"), ("Name", "package-name"), + ("Available Version", "available-version")] + fields_less = [("Repository", "repository"), ("Name", "package-name")] + prefix = "zypper_update_pending" + + __print_pending_data(data, all_info, fields_more, fields_less, prefix, filters) + +def print_pending_patches(data, all_info, filters=None): + fields_more = [ + ("Repository", "repository"), ("Name", "patch-name"), ("Category", "category"), + ("Severity", "severity"), ("Interactive", "interactive"), ("Status", "status") + ] + fields_less = [ + ("Repository", "repository"), ("Name", "patch-name"), + ("Interactive", "interactive"), ("Status", "status") + ] + prefix = "zypper_patch_pending" + + __print_pending_data(data, all_info, fields_more, fields_less, prefix, filters) + +def print_orphaned_packages(data): + fields = [ + ("Package", "package"), ("Installed Version", "installed-version") + ] + prefix = "zypper_package_orphan" + + __print_pending_data(data, True, fields, None, prefix, None) + +def __print_data_sum(data, prefix, filters=None): + filters = filters or {} + if len(data) == 0: + print(prefix + "{total} 0") + else: + gauge = 0 + for package in data: + check = all(package.get(k) == v for k, v in filters.items()) + if check: + gauge += 1 + print(prefix + "{total} " + str(gauge)) + +def print_updates_sum(data, filters=None): + prefix = "zypper_updates_pending_total" + + __print_data_sum(data, prefix, filters) + +def print_patches_sum(data, prefix="zypper_patches_pending_total", filters=None): + __print_data_sum(data, prefix, filters) + +def print_reboot_required(): + needs_restarting_path = '/usr/bin/needs-restarting' + is_path_ok = os.path.isfile(needs_restarting_path) and os.access(needs_restarting_path, os.X_OK) + + if is_path_ok: + result = subprocess.run( + [needs_restarting_path, '-r'], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False) + + print('# HELP node_reboot_required Node require reboot to activate installed updates or patches. (0 = not needed, 1 = needed)') + print('# TYPE node_reboot_required gauge') + if result.returncode == 0: + print('node_reboot_required 0') + else: + print('node_reboot_required 1') + +def print_zypper_version(): + result = subprocess.run( + ['/usr/bin/zypper', '-V'], + stdout=subprocess.PIPE, + check=False).stdout.decode('utf-8') + + print("zypper_version " + result.split()[1]) + + +def __extract_lu_data(raw: str): + raw_lines = raw.splitlines()[2:] + extracted_data = [] + + for line in raw_lines: + parts = [part.strip() for part in line.split('|')] + if len(parts) >= 5: + extracted_data.append({ + "Repository": parts[1], + "Name": parts[2], + "Current Version": parts[3], + "Available Version": parts[4], + "Arch": parts[5] + }) + + return extracted_data + +def __extract_lp_data(raw: str): + raw_lines = raw.splitlines()[2:] + extracted_data = [] + + for line in raw_lines: + parts = [part.strip() for part in line.split('|')] + if len(parts) >= 5: + extracted_data.append({ + "Repository": parts[0], + "Name": parts[1], + "Category": parts[2], + "Severity": parts[3], + "Interactive": parts[4], + "Status": parts[5] + }) + + return extracted_data + +def __extract_orphaned_data(raw: str): + raw_lines = raw.splitlines()[2:] + extracted_data = [] + + for line in raw_lines: + parts = [part.strip() for part in line.split('|')] + if len(parts) >= 5: + extracted_data.append({ + "Package": parts[3], + "Installed Version": parts[5] + }) + + return extracted_data + +def __parse_arguments(argv): + parser = argparse.ArgumentParser() + parser.add_mutually_exclusive_group(required=False) + parser.add_argument( + "-m", + "--more", + dest="all_info", + action='store_true', + help="Print all the package infos", + ) + parser.add_argument( + "-l", + "--less", + dest="all_info", + action='store_false', + help="Print less package infos", + ) + parser.set_defaults(all_info=True) + return parser.parse_args(argv) + +def main(argv: Sequence[str] | None = None) -> int: + args = __parse_arguments(argv) + + raw_zypper_lu = subprocess.run( + ['cat', 'testlu.txt'], + #['/usr/bin/zypper', '--quiet', 'lu'], + stdout=subprocess.PIPE, + check=False + ).stdout.decode('utf-8') + data_zypper_lu = __extract_lu_data(raw_zypper_lu) + + raw_zypper_lp = subprocess.run( + ['cat', 'testlp.txt'], + #['/usr/bin/zypper', '--quiet', 'lp'], + stdout=subprocess.PIPE, + check=False + ).stdout.decode('utf-8') + data_zypper_lp = __extract_lp_data(raw_zypper_lp) + + raw_zypper_lp = subprocess.run( + ['cat', 'testlp.txt'], + #['/usr/bin/zypper', '--quiet', 'lp'], + stdout=subprocess.PIPE, + check=False + ).stdout.decode('utf-8') + data_zypper_lp = __extract_lp_data(raw_zypper_lp) + + raw_zypper_orphaned = subprocess.run( + ['cat', 'testlp.txt'], + #['/usr/bin/zypper', '--quiet', 'pa', --orphaned'], + stdout=subprocess.PIPE, + check=False + ).stdout.decode('utf-8') + data_zypper_orphaned = __extract_orphaned_data(raw_zypper_orphaned) + + print('# HELP zypper_update_pending zypper package update available from repository. (0 = not available, 1 = available)') + print('# TYPE zypper_update_pending gauge') + print_pending_updates(data_zypper_lu, args.all_info) + + print('# HELP zypper_updates_pending_total zypper packages updates available in total') + print('# TYPE zypper_updates_pending_total counter') + print_updates_sum(data_zypper_lu) + + print('# HELP zypper_patch_pending zypper patch available from repository. (0 = not available, 1 = available)') + print('# TYPE zypper_patch_pending gauge') + print_pending_patches(data_zypper_lp, args.all_info) + + print('# HELP zypper_patches_pending_total zypper patches available total') + print('# TYPE zypper_patches_pending_total counter') + print_patches_sum(data_zypper_lp) + + print('# HELP zypper_patches_pending_security_total zypper patches available with category security total') + print('# TYPE zypper_patches_pending_security_total counter') + print_patches_sum(data_zypper_lp, + prefix="zypper_patches_pending_security_total", + filters={'Category':'security'}) + + print('# HELP zypper_patches_pending_security_important_total zypper patches available with category security severity important total') + print('# TYPE zypper_patches_pending_security_important_total counter') + print_patches_sum(data_zypper_lp, + prefix="zypper_patches_pending_security_important_total", + filters={'Category':'security', 'Severity': 'important'}) + + print('# HELP zypper_patches_pending_reboot_total zypper patches available which require reboot total') + print('# TYPE zypper_patches_pending_reboot_total counter') + print_patches_sum(data_zypper_lp, + prefix="zypper_patches_pending_reboot_total", + filters={'Interactive': 'reboot'}) + + print_reboot_required() + + print('# HELP zypper_version zypper installed package version') + print('# TYPE zypper_version gauges') + print_zypper_version() + + print('# HELP zypper_package_orphan zypper packages with no update source (orphaned)') + print('# TYPE zypper_package_orphan gauges') + print_orphaned_packages(data_zypper_orphaned) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/zypper.sh b/zypper.sh index 72b8edf..624ac68 100755 --- a/zypper.sh +++ b/zypper.sh @@ -128,7 +128,7 @@ get_updates_sum() { get_pending_patches() { if [ -z "$1" ]; then - echo 'zypper_patch_pending{repository="",patch-name="",category="",severity="",interactive="",status""} 0' + echo 'zypper_patch_pending{repository="",patch-name="",category="",severity="",interactive="",status=""} 0' else echo "$1" | awk -v output_format=$2 "$filter_pending_patches"