[Phase 4] Create NetBox webhook trigger workflow #26

Open
opened 2026-01-10 13:17:37 +00:00 by Damien · 0 comments
Owner

Description

Créer le workflow Kestra qui reçoit les webhooks NetBox et déclenche la réconciliation automatique quand l'intent change.

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                         NetBox                                   │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │  Webhook Configuration                                       ││
│  │  - URL: http://kestra:8080/api/v1/executions/webhook/...    ││
│  │  - Events: device.updated, config_context.updated           ││
│  └──────────────────────────┬──────────────────────────────────┘│
└─────────────────────────────┼───────────────────────────────────┘
                              │ POST
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│              Kestra: netbox-webhook.yml                          │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │  1. Parse webhook payload                                    ││
│  │  2. Validate event type                                      ││
│  │  3. Extract affected device(s)                               ││
│  │  4. Trigger fabric-reconcile subflow                         ││
│  └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘

Workflow: netbox-webhook.yml

id: netbox-webhook
namespace: network.fabric
description: Receive NetBox webhooks and trigger reconciliation

tasks:
  - id: log_webhook
    type: io.kestra.plugin.core.log.Log
    message: |
      📥 NetBox Webhook Received
      Event: {{ trigger.body.event }}
      Model: {{ trigger.body.model }}
      Data: {{ trigger.body.data | json }}

  - id: parse_event
    type: io.kestra.plugin.core.flow.Switch
    value: "{{ trigger.body.event }}"
    cases:
      "updated":
        - id: handle_update
          type: io.kestra.plugin.core.flow.If
          condition: "{{ trigger.body.model in ['dcim.device', 'extras.configcontext'] }}"
          then:
            - id: extract_device
              type: io.kestra.plugin.scripts.python.Script
              containerImage: python:3.12-slim
              script: |
                from kestra import Kestra
                import json
                
                body = json.loads('''{{ trigger.body | json }}''')
                
                if body['model'] == 'dcim.device':
                    device_name = body['data']['name']
                elif body['model'] == 'extras.configcontext':
                    # ConfigContext peut affecter plusieurs devices
                    # On déclenche une réconciliation globale
                    device_name = None
                else:
                    device_name = None
                
                Kestra.outputs({
                    "device": device_name,
                    "event_type": body['event'],
                    "model": body['model']
                })

            - id: notify_intent_change
              type: io.kestra.plugin.notifications.slack.SlackIncomingWebhook
              url: "{{ secret('SLACK_WEBHOOK') }}"
              payload: |
                {
                  "text": "📝 *NetBox Intent Changed*",
                  "attachments": [{
                    "color": "#0052cc",
                    "fields": [
                      {"title": "Model", "value": "{{ outputs.extract_device.vars.model }}", "short": true},
                      {"title": "Device", "value": "{{ outputs.extract_device.vars.device ?? 'All devices' }}", "short": true}
                    ]
                  }]
                }

            - id: trigger_reconcile
              type: io.kestra.plugin.core.flow.Subflow
              namespace: network.fabric
              flowId: fabric-reconcile
              inputs:
                device: "{{ outputs.extract_device.vars.device }}"
                dry_run: true  # Plan only by default
              wait: false  # Don't block webhook response

      "created":
        - id: log_created
          type: io.kestra.plugin.core.log.Log
          message: "New object created: {{ trigger.body.model }}"

      "deleted":
        - id: log_deleted
          type: io.kestra.plugin.core.log.Log
          message: "Object deleted: {{ trigger.body.model }}"

  - id: default_case
    type: io.kestra.plugin.core.log.Log
    runIf: "{{ trigger.body.event not in ['updated', 'created', 'deleted'] }}"
    message: "Unknown event type: {{ trigger.body.event }}"

triggers:
  - id: netbox_webhook
    type: io.kestra.plugin.core.trigger.Webhook
    key: "{{ secret('NETBOX_WEBHOOK_KEY') }}"

errors:
  - id: log_error
    type: io.kestra.plugin.core.log.Log
    message: "❌ Webhook processing failed: {{ errorMessage }}"

Configuration NetBox

Créer le webhook dans NetBox

