From ccdfbcb2535ff849126d1c1acf5537977a1d9fdc Mon Sep 17 00:00:00 2001 From: Garfield Freeman Date: Thu, 12 Aug 2021 11:32:29 -0700 Subject: [PATCH] feat: Add hierarchy retrieval info This feature is to help users understand the various pan-os-python object hierarchies available for a given object. --- panos/__init__.py | 50 +++++++++++++++++++++ panos/base.py | 97 +++++++++++++++++++++++++++++++++++++++++ panos/device.py | 2 + panos/firewall.py | 2 + panos/panorama.py | 28 ++++++++++++ panos/predefined.py | 6 +++ tests/test_standards.py | 51 +++++++++++++++++----- 7 files changed, 226 insertions(+), 10 deletions(-) diff --git a/panos/__init__.py b/panos/__init__.py index 14cb5e57..6e25eb41 100755 --- a/panos/__init__.py +++ b/panos/__init__.py @@ -459,3 +459,53 @@ def node_color(module): return nodecolor[module] except KeyError: return "" + + +def object_classes(): + import inspect + from panos import errors + from panos import base + + current_module = sys.modules[__name__] + + omits = [] + for pkg in (current_module, errors, base): + for name, the_cls in inspect.getmembers(pkg, inspect.isclass): + if not the_cls.__module__.startswith("panos"): + continue + if the_cls not in omits: + omits.append(the_cls) + + from panos import device + from panos import firewall + from panos import ha + from panos import network + from panos import objects + from panos import panorama + from panos import policies + from panos import predefined + + classes = {} + for pkg in (device, firewall, ha, network, objects, panorama, policies, predefined): + for name, the_cls in inspect.getmembers(pkg, inspect.isclass): + if not the_cls.__module__.startswith("panos"): + continue + if the_cls in omits: + continue + if getattr(the_cls, "IS_BASE_CLASS", False): + continue + classes[childtype_name(the_cls)] = the_cls + + return classes + + +def childtype_name(cls): + return "{0}.{1}".format(cls.__module__.split(".")[1], cls.__name__) + + +def parents_for(cls, classes): + return [ + x + for ctn, x in classes.items() + if childtype_name(cls) in getattr(x, "CHILDTYPES", []) + ] diff --git a/panos/base.py b/panos/base.py index efc0ca0b..adfac7ab 100644 --- a/panos/base.py +++ b/panos/base.py @@ -2078,6 +2078,103 @@ def retrieve_panos_version(self): return panos_version + def hierarchy_info(self): + """This function returns hierarchical information about this object. + + All objects in pan-os-python can be added as children to other objects, + so this function details what configurations are valid for this + particular object. + + Returns: + dict: Hierarchy information about this object. + """ + from panos.firewall import Firewall + from panos.panorama import DeviceGroup + from panos.panorama import Panorama + from panos.panorama import Template + from panos.panorama import TemplateStack + + ans = { + "configurations": [], + "valid": False, + } + + classes = panos.object_classes() + configs = [ + [self.__class__,], + ] + updated_configs = [] + + # Find all possible config trees. + while True: + for num, chain in enumerate(configs): + parents = panos.parents_for(chain[-1], classes) + if parents: + configs.pop(num) + for p in parents: + configs.append( + chain + [p,] + ) + break + else: + break + + # Because Firewall objects can be children of Panorama objects, + # we need to do another pass to check for multi-PanDevice configs + # because Panorama is not strictly necessary. + for num in range(len(configs)): + chain = configs[num] + if Firewall in chain and Panorama in chain: + configs.append(chain[: chain.index(Firewall) + 1]) + + # Remove dupes. + updated_configs = [] + for chain in configs: + if chain not in updated_configs: + updated_configs.append(chain) + configs = updated_configs + + # Remove any DeviceGroup > Firewall hierarchies. + updated_configs = [] + for chain in configs: + fw_index = -1 + dg_index = -1 + for num, x in enumerate(chain): + if x == Firewall: + fw_index = num + elif x == DeviceGroup: + dg_index = num + if fw_index == -1 or dg_index == -1 or fw_index + 1 != dg_index: + updated_configs.append(chain) + configs = updated_configs + + # Remove Template / TemplateStack hierarchies if there is a DeviceGroup + # hierarchy. + for chain in configs: + if DeviceGroup in chain: + configs = [ + x for x in configs if Template not in x and TemplateStack not in x + ] + break + + # Get the current config tree. + cur_tree = [] + p = self + while p is not None: + cur_tree.append(p) + p = p.parent + + # Reverse the trees to match reality. + for x in configs: + x.reverse() + cur_tree.reverse() + + return { + "configurations": configs, + "current": cur_tree, + "valid": cur_tree in configs, + } + class VersioningSupport(object): """A class that supports getting version specific values of something. diff --git a/panos/device.py b/panos/device.py index d179e6c6..9302ddbe 100644 --- a/panos/device.py +++ b/panos/device.py @@ -148,6 +148,8 @@ class Vsys(VersionedPanObject): "network.Vlan", "network.VirtualRouter", "network.VirtualWire", + "network.Layer2Subinterface", + "network.Layer3Subinterface", "network.Zone", ) diff --git a/panos/firewall.py b/panos/firewall.py index 2a0f0488..ad0f98df 100644 --- a/panos/firewall.py +++ b/panos/firewall.py @@ -99,6 +99,8 @@ class Firewall(PanDevice): "network.LoopbackInterface", "network.TunnelInterface", "network.VlanInterface", + "network.Layer2Subinterface", + "network.Layer3Subinterface", "network.Vlan", "network.VirtualRouter", "network.ManagementProfile", diff --git a/panos/panorama.py b/panos/panorama.py index ff1c64bc..1231cca2 100644 --- a/panos/panorama.py +++ b/panos/panorama.py @@ -176,6 +176,7 @@ class Template(VersionedPanObject): SUFFIX = ENTRY CHILDTYPES = ( "device.Vsys", + "device.VsysResources", "device.SystemSettings", "device.LogSettingsSystem", "device.LogSettingsConfig", @@ -197,6 +198,11 @@ class Template(VersionedPanObject): "network.IpsecCryptoProfile", "network.IkeCryptoProfile", "network.GreTunnel", + "network.Zone", + "network.IpsecTunnelIpv4ProxyId", + "network.IpsecTunnelIpv6ProxyId", + "network.Layer2Subinterface", + "network.Layer3Subinterface", "panorama.TemplateVariable", ) @@ -280,6 +286,11 @@ class TemplateStack(VersionedPanObject): "network.IpsecCryptoProfile", "network.IkeCryptoProfile", "network.GreTunnel", + "network.Zone", + "network.IpsecTunnelIpv4ProxyId", + "network.IpsecTunnelIpv6ProxyId", + "network.Layer2Subinterface", + "network.Layer3Subinterface", "panorama.TemplateVariable", ) @@ -392,11 +403,28 @@ class Panorama(base.PanDevice): "device.HttpServerProfile", "device.CertificateProfile", "device.SslDecrypt", + "objects.AddressObject", + "objects.AddressGroup", + "objects.ServiceObject", + "objects.ServiceGroup", + "objects.Tag", + "objects.ApplicationObject", + "objects.ApplicationGroup", + "objects.ApplicationFilter", + "objects.ApplicationContainer", + "objects.ScheduleObject", + "objects.SecurityProfileGroup", + "objects.CustomUrlCategory", + "objects.LogForwardingProfile", + "objects.DynamicUserGroup", + "objects.Region", + "objects.Edl", "firewall.Firewall", "panorama.DeviceGroup", "panorama.Template", "panorama.TemplateStack", "plugins.CloudServicesPlugin", + "policies.Rulebase", ) def __init__( diff --git a/panos/predefined.py b/panos/predefined.py index fdbaafa6..c522792d 100644 --- a/panos/predefined.py +++ b/panos/predefined.py @@ -45,6 +45,12 @@ class Predefined(object): XPATH = "/config/predefined" SINGLE_ENTRY_XPATH = "/entry[@name='{0}']" ALL_ENTRIES_XPATH = "/entry" + CHILDTYPES = ( + "objects.ApplicationContainer", + "objects.ApplicationObject", + "objects.ServiceObject", + "objects.Tag", + ) def __init__(self, device=None, *args, **kwargs): # Create a class logger diff --git a/tests/test_standards.py b/tests/test_standards.py index e39b0156..bad3d5b1 100644 --- a/tests/test_standards.py +++ b/tests/test_standards.py @@ -208,11 +208,6 @@ def inst(cls): return cls() -def childtype_string(cls): - """Get this class as a string suitable for a PanObject.CHILDTYPES entry.""" - return "{0}.{1}".format(cls.__module__.split(".")[1], cls.__name__) - - def versions(): val = MIN_PANOS_VERSION while True: @@ -295,7 +290,7 @@ def test_firewall_object_childtypes(panobj): if panobj.__module__ == "panos.panorama": pytest.skip("Skipping panorama specific classes for firewall test") - cts = childtype_string(panobj) + cts = panos.childtype_name(panobj) if cts in PANORAMA_OBJECTS: pytest.skip("Skipping Panoroama-only objects") @@ -315,7 +310,7 @@ def test_object_with_vsys_root_is_in_vsys_childtypes(panobj): if panobj.__module__ == "panos.panorama": pytest.skip("Skipping panorama specific classes for firewall test") - cts = childtype_string(panobj) + cts = panos.childtype_name(panobj) if panobj.ROOT != base.Root.VSYS: pytest.skip("Not a vsys object") @@ -331,7 +326,7 @@ def test_object_with_vsys_root_is_in_firewall_childtypes(panobj): if panobj.__module__ == "panos.panorama": pytest.skip("Skipping panorama specific classes for firewall test") - cts = childtype_string(panobj) + cts = panos.childtype_name(panobj) if panobj.ROOT != base.Root.VSYS: pytest.skip("Not a vsys object") @@ -347,7 +342,7 @@ def test_object_with_non_vsys_root_is_not_in_vsys_childtypes(panobj): if panobj.__module__ == "panos.panorama": pytest.skip("Skipping panorama specific classes for firewall test") - cts = childtype_string(panobj) + cts = panos.childtype_name(panobj) if hasattr(panobj, "ALWAYS_IMPORT"): pytest.skip("Skipping importable object") @@ -363,7 +358,7 @@ def test_vsys_importable_childtypes(panobj): if panobj.__module__ == "panos.panorama": pytest.skip("Skipping panorama specific classes for firewall test") - cts = childtype_string(panobj) + cts = panos.childtype_name(panobj) if not hasattr(panobj, "ALWAYS_IMPORT"): pytest.skip("Skipping standard object") @@ -460,3 +455,39 @@ def test_policy_rule_is_in_all_rulebase_childtypes(policy_rule): for cls in [policies.Rulebase, policies.PreRulebase, policies.PostRulebase]: assert cts in cls.CHILDTYPES + + +def test_parent_aware_children_show_in_parent_childtypes(versioned_object): + obj = inst(versioned_object) + + classes = set([]) + for combo in obj._xpaths.settings: + cls_str = combo[0] + if cls_str is None: + continue + + cls = None + for x in ( + device, + firewall, + ha, + network, + objects, + panorama, + policies, + predefined, + ): + if hasattr(x, cls_str): + cls = getattr(x, cls_str) + break + else: + assert False, "Could not find class {0}".format(cls_str) + + if cls is not None: + classes.add(cls) + + msg = "Child {0} has parent {1}, but does not show in parent's CHILDTYPES" + for cls in classes: + assert panos.childtype_name(versioned_object) in cls.CHILDTYPES, msg.format( + versioned_object, cls + )