Skip to content

Commit 66d4e85

Browse files
Added several new modules (#11)
* Added several new modules * Updated unit tests * Updated README
1 parent 3f9e8cf commit 66d4e85

31 files changed

+3860
-87
lines changed

.travis.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ env:
99
global:
1010
- COLLECTION_NAMESPACE: fragmentedpacket
1111
- COLLECTION_NAME: netbox_modules
12-
- COLLECTION_VERSION: 0.0.9
12+
- COLLECTION_VERSION: 0.1.0
1313

1414
matrix:
1515
include:
@@ -42,7 +42,7 @@ matrix:
4242
- ansible-galaxy collection install $COLLECTION_NAMESPACE-$COLLECTION_NAME-$COLLECTION_VERSION.tar.gz -p /home/travis/.ansible/collections
4343

4444
script:
45-
- ansible-test units --python $PYTHON_VER -vvv
45+
- ansible-test units --python $PYTHON_VER -v
4646
- black . --check
4747
- "sleep 60"
4848
- python tests/integration/netbox-deploy.py
@@ -77,7 +77,7 @@ matrix:
7777
- ansible-galaxy collection install $COLLECTION_NAMESPACE-$COLLECTION_NAME-$COLLECTION_VERSION.tar.gz -p /home/travis/.ansible/collections
7878

7979
script:
80-
- ansible-test units --python $PYTHON_VER -vv
80+
- ansible-test units --python $PYTHON_VER -v
8181
- black . --check
8282
- "sleep 60"
8383
- python tests/integration/netbox-deploy.py

README.md

Lines changed: 276 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
[![Build Status](https://travis-ci.org/FragmentedPacket/netbox_modules.svg?branch=master)](https://travis-ci.org/FragmentedPacket/netbox_modules)
2-
# Netbox modules for Ansible using Ansible Collections
1+
[![Build Status](https://travis-ci.org/FragmentedPacket/netbox_modules.svg?branch=master)](https://travis-ci.org/FragmentedPacket/netbox_modules)
2+
# Netbox modules for Ansible using Ansible Collections
33

44
**THIS IS A WIP DUE TO COLLECTIONS BEING IN TECH REVIEW CURRENTLY (Ansible 2.8) BUT WILL BE UPDATED AS NECESSARY AS COLLECTIONS MATURES**
55

@@ -27,11 +27,22 @@
2727

2828
- netbox_device
2929
- netbox_device_interface
30+
- netbox_device_role
31+
- netbox_device_type
3032
- netbox_ip_address
33+
- netbox_ipam_role
34+
- netbox_manufacturer
35+
- netbox_platform
3136
- netbox_prefix
37+
- netbox_rack_group
38+
- netbox_rack_role
39+
- netbox_rack
3240
- netbox_site
3341
- netbox_tenant
3442
- netbox_tenant_group
43+
- netbox_vlan_group
44+
- netbox_vlan
45+
- netbox_vrf
3546

3647
## How to Use
3748

@@ -68,15 +79,272 @@
6879
6980
The structure of the Netbox modules attempts to follow the layout of the Netbox API by having a module_util for each application (`dcim, ipam, tenancy, etc`) that inherits from a base module (`NetboxModule - netbox_utils.py`) and then implements the specific endpoints within the correct application module.
7081

71-
ex. Add logic for adding devices under netbox_dcim.py or ip addresses under netbox_ipam.py
82+
e.g. Add logic for adding devices under netbox_dcim.py or ip addresses under netbox_ipam.py
7283

7384
In turn when creating the actual modules, we're just calling a single function and passing in the Ansible Module and the endpoint. This means all the logic is within the specific application's module_util module and a lot of the logic should be the same for most endpoints since it is a basic operation of using the desired state of the endpoint and then either making sure it exists, updating it if it does exist, or removing it. There may be some special logic for other endpoints, but it should be minimal.
7485

7586
(Ansible Module) netbox_{{ endpoint }} -> (Module Util) netbox_{{ application }} -> (Module Util) netbox_utils
7687

77-
### Testing
88+
These modules are built using the pynetbox Python library which allows you to interact with Netbox using objects. Most of this is abstracted away when creating more modules, but something to be aware of. The reasoning for using underscores within the endpoint names is so the endpoints work with pynetbox.
7889

79-
1. Please update `tests/unit/module_utils/test_netbox_base_class.py` if editing anything within the base class that needs to be tested.
80-
2. Please add or update an existing play to test the new Netbox module for integration testing within `tests/integration/integration-tests.yml`
81-
3. Run `black .` within the base directory for black formatting as it's required for tests to pass
82-
4. Check necessary dependencies defined within `.travis.yml` for now if you're wanting to test locally
90+
An example of connecting to a Netbox instance and then choosing the application, endpoint, and operation:
91+
```python
92+
import pynetbox
93+
94+
nb = pynetbox.api("http://localhost:32768", "0123456789abcdef0123456789abcdef01234567")
95+
96+
# applications
97+
nb.circuits
98+
nb.dcim
99+
nb.extras
100+
nb.ipam
101+
nb.secrets
102+
nb.tenancy
103+
nb.virtualization
104+
105+
# endpoints (small sample)
106+
nb.circuits.providers
107+
nb.dcim.devices
108+
nb.dcim.device_types
109+
nb.ipam.vrfs
110+
nb.ipam.ip_addresses
111+
nb.tenancy.tenant_groups
112+
113+
# operations
114+
## Grabs a list of all endpoints
115+
nb.dcim.devices.**all**
116+
## Can pass a list of dicts to create multiple of the endpoints or just a dict to create a single endpoint
117+
nb.dcim.devices.**create**
118+
## Can filter to grab a name of the endpoint being filtered, not an object (Uses the same search criteria as the API)
119+
nb.dcim.devices.**filter**
120+
e.g. nb.dcim.devices.filter(name="test")
121+
## Will retrieve the actual object that can be manipulated (updated, etc.) (Uses the same search criteria as the API)
122+
nb.dcim.devices.**get**
123+
e.g. nb.dcim.devices.get(name="test")
124+
125+
# Manipulate object after using .get
126+
## Now you can manipulate the object the same as a Python object
127+
device = nb.dcim.devices.get(name="test")
128+
device.description = "Test Description"
129+
## Patch operation (patches the data to the API)
130+
device.save()
131+
132+
## If you were to just update the data in a fell swoop
133+
serial = {"serial": "FXS10001", "description": "Test Description"}
134+
## this operation will update the device and use the .save() method behind the scenes
135+
device.update(serial)
136+
```
137+
138+
### Adding an Endpoint
139+
140+
#### Updating Variables within Module Utils
141+
142+
First thing is to setup several variables within **netbox_utils** and **netbox_application** module utils:
143+
144+
Check the following variable to make sure the endpoint is within the correct application within **netbox_utils**:
145+
146+
```python
147+
API_APPS_ENDPOINTS = dict(
148+
circuits=[],
149+
dcim=[
150+
"devices",
151+
"device_roles",
152+
"device_types",
153+
"interfaces",
154+
"manufacturers",
155+
"platforms",
156+
"racks",
157+
"rack_groups",
158+
"rack_roles",
159+
"regions",
160+
"sites",
161+
],
162+
extras=[],
163+
ipam=["ip_addresses", "prefixes", "roles", "vlans", "vlan_groups", "vrfs"],
164+
secrets=[],
165+
tenancy=["tenants", "tenant_groups"],
166+
virtualization=["clusters"],
167+
)
168+
```
169+
170+
Create a new variable in the **netbox_application** module until that matches the endpoint with any spaces being converted to underscores and all lowercase:
171+
172+
```python
173+
NB_DEVICE_TYPES = "device_types"
174+
```
175+
176+
Add the endpoint to the **run** method of supported endpoints:
177+
178+
```python
179+
class NetboxDcimModule(NetboxModule):
180+
def __init__(self, module, endpoint):
181+
super().__init__(module, endpoint)
182+
183+
def run(self):
184+
"""
185+
This function should have all necessary code for endpoints within the application
186+
to create/update/delete the endpoint objects
187+
Supported endpoints:
188+
- device_types
189+
```
190+
191+
Add the endpoint to the **ENDPOINT_NAME_MAPPING** variable within the **netbox_utils** module util.
192+
193+
```python
194+
ENDPOINT_NAME_MAPPING = {
195+
"device_types": "device_type",
196+
}
197+
```
198+
199+
Log into your Netbox instance and navigate to `/api/docs` and searching for the **POST** documents for the given endpoint you're looking to create.
200+
![POST Results](docs/media/postresults.PNG)
201+
The module should implement all available fields that are not the **id** or **readOnly** such as the **created, last_updated, device_count** in the example above.
202+
203+
Add the endpoint to the **ALLOWED_QUERY_PARAMS** variable within the **netbox_utils** module util. This should be something unique for the endpoint and will be used within the **_build_query_params** method to dynamically build query params.
204+
205+
```python
206+
ALLOWED_QUERY_PARAMS = {
207+
"device_type": set(["slug"]),
208+
}
209+
```
210+
211+
If the endpoint has a key that uses an **Array**, you will need to check the **_choices** of the application the endpoint is in and build those into **netbox_utils** module util.
212+
213+
```python
214+
SUBDEVICE_ROLES = dict(parent=True, child=False)
215+
216+
REQUIRED_ID_FIND = {
217+
"device_types": [{"subdevice_role": SUBDEVICE_ROLES}],
218+
}
219+
# This is the method that uses the REQUIRED_ID_FIND variable (no change should be required within the method)
220+
def _change_choices_id(self, endpoint, data):
221+
"""Used to change data that is static and under _choices for the application.
222+
e.g. DEVICE_STATUS
223+
:returns data (dict): Returns the user defined data back with updated fields for _choices
224+
:params endpoint (str): The endpoint that will be used for mapping to required _choices
225+
:params data (dict): User defined data passed into the module
226+
"""
227+
if REQUIRED_ID_FIND.get(endpoint):
228+
required_choices = REQUIRED_ID_FIND[endpoint]
229+
for choice in required_choices:
230+
for key, value in choice.items():
231+
if data.get(key):
232+
try:
233+
data[key] = value[data[key].lower()]
234+
except KeyError:
235+
self._handle_errors(
236+
msg="%s may not be a valid choice. If it is valid, please submit bug report."
237+
% (key)
238+
)
239+
240+
return data
241+
```
242+
243+
If the key is something that pertains to a different endpoint such as **manufacturer** it will need to be added to a few variables within **netbox_utils**.
244+
245+
```python
246+
CONVERT_TO_ID = dict(
247+
manufacturer="manufacturers",
248+
)
249+
QUERY_TYPES = dict(
250+
manufacturer="slug",
251+
)
252+
```
253+
254+
If **slug** and **name** is required, we should leave **slug** out as an option within the module docs and generate it dynamically. Add the endpoint to **SLUG_REQUIRED** within **netbox_utils** module util.
255+
256+
```python
257+
SLUG_REQUIRED = {
258+
"device_roles",
259+
"ipam_roles",
260+
"rack_groups",
261+
"rack_roles",
262+
"roles",
263+
"manufacturers",
264+
"platforms",
265+
"vlan_groups",
266+
}
267+
```
268+
269+
Add code to the **netbox_application** module util to convert name to **slug**"
270+
271+
```python
272+
if self.endpoint in SLUG_REQUIRED:
273+
if not data.get("slug"):
274+
data["slug"] = self._to_slug(name)
275+
```
276+
277+
If either **role** or **group** are within the acceptable keys to POST to the endpoint, we should prefix it with the endpoint name. This is to prevent the code from trying to fetch an ID from the wrong endpoint.
278+
Add the new key to **CONVERT_KEYS** within **netbox_utils** module util.
279+
280+
```python
281+
CONVERT_KEYS = {
282+
"prefix_role": "role",
283+
"rack_group": "group",
284+
"rack_role": "role",
285+
"tenant_group": "group",
286+
"vlan_role": "role",
287+
"vlan_group": "group",
288+
}
289+
290+
# Adding the method that uses this code (no change should be required within the method)
291+
def _convert_identical_keys(self, data):
292+
"""
293+
Used to change non-clashing keys for each module into identical keys that are required
294+
to be passed to pynetbox
295+
ex. rack_role back into role to pass to Netbox
296+
Returns data
297+
:params data (dict): Data dictionary after _find_ids method ran
298+
"""
299+
for key in data:
300+
if key in CONVERT_KEYS:
301+
new_key = CONVERT_KEYS[key]
302+
value = data.pop(key)
303+
data[new_key] = value
304+
305+
return data
306+
```
307+
308+
#### Creating **netbox_endpoint** Module
309+
310+
Copying an existing module that has close to the same options is typically the path to least resistence and then updating portions of it to fit the new module.
311+
312+
- Change the author: `Copyright: (c) 2018, Mikhail Yohman (@FragmentedPacket) <mikhail.yohman@gmail.com>`
313+
- Update the **DOCUMENTATION**/**EXAMPLES**/**RETURN** string with the necessary information
314+
- Main things are module, descriptions, author, version and the sub options under data
315+
- The **RETURN** should return the singluar of the endpoint name (done dynamically, but needs to be documented correctly)
316+
- Update the module_util, module, and endpoint variable for the endpoint
317+
318+
```python
319+
from ansible_collections.fragmentedpacket.netbox_modules.plugins.module_utils.netbox_dcim import (
320+
NetboxDcimModule,
321+
NB_DEVICE_ROLES,
322+
)
323+
```
324+
325+
- Update the **main()** as necessary:
326+
327+
```python
328+
# Add if name is required or change to match required fields
329+
if not module.params["data"].get("name"):
330+
module.fail_json(msg="missing name")
331+
# Make sure the objects are endpoint name and the correct class and variable are being called for the endpoint
332+
netbox_device_role = NetboxDcimModule(module, NB_DEVICE_ROLES)
333+
netbox_device_role.run()
334+
```
335+
336+
#### Testing
337+
338+
- Please update `tests/unit/module_utils/test_netbox_base_class.py` if editing anything within the base class that needs to be tested. This will most likely be needed as there are a few unit tests that test the data of **ALLOWED_QUERY_PARAMS**, etc.
339+
340+
```python
341+
def test_normalize_data_returns_correct_data()
342+
def test_find_app_returns_valid_app()
343+
def test_change_choices_id()
344+
def test_build_query_params_no_child()
345+
def test_build_query_params_child()
346+
```
347+
348+
- Please add or update an existing play to test the new Netbox module for integration testing within `tests/integration/integration-tests.yml`. Make sure to test creation, duplicate, update (if possible), and deletion along with any other conditions that may want to be tested.
349+
- Run `black .` within the base directory for black formatting as it's required for tests to pass
350+
- Check necessary dependencies defined within `.travis.yml` for now if you're wanting to test locally

docs/media/postresults.PNG

44.1 KB
Loading

galaxy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace: fragmentedpacket
99
name: netbox_modules
1010

1111
# The version of the collection. Must be compatible with semantic versioning
12-
version: 0.0.9
12+
version: 0.1.0
1313

1414
# The path to the Markdown (.md) readme file. This path is relative to the root of the collection
1515
readme: README.md

0 commit comments

Comments
 (0)