PyPANRestV2 2.1.2__tar.gz → 2.1.3__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.4
1
+ Metadata-Version: 2.1
2
2
  Name: PyPANRestV2
3
- Version: 2.1.2
3
+ Version: 2.1.3
4
4
  Summary: Python tools for interacting with Palo Alto Networks REST API.
5
5
  License: MIT
6
6
  Author: Mark Rzepa
@@ -10,8 +10,6 @@ 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
15
13
  Requires-Dist: dnspython (>=2.6.1)
16
14
  Requires-Dist: icecream (>=2.1.3)
17
15
  Requires-Dist: pycountry (>=23.12.11)
@@ -66,51 +66,197 @@ class Templates(PanoramaTab):
66
66
 
67
67
 
68
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']
69
+ """Represents a Panorama template stack and its templates, devices, and variables.
71
70
 
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': []}
71
+ This class models the JSON structure used by the Panorama REST API for
72
+ template stacks, including:
77
73
 
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.
74
+ - Stack-level templates under ``templates.member``.
75
+ - Devices assigned to the stack under ``devices.entry``.
76
+ - Stack-level variable definitions under ``variable.entry``.
77
+ - Per-device variable assignments under
78
+ ``devices.entry[*].variable.entry``.
81
79
 
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.
80
+ Stack-level variables
81
+ ---------------------
82
+ Stack-level variables describe which variables are available to the
83
+ template stack and what *type* they are. Each definition looks like::
86
84
 
87
- Args:
88
- name: The name of the device to be added.
89
- variable: (Optional) The data or configuration associated with the device.
85
+ {
86
+ "@name": "$mgmt_ip",
87
+ "type": {"ip-netmask": "0.0.0.0/0"}
88
+ }
90
89
 
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:
90
+ and is stored inside ``self.variable['entry']`` and serialized as the
91
+ ``variable`` block on the template stack.
92
+
93
+ Per-device variables
94
+ --------------------
95
+ Devices are stored in ``self.devices['entry']``. Each device can contain
96
+ a per-device ``variable`` block that assigns values to the stack-level
97
+ variables for that device::
98
+
99
+ {
100
+ "@name": "0123456789", # device serial
101
+ "variable": {
102
+ "entry": [
103
+ {
104
+ "@name": "$mgmt_ip",
105
+ "type": {"ip-netmask": "10.0.0.1/32"}
106
+ }
107
+ ]
108
+ }
109
+ }
110
+
111
+ Typical usage
112
+ -------------
113
+
114
+ Create or load a template stack::
115
+
116
+ ts = TemplateStacks(panorama, name="Branch-Stack")
117
+ ts.refresh() # optional, to pull live data
118
+
119
+ Define variables at the stack level::
120
+
121
+ ts.update_variable("$mgmt_ip", "ip-netmask", "0.0.0.0/0")
122
+ ts.update_variable("$hostname", "hostname", "default-host")
123
+ ts.edit()
124
+
125
+ Add a device to the stack::
126
+
127
+ ts.add_device("0123456789")
128
+ ts.edit()
129
+
130
+ Set a device variable value (recommended entry point)::
131
+
132
+ ts.set_device_variable_value(
133
+ device_serial="0123456789",
134
+ variable_name="$mgmt_ip",
135
+ value="10.1.2.3/32",
136
+ )
137
+ ts.edit()
138
+
139
+ The :meth:`set_device_variable_value` helper only requires the device
140
+ serial, variable name, and value. It automatically infers the variable
141
+ type from the stack-level definition and creates the device entry if it
142
+ does not already exist (unless disabled via
143
+ ``create_device_if_missing=False``).
144
+ """
145
+ variable_types = ['ip-netmask', 'ip-range', 'hostname', 'ipv4-subnet', 'ipv6-subnet', 'pre-shared-key',
146
+ 'fqdn', 'group-id', 'device-priority', 'device-id', 'interface',
147
+ 'as-number', 'qos-profile', 'egress-max', 'link-tag']
148
+
149
+ def __init__(self, PANDevice, **kwargs):
150
+ super().__init__(PANDevice, max_description_length=255, max_name_length=63, **kwargs)
151
+ self.templates: Dict = kwargs.get('templates', {'member': []})
152
+ self.devices: Dict = kwargs.get('devices', {'entry': []})
153
+ self.variable: Dict = kwargs.get('variable', {'entry': []})
154
+
155
+ def _ensure_devices_container(self) -> None:
156
+ if not isinstance(self.devices, dict) or 'entry' not in self.devices:
157
+ self.devices = {'entry': []}
158
+ if 'devices' not in self.entry:
159
+ self.entry['devices'] = self.devices
160
+
161
+ def _ensure_device_variables_container(self, device_entry: Dict[str, Any]) -> None:
162
+ if 'variable' not in device_entry or not isinstance(device_entry['variable'], dict):
163
+ device_entry['variable'] = {'entry': []}
164
+
165
+ def add_device(self, name: str, variables: Optional[Dict] = None) -> bool:
166
+ self._ensure_devices_container()
167
+
168
+ if variables is not None:
169
+ # variables must be a variable dict: {'entry': [ ... ]}
170
+ if not self.validate_variable_structure(variables):
100
171
  logger.debug(f"Invalid variable structure for device {name}. Not adding.")
