Update for test related operations.

Set finished to true when test completed.
Implement delete tests.
Add place holders for show test status and report.
Remove label field from test web page.
Enhance error handling for invalid test_id.

Change-Id: I0b3a81871867cd5e5c3ab8a1dd8f63911926a0e9
This commit is contained in:
Raymond Wong 2014-03-19 14:15:54 -07:00
parent 5c05c7609a
commit 84c0057204
6 changed files with 207 additions and 134 deletions

View File

@ -29,25 +29,25 @@
</button>
<button type="button"
class="btn btn-default btn-xs"
rel="tooltip"
rel="tooltip"
title="Edit Cloud"
data-placement="top"
onclick="window.location='/edit-cloud/{{ cloud.id }}'">
<span class="glyphicon glyphicon-pencil"></span>
<span class="glyphicon glyphicon-pencil"></span>
</button>
<button type="button"
<button type="button"
class="btn btn-default btn-xs"
rel="tooltip"
rel="tooltip"
title="Delete Cloud"
data-placement="top"
onclick="confirm_delete_cloud('{{ cloud.id }}','{{ cloud.label}}')">
<span class="glyphicon glyphicon-trash"></span>
<span class="glyphicon glyphicon-trash"></span>
</button>
</div>
</td>
</tr>
{% endfor %}
</table>
</table>
</div>
<div class="panel panel-default unit one-of-two">
@ -66,29 +66,29 @@
{% if test.finished %}
<button type="button"
class="btn btn-default btn-xs"
rel="tooltip"
rel="tooltip"
title="Show Report"
data-placement="top"
onclick="window.location='/show-report/{{ test.id }}'">
<span class="glyphicon glyphicon-file"></span>
<span class="glyphicon glyphicon-file"></span>
</button>
{% else %}
<button type="button"
class="btn btn-default btn-xs"
rel="tooltip"
rel="tooltip"
title="Test In Progress..."
data-placement="top"
onclick="window.location='/show-status/{{ test.id }}'">
<span class="glyphicon glyphicon-refresh"></span>
<span class="glyphicon glyphicon-refresh"></span>
</button>
{% endif %}
<button type="button"
<button type="button"
class="btn btn-default btn-xs"
rel="tooltip"
rel="tooltip"
title="Delete Test"
data-placement="top"
onclick="confirm_delete_test('{{ test.id }}','{{ cloud.label}}')">
<span class="glyphicon glyphicon-trash"></span>
<span class="glyphicon glyphicon-trash"></span>
</button>
</div>
</td>

View File

@ -0,0 +1,8 @@
{% extends "layout.html" %}
{% block title %}Test Report{% endblock %}
{% block body %}
<h2>Test Report</h2>
Test ID: {{ test.id }}
<br>
<input type='button' value="Back" onclick="location.href='{{ next_url }}'">
{% endblock %}

View File

@ -0,0 +1,8 @@
{% extends "layout.html" %}
{% block title %}Test Status{% endblock %}
{% block body %}
<h2>Test Status</h2>
Test ID: {{ test.id }}
<br>
<input type='button' value="Back" onclick="location.href='{{ next_url }}'">
{% endblock %}

View File

