1# -*- coding: utf-8 -*-
2# This code is part of Ansible, but is an independent component.
3# This particular file snippet, and this file snippet only, is BSD licensed.
4# Modules you write using this snippet, which is embedded dynamically by Ansible
5# still belong to the author of the module, and may assign their own license
6# to the complete work.
7#
8# Copyright (c) 2017, Sumit Kumar <sumit4@netapp.com>
9# Copyright (c) 2017, Michael Price <michael.price@netapp.com>
10# All rights reserved.
11#
12# Redistribution and use in source and binary forms, with or without modification,
13# are permitted provided that the following conditions are met:
14#
15#    * Redistributions of source code must retain the above copyright
16#      notice, this list of conditions and the following disclaimer.
17#    * Redistributions in binary form must reproduce the above copyright notice,
18#      this list of conditions and the following disclaimer in the documentation
19#      and/or other materials provided with the distribution.
20#
21# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
22# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
24# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
25# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
28# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
29# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
31from __future__ import (absolute_import, division, print_function)
32__metaclass__ = type
33
34import json
35import os
36import random
37import mimetypes
38
39from pprint import pformat
40from ansible.module_utils import six
41from ansible.module_utils.basic import AnsibleModule, missing_required_lib
42from ansible.module_utils.six.moves.urllib.error import HTTPError, URLError
43from ansible.module_utils.urls import open_url
44from ansible.module_utils.api import basic_auth_argument_spec
45from ansible.module_utils.common.text.converters import to_native
46
47try:
48    from ansible.module_utils.ansible_release import __version__ as ansible_version
49except ImportError:
50    ansible_version = 'unknown'
51
52try:
53    from netapp_lib.api.zapi import zapi
54    HAS_NETAPP_LIB = True
55except ImportError:
56    HAS_NETAPP_LIB = False
57
58try:
59    import requests
60    HAS_REQUESTS = True
61except ImportError:
62    HAS_REQUESTS = False
63
64import ssl
65try:
66    from urlparse import urlparse, urlunparse
67except ImportError:
68    from urllib.parse import urlparse, urlunparse
69
70
71HAS_SF_SDK = False
72SF_BYTE_MAP = dict(
73    # Management GUI displays 1024 ** 3 as 1.1 GB, thus use 1000.
74    bytes=1,
75    b=1,
76    kb=1000,
77    mb=1000 ** 2,
78    gb=1000 ** 3,
79    tb=1000 ** 4,
80    pb=1000 ** 5,
81    eb=1000 ** 6,
82    zb=1000 ** 7,
83    yb=1000 ** 8
84)
85
86POW2_BYTE_MAP = dict(
87    # Here, 1 kb = 1024
88    bytes=1,
89    b=1,
90    kb=1024,
91    mb=1024 ** 2,
92    gb=1024 ** 3,
93    tb=1024 ** 4,
94    pb=1024 ** 5,
95    eb=1024 ** 6,
96    zb=1024 ** 7,
97    yb=1024 ** 8
98)
99
100try:
101    from solidfire.factory import ElementFactory
102    from solidfire.custom.models import TimeIntervalFrequency
103    from solidfire.models import Schedule, ScheduleInfo
104
105    HAS_SF_SDK = True
106except Exception:
107    HAS_SF_SDK = False
108
109
110def has_netapp_lib():
111    return HAS_NETAPP_LIB
112
113
114def has_sf_sdk():
115    return HAS_SF_SDK
116
117
118def na_ontap_host_argument_spec():
119
120    return dict(
121        hostname=dict(required=True, type='str'),
122        username=dict(required=True, type='str', aliases=['user']),
123        password=dict(required=True, type='str', aliases=['pass'], no_log=True),
124        https=dict(required=False, type='bool', default=False),
125        validate_certs=dict(required=False, type='bool', default=True),
126        http_port=dict(required=False, type='int'),
127        ontapi=dict(required=False, type='int'),
128        use_rest=dict(required=False, type='str', default='Auto', choices=['Never', 'Always', 'Auto'])
129    )
130
131
132def ontap_sf_host_argument_spec():
133
134    return dict(
135        hostname=dict(required=True, type='str'),
136        username=dict(required=True, type='str', aliases=['user']),
137        password=dict(required=True, type='str', aliases=['pass'], no_log=True)
138    )
139
140
141def aws_cvs_host_argument_spec():
142
143    return dict(
144        api_url=dict(required=True, type='str'),
145        validate_certs=dict(required=False, type='bool', default=True),
146        api_key=dict(required=True, type='str', no_log=True),
147        secret_key=dict(required=True, type='str', no_log=True)
148    )
149
150
151def create_sf_connection(module, port=None):
152    hostname = module.params['hostname']
153    username = module.params['username']
154    password = module.params['password']
155
156    if HAS_SF_SDK and hostname and username and password:
157        try:
158            return_val = ElementFactory.create(hostname, username, password, port=port)
159            return return_val
160        except Exception:
161            raise Exception("Unable to create SF connection")
162    else:
163        module.fail_json(msg="the python SolidFire SDK module is required")
164
165
166def setup_na_ontap_zapi(module, vserver=None):
167    hostname = module.params['hostname']
168    username = module.params['username']
169    password = module.params['password']
170    https = module.params['https']
171    validate_certs = module.params['validate_certs']
172    port = module.params['http_port']
173    version = module.params['ontapi']
174
175    if HAS_NETAPP_LIB:
176        # set up zapi
177        server = zapi.NaServer(hostname)
178        server.set_username(username)
179        server.set_password(password)
180        if vserver:
181            server.set_vserver(vserver)
182        if version:
183            minor = version
184        else:
185            minor = 110
186        server.set_api_version(major=1, minor=minor)
187        # default is HTTP
188        if https:
189            if port is None:
190                port = 443
191            transport_type = 'HTTPS'
192            # HACK to bypass certificate verification
193            if validate_certs is False:
194                if not os.environ.get('PYTHONHTTPSVERIFY', '') and getattr(ssl, '_create_unverified_context', None):
195                    ssl._create_default_https_context = ssl._create_unverified_context
196        else:
197            if port is None:
198                port = 80
199            transport_type = 'HTTP'
200        server.set_transport_type(transport_type)
201        server.set_port(port)
202        server.set_server_type('FILER')
203        return server
204    else:
205        module.fail_json(msg="the python NetApp-Lib module is required")
206
207
208def setup_ontap_zapi(module, vserver=None):
209    hostname = module.params['hostname']
210    username = module.params['username']
211    password = module.params['password']
212
213    if HAS_NETAPP_LIB:
214        # set up zapi
215        server = zapi.NaServer(hostname)
216        server.set_username(username)
217        server.set_password(password)
218        if vserver:
219            server.set_vserver(vserver)
220        # Todo : Replace hard-coded values with configurable parameters.
221        server.set_api_version(major=1, minor=110)
222        server.set_port(80)
223        server.set_server_type('FILER')
224        server.set_transport_type('HTTP')
225        return server
226    else:
227        module.fail_json(msg="the python NetApp-Lib module is required")
228
229
230def eseries_host_argument_spec():
231    """Retrieve a base argument specification common to all NetApp E-Series modules"""
232    argument_spec = basic_auth_argument_spec()
233    argument_spec.update(dict(
234        api_username=dict(type='str', required=True),
235        api_password=dict(type='str', required=True, no_log=True),
236        api_url=dict(type='str', required=True),
237        ssid=dict(type='str', required=False, default='1'),
238        validate_certs=dict(type='bool', required=False, default=True)
239    ))
240    return argument_spec
241
242
243class NetAppESeriesModule(object):
244    """Base class for all NetApp E-Series modules.
245
246    Provides a set of common methods for NetApp E-Series modules, including version checking, mode (proxy, embedded)
247    verification, http requests, secure http redirection for embedded web services, and logging setup.
248
249    Be sure to add the following lines in the module's documentation section:
250    extends_documentation_fragment:
251        - netapp.eseries
252
253    :param dict(dict) ansible_options: dictionary of ansible option definitions
254    :param str web_services_version: minimally required web services rest api version (default value: "02.00.0000.0000")
255    :param bool supports_check_mode: whether the module will support the check_mode capabilities (default=False)
256    :param list(list) mutually_exclusive: list containing list(s) of mutually exclusive options (optional)
257    :param list(list) required_if: list containing list(s) containing the option, the option value, and then
258    a list of required options. (optional)
259    :param list(list) required_one_of: list containing list(s) of options for which at least one is required. (optional)
260    :param list(list) required_together: list containing list(s) of options that are required together. (optional)
261    :param bool log_requests: controls whether to log each request (default: True)
262    """
263    DEFAULT_TIMEOUT = 60
264    DEFAULT_SECURE_PORT = "8443"
265    DEFAULT_REST_API_PATH = "devmgr/v2/"
266    DEFAULT_REST_API_ABOUT_PATH = "devmgr/utils/about"
267    DEFAULT_HEADERS = {"Content-Type": "application/json", "Accept": "application/json",
268                       "netapp-client-type": "Ansible-%s" % ansible_version}
269    HTTP_AGENT = "Ansible / %s" % ansible_version
270    SIZE_UNIT_MAP = dict(bytes=1, b=1, kb=1024, mb=1024**2, gb=1024**3, tb=1024**4,
271                         pb=1024**5, eb=1024**6, zb=1024**7, yb=1024**8)
272
273    def __init__(self, ansible_options, web_services_version=None, supports_check_mode=False,
274                 mutually_exclusive=None, required_if=None, required_one_of=None, required_together=None,
275                 log_requests=True):
276        argument_spec = eseries_host_argument_spec()
277        argument_spec.update(ansible_options)
278
279        self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=supports_check_mode,
280                                    mutually_exclusive=mutually_exclusive, required_if=required_if,
281                                    required_one_of=required_one_of, required_together=required_together)
282
283        args = self.module.params
284        self.web_services_version = web_services_version if web_services_version else "02.00.0000.0000"
285        self.ssid = args["ssid"]
286        self.url = args["api_url"]
287        self.log_requests = log_requests
288        self.creds = dict(url_username=args["api_username"],
289                          url_password=args["api_password"],
290                          validate_certs=args["validate_certs"])
291
292        if not self.url.endswith("/"):
293            self.url += "/"
294
295        self.is_embedded_mode = None
296        self.is_web_services_valid_cache = None
297
298    def _check_web_services_version(self):
299        """Verify proxy or embedded web services meets minimum version required for module.
300
301        The minimum required web services version is evaluated against version supplied through the web services rest
302        api. AnsibleFailJson exception will be raised when the minimum is not met or exceeded.
303
304        This helper function will update the supplied api url if secure http is not used for embedded web services
305
306        :raise AnsibleFailJson: raised when the contacted api service does not meet the minimum required version.
307        """
308        if not self.is_web_services_valid_cache:
309
310            url_parts = urlparse(self.url)
311            if not url_parts.scheme or not url_parts.netloc:
312                self.module.fail_json(msg="Failed to provide valid API URL. Example: https://192.168.1.100:8443/devmgr/v2. URL [%s]." % self.url)
313
314            if url_parts.scheme not in ["http", "https"]:
315                self.module.fail_json(msg="Protocol must be http or https. URL [%s]." % self.url)
316
317            self.url = "%s://%s/" % (url_parts.scheme, url_parts.netloc)
318            about_url = self.url + self.DEFAULT_REST_API_ABOUT_PATH
319            rc, data = request(about_url, timeout=self.DEFAULT_TIMEOUT, headers=self.DEFAULT_HEADERS, ignore_errors=True, **self.creds)
320
321            if rc != 200:
322                self.module.warn("Failed to retrieve web services about information! Retrying with secure ports. Array Id [%s]." % self.ssid)
323                self.url = "https://%s:8443/" % url_parts.netloc.split(":")[0]
324                about_url = self.url + self.DEFAULT_REST_API_ABOUT_PATH
325                try:
326                    rc, data = request(about_url, timeout=self.DEFAULT_TIMEOUT, headers=self.DEFAULT_HEADERS, **self.creds)
327                except Exception as error:
328                    self.module.fail_json(msg="Failed to retrieve the webservices about information! Array Id [%s]. Error [%s]."
329                                              % (self.ssid, to_native(error)))
330
331            major, minor, other, revision = data["version"].split(".")
332            minimum_major, minimum_minor, other, minimum_revision = self.web_services_version.split(".")
333
334            if not (major > minimum_major or
335                    (major == minimum_major and minor > minimum_minor) or
336                    (major == minimum_major and minor == minimum_minor and revision >= minimum_revision)):
337                self.module.fail_json(msg="Web services version does not meet minimum version required. Current version: [%s]."
338                                          " Version required: [%s]." % (data["version"], self.web_services_version))
339
340            self.module.log("Web services rest api version met the minimum required version.")
341            self.is_web_services_valid_cache = True
342
343    def is_embedded(self):
344        """Determine whether web services server is the embedded web services.
345
346        If web services about endpoint fails based on an URLError then the request will be attempted again using
347        secure http.
348
349        :raise AnsibleFailJson: raised when web services about endpoint failed to be contacted.
350        :return bool: whether contacted web services is running from storage array (embedded) or from a proxy.
351        """
352        self._check_web_services_version()
353
354        if self.is_embedded_mode is None:
355            about_url = self.url + self.DEFAULT_REST_API_ABOUT_PATH
356            try:
357                rc, data = request(about_url, timeout=self.DEFAULT_TIMEOUT, headers=self.DEFAULT_HEADERS, **self.creds)
358                self.is_embedded_mode = not data["runningAsProxy"]
359            except Exception as error:
360                self.module.fail_json(msg="Failed to retrieve the webservices about information! Array Id [%s]. Error [%s]."
361                                          % (self.ssid, to_native(error)))
362
363        return self.is_embedded_mode
364
365    def request(self, path, data=None, method='GET', headers=None, ignore_errors=False):
366        """Issue an HTTP request to a url, retrieving an optional JSON response.
367
368        :param str path: web services rest api endpoint path (Example: storage-systems/1/graph). Note that when the
369        full url path is specified then that will be used without supplying the protocol, hostname, port and rest path.
370        :param data: data required for the request (data may be json or any python structured data)
371        :param str method: request method such as GET, POST, DELETE.
372        :param dict headers: dictionary containing request headers.
373        :param bool ignore_errors: forces the request to ignore any raised exceptions.
374        """
375        self._check_web_services_version()
376
377        if headers is None:
378            headers = self.DEFAULT_HEADERS
379
380        if not isinstance(data, str) and headers["Content-Type"] == "application/json":
381            data = json.dumps(data)
382
383        if path.startswith("/"):
384            path = path[1:]
385        request_url = self.url + self.DEFAULT_REST_API_PATH + path
386
387        # if self.log_requests:
388        self.module.log(pformat(dict(url=request_url, data=data, method=method)))
389
390        return request(url=request_url, data=data, method=method, headers=headers, use_proxy=True, force=False, last_mod_time=None,
391                       timeout=self.DEFAULT_TIMEOUT, http_agent=self.HTTP_AGENT, force_basic_auth=True, ignore_errors=ignore_errors, **self.creds)
392
393
394def create_multipart_formdata(files, fields=None, send_8kb=False):
395    """Create the data for a multipart/form request.
396
397    :param list(list) files: list of lists each containing (name, filename, path).
398    :param list(list) fields: list of lists each containing (key, value).
399    :param bool send_8kb: only sends the first 8kb of the files (default: False).
400    """
401    boundary = "---------------------------" + "".join([str(random.randint(0, 9)) for x in range(27)])
402    data_parts = list()
403    data = None
404
405    if six.PY2:  # Generate payload for Python 2
406        newline = "\r\n"
407        if fields is not None:
408            for key, value in fields:
409                data_parts.extend(["--%s" % boundary,
410                                   'Content-Disposition: form-data; name="%s"' % key,
411                                   "",
412                                   value])
413
414        for name, filename, path in files:
415            with open(path, "rb") as fh:
416                value = fh.read(8192) if send_8kb else fh.read()
417
418                data_parts.extend(["--%s" % boundary,
419                                   'Content-Disposition: form-data; name="%s"; filename="%s"' % (name, filename),
420                                   "Content-Type: %s" % (mimetypes.guess_type(path)[0] or "application/octet-stream"),
421                                   "",
422                                   value])
423        data_parts.extend(["--%s--" % boundary, ""])
424        data = newline.join(data_parts)
425
426    else:
427        newline = six.b("\r\n")
428        if fields is not None:
429            for key, value in fields:
430                data_parts.extend([six.b("--%s" % boundary),
431                                   six.b('Content-Disposition: form-data; name="%s"' % key),
432                                   six.b(""),
433                                   six.b(value)])
434
435        for name, filename, path in files:
436            with open(path, "rb") as fh:
437                value = fh.read(8192) if send_8kb else fh.read()
438
439                data_parts.extend([six.b("--%s" % boundary),
440                                   six.b('Content-Disposition: form-data; name="%s"; filename="%s"' % (name, filename)),
441                                   six.b("Content-Type: %s" % (mimetypes.guess_type(path)[0] or "application/octet-stream")),
442                                   six.b(""),
443                                   value])
444        data_parts.extend([six.b("--%s--" % boundary), b""])
445        data = newline.join(data_parts)
446
447    headers = {
448        "Content-Type": "multipart/form-data; boundary=%s" % boundary,
449        "Content-Length": str(len(data))}
450
451    return headers, data
452
453
454def request(url, data=None, headers=None, method='GET', use_proxy=True,
455            force=False, last_mod_time=None, timeout=10, validate_certs=True,
456            url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False):
457    """Issue an HTTP request to a url, retrieving an optional JSON response."""
458
459    if headers is None:
460        headers = {"Content-Type": "application/json", "Accept": "application/json"}
461    headers.update({"netapp-client-type": "Ansible-%s" % ansible_version})
462
463    if not http_agent:
464        http_agent = "Ansible / %s" % ansible_version
465
466    try:
467        r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy,
468                     force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs,
469                     url_username=url_username, url_password=url_password, http_agent=http_agent,
470                     force_basic_auth=force_basic_auth)
471    except HTTPError as err:
472        r = err.fp
473
474    try:
475        raw_data = r.read()
476        if raw_data:
477            data = json.loads(raw_data)
478        else:
479            raw_data = None
480    except Exception:
481        if ignore_errors:
482            pass
483        else:
484            raise Exception(raw_data)
485
486    resp_code = r.getcode()
487
488    if resp_code >= 400 and not ignore_errors:
489        raise Exception(resp_code, data)
490    else:
491        return resp_code, data
492
493
494def ems_log_event(source, server, name="Ansible", id="12345", version=ansible_version,
495                  category="Information", event="setup", autosupport="false"):
496    ems_log = zapi.NaElement('ems-autosupport-log')
497    # Host name invoking the API.
498    ems_log.add_new_child("computer-name", name)
499    # ID of event. A user defined event-id, range [0..2^32-2].
500    ems_log.add_new_child("event-id", id)
501    # Name of the application invoking the API.
502    ems_log.add_new_child("event-source", source)
503    # Version of application invoking the API.
504    ems_log.add_new_child("app-version", version)
505    # Application defined category of the event.
506    ems_log.add_new_child("category", category)
507    # Description of event to log. An application defined message to log.
508    ems_log.add_new_child("event-description", event)
509    ems_log.add_new_child("log-level", "6")
510    ems_log.add_new_child("auto-support", autosupport)
511    server.invoke_successfully(ems_log, True)
512
513
514def get_cserver_zapi(server):
515    vserver_info = zapi.NaElement('vserver-get-iter')
516    query_details = zapi.NaElement.create_node_with_children('vserver-info', **{'vserver-type': 'admin'})
517    query = zapi.NaElement('query')
518    query.add_child_elem(query_details)
519    vserver_info.add_child_elem(query)
520    result = server.invoke_successfully(vserver_info,
521                                        enable_tunneling=False)
522    attribute_list = result.get_child_by_name('attributes-list')
523    vserver_list = attribute_list.get_child_by_name('vserver-info')
524    return vserver_list.get_child_content('vserver-name')
525
526
527def get_cserver(connection, is_rest=False):
528    if not is_rest:
529        return get_cserver_zapi(connection)
530
531    params = {'fields': 'type'}
532    api = "private/cli/vserver"
533    json, error = connection.get(api, params)
534    if json is None or error is not None:
535        # exit if there is an error or no data
536        return None
537    vservers = json.get('records')
538    if vservers is not None:
539        for vserver in vservers:
540            if vserver['type'] == 'admin':     # cluster admin
541                return vserver['vserver']
542        if len(vservers) == 1:                  # assume vserver admin
543            return vservers[0]['vserver']
544
545    return None
546
547
548class OntapRestAPI(object):
549    def __init__(self, module, timeout=60):
550        self.module = module
551        self.username = self.module.params['username']
552        self.password = self.module.params['password']
553        self.hostname = self.module.params['hostname']
554        self.use_rest = self.module.params['use_rest']
555        self.verify = self.module.params['validate_certs']
556        self.timeout = timeout
557        self.url = 'https://' + self.hostname + '/api/'
558        self.errors = list()
559        self.debug_logs = list()
560        self.check_required_library()
561
562    def check_required_library(self):
563        if not HAS_REQUESTS:
564            self.module.fail_json(msg=missing_required_lib('requests'))
565
566    def send_request(self, method, api, params, json=None, return_status_code=False):
567        ''' send http request and process reponse, including error conditions '''
568        url = self.url + api
569        status_code = None
570        content = None
571        json_dict = None
572        json_error = None
573        error_details = None
574
575        def get_json(response):
576            ''' extract json, and error message if present '''
577            try:
578                json = response.json()
579            except ValueError:
580                return None, None
581            error = json.get('error')
582            return json, error
583
584        try:
585            response = requests.request(method, url, verify=self.verify, auth=(self.username, self.password), params=params, timeout=self.timeout, json=json)
586            content = response.content  # for debug purposes
587            status_code = response.status_code
588            # If the response was successful, no Exception will be raised
589            response.raise_for_status()
590            json_dict, json_error = get_json(response)
591        except requests.exceptions.HTTPError as err:
592            __, json_error = get_json(response)
593            if json_error is None:
594                self.log_error(status_code, 'HTTP error: %s' % err)
595                error_details = str(err)
596            # If an error was reported in the json payload, it is handled below
597        except requests.exceptions.ConnectionError as err:
598            self.log_error(status_code, 'Connection error: %s' % err)
599            error_details = str(err)
600        except Exception as err:
601            self.log_error(status_code, 'Other error: %s' % err)
602            error_details = str(err)
603        if json_error is not None:
604            self.log_error(status_code, 'Endpoint error: %d: %s' % (status_code, json_error))
605            error_details = json_error
606        self.log_debug(status_code, content)
607        if return_status_code:
608            return status_code, error_details
609        return json_dict, error_details
610
611    def get(self, api, params):
612        method = 'GET'
613        return self.send_request(method, api, params)
614
615    def post(self, api, data, params=None):
616        method = 'POST'
617        return self.send_request(method, api, params, json=data)
618
619    def patch(self, api, data, params=None):
620        method = 'PATCH'
621        return self.send_request(method, api, params, json=data)
622
623    def delete(self, api, data, params=None):
624        method = 'DELETE'
625        return self.send_request(method, api, params, json=data)
626
627    def _is_rest(self, used_unsupported_rest_properties=None):
628        if self.use_rest == "Always":
629            if used_unsupported_rest_properties:
630                error = "REST API currently does not support '%s'" % \
631                        ', '.join(used_unsupported_rest_properties)
632                return True, error
633            else:
634                return True, None
635        if self.use_rest == 'Never' or used_unsupported_rest_properties:
636            # force ZAPI if requested or if some parameter requires it
637            return False, None
638        method = 'HEAD'
639        api = 'cluster/software'
640        status_code, __ = self.send_request(method, api, params=None, return_status_code=True)
641        if status_code == 200:
642            return True, None
643        return False, None
644
645    def is_rest(self, used_unsupported_rest_properties=None):
646        ''' only return error if there is a reason to '''
647        use_rest, error = self._is_rest(used_unsupported_rest_properties)
648        if used_unsupported_rest_properties is None:
649            return use_rest
650        return use_rest, error
651
652    def log_error(self, status_code, message):
653        self.errors.append(message)
654        self.debug_logs.append((status_code, message))
655
656    def log_debug(self, status_code, content):
657        self.debug_logs.append((status_code, content))
658
659
660class AwsCvsRestAPI(object):
661    def __init__(self, module, timeout=60):
662        self.module = module
663        self.api_key = self.module.params['api_key']
664        self.secret_key = self.module.params['secret_key']
665        self.api_url = self.module.params['api_url']
666        self.verify = self.module.params['validate_certs']
667        self.timeout = timeout
668        self.url = 'https://' + self.api_url + '/v1/'
669        self.check_required_library()
670
671    def check_required_library(self):
672        if not HAS_REQUESTS:
673            self.module.fail_json(msg=missing_required_lib('requests'))
674
675    def send_request(self, method, api, params, json=None):
676        ''' send http request and process reponse, including error conditions '''
677        url = self.url + api
678        status_code = None
679        content = None
680        json_dict = None
681        json_error = None
682        error_details = None
683        headers = {
684            'Content-type': "application/json",
685            'api-key': self.api_key,
686            'secret-key': self.secret_key,
687            'Cache-Control': "no-cache",
688        }
689
690        def get_json(response):
691            ''' extract json, and error message if present '''
692            try:
693                json = response.json()
694
695            except ValueError:
696                return None, None
697            success_code = [200, 201, 202]
698            if response.status_code not in success_code:
699                error = json.get('message')
700            else:
701                error = None
702            return json, error
703        try:
704            response = requests.request(method, url, headers=headers, timeout=self.timeout, json=json)
705            status_code = response.status_code
706            # If the response was successful, no Exception will be raised
707            json_dict, json_error = get_json(response)
708        except requests.exceptions.HTTPError as err:
709            __, json_error = get_json(response)
710            if json_error is None:
711                error_details = str(err)
712        except requests.exceptions.ConnectionError as err:
713            error_details = str(err)
714        except Exception as err:
715            error_details = str(err)
716        if json_error is not None:
717            error_details = json_error
718
719        return json_dict, error_details
720
721    # If an error was reported in the json payload, it is handled below
722    def get(self, api, params=None):
723        method = 'GET'
724        return self.send_request(method, api, params)
725
726    def post(self, api, data, params=None):
727        method = 'POST'
728        return self.send_request(method, api, params, json=data)
729
730    def patch(self, api, data, params=None):
731        method = 'PATCH'
732        return self.send_request(method, api, params, json=data)
733
734    def put(self, api, data, params=None):
735        method = 'PUT'
736        return self.send_request(method, api, params, json=data)
737
738    def delete(self, api, data, params=None):
739        method = 'DELETE'
740        return self.send_request(method, api, params, json=data)
741
742    def get_state(self, jobId):
743        """ Method to get the state of the job """
744        method = 'GET'
745        response, status_code = self.get('Jobs/%s' % jobId)
746        while str(response['state']) not in 'done':
747            response, status_code = self.get('Jobs/%s' % jobId)
748        return 'done'
749