Skip to content

Commit dd38949

Browse files
cpmills1975FragmentedPacket
authored andcommitted
Added netbox lookup plugin
1 parent b276a30 commit dd38949

File tree

3 files changed

+234
-2
lines changed

3 files changed

+234
-2
lines changed

.travis.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ matrix:
2020
- pip install -U pip
2121
- pip install pytest==4.6.5 pytest-mock pytest-xdist jinja2 PyYAML black==19.3b0
2222
- pip install pynetbox==4.0.6 cryptography codecov
23+
- pip install jmespath
2324
- cd ../../
2425
# This is due to ansible-test only being available within devel branch
2526
- git clone https://github.com/ansible/ansible.git
@@ -44,7 +45,7 @@ matrix:
4445
script:
4546
- ansible-test units --python $PYTHON_VER -v
4647
- black . --check
47-
- "sleep 60"
48+
- timeout 300 bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:32768)" != "200" ]]; do echo "waiting for Netbox"; sleep 5; done' || false
4849
- python tests/integration/netbox-deploy.py
4950
- ansible-playbook tests/integration/integration-tests.yml -v
5051

@@ -55,6 +56,7 @@ matrix:
5556
- pip install -U pip
5657
- pip install pytest==4.6.5 pytest-mock pytest-xdist jinja2 PyYAML black==19.3b0
5758
- pip install pynetbox==4.0.6 cryptography
59+
- pip install jmespath
5860
- cd ../../
5961
# This is due to ansible-test only being available within devel branch
6062
- git clone https://github.com/ansible/ansible.git
@@ -79,6 +81,6 @@ matrix:
7981
script:
8082
- ansible-test units --python $PYTHON_VER -v
8183
- black . --check
82-
- "sleep 60"
84+
- timeout 300 bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:32768)" != "200" ]]; do echo "waiting for Netbox"; sleep 5; done' || false
8385
- python tests/integration/netbox-deploy.py
8486
- ansible-playbook tests/integration/integration-tests.yml -v