101
- logger.debug(f"Variables provided: {variable}")
172
+ logger.debug(f"Variables provided: {variables}")
102
173
  return False
174
+ device_entry: Dict[str, Any] = {'@name': name, 'variable': variables}
103
175
  else:
104
- # Handle case where no variables are provided
105
- logger.debug(f"Adding device {name} without variables to template stack {self.name}")
106
176
  device_entry = {'@name': name}
107
177
 
108
- # Add the device entry to the devices list
109
178
  self.devices['entry'].append(device_entry)
110
179
  self.entry['devices'] = self.devices
111
180
 
112
181
  return True
113
182
 
183
+ def remove_device(self, name: str) -> bool:
184
+ self._ensure_devices_container()
185
+ for idx, device_entry in enumerate(self.devices['entry']):
186
+ if device_entry.get('@name') == name:
187
+ del self.devices['entry'][idx]
188
+ self.entry['devices'] = self.devices
189
+ return True
190
+ return False
191
+
192
+ def set_device_variable_value(
193
+ self,
194
+ device_serial: str,
195
+ variable_name: str,
196
+ value: str,
197
+ create_device_if_missing: bool = True,
198
+ ) -> None:
199
+ """Set a per-device variable value using only serial, name, and value.
200
+
201
+ Parameters
202
+ ----------
203
+ device_serial:
204
+ Serial number of the device in the template stack.
205
+ variable_name:
206
+ Name of the variable (for example ``"$mgmt_ip"``). The variable
207
+ must already be defined at the stack level via
208
+ :meth:`update_variable`.
209
+ value:
210
+ Value to assign to this variable for the specified device.
211
+ create_device_if_missing:
212
+ If ``True`` (default), a new device entry is added to
213
+ ``devices.entry`` if one with ``@name == device_serial`` does not
214
+ already exist. If ``False``, the method will only update existing
215
+ devices.
216
+
217
+ Notes
218
+ -----
219
+ The method looks up ``variable_name`` in the stack-level
220
+ ``self.variable['entry']`` block to determine the correct variable
221
+ *type* (for example ``"ip-netmask"`` or ``"hostname"``). It then
222
+ delegates to :meth:`update_device_variable` to create or update the
223
+ per-device variable entry under
224
+ ``devices.entry[*].variable.entry``.
225
+ """
226
+
227
+ # Ensure we have stack-level variable definitions
228
+ if not self.variable or 'entry' not in self.variable:
229
+ raise ValueError(
230
+ f"No stack-level variables defined on template stack {self.name}; "
231
+ f"cannot infer type for {variable_name!r}."
232
+ )
233
+
234
+ # Find the variable definition to determine its type key
235
+ var_type_key: Optional[str] = None
236
+ for var_def in self.variable['entry']:
237
+ if var_def.get('@name') == variable_name and isinstance(var_def.get('type'), dict):
238
+ if var_def['type']:
239
+ var_type_key = next(iter(var_def['type']))
240
+ break
241
+
242
+ if not var_type_key:
243
+ raise ValueError(
244
+ f"Variable definition {variable_name!r} not found on template stack {self.name}; "
245
+ "define it first on the outer 'variable' block."
246
+ )
247
+
248
+ # Optionally create the device entry if it does not exist yet
249
+ device_exists = any(
250
+ isinstance(d, dict) and d.get('@name') == device_serial
251
+ for d in self.devices.get('entry', [])
252
+ )
253
+
254
+ if not device_exists and create_device_if_missing:
255
+ self.add_device(device_serial)
256
+
257
+ # Delegate to the lower-level helper that knows about types
258
+ self.update_device_variable(device_serial, variable_name, var_type_key, value)
259
+
114
260
  def update_variable(self, name: str, variable_type: str, variable_value: str):
