69
69
"sites" ,
70
70
"virtual_chassis" ,
71
71
],
72
- extras = [],
72
+ extras = ["tags" ],
73
73
ipam = [
74
74
"aggregates" ,
75
75
"ip_addresses" ,
133
133
134
134
# Specifies keys within data that need to be converted to ID and the endpoint to be used when queried
135
135
CONVERT_TO_ID = {
136
+ "assigned_object" : "assigned_object" ,
136
137
"circuit" : "circuits" ,
137
138
"circuit_type" : "circuit_types" ,
138
139
"circuit_termination" : "circuit_terminations" ,
180
181
"rir" : "rirs" ,
181
182
"services" : "services" ,
182
183
"site" : "sites" ,
184
+ "tags" : "tags" ,
183
185
"tagged_vlans" : "vlans" ,
184
186
"tenant" : "tenants" ,
185
187
"tenant_group" : "tenant_groups" ,
250
252
251
253
ALLOWED_QUERY_PARAMS = {
252
254
"aggregate" : set (["prefix" , "rir" ]),
255
+ "assigned_object" : set (["name" , "device" , "virtual_machine" ]),
253
256
"circuit" : set (["cid" ]),
254
257
"circuit_type" : set (["slug" ]),
255
258
"circuit_termination" : set (["circuit" , "term_side" ]),
309
312
"role" : set (["slug" ]),
310
313
"services" : set (["device" , "virtual_machine" , "name" , "port" , "protocol" ]),
311
314
"site" : set (["slug" ]),
315
+ "tags" : set (["slug" ]),
312
316
"tagged_vlans" : set (["name" , "site" , "vid" , "vlan_group" , "tenant" ]),
313
317
"tenant" : set (["slug" ]),
314
318
"tenant_group" : set (["slug" ]),
369
373
370
374
# This is used to map non-clashing keys to Netbox API compliant keys to prevent bad logic in code for similar keys but different modules
371
375
CONVERT_KEYS = {
376
+ "assigned_object" : "assigned_object_id" ,
372
377
"circuit_type" : "type" ,
373
378
"cluster_type" : "type" ,
374
379
"cluster_group" : "group" ,
400
405
"rirs" ,
401
406
"roles" ,
402
407
"sites" ,
408
+ "tags" ,
403
409
"tenants" ,
404
410
"tenant_groups" ,
405
411
"manufacturers" ,
@@ -557,6 +563,9 @@ def _convert_identical_keys(self, data):
557
563
if self .endpoint == "power_panels" and key == "rack_group" :
558
564
temp_dict [key ] = data [key ]
559
565
elif key in CONVERT_KEYS :
566
+ # This will keep the original key for assigned_object, but also convert to assigned_object_id
567
+ if key == "assigned_object" :
568
+ temp_dict [key ] = data [key ]
560
569
new_key = CONVERT_KEYS [key ]
561
570
temp_dict [new_key ] = data [key ]
562
571
else :
@@ -570,7 +579,10 @@ def _remove_arg_spec_default(self, data):
570
579
"""
571
580
new_dict = dict ()
572
581
for k , v in data .items ():
573
- if v is not None :
582
+ if isinstance (v , dict ):
583
+ v = self ._remove_arg_spec_default (v )
584
+ new_dict [k ] = v
585
+ elif v is not None :
574
586
new_dict [k ] = v
575
587
576
588
return new_dict
@@ -649,7 +661,18 @@ def _build_query_params(
649
661
elif parent == "prefix" and module_data .get ("parent" ):
650
662
query_dict .update ({"prefix" : module_data ["parent" ]})
651
663
652
- elif parent == "ip_addreses" :
664
+ # This is for netbox_ipam and netbox_ip_address module
665
+ elif parent == "ip_address" and module_data .get ("assigned_object_type" ):
666
+ if module_data ["assigned_object_type" ] == "virtualization.vminterface" :
667
+ query_dict .update (
668
+ {"vminterface_id" : module_data .get ("assigned_object_id" )}
669
+ )
670
+ elif module_data ["assigned_object_type" ] == "dcim.interface" :
671
+ query_dict .update (
672
+ {"interface_id" : module_data .get ("assigned_object_id" )}
673
+ )
674
+
675
+ elif parent == "ip_addresses" :
653
676
if isinstance (module_data ["device" ], int ):
654
677
query_dict .update ({"device_id" : module_data ["device" ]})
655
678
else :
@@ -739,10 +762,14 @@ def _find_ids(self, data, user_query_params):
739
762
"""
740
763
for k , v in data .items ():
741
764
if k in CONVERT_TO_ID :
765
+ if self .version < 2.9 and k == "tags" :
766
+ continue
742
767
if k == "termination_a" :
743
768
endpoint = CONVERT_TO_ID [data .get ("termination_a_type" )]
744
769
elif k == "termination_b" :
745
770
endpoint = CONVERT_TO_ID [data .get ("termination_b_type" )]
771
+ elif k == "assigned_object" :
772
+ endpoint = "interfaces"
746
773
else :
747
774
endpoint = CONVERT_TO_ID [k ]
748
775
search = v
@@ -751,17 +778,23 @@ def _find_ids(self, data, user_query_params):
751
778
nb_endpoint = getattr (nb_app , endpoint )
752
779
753
780
if isinstance (v , dict ):
754
- if k == "interface" and v .get ("virtual_machine" ):
781
+ if (k == "interface" or k == "assigned_object" ) and v .get (
782
+ "virtual_machine"
783
+ ):
755
784
nb_app = getattr (self .nb , "virtualization" )
756
785
nb_endpoint = getattr (nb_app , endpoint )
757
786
query_params = self ._build_query_params (k , data , child = v )
758
787
query_id = self ._nb_endpoint_get (nb_endpoint , query_params , k )
759
-
760
788
elif isinstance (v , list ):
761
789
id_list = list ()
762
790
for list_item in v :
763
- norm_data = self ._normalize_data (list_item )
764
- temp_dict = self ._build_query_params (k , data , child = norm_data )
791
+ if k == "tags" and isinstance (list_item , str ):
792
+ temp_dict = {"slug" : self ._to_slug (list_item )}
793
+ elif isinstance (list_item , dict ):
794
+ norm_data = self ._normalize_data (list_item )
795
+ temp_dict = self ._build_query_params (
796
+ k , data , child = norm_data
797
+ )
765
798
query_id = self ._nb_endpoint_get (nb_endpoint , temp_dict , k )
766
799
if query_id :
767
800
id_list .append (query_id .id )
@@ -833,6 +866,14 @@ def _normalize_data(self, data):
833
866
if k == "mac_address" :
834
867
data [k ] = v .upper ()
835
868
869
+ # We need to assign the correct type for the assigned object so the user doesn't have to worry about this.
870
+ # We determine it by whether or not they pass in a device or virtual_machine
871
+ if data .get ("assigned_object" ):
872
+ if data ["assigned_object" ].get ("device" ):
873
+ data ["assigned_object_type" ] = "dcim.interface"
874
+ if data ["assigned_object" ].get ("virtual_machine" ):
875
+ data ["assigned_object_type" ] = "virtualization.vminterface"
876
+
836
877
return data
837
878
838
879
def _create_netbox_object (self , nb_endpoint , data ):
@@ -978,14 +1019,51 @@ def __init__(
978
1019
argument_spec ,
979
1020
bypass_checks = False ,
980
1021
no_log = False ,
981
- mutually_exclusive = None ,
982
- required_together = None ,
1022
+ mutually_exclusive = mutually_exclusive ,
1023
+ required_together = required_together ,
983
1024
required_one_of = required_one_of ,
984
1025
add_file_common_args = False ,
985
1026
supports_check_mode = supports_check_mode ,
986
1027
required_if = required_if ,
987
1028
)
988
1029
1030
+ def _check_mutually_exclusive (self , spec , param = None ):
1031
+ if param is None :
1032
+ param = self .params
1033
+
1034
+ try :
1035
+ self .check_mutually_exclusive (spec , param )
1036
+ except TypeError as e :
1037
+ msg = to_native (e )
1038
+ if self ._options_context :
1039
+ msg += " found in %s" % " -> " .join (self ._options_context )
1040
+ self .fail_json (msg = msg )
1041
+
1042
+ def check_mutually_exclusive (self , terms , module_parameters ):
1043
+ """Check mutually exclusive terms against argument parameters
1044
+ Accepts a single list or list of lists that are groups of terms that should be
1045
+ mutually exclusive with one another
1046
+ :arg terms: List of mutually exclusive module parameters
1047
+ :arg module_parameters: Dictionary of module parameters
1048
+ :returns: Empty list or raises TypeError if the check fails.
1049
+ """
1050
+
1051
+ results = []
1052
+ if terms is None :
1053
+ return results
1054
+
1055
+ for check in terms :
1056
+ count = self .count_terms (check , module_parameters ["data" ])
1057
+ if count > 1 :
1058
+ results .append (check )
1059
+
1060
+ if results :
1061
+ full_list = ["|" .join (check ) for check in results ]
1062
+ msg = "parameters are mutually exclusive: %s" % ", " .join (full_list )
1063
+ raise TypeError (to_native (msg ))
1064
+
1065
+ return results
1066
+
989
1067
def _check_required_if (self , spec , param = None ):
990
1068
""" ensure that parameters which conditionally required are present """
991
1069
if spec is None :
@@ -1089,6 +1167,50 @@ def check_required_one_of(self, terms, module_parameters):
1089
1167
1090
1168
return results
1091
1169
1170
+ def _check_required_together (self , spec , param = None ):
1171
+ if spec is None :
1172
+ return
1173
+ if param is None :
1174
+ param = self .params
1175
+
1176
+ try :
1177
+ self .check_required_together (spec , param )
1178
+ except TypeError as e :
1179
+ msg = to_native (e )
1180
+ if self ._options_context :
1181
+ msg += " found in %s" % " -> " .join (self ._options_context )
1182
+ self .fail_json (msg = msg )
1183
+
1184
+ def check_required_together (self , terms , module_parameters ):
1185
+ """Check each list of terms to ensure every parameter in each list exists
1186
+ in the given module parameters
1187
+ Accepts a list of lists or tuples
1188
+ :arg terms: List of lists of terms to check. Each list should include
1189
+ parameters that are all required when at least one is specified
1190
+ in the module_parameters.
1191
+ :arg module_parameters: Dictionary of module parameters
1192
+ :returns: Empty list or raises TypeError if the check fails.
1193
+ """
1194
+
1195
+ results = []
1196
+ if terms is None :
1197
+ return results
1198
+
1199
+ for term in terms :
1200
+ counts = [
1201
+ self .count_terms (field , module_parameters ["data" ]) for field in term
1202
+ ]
1203
+ non_zero = [c for c in counts if c > 0 ]
1204
+ if len (non_zero ) > 0 :
1205
+ if 0 in counts :
1206
+ results .append (term )
1207
+ if results :
1208
+ for term in results :
1209
+ msg = "parameters are required together: %s" % ", " .join (term )
1210
+ raise TypeError (to_native (msg ))
1211
+
1212
+ return results
1213
+
1092
1214
def count_terms (self , terms , module_parameters ):
1093
1215
"""Count the number of occurrences of a key in a given dictionary
1094
1216
:arg terms: String or iterable of values to check
0 commit comments