plugins/lookup/netbox.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# Copyright: (c) 2019. Chris Mills <chris@discreet-its.co.uk>
4+
# GNU General Public License v3.0+
5+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6+
"""
7+
netbox.py
8+
9+
A lookup function designed to return data from the Netbox application
10+
"""
11+
12+
from __future__ import absolute_import, division, print_function
13+
14+
from pprint import pformat
15+
16+
from ansible.errors import AnsibleError
17+
from ansible.plugins.lookup import LookupBase
18+
from ansible.utils.display import Display
19+
20+
import pynetbox
21+
22+
__metaclass__ = type
23+
24+
DOCUMENTATION = """
25+
lookup: netbox
26+
author: Chris Mills (@cpmills1975)
27+
version_added: "2.9"
28+
short_description: Queries and returns elements from Netbox
29+
description:
30+
- Queries Netbox via its API to return virtually any information
31+
capable of being held in Netbox.
32+
- While secrets can be queried, the plugin doesn't yet support
33+
decrypting them.
34+
options:
35+
_terms:
36+
description:
37+
- The Netbox object type to query
38+
required: True
39+
api_endpoint:
40+
description:
41+
- The URL to the Netbox instance to query
42+
required: True
43+
token:
44+
description:
45+
- The API token created through Netbox
46+
required: True
47+
requirements:
48+
- pynetbox
49+
"""
50+
51+
EXAMPLES = """
52+
tasks:
53+
# query a list of devices
54+
- name: Obtain list of devices from Netbox
55+
debug:
56+
msg: >
57+
"Device {{ item.value.display_name }} (ID: {{ item.key }}) was
58+
manufactured by {{ item.value.device_type.manufacturer.name }}"
59+
loop: "{{ query('netbox', 'devices',
60+
api_endpoint='http://localhost/',
61+
token='<redacted>') }}"
62+
"""
63+
64+
RETURN = """
65+
_list:
66+
description:
67+
- list of composed dictonaries with key and value
68+
type: list
69+
"""
70+
71+
72+
def get_endpoint(netbox, term):
73+
"""
74+
get_endpoint(netbox, term)
75+
netbox: a predefined pynetbox.api() pointing to a valid instance
76+
of Netbox
77+
term: the term passed to the lookup function upon which the api
78+
call will be identified
79+
"""
80+
81+
netbox_endpoint_map = {
82+
"aggregates": {"endpoint": netbox.ipam.aggregates},
83+
"circuit-terminations": {"endpoint": netbox.circuits.circuit_terminations},
84+
"circuit-types": {"endpoint": netbox.circuits.circuit_types},
85+
"circuits": {"endpoint": netbox.circuits.circuits},
86+
"circuit-providers": {"endpoint": netbox.circuits.providers},
87+
"cables": {"endpoint": netbox.dcim.cables},
88+
"cluster-groups": {"endpoint": netbox.virtualization.cluster_groups},
89+
"cluster-types": {"endpoint": netbox.virtualization.cluster_types},
90+
"clusters": {"endpoint": netbox.virtualization.clusters},
91+
"config-contexts": {"endpoint": netbox.extras.config_contexts},
92+
"console-connections": {"endpoint": netbox.dcim.console_connections},
93+
"console-ports": {"endpoint": netbox.dcim.console_ports},
94+
"console-server-port-templates": {
95+
"endpoint": netbox.dcim.console_server_port_templates
96+
},
97+
"console-server-ports": {"endpoint": netbox.dcim.console_server_ports},
98+
"device-bay-templates": {"endpoint": netbox.dcim.device_bay_templates},
99+
"device-bays": {"endpoint": netbox.dcim.device_bays},
100+
"device-roles": {"endpoint": netbox.dcim.device_roles},
101+
"device-types": {"endpoint": netbox.dcim.device_types},
102+
"devices": {"endpoint": netbox.dcim.devices},
103+
"export-templates": {"endpoint": netbox.dcim.export_templates},
104+
"front-port-templates": {"endpoint": netbox.dcim.front_port_templates},
105+
"front-ports": {"endpoint": netbox.dcim.front_ports},
106+
"graphs": {"endpoint": netbox.extras.graphs},
107+
"image-attachments": {"endpoint": netbox.extras.image_attachments},
108+
"interface-connections": {"endpoint": netbox.dcim.interface_connections},
109+
"interface-templates": {"endpoint": netbox.dcim.interface_templates},
110+
"interfaces": {"endpoint": netbox.dcim.interfaces},
111+
"inventory-items": {"endpoint": netbox.dcim.inventory_items},
112+
"ip-addresses": {"endpoint": netbox.ipam.ip_addresses},
113+
"manufacturers": {"endpoint": netbox.dcim.manufacturers},
114+
"object-changes": {"endpoint": netbox.extras.object_changes},
115+
"platforms": {"endpoint": netbox.dcim.platforms},
116+
"power-connections": {"endpoint": netbox.dcim.power_connections},
117+
"power-outlet-templates": {"endpoint": netbox.dcim.power_outlet_templates},
118+
"power-outlets": {"endpoint": netbox.dcim.power_outlets},
119+
"power-port-templates": {"endpoint": netbox.dcim.power_port_templates},
120+
"power-ports": {"endpoint": netbox.dcim.power_ports},
121+
"prefixes": {"endpoint": netbox.ipam.prefixes},
122+
"rack-groups": {"endpoint": netbox.dcim.rack_groups},
123+
"rack-reservations": {"endpoint": netbox.dcim.rack_reservations},
124+
"rack-roles": {"endpoint": netbox.dcim.rack_roles},
125+
"racks": {"endpoint": netbox.dcim.racks},
126+
"rear-port-templates": {"endpoint": netbox.dcim.rear_port_templates},
127+
"rear-ports": {"endpoint": netbox.dcim.rear_ports},
128+
"regions": {"endpoint": netbox.dcim.regions},
129+
"reports": {"endpoint": netbox.extras.reports},
130+
"rirs": {"endpoint": netbox.ipam.rirs},
131+
"roles": {"endpoint": netbox.ipam.roles},
132+
"secret-roles": {"endpoint": netbox.secrets.secret_roles},
133+
# Note: Currently unable to decrypt secrets as key wizardry needs to
134+
# take place first but term will return unencrypted elements of secrets
135+
# i.e. that they exist etc.
136+
"secrets": {"endpoint": netbox.secrets.secrets},
137+
"services": {"endpoint": netbox.ipam.services},
138+
"sites": {"endpoint": netbox.dcim.sites},
139+
"tags": {"endpoint": netbox.extras.tags},
140+
"tenant-groups": {"endpoint": netbox.tenancy.tenant_groups},
141+
"tenants": {"endpoint": netbox.tenancy.tenants},
142+
"topology-maps": {"endpoint": netbox.extras.topology_maps},
143+
"virtual-chassis": {"endpoint": netbox.dcim.virtual_chassis},
144+
"virtual-machines": {"endpoint": netbox.dcim.virtual_machines},
145+
"virtualization-interfaces": {"endpoint": netbox.virtualization.interfaces},
146+
"vlan-groups": {"endpoint": netbox.ipam.vlan_groups},
147+
"vlans": {"endpoint": netbox.ipam.vlans},
148+
"vrfs": {"endpoint": netbox.ipam.vrfs},
149+
}
150+
151+
return netbox_endpoint_map[term]["endpoint"]
152+
153+
154+
class LookupModule(LookupBase):
155+
"""
156+
LookupModule(LookupBase) is defined by Ansible
157+
"""
158+
159+
def run(self, terms, variables=None, **kwargs):
160+
161+
netbox_api_token = kwargs.get("token")
162+
netbox_api_endpoint = kwargs.get("api_endpoint")
163+
netbox_private_key_file = kwargs.get("key_file")
164+
165+
if not isinstance(terms, list):
166+
terms = [terms]
167+
168+
netbox = pynetbox.api(
169+
netbox_api_endpoint,
170+
token=netbox_api_token,
171+
private_key_file=netbox_private_key_file,
172+
)
173+
174+
results = []
175+
for term in terms:
176+
177+
try:
178+
endpoint = get_endpoint(netbox, term)
179+
except KeyError:
180+
raise AnsibleError("Unrecognised term %s. Check documentation" % term)
181+
182+
Display().vvvv(
183+
u"Netbox lookup for %s to %s using token %s"
184+
% (term, netbox_api_endpoint, netbox_api_token)
185+
)
186+
for res in endpoint.all():
187+
188+
Display().vvvvv(pformat(dict(res)))
189+
190+
key = dict(res)["id"]
191+
result = {key: dict(res)}
192+
193+
results.extend(self._flatten_hash_to_list(result))
194+
195+
return results

