PyPANRestV2 2.1.0__tar.gz → 2.1.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: PyPANRestV2
3
- Version: 2.1.0
3
+ Version: 2.1.2
4
4
  Summary: Python tools for interacting with Palo Alto Networks REST API.
5
5
  License: MIT
6
6
  Author: Mark Rzepa
@@ -10,6 +10,8 @@ Classifier: License :: OSI Approved :: MIT License
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
13
15
  Requires-Dist: dnspython (>=2.6.1)
14
16
  Requires-Dist: icecream (>=2.1.3)
15
17
  Requires-Dist: pycountry (>=23.12.11)
@@ -30,14 +32,22 @@ Description-Content-Type: text/markdown
30
32
 
31
33
  - **High-Level Abstraction**: Simplifies interaction with the Palo Alto Networks API.
32
34
  - **Support for Firewalls and Panorama**: Manage both individual firewalls and Panorama devices.
33
- - **REST API Integration**: Allows seamless communication with devices.
35
+ - **REST API Integration**: Allows seamless communication with devices using REST API.
36
+ - **XML API Support**: Handles XML API calls for configurations not yet available in REST API.
34
37
  - **Convenient Pythonic Objects**: Intuitive Python objects for interacting with specific sections of Palo Alto firewall configurations.
38
+ - **Error Handling**: Custom exceptions for better error management and troubleshooting.
35
39
 
36
40
  ---
37
41
 
38
42
  ## Installation
39
43
 
40
- To install `PyPanRestV2`, clone the repository and install it as a package:
44
+ You can install `PyPanRestV2` using pip:
45
+
46
+ ```bash
47
+ pip install pypanrestv2
48
+ ```
49
+
50
+ Alternatively, you can clone the repository and install it as a package for development:
41
51
 
42
52
  ```bash
43
53
  # Clone the repository
@@ -60,7 +70,7 @@ This will install the package and all required dependencies automatically. The `
60
70
  Start by importing the necessary classes from the library:
61
71
 
62
72
  ```python
63
- from pypantrestv2 import Firewall, Panorama
73
+ from pypanrestv2 import Firewall, Panorama
64
74
  ```
65
75
 
66
76
  ### Connect to a Firewall or Panorama Device
@@ -84,12 +94,12 @@ from pypanrestv2.Policies import SecurityRules
84
94
 
85
95
  # Create a new security rule
86
96
  security_rule = SecurityRules(firewall, name='allow_web')
87
- security_rule.source_zone = ['trust']
88
- security_rule.destination_zone = ['untrust']
89
- security_rule.source = ['any']
90
- security_rule.destination = ['any']
91
- security_rule.application = ['web-browsing']
92
- security_rule.service = ['application-default']
97
+ security_rule.from_ = {'member': ['trust']}
98
+ security_rule.to = {'member': ['untrust']}
99
+ security_rule.source = {'member': ['any']}
100
+ security_rule.destination = {'member': ['any']}
101
+ security_rule.application = {'member': ['web-browsing']}
102
+ security_rule.service = {'member': ['application-default']}
93
103
  security_rule.action = 'allow'
94
104
  security_rule.create()
95
105
 
@@ -114,16 +124,24 @@ address.create()
114
124
  all_addresses = Addresses.get_all(firewall)
115
125
  ```
116
126
 
117
- #### 3. Working with Panorama Device Groups
127
+ #### 3. Working with Panorama Policies and Rulebase
118
128
  ```python
119
129
  from pypanrestv2 import Panorama
130
+ from pypanrestv2.Policies import SecurityRules
120
131
 
121
132
  # Initialize Panorama connection
122
133
  panorama = Panorama(base_url='panorama.example.com', api_key='YOUR_API_KEY')
123
134
 
