109109 type: boolean
110110 version_added: "0.2.1"
111111 group_by:
112- description: Keys used to create groups. The I(plurals) option controls which of these are valid.
112+ description:
113+ - Keys used to create groups. The I(plurals) option controls which of these are valid.
114+ - I(rack_group) is supported on NetBox versions 2.10 or lower only
115+ - I(location) is supported on NetBox versions 2.11 or higher only
113116 type: list
114117 choices:
115118 - sites
116119 - site
120+ - location
117121 - tenants
118122 - tenant
119123 - racks
@@ -412,7 +416,6 @@ def group_extractors(self):
412416 self ._pluralize_group_by ("site" ): self .extract_site ,
413417 self ._pluralize_group_by ("tenant" ): self .extract_tenant ,
414418 self ._pluralize_group_by ("rack" ): self .extract_rack ,
415- "rack_group" : self .extract_rack_group ,
416419 "rack_role" : self .extract_rack_role ,
417420 self ._pluralize_group_by ("tag" ): self .extract_tags ,
418421 self ._pluralize_group_by ("role" ): self .extract_device_role ,
@@ -421,6 +424,16 @@ def group_extractors(self):
421424 self ._pluralize_group_by ("manufacturer" ): self .extract_manufacturer ,
422425 }
423426
427+ # Locations were added in 2.11 replacing rack-groups.
428+ if self .api_version >= version .parse ("2.11" ):
429+ extractors .update (
430+ {"location" : self .extract_location ,}
431+ )
432+ else :
433+ extractors .update (
434+ {"rack_group" : self .extract_rack_group ,}
435+ )
436+
424437 if self .services :
425438 extractors .update (
426439 {"services" : self .extract_services ,}
@@ -704,6 +717,25 @@ def extract_regions(self, host):
704717 object_parent_lookup = self .regions_parent_lookup ,
705718 )
706719
720+ def extract_location (self , host ):
721+ # A host may have a location. A location may have a parent location.
722+ # Produce a list of locations:
723+ # - it will be empty if the device has no location
724+ # - it will have 1 element if the device's location has no parent
725+ # - it will have multiple elements if the location has a parent location
726+
727+ try :
728+ location_id = host ["location" ]["id" ]
729+ except (KeyError , TypeError ):
730+ # Device has no location
731+ return []
732+
733+ return self ._objects_array_following_parents (
734+ initial_object_id = location_id ,
735+ object_lookup = self .locations_lookup ,
736+ object_parent_lookup = self .locations_parent_lookup ,
737+ )
738+
707739 def extract_cluster (self , host ):
708740 try :
709741 # cluster does not have a slug
@@ -787,6 +819,35 @@ def get_region_parent(region):
787819 filter (lambda x : x is not None , map (get_region_parent , regions ))
788820 )
789821
822+ def refresh_locations_lookup (self ):
823+ # Locations were added in v2.11. Return empty lookups for previous versions.
824+ if self .api_version < version .parse ("2.11" ):
825+ return
826+
827+ url = self .api_endpoint + "/api/dcim/locations/?limit=0"
828+ locations = self .get_resource_list (api_url = url )
829+ self .locations_lookup = dict (
830+ (location ["id" ], location ["slug" ]) for location in locations
831+ )
832+
833+ def get_location_parent (location ):
834+ # Will fail if location does not have a parent location
835+ try :
836+ return (location ["id" ], location ["parent" ]["id" ])
837+ except Exception :
838+ return (location ["id" ], None )
839+
840+ def get_location_site (location ):
841+ # Locations MUST be assigned to a site
842+ return (location ["id" ], location ["site" ]["id" ])
843+
844+ # Dictionary of location id to parent location id
845+ self .locations_parent_lookup = dict (
846+ filter (None , map (get_location_parent , locations ))
847+ )
848+ # Location to site lookup
849+ self .locations_site_lookup = dict (map (get_location_site , locations ))
850+
790851 def refresh_tenants_lookup (self ):
791852 url = self .api_endpoint + "/api/tenancy/tenants/?limit=0"
792853 tenants = self .get_resource_list (api_url = url )
@@ -813,16 +874,11 @@ def get_role_for_rack(rack):
813874 self .racks_role_lookup = dict (map (get_role_for_rack , racks ))
814875
815876 def refresh_rack_groups_lookup (self ):
877+ # Locations were added in v2.11 replacing rack groups. Do nothing for 2.11+
816878 if self .api_version >= version .parse ("2.11" ):
817- # In NetBox v2.11 Breaking Changes:
818- # The RackGroup model has been renamed to Location
819- # (see netbox-community/netbox#4971).
820- # Its REST API endpoint has changed from /api/dcim/rack-groups/
821- # to /api/dcim/locations/
822- # https://netbox.readthedocs.io/en/stable/release-notes/#v2110-2021-04-16
823- url = self .api_endpoint + "/api/dcim/locations/?limit=0"
824- else :
825- url = self .api_endpoint + "/api/dcim/rack-groups/?limit=0"
879+ return
880+
881+ url = self .api_endpoint + "/api/dcim/rack-groups/?limit=0"
826882 rack_groups = self .get_resource_list (api_url = url )
827883 self .rack_groups_lookup = dict (
828884 (rack_group ["id" ], rack_group ["slug" ]) for rack_group in rack_groups
@@ -1054,6 +1110,7 @@ def lookup_processes(self):
10541110 lookups = [
10551111 self .refresh_sites_lookup ,
10561112 self .refresh_regions_lookup ,
1113+ self .refresh_locations_lookup ,
10571114 self .refresh_tenants_lookup ,
10581115 self .refresh_racks_lookup ,
10591116 self .refresh_rack_groups_lookup ,
@@ -1288,26 +1345,18 @@ def generate_group_name(self, grouping, group):
12881345 return "_" .join ([grouping , group ])
12891346
12901347 def add_host_to_groups (self , host , hostname ):
1291-
1292- # If we're grouping by regions, hosts are not added to region groups
1293- # - the site groups are added as sub-groups of regions
1294- # So, we need to make sure we're also grouping by sites if regions are enabled
1295-
1296- if "region" in self .group_by :
1297- # Make sure "site" or "sites" grouping also exists, depending on plurals options
1298- site_group_by = self ._pluralize_group_by ("site" )
1299- if site_group_by not in self .group_by :
1300- self .group_by .append (site_group_by )
1348+ site_group_by = self ._pluralize_group_by ("site" )
13011349
13021350 for grouping in self .group_by :
13031351
1304- # Don't handle regions here - that will happen in main()
1305- if grouping == "region" :
1352+ # Don't handle regions here since no hosts are ever added to region groups
1353+ # Sites and locations are also specially handled in the main()
1354+ if grouping in ["region" , site_group_by , "location" ]:
13061355 continue
13071356
13081357 if grouping not in self .group_extractors :
13091358 raise AnsibleError (
1310- 'group_by option "%s" is not valid. (Maybe check the plurals option? It can determine what group_by options are valid) '
1359+ 'group_by option "%s" is not valid. Check group_by documentation or check the plurals option. It can determine what group_by options are valid. '
13111360 % grouping
13121361 )
13131362
@@ -1331,53 +1380,76 @@ def add_host_to_groups(self, host, hostname):
13311380 transformed_group_name = self .inventory .add_group (group = group_name )
13321381 self .inventory .add_host (group = transformed_group_name , host = hostname )
13331382
1334- def _add_region_groups (self ):
1383+ def _add_site_groups (self ):
1384+ # Map site id to transformed group names
1385+ self .site_group_names = dict ()
13351386
1336- # Mapping of region id to group name
1337- region_transformed_group_names = dict ()
1338-
1339- # Create groups for each region
1340- for region_id in self .regions_lookup :
1341- region_group_name = self .generate_group_name (
1342- "region" , self .regions_lookup [region_id ]
1387+ for site_id , site_name in self .sites_lookup .items ():
1388+ site_group_name = self .generate_group_name (
1389+ self ._pluralize_group_by ("site" ), site_name
13431390 )
1344- region_transformed_group_names [region_id ] = self .inventory .add_group (
1345- group = region_group_name
1391+ # Add the site group to get its transformed name
1392+ site_transformed_group_name = self .inventory .add_group (
1393+ group = site_group_name
13461394 )
1395+ self .site_group_names [site_id ] = site_transformed_group_name
13471396
1348- # Now that all region groups exist, add relationships between them
1349- for region_id in self .regions_lookup :
1350- region_group_name = region_transformed_group_names [region_id ]
1351- parent_region_id = self .regions_parent_lookup .get (region_id , None )
1352- if (
1353- parent_region_id is not None
1354- and parent_region_id in region_transformed_group_names
1355- ):
1356- parent_region_name = region_transformed_group_names [parent_region_id ]
1357- self .inventory .add_child (parent_region_name , region_group_name )
1397+ def _add_region_groups (self ):
1398+ # Mapping of region id to group name
1399+ region_transformed_group_names = self ._setup_nested_groups (
1400+ "region" , self .regions_lookup , self .regions_parent_lookup
1401+ )
13581402
13591403 # Add site groups as children of region groups
13601404 for site_id in self .sites_lookup :
13611405 region_id = self .sites_region_lookup .get (site_id , None )
13621406 if region_id is None :
13631407 continue
13641408
1365- region_transformed_group_name = region_transformed_group_names [region_id ]
1366-
1367- site_name = self .sites_lookup [site_id ]
1368- site_group_name = self .generate_group_name (
1369- self ._pluralize_group_by ("site" ), site_name
1370- )
1371- # Add the site group to get its transformed name
1372- # Will already be created by add_host_to_groups - it's ok to call add_group again just to get its name
1373- site_transformed_group_name = self .inventory .add_group (
1374- group = site_group_name
1409+ self .inventory .add_child (
1410+ region_transformed_group_names [region_id ],
1411+ self .site_group_names [site_id ],
13751412 )
13761413
1414+ def _add_location_groups (self ):
1415+ # Mapping of location id to group name
1416+ self .location_group_names = self ._setup_nested_groups (
1417+ "location" , self .locations_lookup , self .locations_parent_lookup
1418+ )
1419+
1420+ # Add location to site groups as children
1421+ for location_id , location_slug in self .locations_lookup .items ():
1422+ if self .locations_parent_lookup .get (location_id , None ):
1423+ # Only top level locations should be children of sites
1424+ continue
1425+
1426+ site_transformed_group_name = self .site_group_names [
1427+ self .locations_site_lookup [location_id ]
1428+ ]
1429+
13771430 self .inventory .add_child (
1378- region_transformed_group_name , site_transformed_group_name
1431+ site_transformed_group_name , self . location_group_names [ location_id ]
13791432 )
13801433
1434+ def _setup_nested_groups (self , group , lookup , parent_lookup ):
1435+ # Mapping of id to group name
1436+ transformed_group_names = dict ()
1437+
1438+ # Create groups for each object
1439+ for obj_id in lookup :
1440+ group_name = self .generate_group_name (group , lookup [obj_id ])
1441+ transformed_group_names [obj_id ] = self .inventory .add_group (group = group_name )
1442+
1443+ # Now that all groups exist, add relationships between them
1444+ for obj_id in lookup :
1445+ group_name = transformed_group_names [obj_id ]
1446+ parent_id = parent_lookup .get (obj_id , None )
1447+ if parent_id is not None and parent_id in transformed_group_names :
1448+ parent_name = transformed_group_names [parent_id ]
1449+ self .inventory .add_child (parent_name , group_name )
1450+
1451+ return transformed_group_names
1452+
13811453 def _fill_host_variables (self , host , hostname ):
13821454 extracted_primary_ip = self .extract_primary_ip (host = host )
13831455 if extracted_primary_ip :
@@ -1413,6 +1485,9 @@ def _fill_host_variables(self, host, hostname):
14131485 if attribute == "region" :
14141486 attribute = "regions"
14151487
1488+ if attribute == "location" :
1489+ attribute = "locations"
1490+
14161491 if attribute == "rack_group" :
14171492 attribute = "rack_groups"
14181493
@@ -1456,6 +1531,27 @@ def main(self):
14561531 # - can skip any device/vm without any IPs
14571532 self .refresh_lookups (self .lookup_processes_secondary )
14581533
1534+ # If we're grouping by regions, hosts are not added to region groups
1535+ # If we're grouping by locations, hosts may be added to the site or location
1536+ # - the site groups are added as sub-groups of regions
1537+ # - the location groups are added as sub-groups of sites
1538+ # So, we need to make sure we're also grouping by sites if regions or locations are enabled
1539+ site_group_by = self ._pluralize_group_by ("site" )
1540+ if (
1541+ site_group_by in self .group_by
1542+ or "location" in self .group_by
1543+ or "region" in self .group_by
1544+ ):
1545+ self ._add_site_groups ()
1546+
1547+ # Create groups for locations. Will be a part of site groups.
1548+ if "location" in self .group_by and self .api_version >= version .parse ("2.11" ):
1549+ self ._add_location_groups ()
1550+
1551+ # Create groups for regions, containing the site groups
1552+ if "region" in self .group_by :
1553+ self ._add_region_groups ()
1554+
14591555 for host in chain (self .devices_list , self .vms_list ):
14601556
14611557 virtual_chassis_master = self ._get_host_virtual_chassis_master (host )
@@ -1488,9 +1584,18 @@ def main(self):
14881584 )
14891585 self .add_host_to_groups (host = host , hostname = hostname )
14901586
1491- # Create groups for regions, containing the site groups
1492- if "region" in self .group_by :
1493- self ._add_region_groups ()
1587+ # Special processing for sites and locations as those groups were already created
1588+ if getattr (self , "location_group_names" , None ) and host .get ("location" ):
1589+ # Add host to location group when host is assigned to the location
1590+ self .inventory .add_host (
1591+ group = self .location_group_names [host ["location" ]["id" ]],
1592+ host = hostname ,
1593+ )
1594+ elif getattr (self , "site_group_names" , None ) and host .get ("site" ):
1595+ # Add host to site group when host is NOT assigned to a location
1596+ self .inventory .add_host (
1597+ group = self .site_group_names [host ["site" ]["id" ]], host = hostname ,
1598+ )
14941599
14951600 def parse (self , inventory , loader , path , cache = True ):
14961601 super (InventoryModule , self ).parse (inventory , loader , path )
0 commit comments