Post test results signed with RSA key
Added feature to upload test results in to Refstack API signed with your personal RSA key. Just do upload with key '-i PATH_TO_PRIVATE_KEY'. You can use your general OpenSSH key ~/.ssh/id-rsa or generate a new one with 'ssh-keygen -b 4096'. Change-Id: Icce6d7146f0fa9de892d1f4785ef24f17fc9b286
This commit is contained in:
parent
748d259296
commit
29bd7040d2
@ -1,4 +1,4 @@
|
||||
[DEFAULT]
|
||||
test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 ${PYTHON:-python} -m subunit.run discover -t ./ ./refstack_client/tests/unit $LISTOPT $IDOPTION
|
||||
test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 coverage run -m subunit.run discover -t ./ -s ./refstack_client/tests/unit $LISTOPT $IDOPTION
|
||||
test_id_option=--load-list $IDFILE
|
||||
test_list_option=--list
|
||||
|
@ -68,6 +68,14 @@ We've created an "easy button" for Ubuntu, Centos, RHEL and openSuSe.
|
||||
'upload' command, you can also override the Refstack API server uploaded to
|
||||
with the --url option.
|
||||
|
||||
**Note:**
|
||||
|
||||
a. Adding -i <path-to-private-key> option will upload test result with
|
||||
digital signature. For signing refstack-client uses private RSA key.
|
||||
OpenSSH format of rsa keys supported, so you can just use your ssh key
|
||||
'~/.ssh/id-rsa' or generate a new one with 'ssh-keygen -b 4096'.
|
||||
For now, signed test results can be considereded as private.
|
||||
|
||||
**Tempest Hacking**
|
||||
|
||||
By default, refstack-client installs Tempest into the `.tempest` directory.
|
||||
|
@ -25,6 +25,7 @@ Tempest configuration file.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import binascii
|
||||
import ConfigParser
|
||||
import json
|
||||
import logging
|
||||
@ -33,8 +34,10 @@ import requests
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from Crypto.Hash import SHA256
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Signature import PKCS1_v1_5
|
||||
from keystoneclient.v2_0 import client as ksclient
|
||||
|
||||
from subunit_processor import SubunitProcessor
|
||||
|
||||
|
||||
@ -163,20 +166,36 @@ class RefstackClient:
|
||||
results = subunit_processor.process_stream()
|
||||
return results
|
||||
|
||||
def post_results(self, url, content):
|
||||
def post_results(self, url, content, sign_with=None):
|
||||
'''Post the combined results back to the server.'''
|
||||
endpoint = '%s/v1/results/' % url
|
||||
headers = {'Content-type': 'application/json'}
|
||||
data = json.dumps(content)
|
||||
self.logger.debug('API request content: %s ' % content)
|
||||
if sign_with:
|
||||
data_hash = SHA256.new()
|
||||
data_hash.update(data.encode('utf-8'))
|
||||
with open(sign_with) as key_pair_file:
|
||||
try:
|
||||
key = RSA.importKey(key_pair_file.read())
|
||||
except (IOError, ValueError) as e:
|
||||
self.logger.info('Error during upload key pair %s'
|
||||
'' % key_pair_file)
|
||||
self.logger.exception(e)
|
||||
return
|
||||
signer = PKCS1_v1_5.new(key)
|
||||
sign = signer.sign(data_hash)
|
||||
headers['X-Signature'] = binascii.b2a_hex(sign)
|
||||
headers['X-Public-Key'] = key.publickey().exportKey('OpenSSH')
|
||||
try:
|
||||
url = '%s/v1/results/' % self.args.url
|
||||
headers = {'Content-type': 'application/json'}
|
||||
|
||||
response = requests.post(url,
|
||||
data=json.dumps(content),
|
||||
response = requests.post(endpoint,
|
||||
data=data,
|
||||
headers=headers)
|
||||
self.logger.info(url + " Response: " + str(response.text))
|
||||
self.logger.info(endpoint + " Response: " + str(response.text))
|
||||
except Exception as e:
|
||||
self.logger.critical('Failed to post %s - %s ' % (url, e))
|
||||
raise
|
||||
self.logger.info('Failed to post %s - %s ' % (endpoint, e))
|
||||
self.logger.exception(e)
|
||||
return
|
||||
|
||||
def test(self):
|
||||
'''Execute Tempest test against the cloud.'''
|
||||
@ -240,7 +259,8 @@ class RefstackClient:
|
||||
# If the user specified the upload argument, then post
|
||||
# the results.
|
||||
if self.args.upload:
|
||||
self.post_results(self.args.url, content)
|
||||
self.post_results(self.args.url, content,
|
||||
sign_with=self.args.priv_key)
|
||||
else:
|
||||
self.logger.error("Problem executing Tempest script. Exit code %d",
|
||||
process.returncode)
|
||||
@ -251,7 +271,8 @@ class RefstackClient:
|
||||
json_file = open(self.upload_file)
|
||||
json_data = json.load(json_file)
|
||||
json_file.close()
|
||||
self.post_results(self.args.url, json_data)
|
||||
self.post_results(self.args.url, json_data,
|
||||
sign_with=self.args.priv_key)
|
||||
|
||||
|
||||
def parse_cli_args(args=None):
|
||||
@ -273,28 +294,37 @@ def parse_cli_args(args=None):
|
||||
action='count',
|
||||
help='Show verbose output.')
|
||||
|
||||
url_arg = argparse.ArgumentParser(add_help=False)
|
||||
url_arg.add_argument('--url',
|
||||
action='store',
|
||||
required=False,
|
||||
default='http://api.refstack.net',
|
||||
type=str,
|
||||
help='Refstack API URL to upload results to '
|
||||
'(--url http://localhost:8000).')
|
||||
shared_args.add_argument('--url',
|
||||
action='store',
|
||||
required=False,
|
||||
default='http://api.refstack.net',
|
||||
type=str,
|
||||
help='Refstack API URL to upload results to '
|
||||
'(--url http://localhost:8000).')
|
||||
|
||||
shared_args.add_argument('-i', '--sign',
|
||||
type=str,
|
||||
required=False,
|
||||
dest='priv_key',
|
||||
help='Private RSA key. '
|
||||
'OpenSSH RSA keys format supported ('
|
||||
'-i ~/.ssh/id-rsa)')
|
||||
|
||||
# Upload command
|
||||
parser_upload = subparsers.add_parser(
|
||||
'upload', parents=[shared_args, url_arg],
|
||||
'upload', parents=[shared_args],
|
||||
help='Upload an existing result file.'
|
||||
)
|
||||
|
||||
parser_upload.add_argument('file',
|
||||
type=str,
|
||||
help='Path of JSON results file.')
|
||||
|
||||
parser_upload.set_defaults(func="upload")
|
||||
|
||||
# Test command
|
||||
parser_test = subparsers.add_parser(
|
||||
'test', parents=[shared_args, url_arg],
|
||||
'test', parents=[shared_args],
|
||||
help='Run Tempest against a cloud.')
|
||||
|
||||
parser_test.add_argument('-c', '--conf-file',
|
||||
|
51
refstack_client/tests/unit/rsa_key
Normal file
51
refstack_client/tests/unit/rsa_key
Normal file
@ -0,0 +1,51 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
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
|
||||
-----END RSA PRIVATE KEY-----
|
@ -45,7 +45,7 @@ class TestRefstackClient(unittest.TestCase):
|
||||
self.addCleanup(patcher.stop)
|
||||
return thing
|
||||
|
||||
def mock_argv(self, conf_file_name=None, verbose=None):
|
||||
def mock_argv(self, conf_file_name=None, verbose=None, priv_key=None):
|
||||
"""
|
||||
Build argv for test.
|
||||
:param conf_file_name: Configuration file name
|
||||
@ -58,6 +58,8 @@ class TestRefstackClient(unittest.TestCase):
|
||||
'-c', conf_file_name,
|
||||
'--test-cases', 'tempest.api.compute',
|
||||
'--url', 'http://127.0.0.1']
|
||||
if priv_key:
|
||||
argv.extend(('-i', priv_key))
|
||||
if verbose:
|
||||
argv.append(verbose)
|
||||
return argv
|
||||
@ -245,6 +247,30 @@ class TestRefstackClient(unittest.TestCase):
|
||||
'%s' % expected_response
|
||||
)
|
||||
|
||||
def test_post_results_with_sign(self):
|
||||
"""
|
||||
Test the post_results method, ensuring a requests call is made.
|
||||
"""
|
||||
args = rc.parse_cli_args(self.mock_argv(priv_key='rsa_key'))
|
||||
client = rc.RefstackClient(args)
|
||||
client.logger.info = MagicMock()
|
||||
content = {'duration_seconds': 0,
|
||||
'cpid': 'test-id',
|
||||
'results': [{'name': 'tempest.passed.test'}]}
|
||||
expected_response = json.dumps({'test_id': 42})
|
||||
|
||||
@httmock.urlmatch(netloc=r'(.*\.)?127.0.0.1$', path='/v1/results/')
|
||||
def refstack_api_mock(url, request):
|
||||
return expected_response
|
||||
|
||||
with httmock.HTTMock(refstack_api_mock):
|
||||
client.post_results("http://127.0.0.1", content,
|
||||
sign_with=self.test_path + '/rsa_key')
|
||||
client.logger.info.assert_called_with(
|
||||
'http://127.0.0.1/v1/results/ Response: '
|
||||
'%s' % expected_response
|
||||
)
|
||||
|
||||
def test_run_tempest(self):
|
||||
"""
|
||||
Test that the test command will run the tempest script using the
|
||||
@ -299,6 +325,40 @@ class TestRefstackClient(unittest.TestCase):
|
||||
|
||||
self.assertTrue(client.post_results.called)
|
||||
|
||||
def test_run_tempest_upload_with_sign(self):
|
||||
"""
|
||||
Test that the test command will run the tempest script and call
|
||||
post_results when the --upload argument is passed in.
|
||||
"""
|
||||
argv = self.mock_argv(verbose='-vv', priv_key='rsa_key')
|
||||
argv.append('--upload')
|
||||
args = rc.parse_cli_args(argv)
|
||||
client = rc.RefstackClient(args)
|
||||
client.tempest_dir = self.test_path
|
||||
mock_popen = self.patch(
|
||||
'refstack_client.refstack_client.subprocess.Popen',
|
||||
return_value=MagicMock(returncode=0))
|
||||
self.patch("os.path.isfile", return_value=True)
|
||||
self.mock_keystone()
|
||||
client.get_passed_tests = MagicMock(return_value=['test'])
|
||||
client.post_results = MagicMock()
|
||||
client._save_json_results = MagicMock()
|
||||
client.test()
|
||||
mock_popen.assert_called_with(
|
||||
('%s/run_tempest.sh' % self.test_path, '-C', self.conf_file_name,
|
||||
'-V', '-t', '--', 'tempest.api.compute'),
|
||||
stderr=None
|
||||
)
|
||||
|
||||
self.assertTrue(client.post_results.called)
|
||||
client.post_results.assert_called_with(
|
||||
'http://127.0.0.1',
|
||||
{'duration_seconds': 0,
|
||||
'cpid': 'test-id',
|
||||
'results': ['test']},
|
||||
sign_with='rsa_key'
|
||||
)
|
||||
|
||||
def test_run_tempest_no_conf_file(self):
|
||||
"""
|
||||
Test when a nonexistent configuration file is passed in.
|
||||
@ -377,7 +437,8 @@ class TestRefstackClient(unittest.TestCase):
|
||||
'results': [{'name': 'tempest.passed.test'}]}
|
||||
|
||||
client.post_results.assert_called_with('http://api.test.org',
|
||||
expected_json)
|
||||
expected_json,
|
||||
sign_with=None)
|
||||
|
||||
def test_upload_nonexisting_file(self):
|
||||
"""
|
||||
|
@ -1,3 +1,4 @@
|
||||
python-keystoneclient>=0.10.0
|
||||
gitpython>=0.3.2.RC1
|
||||
python-subunit>=0.0.18
|
||||
python-subunit>=0.0.18
|
||||
pycrypto>=2.6.1
|
8
tox.ini
8
tox.ini
@ -23,7 +23,13 @@ distribute = false
|
||||
commands = {posargs}
|
||||
|
||||
[testenv:cover]
|
||||
commands = python setup.py testr --coverage --testr-args='{posargs}'
|
||||
commands = coverage run \
|
||||
--include './refstack_client/*' \
|
||||
--omit './refstack_client/tests*' \
|
||||
-m testtools.run discover refstack_client.tests.unit
|
||||
coverage report
|
||||
coverage html -d cover
|
||||
/bin/rm .coverage
|
||||
|
||||
[tox:jenkins]
|
||||
downloadcache = ~/cache/pip
|
||||
|
Loading…
Reference in New Issue
Block a user