109
109
type: boolean
110
110
version_added: "0.2.1"
111
111
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
113
116
type: list
114
117
choices:
115
118
- sites
116
119
- site
120
+ - location
117
121
- tenants
118
122
- tenant
119
123
- racks
@@ -412,7 +416,6 @@ def group_extractors(self):
412
416
self ._pluralize_group_by ("site" ): self .extract_site ,
413
417
self ._pluralize_group_by ("tenant" ): self .extract_tenant ,
414
418
self ._pluralize_group_by ("rack" ): self .extract_rack ,
415
- "rack_group" : self .extract_rack_group ,
416
419
"rack_role" : self .extract_rack_role ,
417
420
self ._pluralize_group_by ("tag" ): self .extract_tags ,
418
421
self ._pluralize_group_by ("role" ): self .extract_device_role ,
@@ -421,6 +424,16 @@ def group_extractors(self):
421
424
self ._pluralize_group_by ("manufacturer" ): self .extract_manufacturer ,
422
425
}
423
426
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
+
424
437
if self .services :
425
438
extractors .update (
426
439
{"services" : self .extract_services ,}
@@ -704,6 +717,25 @@ def extract_regions(self, host):
704
717
object_parent_lookup = self .regions_parent_lookup ,
705
718
)
706
719
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
+
707
739
def extract_cluster (self , host ):
708
740
try :
709
741
# cluster does not have a slug
@@ -787,6 +819,35 @@ def get_region_parent(region):
787
819
filter (lambda x : x is not None , map (get_region_parent , regions ))
788
820
)
789
821
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
+
790
851
def refresh_tenants_lookup (self ):
791
852
url = self .api_endpoint + "/api/tenancy/tenants/?limit=0"
792
853
tenants = self .get_resource_list (api_url = url )
@@ -813,16 +874,11 @@ def get_role_for_rack(rack):
813
874
self .racks_role_lookup = dict (map (get_role_for_rack , racks ))
814
875
815
876
def refresh_rack_groups_lookup (self ):
877
+ # Locations were added in v2.11 replacing rack groups. Do nothing for 2.11+
816
878
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"
826
882
rack_groups = self .get_resource_list (api_url = url )
827
883
self .rack_groups_lookup = dict (
828
884
(rack_group ["id" ], rack_group ["slug" ]) for rack_group in rack_groups
@@ -1054,6 +1110,7 @@ def lookup_processes(self):
1054
1110
lookups = [
1055
1111
self .refresh_sites_lookup ,
1056
1112
self .refresh_regions_lookup ,
1113
+ self .refresh_locations_lookup ,
1057
1114
self .refresh_tenants_lookup ,
1058
1115
self .refresh_racks_lookup ,
1059
1116
self .refresh_rack_groups_lookup ,
@@ -1288,26 +1345,18 @@ def generate_group_name(self, grouping, group):
1288
1345
return "_" .join ([grouping , group ])
1289
1346
1290
1347
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" )
1301
1349
1302
1350
for grouping in self .group_by :
1303
1351
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" ]:
1306
1355
continue
1307
1356
1308
1357
if grouping not in self .group_extractors :
1309
1358
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. '
1311
1360
% grouping
1312
1361
)
1313
1362
@@ -1331,53 +1380,76 @@ def add_host_to_groups(self, host, hostname):
1331
1380
transformed_group_name = self .inventory .add_group (group = group_name )
1332
1381
self .inventory .add_host (group = transformed_group_name , host = hostname )
1333
1382
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 ()
1335
1386
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
1343
1390
)
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
1346
1394
)
1395
+ self .site_group_names [site_id ] = site_transformed_group_name
1347
1396
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
+ )
1358
1402
1359
1403
# Add site groups as children of region groups
1360
1404
for site_id in self .sites_lookup :
1361
1405
region_id = self .sites_region_lookup .get (site_id , None )
1362
1406
if region_id is None :
1363
1407
continue
1364
1408
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 ],
1375
1412
)
1376
1413
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
+
1377
1430
self .inventory .add_child (
1378
- region_transformed_group_name , site_transformed_group_name
1431
+ site_transformed_group_name , self . location_group_names [ location_id ]
1379
1432
)
1380
1433
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
+
1381
1453
def _fill_host_variables (self , host , hostname ):
1382
1454
extracted_primary_ip = self .extract_primary_ip (host = host )
1383
1455
if extracted_primary_ip :
@@ -1413,6 +1485,9 @@ def _fill_host_variables(self, host, hostname):
1413
1485
if attribute == "region" :
1414
1486
attribute = "regions"
1415
1487
1488
+ if attribute == "location" :
1489
+ attribute = "locations"
1490
+
1416
1491
if attribute == "rack_group" :
1417
1492
attribute = "rack_groups"
1418
1493
@@ -1456,6 +1531,27 @@ def main(self):
1456
1531
# - can skip any device/vm without any IPs
1457
1532
self .refresh_lookups (self .lookup_processes_secondary )
1458
1533
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
+
1459
1555
for host in chain (self .devices_list , self .vms_list ):
1460
1556
1461
1557
virtual_chassis_master = self ._get_host_virtual_chassis_master (host )
@@ -1488,9 +1584,18 @@ def main(self):
1488
1584
)
1489
1585
self .add_host_to_groups (host = host , hostname = hostname )
1490
1586
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
+ )
1494
1599
1495
1600
def parse (self , inventory , loader , path , cache = True ):
1496
1601
super (InventoryModule , self ).parse (inventory , loader , path )
0 commit comments