124
- # Add a device to a device group
125
- device_group = panorama.device_groups.get('Branch_Offices')
126
- device_group.add_device('serial123')
135
+ # Create a security rule in the pre-rulebase of a device group
136
+ security_rule = SecurityRules(panorama, name='allow_internal', rulebase='Pre')
137
+ security_rule.from_ = {'member': ['trust']}
138
+ security_rule.to = {'member': ['untrust']}
139
+ security_rule.source = {'member': ['any']}
140
+ security_rule.destination = {'member': ['any']}
141
+ security_rule.application = {'member': ['web-browsing']}
142
+ security_rule.service = {'member': ['application-default']}
143
+ security_rule.action = 'allow'
144
+ security_rule.create()
127
145
  ```
128
146
 
129
147
  ---
@@ -132,15 +150,22 @@ device_group.add_device('serial123')
132
150
 
133
151
  Visit the project's GitHub repository for source code, documentation, enhancements, and contributions:
134
152
 
135
- [PyPanRestV2 Repository on GitHub](https://github.com/wellhealthtechnologies/PyPanRestV2.git)
153
+ [PyPanRestV2 Repository on GitHub](https://github.com/mrzepa/pypanrestv2.git)
136
154
 
137
155
  ---
138
156
 
139
157
  ## Requirements
140
158
 
141
- - **Python 3.11+** (or higher)
142
- - **Palo Alto Devices** or Panorama
143
- - Python modules listed in requirements.txt
159
+ - **Python 3.11+**
160
+ - **Palo Alto Networks Devices** running PAN-OS 9.0+ or Panorama
161
+ - Python dependencies:
162
+ - dnspython
163
+ - icecream
164
+ - pycountry
165
+ - python-dotenv
166
+ - requests
167
+ - tqdm
168
+ - validators
144
169
 
145
170
  ---
146
171
 
@@ -196,7 +221,7 @@ Be sure to check the documentation, if provided, before starting contributions.
196
221
 
197
222
  ## License
198
223
 
199
- This project is licensed under the MIT. See the [LICENSE](./https://opensource.org/license/mit) file for details.
224
+ This project is licensed under the MIT License. See the [LICENSE](https://opensource.org/license/mit) file for details.
200
225
 
201
226
  ---
202
227
 
@@ -8,14 +8,22 @@
8
8
 
9
9
  - **High-Level Abstraction**: Simplifies interaction with the Palo Alto Networks API.
10
10
  - **Support for Firewalls and Panorama**: Manage both individual firewalls and Panorama devices.
11
- - **REST API Integration**: Allows seamless communication with devices.
11
+ - **REST API Integration**: Allows seamless communication with devices using REST API.
12
+ - **XML API Support**: Handles XML API calls for configurations not yet available in REST API.
12
13
  - **Convenient Pythonic Objects**: Intuitive Python objects for interacting with specific sections of Palo Alto firewall configurations.
14
+ - **Error Handling**: Custom exceptions for better error management and troubleshooting.
13
15
 
14
16
  ---
15
17
 
16
18
  ## Installation
17
19
 
18
- To install `PyPanRestV2`, clone the repository and install it as a package:
20
+ You can install `PyPanRestV2` using pip:
21
+
22
+ ```bash
23
+ pip install pypanrestv2
24
+ ```
25
+
26
+ Alternatively, you can clone the repository and install it as a package for development:
19
27
 
20
28
  ```bash
21
29
  # Clone the repository
@@ -38,7 +46,7 @@ This will install the package and all required dependencies automatically. The `
38
46
  Start by importing the necessary classes from the library:
39
47
 
40
48
  ```python
41
- from pypantrestv2 import Firewall, Panorama
49
+ from pypanrestv2 import Firewall, Panorama
42
50
  ```
43
51
 
44
52
  ### Connect to a Firewall or Panorama Device
@@ -62,12 +70,12 @@ from pypanrestv2.Policies import SecurityRules
62
70
 
63
71
  # Create a new security rule
64
72
  security_rule = SecurityRules(firewall, name='allow_web')
65
- security_rule.source_zone = ['trust']
66
- security_rule.destination_zone = ['untrust']
67
- security_rule.source = ['any']
68
- security_rule.destination = ['any']
69
- security_rule.application = ['web-browsing']
70
- security_rule.service = ['application-default']
73
+ security_rule.from_ = {'member': ['trust']}
74
+ security_rule.to = {'member': ['untrust']}
75
+ security_rule.source = {'member': ['any']}
76
+ security_rule.destination = {'member': ['any']}
77
+ security_rule.application = {'member': ['web-browsing']}
78
+ security_rule.service = {'member': ['application-default']}
71
79
  security_rule.action = 'allow'
72
80
  security_rule.create()
73
81
 
@@ -92,16 +100,24 @@ address.create()
92
100
  all_addresses = Addresses.get_all(firewall)
93
101
  ```