@ -1,19 +1,16 @@
{% extends "layout.html" %}
{% block title %}Test Cloud{% endblock %}
{% extends "layout.html" %}
{% block title %}Test Cloud{% endblock %}
{% block body %}
<h2>Test Cloud</h2>
<form action = "" method = 'post'>
<h2>Test Cloud</h2>
<form action="" method='post'>
<dl>
<dt>Test Run Name:</dt>
<dd><input type = 'text' name = 'label' size = '30' value = "{{ request.values.label }}"></dd>
<dt>Test User ({{ names.user }}) Password:</dt>
<dd><input type = 'password' name = 'pw_user' size = '30' value = "{{ request.values.pw_user }}"></dd>
<dt>Admin User ({{ names.admin }}) Password:</dt>
<dd><input type = 'password' name = 'pw_admin' size = '30' value = "{{ request.values.pw_admin }}"></dd>
<dt>Test User ({{ names.user }}) Password:</dt>
<dd><input type='password' name='pw_user' size='30' value="{{ request.values.pw_user }}"></dd>
<dt>Admin User ({{ names.admin }}) Password:</dt>
<dd><input type='password' name='pw_admin' size='30' value="{{ request.values.pw_admin }}"></dd>
</dl>
<p>
<input type = 'button' value = "Cancel" onclick = "location.href = '/'">
<input type = 'submit' value = "Run">
<input type = 'hidden' name = 'next' value = "{{ next }}">
</form>
<input type='button' value="Cancel" onclick="location.href='{{ next_url }}'">
<input type='submit' value="Run">
</form>
{% endblock %}

View File

