diff options
author | Claudiu Belu <cbelu@cloudbasesolutions.com> | 2014-06-26 09:30:10 -0400 |
---|---|---|
committer | Andrey Kurilin <akurilin@mirantis.com> | 2015-08-05 19:55:48 +0000 |
commit | 4a812d953b27d3481fbe9d6af8723008f2060f94 (patch) | |
tree | a5fe7a8394afef40a64ab591c29a7bfa3ca54462 | |
parent | 02889515a5db5ea43a7aa2bc76ffa70248861ce9 (diff) | |
download | python-novaclient-4a812d953b27d3481fbe9d6af8723008f2060f94.tar.gz |
Adds support for x509 certificates as keypairs
Adds new parameter --key-type for novaclient for creating x509
certificates as keypairs. If no --key-type is specified, a ssh
keypair is created, for backwards compatibility.
Adds 'Type' column for keypair-list, displaying the keypair type.
This commit will have to merge after:
https://review.openstack.org/#/c/140313
Depends-On: I215662f2f92a01921a866c3218031787a9eaf915
Implements: blueprint keypair-x509-certificates
Co-Authored-By: Andrey Kurilin <andr.kurilin@gmail.com>
Co-Authored-By: Alex Xu <hejie.xu@intel.com>
Change-Id: I12bb13e24b660ffb6da0e5be275acbba7453d011
-rw-r--r-- | novaclient/__init__.py | 2 | ||||
-rw-r--r-- | novaclient/tests/functional/fake_crypto.py | 49 | ||||
-rw-r--r-- | novaclient/tests/functional/test_keypairs.py | 125 | ||||
-rw-r--r-- | novaclient/tests/unit/v2/test_keypairs.py | 46 | ||||
-rw-r--r-- | novaclient/tests/unit/v2/test_shell.py | 67 | ||||
-rw-r--r-- | novaclient/v2/keypairs.py | 17 | ||||
-rw-r--r-- | novaclient/v2/shell.py | 32 |
7 files changed, 315 insertions, 23 deletions
diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 43e5f256..a52cedc8 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -20,4 +20,4 @@ from novaclient import api_versions __version__ = pbr.version.VersionInfo('python-novaclient').version_string() API_MIN_VERSION = api_versions.APIVersion("2.1") -API_MAX_VERSION = api_versions.APIVersion("2.1") +API_MAX_VERSION = api_versions.APIVersion("2.2") diff --git a/novaclient/tests/functional/fake_crypto.py b/novaclient/tests/functional/fake_crypto.py new file mode 100644 index 00000000..56df5151 --- /dev/null +++ b/novaclient/tests/functional/fake_crypto.py @@ -0,0 +1,49 @@ +# Copyright 2015 Cloudbase Solutions +# 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. + + +def get_x509_cert_and_fingerprint(): + fingerprint = "a1:6f:6d:ea:a6:36:d0:3a:c6:eb:b6:ee:07:94:3e:2a:90:98:2b:c9" + certif = ( + "-----BEGIN CERTIFICATE-----\n" + "MIIDIjCCAgqgAwIBAgIJAIE8EtWfZhhFMA0GCSqGSIb3DQEBCwUAMCQxIjAgBgNV\n" + "BAMTGWNsb3VkYmFzZS1pbml0LXVzZXItMTM1NTkwHhcNMTUwMTI5MTgyMzE4WhcN\n" + "MjUwMTI2MTgyMzE4WjAkMSIwIAYDVQQDExljbG91ZGJhc2UtaW5pdC11c2VyLTEz\n" + "NTU5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv4lv95ofkXLIbALU\n" + "UEb1f949TYNMUvMGNnLyLgGOY+D61TNG7RZn85cRg9GVJ7KDjSLN3e3LwH5rgv5q\n" + "pU+nM/idSMhG0CQ1lZeExTsMEJVT3bG7LoU5uJ2fJSf5+hA0oih2M7/Kap5ggHgF\n" + "h+h8MWvDC9Ih8x1aadkk/OEmJsTrziYm0C/V/FXPHEuXfZn8uDNKZ/tbyfI6hwEj\n" + "nLz5Zjgg29n6tIPYMrnLNDHScCwtNZOcnixmWzsxCt1bxsAEA/y9gXUT7xWUf52t\n" + "2+DGQbLYxo0PHjnPf3YnFXNavfTt+4c7ZdHhOQ6ZA8FGQ2LJHDHM1r2/8lK4ld2V\n" + "qgNTcQIDAQABo1cwVTATBgNVHSUEDDAKBggrBgEFBQcDAjA+BgNVHREENzA1oDMG\n" + "CisGAQQBgjcUAgOgJQwjY2xvdWRiYXNlLWluaXQtdXNlci0xMzU1OUBsb2NhbGhv\n" + "c3QwDQYJKoZIhvcNAQELBQADggEBAHHX/ZUOMR0ZggQnfXuXLIHWlffVxxLOV/bE\n" + "7JC/dtedHqi9iw6sRT5R6G1pJo0xKWr2yJVDH6nC7pfxCFkby0WgVuTjiu6iNRg2\n" + "4zNJd8TGrTU+Mst+PPJFgsxrAY6vjwiaUtvZ/k8PsphHXu4ON+oLurtVDVgog7Vm\n" + "fQCShx434OeJj1u8pb7o2WyYS5nDVrHBhlCAqVf2JPKu9zY+i9gOG2kimJwH7fJD\n" + "xXpMIwAQ+flwlHR7OrE0L8TNcWwKPRAY4EPcXrT+cWo1k6aTqZDSK54ygW2iWtni\n" + "ZBcstxwcB4GIwnp1DrPW9L2gw5eLe1Sl6wdz443TW8K/KPV9rWQ=\n" + "-----END CERTIFICATE-----\n") + return certif, fingerprint + + +def get_ssh_pub_key_and_fingerprint(): + fingerprint = "1e:2c:9b:56:79:4b:45:77:f9:ca:7a:98:2c:b0:d5:3c" + public_key = ("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGg" + "B4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0l" + "RE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv" + "9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYc" + "pSxsIbECHw== Generated-by-Nova") + return public_key, fingerprint diff --git a/novaclient/tests/functional/test_keypairs.py b/novaclient/tests/functional/test_keypairs.py new file mode 100644 index 00000000..02bf3ecd --- /dev/null +++ b/novaclient/tests/functional/test_keypairs.py @@ -0,0 +1,125 @@ +# 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. + +import tempfile +import uuid + +from tempest_lib import exceptions + +from novaclient.tests.functional import base +from novaclient.tests.functional import fake_crypto + + +class TestKeypairsNovaClient(base.ClientTestBase): + """Keypairs functional tests. + """ + + def _serialize_kwargs(self, kwargs): + kwargs_pairs = ['--%(key)s %(val)s' % {'key': key.replace('_', '-'), + 'val': val} + for key, val in kwargs.items()] + return " ".join(kwargs_pairs) + + def _create_keypair(self, **kwargs): + key_name = self._raw_create_keypair(**kwargs) + self.addCleanup(self.nova, 'keypair-delete %s' % key_name) + return key_name + + def _raw_create_keypair(self, **kwargs): + key_name = 'keypair-' + str(uuid.uuid4()) + kwargs_str = self._serialize_kwargs(kwargs) + self.nova('keypair-add %s %s' % (kwargs_str, key_name)) + return key_name + + def _show_keypair(self, key_name): + return self.nova('keypair-show %s' % key_name) + + def _list_keypairs(self): + return self.nova('keypair-list') + + def _delete_keypair(self, key_name): + self.nova('keypair-delete %s' % key_name) + + def _create_public_key_file(self, public_key): + pubfile = tempfile.mkstemp()[1] + with open(pubfile, 'w') as f: + f.write(public_key) + return pubfile + + def test_create_keypair(self): + key_name = self._create_keypair() + keypair = self._show_keypair(key_name) + self.assertIn(key_name, keypair) + + return keypair + + def _test_import_keypair(self, fingerprint, **create_kwargs): + key_name = self._create_keypair(**create_kwargs) + keypair = self._show_keypair(key_name) + self.assertIn(key_name, keypair) + self.assertIn(fingerprint, keypair) + + return keypair + + def test_import_keypair(self): + pub_key, fingerprint = fake_crypto.get_ssh_pub_key_and_fingerprint() + pub_key_file = self._create_public_key_file(pub_key) + self._test_import_keypair(fingerprint, pub_key=pub_key_file) + + def test_list_keypair(self): + key_name = self._create_keypair() + keypairs = self._list_keypairs() + self.assertIn(key_name, keypairs) + + def test_delete_keypair(self): + key_name = self._raw_create_keypair() + keypair = self._show_keypair(key_name) + self.assertIsNotNone(keypair) + + self._delete_keypair(key_name) + + # keypair-show should fail if no keypair with given name is found. + self.assertRaises(exceptions.CommandFailed, + self._show_keypair, key_name) + + +class TestKeypairsNovaClientV22(TestKeypairsNovaClient): + """Keypairs functional tests for v2.2 nova-api microversion. + """ + + def nova(self, *args, **kwargs): + return self.cli_clients.nova(flags='--os-compute-api-version 2.2 ' + '--service-type computev21', + *args, **kwargs) + + def test_create_keypair(self): + keypair = super(TestKeypairsNovaClientV22, self).test_create_keypair() + self.assertIn('ssh', keypair) + + def test_create_keypair_x509(self): + key_name = self._create_keypair(key_type='x509') + keypair = self._show_keypair(key_name) + self.assertIn(key_name, keypair) + self.assertIn('x509', keypair) + + def test_import_keypair(self): + pub_key, fingerprint = fake_crypto.get_ssh_pub_key_and_fingerprint() + pub_key_file = self._create_public_key_file(pub_key) + keypair = self._test_import_keypair(fingerprint, pub_key=pub_key_file) + self.assertIn('ssh', keypair) + + def test_import_keypair_x509(self): + certif, fingerprint = fake_crypto.get_x509_cert_and_fingerprint() + pub_key_file = self._create_public_key_file(certif) + keypair = self._test_import_keypair(fingerprint, key_type='x509', + pub_key=pub_key_file) + self.assertIn('x509', keypair) diff --git a/novaclient/tests/unit/v2/test_keypairs.py b/novaclient/tests/unit/v2/test_keypairs.py index ac3602f6..109d350a 100644 --- a/novaclient/tests/unit/v2/test_keypairs.py +++ b/novaclient/tests/unit/v2/test_keypairs.py @@ -11,6 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import keypairs as data from novaclient.tests.unit import utils @@ -54,12 +55,49 @@ class KeypairsTest(utils.FixturedTestCase): self.cs.keypairs.delete(kp) self.assert_called('DELETE', '/%s/test' % self.keypair_prefix) + +class KeypairsV2TestCase(KeypairsTest): + def setUp(self): + super(KeypairsV2TestCase, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.0") + + def test_create_keypair(self): + name = "foo" + kp = self.cs.keypairs.create(name) + self.assert_called('POST', '/%s' % self.keypair_prefix, + body={'keypair': {'name': name}}) + self.assertIsInstance(kp, keypairs.Keypair) + + def test_import_keypair(self): + name = "foo" + pub_key = "fake-public-key" + kp = self.cs.keypairs.create(name, pub_key) + self.assert_called('POST', '/%s' % self.keypair_prefix, + body={'keypair': {'name': name, + 'public_key': pub_key}}) + self.assertIsInstance(kp, keypairs.Keypair) + + +class KeypairsV22TestCase(KeypairsTest): + def setUp(self): + super(KeypairsV22TestCase, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.2") + def test_create_keypair(self): - kp = self.cs.keypairs.create("foo") - self.assert_called('POST', '/%s' % self.keypair_prefix) + name = "foo" + key_type = "some_type" + kp = self.cs.keypairs.create(name, key_type=key_type) + self.assert_called('POST', '/%s' % self.keypair_prefix, + body={'keypair': {'name': name, + 'type': key_type}}) self.assertIsInstance(kp, keypairs.Keypair) def test_import_keypair(self): - kp = self.cs.keypairs.create("foo", "fake-public-key") - self.assert_called('POST', '/%s' % self.keypair_prefix) + name = "foo" + pub_key = "fake-public-key" + kp = self.cs.keypairs.create(name, pub_key) + self.assert_called('POST', '/%s' % self.keypair_prefix, + body={'keypair': {'name': name, + 'public_key': pub_key, + 'type': 'ssh'}}) self.assertIsInstance(kp, keypairs.Keypair) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 1a540ee2..4d83464e 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -75,11 +75,15 @@ class ShellTest(utils.TestCase): @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) - def run_command(self, cmd, mock_stderr, mock_stdout): + def run_command(self, cmd, mock_stderr, mock_stdout, api_version=None): + version_options = [] + if api_version: + version_options.extend(["--os-compute-api-version", api_version, + "--service-type", "computev21"]) if isinstance(cmd, list): - self.shell.main(cmd) + self.shell.main(version_options + cmd) else: - self.shell.main(cmd.split()) + self.shell.main(version_options + cmd.split()) return mock_stdout.getvalue(), mock_stderr.getvalue() def assert_called(self, method, url, body=None, **kwargs): @@ -2361,28 +2365,61 @@ class ShellTest(utils.TestCase): self.run_command, "ssh --ipv6 --network nonexistent server") - def test_keypair_add(self): - self.run_command('keypair-add test') - self.assert_called('POST', '/os-keypairs', - {'keypair': - {'name': 'test'}}) + def _check_keypair_add(self, expected_key_type=None, extra_args='', + api_version=None): + self.run_command("keypair-add %s test" % extra_args, + api_version=api_version) + expected_body = {"keypair": {"name": "test"}} + if expected_key_type: + expected_body["keypair"]["type"] = expected_key_type + self.assert_called("POST", "/os-keypairs", expected_body) - def test_keypair_import(self): + def test_keypair_add_v20(self): + self._check_keypair_add(api_version="2.0") + + def test_keypair_add_v22(self): + self._check_keypair_add('ssh', api_version="2.2") + + def test_keypair_add_ssh(self): + self._check_keypair_add('ssh', '--key-type ssh', api_version="2.2") + + def test_keypair_add_ssh_x509(self): + self._check_keypair_add('x509', '--key-type x509', api_version="2.2") + + def _check_keypair_import(self, expected_key_type=None, extra_args='', + api_version=None): with mock.patch.object(builtins, 'open', mock.mock_open(read_data='FAKE_PUBLIC_KEY')): - self.run_command('keypair-add --pub-key test.pub test') + self.run_command('keypair-add --pub-key test.pub %s test' % + extra_args, api_version=api_version) + expected_body = {"keypair": {'public_key': 'FAKE_PUBLIC_KEY', + 'name': 'test'}} + if expected_key_type: + expected_body["keypair"]["type"] = expected_key_type self.assert_called( - 'POST', '/os-keypairs', { - 'keypair': {'public_key': 'FAKE_PUBLIC_KEY', - 'name': 'test'}}) + 'POST', '/os-keypairs', expected_body) + + def test_keypair_import_v20(self): + self._check_keypair_import(api_version="2.0") + + def test_keypair_import_v22(self): + self._check_keypair_import('ssh', api_version="2.2") + + def test_keypair_import_ssh(self): + self._check_keypair_import('ssh', '--key-type ssh', api_version="2.2") + + def test_keypair_import_x509(self): + self._check_keypair_import('x509', '--key-type x509', + api_version="2.2") def test_keypair_stdin(self): with mock.patch('sys.stdin', six.StringIO('FAKE_PUBLIC_KEY')): - self.run_command('keypair-add --pub-key - test') + self.run_command('keypair-add --pub-key - test', api_version="2.2") self.assert_called( 'POST', '/os-keypairs', { 'keypair': - {'public_key': 'FAKE_PUBLIC_KEY', 'name': 'test'}}) + {'public_key': 'FAKE_PUBLIC_KEY', 'name': 'test', + 'type': 'ssh'}}) def test_keypair_list(self): self.run_command('keypair-list') diff --git a/novaclient/v2/keypairs.py b/novaclient/v2/keypairs.py index 96caff61..02196051 100644 --- a/novaclient/v2/keypairs.py +++ b/novaclient/v2/keypairs.py @@ -17,6 +17,7 @@ Keypair interface (1.1 extension). """ +from novaclient import api_versions from novaclient import base @@ -65,6 +66,7 @@ class KeypairManager(base.ManagerWithFind): return self._get("/%s/%s" % (self.keypair_prefix, base.getid(keypair)), "keypair") + @api_versions.wraps("2.0", "2.1") def create(self, name, public_key=None): """ Create a keypair @@ -77,6 +79,21 @@ class KeypairManager(base.ManagerWithFind): body['keypair']['public_key'] = public_key return self._create('/%s' % self.keypair_prefix, body, 'keypair') + @api_versions.wraps("2.2") + def create(self, name, public_key=None, key_type="ssh"): + """ + Create a keypair + + :param name: name for the keypair to create + :param public_key: existing public key to import + :param key_type: keypair type to create + """ + body = {'keypair': {'name': name, + 'type': key_type}} + if public_key: + body['keypair']['public_key'] = public_key + return self._create('/%s' % self.keypair_prefix, body, 'keypair') + def delete(self, key): """ Delete a keypair diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index c8ef86ba..a2552655 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -34,6 +34,7 @@ from oslo_utils import timeutils from oslo_utils import uuidutils import six +from novaclient import api_versions from novaclient import client from novaclient import exceptions from novaclient.i18n import _ @@ -2874,6 +2875,16 @@ def do_secgroup_delete_group_rule(cs, args): raise exceptions.CommandError(_("Rule not found")) +@api_versions.wraps("2.0", "2.1") +def _keypair_create(cs, args, name, pub_key): + return cs.keypairs.create(name, pub_key) + + +@api_versions.wraps("2.2") +def _keypair_create(cs, args, name, pub_key): + return cs.keypairs.create(name, pub_key, key_type=args.key_type) + + @cliutils.arg('name', metavar='<name>', help=_('Name of key.')) @cliutils.arg( '--pub-key', @@ -2883,11 +2894,16 @@ def do_secgroup_delete_group_rule(cs, args): @cliutils.arg( '--pub_key', help=argparse.SUPPRESS) +@cliutils.arg( + '--key-type', + metavar='<key-type>', + default='ssh', + help=_('Keypair type. Can be ssh or x509.'), + start_version="2.2") def do_keypair_add(cs, args): """Create a new key pair for use with servers.""" name = args.name pub_key = args.pub_key - if pub_key: if pub_key == '-': pub_key = sys.stdin.read() @@ -2901,7 +2917,7 @@ def do_keypair_add(cs, args): % {'key': pub_key, 'exc': e} ) - keypair = cs.keypairs.create(name, pub_key) + keypair = _keypair_create(cs, args, name, pub_key) if not pub_key: private_key = keypair.private_key @@ -2915,10 +2931,20 @@ def do_keypair_delete(cs, args): cs.keypairs.delete(name) +@api_versions.wraps("2.0", "2.1") +def _get_keypairs_list_columns(cs, args): + return ['Name', 'Fingerprint'] + + +@api_versions.wraps("2.2") +def _get_keypairs_list_columns(cs, args): + return ['Name', 'Type', 'Fingerprint'] + + def do_keypair_list(cs, args): """Print a list of keypairs for a user""" keypairs = cs.keypairs.list() - columns = ['Name', 'Fingerprint'] + columns = _get_keypairs_list_columns(cs, args) utils.print_list(keypairs, columns) |