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