diff --git a/zypper.py b/zypper.py new file mode 100755 index 0000000..34b26bb --- /dev/null +++ b/zypper.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python3 + +""" +Description: Expose metrics from zypper updates and patches. + +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. + +Examples: + + zypper.py --less + zypper.py -m + +Authors: Gabriele Puliti + Bernd Shubert +""" + +import argparse +import subprocess +import os +import sys + +from collections.abc import Sequence +from prometheus_client import CollectorRegistry, Gauge, Info, generate_latest + +REGISTRY = CollectorRegistry() +NAMESPACE = "zypper" + + +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]) + info.info({field_str: '0'}) + else: + for package in data: + 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): + if all_info: + fields = [("Repository", "repository"), + ("Name", "package-name"), + ("Available Version", + "available-version")] + 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, fields, info, filters) + + +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")] + 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, fields, info, filters) + + +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, fields, info, filters) + + +def print_data_sum(data, prefix, description, filters=None): + gauge = Gauge(prefix, + description, + namespace=NAMESPACE, + registry=REGISTRY) + filters = filters or {} + if len(data) == 0: + gauge.set(0) + else: + for package in data: + check = all(package.get(k) == v for k, v in filters.items()) + if check: + gauge.inc() + + +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: + 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) + if result.returncode == 0: + info.info({"node_reboot_required": "0"}) + else: + info.info({"node_reboot_required": "1"}) + + +def print_zypper_version(): + result = subprocess.run( + ['/usr/bin/zypper', '-V'], + stdout=subprocess.PIPE, + check=False).stdout.decode('utf-8') + info = Info("zypper_version", "zypper installed package version") + + info.info({"zypper_version": result.split()[1]}) + + +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) >= max(fields.values()) + 1: + extracted_data.append({ + field: parts[index] for field, index in fields.items() + }) + + return extracted_data + + +def stdout_zypper_command(command): + result = subprocess.run( + command, + stdout=subprocess.PIPE, + check=False + ) + + if result.returncode != 0: + raise RuntimeError(f"zypper returned exit code {result.returncode}: {result.stderr}") + + return result.stdout.decode('utf-8') + + +def extract_lu_data(raw: str): + fields = { + "Repository": 1, + "Name": 2, + "Current Version": 3, + "Available Version": 4, + "Arch": 5 + } + + return __extract_data(raw, fields) + + +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", + "--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) + data_zypper_lu = extract_lu_data( + stdout_zypper_command(['/usr/bin/zypper', '--quiet', 'lu']) + ) + data_zypper_lp = extract_lp_data( + stdout_zypper_command(['/usr/bin/zypper', '--quiet', 'lp']) + ) + data_zypper_orphaned = extract_orphaned_data( + stdout_zypper_command(['/usr/bin/zypper', '--quiet', 'pa', '--orphaned']) + ) + + 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 + + +if __name__ == "__main__": + try: + main() + except Exception as e: + print("ERROR: {}".format(e), file=sys.stderr) + sys.exit(1) + + print(generate_latest(REGISTRY).decode(), end="") diff --git a/zypper.sh b/zypper.sh new file mode 100755 index 0000000..54d3279 --- /dev/null +++ b/zypper.sh @@ -0,0 +1,263 @@ +#!/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, doesn't work on all test systems +set -o nounset # fail if unset variables +set -o pipefail # reflect exit status + +if [[ "${1-}" =~ ^-*h(elp)?$ ]]; then + # 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! + +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 0 +fi + +# Check if we are root +if [ "$EUID" -ne 0 ]; then + echo "${0##*/}: Please run as root!" >&2 + exit 1 +fi + +# shellcheck disable=SC2016 +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 +} +' + +# shellcheck disable=SC2016 +filter_pending_patches=' +BEGIN { + FS=" \\| "; # set field separator to " | " +} + +NR { + # Extract and format repository, patch_name, category, severity, 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\",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 +} +' + +# shellcheck disable=SC2016 +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_patch_pending{repository=\"\",patch-name=\"\",category=\"\",severity=\"\",interactive=\"\",status=\"\"} 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 -c "| security" + fi + } | + awk '{print "zypper_patches_pending_security_total "$1}' +} + +get_pending_security_important_patches() { + { + if [ -z "$1" ]; then + echo "0" + else + echo "$1" | + grep -c "| security.*important" + fi + } | + awk '{print "zypper_patches_pending_security_important_total "$1}' +} + +get_pending_reboot_patches() { + { + if [ -z "$1" ]; then + echo "0" + else + echo "$1" | + grep -c "reboot" + 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 | sed -E '/(^$|^Repository|^---)/d'| sed '/|/!d')" + 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 gauge' + get_zypper_version "$zypper_version" + + echo '# HELP zypper_package_orphan zypper packages with no update source (orphaned) ' + echo '# TYPE zypper_package_orphan gauge' + get_orphan_packages "$zypper_orphan_packages" +} + +main "$@"