diff --git a/panos/base.py b/panos/base.py
index 1cc2784f..aea62497 100644
--- a/panos/base.py
+++ b/panos/base.py
@@ -686,6 +686,8 @@ def update(self, variable):
)
device.set_config_changed()
path, value, var_path = self._get_param_specific_info(variable)
+ if var_path.vartype == "attrib":
+ raise NotImplementedError("Cannot update 'attrib' style params")
xpath = "{0}/{1}".format(self.xpath(), path)
if value is None:
@@ -2475,6 +2477,11 @@ def element(self, with_children=True, comparable=False):
self.xml_merge(ans, itertools.chain(*iterchain))
# Now that the whole element is built, mixin an attrib vartypes.
+ #
+ # We do this here instead of in xml_merge() because attributes are considered
+ # part of the identity in that function, and I'm not sure we want to manage
+ # a list of what attributes are considered part of an element's identity and
+ # what should be mixed in.
for p in paths:
if p.vartype != "attrib":
continue
@@ -2484,24 +2491,31 @@ def element(self, with_children=True, comparable=False):
if attrib_value is None or p.exclude:
continue
e = ans
- find_path = [
- ".",
- ]
for ap in attrib_path:
if not ap:
continue
+ finder = None
+ tag = None
+ attribs = {}
if ap.startswith("entry "):
junk, var_to_use = ap.split()
sol_value = panos.string_or_list(settings[var_to_use])[0]
- find_path.append("entry[@name='{0}']".format(sol_val))
+ finder = "entry[@name='{0}']".format(sol_value)
+ tag = "entry"
+ attribs["name"] = sol_value
elif ap == "entry[@name='localhost.localdomain']":
- find_path.append(ap)
+ finder = ap
+ tag = "entry"
+ attribs["name"] = "localhost.localdomain"
else:
- find_path.append(ap.format(**settings))
- if len(find_path) > 1:
- e = e.find("/".join(find_path))
- if e is not None:
- e.attrib[attrib_name] = attrib_value
+ finder = ap.format(**settings)
+ tag = finder
+ e2 = e.find("./{0}".format(finder))
+ if e2 is None:
+ e = ET.SubElement(e, tag, attribs)
+ else:
+ e = e2
+ e.attrib[attrib_name] = attrib_value
return ans
@@ -2842,6 +2856,8 @@ def _set_inner_xml_tag_text(self, elm, value, comparable=False):
elif self.vartype == "none":
# There is no variable, so don't try to populate it
pass
+ elif self.vartype == "attrib":
+ raise ValueError("attrib not yet supported for classic objects")
else:
elm.text = str(value)
@@ -2945,10 +2961,13 @@ def element(self, elm, settings, comparable=False):
return None
e = elm
+ attr = None
# Build the element
tokens = self.path.split("/")
if self.vartype == "exist":
del tokens[-1]
+ elif self.vartype == "attrib":
+ attr = tokens.pop()
for token in tokens:
if not token:
continue
@@ -2965,7 +2984,7 @@ def element(self, elm, settings, comparable=False):
e.append(child)
e = child
- self._set_inner_xml_tag_text(e, value, comparable)
+ self._set_inner_xml_tag_text(e, value, comparable, attr)
return elm
@@ -3013,10 +3032,13 @@ def parse_xml(self, xml, settings, possibilities):
# thus not needed
return None
+ attr = None
e = xml
tokens = self.path.split("/")
if self.vartype == "exist":
del tokens[-1]
+ elif self.vartype == "attrib":
+ attr = tokens.pop()
for p in tokens:
# Skip this path part if there is no path part
if not p:
@@ -3063,15 +3085,16 @@ def parse_xml(self, xml, settings, possibilities):
e = ans
# Pull the value, properly formatted, from this last element
- self.parse_value_from_xml_last_tag(e, settings)
+ self.parse_value_from_xml_last_tag(e, settings, attr)
- def _set_inner_xml_tag_text(self, elm, value, comparable=False):
+ def _set_inner_xml_tag_text(self, elm, value, comparable=False, attr=None):
"""Sets the final elm's .text as appropriate given the vartype.
Args:
elm (xml.etree.ElementTree.Element): The element whose .text to set.
value (various): The value to put in the .text, conforming to the vartype of this parameter.
comparable (bool): Make updates for element string comparisons. For encrypted fields, if the text should be set to a password hash (True) or left as a basestring (False). For entry and member vartypes, sort the entries (True) or leave them as-is (False).
+ attr (str): For `vartype="attrib"`, the attribute name.
"""
# Format the element text appropriately
@@ -3102,10 +3125,12 @@ def _set_inner_xml_tag_text(self, elm, value, comparable=False):
elm.text = str(int(value))
elif self.vartype == "encrypted" and comparable:
elm.text = self._sha1_hash(str(value))
+ elif self.vartype == "attrib":
+ elm.attrib[attr] = value
else:
elm.text = str(value)
- def parse_value_from_xml_last_tag(self, elm, settings):
+ def parse_value_from_xml_last_tag(self, elm, settings, attr):
"""Actually do the parsing for this parameter.
The value parsed is saved into the ``settings`` dict.
@@ -3115,6 +3140,7 @@ def parse_value_from_xml_last_tag(self, elm, settings):
document passed in to ``parse_xml()`` that contains the actual
value to parse out for this parameter.
settings (dict): The dict where the parsed value will be saved.
+ attr (str): For `vartype="attrib"`, the attribute name.
Raises:
ValueError: If a param is in an incorrect format.
@@ -3143,6 +3169,8 @@ def parse_value_from_xml_last_tag(self, elm, settings):
pass
elif self.vartype == "int":
settings[self.param] = int(elm.text)
+ elif self.vartype == "attrib":
+ settings[self.param] = elm.attrib.get(attr, None)
else:
settings[self.param] = elm.text
diff --git a/tests/test_params.py b/tests/test_params.py
new file mode 100644
index 00000000..4073ae2e
--- /dev/null
+++ b/tests/test_params.py
@@ -0,0 +1,212 @@
+import pytest
+import xml.etree.ElementTree as ET
+
+from panos.base import ENTRY, Root
+from panos.base import VersionedPanObject, VersionedParamPath
+from panos.firewall import Firewall
+
+
+class FakeObject(VersionedPanObject):
+ """Fake object for testing."""
+
+ SUFFIX = ENTRY
+ ROOT = Root.VSYS
+
+ def _setup(self):
+ self._xpaths.add_profile(value="/fake")
+
+ params = []
+
+ params.append(VersionedParamPath("uuid", vartype="attrib", path="uuid",),)
+ params.append(VersionedParamPath("size", vartype="int", path="size",),)
+ params.append(VersionedParamPath("listing", vartype="member", path="listing",),)
+ params.append(VersionedParamPath("pb1", vartype="exist", path="pb1",),)
+ params.append(VersionedParamPath("pb2", vartype="exist", path="pb2",),)
+ params.append(VersionedParamPath("live", vartype="yesno", path="live",),)
+ params.append(
+ VersionedParamPath("disabled", vartype="yesno", path="disabled",),
+ )
+ params.append(
+ VersionedParamPath("uuid2", vartype="attrib", path="level-2/uuid",),
+ )
+ params.append(VersionedParamPath("age", vartype="int", path="level-2/age",),)
+ params.append(
+ VersionedParamPath(
+ "interfaces", vartype="member", path="level-2/interface",
+ ),
+ )
+
+ self._params = tuple(params)
+
+
+def _verify_render(o, expected):
+ ans = o.element_str().decode("utf-8")
+
+ assert ans == expected
+
+
+def _refreshed_object():
+ fw = Firewall("127.0.0.1", "admin", "admin", "secret")
+ fw._version_info = (9999, 0, 0)
+
+ o = FakeObject()
+ fw.add(o)
+
+ o = o.refreshall_from_xml(_refresh_xml())[0]
+
+ return o
+
+
+def _refresh_xml():
+ return ET.fromstring(
+ """
+
+
+ 5
+
+ first
+ second
+
+
+ yes
+
+ 12
+
+ third
+ fourth
+
+
+
+"""
+ )
+
+
+# int at base level
+def test_render_int():
+ _verify_render(
+ FakeObject("test", size=5), '5',
+ )
+
+
+def test_parse_int():
+ o = _refreshed_object()
+
+ assert o.size == 5
+
+
+# member list at base level
+def test_render_member():
+ _verify_render(
+ FakeObject("test", listing=["one", "two"]),
+ 'onetwo',
+ )
+
+
+def test_parse_member():
+ o = _refreshed_object()
+
+ assert o.listing == ["first", "second"]
+
+
+# exist at base level
+def test_render_exist():
+ _verify_render(
+ FakeObject("test", pb1=True), '',
+ )
+
+
+def test_parse_exists():
+ o = _refreshed_object()
+
+ assert o.pb1
+ assert not o.pb2
+
+
+# yesno at base level
+def test_render_yesno():
+ _verify_render(
+ FakeObject("test", disabled=True),
+ 'yes',
+ )
+
+
+def test_parse_yesno():
+ o = _refreshed_object()
+
+ assert o.disabled
+
+
+# attrib
+def test_render_attrib():
+ _verify_render(
+ FakeObject("test", uuid="123-456"), '',
+ )
+
+
+def test_parse_attrib():
+ o = _refreshed_object()
+
+ assert o.uuid == "123-456"
+
+
+# int at depth 1
+def test_render_d1_int():
+ _verify_render(
+ FakeObject("test", age=12),
+ '12',
+ )
+
+
+def test_parse_d1_int():
+ o = _refreshed_object()
+
+ assert o.age == 12
+
+
+# member list at depth 1
+def test_render_d1_member():
+ _verify_render(
+ FakeObject("test", interfaces=["third", "fourth"]),
+ "".join(
+ [
+ '',
+ "thirdfourth",
+ "",
+ ]
+ ),
+ )
+
+
+def test_parse_d1_member():
+ o = _refreshed_object()
+
+ assert o.interfaces == ["third", "fourth"]
+
+
+# uuid at depth 1
+def test_render_d1_attrib_standalone():
+ _verify_render(
+ FakeObject("test", uuid2="456-789"),
+ '',
+ )
+
+
+def test_render_d1_attrib_mixed():
+ _verify_render(
+ FakeObject("test", uuid2="456-789", age=12),
+ '12',
+ )
+
+
+def test_parse_d1_attrib():
+ o = _refreshed_object()
+
+ assert o.uuid2 == "456-789"
+
+
+# should raise an exception
+def test_update_attrib_raises_not_implemented_exception():
+ o = _refreshed_object()
+
+ with pytest.raises(NotImplementedError):
+ o.update("uuid")