tests/integration/integration-tests.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3006,3 +3006,38 @@
30063006
- test_four['inventory_item']['part_id'] == "10G-SFP+"
30073007
- test_four['inventory_item']['tags'][0] == "Schnozzberry"
30083008
- test_four['msg'] == "inventory_item 10G-SFP+ deleted"
3009+
3010+
##
3011+
##
3012+
### NETBOX_LOOKUP
3013+
##
3014+
##
3015+
- name: "TEST NETBOX_LOOKUP"
3016+
connection: local
3017+
hosts: localhost
3018+
gather_facts: False
3019+
3020+
tasks:
3021+
- name: "NETBOX_LOOKUP 1: Lookup returns exactly two sites"
3022+
assert:
3023+
that: "{{ query_result|count }} == 3"
3024+
vars:
3025+
query_result: "{{ query('fragmentedpacket.netbox_modules.netbox', 'sites', api_endpoint='http://localhost:32768', token='0123456789abcdef0123456789abcdef01234567') }}"
3026+
3027+
- name: "NETBOX_LOOKUP 2: Query doesn't return Wibble (sanity check json_query)"
3028+
assert:
3029+
that: "{{ query_result|json_query('[?value.display_name==`Wibble`]')|count }} == 0"
3030+
vars:
3031+
query_result: "{{ query('fragmentedpacket.netbox_modules.netbox', 'devices', api_endpoint='http://localhost:32768', token='0123456789abcdef0123456789abcdef01234567') }}"
3032+
3033+
- name: "NETBOX_LOOKUP 3: Device query returns exactly one TestDeviceR1"
3034+
assert:
3035+
that: "{{ query_result|json_query('[?value.display_name==`TestDeviceR1`]')|count }} == 1"
3036+
vars:
3037+
query_result: "{{ query('fragmentedpacket.netbox_modules.netbox', 'devices', api_endpoint='http://localhost:32768', token='0123456789abcdef0123456789abcdef01234567') }}"
3038+
3039+
- name: "NETBOX_LOOKUP 4: VLAN ID 400 can be queried and is named 'Test VLAN'"
3040+
assert:
3041+
that: "{{ (query_result|json_query('[?value.vid==`400`].value.name'))[0] == 'Test VLAN' }}"
3042+
vars:
3043+
query_result: "{{ query('fragmentedpacket.netbox_modules.netbox', 'vlans', api_endpoint='http://localhost:32768', token='0123456789abcdef0123456789abcdef01234567') }}"

0 commit comments

Comments
 (0)