136136 ans1-10.10.2.0-24
137137"""
138138
139+ import copy
140+ from ipaddress import IPv4Address
139141from ansible .module_utils .basic import AnsibleModule
140142from ansible_collections .community .proxmox .plugins .module_utils .proxmox_sdn import ProxmoxSdnAnsible
141143from ansible_collections .community .proxmox .plugins .module_utils .proxmox import (
142144 proxmox_auth_argument_spec ,
143- ansible_to_proxmox_bool
145+ ansible_to_proxmox_bool ,
146+ compare_list_of_dicts
147+
144148)
145149
146150
@@ -152,6 +156,7 @@ def get_proxmox_args():
152156 vnet = dict (type = "str" , required = True ),
153157 zone = dict (type = "str" , required = False ),
154158 dhcp_dns_server = dict (type = "str" , required = False ),
159+ dhcp_range_update_mode = dict (type = 'str' , choices = ['append' , 'overwrite' ], default = 'append' ),
155160 dhcp_range = dict (
156161 type = 'list' ,
157162 elements = 'dict' ,
@@ -178,10 +183,41 @@ def get_ansible_module():
178183 required_if = [
179184 ('state' , 'present' , ['subnet' , 'vnet' , 'zone' ]),
180185 ('state' , 'absent' , ['zone' , 'vnet' , 'subnet' ]),
186+ # ('dhcp_range_update_mode', 'overwrite', ['dhcp_range'])
181187 ]
182188 )
183189
184190
191+ def get_dhcp_range (dhcp_range = None ):
192+ if not dhcp_range :
193+ return None
194+ def extract (item ):
195+ start = item .get ('start-address' ) or item .get ('start' )
196+ end = item .get ('end-address' ) or item .get ('end' )
197+ return f"start-address={ start } ,end-address={ end } "
198+ return [extract (x ) for x in dhcp_range ]
199+
200+
201+ def compare_dhcp_ranges (existing_ranges , new_ranges ):
202+ def to_tuple (r ):
203+ return int (IPv4Address (r ['start-address' ])), int (IPv4Address (r ['end-address' ]))
204+
205+ existing_intervals = [to_tuple (r ) for r in existing_ranges ]
206+
207+ new_dhcp_ranges = []
208+ partial_overlap = False
209+
210+ for dhcp_range in new_ranges :
211+ tuple_dhcp_range = to_tuple (dhcp_range )
212+ if tuple_dhcp_range not in existing_intervals :
213+ new_dhcp_ranges .append (dhcp_range )
214+ for (start , end ) in existing_intervals :
215+ if not (tuple_dhcp_range [1 ] < start or tuple_dhcp_range [0 ] > end ):
216+ if tuple_dhcp_range != (start , end ):
217+ partial_overlap = True
218+ return new_dhcp_ranges , partial_overlap
219+
220+
185221class ProxmoxSubnetAnsible (ProxmoxSdnAnsible ):
186222 def __init__ (self , module ):
187223 super (ProxmoxSubnetAnsible , self ).__init__ (module )
@@ -196,7 +232,7 @@ def run(self):
196232 'type' : 'subnet' ,
197233 'vnet' : self .params .get ('vnet' ),
198234 'dhcp-dns-server' : self .params .get ('dhcp_dns_server' ),
199- 'dhcp-range' : self .get_dhcp_range ( ),
235+ 'dhcp-range' : get_dhcp_range ( dhcp_range = self .params . get ( 'dhcp_range' ) ),
200236 'dnszoneprefix' : self .params .get ('dnszoneprefix' ),
201237 'gateway' : self .params .get ('gateway' ),
202238 'lock-token' : self .params .get ('lock_token' ) or self .get_global_sdn_lock (),
@@ -208,27 +244,92 @@ def run(self):
208244 elif state == 'absent' :
209245 self .subnet_absent (** subnet_params )
210246
211- def get_dhcp_range (self ):
212- if self . params . get ( 'dhcp_range' ) is None :
213- return None
214- dhcp_range = [ f"start-address= { x [ 'start' ] } ,end-address= { x [ 'end' ] } " for x in self . params . get ( 'dhcp_range' )]
215- return dhcp_range
247+ def get_subnets (self , vnet_name ):
248+ try :
249+ return self . proxmox_api . cluster (). sdn (). vnets ( vnet_name ). subnets (). get ()
250+ except Exception as e :
251+ self . module . fail_json ( f'Failed to retrieve subnet isubnet_paramsnfo { e } ' )
216252
217- def subnet_present (self , update , ** subnet_params ):
218- vnet_name = subnet_params ['vnet' ]
253+ def update_subnet (self , ** subnet_params ):
254+ new_subnet = copy .deepcopy (subnet_params )
255+ subnet_id = f"{ self .params ['zone' ]} -{ new_subnet ['subnet' ].replace ('/' , '-' )} "
219256 lock = subnet_params ['lock-token' ]
220- subnet_cidr = subnet_params [ 'subnet ' ]
221- subnet_id = f" { self .params [ 'zone' ] } - { subnet_params [ 'subnet' ]. replace ( '/' , '-' ) } "
257+ vnet_name = new_subnet [ 'vnet ' ]
258+ dhcp_range_update_mode = self .params . get ( 'dhcp_range_update_mode' )
222259
223- try :
224- vnet = getattr (self .proxmox_api .cluster ().sdn ().vnets (), vnet_name )
260+ new_subnet ['cidr' ] = new_subnet ['subnet' ]
261+ new_subnet ['network' ] = new_subnet ['subnet' ].split ('/' )[0 ]
262+ new_subnet ['mask' ] = new_subnet ['subnet' ].split ('/' )[1 ]
263+ new_subnet ['zone' ] = self .params .get ('zone' )
264+ new_subnet ['id' ] = subnet_id
265+ new_subnet ['subnet' ] = subnet_id
225266
226- # Check if subnet already present
227- if subnet_id in [x ['subnet' ] for x in vnet ().subnets ().get ()]:
228- if update :
229- subnet = getattr (vnet ().subnets (), subnet_id )
267+ subnet_params ['delete' ] = self .params .get ('delete' )
268+
269+ existing_subnets = self .get_subnets (vnet_name )
270+
271+ # Check for subnet params other than dhcp-range
272+ _ , subnet_update = compare_list_of_dicts (
273+ existing_list = existing_subnets ,
274+ new_list = [new_subnet ],
275+ uid = 'id' ,
276+ params_to_ignore = ['digest' , 'dhcp-range' , 'lock-token' ]
277+ )
278+
279+ existing_subnet = [x for x in existing_subnets if x ['subnet' ] == subnet_id ][0 ]
280+
281+ # Check dhcp-range
282+ update_dhcp = False
283+ if self .params .get ('dhcp_range' ):
284+ new_dhcp_range = [
285+ {'start-address' : d .get ('start' ), 'end-address' : d .get ('end' )}
286+ for d in self .params .get ('dhcp_range' )
287+ ]
288+ new_dhcp , partial_overlap = compare_dhcp_ranges (
289+ existing_ranges = existing_subnet ['dhcp-range' ],
290+ new_ranges = new_dhcp_range
291+ )
292+
293+ if dhcp_range_update_mode == 'append' :
294+ if partial_overlap :
295+ self .module .fail_json (
296+ msg = f"There are overlapping DHCP ranges. this is not allowed. "
297+ f"Existing range - { existing_subnet ['dhcp-range' ]} "
298+ f"New Range - { new_dhcp_range } "
299+ )
300+
301+ if len (new_dhcp ) > 0 :
302+ update_dhcp = True
303+ new_dhcp .extend (existing_subnet ['dhcp-range' ]) # By Default API overwrites DHCP Range
304+ subnet_params ['dhcp-range' ] = get_dhcp_range (new_dhcp )
305+
306+ elif dhcp_range_update_mode == 'overwrite' and new_dhcp :
307+ update_dhcp = True
308+
309+ elif not self .params .get ('dhcp_range' ) and existing_subnet ['dhcp-range' ]:
310+ if dhcp_range_update_mode == 'append' :
311+ self .module .warn (
312+ "dhcp_range_update_mode is set to append, but you didn't provide any DHCP ranges for the subnet. "
313+ "Existing ranges will be ignored."
314+ )
315+
316+ elif dhcp_range_update_mode == 'overwrite' :
317+ update_dhcp = True
318+ self .module .warn (
319+ "dhcp_range_update_mode is set to overwrite, but no DHCP ranges were provided for the subnet. "
320+ "All existing DHCP ranges will be deleted."
321+ )
322+ if self .params .get ('delete' ):
323+ subnet_params ['delete' ] = f"{ subnet_params ['delete' ]} ,dhcp-range"
324+ else :
325+ subnet_params ['delete' ] = "dhcp-range"
326+
327+ if subnet_update or update_dhcp :
328+ self .module .warn (f"{ subnet_params } , { update_dhcp } " )
329+ if self .params .get ('update' ):
330+ try :
331+ subnet = getattr (self .proxmox_api .cluster ().sdn ().vnets (vnet_name ).subnets (), subnet_id )
230332 subnet_params ['digest' ] = subnet .get ()['digest' ]
231- subnet_params ['delete' ] = self .params .get ('delete' )
232333 del subnet_params ['type' ]
233334 del subnet_params ['subnet' ]
234335
@@ -237,11 +338,37 @@ def subnet_present(self, update, **subnet_params):
237338 self .module .exit_json (
238339 changed = True , subnet = subnet_id , msg = f'Updated subnet { subnet_id } '
239340 )
240- else :
241- self .release_lock (lock = lock )
242- self .module .exit_json (
243- changed = False , subnet = subnet_id , msg = f'subnet { subnet_id } already present and update is false. '
341+ except Exception as e :
342+ self .rollback_sdn_changes_and_release_lock (lock = lock )
343+ self .module .fail_json (
344+ msg = f'Failed to update subnet. Rolling back all changes : { e } '
244345 )
346+ else :
347+ self .release_lock (lock = lock )
348+ self .module .fail_json (
349+ msg = f"Subnet { subnet_id } needs to be updated but update is false."
350+ )
351+ else :
352+ self .release_lock (lock = lock )
353+ self .module .exit_json (
354+ changed = False ,
355+ subnet = subnet_id ,
356+ msg = f'subnet { subnet_id } is already present with correct parameters.'
357+ )
358+
359+ def subnet_present (self , update , ** subnet_params ):
360+ vnet_name = subnet_params ['vnet' ]
361+ lock = subnet_params ['lock-token' ]
362+ subnet_cidr = subnet_params ['subnet' ]
363+ subnet_id = f"{ self .params ['zone' ]} -{ subnet_params ['subnet' ].replace ('/' , '-' )} "
364+
365+ try :
366+ vnet = getattr (self .proxmox_api .cluster ().sdn ().vnets (), vnet_name )
367+ existing_subnets = self .get_subnets (vnet_name )
368+
369+ # Check if subnet already present
370+ if subnet_id in [x ['subnet' ] for x in existing_subnets ]:
371+ self .update_subnet (** subnet_params )
245372 else :
246373 vnet .subnets ().post (** subnet_params )
247374 self .apply_sdn_changes_and_release_lock (lock = lock )
0 commit comments