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, Sumit Kumar <sumit4@netapp.com> 8# Copyright (c) 2017, Michael Price <michael.price@netapp.com> 9# All rights reserved. 10# 11# Redistribution and use in source and binary forms, with or without modification, 12# are permitted provided that the following conditions are met: 13# 14# * Redistributions of source code must retain the above copyright 15# notice, this list of conditions and the following disclaimer. 16# * 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 30''' 31common routines for um_info 32''' 33 34from __future__ import (absolute_import, division, print_function) 35__metaclass__ = type 36 37import logging 38from ansible.module_utils.basic import missing_required_lib 39from ansible.module_utils._text import to_native 40 41try: 42 from ansible.module_utils.ansible_release import __version__ as ansible_version 43except ImportError: 44 ansible_version = 'unknown' 45 46COLLECTION_VERSION = "21.7.0" 47 48try: 49 import requests 50 HAS_REQUESTS = True 51except ImportError: 52 HAS_REQUESTS = False 53 54ERROR_MSG = dict( 55 no_cserver='This module is expected to run as cluster admin' 56) 57 58LOG = logging.getLogger(__name__) 59LOG_FILE = '/tmp/um_apis.log' 60 61 62def na_um_host_argument_spec(): 63 64 return dict( 65 hostname=dict(required=True, type='str'), 66 username=dict(required=True, type='str'), 67 password=dict(required=True, type='str', no_log=True), 68 validate_certs=dict(required=False, type='bool', default=True), 69 http_port=dict(required=False, type='int'), 70 feature_flags=dict(required=False, type='dict', default=dict()), 71 max_records=dict(required=False, type='int') 72 ) 73 74 75def has_feature(module, feature_name): 76 feature = get_feature(module, feature_name) 77 if isinstance(feature, bool): 78 return feature 79 module.fail_json(msg="Error: expected bool type for feature flag: %s" % feature_name) 80 81 82def get_feature(module, feature_name): 83 ''' if the user has configured the feature, use it 84 otherwise, use our default 85 ''' 86 default_flags = dict( 87 strict_json_check=True, # if true, fail if response.content in not empty and is not valid json 88 trace_apis=False, # if true, append REST requests/responses to LOG_FILE 89 90 ) 91 92 if module.params['feature_flags'] is not None and feature_name in module.params['feature_flags']: 93 return module.params['feature_flags'][feature_name] 94 if feature_name in default_flags: 95 return default_flags[feature_name] 96 module.fail_json(msg="Internal error: unexpected feature flag: %s" % feature_name) 97 98 99class UMRestAPI(object): 100 ''' send REST request and process response ''' 101 def __init__(self, module, timeout=60): 102 self.module = module 103 self.username = self.module.params['username'] 104 self.password = self.module.params['password'] 105 self.hostname = self.module.params['hostname'] 106 self.verify = self.module.params['validate_certs'] 107 self.max_records = self.module.params['max_records'] 108 self.timeout = timeout 109 if self.module.params.get('http_port') is not None: 110 self.url = 'https://%s:%d' % (self.hostname, self.module.params['http_port']) 111 else: 112 self.url = 'https://%s' % self.hostname 113 self.errors = list() 114 self.debug_logs = list() 115 self.check_required_library() 116 if has_feature(module, 'trace_apis'): 117 logging.basicConfig(filename=LOG_FILE, level=logging.DEBUG, format='%(asctime)s %(levelname)-8s %(message)s') 118 119 def check_required_library(self): 120 if not HAS_REQUESTS: 121 self.module.fail_json(msg=missing_required_lib('requests')) 122 123 def get_records(self, message, api): 124 records = list() 125 try: 126 if message['total_records'] > 0: 127 records = message['records'] 128 if message['total_records'] != len(records): 129 self.module.warn('Mismatch between received: %d and expected: %d records.' % (len(records), message['total_records'])) 130 except KeyError as exc: 131 self.module.fail_json(msg='Error: unexpected response from %s: %s - expecting key: %s' 132 % (api, message, to_native(exc))) 133 return records 134 135 def send_request(self, method, api, params, json=None, accept=None): 136 ''' send http request and process response, including error conditions ''' 137 url = self.url + api 138 status_code = None 139 content = None 140 json_dict = None 141 json_error = None 142 error_details = None 143 headers = None 144 if accept is not None: 145 headers = dict() 146 # accept is used to turn on/off HAL linking 147 if accept is not None: 148 headers['accept'] = accept 149 150 def check_contents(response): 151 '''json() may fail on an empty value, but it's OK if no response is expected. 152 To avoid false positives, only report an issue when we expect to read a value. 153 The first get will see it. 154 ''' 155 if method == 'GET' and has_feature(self.module, 'strict_json_check'): 156 contents = response.content 157 if len(contents) > 0: 158 raise ValueError("Expecting json, got: %s" % contents) 159 160 def get_json(response): 161 ''' extract json, and error message if present ''' 162 try: 163 json = response.json() 164 except ValueError: 165 check_contents(response) 166 return None, None 167 error = json.get('error') 168 return json, error 169 170 self.log_debug('sending', repr(dict(method=method, url=url, verify=self.verify, params=params, 171 timeout=self.timeout, json=json, headers=headers))) 172 try: 173 response = requests.request(method, url, verify=self.verify, auth=(self.username, self.password), 174 params=params, timeout=self.timeout, json=json, headers=headers) 175 content = response.content # for debug purposes 176 status_code = response.status_code 177 # If the response was successful, no Exception will be raised 178 response.raise_for_status() 179 json_dict, json_error = get_json(response) 180 except requests.exceptions.HTTPError as err: 181 __, json_error = get_json(response) 182 if json_error is None: 183 self.log_error(status_code, 'HTTP error: %s' % err) 184 error_details = str(err) 185 # If an error was reported in the json payload, it is handled below 186 except requests.exceptions.ConnectionError as err: 187 self.log_error(status_code, 'Connection error: %s' % err) 188 error_details = str(err) 189 except Exception as err: 190 self.log_error(status_code, 'Other error: %s' % err) 191 error_details = str(err) 192 if json_error is not None: 193 self.log_error(status_code, 'Endpoint error: %d: %s' % (status_code, json_error)) 194 error_details = json_error 195 self.log_debug(status_code, content) 196 return json_dict, error_details 197 198 def get(self, api, params): 199 200 def get_next_api(message): 201 '''make sure _links is present, and href is present if next is present 202 return api if next is present, None otherwise 203 return error if _links or href are missing 204 ''' 205 api, error = None, None 206 if message is None or '_links' not in message: 207 error = 'Expecting _links key in %s' % message 208 elif 'next' in message['_links']: 209 if 'href' in message['_links']['next']: 210 api = message['_links']['next']['href'] 211 else: 212 error = 'Expecting href key in %s' % message['_links']['next'] 213 return api, error 214 215 method = 'GET' 216 records = list() 217 if self.max_records is not None: 218 if params and 'max_records' not in params: 219 params['max_records'] = self.max_records 220 else: 221 params = dict(max_records=self.max_records) 222 api = '/api/%s' % api 223 224 while api: 225 message, error = self.send_request(method, api, params) 226 if error: 227 return message, error 228 api, error = get_next_api(message) 229 if error: 230 return message, error 231 if 'records' in message: 232 records.extend(message['records']) 233 params = None # already included in the next link 234 235 if records: 236 message['records'] = records 237 return message, error 238 239 def log_error(self, status_code, message): 240 LOG.error("%s: %s", status_code, message) 241 self.errors.append(message) 242 self.debug_logs.append((status_code, message)) 243 244 def log_debug(self, status_code, content): 245 LOG.debug("%s: %s", status_code, content) 246 self.debug_logs.append((status_code, content)) 247