Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F3779061
D2790.id8221.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
10 KB
Referenced Files
None
Subscribers
None
D2790.id8221.diff
View Options
diff --git a/roles/salt-primary/software/init.sls b/roles/salt-primary/software/init.sls
--- a/roles/salt-primary/software/init.sls
+++ b/roles/salt-primary/software/init.sls
@@ -20,6 +20,7 @@
# For staging-commit-message
- {{ packages_prefixes.python3 }}gitpython
# Pillar
+ - {{ packages_prefixes.python3 }}pynetbox
- {{ packages_prefixes.python3 }}salt-tower
{{ dirs.bin }}/staging-commit-message:
diff --git a/utils/netbox/pillarize.py b/utils/netbox/pillarize.py
new file mode 100755
--- /dev/null
+++ b/utils/netbox/pillarize.py
@@ -0,0 +1,355 @@
+#!/usr/bin/env python3
+
+# -------------------------------------------------------------
+# NetBox — Pillar information for Salt
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# License: BSD-2-Clause
+# Dependencies: PyYAML, pynetbox
+# -------------------------------------------------------------
+
+
+import logging
+import os
+import sys
+
+import pynetbox
+import yaml
+
+
+VRF_RD_DRAKE = "nasqueron.drake"
+
+
+# -------------------------------------------------------------
+# Get NetBox config and credentials
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def get_netbox_config_from_salt():
+ config_path = "/usr/local/etc/salt/master.d/netbox.conf"
+
+ if not os.path.exists(config_path):
+ return False, None
+
+ with open(config_path) as fd:
+ salt_config = yaml.safe_load(fd)
+ salt_config = salt_config["ext_pillar"][0]["netbox"]
+ return True, {
+ "server": salt_config["api_url"].replace("/api/", ""),
+ "token": salt_config["api_token"],
+ }
+
+
+def get_netbox_config_from_config_dir():
+ try:
+ config_path = os.path.join(os.environ["HOME"], ".config", "netbox", "auth.yaml")
+ except KeyError:
+ return False, None
+
+ if not os.path.exists(config_path):
+ return False, None
+
+ with open(config_path) as fd:
+ return True, yaml.safe_load(fd)
+
+
+def get_netbox_config():
+ methods = [get_netbox_config_from_salt, get_netbox_config_from_config_dir]
+
+ for method in methods:
+ has_config, config = method()
+ if has_config:
+ return config
+
+ raise RuntimeError("Can't find NetBox config")
+
+
+# -------------------------------------------------------------
+# Service container
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def init_app(node):
+ """Prepare a services container for appplication."""
+ config = get_netbox_config()
+
+ return {
+ "node": node,
+ "config": config,
+ "netbox": connect_to_netbox(config),
+ }
+
+
+def connect_to_netbox(config):
+ return pynetbox.api(config["server"], token=config["token"])
+
+
+# -------------------------------------------------------------
+# Build pillar
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def build_pillar(app):
+ return {
+ "etc_hosts": build_etc_hosts(app["netbox"]),
+ "node": build_node_pillar(app["netbox"], app["node"]),
+ }
+
+
+# -------------------------------------------------------------
+# Pillar data :: etc_hosts
+ # Entries for /etc/hosts
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def build_etc_hosts(nb):
+ ip_addresses = nb.ipam.ip_addresses.filter(vrf=VRF_RD_DRAKE)
+
+ return [compile_etc_host(ip) for ip in ip_addresses if len(ip.dns_name) > 0]
+
+
+def compile_etc_host(ip):
+ address = clean_ip(ip.address)
+ short = get_short_dns_name(ip.dns_name)
+ return f"{address} {short} {ip.dns_name}"
+
+
+# -------------------------------------------------------------
+# Pillar data :: node
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def build_node_pillar(nb, node):
+ device = nb.dcim.devices.get(name=node)
+ if device is not None:
+ return build_dedicated_node_pillar(nb, device)
+
+ vm = nb.virtualization.virtual_machines.get(name=node)
+ if vm is not None:
+ return build_vm_node_pillar(nb, vm)
+
+ raise RuntimeError("Can't find pillar data for the node. Please add it to NetBox.")
+
+
+def build_dedicated_node_pillar(nb, device):
+ node = device.local_context_data
+ if not node:
+ node = {}
+
+ device.primary_ip.full_details()
+ node["hostname"] = device.primary_ip.dns_name
+
+ # GRE interfaces aren't included in node pillar, but in network one
+ interfaces = nb.dcim.interfaces.filter(device=device.name)
+ node["network"] = {
+ "interfaces": {
+ compute_interface_name(interface): build_dcim_interface(nb, interface)
+ for interface in interfaces
+ if interface.enabled and interface.type.label != "Virtual"
+ }
+ }
+
+ return node
+
+
+def build_vm_node_pillar(nb, device):
+ node = device.local_context_data
+ if not node:
+ node = {}
+
+ device.primary_ip.full_details()
+ node["hostname"] = device.primary_ip.dns_name
+
+ # GRE interfaces aren't included in node pillar, but in network one
+ interfaces = nb.virtualization.interfaces.filter(virtual_machine=device.name)
+ node["network"] = {
+ "interfaces": {
+ compute_interface_name(interface): build_vm_interface(nb, interface)
+ for interface in interfaces
+ if interface.enabled and not interface.custom_fields["virt_if_virtual"]
+ }
+ }
+
+ return node
+
+
+def build_dcim_interface(nb, interface):
+ result = {
+ "device": interface.name,
+ }
+
+ if interface.custom_fields["if_uuid"]:
+ result["uuid"] = interface.custom_fields["if_uuid"]
+
+ ip_addresses = [
+ ip
+ for ip in nb.ipam.ip_addresses.all()
+ if is_assigned_to_dcim_interface(ip, interface.id)
+ ]
+
+ ip_addresses_by_family = get_ip_addresses_by_family(ip_addresses)
+ if len(ip_addresses_by_family["IPv4"]) > 0:
+ result["ipv4"] = build_inet_result(
+ ip_addresses_by_family, "IPv4", "default_gateways", interface
+ )
+
+ if len(ip_addresses_by_family["IPv6"]) > 0:
+ result["ipv6"] = build_inet_result(
+ ip_addresses_by_family, "IPv6", "default_gateways", interface
+ )
+
+ return result
+
+
+def build_vm_interface(nb, interface):
+ result = {
+ "device": interface.name,
+ }
+
+ if interface.custom_fields["if_uuid_virt"]:
+ result["uuid"] = interface.custom_fields["if_uuid_virt"]
+
+ ip_addresses = [
+ ip
+ for ip in nb.ipam.ip_addresses.all()
+ if is_assigned_to_vminterface(ip, interface.id)
+ ]
+
+ ip_addresses_by_family = get_ip_addresses_by_family(ip_addresses)
+ if len(ip_addresses_by_family["IPv4"]) > 0:
+ result["ipv4"] = build_inet_result(
+ ip_addresses_by_family, "IPv4", "default_gateways_virt", interface
+ )
+
+ return result
+
+
+def get_ip_addresses_by_family(ip_addresses):
+ ip_addresses_by_family = {"IPv4": [], "IPv6": []}
+
+ for ip in ip_addresses:
+ ip_addresses_by_family[ip.family.label].append(ip)
+
+ return ip_addresses_by_family
+
+
+def build_inet_result(ip_addresses_by_family, family, gw_field, interface):
+ primary_ip = ip_addresses_by_family[family][0]
+ result = {
+ "address": clean_ip(primary_ip.address),
+ }
+
+ if family == "IPv4":
+ result["netmask"] = ipv4_to_netmask(primary_ip.address)
+ else:
+ result["prefix"] = get_prefix_len(primary_ip.address)
+
+ if len(ip_addresses_by_family[family]) > 1:
+ result["aliases"] = [
+ clean_ip(ip.address) for ip in ip_addresses_by_family[family]
+ ]
+
+ try:
+ for gateway in interface.custom_fields[gw_field]:
+ gw = gateway["address"]
+
+ if is_from_family(gw, family):
+ result["gateway"] = clean_ip(gw)
+ except KeyError:
+ pass
+ except TypeError:
+ pass
+
+ return result
+
+
+# -------------------------------------------------------------
+# Helper functions to use NetBox API
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def is_assigned_to_interface(ip, type, interface_id):
+ return ip.assigned_object_type == type and ip.assigned_object_id == interface_id
+
+
+def is_assigned_to_vminterface(ip, interface_id):
+ return is_assigned_to_interface(ip, "virtualization.vminterface", interface_id)
+
+
+def is_assigned_to_dcim_interface(ip, interface_id):
+ return is_assigned_to_interface(ip, "dcim.interface", interface_id)
+
+
+def filter_ip_addresses(nb, interface_type, interface_id):
+ return [
+ ip
+ for ip in nb.ipam.ip_addresses.all()
+ if is_assigned_to_vminterface(ip, interface_id)
+ ]
+
+
+def compute_interface_name(interface):
+ if len(interface.description) > 0:
+ return interface.description.lower().strip().replace(" ", "_")
+
+ return interface.name
+
+
+# -------------------------------------------------------------
+# Helper functions to manipulate IP and networks
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def clean_ip(ip):
+ pos = ip.find("/")
+ return ip[0:pos]
+
+
+def get_short_dns_name(hostname):
+ pos = hostname.find(".")
+ return hostname[0:pos]
+
+
+def is_from_family(ip, family):
+ return (family == "IPv4" and "." in ip) or (family == "IPv6" and ":" in ip)
+
+
+def get_prefix_len(ip):
+ pos = ip.find("/") + 1
+ return int(ip[pos:])
+
+
+def ipv4_to_netmask(ip):
+ return cidr_to_netmask(get_prefix_len(ip))
+
+
+def cidr_to_netmask(cidr):
+ """Compute the netmask for a CIDR prefix."""
+ mask = (0xFFFFFFFF >> (32 - cidr)) << (32 - cidr)
+ return (
+ str((0xFF000000 & mask) >> 24)
+ + "."
+ + str((0x00FF0000 & mask) >> 16)
+ + "."
+ + str((0x0000FF00 & mask) >> 8)
+ + "."
+ + str((0x000000FF & mask))
+ )
+
+
+# -------------------------------------------------------------
+# Application entry-point
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+if __name__ == "__main__":
+ LOGLEVEL = os.environ.get("LOGLEVEL", "WARNING").upper()
+ logging.basicConfig(level=LOGLEVEL)
+
+ if len(sys.argv) != 2:
+ print(f"Usage: {sys.argv[0]} <node name>", file=sys.stderr)
+ sys.exit(127)
+
+ app = init_app(sys.argv[1])
+ pillar = build_pillar(app)
+ print(yaml.dump(pillar))
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Tue, Nov 26, 07:10 (20 h, 34 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2264205
Default Alt Text
D2790.id8221.diff (10 KB)
Attached To
Mode
D2790: WIP: Generate a pillar from NetBox information
Attached
Detach File
Event Timeline
Log In to Comment