mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
Fortinet FortiManager Connection Plugin RC, plus associated utilities (#50336)
* PR Candidate for FortiManager Connection Plugin, plus associated Utilities. * Update fortimanager.py Adding additional comments * Committing changes for PR as requested by Ansible Staff * Minor doc change to kick off new shippable test. Unrelated code (not our stuff) caused a failure on the last test. * Removed generic methods for get/set/etc. Moved a copy of FMGRLockCTX into the plugin for portability, and to left the original in the mod_utils/fortimanager.py as deprecated code for pre-2.7 customers still running on pyFMG and not the plugin. Tested all playbooks and all modules, and all appears well.
This commit is contained in:
parent
6fcbfcf6b6
commit
e8209c23da
3 changed files with 1018 additions and 1 deletions
288
lib/ansible/module_utils/network/fortimanager/common.py
Normal file
288
lib/ansible/module_utils/network/fortimanager/common.py
Normal file
|
@ -0,0 +1,288 @@
|
|||
# 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) 2017 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.
|
||||
|
||||
|
||||
# BEGIN STATIC DATA / MESSAGES
|
||||
class FMGRMethods:
|
||||
GET = "get"
|
||||
SET = "set"
|
||||
EXEC = "exec"
|
||||
EXECUTE = "exec"
|
||||
UPDATE = "update"
|
||||
ADD = "add"
|
||||
DELETE = "delete"
|
||||
REPLACE = "replace"
|
||||
CLONE = "clone"
|
||||
MOVE = "move"
|
||||
|
||||
|
||||
BASE_HEADERS = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
|
||||
|
||||
# FMGR RETURN CODES
|
||||
FMGR_RC = {
|
||||
"fmgr_return_codes": {
|
||||
0: {
|
||||
"msg": "OK",
|
||||
"changed": True,
|
||||
"stop_on_success": True
|
||||
},
|
||||
-100000: {
|
||||
"msg": "Module returned without actually running anything. "
|
||||
"Check parameters, and please contact the authors if needed.",
|
||||
"failed": True
|
||||
},
|
||||
-2: {
|
||||
"msg": "Object already exists.",
|
||||
"skipped": True,
|
||||
"changed": False,
|
||||
"good_codes": [0, -2]
|
||||
},
|
||||
-6: {
|
||||
"msg": "Invalid Url. Sometimes this can happen because the path is mapped to a hostname or object that"
|
||||
" doesn't exist. Double check your input object parameters."
|
||||
},
|
||||
-3: {
|
||||
"msg": "Object doesn't exist.",
|
||||
"skipped": True,
|
||||
"changed": False,
|
||||
"good_codes": [0, -3]
|
||||
},
|
||||
-10131: {
|
||||
"msg": "Object dependency failed. Do all named objects in parameters exist?",
|
||||
"changed": False,
|
||||
"skipped": True
|
||||
},
|
||||
-9998: {
|
||||
"msg": "Duplicate object. Try using mode='set', if using add. STOPPING. Use 'ignore_errors=yes' in playbook"
|
||||
"to override and mark successful.",
|
||||
},
|
||||
-20042: {
|
||||
"msg": "Device Unreachable.",
|
||||
"skipped": True
|
||||
},
|
||||
-10033: {
|
||||
"msg": "Duplicate object. Try using mode='set', if using add.",
|
||||
"changed": False,
|
||||
"skipped": True
|
||||
},
|
||||
-10000: {
|
||||
"msg": "Duplicate object. Try using mode='set', if using add.",
|
||||
"changed": False,
|
||||
"skipped": True
|
||||
},
|
||||
-20010: {
|
||||
"msg": "Device already added to FortiManager. Serial number already in use.",
|
||||
"good_codes": [0, -20010],
|
||||
"changed": False,
|
||||
"stop_on_success": True
|
||||
},
|
||||
-20002: {
|
||||
"msg": "Invalid Argument -- Does this Device exist on FortiManager?",
|
||||
"changed": False,
|
||||
"skipped": True,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DEFAULT_RESULT_OBJ = (-100000, {"msg": "Nothing Happened. Check that handle_response is being called!"})
|
||||
FAIL_SOCKET_MSG = {"msg": "Socket Path Empty! The persistent connection manager is messed up. "
|
||||
"Try again in a few moments."}
|
||||
|
||||
|
||||
# BEGIN ERROR EXCEPTIONS
|
||||
class FMGBaseException(Exception):
|
||||
"""Wrapper to catch the unexpected"""
|
||||
|
||||
def __init__(self, msg=None, *args, **kwargs):
|
||||
if msg is None:
|
||||
msg = "An exception occurred within the fortimanager.py httpapi connection plugin."
|
||||
super(FMGBaseException, self).__init__(msg, *args)
|
||||
|
||||
# END ERROR CLASSES
|
||||
|
||||
|
||||
# BEGIN CLASSES
|
||||
class FMGRCommon(object):
|
||||
|
||||
@staticmethod
|
||||
def format_request(method, url, *args, **kwargs):
|
||||
"""
|
||||
Formats the payload from the module, into a payload the API handler can use.
|
||||
|
||||
:param url: Connection URL to access
|
||||
:type url: string
|
||||
:param method: The preferred API Request method (GET, ADD, POST, etc....)
|
||||
:type method: basestring
|
||||
:param kwargs: The payload dictionary from the module to be converted.
|
||||
|
||||
:return: Properly formatted dictionary payload for API Request via Connection Plugin.
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
params = [{"url": url}]
|
||||
if args:
|
||||
for arg in args:
|
||||
params[0].update(arg)
|
||||
if kwargs:
|
||||
keylist = list(kwargs)
|
||||
for k in keylist:
|
||||
kwargs[k.replace("__", "-")] = kwargs.pop(k)
|
||||
if method == "get" or method == "clone":
|
||||
params[0].update(kwargs)
|
||||
else:
|
||||
if kwargs.get("data", False):
|
||||
params[0]["data"] = kwargs["data"]
|
||||
else:
|
||||
params[0]["data"] = kwargs
|
||||
return params
|
||||
|
||||
@staticmethod
|
||||
def split_comma_strings_into_lists(obj):
|
||||
"""
|
||||
Splits a CSV String into a list. Also takes a dictionary, and converts any CSV strings in any key, to a list.
|
||||
|
||||
:param obj: object in CSV format to be parsed.
|
||||
:type obj: str or dict
|
||||
|
||||
:return: A list containing the CSV items.
|
||||
:rtype: list
|
||||
"""
|
||||
return_obj = ()
|
||||
if isinstance(obj, dict):
|
||||
if len(obj) > 0:
|
||||
for k, v in obj.items():
|
||||
if isinstance(v, str):
|
||||
new_list = list()
|
||||
if "," in v:
|
||||
new_items = v.split(",")
|
||||
for item in new_items:
|
||||
new_list.append(item.strip())
|
||||
obj[k] = new_list
|
||||
return_obj = obj
|
||||
elif isinstance(obj, str):
|
||||
return_obj = obj.replace(" ", "").split(",")
|
||||
|
||||
return return_obj
|
||||
|
||||
@staticmethod
|
||||
def cidr_to_netmask(cidr):
|
||||
"""
|
||||
Converts a CIDR Network string to full blown IP/Subnet format in decimal format.
|
||||
Decided not use IP Address module to keep includes to a minimum.
|
||||
|
||||
:param cidr: String object in CIDR format to be processed
|
||||
:type cidr: str
|
||||
|
||||
:return: A string object that looks like this "x.x.x.x/y.y.y.y"
|
||||
:rtype: str
|
||||
"""
|
||||
if isinstance(cidr, str):
|
||||
cidr = int(cidr)
|
||||
mask = (0xffffffff >> (32 - cidr)) << (32 - cidr)
|
||||
return (str((0xff000000 & mask) >> 24) + '.'
|
||||
+ str((0x00ff0000 & mask) >> 16) + '.'
|
||||
+ str((0x0000ff00 & mask) >> 8) + '.'
|
||||
+ str((0x000000ff & mask)))
|
||||
|
||||
@staticmethod
|
||||
def paramgram_child_list_override(list_overrides, paramgram, module):
|
||||
"""
|
||||
If a list of items was provided to a "parent" paramgram attribute, the paramgram needs to be rewritten.
|
||||
The child keys of the desired attribute need to be deleted, and then that "parent" keys' contents is replaced
|
||||
With the list of items that was provided.
|
||||
|
||||
:param list_overrides: Contains the response from the FortiManager.
|
||||
:type list_overrides: list
|
||||
:param paramgram: Contains the paramgram passed to the modules' local modify function.
|
||||
:type paramgram: dict
|
||||
:param module: Contains the Ansible Module Object being used by the module.
|
||||
:type module: classObject
|
||||
|
||||
:return: A new "paramgram" refactored to allow for multiple entries being added.
|
||||
:rtype: dict
|
||||
"""
|
||||
if len(list_overrides) > 0:
|
||||
for list_variable in list_overrides:
|
||||
try:
|
||||
list_variable = list_variable.replace("-", "_")
|
||||
override_data = module.params[list_variable]
|
||||
if override_data:
|
||||
del paramgram[list_variable]
|
||||
paramgram[list_variable] = override_data
|
||||
except BaseException as e:
|
||||
raise FMGBaseException("Error occurred merging custom lists for the paramgram parent: " + str(e))
|
||||
return paramgram
|
||||
|
||||
@staticmethod
|
||||
def syslog(module, msg):
|
||||
try:
|
||||
module.log(msg=msg)
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
|
||||
# RECURSIVE FUNCTIONS START
|
||||
def prepare_dict(obj):
|
||||
"""
|
||||
Removes any keys from a dictionary that are only specific to our use in the module. FortiManager will reject
|
||||
requests with these empty/None keys in it.
|
||||
|
||||
:param obj: Dictionary object to be processed.
|
||||
:type obj: dict
|
||||
|
||||
:return: Processed dictionary.
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
list_of_elems = ["mode", "adom", "host", "username", "password"]
|
||||
|
||||
if isinstance(obj, dict):
|
||||
obj = dict((key, prepare_dict(value)) for (key, value) in obj.items() if key not in list_of_elems)
|
||||
return obj
|
||||
|
||||
|
||||
def scrub_dict(obj):
|
||||
"""
|
||||
Removes any keys from a dictionary that are EMPTY -- this includes parent keys. FortiManager doesn't
|
||||
like empty keys in dictionaries
|
||||
|
||||
:param obj: Dictionary object to be processed.
|
||||
:type obj: dict
|
||||
|
||||
:return: Processed dictionary.
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
if isinstance(obj, dict):
|
||||
return dict((k, scrub_dict(v)) for k, v in obj.items() if v and scrub_dict(v))
|
||||
else:
|
||||
return obj
|
|
@ -27,15 +27,385 @@
|
|||
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
# check for pyFMG lib
|
||||
from ansible.module_utils.network.fortimanager.common import FMGR_RC
|
||||
from ansible.module_utils.network.fortimanager.common import FMGBaseException
|
||||
from ansible.module_utils.network.fortimanager.common import FMGRCommon
|
||||
from ansible.module_utils.network.fortimanager.common import scrub_dict
|
||||
|
||||
# check for pyFMG lib - DEPRECATING
|
||||
try:
|
||||
from pyFMG.fortimgr import FortiManager
|
||||
HAS_PYFMGR = True
|
||||
except ImportError:
|
||||
HAS_PYFMGR = False
|
||||
|
||||
# check for debug lib
|
||||
try:
|
||||
from ansible.module_utils.network.fortimanager.fortimanager_debug import debug_dump
|
||||
HAS_FMGR_DEBUG = True
|
||||
except ImportError:
|
||||
HAS_FMGR_DEBUG = False
|
||||
|
||||
|
||||
# BEGIN HANDLER CLASSES
|
||||
class FortiManagerHandler(object):
|
||||
def __init__(self, conn, check_mode=False):
|
||||
self._conn = conn
|
||||
self._check_mode = check_mode
|
||||
self._tools = FMGRCommon
|
||||
|
||||
def process_request(self, url, datagram, method):
|
||||
"""
|
||||
Formats and Runs the API Request via Connection Plugin. Streamlined for use FROM Modules.
|
||||
|
||||
:param url: Connection URL to access
|
||||
:type url: string
|
||||
:param datagram: The prepared payload for the API Request in dictionary format
|
||||
:type datagram: dict
|
||||
:param method: The preferred API Request method (GET, ADD, POST, etc....)
|
||||
:type method: basestring
|
||||
|
||||
:return: Dictionary containing results of the API Request via Connection Plugin
|
||||
:rtype: dict
|
||||
"""
|
||||
data = self._tools.format_request(method, url, **datagram)
|
||||
response = self._conn.send_request(method, data)
|
||||
if HAS_FMGR_DEBUG:
|
||||
try:
|
||||
debug_dump(response, datagram, url, method)
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
return response
|
||||
|
||||
def govern_response(self, module, results, msg=None, good_codes=None,
|
||||
stop_on_fail=None, stop_on_success=None, skipped=None,
|
||||
changed=None, unreachable=None, failed=None, success=None, changed_if_success=None,
|
||||
ansible_facts=None):
|
||||
"""
|
||||
This function will attempt to apply default values to canned responses from FortiManager we know of.
|
||||
This saves time, and turns the response in the module into a "one-liner", while still giving us...
|
||||
the flexibility to directly use return_response in modules if we have too. This function saves repeated code.
|
||||
|
||||
:param module: The Ansible Module CLASS object, used to run fail/exit json
|
||||
:type module: object
|
||||
:param msg: An overridable custom message from the module that called this.
|
||||
:type msg: string
|
||||
:param results: A dictionary object containing an API call results
|
||||
:type results: dict
|
||||
:param good_codes: A list of exit codes considered successful from FortiManager
|
||||
:type good_codes: list
|
||||
:param stop_on_fail: If true, stops playbook run when return code is NOT IN good codes (default: true)
|
||||
:type stop_on_fail: boolean
|
||||
:param stop_on_success: If true, stops playbook run when return code is IN good codes (default: false)
|
||||
:type stop_on_success: boolean
|
||||
:param changed: If True, tells Ansible that object was changed (default: false)
|
||||
:type skipped: boolean
|
||||
:param skipped: If True, tells Ansible that object was skipped (default: false)
|
||||
:type skipped: boolean
|
||||
:param unreachable: If True, tells Ansible that object was unreachable (default: false)
|
||||
:type unreachable: boolean
|
||||
:param failed: If True, tells Ansible that execution was a failure. Overrides good_codes. (default: false)
|
||||
:type unreachable: boolean
|
||||
:param success: If True, tells Ansible that execution was a success. Overrides good_codes. (default: false)
|
||||
:type unreachable: boolean
|
||||
:param changed_if_success: If True, defaults to changed if successful if you specify or not"
|
||||
:type changed_if_success: boolean
|
||||
:param ansible_facts: A prepared dictionary of ansible facts from the execution.
|
||||
:type ansible_facts: dict
|
||||
"""
|
||||
if module is None and results is None:
|
||||
raise FMGBaseException("govern_response() was called without a module and/or results tuple! Fix!")
|
||||
# Get the Return code from results
|
||||
try:
|
||||
rc = results[0]
|
||||
except BaseException:
|
||||
raise FMGBaseException("govern_response() was called without the return code at results[0]")
|
||||
|
||||
# init a few items
|
||||
rc_data = None
|
||||
|
||||
# Get the default values for the said return code.
|
||||
try:
|
||||
rc_codes = FMGR_RC.get('fmgr_return_codes')
|
||||
rc_data = rc_codes.get(rc)
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
if not rc_data:
|
||||
rc_data = {}
|
||||
# ONLY add to overrides if not none -- This is very important that the keys aren't added at this stage
|
||||
# if they are empty. And there aren't that many, so let's just do a few if then statements.
|
||||
if good_codes is not None:
|
||||
rc_data["good_codes"] = good_codes
|
||||
if stop_on_fail is not None:
|
||||
rc_data["stop_on_fail"] = stop_on_fail
|
||||
if stop_on_success is not None:
|
||||
rc_data["stop_on_success"] = stop_on_success
|
||||
if skipped is not None:
|
||||
rc_data["skipped"] = skipped
|
||||
if changed is not None:
|
||||
rc_data["changed"] = changed
|
||||
if unreachable is not None:
|
||||
rc_data["unreachable"] = unreachable
|
||||
if failed is not None:
|
||||
rc_data["failed"] = failed
|
||||
if success is not None:
|
||||
rc_data["success"] = success
|
||||
if changed_if_success is not None:
|
||||
rc_data["changed_if_success"] = changed_if_success
|
||||
if results is not None:
|
||||
rc_data["results"] = results
|
||||
if msg is not None:
|
||||
rc_data["msg"] = msg
|
||||
if ansible_facts is None:
|
||||
rc_data["ansible_facts"] = {}
|
||||
else:
|
||||
rc_data["ansible_facts"] = ansible_facts
|
||||
|
||||
return self.return_response(module=module,
|
||||
results=results,
|
||||
msg=rc_data.get("msg", "NULL"),
|
||||
good_codes=rc_data.get("good_codes", (0,)),
|
||||
stop_on_fail=rc_data.get("stop_on_fail", True),
|
||||
stop_on_success=rc_data.get("stop_on_success", False),
|
||||
skipped=rc_data.get("skipped", False),
|
||||
changed=rc_data.get("changed", False),
|
||||
changed_if_success=rc_data.get("changed_if_success", False),
|
||||
unreachable=rc_data.get("unreachable", False),
|
||||
failed=rc_data.get("failed", False),
|
||||
success=rc_data.get("success", False),
|
||||
ansible_facts=rc_data.get("ansible_facts", dict()))
|
||||
|
||||
@staticmethod
|
||||
def return_response(module, results, msg="NULL", good_codes=(0,),
|
||||
stop_on_fail=True, stop_on_success=False, skipped=False,
|
||||
changed=False, unreachable=False, failed=False, success=False, changed_if_success=True,
|
||||
ansible_facts=()):
|
||||
"""
|
||||
This function controls the logout and error reporting after an method or function runs. The exit_json for
|
||||
ansible comes from logic within this function. If this function returns just the msg, it means to continue
|
||||
execution on the playbook. It is called from the ansible module, or from the self.govern_response function.
|
||||
|
||||
:param module: The Ansible Module CLASS object, used to run fail/exit json
|
||||
:type module: object
|
||||
:param msg: An overridable custom message from the module that called this.
|
||||
:type msg: string
|
||||
:param results: A dictionary object containing an API call results
|
||||
:type results: dict
|
||||
:param good_codes: A list of exit codes considered successful from FortiManager
|
||||
:type good_codes: list
|
||||
:param stop_on_fail: If true, stops playbook run when return code is NOT IN good codes (default: true)
|
||||
:type stop_on_fail: boolean
|
||||
:param stop_on_success: If true, stops playbook run when return code is IN good codes (default: false)
|
||||
:type stop_on_success: boolean
|
||||
:param changed: If True, tells Ansible that object was changed (default: false)
|
||||
:type skipped: boolean
|
||||
:param skipped: If True, tells Ansible that object was skipped (default: false)
|
||||
:type skipped: boolean
|
||||
:param unreachable: If True, tells Ansible that object was unreachable (default: false)
|
||||
:type unreachable: boolean
|
||||
:param failed: If True, tells Ansible that execution was a failure. Overrides good_codes. (default: false)
|
||||
:type unreachable: boolean
|
||||
:param success: If True, tells Ansible that execution was a success. Overrides good_codes. (default: false)
|
||||
:type unreachable: boolean
|
||||
:param changed_if_success: If True, defaults to changed if successful if you specify or not"
|
||||
:type changed_if_success: boolean
|
||||
:param ansible_facts: A prepared dictionary of ansible facts from the execution.
|
||||
:type ansible_facts: dict
|
||||
|
||||
:return: A string object that contains an error message
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
# VALIDATION ERROR
|
||||
if (len(results) == 0) or (failed and success) or (changed and unreachable):
|
||||
module.exit_json(msg="Handle_response was called with no results, or conflicting failed/success or "
|
||||
"changed/unreachable parameters. Fix the exit code on module. "
|
||||
"Generic Failure", failed=True)
|
||||
|
||||
# IDENTIFY SUCCESS/FAIL IF NOT DEFINED
|
||||
if not failed and not success:
|
||||
if len(results) > 0:
|
||||
if results[0] not in good_codes:
|
||||
failed = True
|
||||
elif results[0] in good_codes:
|
||||
success = True
|
||||
|
||||
if len(results) > 0:
|
||||
# IF NO MESSAGE WAS SUPPLIED, GET IT FROM THE RESULTS, IF THAT DOESN'T WORK, THEN WRITE AN ERROR MESSAGE
|
||||
if msg == "NULL":
|
||||
try:
|
||||
msg = results[1]['status']['message']
|
||||
except BaseException:
|
||||
msg = "No status message returned at results[1][status][message], " \
|
||||
"and none supplied to msg parameter for handle_response."
|
||||
|
||||
if failed:
|
||||
# BECAUSE SKIPPED/FAILED WILL OFTEN OCCUR ON CODES THAT DON'T GET INCLUDED, THEY ARE CONSIDERED FAILURES
|
||||
# HOWEVER, THEY ARE MUTUALLY EXCLUSIVE, SO IF IT IS MARKED SKIPPED OR UNREACHABLE BY THE MODULE LOGIC
|
||||
# THEN REMOVE THE FAILED FLAG SO IT DOESN'T OVERRIDE THE DESIRED STATUS OF SKIPPED OR UNREACHABLE.
|
||||
if failed and skipped:
|
||||
failed = False
|
||||
if failed and unreachable:
|
||||
failed = False
|
||||
if stop_on_fail:
|
||||
module.exit_json(msg=msg, failed=failed, changed=changed, unreachable=unreachable, skipped=skipped,
|
||||
results=results[1], ansible_facts=ansible_facts, rc=results[0],
|
||||
invocation={"module_args": ansible_facts["ansible_params"]})
|
||||
elif success:
|
||||
if changed_if_success:
|
||||
changed = True
|
||||
success = False
|
||||
if stop_on_success:
|
||||
module.exit_json(msg=msg, success=success, changed=changed, unreachable=unreachable,
|
||||
skipped=skipped, results=results[1], ansible_facts=ansible_facts, rc=results[0],
|
||||
invocation={"module_args": ansible_facts["ansible_params"]})
|
||||
return msg
|
||||
|
||||
def construct_ansible_facts(self, response, ansible_params, paramgram, *args, **kwargs):
|
||||
"""
|
||||
Constructs a dictionary to return to ansible facts, containing various information about the execution.
|
||||
|
||||
:param response: Contains the response from the FortiManager.
|
||||
:type response: dict
|
||||
:param ansible_params: Contains the parameters Ansible was called with.
|
||||
:type ansible_params: dict
|
||||
:param paramgram: Contains the paramgram passed to the modules' local modify function.
|
||||
:type paramgram: dict
|
||||
:param args: Free-form arguments that could be added.
|
||||
:param kwargs: Free-form keyword arguments that could be added.
|
||||
|
||||
:return: A dictionary containing lots of information to append to Ansible Facts.
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
facts = {
|
||||
"response": response,
|
||||
"ansible_params": scrub_dict(ansible_params),
|
||||
"paramgram": scrub_dict(paramgram),
|
||||
"connected_fmgr": self._conn.return_connected_fmgr()
|
||||
}
|
||||
|
||||
if args:
|
||||
facts["custom_args"] = args
|
||||
if kwargs:
|
||||
facts.update(kwargs)
|
||||
|
||||
return facts
|
||||
|
||||
|
||||
##########################
|
||||
# BEGIN DEPRECATED METHODS
|
||||
##########################
|
||||
|
||||
# SOME OF THIS CODE IS DUPLICATED IN THE PLUGIN, BUT THOSE ARE PLUGIN SPECIFIC. THIS VERSION STILL ALLOWS FOR
|
||||
# THE USAGE OF PYFMG FOR CUSTOMERS WHO HAVE NOT YET UPGRADED TO ANSIBLE 2.7
|
||||
|
||||
# LEGACY PYFMG METHODS START
|
||||
# USED TO DETERMINE LOCK CONTEXT ON A FORTIMANAGER. A DATABASE LOCKING CONCEPT THAT NEEDS TO BE ACCOUNTED FOR.
|
||||
|
||||
class FMGLockContext(object):
|
||||
"""
|
||||
- DEPRECATING: USING CONNECTION MANAGER NOW INSTEAD. EVENTUALLY THIS CLASS WILL DISAPPEAR. PLEASE
|
||||
- CONVERT ALL MODULES TO CONNECTION MANAGER METHOD.
|
||||
- LEGACY pyFMG HANDLER OBJECT: REQUIRES A CHECK FOR PY FMG AT TOP OF PAGE
|
||||
"""
|
||||
def __init__(self, fmg):
|
||||
self._fmg = fmg
|
||||
self._locked_adom_list = list()
|
||||
self._uses_workspace = False
|
||||
self._uses_adoms = False
|
||||
|
||||
@property
|
||||
def uses_workspace(self):
|
||||
return self._uses_workspace
|
||||
|
||||
@uses_workspace.setter
|
||||
def uses_workspace(self, val):
|
||||
self._uses_workspace = val
|
||||
|
||||
@property
|
||||
def uses_adoms(self):
|
||||
return self._uses_adoms
|
||||
|
||||
@uses_adoms.setter
|
||||
def uses_adoms(self, val):
|
||||
self._uses_adoms = val
|
||||
|
||||
def add_adom_to_lock_list(self, adom):
|
||||
if adom not in self._locked_adom_list:
|
||||
self._locked_adom_list.append(adom)
|
||||
|
||||
def remove_adom_from_lock_list(self, adom):
|
||||
if adom in self._locked_adom_list:
|
||||
self._locked_adom_list.remove(adom)
|
||||
|
||||
def check_mode(self):
|
||||
url = "/cli/global/system/global"
|
||||
code, resp_obj = self._fmg.get(url, fields=["workspace-mode", "adom-status"])
|
||||
try:
|
||||
if resp_obj["workspace-mode"] != 0:
|
||||
self.uses_workspace = True
|
||||
except KeyError:
|
||||
self.uses_workspace = False
|
||||
try:
|
||||
if resp_obj["adom-status"] == 1:
|
||||
self.uses_adoms = True
|
||||
except KeyError:
|
||||
self.uses_adoms = False
|
||||
|
||||
def run_unlock(self):
|
||||
for adom_locked in self._locked_adom_list:
|
||||
self.unlock_adom(adom_locked)
|
||||
|
||||
def lock_adom(self, adom=None, *args, **kwargs):
|
||||
if adom:
|
||||
if adom.lower() == "global":
|
||||
url = "/dvmdb/global/workspace/lock/"
|
||||
else:
|
||||
url = "/dvmdb/adom/{adom}/workspace/lock/".format(adom=adom)
|
||||
else:
|
||||
url = "/dvmdb/adom/root/workspace/lock"
|
||||
code, respobj = self._fmg.execute(url, {}, *args, **kwargs)
|
||||
if code == 0 and respobj["status"]["message"].lower() == "ok":
|
||||
self.add_adom_to_lock_list(adom)
|
||||
return code, respobj
|
||||
|
||||
def unlock_adom(self, adom=None, *args, **kwargs):
|
||||
if adom:
|
||||
if adom.lower() == "global":
|
||||
url = "/dvmdb/global/workspace/unlock/"
|
||||
else:
|
||||
url = "/dvmdb/adom/{adom}/workspace/unlock/".format(adom=adom)
|
||||
else:
|
||||
url = "/dvmdb/adom/root/workspace/unlock"
|
||||
code, respobj = self._fmg.execute(url, {}, *args, **kwargs)
|
||||
if code == 0 and respobj["status"]["message"].lower() == "ok":
|
||||
self.remove_adom_from_lock_list(adom)
|
||||
return code, respobj
|
||||
|
||||
def commit_changes(self, adom=None, aux=False, *args, **kwargs):
|
||||
if adom:
|
||||
if aux:
|
||||
url = "/pm/config/adom/{adom}/workspace/commit".format(adom=adom)
|
||||
else:
|
||||
if adom.lower() == "global":
|
||||
url = "/dvmdb/global/workspace/commit/"
|
||||
else:
|
||||
url = "/dvmdb/adom/{adom}/workspace/commit".format(adom=adom)
|
||||
else:
|
||||
url = "/dvmdb/adom/root/workspace/commit"
|
||||
return self._fmg.execute(url, {}, *args, **kwargs)
|
||||
|
||||
|
||||
# DEPRECATED -- USE PLUGIN INSTEAD
|
||||
class AnsibleFortiManager(object):
|
||||
"""
|
||||
- DEPRECATING: USING CONNECTION MANAGER NOW INSTEAD. EVENTUALLY THIS CLASS WILL DISAPPEAR. PLEASE
|
||||
- CONVERT ALL MODULES TO CONNECTION MANAGER METHOD.
|
||||
- LEGACY pyFMG HANDLER OBJECT: REQUIRES A CHECK FOR PY FMG AT TOP OF PAGE
|
||||
"""
|
||||
|
||||
def __init__(self, module, ip=None, username=None, passwd=None, use_ssl=True, verify_ssl=False, timeout=300):
|
||||
self.ip = ip
|
||||
|
@ -85,3 +455,7 @@ class AnsibleFortiManager(object):
|
|||
|
||||
def clone(self, url, data):
|
||||
return self.fmgr_instance.clone(url, **data)
|
||||
|
||||
##########################
|
||||
# END DEPRECATED METHODS
|
||||
##########################
|
||||
|
|
355
lib/ansible/plugins/httpapi/fortimanager.py
Normal file
355
lib/ansible/plugins/httpapi/fortimanager.py
Normal file
|
@ -0,0 +1,355 @@
|
|||
# Copyright (c) 2018 Fortinet and/or its affiliates.
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = """
|
||||
---
|
||||
author:
|
||||
- Luke Weighall (@lweighall)
|
||||
- Andrew Welsh (@Ghilli3)
|
||||
- Jim Huber (@p4r4n0y1ng)
|
||||
httpapi : fortimanager
|
||||
short_description: HttpApi Plugin for Fortinet FortiManager Appliance or VM
|
||||
description:
|
||||
- This HttpApi plugin provides methods to connect to Fortinet FortiManager Appliance or VM via JSON RPC API
|
||||
version_added: "2.8"
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
from ansible.plugins.httpapi import HttpApiBase
|
||||
from ansible.module_utils.basic import to_text
|
||||
from ansible.module_utils.network.fortimanager.common import BASE_HEADERS
|
||||
from ansible.module_utils.network.fortimanager.common import FMGBaseException
|
||||
from ansible.module_utils.network.fortimanager.common import FMGRCommon
|
||||
from ansible.module_utils.network.fortimanager.common import FMGRMethods
|
||||
|
||||
|
||||
class HttpApi(HttpApiBase):
|
||||
def __init__(self, connection):
|
||||
super(HttpApi, self).__init__(connection)
|
||||
self._req_id = 0
|
||||
self._sid = None
|
||||
self._url = "/jsonrpc"
|
||||
self._host = None
|
||||
self._tools = FMGRCommon
|
||||
self._debug = False
|
||||
self._connected_fmgr = None
|
||||
self._last_response_msg = None
|
||||
self._last_response_code = None
|
||||
self._last_data_payload = None
|
||||
self._last_url = None
|
||||
self._last_response_raw = None
|
||||
self._locked_adom_list = list()
|
||||
self._uses_workspace = False
|
||||
self._uses_adoms = False
|
||||
|
||||
def set_become(self, become_context):
|
||||
"""
|
||||
ELEVATION IS NOT REQUIRED ON FORTINET DEVICES - SKIPPED
|
||||
:param become_context: Unused input.
|
||||
:return: None
|
||||
"""
|
||||
return None
|
||||
|
||||
def update_auth(self, response, response_data):
|
||||
"""
|
||||
TOKENS ARE NOT USED SO NO NEED TO UPDATE AUTH
|
||||
:param response: Unused input.
|
||||
:param response_data Unused_input.
|
||||
:return: None
|
||||
"""
|
||||
return None
|
||||
|
||||
def login(self, username, password):
|
||||
"""
|
||||
This function will log the plugin into FortiManager, and return the results.
|
||||
:param username: Username of FortiManager Admin
|
||||
:param password: Password of FortiManager Admin
|
||||
|
||||
:return: Dictionary of status, if it logged in or not.
|
||||
"""
|
||||
self.send_request(FMGRMethods.EXEC, self._tools.format_request(FMGRMethods.EXEC, "sys/login/user",
|
||||
passwd=password, user=username,))
|
||||
|
||||
if "FortiManager object connected to FortiManager" in self.__str__():
|
||||
# If Login worked, then inspect the FortiManager for Workspace Mode, and it's system information.
|
||||
self.inspect_fmgr()
|
||||
return
|
||||
else:
|
||||
raise FMGBaseException(msg="Unknown error while logging in...connection was lost during login operation...."
|
||||
" Exiting")
|
||||
|
||||
def inspect_fmgr(self):
|
||||
# CHECK FOR WORKSPACE MODE TO SEE IF WE HAVE TO ENABLE ADOM LOCKS
|
||||
|
||||
self.check_mode()
|
||||
# CHECK FOR SYSTEM STATUS -- SHOULD RETURN 0
|
||||
status = self.get_system_status()
|
||||
if status[0] == -11:
|
||||
# THE CONNECTION GOT LOST SOMEHOW, REMOVE THE SID AND REPORT BAD LOGIN
|
||||
self.logout()
|
||||
raise FMGBaseException(msg="Error -11 -- the Session ID was likely malformed somehow. Contact authors."
|
||||
" Exiting")
|
||||
elif status[0] == 0:
|
||||
try:
|
||||
self._connected_fmgr = status[1]
|
||||
self._host = self._connected_fmgr["Hostname"]
|
||||
except BaseException:
|
||||
pass
|
||||
return
|
||||
|
||||
def logout(self):
|
||||
"""
|
||||
This function will logout of the FortiManager.
|
||||
"""
|
||||
if self.sid is not None:
|
||||
if self.uses_workspace:
|
||||
self.run_unlock()
|
||||
ret_code, response = self.send_request(FMGRMethods.EXEC,
|
||||
self._tools.format_request(FMGRMethods.EXEC, "sys/logout"))
|
||||
self.sid = None
|
||||
return ret_code, response
|
||||
|
||||
def send_request(self, method, params):
|
||||
"""
|
||||
Responsible for actual sending of data to the connection httpapi base plugin. Does some formatting as well.
|
||||
:param params: A formatted dictionary that was returned by self.common_datagram_params()
|
||||
before being called here.
|
||||
:param method: The preferred API Request method (GET, ADD, POST, etc....)
|
||||
:type method: basestring
|
||||
|
||||
:return: Dictionary of status, if it logged in or not.
|
||||
"""
|
||||
|
||||
try:
|
||||
if self.sid is None and params[0]["url"] != "sys/login/user":
|
||||
raise FMGBaseException("An attempt was made to login with the SID None and URL != login url.")
|
||||
except IndexError:
|
||||
raise FMGBaseException("An attempt was made at communicating with a FMG with "
|
||||
"no valid session and an incorrectly formatted request.")
|
||||
except Exception:
|
||||
raise FMGBaseException("An attempt was made at communicating with a FMG with "
|
||||
"no valid session and an unexpected error was discovered.")
|
||||
|
||||
self._update_request_id()
|
||||
json_request = {
|
||||
"method": method,
|
||||
"params": params,
|
||||
"session": self.sid,
|
||||
"id": self.req_id,
|
||||
"verbose": 1
|
||||
}
|
||||
data = json.dumps(json_request, ensure_ascii=False).replace('\\\\', '\\')
|
||||
try:
|
||||
# Sending URL and Data in Unicode, per Ansible Specifications for Connection Plugins
|
||||
response, response_data = self.connection.send(path=to_text(self._url), data=to_text(data),
|
||||
headers=BASE_HEADERS)
|
||||
# Get Unicode Response - Must convert from StringIO to unicode first so we can do a replace function below
|
||||
result = json.loads(to_text(response_data.getvalue()))
|
||||
self._update_self_from_response(result, self._url, data)
|
||||
return self._handle_response(result)
|
||||
except Exception as err:
|
||||
raise FMGBaseException(err)
|
||||
|
||||
def _handle_response(self, response):
|
||||
self._set_sid(response)
|
||||
if isinstance(response["result"], list):
|
||||
result = response["result"][0]
|
||||
else:
|
||||
result = response["result"]
|
||||
if "data" in result:
|
||||
return result["status"]["code"], result["data"]
|
||||
else:
|
||||
return result["status"]["code"], result
|
||||
|
||||
def _update_self_from_response(self, response, url, data):
|
||||
self._last_response_raw = response
|
||||
if isinstance(response["result"], list):
|
||||
result = response["result"][0]
|
||||
else:
|
||||
result = response["result"]
|
||||
if "status" in result:
|
||||
self._last_response_code = result["status"]["code"]
|
||||
self._last_response_msg = result["status"]["message"]
|
||||
self._last_url = url
|
||||
self._last_data_payload = data
|
||||
|
||||
def _set_sid(self, response):
|
||||
if self.sid is None and "session" in response:
|
||||
self.sid = response["session"]
|
||||
|
||||
def return_connected_fmgr(self):
|
||||
"""
|
||||
Returns the data stored under self._connected_fmgr
|
||||
|
||||
:return: dict
|
||||
"""
|
||||
try:
|
||||
if self._connected_fmgr:
|
||||
return self._connected_fmgr
|
||||
except BaseException:
|
||||
raise FMGBaseException("Couldn't Retrieve Connected FMGR Stats")
|
||||
|
||||
def get_system_status(self):
|
||||
"""
|
||||
Returns the system status page from the FortiManager, for logging and other uses.
|
||||
return: status
|
||||
"""
|
||||
status = self.send_request(FMGRMethods.GET, self._tools.format_request(FMGRMethods.GET, "sys/status"))
|
||||
return status
|
||||
|
||||
@property
|
||||
def debug(self):
|
||||
return self._debug
|
||||
|
||||
@debug.setter
|
||||
def debug(self, val):
|
||||
self._debug = val
|
||||
|
||||
@property
|
||||
def req_id(self):
|
||||
return self._req_id
|
||||
|
||||
@req_id.setter
|
||||
def req_id(self, val):
|
||||
self._req_id = val
|
||||
|
||||
def _update_request_id(self, reqid=0):
|
||||
self.req_id = reqid if reqid != 0 else self.req_id + 1
|
||||
|
||||
@property
|
||||
def sid(self):
|
||||
return self._sid
|
||||
|
||||
@sid.setter
|
||||
def sid(self, val):
|
||||
self._sid = val
|
||||
|
||||
def __str__(self):
|
||||
if self.sid is not None and self.connection._url is not None:
|
||||
return "FortiManager object connected to FortiManager: " + str(self.connection._url)
|
||||
return "FortiManager object with no valid connection to a FortiManager appliance."
|
||||
|
||||
##################################
|
||||
# BEGIN DATABASE LOCK CONTEXT CODE
|
||||
##################################
|
||||
|
||||
@property
|
||||
def uses_workspace(self):
|
||||
return self._uses_workspace
|
||||
|
||||
@uses_workspace.setter
|
||||
def uses_workspace(self, val):
|
||||
self._uses_workspace = val
|
||||
|
||||
@property
|
||||
def uses_adoms(self):
|
||||
return self._uses_adoms
|
||||
|
||||
@uses_adoms.setter
|
||||
def uses_adoms(self, val):
|
||||
self._uses_adoms = val
|
||||
|
||||
def add_adom_to_lock_list(self, adom):
|
||||
if adom not in self._locked_adom_list:
|
||||
self._locked_adom_list.append(adom)
|
||||
|
||||
def remove_adom_from_lock_list(self, adom):
|
||||
if adom in self._locked_adom_list:
|
||||
self._locked_adom_list.remove(adom)
|
||||
|
||||
def check_mode(self):
|
||||
"""
|
||||
Checks FortiManager for the use of Workspace mode
|
||||
"""
|
||||
url = "/cli/global/system/global"
|
||||
code, resp_obj = self.send_request(FMGRMethods.GET, self._tools.format_request(FMGRMethods.GET, url,
|
||||
fields=["workspace-mode",
|
||||
"adom-status"]))
|
||||
try:
|
||||
if resp_obj["workspace-mode"] != 0:
|
||||
self.uses_workspace = True
|
||||
except KeyError:
|
||||
self.uses_workspace = False
|
||||
try:
|
||||
if resp_obj["adom-status"] == 1:
|
||||
self.uses_adoms = True
|
||||
except KeyError:
|
||||
self.uses_adoms = False
|
||||
|
||||
def run_unlock(self):
|
||||
"""
|
||||
Checks for ADOM status, if locked, it will unlock
|
||||
"""
|
||||
for adom_locked in self._locked_adom_list:
|
||||
self.unlock_adom(adom_locked)
|
||||
|
||||
def lock_adom(self, adom=None, *args, **kwargs):
|
||||
"""
|
||||
Locks an ADOM for changes
|
||||
"""
|
||||
if adom:
|
||||
if adom.lower() == "global":
|
||||
url = "/dvmdb/global/workspace/lock/"
|
||||
else:
|
||||
url = "/dvmdb/adom/{adom}/workspace/lock/".format(adom=adom)
|
||||
else:
|
||||
url = "/dvmdb/adom/root/workspace/lock"
|
||||
code, respobj = self.send_request(FMGRMethods.EXEC, self._tools.format_request(FMGRMethods.EXEC, url))
|
||||
if code == 0 and respobj["status"]["message"].lower() == "ok":
|
||||
self.add_adom_to_lock_list(adom)
|
||||
return code, respobj
|
||||
|
||||
def unlock_adom(self, adom=None, *args, **kwargs):
|
||||
"""
|
||||
Unlocks an ADOM after changes
|
||||
"""
|
||||
if adom:
|
||||
if adom.lower() == "global":
|
||||
url = "/dvmdb/global/workspace/unlock/"
|
||||
else:
|
||||
url = "/dvmdb/adom/{adom}/workspace/unlock/".format(adom=adom)
|
||||
else:
|
||||
url = "/dvmdb/adom/root/workspace/unlock"
|
||||
code, respobj = self.send_request(FMGRMethods.EXEC, self._tools.format_request(FMGRMethods.EXEC, url))
|
||||
if code == 0 and respobj["status"]["message"].lower() == "ok":
|
||||
self.remove_adom_from_lock_list(adom)
|
||||
return code, respobj
|
||||
|
||||
def commit_changes(self, adom=None, aux=False, *args, **kwargs):
|
||||
"""
|
||||
Commits changes to an ADOM
|
||||
"""
|
||||
if adom:
|
||||
if aux:
|
||||
url = "/pm/config/adom/{adom}/workspace/commit".format(adom=adom)
|
||||
else:
|
||||
if adom.lower() == "global":
|
||||
url = "/dvmdb/global/workspace/commit/"
|
||||
else:
|
||||
url = "/dvmdb/adom/{adom}/workspace/commit".format(adom=adom)
|
||||
else:
|
||||
url = "/dvmdb/adom/root/workspace/commit"
|
||||
return self.send_request(FMGRMethods.EXEC, self._tools.format_request(FMGRMethods.EXEC, url))
|
||||
|
||||
################################
|
||||
# END DATABASE LOCK CONTEXT CODE
|
||||
################################
|
Loading…
Reference in a new issue