blob: 2c745c29f6ad91e95cf6ed597de02a31ec362dcc [file] [log] [blame]
# 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,
)