Skip to content

Commit

Permalink
feat: Add normalized objects for firewall and Panorama commits
Browse files Browse the repository at this point in the history
PR #220 

This adds three new objects, `firewall.FirewallCommit`, `panorama.PanoramaCommit`, and `panorama.PanoramaCommitAll`.  These objects can be sent as the `cmd` argument to a `PanDevice.commit()` invocation.  These normalizations add missing commit styles, especially in the case of Panorama commit-all, and fixes how forced commits are performed on the firewall.
  • Loading branch information
shinmog authored Jul 16, 2020
1 parent f02143e commit 0983558
Show file tree
Hide file tree
Showing 3 changed files with 316 additions and 4 deletions.
23 changes: 19 additions & 4 deletions pandevice/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4463,21 +4463,26 @@ 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:
sync (bool): Block until the commit is finished (Default: False)
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,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand Down
72 changes: 72 additions & 0 deletions pandevice/firewall.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
225 changes: 225 additions & 0 deletions pandevice/panorama.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

0 comments on commit 0983558

Please sign in to comment.