Eigenen Menüpunkt unter Setup für Plugins

Guten Morgen,

ich möchte die Frage aufwerfen, ob sich ein eigener Menüpunkt unter Setup realisieren lässt, um beispielsweise Plugin-Konfigurationen zentral zu verwalten.

Hintergrund: Wir haben ein Notification Plugin entwickelt, das per Button ServiceNow-Tickets generiert. Über einen API-Call mit entsprechenden Credentials/Token wird ein Ticket erstellt, das automatisch Host, Service und Status in der Beschreibung aufnimmt.

Aktuell sind die Zugangsdaten direkt im Script hinterlegt, was aus Sicherheits- und Wartungssicht suboptimal ist. Wünschenswert wäre eine Konfigurationsmöglichkeit analog zu Agent Plugins oder Rules, um sensible Daten wie Credentials, API-Endpoints etc. strukturiert zu hinterlegen. Da das Plugin global in CMK eingesetzt wird, erscheint ein dedizierter Menüpunkt als architektonisch sauberere Lösung.

Hat bereits jemand eine vergleichbare Implementierung vorgenommen und kann Erfahrungen teilen?

Viele Grüße

Das was du umsetzen möchtest sollte im CheckMK 2.4 über den Punkt “Parameters for notification methods” umsetzbar sein.

Hier sind für alle installierten Notification Plugins die jeweils möglichen Parameter konfigurierbar.

Dies bedeutet auch, dass man die Funktionen des Passwort Store verwenden kann um Credentials nicht im Klartext rumliegen zu lassen.

Hi,

habe eben erst euren Post gesehen … wie weit seit Ihr den damit ich habe bzw. bin an was ähnlichem zu SNOW dran

1. Parameter Erweiterung

Assigment Group und andere Parameter

Parameter
def handle_problem(url, proxies, user, pwd, short_desc, desc, hostname, servicename, problem_id,
                   caller, urgency, impact, timeout):
    url += "/api/now/table/incident"
    headers = {"Content-Type": "application/json", "Accept": "application/json"}
    response = requests.post(url,
                             proxies=proxies,
                             auth=(user, pwd),
                             headers=headers,
                             timeout=timeout,
                             json={
                                 "short_description": short_desc,
                                 "description": desc,
                                 "urgency": urgency,
                                 "impact": impact,
                                 "caller_id": caller,
                                 "u_requester": caller,                                 ####################custom made
                                 "category": "event",                                   ####################custom made
                                 "contact_type": "interface",                           ####################custom made
                                 "business_service": "ITPF Monitoring/Infrastructure",  ####################custom made
                                 "cmdb_ci": hostname,                                   ####################custom made
                                 "assignment_group": "it_production_control",           ####################custom made
                                 "work_notes": "Check_MK Problem ID: %s" % problem_id   ####################custom made
                             })

2. Ticket Nummer
wird als comment in CMK an den Host angehangen

TicketNr
   if response.status_code != 201:
        sys.stderr.write('Status: %s\n' % response.status_code)
        return 2
    incident_nr = response.json()["result"]["number"]
    sys.stdout.write('Ticket successfully created with incident number: %s.\n' % incident_nr)
    socket_path="unix:"+omd_root+"/tmp/run/live"
    connection=livestatus.SingleSiteConnection(socket_path)
    connection.command("[%s] ADD_HOST_COMMENT;%s;1;%s;Ticket created with IncidentNr: %s\n"%(int(time.time()),hostname,caller,incident_nr)) ####################custom made
    sys.stdout.write('Ticket successfully add comment\n')                              
    return 0

3. Snappin
Ticket Erstellung und Status

1 Like

(post deleted by author)

so also Punkt 1 ist ja schon im neuen SNOW Plugin mittels der custom_fields erledigt:

inwiefern dort Makros wie $HOSTNAME$, .. bzw.

Variablen aus dem Notification Script wie z. B.

