1# This code is part of Ansible, but is an independent component.
2# This particular file snippet, and this file snippet only, is BSD licensed.
3# Modules you write using this snippet, which is embedded dynamically by Ansible
4# still belong to the author of the module, and may assign their own license
5# to the complete work.
6#
7# (c) 2017-2020 Fortinet, Inc
8# All rights reserved.
9#
10# Redistribution and use in source and binary forms, with or without modification,
11# are permitted provided that the following conditions are met:
12#
13#    * Redistributions of source code must retain the above copyright
14#      notice, this list of conditions and the following disclaimer.
15#    * Redistributions in binary form must reproduce the above copyright notice,
16#      this list of conditions and the following disclaimer in the documentation
17#      and/or other materials provided with the distribution.
18#
19# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
22# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
27# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28#
29from __future__ import (absolute_import, division, print_function)
30__metaclass__ = type
31
32from ansible_collections.fortinet.fortimanager.plugins.module_utils.common import FMGR_RC
33from ansible_collections.fortinet.fortimanager.plugins.module_utils.common import FMGBaseException
34from ansible_collections.fortinet.fortimanager.plugins.module_utils.common import FMGRCommon
35from ansible_collections.fortinet.fortimanager.plugins.module_utils.common import scrub_dict
36
37# check for pyFMG lib - DEPRECATING
38try:
39    from pyFMG.fortimgr import FortiManager
40    HAS_PYFMGR = True
41except ImportError:
42    HAS_PYFMGR = False
43
44# ACTIVE BUG WITH OUR DEBUG IMPORT CALL -- BECAUSE IT'S UNDER MODULE_UTILITIES
45# WHEN module_common.recursive_finder() runs under the module loader, it looks for this namespace debug import
46# and because it's not there, it always fails, regardless of it being under a try/catch here.
47# we're going to move it to a different namespace.
48# # check for debug lib
49# try:
50#     from ansible.module_utils.network.fortimanager.fortimanager_debug import debug_dump
51#     HAS_FMGR_DEBUG = True
52# except:
53#     HAS_FMGR_DEBUG = False
54
55
56# BEGIN HANDLER CLASSES
57class FortiManagerHandler(object):
58    def __init__(self, conn, module):
59        self._conn = conn
60        self._module = module
61        self._tools = FMGRCommon
62        self.process_workspace_lock()
63
64    def process_workspace_lock(self):
65        self._conn.process_workspace_locking(self._module.params)
66
67    def process_request(self, url, datagram, method):
68        """
69        Formats and Runs the API Request via Connection Plugin. Streamlined for use FROM Modules.
70
71        :param url: Connection URL to access
72        :type url: string
73        :param datagram: The prepared payload for the API Request in dictionary format
74        :type datagram: dict
75        :param method: The preferred API Request method (GET, ADD, POST, etc....)
76        :type method: basestring
77
78        :return: Dictionary containing results of the API Request via Connection Plugin
79        :rtype: dict
80        """
81        data = self._tools.format_request(method, url, **datagram)
82        response = self._conn.send_request(method, data)
83
84        # if HAS_FMGR_DEBUG:
85        #     try:
86        #         debug_dump(response, datagram, self._module.paramgram, url, method)
87        #     except BaseException:
88        #         pass
89
90        return response
91
92    def govern_response(self, module, results, msg=None, good_codes=None,
93                        stop_on_fail=None, stop_on_success=None, skipped=None,
94                        changed=None, unreachable=None, failed=None, success=None, changed_if_success=None,
95                        ansible_facts=None):
96        """
97        This function will attempt to apply default values to canned responses from FortiManager we know of.
98        This saves time, and turns the response in the module into a "one-liner", while still giving us...
99        the flexibility to directly use return_response in modules if we have too. This function saves repeated code.
100
101        :param module: The Ansible Module CLASS object, used to run fail/exit json
102        :type module: object
103        :param msg: An overridable custom message from the module that called this.
104        :type msg: string
105        :param results: A dictionary object containing an API call results
106        :type results: dict
107        :param good_codes: A list of exit codes considered successful from FortiManager
108        :type good_codes: list
109        :param stop_on_fail: If true, stops playbook run when return code is NOT IN good codes (default: true)
110        :type stop_on_fail: boolean
111        :param stop_on_success: If true, stops playbook run when return code is IN good codes (default: false)
112        :type stop_on_success: boolean
113        :param changed: If True, tells Ansible that object was changed (default: false)
114        :type skipped: boolean
115        :param skipped: If True, tells Ansible that object was skipped (default: false)
116        :type skipped: boolean
117        :param unreachable: If True, tells Ansible that object was unreachable (default: false)
118        :type unreachable: boolean
119        :param failed: If True, tells Ansible that execution was a failure. Overrides good_codes. (default: false)
120        :type unreachable: boolean
121        :param success: If True, tells Ansible that execution was a success. Overrides good_codes. (default: false)
122        :type unreachable: boolean
123        :param changed_if_success: If True, defaults to changed if successful if you specify or not"
124        :type changed_if_success: boolean
125        :param ansible_facts: A prepared dictionary of ansible facts from the execution.
126        :type ansible_facts: dict
127        """
128        if module is None and results is None:
129            raise FMGBaseException("govern_response() was called without a module and/or results tuple! Fix!")
130        # Get the Return code from results
131        try:
132            rc = results[0]
133        except BaseException:
134            raise FMGBaseException("govern_response() was called without the return code at results[0]")
135
136        # init a few items
137        rc_data = None
138
139        # Get the default values for the said return code.
140        try:
141            rc_codes = FMGR_RC.get('fmgr_return_codes')
142            rc_data = rc_codes.get(rc)
143        except BaseException:
144            pass
145
146        if not rc_data:
147            rc_data = {}
148        # ONLY add to overrides if not none -- This is very important that the keys aren't added at this stage
149        # if they are empty. And there aren't that many, so let's just do a few if then statements.
150        if good_codes is not None:
151            rc_data["good_codes"] = good_codes
152        if stop_on_fail is not None:
153            rc_data["stop_on_fail"] = stop_on_fail
154        if stop_on_success is not None:
155            rc_data["stop_on_success"] = stop_on_success
156        if skipped is not None:
157            rc_data["skipped"] = skipped
158        if changed is not None:
159            rc_data["changed"] = changed
160        if unreachable is not None:
161            rc_data["unreachable"] = unreachable
162        if failed is not None:
163            rc_data["failed"] = failed
164        if success is not None:
165            rc_data["success"] = success
166        if changed_if_success is not None:
167            rc_data["changed_if_success"] = changed_if_success
168        if results is not None:
169            rc_data["results"] = results
170        if msg is not None:
171            rc_data["msg"] = msg
172        if ansible_facts is None:
173            rc_data["ansible_facts"] = {}
174        else:
175            rc_data["ansible_facts"] = ansible_facts
176
177        return self.return_response(module=module,
178                                    results=results,
179                                    msg=rc_data.get("msg", "NULL"),
180                                    good_codes=rc_data.get("good_codes", (0,)),
181                                    stop_on_fail=rc_data.get("stop_on_fail", True),
182                                    stop_on_success=rc_data.get("stop_on_success", False),
183                                    skipped=rc_data.get("skipped", False),
184                                    changed=rc_data.get("changed", False),
185                                    changed_if_success=rc_data.get("changed_if_success", False),
186                                    unreachable=rc_data.get("unreachable", False),
187                                    failed=rc_data.get("failed", False),
188                                    success=rc_data.get("success", False),
189                                    ansible_facts=rc_data.get("ansible_facts", dict()))
190
191    @staticmethod
192    def return_response(module, results, msg="NULL", good_codes=(0,),
193                        stop_on_fail=True, stop_on_success=False, skipped=False,
194                        changed=False, unreachable=False, failed=False, success=False, changed_if_success=True,
195                        ansible_facts=()):
196        """
197        This function controls the logout and error reporting after an method or function runs. The exit_json for
198        ansible comes from logic within this function. If this function returns just the msg, it means to continue
199        execution on the playbook. It is called from the ansible module, or from the self.govern_response function.
200
201        :param module: The Ansible Module CLASS object, used to run fail/exit json
202        :type module: object
203        :param msg: An overridable custom message from the module that called this.
204        :type msg: string
205        :param results: A dictionary object containing an API call results
206        :type results: dict
207        :param good_codes: A list of exit codes considered successful from FortiManager
208        :type good_codes: list
209        :param stop_on_fail: If true, stops playbook run when return code is NOT IN good codes (default: true)
210        :type stop_on_fail: boolean
211        :param stop_on_success: If true, stops playbook run when return code is IN good codes (default: false)
212        :type stop_on_success: boolean
213        :param changed: If True, tells Ansible that object was changed (default: false)
214        :type skipped: boolean
215        :param skipped: If True, tells Ansible that object was skipped (default: false)
216        :type skipped: boolean
217        :param unreachable: If True, tells Ansible that object was unreachable (default: false)
218        :type unreachable: boolean
219        :param failed: If True, tells Ansible that execution was a failure. Overrides good_codes. (default: false)
220        :type unreachable: boolean
221        :param success: If True, tells Ansible that execution was a success. Overrides good_codes. (default: false)
222        :type unreachable: boolean
223        :param changed_if_success: If True, defaults to changed if successful if you specify or not"
224        :type changed_if_success: boolean
225        :param ansible_facts: A prepared dictionary of ansible facts from the execution.
226        :type ansible_facts: dict
227
228        :return: A string object that contains an error message
229        :rtype: str
230        """
231
232        # VALIDATION ERROR
233        if (len(results) == 0) or (failed and success) or (changed and unreachable):
234            module.exit_json(msg="Handle_response was called with no results, or conflicting failed/success or "
235                                 "changed/unreachable parameters. Fix the exit code on module. "
236                                 "Generic Failure", failed=True)
237
238        # IDENTIFY SUCCESS/FAIL IF NOT DEFINED
239        if not failed and not success:
240            if len(results) > 0:
241                if results[0] not in good_codes:
242                    failed = True
243                elif results[0] in good_codes:
244                    success = True
245
246        if len(results) > 0:
247            # IF NO MESSAGE WAS SUPPLIED, GET IT FROM THE RESULTS, IF THAT DOESN'T WORK, THEN WRITE AN ERROR MESSAGE
248            if msg == "NULL":
249                try:
250                    msg = results[1]['status']['message']
251                except BaseException:
252                    msg = "No status message returned at results[1][status][message], " \
253                          "and none supplied to msg parameter for handle_response."
254
255            if failed:
256                # BECAUSE SKIPPED/FAILED WILL OFTEN OCCUR ON CODES THAT DON'T GET INCLUDED, THEY ARE CONSIDERED FAILURES
257                # HOWEVER, THEY ARE MUTUALLY EXCLUSIVE, SO IF IT IS MARKED SKIPPED OR UNREACHABLE BY THE MODULE LOGIC
258                # THEN REMOVE THE FAILED FLAG SO IT DOESN'T OVERRIDE THE DESIRED STATUS OF SKIPPED OR UNREACHABLE.
259                if failed and skipped:
260                    failed = False
261                if failed and unreachable:
262                    failed = False
263                if stop_on_fail:
264                    module.exit_json(failed=failed, changed=changed, unreachable=unreachable, skipped=skipped, meta=results[1])
265            elif success:
266                if changed_if_success:
267                    changed = True
268                    success = False
269                if stop_on_success:
270                    module.exit_json(success=success, changed=changed, unreachable=unreachable, skipped=skipped, meta=results[1])
271        return msg
272
273    def construct_ansible_facts(self, response, ansible_params, paramgram, *args, **kwargs):
274        """
275        Constructs a dictionary to return to ansible facts, containing various information about the execution.
276
277        :param response: Contains the response from the FortiManager.
278        :type response: dict
279        :param ansible_params: Contains the parameters Ansible was called with.
280        :type ansible_params: dict
281        :param paramgram: Contains the paramgram passed to the modules' local modify function.
282        :type paramgram: dict
283        :param args: Free-form arguments that could be added.
284        :param kwargs: Free-form keyword arguments that could be added.
285
286        :return: A dictionary containing lots of information to append to Ansible Facts.
287        :rtype: dict
288        """
289
290        facts = {
291            "response": response,
292            "ansible_params": scrub_dict(ansible_params),
293            "paramgram": scrub_dict(paramgram),
294            "connected_fmgr": self._conn.return_connected_fmgr()
295        }
296
297        if args:
298            facts["custom_args"] = args
299        if kwargs:
300            facts.update(kwargs)
301
302        return facts
303
304
305##########################
306# BEGIN DEPRECATED METHODS
307##########################
308
309# SOME OF THIS CODE IS DUPLICATED IN THE PLUGIN, BUT THOSE ARE PLUGIN SPECIFIC. THIS VERSION STILL ALLOWS FOR
310# THE USAGE OF PYFMG FOR CUSTOMERS WHO HAVE NOT YET UPGRADED TO ANSIBLE 2.7
311
312# LEGACY PYFMG METHODS START
313# USED TO DETERMINE LOCK CONTEXT ON A FORTIMANAGER. A DATABASE LOCKING CONCEPT THAT NEEDS TO BE ACCOUNTED FOR.
314
315class FMGLockContext(object):
316    """
317    - DEPRECATING: USING CONNECTION MANAGER NOW INSTEAD. EVENTUALLY THIS CLASS WILL DISAPPEAR. PLEASE
318    - CONVERT ALL MODULES TO CONNECTION MANAGER METHOD.
319    - LEGACY pyFMG HANDLER OBJECT: REQUIRES A CHECK FOR PY FMG AT TOP OF PAGE
320    """
321    def __init__(self, fmg):
322        self._fmg = fmg
323        self._locked_adom_list = list()
324        self._uses_workspace = False
325        self._uses_adoms = False
326
327    @property
328    def uses_workspace(self):
329        return self._uses_workspace
330
331    @uses_workspace.setter
332    def uses_workspace(self, val):
333        self._uses_workspace = val
334
335    @property
336    def uses_adoms(self):
337        return self._uses_adoms
338
339    @uses_adoms.setter
340    def uses_adoms(self, val):
341        self._uses_adoms = val
342
343    def add_adom_to_lock_list(self, adom):
344        if adom not in self._locked_adom_list:
345            self._locked_adom_list.append(adom)
346
347    def remove_adom_from_lock_list(self, adom):
348        if adom in self._locked_adom_list:
349            self._locked_adom_list.remove(adom)
350
351    def check_mode(self):
352        url = "/cli/global/system/global"
353        code, resp_obj = self._fmg.get(url, fields=["workspace-mode", "adom-status"])
354        try:
355            if resp_obj["workspace-mode"] != 0:
356                self.uses_workspace = True
357        except KeyError:
358            self.uses_workspace = False
359        try:
360            if resp_obj["adom-status"] == 1:
361                self.uses_adoms = True
362        except KeyError:
363            self.uses_adoms = False
364
365    def run_unlock(self):
366        for adom_locked in self._locked_adom_list:
367            self.unlock_adom(adom_locked)
368
369    def lock_adom(self, adom=None, *args, **kwargs):
370        if adom:
371            if adom.lower() == "global":
372                url = "/dvmdb/global/workspace/lock/"
373            else:
374                url = "/dvmdb/adom/{adom}/workspace/lock/".format(adom=adom)
375        else:
376            url = "/dvmdb/adom/root/workspace/lock"
377        code, respobj = self._fmg.execute(url, {}, *args, **kwargs)
378        if code == 0 and respobj["status"]["message"].lower() == "ok":
379            self.add_adom_to_lock_list(adom)
380        return code, respobj
381
382    def unlock_adom(self, adom=None, *args, **kwargs):
383        if adom:
384            if adom.lower() == "global":
385                url = "/dvmdb/global/workspace/unlock/"
386            else:
387                url = "/dvmdb/adom/{adom}/workspace/unlock/".format(adom=adom)
388        else:
389            url = "/dvmdb/adom/root/workspace/unlock"
390        code, respobj = self._fmg.execute(url, {}, *args, **kwargs)
391        if code == 0 and respobj["status"]["message"].lower() == "ok":
392            self.remove_adom_from_lock_list(adom)
393        return code, respobj
394
395    def commit_changes(self, adom=None, aux=False, *args, **kwargs):
396        if adom:
397            if aux:
398                url = "/pm/config/adom/{adom}/workspace/commit".format(adom=adom)
399            else:
400                if adom.lower() == "global":
401                    url = "/dvmdb/global/workspace/commit/"
402                else:
403                    url = "/dvmdb/adom/{adom}/workspace/commit".format(adom=adom)
404        else:
405            url = "/dvmdb/adom/root/workspace/commit"
406        return self._fmg.execute(url, {}, *args, **kwargs)
407
408
409# DEPRECATED -- USE PLUGIN INSTEAD
410class AnsibleFortiManager(object):
411    """
412    - DEPRECATING: USING CONNECTION MANAGER NOW INSTEAD. EVENTUALLY THIS CLASS WILL DISAPPEAR. PLEASE
413    - CONVERT ALL MODULES TO CONNECTION MANAGER METHOD.
414    - LEGACY pyFMG HANDLER OBJECT: REQUIRES A CHECK FOR PY FMG AT TOP OF PAGE
415    """
416
417    def __init__(self, module, ip=None, username=None, passwd=None, use_ssl=True, verify_ssl=False, timeout=300):
418        self.ip = ip
419        self.username = username
420        self.passwd = passwd
421        self.use_ssl = use_ssl
422        self.verify_ssl = verify_ssl
423        self.timeout = timeout
424        self.fmgr_instance = None
425
426        if not HAS_PYFMGR:
427            module.fail_json(msg='Could not import the python library pyFMG required by this module')
428
429        self.module = module
430
431    def login(self):
432        if self.ip is not None:
433            self.fmgr_instance = FortiManager(self.ip, self.username, self.passwd, use_ssl=self.use_ssl,
434                                              verify_ssl=self.verify_ssl, timeout=self.timeout, debug=False,
435                                              disable_request_warnings=True)
436            return self.fmgr_instance.login()
437
438    def logout(self):
439        if self.fmgr_instance.sid is not None:
440            self.fmgr_instance.logout()
441
442    def get(self, url, data):
443        return self.fmgr_instance.get(url, **data)
444
445    def set(self, url, data):
446        return self.fmgr_instance.set(url, **data)
447
448    def update(self, url, data):
449        return self.fmgr_instance.update(url, **data)
450
451    def delete(self, url, data):
452        return self.fmgr_instance.delete(url, **data)
453
454    def add(self, url, data):
455        return self.fmgr_instance.add(url, **data)
456
457    def execute(self, url, data):
458        return self.fmgr_instance.execute(url, **data)
459
460    def move(self, url, data):
461        return self.fmgr_instance.move(url, **data)
462
463    def clone(self, url, data):
464        return self.fmgr_instance.clone(url, **data)
465
466##########################
467# END DEPRECATED METHODS
468##########################
469