diff --git a/pandevice/base.py b/pandevice/base.py index cbfbc1ab..d32aa2fa 100644 --- a/pandevice/base.py +++ b/pandevice/base.py @@ -4463,7 +4463,9 @@ def config_synced(self): # Commit methods - def commit(self, sync=False, exception=False, cmd=None, admins=None): + def commit( + self, sync=False, exception=False, cmd=None, admins=None, sync_all=False + ): """Trigger a commit Args: @@ -4471,13 +4473,16 @@ def commit(self, sync=False, exception=False, cmd=None, admins=None): exception (bool): Create an exception on commit errors (Default: False) cmd (str): Commit options in XML format admins (str/list): name or list of admins whose changes need to be committed + sync_all (bool): If this is a Panorama commit, wait for firewalls jobs to finish (Default: False) Returns: dict: Commit results """ self._logger.debug("Commit initiated on device: %s" % (self.id,)) - return self._commit(sync=sync, exception=exception, cmd=cmd, admins=admins) + return self._commit( + sync=sync, exception=exception, cmd=cmd, admins=admins, sync_all=sync_all + ) def _commit( self, @@ -4510,6 +4515,17 @@ def _commit( messages: list of warnings or errors """ + action = None + + # Adding in handling for the commit normalizations. + if ( + cmd is not None + and hasattr(cmd, "element") + and hasattr(cmd, "commit_action") + ): + action = cmd.commit_action + cmd = cmd.element() + # TODO: Support per-vsys commit if isinstance(cmd, pan.commit.PanCommit): cmd = cmd.cmd() @@ -4538,8 +4554,7 @@ def _commit( ) if commit_all: action = "all" - else: - action = None + self._logger.debug("Initiating commit") commit_response = self.xapi.commit( cmd=cmd, diff --git a/pandevice/firewall.py b/pandevice/firewall.py index 2a54623d..7046f644 100644 --- a/pandevice/firewall.py +++ b/pandevice/firewall.py @@ -502,3 +502,75 @@ def set_shared_policy_synced(self, sync_status): raise err.PanDeviceError( "Unknown shared policy status: %s" % str(sync_status) ) + + +class FirewallCommit(object): + """Normalization of a firewall commit.""" + + def __init__( + self, + description=None, + admins=None, + exclude_device_and_network=False, + exclude_shared_objects=False, + exclude_policy_and_objects=False, + force=False, + ): + self.description = description + self.admins = admins + if admins and not isinstance(admins, list): + raise ValueError("admins must be a list") + self.exclude_device_and_network = exclude_device_and_network + self.exclude_shared_objects = exclude_shared_objects + self.exclude_policy_and_objects = exclude_policy_and_objects + self.force = force + + @property + def commit_action(self): + return None + + def is_partial(self): + pp_list = [ + self.admins, + self.exclude_device_and_network, + self.exclude_shared_objects, + self.exclude_policy_and_objects, + self.force, + ] + + return any(x for x in pp_list) + + def element_str(self): + return ET.tostring(self.element(), encoding="utf-8") + + def element(self): + """Returns an xml representation of the commit requested. + + Returns: + xml.etree.ElementTree + """ + root = ET.Element("commit") + + if self.description: + ET.SubElement(root, "description").text = self.description + + if self.is_partial(): + partial = ET.Element("partial") + if self.admins: + e = ET.SubElement(partial, "admin") + for name in self.admins: + ET.SubElement(e, "member").text = name + if self.exclude_device_and_network: + ET.SubElement(partial, "device-and-network").text = "excluded" + if self.exclude_shared_objects: + ET.SubElement(partial, "shared-object").text = "excluded" + if self.exclude_policy_and_objects: + ET.SubElement(partial, "policy-and-objects").text = "excluded" + + if self.force: + fe = ET.SubElement(root, "force") + fe.append(partial) + else: + root.append(partial) + + return root diff --git a/pandevice/panorama.py b/pandevice/panorama.py index b5da2f3f..9733c0a8 100644 --- a/pandevice/panorama.py +++ b/pandevice/panorama.py @@ -395,6 +395,8 @@ def commit_all( ): """Trigger a commit-all (commit to devices) on Panorama + NOTE: Use the new panorama.PanoramaCommitAll with commit() instead. + Args: sync (bool): Block until the Panorama commit is finished (Default: False) sync_all (bool): Block until every Firewall commit is finished, requires sync=True (Default: False) @@ -748,3 +750,226 @@ def get_vm_auth_keys(self): ) return ans + + +class PanoramaCommit(object): + """Normalization of a Panorama commit.""" + + def __init__( + self, + description=None, + admins=None, + device_groups=None, + templates=None, + template_stacks=None, + wildfire_appliances=None, + wildfire_clusters=None, + log_collectors=None, + log_collector_groups=None, + exclude_device_and_network=False, + exclude_shared_objects=False, + force=False, + ): + largs = [ + "admins", + "device_groups", + "templates", + "template_stacks", + "wildfire_appliances", + "wildfire_clusters", + "log_collectors", + "log_collector_groups", + ] + for x in largs: + if locals()[x] is not None and not isinstance(locals()[x], list): + raise ValueError("{0} must be a list".format(x)) + self.description = description + self.admins = admins + self.device_groups = device_groups + self.templates = templates + self.template_stacks = template_stacks + self.wildfire_appliances = wildfire_appliances + self.wildfire_clusters = wildfire_clusters + self.log_collectors = log_collectors + self.log_collector_groups = log_collector_groups + self.exclude_device_and_network = exclude_device_and_network + self.exclude_shared_objects = exclude_shared_objects + self.force = force + + @property + def commit_action(self): + return None + + def is_partial(self): + pp_list = [ + self.admins, + self.device_groups, + self.templates, + self.template_stacks, + self.wildfire_appliances, + self.wildfire_clusters, + self.log_collectors, + self.log_collector_groups, + self.exclude_device_and_network, + self.exclude_shared_objects, + self.force, + ] + + return any(x for x in pp_list) + + def element_str(self): + return ET.tostring(self.element(), encoding="utf-8") + + def element(self): + """Returns an xml representation of the commit requested. + + Returns: + xml.etree.ElementTree + """ + root = ET.Element("commit") + + if self.description: + ET.SubElement(root, "description").text = self.description + + if self.is_partial(): + partial = ET.Element("partial") + mlist = [ + ("admin", self.admins), + ("device-group", self.device_groups), + ("template", self.templates), + ("template-stack", self.template_stacks), + ("wildfire-appliance", self.wildfire_appliances), + ("wildfire-appliance-cluster", self.wildfire_clusters), + ("log-collector", self.log_collectors), + ("log-collector-group", self.log_collector_groups), + ] + for loc, vals in mlist: + if vals: + e = ET.SubElement(partial, loc) + for name in vals: + ET.SubElement(e, "member").text = name + + if self.exclude_device_and_network: + ET.SubElement(partial, "device-and-network").text = "excluded" + if self.exclude_shared_objects: + ET.SubElement(partial, "shared-object").text = "excluded" + + if self.force: + fe = ET.SubElement(root, "force") + fe.append(partial) + else: + root.append(partial) + + return root + + +class PanoramaCommitAll(object): + """Normalization of a Panorama commit all.""" + + STYLE_DEVICE_GROUP = "device group" + STYLE_TEMPLATE = "template" + STYLE_TEMPLATE_STACK = "template stack" + STYLE_LOG_COLLECTOR_GROUP = "log collector group" + STYLE_WILDFIRE_APPLIANCE = "wildfire appliance" + STYLE_WILDFIRE_CLUSTER = "wildfire cluster" + + def __init__( + self, + style, + name, + description=None, + include_template=None, + force_template_values=None, + devices=None, + ): + if style and style not in ( + self.STYLE_DEVICE_GROUP, + self.STYLE_TEMPLATE, + self.STYLE_TEMPLATE_STACK, + self.STYLE_LOG_COLLECTOR_GROUP, + self.STYLE_WILDFIRE_APPLIANCE, + self.STYLE_WILDFIRE_CLUSTER, + ): + raise ValueError("Invalid style {0}".format(style)) + if devices and not isinstance(devices, list): + raise ValueError("devices must be a list") + + self.style = style + self.name = name + self.description = description + self.include_template = include_template + self.force_template_values = force_template_values + self.devices = devices + + @property + def commit_action(self): + return "all" + + def element_str(self): + return ET.tostring(self.element(), encoding="utf-8") + + def element(self): + """Returns an xml representation of the commit all. + + Returns: + xml.etree.ElementTree + """ + root = ET.Element("commit-all") + + body = None + if self.style == self.STYLE_DEVICE_GROUP: + body = ET.Element("shared-policy") + dgInfo = ET.SubElement(body, "device-group") + dge = ET.SubElement(dgInfo, "entry", {"name": self.name}) + if self.devices: + de = ET.SubElement(dge, "devices") + for x in self.devices: + ET.SubElement(de, "entry", {"name": x}) + if self.description: + ET.SubElement(body, "description").text = self.description + if self.include_template: + ET.SubElement(body, "include-template").text = "yes" + if self.force_template_values: + ET.SubElement(body, "force-template-values").text = "yes" + elif self.style == self.STYLE_TEMPLATE: + body = ET.Element("template") + ET.SubElement(body, "name").text = self.name + if self.description: + ET.SubElement(body, "description").text = self.description + if self.force_template_values: + ET.SubElement(body, "force-template-values").text = "yes" + if self.devices: + de = ET.SubElement(body, "device") + for x in self.devices: + ET.SubElement(de, "member").text = x + elif self.style == self.STYLE_TEMPLATE_STACK: + body = ET.Element("template-stack") + ET.SubElement(body, "name").text = self.name + if self.description: + ET.SubElement(body, "description").text = self.description + if self.force_template_values: + ET.SubElement(body, "force-template-values").text = "yes" + if self.devices: + de = ET.SubElement(body, "device") + for x in self.devices: + ET.SubElement(de, "member").text = x + elif self.style == self.STYLE_LOG_COLLECTOR_GROUP: + body = ET.Element("log-collector-config") + ET.SubElement(body, "log-collector-group").text = self.name + if self.description: + ET.SubElement(body, "description").text = self.description + elif self.style == self.STYLE_WILDFIRE_APPLIANCE: + body = ET.Element("wildfire-appliance-config") + if self.description: + ET.SubElement(body, "description").text = self.description + ET.SubElement(body, "wildfire-appliance").text = self.name + elif self.style == self.STYLE_WILDFIRE_CLUSTER: + body = ET.Element("wildfire-appliance-config") + if self.description: + ET.SubElement(body, "description").text = self.description + ET.SubElement(body, "wildfire-appliance-cluster").text = self.name + + if body is not None: + root.append(body) + + return root