
Added an --offline argument which will prevent Tempest results from being uploaded to the Refstack server if used. Change-Id: I40df179288354a6eb7fac1d0ed634d722a780817 Story: https://storyboard.openstack.org/#!/story/190
239 lines
9.0 KiB
Python
Executable File
239 lines
9.0 KiB
Python
Executable File
#!/usr/bin/env python
|
|
#
|
|
# Copyright (c) 2014 Piston Cloud Computing, 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.
|
|
#
|
|
|
|
|
|
"""
|
|
Run Tempest and upload results to Refstack.
|
|
|
|
This module runs the Tempest test suite on an OpenStack environment given a
|
|
Tempest configuration file.
|
|
|
|
"""
|
|
|
|
import argparse
|
|
import ConfigParser
|
|
import json
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
import time
|
|
|
|
from keystoneclient.v2_0 import client as ksclient
|
|
|
|
from subunit_processor import SubunitProcessor
|
|
|
|
|
|
class RefstackClient:
|
|
log_format = "%(asctime)s %(name)s:%(lineno)d %(levelname)s %(message)s"
|
|
|
|
def __init__(self, args):
|
|
'''Prepare a tempest test against a cloud.'''
|
|
self.logger = logging.getLogger("refstack_client")
|
|
self.console_log_handle = logging.StreamHandler()
|
|
self.console_log_handle.setFormatter(
|
|
logging.Formatter(self.log_format))
|
|
self.logger.addHandler(self.console_log_handle)
|
|
|
|
if args.verbose > 1:
|
|
self.logger.setLevel(logging.DEBUG)
|
|
elif args.verbose == 1:
|
|
self.logger.setLevel(logging.INFO)
|
|
else:
|
|
self.logger.setLevel(logging.ERROR)
|
|
|
|
# assign local vars to match args
|
|
self.url = args.url
|
|
self.tempest_dir = args.tempest_dir
|
|
self.tempest_script = os.path.join(self.tempest_dir, 'run_tempest.sh')
|
|
self.test_cases = args.test_cases
|
|
self.verbose = args.verbose
|
|
self.offline = args.offline
|
|
|
|
# Check that the config file exists.
|
|
if not os.path.isfile(args.conf_file):
|
|
self.logger.error("File not valid: %s" % args.conf_file)
|
|
exit(1)
|
|
|
|
self.conf_file = args.conf_file
|
|
self.conf = ConfigParser.SafeConfigParser()
|
|
self.conf.read(self.conf_file)
|
|
|
|
self.results_file = self._get_subunit_output_file()
|
|
self.cpid = self._get_cpid_from_keystone()
|
|
|
|
def post_results(self, content):
|
|
'''Post the combined results back to the server.'''
|
|
|
|
# TODO(cdiep): Post results once the API is available as outlined here:
|
|
# github.com/stackforge/refstack/blob/master/specs/approved/api-v1.md
|
|
|
|
self.logger.debug('API request content: %s ' % content)
|
|
|
|
def _get_subunit_output_file(self):
|
|
'''This method reads from the next-stream file in the .testrepository
|
|
directory of the given Tempest path. The integer here is the name
|
|
of the file where subunit output will be saved to.'''
|
|
try:
|
|
subunit_file = open(os.path.join(
|
|
self.tempest_dir, '.testrepository',
|
|
'next-stream'), 'r').read().rstrip()
|
|
except (IOError, OSError):
|
|
self.logger.debug('The .testrepository/next-stream file was not '
|
|
'found. Assuming subunit results will be stored '
|
|
'in file 0.')
|
|
|
|
# Testr saves the first test stream to .testrepository/0 when
|
|
# there is a newly generated .testrepository directory.
|
|
subunit_file = "0"
|
|
|
|
return os.path.join(self.tempest_dir, '.testrepository', subunit_file)
|
|
|
|
def _get_cpid_from_keystone(self):
|
|
'''This will get the Keystone service ID which is used as the CPID.'''
|
|
try:
|
|
args = {'auth_url': self.conf.get('identity', 'uri'),
|
|
'username': self.conf.get('identity', 'admin_username'),
|
|
'password': self.conf.get('identity', 'admin_password')}
|
|
|
|
if self.conf.has_option('identity', 'admin_tenant_id'):
|
|
args['tenant_id'] = self.conf.get('identity',
|
|
'admin_tenant_id')
|
|
else:
|
|
args['tenant_name'] = self.conf.get('identity',
|
|
'admin_tenant_name')
|
|
|
|
client = ksclient.Client(**args)
|
|
services = client.services.list()
|
|
for service in services:
|
|
if service.type == "identity":
|
|
return service.id
|
|
|
|
except ConfigParser.Error as e:
|
|
# Most likely a missing section or option in the config file.
|
|
self.logger.error("Invalid Config File: %s" % e)
|
|
exit(1)
|
|
|
|
def _form_json_content(self, cpid, duration, results):
|
|
'''This method will create the JSON content for the request.'''
|
|
content = {}
|
|
content['cpid'] = cpid
|
|
content['duration_seconds'] = duration
|
|
content['results'] = results
|
|
return json.dumps(content)
|
|
|
|
def get_passed_tests(self, result_file):
|
|
'''Get just the tests IDs in a subunit file that passed Tempest.'''
|
|
subunit_processor = SubunitProcessor(result_file)
|
|
results = subunit_processor.process_stream()
|
|
return results
|
|
|
|
def run(self):
|
|
'''Execute tempest test against the cloud.'''
|
|
try:
|
|
self.logger.info("Starting Tempest test...")
|
|
start_time = time.time()
|
|
|
|
# Run the tempest script, specifying the conf file and the
|
|
# flag telling it to not use a virtual environment.
|
|
cmd = (self.tempest_script, '-C', self.conf_file, '-N', '-t')
|
|
|
|
# Add the tempest test cases to test as arguments.
|
|
if self.test_cases:
|
|
cmd += ('--', self.test_cases)
|
|
else:
|
|
cmd += ('--', "tempest.api")
|
|
|
|
# If there were two verbose flags, show tempest results.
|
|
if self.verbose > 1:
|
|
stderr = None
|
|
else:
|
|
# Suppress tempest results output. Note that testr prints
|
|
# results to stderr.
|
|
stderr = open(os.devnull, 'w')
|
|
|
|
# Execute the tempest test script in a subprocess.
|
|
process = subprocess.Popen(cmd, stderr=stderr)
|
|
process.communicate()
|
|
|
|
end_time = time.time()
|
|
elapsed = end_time - start_time
|
|
duration = int(elapsed)
|
|
|
|
self.logger.info('Tempest test complete.')
|
|
self.logger.info('Subunit results located in: %s' %
|
|
self.results_file)
|
|
|
|
results = self.get_passed_tests(self.results_file)
|
|
self.logger.info("Number of passed tests: %d" % len(results))
|
|
|
|
# If the user did not specify the offline argument, then upload
|
|
# the results.
|
|
if not self.offline:
|
|
content = self._form_json_content(self.cpid, duration, results)
|
|
self.post_results(content)
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
self.logger.error('%s failed to complete' % (e))
|
|
|
|
|
|
def parse_cli_args(args=None):
|
|
|
|
parser = argparse.ArgumentParser(description='Starts a tempest test',
|
|
formatter_class=argparse.
|
|
ArgumentDefaultsHelpFormatter)
|
|
|
|
parser.add_argument('-v', '--verbose',
|
|
action='count',
|
|
help='Show verbose output. Note that -vv will show '
|
|
'Tempest test result output.')
|
|
|
|
parser.add_argument('--offline',
|
|
action='store_true',
|
|
help='Do not upload test results after running '
|
|
'Tempest.')
|
|
|
|
parser.add_argument('--url',
|
|
action='store',
|
|
required=False,
|
|
default='https://api.refstack.org',
|
|
type=str,
|
|
help='Refstack API URL to post results to (e.g. --url '
|
|
'https://127.0.0.1:8000).')
|
|
|
|
parser.add_argument('--tempest-dir',
|
|
action='store',
|
|
required=False,
|
|
dest='tempest_dir',
|
|
default='test_runner/src/tempest',
|
|
help='Path of the tempest project directory.')
|
|
|
|
parser.add_argument('-c', '--conf-file',
|
|
action='store',
|
|
required=True,
|
|
dest='conf_file',
|
|
type=str,
|
|
help='Path of the tempest configuration file to use.')
|
|
|
|
parser.add_argument('-t', '--test-cases',
|
|
action='store',
|
|
required=False,
|
|
dest='test_cases',
|
|
type=str,
|
|
help='Specify a subset of test cases to run '
|
|
'(e.g. --test-cases tempest.api.compute).')
|
|
return parser.parse_args(args=args)
|