@dataclass
class NotificationContext:
    hostname: str
    servicename: str
    problem_id: str    
    ack_author: str
    ack_comment: str
    short_desc: str
    desc: str

verarbeitet werden
"work_notes" - "Check_MK Problem ID: {notification_context.problem_id}"

werde ich dann mal ausgiebig testen …

Verwendung im API-Request

Die Custom Fields werden in den JSON-Body gemerged:

python

# In _get_endpoint_specific_context():
custom_fields = _get_custom_fields(raw_context, "PARAMETER_MGMT_TYPE_2_CUSTOM_FIELDS")

# In process_notifications():
response = requests.post(
    url=context.url_for_update(),
    json=context.json_for_problems | context.custom_fields,  # ← MERGE!
    #    ^^^^^^^^^^^^^^^^^^^^^^^^   ^^^^^^^^^^^^^^^^^^^^^
    #    Standard-Felder            Custom Fields
)

Resultierender JSON-Body an ServiceNow:

json

{
  "short_description": "Check_MK: server01 - DOWN",
  "description": "Host: server01\nOutput: Connection refused",
  "urgency": "3",
  "impact": "3",
  "caller_id": "monitoring",
  "work_notes": "Check_MK Problem ID: 12345",
  
  // ← AB HIER: Custom Fields aus WATO
  "u_requester": "monitoring",
  "cmdb_ci": "server01.example.com",
  "category": "event",
  "contact_type": "interface",
  "business_service": "ITPF Monitoring/Infrastructure",
  "assignment_group": "it_production_control"
}

python

# Code
payload = base_payload | context.custom_fields
#                        ^^^^^^^^^^^^^^^^^^^^^^
#                        Aus WATO geladen!

# WATO-GUI:
Custom Fields:
  [+] u_requester        = $PARAMETER_MGMT_TYPE_2_CALLER$
  [+] category           = event
  [+] contact_type       = interface
  [+] business_service   = ITPF Monitoring/Infrastructure
  [+] cmdb_ci            = $HOSTNAME$
  [+] assignment_group   = it_production_control

zu Punkt 2

Ticket Nr. als Comment an den HOST anhängen

GIT diff

diff

--git a/cmk/notification_plugins/servicenow.py b/cmk/notification_plugins/servicenow.py
index abc123..def456 100644
--- a/cmk/notification_plugins/servicenow.py
+++ b/cmk/notification_plugins/servicenow.py
@@ -6,9 +6,12 @@
 
 import sys
+import os
+import time
 from collections.abc import MutableMapping
 from dataclasses import dataclass
 from typing import Literal
 
 import requests
+import livestatus
 
 from cmk.utils.http_proxy_config import deserialize_http_proxy_config
@@ -300,6 +303,30 @@ def _transform_case_state(raw_spec: str) -> str:
     return CASE_STATES.get(raw_spec, "0")
 
 
+def _add_host_comment(
+    hostname: str,
+    caller: str,
+    incident_nr: str,
+) -> None:
+    """Add a host comment in Checkmk via Livestatus after creating an incident."""
+    try:
+        omd_root = os.getenv("OMD_ROOT", "")
+        socket_path = f"unix:{omd_root}/tmp/run/live"
+        conn = livestatus.SingleSiteConnection(socket_path)
+        conn.command(
+            f"[{int(time.time())}] ADD_HOST_COMMENT;{hostname};1;{caller};"
+            f"Ticket created with IncidentNr: {incident_nr}\n"
+        )
+        sys.stdout.write(f"Host comment added for {hostname}: {incident_nr}\n")
+    except Exception as e:
+        sys.stderr.write(f"WARNING: Failed to add host comment: {e}\n")
+
+
 def _is_valid_status_code(response: requests.models.Response, expected: int | None = None) -> bool:
     """Check response code of request and log error details in case of failure"""
     expected_code = expected if expected else 200
