Skip to content

Commit

Permalink
Optionally set napalm connection options if they exist in netbox (#569)
Browse files Browse the repository at this point in the history
Co-authored-by: Jeremiah Millay <jmillay@floatingstatic.net>
  • Loading branch information
floatingstatic and floatingstatic committed Sep 6, 2020
1 parent 1a525a5 commit 916b841
Show file tree
Hide file tree
Showing 22 changed files with 1,485 additions and 51 deletions.
81 changes: 63 additions & 18 deletions nornir/plugins/inventory/netbox.py
Expand Up @@ -16,6 +16,7 @@ def __init__(
ssl_verify: Union[bool, str] = True,
flatten_custom_fields: bool = True,
filter_parameters: Optional[Dict[str, Any]] = None,
use_platform_args: bool = False,
**kwargs: Any,
) -> None:
"""
Expand All @@ -29,6 +30,8 @@ def __init__(
ssl_verify: Enable/disable certificate validation or provide path to CA bundle file
flatten_custom_fields: Whether to assign custom fields directly to the host or not
filter_parameters: Key-value pairs to filter down hosts
use_platform_args: Whether to import NetBox NAPALM platform options as
connection options for the NAPALM driver
"""
filter_parameters = filter_parameters or {}
nb_url = nb_url or os.environ.get("NB_URL", "http://localhost:8080")
Expand All @@ -40,26 +43,22 @@ def __init__(
session.headers.update({"Authorization": f"Token {nb_token}"})
session.verify = ssl_verify

# Fetch all devices from Netbox
# Since the api uses pagination we have to fetch until no next is provided

url = f"{nb_url}/api/dcim/devices/?limit=0"
nb_devices: List[Dict[str, Any]] = []

while url:
r = session.get(url, params=filter_parameters)

if not r.status_code == 200:
raise ValueError(f"Failed to get devices from Netbox instance {nb_url}")

resp = r.json()
nb_devices.extend(resp.get("results"))
# Fetch all platforms from Netbox
if use_platform_args:
nb_platforms = self._get_resources(
session, f"{nb_url}/api/dcim/platforms/?limit=0", {}
)
platform_field = "slug" if use_slugs else "name"
napalm_args = {p[platform_field]: p["napalm_args"] for p in nb_platforms}

url = resp.get("next")
# Fetch all devices from Netbox
nb_devices = self._get_resources(
session, f"{nb_url}/api/dcim/devices/?limit=0", filter_parameters
)

hosts = {}
for d in nb_devices:
host: HostsDict = {"data": {}}
host: HostsDict = {"data": {}, "connection_options": {}}

# Add value for IP address
if d.get("primary_ip", {}):
Expand Down Expand Up @@ -87,13 +86,34 @@ def __init__(
host["data"]["model"] = d["device_type"]["slug"]

# Attempt to add 'platform' based of value in 'slug'
host["platform"] = d["platform"]["slug"] if d["platform"] else None
host["platform"] = (
d["platform"]["slug"]
if isinstance(d["platform"], dict)
else d["platform"]
)

else:
host["data"]["site"] = d["site"]["name"]
host["data"]["role"] = d["device_role"]
host["data"]["model"] = d["device_type"]
host["platform"] = d["platform"]
host["platform"] = (
d["platform"]["name"]
if isinstance(d["platform"], dict)
else d["platform"]
)

if use_platform_args:
# Add NAPALM connection options if they exist for the host platform
if napalm_args.get(host["platform"]):
if os.environ.get("NB_PLATFORM_AS_CONN_OPTS", "0") == "1":
# Abuse the platform NAPALM arguments field for use as
# connection_options for any connection driver
host["connection_options"] = napalm_args[host["platform"]]
else:
# Use the platform NAPALM arguments as intended by NetBox
host["connection_options"]["napalm"] = {
"extras": {"optional_args": napalm_args[host["platform"]]}
}

# Assign temporary dict to outer dict
# Netbox allows devices to be unnamed, but the Nornir model does not allow this
Expand All @@ -102,3 +122,28 @@ def __init__(

# Pass the data back to the parent class
super().__init__(hosts=hosts, groups={}, defaults={}, **kwargs)

def _get_resources(
self,
session: requests.sessions.Session,
url: str,
parameters: Optional[Dict[str, Any]],
) -> List[Dict[str, Any]]:
"""Get data from Netbox API"""
nb_result: List[Dict[str, Any]] = []

# Since the api uses pagination we have to fetch until no next is provided
while url:
r = session.get(url, params=parameters)

if not r.status_code == 200:
raise ValueError(
f"Failed to get valid response from Netbox instance {url}"
)

resp = r.json()
nb_result.extend(resp.get("results"))

url = resp.get("next")

return nb_result
6 changes: 3 additions & 3 deletions tests/plugins/inventory/netbox/2.3.5/expected.json
Expand Up @@ -5,7 +5,7 @@
"hostname": "10.0.1.1",
"username": null,
"password": null,
"platform": null,
"platform": "junos",
"data": {
"serial": "",
"vendor": "Juniper",
Expand All @@ -22,7 +22,7 @@
"hostname": "172.16.2.1",
"username": null,
"password": null,
"platform": null,
"platform": "junos",
"data": {
"serial": "",
"vendor": "Juniper",
Expand Down Expand Up @@ -57,7 +57,7 @@
"hostname": "10.0.1.4",
"username": null,
"password": null,
"platform": null,
"platform": "junos",
"data": {
"serial": "",
"vendor": "Juniper",
Expand Down
Expand Up @@ -5,7 +5,7 @@
"hostname": "10.0.1.1",
"username": null,
"password": null,
"platform": null,
"platform": "junos",
"data": {
"serial": "",
"vendor": "Juniper",
Expand All @@ -23,7 +23,7 @@
"hostname": "172.16.2.1",
"username": null,
"password": null,
"platform": null,
"platform": "junos",
"data": {
"serial": "",
"vendor": "Juniper",
Expand Down Expand Up @@ -60,15 +60,15 @@
"hostname": "10.0.1.4",
"username": null,
"password": null,
"platform": null,
"platform": "junos",
"data": {
"serial": "",
"vendor": "Juniper",
"asset_tag": null,
"site": "sunnyvale-ca",
"role": "rt",
"model": "mx480",
"platform": "junos"
"platform": "junos"
},
"connection_options": {},
"groups": []
Expand Down
122 changes: 122 additions & 0 deletions tests/plugins/inventory/netbox/2.3.5/expected_use_platform_args.json
@@ -0,0 +1,122 @@
{
"hosts": {
"1-Core": {
"port": null,
"hostname": "10.0.1.1",
"username": null,
"password": null,
"platform": "junos",
"data": {
"serial": "",
"vendor": "Juniper",
"asset_tag": null,
"site": "sunnyvale-ca",
"role": "rt",
"model": "mx480"
},
"connection_options": {
"napalm": {
"extras": {
"optional_args": {
"ignore_warning": true
}
},
"hostname": null,
"password": null,
"platform": null,
"port": null,
"username": null
}
},
"groups": []
},
"2-Distribution": {
"port": null,
"hostname": "172.16.2.1",
"username": null,
"password": null,
"platform": "junos",
"data": {
"serial": "",
"vendor": "Juniper",
"asset_tag": null,
"site": "sunnyvale-ca",
"role": "rt",
"model": "ex4550-32f"
},
"connection_options": {
"napalm": {
"extras": {
"optional_args": {
"ignore_warning": true
}
},
"hostname": null,
"password": null,
"platform": null,
"port": null,
"username": null
}
},
"groups": []
},
"3-Access": {
"port": null,
"hostname": "192.168.3.1",
"username": null,
"password": null,
"platform": null,
"data": {
"user_defined": 1,
"serial": "",
"vendor": "Cisco",
"asset_tag": null,
"site": "san-jose-ca",
"role": "sw",
"model": "3650-48tq-l"
},
"connection_options": {},
"groups": []
},
"4": {
"port": null,
"hostname": "10.0.1.4",
"username": null,
"password": null,
"platform": "junos",
"data": {
"serial": "",
"vendor": "Juniper",
"asset_tag": null,
"site": "sunnyvale-ca",
"role": "rt",
"model": "mx480"
},
"connection_options": {
"napalm": {
"extras": {
"optional_args": {
"ignore_warning": true
}
},
"hostname": null,
"password": null,
"platform": null,
"port": null,
"username": null
}
},
"groups": []
}
},
"groups": {},
"defaults": {
"port": null,
"hostname": null,
"username": null,
"password": null,
"platform": null,
"data": {},
"connection_options": {}
}
}

0 comments on commit 916b841

Please sign in to comment.