115
261
  if variable_type in self.variable_types:
116
262
  variable_entry = {'@name': name, 'type': {variable_type: variable_value}}
@@ -119,27 +265,52 @@ class TemplateStacks(PanoramaTab):
119
265
 
120
266
  def update_device_variable(self, device_name: str, variable_name: str, variable_type: str,
121
267
  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
- })
268
+ if variable_type not in self.variable_types:
269
+ return
270
+
271
+ self._ensure_devices_container()
272
+
273
+ for device_entry in self.devices['entry']:
274
+ if device_entry.get('@name') != device_name:
275
+ continue
276
+
277
+ self._ensure_device_variables_container(device_entry)
278
+
279
+ variable_found = False
280
+ for var_entry in device_entry['variable']['entry']:
281
+ if var_entry.get('@name') == variable_name:
282
+ var_entry['type'] = {variable_type: variable_value}
283
+ variable_found = True
141
284
  break
142
285
 
286
+ if not variable_found:
287
+ device_entry['variable']['entry'].append({
288
+ '@name': variable_name,
289
+ 'type': {variable_type: variable_value}
290
+ })
291
+
292
+ self.entry['devices'] = self.devices
293
+ break
294
+
295
+ def remove_device_variable(self, device_name: str, variable_name: str) -> bool:
296
+ self._ensure_devices_container()
297
+
298
+ for device_entry in self.devices['entry']:
299
+ if device_entry.get('@name') != device_name:
300
+ continue
301
+
302
+ if 'variable' not in device_entry or 'entry' not in device_entry['variable']:
303
+ return False
304
+
305
+ for idx, var_entry in enumerate(device_entry['variable']['entry']):
306
+ if var_entry.get('@name') == variable_name:
307
+ del device_entry['variable']['entry'][idx]
308
+ self.entry['devices'] = self.devices
309
+ return True
310
+ return False
311
+
312
+ return False
313
+
143
314
  def add_template_member(self, member):
144
315
  self.templates['member'].append(member)
145
316
  self.entry['templates'] = self.templates
@@ -208,7 +379,7 @@ class TemplateStacks(PanoramaTab):
208
379
  def variable(self, value: Dict):
209
380
  if self.validate_variable_structure(value):
210
381
  self._variable = value
211
- self.entry['variables'] = value
382
+ self.entry['variable'] = value
212
383
  else:
213
384
  raise ValueError("Invalid variable structure")
214
385
 
@@ -251,9 +422,9 @@ class TemplateStacks(PanoramaTab):
251
422
  if not isinstance(devices['entry'], list):
252
423
  return False
253
424
  for item in devices['entry']:
254
- if not isinstance(item, dict) or '@name' not in item or 'variable' not in item:
425
+ if not isinstance(item, dict) or '@name' not in item:
255
426
  return False
256
- if not self.validate_variable_structure(item['variable']):
427
+ if 'variable' in item and not self.validate_variable_structure(item['variable']):
257
428
  return False
258
429
  return True
259
430
 
@@ -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.2"
6
+ version = "2.1.3"
7
7
  description = "Python tools for interacting with Palo Alto Networks REST API."
8
8
  authors = [
9
9
  "Mark Rzepa <mark@rzepa.com>"
File without changes