mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 08:42:29 +01:00
382 lines
15 KiB
Python
382 lines
15 KiB
Python
# 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 requests
|
|
import urllib.request, urllib.parse, urllib.error
|
|
import base64
|
|
import hmac
|
|
import hashlib
|
|
import time
|
|
from .cloudstackAPI import queryAsyncJobResult
|
|
from . import jsonHelper
|
|
from marvin.codes import (
|
|
FAILED,
|
|
JOB_FAILED,
|
|
JOB_CANCELLED,
|
|
JOB_SUCCEEDED
|
|
)
|
|
from marvin.cloudstackException import (
|
|
InvalidParameterException,
|
|
GetDetailExceptionInfo)
|
|
|
|
|
|
class CSConnection(object):
|
|
|
|
'''
|
|
@Desc: Connection Class to make API\Command calls to the
|
|
CloudStack Management Server
|
|
Sends the GET\POST requests to CS based upon the
|
|
information provided and retrieves the parsed response.
|
|
'''
|
|
|
|
def __init__(self, mgmtDet, asyncTimeout=3600, logger=None,
|
|
path='client/api'):
|
|
self.apiKey = mgmtDet.apiKey
|
|
self.securityKey = mgmtDet.securityKey
|
|
self.mgtSvr = mgmtDet.mgtSvrIp
|
|
self.port = mgmtDet.port
|
|
self.user = mgmtDet.user
|
|
self.passwd = mgmtDet.passwd
|
|
self.certPath = ()
|
|
if mgmtDet.certCAPath != "NA" and mgmtDet.certPath != "NA":
|
|
self.certPath = (mgmtDet.certCAPath, mgmtDet.certPath)
|
|
self.logger = logger
|
|
self.path = path
|
|
self.retries = 5
|
|
self.__lastError = ''
|
|
self.mgtDetails = mgmtDet
|
|
self.asyncTimeout = asyncTimeout
|
|
self.auth = True
|
|
if self.port == 8096 or \
|
|
(self.apiKey is None and self.securityKey is None):
|
|
self.auth = False
|
|
self.protocol = "https" if mgmtDet.useHttps == "True" else "http"
|
|
self.httpsFlag = True if self.protocol == "https" else False
|
|
self.baseUrl = "%s://%s:%d/%s"\
|
|
% (self.protocol, self.mgtSvr, self.port, self.path)
|
|
|
|
def __copy__(self):
|
|
return CSConnection(self.mgtDetails,
|
|
self.asyncTimeout,
|
|
self.logger,
|
|
self.path)
|
|
|
|
def __poll(self, jobid, response_cmd):
|
|
'''
|
|
@Name : __poll
|
|
@Desc: polls for the completion of a given jobid
|
|
@Input 1. jobid: Monitor the Jobid for CS
|
|
2. response_cmd:response command for request cmd
|
|
@return: FAILED if jobid is cancelled,failed
|
|
Else return async_response
|
|
'''
|
|
try:
|
|
cmd = queryAsyncJobResult.queryAsyncJobResultCmd()
|
|
cmd.jobid = jobid
|
|
timeout = self.asyncTimeout
|
|
start_time = time.time()
|
|
end_time = time.time()
|
|
async_response = FAILED
|
|
self.logger.debug("=== Jobid: %s Started ===" % (str(jobid)))
|
|
while timeout > 0:
|
|
async_response = self.\
|
|
marvinRequest(cmd, response_type=response_cmd)
|
|
if async_response != FAILED:
|
|
job_status = async_response.jobstatus
|
|
if job_status in [JOB_CANCELLED,
|
|
JOB_SUCCEEDED]:
|
|
break
|
|
elif job_status == JOB_FAILED:
|
|
raise Exception("Job failed: %s"\
|
|
% async_response)
|
|
time.sleep(1)
|
|
timeout -= 1
|
|
self.logger.debug("=== JobId:%s is Still Processing, "
|
|
"Will TimeOut in:%s ====" % (str(jobid),
|
|
str(timeout)))
|
|
end_time = time.time()
|
|
tot_time = int(start_time - end_time)
|
|
self.logger.debug(
|
|
"===Jobid:%s ; StartTime:%s ; EndTime:%s ; "
|
|
"TotalTime:%s===" %
|
|
(str(jobid), str(time.ctime(start_time)),
|
|
str(time.ctime(end_time)), str(tot_time)))
|
|
return async_response
|
|
except Exception as e:
|
|
self.__lastError = e
|
|
self.logger.exception("==== __poll: Exception Occurred :%s ====" %
|
|
str(self.__lastError))
|
|
return FAILED
|
|
|
|
def getLastError(self):
|
|
'''
|
|
@Name : getLastError
|
|
@Desc : Returns the last error from marvinRequest
|
|
'''
|
|
return self.__lastError
|
|
|
|
def __sign(self, payload):
|
|
"""
|
|
@Name : __sign
|
|
@Desc:signs a given request URL when the apiKey and
|
|
secretKey are known
|
|
@Input: payload: dictionary of params be signed
|
|
@Output: the signature of the payload
|
|
"""
|
|
params = list(zip(list(payload.keys()), list(payload.values())))
|
|
params.sort(key=lambda k: str.lower(k[0]))
|
|
hash_str = "&".join(
|
|
["=".join(
|
|
[str.lower(r[0]),
|
|
str.lower(
|
|
urllib.parse.quote_plus(str(r[1]), safe="*")
|
|
).replace("+", "%20")]
|
|
) for r in params]
|
|
)
|
|
signature = base64.encodebytes(
|
|
hmac.new(self.securityKey.encode('utf-8'),
|
|
hash_str.encode('utf-8'),
|
|
hashlib.sha1).digest()).strip()
|
|
return signature
|
|
|
|
def __sendPostReqToCS(self, url, payload):
|
|
'''
|
|
@Name : __sendPostReqToCS
|
|
@Desc : Sends the POST Request to CS
|
|
@Input : url: URL to send post req
|
|
payload:Payload information as part of request
|
|
@Output: Returns response from POST output
|
|
else FAILED
|
|
'''
|
|
try:
|
|
response = requests.post(url,
|
|
params=payload,
|
|
cert=self.certPath,
|
|
verify=self.httpsFlag)
|
|
return response
|
|
except Exception as e:
|
|
self.__lastError = e
|
|
self.logger.\
|
|
exception("__sendPostReqToCS : Exception "
|
|
"Occurred: %s" % str(self.__lastError))
|
|
return FAILED
|
|
|
|
def __sendGetReqToCS(self, url, payload):
|
|
'''
|
|
@Name : __sendGetReqToCS
|
|
@Desc : Sends the GET Request to CS
|
|
@Input : url: URL to send post req
|
|
payload:Payload information as part of request
|
|
@Output: Returns response from GET output
|
|
else FAILED
|
|
'''
|
|
try:
|
|
response = requests.get(url,
|
|
params=payload,
|
|
cert=self.certPath,
|
|
verify=self.httpsFlag)
|
|
return response
|
|
except Exception as e:
|
|
self.__lastError = e
|
|
self.logger.exception("__sendGetReqToCS : Exception Occurred: %s" %
|
|
str(self.__lastError))
|
|
return FAILED
|
|
|
|
def __sendCmdToCS(self, command, auth=True, payload={}, method='GET'):
|
|
"""
|
|
@Name : __sendCmdToCS
|
|
@Desc : Makes requests to CS using the Inputs provided
|
|
@Input: command: cloudstack API command name
|
|
eg: deployVirtualMachineCommand
|
|
auth: Authentication (apikey,secretKey) => True
|
|
else False for integration.api.port
|
|
payload: request data composed as a dictionary
|
|
method: GET/POST via HTTP
|
|
@output: FAILED or else response from CS
|
|
"""
|
|
try:
|
|
payload["command"] = command
|
|
payload["response"] = "json"
|
|
|
|
if auth:
|
|
payload["apiKey"] = self.apiKey
|
|
payload["signature"] = self.__sign(payload)
|
|
|
|
# Verify whether protocol is "http" or "https", then send the
|
|
# request
|
|
if self.protocol in ["http", "https"]:
|
|
self.logger.debug("Payload: %s" % str(payload))
|
|
if method == 'POST':
|
|
self.logger.debug("=======Sending POST Cmd : %s======="
|
|
% str(command))
|
|
return self.__sendPostReqToCS(self.baseUrl, payload)
|
|
if method == "GET":
|
|
self.logger.debug("========Sending GET Cmd : %s======="
|
|
% str(command))
|
|
return self.__sendGetReqToCS(self.baseUrl, payload)
|
|
else:
|
|
self.logger.exception("__sendCmdToCS: Invalid Protocol")
|
|
return FAILED
|
|
except Exception as e:
|
|
self.__lastError = e
|
|
self.logger.exception("__sendCmdToCS: Exception:%s" %
|
|
GetDetailExceptionInfo(e))
|
|
return FAILED
|
|
|
|
def __sanitizeCmd(self, cmd):
|
|
"""
|
|
@Name : __sanitizeCmd
|
|
@Desc : Removes None values, Validates all required params are present
|
|
@Input: cmd: Cmd object eg: createPhysicalNetwork
|
|
@Output: Returns command name, asynchronous or not,request payload
|
|
FAILED for failed cases
|
|
"""
|
|
try:
|
|
cmd_name = ''
|
|
payload = {}
|
|
required = []
|
|
isAsync = "false"
|
|
for attribute in dir(cmd):
|
|
if not attribute.startswith('__'):
|
|
if attribute == "isAsync":
|
|
isAsync = getattr(cmd, attribute)
|
|
elif attribute == "required":
|
|
required = getattr(cmd, attribute)
|
|
else:
|
|
payload[attribute] = getattr(cmd, attribute)
|
|
cmd_name = cmd.__class__.__name__.replace("Cmd", "")
|
|
for required_param in required:
|
|
if payload[required_param] is None:
|
|
self.logger.debug("CmdName: %s Parameter : %s is Required"
|
|
% (cmd_name, required_param))
|
|
self.__lastError = InvalidParameterException(
|
|
"Invalid Parameters")
|
|
return FAILED
|
|
for param, value in list(payload.items()):
|
|
if value is None:
|
|
payload.pop(param)
|
|
elif param == 'typeInfo':
|
|
payload.pop(param)
|
|
elif isinstance(value, list):
|
|
if len(value) == 0:
|
|
payload.pop(param)
|
|
else:
|
|
if not isinstance(value[0], dict):
|
|
payload[param] = ",".join(value)
|
|
else:
|
|
payload.pop(param)
|
|
i = 0
|
|
for val in value:
|
|
for k, v in val.items():
|
|
payload["%s[%d].%s" % (param, i, k)] = v
|
|
i += 1
|
|
return cmd_name.strip(), isAsync, payload
|
|
except Exception as e:
|
|
self.__lastError = e
|
|
self.logger.\
|
|
exception("__sanitizeCmd: CmdName : "
|
|
"%s : Exception:%s" % (cmd_name,
|
|
GetDetailExceptionInfo(e)))
|
|
return FAILED
|
|
|
|
def __parseAndGetResponse(self, cmd_response, response_cls, is_async):
|
|
'''
|
|
@Name : __parseAndGetResponse
|
|
@Desc : Verifies the Response(from CS) and returns an
|
|
appropriate json parsed Response
|
|
@Input: cmd_response: Command Response from cs
|
|
response_cls : Mapping class for this Response
|
|
is_async: Whether the cmd is async or not.
|
|
@Output:Response output from CS
|
|
'''
|
|
try:
|
|
try:
|
|
ret = jsonHelper.getResultObj(
|
|
cmd_response.json(),
|
|
response_cls)
|
|
except TypeError:
|
|
ret = jsonHelper.getResultObj(cmd_response.json, response_cls)
|
|
|
|
'''
|
|
If the response is asynchronous, poll and return response
|
|
else return response as it is
|
|
'''
|
|
if is_async == "false":
|
|
self.logger.debug("Response : %s" % str(ret))
|
|
return ret
|
|
else:
|
|
response = self.__poll(ret.jobid, response_cls)
|
|
self.logger.debug("Response : %s" % str(response))
|
|
return response.jobresult if response != FAILED else FAILED
|
|
except Exception as e:
|
|
self.__lastError = e
|
|
self.logger.\
|
|
exception("Exception:%s" % GetDetailExceptionInfo(e))
|
|
return FAILED
|
|
|
|
def marvinRequest(self, cmd, response_type=None, method='GET', data=''):
|
|
"""
|
|
@Name : marvinRequest
|
|
@Desc: Handles Marvin Requests
|
|
@Input cmd: marvin's command from cloudstackAPI
|
|
response_type: response type of the command in cmd
|
|
method: HTTP GET/POST, defaults to GET
|
|
@Output: Response received from CS
|
|
Exception in case of Error\Exception
|
|
"""
|
|
try:
|
|
'''
|
|
1. Verify the Inputs Provided
|
|
'''
|
|
if (cmd is None or cmd == ''):
|
|
self.logger.exception("marvinRequest : Invalid Command Input")
|
|
raise InvalidParameterException("Invalid Parameter")
|
|
|
|
'''
|
|
2. Sanitize the Command
|
|
'''
|
|
sanitize_cmd_out = self.__sanitizeCmd(cmd)
|
|
|
|
if sanitize_cmd_out == FAILED:
|
|
raise self.__lastError
|
|
|
|
cmd_name, is_async, payload = sanitize_cmd_out
|
|
'''
|
|
3. Send Command to CS
|
|
'''
|
|
cmd_response = self.__sendCmdToCS(cmd_name,
|
|
self.auth,
|
|
payload=payload,
|
|
method=method)
|
|
if cmd_response == FAILED:
|
|
raise self.__lastError
|
|
|
|
'''
|
|
4. Check if the Command Response received above is valid or Not.
|
|
If not return Invalid Response
|
|
'''
|
|
ret = self.__parseAndGetResponse(cmd_response,
|
|
response_type,
|
|
is_async)
|
|
if ret == FAILED:
|
|
raise self.__lastError
|
|
return ret
|
|
except Exception as e:
|
|
self.logger.exception("marvinRequest : CmdName: %s Exception: %s" %
|
|
(str(cmd), GetDetailExceptionInfo(e)))
|
|
raise e
|