@@ -360,6 +387,19 @@ def process_notifications(
                 timeout=timeout,
                 json=context.json_for_problems | context.custom_fields,
             )
+            
+            # Add host comment after successful incident creation
+            if (
+                not isinstance(response, Errors)
+                and _is_valid_status_code(response, 201)
+                and isinstance(context.incident, Incident)
+            ):
+                try:
+                    incident_nr = response.json()["result"]["number"]
+                    _add_host_comment(
+                        hostname=context.notification_context.hostname,
+                        caller=context.incident.caller,
+                        incident_nr=incident_nr,
+                    )
+                except (KeyError, ValueError) as e:
+                    sys.stderr.write(f"WARNING: Could not extract incident number: {e}\n")
         else:
             sys.stdout.write(

1. Imports erweitern (nach Zeile 10)

Imports erweitern
# Bestehende Imports:
import sys
from collections.abc import MutableMapping
from dataclasses import dataclass
from typing import Literal

import requests

# ►►► NEU HINZUFÜGEN: ◄◄◄
import os           # NEU
import time         # NEU  
import livestatus   # NEU

from cmk.utils.http_proxy_config import deserialize_http_proxy_config
# ... (Rest der Imports)

2. Neue Funktion _add_host_comment() einfügen

Position: Nach den Helper-Funktionen (nach _transform_case_state), vor process_notifications()

Neue Funktion

python

def _transform_case_state(raw_spec: str) -> str:
    if raw_spec.isnumeric():
        return raw_spec
    return CASE_STATES.get(raw_spec, "0")


# ═══════════════════════════════════════════════════════════════════════════
# ►►► NEU: Host-Kommentar-Feature ◄◄◄
# ═══════════════════════════════════════════════════════════════════════════

def _add_host_comment(
    hostname: str,
    caller: str,
    incident_nr: str,
) -> None:
    """
    Add a host comment in Checkmk via Livestatus after creating an incident.
    
    Only logs a warning if it fails - the incident was already created successfully.
    
    Args:
        hostname: Name of the host
        caller: User who created the incident
        incident_nr: ServiceNow incident number (e.g., "INC0012345")
    """
    try:
        omd_root = os.getenv("OMD_ROOT", "")
        socket_path = f"unix:{omd_root}/tmp/run/live"
        conn = livestatus.SingleSiteConnection(socket_path)
        conn.command(
            f"[{int(time.time())}] ADD_HOST_COMMENT;{hostname};1;{caller};"
            f"Ticket created with IncidentNr: {incident_nr}\n"
        )
        sys.stdout.write(f"Host comment added for {hostname}: {incident_nr}\n")
    except Exception as e:
        # Only warning - the incident was created successfully
        sys.stderr.write(f"WARNING: Failed to add host comment: {e}\n")


# ═══════════════════════════════════════════════════════════════════════════

def process_notifications(
    context: Context,
) -> int:
    """Main processing of notifications for this API endpoint."""
    # ... (bestehender Code)

3. Host-Kommentar nach Ticket-Erstellung aufrufen

Position: In process_notifications(), nach dem erfolgreichen CREATE-Request

process_notifications

Suche nach:

python

    if issue is None:
        if notification_type == "PROBLEM":
            # Create new Issue
            expected_response_code = 201
            response, rc = _do_request(
                method="POST",
                url=context.url_for_update(),
                proxies=proxies,
                auth=auth,
                headers=headers,
                timeout=timeout,
                json=context.json_for_problems | context.custom_fields,
            )

Ersetze mit:

python

    if issue is None:
        if notification_type == "PROBLEM":
            # Create new Issue
            expected_response_code = 201
            response, rc = _do_request(
                method="POST",
                url=context.url_for_update(),
                proxies=proxies,
                auth=auth,
                headers=headers,
                timeout=timeout,
                json=context.json_for_problems | context.custom_fields,
            )
            
            # ►►► NEU: Add host comment after successful incident creation ◄◄◄
            if (
                not isinstance(response, Errors)
                and _is_valid_status_code(response, 201)
                and isinstance(context.incident, Incident)  # Only for incidents, not cases
            ):
                try:
                    incident_nr = response.json()["result"]["number"]
                    _add_host_comment(
                        hostname=context.notification_context.hostname,
                        caller=context.incident.caller,
                        incident_nr=incident_nr,
                    )
                except (KeyError, ValueError) as e:
                    sys.stderr.write(f"WARNING: Could not extract incident number: {e}\n"

Warum isinstance(context.incident, Incident)?

Details

Das offizielle Plugin unterstützt zwei Arten von Issues:

  • Incidents (haben einen caller)
  • Cases (haben KEINEN caller)

Die Context-Dataclass enthält:

python

@dataclass
class Context:
    incident: Incident | Case  # ← Union-Type!
    notification_context: NotificationContext
    # ...

Der Type-Check stellt sicher dass wir nur bei Incidents den Kommentar setzen:

python

if isinstance(context.incident, Incident):
    # context.incident.caller existiert garantiert
    _add_host_comment(..., caller=context.incident.caller, ...)

Warum das try/except um incident_nr?

Details

python

try:
    incident_nr = response.json()["result"]["number"]
    _add_host_comment(...)
except (KeyError, ValueError) as e:
    sys.stderr.write(f"WARNING: Could not extract incident number: {e}\n")

Falls ServiceNow ein unerwartetes Response-Format zurückgibt, crasht das Script nicht.

4. unterschiedliche Teams

:bullseye: Was ist das Problem?

Bei uns gibt es zwei verschiedene Teams:

Phase Team Assignment Group
PROBLEM erstellen Team auf team_auf
RECOVERY schließen Team zu team_zu

Warum?

  • Team auf erstellt Tickets bei Problemen
  • Team zu schließt Tickets nach Recovery

1. WATO Rules

1. In den WATO-Rules (_servicenow.py)

python

# In _incident_fs():
"recovery_assignment_group": DictElement(
    parameter_form=String(
        title=Title("Assignment group for recovery"),
        help_text=Help(
            "Optional: Set a different assignment group when resolving incidents. "
            "If not set, the assignment group remains unchanged."
        ),
        field_size=FieldSize.MEDIUM,
    ),
    required=False,
),

2. Im Script (servicenow.py):

2. Im Script

python

# In Incident-Dataclass erweitern:
@dataclass
class Incident:
    caller: str
    urgency: str
    impact: str
    recovery_state: str
    ack_state: str
    dtstart_state: str
    dtend_state: str
    recovery_assignment_group: str | None = None  # ← NEU


# In _get_json_for_recoveries() erweitern:
def _get_json_for_recoveries(
    endpoint_context: EndpointContext,
    notification_context: NotificationContext,
) -> JSONObject:
    if isinstance(endpoint_context, Incident):
        payload = {
            "close_code": "Closed/Resolved by Check_MK",
            "state": endpoint_context.recovery_state,
            "caller_id": endpoint_context.caller,
            "close_notes": notification_context.desc,
        }
        
        # ►►► NEU: Optional assignment_group bei Recovery ◄◄◄
        if endpoint_context.recovery_assignment_group:
            payload["assignment_group"] = endpoint_context.recovery_assignment_group
        
        return payload

… nun nur noch das Snapin anpassen :wink:

Guten Morgen, der Post ging irgendwie in Vergessenheit :smiley:
Danke für die Tipps, tatsächlich habe ich das Problem durch die in CMK 2.4 eingeführten Notfications Parameter gelöst und das Plugin konnte fertig gestellt werden :slight_smile:

Habt Ihr das auch umgesetzt oder händische Tickets direkt im SNOW?

Wenn ich das Snapin fertig habe kann ich euch das auch zukommen lassen.

Gruß Bernd