# Via API ou UI NetBox
webhook_config = {
    "name": "Kestra Fabric Orchestrator",
    "type_create": False,
    "type_update": True,
    "type_delete": True,
    "type_job_start": False,
    "type_job_end": False,
    "payload_url": "http://kestra:8080/api/v1/executions/webhook/network.fabric/netbox-webhook/{WEBHOOK_KEY}",
    "http_method": "POST",
    "http_content_type": "application/json",
    "additional_headers": "",
    "body_template": "",  # Use default NetBox payload
    "secret": "",  # Optional HMAC secret
    "ssl_verification": True,
    "ca_file_path": None,
    "enabled": True,
    "content_types": [
        "dcim.device",
        "extras.configcontext"
    ]
}

Payload NetBox attendu

{
  "event": "updated",
  "timestamp": "2024-01-15T10:30:00Z",
  "model": "dcim.device",
  "username": "admin",
  "request_id": "abc-123",
  "data": {
    "id": 1,
    "name": "leaf1",
    "device_type": {...},
    "site": {...},
    "local_context_data": {...},
    ...
  },
  "snapshots": {
    "prechange": {...},
    "postchange": {...}
  }
}

Tasks

  • Créer kestra/flows/netbox-webhook.yml
  • Configurer le webhook dans NetBox
  • Générer et stocker la clé webhook dans Kestra secrets
  • Tester avec un changement de device dans NetBox
  • Tester avec un changement de ConfigContext
  • Ajouter rate limiting si nécessaire
  • Documenter la configuration dans le README

Debouncing (optionnel)

Pour éviter les exécutions multiples lors de changements rapides :

  - id: check_debounce
    type: io.kestra.plugin.core.kv.Get
    key: "webhook_debounce_{{ trigger.body.data.name }}"
    errorOnMissing: false

  - id: skip_if_recent
    type: io.kestra.plugin.core.flow.If
    condition: "{{ outputs.check_debounce.value != null }}"
    then:
      - id: log_debounce
        type: io.kestra.plugin.core.log.Log
        message: "Skipping - recent webhook for same device"
      - id: end_debounce
        type: io.kestra.plugin.core.flow.End

  - id: set_debounce
    type: io.kestra.plugin.core.kv.Set
    key: "webhook_debounce_{{ trigger.body.data.name }}"
    value: "{{ now() }}"
    ttl: PT30S  # 30 seconds debounce

Output

  • kestra/flows/netbox-webhook.yml
  • Documentation de configuration NetBox

Dependencies

  • #24 (Kestra infrastructure)
  • #10 (fabric-reconcile workflow)
