PyPANRestV2 2.1.2__tar.gz → 2.1.4__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.4
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,216 @@ 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
+ # Infer the variable type (from stack-level block or existing devices)
228
+ var_type_key = self._infer_variable_type(variable_name)
229
+
230
+ if not var_type_key:
231
+ raise ValueError(
232
+ f"Variable definition {variable_name!r} not found for template stack {self.name}; "
233
+ "define it at the stack level or ensure another device has it assigned."
234
+ )
235
+
236
+ # Optionally create the device entry if it does not exist yet
237
+ device_exists = any(
238
+ isinstance(d, dict) and d.get('@name') == device_serial
239
+ for d in self.devices.get('entry', [])
240
+ )
241
+
242
+ if not device_exists and create_device_if_missing:
243
+ self.add_device(device_serial)
244
+
245
+ # Delegate to the lower-level helper that knows about types
246
+ self.update_device_variable(device_serial, variable_name, var_type_key, value)
247
+
248
+ def _infer_variable_type(self, variable_name: str) -> Optional[str]:
249
+ """Return the variable type key for a given variable name, if known.
250
+
251
+ The lookup order is:
252
+
253
+ 1. Stack-level ``variable.entry`` definitions.
254
+ 2. Existing device-level ``devices.entry[*].variable.entry`` blocks.
255
+ """
256
+
257
+ # 1) Look at stack-level variable definitions, if present
258
+ if self.variable and isinstance(self.variable, dict) and 'entry' in self.variable:
259
+ for var_def in self.variable.get('entry', []):
260
+ if var_def.get('@name') == variable_name and isinstance(var_def.get('type'), dict):
261
+ if var_def['type']:
262
+ return next(iter(var_def['type']))
263
+
264
+ # 2) Fallback: inspect variables from existing devices in the stack
265
+ if self.devices and isinstance(self.devices, dict):
266
+ for dev in self.devices.get('entry', []):
267
+ if not isinstance(dev, dict):
268
+ continue
269
+ dev_var = dev.get('variable')
270
+ if not isinstance(dev_var, dict):
271
+ continue
272
+ for var in dev_var.get('entry', []):
273
+ if var.get('@name') == variable_name and isinstance(var.get('type'), dict):
274
+ if var['type']:
275
+ return next(iter(var['type']))
276
+
277
+ return None
278
+
114
279
  def update_variable(self, name: str, variable_type: str, variable_value: str):
115
280
  if variable_type in self.variable_types:
116
281
  variable_entry = {'@name': name, 'type': {variable_type: variable_value}}
@@ -119,27 +284,52 @@ class TemplateStacks(PanoramaTab):
119
284
 
120
285
  def update_device_variable(self, device_name: str, variable_name: str, variable_type: str,
121
286
  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
- })
287
+ if variable_type not in self.variable_types:
288
+ return
289
+
290
+ self._ensure_devices_container()
291
+
292
+ for device_entry in self.devices['entry']:
293
+ if device_entry.get('@name') != device_name:
294
+ continue
295
+
296
+ self._ensure_device_variables_container(device_entry)
297
+
298
+ variable_found = False
299
+ for var_entry in device_entry['variable']['entry']:
300
+ if var_entry.get('@name') == variable_name:
301
+ var_entry['type'] = {variable_type: variable_value}
302
+ variable_found = True
141
303
  break
142
304
 
305
+ if not variable_found:
306
+ device_entry['variable']['entry'].append({
307
+ '@name': variable_name,
308
+ 'type': {variable_type: variable_value}
309
+ })
310
+
311
+ self.entry['devices'] = self.devices
312
+ break
313
+
314
+ def remove_device_variable(self, device_name: str, variable_name: str) -> bool:
315
+ self._ensure_devices_container()
316
+
317
+ for device_entry in self.devices['entry']:
318
+ if device_entry.get('@name') != device_name:
319
+ continue
320
+
321
+ if 'variable' not in device_entry or 'entry' not in device_entry['variable']:
322
+ return False
323
+
324
+ for idx, var_entry in enumerate(device_entry['variable']['entry']):
325
+ if var_entry.get('@name') == variable_name:
326
+ del device_entry['variable']['entry'][idx]
327
+ self.entry['devices'] = self.devices
328
+ return True
329
+ return False
330
+
331
+ return False
332
+
143
333
  def add_template_member(self, member):
144
334
  self.templates['member'].append(member)
145
335
  self.entry['templates'] = self.templates
@@ -208,7 +398,7 @@ class TemplateStacks(PanoramaTab):
208
398
  def variable(self, value: Dict):
209
399
  if self.validate_variable_structure(value):
210
400
  self._variable = value
211
- self.entry['variables'] = value
401
+ self.entry['variable'] = value
212
402
  else:
213
403
  raise ValueError("Invalid variable structure")
214
404
 
@@ -251,9 +441,9 @@ class TemplateStacks(PanoramaTab):
251
441
  if not isinstance(devices['entry'], list):
252
442
  return False
253
443
  for item in devices['entry']:
254
- if not isinstance(item, dict) or '@name' not in item or 'variable' not in item:
444
+ if not isinstance(item, dict) or '@name' not in item:
255
445
  return False
256
- if not self.validate_variable_structure(item['variable']):
446
+ if 'variable' in item and not self.validate_variable_structure(item['variable']):
257
447
  return False
258
448
  return True
259
449
 
@@ -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.4"
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