|
| 1 | +#!/usr/bin/env python3 |
| 2 | + |
| 3 | +""" |
| 4 | +Description: #TODO: short version |
| 5 | +
|
| 6 | +#TODO: long version |
| 7 | +
|
| 8 | +#TODO: example |
| 9 | +
|
| 10 | + #TODO: code of the example |
| 11 | +
|
| 12 | +#TODO: additional information |
| 13 | +
|
| 14 | +Dependencies: #TODO: add if needed |
| 15 | +
|
| 16 | +Authors: Gabriele Puliti <[email protected]> |
| 17 | + |
| 18 | +""" |
| 19 | + |
| 20 | +import argparse |
| 21 | +import subprocess |
| 22 | +import os |
| 23 | + |
| 24 | +from collections.abc import Sequence |
| 25 | + |
| 26 | +def __print_package_info(package, fields, prefix, filters=None): |
| 27 | + filters = filters or {} |
| 28 | + check = all(package.get(k) == v for k, v in filters.items()) |
| 29 | + |
| 30 | + if check: |
| 31 | + field_str = ",".join([f'{name}="{package[field]}"' for field, name in fields]) |
| 32 | + print(f"{prefix}{{{field_str}}} 1") |
| 33 | + |
| 34 | +def __print_pending_data(data, all_info, fields_more, fields_less, prefix, filters=None): |
| 35 | + if all_info: |
| 36 | + fields = fields_more |
| 37 | + else: |
| 38 | + fields = fields_less |
| 39 | + |
| 40 | + if len(data) == 0: |
| 41 | + field_str = ",".join([f'{name}=""' for _, name in fields]) |
| 42 | + print(f"{prefix}{{{field_str}}} 0") |
| 43 | + else: |
| 44 | + for package in data: |
| 45 | + __print_package_info(package, fields, prefix, filters) |
| 46 | + |
| 47 | +def print_pending_updates(data, all_info, filters=None): |
| 48 | + fields_more = [("Repository", "repository"), ("Name", "package-name"), |
| 49 | + ("Available Version", "available-version")] |
| 50 | + fields_less = [("Repository", "repository"), ("Name", "package-name")] |
| 51 | + prefix = "zypper_update_pending" |
| 52 | + |
| 53 | + __print_pending_data(data, all_info, fields_more, fields_less, prefix, filters) |
| 54 | + |
| 55 | +def print_pending_patches(data, all_info, filters=None): |
| 56 | + fields_more = [ |
| 57 | + ("Repository", "repository"), ("Name", "patch-name"), ("Category", "category"), |
| 58 | + ("Severity", "severity"), ("Interactive", "interactive"), ("Status", "status") |
| 59 | + ] |
| 60 | + fields_less = [ |
| 61 | + ("Repository", "repository"), ("Name", "patch-name"), |
| 62 | + ("Interactive", "interactive"), ("Status", "status") |
| 63 | + ] |
| 64 | + prefix = "zypper_patch_pending" |
| 65 | + |
| 66 | + __print_pending_data(data, all_info, fields_more, fields_less, prefix, filters) |
| 67 | + |
| 68 | +def print_orphaned_packages(data): |
| 69 | + fields = [ |
| 70 | + ("Package", "package"), ("Installed Version", "installed-version") |
| 71 | + ] |
| 72 | + prefix = "zypper_package_orphan" |
| 73 | + |
| 74 | + __print_pending_data(data, True, fields, None, prefix, None) |
| 75 | + |
| 76 | +def __print_data_sum(data, prefix, filters=None): |
| 77 | + filters = filters or {} |
| 78 | + if len(data) == 0: |
| 79 | + print(prefix + "{total} 0") |
| 80 | + else: |
| 81 | + gauge = 0 |
| 82 | + for package in data: |
| 83 | + check = all(package.get(k) == v for k, v in filters.items()) |
| 84 | + if check: |
| 85 | + gauge += 1 |
| 86 | + print(prefix + "{total} " + str(gauge)) |
| 87 | + |
| 88 | +def print_updates_sum(data, filters=None): |
| 89 | + prefix = "zypper_updates_pending_total" |
| 90 | + |
| 91 | + __print_data_sum(data, prefix, filters) |
| 92 | + |
| 93 | +def print_patches_sum(data, prefix="zypper_patches_pending_total", filters=None): |
| 94 | + __print_data_sum(data, prefix, filters) |
| 95 | + |
| 96 | +def print_reboot_required(): |
| 97 | + needs_restarting_path = '/usr/bin/needs-restarting' |
| 98 | + is_path_ok = os.path.isfile(needs_restarting_path) and os.access(needs_restarting_path, os.X_OK) |
| 99 | + |
| 100 | + if is_path_ok: |
| 101 | + result = subprocess.run( |
| 102 | + [needs_restarting_path, '-r'], |
| 103 | + stdout=subprocess.DEVNULL, |
| 104 | + stderr=subprocess.DEVNULL, |
| 105 | + check=False) |
| 106 | + |
| 107 | + print('# HELP node_reboot_required Node require reboot to activate installed updates or patches. (0 = not needed, 1 = needed)') |
| 108 | + print('# TYPE node_reboot_required gauge') |
| 109 | + if result.returncode == 0: |
| 110 | + print('node_reboot_required 0') |
| 111 | + else: |
| 112 | + print('node_reboot_required 1') |
| 113 | + |
| 114 | +def print_zypper_version(): |
| 115 | + result = subprocess.run( |
| 116 | + ['/usr/bin/zypper', '-V'], |
| 117 | + stdout=subprocess.PIPE, |
| 118 | + check=False).stdout.decode('utf-8') |
| 119 | + |
| 120 | + print("zypper_version " + result.split()[1]) |
| 121 | + |
| 122 | + |
| 123 | +def __extract_lu_data(raw: str): |
| 124 | + raw_lines = raw.splitlines()[2:] |
| 125 | + extracted_data = [] |
| 126 | + |
| 127 | + for line in raw_lines: |
| 128 | + parts = [part.strip() for part in line.split('|')] |
| 129 | + if len(parts) >= 5: |
| 130 | + extracted_data.append({ |
| 131 | + "Repository": parts[1], |
| 132 | + "Name": parts[2], |
| 133 | + "Current Version": parts[3], |
| 134 | + "Available Version": parts[4], |
| 135 | + "Arch": parts[5] |
| 136 | + }) |
| 137 | + |
| 138 | + return extracted_data |
| 139 | + |
| 140 | +def __extract_lp_data(raw: str): |
| 141 | + raw_lines = raw.splitlines()[2:] |
| 142 | + extracted_data = [] |
| 143 | + |
| 144 | + for line in raw_lines: |
| 145 | + parts = [part.strip() for part in line.split('|')] |
| 146 | + if len(parts) >= 5: |
| 147 | + extracted_data.append({ |
| 148 | + "Repository": parts[0], |
| 149 | + "Name": parts[1], |
| 150 | + "Category": parts[2], |
| 151 | + "Severity": parts[3], |
| 152 | + "Interactive": parts[4], |
| 153 | + "Status": parts[5] |
| 154 | + }) |
| 155 | + |
| 156 | + return extracted_data |
| 157 | + |
| 158 | +def __extract_orphaned_data(raw: str): |
| 159 | + raw_lines = raw.splitlines()[2:] |
| 160 | + extracted_data = [] |
| 161 | + |
| 162 | + for line in raw_lines: |
| 163 | + parts = [part.strip() for part in line.split('|')] |
| 164 | + if len(parts) >= 5: |
| 165 | + extracted_data.append({ |
| 166 | + "Package": parts[3], |
| 167 | + "Installed Version": parts[5] |
| 168 | + }) |
| 169 | + |
| 170 | + return extracted_data |
| 171 | + |
| 172 | +def __parse_arguments(argv): |
| 173 | + parser = argparse.ArgumentParser() |
| 174 | + parser.add_mutually_exclusive_group(required=False) |
| 175 | + parser.add_argument( |
| 176 | + "-m", |
| 177 | + "--more", |
| 178 | + dest="all_info", |
| 179 | + action='store_true', |
| 180 | + help="Print all the package infos", |
| 181 | + ) |
| 182 | + parser.add_argument( |
| 183 | + "-l", |
| 184 | + "--less", |
| 185 | + dest="all_info", |
| 186 | + action='store_false', |
| 187 | + help="Print less package infos", |
| 188 | + ) |
| 189 | + parser.set_defaults(all_info=True) |
| 190 | + return parser.parse_args(argv) |
| 191 | + |
| 192 | +def main(argv: Sequence[str] | None = None) -> int: |
| 193 | + args = __parse_arguments(argv) |
| 194 | + |
| 195 | + raw_zypper_lu = subprocess.run( |
| 196 | + ['cat', 'testlu.txt'], |
| 197 | + #['/usr/bin/zypper', '--quiet', 'lu'], |
| 198 | + stdout=subprocess.PIPE, |
| 199 | + check=False |
| 200 | + ).stdout.decode('utf-8') |
| 201 | + data_zypper_lu = __extract_lu_data(raw_zypper_lu) |
| 202 | + |
| 203 | + raw_zypper_lp = subprocess.run( |
| 204 | + ['cat', 'testlp.txt'], |
| 205 | + #['/usr/bin/zypper', '--quiet', 'lp'], |
| 206 | + stdout=subprocess.PIPE, |
| 207 | + check=False |
| 208 | + ).stdout.decode('utf-8') |
| 209 | + data_zypper_lp = __extract_lp_data(raw_zypper_lp) |
| 210 | + |
| 211 | + raw_zypper_lp = subprocess.run( |
| 212 | + ['cat', 'testlp.txt'], |
| 213 | + #['/usr/bin/zypper', '--quiet', 'lp'], |
| 214 | + stdout=subprocess.PIPE, |
| 215 | + check=False |
| 216 | + ).stdout.decode('utf-8') |
| 217 | + data_zypper_lp = __extract_lp_data(raw_zypper_lp) |
| 218 | + |
| 219 | + raw_zypper_orphaned = subprocess.run( |
| 220 | + ['cat', 'testlp.txt'], |
| 221 | + #['/usr/bin/zypper', '--quiet', 'pa', --orphaned'], |
| 222 | + stdout=subprocess.PIPE, |
| 223 | + check=False |
| 224 | + ).stdout.decode('utf-8') |
| 225 | + data_zypper_orphaned = __extract_orphaned_data(raw_zypper_orphaned) |
| 226 | + |
| 227 | + print('# HELP zypper_update_pending zypper package update available from repository. (0 = not available, 1 = available)') |
| 228 | + print('# TYPE zypper_update_pending gauge') |
| 229 | + print_pending_updates(data_zypper_lu, args.all_info) |
| 230 | + |
| 231 | + print('# HELP zypper_updates_pending_total zypper packages updates available in total') |
| 232 | + print('# TYPE zypper_updates_pending_total counter') |
| 233 | + print_updates_sum(data_zypper_lu) |
| 234 | + |
| 235 | + print('# HELP zypper_patch_pending zypper patch available from repository. (0 = not available, 1 = available)') |
| 236 | + print('# TYPE zypper_patch_pending gauge') |
| 237 | + print_pending_patches(data_zypper_lp, args.all_info) |
| 238 | + |
| 239 | + print('# HELP zypper_patches_pending_total zypper patches available total') |
| 240 | + print('# TYPE zypper_patches_pending_total counter') |
| 241 | + print_patches_sum(data_zypper_lp) |
| 242 | + |
| 243 | + print('# HELP zypper_patches_pending_security_total zypper patches available with category security total') |
| 244 | + print('# TYPE zypper_patches_pending_security_total counter') |
| 245 | + print_patches_sum(data_zypper_lp, |
| 246 | + prefix="zypper_patches_pending_security_total", |
| 247 | + filters={'Category':'security'}) |
| 248 | + |
| 249 | + print('# HELP zypper_patches_pending_security_important_total zypper patches available with category security severity important total') |
| 250 | + print('# TYPE zypper_patches_pending_security_important_total counter') |
| 251 | + print_patches_sum(data_zypper_lp, |
| 252 | + prefix="zypper_patches_pending_security_important_total", |
| 253 | + filters={'Category':'security', 'Severity': 'important'}) |
| 254 | + |
| 255 | + print('# HELP zypper_patches_pending_reboot_total zypper patches available which require reboot total') |
| 256 | + print('# TYPE zypper_patches_pending_reboot_total counter') |
| 257 | + print_patches_sum(data_zypper_lp, |
| 258 | + prefix="zypper_patches_pending_reboot_total", |
| 259 | + filters={'Interactive': 'reboot'}) |
| 260 | + |
| 261 | + print_reboot_required() |
| 262 | + |
| 263 | + print('# HELP zypper_version zypper installed package version') |
| 264 | + print('# TYPE zypper_version gauges') |
| 265 | + print_zypper_version() |
| 266 | + |
| 267 | + print('# HELP zypper_package_orphan zypper packages with no update source (orphaned)') |
| 268 | + print('# TYPE zypper_package_orphan gauges') |
| 269 | + print_orphaned_packages(data_zypper_orphaned) |
| 270 | + |
| 271 | + return 0 |
| 272 | + |
| 273 | + |
| 274 | +if __name__ == "__main__": |
| 275 | + raise SystemExit(main()) |
0 commit comments