94
102
 
95
- #### 3. Working with Panorama Device Groups
103
+ #### 3. Working with Panorama Policies and Rulebase
96
104
  ```python
97
105
  from pypanrestv2 import Panorama
106
+ from pypanrestv2.Policies import SecurityRules
98
107
 
99
108
  # Initialize Panorama connection
100
109
  panorama = Panorama(base_url='panorama.example.com', api_key='YOUR_API_KEY')
101
110
 
102
- # Add a device to a device group
103
- device_group = panorama.device_groups.get('Branch_Offices')
104
- device_group.add_device('serial123')
111
+ # Create a security rule in the pre-rulebase of a device group
112
+ security_rule = SecurityRules(panorama, name='allow_internal', rulebase='Pre')
113
+ security_rule.from_ = {'member': ['trust']}
114
+ security_rule.to = {'member': ['untrust']}
115
+ security_rule.source = {'member': ['any']}
116
+ security_rule.destination = {'member': ['any']}
117
+ security_rule.application = {'member': ['web-browsing']}
118
+ security_rule.service = {'member': ['application-default']}
119
+ security_rule.action = 'allow'
120
+ security_rule.create()
105
121
  ```
106
122
 
107
123
  ---
@@ -110,15 +126,22 @@ device_group.add_device('serial123')
110
126
 
111
127
  Visit the project's GitHub repository for source code, documentation, enhancements, and contributions:
112
128
 
