Skip to content

Commit 0a623df

Browse files
committed
Inventory: Add region group_by option (netbox-community#178)
1 parent 2436f50 commit 0a623df

File tree

3 files changed

+131
-6
lines changed

3 files changed

+131
-6
lines changed

plugins/inventory/nb_inventory.py

Lines changed: 129 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
- manufacturer
8989
- platforms
9090
- platform
91+
- region
9192
default: []
9293
group_names_raw:
9394
description: Will not add the group_by choice name to the group names
@@ -493,6 +494,7 @@ def group_extractors(self):
493494
"manufacturers": self.extract_manufacturer,
494495
"interfaces": self.extract_interfaces,
495496
"custom_fields": self.extract_custom_fields,
497+
"region": self.extract_regions,
496498
}
497499
else:
498500
return {
@@ -511,6 +513,7 @@ def group_extractors(self):
511513
"manufacturer": self.extract_manufacturer,
512514
"interfaces": self.extract_interfaces,
513515
"custom_fields": self.extract_custom_fields,
516+
"region": self.extract_regions,
514517
}
515518

516519
def _pluralize(self, something):
@@ -681,6 +684,39 @@ def extract_custom_fields(self, host):
681684
except Exception:
682685
return
683686

687+
def extract_regions(self, host):
688+
# A host may have a site. A site may have a region. A region may have a parent region.
689+
# Produce a list of regions:
690+
# - it will be empty if the device has no site, or the site has no region set
691+
# - it will have 1 element if the site's region has no parent
692+
# - it will have multiple elements if the site's region has a parent region
693+
694+
site = host.get("site", None)
695+
if not isinstance(site, dict):
696+
# Device has no site
697+
return []
698+
699+
site_id = site.get("id", None)
700+
if site_id == None:
701+
# Device has no site
702+
return []
703+
704+
regions = []
705+
region_id = self.sites_region_lookup[site_id]
706+
707+
# Keep looping until the region has no parent
708+
while region_id != None:
709+
region_slug = self.regions_lookup[region_id]
710+
if region_slug in regions:
711+
# Somehow we've got an infinite loop? (Shouldn't ever happen)
712+
break
713+
regions.append(region_slug)
714+
715+
# Get the parent of this region
716+
region_id = self.regions_parent_lookup[region_id]
717+
718+
return regions
719+
684720
def refresh_platforms_lookup(self):
685721
url = self.api_endpoint + "/api/dcim/platforms/?limit=0"
686722
platforms = self.get_resource_list(api_url=url)
@@ -693,10 +729,25 @@ def refresh_sites_lookup(self):
693729
sites = self.get_resource_list(api_url=url)
694730
self.sites_lookup = dict((site["id"], site["slug"]) for site in sites)
695731

732+
def get_region_for_site(site):
733+
# Will fail if site does not have a region defined in Netbox
734+
try:
735+
return (site["id"], site["region"]["id"])
736+
except Exception:
737+
return (site["id"], None)
738+
739+
# Diction of site id to region id
740+
self.sites_region_lookup = dict(
741+
filter(lambda x: x is not None, map(get_region_for_site, sites))
742+
)
743+
696744
def refresh_regions_lookup(self):
697745
url = self.api_endpoint + "/api/dcim/regions/?limit=0"
698746
regions = self.get_resource_list(api_url=url)
699747
self.regions_lookup = dict((region["id"], region["slug"]) for region in regions)
748+
self.regions_parent_lookup = dict(
749+
(region["id"], region["parent"]) for region in regions
750+
)
700751

701752
def refresh_tenants_lookup(self):
702753
url = self.api_endpoint + "/api/tenancy/tenants/?limit=0"
@@ -863,8 +914,31 @@ def extract_name(self, host):
863914
# We default to an UUID for hostname in case the name is not set in NetBox
864915
return host["name"] or str(uuid.uuid4())
865916

