| # Licensed to the Apache Software Foundation (ASF) under one or more |
| # contributor license agreements. See the NOTICE file distributed with |
| # this work for additional information regarding copyright ownership. |
| # The ASF licenses this file to You under the Apache License, Version 2.0 |
| # (the "License"); you may not use this file except in compliance with |
| # the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import base64 |
| import hashlib |
| import copy |
| import hmac |
| |
| from libcloud.utils.py3 import httplib |
| from libcloud.utils.py3 import urlencode |
| from libcloud.utils.py3 import urlquote |
| from libcloud.utils.py3 import b |
| |
| from libcloud.common.types import ProviderError |
| from libcloud.common.base import ConnectionUserAndKey, PollingConnection |
| from libcloud.common.base import JsonResponse |
| from libcloud.common.types import MalformedResponseError |
| from libcloud.compute.types import InvalidCredsError |
| |
| |
| class CloudStackResponse(JsonResponse): |
| def parse_error(self): |
| if self.status == httplib.UNAUTHORIZED: |
| raise InvalidCredsError('Invalid provider credentials') |
| |
| value = None |
| body = self.parse_body() |
| if hasattr(body, 'values'): |
| values = list(body.values())[0] |
| if 'errortext' in values: |
| value = values['errortext'] |
| if value is None: |
| value = self.body |
| |
| if not value: |
| value = 'WARNING: error message text sent by provider was empty.' |
| |
| error = ProviderError(value=value, http_code=self.status, |
| driver=self.connection.driver) |
| raise error |
| |
| |
| class CloudStackConnection(ConnectionUserAndKey, PollingConnection): |
| responseCls = CloudStackResponse |
| poll_interval = 1 |
| request_method = '_sync_request' |
| timeout = 600 |
| |
| ASYNC_PENDING = 0 |
| ASYNC_SUCCESS = 1 |
| ASYNC_FAILURE = 2 |
| |
| def encode_data(self, data): |
| """ |
| Must of the data is sent as part of query params (eeww), |
| but in newer versions, userdata argument can be sent as a |
| urlencoded data in the request body. |
| """ |
| if data: |
| data = urlencode(data) |
| |
| return data |
| |
| def _make_signature(self, params): |
| signature = [(k.lower(), v) for k, v in list(params.items())] |
| signature.sort(key=lambda x: x[0]) |
| |
| pairs = [] |
| for pair in signature: |
| key = urlquote(str(pair[0]), safe='[]') |
| value = urlquote(str(pair[1]), safe='[]*') |
| item = '%s=%s' % (key, value) |
| pairs .append(item) |
| |
| signature = '&'.join(pairs) |
| |
| signature = signature.lower().replace('+', '%20') |
| signature = hmac.new(b(self.key), msg=b(signature), |
| digestmod=hashlib.sha1) |
| return base64.b64encode(b(signature.digest())) |
| |
| def add_default_params(self, params): |
| params['apiKey'] = self.user_id |
| params['response'] = 'json' |
| |
| return params |
| |
| def pre_connect_hook(self, params, headers): |
| params['signature'] = self._make_signature(params) |
| |
| return params, headers |
| |
| def _async_request(self, command, action=None, params=None, data=None, |
| headers=None, method='GET', context=None): |
| if params: |
| context = copy.deepcopy(params) |
| else: |
| context = {} |
| |
| # Command is specified as part of GET call |
| context['command'] = command |
| result = super(CloudStackConnection, self).async_request( |
| action=action, params=params, data=data, headers=headers, |
| method=method, context=context) |
| return result['jobresult'] |
| |
| def get_request_kwargs(self, action, params=None, data='', headers=None, |
| method='GET', context=None): |
| command = context['command'] |
| request_kwargs = {'command': command, 'action': action, |
| 'params': params, 'data': data, |
| 'headers': headers, 'method': method} |
| return request_kwargs |
| |
| def get_poll_request_kwargs(self, response, context, request_kwargs): |
| job_id = response['jobid'] |
| params = {'jobid': job_id} |
| kwargs = {'command': 'queryAsyncJobResult', 'params': params} |
| return kwargs |
| |
| def has_completed(self, response): |
| status = response.get('jobstatus', self.ASYNC_PENDING) |
| |
| if status == self.ASYNC_FAILURE: |
| msg = response.get('jobresult', {}).get('errortext', status) |
| raise Exception(msg) |
| |
| return status == self.ASYNC_SUCCESS |
| |
| def _sync_request(self, command, action=None, params=None, data=None, |
| headers=None, method='GET'): |
| """ |
| This method handles synchronous calls which are generally fast |
| information retrieval requests and thus return 'quickly'. |
| """ |
| # command is always sent as part of "command" query parameter |
| if params: |
| params = copy.deepcopy(params) |
| else: |
| params = {} |
| |
| params['command'] = command |
| result = self.request(action=self.driver.path, params=params, |
| data=data, headers=headers, method=method) |
| |
| command = command.lower() |
| |
| # Work around for older verions which don't return "response" suffix |
| # in delete ingress rule response command name |
| if (command == 'revokesecuritygroupingress' and |
| 'revokesecuritygroupingressresponse' not in result.object): |
| command = command |
| elif (command == 'restorevirtualmachine' and |
| 'restorevmresponse' in result.object): |
| command = "restorevmresponse" |
| else: |
| command = command + 'response' |
| |
| if command not in result.object: |
| raise MalformedResponseError( |
| "Unknown response format {}".format(command), |
| body=result.body, |
| driver=self.driver) |
| result = result.object[command] |
| return result |
| |
| |
| class CloudStackDriverMixIn(object): |
| host = None |
| path = None |
| |
| connectionCls = CloudStackConnection |
| |
| def __init__(self, key, secret=None, secure=True, host=None, port=None): |
| host = host or self.host |
| super(CloudStackDriverMixIn, self).__init__(key, secret, secure, host, |
| port) |
| |
| def _sync_request(self, command, action=None, params=None, data=None, |
| headers=None, method='GET'): |
| return self.connection._sync_request(command=command, action=action, |
| params=params, data=data, |
| headers=headers, method=method) |
| |
| def _async_request(self, command, action=None, params=None, data=None, |
| headers=None, method='GET', context=None): |
| return self.connection._async_request(command=command, action=action, |
| params=params, data=data, |
| headers=headers, method=method, |
| context=context) |