fuel-qa/fuelweb_test/helpers/rally.py

428 lines
19 KiB
Python

# Copyright 2015 Mirantis, Inc.
#
# 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.
from __future__ import division
import json
import os
from devops.helpers.helpers import wait
from proboscis.asserts import assert_equal
from proboscis.asserts import assert_true
from fuelweb_test import logger
class RallyEngine(object):
def __init__(self,
admin_remote,
container_repo,
proxy_url=None,
user_id=0,
dir_for_home='/var/rally_home',
home_bind_path='/home/rally'):
self.admin_remote = admin_remote
self.container_repo = container_repo
self.repository_tag = 'latest'
self.proxy_url = proxy_url or ""
self.user_id = user_id
self.dir_for_home = dir_for_home
self.home_bind_path = home_bind_path
self.setup()
def image_exists(self, tag='latest'):
cmd = "docker images | awk 'NR > 1{print $1\" \"$2}'"
logger.debug('Checking Docker images...')
result = self.admin_remote.execute(cmd)
logger.debug(result)
existing_images = [line.strip().split() for line in result['stdout']]
return [self.container_repo, tag] in existing_images
def pull_image(self):
# TODO(apanchenko): add possibility to load image from local path or
# remote link provided in settings, in order to speed up downloading
cmd = 'docker pull {0}'.format(self.container_repo)
logger.debug('Downloading Rally repository/image from registry...')
result = self.admin_remote.execute(cmd)
logger.debug(result)
return self.image_exists()
def run_container_command(self, command, in_background=False):
command = str(command).replace(r"'", r"'\''")
options = ''
if in_background:
options = '{0} -d'.format(options)
cmd = ("docker run {options} --user {user_id} --net=\"host\" -e "
"\"http_proxy={proxy_url}\" -e \"https_proxy={proxy_url}\" "
"-v {dir_for_home}:{home_bind_path} {container_repo}:{tag} "
"/bin/bash -c '{command}'".format(
options=options,
user_id=self.user_id,
proxy_url=self.proxy_url,
dir_for_home=self.dir_for_home,
home_bind_path=self.home_bind_path,
container_repo=self.container_repo,
tag=self.repository_tag,
command=command))
logger.debug('Executing command "{0}" in Rally container {1}..'.format(
cmd, self.container_repo))
result = self.admin_remote.execute(cmd)
logger.debug(result)
return result
def setup_utils(self):
utils = ['gawk', 'vim', 'curl']
cmd = ('unset http_proxy https_proxy; apt-get update; '
'apt-get install -y {0}'.format(' '.join(utils)))
logger.debug('Installing utils "{0}" to the Rally container...'.format(
utils))
result = self.run_container_command(cmd)
assert_equal(result['exit_code'], 0,
'Utils installation failed in Rally container: '
'{0}'.format(result))
def create_database(self):
check_rally_db_cmd = 'test -s .rally.sqlite'
result = self.run_container_command(check_rally_db_cmd)
if result['exit_code'] == 0:
return
logger.debug('Recreating Database for Rally...')
create_rally_db_cmd = 'rally-manage db recreate'
result = self.run_container_command(create_rally_db_cmd)
assert_equal(result['exit_code'], 0,
'Rally Database creation failed: {0}!'.format(result))
result = self.run_container_command(check_rally_db_cmd)
assert_equal(result['exit_code'], 0, 'Failed to create Database for '
'Rally: {0} !'.format(result))
def prepare_image(self):
self.create_database()
self.setup_utils()
last_container_cmd = "docker ps -lq"
result = self.admin_remote.execute(last_container_cmd)
assert_equal(result['exit_code'], 0,
"Unable to get last container ID: {0}!".format(result))
last_container = ''.join([line.strip() for line in result['stdout']])
commit_cmd = 'docker commit {0} {1}:ready'.format(last_container,
self.container_repo)
result = self.admin_remote.execute(commit_cmd)
assert_equal(result['exit_code'], 0,
'Commit to Docker image "{0}" failed: {1}.'.format(
self.container_repo, result))
return self.image_exists(tag='ready')
def setup_bash_alias(self):
alias_name = 'rally_docker'
check_alias_cmd = '. /root/.bashrc && alias {0}'.format(alias_name)
result = self.admin_remote.execute(check_alias_cmd)
if result['exit_code'] == 0:
return
logger.debug('Creating bash alias for Rally inside container...')
create_alias_cmd = ("alias {alias_name}='docker run --user {user_id} "
"--net=\"host\" -e \"http_proxy={proxy_url}\" -t "
"-i -v {dir_for_home}:{home_bind_path} "
"{container_repo}:{tag} rally'".format(
alias_name=alias_name,
user_id=self.user_id,
proxy_url=self.proxy_url,
dir_for_home=self.dir_for_home,
home_bind_path=self.home_bind_path,
container_repo=self.container_repo,
tag=self.repository_tag))
result = self.admin_remote.execute('echo "{0}">> /root/.bashrc'.format(
create_alias_cmd))
assert_equal(result['exit_code'], 0,
"Alias creation for running Rally from container failed: "
"{0}.".format(result))
result = self.admin_remote.execute(check_alias_cmd)
assert_equal(result['exit_code'], 0,
"Alias creation for running Rally from container failed: "
"{0}.".format(result))
def setup(self):
if not self.image_exists():
assert_true(self.pull_image(),
"Docker image for Rally not found!")
if not self.image_exists(tag='ready'):
assert_true(self.prepare_image(),
"Docker image for Rally is not ready!")
self.repository_tag = 'ready'
self.setup_bash_alias()
def list_deployments(self):
cmd = (r"rally deployment list | awk -F "
r"'[[:space:]]*\\\\|[[:space:]]*' '/\ydeploy\y/{print $2}'")
result = self.run_container_command(cmd)
logger.debug('Rally deployments list: {0}'.format(result))
return [line.strip() for line in result['stdout']]
def show_deployment(self, deployment_uuid):
cmd = ("rally deployment show {0} | awk -F "
"'[[:space:]]*\\\\|[[:space:]]*' '/\w/{{print $2\",\"$3\",\"$4"
"\",\"$5\",\"$6\",\"$7\",\"$8}}'").format(deployment_uuid)
result = self.run_container_command(cmd)
assert_equal(len(result['stdout']), 2,
"Command 'rally deployment show' returned unexpected "
"value: expected 2 lines, got {0}: ".format(result))
keys = [k for k in result['stdout'][0].strip().split(',') if k != '']
values = [v for v in result['stdout'][1].strip().split(',') if v != '']
return {keys[i]: values[i] for i in range(0, len(keys))}
def list_tasks(self):
cmd = "rally task list --uuids-only"
result = self.run_container_command(cmd)
logger.debug('Rally tasks list: {0}'.format(result))
return [line.strip() for line in result['stdout']]
def get_task_status(self, task_uuid):
cmd = "rally task status {0}".format(task_uuid)
result = self.run_container_command(cmd)
assert_equal(result['exit_code'], 0,
"Getting Rally task status failed: {0}".format(result))
task_status = ''.join(result['stdout']).strip().split()[-1]
logger.debug('Rally task "{0}" has status "{1}".'.format(task_uuid,
task_status))
return task_status
class RallyDeployment(object):
def __init__(self, rally_engine, cluster_vip, username, password, tenant,
key_port=5000, proxy_url=''):
self.rally_engine = rally_engine
self.cluster_vip = cluster_vip
self.username = username
self.password = password
self.tenant_name = tenant
self.keystone_port = str(key_port)
self.proxy_url = proxy_url
self.auth_url = "http://{0}:{1}/v2.0/".format(self.cluster_vip,
self.keystone_port)
self.set_proxy = not self.is_proxy_set
self._uuid = None
self.create_deployment()
@property
def uuid(self):
if self._uuid is None:
for d_uuid in self.rally_engine.list_deployments():
deployment = self.rally_engine.show_deployment(d_uuid)
logger.debug("Deployment info: {0}".format(deployment))
if self.auth_url in deployment['auth_url'] and \
self.username == deployment['username'] and \
self.tenant_name == deployment['tenant_name']:
self._uuid = d_uuid
break
return self._uuid
@property
def is_proxy_set(self):
cmd = '[ "${{http_proxy}}" == "{0}" ]'.format(self.proxy_url)
return self.rally_engine.run_container_command(cmd)['exit_code'] == 0
@property
def is_deployment_exist(self):
return self.uuid is not None
def create_deployment(self):
if self.is_deployment_exist:
return
cmd = ('rally deployment create --name "{0}" --filename '
'<(echo \'{{ "admin": {{ "password": "{1}", "tenant_name": "{2}'
'", "username": "{3}" }}, "auth_url": "{4}", "endpoint": null, '
'"type": "ExistingCloud", "https_insecure": true }}\')').format(
self.cluster_vip, self.password, self.tenant_name, self.username,
self.auth_url)
result = self.rally_engine.run_container_command(cmd)
assert_true(self.is_deployment_exist,
'Rally deployment creation failed: {0}'.format(result))
logger.debug('Rally deployment created: {0}'.format(result))
assert_true(self.check_deployment(),
"Rally deployment check failed.")
def check_deployment(self, deployment_uuid=''):
cmd = 'rally deployment check {0}'.format(deployment_uuid)
result = self.rally_engine.run_container_command(cmd)
if result['exit_code'] == 0:
return True
else:
logger.error('Rally deployment check failed: {0}'.format(result))
return False
class RallyTask(object):
def __init__(self, rally_deployment, test_type):
self.deployment = rally_deployment
self.engine = self.deployment.rally_engine
self.test_type = test_type
self.uuid = None
self._status = None
@property
def status(self):
if self.uuid is None:
self._status = None
else:
self._status = self.engine.get_task_status(self.uuid)
return self._status
def prepare_scenario(self):
scenario_file = '{0}/fuelweb_test/rally/scenarios/{1}.json'.format(
os.environ.get("WORKSPACE", "./"), self.test_type)
remote_path = '{0}/{1}.json'.format(self.engine.dir_for_home,
self.test_type)
self.engine.admin_remote.upload(scenario_file, remote_path)
result = self.engine.admin_remote.execute('test -f {0}'.format(
remote_path))
assert_equal(result['exit_code'], 0,
"Scenario upload filed: {0}".format(result))
return '{0}.json'.format(self.test_type)
def start(self):
scenario = self.prepare_scenario()
temp_file = '{0}_results.tmp.txt'.format(scenario)
cmd = 'rally task start {0} &> {1}'.format(scenario, temp_file)
result = self.engine.run_container_command(cmd, in_background=True)
logger.debug('Started Rally task: {0}'.format(result))
cmd = ("awk 'BEGIN{{retval=1}};/^Using task:/{{print $NF; retval=0}};"
"END {{exit retval}}' {0}").format(temp_file)
wait(lambda: self.engine.run_container_command(cmd)['exit_code'] == 0,
timeout=30, timeout_msg='Rally task {!r} creation timeout'
''.format(result))
result = self.engine.run_container_command(cmd)
task_uuid = ''.join(result['stdout']).strip()
assert_true(task_uuid in self.engine.list_tasks(),
"Rally task creation failed: {0}".format(result))
self.uuid = task_uuid
def abort(self, task_id):
logger.debug('Stop Rally task {0}'.format(task_id))
cmd = 'rally task abort {0}'.format(task_id)
self.engine.run_container_command(cmd)
assert_true(
self.status in ('finished', 'aborted'),
"Rally task {0} was not aborted; current task status "
"is {1}".format(task_id, self.status))
def get_results(self):
if self.status == 'finished':
cmd = 'rally task results {0}'.format(self.uuid)
result = self.engine.run_container_command(cmd)
assert_equal(result['exit_code'], 0,
"Getting task results failed: {0}".format(result))
logger.debug("Rally task {0} result: {1}".format(self.uuid,
result))
return ''.join(result['stdout'])
class RallyResult(object):
def __init__(self, json_results):
self.values = {
'full_duration': 0.00,
'load_duration': 0.00,
'errors': 0
}
self.raw_data = []
self.parse_raw_results(json_results)
def parse_raw_results(self, raw_results):
data = json.loads(raw_results)
assert_equal(len(data), 1,
"Current implementation of RallyResult class doesn't "
"support results with length greater than '1'!")
self.raw_data = data[0]
self.values['full_duration'] = data[0]['full_duration']
self.values['load_duration'] = data[0]['load_duration']
self.values['errors'] = sum([len(result['error'])
for result in data[0]['result']])
@staticmethod
def compare(first_result, second_result, deviation=0.1):
"""
Compare benchmark results
:param first_result: RallyResult
:param second_result: RallyResult
:param deviation: float
:return: bool
"""
message = ''
equal = True
for val in first_result.values.keys():
logger.debug('Comparing {2}: {0} and {1}'.format(
first_result.values[val], second_result.values[val],
val
))
if first_result.values[val] == 0 or second_result.values[val] == 0:
if first_result.values[val] != second_result.values[val]:
message += "Values of '{0}' are: {1} and {2}. ".format(
val,
first_result.values[val],
second_result.values[val])
equal = False
continue
diff = abs(
first_result.values[val] / second_result.values[val] - 1)
if diff > deviation:
message += "Values of '{0}' are: {1} and {2}. ".format(
val, first_result.values[val], second_result.values[val])
equal = False
if not equal:
logger.info("Rally benchmark results aren't equal: {0}".format(
message))
return equal
def show(self):
return json.dumps(self.raw_data)
class RallyBenchmarkTest(object):
def __init__(self, container_repo, environment, cluster_id,
test_type):
self.admin_remote = environment.d_env.get_admin_remote()
self.cluster_vip = environment.fuel_web.get_mgmt_vip(cluster_id)
self.cluster_credentials = \
environment.fuel_web.get_cluster_credentials(cluster_id)
self.proxy_url = environment.fuel_web.get_alive_proxy(cluster_id)
logger.debug('Rally proxy URL is: {0}'.format(self.proxy_url))
self.container_repo = container_repo
self.home_dir = 'rally-{0}'.format(cluster_id)
self.test_type = test_type
self.engine = RallyEngine(
admin_remote=self.admin_remote,
container_repo=self.container_repo,
proxy_url=self.proxy_url,
dir_for_home='/var/{0}/'.format(self.home_dir)
)
self.deployment = RallyDeployment(
rally_engine=self.engine,
cluster_vip=self.cluster_vip,
username=self.cluster_credentials['username'],
password=self.cluster_credentials['password'],
tenant=self.cluster_credentials['tenant'],
proxy_url=self.proxy_url
)
self.current_task = None
def run(self, timeout=60 * 10, result=True):
self.current_task = RallyTask(self.deployment, self.test_type)
logger.info('Starting Rally benchmark test...')
self.current_task.start()
assert_equal(self.current_task.status, 'running',
'Rally task was started, but it is not running, status: '
'{0}'.format(self.current_task.status))
if result:
wait(lambda: self.current_task.status == 'finished',
timeout=timeout, timeout_msg='Rally benchmark test timeout')
logger.info('Rally benchmark test is finished.')
return RallyResult(json_results=self.current_task.get_results())