From c76e80204dc5664cea39690275254bf517b6b44a Mon Sep 17 00:00:00 2001 From: Wabri <12409541+Wabri@users.noreply.github.com> Date: Mon, 14 Oct 2024 13:32:51 +0200 Subject: [PATCH 01/14] feat(zypper.sh): add zypper updates and patches script metrics Co-authored-by: Bernd Schubert Signed-off-by: Wabri <12409541+Wabri@users.noreply.github.com> Signed-off-by: Bernd Schubert Signed-off-by: Wabri <12409541+Wabri@users.noreply.github.com> --- zypper.sh | 264 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100755 zypper.sh diff --git a/zypper.sh b/zypper.sh new file mode 100755 index 00000000..a6fc27d9 --- /dev/null +++ b/zypper.sh @@ -0,0 +1,264 @@ +#!/usr/bin/env bash +# +# Description: Expose metrics from zypper updates and patches. +# Author: Bernd Schubert +# Contributer: Gabriele Puliti +# Based on yum.sh by Slawomir Gonet + +set -o errexit # exit on first error +set -o nounset # fail if unset variables +set -o pipefail # reflect exit status + +if [[ "${1-}" =~ ^-*h(elp)?$ ]]; then + echo """Usage: zypper.sh [OPTION] +This is an script to extract monitoring values for the zypper package + +It work only with root permission! + +Available options: + -l, --less Extract only the necessary + -m, --more Extract everything (this is the default value) + +Examples: + zypper.sh --less + zypper.sh -m +""" + exit +fi + +# Check if we are root +if [ "$EUID" -ne 0 ]; then + echo "${0##*/}: Please run as root!" >&2 + exit 1 +fi + +filter_pending_updates=' +BEGIN { + FS=" \\| "; # set field separator to " | " +} + +NR { + # Extract and format repository, package-name, and available version + repository = $2 + package_name = $3 + available_version = $5 + + # Remove trailing whitespace + gsub(/[[:space:]]+$/, "", repository) + gsub(/[[:space:]]+$/, "", package_name) + gsub(/[[:space:]]+$/, "", available_version) + + # Print the output in the required format + if (output_format == "-l" || output_format == "--less") + printf "zypper_update_pending{repository=\"%s\",package-name=\"%s\"} 1\n", repository, package_name + else if (output_format == "-m" || output_format == "--more") + printf "zypper_update_pending{repository=\"%s\",package-name=\"%s\",available-version=\"%s\"} 1\n", repository, package_name, available_version +} +' + +filter_pending_patches=' +BEGIN { + FS=" \\| "; # set field separator to " | " +} + +NR { + # Extract and format repository, patch_name, severity, category, interactive and status + repository = $1 + patch_name = $2 + category = $3 + severity = $4 + interactive = $5 + status = $6 + + # Remove trailing whitespace + gsub(/[[:space:]]+$/, "", repository) + gsub(/[[:space:]]+$/, "", patch_name) + gsub(/[[:space:]]+$/, "", category) + gsub(/[[:space:]]+$/, "", severity) + gsub(/[[:space:]]+$/, "", interactive) + gsub(/[[:space:]]+$/, "", status) + + # Print the output in the required format + if (output_format == "-l" || output_format == "--less") + printf "zypper_patch_pending{repository=\"%s\",patch-name=\"%s\",category=\"%s\",severity=\"%s\",interactive=\"%s\",status=\"%s\"} 1\n", repository, patch_name, category, severity, interactive, status + else if (output_format == "-m" || output_format == "--more") + printf "zypper_patch_pending{repository=\"%s\",patch-name=\"%s\",category=\"%s\",severity=\"%s\",interactive=\"%s\",status=\"%s\"} 1\n", repository, patch_name, category, severity, interactive, status +} +' + +filter_orphan_packages=' +BEGIN { + FS=" \\| "; # set field separator to " | " +} + +NR { + # Extract and format package, and installed version + package = $3 + installed_version = $5 + + # Remove trailing whitespace + gsub(/[[:space:]]+$/, "", package) + gsub(/[[:space:]]+$/, "", installed_version) + + # Print the output in the required format + printf "zypper_package_orphan{package=\"%s\",installed-version=\"%s\"} 1\n", package, installed_version +} +' + +get_pending_updates() { + if [ -z "$1" ]; then + echo 'zypper_update_pending{repository="",package-name="",available-version=""} 0' + else + echo "$1" | + awk -v output_format=$2 "$filter_pending_updates" + fi +} + +get_updates_sum() { + { + if [ -z "$1" ]; then + echo "0" + else + echo "$1" | + wc -l + fi + } | + awk '{print "zypper_updates_pending_total{total} "$1}' +} + +get_pending_patches() { + if [ -z "$1" ]; then + echo 'zypper_update_pending{repository="",package-name="",available-version=""} 0' + else + echo "$1" | + awk -v output_format=$2 "$filter_pending_patches" + fi +} + +get_pending_security_patches() { + { + if [ -z "$1" ]; then + echo "0" + else + echo "$1" | + grep security | + wc -l + fi + } | + awk '{print "zypper_patches_pending_security_total "$1}' +} + +get_pending_security_important_patches() { + { + if [ -z "$1" ]; then + echo "0" + else + echo "$1" | + grep security | + grep important | + wc -l + fi + } | + awk '{print "zypper_patches_pending_security_important_total "$1}' +} + +get_pending_reboot_patches() { + { + if [ -z "$1" ]; then + echo "0" + else + echo "$1" | + grep reboot | + wc -l + fi + } | + awk '{print "zypper_patches_pending_reboot_total "$1}' +} + +get_patches_sum() { + { + if [ -z "$1" ]; then + echo "0" + else + echo "$1" | + wc -l + fi + } | + awk '{print "zypper_patches_pending_total "$1}' +} + +get_zypper_version() { + echo "$1" | + awk '{print "zypper_version "$2}' +} + +get_orphan_packages() { + if [ -z "$1" ]; then + echo 'zypper_package_orphan{package="",installed-version=""} 0' + else + echo "$1" | + awk "$filter_orphan_packages" + fi +} + +main() { + # If there are no paramenter passed then use the more format + if [ $# -eq 0 ]; then + output_format="--more" + else + output_format="$1" + fi + + zypper_lu_quiet_tail_n3="$(/usr/bin/zypper --quiet lu | tail -n +3)" + zypper_lp_quiet_tail_n3="$(/usr/bin/zypper --quiet lp | tail -n +3)" + zypper_version="$(/usr/bin/zypper -V)" + zypper_orphan_packages="$(zypper --quiet pa --orphaned | tail -n +3)" + + echo '# HELP zypper_update_pending zypper package update available from repository. (0 = not available, 1 = available)' + echo '# TYPE zypper_update_pending gauge' + get_pending_updates "$zypper_lu_quiet_tail_n3" "$output_format" + + echo '# HELP zypper_updates_pending_total zypper packages updates available in total' + echo '# TYPE zypper_updates_pending_total counter' + get_updates_sum "$zypper_lu_quiet_tail_n3" + + echo '# HELP zypper_patch_pending zypper patch available from repository. (0 = not available, 1 = available)' + echo '# TYPE zypper_patch_pending gauge' + get_pending_patches "$zypper_lp_quiet_tail_n3" "$output_format" + + echo '# HELP zypper_patches_pending_total zypper patches available total' + echo '# TYPE zypper_patches_pending_total counter' + get_patches_sum "$zypper_lp_quiet_tail_n3" + + echo '# HELP zypper_patches_pending_security_total zypper patches available with category security total' + echo '# TYPE zypper_patches_pending_security_total counter' + get_pending_security_patches "$zypper_lp_quiet_tail_n3" + + echo '# HELP zypper_patches_pending_security_important_total zypper patches available with category security severity important total' + echo '# TYPE zypper_patches_pending_security_important_total counter' + get_pending_security_important_patches "$zypper_lp_quiet_tail_n3" + + echo '# HELP zypper_patches_pending_reboot_total zypper patches available which require reboot total' + echo '# TYPE zypper_patches_pending_reboot_total counter' + get_pending_reboot_patches "$zypper_lp_quiet_tail_n3" + + if [[ -x /usr/bin/needs-restarting ]]; then + echo '# HELP node_reboot_required Node require reboot to active installed updates or patches. (0 = not needed, 1 = needed)' + echo '# TYPE node_reboot_required gauge' + if /usr/bin/needs-restarting -r >/dev/null 2>&1; then + echo 'node_reboot_required 0' + else + echo 'node_reboot_required 1' + fi + fi + + echo '# HELP zypper_version zypper installed package version' + echo '# TYPE zypper_version gauges' + get_zypper_version "$zypper_version" + + echo '# HELP zypper_package_orphan zypper packages with no update source (orphaned) ' + echo '# TYPE zypper_package_orphan gauges' + get_orphan_packages "$zypper_orphan_packages" +} + +main "$@" From f4db4d224c444bcd9c358cc40994f45a33a72654 Mon Sep 17 00:00:00 2001 From: Bernd Schubert Date: Mon, 14 Oct 2024 14:27:43 +0200 Subject: [PATCH 02/14] fixing duplicates, reducing output for patches less option Signed-off-by: Wabri <12409541+Wabri@users.noreply.github.com> Signed-off-by: Bernd Schubert Signed-off-by: Wabri <12409541+Wabri@users.noreply.github.com> --- zypper.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/zypper.sh b/zypper.sh index a6fc27d9..b7c16dcf 100755 --- a/zypper.sh +++ b/zypper.sh @@ -62,7 +62,7 @@ BEGIN { } NR { - # Extract and format repository, patch_name, severity, category, interactive and status + # Extract and format repository, patch_name, category, severity, interactive and status repository = $1 patch_name = $2 category = $3 @@ -80,7 +80,7 @@ NR { # Print the output in the required format if (output_format == "-l" || output_format == "--less") - printf "zypper_patch_pending{repository=\"%s\",patch-name=\"%s\",category=\"%s\",severity=\"%s\",interactive=\"%s\",status=\"%s\"} 1\n", repository, patch_name, category, severity, interactive, status + printf "zypper_patch_pending{repository=\"%s\",patch-name=\"%s\",interactive=\"%s\",status=\"%s\"} 1\n", repository, patch_name, interactive, status else if (output_format == "-m" || output_format == "--more") printf "zypper_patch_pending{repository=\"%s\",patch-name=\"%s\",category=\"%s\",severity=\"%s\",interactive=\"%s\",status=\"%s\"} 1\n", repository, patch_name, category, severity, interactive, status } @@ -128,7 +128,7 @@ get_updates_sum() { get_pending_patches() { if [ -z "$1" ]; then - echo 'zypper_update_pending{repository="",package-name="",available-version=""} 0' + echo 'zypper_patch_pending{repository="",patch-name="",category="",severity="",interactive="",status""} 0' else echo "$1" | awk -v output_format=$2 "$filter_pending_patches" @@ -210,7 +210,7 @@ main() { fi zypper_lu_quiet_tail_n3="$(/usr/bin/zypper --quiet lu | tail -n +3)" - zypper_lp_quiet_tail_n3="$(/usr/bin/zypper --quiet lp | tail -n +3)" + zypper_lp_quiet_tail_n3="$(/usr/bin/zypper --quiet lp | sed -E '/(^$|^Repository|^---)/d'| sed '/|/!d')" zypper_version="$(/usr/bin/zypper -V)" zypper_orphan_packages="$(zypper --quiet pa --orphaned | tail -n +3)" From 87d70604c7936189102d9028853e26a68491aa78 Mon Sep 17 00:00:00 2001 From: Bernd Schubert Date: Tue, 15 Oct 2024 13:36:31 +0200 Subject: [PATCH 03/14] fixing security patch counting Signed-off-by: Bernd Schubert Signed-off-by: Wabri <12409541+Wabri@users.noreply.github.com> --- zypper.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zypper.sh b/zypper.sh index b7c16dcf..8a9d0169 100755 --- a/zypper.sh +++ b/zypper.sh @@ -5,7 +5,7 @@ # Contributer: Gabriele Puliti # Based on yum.sh by Slawomir Gonet -set -o errexit # exit on first error +#set -o errexit # exit on first error, doesn't work on all test systems set -o nounset # fail if unset variables set -o pipefail # reflect exit status @@ -141,7 +141,7 @@ get_pending_security_patches() { echo "0" else echo "$1" | - grep security | + grep "| security" | wc -l fi } | @@ -154,7 +154,7 @@ get_pending_security_important_patches() { echo "0" else echo "$1" | - grep security | + grep "| security" | grep important | wc -l fi From ba05c807b3d25b64a0826051f00f17f357d060e7 Mon Sep 17 00:00:00 2001 From: GabrielePuliti <12409541+Wabri@users.noreply.github.com> Date: Sat, 19 Oct 2024 17:41:06 +0200 Subject: [PATCH 04/14] Update zypper.sh Co-authored-by: Daniel Swarbrick Signed-off-by: GabrielePuliti <12409541+Wabri@users.noreply.github.com> Signed-off-by: Wabri <12409541+Wabri@users.noreply.github.com> --- zypper.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zypper.sh b/zypper.sh index 8a9d0169..98b99c1a 100755 --- a/zypper.sh +++ b/zypper.sh @@ -253,7 +253,7 @@ main() { fi echo '# HELP zypper_version zypper installed package version' - echo '# TYPE zypper_version gauges' + echo '# TYPE zypper_version gauge' get_zypper_version "$zypper_version" echo '# HELP zypper_package_orphan zypper packages with no update source (orphaned) ' From 8ed9cb490ca187f0af0962f655ee7caea7a699d3 Mon Sep 17 00:00:00 2001 From: GabrielePuliti <12409541+Wabri@users.noreply.github.com> Date: Sat, 19 Oct 2024 17:41:22 +0200 Subject: [PATCH 05/14] Update zypper.sh Co-authored-by: Daniel Swarbrick Signed-off-by: GabrielePuliti <12409541+Wabri@users.noreply.github.com> Signed-off-by: Wabri <12409541+Wabri@users.noreply.github.com> --- zypper.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zypper.sh b/zypper.sh index 98b99c1a..72b8edf3 100755 --- a/zypper.sh +++ b/zypper.sh @@ -257,7 +257,7 @@ main() { get_zypper_version "$zypper_version" echo '# HELP zypper_package_orphan zypper packages with no update source (orphaned) ' - echo '# TYPE zypper_package_orphan gauges' + echo '# TYPE zypper_package_orphan gauge' get_orphan_packages "$zypper_orphan_packages" } From 1e965cbb180c98c12be1dc0b19eab000fc7fd176 Mon Sep 17 00:00:00 2001 From: Wabri <12409541+Wabri@users.noreply.github.com> Date: Sat, 19 Oct 2024 19:01:50 +0200 Subject: [PATCH 06/14] feat(zypper.py): add zypper.py by translate to python zypper.sh Signed-off-by: Wabri <12409541+Wabri@users.noreply.github.com> --- zypper.py | 264 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ zypper.sh | 2 +- 2 files changed, 265 insertions(+), 1 deletion(-) create mode 100755 zypper.py diff --git a/zypper.py b/zypper.py new file mode 100755 index 00000000..b0e1d079 --- /dev/null +++ b/zypper.py @@ -0,0 +1,264 @@ +#!/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 = [ + ("Name", "package"), ("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({ + "Name": parts[3], + "Version": parts[4] + }) + + 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( + ['/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( + ['/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( + ['/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 72b8edf3..624ac686 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" From 5ba6820521ed5bcd3118d5048860997d8f595684 Mon Sep 17 00:00:00 2001 From: Wabri <12409541+Wabri@users.noreply.github.com> Date: Wed, 23 Oct 2024 11:44:55 +0200 Subject: [PATCH 07/14] feat(zypper.py): update script header with more information Signed-off-by: Wabri <12409541+Wabri@users.noreply.github.com> --- zypper.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/zypper.py b/zypper.py index b0e1d079..fe7fc8f6 100755 --- a/zypper.py +++ b/zypper.py @@ -1,17 +1,16 @@ #!/usr/bin/env python3 """ -Description: #TODO: short version +Description: Expose metrics from zypper updates and patches. -#TODO: long version +The script can take 2 arguments: `--more` and `--less`, the selection basically change how many informations are printed. -#TODO: example +The `--more` is by default. - #TODO: code of the example +Examples: -#TODO: additional information - -Dependencies: #TODO: add if needed + zypper.py --less + zypper.py -m Authors: Gabriele Puliti Bernd Shubert From ac1553b7a3e91d431e81e0abafaa0518c17bd515 Mon Sep 17 00:00:00 2001 From: Wabri <12409541+Wabri@users.noreply.github.com> Date: Wed, 23 Oct 2024 16:02:40 +0200 Subject: [PATCH 08/14] fix(zypper.py): remove linting issues Signed-off-by: Wabri <12409541+Wabri@users.noreply.github.com> --- zypper.py | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/zypper.py b/zypper.py index fe7fc8f6..69f13c7b 100755 --- a/zypper.py +++ b/zypper.py @@ -3,7 +3,8 @@ """ Description: Expose metrics from zypper updates and patches. -The script can take 2 arguments: `--more` and `--less`, the selection basically change how many informations are printed. +The script can take 2 arguments: `--more` and `--less`. +The selection of the arguments change how many informations are going to be printed. The `--more` is by default. @@ -22,6 +23,7 @@ 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()) @@ -30,6 +32,7 @@ def __print_package_info(package, fields, prefix, filters=None): 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 @@ -43,6 +46,7 @@ def __print_pending_data(data, all_info, fields_more, fields_less, prefix, filte 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")] @@ -51,6 +55,7 @@ def print_pending_updates(data, all_info, filters=None): __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"), @@ -64,6 +69,7 @@ def print_pending_patches(data, all_info, filters=None): __print_pending_data(data, all_info, fields_more, fields_less, prefix, filters) + def print_orphaned_packages(data): fields = [ ("Name", "package"), ("Version", "installed-version") @@ -72,6 +78,7 @@ def print_orphaned_packages(data): __print_pending_data(data, True, fields, None, prefix, None) + def __print_data_sum(data, prefix, filters=None): filters = filters or {} if len(data) == 0: @@ -84,14 +91,17 @@ def __print_data_sum(data, prefix, filters=None): 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) @@ -103,13 +113,15 @@ def print_reboot_required(): 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('# 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'], @@ -136,6 +148,7 @@ def __extract_lu_data(raw: str): return extracted_data + def __extract_lp_data(raw: str): raw_lines = raw.splitlines()[2:] extracted_data = [] @@ -154,6 +167,7 @@ def __extract_lp_data(raw: str): return extracted_data + def __extract_orphaned_data(raw: str): raw_lines = raw.splitlines()[2:] extracted_data = [] @@ -168,6 +182,7 @@ def __extract_orphaned_data(raw: str): return extracted_data + def __parse_arguments(argv): parser = argparse.ArgumentParser() parser.add_mutually_exclusive_group(required=False) @@ -188,6 +203,7 @@ def __parse_arguments(argv): parser.set_defaults(all_info=True) return parser.parse_args(argv) + def main(argv: Sequence[str] | None = None) -> int: args = __parse_arguments(argv) @@ -212,7 +228,8 @@ def main(argv: Sequence[str] | None = None) -> int: ).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('# 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) @@ -220,7 +237,8 @@ def main(argv: Sequence[str] | None = None) -> int: 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('# 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) @@ -228,19 +246,22 @@ def main(argv: Sequence[str] | None = None) -> int: 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('# 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'}) + filters={'Category': 'security'}) - print('# HELP zypper_patches_pending_security_important_total zypper patches available with category security severity important total') + 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'}) + filters={'Category': 'security', 'Severity': 'important'}) - print('# HELP zypper_patches_pending_reboot_total zypper patches available which require reboot total') + 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", From 2d5abc9133c1a14a9d5e74d7b79eb435936d753c Mon Sep 17 00:00:00 2001 From: Wabri <12409541+Wabri@users.noreply.github.com> Date: Thu, 24 Oct 2024 12:04:33 +0200 Subject: [PATCH 09/14] fix(zypper.sh): remove lint problems Signed-off-by: Wabri <12409541+Wabri@users.noreply.github.com> --- zypper.py | 16 ++++++++-------- zypper.sh | 31 +++++++++++++++---------------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/zypper.py b/zypper.py index 69f13c7b..d862ec03 100755 --- a/zypper.py +++ b/zypper.py @@ -3,7 +3,7 @@ """ Description: Expose metrics from zypper updates and patches. -The script can take 2 arguments: `--more` and `--less`. +The script can take 2 arguments: `--more` and `--less`. The selection of the arguments change how many informations are going to be printed. The `--more` is by default. @@ -113,8 +113,8 @@ def print_reboot_required(): 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('# 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') @@ -228,7 +228,7 @@ def main(argv: Sequence[str] | None = None) -> int: ).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 '\ + 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) @@ -237,7 +237,7 @@ def main(argv: Sequence[str] | None = None) -> int: 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 '\ + 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) @@ -246,21 +246,21 @@ def main(argv: Sequence[str] | None = None) -> int: 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 '\ + 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 '\ + 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 '\ + 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, diff --git a/zypper.sh b/zypper.sh index 624ac686..54d32790 100755 --- a/zypper.sh +++ b/zypper.sh @@ -10,7 +10,8 @@ set -o nounset # fail if unset variables set -o pipefail # reflect exit status if [[ "${1-}" =~ ^-*h(elp)?$ ]]; then - echo """Usage: zypper.sh [OPTION] + # shellcheck disable=SC1078 + echo "Usage: zypper.sh [OPTION] This is an script to extract monitoring values for the zypper package It work only with root permission! @@ -21,9 +22,8 @@ Available options: Examples: zypper.sh --less - zypper.sh -m -""" - exit + zypper.sh -m" + exit 0 fi # Check if we are root @@ -32,6 +32,7 @@ if [ "$EUID" -ne 0 ]; then exit 1 fi +# shellcheck disable=SC2016 filter_pending_updates=' BEGIN { FS=" \\| "; # set field separator to " | " @@ -56,6 +57,7 @@ NR { } ' +# shellcheck disable=SC2016 filter_pending_patches=' BEGIN { FS=" \\| "; # set field separator to " | " @@ -86,6 +88,7 @@ NR { } ' +# shellcheck disable=SC2016 filter_orphan_packages=' BEGIN { FS=" \\| "; # set field separator to " | " @@ -107,10 +110,10 @@ NR { get_pending_updates() { if [ -z "$1" ]; then - echo 'zypper_update_pending{repository="",package-name="",available-version=""} 0' + echo "zypper_update_pending{repository=\"\",package-name=\"\",available-version=\"\"} 0" else echo "$1" | - awk -v output_format=$2 "$filter_pending_updates" + awk -v output_format="$2" "$filter_pending_updates" fi } @@ -128,10 +131,10 @@ 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" + awk -v output_format="$2" "$filter_pending_patches" fi } @@ -141,8 +144,7 @@ get_pending_security_patches() { echo "0" else echo "$1" | - grep "| security" | - wc -l + grep -c "| security" fi } | awk '{print "zypper_patches_pending_security_total "$1}' @@ -154,9 +156,7 @@ get_pending_security_important_patches() { echo "0" else echo "$1" | - grep "| security" | - grep important | - wc -l + grep -c "| security.*important" fi } | awk '{print "zypper_patches_pending_security_important_total "$1}' @@ -168,8 +168,7 @@ get_pending_reboot_patches() { echo "0" else echo "$1" | - grep reboot | - wc -l + grep -c "reboot" fi } | awk '{print "zypper_patches_pending_reboot_total "$1}' @@ -194,7 +193,7 @@ get_zypper_version() { get_orphan_packages() { if [ -z "$1" ]; then - echo 'zypper_package_orphan{package="",installed-version=""} 0' + echo "zypper_package_orphan{package=\"\",installed-version=\"\"} 0" else echo "$1" | awk "$filter_orphan_packages" From 03a397c997a7dd4a20b56c55f1bea20278b2b572 Mon Sep 17 00:00:00 2001 From: Wabri <12409541+Wabri@users.noreply.github.com> Date: Sun, 27 Oct 2024 10:05:27 +0100 Subject: [PATCH 10/14] refactor(zypper.py): using the prometheus client library Signed-off-by: Wabri <12409541+Wabri@users.noreply.github.com> --- zypper.py | 163 +++++++++++++++++++++++++----------------------------- 1 file changed, 75 insertions(+), 88 deletions(-) diff --git a/zypper.py b/zypper.py index d862ec03..ccbdae0f 100755 --- a/zypper.py +++ b/zypper.py @@ -20,86 +20,84 @@ import argparse import subprocess import os +import sys from collections.abc import Sequence +from prometheus_client import CollectorRegistry, Gauge, Info, generate_latest - -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") +REGISTRY = CollectorRegistry() +NAMESPACE = "zypper" -def __print_pending_data(data, all_info, fields_more, fields_less, prefix, filters=None): - if all_info: - fields = fields_more - else: - fields = fields_less +def __print_pending_data(data, fields, info, filters=None): + filters = filters or {} if len(data) == 0: field_str = ",".join([f'{name}=""' for _, name in fields]) - print(f"{prefix}{{{field_str}}} 0") + info.info({field_str: '0'}) else: for package in data: - __print_package_info(package, fields, prefix, filters) + 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]) + info.info({field_str: '1'}) def print_pending_updates(data, all_info, filters=None): - fields_more = [("Repository", "repository"), ("Name", "package-name"), + if all_info: + fields = [("Repository", "repository"), ("Name", "package-name"), ("Available Version", "available-version")] - fields_less = [("Repository", "repository"), ("Name", "package-name")] + else: + fields = [("Repository", "repository"), ("Name", "package-name")] prefix = "zypper_update_pending" + description = "zypper package update available from repository. (0 = not available, 1 = available)" + info = Info(prefix, description) - __print_pending_data(data, all_info, fields_more, fields_less, prefix, filters) + __print_pending_data(data, fields, info, 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") - ] + if all_info: + fields = [ + ("Repository", "repository"), ("Name", "patch-name"), ("Category", "category"), + ("Severity", "severity"), ("Interactive", "interactive"), ("Status", "status") + ] + else: + fields = [ + ("Repository", "repository"), ("Name", "patch-name"), + ("Interactive", "interactive"), ("Status", "status") + ] prefix = "zypper_patch_pending" + description = "zypper patch available from repository. (0 = not available , 1 = available)" + info = Info(prefix, description) - __print_pending_data(data, all_info, fields_more, fields_less, prefix, filters) + __print_pending_data(data, fields, info, filters) -def print_orphaned_packages(data): +def print_orphaned_packages(data, filters=None): fields = [ ("Name", "package"), ("Version", "installed-version") ] prefix = "zypper_package_orphan" + description = "zypper packages with no update source (orphaned)" + info = Info(prefix, description) - __print_pending_data(data, True, fields, None, prefix, None) + __print_pending_data(data, fields, info, filters) -def __print_data_sum(data, prefix, filters=None): +def print_data_sum(data, prefix, description, filters=None): + gauge = Gauge(prefix, + description, + namespace=NAMESPACE, + registry=REGISTRY) filters = filters or {} if len(data) == 0: - print(prefix + "{total} 0") + gauge.set(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) + gauge.inc() def print_reboot_required(): @@ -107,19 +105,19 @@ def print_reboot_required(): is_path_ok = os.path.isfile(needs_restarting_path) and os.access(needs_restarting_path, os.X_OK) if is_path_ok: + prefix = "node_reboot_required" + description = "Node require reboot to activate installed updates or patches. (0 = not needed, 1 = needed)" + info = Info(prefix, description) 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') + info.info({"node_reboot_required": "0"}) else: - print('node_reboot_required 1') + info.info({"node_reboot_required": "1"}) def print_zypper_version(): @@ -127,8 +125,9 @@ def print_zypper_version(): ['/usr/bin/zypper', '-V'], stdout=subprocess.PIPE, check=False).stdout.decode('utf-8') + info = Info("zypper_version", "zypper installed package version") - print("zypper_version " + result.split()[1]) + info.info({"zypper_version": result.split()[1]}) def __extract_lu_data(raw: str): @@ -228,57 +227,45 @@ def main(argv: Sequence[str] | None = None) -> int: ).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_data_sum(data_zypper_lu, "zypper_updates_pending_total", "zypper packages updates available in total") - 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_data_sum(data_zypper_lp, + "zypper_patches_pending_total", + "zypper patches available total") + + print_data_sum(data_zypper_lp, + "zypper_patches_pending_security_total", + "zypper patches available with category security total", + filters={'Category': 'security'}) + + print_data_sum(data_zypper_lp, + "zypper_patches_pending_security_important_total", + "zypper patches available with category security severity important total", + filters={'Category': 'security', 'Severity': 'important'}) + + print_data_sum(data_zypper_lp, + "zypper_patches_pending_reboot_total", + "zypper patches available which require 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()) + try: + main() + except Exception as e: + print("ERROR: {}".format(e), file=sys.stderr) + sys.exit(1) + + print(generate_latest(REGISTRY).decode(), end="") From 86d2c1f3614d2e66c4d4fa6720aab7f051f1e46b Mon Sep 17 00:00:00 2001 From: Wabri <12409541+Wabri@users.noreply.github.com> Date: Sun, 27 Oct 2024 11:43:20 +0100 Subject: [PATCH 11/14] refactor(zypper.py): add error checking for subprocess calls Signed-off-by: Wabri <12409541+Wabri@users.noreply.github.com> --- zypper.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/zypper.py b/zypper.py index ccbdae0f..8f68569c 100755 --- a/zypper.py +++ b/zypper.py @@ -210,22 +210,34 @@ def main(argv: Sequence[str] | None = None) -> int: ['/usr/bin/zypper', '--quiet', 'lu'], stdout=subprocess.PIPE, check=False - ).stdout.decode('utf-8') - data_zypper_lu = __extract_lu_data(raw_zypper_lu) + ) + data_zypper_lu = [] + if raw_zypper_lu.returncode == 0: + data_zypper_lu = __extract_lu_data(raw_zypper_lu.stdout.decode('utf-8')) + else: + raise RuntimeError("zypper returned exit code %d" % raw_zypper_lu.returncode) raw_zypper_lp = subprocess.run( ['/usr/bin/zypper', '--quiet', 'lp'], stdout=subprocess.PIPE, check=False - ).stdout.decode('utf-8') - data_zypper_lp = __extract_lp_data(raw_zypper_lp) + ) + data_zypper_lp = [] + if raw_zypper_lp.returncode == 0: + data_zypper_lp = __extract_lp_data(raw_zypper_lp.stdout.decode('utf-8')) + else: + raise RuntimeError("zypper returned exit code %d" % raw_zypper_lp.returncode) raw_zypper_orphaned = subprocess.run( ['/usr/bin/zypper', '--quiet', 'pa', '--orphaned'], stdout=subprocess.PIPE, check=False - ).stdout.decode('utf-8') - data_zypper_orphaned = __extract_orphaned_data(raw_zypper_orphaned) + ) + data_zypper_orphaned = [] + if raw_zypper_orphaned.returncode == 0: + data_zypper_orphaned = __extract_orphaned_data(raw_zypper_orphaned.stdout.decode('utf-8')) + else: + raise RuntimeError("zypper returned exit code %d" % raw_zypper_orphaned.returncode) print_pending_updates(data_zypper_lu, args.all_info) From 2d5c31f20620d241c8bcda6af10e5cfbf36d15f5 Mon Sep 17 00:00:00 2001 From: Wabri <12409541+Wabri@users.noreply.github.com> Date: Sun, 27 Oct 2024 17:48:20 +0100 Subject: [PATCH 12/14] refactor(zypper.py): better error handling and increase readability Signed-off-by: Wabri <12409541+Wabri@users.noreply.github.com> --- zypper.py | 132 +++++++++++++++++++++++------------------------------- 1 file changed, 57 insertions(+), 75 deletions(-) diff --git a/zypper.py b/zypper.py index 8f68569c..d8d605c0 100755 --- a/zypper.py +++ b/zypper.py @@ -130,60 +130,70 @@ def print_zypper_version(): info.info({"zypper_version": result.split()[1]}) -def __extract_lu_data(raw: str): +def __extract_data(raw, fields): raw_lines = raw.splitlines()[2:] extracted_data = [] for line in raw_lines: parts = [part.strip() for part in line.split('|')] - if len(parts) >= 5: + if len(parts) >= max(fields.values()) + 1: extracted_data.append({ - "Repository": parts[1], - "Name": parts[2], - "Current Version": parts[3], - "Available Version": parts[4], - "Arch": parts[5] + field: parts[index] for field, index in fields.items() }) return extracted_data -def __extract_lp_data(raw: str): - raw_lines = raw.splitlines()[2:] - extracted_data = [] +def stdout_zypper_command(command): + result = subprocess.run( + command, + stdout=subprocess.PIPE, + check=False + ) - 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] - }) + if result.returncode != 0: + raise RuntimeError(f"zypper returned exit code {result.returncode}") - return extracted_data + return result.stdout.decode('utf-8') -def __extract_orphaned_data(raw: str): - raw_lines = raw.splitlines()[2:] - extracted_data = [] +def extract_lu_data(raw: str): + fields = { + "Repository": 1, + "Name": 2, + "Current Version": 3, + "Available Version": 4, + "Arch": 5 + } - for line in raw_lines: - parts = [part.strip() for part in line.split('|')] - if len(parts) >= 5: - extracted_data.append({ - "Name": parts[3], - "Version": parts[4] - }) + return __extract_data(raw, fields) - return extracted_data + +def extract_lp_data(raw: str): + fields = { + "Repository": 0, + "Name": 1, + "Category": 2, + "Severity": 3, + "Interactive": 4, + "Status": 5 + } + + return __extract_data(raw, fields) + + +def extract_orphaned_data(raw: str): + fields = { + "Name": 3, + "Version": 4 + } + + return __extract_data(raw, fields) def __parse_arguments(argv): parser = argparse.ArgumentParser() + parser.add_mutually_exclusive_group(required=False) parser.add_argument( "-m", @@ -200,74 +210,46 @@ def __parse_arguments(argv): 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( - ['/usr/bin/zypper', '--quiet', 'lu'], - stdout=subprocess.PIPE, - check=False + data_zypper_lu = extract_lu_data( + stdout_zypper_command(['/usr/bin/zypper', '--quiet', 'lu']) ) - data_zypper_lu = [] - if raw_zypper_lu.returncode == 0: - data_zypper_lu = __extract_lu_data(raw_zypper_lu.stdout.decode('utf-8')) - else: - raise RuntimeError("zypper returned exit code %d" % raw_zypper_lu.returncode) - - raw_zypper_lp = subprocess.run( - ['/usr/bin/zypper', '--quiet', 'lp'], - stdout=subprocess.PIPE, - check=False + data_zypper_lp = extract_lp_data( + stdout_zypper_command(['/usr/bin/zypper', '--quiet', 'lp']) ) - data_zypper_lp = [] - if raw_zypper_lp.returncode == 0: - data_zypper_lp = __extract_lp_data(raw_zypper_lp.stdout.decode('utf-8')) - else: - raise RuntimeError("zypper returned exit code %d" % raw_zypper_lp.returncode) - - raw_zypper_orphaned = subprocess.run( - ['/usr/bin/zypper', '--quiet', 'pa', '--orphaned'], - stdout=subprocess.PIPE, - check=False + data_zypper_orphaned = extract_orphaned_data( + stdout_zypper_command(['/usr/bin/zypper', '--quiet', 'pa', '--orphaned']) ) - data_zypper_orphaned = [] - if raw_zypper_orphaned.returncode == 0: - data_zypper_orphaned = __extract_orphaned_data(raw_zypper_orphaned.stdout.decode('utf-8')) - else: - raise RuntimeError("zypper returned exit code %d" % raw_zypper_orphaned.returncode) - - print_pending_updates(data_zypper_lu, args.all_info) - - print_data_sum(data_zypper_lu, "zypper_updates_pending_total", "zypper packages updates available in total") - - print_pending_patches(data_zypper_lp, args.all_info) + print_pending_updates(data_zypper_lu, + args.all_info) + print_data_sum(data_zypper_lu, + "zypper_updates_pending_total", + "zypper packages updates available in total") + print_pending_patches(data_zypper_lp, + args.all_info) print_data_sum(data_zypper_lp, "zypper_patches_pending_total", "zypper patches available total") - print_data_sum(data_zypper_lp, "zypper_patches_pending_security_total", "zypper patches available with category security total", filters={'Category': 'security'}) - print_data_sum(data_zypper_lp, "zypper_patches_pending_security_important_total", "zypper patches available with category security severity important total", filters={'Category': 'security', 'Severity': 'important'}) - print_data_sum(data_zypper_lp, "zypper_patches_pending_reboot_total", "zypper patches available which require reboot total", filters={'Interactive': 'reboot'}) - print_reboot_required() - print_zypper_version() - print_orphaned_packages(data_zypper_orphaned) return 0 From 8b2a2f295d75b4cd2bd111871f6566fcb75ec5f9 Mon Sep 17 00:00:00 2001 From: Wabri <12409541+Wabri@users.noreply.github.com> Date: Sun, 27 Oct 2024 17:56:36 +0100 Subject: [PATCH 13/14] feat(zypper.py): add stderr to raise error of zypper subprocess Signed-off-by: Wabri <12409541+Wabri@users.noreply.github.com> --- zypper.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/zypper.py b/zypper.py index d8d605c0..a3924e33 100755 --- a/zypper.py +++ b/zypper.py @@ -113,7 +113,6 @@ def print_reboot_required(): stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False) - if result.returncode == 0: info.info({"node_reboot_required": "0"}) else: @@ -152,7 +151,7 @@ def stdout_zypper_command(command): ) if result.returncode != 0: - raise RuntimeError(f"zypper returned exit code {result.returncode}") + raise RuntimeError(f"zypper returned exit code {result.returncode}: {result.stderr}") return result.stdout.decode('utf-8') From 65e2e6dafeb83c10a1e4fc5fb35873c39ae1cc65 Mon Sep 17 00:00:00 2001 From: Wabri <12409541+Wabri@users.noreply.github.com> Date: Mon, 28 Oct 2024 11:30:28 +0100 Subject: [PATCH 14/14] fix(zypper.py): fix linting problems Signed-off-by: Wabri <12409541+Wabri@users.noreply.github.com> --- zypper.py | 48 +++++++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/zypper.py b/zypper.py index a3924e33..34b26bb4 100755 --- a/zypper.py +++ b/zypper.py @@ -45,12 +45,18 @@ def __print_pending_data(data, fields, info, filters=None): def print_pending_updates(data, all_info, filters=None): if all_info: - fields = [("Repository", "repository"), ("Name", "package-name"), - ("Available Version", "available-version")] + fields = [("Repository", "repository"), + ("Name", "package-name"), + ("Available Version", + "available-version")] else: - fields = [("Repository", "repository"), ("Name", "package-name")] + fields = [("Repository", "repository"), + ("Name", "package-name")] prefix = "zypper_update_pending" - description = "zypper package update available from repository. (0 = not available, 1 = available)" + description = ( + "zypper package update available from repository. " + "(0 = not available, 1 = available)" + ) info = Info(prefix, description) __print_pending_data(data, fields, info, filters) @@ -58,15 +64,17 @@ def print_pending_updates(data, all_info, filters=None): def print_pending_patches(data, all_info, filters=None): if all_info: - fields = [ - ("Repository", "repository"), ("Name", "patch-name"), ("Category", "category"), - ("Severity", "severity"), ("Interactive", "interactive"), ("Status", "status") - ] + fields = [("Repository", "repository"), + ("Name", "patch-name"), + ("Category", "category"), + ("Severity", "severity"), + ("Interactive", "interactive"), + ("Status", "status")] else: - fields = [ - ("Repository", "repository"), ("Name", "patch-name"), - ("Interactive", "interactive"), ("Status", "status") - ] + fields = [("Repository", "repository"), + ("Name", "patch-name"), + ("Interactive", "interactive"), + ("Status", "status")] prefix = "zypper_patch_pending" description = "zypper patch available from repository. (0 = not available , 1 = available)" info = Info(prefix, description) @@ -75,9 +83,8 @@ def print_pending_patches(data, all_info, filters=None): def print_orphaned_packages(data, filters=None): - fields = [ - ("Name", "package"), ("Version", "installed-version") - ] + fields = [("Name", "package"), + ("Version", "installed-version")] prefix = "zypper_package_orphan" description = "zypper packages with no update source (orphaned)" info = Info(prefix, description) @@ -87,9 +94,9 @@ def print_orphaned_packages(data, filters=None): def print_data_sum(data, prefix, description, filters=None): gauge = Gauge(prefix, - description, - namespace=NAMESPACE, - registry=REGISTRY) + description, + namespace=NAMESPACE, + registry=REGISTRY) filters = filters or {} if len(data) == 0: gauge.set(0) @@ -106,7 +113,10 @@ def print_reboot_required(): if is_path_ok: prefix = "node_reboot_required" - description = "Node require reboot to activate installed updates or patches. (0 = not needed, 1 = needed)" + description = ( + "Node require reboot to activate installed updates or patches. " + "(0 = not needed, 1 = needed)" + ) info = Info(prefix, description) result = subprocess.run( [needs_restarting_path, '-r'],