Add command to generate pubkey's signature

Uploading public key in to Refstack requires pub key self-signature
This signature can be generated with refstac-client using "sign"
command. For example:

./refstack-client sign ~/.ssh/id_rsa

Will print public key ~/.ssh/id_rsa.pub and self signature for it.

Change-Id: I0ee3e89c5e8bcae85eae63500a5fd98cea12c273
This commit is contained in:
sslypushenko
2015-07-10 18:28:32 +03:00
parent 43777f9820
commit 67de4f69dc
2 changed files with 107 additions and 44 deletions

View File

@@ -342,6 +342,37 @@ class RefstackClient:
except KeyboardInterrupt: except KeyboardInterrupt:
return return
def _sign_pubkey(self):
"""Generate self signature for public key"""
try:
with open(self.args.priv_key_to_sign) as priv_key_file:
private_key = RSA.importKey(priv_key_file.read())
except (IOError, ValueError) as e:
self.logger.error('Error reading private key %s'
'' % self.args.priv_key_to_sign)
self.logger.exception(e)
return
pubkey_filename = '.'.join((self.args.priv_key_to_sign, 'pub'))
try:
with open(pubkey_filename) as pub_key_file:
pub_key = pub_key_file.read()
except IOError:
self.logger.error('Public key file %s not found. '
'Public key is generated from private one.'
'' % pubkey_filename)
pub_key = private_key.publickey().exportKey('OpenSSH')
data_hash = SHA256.new()
data_hash.update('signature'.encode('utf-8'))
signer = PKCS1_v1_5.new(private_key)
signature = binascii.b2a_hex(signer.sign(data_hash))
return pub_key, signature
def self_sign(self):
"""Generate signature for public key."""
pub_key, signature = self._sign_pubkey()
print('Public key:\n%s\n' % pub_key)
print('Self signature:\n%s\n' % signature)
def parse_cli_args(args=None): def parse_cli_args(args=None):
@@ -349,52 +380,57 @@ def parse_cli_args(args=None):
'To see help on specific argument, do:\n' 'To see help on specific argument, do:\n'
'refstack-client <ARG> -h') 'refstack-client <ARG> -h')
parser = argparse.ArgumentParser(description='Refstack-client arguments', parser = argparse.ArgumentParser(
formatter_class=argparse. description='Refstack-client arguments',
ArgumentDefaultsHelpFormatter, formatter_class=argparse.ArgumentDefaultsHelpFormatter,
usage=usage_string) usage=usage_string
)
subparsers = parser.add_subparsers(help='Available subcommands.') subparsers = parser.add_subparsers(help='Available subcommands.')
# Arguments that go with all subcommands. # Arguments that go with all subcommands.
shared_args = argparse.ArgumentParser(add_help=False) shared_args = argparse.ArgumentParser(add_help=False)
shared_args.add_argument('-v', '--verbose', shared_args.add_argument('-v', '--verbose',
action='count', action='count',
help='Show verbose output.') help='Show verbose output.')
shared_args.add_argument('--url',
action='store',
required=False,
default=os.environ.get(
'REFSTACK_URL', 'http://api.refstack.net'),
type=str,
help='Refstack API URL to upload results to. '
'Defaults to env[REFSTACK_URL] or '
'http://api.refstack.net if it is not set '
'(--url http://localhost:8000).')
shared_args.add_argument('-k', '--insecure',
action='store_false',
dest='insecure',
required=False,
help='Assume Yes to all prompt queries')
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)')
shared_args.add_argument('-y', shared_args.add_argument('-y',
action='store_true', action='store_true',
dest='quiet', dest='quiet',
required=False, required=False,
help='Assume Yes to all prompt queries') help='Assume Yes to all prompt queries')
# Arguments that go with network-related subcommands (test, list, etc.).
network_args = argparse.ArgumentParser(add_help=False)
network_args.add_argument('--url',
action='store',
required=False,
default=os.environ.get(
'REFSTACK_URL', 'http://api.refstack.net'),
type=str,
help='Refstack API URL to upload results to. '
'Defaults to env[REFSTACK_URL] or '
'http://api.refstack.net if it is not set '
'(--url http://localhost:8000).')
network_args.add_argument('-k', '--insecure',
action='store_false',
dest='insecure',
required=False,
help='Skip SSL checks while interacting '
'with Refstack API')
network_args.add_argument('-i', '--sign',
type=str,
required=False,
dest='priv_key',
help='Path to private RSA key. '
'OpenSSH RSA keys format supported')
# Upload command # Upload command
parser_upload = subparsers.add_parser( parser_upload = subparsers.add_parser(
'upload', parents=[shared_args], 'upload', parents=[shared_args, network_args],
help='Upload an existing result file.' help='Upload an existing result file.'
) )
@@ -406,7 +442,7 @@ def parse_cli_args(args=None):
# Test command # Test command
parser_test = subparsers.add_parser( parser_test = subparsers.add_parser(
'test', parents=[shared_args], 'test', parents=[shared_args, network_args],
help='Run Tempest against a cloud.') help='Run Tempest against a cloud.')
parser_test.add_argument('-c', '--conf-file', parser_test.add_argument('-c', '--conf-file',
@@ -457,7 +493,7 @@ def parse_cli_args(args=None):
# List command # List command
parser_list = subparsers.add_parser( parser_list = subparsers.add_parser(
'list', parents=[shared_args], 'list', parents=[shared_args, network_args],
help='List last results from Refstack') help='List last results from Refstack')
parser_list.add_argument('--start-date', parser_list.add_argument('--start-date',
required=False, required=False,
@@ -475,4 +511,16 @@ def parse_cli_args(args=None):
'(e.g. --end-date "2015-04-24 01:23:56").') '(e.g. --end-date "2015-04-24 01:23:56").')
parser_list.set_defaults(func='list') parser_list.set_defaults(func='list')
# Sign command
parser_sign = subparsers.add_parser(
'sign', parents=[shared_args],
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
help='Generate signature for public key.')
parser_sign.add_argument('priv_key_to_sign',
type=str,
help='Path to private RSA key. '
'OpenSSH RSA keys format supported')
parser_sign.set_defaults(func='self_sign')
return parser.parse_args(args=args) return parser.parse_args(args=args)

View File

@@ -52,13 +52,12 @@ class TestRefstackClient(unittest.TestCase):
:param verbose: verbosity level :param verbose: verbosity level
:return: argv :return: argv
""" """
argv = [command, argv = [command]
'--url', 'http://127.0.0.1', '-y']
if kwargs.get('priv_key', None):
argv.extend(('-i', kwargs.get('priv_key', None)))
if kwargs.get('verbose', None): if kwargs.get('verbose', None):
argv.append(kwargs.get('verbose', None)) argv.append(kwargs.get('verbose', None))
argv.extend(['--url', 'http://127.0.0.1', '-y'])
if kwargs.get('priv_key', None):
argv.extend(('-i', kwargs.get('priv_key', None)))
if command == 'test': if command == 'test':
argv.extend( argv.extend(
('-c', kwargs.get('conf_file_name', self.conf_file_name))) ('-c', kwargs.get('conf_file_name', self.conf_file_name)))
@@ -271,7 +270,9 @@ class TestRefstackClient(unittest.TestCase):
""" """
Test the post_results method, ensuring a requests call is made. Test the post_results method, ensuring a requests call is made.
""" """
args = rc.parse_cli_args(self.mock_argv(priv_key='rsa_key')) argv = self.mock_argv(command='upload', priv_key='rsa_key')
argv.append('fake.json')
args = rc.parse_cli_args(argv)
client = rc.RefstackClient(args) client = rc.RefstackClient(args)
client.logger.info = MagicMock() client.logger.info = MagicMock()
content = {'duration_seconds': 0, content = {'duration_seconds': 0,
@@ -326,7 +327,7 @@ class TestRefstackClient(unittest.TestCase):
""" """
argv = self.mock_argv(verbose='-vv', argv = self.mock_argv(verbose='-vv',
test_cases='tempest.api.compute') test_cases='tempest.api.compute')
argv.insert(1, '--upload') argv.insert(2, '--upload')
args = rc.parse_cli_args(argv) args = rc.parse_cli_args(argv)
client = rc.RefstackClient(args) client = rc.RefstackClient(args)
client.tempest_dir = self.test_path client.tempest_dir = self.test_path
@@ -354,13 +355,14 @@ class TestRefstackClient(unittest.TestCase):
""" """
argv = self.mock_argv(verbose='-vv', priv_key='rsa_key', argv = self.mock_argv(verbose='-vv', priv_key='rsa_key',
test_cases='tempest.api.compute') test_cases='tempest.api.compute')
argv.insert(1, '--upload') argv.insert(2, '--upload')
args = rc.parse_cli_args(argv) args = rc.parse_cli_args(argv)
client = rc.RefstackClient(args) client = rc.RefstackClient(args)
client.tempest_dir = self.test_path client.tempest_dir = self.test_path
mock_popen = self.patch( mock_popen = self.patch(
'refstack_client.refstack_client.subprocess.Popen', 'refstack_client.refstack_client.subprocess.Popen',
return_value=MagicMock(returncode=0)) return_value=MagicMock(returncode=0)
)
self.patch("os.path.isfile", return_value=True) self.patch("os.path.isfile", return_value=True)
self.mock_keystone() self.mock_keystone()
client.get_passed_tests = MagicMock(return_value=['test']) client.get_passed_tests = MagicMock(return_value=['test'])
@@ -433,8 +435,8 @@ class TestRefstackClient(unittest.TestCase):
""" """
argv = self.mock_argv(verbose='-vv', argv = self.mock_argv(verbose='-vv',
test_cases='tempest.api.compute') test_cases='tempest.api.compute')
argv.insert(1, '--result-file-tag') argv.insert(2, '--result-file-tag')
argv.insert(2, 'my-test') argv.insert(3, 'my-test')
args = rc.parse_cli_args(argv) args = rc.parse_cli_args(argv)
client = rc.RefstackClient(args) client = rc.RefstackClient(args)
client.tempest_dir = self.test_path client.tempest_dir = self.test_path
@@ -479,7 +481,8 @@ class TestRefstackClient(unittest.TestCase):
""" """
upload_file_path = self.test_path + "/.testrepository/0.json" upload_file_path = self.test_path + "/.testrepository/0.json"
args = rc.parse_cli_args( args = rc.parse_cli_args(
self.mock_argv(command='upload') + [upload_file_path]) self.mock_argv(command='upload', priv_key='rsa_key')
+ [upload_file_path])
client = rc.RefstackClient(args) client = rc.RefstackClient(args)
client.post_results = MagicMock() client.post_results = MagicMock()
@@ -495,7 +498,7 @@ class TestRefstackClient(unittest.TestCase):
} }
client.post_results.assert_called_with('http://127.0.0.1', client.post_results.assert_called_with('http://127.0.0.1',
expected_json, expected_json,
sign_with=None) sign_with='rsa_key')
def test_upload_nonexisting_file(self): def test_upload_nonexisting_file(self):
""" """
@@ -559,3 +562,15 @@ class TestRefstackClient(unittest.TestCase):
client.yield_results = MagicMock(return_value=mock_results) client.yield_results = MagicMock(return_value=mock_results)
client.list() client.list()
self.assertTrue(mock_stdout.write.called) self.assertTrue(mock_stdout.write.called)
def test_sign_pubkey(self):
"""
Test that the test command will run the tempest script and call
post_results when the --upload argument is passed in.
"""
args = rc.parse_cli_args(['sign',
os.path.join(self.test_path, 'rsa_key')])
client = rc.RefstackClient(args)
pubkey, signature = client._sign_pubkey()
self.assertTrue(pubkey.startswith('ssh-rsa AAAA'))
self.assertTrue(signature.startswith('413cb954'))