113
- [PyPanRestV2 Repository on GitHub](https://github.com/wellhealthtechnologies/PyPanRestV2.git)
129
+ [PyPanRestV2 Repository on GitHub](https://github.com/mrzepa/pypanrestv2.git)
114
130
 
115
131
  ---
116
132
 
117
133
  ## Requirements
118
134
 
119
- - **Python 3.11+** (or higher)
120
- - **Palo Alto Devices** or Panorama
121
- - Python modules listed in requirements.txt
135
+ - **Python 3.11+**
136
+ - **Palo Alto Networks Devices** running PAN-OS 9.0+ or Panorama
137
+ - Python dependencies:
138
+ - dnspython
139
+ - icecream
140
+ - pycountry
141
+ - python-dotenv
142
+ - requests
143
+ - tqdm
144
+ - validators
122
145
 
123
146
  ---
124
147
 
@@ -174,7 +197,7 @@ Be sure to check the documentation, if provided, before starting contributions.
174
197
 
175
198
  ## License
176
199
 
177
- This project is licensed under the MIT. See the [LICENSE](./https://opensource.org/license/mit) file for details.
200
+ This project is licensed under the MIT License. See the [LICENSE](https://opensource.org/license/mit) file for details.
178
201
 
179
202
  ---
180
203
 
@@ -378,80 +378,96 @@ class AddressGroups(Object):
378
378
  """
379
379
 
380
380
  valid_types = ['static', 'dynamic']
381
- CompareAttributeList = ['member', 'filter'].extend(valid_types)
381
+ CompareAttributeList = ['member', 'filter'] + valid_types
382
382
 
383
383
  def __init__(self, PANDevice, **kwargs):
384
384
  super().__init__(PANDevice, max_name_length=64, max_description_length=1024, has_tags=False, **kwargs)
385
- # Initialize MemberObj list with Address objects if 'member' list is provided
385
+ # Initialize container for resolved member objects
386
386
  self.MemberObj: list = []
387
- self.static: Dict[str, List[str]] = {}
388
- self.dynamic: Dict[str, List[str]] = {}
387
+
388
+ # Start unset; allow instantiation without static/dynamic
389
+ self._static: Optional[Dict[str, List[str]]] = None
390
+ self._dynamic: Optional[Dict[str, str]] = None
391
+
392
+ # Optionally accept one of 'static' or 'dynamic' from kwargs
393
+ provided_static = kwargs.get('static')
394
+ provided_dynamic = kwargs.get('dynamic')
395
+
396
+ if provided_static is not None and provided_dynamic is not None:
397
+ raise ValueError("Only one of 'static' or 'dynamic' can be provided.")
398
+
399
+ if provided_static is not None:
400
+ self.static = provided_static
401
+ elif provided_dynamic is not None:
402
+ self.dynamic = provided_dynamic
389
403
 
390
404
  @property
391
405
  def static(self):
392
406
  return self._static
393
407
 
394
408
  @static.setter
395
- def static(self, value: dict):
409
+ def static(self, value: Optional[dict]):
396
410
  """
397
- The setter method for the 'static' attribute.
398
- Validates that the value is a dictionary containing the key 'member' with a value of type list,
399
- and this list contains at least one item.
400
-
401
- :param value: The value to set for the 'static' attribute.
402
- :type value: dict
403
- :raises ValueError: If the value does not meet the specified criteria.
411
+ Set or clear the 'static' attribute.
412
+ When set, it must be a dict containing key 'member' with a non-empty list.
413
+ Setting 'static' clears 'dynamic' (mutually exclusive).
404
414
  """
415
+ if value is None:
416
+ self._static = None
417
+ # Remove from entry if present
418
+ self.entry.pop('static', None)
419
+ return
420
+
405
421
  if not isinstance(value, dict):
406
422
  raise ValueError("The 'static' attribute must be a dictionary.")
407
-
408
423
  if 'member' not in value:
409
424
  raise ValueError("The dictionary must contain the key 'member'.")
410
-
411
425
  if not isinstance(value['member'], list):
412
426
  raise ValueError("The 'member' key must have a list as its value.")
413
-
414
427
  if len(value['member']) < 1:
415
428
  raise ValueError("The list under 'member' key must contain at least one item.")
416
429
 
417
- self._static = value
418
- self.entry.update({'static': value})
430
+ # Set static and clear dynamic to ensure mutual exclusivity
431
+ self._static = {'member': list(value['member'])}
432
+ self.entry.update({'static': self._static})
433
+ # Clear dynamic side if set
434
+ self._dynamic = None
435
+ self.entry.pop('dynamic', None)
419
436
 
420
437
  @property
421
- def dynamic(self) -> dict:
438
+ def dynamic(self) -> Optional[dict]:
422
439
  """
423
440
  The 'dynamic' property getter.
424
441
  """
425
442
  return self._dynamic
426
443
 
427
444
  @dynamic.setter
428
- def dynamic(self, value: dict) -> None:
445
+ def dynamic(self, value: Optional[dict]) -> None:
429
446
  """
430
- The setter for the 'dynamic' attribute.
431
- Validates that the value is a dictionary with a key 'filter' whose value is a string of max length 2047.
432
-
433
- :param value: The dictionary to set as the 'dynamic' attribute.
434
- :raises ValueError: If the value does not meet the specified criteria.
447
+ Set or clear the 'dynamic' attribute.
448
+ When set, it must be a dict with a key 'filter' whose value is a string of max length 2047.
449
+ Setting 'dynamic' clears 'static' (mutually exclusive).
435
450
  """
436
- # Check if the value is a dictionary
451
+ if value is None:
452
+ self._dynamic = None
453
+ self.entry.pop('dynamic', None)
454
+ return
455
+
437
456
  if not isinstance(value, dict):
438
457
  raise ValueError("The 'dynamic' attribute must be a dictionary.")
439
-
440
- # Check if the dictionary has a key named 'filter'
441
458
  if 'filter' not in value:
442
459
  raise ValueError("The dictionary must contain the key 'filter'.")
443
-
444
- # Check if the value of 'filter' is a string
445
460
  if not isinstance(value['filter'], str):
446
461
  raise ValueError("The 'filter' key must have a string as its value.")
447
-
448
- # Check if the string length of 'filter' is within the limit
449
462
  if len(value['filter']) > 2047:
450
463
  raise ValueError("The 'filter' value must not exceed 2047 characters.")
451
464
 
452
- # If all checks pass, set the value
453
- self._dynamic = value
454
- self.entry.update({'dynamic': value})
465
+ # Set dynamic and clear static to ensure mutual exclusivity
466
+ self._dynamic = {'filter': value['filter']}
467
+ self.entry.update({'dynamic': self._dynamic})
468
+ # Clear static side if set
469
+ self._static = None
470
+ self.entry.pop('static', None)
455
471
 
456
472
  @property
457
473
  def MemberObj(self):
@@ -469,22 +485,27 @@ class AddressGroups(Object):
469
485
  del self._MemberObj
470
486
 
471
487
  def add_member(self, member):
472
- if not hasattr(self, 'static'):
488
+ # Ensure we are in static mode
489
+ if self.static is None:
473
490
  raise ValueError("Can only add members to a 'static' type AddressGroup")
491
+ members = self._static.setdefault('member', [])
474
492
  if isinstance(member, Addresses):
475
- if member.name not in self.member:
476
- self.member.append(member.name)
493
+ if member.name not in members:
494
+ members.append(member.name)
477
495
  self.MemberObj.append(member)
478
496
  elif isinstance(member, str):
479
- if member not in self.member:
480
- self.member.append(member)
481
- # Optionally, resolve the Addresses object from the name and add to MemberObj
497
+ if member not in members:
498
+ members.append(member)
482
499
  else:
483
500
  raise TypeError("Member must be an Addresses object or a string")
501
+ # Reflect changes into entry
502
+ self.entry.update({'static': self._static})
484
503
 
485
504
  def remove_member(self, member):
486
- if not hasattr(self, 'static'):
505
+ # Ensure we are in static mode
506
+ if self.static is None:
487
507
  raise ValueError("Can only remove members from a 'static' type AddressGroup")
508
+ members = self._static.get('member', [])
488
509
  if isinstance(member, Addresses):
489
510
  member_name = member.name
490
511
  elif isinstance(member, str):
@@ -492,35 +513,31 @@ class AddressGroups(Object):
492
513
  else:
493
514
  raise TypeError("Member must be an Addresses object or a string")
494
515
 
495
- if member_name in self.member:
496
- self.member.remove(member_name)
497
- self.MemberObj = [obj for obj in self.MemberObj if obj.name != member_name]
516
+ if member_name in members:
517
+ members.remove(member_name)
518
+ self.MemberObj = [obj for obj in self.MemberObj if getattr(obj, 'name', None) != member_name]
519
+ # Reflect changes into entry
520
+ self.entry.update({'static': self._static})
498
521
 
499
522
  def set_filter(self, filter_str):
500
- if not hasattr(self, 'dynamic'):
501
- raise ValueError("Filter can only be set for a 'dynamic' type AddressGroup")
523
+ # Ensure we are in dynamic mode (and set if not yet set)
524
+ if not isinstance(filter_str, str):
525
+ raise TypeError("Filter must be a string")
502
526
  if len(filter_str) > 2047:
503
527
  raise ValueError("Filter length exceeds the maximum allowed characters (2047)")
504
- self.dynamic = filter_str
528
+ # This will also clear static via the setter
529
+ self.dynamic = {'filter': filter_str}
505
530
 
506
531
  def get_object(self, obj_type, name: str, location: str, device_group: str, vsys: str) -> Optional[Any]:
507
532
  """
508
533
  Attempt to instantiate an object of type `obj_type` with the provided parameters.
509
534
  Returns the instantiated object if it exists, None otherwise.
510
-
511
- :param obj_type: The class of the object to be instantiated.
512
- :param name: The name of the object.
513
- :param location: The location of the object.
514
- :param device_group: The device group the object belongs to.
515
- :param vsys: The virtual system the object belongs to.
516
- :return: An instance of `obj_type` if successful, None otherwise.
517
535
  """
518
536
  try:
519
537
  obj = obj_type(PANDevice=self.PANDevice, name=name, location=location, device_group=device_group, vsys=vsys)
520
538
  if obj.get(ANYLOCATION=True, IsSearch=True):
521
539
  return obj
522
540
  except Exception as e:
523
- # Here you should log the exception e
524
541
  logger.error(f"Error instantiating object of type {obj_type.__name__}: {e}")
525
542
  return None
526
543
 
@@ -530,7 +547,7 @@ class AddressGroups(Object):
530
547
  Tries to instantiate Address objects first; if that fails, tries AddressGroups.
531
548
  Raises an exception if the static member list is empty or the objects cannot be found.
532
549
  """
533
- members = self.static.get('member')
550
+ members = (self.static or {}).get('member')
534
551
  if not members:
535
552
  raise ValueError('Cannot populate an empty group.')
536
553
 
@@ -562,12 +562,7 @@ class NatRules(Policy):
562
562
  if value == 'any':
563
563
  self._service = 'any'
564
564
  else:
565
- check_service = Services(self.PANDevice, name=value, location=self.location, device_group=self.device_group)
566
- if check_service.get(ANYLOCATION=True, IsSearch=True):
567
- self._service = value
568
- else:
569
- raise ValueError(f"The service '{value}' does not exist.")
570
-
565
+ self._service = value
571
566
  # Update the entry dictionary
572
567
  self.entry.update({'service': self._service})
573
568
 
@@ -604,28 +599,29 @@ class NatRules(Policy):
604
599
 
605
600
  @source_translation.setter
606
601
  def source_translation(self, value: dict) -> None:
607
- if not isinstance(value, dict):
608
- raise TypeError("source_translation must be a dictionary.")
609
-
610
- # Validate the keys of the main dict
611
- valid_keys = ['dynamic-ip-and-port', 'dynamic-ip', 'static-ip']
612
- if all(key not in valid_keys for key in value.keys()):
613
- raise ValueError(f"Invalid key in source_translation. Must be one of: {', '.join(valid_keys)}")
614
-
615
- keys_present = [key for key in valid_keys if key in value]
616
- if len(keys_present) > 1:
617
- raise ValueError("Only one of 'dynamic-ip-and-port', 'dynamic-ip', or 'static-ip' can be set at a time.")
618
-
619
- if 'dynamic-ip-and-port' in value:
620
- self._validate_dynamic_ip_and_port(value['dynamic-ip-and-port'])
621
- elif 'dynamic-ip' in value:
622
- self._validate_dynamic_ip(value['dynamic-ip'])
623
- elif 'static-ip' in value:
624
- self._validate_static_ip(value['static-ip'])
625
-
626
- # Update the entry dictionary to reflect the change
627
- self._source_translation = value
628
- self.entry.update({'source-translation': self._source_translation})
602
+ if value:
603
+ if not isinstance(value, dict):
604
+ raise TypeError("source_translation must be a dictionary.")
605
+
606
+ # Validate the keys of the main dict
607
+ valid_keys = ['dynamic-ip-and-port', 'dynamic-ip', 'static-ip']
608
+ if all(key not in valid_keys for key in value.keys()):
609
+ raise ValueError(f"Invalid key in source_translation. Must be one of: {', '.join(valid_keys)}")
610
+
611
+ keys_present = [key for key in valid_keys if key in value]
612
+ if len(keys_present) > 1:
613
+ raise ValueError("Only one of 'dynamic-ip-and-port', 'dynamic-ip', or 'static-ip' can be set at a time.")
614
+
615
+ if 'dynamic-ip-and-port' in value:
616
+ self._validate_dynamic_ip_and_port(value['dynamic-ip-and-port'])
617
+ elif 'dynamic-ip' in value:
618
+ self._validate_dynamic_ip(value['dynamic-ip'])
619
+ elif 'static-ip' in value:
620
+ self._validate_static_ip(value['static-ip'])
621
+
622
+ # Update the entry dictionary to reflect the change
623
+ self._source_translation = value
624
+ self.entry.update({'source-translation': self._source_translation})
629
625
 
630
626
  def _validate_dynamic_ip_and_port(self, value: dict) -> None:
631
627
  valid_sub_keys = ['translated-addresses', 'interface-address']
@@ -3,7 +3,7 @@ requires = ["poetry-core>=1.0.0"]
3
3
  build-backend = "poetry.core.masonry.api"
4
4
  [tool.poetry]
5
5
  name = "PyPANRestV2"
6
- version = "2.1.0"
6
+ version = "2.1.2"
7
7
  description = "Python tools for interacting with Palo Alto Networks REST API."
8
8
  authors = [
9
9
  "Mark Rzepa <mark@rzepa.com>"