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# Copyright (c) 2017-2021, NetApp Ansible Team <ng-ansibleteam@netapp.com> 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 29""" 30netapp.py: wrapper around send_requests and other utilities 31""" 32 33from __future__ import (absolute_import, division, print_function) 34__metaclass__ = type 35 36import logging 37import time 38from ansible.module_utils.basic import missing_required_lib 39 40try: 41 from ansible.module_utils.ansible_release import __version__ as ansible_version 42except ImportError: 43 ansible_version = 'unknown' 44 45COLLECTION_VERSION = "21.11.0" 46PROD_ENVIRONMENT = { 47 'CLOUD_MANAGER_HOST': 'cloudmanager.cloud.netapp.com', 48 'AUTH0_DOMAIN': 'netapp-cloud-account.auth0.com', 49 'SA_AUTH_HOST': 'cloudmanager.cloud.netapp.com/auth/oauth/token', 50 'AUTH0_CLIENT': 'Mu0V1ywgYteI6w1MbD15fKfVIUrNXGWC', 51 'AMI_FILTER': 'Setup-As-Service-AMI-Prod*', 52 'AWS_ACCOUNT': '952013314444', 53 'GCP_IMAGE_PROJECT': 'netapp-cloudmanager', 54 'GCP_IMAGE_FAMILY': 'cloudmanager', 55 'CVS_HOST_NAME': 'https://api.services.cloud.netapp.com' 56} 57STAGE_ENVIRONMENT = { 58 'CLOUD_MANAGER_HOST': 'staging.cloudmanager.cloud.netapp.com', 59 'AUTH0_DOMAIN': 'staging-netapp-cloud-account.auth0.com', 60 'SA_AUTH_HOST': 'staging.cloudmanager.cloud.netapp.com/auth/oauth/token', 61 'AUTH0_CLIENT': 'O6AHa7kedZfzHaxN80dnrIcuPBGEUvEv', 62 'AMI_FILTER': 'Setup-As-Service-AMI-*', 63 'AWS_ACCOUNT': '282316784512', 64 'GCP_IMAGE_PROJECT': 'tlv-automation', 65 'GCP_IMAGE_FAMILY': 'occm-automation', 66 'CVS_HOST_NAME': 'https://staging.api.services.cloud.netapp.com' 67} 68 69try: 70 import requests 71 HAS_REQUESTS = True 72except ImportError: 73 HAS_REQUESTS = False 74 75 76POW2_BYTE_MAP = dict( 77 # Here, 1 kb = 1024 78 bytes=1, 79 b=1, 80 kb=1024, 81 mb=1024 ** 2, 82 gb=1024 ** 3, 83 tb=1024 ** 4, 84 pb=1024 ** 5, 85 eb=1024 ** 6, 86 zb=1024 ** 7, 87 yb=1024 ** 8 88) 89 90 91LOG = logging.getLogger(__name__) 92LOG_FILE = '/tmp/cloudmanager_apis.log' 93 94 95def cloudmanager_host_argument_spec(): 96 97 return dict( 98 refresh_token=dict(required=False, type='str', no_log=True), 99 sa_client_id=dict(required=False, type='str', no_log=True), 100 sa_secret_key=dict(required=False, type='str', no_log=True), 101 environment=dict(required=False, type='str', choices=['prod', 'stage'], default='prod'), 102 feature_flags=dict(required=False, type='dict') 103 ) 104 105 106def has_feature(module, feature_name): 107 feature = get_feature(module, feature_name) 108 if isinstance(feature, bool): 109 return feature 110 module.fail_json(msg="Error: expected bool type for feature flag: %s" % feature_name) 111 112 113def get_feature(module, feature_name): 114 ''' if the user has configured the feature, use it 115 otherwise, use our default 116 ''' 117 default_flags = dict( 118 trace_apis=False, # if True, append REST requests/responses to /tmp/cloudmanager_apis.log 119 trace_headers=False, # if True, and if trace_apis is True, include <large> headers in trace 120 show_modified=True, 121 simulator=False, # if True, it is running on simulator 122 ) 123 124 if module.params['feature_flags'] is not None and feature_name in module.params['feature_flags']: 125 return module.params['feature_flags'][feature_name] 126 if feature_name in default_flags: 127 return default_flags[feature_name] 128 module.fail_json(msg="Internal error: unexpected feature flag: %s" % feature_name) 129 130 131class CloudManagerRestAPI(object): 132 """ wrapper around send_request """ 133 def __init__(self, module, timeout=60): 134 self.module = module 135 self.timeout = timeout 136 self.refresh_token = self.module.params['refresh_token'] 137 self.sa_client_id = self.module.params['sa_client_id'] 138 self.sa_secret_key = self.module.params['sa_secret_key'] 139 self.environment = self.module.params['environment'] 140 if self.environment == 'prod': 141 self.environment_data = PROD_ENVIRONMENT 142 elif self.environment == 'stage': 143 self.environment_data = STAGE_ENVIRONMENT 144 self.url = 'https://' 145 self.api_root_path = None 146 self.check_required_library() 147 if has_feature(module, 'trace_apis'): 148 logging.basicConfig(filename=LOG_FILE, level=logging.DEBUG, format='%(asctime)s %(levelname)-8s %(message)s') 149 self.log_headers = has_feature(module, 'trace_headers') # requires trace_apis to do anything 150 self.simulator = has_feature(module, 'simulator') 151 self.token_type, self.token = self.get_token() 152 153 def check_required_library(self): 154 if not HAS_REQUESTS: 155 self.module.fail_json(msg=missing_required_lib('requests')) 156 157 def format_cliend_id(self, client_id): 158 return client_id if client_id.endswith('clients') else client_id + 'clients' 159 160 def send_request(self, method, api, params, json=None, data=None, header=None, authorized=True): 161 ''' send http request and process response, including error conditions ''' 162 url = self.url + api 163 headers = { 164 'Content-type': "application/json", 165 'Referer': "Ansible_NetApp", 166 } 167 if authorized: 168 headers['Authorization'] = self.token_type + " " + self.token 169 if header is not None: 170 headers.update(header) 171 return self._send_request(method, url, params, json, data, headers) 172 173 def _send_request(self, method, url, params, json, data, headers): 174 json_dict = None 175 json_error = None 176 error_details = None 177 on_cloud_request_id = None 178 response = None 179 status_code = None 180 181 def get_json(response): 182 ''' extract json, and error message if present ''' 183 error = None 184 try: 185 json = response.json() 186 except ValueError: 187 return None, None 188 success_code = [200, 201, 202] 189 if response.status_code not in success_code: 190 error = json.get('message') 191 self.log_error(response.status_code, 'HTTP error: %s' % error) 192 return json, error 193 194 self.log_request(method=method, url=url, params=params, json=json, data=data, headers=headers) 195 try: 196 response = requests.request(method, url, headers=headers, timeout=self.timeout, params=params, json=json, data=data) 197 status_code = response.status_code 198 if status_code >= 300 or status_code < 200: 199 self.log_error(status_code, 'HTTP status code error: %s' % response.content) 200 return response.content, str(status_code), on_cloud_request_id 201 # If the response was successful, no Exception will be raised 202 json_dict, json_error = get_json(response) 203 if response.headers.get('OnCloud-Request-Id', '') != '': 204 on_cloud_request_id = response.headers.get('OnCloud-Request-Id') 205 except requests.exceptions.HTTPError as err: 206 self.log_error(status_code, 'HTTP error: %s' % err) 207 error_details = str(err) 208 except requests.exceptions.ConnectionError as err: 209 self.log_error(status_code, 'Connection error: %s' % err) 210 error_details = str(err) 211 except Exception as err: 212 self.log_error(status_code, 'Other error: %s' % err) 213 error_details = str(err) 214 if json_error is not None: 215 self.log_error(status_code, 'Endpoint error: %d: %s' % (status_code, json_error)) 216 error_details = json_error 217 if response: 218 self.log_debug(status_code, response.content) 219 return json_dict, error_details, on_cloud_request_id 220 221 # If an error was reported in the json payload, it is handled below 222 def get(self, api, params=None, header=None): 223 method = 'GET' 224 return self.send_request(method=method, api=api, params=params, json=None, header=header) 225 226 def post(self, api, data, params=None, header=None, gcp_type=False, authorized=True): 227 method = 'POST' 228 if gcp_type: 229 return self.send_request(method=method, api=api, params=params, data=data, header=header) 230 else: 231 return self.send_request(method=method, api=api, params=params, json=data, header=header, authorized=authorized) 232 233 def patch(self, api, data, params=None, header=None): 234 method = 'PATCH' 235 return self.send_request(method=method, api=api, params=params, json=data, header=header) 236 237 def put(self, api, data, params=None, header=None): 238 method = 'PUT' 239 return self.send_request(method=method, api=api, params=params, json=data, header=header) 240 241 def delete(self, api, data, params=None, header=None): 242 method = 'DELETE' 243 return self.send_request(method=method, api=api, params=params, json=data, header=header) 244 245 def get_token(self): 246 if self.sa_client_id is not None and self.sa_client_id != "" and self.sa_secret_key is not None and self.sa_secret_key != "": 247 response, error, ocr_id = self.post(self.environment_data['SA_AUTH_HOST'], 248 data={"grant_type": "client_credentials", "client_secret": self.sa_secret_key, 249 "client_id": self.sa_client_id, "audience": "https://api.cloud.netapp.com"}, 250 authorized=False) 251 elif self.refresh_token is not None and self.refresh_token != "": 252 response, error, ocr_id = self.post(self.environment_data['AUTH0_DOMAIN'] + '/oauth/token', 253 data={"grant_type": "refresh_token", "refresh_token": self.refresh_token, 254 "client_id": self.environment_data['AUTH0_CLIENT'], 255 "audience": "https://api.cloud.netapp.com"}, 256 authorized=False) 257 else: 258 self.module.fail_json(msg='Missing refresh_token or sa_client_id and sa_secret_key') 259 260 if error: 261 self.module.fail_json(msg='Error acquiring token: %s, %s' % (str(error), str(response))) 262 token = response['access_token'] 263 token_type = response['token_type'] 264 265 return token_type, token 266 267 def wait_on_completion(self, api_url, action_name, task, retries, wait_interval): 268 while True: 269 cvo_status, failure_error_message, error = self.check_task_status(api_url) 270 if error is not None: 271 return error 272 if cvo_status == -1: 273 return 'Failed to %s %s, error: %s' % (task, action_name, failure_error_message) 274 elif cvo_status == 1: 275 return None # success 276 # status value 0 means pending 277 if retries == 0: 278 return 'Taking too long for %s to %s or not properly setup' % (action_name, task) 279 time.sleep(wait_interval) 280 retries = retries - 1 281 282 def check_task_status(self, api_url): 283 headers = { 284 'X-Agent-Id': self.format_cliend_id(self.module.params['client_id']) 285 } 286 287 network_retries = 3 288 while True: 289 result, error, dummy = self.get(api_url, None, header=headers) 290 if error is not None: 291 if network_retries <= 0: 292 return 0, '', error 293 time.sleep(1) 294 network_retries -= 1 295 else: 296 response = result 297 break 298 return response['status'], response['error'], None 299 300 def log_error(self, status_code, message): 301 LOG.error("%s: %s", status_code, message) 302 303 def log_debug(self, status_code, content): 304 LOG.debug("%s: %s", status_code, content) 305 306 def log_request(self, method, params, url, json, data, headers): 307 contents = { 308 'method': method, 309 'url': url, 310 'json': json, 311 'data': data 312 } 313 if params: 314 contents['params'] = params 315 if self.log_headers: 316 contents['headers'] = headers 317 self.log_debug('sending', repr(contents)) 318