Letzte Woche habe ich Version 0.2.1 des Zypper-Patch-Status-Collectors veröffentlicht. Mit diesem kleinen, in Python geschriebenen, Helfer lässt sich der Aktualisierungsstand eines openSUSE-Systems durch Prometheus überwachen. Prometheus kann so nicht nur Alarme generieren wenn Sicherheitsaktualisierungen noch nicht angewandt sind, sondern auch wenn einzelne Dienste nach Aktualisierungen noch nicht neu gestartet wurden oder das System neu gestartet werden müsste.

Der Collector nutzt Zypper um nach ausstehen Patches und Service- oder Systemneustartbedarf zu schauen und gibt diese Information im Format von Prometheus-Metriken aus. Dies sieht dann so aus wie in der README beschrieben:

# HELP zypper_applicable_patches The current count of applicable patches
# TYPE zypper_applicable_patches gauge
zypper_applicable_patches{category="security",severity="critical"} 0
zypper_applicable_patches{category="security",severity="important"} 2
zypper_applicable_patches{category="security",severity="moderate"} 0
zypper_applicable_patches{category="security",severity="low"} 0
zypper_applicable_patches{category="security",severity="unspecified"} 0
zypper_applicable_patches{category="recommended",severity="critical"} 0
zypper_applicable_patches{category="recommended",severity="important"} 0
zypper_applicable_patches{category="recommended",severity="moderate"} 0
zypper_applicable_patches{category="recommended",severity="low"} 0
zypper_applicable_patches{category="recommended",severity="unspecified"} 0
zypper_applicable_patches{category="optional",severity="critical"} 0
zypper_applicable_patches{category="optional",severity="important"} 0
zypper_applicable_patches{category="optional",severity="moderate"} 0
zypper_applicable_patches{category="optional",severity="low"} 0
zypper_applicable_patches{category="optional",severity="unspecified"} 0
zypper_applicable_patches{category="feature",severity="critical"} 0
zypper_applicable_patches{category="feature",severity="important"} 0
zypper_applicable_patches{category="feature",severity="moderate"} 0
zypper_applicable_patches{category="feature",severity="low"} 0
zypper_applicable_patches{category="feature",severity="unspecified"} 0
zypper_applicable_patches{category="document",severity="critical"} 0
zypper_applicable_patches{category="document",severity="important"} 0
zypper_applicable_patches{category="document",severity="moderate"} 0
zypper_applicable_patches{category="document",severity="low"} 0
zypper_applicable_patches{category="document",severity="unspecified"} 0
zypper_applicable_patches{category="yast",severity="critical"} 0
zypper_applicable_patches{category="yast",severity="important"} 0
zypper_applicable_patches{category="yast",severity="moderate"} 0
zypper_applicable_patches{category="yast",severity="low"} 0
zypper_applicable_patches{category="yast",severity="unspecified"} 0
# HELP zypper_service_needs_restart Set to 1 if service requires a restart due to using no-longer-existing libraries.
# TYPE zypper_service_needs_restart gauge
zypper_service_needs_restart{service="nscd"} 1
zypper_service_needs_restart{service="dbus"} 1
zypper_service_needs_restart{service="cups"} 1
zypper_service_needs_restart{service="sshd"} 1
zypper_service_needs_restart{service="cron"} 1
# HELP zypper_product_end_of_life Unix timestamp on when support for the product will end.
# TYPE zypper_product_end_of_life gauge
zypper_product_end_of_life{product="openSUSE"} 1606694400
zypper_product_end_of_life{product="openSUSE_Addon_NonOss"} 1000000000000001
# HELP zypper_needs_rebooting Whether the system requires a reboot as core libraries or services have been updated.
# TYPE zypper_needs_rebooting gauge
zypper_needs_rebooting 0
# HELP zypper_scrape_success Whether the last scrape for zypper data was successful.
# TYPE zypper_scrape_success gauge
zypper_scrape_success 1

In diesem Beispiel stehen zwei Sicherheitspatches aus und mehrere Services, unter anderem der SSH Daemon, bräuchten einen Neustart weil sie noch mit bereits gelöschten, also durch ein Update ersetzten, Bibliotheken arbeiten.

