88
88
- manufacturer
89
89
- platforms
90
90
- platform
91
+ - region
91
92
default: []
92
93
group_names_raw:
93
94
description: Will not add the group_by choice name to the group names
@@ -493,6 +494,7 @@ def group_extractors(self):
493
494
"manufacturers" : self .extract_manufacturer ,
494
495
"interfaces" : self .extract_interfaces ,
495
496
"custom_fields" : self .extract_custom_fields ,
497
+ "region" : self .extract_regions ,
496
498
}
497
499
else :
498
500
return {
@@ -511,6 +513,7 @@ def group_extractors(self):
511
513
"manufacturer" : self .extract_manufacturer ,
512
514
"interfaces" : self .extract_interfaces ,
513
515
"custom_fields" : self .extract_custom_fields ,
516
+ "region" : self .extract_regions ,
514
517
}
515
518
516
519
def _pluralize (self , something ):
@@ -681,6 +684,39 @@ def extract_custom_fields(self, host):
681
684
except Exception :
682
685
return
683
686
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
+
684
720
def refresh_platforms_lookup (self ):
685
721
url = self .api_endpoint + "/api/dcim/platforms/?limit=0"
686
722
platforms = self .get_resource_list (api_url = url )
@@ -693,10 +729,25 @@ def refresh_sites_lookup(self):
693
729
sites = self .get_resource_list (api_url = url )
694
730
self .sites_lookup = dict ((site ["id" ], site ["slug" ]) for site in sites )
695
731
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
+
696
744
def refresh_regions_lookup (self ):
697
745
url = self .api_endpoint + "/api/dcim/regions/?limit=0"
698
746
regions = self .get_resource_list (api_url = url )
699
747
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
+ )
700
751
701
752
def refresh_tenants_lookup (self ):
702
753
url = self .api_endpoint + "/api/tenancy/tenants/?limit=0"
@@ -863,8 +914,31 @@ def extract_name(self, host):
863
914
# We default to an UUID for hostname in case the name is not set in NetBox
864
915
return host ["name" ] or str (uuid .uuid4 ())
865
916
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
+
866
923
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
+
867
936
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
+
868
942
groups_for_host = self .group_extractors [grouping ](host )
869
943
870
944
if not groups_for_host :
@@ -875,16 +949,60 @@ def add_host_to_groups(self, host, hostname):
875
949
groups_for_host = [groups_for_host ]
876
950
877
951
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 )
882
953
883
954
# Group names may be transformed by the ansible TRANSFORM_INVALID_GROUP_CHARS setting
884
955
# add_group returns the actual group name used
885
956
transformed_group_name = self .inventory .add_group (group = group_name )
886
957
self .inventory .add_host (group = transformed_group_name , host = hostname )
887
958
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
+
888
1006
def _fill_host_variables (self , host , hostname ):
889
1007
for attribute , extractor in self .group_extractors .items ():
890
1008
extracted_value = extractor (host )
@@ -896,6 +1014,9 @@ def _fill_host_variables(self, host, hostname):
896
1014
if attribute == "tag" :
897
1015
attribute = "tags"
898
1016
1017
+ if attribute == "region" :
1018
+ attribute = "regions"
1019
+
899
1020
self .inventory .set_variable (hostname , attribute , extracted_value )
900
1021
901
1022
extracted_primary_ip = self .extract_primary_ip (host = host )
@@ -937,6 +1058,10 @@ def main(self):
937
1058
)
938
1059
self .add_host_to_groups (host = host , hostname = hostname )
939
1060
1061
+ # Create groups for regions, containing the site groups
1062
+ if "region" in self .group_by :
1063
+ self ._add_region_groups ()
1064
+
940
1065
def parse (self , inventory , loader , path , cache = True ):
941
1066
super (InventoryModule , self ).parse (inventory , loader , path )
942
1067
self ._read_config_data (path = path )
0 commit comments