917+
def generate_group_name(self, grouping, group):
918+
if self.group_names_raw:
919+
return group
920+
else:
921+
return "_".join([grouping, group])
922+
866923
def add_host_to_groups(self, host, hostname):
924+
925+
# If we're grouping by regions, hosts are not added to region groups
926+
# - the site groups are added as sub-groups of regions
927+
# So, we need to make sure we're also grouping by sites if regions are enabled
928+
929+
if "region" in self.group_by:
930+
# Make sure "site" or "sites" grouping also exists, depending on plurals options
931+
if self.plurals and "sites" not in self.group_by:
932+
self.group_by.append("sites")
933+
elif not self.plurals and "site" not in self.group_by:
934+
self.group_by.append("site")
935+
867936
for grouping in self.group_by:
937+
938+
# Don't handle regions here - that will happen in main()
939+
if grouping == "region":
940+
continue
941+
868942
groups_for_host = self.group_extractors[grouping](host)
869943

870944
if not groups_for_host:
@@ -875,16 +949,60 @@ def add_host_to_groups(self, host, hostname):
875949
groups_for_host = [groups_for_host]
876950

877951
for group_for_host in groups_for_host:
878-
if self.group_names_raw:
879-
group_name = group_for_host
880-
else:
881-
group_name = "_".join([grouping, group_for_host])
952+
group_name = self.generate_group_name(grouping, group_for_host)
882953

883954
# Group names may be transformed by the ansible TRANSFORM_INVALID_GROUP_CHARS setting
884955
# add_group returns the actual group name used
885956
transformed_group_name = self.inventory.add_group(group=group_name)
886957
self.inventory.add_host(group=transformed_group_name, host=hostname)
887958

959+
def _add_region_groups(self):
960+
961+
# Mapping of region id to group name
962+
region_transformed_group_names = dict()
963+
964+
# Create groups for each region
965+
for region_id in self.regions_lookup:
966+
region_group_name = self.generate_group_name(
967+
"region", self.regions_lookup[region_id]
968+
)
969+
region_transformed_group_names[region_id] = self.inventory.add_group(
970+
group=region_group_name
971+
)
972+
973+
# Now that all region groups exist, add relationships between them
974+
for region_id in self.regions_lookup:
975+
region_group_name = region_transformed_group_names[region_id]
976+
parent_region_id = self.regions_parent_lookup.get(region_id, None)
977+
if (
978+
parent_region_id != None
979+
and parent_region_id in region_transformed_group_names
980+
):
981+
parent_region_name = region_transformed_group_names[parent_region_id]
982+
self.inventory.add_child(parent_region_name, region_group_name)
983+
984+
# Add site groups as children of region groups
985+
for site_id in self.sites_lookup:
986+
region_id = self.sites_region_lookup.get(site_id, None)
987+
if region_id == None:
988+
continue
989+
990+
region_transformed_group_name = region_transformed_group_names[region_id]
991+
992+
site_name = self.sites_lookup[site_id]
993+
site_group_name = self.generate_group_name(
994+
"sites" if self.plurals else "site", site_name
995+
)
996+
# Add the site group to get its transformed name
997+
# Will already be created by add_host_to_groups - it's ok to call add_group again just to get its name
998+
site_transformed_group_name = self.inventory.add_group(
999+
group=site_group_name
1000+
)
1001+
1002+
self.inventory.add_child(
1003+
region_transformed_group_name, site_transformed_group_name
1004+
)
1005+
8881006
def _fill_host_variables(self, host, hostname):
8891007
for attribute, extractor in self.group_extractors.items():
8901008
extracted_value = extractor(host)
@@ -896,6 +1014,9 @@ def _fill_host_variables(self, host, hostname):
8961014
if attribute == "tag":
8971015
attribute = "tags"
8981016

1017+
if attribute == "region":
1018+
attribute = "regions"
1019+
8991020
self.inventory.set_variable(hostname, attribute, extracted_value)
9001021

9011022
extracted_primary_ip = self.extract_primary_ip(host=host)
@@ -937,6 +1058,10 @@ def main(self):
9371058
)
9381059
self.add_host_to_groups(host=host, hostname=hostname)
9391060

1061+
# Create groups for regions, containing the site groups
1062+
if "region" in self.group_by:
1063+
self._add_region_groups()
1064+
9401065
def parse(self, inventory, loader, path, cache=True):
9411066
super(InventoryModule, self).parse(inventory, loader, path)
9421067
self._read_config_data(path=path)

tests/integration/test-inventory-plurals.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ group_by:
1717
- device_types
1818
- manufacturers
1919
- platforms
20-
20+
- region

tests/integration/test-inventory.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ group_by:
1717
- device_type
1818
- manufacturer
1919
- platform
20-
20+
- region

0 commit comments

Comments
 (0)