Durch Umleiten der Ausgabe in eine prom Datei ins Textfile-Collector-Verzeichnis des Node-Exporters von Prometheus kommen die Metriken dann ins Monitoring. Wurde der Node-Exporter wie folgt aufgerufen:

node_exporter --collector.textfile.directory /var/lib/node_exporter/collector

Dann lassen sie die Metriken wie folgt in Prometheus abladen:

zypper-patch-status-collector > /var/lib/node_exporter/collector/zypper.prom

Stündlich per Systemd Timer aufgerufen hat Prometheus dann eine gute Übersicht über den Aktualisierungszustand der beobachteten openSUSE-Systeme.

Sind alle Metriken in Prometheus lassen sich dann auch verschiedene nützliche Alarme definieren. Das folgende ist die Liste der auf dem Collector basierenden Alarme die ich momentan in meinem Alertmanager definiert habe:

- alert: 'ZypperPatchesPending'
  expr: 'sum(zypper_applicable_patches) by (instance) > 0'
  for: '5m'
  labels:
    alert_severity: 'warning'
  annotations:
    summary: 'There are new patches available for {{ $labels.instance }}.'
    description: 'Run `zypper patch --with-update` on {{ $labels.instance }}.'
- alert: 'ZypperCriticalPatchesPending'
  expr: 'sum(zypper_applicable_patches{category="security"}) by (instance) + sum(zypper_applicable_patches{severity="critical"}) by (instance) > 0'
  for: '5m'
  labels:
    alert_severity: 'page'
  annotations:
    summary: 'There are security patches pending on {{ $labels.instance }}.'
    description: 'Run `zypper patch --with-update` on {{ $labels.instance }}.'
- alert: 'ZypperSuggestsServiceRestart'
  expr: 'zypper_service_needs_restart'
  for: '5m'
  labels:
    alert_severity: 'warning'
  annotations:
    summary: 'Zypper suggest to restart {{ $labels.service }} on {{ $labels.instance }}.'
    description: 'Run `systemctl restart {{ $labels.service }}` on {{ $labels.instance }}.'
- alert: 'ZypperSuggestsReboot'
  expr: 'zypper_needs_rebooting != 0'
  for: '5m'
  labels:
    alert_severity: 'warning'
  annotations:
    summary: 'Zypper suggest to reboot {{ $labels.instance }}.'
    description: 'Run `systemctl reboot` on {{ $labels.instance }}.'
- alert: 'ProductEndOfLifeNear'
  expr: 'zypper_product_end_of_life < time() + 4 * 7 * 24 * 3600'
  for: '5m'
  labels:
    alert_severity: 'warning'
  annotations:
    summary: '{{ $labels.product }} on {{ $labels.instance }} reaches end of life within four weeks.'
    description: 'Upgrade {{ $labels.product }} on {{ $labels.instance }} to the next version.'
- alert: 'ProductEndOfLifeReached'
  expr: 'zypper_product_end_of_life < time()'
  for: '5m'
  labels:
    alert_severity: 'page'
  annotations:
    summary: '{{ $labels.product }} on {{ $labels.instance }} reached end of life.'
    description: 'Upgrade {{ $labels.product }} on {{ $labels.instance }} to the next version.'
- alert: 'ZypperPatchDataOutdated'
  expr: 'time() - node_textfile_mtime{file="zypper.prom"} > 24 * 3600'
  for: '5m'
  labels:
    alert_severity: 'page'
  annotations:
    summary: 'Patch status has not been updated for 24 hours.'
    description: |
      The patch status of {{ $labels.instance }} has not been updated for 24 hours. Check the status of the timer and the service:
        systemctl status zypper-patch-status-collector.timer
        systemctl status zypper-patch-status-collector.service
- alert: 'ZypperScrapeFailed'
  expr: 'zypper_scrape_success == 0'
  for: '24h'
  labels:
    alert_severity: 'page'
  annotations:
    summary: 'Failed to successfully query patch status for 24 hours now.'
    description: |
      Querying zypper for the current patch status has not been successful for 24 hours. Check the status of the service:
        systemctl status zypper-patch-status-collector.service

Die Installation des Collectors erfolgt am einfachsten über mein Community-Paket auf software.opensuse.org. Ich veröffentliche das Paket zwar auch auf pypi.org, aber ein Werkzeug mit Bezug zu Zypper am System vorbei zu installieren wäre dann doch etwas sehr verquer.