Skip to content

Commit 6ee2cec

Browse files
Inventory: Create groups based on rack groups, and rack roles (#204)
1 parent cf668c7 commit 6ee2cec

20 files changed

+409
-80
lines changed

.travis.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ before_script:
119119
# https://github.com/ansible/ansible/issues/68415
120120
- chmod +x tests/integration/targets/inventory/runme.sh
121121
- chmod +x tests/integration/targets/inventory/compare_inventory_json.py
122+
- chmod +x tests/integration/render_config.sh
123+
124+
# Run render_config.sh to pass environment variables to integration tests
125+
# https://www.ansible.com/blog/adding-integration-tests-to-ansible-content-collections
126+
- tests/integration/render_config.sh tests/integration/targets/inventory/runme_config.template > tests/integration/targets/inventory/runme_config
122127

123128
script:
124129
# Check python syntax

plugins/inventory/nb_inventory.py

Lines changed: 98 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@
112112
- tenant
113113
- racks
114114
- rack
115+
- rack_group
116+
- rack_role
115117
- tags
116118
- tag
117119
- device_roles
@@ -367,6 +369,8 @@ def group_extractors(self):
367369
self._pluralize_group_by("site"): self.extract_site,
368370
self._pluralize_group_by("tenant"): self.extract_tenant,
369371
self._pluralize_group_by("rack"): self.extract_rack,
372+
"rack_group": self.extract_rack_group,
373+
"rack_role": self.extract_rack_role,
370374
self._pluralize_group_by("tag"): self.extract_tags,
371375
self._pluralize_group_by("role"): self.extract_device_role,
372376
self._pluralize_group_by("platform"): self.extract_platform,
@@ -411,6 +415,28 @@ def _pluralize(self, extracted_value):
411415
else:
412416
return extracted_value
413417

418+
def _objects_array_following_parents(
419+
self, initial_object_id, object_lookup, object_parent_lookup
420+
):
421+
objects = []
422+
423+
object_id = initial_object_id
424+
425+
# Keep looping until the object has no parent
426+
while object_id is not None:
427+
object_slug = object_lookup[object_id]
428+
429+
if object_slug in objects:
430+
# Won't ever happen - defensively guard against infinite loop
431+
break
432+
433+
objects.append(object_slug)
434+
435+
# Get the parent of this object
436+
object_id = object_parent_lookup[object_id]
437+
438+
return objects
439+
414440
def extract_disk(self, host):
415441
return host.get("disk")
416442

@@ -451,6 +477,35 @@ def extract_rack(self, host):
451477
except Exception:
452478
return
453479

480+
def extract_rack_group(self, host):
481+
# A host may have a rack. A rack may have a rack_group. A rack_group may have a parent rack_group.
482+
# Produce a list of rack_groups:
483+
# - it will be empty if the device has no rack, or the rack has no rack_group
484+
# - it will have 1 element if the rack's group has no parent
485+
# - it will have multiple elements if the rack's group has a parent group
486+
487+
rack = host.get("rack", None)
488+
if not isinstance(rack, dict):
489+
# Device has no rack
490+
return None
491+
492+
rack_id = rack.get("id", None)
493+
if rack_id is None:
494+
# Device has no rack
495+
return None
496+
497+
return self._objects_array_following_parents(
498+
initial_object_id=self.racks_group_lookup[rack_id],
499+
object_lookup=self.rack_groups_lookup,
500+
object_parent_lookup=self.rack_group_parent_lookup,
501+
)
502+
503+
def extract_rack_role(self, host):
504+
try:
505+
return self.racks_role_lookup[host["rack"]["id"]]
506+
except Exception:
507+
return
508+
454509
def extract_site(self, host):
455510
try:
456511
return self._pluralize(self.sites_lookup[host["site"]["id"]])
@@ -570,21 +625,11 @@ def extract_regions(self, host):
570625
# Device has no site
571626
return []
572627

573-
regions = []
574-
region_id = self.sites_region_lookup[site_id]
575-
576-
# Keep looping until the region has no parent
577-
while region_id is not None:
578-
region_slug = self.regions_lookup[region_id]
579-
if region_slug in regions:
580-
# Won't ever happen - defensively guard against infinite loop
581-
break
582-
regions.append(region_slug)
583-
584-
# Get the parent of this region
585-
region_id = self.regions_parent_lookup[region_id]
586-
587-
return regions
628+
return self._objects_array_following_parents(
629+
initial_object_id=self.sites_region_lookup[site_id],
630+
object_lookup=self.regions_lookup,
631+
object_parent_lookup=self.regions_parent_lookup,
632+
)
588633

589634
def extract_cluster(self, host):
590635
try:
@@ -628,9 +673,7 @@ def get_region_for_site(site):
628673
return (site["id"], None)
629674

630675
# Dictionary of site id to region id
631-
self.sites_region_lookup = dict(
632-
filter(lambda x: x is not None, map(get_region_for_site, sites))
633-
)
676+
self.sites_region_lookup = dict(map(get_region_for_site, sites))
634677

635678
def refresh_regions_lookup(self):
636679
url = self.api_endpoint + "/api/dcim/regions/?limit=0"
@@ -659,6 +702,37 @@ def refresh_racks_lookup(self):
659702
racks = self.get_resource_list(api_url=url)
660703
self.racks_lookup = dict((rack["id"], rack["name"]) for rack in racks)
661704

705+
def get_group_for_rack(rack):
706+
try:
707+
return (rack["id"], rack["group"]["id"])
708+
except Exception:
709+
return (rack["id"], None)
710+
711+
def get_role_for_rack(rack):
712+
try:
713+
return (rack["id"], rack["role"]["slug"])
714+
except Exception:
715+
return (rack["id"], None)
716+
717+
self.racks_group_lookup = dict(map(get_group_for_rack, racks))
718+
self.racks_role_lookup = dict(map(get_role_for_rack, racks))
719+
720+
def refresh_rack_groups_lookup(self):
721+
url = self.api_endpoint + "/api/dcim/rack-groups/?limit=0"
722+
rack_groups = self.get_resource_list(api_url=url)
723+
self.rack_groups_lookup = dict(
724+
(rack_group["id"], rack_group["slug"]) for rack_group in rack_groups
725+
)
726+
727+
def get_rack_group_parent(rack_group):
728+
try:
729+
return (rack_group["id"], rack_group["parent"]["id"])
730+
except Exception:
731+
return (rack_group["id"], None)
732+
733+
# Dictionary of rack group id to parent rack group id
734+
self.rack_group_parent_lookup = dict(map(get_rack_group_parent, rack_groups))
735+
662736
def refresh_device_roles_lookup(self):
663737
url = self.api_endpoint + "/api/dcim/device-roles/?limit=0"
664738
device_roles = self.get_resource_list(api_url=url)
@@ -698,13 +772,8 @@ def get_cluster_group(cluster):
698772
except Exception:
699773
return (cluster["id"], None)
700774

701-
self.clusters_type_lookup = dict(
702-
filter(lambda x: x is not None, map(get_cluster_type, clusters))
703-
)
704-
705-
self.clusters_group_lookup = dict(
706-
filter(lambda x: x is not None, map(get_cluster_group, clusters))
707-
)
775+
self.clusters_type_lookup = dict(map(get_cluster_type, clusters))
776+
self.clusters_group_lookup = dict(map(get_cluster_group, clusters))
708777

709778
def refresh_services(self):
710779
url = self.api_endpoint + "/api/ipam/services/?limit=0"
@@ -855,6 +924,7 @@ def lookup_processes(self):
855924
self.refresh_regions_lookup,
856925
self.refresh_tenants_lookup,
857926
self.refresh_racks_lookup,
927+
self.refresh_rack_groups_lookup,
858928
self.refresh_device_roles_lookup,
859929
self.refresh_platforms_lookup,
860930
self.refresh_device_types_lookup,
@@ -1184,6 +1254,9 @@ def _fill_host_variables(self, host, hostname):
11841254
if attribute == "region":
11851255
attribute = "regions"
11861256

1257+
if attribute == "rack_group":
1258+
attribute = "rack_groups"
1259+
11871260
# Flatten the dict into separate host vars, if enabled
11881261
if isinstance(extracted_value, dict) and (
11891262
(attribute == "config_context" and self.flatten_config_context)

tests/integration/netbox-deploy.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
import os
88
import pynetbox
99

10-
# Set nb variable to connec to Netbox and use the veriable in future calls
10+
# NOTE: If anything depends on specific versions of NetBox, can check INTEGRATION_TESTS in env
11+
# os.environ["INTEGRATION_TESTS"]
12+
13+
14+
# Set nb variable to connect to Netbox and use the veriable in future calls
1115
nb = pynetbox.api("http://localhost:32768", "0123456789abcdef0123456789abcdef01234567")
1216

1317

@@ -159,23 +163,34 @@
159163
core_switch = nb.dcim.device_roles.get(slug="core-switch")
160164

161165

162-
## Create Racks
163-
racks = [{"name": "Test Rack", "slug": "test-rack", "site": test_site2.id}]
164-
created_racks = nb.dcim.racks.create(racks)
165-
test_rack = nb.dcim.racks.get(slug="test-rack")
166-
167-
168166
## Create Rack Groups
169167
rack_groups = [
170-
{"name": "Test Rack Group", "slug": "test-rack-group", "site": test_site.id}
168+
{"name": "Test Rack Group", "slug": "test-rack-group", "site": test_site.id},
169+
{"name": "Parent Rack Group", "slug": "parent-rack-group", "site": test_site.id},
171170
]
172171
created_rack_groups = nb.dcim.rack_groups.create(rack_groups)
173172

173+
### Create Rack Group Parent relationship
174+
created_rack_groups[0].parent = created_rack_groups[1]
175+
created_rack_groups[0].save()
174176

175177
## Create Rack Roles
176178
rack_roles = [{"name": "Test Rack Role", "slug": "test-rack-role", "color": "4287f5"}]
177179
created_rack_roles = nb.dcim.rack_roles.create(rack_roles)
178180

181+
## Create Racks
182+
racks = [
183+
{
184+
"name": "Test Rack Site 2",
185+
"site": test_site2.id,
186+
"role": created_rack_roles[0].id,
187+
},
188+
{"name": "Test Rack", "site": test_site.id, "group": created_rack_groups[0].id},
189+
]
190+
created_racks = nb.dcim.racks.create(racks)
191+
test_rack = nb.dcim.racks.get(name="Test Rack") # racks don't have slugs
192+
test_rack_site2 = nb.dcim.racks.get(name="Test Rack Site 2")
193+
179194

180195
## Create Devices
181196
devices = [
@@ -191,13 +206,14 @@
191206
"device_type": cisco_test.id,
192207
"device_role": core_switch.id,
193208
"site": test_site.id,
209+
"rack": test_rack.id,
194210
},
195211
{
196212
"name": "R1-Device",
197213
"device_type": cisco_test.id,
198214
"device_role": core_switch.id,
199215
"site": test_site2.id,
200-
"rack": test_rack.id,
216+
"rack": test_rack_site2.id,
201217
},
202218
{
203219
"name": "Test Nexus One",
@@ -316,7 +332,6 @@
316332

317333

318334
## Create Services
319-
320335
services = [
321336
{"device": test100.id, "name": "ssh", "port": 22, "protocol": "tcp"},
322337
{

tests/integration/render_config.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env bash
2+
3+
# The only way to pass variables to integration tests - write to a file inside the integration test directory.
4+
# Usage: render_config.sh runme_config.template > runme_config
5+
# Copied from:
6+
# https://www.ansible.com/blog/adding-integration-tests-to-ansible-content-collections
7+
# https://github.com/xlab-si/digital_ocean.digital_ocean/blob/master/tests/utils/render.sh
8+
9+
set -o xtrace # Print commands as they're run
10+
set -o errexit # abort on nonzero exitstatus
11+
set -o nounset # abort on unbound variable
12+
set -o pipefail # don't hide errors within pipes
13+
14+
15+
function main()
16+
{
17+
readonly template="$1"
18+
readonly content="$(cat "${template}")"
19+
20+
eval "echo \"$content\""
21+
}
22+
23+
main "$@"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
runme_config

0 commit comments

Comments
 (0)