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