diff --git a/refstack_client/refstack_client.py b/refstack_client/refstack_client.py index c1bc6d1..a84a124 100755 --- a/refstack_client/refstack_client.py +++ b/refstack_client/refstack_client.py @@ -342,6 +342,37 @@ class RefstackClient: except KeyboardInterrupt: 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): @@ -349,52 +380,57 @@ def parse_cli_args(args=None): 'To see help on specific argument, do:\n' 'refstack-client -h') - parser = argparse.ArgumentParser(description='Refstack-client arguments', - formatter_class=argparse. - ArgumentDefaultsHelpFormatter, - usage=usage_string) + parser = argparse.ArgumentParser( + description='Refstack-client arguments', + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + usage=usage_string + ) subparsers = parser.add_subparsers(help='Available subcommands.') # Arguments that go with all subcommands. shared_args = argparse.ArgumentParser(add_help=False) + shared_args.add_argument('-v', '--verbose', action='count', 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', action='store_true', dest='quiet', required=False, 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 parser_upload = subparsers.add_parser( - 'upload', parents=[shared_args], + 'upload', parents=[shared_args, network_args], help='Upload an existing result file.' ) @@ -406,7 +442,7 @@ def parse_cli_args(args=None): # Test command parser_test = subparsers.add_parser( - 'test', parents=[shared_args], + 'test', parents=[shared_args, network_args], help='Run Tempest against a cloud.') parser_test.add_argument('-c', '--conf-file', @@ -457,7 +493,7 @@ def parse_cli_args(args=None): # List command parser_list = subparsers.add_parser( - 'list', parents=[shared_args], + 'list', parents=[shared_args, network_args], help='List last results from Refstack') parser_list.add_argument('--start-date', required=False, @@ -475,4 +511,16 @@ def parse_cli_args(args=None): '(e.g. --end-date "2015-04-24 01:23:56").') 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) diff --git a/refstack_client/tests/unit/test_client.py b/refstack_client/tests/unit/test_client.py index 8dac226..775e397 100755 --- a/refstack_client/tests/unit/test_client.py +++ b/refstack_client/tests/unit/test_client.py @@ -52,13 +52,12 @@ class TestRefstackClient(unittest.TestCase): :param verbose: verbosity level :return: argv """ - argv = [command, - '--url', 'http://127.0.0.1', '-y'] - if kwargs.get('priv_key', None): - argv.extend(('-i', kwargs.get('priv_key', None))) + argv = [command] if 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': argv.extend( ('-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. """ - 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.logger.info = MagicMock() content = {'duration_seconds': 0, @@ -326,7 +327,7 @@ class TestRefstackClient(unittest.TestCase): """ argv = self.mock_argv(verbose='-vv', test_cases='tempest.api.compute') - argv.insert(1, '--upload') + argv.insert(2, '--upload') args = rc.parse_cli_args(argv) client = rc.RefstackClient(args) client.tempest_dir = self.test_path @@ -354,13 +355,14 @@ class TestRefstackClient(unittest.TestCase): """ argv = self.mock_argv(verbose='-vv', priv_key='rsa_key', test_cases='tempest.api.compute') - argv.insert(1, '--upload') + argv.insert(2, '--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)) + return_value=MagicMock(returncode=0) + ) self.patch("os.path.isfile", return_value=True) self.mock_keystone() client.get_passed_tests = MagicMock(return_value=['test']) @@ -433,8 +435,8 @@ class TestRefstackClient(unittest.TestCase): """ argv = self.mock_argv(verbose='-vv', test_cases='tempest.api.compute') - argv.insert(1, '--result-file-tag') - argv.insert(2, 'my-test') + argv.insert(2, '--result-file-tag') + argv.insert(3, 'my-test') args = rc.parse_cli_args(argv) client = rc.RefstackClient(args) client.tempest_dir = self.test_path @@ -479,7 +481,8 @@ class TestRefstackClient(unittest.TestCase): """ upload_file_path = self.test_path + "/.testrepository/0.json" 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.post_results = MagicMock() @@ -495,7 +498,7 @@ class TestRefstackClient(unittest.TestCase): } client.post_results.assert_called_with('http://127.0.0.1', expected_json, - sign_with=None) + sign_with='rsa_key') def test_upload_nonexisting_file(self): """ @@ -559,3 +562,15 @@ class TestRefstackClient(unittest.TestCase): client.yield_results = MagicMock(return_value=mock_results) client.list() 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'))