1# Copyright (c) 2018 Fortinet and/or its affiliates.
2#
3# This file is part of Ansible
4#
5# Ansible is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Ansible is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
17#
18
19from __future__ import (absolute_import, division, print_function)
20__metaclass__ = type
21
22DOCUMENTATION = """
23---
24author:
25    - Luke Weighall (@lweighall)
26    - Andrew Welsh (@Ghilli3)
27    - Jim Huber (@p4r4n0y1ng)
28httpapi : fortimanager
29short_description: HttpApi Plugin for Fortinet FortiManager Appliance or VM
30description:
31  - This HttpApi plugin provides methods to connect to Fortinet FortiManager Appliance or VM via JSON RPC API
32version_added: "2.8"
33
34"""
35
36import json
37from ansible.plugins.httpapi import HttpApiBase
38from ansible.module_utils.basic import to_text
39from ansible.module_utils.network.fortimanager.common import BASE_HEADERS
40from ansible.module_utils.network.fortimanager.common import FMGBaseException
41from ansible.module_utils.network.fortimanager.common import FMGRCommon
42from ansible.module_utils.network.fortimanager.common import FMGRMethods
43
44
45class HttpApi(HttpApiBase):
46    def __init__(self, connection):
47        super(HttpApi, self).__init__(connection)
48        self._req_id = 0
49        self._sid = None
50        self._url = "/jsonrpc"
51        self._host = None
52        self._tools = FMGRCommon
53        self._debug = False
54        self._connected_fmgr = None
55        self._last_response_msg = None
56        self._last_response_code = None
57        self._last_data_payload = None
58        self._last_url = None
59        self._last_response_raw = None
60        self._locked_adom_list = list()
61        self._uses_workspace = False
62        self._uses_adoms = False
63
64    def set_become(self, become_context):
65        """
66        ELEVATION IS NOT REQUIRED ON FORTINET DEVICES - SKIPPED
67        :param become_context: Unused input.
68        :return: None
69        """
70        return None
71
72    def update_auth(self, response, response_data):
73        """
74        TOKENS ARE NOT USED SO NO NEED TO UPDATE AUTH
75        :param response: Unused input.
76        :param response_data Unused_input.
77        :return: None
78        """
79        return None
80
81    def login(self, username, password):
82        """
83        This function will log the plugin into FortiManager, and return the results.
84        :param username: Username of FortiManager Admin
85        :param password: Password of FortiManager Admin
86
87        :return: Dictionary of status, if it logged in or not.
88        """
89        self.send_request(FMGRMethods.EXEC, self._tools.format_request(FMGRMethods.EXEC, "sys/login/user",
90                                                                       passwd=password, user=username,))
91
92        if "FortiManager object connected to FortiManager" in self.__str__():
93            # If Login worked, then inspect the FortiManager for Workspace Mode, and it's system information.
94            self.inspect_fmgr()
95            return
96        else:
97            raise FMGBaseException(msg="Unknown error while logging in...connection was lost during login operation...."
98                                       " Exiting")
99
100    def inspect_fmgr(self):
101        # CHECK FOR WORKSPACE MODE TO SEE IF WE HAVE TO ENABLE ADOM LOCKS
102
103        self.check_mode()
104        # CHECK FOR SYSTEM STATUS -- SHOULD RETURN 0
105        status = self.get_system_status()
106        if status[0] == -11:
107            # THE CONNECTION GOT LOST SOMEHOW, REMOVE THE SID AND REPORT BAD LOGIN
108            self.logout()
109            raise FMGBaseException(msg="Error -11 -- the Session ID was likely malformed somehow. Contact authors."
110                                       " Exiting")
111        elif status[0] == 0:
112            try:
113                self._connected_fmgr = status[1]
114                self._host = self._connected_fmgr["Hostname"]
115            except BaseException:
116                pass
117        return
118
119    def logout(self):
120        """
121        This function will logout of the FortiManager.
122        """
123        if self.sid is not None:
124            if self.uses_workspace:
125                self.run_unlock()
126            ret_code, response = self.send_request(FMGRMethods.EXEC,
127                                                   self._tools.format_request(FMGRMethods.EXEC, "sys/logout"))
128            self.sid = None
129            return ret_code, response
130
131    def send_request(self, method, params):
132        """
133        Responsible for actual sending of data to the connection httpapi base plugin. Does some formatting as well.
134        :param params: A formatted dictionary that was returned by self.common_datagram_params()
135        before being called here.
136        :param method: The preferred API Request method (GET, ADD, POST, etc....)
137        :type method: basestring
138
139        :return: Dictionary of status, if it logged in or not.
140        """
141
142        try:
143            if self.sid is None and params[0]["url"] != "sys/login/user":
144                raise FMGBaseException("An attempt was made to login with the SID None and URL != login url.")
145        except IndexError:
146            raise FMGBaseException("An attempt was made at communicating with a FMG with "
147                                   "no valid session and an incorrectly formatted request.")
148        except Exception:
149            raise FMGBaseException("An attempt was made at communicating with a FMG with "
150                                   "no valid session and an unexpected error was discovered.")
151
152        self._update_request_id()
153        json_request = {
154            "method": method,
155            "params": params,
156            "session": self.sid,
157            "id": self.req_id,
158            "verbose": 1
159        }
160        data = json.dumps(json_request, ensure_ascii=False).replace('\\\\', '\\')
161        try:
162            # Sending URL and Data in Unicode, per Ansible Specifications for Connection Plugins
163            response, response_data = self.connection.send(path=to_text(self._url), data=to_text(data),
164                                                           headers=BASE_HEADERS)
165            # Get Unicode Response - Must convert from StringIO to unicode first so we can do a replace function below
166            result = json.loads(to_text(response_data.getvalue()))
167            self._update_self_from_response(result, self._url, data)
168            return self._handle_response(result)
169        except Exception as err:
170            raise FMGBaseException(err)
171
172    def _handle_response(self, response):
173        self._set_sid(response)
174        if isinstance(response["result"], list):
175            result = response["result"][0]
176        else:
177            result = response["result"]
178        if "data" in result:
179            return result["status"]["code"], result["data"]
180        else:
181            return result["status"]["code"], result
182
183    def _update_self_from_response(self, response, url, data):
184        self._last_response_raw = response
185        if isinstance(response["result"], list):
186            result = response["result"][0]
187        else:
188            result = response["result"]
189        if "status" in result:
190            self._last_response_code = result["status"]["code"]
191            self._last_response_msg = result["status"]["message"]
192            self._last_url = url
193            self._last_data_payload = data
194
195    def _set_sid(self, response):
196        if self.sid is None and "session" in response:
197            self.sid = response["session"]
198
199    def return_connected_fmgr(self):
200        """
201        Returns the data stored under self._connected_fmgr
202
203        :return: dict
204        """
205        try:
206            if self._connected_fmgr:
207                return self._connected_fmgr
208        except BaseException:
209            raise FMGBaseException("Couldn't Retrieve Connected FMGR Stats")
210
211    def get_system_status(self):
212        """
213        Returns the system status page from the FortiManager, for logging and other uses.
214        return: status
215        """
216        status = self.send_request(FMGRMethods.GET, self._tools.format_request(FMGRMethods.GET, "sys/status"))
217        return status
218
219    @property
220    def debug(self):
221        return self._debug
222
223    @debug.setter
224    def debug(self, val):
225        self._debug = val
226
227    @property
228    def req_id(self):
229        return self._req_id
230
231    @req_id.setter
232    def req_id(self, val):
233        self._req_id = val
234
235    def _update_request_id(self, reqid=0):
236        self.req_id = reqid if reqid != 0 else self.req_id + 1
237
238    @property
239    def sid(self):
240        return self._sid
241
242    @sid.setter
243    def sid(self, val):
244        self._sid = val
245
246    def __str__(self):
247        if self.sid is not None and self.connection._url is not None:
248            return "FortiManager object connected to FortiManager: " + str(self.connection._url)
249        return "FortiManager object with no valid connection to a FortiManager appliance."
250
251    ##################################
252    # BEGIN DATABASE LOCK CONTEXT CODE
253    ##################################
254
255    @property
256    def uses_workspace(self):
257        return self._uses_workspace
258
259    @uses_workspace.setter
260    def uses_workspace(self, val):
261        self._uses_workspace = val
262
263    @property
264    def uses_adoms(self):
265        return self._uses_adoms
266
267    @uses_adoms.setter
268    def uses_adoms(self, val):
269        self._uses_adoms = val
270
271    def add_adom_to_lock_list(self, adom):
272        if adom not in self._locked_adom_list:
273            self._locked_adom_list.append(adom)
274
275    def remove_adom_from_lock_list(self, adom):
276        if adom in self._locked_adom_list:
277            self._locked_adom_list.remove(adom)
278
279    def check_mode(self):
280        """
281        Checks FortiManager for the use of Workspace mode
282        """
283        url = "/cli/global/system/global"
284        code, resp_obj = self.send_request(FMGRMethods.GET, self._tools.format_request(FMGRMethods.GET, url,
285                                                                                       fields=["workspace-mode",
286                                                                                               "adom-status"]))
287        try:
288            if resp_obj["workspace-mode"] != 0:
289                self.uses_workspace = True
290        except KeyError:
291            self.uses_workspace = False
292        try:
293            if resp_obj["adom-status"] == 1:
294                self.uses_adoms = True
295        except KeyError:
296            self.uses_adoms = False
297
298    def run_unlock(self):
299        """
300        Checks for ADOM status, if locked, it will unlock
301        """
302        for adom_locked in self._locked_adom_list:
303            self.unlock_adom(adom_locked)
304
305    def lock_adom(self, adom=None, *args, **kwargs):
306        """
307        Locks an ADOM for changes
308        """
309        if adom:
310            if adom.lower() == "global":
311                url = "/dvmdb/global/workspace/lock/"
312            else:
313                url = "/dvmdb/adom/{adom}/workspace/lock/".format(adom=adom)
314        else:
315            url = "/dvmdb/adom/root/workspace/lock"
316        code, respobj = self.send_request(FMGRMethods.EXEC, self._tools.format_request(FMGRMethods.EXEC, url))
317        if code == 0 and respobj["status"]["message"].lower() == "ok":
318            self.add_adom_to_lock_list(adom)
319        return code, respobj
320
321    def unlock_adom(self, adom=None, *args, **kwargs):
322        """
323        Unlocks an ADOM after changes
324        """
325        if adom:
326            if adom.lower() == "global":
327                url = "/dvmdb/global/workspace/unlock/"
328            else:
329                url = "/dvmdb/adom/{adom}/workspace/unlock/".format(adom=adom)
330        else:
331            url = "/dvmdb/adom/root/workspace/unlock"
332        code, respobj = self.send_request(FMGRMethods.EXEC, self._tools.format_request(FMGRMethods.EXEC, url))
333        if code == 0 and respobj["status"]["message"].lower() == "ok":
334            self.remove_adom_from_lock_list(adom)
335        return code, respobj
336
337    def commit_changes(self, adom=None, aux=False, *args, **kwargs):
338        """
339        Commits changes to an ADOM
340        """
341        if adom:
342            if aux:
343                url = "/pm/config/adom/{adom}/workspace/commit".format(adom=adom)
344            else:
345                if adom.lower() == "global":
346                    url = "/dvmdb/global/workspace/commit/"
347                else:
348                    url = "/dvmdb/adom/{adom}/workspace/commit".format(adom=adom)
349        else:
350            url = "/dvmdb/adom/root/workspace/commit"
351        return self.send_request(FMGRMethods.EXEC, self._tools.format_request(FMGRMethods.EXEC, url))
352
353    ################################
354    # END DATABASE LOCK CONTEXT CODE
355    ################################
356