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/ApplicationHelper.py +220 -0
- pypanrestv2/Base.py +1772 -0
- pypanrestv2/Device.py +21 -0
- pypanrestv2/Exceptions.py +11 -0
- pypanrestv2/Network.py +1722 -0
- pypanrestv2/Objects.py +1428 -0
- pypanrestv2/Panorama.py +426 -0
- pypanrestv2/Policies.py +755 -0
- pypanrestv2/XDR.py +299 -0
- pypanrestv2/__init__.py +4 -0
- pypanrestv2-2.1.0.dist-info/METADATA +209 -0
- pypanrestv2-2.1.0.dist-info/RECORD +13 -0
- pypanrestv2-2.1.0.dist-info/WHEEL +4 -0
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)
|