Repository: cloudstack Updated Branches: refs/heads/4.4-forward 5e0671376 -> 8546f76b7
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/404ac549/tools/marvin/marvin/marvinPlugin.py ---------------------------------------------------------------------- diff --git a/tools/marvin/marvin/marvinPlugin.py b/tools/marvin/marvin/marvinPlugin.py index df7d7a3..4200a65 100644 --- a/tools/marvin/marvin/marvinPlugin.py +++ b/tools/marvin/marvin/marvinPlugin.py @@ -14,25 +14,24 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. - import marvin -import sys +from sys import stdout, exit import logging +import time +import os import nose.core from marvin.cloudstackTestCase import cloudstackTestCase from marvin.marvinInit import MarvinInit from nose.plugins.base import Plugin from marvin.codes import (SUCCESS, FAILED, - EXCEPTION, - UNKNOWN_ERROR - ) -import traceback -import time -import os + EXCEPTION) +from marvin.lib.utils import random_gen +from marvin.cloudstackException import GetDetailExceptionInfo class MarvinPlugin(Plugin): + """ Custom plugin for the cloudstackTestCases to be run using nose """ @@ -40,18 +39,35 @@ class MarvinPlugin(Plugin): name = "marvin" def __init__(self): - self.identifier = None - self.testClient = None - self.parsedConfig = None - self.configFile = None - self.loadFlag = None + self.__identifier = None + self.__testClient = None + self.__logFolderPath = None + self.__parsedConfig = None + ''' + Contains Config File + ''' + self.__configFile = None + ''' + Signifies the Zone against which all tests will be Run + ''' + self.__zoneForTests = None + ''' + Signifies the flag whether to deploy the New DC or Not + ''' + self.__deployDcFlag = None self.conf = None - self.debugStream = sys.stdout - self.testRunner = None - self.testResult = SUCCESS - self.startTime = None - self.testName = None - self.tcRunLogger = None + self.__resultStream = stdout + self.__testRunner = None + self.__testResult = SUCCESS + self.__startTime = None + self.__testName = None + self.__tcRunLogger = None + self.__testModName = '' + self.__hypervisorType = None + ''' + The Log Path provided by user where all logs are routed to + ''' + self.__userLogPath = None Plugin.__init__(self) def configure(self, options, conf): @@ -60,20 +76,20 @@ class MarvinPlugin(Plugin): self.enabled (True|False) determines whether marvin's tests will run. By default non-default plugins like marvin will be disabled """ + self.enabled = True if hasattr(options, self.enableOpt): if not getattr(options, self.enableOpt): self.enabled = False return - else: - self.enabled = True - self.configFile = options.config_file - self.loadFlag = options.load - self.logFolderPath = options.log_folder_path + self.__configFile = options.configFile + self.__deployDcFlag = options.deployDc + self.__zoneForTests = options.zone + self.__hypervisorType = options.hypervisor_type + self.__userLogPath = options.logFolder self.conf = conf - ''' - Initializes the marvin with required settings - ''' - self.startMarvin() + if self.startMarvin() == FAILED: + print "\nStarting Marvin Failed, exiting. Please Check" + exit(1) def options(self, parser, env): """ @@ -82,20 +98,31 @@ class MarvinPlugin(Plugin): parser.add_option("--marvin-config", action="store", default=env.get('MARVIN_CONFIG', './datacenter.cfg'), - dest="config_file", - help="Marvin's configuration file where the " + - "datacenter information is specified" + - " [MARVIN_CONFIG]") - parser.add_option("--load", action="store_true", + dest="configFile", + help="Marvin's configuration file is required." + "The config file containing the datacenter and " + "other management server " + "information is specified") + parser.add_option("--deploy", action="store_true", default=False, - dest="load", - help="Only load the deployment configuration given") - parser.add_option("--log-folder-path", - action="store", + dest="deployDc", + help="Deploys the DC with Given Configuration." + "Requires only when DC needs to be deployed") + parser.add_option("--zone", action="store", + default=None, + dest="zone", + help="Runs all tests against this specified zone") + parser.add_option("--hypervisor", action="store", + default=None, + dest="hypervisor_type", + help="Runs all tests against the specified " + "zone and hypervisor Type") + parser.add_option("--log-folder-path", action="store", default=None, - dest="log_folder_path", - help="Path to the folder " - "where log files will be stored") + dest="logFolder", + help="Collects all logs under the user specified" + "folder" + ) Plugin.options(self, parser, env) def wantClass(self, cls): @@ -105,93 +132,124 @@ class MarvinPlugin(Plugin): return True return None - def wantFile(self, filename): + def __checkImport(self, filename): ''' - Only python files will be used as test modules + @Name : __checkImport + @Desc : Verifies to run the available test module for any Import + Errors before running and check + whether if it is importable. + This will check for test modules which has some issues to be + getting imported. + Returns False or True based upon the result. ''' - parts = filename.split(os.path.sep) - base, ext = os.path.splitext(parts[-1]) - if ext == '.py': - return True - else: + try: + if os.path.isfile(filename): + ret = os.path.splitext(filename) + if ret[1] == ".py": + os.system("python " + filename) + return True + return False + except ImportError as e: + print "FileName :%s : Error : %s" % \ + (filename, GetDetailExceptionInfo(e)) return False + def wantFile(self, filename): + ''' + @Desc : Only python files will be used as test modules + ''' + return self.__checkImport(filename) + def loadTestsFromTestCase(self, cls): if cls.__name__ != 'cloudstackTestCase': - self.identifier = cls.__name__ + self.__identifier = cls.__name__ self._injectClients(cls) def beforeTest(self, test): - self.testName = test.__str__().split()[0] - self.testClient.identifier = '-'.join([self.identifier, self.testName]) - self.tcRunLogger.name = test.__str__() - - def prepareTestRunner(self, runner): - return self.testRunner + self.__testModName = test.__str__() + self.__testName = test.__str__().split()[0] + self.__testClient.identifier = '-'.\ + join([self.__identifier, self.__testName]) + if self.__tcRunLogger: + self.__tcRunLogger.name = test.__str__() def startTest(self, test): """ Currently used to record start time for tests Dump Start Msg of TestCase to Log """ - self.tcRunLogger.debug("::::::::::::STARTED : TC: " + - str(self.testName) + " :::::::::::") - self.startTime = time.time() + if self.__tcRunLogger: + self.__tcRunLogger.debug("\n\n::::::::::::STARTED : TC: " + + str(self.__testName) + " :::::::::::") + self.__startTime = time.time() + + def printMsg(self, status, tname, err): + if status in [FAILED, EXCEPTION] and self.__tcRunLogger: + self.__tcRunLogger.\ + fatal("%s: %s: %s" % (status, + tname, + GetDetailExceptionInfo(err))) + write_str = "=== TestName: %s | Status : %s ===\n" % (tname, status) + self.__resultStream.write(write_str) + print write_str - def getErrorInfo(self, err): + def addSuccess(self, test, capt): ''' - Extracts and returns the sanitized error message + Adds the Success Messages to logs ''' - if err is not None: - return str(traceback.format_exc()) - else: - return UNKNOWN_ERROR + self.printMsg(SUCCESS, self.__testName, "Test Case Passed") + self.__testresult = SUCCESS def handleError(self, test, err): ''' Adds Exception throwing test cases and information to log. ''' - err_msg = self.getErrorInfo(err) - self.tcRunLogger.fatal("%s: %s: %s" % - (EXCEPTION, self.testName, err_msg)) - self.testResult = EXCEPTION + self.printMsg(EXCEPTION, self.__testName, GetDetailExceptionInfo(err)) + self.__testResult = EXCEPTION + + def prepareTestRunner(self, runner): + if self.__testRunner: + return self.__testRunner def handleFailure(self, test, err): ''' Adds Failing test cases and information to log. ''' - err_msg = self.getErrorInfo(err) - self.tcRunLogger.fatal("%s: %s: %s" % - (FAILED, self.testName, err_msg)) - self.testResult = FAILED + self.printMsg(FAILED, self.__testName, GetDetailExceptionInfo(err)) + self.__testResult = FAILED def startMarvin(self): ''' - Initializes the Marvin - creates the test Client - creates the runlogger for logging - Parses the config and creates a parsedconfig - Creates a debugstream for tc debug log + @Name : startMarvin + @Desc : Initializes the Marvin + creates the test Client + creates the runlogger for logging + Parses the config and creates a parsedconfig + Creates a debugstream for tc debug log ''' try: - obj_marvininit = MarvinInit(self.configFile, - self.loadFlag, - self.logFolderPath) - if obj_marvininit.init() == SUCCESS: - self.testClient = obj_marvininit.getTestClient() - self.tcRunLogger = obj_marvininit.getLogger() - self.parsedConfig = obj_marvininit.getParsedConfig() - self.debugStream = obj_marvininit.getDebugFile() - self.testRunner = nose.core.TextTestRunner(stream= - self.debugStream, - descriptions=True, - verbosity=2, - config=self.conf) + obj_marvininit = MarvinInit(self.__configFile, + self.__deployDcFlag, + None, + self.__zoneForTests, + self.__hypervisorType, + self.__userLogPath) + if obj_marvininit and obj_marvininit.init() == SUCCESS: + self.__testClient = obj_marvininit.getTestClient() + self.__tcRunLogger = obj_marvininit.getLogger() + self.__parsedConfig = obj_marvininit.getParsedConfig() + self.__resultStream = obj_marvininit.getResultFile() + self.__logFolderPath = obj_marvininit.getLogFolderPath() + self.__testRunner = nose.core.\ + TextTestRunner(stream=self.__resultStream, + descriptions=True, + verbosity=2, + config=self.conf) return SUCCESS - else: - return FAILED - except Exception, e: - print "Exception Occurred under startMarvin: %s" % str(e) + return FAILED + except Exception as e: + print "Exception Occurred under startMarvin: %s" % \ + GetDetailExceptionInfo(e) return FAILED def stopTest(self, test): @@ -199,27 +257,51 @@ class MarvinPlugin(Plugin): Currently used to record end time for tests """ endTime = time.time() - if self.startTime is not None: - totTime = int(endTime - self.startTime) - self.tcRunLogger.debug("TestCaseName: %s; Time Taken: " - "%s Seconds; " - "StartTime: %s; EndTime: %s; Result: %s" - % (self.testName, str(totTime), - str(time.ctime(self.startTime)), - str(time.ctime(endTime)), - self.testResult)) + if self.__startTime: + totTime = int(endTime - self.__startTime) + if self.__tcRunLogger: + self.__tcRunLogger.\ + debug("TestCaseName: %s; " + "Time Taken: %s Seconds; StartTime: %s; " + "EndTime: %s; Result: %s" % + (self.__testName, str(totTime), + str(time.ctime(self.__startTime)), + str(time.ctime(endTime)), + self.__testResult)) def _injectClients(self, test): - setattr(test, "debug", self.tcRunLogger.debug) - setattr(test, "info", self.tcRunLogger.info) - setattr(test, "warn", self.tcRunLogger.warning) - setattr(test, "error", self.tcRunLogger.error) - setattr(test, "testClient", self.testClient) - setattr(test, "config", self.parsedConfig) - if self.testClient.identifier is None: - self.testClient.identifier = self.identifier - setattr(test, "clstestclient", self.testClient) + setattr(test, "debug", self.__tcRunLogger.debug) + setattr(test, "info", self.__tcRunLogger.info) + setattr(test, "warn", self.__tcRunLogger.warning) + setattr(test, "error", self.__tcRunLogger.error) + setattr(test, "testClient", self.__testClient) + setattr(test, "config", self.__parsedConfig) + if self.__testClient.identifier is None: + self.__testClient.identifier = self.__identifier + setattr(test, "clstestclient", self.__testClient) if hasattr(test, "user"): # when the class-level attr applied. all test runs as 'user' - self.testClient.createUserApiClient(test.UserName, test.DomainName, - test.AcctType) + self.__testClient.getUserApiClient(test.UserName, + test.DomainName, + test.AcctType) + + def finalize(self, result): + try: + if not self.__userLogPath: + src = self.__logFolderPath + log_cfg = self.__parsedConfig.logger + tmp = log_cfg.__dict__.get('LogFolderPath') + "/MarvinLogs" + dst = tmp + "//" + random_gen() + mod_name = "test_suite" + if self.__testModName: + mod_name = self.__testModName.split(".") + if len(mod_name) > 2: + mod_name = mod_name[-2] + if mod_name: + dst = tmp + "/" + mod_name + "_" + random_gen() + cmd = "mv " + src + " " + dst + os.system(cmd) + print "===final results are now copied to: %s===" % str(dst) + except Exception, e: + print "=== Exception occurred under finalize :%s ===" % \ + str(GetDetailExceptionInfo(e)) http://git-wip-us.apache.org/repos/asf/cloudstack/blob/404ac549/tools/marvin/marvin/src/__init__.py ---------------------------------------------------------------------- diff --git a/tools/marvin/marvin/src/__init__.py b/tools/marvin/marvin/src/__init__.py new file mode 100644 index 0000000..13a8339 --- /dev/null +++ b/tools/marvin/marvin/src/__init__.py @@ -0,0 +1,16 @@ +# 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. http://git-wip-us.apache.org/repos/asf/cloudstack/blob/404ac549/tools/marvin/marvin/sshClient.py ---------------------------------------------------------------------- diff --git a/tools/marvin/marvin/sshClient.py b/tools/marvin/marvin/sshClient.py index fd8726c..611c8b5 100644 --- a/tools/marvin/marvin/sshClient.py +++ b/tools/marvin/marvin/sshClient.py @@ -16,29 +16,49 @@ # under the License. import paramiko +from paramiko import (BadHostKeyException, + AuthenticationException, + SSHException, + SSHClient, + AutoAddPolicy, + Transport, + SFTPClient) +import socket import time -import cloudstackException +from marvin.cloudstackException import ( + internalError, + GetDetailExceptionInfo +) import contextlib import logging from marvin.codes import ( - SUCCESS, FAIL, INVALID_INPUT, EXCEPTION_OCCURRED - ) + SUCCESS, FAILED, INVALID_INPUT, EXCEPTION_OCCURRED +) from contextlib import closing class SshClient(object): + ''' - Added timeout flag for ssh connect calls.Default to 3.0 seconds + @Desc : SSH Library for Marvin. + Facilitates SSH,SCP services to marvin users + @Input: host: Host to connect + port: port on host to connect + user: Username to be used for connecting + passwd: Password for connection + retries and delay applies for establishing connection + timeout : Applies while executing command ''' - def __init__(self, host, port, user, passwd, retries=20, delay=30, - log_lvl=logging.INFO, keyPairFiles=None, timeout=10.0): + + def __init__(self, host, port, user, passwd, retries=60, delay=10, + log_lvl=logging.DEBUG, keyPairFiles=None, timeout=10.0): self.host = None self.port = 22 self.user = user self.passwd = passwd self.keyPairFiles = keyPairFiles - self.ssh = paramiko.SSHClient() - self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + self.ssh = SSHClient() + self.ssh.set_missing_host_key_policy(AutoAddPolicy()) self.logger = logging.getLogger('sshClient') self.retryCnt = 0 self.delay = 0 @@ -47,8 +67,8 @@ class SshClient(object): ch.setLevel(log_lvl) self.logger.addHandler(ch) - #Check invalid host value and raise exception - #Atleast host is required for connection + # Check invalid host value and raise exception + # Atleast host is required for connection if host is not None and host != '': self.host = host if retries is not None and retries > 0: @@ -57,11 +77,10 @@ class SshClient(object): self.delay = delay if timeout is not None and timeout > 0: self.timeout = timeout - if port is not None or port >= 0: + if port is not None and port >= 0: self.port = port - if self.createConnection() == FAIL: - raise cloudstackException.\ - internalError("Connection Failed") + if self.createConnection() == FAILED: + raise internalError("SSH Connection Failed") def execute(self, command): stdin, stdout, stderr = self.ssh.exec_command(command) @@ -86,15 +105,16 @@ class SshClient(object): @Desc: Creates an ssh connection for retries mentioned,along with sleep mentioned @Output: SUCCESS on successful connection - FAIL If connection through ssh failed + FAILED If connection through ssh failed ''' - ret = FAIL + ret = FAILED + except_msg = '' while self.retryCnt >= 0: try: - self.logger.debug("SSH Connection: Host:%s User:%s\ - Port:%s" % - (self.host, self.user, str(self.port) - )) + self.logger.debug("====Trying SSH Connection: Host:%s User:%s\ + Port:%s RetryCnt:%s===" % + (self.host, self.user, str(self.port), + str(self.retryCnt))) if self.keyPairFiles is None: self.ssh.connect(hostname=self.host, port=self.port, @@ -102,22 +122,39 @@ class SshClient(object): password=self.passwd, timeout=self.timeout) else: + self.ssh.load_host_keys(self.keyPairFiles) self.ssh.connect(hostname=self.host, port=self.port, username=self.user, password=self.passwd, key_filename=self.keyPairFiles, timeout=self.timeout, - look_for_keys=False + look_for_keys=True ) + self.logger.debug("===SSH to Host %s port : %s SUCCESSFUL===" + % (str(self.host), str(self.port))) ret = SUCCESS break - except Exception as se: - self.retryCnt = self.retryCnt - 1 - if self.retryCnt == 0: + except BadHostKeyException as e: + except_msg = GetDetailExceptionInfo(e) + except AuthenticationException as e: + except_msg = GetDetailExceptionInfo(e) + except SSHException as e: + except_msg = GetDetailExceptionInfo(e) + except socket.error as e: + except_msg = GetDetailExceptionInfo(e) + except Exception as e: + except_msg = GetDetailExceptionInfo(e) + finally: + if self.retryCnt == 0 or ret == SUCCESS: break + if except_msg != '': + self.logger.\ + exception("SshClient: Exception under " + "createConnection: %s" % except_msg) + self.retryCnt = self.retryCnt - 1 time.sleep(self.delay) - return ret + return ret def runCommand(self, command): ''' @@ -126,57 +163,54 @@ class SshClient(object): returns the result along with status code @Input: command to execute @Output: 1: status of command executed. - Default to None SUCCESS : If command execution is successful - FAIL : If command execution has failed - EXCEPTION_OCCURRED: Exception occurred while executing - command - INVALID_INPUT : If invalid value for command is passed + FAILED : If command execution has failed 2: stdin,stdout,stderr values of command output ''' - excep_msg = '' - ret = {"status": None, "stdin": None, "stdout": None, "stderr": None} + ret = {"status": FAILED, "stdin": None, "stdout": None, + "stderr": INVALID_INPUT} if command is None or command == '': - ret["status"] = INVALID_INPUT return ret try: status_check = 1 - stdin, stdout, stderr = self.ssh.exec_command(command) - output = stdout.readlines() - errors = stderr.readlines() - inp = stdin.readlines() - ret["stdin"] = inp - ret["stdout"] = output - ret["stderr"] = errors + stdin, stdout, stderr = self.ssh.\ + exec_command(command, timeout=self.timeout) if stdout is not None: status_check = stdout.channel.recv_exit_status() - if status_check == 0: - ret["status"] = SUCCESS - else: - ret["status"] = FAIL + if status_check == 0: + ret["status"] = SUCCESS + ret["stdout"] = stdout.readlines() + if stderr is not None: + ret["stderr"] = stderr.readlines() except Exception as e: - excep_msg = str(e) - ret["status"] = EXCEPTION_OCCURRED + ret["stderr"] = GetDetailExceptionInfo(e) + self.logger.exception("SshClient: Exception under runCommand :%s" % + GetDetailExceptionInfo(e)) finally: - self.logger.debug(" Host: %s Cmd: %s Output:%s Exception: %s" % - (self.host, command, str(ret), excep_msg)) + self.logger.debug(" Host: %s Cmd: %s Output:%s" % + (self.host, command, str(ret))) return ret def scp(self, srcFile, destPath): - transport = paramiko.Transport((self.host, int(self.port))) + transport = Transport((self.host, int(self.port))) transport.connect(username=self.user, password=self.passwd) - sftp = paramiko.SFTPClient.from_transport(transport) + sftp = SFTPClient.from_transport(transport) try: sftp.put(srcFile, destPath) - except IOError, e: + except IOError as e: raise e + def __del__(self): + self.close() + def close(self): if self.ssh is not None: self.ssh.close() + self.ssh = None if __name__ == "__main__": - with contextlib.closing(SshClient("10.223.75.10", 22, "root", - "password")) as ssh: - print ssh.execute("ls -l") + with contextlib.closing(SshClient("127.0.0.1", 22, "root", + "asdf!@34")) as ssh: + ret = ssh.runCommand("ls -l") + print ret http://git-wip-us.apache.org/repos/asf/cloudstack/blob/404ac549/tools/marvin/marvin/tcExecuteEngine.py ---------------------------------------------------------------------- diff --git a/tools/marvin/marvin/tcExecuteEngine.py b/tools/marvin/marvin/tcExecuteEngine.py index f959e7e..e2f4d11 100644 --- a/tools/marvin/marvin/tcExecuteEngine.py +++ b/tools/marvin/marvin/tcExecuteEngine.py @@ -23,6 +23,7 @@ from functools import partial class TestCaseExecuteEngine(object): + def __init__(self, testclient, config, tc_logger=None, debug_stream=None): """ Initialize the testcase execution engine, just the basics here @@ -53,7 +54,7 @@ class TestCaseExecuteEngine(object): if isinstance(test, unittest.BaseTestSuite): self.injectTestCase(test) else: - #inject testclient and logger into each unittest + # inject testclient and logger into each unittest self.tcRunLogger.name = test.__str__() setattr(test, "testClient", self.testclient) setattr(test, "config", self.config) @@ -61,9 +62,10 @@ class TestCaseExecuteEngine(object): setattr(test.__class__, "clstestclient", self.testclient) if hasattr(test, "user"): # attribute when test is entirely executed as user - self.testclient.createUserApiClient(test.UserName, - test.DomainName, - test.AcctType) + self.testclient.\ + getUserApiClient(test.UserName, + test.DomainName, + test.AcctType) def run(self): if self.suite: http://git-wip-us.apache.org/repos/asf/cloudstack/blob/404ac549/tools/marvin/marvin/testSetupSuccess.py ---------------------------------------------------------------------- diff --git a/tools/marvin/marvin/testSetupSuccess.py b/tools/marvin/marvin/testSetupSuccess.py index 1701626..8b000f9 100644 --- a/tools/marvin/marvin/testSetupSuccess.py +++ b/tools/marvin/marvin/testSetupSuccess.py @@ -23,6 +23,7 @@ from time import sleep as delay class TestSetupSuccess(cloudstackTestCase): + """ Test to verify if the cloudstack is ready to launch tests upon 1. Verify that system VMs are up and running in all zones http://git-wip-us.apache.org/repos/asf/cloudstack/blob/404ac549/tools/marvin/setup.py ---------------------------------------------------------------------- diff --git a/tools/marvin/setup.py b/tools/marvin/setup.py index 9ce3951..4c775ad 100644 --- a/tools/marvin/setup.py +++ b/tools/marvin/setup.py @@ -6,9 +6,9 @@ # 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 @@ -30,33 +30,37 @@ except ImportError: VERSION = '0.1.0' import os + + def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read().strip() setup(name="Marvin", - version=VERSION, - description="Marvin - Python client for Apache CloudStack", - author="Edison Su", - author_email="edison...@citrix.com", - maintainer="Prasanna Santhanam", - maintainer_email="t...@apache.org", - long_description="Marvin is the Apache CloudStack python client written around the unittest framework", - platforms=("Any",), - url="https://builds.apache.org/job/cloudstack-marvin/", - packages=["marvin", "marvin.cloudstackAPI", "marvin.integration", - "marvin.integration.lib", "marvin.sandbox", - "marvin.sandbox.advanced", "marvin.sandbox.advancedsg", "marvin.sandbox.basic"], - license="LICENSE.txt", - install_requires=[ - "mysql-connector-python", - "requests", - "paramiko", - "nose", - "ddt >= 0.4.0" - ], - py_modules=['marvin.marvinPlugin'], - zip_safe=False, - entry_points={ - 'nose.plugins': ['marvinPlugin = marvin.marvinPlugin:MarvinPlugin'] - }, -) + version=VERSION, + description="Marvin - Python client for Apache CloudStack", + author="Edison Su", + author_email="edison...@citrix.com", + maintainer="Prasanna Santhanam", + maintainer_email="t...@apache.org", + long_description="Marvin is the Apache CloudStack python " + "client written around the unittest framework", + platforms=("Any",), + url="https://builds.apache.org/job/cloudstack-marvin/", + packages=["marvin", "marvin.cloudstackAPI", + "marvin.lib", "marvin.config", "marvin.sandbox", + "marvin.sandbox.advanced", "marvin.sandbox.advancedsg", + "marvin.sandbox.basic"], + license="LICENSE.txt", + install_requires=[ + "mysql-connector-python", + "requests", + "paramiko", + "nose", + "ddt >= 0.4.0" + ], + py_modules=['marvin.marvinPlugin'], + zip_safe=False, + entry_points={ + 'nose.plugins': ['marvinPlugin = marvin.marvinPlugin:MarvinPlugin'] + }, + )