PyPANRestV2 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pypanrestv2/Network.py ADDED
@@ -0,0 +1,1722 @@
1
+ from typing import Optional, Dict, Any, Tuple, Union, List, Protocol, Set, TypeVar
2
+ from . import ApplicationHelper
3
+ from . import Exceptions
4
+ import pycountry
5
+ import ipaddress
6
+ import builtins
7
+ import time
8
+ import re
9
+ from datetime import datetime
10
+ from icecream import ic
11
+ import sys
12
+ import xmltodict
13
+ import dns.resolver
14
+ import requests
15
+ from requests.packages.urllib3.exceptions import InsecureRequestWarning
16
+ import logging
17
+ import xml.etree.ElementTree as ET
18
+ from pypanrestv2.Base import Base, PAN, Panorama, Firewall
19
+ import pypanrestv2.Objects
20
+ logger = logging.getLogger(__name__)
21
+ requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
22
+
23
+ class Network(Base, PAN):
24
+ allowed_name_pattern = re.compile(r"[0-9a-zA-Z._-]+", re.IGNORECASE)
25
+
26
+ def __init__(self, PANDevice: Panorama | Firewall, **kwargs):
27
+ self.PANDevice = PANDevice
28
+ self.PANDevice.valid_location.extend(['template', 'template-stack'])
29
+ Base.__init__(self, PANDevice, **kwargs)
30
+ PAN.__init__(self, PANDevice.base_url, api_key=PANDevice.api_key)
31
+ self.endpoint = 'Network'
32
+
33
+ def _build_params(self) -> Dict[str, str]:
34
+ """
35
+ Builds the parameter dictionary for the API request based on the object's state.
36
+
37
+ Returns:
38
+ Dict[str, str]: The parameters for the API request.
39
+ """
40
+ params = {'location': self.location} if self.location else {}
41
+ if self.name:
42
+ params['name'] = self.name
43
+ if self.location == 'template':
44
+ params.update({self.location: self.template})
45
+ if self.location == 'template-stack':
46
+ params.update({self.location: self.template_stack})
47
+ if self.location == 'vsys':
48
+ params.update({self.location: self.vsys})
49
+ if type(self).__name__ == 'Zones' and hasattr(self, 'vsys'):
50
+ params['vsys'] = self.vsys
51
+
52
+ return params
53
+
54
+ def _update_entry_with_address(self, address: str) -> None:
55
+ if 'ip' not in self.entry:
56
+ self.entry['ip'] = {'entry': []}
57
+ self.entry['ip']['entry'].append({'@name': address})
58
+
59
+ def _validate_and_append_address(self, address: str) -> None:
60
+ try:
61
+ ipaddress.IPv4Interface(address)
62
+ self._ip_address.append(address)
63
+ self._update_entry_with_address(address)
64
+ except AddressValueError:
65
+ if address.startswith('$'):
66
+ self._ip_address.append(address)
67
+ self._update_entry_with_address(address)
68
+ else:
69
+ if am_i_an_address_object.refresh():
70
+ self._ip_address.append(am_i_an_address_object.value)
71
+ self._update_entry_with_address(am_i_an_address_object.value)
72
+ else:
73
+ raise AddressValueError(f"{address} is not a valid IP address, variable, or address object.")
74
+
75
+ def set_interface_addresses(self, value: Union[str, List[str]]) -> None:
76
+ """
77
+ Set interface addresses with validation for IP addresses, variables, or address objects.
78
+ """
79
+ if isinstance(value, str):
80
+ self._validate_and_append_address(value)
81
+ elif isinstance(value, list):
82
+ for addr in value:
83
+ self._validate_and_append_address(addr)
84
+ else:
85
+ raise TypeError("Value must be a string or a list of strings.")
86
+
87
+ @staticmethod
88
+ def _validate_interface_address(ip: str) -> bool:
89
+ # If the IP address starts with '$', it's considered a valid Palo Alto variable
90
+ if ip.startswith('$'):
91
+ return True
92
+
93
+ # Validate the IP address
94
+ try:
95
+ ipaddress.ip_interface(ip)
96
+ return True
97
+ except ValueError:
98
+ try:
99
+ ipaddress.ip_address(ip)
100
+ return True
101
+ except ValueError:
102
+ return False
103
+
104
+ @staticmethod
105
+ def _validate_ip_address(ip: str) -> bool:
106
+ # If the IP address starts with '$', it's considered a valid Palo Alto variable
107
+ if ip.startswith('$'):
108
+ return True
109
+
110
+ # Validate the IP address
111
+ try:
112
+ ipaddress.ip_address(ip)
113
+ return True
114
+ except ValueError:
115
+ return False
116
+
117
+ class Zones(Network):
118
+ valid_network = ['tap', 'virtual-wire', 'layer2', 'layer3', 'tunnel', 'external']
119
+
120
+ def __init__(self, PANDevice: Panorama | Firewall, **kwargs):
121
+ super().__init__(PANDevice, max_name_length=32, **kwargs)
122
+ self.enable_user_identification: str = kwargs.get('enable_user_identification', 'no')
123
+ self.enable_device_identification: str = kwargs.get('enable_device_identification', 'no')
124
+ self.network: dict = kwargs.get('network', {})
125
+ self.user_acl: dict = kwargs.get('user_acl')
126
+ self.device_acl: dict = kwargs.get('device_acl')
127
+
128
+ @property
129
+ def network(self):
130
+ return self._network
131
+
132
+ @network.setter
133
+ def network(self, value: dict):
134
+ if value:
135
+ if not isinstance(value, dict):
136
+ raise ValueError("Network must be a dictionary.")
137
+
138
+ # Define the default values for 'prenat-identification'
139
+ default_prenat_identification = {
140
+ 'enable-prenat-user-identification': 'no',
141
+ 'enable-prenat-device-identification': 'no',
142
+ 'enable-prenat-source-policy-lookup': 'no',
143
+ 'enable-prenat-source-ip-downstream': 'no',
144
+ }
145
+
146
+ # Add default 'prenat-identification' if not provided
147
+ if 'prenat-identification' not in value:
148
+ value['prenat-identification'] = default_prenat_identification
149
+
150
+ if 'zone-protection-profile' not in value:
151
+ value['zone-protection-profile'] = ''
152
+
153
+ if 'enable-packet-buffer-protection' not in value:
154
+ value['enable-packet-buffer-protection'] = 'yes'
155
+
156
+ if 'net-inspection' not in value:
157
+ value['net-inspection'] = 'no'
158
+
159
+ required_keys = ['zone-protection-profile', 'enable-packet-buffer-protection', 'log-setting',
160
+ 'net-inspection', 'prenat-identification']
161
+ if not all(key in value for key in required_keys):
162
+ raise KeyError(f"Network dictionary must contain the keys: {', '.join(required_keys)}")
163
+
164
+ if value.get('enable-packet-buffer-protection', 'yes') not in self.yes_no:
165
+ raise ValueError("enable-packet-buffer-protection must be 'yes' or 'no'")
166
+
167
+ if len(value.get('log-setting', '')) > 63:
168
+ raise ValueError("log-setting value must be 63 characters or fewer")
169
+
170
+ if value.get('net-inspection', 'no') not in self.yes_no:
171
+ raise ValueError("net-inspection must be 'yes' or 'no'")
172
+
173
+ if not isinstance(value.get('prenat-identification'), dict):
174
+ raise ValueError("prenat-identification must be a dictionary")
175
+
176
+ parent_id_required_keys = ['enable-prenat-user-identification', 'enable-prenat-device-identification',
177
+ 'enable-prenat-source-policy-lookup', 'enable-prenat-source-ip-downstream']
178
+
179
+ # if not all(key in value for key in parent_id_required_keys):
180
+ # raise KeyError(f"prenat-identification dictionary must contain the keys: {', '.join(parent_id_required_keys)}, provided keys: {', '.join(value.get('prenat-identification', {}).keys())}")
181
+
182
+ if value.get('prenat-identification', {}).get('enable-prenat-user-identification', 'no') not in self.yes_no:
183
+ raise ValueError("enable-prenat-user-identification must be 'yes' or 'no'")
184
+ if value.get('prenat-identification', {}).get('enable-prenat-device-identification', 'no') not in self.yes_no:
185
+ raise ValueError("enable-prenat-device-identification must be 'yes' or 'no'")
186
+ if value.get('prenat-identification', {}).get('enable-prenat-source-policy-lookup', 'no') not in self.yes_no:
187
+ raise ValueError("enable-prenat-source-policy-lookup must be 'yes' or 'no'")
188
+ if value.get('prenat-identification', {}).get('enable-prenat-source-ip-downstream', 'no') not in self.yes_no:
189
+ raise ValueError("enable-prenat-source-ip-downstream must be 'yes' or 'no'")
190
+
191
+ network_keys = [key for key in value.keys() if key in self.valid_network]
192
+ if len(network_keys) != 1:
193
+ raise ValueError("Network dictionary must contain exactly one key from valid_network")
194
+
195
+ network_key = network_keys[0]
196
+ # Default behavior when key is 'tunnel'
197
+ if network_key == 'tunnel' and value[network_key]:
198
+ raise ValueError("'tunnel' key must be associated with an empty dictionary")
199
+
200
+ # Handle non-'tunnel' network_key (e.g., 'layer3')
201
+ if network_key != 'tunnel':
202
+ # Allow empty dictionaries (new Zones)
203
+ network_member = value[network_key].get('member', None)
204
+ if network_member is not None and not isinstance(network_member, list):
205
+ raise ValueError(
206
+ f"The value for '{network_key}' must be a dictionary with a 'member' key containing a list of strings, "
207
+ f"or an empty dictionary for newly created Zones."
208
+ )
209
+
210
+ self._network = value
211
+ self.entry.update({'network': value})
212
+
213
+ @property
214
+ def enable_user_identification(self):
215
+ return self._enable_user_identification
216
+
217
+ @enable_user_identification.setter
218
+ def enable_user_identification(self, value: str):
219
+ if value not in self.yes_no:
220
+ raise ValueError("enable_user_identification must be 'yes' or 'no'")
221
+ self._enable_user_identification = value
222
+ self.entry.update({'enable-user-identification': value})
223
+
224
+ @property
225
+ def enable_device_identification(self):
226
+ return self._enable_device_identification
227
+
228
+ @enable_device_identification.setter
229
+ def enable_device_identification(self, value: str):
230
+ if value not in self.yes_no:
231
+ raise ValueError("enable_device_identification must be 'yes' or 'no'")
232
+ self._enable_device_identification = value
233
+ self.entry.update({'enable-device-identification': value})
234
+
235
+ @property
236
+ def user_acl(self):
237
+ return self._user_acl
238
+
239
+ @user_acl.setter
240
+ def user_acl(self, value: dict):
241
+ if value:
242
+ self._validate_acl_structure(value, 'user_acl')
243
+ self._user_acl = value
244
+ self.entry.update({'user-acl': value})
245
+
246
+ @property
247
+ def device_acl(self):
248
+ return self._device_acl
249
+
250
+ @device_acl.setter
251
+ def device_acl(self, value: dict):
252
+ if value:
253
+ self._validate_acl_structure(value, 'device_acl')
254
+ self._device_acl = value
255
+ self.entry.update({'device-acl': value})
256
+
257
+ @staticmethod
258
+ def _validate_acl_structure(acl_dict: dict, acl_type: str):
259
+ if not isinstance(acl_dict, dict) or 'include-list' not in acl_dict or 'exclude-list' not in acl_dict:
260
+ raise ValueError(f"{acl_type} must be a dictionary with 'include-list' and 'exclude-list' keys.")
261
+
262
+ for key in ['include-list', 'exclude-list']:
263
+ if key in acl_dict:
264
+ if not isinstance(acl_dict[key], dict) or 'member' not in acl_dict[key]:
265
+ raise ValueError(f"'{key}' in {acl_type} must be a dictionary with a 'member' key.")
266
+ if not isinstance(acl_dict[key]['member'], list):
267
+ raise ValueError(f"'member' in '{key}' of {acl_type} must be a list.")
268
+ if not all(isinstance(item, str) for item in acl_dict[key]['member']):
269
+ raise ValueError(f"All items in 'member' of '{key}' in {acl_type} must be strings.")
270
+
271
+ # Pop the key if the member list is empty
272
+ if not acl_dict[key]['member']:
273
+ acl_dict.pop(key)
274
+
275
+ def add_interface(self, interface_name: str) -> dict:
276
+ """
277
+ Adds a named interface to the zone, updating self.entry['network'] under the appropriate network type key.
278
+ Ensures compliance with the constraints of the network setter.
279
+
280
+ Args:
281
+ interface_name (str): The name of the interface to add.
282
+
283
+ Returns:
284
+ dict: The updated network dictionary.
285
+ """
286
+ if not isinstance(interface_name, str) or not interface_name.strip():
287
+ raise ValueError("Interface name must be a non-empty string.")
288
+
289
+ network_keys = [key for key in self.network.keys() if key in self.valid_network]
290
+ network_key = network_keys[0]
291
+
292
+ # if 'member' not in self.network[network_key]:
293
+ # # If the 'member' key does not exist, add it as an empty list
294
+ # self.network[network_key]['member'] = []
295
+ #
296
+ # # Add the interface name if it doesn't already exist in the 'member' list
297
+ # if interface_name not in self.network[network_key]['member']:
298
+ # self.network[network_key]['member'].append(interface_name)
299
+ #
300
+ # # Update self.entry and return the updated network
301
+ # self.entry.update({'network': self.network})
302
+ # return self.network
303
+
304
+ # use XML since the rest api is current broken for PANOS 11.1
305
+ xpath =f"xpath=/config/devices/entry[@name='localhost.localdomain']/template/entry[@name='{self.template}']/config/devices/entry[@name='vsys']/vsys/entry[@name='{self.vsys}']/zone/entry[@name='{self.name}']/network/{network_key}"
306
+ element=f"<member>{interface_name}</member>"
307
+ getresult = self.get_xml(xpath)
308
+ ic(getresult)
309
+ result = self.set_xml(xpath, element)
310
+ return result
311
+
312
+ class DHCPServers(Network):
313
+ """
314
+ Special note about the name attribute. For DHCP servers, the name is the interface the DHCP server is attached too.
315
+ """
316
+ def __init__(self, PANDevice: Panorama | Firewall, **kwargs):
317
+ super().__init__(PANDevice, max_name_length=32, **kwargs)
318
+ self.name: str = kwargs.get('name')
319
+ self.probe_ip: str = kwargs.get('probe_ip', 'no')
320
+ self.option: dict = kwargs.get('option')
321
+ self.ip_pool: dict = kwargs.get('ip_pool')
322
+ self.reserved: dict = kwargs.get('reserved')
323
+ self.mode: dict = kwargs.get('mode')
324
+
325
+ @property
326
+ def name(self):
327
+ return self._name
328
+
329
+ @name.setter
330
+ def name(self, value):
331
+ if value:
332
+ if value.startswith("ethernet"):
333
+ # Expecting format: ethernetX/Y where X and Y are digits
334
+ if not self._validate_ethernet_name(value):
335
+ raise ValueError("Invalid ethernet format. Expected format: ethernetX/Y.")
336
+ elif value.startswith("vlan"):
337
+ # Expecting format: vlan.X where X is a digit
338
+ if not self._validate_vlan_name(value):
339
+ raise ValueError("Invalid vlan format. Expected format: vlan.X.")
340
+ else:
341
+ raise ValueError("Name must start with 'ethernet' or 'vlan'.")
342
+
343
+ self._name = value
344
+ self.entry.update({'@name': value})
345
+
346
+ @staticmethod
347
+ def _validate_ethernet_name(name):
348
+ # Check if name follows the ethernetX/Y format
349
+ pattern = r"^ethernet\d+/\d+(\.\d+)?$"
350
+ return re.match(pattern, name) is not None
351
+
352
+ @staticmethod
353
+ def _validate_vlan_name(name):
354
+ # Check if name follows the vlan.X format
355
+ pattern = r"^vlan\.\d+$"
356
+ return re.match(pattern, name) is not None
357
+
358
+ @property
359
+ def mode(self):
360
+ return self._mode
361
+
362
+ @mode.setter
363
+ def mode(self, value):
364
+ if value:
365
+ if isinstance(value, dict) and 'text' in value:
366
+ determined_value = value['text']
367
+ else:
368
+ determined_value = value
369
+ if determined_value not in ['enabled', 'disabled', 'auto']:
370
+ raise AttributeError(f'The mode attribute must be one of enabled, '
371
+ f'disabled or auto.')
372
+ self._mode = determined_value
373
+ self.entry.update({'mode': determined_value})
374
+ else:
375
+ self._mode = None
376
+
377
+ @property
378
+ def probe_ip(self):
379
+ return self._probeIP
380
+
381
+ @probe_ip.setter
382
+ def probe_ip(self, value):
383
+ if value:
384
+ if isinstance(value, dict) and 'text' in value:
385
+ determined_value = value['text']
386
+ else:
387
+ determined_value = value
388
+ if determined_value not in self.yes_no:
389
+ raise AttributeError(f'The ip_probe attribute must be one of {self.yes_no}')
390
+ self._probeIP = determined_value
391
+ self.entry.update({'probe-ip': determined_value})
392
+
393
+ @property
394
+ def ip_pool(self):
395
+ return self._ipPool
396
+
397
+ @ip_pool.setter
398
+ def ip_pool(self, value):
399
+ if value:
400
+ if not isinstance(value, dict) or 'member' not in value or not isinstance(value['member'], list):
401
+ raise TypeError("ip_pool must be a dictionary with a 'member' key pointing to a list.")
402
+
403
+ for member in value['member']:
404
+ if '-' in member:
405
+ # Handle IP range
406
+ ip_range = member.split('-')
407
+ if len(ip_range) != 2:
408
+ raise ValueError(f"{member} is not a valid IP address range.")
409
+
410
+ start_ip, end_ip = ip_range
411
+ try:
412
+ ipaddress.IPv4Address(start_ip)
413
+ ipaddress.IPv4Address(end_ip)
414
+ except ipaddress.AddressValueError:
415
+ raise ValueError(f"{member} is not a valid IP address range.")
416
+ else:
417
+ # Handle individual IP
418
+ try:
419
+ ipaddress.IPv4Address(member)
420
+ except ipaddress.AddressValueError:
421
+ raise ValueError(f"{member} is not a valid IP address.")
422
+
423
+ self._ipPool = value
424
+ self.entry.update({'ip-pool': value})
425
+
426
+ @property
427
+ def reserved(self):
428
+ return self._reserved
429
+
430
+ @reserved.setter
431
+ def reserved(self, value):
432
+ if value:
433
+ if not isinstance(value, dict) or 'entry' not in value:
434
+ raise TypeError("The 'reserved' attribute must be a dictionary containing an 'entry' key.")
435
+
436
+ if not isinstance(value['entry'], list):
437
+ raise TypeError("The 'entry' key must map to a list.")
438
+
439
+ for item in value['entry']:
440
+ if not isinstance(item, dict) or not all(key in item for key in ['@name', 'mac', 'description']):
441
+ raise ValueError(
442
+ "Each item in 'entry' must be a dictionary with '@name', 'mac', and 'description' keys.")
443
+
444
+ # Validate @name as IPv4
445
+ try:
446
+ ipaddress.IPv4Address(item['@name'])
447
+ except ipaddress.AddressValueError:
448
+ raise ValueError(f"{item['@name']} is not a valid IPv4 address.")
449
+
450
+ # Validate MAC address format
451
+ if not re.match(r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$", item['mac']):
452
+ raise ValueError(f"{item['mac']} is not a valid MAC address.")
453
+
454
+ # Validate description length
455
+ if not isinstance(item['description'], str) or len(item['description']) > 255:
456
+ raise ValueError("The 'description' must be a string with 255 characters or fewer.")
457
+
458
+ self._reserved = value
459
+ self.entry.update({'reserved': value})
460
+
461
+ @property
462
+ def option(self):
463
+ return self._option
464
+
465
+ @option.setter
466
+ def option(self, value):
467
+ if value:
468
+ if not isinstance(value, dict):
469
+ raise TypeError("Option must be a dictionary.")
470
+
471
+ # Validate 'lease'
472
+ if 'lease' not in value or not isinstance(value['lease'], dict):
473
+ raise ValueError("Lease must be present and a dictionary.")
474
+
475
+ lease_keys = list(value['lease'].keys())
476
+ if len(lease_keys) != 1 or (lease_keys[0] not in ['unlimited', 'timeout']):
477
+ raise ValueError("Lease must contain exactly one key: either 'unlimited' or 'timeout'.")
478
+
479
+ if 'unlimited' in lease_keys and value['lease']['unlimited'] != {}:
480
+ raise ValueError("Unlimited must be an empty dictionary.")
481
+
482
+ if 'timeout' in lease_keys:
483
+ timeout = value['lease']['timeout']
484
+ if not (isinstance(timeout, int) and 0 <= timeout <= 1000000):
485
+ raise ValueError("Timeout must be an integer between 0 and 1,000,000.")
486
+
487
+ # Validate 'inheritance'
488
+ if 'inheritance' in value and 'source' not in value['inheritance']:
489
+ raise ValueError("Inheritance dictionary must have a 'source' key.")
490
+
491
+ # Validate IP addresses for gateway, dns, wins, nis, ntp
492
+ for key in ['gateway', 'dns', 'wins', 'nis', 'ntp']:
493
+ if key in value:
494
+ if key == 'gateway':
495
+ if not self._validate_ip_address(value[key]):
496
+ raise ValueError(f"{key} must be a valid IP address.")
497
+ else:
498
+ for sub_key in ['primary', 'secondary']:
499
+ if sub_key in value[key] and not self._validate_ip_address(value[key][sub_key]):
500
+ raise ValueError(f"{key}.{sub_key} must be a valid IP address.")
501
+
502
+ # Validate 'subnet-mask'
503
+ if 'subnet-mask' in value and not self._validate_ip_address(value['subnet-mask']):
504
+ raise ValueError("subnet-mask must be a valid subnet mask.")
505
+
506
+ if 'dns-suffix' in value:
507
+ if not isinstance(value['dns-suffix'], str):
508
+ raise ValueError("dns-suffix must be a string.")
509
+ self._option = value
510
+ self.entry.update({'option': value})
511
+
512
+ def add_reserved_entry(self, name, mac, description=None):
513
+ if not hasattr(self, '_reserved'):
514
+ self.reserved = {'entry': []}
515
+ # Validate the inputs
516
+ if not re.match(r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$", mac):
517
+ raise ValueError(f"{mac} is not a valid MAC address.")
518
+ try:
519
+ ipaddress.IPv4Address(name)
520
+ except ipaddress.AddressValueError:
521
+ raise ValueError(f"{name} is not a valid IPv4 address.")
522
+ if description and (not isinstance(description, str) or len(description) > 255):
523
+ raise ValueError("The 'description' must be a string with 255 characters or fewer.")
524
+
525
+ # Add the new entry
526
+ new_entry = {'@name': name, 'mac': mac}
527
+ if description:
528
+ new_entry['description'] = description
529
+
530
+ if 'entry' not in self.reserved:
531
+ self.reserved['entry'] = []
532
+
533
+ self.reserved['entry'].append(new_entry)
534
+ self.entry.update({'reserved': self.reserved})
535
+
536
+ def remove_reserved_entry(self, name, mac):
537
+ if 'entry' not in self.reserved or not isinstance(self.reserved['entry'], list):
538
+ raise ValueError("No reserved entries to remove.")
539
+
540
+ # Find and remove the entry
541
+ entry_to_remove = None
542
+ for entry in self.reserved['entry']:
543
+ if entry['@name'] == name and entry['mac'] == mac:
544
+ entry_to_remove = entry
545
+ break
546
+
547
+ if entry_to_remove:
548
+ self.reserved['entry'].remove(entry_to_remove)
549
+ self.entry.update({'reserved': self.reserved})
550
+ else:
551
+ raise ValueError(f"No entry found with name {name} and MAC {mac}.")
552
+
553
+ class Interfaces(Network):
554
+ def __init__(self, PANDevice: Panorama | Firewall, **kwargs):
555
+ super().__init__(PANDevice, **kwargs)
556
+ self.df_ignore: str = kwargs.get('df_ignore', 'no')
557
+ self.mtu: int = kwargs.get('mtu', 1500)
558
+ self.ip: dict = kwargs.get('ip')
559
+ self.ipv6: dict = kwargs.get('ipv6')
560
+ self.bonjour: dict = kwargs.get('bonjour')
561
+ self.interface_management_profile: str = kwargs.get('interface_management_profile')
562
+ self.netflow_profile: str = kwargs.get('netflow_profile')
563
+ self.comment: str = kwargs.get('comment')
564
+
565
+ @property
566
+ def df_ignore(self):
567
+ return self._df_ignore
568
+
569
+ @df_ignore.setter
570
+ def df_ignore(self, value: str) -> None:
571
+ if not isinstance(value, str):
572
+ raise TypeError("df_ignore must be a string.")
573
+ value = value.lower()
574
+ if value not in self.yes_no:
575
+ raise ValueError("df_ignore must be 'yes' or 'no'.")
576
+
577
+ self._df_ignore = value
578
+ self.entry.update({'df-ignore': value})
579
+
580
+ @property
581
+ def mtu(self):
582
+ return self._mtu
583
+
584
+ @mtu.setter
585
+ def mtu(self, value: int) -> None:
586
+ if not isinstance(value, int):
587
+ raise TypeError("MTU must be an integer.")
588
+ if not 576 <= value <= 9216:
589
+ raise ValueError("MTU must be between 576 and 9216.")
590
+ self._mtu = value
591
+ self.entry.update({'mtu': value})
592
+
593
+ @property
594
+ def bonjour(self):
595
+ return self._bonjour
596
+
597
+ @bonjour.setter
598
+ def bonjour(self, value: str) -> None:
599
+ if value is not None:
600
+ if not isinstance(value, dict):
601
+ raise TypeError("Bonjour must be a dictionary or None.")
602
+
603
+ # Initialize with default values
604
+ bonjour_dict = {'enable': 'no', 'ttl-check': 'no', 'group-id': 0}
605
+
606
+ # Validate and update 'enable' key if present
607
+ if 'enable' in value:
608
+ if value['enable'] not in self.yes_no:
609
+ raise ValueError("Bonjour 'enable' must be 'yes' or 'no'.")
610
+ bonjour_dict['enable'] = value['enable']
611
+
612
+ # Validate and update 'ttl-check' key if present
613
+ if 'ttl-check' in value:
614
+ if value['ttl-check'] not in self.yes_no:
615
+ raise ValueError("Bonjour 'ttl-check' must be 'yes' or 'no'.")
616
+ bonjour_dict['ttl-check'] = value['ttl-check']
617
+
618
+ # Validate and update 'group-id' key if present
619
+ if 'group-id' in value:
620
+ if not isinstance(value['group-id'], int) or not 0 <= value['group-id'] <= 16:
621
+ raise ValueError("Bonjour 'group-id' must be an integer between 0 and 16.")
622
+ bonjour_dict['group-id'] = value['group-id']
623
+
624
+ # Update the _bonjour attribute if validations pass
625
+ self._bonjour = bonjour_dict
626
+ self.entry.update({'bonjour': bonjour_dict})
627
+ else:
628
+ self._bonjour = None
629
+
630
+ @property
631
+ def ip(self):
632
+ return self._ip
633
+
634
+ @ip.setter
635
+ def ip(self, value: str) -> None:
636
+ if value:
637
+ if not isinstance(value, dict):
638
+ raise TypeError("IP must be a dictionary with an 'entry' key.")
639
+ if 'entry' not in value:
640
+ raise ValueError("IP dictionary must contain an 'entry' key.")
641
+ if not isinstance(value['entry'], list):
642
+ raise TypeError("The 'entry' key must map to a list.")
643
+
644
+ # Validate each address in the list
645
+ for entry in value['entry']:
646
+ # Ensure each item in the list is a dictionary and contains the '@name' key
647
+ if not isinstance(entry, dict) or '@name' not in entry:
648
+ raise ValueError("Each entry in the 'entry' list must be a dictionary with an '@name' key.")
649
+
650
+ # Validate the '@name' value (the IP address)
651
+ address = entry['@name']
652
+ if not self._validate_interface_address(address):
653
+ raise ValueError(f"{address} is not a valid IP address, Palo Alto variable, or interface address.")
654
+
655
+ self._ip = value
656
+ self.entry.update({'ip': value})
657
+
658
+ @property
659
+ def interface_management_profile(self):
660
+ return self._interface_management_profile
661
+
662
+ @interface_management_profile.setter
663
+ def interface_management_profile(self, value: str) -> None:
664
+ if value:
665
+ if not isinstance(value, str):
666
+ raise TypeError("interface_management_profile must be a string.")
667
+
668
+ if len(value) > 31:
669
+ raise ValueError("interface_management_profile cannot be longer than 31 characters.")
670
+
671
+ self._interface_management_profile = value
672
+ self.entry.update({'interface-management-profile': value})
673
+
674
+ @property
675
+ def netflow_profile(self):
676
+ return self._netflow_profile
677
+
678
+ @netflow_profile.setter
679
+ def netflow_profile(self, value: str) -> None:
680
+ if value:
681
+ if not isinstance(value, str):
682
+ raise TypeError("netflow_profile must be a string.")
683
+
684
+ if len(value) > 63:
685
+ raise ValueError("netflow_profile cannot be longer than 63 characters.")
686
+
687
+ self._netflow_profile = value
688
+ self.entry.update({'netflow-profile': value})
689
+
690
+ @property
691
+ def comment(self):
692
+ return self._comment
693
+
694
+ @comment.setter
695
+ def comment(self, value: str) -> None:
696
+ if value:
697
+ if not isinstance(value, str):
698
+ raise TypeError("comment must be a string.")
699
+
700
+ if len(value) > 1023:
701
+ raise ValueError("comment cannot be longer than 1023 characters.")
702
+
703
+ self._comment = value
704
+ self.entry.update({'comment': value})
705
+
706
+
707
+ class EthernetInterfaces(Interfaces):
708
+ def __init__(self, PANDevice: Panorama | Firewall, **kwargs):
709
+ super().__init__(PANDevice, max_name_length=32, **kwargs)
710
+ self._name: str = kwargs.get('name', '')
711
+
712
+ def valid_name(self, value: str, max_name_length: int) -> bool:
713
+ """
714
+ Validates if the provided string is a valid name according to the allowed character set and length.
715
+
716
+ Parameters:
717
+ value (str): The string to validate.
718
+ max_name_length (int): The maximum length of the name to be validated
719
+
720
+ Returns:
721
+ bool: True if the string is valid, otherwise raises a ValueError.
722
+
723
+ Raises:
724
+ ValueError: If the string contains invalid characters or exceeds the maximum length.
725
+
726
+ """
727
+
728
+ # Define the allowed character set using a regular expression.
729
+ # The pattern now ensures the entire string consists of the allowed characters.
730
+ if isinstance(self, EthernetInterfaces):
731
+ pattern = r"^[ 0-9a-zA-Z._/-]+$"
732
+ else:
733
+ pattern = r"^[ 0-9a-zA-Z._-]+$"
734
+
735
+ allowed = re.compile(pattern, re.IGNORECASE)
736
+
737
+ # Check length first for efficiency
738
+ if len(value) > max_name_length:
739
+ raise ValueError(f"The name exceeds the maximum length of {max_name_length} characters.")
740
+ if not allowed.match(value):
741
+ raise ValueError(
742
+ "The name contains invalid characters. Only alphanumeric characters, spaces, and ._- are allowed.")
743
+
744
+ return True
745
+
746
+ @property
747
+ def name(self) -> str:
748
+ return self._name
749
+
750
+ @name.setter
751
+ def name(self, value: str) -> None:
752
+ if value:
753
+ if not self.valid_name(value, self.max_name_length):
754
+ raise ValueError(f"Provided name '{value}' is invalid.")
755
+ self._name = value
756
+ self.entry.update({'@name': self._name})
757
+ else:
758
+ self._name = None
759
+
760
+ class AggregateEthernetInterfaces(Interfaces):
761
+ def __init__(self, PANDevice: Panorama | Firewall, **kwargs):
762
+ super().__init__(PANDevice, max_name_length=32, **kwargs)
763
+
764
+ class VLANInterfaces(Interfaces):
765
+ def __init__(self, PANDevice: Panorama | Firewall, **kwargs):
766
+ super().__init__(PANDevice, max_name_length=32, **kwargs)
767
+ self._name: str = kwargs.get('name')
768
+
769
+ @property
770
+ def name(self):
771
+ return self._name
772
+
773
+ @name.setter
774
+ def name(self, value):
775
+ if value:
776
+ if not isinstance(value, str):
777
+ raise TypeError("Name must be a string.")
778
+ if value:
779
+ if not value.startswith('tunnel.'):
780
+ raise ValueError("Name must start with 'tunnel.'")
781
+
782
+ try:
783
+ tunnel_number = int(value.split('.')[1])
784
+ except (IndexError, ValueError):
785
+ raise ValueError("Name must end with a number between 1 and 9999.")
786
+
787
+ if not 1 <= tunnel_number <= 9999:
788
+ raise ValueError("The number following 'tunnel.' must be between 1 and 9999.")
789
+
790
+ self._name = value
791
+ self.entry.update({'@name': value})
792
+
793
+ class VirtualWires(Network):
794
+ def __init__(self, PANDevice: Panorama | Firewall, **kwargs):
795
+ super().__init__(PANDevice, max_name_length=32, **kwargs)
796
+ pass
797
+
798
+ class LoopbackInterfaces(Interfaces):
799
+ def __init__(self, PANDevice: Panorama | Firewall, **kwargs):
800
+ super().__init__(PANDevice, max_name_length=32, **kwargs)
801
+
802
+ class TunnelInterfaces(Interfaces):
803
+ def __init__(self, PANDevice: Panorama | Firewall, **kwargs):
804
+ super().__init__(PANDevice, **kwargs)
805
+ self._name: str = kwargs.get('name')
806
+ self.link_tag: str = kwargs.get('link_tag')
807
+
808
+ @property
809
+ def name(self):
810
+ return self._name
811
+
812
+ @name.setter
813
+ def name(self, value):
814
+ if value:
815
+ if not isinstance(value, str):
816
+ raise TypeError("Name must be a string.")
817
+ if value:
818
+ if not value.startswith('tunnel.'):
819
+ raise ValueError("Name must start with 'tunnel.'")
820
+
821
+ try:
822
+ tunnel_number = int(value.split('.')[1])
823
+ except (IndexError, ValueError):
824
+ raise ValueError("Name must end with a number between 1 and 9999.")
825
+
826
+ if not 1 <= tunnel_number <= 9999:
827
+ raise ValueError("The number following 'tunnel.' must be between 1 and 9999.")
828
+
829
+ self._name = value
830
+ self.entry.update({'@name': value})
831
+
832
+ @property
833
+ def link_tag(self):
834
+ return self._link_tag
835
+
836
+ @link_tag.setter
837
+ def link_tag(self, value: str) -> None:
838
+ if value:
839
+ if not isinstance(value, str):
840
+ raise TypeError("link_tag must be a string.")
841
+
842
+ if len(value) > 127:
843
+ raise ValueError("link_tag cannot be longer than 127 characters.")
844
+
845
+ self._link_tag = value
846
+ self.entry.update({'link-tag': value})
847
+
848
+ class SDWANInterfaces(Network):
849
+ def __init__(self, PANDevice: Panorama | Firewall, **kwargs):
850
+ super().__init__(PANDevice, **kwargs)
851
+ self.name: str = kwargs.get('name')
852
+ self.comment: str = kwargs.get('comment')
853
+ self.link_tag: str = kwargs.get('link_tag')
854
+ self.cluster_name: str = kwargs.get('cluster_name')
855
+ self.allow_saas_monitor: str = kwargs.get('allow_saas_monitor', 'no')
856
+ self.metric: int = kwargs.get('metric', 10)
857
+ self.interface: dict = kwargs.get('interface')
858
+
859
+ @property
860
+ def name(self):
861
+ return self._name
862
+
863
+ @name.setter
864
+ def name(self, value):
865
+ if not isinstance(value, str):
866
+ raise TypeError("Name must be a string.")
867
+
868
+ if not value.startswith('sdwan.'):
869
+ raise ValueError("Name must start with 'sdwan.'")
870
+
871
+ try:
872
+ interface_number = int(value.split('.')[1])
873
+ except (IndexError, ValueError):
874
+ raise ValueError("Name must end with a number between 1 and 9999.")
875
+
876
+ if not 1 <= interface_number <= 9999:
877
+ raise ValueError("The number following 'sdwan.' must be between 1 and 9999.")
878
+
879
+ self._name = value
880
+ self.entry.update({'@name': value})
881
+
882
+ @property
883
+ def link_tag(self):
884
+ return self._link_tag
885
+
886
+ @link_tag.setter
887
+ def link_tag(self, value: str) -> None:
888
+ if value:
889
+ if not isinstance(value, str):
890
+ raise TypeError("link_tag must be a string.")
891
+
892
+ if len(value) > 127:
893
+ raise ValueError("link_tag cannot be longer than 127 characters.")
894
+
895
+ self._link_tag = value
896
+ self.entry.update({'link-tag': value})
897
+
898
+ @property
899
+ def comment(self):
900
+ return self._comment
901
+
902
+ @comment.setter
903
+ def comment(self, value: str) -> None:
904
+ if value:
905
+ if not isinstance(value, str):
906
+ raise TypeError("comment must be a string.")
907
+
908
+ if len(value) > 1023:
909
+ raise ValueError("comment cannot be longer than 1023 characters.")
910
+
911
+ self._comment = value
912
+ self.entry.update({'comment': value})
913
+
914
+ @property
915
+ def interface(self):
916
+ return self._interface
917
+
918
+ @interface.setter
919
+ def interface(self, value: dict) -> None:
920
+ if value:
921
+ if not isinstance(value, dict):
922
+ raise TypeError("Interface must be a dictionary.")
923
+
924
+ if 'member' not in value:
925
+ raise ValueError("Interface dictionary must contain a 'member' key.")
926
+
927
+ if not isinstance(value['member'], list):
928
+ raise TypeError("The 'member' key must map to a list.")
929
+
930
+ for item in value['member']:
931
+ if not isinstance(item, str):
932
+ raise TypeError("Each member in the 'member' list must be a string.")
933
+
934
+ self._interface = value
935
+ self.entry.update({'interface': value})
936
+
937
+ @property
938
+ def cluster_name(self):
939
+ return self._cluster_name
940
+
941
+ @cluster_name.setter
942
+ def cluster_name(self, value: str) -> None:
943
+ if value:
944
+ if not isinstance(value, str):
945
+ raise TypeError("cluster_name must be a string.")
946
+
947
+ if len(value) > 64:
948
+ raise ValueError("cluster_name cannot be longer than 64 characters.")
949
+
950
+ self._cluster_name = value
951
+ self.entry.update({'cluster-name': value})
952
+
953
+ @property
954
+ def allow_saas_monitor(self):
955
+ return self._allow_saas_monitor
956
+
957
+ @allow_saas_monitor.setter
958
+ def allow_saas_monitor(self, value: str) -> None:
959
+ if value:
960
+ if not isinstance(value, str):
961
+ raise TypeError("allow_saas_monitor must be a string.")
962
+
963
+ if value not in self.yes_no:
964
+ raise ValueError("allow_saas_monitor must be 'yes' or 'no'.")
965
+
966
+ self._allow_saas_monitor = value
967
+ self.entry.update({'allow-saas-monitor': value})
968
+
969
+ @property
970
+ def metric(self):
971
+ return self._metric
972
+
973
+ @metric.setter
974
+ def metric(self, value: int) -> None:
975
+ if value:
976
+ if not isinstance(value, int):
977
+ raise TypeError("metric must be an integer.")
978
+
979
+ if not 1 <= value <= 65535:
980
+ raise ValueError("metric must be in the range of 1 to 65535.")
981
+
982
+ self._metric = value
983
+ self.entry.update({'metric': value})
984
+
985
+ class AutoKey:
986
+ class ProxyId:
987
+ def __init__(self, **kwargs):
988
+ self.name: str = kwargs.get('name')
989
+ self.local: str = kwargs.get('local')
990
+ self.remote: str = kwargs.get('remote')
991
+ self.protocol: dict = kwargs.get('protocol')
992
+
993
+ @property
994
+ def name(self):
995
+ return self._name
996
+
997
+ @name.setter
998
+ def name(self, value: str) -> None:
999
+ if not isinstance(value, str):
1000
+ raise TypeError("Name must be a string.")
1001
+
1002
+ if len(value) > 31:
1003
+ raise ValueError("Name cannot be longer than 31 characters.")
1004
+
1005
+ if not re.match(r"^[0-9a-zA-Z._-]*$", value):
1006
+ raise ValueError("Name can only contain alphanumeric characters and . _ -")
1007
+
1008
+ self._name = value
1009
+
1010
+ @staticmethod
1011
+ def _validate_ip_or_subnet(value: str) -> bool:
1012
+ try:
1013
+ # This will validate for both IP address and subnet
1014
+ ipaddress.ip_network(value, strict=False)
1015
+ return True
1016
+ except ValueError:
1017
+ raise ValueError("Value must be a valid IP address or subnet.")
1018
+
1019
+ @property
1020
+ def local(self):
1021
+ return self._local
1022
+
1023
+ @local.setter
1024
+ def local(self, value: str) -> None:
1025
+ if value:
1026
+ self._validate_ip_or_subnet(value)
1027
+ self._local = value
1028
+
1029
+ @property
1030
+ def remote(self):
1031
+ return self._remote
1032
+
1033
+ @remote.setter
1034
+ def remote(self, value: str) -> None:
1035
+ if value:
1036
+ self._validate_ip_or_subnet(value)
1037
+ self._remote = value
1038
+
1039
+ @property
1040
+ def protocol(self):
1041
+ return self._protocol
1042
+
1043
+ @protocol.setter
1044
+ def protocol(self, value: dict) -> None:
1045
+ if value:
1046
+ if not isinstance(value, dict):
1047
+ raise TypeError("Protocol must be a dictionary.")
1048
+
1049
+ if len(value) != 1:
1050
+ raise ValueError("Protocol dictionary must contain exactly one key.")
1051
+
1052
+ key = next(iter(value)) # Get the first key to determine the type.
1053
+
1054
+ if key == 'number':
1055
+ if not isinstance(value[key], int) or not 1 <= value[key] <= 254:
1056
+ raise ValueError("The 'number' key must have an integer value between 1 and 254.")
1057
+
1058
+ elif key == 'any':
1059
+ if value[key] != {}:
1060
+ raise ValueError("The 'any' key must have an empty dictionary as its value.")
1061
+
1062
+ elif key in ['tcp', 'udp']:
1063
+ if not isinstance(value[key], dict):
1064
+ raise ValueError(f"The '{key}' key must have a dictionary as its value.")
1065
+
1066
+ for port_key in ['local-port', 'remote-port']:
1067
+ if port_key not in value[key]:
1068
+ value[key][port_key] = 0 # Assign default value if not present
1069
+
1070
+ if not isinstance(value[key][port_key], int) or not 0 <= value[key][port_key] <= 65535:
1071
+ raise ValueError(f"The '{port_key}' must be an integer between 0 and 65535.")
1072
+
1073
+ else:
1074
+ raise ValueError("The protocol key must be one of 'number', 'any', 'tcp', or 'udp'.")
1075
+
1076
+ self._protocol = value
1077
+
1078
+ def to_dict(self) -> dict:
1079
+ return {
1080
+ '@name': self.name,
1081
+ 'local': self.local,
1082
+ 'remote': self.remote,
1083
+ 'protocol': self.protocol
1084
+ }
1085
+
1086
+ def __init__(self, **kwargs):
1087
+ self._ike_gateway: dict = kwargs.get('ike_gateway')
1088
+ self._ipsec_crypto_profile: str = kwargs.get('ipsec_crypto_profile', 'default')
1089
+ self._proxy_id: dict = {'entry': []}
1090
+ self._proxy_id_v6: dict = {'entry': []}
1091
+
1092
+ @property
1093
+ def proxy_id(self):
1094
+ return self._proxy_id
1095
+
1096
+ @proxy_id.setter
1097
+ def proxy_id(self, value: dict) -> None:
1098
+ if not isinstance(value, dict) or 'entry' not in value or not isinstance(value['entry'], list):
1099
+ raise TypeError("proxy_id must be a dictionary with a 'entry' key that is a list.")
1100
+ self._proxy_id = value
1101
+
1102
+ def add_proxy_id(self, proxy_id: ProxyId) -> None:
1103
+ if not isinstance(proxy_id, AutoKey.ProxyId):
1104
+ raise TypeError("proxy_id must be an instance of AutoKey.ProxyId.")
1105
+ self._proxy_id['entry'].append(proxy_id.to_dict())
1106
+
1107
+ def remove_proxy_id(self, name: str) -> bool:
1108
+ for i, pid in enumerate(self._proxy_id['entry']):
1109
+ if pid.get('@name', '') == name:
1110
+ del self._proxy_id['entry'][i]
1111
+ return True
1112
+ return False
1113
+
1114
+ @property
1115
+ def proxy_id_v6(self):
1116
+ return self._proxy_id_v6
1117
+
1118
+ @proxy_id_v6.setter
1119
+ def proxy_id_v6(self, value: dict) -> None:
1120
+ if not isinstance(value, dict) or 'entry' not in value or not isinstance(value['entry'], list):
1121
+ raise TypeError("proxy_id_v6 must be a dictionary with a 'entry' key that is a list.")
1122
+ self._proxy_id_v6 = value
1123
+
1124
+ def add_proxy_id_v6(self, proxy_id: ProxyId) -> None:
1125
+ if not isinstance(proxy_id_v6, AutoKey.ProxyId):
1126
+ raise TypeError("proxy_id must be an instance of AutoKey.ProxyId.")
1127
+ self._proxy_id['entry'].append(proxy_id.to_dict())
1128
+
1129
+ def remove_proxy_id_v6(self, name: str) -> bool:
1130
+ for i, pid in enumerate(self._proxy_id_v6['entry']):
1131
+ if pid.get('@name', '') == name:
1132
+ del self._proxy_id_v6['entry'][i]
1133
+ return True
1134
+ return False
1135
+
1136
+ @property
1137
+ def ipsec_crypto_profile(self):
1138
+ return self._ipsec_crypto_profile
1139
+
1140
+ @ipsec_crypto_profile.setter
1141
+ def ipsec_crypto_profile(self, value: str) -> None:
1142
+ if not isinstance(value, str):
1143
+ raise TypeError("ipsec_crypto_profile must be a string.")
1144
+ self._ipsec_crypto_profile = value
1145
+
1146
+ @property
1147
+ def ike_gateway(self):
1148
+ return self._ike_gateway
1149
+
1150
+ @ike_gateway.setter
1151
+ def ike_gateway(self, value: dict) -> None:
1152
+ if not isinstance(value, dict):
1153
+ raise TypeError("ike_gateway must be a dictionary.")
1154
+
1155
+ if 'entry' not in value:
1156
+ raise ValueError("ike_gateway dictionary must contain an 'entry' key.")
1157
+
1158
+ if not isinstance(value['entry'], list):
1159
+ raise TypeError("The 'entry' key in ike_gateway must be a list.")
1160
+
1161
+ for item in value['entry']:
1162
+ if not isinstance(item, dict):
1163
+ raise TypeError("Each item in the 'entry' list must be a dictionary.")
1164
+
1165
+ if '@name' not in item:
1166
+ raise ValueError("Each dictionary in the 'entry' list must have an '@name' key.")
1167
+
1168
+ if not isinstance(item['@name'], str):
1169
+ raise TypeError("The '@name' key value must be a string.")
1170
+
1171
+ if len(item['@name']) > 63:
1172
+ raise ValueError("The value of '@name' must not exceed 63 characters.")
1173
+
1174
+ self._ike_gateway = value
1175
+
1176
+ def to_dict(self) -> dict:
1177
+ return {
1178
+ 'ike-gateway': self.ike_gateway,
1179
+ 'ipsec-crypto-profile': self.ipsec_crypto_profile,
1180
+ 'proxy-id': self.proxy_id,
1181
+ 'proxy-id-v6': self.proxy_id_v6
1182
+ }
1183
+ class IPSecTunnels(Network):
1184
+ def __init__(self, PANDevice: Panorama | Firewall, **kwargs):
1185
+ super().__init__(PANDevice, max_name_length=64, **kwargs)
1186
+ self._disabled: str = kwargs.get('disabled', 'no')
1187
+ self._comment: str = kwargs.get('comment')
1188
+ self._tunnel_interface: str = kwargs.get('tunnel_interface')
1189
+ self._anti_replay: str = kwargs.get('anti_replay', 'yes')
1190
+ self._anti_replay_window: str = kwargs.get('anti_replay_window', '1024')
1191
+ self._copy_tos: str = kwargs.get('copy_tos', 'no')
1192
+ self._copy_flow_label: str = kwargs.get('copy_flow_label', 'no')
1193
+ self._enable_gre_encapsulation: str = kwargs.get('enable_gre_encapsulation', 'no')
1194
+ self.auto_key_arg = kwargs.get('auto_key', {})
1195
+ if isinstance(self.auto_key_arg, AutoKey):
1196
+ self.auto_key = self.auto_key_arg
1197
+ else:
1198
+ self.auto_key = AutoKey(**auto_key_arg)
1199
+
1200
+ @property
1201
+ def auto_key(self):
1202
+ return self._auto_key
1203
+
1204
+ @auto_key.setter
1205
+ def auto_key(self, value: AutoKey) -> None:
1206
+ if not isinstance(value, AutoKey):
1207
+ raise TypeError("auto_key must be an instance of AutoKey.")
1208
+ if not self.entry.get('auto-key'):
1209
+ self.entry.update({'auto-key': {}})
1210
+ if value.ipsec_crypto_profile:
1211
+ self.entry['auto-key'].update({'ipsec-crypto-profile': value.ipsec_crypto_profile})
1212
+ if value.ike_gateway:
1213
+ self.entry['auto-key'].update({'ike-gateway': value.ike_gateway})
1214
+ self._auto_key = value
1215
+
1216
+ @property
1217
+ def comment(self):
1218
+ return self._comment
1219
+
1220
+ @comment.setter
1221
+ def comment(self, value: str) -> None:
1222
+ if not isinstance(value, str):
1223
+ raise TypeError("comment must be a string.")
1224
+
1225
+ if len(value) > 1023:
1226
+ raise ValueError("comment cannot be longer than 1023 characters.")
1227
+
1228
+ self._comment = value
1229
+ self.entry.update({'comment': value})
1230
+
1231
+ @property
1232
+ def disabled(self) -> str:
1233
+ return self._disabled
1234
+
1235
+ @disabled.setter
1236
+ def disabled(self, value: str) -> None:
1237
+ if value not in self.yes_no:
1238
+ raise ValueError("disabled must be either 'yes' or 'no'.")
1239
+ self._disabled = value
1240
+ self.entry.update({'disabled': value})
1241
+
1242
+ @property
1243
+ def anti_replay(self) -> str:
1244
+ return self._anti_replay
1245
+
1246
+ @anti_replay.setter
1247
+ def anti_replay(self, value: str) -> None:
1248
+ if value not in self.yes_no:
1249
+ raise ValueError("anti_replay must be either 'yes' or 'no'.")
1250
+ self._anti_replay = value
1251
+ self.entry.update({'anti-replay': value})
1252
+
1253
+ @property
1254
+ def copy_tos(self) -> str:
1255
+ return self._copy_tos
1256
+
1257
+ @copy_tos.setter
1258
+ def copy_tos(self, value: str) -> None:
1259
+ if value not in self.yes_no:
1260
+ raise ValueError("copy_tos must be either 'yes' or 'no'.")
1261
+ self._copy_tos = value
1262
+ self.entry.update({'copy-tos': value})
1263
+
1264
+ @property
1265
+ def copy_flow_label(self) -> str:
1266
+ return self._copy_flow_label
1267
+
1268
+ @copy_flow_label.setter
1269
+ def copy_flow_label(self, value: str) -> None:
1270
+ if value not in self.yes_no:
1271
+ raise ValueError("copy_flow_label must be either 'yes' or 'no'.")
1272
+ self._copy_flow_label = value
1273
+ self.entry.update({'copy-flow-label': value})
1274
+
1275
+ @property
1276
+ def enable_gre_encapsulation(self) -> str:
1277
+ return self._enable_gre_encapsulation
1278
+
1279
+ @enable_gre_encapsulation.setter
1280
+ def enable_gre_encapsulation(self, value: str) -> None:
1281
+ if value not in self.yes_no:
1282
+ raise ValueError("enable_gre_encapsulation must be either 'yes' or 'no'.")
1283
+ self._enable_gre_encapsulation = value
1284
+ self.entry.update({'enable-gre-encapsulation': value})
1285
+
1286
+ @property
1287
+ def anti_replay_window(self):
1288
+ return self._anti_replay_window
1289
+
1290
+ @anti_replay_window.setter
1291
+ def anti_replay_window(self, value: str) -> None:
1292
+ if value not in ["64", "128", "256", "512", "1024", "2048", "4096"]:
1293
+ raise ValueError(f"anti_replay_window must be one of {self.valid_anti_replay_window_values}.")
1294
+ self._anti_replay_window = value
1295
+ self.entry.update({'anti-replay-window': value})
1296
+
1297
+ @property
1298
+ def tunnel_interface(self):
1299
+ return self._tunnel_interface
1300
+
1301
+ @tunnel_interface.setter
1302
+ def tunnel_interface(self, value: str) -> None:
1303
+ if not isinstance(value, str):
1304
+ raise TypeError("tunnel_interface must be a string.")
1305
+
1306
+ if not value.startswith('tunnel.'):
1307
+ raise ValueError("tunnel_interface must start with 'tunnel.'")
1308
+
1309
+ self._tunnel_interface = value
1310
+ self.entry.update({'tunnel-interface': value})
1311
+
1312
+ class IKEGatewayNetworkProfiles(Network):
1313
+ def __init__(self, PANDevice: Panorama | Firewall, **kwargs):
1314
+ super().__init__(PANDevice, max_name_length=64, **kwargs)
1315
+ self._disabled: str = kwargs.get('disabled')
1316
+ self._comment: str = kwargs.get('comment')
1317
+ self._ipv6: str = kwargs.get('ipv6')
1318
+ self._peer_address: dict = kwargs.get('peer_address')
1319
+ self._local_address: dict = kwargs.get('local_address')
1320
+ self._peer_id: dict = kwargs.get('peer_id')
1321
+ self._local_id: dict = kwargs.get('local_id')
1322
+ self._authentication: dict = kwargs.get('authentication')
1323
+ self._protocol: dict = kwargs.get('protocol')
1324
+ self._protocol_common: dict = kwargs.get('protocol_common')
1325
+
1326
+ @property
1327
+ def comment(self):
1328
+ return self._comment
1329
+
1330
+ @comment.setter
1331
+ def comment(self, value: str) -> None:
1332
+ if not isinstance(value, str):
1333
+ raise TypeError("comment must be a string.")
1334
+
1335
+ if len(value) > 1023:
1336
+ raise ValueError("comment cannot be longer than 1023 characters.")
1337
+
1338
+ self._comment = value
1339
+ self.entry.update({'comment': value})
1340
+
1341
+ @property
1342
+ def disabled(self) -> str:
1343
+ return self._disabled
1344
+
1345
+ @disabled.setter
1346
+ def disabled(self, value: str) -> None:
1347
+ if value not in self.yes_no:
1348
+ raise ValueError("disabled must be either 'yes' or 'no'.")
1349
+ self._disabled = value
1350
+ self.entry.update({'disabled': value})
1351
+
1352
+ @property
1353
+ def ipv6(self) -> str:
1354
+ return self._ipv6
1355
+
1356
+ @ipv6.setter
1357
+ def ipv6(self, value: str) -> None:
1358
+ if value not in self.yes_no:
1359
+ raise ValueError("disabled must be either 'yes' or 'no'.")
1360
+ self._ipv6 = value
1361
+ self.entry.update({'ipv6': value})
1362
+
1363
+ @property
1364
+ def peer_address(self) -> dict:
1365
+ return self._peer_address
1366
+
1367
+ @peer_address.setter
1368
+ def peer_address(self, value: dict) -> None:
1369
+ if not isinstance(value, dict):
1370
+ raise TypeError("peer_address must be a dictionary.")
1371
+
1372
+ if len(value) != 1:
1373
+ raise ValueError("peer_address dictionary must contain exactly one key.")
1374
+
1375
+ key = next(iter(value)) # Get the first key to determine the type.
1376
+
1377
+ if key == 'ip':
1378
+ if not isinstance(value[key], str):
1379
+ raise ValueError("The 'ip' key must be a string.")
1380
+ if self._validate_ip_address(value[key]) is False:
1381
+ raise ValueError("The 'ip' key must be a valid IP address.")
1382
+
1383
+ elif key == 'fqdn':
1384
+ if not isinstance(value[key], str) and len(value[key]) <= 255:
1385
+ raise ValueError("The 'fqdn' key must be a string <= 255 characters.")
1386
+
1387
+ elif key == 'dynamic':
1388
+ if not isinstance(value[key], dict) or len(value[key]) > 0:
1389
+ raise ValueError("The 'dynamic' key must be an empty dict.")
1390
+ self._peer_address = value
1391
+ self.entry.update({'peer-address': value})
1392
+
1393
+ @property
1394
+ def local_address(self) -> dict:
1395
+ return self._local_address
1396
+
1397
+ @local_address.setter
1398
+ def local_address(self, value: dict) -> None:
1399
+ if not isinstance(value, dict):
1400
+ raise TypeError("local_address must be a dictionary.")
1401
+
1402
+ if len(value) != 2:
1403
+ raise ValueError(
1404
+ "local_address dictionary must contain exactly two keys: 'ip'/'floating-ip' and 'interface'.")
1405
+
1406
+ ip_keys = ['ip', 'floating-ip']
1407
+ if not any(key in value for key in ip_keys):
1408
+ raise ValueError("local_address dictionary must contain either 'ip' or 'floating-ip' key.")
1409
+
1410
+ if 'ip' in value and 'floating-ip' in value:
1411
+ raise ValueError("local_address dictionary must not contain both 'ip' and 'floating-ip' keys.")
1412
+
1413
+ if 'interface' not in value:
1414
+ raise ValueError("local_address dictionary must contain an 'interface' key.")
1415
+
1416
+ ip_key = 'ip' if 'ip' in value else 'floating-ip'
1417
+ if not isinstance(value[ip_key], str) or not self._validate_interface_address(value[ip_key]):
1418
+ raise ValueError(f"The '{ip_key}' key must be a valid IP address string.")
1419
+
1420
+ if not isinstance(value['interface'], str):
1421
+ raise ValueError("The 'interface' key must be a string.")
1422
+
1423
+ self._local_address = value
1424
+ self.entry.update({'local-address': value})
1425
+
1426
+ @property
1427
+ def peer_id(self) -> dict:
1428
+ return self._peer_id
1429
+
1430
+ @peer_id.setter
1431
+ def peer_id(self, value: dict) -> None:
1432
+ if not isinstance(value, dict):
1433
+ raise TypeError("peer_id must be a dictionary.")
1434
+
1435
+ required_keys = {'type'}
1436
+ if not required_keys.issubset(value.keys()):
1437
+ raise ValueError(f"peer_id dictionary must contain keys: {required_keys}")
1438
+
1439
+ if not isinstance(value['type'], str):
1440
+ raise TypeError("'type' key in peer_id must be a string.")
1441
+
1442
+ if not isinstance(value['id'], str) or not 1 <= len(value['id']) <= 1024:
1443
+ raise ValueError(
1444
+ "'id' key in peer_id must be between 1 to 1024 characters long.")
1445
+
1446
+ if value.get('matching'):
1447
+ if value['matching'] not in ['exact', 'wildcard']:
1448
+ raise ValueError("'matching' key in peer_id must be either 'exact' or 'wildcard'.")
1449
+
1450
+ self._peer_id = value
1451
+ self.entry.update({'peer-id': value})
1452
+
1453
+ @property
1454
+ def local_id(self) -> dict:
1455
+ return self._local_id
1456
+
1457
+ @local_id.setter
1458
+ def local_id(self, value: dict) -> None:
1459
+ if not isinstance(value, dict):
1460
+ raise TypeError("local_id must be a dictionary.")
1461
+
1462
+ required_keys = {'type'}
1463
+ if not required_keys.issubset(value.keys()):
1464
+ raise ValueError(f"local_id dictionary must contain keys: {required_keys}")
1465
+
1466
+ if not isinstance(value['type'], str):
1467
+ raise TypeError("'type' key in local_id must be a string.")
1468
+
1469
+ if not isinstance(value['id'], str) or not 1 <= len(value['id']) <= 1024:
1470
+ raise ValueError(
1471
+ "'id' key in local_id must be a string that matches the specified pattern and is between 1 to 1024 characters long.")
1472
+
1473
+ self._local_id = value
1474
+ self.entry.update({'local-id': value})
1475
+
1476
+ @property
1477
+ def authentication(self) -> dict:
1478
+ return self._authentication
1479
+
1480
+ @authentication.setter
1481
+ def authentication(self, value: dict) -> None:
1482
+ if not isinstance(value, dict):
1483
+ raise TypeError("Authentication must be a dictionary.")
1484
+
1485
+ keys = value.keys()
1486
+ if not ('pre-shared-key' in keys) ^ ('certificate' in keys):
1487
+ raise ValueError(
1488
+ "Authentication dictionary must contain either 'pre-shared-key' or 'certificate', but not both.")
1489
+
1490
+ if 'pre-shared-key' in keys:
1491
+ psk = value['pre-shared-key']
1492
+ if not isinstance(psk, dict):
1493
+ raise TypeError("'pre-shared-key' must be a dictionary.")
1494
+
1495
+ if 'key' not in psk:
1496
+ raise ValueError("'pre-shared-key' dictionary must contain a 'key'.")
1497
+
1498
+ if not isinstance(psk['key'], str) or len(psk['key']) > 255:
1499
+ raise ValueError(
1500
+ "The 'key' in 'pre-shared-key' must be a string with a maximum length of 255 characters.")
1501
+
1502
+ # For 'certificate', more validation logic will be added later as its structure is defined.
1503
+ if 'certificate' in keys:
1504
+ # Placeholder for future certificate validation
1505
+ pass
1506
+
1507
+ self._authentication = value
1508
+ self.entry.update({'authentication': value})
1509
+
1510
+ @property
1511
+ def protocol(self) -> dict:
1512
+ return self._protocol
1513
+
1514
+ @protocol.setter
1515
+ def protocol(self, value: dict) -> None:
1516
+ if not value or not isinstance(value, dict):
1517
+ raise TypeError("Protocol must be a dictionary.")
1518
+
1519
+ # Validate 'version'
1520
+ version = value.get('version', 'ikev1')
1521
+ if version not in ["ikev1", "ikev2", "ikev2-preferred"]:
1522
+ raise ValueError("The 'version' key must be one of 'ikev1', 'ikev2', 'ikev2-preferred'.")
1523
+ value['version'] = version
1524
+
1525
+ # Validate 'ikev1'
1526
+ ikev1 = value.get('ikev1')
1527
+ if ikev1:
1528
+ if not isinstance(ikev1, dict):
1529
+ raise TypeError("'ikev1' must be a dictionary.")
1530
+
1531
+ exchange_mode = ikev1.get('exchange-mode', 'auto')
1532
+ if exchange_mode not in ['auto', 'main', 'aggressive']:
1533
+ raise ValueError("'exchange-mode' must be one of 'auto', 'main', 'aggressive'.")
1534
+ ikev1['exchange-mode'] = exchange_mode
1535
+
1536
+ ike_crypto_profile = ikev1.get('ike-crypto-profile', 'default')
1537
+ if not isinstance(ike_crypto_profile, str):
1538
+ raise TypeError("'ike-crypto-profile' must be a string.")
1539
+ ikev1['ike-crypto-profile'] = ike_crypto_profile
1540
+
1541
+ self.validate_dpd(ikev1.get('dpd', {'enable': 'yes', 'interval': 5}), include_retry=True)
1542
+ value['ikev1'] = ikev1
1543
+
1544
+ # Validate 'ikev2'
1545
+ ikev2 = value.get('ikev2')
1546
+ if ikev2:
1547
+ if not isinstance(ikev2, dict):
1548
+ raise TypeError("'ikev2' must be a dictionary.")
1549
+
1550
+ ikev2_crypto_profile = ikev2.get('ike-crypto-profile', 'default')
1551
+ if not isinstance(ikev2_crypto_profile, str):
1552
+ raise TypeError("'ike-crypto-profile' for ikev2 must be a string.")
1553
+ ikev2['ike-crypto-profile'] = ikev2_crypto_profile
1554
+
1555
+ required_cookie = ikev2.get('required-cookie')
1556
+ if required_cookie:
1557
+ if required_cookie not in self.yes_no:
1558
+ raise ValueError("'required-cookie' must be 'yes' or 'no'.")
1559
+ ikev2['required-cookie'] = required_cookie
1560
+
1561
+ self.validate_dpd(ikev2.get('dpd', {'enable': 'yes', 'interval': 5}), include_retry=False)
1562
+ ikev2['dpd'] = ikev2.get('dpd', {'enable': 'yes'})
1563
+ value['ikev2'] = ikev2
1564
+
1565
+ self._protocol = value
1566
+ self.entry.update({'protocol': value})
1567
+
1568
+ def validate_dpd(self, dpd_dict: dict, include_retry: bool) -> None:
1569
+ if not isinstance(dpd_dict, dict):
1570
+ raise TypeError("DPD must be a dictionary.")
1571
+
1572
+ dpd_keys = ['enable', 'interval'] + (['retry'] if include_retry else [])
1573
+ if not all(key in dpd_dict for key in dpd_keys):
1574
+ raise ValueError(f"DPD dictionary must contain the keys: {dpd_keys}")
1575
+
1576
+ if dpd_dict['enable']:
1577
+ if dpd_dict['enable'] not in self.yes_no:
1578
+ raise ValueError("'enable' must be 'yes' or 'no'.")
1579
+ else:
1580
+ dpd_dict['enable'] = 'yes'
1581
+
1582
+ if dpd_dict['interval']:
1583
+ if not (2 <= dpd_dict['interval'] <= 100 and isinstance(dpd_dict['interval'], int)):
1584
+ raise ValueError("'interval' must be an integer between 2 and 100.")
1585
+ else:
1586
+ dpd_dict['interval'] = 5
1587
+
1588
+ if include_retry:
1589
+ if dpd_dict['retry']:
1590
+ if include_retry and not (2 <= dpd_dict['retry'] <= 100 and isinstance(dpd_dict['retry'], int)):
1591
+ raise ValueError("'retry' must be an integer between 2 and 100.")
1592
+ else:
1593
+ dpd_dict['retry'] = 5
1594
+
1595
+ @property
1596
+ def protocol_common(self) -> dict:
1597
+ return self._protocol_common
1598
+
1599
+ @protocol_common.setter
1600
+ def protocol_common(self, value: dict) -> None:
1601
+ if not value or not isinstance(value, dict):
1602
+ raise TypeError("protocol_common must be a dictionary.")
1603
+
1604
+ # Validate 'nat-traversal'
1605
+ nat_traversal = value.get('nat-traversal')
1606
+ if not isinstance(nat_traversal, dict):
1607
+ raise TypeError("'nat-traversal' must be a dictionary.")
1608
+
1609
+ nat_traversal['enable'] = nat_traversal.get('enable', 'no')
1610
+ if nat_traversal['enable'] not in self.yes_no:
1611
+ raise ValueError("'enable' in 'nat-traversal' must be 'yes' or 'no'.")
1612
+
1613
+ if nat_traversal.get('keep-alive-interval'):
1614
+ nat_traversal['keep-alive-interval'] = int(nat_traversal.get('keep-alive-interval'))
1615
+ if not 10 <= nat_traversal['keep-alive-interval'] <= 3600:
1616
+ raise ValueError("'keep-alive-interval' in 'nat-traversal' must be between 10 and 3600.")
1617
+
1618
+ if nat_traversal.get('udp-checksum-enable'):
1619
+ nat_traversal['udp-checksum-enable'] = nat_traversal.get('udp-checksum-enable', 'no')
1620
+ if nat_traversal['udp-checksum-enable'] not in self.yes_no:
1621
+ raise ValueError("'udp-checksum-enable' in 'nat-traversal' must be 'yes' or 'no'.")
1622
+
1623
+ value['nat-traversal'] = nat_traversal
1624
+
1625
+ # Validate 'passive-mode'
1626
+ value['passive-mode'] = value.get('passive-mode', 'no')
1627
+ if value['passive-mode'] not in self.yes_no:
1628
+ raise ValueError("'passive-mode' must be 'yes' or 'no'.")
1629
+
1630
+ # Validate 'fragmentation'
1631
+ fragmentation = value.get('fragmentation', {'enable': 'no'})
1632
+ if fragmentation:
1633
+ if not isinstance(fragmentation, dict):
1634
+ raise TypeError("'fragmentation' must be a dictionary.")
1635
+
1636
+ fragmentation['enable'] = fragmentation.get('enable', 'no')
1637
+ if fragmentation['enable'] not in self.yes_no:
1638
+ raise ValueError("'enable' in 'fragmentation' must be 'yes' or 'no'.")
1639
+
1640
+ value['fragmentation'] = fragmentation
1641
+
1642
+ self._protocol_common = value
1643
+ self.entry.update({'protocol-common': value})
1644
+
1645
+ class VirtualRouters(Network):
1646
+ """
1647
+ Initialize a VirtualRouters instance.
1648
+
1649
+ :param PANDevice: A Panorama or Firewall instance.
1650
+ :param kwargs: Additional keyword arguments including interface, routing_table, multicast,
1651
+ protocol, admin_dists, and ecmp, which are dictionaries representing different
1652
+ aspects of the virtual router's configuration.
1653
+ """
1654
+ def __init__(self, PANDevice: Panorama | Firewall, **kwargs):
1655
+ super().__init__(PANDevice, max_name_length=32, **kwargs)
1656
+ self.interface: dict = kwargs.get('interface')
1657
+ # self.routing_table: dict = kwargs.get('routing_table')
1658
+ # self.multicast: dict = kwargs.get('multicast')
1659
+ # self.protocol: dict = kwargs.get('protocol')
1660
+ self._admin_dists = {
1661
+ 'static': 10,
1662
+ 'static-ipv6': 10,
1663
+ 'ospf-int': 30,
1664
+ 'ospf-ext': 110,
1665
+ 'ospfv3-int': 30,
1666
+ 'ospfv3-ext': 110,
1667
+ 'ibgp': 200,
1668
+ 'ebgp': 20,
1669
+ 'rip': 120
1670
+ }
1671
+ self.admin_dists = kwargs.get('admin_dists')
1672
+ # self.ecmp: dict = kwargs.get('ecmp')
1673
+
1674
+ @property
1675
+ def admin_dists(self) -> dict:
1676
+ return self._admin_dists
1677
+
1678
+ @admin_dists.setter
1679
+ def admin_dists(self, value: dict):
1680
+ if value:
1681
+ if not isinstance(value, dict):
1682
+ raise TypeError("admin_dists must be a dictionary.")
1683
+
1684
+ expected_keys = {
1685
+ 'static', 'static-ipv6', 'ospf-int', 'ospf-ext',
1686
+ 'ospfv3-int', 'ospfv3-ext', 'ibgp', 'ebgp', 'rip'
1687
+ }
1688
+ # Check for any unexpected keys
1689
+ extra_keys = value.keys() - expected_keys
1690
+ if extra_keys:
1691
+ raise KeyError(f"Unexpected keys in admin_dists: {extra_keys}")
1692
+
1693
+ for key, default_value in self._admin_dists.items():
1694
+ if key in value:
1695
+ if not isinstance(value[key], int):
1696
+ raise TypeError(f"Value for '{key}' must be an integer.")
1697
+ if not 10 <= value[key] <= 240:
1698
+ raise ValueError(f"Value for '{key}' must be between 10 and 240.")
1699
+ self._admin_dists[key] = value[key]
1700
+ else:
1701
+ self._admin_dists[key] = default_value
1702
+ self.entry.update({'admin-dists': self._admin_dists})
1703
+
1704
+ @property
1705
+ def interface(self) -> dict:
1706
+ return self._interface
1707
+
1708
+ @interface.setter
1709
+ def interface(self, value: dict):
1710
+ if value:
1711
+ if not isinstance(value, dict):
1712
+ raise TypeError("Interface must be a dictionary.")
1713
+ if 'member' not in value:
1714
+ raise ValueError("The 'member' key is missing in the interface dictionary.")
1715
+ if not isinstance(value['member'], list) or not all(isinstance(item, str) for item in value['member']):
1716
+ raise ValueError("The 'member' key must be associated with a list of strings.")
1717
+ self._interface = value
1718
+ self.entry.update({'interface': self._interface})
1719
+
1720
+ class DNSProxies(Network):
1721
+ def __init__(self, PANDevice: Panorama | Firewall, **kwargs):
1722
+ super().__init__(PANDevice, max_name_length=32, **kwargs)