## Description Créer le workflow Kestra qui reçoit les webhooks NetBox et déclenche la réconciliation automatique quand l'intent change. ## Architecture ``` ┌─────────────────────────────────────────────────────────────────┐ │ NetBox │ │ ┌─────────────────────────────────────────────────────────────┐│ │ │ Webhook Configuration ││ │ │ - URL: http://kestra:8080/api/v1/executions/webhook/... ││ │ │ - Events: device.updated, config_context.updated ││ │ └──────────────────────────┬──────────────────────────────────┘│ └─────────────────────────────┼───────────────────────────────────┘ │ POST ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Kestra: netbox-webhook.yml │ │ ┌─────────────────────────────────────────────────────────────┐│ │ │ 1. Parse webhook payload ││ │ │ 2. Validate event type ││ │ │ 3. Extract affected device(s) ││ │ │ 4. Trigger fabric-reconcile subflow ││ │ └─────────────────────────────────────────────────────────────┘│ └─────────────────────────────────────────────────────────────────┘ ``` ## Workflow: `netbox-webhook.yml` ```yaml id: netbox-webhook namespace: network.fabric description: Receive NetBox webhooks and trigger reconciliation tasks: - id: log_webhook type: io.kestra.plugin.core.log.Log message: | 📥 NetBox Webhook Received Event: {{ trigger.body.event }} Model: {{ trigger.body.model }} Data: {{ trigger.body.data | json }} - id: parse_event type: io.kestra.plugin.core.flow.Switch value: "{{ trigger.body.event }}" cases: "updated": - id: handle_update type: io.kestra.plugin.core.flow.If condition: "{{ trigger.body.model in ['dcim.device', 'extras.configcontext'] }}" then: - id: extract_device type: io.kestra.plugin.scripts.python.Script containerImage: python:3.12-slim script: | from kestra import Kestra import json body = json.loads('''{{ trigger.body | json }}''') if body['model'] == 'dcim.device': device_name = body['data']['name'] elif body['model'] == 'extras.configcontext': # ConfigContext peut affecter plusieurs devices # On déclenche une réconciliation globale device_name = None else: device_name = None Kestra.outputs({ "device": device_name, "event_type": body['event'], "model": body['model'] }) - id: notify_intent_change type: io.kestra.plugin.notifications.slack.SlackIncomingWebhook url: "{{ secret('SLACK_WEBHOOK') }}" payload: | { "text": "📝 *NetBox Intent Changed*", "attachments": [{ "color": "#0052cc", "fields": [ {"title": "Model", "value": "{{ outputs.extract_device.vars.model }}", "short": true}, {"title": "Device", "value": "{{ outputs.extract_device.vars.device ?? 'All devices' }}", "short": true} ] }] } - id: trigger_reconcile type: io.kestra.plugin.core.flow.Subflow namespace: network.fabric flowId: fabric-reconcile inputs: device: "{{ outputs.extract_device.vars.device }}" dry_run: true # Plan only by default wait: false # Don't block webhook response "created": - id: log_created type: io.kestra.plugin.core.log.Log message: "New object created: {{ trigger.body.model }}" "deleted": - id: log_deleted type: io.kestra.plugin.core.log.Log message: "Object deleted: {{ trigger.body.model }}" - id: default_case type: io.kestra.plugin.core.log.Log runIf: "{{ trigger.body.event not in ['updated', 'created', 'deleted'] }}" message: "Unknown event type: {{ trigger.body.event }}" triggers: - id: netbox_webhook type: io.kestra.plugin.core.trigger.Webhook key: "{{ secret('NETBOX_WEBHOOK_KEY') }}" errors: - id: log_error type: io.kestra.plugin.core.log.Log message: "❌ Webhook processing failed: {{ errorMessage }}" ``` ## Configuration NetBox ### Créer le webhook dans NetBox ```python # Via API ou UI NetBox webhook_config = { "name": "Kestra Fabric Orchestrator", "type_create": False, "type_update": True, "type_delete": True, "type_job_start": False, "type_job_end": False, "payload_url": "http://kestra:8080/api/v1/executions/webhook/network.fabric/netbox-webhook/{WEBHOOK_KEY}", "http_method": "POST", "http_content_type": "application/json", "additional_headers": "", "body_template": "", # Use default NetBox payload "secret": "", # Optional HMAC secret "ssl_verification": True, "ca_file_path": None, "enabled": True, "content_types": [ "dcim.device", "extras.configcontext" ] } ``` ### Payload NetBox attendu ```json { "event": "updated", "timestamp": "2024-01-15T10:30:00Z", "model": "dcim.device", "username": "admin", "request_id": "abc-123", "data": { "id": 1, "name": "leaf1", "device_type": {...}, "site": {...}, "local_context_data": {...}, ... }, "snapshots": { "prechange": {...}, "postchange": {...} } } ``` ## Tasks - [ ] Créer `kestra/flows/netbox-webhook.yml` - [ ] Configurer le webhook dans NetBox - [ ] Générer et stocker la clé webhook dans Kestra secrets - [ ] Tester avec un changement de device dans NetBox - [ ] Tester avec un changement de ConfigContext - [ ] Ajouter rate limiting si nécessaire - [ ] Documenter la configuration dans le README ## Debouncing (optionnel) Pour éviter les exécutions multiples lors de changements rapides : ```yaml - id: check_debounce type: io.kestra.plugin.core.kv.Get key: "webhook_debounce_{{ trigger.body.data.name }}" errorOnMissing: false - id: skip_if_recent type: io.kestra.plugin.core.flow.If condition: "{{ outputs.check_debounce.value != null }}" then: - id: log_debounce type: io.kestra.plugin.core.log.Log message: "Skipping - recent webhook for same device" - id: end_debounce type: io.kestra.plugin.core.flow.End - id: set_debounce type: io.kestra.plugin.core.kv.Set key: "webhook_debounce_{{ trigger.body.data.name }}" value: "{{ now() }}" ttl: PT30S # 30 seconds debounce ``` ## Output - `kestra/flows/netbox-webhook.yml` - Documentation de configuration NetBox ## Dependencies - #24 (Kestra infrastructure) - #10 (fabric-reconcile workflow)
Damien added the phase-4-event-driven label 2026-01-10 13:22:41 +00:00
Sign in to join this conversation.