From 0dedfcd70f338db6450ff29a97896daf6a90ba04 Mon Sep 17 00:00:00 2001 From: Garfield Lee Freeman Date: Thu, 27 Sep 2018 14:36:42 -0700 Subject: [PATCH] Adding connector for network/panos modules (#46142) --- .github/BOTMETA.yml | 2 + .../module_utils/network/panos/__init__.py | 0 .../module_utils/network/panos/panos.py | 315 ++++++++++++++++++ 3 files changed, 317 insertions(+) create mode 100644 lib/ansible/module_utils/network/panos/__init__.py create mode 100644 lib/ansible/module_utils/network/panos/panos.py diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 7c626c6191..3da7f1a062 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -554,6 +554,8 @@ files: maintainers: $team_onyx $module_utils/network/ordnance: maintainers: alexanderturner djh00t + $module_utils/network/panos: + maintainers: ivanbojer jtschichold shinmog $module_utils/network/routeros: maintainers: heuels $module_utils/network/slxos: diff --git a/lib/ansible/module_utils/network/panos/__init__.py b/lib/ansible/module_utils/network/panos/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/ansible/module_utils/network/panos/panos.py b/lib/ansible/module_utils/network/panos/panos.py new file mode 100644 index 0000000000..1777fcadcb --- /dev/null +++ b/lib/ansible/module_utils/network/panos/panos.py @@ -0,0 +1,315 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# Copyright (c) 2018 Palo Alto Networks techbizdev, +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +HAS_PANDEVICE = True +try: + from pandevice.base import PanDevice + from pandevice.panorama import DeviceGroup, Template, TemplateStack + from pandevice.policies import PreRulebase, PostRulebase, Rulebase + from pandevice.device import Vsys + from pandevice.errors import PanDeviceError +except ImportError: + HAS_PANDEVICE = False + + +class ConnectionHelper(object): + def __init__(self, panorama_error, firewall_error): + """Performs connection initialization and determines params.""" + # Params for AnsibleModule. + self.argument_spec = {} + self.required_one_of = [] + + # Params for pandevice tree construction. + self.vsys = None + self.device_group = None + self.vsys_dg = None + self.rulebase = None + self.template = None + self.template_stack = None + self.vsys_importable = None + self.panorama_error = panorama_error + self.firewall_error = firewall_error + + # The PAN-OS device. + self.device = None + + def get_pandevice_parent(self, module): + """Builds the pandevice object tree, returning the parent object. + + If pandevice is not installed, then module.fail_json() will be + invoked. + + Arguments: + * module(AnsibleModule): the ansible module. + + Returns: + * The parent pandevice object based on the spec given to + get_connection(). + """ + # Sanity check. + if not HAS_PANDEVICE: + module.fail_json(msg='Missing required library "pandevice".') + + d, host_arg = None, None + if module.params['provider'] and module.params['provider']['host']: + d = module.params['provider'] + host_arg = 'host' + elif module.params['ip_address'] is not None: + d = module.params + host_arg = 'ip_address' + else: + module.fail_json(msg='New or classic provider params are required.') + + # Create the connection object. + try: + self.device = PanDevice.create_from_device( + d[host_arg], d['username'], d['password'], d['api_key']) + except PanDeviceError as e: + module.fail_json(msg='Failed connection: {0}'.format(e)) + + parent = self.device + not_found = '{0} "{1}" is not present.' + if hasattr(self.device, 'refresh_devices'): + # Panorama connection. + # Error if Panorama is not supported. + if self.panorama_error is not None: + module.fail_json(msg=self.panorama_error) + + # Spec: template stack. + if self.template_stack is not None: + name = module.params[self.template_stack] + stacks = TemplateStack.refreshall(parent) + for ts in stacks: + if ts.name == name: + parent = ts + break + else: + module.fail_json(msg=not_found.format( + 'Template stack', name, + )) + + # Spec: template. + if self.template is not None: + name = module.params[self.template] + templates = Template.refreshall(parent) + for t in templates: + if t.name == name: + parent = t + break + else: + module.fail_json(msg=not_found.format( + 'Template', name, + )) + + # Spec: vsys importable. + if self.vsys_importable is not None: + name = module.params[self.vsys_importable] + if name is not None: + vo = Vsys(name) + parent.add(vo) + parent = vo + + # Spec: vsys_dg or device_group. + dg_name = self.vsys_dg or self.device_group + if dg_name is not None: + name = module.params[dg_name] + if name not in (None, 'shared'): + groups = DeviceGroup.refreshall(parent) + for dg in groups: + if dg.name == name: + parent = dg + break + else: + module.fail_json(msg=not_found.format( + 'Device group', name, + )) + + # Spec: rulebase. + if self.rulebase is not None: + if module.params[self.rulebase] in (None, 'pre-rulebase'): + rb = PreRulebase() + parent.add(rb) + parent = rb + elif module.params[self.rulebase] == 'post-rulebase': + rb = PostRulebase() + parent.add(rb) + parent = rb + else: + module.fail_json(msg=not_found.format( + 'Rulebase', module.params[self.rulebase])) + else: + # Firewall connection. + # Error if firewalls are not supported. + if self.firewall_error is not None: + module.fail_json(msg=self.firewall_error) + + # Spec: vsys or vsys_dg or vsys_importable. + vsys_name = self.vsys_dg or self.vsys or self.vsys_importable + if vsys_name is not None: + self.con.vsys = module.params[vsys_name] + + # Spec: rulebase. + if self.rulebase is not None: + rb = Rulebase() + parent.add(rb) + parent = rb + + # Done. + return parent + + +def get_connection(vsys=None, device_group=None, + vsys_dg=None, vsys_importable=None, + rulebase=None, template=None, template_stack=None, + classic_provider_spec=False, + panorama_error=None, firewall_error=None): + """Returns a helper object that handles pandevice object tree init. + + All arguments to this function (except panorama_error, firewall_error, + and classic_provider_spec) can be any of the following types: + + * None - do not include this in the spec + * True - use the default param name + * string - use this string for the param name + + Arguments: + vsys: Firewall only - The vsys. + device_group: Panorama only - The device group. + vsys_dg: The param name if vsys and device_group are a shared param. + vsys_importable: Either this or `vsys` should be specified. For: + - Interfaces + - VLANs + - Virtual Wires + - Virtual Routers + rulebase: This is a policy of some sort. + template: Panorama - The template name. + template_stack: Panorama - The template stack name. + classic_provider_spec(bool): Include the ip_address, username, + password, api_key params in the base spec, and make the + "provider" param optional. + panorama_error(str): The error message if the device is Panorama. + firewall_error(str): The error message if the device is a firewall. + + Returns: + ConnectionHelper + """ + helper = ConnectionHelper(panorama_error, firewall_error) + req = [] + spec = { + 'provider': { + 'required': True, + 'type': 'dict', + 'required_one_of': [['password', 'api_key'], ], + 'options': { + 'host': {'required': True}, + 'username': {'default': 'admin'}, + 'password': {'no_log': True}, + 'api_key': {'no_log': True}, + }, + }, + } + + if classic_provider_spec: + spec['provider']['required'] = False + spec['provider']['options']['host']['required'] = False + del(spec['provider']['required_one_of']) + spec.update({ + 'ip_address': {'required': False}, + 'username': {'default': 'admin'}, + 'password': {'no_log': True}, + 'api_key': {'no_log': True}, + }) + req.extend([ + ['provider', 'ip_address'], + ['provider', 'password', 'api_key'], + ]) + + if vsys_dg is not None: + if isinstance(vsys_dg, bool): + param = 'vsys_dg' + else: + param = vsys_dg + spec[param] = {} + helper.vsys_dg = param + else: + if vsys is not None: + if isinstance(vsys, bool): + param = 'vsys' + else: + param = vsys + spec[param] = {'default': 'vsys1'} + helper.vsys = param + if device_group is not None: + if isinstance(device_group, bool): + param = 'device_group' + else: + param = device_group + spec[param] = {'default': 'shared'} + helper.device_group = param + if vsys_importable is not None: + if isinstance(vsys_importable, bool): + param = 'vsys' + else: + param = vsys_importable + spec[param] = {} + helper.vsys_importable = param + + if rulebase is not None: + if isinstance(rulebase, bool): + param = 'rulebase' + else: + param = rulebase + spec[param] = { + 'default': 'pre-rulebase', + 'choices': ['pre-rulebase', 'post-rulebase'], + } + helper.rulebase = param + + if template is not None: + if isinstance(template, bool): + param = 'template' + else: + param = template + spec[param] = {} + helper.template = param + + if template_stack is not None: + if isinstance(template_stack, bool): + param = 'template_stack' + else: + param = template_stack + spec[param] = {} + helper.template_stack = param + + # Done. + helper.argument_spec = spec + helper.required_one_of = req + return helper