diff --git a/HISTORY.rst b/HISTORY.rst
index 300c93a6..22dc0921 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,6 +3,25 @@
History
=======
+0.14.0
+------
+
+Released: 2020-01-14
+
+Status: Alpha
+
+New Classes:
+
+- `objects.DynamicUserGroup`
+- `policies.PolicyBasedForwarding`
+
+Other Updates:
+
+- Added dynamic user group (DUG) support to the userid namespace
+- Fixes to `network.AggregateInterface`
+- Removed default value from `network.IkeGateway.peer_id_check`
+- Docstring updates
+
0.13.0
------
diff --git a/pandevice/__init__.py b/pandevice/__init__.py
index 778d61a4..f7f100d8 100755
--- a/pandevice/__init__.py
+++ b/pandevice/__init__.py
@@ -23,7 +23,7 @@
__author__ = 'Palo Alto Networks'
__email__ = 'techpartners@paloaltonetworks.com'
-__version__ = '0.13.0'
+__version__ = '0.14.0'
import logging
diff --git a/pandevice/device.py b/pandevice/device.py
index 2cac9443..36bef3aa 100644
--- a/pandevice/device.py
+++ b/pandevice/device.py
@@ -118,6 +118,7 @@ class Vsys(VersionedPanObject):
"objects.SecurityProfileGroup",
"objects.CustomUrlCategory",
"objects.LogForwardingProfile",
+ "objects.DynamicUserGroup",
"policies.Rulebase",
"network.EthernetInterface",
"network.AggregateInterface",
diff --git a/pandevice/firewall.py b/pandevice/firewall.py
index 52194fac..9c5654fc 100644
--- a/pandevice/firewall.py
+++ b/pandevice/firewall.py
@@ -83,6 +83,7 @@ class Firewall(PanDevice):
"objects.SecurityProfileGroup",
"objects.CustomUrlCategory",
"objects.LogForwardingProfile",
+ "objects.DynamicUserGroup",
"policies.Rulebase",
"network.EthernetInterface",
"network.AggregateInterface",
diff --git a/pandevice/network.py b/pandevice/network.py
index a55260a1..a372436f 100644
--- a/pandevice/network.py
+++ b/pandevice/network.py
@@ -736,7 +736,7 @@ class Layer3Subinterface(Subinterface):
management_profile (ManagementProfile): Interface Management Profile
mtu(int): MTU for interface
adjust_tcp_mss (bool): Adjust TCP MSS
- netflow_profile (NetflowProfile): Netflow profile
+ netflow_profile (str): Netflow profile
comment (str): The interface's comment
ipv4_mss_adjust(int): TCP MSS adjustment for ipv4
ipv6_mss_adjust(int): TCP MSS adjustment for ipv6
@@ -826,7 +826,7 @@ class Layer2Subinterface(Subinterface):
tag (int): Tag for the interface, aka vlan id
lldp_enabled (bool): Enable LLDP
lldp_profile (str): Reference to an lldp profile
- netflow_profile_l2 (NetflowProfile): Reference to a netflow profile
+ netflow_profile_l2 (str): Netflow profile
comment (str): The interface's comment
"""
@@ -934,10 +934,10 @@ class EthernetInterface(PhysicalInterface):
Profile
mtu(int): Layer3: MTU for interface
adjust_tcp_mss (bool): Layer3: Adjust TCP MSS
- netflow_profile (NetflowProfile): Netflow profile
+ netflow_profile (str): Netflow profile
lldp_enabled (bool): Layer2: Enable LLDP
lldp_profile (str): Layer2: Reference to an lldp profile
- netflow_profile_l2 (NetflowProfile): Netflow profile
+ netflow_profile_l2 (str): Netflow profile
link_speed (str): Link speed: eg. auto, 10, 100, 1000
link_duplex (str): Link duplex: eg. auto, full, half
link_state (str): Link state: eg. auto, up, down
@@ -1109,7 +1109,7 @@ class AggregateInterface(PhysicalInterface):
management_profile (ManagementProfile): Layer3: Interface Management Profile
mtu(int): Layer3: MTU for interface
adjust_tcp_mss (bool): Layer3: Adjust TCP MSS
- netflow_profile (NetflowProfile): Netflow profile
+ netflow_profile (str): Netflow profile
lldp_enabled (bool): Enable LLDP
lldp_profile (str): Reference to an lldp profile
comment (str): The interface's comment
@@ -1171,13 +1171,15 @@ def _setup(self):
vartype='yesno', path='{mode}/adjust-tcp-mss/enable')
params.append(VersionedParamPath(
'netflow_profile',
- condition={'mode': ['layer3', 'layer2', 'vwire']},
+ condition={'mode': ['layer3', 'layer2', 'virtual-wire']},
path='{mode}/netflow-profile'))
params.append(VersionedParamPath(
- 'lldp_enabled', condition={'mode': ['layer3', 'layer2', 'vwire']},
+ 'lldp_enabled',
+ condition={'mode': ['layer3', 'layer2', 'virtual-wire']},
path='{mode}/lldp/enable', vartype='yesno'))
params.append(VersionedParamPath(
- 'lldp_profile', condition={'mode': ['layer3', 'layer2', 'vwire']},
+ 'lldp_profile',
+ condition={'mode': ['layer3', 'layer2', 'virtual-wire']},
path='{mode}/lldp/profile'))
params.append(VersionedParamPath(
'comment', path='comment'))
@@ -1231,7 +1233,7 @@ class VlanInterface(Interface):
management_profile (ManagementProfile): Interface Management Profile
mtu(int): MTU for interface
adjust_tcp_mss (bool): Adjust TCP MSS
- netflow_profile (NetflowProfile): Netflow profile
+ netflow_profile (str): Netflow profile
comment (str): The interface's comment
ipv4_mss_adjust(int): TCP MSS adjustment for ipv4
ipv6_mss_adjust(int): TCP MSS adjustment for ipv6
@@ -1337,7 +1339,7 @@ class LoopbackInterface(Interface):
management_profile (ManagementProfile): Interface Management Profile
mtu(int): MTU for interface
adjust_tcp_mss (bool): Adjust TCP MSS
- netflow_profile (NetflowProfile): Netflow profile
+ netflow_profile (str): Netflow profile
comment (str): The interface's comment
ipv4_mss_adjust(int): TCP MSS adjustment for ipv4
ipv6_mss_adjust(int): TCP MSS adjustment for ipv6
@@ -1399,7 +1401,7 @@ class TunnelInterface(Interface):
ipv6_enabled (bool): IPv6 Enabled (requires IPv6Address child object)
management_profile (ManagementProfile): Interface Management Profile
mtu(int): MTU for interface
- netflow_profile (NetflowProfile): Netflow profile
+ netflow_profile (str): Netflow profile
comment (str): The interface's comment
"""
@@ -3060,7 +3062,7 @@ def _setup(self):
params.append(VersionedParamPath(
'peer_id_value', path='peer-id/id'))
params.append(VersionedParamPath(
- 'peer_id_check', default='exact',
+ 'peer_id_check',
values=('exact', 'wildcard'), path='peer-id/matching'))
params.append(VersionedParamPath(
'local_cert', condition={'auth_type': 'certificate'},
diff --git a/pandevice/objects.py b/pandevice/objects.py
index 87ad31bd..dbd2edc5 100644
--- a/pandevice/objects.py
+++ b/pandevice/objects.py
@@ -702,3 +702,35 @@ def _setup(self):
condition={'action_type': 'tagging'})
self._params = tuple(params)
+
+
+class DynamicUserGroup(VersionedPanObject):
+ """Dynamic user group.
+
+ Note: PAN-OS 9.1+
+
+ Args:
+ name: Name of the dynamic user group
+ description (str): Description of this object
+ filter: Tag-based filter.
+ tag (list): Administrative tags
+
+ """
+ ROOT = Root.VSYS
+ SUFFIX = ENTRY
+
+ def _setup(self):
+ # xpaths
+ self._xpaths.add_profile(value='/dynamic-user-group')
+
+ # params
+ params = []
+
+ params.append(VersionedParamPath(
+ 'description', path='description'))
+ params.append(VersionedParamPath(
+ 'filter', path='filter'))
+ params.append(VersionedParamPath(
+ 'tag', path='tag', vartype='member'))
+
+ self._params = tuple(params)
diff --git a/pandevice/policies.py b/pandevice/policies.py
index 5135c2a2..33a36666 100644
--- a/pandevice/policies.py
+++ b/pandevice/policies.py
@@ -39,8 +39,9 @@ class Rulebase(VersionedPanObject):
"""
ROOT = Root.VSYS
CHILDTYPES = (
- "policies.SecurityRule",
"policies.NatRule",
+ "policies.PolicyBasedForwarding",
+ "policies.SecurityRule",
)
def _setup(self):
@@ -112,6 +113,7 @@ class SecurityRule(VersionedPanObject):
(applies to panorama/device groups only)
target (list): Apply this policy to the listed firewalls only
(applies to panorama/device groups only)
+ uuid (str): (PAN-OS 9.0+) The UUID for this rule.
"""
# TODO: Add QoS variables
@@ -249,6 +251,7 @@ class NatRule(VersionedPanObject):
target (list): Apply this policy to the listed firewalls only
(applies to panorama/device groups only)
tag (list): Administrative tags
+ uuid (str): (PAN-OS 9.0+) The UUID for this rule.
"""
SUFFIX = ENTRY
@@ -442,3 +445,141 @@ def _setup(self):
vartype='attrib', path='uuid')
self._params = tuple(params)
+
+
+class PolicyBasedForwarding(VersionedPanObject):
+ """PBF rule.
+
+ Args:
+ description (str): The descripton
+ tags (str/list): List of tags
+ from_type (str): Source from type. Valid values are 'zone' (default)
+ or 'interface'.
+ from_values (str/list): The source values for the given type.
+ source_addresses (str/list): List of source IP addresses.
+ source_users (str/list): List of source users.
+ negate_source (bool): Set to negate the source.
+ destination_addresses (str/list): List of destination addresses.
+ negate_destination (bool): Set to negate the destination.
+ applications (str/list): List of applications.
+ services (str/list): List of services.
+ schedule (str): The schedule.
+ disabled (bool): Set to disable this rule.
+ action (str): The action to take. Valid values are 'forward'
+ (default), 'forward-to-vsys', 'discard', or 'no-pbf'.
+ forward_vsys (str): The vsys to forward to if action is set to
+ forward to a vsys.
+ forward_egress_interface (str): The egress interface.
+ forward_next_hop_type (str): The next hop type. Valid values
+ are 'ip-address', 'fqdn', or None (default).
+ forward_next_hop_value (str): The next hop value if the forward
+ next hop type is not None.
+ forward_monitor_profile (str): The monitor profile to use.
+ forward_monitor_ip_address (str): The monitor IP address.
+ forward_monitor_disable_if_unreachable (bool): Set to disable
+ this rule if nexthop / monitor IP is unreachable.
+ enable_enforce_symmetric_return (bool): Set to enforce
+ symmetric return.
+ symmetric_return_addresses (str/list): List of symmetric return
+ addresses.
+ target (list): Apply this policy to the listed firewalls only
+ (applies to panorama/device groups only)
+ negate_target (bool): Target all but the listed target firewalls
+ (applies to panorama/device groups only)
+ uuid (str): (PAN-OS 9.0+) The UUID for this rule.
+
+ """
+ SUFFIX = ENTRY
+ ROOT = Root.VSYS
+
+ def _setup(self):
+ # xpaths
+ self._xpaths.add_profile(value='/pbf/rules')
+
+ # params
+ params = []
+
+ params.append(VersionedParamPath(
+ 'description', path='description'))
+ params.append(VersionedParamPath(
+ 'tags', vartype='member', path='tag'))
+ params.append(VersionedParamPath(
+ 'from_type', default='zone',
+ values=['zone', 'interface'], path='from/{from_type}'))
+ params.append(VersionedParamPath(
+ 'from_value', vartype='member',
+ path='from/{from_type}'))
+ params.append(VersionedParamPath(
+ 'source_addresses', vartype='member', path='source'))
+ params.append(VersionedParamPath(
+ 'source_users', vartype='member', path='source-user'))
+ params.append(VersionedParamPath(
+ 'negate_source', vartype='yesno', path='negate-source'))
+ params.append(VersionedParamPath(
+ 'destination_addresses', vartype='member', path='destination'))
+ params.append(VersionedParamPath(
+ 'negate_destination', vartype='yesno', path='negate-destination'))
+ params.append(VersionedParamPath(
+ 'applications', vartype='member', path='application'))
+ params.append(VersionedParamPath(
+ 'services', vartype='member', path='service'))
+ params.append(VersionedParamPath(
+ 'schedule', path='schedule'))
+ params.append(VersionedParamPath(
+ 'disabled', vartype='yesno', path='disabled'))
+ params.append(VersionedParamPath(
+ 'action', default='forward',
+ values=['forward', 'forward-to-vsys', 'discard', 'no-pbf'],
+ path='action/{action}'))
+ params.append(VersionedParamPath(
+ 'forward_vsys',
+ condition={'action': 'forward-to-vsys'},
+ path='action/{action}/forward-to-vsys'))
+ params.append(VersionedParamPath(
+ 'forward_egress_interface',
+ condition={'action': 'forward'},
+ path='action/{action}/egress-interface'))
+ params.append(VersionedParamPath(
+ 'forward_next_hop_type',
+ condition={'action': 'forward'},
+ values=['ip-address', 'fqdn', None],
+ path='action/{action}/nexthop/{forward_next_hop_type}'))
+ params.append(VersionedParamPath(
+ 'forward_next_hop_value',
+ condition={
+ 'action': 'forward',
+ 'forward_next_hop_type': ['ip-address', 'fqdn'],
+ },
+ path='action/{action}/nexthop/{forward_next_hop_type}'))
+ params.append(VersionedParamPath(
+ 'forward_monitor_profile',
+ condition={'action': 'forward'},
+ path='action/{action}/monitor/profile'))
+ params.append(VersionedParamPath(
+ 'forward_monitor_ip_address',
+ condition={'action': 'forward'},
+ path='action/{action}/monitor/ip-address'))
+ params.append(VersionedParamPath(
+ 'forward_monitor_disable_if_unreachable', vartype='yesno',
+ condition={'action': 'forward'},
+ path='action/{action}/monitor/disable-if-unreachable'))
+ params.append(VersionedParamPath(
+ 'enable_enforce_symmetric_return', vartype='yesno',
+ path='enforce-symmetric-return/enabled'))
+ params.append(VersionedParamPath(
+ 'symmetric_return_addresses', vartype='entry',
+ path='enforce-symmetric-return/nexthop-address-list'))
+ params.append(VersionedParamPath(
+ 'active_active_device_binding',
+ path='active-active-device-binding'))
+ params.append(VersionedParamPath(
+ 'target', vartype='entry', path='target/devices'))
+ params.append(VersionedParamPath(
+ 'negate_target', vartype='yesno', path='target/negate'))
+ params.append(VersionedParamPath(
+ 'uuid', exclude=True))
+ params[-1].add_profile(
+ '9.0.0',
+ vartype='attrib', path='uuid')
+
+ self._params = tuple(params)
diff --git a/pandevice/userid.py b/pandevice/userid.py
index b214ea88..9fac34f2 100644
--- a/pandevice/userid.py
+++ b/pandevice/userid.py
@@ -450,3 +450,275 @@ def audit_registered_ip(self, ip_tags_pairs):
for ip, tags in requested_list.items():
self.register(ip, tags)
self.batch_end()
+
+ def set_group(self, group, users):
+ """
+ Set a group's membership to the specified users.
+
+ This method can be batched with batch_start() and batch_end().
+
+ Args:
+ group: The group name.
+ users (list): The users to be in this group.
+
+ """
+ root, payload = self._create_uidmessage()
+
+ # Find the groups section.
+ groups = payload.find('./groups')
+ if groups is None:
+ groups = ET.SubElement(payload, 'groups')
+
+ # Find the group.
+ entries = groups.findall('./entry')
+ for entry in entries:
+ if entry.attrib['name'] == group:
+ ge = entry.find('./members')
+ break
+ else:
+ entry = ET.SubElement(groups, 'entry', {'name': group})
+ ge = ET.SubElement(entry, 'members')
+
+ # Now add in the users to this group.
+ for user in users:
+ ET.SubElement(ge, 'entry', {'name': user})
+
+ # Done.
+ self.send(root)
+
+ def get_groups(self, style=None):
+ """
+ Get a list of groups.
+
+ Args:
+ style: The type of groups to retrieve. If unspecified, returns a list of
+ all groups. Can be "custom-group", "dynamic", or "xmlapi".
+
+ Returns:
+ list
+
+ """
+ msg = ['', ]
+ if style is not None:
+ msg.append("".format(style))
+ msg.append('
')
+ cmd = ''.join(msg)
+ vsys = self.device.vsys or 'vsys1'
+
+ resp = self.device.op(cmd, vsys=self.device.vsys, cmd_xml=False)
+ if resp is None:
+ return
+
+ '''
+ Example returned XML:
+
+ 9.1:
+
+
+
+ '''
+
+ data = resp.find('./result')
+ if data is None:
+ return
+
+ lines = data.text.split('\n')
+ ans = []
+ for line in lines:
+ if line.startswith('Total: '):
+ break
+ val = line.strip()
+ if val:
+ ans.append(val)
+
+ return ans
+
+ def get_group_members(self, group):
+ """
+ Returns a list of users in the given group.
+
+ Args:
+ group: The name of the group.
+
+ Returns:
+ list
+
+ """
+ cmd = (
+ '' +
+ group +
+ ''
+ )
+ vsys = self.device.vsys or 'vsys1'
+
+ resp = self.device.op(cmd, vsys=vsys, cmd_xml=False)
+ if resp is None:
+ return
+
+ '''
+ Example returned XML:
+
+ 9.1:
+
+
+ '''
+
+ data = resp.find('./result')
+ if data is None:
+ return
+
+ lines = data.text.split('\n')
+ ans = [x.split(']')[1].strip() for x in lines if len(x.split(']')) == 2]
+
+ return ans
+
+ def get_user_tags(self, user=None, prefix=None):
+ """
+ Get the dynamic user tags.
+
+ Note: PAN-OS 9.1+
+
+ Args:
+ user: Get only this user's tags, not all users and all tags.
+ prefix: Override class tag prefix.
+
+ Returns:
+ dict: Dict where the user is the key and the value is a list of tags.
+
+ """
+ if prefix is None:
+ prefix = self.prefix
+
+ limit = 500
+ start = 1
+ start_elm = None
+ msg = ['')
+
+ cmd = ET.fromstring(''.join(msg))
+ if user is None:
+ start_elm = cmd.find('./object/registered-user/all/start-point')
+
+ ans = {}
+ while True:
+ resp = self.device.op(cmd=ET.tostring(cmd, encoding='utf-8'),
+ vsys=self.device.vsys, cmd_xml=False)
+ entries = resp.findall('./result/entry')
+ for entry in entries:
+ key = entry.attrib['user']
+ val = []
+ members = entry.findall('./tag/member')
+ for member in members:
+ tag = member.text
+ if not prefix or tag.startswith(prefix):
+ val.append(tag)
+ ans[key] = val
+
+ if start_elm is None or limit <= 0 or len(entries) < limit:
+ break
+
+ start += len(entries)
+ start_elm.text = '{0}'.format(start)
+
+ # Done.
+ return ans
+
+ def tag_user(self, user, tags, timeout=None, prefix=None):
+ """
+ Tags the user with the specified tags.
+
+ This method can be batched with batch_start() and batch_end().
+
+ Note: PAN-OS 9.1+
+
+ Args:
+ user: The user.
+ tags (list): The list of tags to apply.
+ timeout (int): (Optional) The timeout for the given tags.
+ prefix: Override class tag prefix.
+
+ """
+ if timeout is not None:
+ timeout = int(timeout)
+
+ if prefix is None:
+ prefix = self.prefix or ''
+
+ root, payload = self._create_uidmessage()
+
+ # Find the register user tags section.
+ ru = payload.find('./register-user')
+ if ru is None:
+ ru = ET.SubElement(payload, 'register-user')
+
+ # Find the tags section for this specific user.
+ entries = ru.findall('./entry')
+ for entry in entries:
+ if entry.attrib['name'] == user:
+ te = entry.find('./tag')
+ break
+ else:
+ entry = ET.SubElement(ru, 'entry', {'user': user, })
+ te = ET.SubElement(entry, 'tag')
+
+ # Now add in the tags with the specified timeout.
+ props = {}
+ if timeout is not None:
+ props['timeout'] = '{0}'.format(timeout)
+ for tag in tags:
+ ET.SubElement(te, 'member', props).text = prefix + tag
+
+ # Done.
+ self.send(root)
+
+ def untag_user(self, user, tags=None, prefix=None):
+ """
+ Removes tags associated with a user.
+
+ This method can be batched with batch_start() and batch_end().
+
+ Note: PAN-OS 9.1+
+
+ Args:
+ user: The user.
+ tags (list): (Optional) Remove only these tags instead of all tags.
+ prefix: Override class tag prefix.
+
+ """
+ root, payload = self._create_uidmessage()
+
+ if prefix is None:
+ prefix = self.prefix or ''
+
+ # Find the unregister user tags section.
+ uu = payload.find('./unregister-user')
+ if uu is None:
+ uu = ET.SubElement(payload, 'unregister-user')
+
+ # Find the tags section for this specific user.
+ entries = uu.findall('./entry')
+ for entry in entries:
+ if entry.attrib['name'] == user:
+ break
+ else:
+ entry = ET.SubElement(uu, 'entry', {'user': user, })
+
+ # Do tag removal.
+ te = entry.find('./tag')
+ if tags is not None:
+ if te is None:
+ te = ET.SubElement(entry, 'tag')
+ for tag in tags:
+ ET.SubElement(te, 'member').text = prefix + tag
+ elif te is not None:
+ entry.remove(te)
+
+ # Done.
+ self.send(root)
diff --git a/setup.py b/setup.py
index d869e3df..83e55c45 100755
--- a/setup.py
+++ b/setup.py
@@ -23,7 +23,7 @@
setup_kwargs = dict(
name='pandevice',
- version='0.13.0',
+ version='0.14.0',
description='Framework for interacting with Palo Alto Networks devices via API',
long_description='The Palo Alto Networks Device Framework is a way to interact with Palo Alto Networks devices (including Next-generation Firewalls and Panorama) using the device API that is object oriented and conceptually similar to interaction with the device via the GUI or CLI.',
author='Palo Alto Networks',