diff --git a/.gitignore b/.gitignore index 1707d7e..c992f7a 100644 --- a/.gitignore +++ b/.gitignore @@ -60,5 +60,4 @@ kb*.log *.html *.qcow2 scale/dib/kloudbuster.d/ - .vagrant/ diff --git a/kloudbuster/base_compute.py b/kloudbuster/base_compute.py index 03521b7..2c8e695 100644 --- a/kloudbuster/base_compute.py +++ b/kloudbuster/base_compute.py @@ -87,9 +87,7 @@ class BaseCompute(object): return instance if instance.status == 'ERROR': LOG.error('Instance creation error:' + instance.fault['message']) - break - # print "[%s] VM status=%s, retrying %s of %s..." \ - # % (vmname, instance.status, (retry_attempt + 1), retry_count) + return None time.sleep(2) def get_server_list(self): diff --git a/kloudbuster/kb_config.py b/kloudbuster/kb_config.py index 19073fc..c137bd6 100644 --- a/kloudbuster/kb_config.py +++ b/kloudbuster/kb_config.py @@ -20,6 +20,7 @@ import log as logging from oslo_config import cfg import credentials +import kb_vm_agent CONF = cfg.CONF LOG = logging.getLogger(__name__) @@ -109,6 +110,13 @@ class KBConfig(object): self.client_cfg['vms_per_network'] =\ self.get_total_vm_count(self.server_cfg) + 1 + # Use the default image name for Glance + # defaults to something like "kloudbuster_v3" + if not self.server_cfg['image_name']: + self.server_cfg['image_name'] = kb_vm_agent.get_image_name() + if not self.client_cfg['image_name']: + self.client_cfg['image_name'] = kb_vm_agent.get_image_name() + def init_with_cli(self): self.get_credentials() self.get_configs() diff --git a/kloudbuster/kb_gen_chart.py b/kloudbuster/kb_gen_chart.py index 57fa6bc..df63fa4 100755 --- a/kloudbuster/kb_gen_chart.py +++ b/kloudbuster/kb_gen_chart.py @@ -183,7 +183,7 @@ def main(): parser = argparse.ArgumentParser(description='KloudBuster Chart Generator V' + __version__) parser.add_argument('-c', '--chart', dest='chart', - action='store', + action='store', required=True, help='create and save chart in html file', metavar='') diff --git a/kloudbuster/kb_runner.py b/kloudbuster/kb_runner.py index c3a6471..83df227 100644 --- a/kloudbuster/kb_runner.py +++ b/kloudbuster/kb_runner.py @@ -14,7 +14,6 @@ from collections import deque from distutils.version import LooseVersion -from sets import Set import threading import time @@ -22,7 +21,7 @@ import log as logging import redis # A set of warned VM version mismatches -vm_version_mismatches = Set() +vm_version_mismatches = set() LOG = logging.getLogger(__name__) @@ -55,6 +54,7 @@ class KBRunner(object): self.tool_result = {} self.expected_agent_version = expected_agent_version self.agent_version = None + self.report = {'seq': 0, 'report': None} # Redis self.redis_obj = None @@ -180,7 +180,10 @@ class KBRunner(object): LOG.info(log_msg) if sample_count != 0: - print http_tool.consolidate_samples(samples, len(self.client_dict)) + report = http_tool.consolidate_samples(samples, len(self.client_dict)) + self.report['seq'] = self.report['seq'] + 1 + self.report['report'] = report + LOG.info('Periodical report: %s.' % str(self.report)) samples = [] retry = retry + 1 diff --git a/kloudbuster/kb_server/config.py b/kloudbuster/kb_server/config.py index 2ae14a9..3012ef2 100644 --- a/kloudbuster/kb_server/config.py +++ b/kloudbuster/kb_server/config.py @@ -25,10 +25,10 @@ app = { 'static_root': '%(confdir)s/public', 'template_path': '%(confdir)s/kb_server/templates', 'debug': True, - 'errors': { - 404: '/error/404', - '__force_dict__': True - } + # 'errors': { + # 404: '/error/404', + # '__force_dict__': True + # } } logging = { diff --git a/kloudbuster/kb_server/kb_server/controllers/api_cfg.py b/kloudbuster/kb_server/kb_server/controllers/api_cfg.py index 880ddd0..6f14453 100644 --- a/kloudbuster/kb_server/kb_server/controllers/api_cfg.py +++ b/kloudbuster/kb_server/kb_server/controllers/api_cfg.py @@ -12,36 +12,57 @@ # License for the specific language governing permissions and limitations # under the License. +import hashlib +import json import os import sys -import threading +import traceback kb_main_path = os.path.split(os.path.abspath(__file__))[0] + "/../../.." sys.path.append(kb_main_path) from credentials import Credentials +from kb_session import KBSession +from kb_session import KBSessionManager +import log as logging from configure import Configuration from kb_config import KBConfig from pecan import expose from pecan import response -lock = threading.Lock() -kb_config = KBConfig() - class ConfigController(object): @expose(generic=True) - def running_config(self): + def default_config(self): + kb_config = KBConfig() + # @TODO(Bug in Python???) + # return json.dumps(dict(kb_config.config_scale)) return str(kb_config.config_scale) + @expose(generic=True) + def running_config(self, *args): + if len(args): + session_id = args[0] + else: + response.status = 400 + response.text = u"Please specify the session_id." + return response.text + if KBSessionManager.has(session_id): + kb_config_obj = KBSessionManager.get(session_id).kb_config + config_scale = kb_config_obj.config_scale + config_scale['server'] = kb_config_obj.server_cfg + config_scale['client'] = kb_config_obj.client_cfg + config_scale = dict(config_scale) + # @TODO(Bug in Python???) + # return json.dumps(config_scale) + return str(config_scale) + else: + response.status = 404 + response.text = u"Session ID is not found or invalid." + return response.text + @running_config.when(method='POST') def running_config_POST(self, arg): - if not lock.acquire(False): - response.status = 403 - response.text = u"An instance of KloudBuster is running, you cannot change"\ - "the config until the run is finished!" - return response.text - try: # Expectation: # { @@ -51,7 +72,7 @@ class ConfigController(object): # 'topo_cfg': {} # 'tenants_cfg': {} # } - user_config = eval(arg) + user_config = json.loads(arg) # Parsing credentials from application input cred_config = user_config['credentials'] @@ -65,6 +86,13 @@ class ConfigController(object): # Use the same openrc file for both cases cred_testing = cred_tested + session_id = hashlib.md5(str(cred_config)).hexdigest() + kb_config = KBConfig() + if KBSessionManager.has(session_id): + response.status = 403 + response.text = u"Session is already existed." + return response.text + # Parsing server and client configs from application input # Save the public key into a temporary file if 'public_key' in user_config['kb_cfg']: @@ -74,6 +102,9 @@ class ConfigController(object): f.close() kb_config.config_scale['public_key_file'] = pubkey_filename + if 'prompt_before_run' in user_config['kb_cfg']: + kb_config.config_scale['prompt_before_run'] = False + if user_config['kb_cfg']: alt_config = Configuration.from_string(user_config['kb_cfg']).configure() kb_config.config_scale = kb_config.config_scale.merge(alt_config) @@ -89,16 +120,58 @@ class ConfigController(object): tenants_list = Configuration.from_string(user_config['tenants_list']).configure() else: tenants_list = None - except Exception as e: - response.status = 403 - response.text = u"Error while parsing configurations: %s" % (e.message) - lock.release() + except Exception: + response.status = 400 + response.text = u"Error while parsing configurations: \n%s" % (traceback.format_exc) return response.text + logging.setup("kloudbuster", logfile="/tmp/kb_log_%s" % session_id) kb_config.init_with_rest_api(cred_tested=cred_tested, cred_testing=cred_testing, topo_cfg=topo_cfg, tenants_list=tenants_list) - lock.release() - return "OK!" + kb_session = KBSession() + kb_session.kb_config = kb_config + KBSessionManager.add(session_id, kb_session) + + return str(session_id) + + @running_config.when(method='PUT') + def running_config_PUT(self, *args): + # @TODO(Not completed! ENOTSUP) + if len(args): + session_id = args[0] + else: + response.status = 400 + response.text = u"Please specify the session_id." + return response.text + if KBSessionManager.has(session_id): + # kb_session = KBSessionManager.get(session_id) + # + # + # + return "OK!" + else: + response.status = 404 + response.text = u"Session ID is not found or invalid." + return response.text + + @running_config.when(method='DELETE') + def running_config_DELETE(self, *args): + if len(args): + session_id = args[0] + else: + response.status = 400 + response.text = u"Please specify the session_id." + return response.text + if KBSessionManager.has(session_id): + kb_session = KBSessionManager.get(session_id) + if kb_session.kloudbuster: + kb_session.kloudbuster.dispose() + KBSessionManager.delete(session_id) + return "OK!" + else: + response.status = 404 + response.text = u"Session ID is not found or invalid." + return response.text diff --git a/kloudbuster/kb_server/kb_server/controllers/api_kb.py b/kloudbuster/kb_server/kb_server/controllers/api_kb.py index 77ae427..e1e0186 100644 --- a/kloudbuster/kb_server/kb_server/controllers/api_kb.py +++ b/kloudbuster/kb_server/kb_server/controllers/api_kb.py @@ -12,14 +12,15 @@ # License for the specific language governing permissions and limitations # under the License. +import json import os import sys -import traceback +import threading kb_main_path = os.path.split(os.path.abspath(__file__))[0] + "/../../.." sys.path.append(kb_main_path) -from api_cfg import kb_config as kb_config -from api_cfg import lock as kb_config_lock +from kb_session import KBSessionManager +from kloudbuster import __version__ as kb_version from kloudbuster import KloudBuster from pecan import expose @@ -28,37 +29,109 @@ from pecan import response class KBController(object): def __init__(self): - self.kb_status = 'READY' - - @expose(generic=True) - def status(self): - return self.kb_status - - @expose(generic=True) - def run(self): - if (not kb_config.cred_tested) or (not kb_config.cred_testing): - response.status = 403 - response.text = u"Credentials to the cloud are missing."\ - "(Forgot to provide the config?)" - return response.text - if not kb_config_lock.acquire(False): - response.status = 403 - response.text = u"An instance of KloudBuster is running, you cannot "\ - "change the config until the run is finished!" - return response.text + self.kb_thread = None + def kb_thread_handler(self, session_id): + kb_session = KBSessionManager.get(session_id) + kb_session.kb_status = 'RUNNING' + kb_config = kb_session.kb_config try: kloudbuster = KloudBuster( kb_config.cred_tested, kb_config.cred_testing, kb_config.server_cfg, kb_config.client_cfg, kb_config.topo_cfg, kb_config.tenants_list) + kb_session.kloudbuster = kloudbuster + if kloudbuster.check_and_upload_images(): kloudbuster.run() + kb_session.kb_status = 'READY' except Exception: - response.status = 403 - response.text = u"Error while running KloudBuster:\n%s" % traceback.format_exc() - kb_config_lock.release() + kb_session.kb_status = 'ERROR' + + @expose(generic=True) + def status(self, *args): + if len(args): + session_id = args[0] + else: + response.status = 400 + response.text = u"Please specify the session_id." return response.text - kb_config_lock.release() + if KBSessionManager.has(session_id): + status = KBSessionManager.get(session_id).kb_status + return status + else: + response.status = 404 + response.text = u"Session ID is not found or invalid." + return response.text + + @expose(generic=True) + def log(self, *args): + if len(args): + session_id = args[0] + else: + response.status = 400 + response.text = u"Please specify the session_id." + return response.text + + if KBSessionManager.has(session_id): + kb_session = KBSessionManager.get(session_id) + plog = kb_session.kloudbuster.dump_logs(offset=0)\ + if kb_session.kloudbuster else "" + return json.dumps(plog) + else: + response.status = 404 + response.text = u"Session ID is not found or invalid." + return response.text + + @expose(generic=True) + def report(self, *args): + if len(args): + session_id = args[0] + else: + response.status = 400 + response.text = u"Please specify the session_id." + return response.text + + if KBSessionManager.has(session_id): + kb_session = KBSessionManager.get(session_id) + preport = kb_session.kloudbuster.kb_runner.report\ + if kb_session.kloudbuster and kb_session.kloudbuster.kb_runner else "" + return json.dumps(preport) + else: + response.status = 404 + response.text = u"Session ID is not found or invalid." + return response.text + + @expose(generic=True) + def version(self): + return kb_version + + @expose(generic=True) + def run(self, *args): + response.status = 400 + response.text = u"Please POST to this resource." + return response.text + + @run.when(method='POST') + def run_POST(self, *args): + if len(args): + session_id = args[0] + else: + response.status = 400 + response.text = u"Please specify the session_id." + return response.text + if not KBSessionManager.has(session_id): + response.status = 404 + response.text = u"Session ID is not found or invalid." + return response.text + if KBSessionManager.get(session_id).kb_status == 'RUNNING': + response.status = 403 + response.text = u"An instance of KloudBuster is already running." + return response.text + + self.kb_thread = threading.Thread(target=self.kb_thread_handler, args=[session_id]) + self.kb_thread.daemon = True + self.kb_thread.start() + return "OK!" diff --git a/kloudbuster/kb_server/kb_server/controllers/kb_session.py b/kloudbuster/kb_server/kb_server/controllers/kb_session.py new file mode 100644 index 0000000..bf5291c --- /dev/null +++ b/kloudbuster/kb_server/kb_server/controllers/kb_session.py @@ -0,0 +1,53 @@ +# Copyright 2015 Cisco Systems, Inc. All rights reserved. +# +# Licensed 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 threading + +KB_SESSIONS = {} +KB_SESSIONS_LOCK = threading.Lock() + +class KBSessionManager(object): + + @staticmethod + def has(session_id): + global KB_SESSIONS + return True if session_id in KB_SESSIONS else False + + @staticmethod + def get(session_id): + global KB_SESSIONS + return KB_SESSIONS[session_id] if KBSessionManager.has(session_id) else None + + @staticmethod + def add(session_id, new_session): + global KB_SESSIONS + global KB_SESSIONS_LOCK + KB_SESSIONS_LOCK.acquire() + KB_SESSIONS[session_id] = new_session + KB_SESSIONS_LOCK.release() + + @staticmethod + def delete(session_id): + global KB_SESSIONS + global KB_SESSIONS_LOCK + KB_SESSIONS_LOCK.acquire() + KB_SESSIONS.pop(session_id) + KB_SESSIONS_LOCK.release() + + +class KBSession(object): + def __init__(self): + self.kb_status = 'READY' + self.kb_config = None + self.kloudbuster = None diff --git a/kloudbuster/kloudbuster-swagger.yaml b/kloudbuster/kb_server/kloudbuster-swagger.yaml similarity index 73% rename from kloudbuster/kloudbuster-swagger.yaml rename to kloudbuster/kb_server/kloudbuster-swagger.yaml index 374b6c5..6617262 100644 --- a/kloudbuster/kloudbuster-swagger.yaml +++ b/kloudbuster/kb_server/kloudbuster-swagger.yaml @@ -6,7 +6,7 @@ info: data plane. version: "0.1.0" host: 127.0.0.1:8080 -basePath: /api/v1 +basePath: /api schemes: - http - https @@ -15,6 +15,19 @@ consumes: produces: - application/json paths: + /config/default_config: + get: + description: | + Get the default KloudBuster configuration from server + tags: + - config + responses: + 200: + description: The default configuration + schema: + type: string + format: json + /config/running_config: post: description: | @@ -40,6 +53,8 @@ paths: schema: type: string description: Errors in details + 403: + description: Session is already existed /config/running_config/{session_id}: get: @@ -65,6 +80,7 @@ paths: put: description: | + (NOT IMPLEMENTED) Update KloudBuster configuration for an existing session parameters: - name: session_id @@ -108,6 +124,18 @@ paths: 404: description: The session_id is not found or invalid + /kloudbuster/version: + get: + description: Get KloudBuster server version + tags: + - kloudbuster + responses: + 200: + description: OK + schema: + type: string + description: The version of the KloudBuster server + /kloudbuster/status/{session_id}: get: description: | @@ -130,9 +158,17 @@ paths: 404: description: The session_id is not found or invalid - /kloudbuster/version: + /kloudbuster/console_log/{session_id}: get: - description: Get KloudBuster server version + description: | + Get KloudBuster console log for a given session + parameters: + - name: session_id + type: string + format: md5sum + in: path + description: The session to be queried + required: true tags: - kloudbuster responses: @@ -140,7 +176,32 @@ paths: description: OK schema: type: string - description: The version of the KloudBuster server + description: The console log of the given session + 404: + description: The session_id is not found or invalid + + /kloudbuster/report/{session_id}: + get: + description: | + Get KloudBuster periodical report for a given session. Only + last report will be returned. + parameters: + - name: session_id + type: string + format: md5sum + in: path + description: The session to be queried + required: true + tags: + - kloudbuster + responses: + 200: + description: OK + schema: + type: string + description: The periodical report of the given session + 404: + description: The session_id is not found or invalid /kloudbuster/run/{session_id}: post: @@ -162,6 +223,8 @@ paths: 403: description: | KloudBuster is already running on the given session + 404: + description: The session_id is not found or invalid definitions: Configuration_New: diff --git a/kloudbuster/kloudbuster.py b/kloudbuster/kloudbuster.py index 78cdb68..2779179 100755 --- a/kloudbuster/kloudbuster.py +++ b/kloudbuster/kloudbuster.py @@ -232,6 +232,8 @@ class KloudBuster(object): self.final_result = None self.server_vm_create_thread = None self.client_vm_create_thread = None + self.kb_runner = None + self.fp_logfile = None def check_and_upload_images(self): keystone_list = [create_keystone_client(self.server_cred)[0], @@ -256,8 +258,8 @@ class KloudBuster(object): kb_image_name = 'dib/' + kb_vm_agent.get_image_name() + '.qcow2' if not os.path.exists(kb_image_name): LOG.error("VM Image not in Glance and could not find " + kb_image_name + - " to upload, please refer " - "to dib/README.rst for how to build image for KloudBuster.") + " to upload, please refer to dib/README.rst for how to build" + " image for KloudBuster.") return False LOG.info("Image is not found in %s, uploading %s..." % (kloud, kb_image_name)) with open(kb_image_name) as fimage: @@ -330,7 +332,6 @@ class KloudBuster(object): """ The runner for KloudBuster Tests """ - kbrunner = None vm_creation_concurrency = self.client_cfg.vm_creation_concurrency try: tenant_quota = self.calc_tenant_quota() @@ -349,16 +350,19 @@ class KloudBuster(object): self.kb_proxy.user_data['role'] = 'KB-PROXY' self.kb_proxy.boot_info['flavor_type'] = 'kb.proxy' if \ not self.tenants_list['client'] else self.testing_kloud.flavor_to_use - if self.testing_kloud.placement_az: - self.kb_proxy.boot_info['avail_zone'] = "%s:%s" %\ - (self.testing_kloud.placement_az, self.topology.clients_rack.split()[0]) + if self.topology: + proxy_hyper = self.topology.clients_rack.split()[0] + self.kb_proxy.boot_info['avail_zone'] =\ + "%s:%s" % (self.testing_kloud.placement_az, proxy_hyper)\ + if self.testing_kloud.placement_az else "nova:%s" % (proxy_hyper) + self.kb_proxy.boot_info['user_data'] = str(self.kb_proxy.user_data) self.testing_kloud.create_vm(self.kb_proxy) - kbrunner = KBRunner(client_list, self.client_cfg, - kb_vm_agent.get_image_version(), - self.single_cloud) - kbrunner.setup_redis(self.kb_proxy.fip_ip) + self.kb_runner = KBRunner(client_list, self.client_cfg, + kb_vm_agent.get_image_version(), + self.single_cloud) + self.kb_runner.setup_redis(self.kb_proxy.fip_ip) if self.single_cloud: # Find the shared network if the cloud used to testing is same @@ -392,11 +396,11 @@ class KloudBuster(object): self.print_provision_info() # Run the runner to perform benchmarkings - kbrunner.run() - self.final_result = kbrunner.tool_result + self.kb_runner.run() + self.final_result = self.kb_runner.tool_result self.final_result['total_server_vms'] = len(server_list) self.final_result['total_client_vms'] = len(client_list) - # self.final_result['host_stats'] = kbrunner.host_stats + # self.final_result['host_stats'] = self.kb_runner.host_stats LOG.info(self.final_result) except KeyboardInterrupt: traceback.format_exc() @@ -418,6 +422,17 @@ class KloudBuster(object): traceback.print_exc() KBResLogger.dump_and_save('clt', self.testing_kloud.res_logger.resource_list) + def dump_logs(self, offset=0): + if not self.fp_logfile: + self.fp_logfile = open(CONF.log_file) + + self.fp_logfile.seek(offset) + return self.fp_logfile.read() + + def dispose(self): + self.fp_logfile.close() + logging.delete_logfile('kloudbuster') + def get_tenant_vm_count(self, config): return (config['users_per_tenant'] * config['routers_per_user'] * config['networks_per_router'] * config['vms_per_network']) @@ -563,13 +578,6 @@ def main(): LOG.error('Error parsing the configuration file') sys.exit(1) - # Use the default image name for Glance - # defaults to something like "kloudbuster_v3" - if not kb_config.server_cfg['image_name']: - kb_config.server_cfg['image_name'] = kb_vm_agent.get_image_name() - if not kb_config.client_cfg['image_name']: - kb_config.client_cfg['image_name'] = kb_vm_agent.get_image_name() - # The KloudBuster class is just a wrapper class # levarages tenant and user class for resource creations and deletion kloudbuster = KloudBuster( diff --git a/kloudbuster/log.py b/kloudbuster/log.py index 530d955..6f14459 100644 --- a/kloudbuster/log.py +++ b/kloudbuster/log.py @@ -13,6 +13,7 @@ # under the License. import logging +import os from oslo_config import cfg from oslo_log import handlers @@ -41,11 +42,15 @@ KBDEBUG = logging.KBDEBUG WARN = logging.WARN WARNING = logging.WARNING -def setup(product_name, version="unknown"): +def setup(product_name, logfile=None): dbg_color = handlers.ColorHandler.LEVEL_COLORS[logging.DEBUG] handlers.ColorHandler.LEVEL_COLORS[logging.KBDEBUG] = dbg_color - oslogging.setup(CONF, product_name, version) + if logfile: + if os.path.exists(logfile): + os.remove(logfile) + CONF.log_file = logfile + oslogging.setup(CONF, product_name) if CONF.kb_debug: oslogging.getLogger( @@ -59,6 +64,14 @@ def getLogger(name="unknown", version="unknown"): "version": version}) return oslogging._loggers[name] +def delete_logfile(product_name): + if CONF.log_file and os.path.exists(CONF.log_file): + os.remove(CONF.log_file) + # Reset the logging to default (stdout) + CONF.log_file = None + oslogging.setup(CONF, product_name) + + class KloudBusterContextAdapter(oslogging.KeywordArgumentAdapter): def kbdebug(self, msg, *args, **kwargs): diff --git a/kloudbuster/wrk_tool.py b/kloudbuster/wrk_tool.py index 9c98b66..be8d3b0 100644 --- a/kloudbuster/wrk_tool.py +++ b/kloudbuster/wrk_tool.py @@ -129,10 +129,10 @@ class WrkTool(PerfTool): @staticmethod def consolidate_samples(results, vm_count): all_res = WrkTool.consolidate_results(results) - total_count = len(results) / vm_count + total_count = float(len(results)) / vm_count if not total_count: return all_res - all_res['http_rps'] = all_res['http_rps'] / total_count - all_res['http_throughput_kbytes'] = all_res['http_throughput_kbytes'] / total_count + all_res['http_rps'] = int(all_res['http_rps'] / total_count) + all_res['http_throughput_kbytes'] = int(all_res['http_throughput_kbytes'] / total_count) return all_res