| # 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 copy |
| import hmac |
| import base64 |
| import hashlib |
| |
| from libcloud.utils.py3 import b, httplib, urlquote, urlencode |
| from libcloud.common.base import JsonResponse, PollingConnection, ConnectionUserAndKey |
| from libcloud.common.types import ProviderError, 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 = "{}={}".format(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().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 |
| # pylint: disable=maybe-no-member |
| 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: |
| host = None |
| path = None |
| |
| connectionCls = CloudStackConnection |
| |
| def __init__(self, key, secret=None, secure=True, host=None, port=None): |
| host = host or self.host |
| super().__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( # pylint: disable=maybe-no-member |
| 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( # pylint: disable=maybe-no-member |
| command=command, |
| action=action, |
| params=params, |
| data=data, |
| headers=headers, |
| method=method, |
| context=context, |
| ) |