diff --git a/lib/ansible/module_utils/network/fortios/fortios.py b/lib/ansible/module_utils/network/fortios/fortios.py index bf3385aa31b..879921bbd94 100644 --- a/lib/ansible/module_utils/network/fortios/fortios.py +++ b/lib/ansible/module_utils/network/fortios/fortios.py @@ -4,7 +4,8 @@ # still belong to the author of the module, and may assign their own license # to the complete work. # -# Copyright (c), Benjamin Jolivot , 2014 +# Copyright (c), Benjamin Jolivot , 2014, +# Miguel Angel Munoz , 2019 # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, @@ -33,6 +34,9 @@ import traceback from ansible.module_utils._text import to_native from ansible.module_utils.basic import env_fallback +import json + +# BEGIN DEPRECATED # check for pyFG lib try: @@ -72,6 +76,105 @@ fortios_error_codes = { '-61': "Command error" } +# END DEPRECATED + + +class FortiOSHandler(object): + + def __init__(self, conn): + self._conn = conn + + def cmdb_url(self, path, name, vdom=None, mkey=None): + + url = '/api/v2/cmdb/' + path + '/' + name + if mkey: + url = url + '/' + str(mkey) + if vdom: + if vdom == "global": + url += '?global=1' + else: + url += '?vdom=' + vdom + return url + + def schema(self, path, name, vdom=None): + if vdom is None: + url = self.cmdb_url(path, name) + "?action=schema" + else: + url = self.cmdb_url(path, name, vdom=vdom) + "&action=schema" + + status, result_data = self._conn.send_request(url=url) + + if status == 200: + if vdom == "global": + return json.loads(result_data.decode('utf-8'))[0]['results'] + else: + return json.loads(result_data.decode('utf-8'))['results'] + else: + return json.loads(result_data.decode('utf-8')) + + def get_mkeyname(self, path, name, vdom=None): + schema = self.schema(path, name, vdom=vdom) + try: + keyname = schema['mkey'] + except KeyError: + return False + return keyname + + def get_mkey(self, path, name, data, vdom=None): + + keyname = self.get_mkeyname(path, name, vdom) + if not keyname: + return None + else: + try: + mkey = data[keyname] + except KeyError: + return None + return mkey + + def set(self, path, name, data, mkey=None, vdom=None, parameters=None): + + if not mkey: + mkey = self.get_mkey(path, name, data, vdom=vdom) + url = self.cmdb_url(path, name, vdom, mkey) + + status, result_data = self._conn.send_request(url=url, params=parameters, data=json.dumps(data), method='PUT') + + if status == 404 or status == 405 or status == 500: + return self.post(path, name, data, vdom, mkey) + else: + return self.formatresponse(result_data, vdom=vdom) + + def post(self, path, name, data, vdom=None, + mkey=None, parameters=None): + + if mkey: + mkeyname = self.get_mkeyname(path, name, vdom) + data[mkeyname] = mkey + + url = self.cmdb_url(path, name, vdom, mkey=None) + + status, result_data = self._conn.send_request(url=url, params=parameters, data=json.dumps(data), method='POST') + + return self.formatresponse(result_data, vdom=vdom) + + def delete(self, path, name, vdom=None, mkey=None, parameters=None, data=None): + if not mkey: + mkey = self.get_mkey(path, name, data, vdom=vdom) + url = self.cmdb_url(path, name, vdom, mkey) + status, result_data = self._conn.send_request(url=url, params=parameters, data=json.dumps(data), method='DELETE') + return self.formatresponse(result_data, vdom=vdom) + + def formatresponse(self, res, vdom=None): + if vdom == "global": + resp = json.loads(res.decode('utf-8'))[0] + resp['vdom'] = "global" + else: + resp = json.loads(res.decode('utf-8')) + return resp + +# BEGIN DEPRECATED + def backup(module, running_config): backup_path = module.params['backup_path'] @@ -198,3 +301,5 @@ class AnsibleFortios(object): def get_empty_configuration_block(self, block_name, block_type): return FortiConfig(block_name, block_type) + +# END DEPRECATED diff --git a/lib/ansible/plugins/httpapi/fortios.py b/lib/ansible/plugins/httpapi/fortios.py new file mode 100644 index 00000000000..7c40dd89ce8 --- /dev/null +++ b/lib/ansible/plugins/httpapi/fortios.py @@ -0,0 +1,125 @@ +# 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. +# +# (c) 2019 Fortinet, Inc +# All rights reserved. +# +# 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 + +DOCUMENTATION = """ +--- +author: + - Miguel Angel Muñoz (@magonzalez) +httpapi : fortios +short_description: HttpApi Plugin for Fortinet FortiOS Appliance or VM +description: + - This HttpApi plugin provides methods to connect to Fortinet FortiOS Appliance or VM via REST API +version_added: "2.9" +""" + +from ansible.plugins.httpapi import HttpApiBase +from ansible.module_utils.basic import to_text +import urllib +import json +import re + + +class HttpApi(HttpApiBase): + def __init__(self, connection): + super(HttpApi, self).__init__(connection) + + self._ccsrftoken = '' + + def set_become(self, become_context): + """ + Elevation is not required on Fortinet devices - Skipped + :param become_context: Unused input. + :return: None + """ + return None + + def login(self, username, password): + """Call a defined login endpoint to receive an authentication token.""" + + data = "username=" + urllib.parse.quote(username) + "&secretkey=" + urllib.parse.quote(password) + "&ajax=1" + dummy, result_data = self.send_request(url='/logincheck', data=data, method='POST') + if result_data[0] != '1': + raise Exception('Wrong credentials. Please check') + + def logout(self): + """ Call to implement session logout.""" + + self.send_request(url='/logout', method="POST") + + def update_auth(self, response, response_text): + """ + Get cookies and obtain value for csrftoken that will be used on next requests + :param response: Response given by the server. + :param response_text Unused_input. + :return: Dictionary containing headers + """ + + headers = {} + + for attr, val in response.getheaders(): + if attr == 'Set-Cookie' and 'APSCOOKIE_' in val: + headers['Cookie'] = val + + elif attr == 'Set-Cookie' and 'ccsrftoken=' in val: + csrftoken_search = re.search('\"(.*)\"', val) + if csrftoken_search: + self._ccsrftoken = csrftoken_search.group(1) + + headers['x-csrftoken'] = self._ccsrftoken + + return headers + + def handle_httperror(self, exc): + """ + Not required on Fortinet devices - Skipped + :param exc: Unused input. + :return: exc + """ + return exc + + def send_request(self, **message_kwargs): + """ + Responsible for actual sending of data to the connection httpapi base plugin. + :param message_kwargs: A formatted dictionary containing request info: url, data, method + + :return: Status code and response data. + """ + url = message_kwargs.get('url', '/') + data = message_kwargs.get('data', '') + method = message_kwargs.get('method', 'GET') + + try: + response, response_data = self.connection.send(url, data, method=method) + + return response.status, to_text(response_data.getvalue()) + except Exception as err: + raise Exception(err)