@ -17,12 +17,10 @@
from docker_buildfile import DockerBuildFile
import json
import os
from refstack.extensions import db
from refstack.models import Cloud
from refstack.models import Test
from refstack.refstack_config import RefStackConfig
configData = RefStackConfig()
config_data = RefStackConfig()
# Common tempest conf values to be used for all tests
# - Image user and password can be set to any value for testing purpose
@ -47,38 +45,33 @@ class TempestTester(object):
'''Utility class to handle tempest test.'''
test_id = None
testObj = None
cloudObj = None
test_obj = None
def __init__(self, test_id=None):
'''Init method loads specified id.
If test_id exists, this is an existing test.
Otherwise, this is a new test, and test_id will be created later.
'''
def __init__(self, test_id):
'''Extract the corresponding test object from the db.'''
if test_id:
self.test_id = test_id
self.testObj = Test.query.filter_by(id=test_id).first()
if self.testObj is not None:
self.cloudObj = Cloud.query.filter_by(
id=self.testObj.cloud_id).first()
self.test_obj = Test.query.filter_by(id=test_id).first()
if self.test_obj:
self.test_id = test_id
return
raise ValueError('Invalid test id %s' % (test_id))
def generate_miniconf(self):
'''Return a JSON object representing the mini tempest conf.'''
# Get custom tempest config from config file
custom_tempest_conf = configData.get_tempest_config()
custom_tempest_conf = config_data.get_tempest_config()
# Construct cloud specific tempest config from db
if self.cloudObj:
if self.test_obj.cloud:
cloud_tempest_conf = {
"identity":
{
"uri": self.cloudObj.endpoint,
"uri_v3": self.cloudObj.endpoint_v3,
"username": self.cloudObj.test_user,
"alt_username": self.cloudObj.test_user,
"admin_username": self.cloudObj.admin_user
"uri": self.test_obj.cloud.endpoint,
"uri_v3": self.test_obj.cloud.endpoint_v3,
"username": self.test_obj.cloud.test_user,
"alt_username": self.test_obj.cloud.test_user,
"admin_username": self.test_obj.cloud.admin_user
}
}
else:
@ -112,7 +105,7 @@ class TempestTester(object):
'''Return a JSON array of the tempest testcases to be executed.'''
# Set to full tempest test unless it is specified in the config file
testcases = configData.get_tempest_testcases()
testcases = config_data.get_tempest_testcases()
if not testcases:
testcases = {"testcases": ["tempest"]}
@ -127,27 +120,6 @@ class TempestTester(object):
print f.read()
f.close()
def test_cloud(self, cloud_id, extraConfJSON=None):
'''Create and execute a new test with the provided extraConfJSON.'''
# Retrieve the cloud obj from DB
self.cloudObj = Cloud.query.filter_by(id=cloud_id).first()
if not self.cloudObj:
return
# Create new test object in DB and get the real unique test_id
self.testObj = Test()
self.testObj.cloud_id = self.cloudObj.id
self.testObj.cloud = self.cloudObj
db.session.add(self.testObj)
db.session.commit()
self.test_id = self.testObj.id
# Invoke execute_test
self.execute_test(extraConfJSON)
def execute_test(self, extraConfJSON=None):
'''Execute the tempest test with the provided extraConfJSON.'''
@ -157,8 +129,8 @@ class TempestTester(object):
''' TODO: Initial test status in DB '''
if configData.get_test_mode():
test_mode = configData.get_test_mode().upper()
if config_data.get_test_mode():
test_mode = config_data.get_test_mode().upper()
else:
# Default to use docker if not specified in the config file
test_mode = 'DOCKER'
@ -174,19 +146,19 @@ class TempestTester(object):
'''Execute the tempest test in a docker container.'''
''' Create the docker build file '''
dockerFile = os.path.join(configData.get_working_dir(),
dockerFile = os.path.join(config_data.get_working_dir(),
'test_%s.dockerFile' % self.test_id)
fileBuilder = DockerBuildFile()
fileBuilder.test_id = self.test_id
fileBuilder.api_server_address = configData.get_app_address()
fileBuilder.api_server_address = config_data.get_app_address()
''' TODO: Determine tempest URL based on the cloud version '''
''' ForNow: Use the Tempest URL in the config file '''
fileBuilder.tempest_code_url = configData.get_tempest_url()
fileBuilder.tempest_code_url = config_data.get_tempest_url()
fileBuilder.confJSON = extraConfJSON
fileBuilder.build_docker_buildfile(dockerFile)
''' Execute the docker build file '''
outFile = os.path.join(configData.get_working_dir(),
outFile = os.path.join(config_data.get_working_dir(),
'test_%s.dockerOutput' % self.test_id)
cmd = 'nohup docker build - < %s > %s &' % (dockerFile, outFile)

View File

@ -22,9 +22,9 @@ from refstack import app as base_app
from refstack.extensions import db
from refstack.extensions import oid
from refstack.models import Cloud
from refstack.models import Test
from refstack.models import User
from refstack.models import Vendor
from refstack.models import Test
from refstack.tools.tempest_tester import TempestTester
app = base_app.create_app()
@ -35,7 +35,7 @@ public_routes = ['/post-result', '/get-miniconf']
@app.before_request
def before_request():
"""Runs before the request itself."""
if not request.path in public_routes:
if request.path not in public_routes:
g.user = None
if 'openid' in session:
flask.g.user = User.query.\
@ -113,14 +113,34 @@ def create_profile():
@app.route('/delete-cloud/<int:cloud_id>', methods=['GET', 'POST'])
def delete_cloud(cloud_id):
"""Delete function for clouds."""
c = Cloud.query.filter_by(id=cloud_id).first()
cloud = Cloud.query.filter_by(id=cloud_id).first()
if not c:
if not cloud:
flash(u'Not a valid Cloud ID!')
elif not c.user_id == g.user.id:
elif not cloud.user_id == g.user.id:
flash(u"This isn't your cloud!")
else:
db.session.delete(c)
# Delete all the tests associated with the cloud from DB
for test in cloud.tests:
db.session.delete(test)
# Delete the cloud from DB
db.session.delete(cloud)
db.session.commit()
return redirect('/')
@app.route('/delete-test/<int:test_id>', methods=['GET', 'POST'])
def delete_test(test_id):
"""Delete function for tests."""
test = Test.query.filter_by(id=test_id).first()
if not test:
flash(u'Not a valid Test ID!')
elif not test.cloud.user_id == g.user.id:
flash(u"This isn't your test!")
else:
db.session.delete(test)
db.session.commit()
return redirect('/')
@ -128,12 +148,12 @@ def delete_cloud(cloud_id):
@app.route('/edit-cloud/<int:cloud_id>', methods=['GET', 'POST'])
def edit_cloud(cloud_id):
c = Cloud.query.filter_by(id=cloud_id).first()
cloud = Cloud.query.filter_by(id=cloud_id).first()
if not c:
if not cloud:
flash(u'Not a valid Cloud ID!')
return redirect('/')
elif not c.user_id == g.user.id:
elif not cloud.user_id == g.user.id:
flash(u"This isn't your cloud!")
if request.method == 'POST':
@ -150,26 +170,26 @@ def edit_cloud(cloud_id):
elif not request.form['admin_user']:
flash(u'Error: All fields are required')
else:
c.label = request.form['label']
c.endpoint = request.form['endpoint']
c.test_user = request.form['test_user']
c.admin_endpoint = request.form['admin_endpoint']
c.endpoint_v3 = request.form['endpoint_v3']
c.version = request.form['version']
c.admin_user = request.form['admin_user']
cloud.label = request.form['label']
cloud.endpoint = request.form['endpoint']
cloud.test_user = request.form['test_user']
cloud.admin_endpoint = request.form['admin_endpoint']
cloud.endpoint_v3 = request.form['endpoint_v3']
cloud.version = request.form['version']
cloud.admin_user = request.form['admin_user']
db.session.commit()
flash(u'Cloud Saved!')
return redirect('/')
form = dict(label=c.label,
endpoint=c.endpoint,
endpoint_v3=c.endpoint_v3,
admin_endpoint=c.admin_endpoint,
admin_user=c.admin_user,
version=c.version,
test_user=c.test_user)
form = dict(label=cloud.label,
endpoint=cloud.endpoint,
endpoint_v3=cloud.endpoint_v3,
admin_endpoint=cloud.admin_endpoint,
admin_user=cloud.admin_user,
version=cloud.version,
test_user=cloud.test_user)
return render_template('edit_cloud.html', form=form)
@ -192,17 +212,17 @@ def create_cloud():
elif not request.form['admin_user']:
flash(u'Error: All fields are required')
else:
c = Cloud()
c.user_id = g.user.id
c.label = request.form['label']
c.endpoint = request.form['endpoint']
c.test_user = request.form['test_user']
c.admin_endpoint = request.form['admin_endpoint']
c.endpoint_v3 = request.form['endpoint_v3']
c.version = request.form['version']
c.admin_user = request.form['admin_user']
new_cloud = Cloud()
new_cloud.user_id = g.user.id
new_cloud.label = request.form['label']
new_cloud.endpoint = request.form['endpoint']
new_cloud.test_user = request.form['test_user']
new_cloud.admin_endpoint = request.form['admin_endpoint']
new_cloud.endpoint_v3 = request.form['endpoint_v3']
new_cloud.version = request.form['version']
new_cloud.admin_user = request.form['admin_user']
db.session.add(c)
db.session.add(new_cloud)
db.session.commit()
return redirect('/')
@ -258,32 +278,47 @@ def logout():
def test_cloud(cloud_id):
"""Handler for creating a new test."""
c = Cloud.query.filter_by(id=cloud_id).first()
cloud = Cloud.query.filter_by(id=cloud_id).first()
if not c:
if not cloud:
flash(u'Not a valid Cloud ID!')
return redirect('/')
elif not c.user_id == g.user.id:
elif not cloud.user_id == g.user.id:
flash(u"This isn't your cloud!")
return redirect('/')
if request.method == 'POST':
REQUIRED_FIELDS = ('label', 'pw_admin', 'pw_user')
# Check for required fields
REQUIRED_FIELDS = ('pw_admin', 'pw_user')
if not all(request.form[field] for field in REQUIRED_FIELDS):
flash(u'Error: All fields are required')
else:
''' Construct confJSON with the passwords provided '''
# Create new test object in db
new_test = Test()
new_test.cloud = cloud
new_test.cloud_id = cloud.id
db.session.add(new_test)
db.session.commit()
# Construct confJSON with the passwords provided
# and using the same user for alt_user
pw_admin = request.form['pw_admin']
pw_user = request.form['pw_user']
# Using the same user for alt_user
pw_alt = request.form['pw_user']
jstr = '{"identity":{"password":"%s","admin_password":"%s",\
json_str = '{"identity":{"password":"%s","admin_password":"%s",\
"alt_password":"%s"}}' % (pw_user, pw_admin, pw_alt)
TempestTester().test_cloud(cloud_id, jstr)
flash(u'Test Started!')
# Excute the test
try:
TempestTester(new_test.id).execute_test(json_str)
flash(u'Test started!')
except ValueError:
flash(u'Fail to start test!')
return redirect('/')
names = dict(user=c.test_user, admin=c.admin_user)
names = dict(user=cloud.test_user, admin=cloud.admin_user)
return render_template('test_cloud.html', next_url='/', names=names)
@ -300,10 +335,13 @@ def get_miniconf():
"""Return a JSON of mini tempest conf to a remote test runner."""
test_id = request.args.get('test_id', '')
response = make_response(TempestTester(test_id).generate_miniconf())
response.headers["Content-Disposition"] = \
"attachment; filename=miniconf.json"
return response
try:
response = make_response(TempestTester(test_id).generate_miniconf())
response.headers["Content-Disposition"] = \
"attachment; filename=miniconf.json"
return response
except ValueError:
return make_response('Invalid test ID')
@app.route('/get-testcases', methods=['GET'])
@ -311,10 +349,13 @@ def get_testcases():
"""Return a JSON of tempest test cases to a remote test runner."""
test_id = request.args.get('test_id', '')
response = make_response(TempestTester(test_id).generate_testcases())
response.headers["Content-Disposition"] = \
"attachment; filename=testcases.json"
return response
try:
response = make_response(TempestTester(test_id).generate_testcases())
response.headers["Content-Disposition"] = \
"attachment; filename=testcases.json"
return response
except ValueError:
return make_response('Invalid test ID')
@app.route('/post-result', methods=['POST'])
@ -329,17 +370,64 @@ def post_result():
if request.args.get('test_id', ''):
# this data is for a specific test triggered by the gui and we
# want to relate it
_test = Test.query.\
new_test = Test.query.\
filter_by(id=request.args.get('test_id', '')).first()
_test.subunit = f.read()
new_test.subunit = f.read()
new_test.finished = True
else:
# anonymous data .. we still want to capture it
_test = Test()
_test.subunit = f.read()
db.session.add(_test)
new_test = Test()
new_test.subunit = f.read()
new_test.finished = True
db.session.add(new_test)
db.session.commit()
# todo .. set up error handling with correct response codes
return make_response('')
@app.route('/show-status/<int:test_id>', methods=['GET', 'POST'])
def show_status(test_id):
"""Handler for showing test status."""
test = Test.query.filter_by(id=test_id).first()
if not test:
flash(u'Not a valid Test ID!')
return redirect('/')
elif not test.cloud.user_id == g.user.id:
flash(u"This isn't your test!")
return redirect('/')
# This is a place holder for now
''' TODO: Generate the appropriate status page '''
return render_template('show_status.html', next_url='/', test=test)
@app.route('/show-report/<int:test_id>', methods=['GET', 'POST'])
def show_report(test_id):
"""Handler for showing test report."""
test = Test.query.filter_by(id=test_id).first()
if not test:
flash(u'Not a valid Test ID!')
return redirect('/')
elif not test.cloud.user_id == g.user.id:
flash(u"This isn't your test!")
return redirect('/')
# This is a place holder for now
''' TODO: Generate the appropriate report page '''
''' ForNow: send back the subunit data stream for debugging '''
response = make_response(test.subunit)
response.headers["Content-Disposition"] = \
"attachment; filename=subunit.txt"
response.content_type = "text/plain"
return response
# return render_template('show_report.html', next_url='/', test=test)