Netbox/Semaphore usw. läuft bisher nur bei mir zuhause und wie sich aktuell zeigt, aus gutem Grund. Das letzte Netbox Update, genauer gesagt ein Update des DHCP Plugins hat beim Sprung auf Netbox 4.7.0 diverse Änderungen mit sich gebracht, die eines meiner Playbooks leider in die Knie gezwungen haben. Das DHCP Plugin von Peter Eckel ist weiterhin beta, weswegen ich das Setup mit Netbox/Semaphore auch weiterhin (noch) nicht in einem produktiven Umfeld einsetzen werde. Ich vermute die Änderungen vom 04.08.26 im Netbox DHCP Plugin sind der konkrete Grund.
Die Fehlermeldung bei Semaphore lautet (exemplarisch für das dashy LXC):
7:35:10 PM
ok: [dns-redacted] => (item=dashy) => {
7:35:10 PM
"msg": "WARNUNG: dashy hat hw_address_id={'id': 7, 'url': 'https://192.168.2.10/api/dcim/mac-addresses/7/', 'display': 'BC:24:11:xx:xx:xx', 'mac_address': 'BC:24:11:xx:xx:xx', 'description': ''}, keine passende MAC in mac_lookup gefunden - wird uebersprungen"
7:35:10 PM
}
ich habe jedenfalls das dhcp-push skript an die neuen Gegebenheiten angepasst. –>
---
# push-dhcp-standort-a.yml
#
# Discovery-Phase: schreibt noch NICHTS in Technitium, liest nur
# Host-Reservations aus netbox-plugin-dhcp + Scope-Config aus Technitium.
#
# Aufruf:
# ansible-playbook -i inventory/netbox.yml push-dhcp-standort-a.yml --limit dns-standort-a
- name: DHCP-Reservierungen NetBox <-> Technitium - Discovery
hosts: dns-standort-a
gather_facts: false
vars:
netbox_api: "https://192.168.2.10"
netbox_token: "{{ lookup('env', 'NETBOX_TOKEN') }}"
technitium_url: "https://{{ ansible_host }}:53443"
technitium_token: "{{ lookup('env', 'TECHNITIUM_TOKEN') }}"
target_vrf_name: "VRF Standort A"
technitium_scope_name: "StandortA"
dry_run: true # true = nur anzeigen, kein Schreibzugriff
tasks:
- name: Alle VRFs holen (ungefiltert, wegen Leerzeichen im Namen)
ansible.builtin.set_fact:
all_vrfs: >-
{{ query('netbox.netbox.nb_lookup', 'vrfs',
api_endpoint=netbox_api, token=netbox_token,
validate_certs=false)
| map(attribute='value') | list }}
- name: Ziel-VRF lokal per Namen herausfiltern
ansible.builtin.set_fact:
target_vrf: "{{ all_vrfs | selectattr('name', 'equalto', target_vrf_name) | first }}"
- name: Prefixe der Ziel-VRF holen
ansible.builtin.set_fact:
target_prefixes: >-
{{ query('netbox.netbox.nb_lookup', 'prefixes',
api_endpoint=netbox_api, token=netbox_token,
api_filter='vrf_id=' ~ target_vrf.id, validate_certs=false)
| map(attribute='value') | map(attribute='prefix') | list }}
- name: Alle Host Reservations holen
ansible.builtin.set_fact:
all_dhcp_reservations: >-
{{ query('netbox.netbox.nb_lookup', 'hostreservations',
api_endpoint=netbox_api, token=netbox_token,
plugin='netbox-dhcp', validate_certs=false)
| map(attribute='value') | list }}
- name: Anzahl gefundener Reservations
ansible.builtin.debug:
msg: "{{ all_dhcp_reservations | length }} Host Reservations gesamt gefunden"
# Fallback-Lookup fuer den Fall, dass hw_address wieder nur eine ID liefert
# (Plugin < 0.1.10). Seit 0.1.10 ("Fixed nested serializers") liefert
# hw_address bereits ein verschachteltes Objekt inkl. mac_address.
- name: MAC-Address-Objekte holen (limit=0, fuer Lookup-Fallback)
ansible.builtin.uri:
url: "{{ netbox_api }}/api/dcim/mac-addresses/?limit=0"
method: GET
headers:
Authorization: "Token {{ netbox_token }}"
validate_certs: false
status_code: [200]
register: mac_addresses_raw
ignore_errors: true
- name: MAC-Adressen-Lookup-Tabelle bauen (id -> mac_address, Fallback)
ansible.builtin.set_fact:
mac_lookup: "{{ dict(mac_addresses_raw.json.results | map(attribute='id') | map('string')
| zip(mac_addresses_raw.json.results | map(attribute='mac_address'))) }}"
when: mac_addresses_raw.json is defined and mac_addresses_raw.json.results is defined
- name: Aktuelle Technitium-Scope-Konfiguration holen (inkl. reservedLeases)
ansible.builtin.uri:
url: "{{ technitium_url }}/api/dhcp/scopes/get"
method: GET
body_format: form-urlencoded
body:
token: "{{ technitium_token }}"
name: "{{ technitium_scope_name }}"
validate_certs: false
status_code: [200]
register: technitium_scope_raw
no_log: true
# -----------------------------------------------------------------
# Push-Vorbereitung (noch kein Schreibzugriff)
# -----------------------------------------------------------------
- name: Ziel-Reservierungen filtern (IP innerhalb Ziel-VRF-Prefixe)
ansible.builtin.set_fact:
site_reservations: "{{ site_reservations | default([]) + [item] }}"
loop: "{{ all_dhcp_reservations }}"
loop_control:
label: "{{ item.hostname }}"
vars:
record_ip: "{{ item.ipv4_address.address | ansible.utils.ipaddr('address') }}"
when:
- item.ipv4_address is defined
- item.ipv4_address.address is defined
- record_ip is ansible.utils.in_one_network target_prefixes
# hw_address ist seit Plugin 0.1.10 ein verschachteltes Objekt mit
# mac_address inline; nutze das direkt, mit Fallback auf mac_lookup.
- name: Ziel-Reservierungen im Technitium-Format aufbauen
ansible.builtin.set_fact:
target_reserved_leases: "{{ target_reserved_leases | default([]) + [{
'hostName': item.hostname,
'address': item.ipv4_address.address | ansible.utils.ipaddr('address'),
'hardwareAddress': (
(item.hw_address.mac_address if (item.hw_address is mapping and item.hw_address.mac_address is defined)
else mac_lookup[item.hw_address | string] | default('UNBEKANNT'))
| replace(':', '-') | upper
),
'comments': item.comments | default(None)
}] }}"
loop: "{{ site_reservations | default([]) }}"
loop_control:
label: "{{ item.hostname }}"
when: >-
item.hw_address is defined and
((item.hw_address is mapping and item.hw_address.mac_address is defined)
or (item.hw_address is not mapping and (item.hw_address | string) in mac_lookup))
- name: Reservierungen ohne aufgeloeste MAC warnen (werden NICHT gepusht)
ansible.builtin.debug:
msg: "WARNUNG: {{ item.hostname }} hat hw_address={{ item.hw_address | default('NULL') }}, keine MAC aufloesbar - wird uebersprungen"
loop: "{{ site_reservations | default([]) }}"
loop_control:
label: "{{ item.hostname }}"
when: >-
not (item.hw_address is defined and
((item.hw_address is mapping and item.hw_address.mac_address is defined)
or (item.hw_address is not mapping and (item.hw_address | string) in mac_lookup)))
- name: Anzahl push-bereiter Reservierungen
ansible.builtin.debug:
msg: "{{ target_reserved_leases | default([]) | length }} von {{ site_reservations | default([]) | length }} push-bereit"
- name: "DRY RUN - geplante reservedLeases-Liste anzeigen"
ansible.builtin.debug:
var: target_reserved_leases
when: dry_run | bool
- name: Bestehende Technitium-Reservierungen nach hardwareAddress indexieren
ansible.builtin.set_fact:
existing_leases_by_mac: "{{ dict(technitium_scope_raw.json.response.reservedLeases | map(attribute='hardwareAddress')
| zip(technitium_scope_raw.json.response.reservedLeases)) }}"
- name: Aktionsplan bauen (neu / aendern / unveraendert)
ansible.builtin.set_fact:
lease_actions: "{{ lease_actions | default([]) + [{
'mac': item.hardwareAddress,
'target': item,
'existing': existing_leases_by_mac.get(item.hardwareAddress),
'action': ('unveraendert'
if (existing_leases_by_mac.get(item.hardwareAddress) is not none and
existing_leases_by_mac[item.hardwareAddress].address == item.address and
existing_leases_by_mac[item.hardwareAddress].hostName == item.hostName)
else ('aendern' if existing_leases_by_mac.get(item.hardwareAddress) is not none else 'neu'))
}] }}"
loop: "{{ target_reserved_leases | default([]) }}"
loop_control:
label: "{{ item.hostName }}"
loop_var: item
- name: Zusammenfassung des Aktionsplans
ansible.builtin.debug:
msg: >-
{{ lease_actions | selectattr('action', 'equalto', 'neu') | list | length }} neu,
{{ lease_actions | selectattr('action', 'equalto', 'aendern') | list | length }} aendern,
{{ lease_actions | selectattr('action', 'equalto', 'unveraendert') | list | length }} unveraendert
- name: "DRY RUN - Details je geplanter Aktion"
ansible.builtin.debug:
msg: >-
[{{ item.action | upper }}] {{ item.target.hostName }}
({{ item.mac }}) -> {{ item.target.address }}
{{ '(vorher: ' ~ item.existing.hostName ~ ' -> ' ~ item.existing.address ~ ')' if item.action == 'aendern' else '' }}
loop: "{{ lease_actions | rejectattr('action', 'equalto', 'unveraendert') | list }}"
loop_control:
label: "{{ item.target.hostName }}"
when: dry_run | bool
- name: Bestehende Reservierung entfernen vor Aenderung (Remove-vor-Add)
ansible.builtin.uri:
url: "{{ technitium_url }}/api/dhcp/scopes/removeReservedLease"
method: POST
body_format: form-urlencoded
body:
token: "{{ technitium_token }}"
name: "{{ technitium_scope_name }}"
hardwareAddress: "{{ item.mac }}"
validate_certs: false
status_code: [200]
loop: "{{ lease_actions | selectattr('action', 'equalto', 'aendern') | list }}"
loop_control:
label: "{{ item.target.hostName }}"
no_log: true
register: remove_result
when: not (dry_run | bool)
- name: Reservierung anlegen (neu oder nach Remove bei Aenderung)
ansible.builtin.uri:
url: "{{ technitium_url }}/api/dhcp/scopes/addReservedLease"
method: POST
body_format: form-urlencoded
body:
token: "{{ technitium_token }}"
name: "{{ technitium_scope_name }}"
hardwareAddress: "{{ item.mac }}"
ipAddress: "{{ item.target.address }}"
hostName: "{{ item.target.hostName }}"
comments: "{{ item.target.comments | default('') }}"
validate_certs: false
status_code: [200]
loop: "{{ lease_actions | rejectattr('action', 'equalto', 'unveraendert') | list }}"
loop_control:
label: "{{ item.target.hostName }}"
no_log: true
register: add_result
ignore_errors: true
when: not (dry_run | bool)
- name: Fehlgeschlagene Add-Aktionen auflisten
ansible.builtin.debug:
msg: "FEHLER bei {{ item.item.target.hostName }}: {{ item.msg | default('unbekannt') }}"
loop: "{{ add_result.results | default([]) }}"
loop_control:
label: "{{ item.item.target.hostName }}"
when:
- not (dry_run | bool)
- item.failed | default(false)
und kommentartechnisch ein bisschen gestrafft. Ich bin wegen der Fragilität meines aktuellen Setups etwas unglücklich. Ich werde die nächsten Tage ein paar alternative Ansätze bei dem Skript testen, die hoffentlich etwas robuster sind. Bei Netbox sind beim letzten Update diverse neue Felder hinzugekommen, das muss ich mir aber zunächst in Ruhe ansehen.
Nachtrag: Dafür wollte ich keinen eigenen neuen Blogpost erstellen, aber ich habe grade gesehen, dass es in Semaphore UI scheinbar als neues Feature sogenannte Workflows gibt –>
Das sieht einem Blatt bei NodeRed verdächtig ähnlich. Damit ließen sich anschaulich verschiedene Playbooks miteinander verknüpfen bzw. aneinanderreihen, ggf sogar verschiedene Wege je nach Ausgabe wählen. Das sieht jedenfalls äußerst spannend aus und könnte meinen bisherigen Ansatz ganz erheblich vereinfachen. Man könnte die einzelnen Arbeitsschritte in einzelne Skripte modularisieren und durch Aneinanderreihung miteinander verknüpfen. Das wäre wesentlich einfacher zu warten, als ein monolithisches Skript das mit der Zeit immer größer und fehleranfälliger wird.









