Add support of Session Manager for Rest API

1. Add support of Session Manager for Rest API;
2. Add Rest API for getting periodical report;
3. Add Rest API for getting console logs;
4. Fix KB-PROXY placement bug when topology file is provided;
5. Fix the logic for consolidating samples;
6. Refine Swagger definition files for Rest API;
7. Set the -c parameter in kb_gen_chart.py to required;

Known Issue / TODO:
1. The Pecan server logs will be dumped to clients;
2. The RestAPI "log" need remember the offset and only gets the
incremental

Change-Id: If912fbfcabc73a04caa0122ffe942405a667608d
This commit is contained in:
Yichen Wang 2015-08-25 18:15:35 -07:00
parent 8aaa12d2b0
commit 0d30c860fc
13 changed files with 375 additions and 84 deletions

1
.gitignore vendored
View File

@ -60,5 +60,4 @@ kb*.log
*.html
*.qcow2
scale/dib/kloudbuster.d/
.vagrant/

View File

@ -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):

View File

@ -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()

View File

@ -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='<file>')

View File

@ -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

View File

@ -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 = {

View File

@ -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': {<TOPOLOGY_CONFIGS>}
# 'tenants_cfg': {<TENANT_AND_USER_LISTS_FOR_REUSING>}
# }
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

View File

@ -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!"

View File

@ -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

View File

@ -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:

View File

@ -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(

View File

@ -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):

View File

@ -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