1# -*- coding: utf-8 -*- 2 3# This code is part of Ansible, but is an independent component. 4# This particular file snippet, and this file snippet only, is licensed under the 5# Modified BSD License. Modules you write using this snippet, which is embedded 6# dynamically by Ansible, still belong to the author of the module, and may assign 7# their own license to the complete work. 8# 9# Copyright (c), Entrust Datacard Corporation, 2019 10# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) 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# 1. Redistributions of source code must retain the above copyright notice, 15# this list of conditions and the following disclaimer. 16# 2. Redistributions in binary form must reproduce the above copyright notice, 17# this list of conditions and the following disclaimer in the documentation 18# and/or other materials provided with the distribution. 19# 20# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 23# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 24# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 25# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 28# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 30from __future__ import absolute_import, division, print_function 31 32__metaclass__ = type 33 34import json 35import os 36import re 37import time 38import traceback 39 40from ansible.module_utils._text import to_text, to_native 41from ansible.module_utils.basic import missing_required_lib 42from ansible.module_utils.six.moves.urllib.parse import urlencode 43from ansible.module_utils.six.moves.urllib.error import HTTPError 44from ansible.module_utils.urls import Request 45 46YAML_IMP_ERR = None 47try: 48 import yaml 49except ImportError: 50 YAML_FOUND = False 51 YAML_IMP_ERR = traceback.format_exc() 52else: 53 YAML_FOUND = True 54 55valid_file_format = re.compile(r".*(\.)(yml|yaml|json)$") 56 57 58def ecs_client_argument_spec(): 59 return dict( 60 entrust_api_user=dict(type='str', required=True), 61 entrust_api_key=dict(type='str', required=True, no_log=True), 62 entrust_api_client_cert_path=dict(type='path', required=True), 63 entrust_api_client_cert_key_path=dict(type='path', required=True, no_log=True), 64 entrust_api_specification_path=dict(type='path', default='https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml'), 65 ) 66 67 68class SessionConfigurationException(Exception): 69 """ Raised if we cannot configure a session with the API """ 70 71 pass 72 73 74class RestOperationException(Exception): 75 """ Encapsulate a REST API error """ 76 77 def __init__(self, error): 78 self.status = to_native(error.get("status", None)) 79 self.errors = [to_native(err.get("message")) for err in error.get("errors", {})] 80 self.message = to_native(" ".join(self.errors)) 81 82 83def generate_docstring(operation_spec): 84 """Generate a docstring for an operation defined in operation_spec (swagger)""" 85 # Description of the operation 86 docs = operation_spec.get("description", "No Description") 87 docs += "\n\n" 88 89 # Parameters of the operation 90 parameters = operation_spec.get("parameters", []) 91 if len(parameters) != 0: 92 docs += "\tArguments:\n\n" 93 for parameter in parameters: 94 docs += "{0} ({1}:{2}): {3}\n".format( 95 parameter.get("name"), 96 parameter.get("type", "No Type"), 97 "Required" if parameter.get("required", False) else "Not Required", 98 parameter.get("description"), 99 ) 100 101 return docs 102 103 104def bind(instance, method, operation_spec): 105 def binding_scope_fn(*args, **kwargs): 106 return method(instance, *args, **kwargs) 107 108 # Make sure we don't confuse users; add the proper name and documentation to the function. 109 # Users can use !help(<function>) to get help on the function from interactive python or pdb 110 operation_name = operation_spec.get("operationId").split("Using")[0] 111 binding_scope_fn.__name__ = str(operation_name) 112 binding_scope_fn.__doc__ = generate_docstring(operation_spec) 113 114 return binding_scope_fn 115 116 117class RestOperation(object): 118 def __init__(self, session, uri, method, parameters=None): 119 self.session = session 120 self.method = method 121 if parameters is None: 122 self.parameters = {} 123 else: 124 self.parameters = parameters 125 self.url = "{scheme}://{host}{base_path}{uri}".format(scheme="https", host=session._spec.get("host"), base_path=session._spec.get("basePath"), uri=uri) 126 127 def restmethod(self, *args, **kwargs): 128 """Do the hard work of making the request here""" 129 130 # gather named path parameters and do substitution on the URL 131 if self.parameters: 132 path_parameters = {} 133 body_parameters = {} 134 query_parameters = {} 135 for x in self.parameters: 136 expected_location = x.get("in") 137 key_name = x.get("name", None) 138 key_value = kwargs.get(key_name, None) 139 if expected_location == "path" and key_name and key_value: 140 path_parameters.update({key_name: key_value}) 141 elif expected_location == "body" and key_name and key_value: 142 body_parameters.update({key_name: key_value}) 143 elif expected_location == "query" and key_name and key_value: 144 query_parameters.update({key_name: key_value}) 145 146 if len(body_parameters.keys()) >= 1: 147 body_parameters = body_parameters.get(list(body_parameters.keys())[0]) 148 else: 149 body_parameters = None 150 else: 151 path_parameters = {} 152 query_parameters = {} 153 body_parameters = None 154 155 # This will fail if we have not set path parameters with a KeyError 156 url = self.url.format(**path_parameters) 157 if query_parameters: 158 # modify the URL to add path parameters 159 url = url + "?" + urlencode(query_parameters) 160 161 try: 162 if body_parameters: 163 body_parameters_json = json.dumps(body_parameters) 164 response = self.session.request.open(method=self.method, url=url, data=body_parameters_json) 165 else: 166 response = self.session.request.open(method=self.method, url=url) 167 request_error = False 168 except HTTPError as e: 169 # An HTTPError has the same methods available as a valid response from request.open 170 response = e 171 request_error = True 172 173 # Return the result if JSON and success ({} for empty responses) 174 # Raise an exception if there was a failure. 175 try: 176 result_code = response.getcode() 177 result = json.loads(response.read()) 178 except ValueError: 179 result = {} 180 181 if result or result == {}: 182 if result_code and result_code < 400: 183 return result 184 else: 185 raise RestOperationException(result) 186 187 # Raise a generic RestOperationException if this fails 188 raise RestOperationException({"status": result_code, "errors": [{"message": "REST Operation Failed"}]}) 189 190 191class Resource(object): 192 """ Implement basic CRUD operations against a path. """ 193 194 def __init__(self, session): 195 self.session = session 196 self.parameters = {} 197 198 for url in session._spec.get("paths").keys(): 199 methods = session._spec.get("paths").get(url) 200 for method in methods.keys(): 201 operation_spec = methods.get(method) 202 operation_name = operation_spec.get("operationId", None) 203 parameters = operation_spec.get("parameters") 204 205 if not operation_name: 206 if method.lower() == "post": 207 operation_name = "Create" 208 elif method.lower() == "get": 209 operation_name = "Get" 210 elif method.lower() == "put": 211 operation_name = "Update" 212 elif method.lower() == "delete": 213 operation_name = "Delete" 214 elif method.lower() == "patch": 215 operation_name = "Patch" 216 else: 217 raise SessionConfigurationException(to_native("Invalid REST method type {0}".format(method))) 218 219 # Get the non-parameter parts of the URL and append to the operation name 220 # e.g /application/version -> GetApplicationVersion 221 # e.g. /application/{id} -> GetApplication 222 # This may lead to duplicates, which we must prevent. 223 operation_name += re.sub(r"{(.*)}", "", url).replace("/", " ").title().replace(" ", "") 224 operation_spec["operationId"] = operation_name 225 226 op = RestOperation(session, url, method, parameters) 227 setattr(self, operation_name, bind(self, op.restmethod, operation_spec)) 228 229 230# Session to encapsulate the connection parameters of the module_utils Request object, the api spec, etc 231class ECSSession(object): 232 def __init__(self, name, **kwargs): 233 """ 234 Initialize our session 235 """ 236 237 self._set_config(name, **kwargs) 238 239 def client(self): 240 resource = Resource(self) 241 return resource 242 243 def _set_config(self, name, **kwargs): 244 headers = { 245 "Content-Type": "application/json", 246 "Connection": "keep-alive", 247 } 248 self.request = Request(headers=headers, timeout=60) 249 250 configurators = [self._read_config_vars] 251 for configurator in configurators: 252 self._config = configurator(name, **kwargs) 253 if self._config: 254 break 255 if self._config is None: 256 raise SessionConfigurationException(to_native("No Configuration Found.")) 257 258 # set up auth if passed 259 entrust_api_user = self.get_config("entrust_api_user") 260 entrust_api_key = self.get_config("entrust_api_key") 261 if entrust_api_user and entrust_api_key: 262 self.request.url_username = entrust_api_user 263 self.request.url_password = entrust_api_key 264 else: 265 raise SessionConfigurationException(to_native("User and key must be provided.")) 266 267 # set up client certificate if passed (support all-in one or cert + key) 268 entrust_api_cert = self.get_config("entrust_api_cert") 269 entrust_api_cert_key = self.get_config("entrust_api_cert_key") 270 if entrust_api_cert: 271 self.request.client_cert = entrust_api_cert 272 if entrust_api_cert_key: 273 self.request.client_key = entrust_api_cert_key 274 else: 275 raise SessionConfigurationException(to_native("Client certificate for authentication to the API must be provided.")) 276 277 # set up the spec 278 entrust_api_specification_path = self.get_config("entrust_api_specification_path") 279 280 if not entrust_api_specification_path.startswith("http") and not os.path.isfile(entrust_api_specification_path): 281 raise SessionConfigurationException(to_native("OpenAPI specification was not found at location {0}.".format(entrust_api_specification_path))) 282 if not valid_file_format.match(entrust_api_specification_path): 283 raise SessionConfigurationException(to_native("OpenAPI specification filename must end in .json, .yml or .yaml")) 284 285 self.verify = True 286 287 if entrust_api_specification_path.startswith("http"): 288 try: 289 http_response = Request().open(method="GET", url=entrust_api_specification_path) 290 http_response_contents = http_response.read() 291 if entrust_api_specification_path.endswith(".json"): 292 self._spec = json.load(http_response_contents) 293 elif entrust_api_specification_path.endswith(".yml") or entrust_api_specification_path.endswith(".yaml"): 294 self._spec = yaml.safe_load(http_response_contents) 295 except HTTPError as e: 296 raise SessionConfigurationException(to_native("Error downloading specification from address '{0}', received error code '{1}'".format( 297 entrust_api_specification_path, e.getcode()))) 298 else: 299 with open(entrust_api_specification_path) as f: 300 if ".json" in entrust_api_specification_path: 301 self._spec = json.load(f) 302 elif ".yml" in entrust_api_specification_path or ".yaml" in entrust_api_specification_path: 303 self._spec = yaml.safe_load(f) 304 305 def get_config(self, item): 306 return self._config.get(item, None) 307 308 def _read_config_vars(self, name, **kwargs): 309 """ Read configuration from variables passed to the module. """ 310 config = {} 311 312 entrust_api_specification_path = kwargs.get("entrust_api_specification_path") 313 if not entrust_api_specification_path or (not entrust_api_specification_path.startswith("http") and not os.path.isfile(entrust_api_specification_path)): 314 raise SessionConfigurationException( 315 to_native( 316 "Parameter provided for entrust_api_specification_path of value '{0}' was not a valid file path or HTTPS address.".format( 317 entrust_api_specification_path 318 ) 319 ) 320 ) 321 322 for required_file in ["entrust_api_cert", "entrust_api_cert_key"]: 323 file_path = kwargs.get(required_file) 324 if not file_path or not os.path.isfile(file_path): 325 raise SessionConfigurationException( 326 to_native("Parameter provided for {0} of value '{1}' was not a valid file path.".format(required_file, file_path)) 327 ) 328 329 for required_var in ["entrust_api_user", "entrust_api_key"]: 330 if not kwargs.get(required_var): 331 raise SessionConfigurationException(to_native("Parameter provided for {0} was missing.".format(required_var))) 332 333 config["entrust_api_cert"] = kwargs.get("entrust_api_cert") 334 config["entrust_api_cert_key"] = kwargs.get("entrust_api_cert_key") 335 config["entrust_api_specification_path"] = kwargs.get("entrust_api_specification_path") 336 config["entrust_api_user"] = kwargs.get("entrust_api_user") 337 config["entrust_api_key"] = kwargs.get("entrust_api_key") 338 339 return config 340 341 342def ECSClient(entrust_api_user=None, entrust_api_key=None, entrust_api_cert=None, entrust_api_cert_key=None, entrust_api_specification_path=None): 343 """Create an ECS client""" 344 345 if not YAML_FOUND: 346 raise SessionConfigurationException(missing_required_lib("PyYAML"), exception=YAML_IMP_ERR) 347 348 if entrust_api_specification_path is None: 349 entrust_api_specification_path = "https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml" 350 351 # Not functionally necessary with current uses of this module_util, but better to be explicit for future use cases 352 entrust_api_user = to_text(entrust_api_user) 353 entrust_api_key = to_text(entrust_api_key) 354 entrust_api_cert_key = to_text(entrust_api_cert_key) 355 entrust_api_specification_path = to_text(entrust_api_specification_path) 356 357 return ECSSession( 358 "ecs", 359 entrust_api_user=entrust_api_user, 360 entrust_api_key=entrust_api_key, 361 entrust_api_cert=entrust_api_cert, 362 entrust_api_cert_key=entrust_api_cert_key, 363 entrust_api_specification_path=entrust_api_specification_path, 364 ).client() 365