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.
@@ -0,0 +1,426 @@
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
+ logger = logging.getLogger(__name__)
20
+ requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
21
+
22
+ class PanoramaTab(Base, PAN):
23
+ def __init__(self, PANDevice, **kwargs):
24
+ Base.__init__(self, PANDevice, **kwargs)
25
+ PAN.__init__(self, PANDevice.base_url, api_key=PANDevice.api_key)
26
+ self.endpoint: str = 'Panorama'
27
+
28
+ def _build_params(self) -> Dict[str, str]:
29
+ """
30
+ Builds the parameter dictionary for the API request based on the object's state.
31
+
32
+ Returns:
33
+ Dict[str, str]: The parameters for the API request.
34
+ """
35
+ params = {}
36
+ if self.name:
37
+ params['name'] = self.name
38
+
39
+ return params
40
+
41
+ class Templates(PanoramaTab):
42
+ def __init__(self, PANDevice, **kwargs):
43
+ super().__init__(PANDevice, max_description_length=255, max_name_length=63, **kwargs)
44
+ self.PANDevice = PANDevice
45
+ self.settings = kwargs.get('settings', 'vsys1')
46
+
47
+ @property
48
+ def settings(self):
49
+ return self._settings
50
+
51
+ @settings.setter
52
+ def settings(self, value):
53
+ if isinstance(value, dict):
54
+ if 'default-vsys' in value:
55
+ self._settings = value
56
+ self.entry.update({'settings': value})
57
+ return
58
+ elif isinstance(value, str):
59
+ if not value.startswith('vsys'):
60
+ raise ValueError(f'The attribute settings must be a vsys.')
61
+ self._settings = {'default-vsys': value}
62
+ self.entry.update({'settings': {'default-vsys': value}})
63
+ return
64
+ else:
65
+ raise TypeError(f'The attribute settings must be of type str, not {type(value)}.')
66
+
67
+
68
+ class TemplateStacks(PanoramaTab):
69
+ variable_types = ['ip-netmask', 'ip-range', 'fqdn', 'group-id', 'device-priority', 'device-id', 'interface',
70
+ 'as-number', 'qos-profiles', 'egress-max', 'link-tag']
71
+
72
+ def __init__(self, PANDevice, **kwargs):
73
+ super().__init__(PANDevice, max_description_length=255, max_name_length=63, **kwargs)
74
+ self.templates: Dict = {'member': []}
75
+ self.devices: Dict = {'entry': []}
76
+ self.variable: Dict = {'entry': []}
77
+
78
+ def add_device(self, name: str, variable: dict = None) -> bool:
79
+ """
80
+ Adds a device (and its associated variable, if any) to the 'devices' entry.
81
+
82
+ This method validates the structure of the given variable (if provided), creates a new
83
+ device entry containing the specified name (and variable if applicable), and appends it
84
+ to the devices list. It ensures that the updated devices entry is assigned back to
85
+ the main 'entry' attribute.
86
+
87
+ Args:
88
+ name: The name of the device to be added.
89
+ variable: (Optional) The data or configuration associated with the device.
90
+
91
+ Returns:
92
+ bool: True if the device was successfully added, False otherwise.
93
+ """
94
+ # Check if a variable is provided
95
+ if variable:
96
+ if self.validate_variable_structure(variable):
97
+ logger.debug(f"Adding device {name} with variables to template stack {self.name}")
98
+ device_entry = {'@name': name, 'variable': variable}
99
+ else:
100
+ logger.debug(f"Invalid variable structure for device {name}. Not adding.")
101
+ logger.debug(f"Variables provided: {variable}")
102
+ return False
103
+ else:
104
+ # Handle case where no variables are provided
105
+ logger.debug(f"Adding device {name} without variables to template stack {self.name}")
106
+ device_entry = {'@name': name}
107
+
108
+ # Add the device entry to the devices list
109
+ self.devices['entry'].append(device_entry)
110
+ self.entry['devices'] = self.devices
111
+
112
+ return True
113
+
114
+ def update_variable(self, name: str, variable_type: str, variable_value: str):
115
+ if variable_type in self.variable_types:
116
+ variable_entry = {'@name': name, 'type': {variable_type: variable_value}}
117
+ self.variable['entry'].append(variable_entry)
118
+ self.entry['variable'] = self.variable
119
+
120
+ def update_device_variable(self, device_name: str, variable_name: str, variable_type: str,
121
+ variable_value: str) -> None:
122
+ if variable_type in self.variable_types:
123
+ # Find the device by name
124
+ for device_entry in self.devices['entry']:
125
+ if device_entry['@name'] == device_name:
126
+ # Find the variable by name within the device's 'variable' list
127
+ variable_found = False
128
+ for var_entry in device_entry['variable']['entry']:
129
+ if var_entry['@name'] == variable_name:
130
+ # Update existing variable
131
+ var_entry['type'] = {variable_type: variable_value}
132
+ variable_found = True
133
+ break
134
+
135
+ if not variable_found:
136
+ # Add new variable if not found
137
+ device_entry['variable']['entry'].append({
138
+ '@name': variable_name,
139
+ 'type': {variable_type: variable_value}
140
+ })
141
+ break
142
+
143
+ def add_template_member(self, member):
144
+ self.templates['member'].append(member)
145
+ self.entry['templates'] = self.templates
146
+
147
+ def get_variables_from_device(self) -> list:
148
+ """
149
+ Extracts and processes variables from a given device within the current object's devices list.
150
+ This function retrieves the first device from the devices list and, if that device has variables,
151
+ formats and appends them to a list. If no devices are present initially, it attempts to refresh the
152
+ device list before proceeding.
153
+
154
+ Returns:
155
+ list: A list of dictionaries where each dictionary represents a variable with its name and type.
156
+
157
+ """
158
+ def extract_variables():
159
+ variables = []
160
+ device = self.devices['entry'][0] # Fetch the first device
161
+ if 'variable' in device:
162
+ for var in device['variable']['entry']:
163
+ # Extract the type key and set its value to None
164
+ key_name = next(iter(var['type'])) # Get the first (only) key in the type dictionary
165
+ variables.append({
166
+ '@name': var['@name'],
167
+ 'type': {key_name: None}
168
+ })
169
+ self.variable['entry'] = variables
170
+ return variables
171
+
172
+ if self.devices['entry']:
173
+ return extract_variables()
174
+ else:
175
+ self.refresh()
176
+ if not self.devices['entry']:
177
+ logger.warning(f'No devices found in {self.name} template stack.')
178
+ return []
179
+ else:
180
+ return extract_variables()
181
+
182
+ @property
183
+ def templates(self):
184
+ return self._templates
185
+
186
+ @templates.setter
187
+ def templates(self, value: Dict):
188
+ if self.validate_templates_structure(value):
189
+ self._templates = value
190
+ self.entry['templates'] = value
191
+ else:
192
+ raise ValueError("Invalid templates structure")
193
+
194
+ @staticmethod
195
+ def validate_templates_structure(templates: Dict) -> bool:
196
+ # Validate the structure: {'member': []}
197
+ if not isinstance(templates, dict) or 'member' not in templates:
198
+ return False
199
+ if not isinstance(templates['member'], list):
200
+ return False
201
+ return True
202
+
203
+ @property
204
+ def variable(self):
205
+ return self._variable
206
+
207
+ @variable.setter
208
+ def variable(self, value: Dict):
209
+ if self.validate_variable_structure(value):
210
+ self._variable = value
211
+ self.entry['variables'] = value
212
+ else:
213
+ raise ValueError("Invalid variable structure")
214
+
215
+ def validate_variable_structure(self, variable: Dict) -> bool:
216
+ if not isinstance(variable, dict) or 'entry' not in variable:
217
+ logger.debug(f'Variable is not a Dictionary or entry not in variable.')
218
+ return False
219
+ if not isinstance(variable['entry'], list):
220
+ logger.debug(f'Variable entry is not a list.')
221
+ return False
222
+ for item in variable['entry']:
223
+ if not isinstance(item, dict) or '@name' not in item or 'type' not in item:
224
+ logger.debug(f'Missing keys @name and type. You provided {item}')
225
+ return False
226
+ if not isinstance(item['type'], dict) or len(item['type']) != 1:
227
+ logger.debug(f'Key type must be a dictionary with one key. You provided {item["type"]}.')
228
+ return False
229
+ type_key = next(iter(item['type']))
230
+ if type_key not in self.variable_types or not isinstance(item['type'][type_key], str):
231
+ logger.debug(f"Key type is not valid. For variable {item['@name']}, you provided {type_key} "
232
+ f"as type {type(item['type'][type_key])}. Value is {item['type'][type_key]}.")
233
+ return False
234
+ return True
235
+
236
+ @property
237
+ def devices(self):
238
+ return self._devices
239
+
240
+ @devices.setter
241
+ def devices(self, value: Dict):
242
+ if self.validate_devices_structure(value):
243
+ self._devices = value
244
+ self.entry['devices'] = value
245
+ else:
246
+ raise ValueError("Invalid devices structure")
247
+
248
+ def validate_devices_structure(self, devices: Dict) -> bool:
249
+ if not isinstance(devices, dict) or 'entry' not in devices:
250
+ return False
251
+ if not isinstance(devices['entry'], list):
252
+ return False
253
+ for item in devices['entry']:
254
+ if not isinstance(item, dict) or '@name' not in item or 'variable' not in item:
255
+ return False
256
+ if not self.validate_variable_structure(item['variable']):
257
+ return False
258
+ return True
259
+
260
+ def set_variable(self, device_name, variable_name, variable_value, variable_type, variable_descriotion=None):
261
+ # xpath = f"/config/devices/entry[@name='localhost.localdomain']/template-stack/entry[@name='{self.name}']/devices/entry[@name='{device_name}']/variable"
262
+ # element = f"<entry name={variable_name}><type><{variable_type}><{variable_value}></{variable_type}></type></entry>"
263
+ # return self.PANDevice.set_xml(xpath, element)
264
+ pass
265
+
266
+ def get_template_stack(self, template_name: str) -> List[str]:
267
+ """
268
+ Retrieves a list of template stacks containing the specified template name.
269
+
270
+ :param template_name: The name of the template to search for in template stacks.
271
+ :return: A list of template stacks that include the specified template.
272
+ """
273
+ template_stacks = TemplateStacks(self.PANDevice)
274
+ return [
275
+ template_stack.get('@name')
276
+ for template_stack in template_stacks.get()
277
+ if template_name in template_stack.get('templates', {}).get('member', [])
278
+ ]
279
+
280
+ class DeviceGroups(PanoramaTab):
281
+
282
+ def __init__(self, PANDevice, **kwargs):
283
+ super().__init__(PANDevice, max_description_length=255, max_name_length=63, **kwargs)
284
+ self.authorization_code = None
285
+ self.to_sw_version = 'None'
286
+ self.reference_templates = kwargs.get('reference_templates')
287
+ self.entry.update({'devices': {'entry': []}})
288
+
289
+ @property
290
+ def reference_templates(self):
291
+ return self._reference_templates
292
+
293
+ @reference_templates.setter
294
+ def reference_templates(self, value):
295
+ if value:
296
+ if not isinstance(value, list):
297
+ raise TypeError(f'Attribute {sys._getframe().f_code.co_name} must be of type list.')
298
+ for member in value:
299
+ if not isinstance(member, str):
300
+ raise TypeError(f'The items in {sys._getframe().f_code.co_name} list must be of type str.')
301
+ # Check to see if the template exists on the device
302
+ template = Templates(self.PANDevice, name=member)
303
+ if not template.get():
304
+ # try to see if is a Template stack instead
305
+ templatestack = TemplateStacks(self.PANDevice, name=member)
306
+ if not templatestack.get():
307
+ raise ValueError(f'There is no such template or template stack called {memer} on {self.PANDevice.IP}')
308
+ self._reference_templates = value
309
+ self.entry.update({'reference-templates': {'member': value}})
310
+ else:
311
+ self._reference_templates = None
312
+
313
+ @property
314
+ def authorization_code(self):
315
+ return self._authorization_code
316
+
317
+ @authorization_code.setter
318
+ def authorization_code(self, val):
319
+ if val:
320
+ self.Valid_Serial(val, 63)
321
+ self._authorization_code = val
322
+ self.entry.update({'authorization-code': val})
323
+ else:
324
+ self._authorization_code = None
325
+
326
+ @property
327
+ def to_sw_version(self):
328
+ return self._to_sw_version
329
+
330
+ @to_sw_version.setter
331
+ def to_sw_version(self, val):
332
+ if val:
333
+ if not isinstance(val, str):
334
+ raise TypeError(f'Attribute {sys._getframe().f_code.co_name} must be of type str.')
335
+ self._to_sw_version = val
336
+ self.entry.update({'to-sw-version': val})
337
+ else:
338
+ self._to_sw_version = 'None'
339
+
340
+ def getParentDG(self) -> Optional[str]:
341
+ """
342
+ Returns the parent device group for a given child device group using the XML API.
343
+
344
+ Returns:
345
+ Optional[str]: The name of the parent device group if found, 'shared' if the device group
346
+ is top-level, or None if an error occurs or the parent cannot be determined.
347
+ """
348
+ URL = f'{self.PANDevice.base_url}/api/'
349
+ xpath = f'/config/readonly/devices/entry[@name="localhost.localdomain"]/device-group/entry[@name="{self.name}"]/parent-dg'
350
+ params = {
351
+ 'type': 'config',
352
+ 'action': 'get',
353
+ 'xpath': xpath,
354
+ 'key': self.PANDevice.API_KEY # Assuming API_KEY is required for authentication
355
+ }
356
+
357
+ try:
358
+ response = self.session.get(URL, params=params)
359
+ response.raise_for_status() # Check for HTTP errors
360
+
361
+ result = xmltodict.parse(response.text)
362
+ status = result.get('response', {}).get('@status')
363
+
364
+ if status == 'success':
365
+ parent_dg = result.get('response', {}).get('result', {}).get('parent-dg')
366
+ return parent_dg if parent_dg is not None else 'shared'
367
+ else:
368
+ logger.error(f'Could not get parent DG for {self.name}: {result.get("response", {}).get("msg")}')
369
+ return None
370
+ except requests.exceptions.RequestException as e:
371
+ logger.error(f'HTTP error occurred: {e}')
372
+ except Exception as e:
373
+ logger.error(f'Error parsing response: {e}')
374
+ return None
375
+
376
+ def add_device(self, serial: str) -> None:
377
+ """
378
+ Adds a device by serial number to the device group.
379
+
380
+ Parameters:
381
+ - serial (str): Serial number of the firewall to add to this group.
382
+
383
+ Raises:
384
+ - ValueError: If the serial number is invalid or empty.
385
+ - AttributeError: If the devices list or entry dictionary is not properly initialized.
386
+ """
387
+ if not serial:
388
+ raise ValueError("Serial number cannot be empty.")
389
+
390
+ try:
391
+ # Ensure 'entry' is a dictionary as expected.
392
+ if 'devices' not in self.entry or 'entry' not in self.entry['devices'] or not isinstance(
393
+ self.entry['devices']['entry'], list):
394
+ raise AttributeError("'entry' dictionary is not properly initialized.")
395
+
396
+ # Add the serial number to the devices list and entry dictionary.
397
+ self.entry['devices']['entry'].append({'@name': serial})
398
+ except AttributeError as e:
399
+ logger.error(f"Failed to add device: {e}")
400
+
401
+ def add_reference_template(self, template_name: str) -> None:
402
+ """
403
+ Adds a new reference template to the device group.
404
+
405
+ Parameters:
406
+ - template_name (str): The name of the template to add.
407
+
408
+ Raises:
409
+ - ValueError: If the template_name is empty.
410
+ - TypeError: If template_name is not a string.
411
+ """
412
+ if not isinstance(template_name, str):
413
+ raise TypeError("template_name must be a string.")
414
+ if not template_name:
415
+ raise ValueError("template_name cannot be empty.")
416
+
417
+ # Initialize the 'reference-templates' structure if it doesn't exist
418
+ if 'reference-templates' not in self.entry:
419
+ self.entry['reference-templates'] = {'member': []}
420
+
421
+ # Check to ensure the template_name does not already exist to prevent duplicates
422
+ if template_name not in self.entry['reference-templates']['member']:
423
+ self.entry['reference-templates']['member'].append(template_name)
424
+ else:
425
+ # Log or handle the case where the template already exists if needed
426
+ logger.warning(f"Template '{template_name}' already exists in the reference templates.")