Skip to content

Commit 9adc390

Browse files
committed
proxmox_subnet: Update only if needed
1 parent 7909902 commit 9adc390

File tree

1 file changed

+149
-22
lines changed

1 file changed

+149
-22
lines changed

plugins/modules/proxmox_subnet.py

Lines changed: 149 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,15 @@
136136
ans1-10.10.2.0-24
137137
"""
138138

139+
import copy
140+
from ipaddress import IPv4Address
139141
from ansible.module_utils.basic import AnsibleModule
140142
from ansible_collections.community.proxmox.plugins.module_utils.proxmox_sdn import ProxmoxSdnAnsible
141143
from 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+
185221
class 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

Comments
 (0)