Merge "Adds x509 certificate keypair support"
This commit is contained in:
commit
b41b4e08ac
|
@ -35,6 +35,7 @@ from nova import context
|
|||
from nova import network
|
||||
from nova import objects
|
||||
from nova.objects import base as obj_base
|
||||
from nova.objects import keypair as keypair_obj
|
||||
from nova import utils
|
||||
from nova.virt import netutils
|
||||
|
||||
|
@ -309,6 +310,16 @@ class InstanceMetadata(object):
|
|||
metadata['public_keys'] = {
|
||||
self.instance.key_name: self.instance.key_data
|
||||
}
|
||||
|
||||
keypair = keypair_obj.KeyPair.get_by_name(
|
||||
context.get_admin_context(), self.instance.user_id,
|
||||
self.instance.key_name)
|
||||
metadata['keys'] = [
|
||||
{'name': keypair.name,
|
||||
'type': keypair.type,
|
||||
'data': keypair.public_key}
|
||||
]
|
||||
|
||||
metadata['hostname'] = self._get_hostname()
|
||||
metadata['name'] = self.instance.display_name
|
||||
metadata['launch_index'] = self.instance.launch_index
|
||||
|
|
|
@ -3731,7 +3731,8 @@ class KeypairAPI(base.Base):
|
|||
notify.info(context, 'keypair.%s' % event_suffix, payload)
|
||||
|
||||
def _validate_new_key_pair(self, context, user_id, key_name, key_type):
|
||||
if key_type is not keypair_obj.KEYPAIR_TYPE_SSH:
|
||||
if key_type not in [keypair_obj.KEYPAIR_TYPE_SSH,
|
||||
keypair_obj.KEYPAIR_TYPE_X509]:
|
||||
raise exception.InvalidKeypair(
|
||||
reason=_('Specified Keypair type "%s" is invalid') % key_type)
|
||||
|
||||
|
@ -3763,7 +3764,7 @@ class KeypairAPI(base.Base):
|
|||
|
||||
self._notify(context, 'import.start', key_name)
|
||||
|
||||
fingerprint = crypto.generate_fingerprint(public_key)
|
||||
fingerprint = self._generate_fingerprint(public_key, key_type)
|
||||
|
||||
keypair = objects.KeyPair(context)
|
||||
keypair.user_id = user_id
|
||||
|
@ -3785,7 +3786,8 @@ class KeypairAPI(base.Base):
|
|||
|
||||
self._notify(context, 'create.start', key_name)
|
||||
|
||||
private_key, public_key, fingerprint = crypto.generate_key_pair()
|
||||
private_key, public_key, fingerprint = self._generate_key_pair(
|
||||
context, user_id, key_type)
|
||||
|
||||
keypair = objects.KeyPair(context)
|
||||
keypair.user_id = user_id
|
||||
|
@ -3799,6 +3801,18 @@ class KeypairAPI(base.Base):
|
|||
|
||||
return keypair, private_key
|
||||
|
||||
def _generate_fingerprint(self, public_key, key_type):
|
||||
if key_type == keypair_obj.KEYPAIR_TYPE_SSH:
|
||||
return crypto.generate_fingerprint(public_key)
|
||||
elif key_type == keypair_obj.KEYPAIR_TYPE_X509:
|
||||
return crypto.generate_x509_fingerprint(public_key)
|
||||
|
||||
def _generate_key_pair(self, context, user_id, key_type):
|
||||
if key_type == keypair_obj.KEYPAIR_TYPE_SSH:
|
||||
return crypto.generate_key_pair()
|
||||
elif key_type == keypair_obj.KEYPAIR_TYPE_X509:
|
||||
return crypto.generate_winrm_x509_cert(user_id, context.project_id)
|
||||
|
||||
@wrap_exception()
|
||||
def delete_key_pair(self, context, user_id, key_name):
|
||||
"""Delete a keypair by name."""
|
||||
|
|
|
@ -144,6 +144,19 @@ def generate_fingerprint(public_key):
|
|||
reason=_('failed to generate fingerprint'))
|
||||
|
||||
|
||||
def generate_x509_fingerprint(pem_key):
|
||||
try:
|
||||
(out, _err) = utils.execute('openssl', 'x509', '-inform', 'PEM',
|
||||
'-fingerprint', '-noout',
|
||||
process_input=pem_key)
|
||||
fingerprint = string.strip(out.rpartition('=')[2])
|
||||
return fingerprint
|
||||
except processutils.ProcessExecutionError as ex:
|
||||
raise exception.InvalidKeypair(
|
||||
reason=_('failed to generate X509 fingerprint. '
|
||||
'Error message: %s') % ex)
|
||||
|
||||
|
||||
def generate_key_pair(bits=None):
|
||||
with utils.tempdir() as tmpdir:
|
||||
keyfile = os.path.join(tmpdir, 'temp')
|
||||
|
@ -352,6 +365,44 @@ def generate_x509_cert(user_id, project_id, bits=2048):
|
|||
return (private_key, signed_csr)
|
||||
|
||||
|
||||
def generate_winrm_x509_cert(user_id, project_id, bits=2048):
|
||||
"""Generate a cert for passwordless auth for user in project."""
|
||||
subject = '/CN=%s-%s' % (project_id, user_id)
|
||||
upn = '%s@localhost' % user_id
|
||||
|
||||
with utils.tempdir() as tmpdir:
|
||||
keyfile = os.path.abspath(os.path.join(tmpdir, 'temp.key'))
|
||||
conffile = os.path.abspath(os.path.join(tmpdir, 'temp.conf'))
|
||||
|
||||
_create_x509_openssl_config(conffile, upn)
|
||||
|
||||
(certificate, _err) = utils.execute(
|
||||
'openssl', 'req', '-x509', '-nodes', '-days', '3650',
|
||||
'-config', conffile, '-newkey', 'rsa:%s' % bits,
|
||||
'-outform', 'PEM', '-keyout', keyfile, '-subj', subject,
|
||||
'-extensions', 'v3_req_client')
|
||||
|
||||
(out, _err) = utils.execute('openssl', 'pkcs12', '-export',
|
||||
'-inkey', keyfile, '-password', 'pass:',
|
||||
process_input=certificate)
|
||||
|
||||
private_key = out.encode('base64')
|
||||
fingerprint = generate_x509_fingerprint(certificate)
|
||||
|
||||
return (private_key, certificate, fingerprint)
|
||||
|
||||
|
||||
def _create_x509_openssl_config(conffile, upn):
|
||||
content = ("distinguished_name = req_distinguished_name\n"
|
||||
"[req_distinguished_name]\n"
|
||||
"[v3_req_client]\n"
|
||||
"extendedKeyUsage = clientAuth\n"
|
||||
"subjectAltName = otherName:""1.3.6.1.4.1.311.20.2.3;UTF8:%s\n")
|
||||
|
||||
with open(conffile, 'w') as file:
|
||||
file.write(content % upn)
|
||||
|
||||
|
||||
def _ensure_project_folder(project_id):
|
||||
if not os.path.exists(ca_path(project_id)):
|
||||
geninter_sh_path = os.path.abspath(
|
||||
|
|
|
@ -20,6 +20,7 @@ from nova.objects import fields
|
|||
from nova import utils
|
||||
|
||||
KEYPAIR_TYPE_SSH = 'ssh'
|
||||
KEYPAIR_TYPE_X509 = 'x509'
|
||||
|
||||
|
||||
# TODO(berrange): Remove NovaObjectDictCompat
|
||||
|
|
|
@ -123,15 +123,15 @@ def wsgi_app_v21(inner_app_v21=None, fake_auth_context=None,
|
|||
return mapper
|
||||
|
||||
|
||||
def stub_out_key_pair_funcs(stubs, have_key_pair=True):
|
||||
def stub_out_key_pair_funcs(stubs, have_key_pair=True, **kwargs):
|
||||
def key_pair(context, user_id):
|
||||
return [dict(test_keypair.fake_keypair,
|
||||
name='key', public_key='public_key')]
|
||||
name='key', public_key='public_key', **kwargs)]
|
||||
|
||||
def one_key_pair(context, user_id, name):
|
||||
if name == 'key':
|
||||
return dict(test_keypair.fake_keypair,
|
||||
name='key', public_key='public_key')
|
||||
name='key', public_key='public_key', **kwargs)
|
||||
else:
|
||||
raise exc.KeypairNotFound(user_id=user_id, name=name)
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ from nova import exception
|
|||
from nova.objects import keypair as keypair_obj
|
||||
from nova import quota
|
||||
from nova.tests.unit.compute import test_compute
|
||||
from nova.tests.unit import fake_crypto
|
||||
from nova.tests.unit import fake_notifier
|
||||
from nova.tests.unit.objects import test_keypair
|
||||
|
||||
|
@ -169,29 +170,48 @@ class CreateImportSharedTestMixIn(object):
|
|||
class CreateKeypairTestCase(KeypairAPITestCase, CreateImportSharedTestMixIn):
|
||||
func_name = 'create_key_pair'
|
||||
|
||||
def test_success(self):
|
||||
def _check_success(self):
|
||||
keypair, private_key = self.keypair_api.create_key_pair(
|
||||
self.ctxt, self.ctxt.user_id, 'foo')
|
||||
self.ctxt, self.ctxt.user_id, 'foo', key_type=self.keypair_type)
|
||||
self.assertEqual('foo', keypair['name'])
|
||||
self.assertEqual(self.keypair_type, keypair['type'])
|
||||
self._check_notifications()
|
||||
|
||||
def test_success_ssh(self):
|
||||
self._check_success()
|
||||
|
||||
def test_success_x509(self):
|
||||
self.keypair_type = keypair_obj.KEYPAIR_TYPE_X509
|
||||
self._check_success()
|
||||
|
||||
|
||||
class ImportKeypairTestCase(KeypairAPITestCase, CreateImportSharedTestMixIn):
|
||||
func_name = 'import_key_pair'
|
||||
|
||||
def test_success(self):
|
||||
def _check_success(self):
|
||||
keypair = self.keypair_api.import_key_pair(self.ctxt,
|
||||
self.ctxt.user_id,
|
||||
'foo',
|
||||
self.pub_key)
|
||||
self.pub_key,
|
||||
self.keypair_type)
|
||||
|
||||
self.assertEqual('foo', keypair['name'])
|
||||
self.assertEqual(self.keypair_type, keypair['type'])
|
||||
self.assertEqual(self.fingerprint, keypair['fingerprint'])
|
||||
self.assertEqual(self.pub_key, keypair['public_key'])
|
||||
self.assertEqual(self.keypair_type, keypair['type'])
|
||||
self._check_notifications(action='import')
|
||||
|
||||
def test_success_ssh(self):
|
||||
self._check_success()
|
||||
|
||||
def test_success_x509(self):
|
||||
self.keypair_type = keypair_obj.KEYPAIR_TYPE_X509
|
||||
certif, fingerprint = fake_crypto.get_x509_cert_and_fingerprint()
|
||||
self.pub_key = certif
|
||||
self.fingerprint = fingerprint
|
||||
self._check_success()
|
||||
|
||||
def test_bad_key_data(self):
|
||||
exc = self.assertRaises(exception.InvalidKeypair,
|
||||
self.keypair_api.import_key_pair,
|
||||
|
|
|
@ -107,3 +107,28 @@ YZhQPOYoNPEOYru116HdHzjGDVifgWf/nDL8Un5tjJFDSf7jSLtA
|
|||
-----END CERTIFICATE-----
|
||||
"""
|
||||
return pk, csr
|
||||
|
||||
|
||||
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
|
||||
|
|
|
@ -45,6 +45,7 @@ from nova.network import api as network_api
|
|||
from nova.network import model as network_model
|
||||
from nova import objects
|
||||
from nova import test
|
||||
from nova.tests.unit.api.openstack import fakes
|
||||
from nova.tests.unit import fake_block_device
|
||||
from nova.tests.unit import fake_network
|
||||
from nova.tests.unit.objects import test_security_group
|
||||
|
@ -60,9 +61,10 @@ def fake_inst_obj(context):
|
|||
inst = objects.Instance(
|
||||
context=context,
|
||||
id=1,
|
||||
user_id='fake_user',
|
||||
uuid='b65cee2f-8c69-4aeb-be2f-f79742548fc2',
|
||||
project_id='test',
|
||||
key_name="mykey",
|
||||
key_name="key",
|
||||
key_data="ssh-rsa AAAAB3Nzai....N3NtHw== someuser@somehost",
|
||||
host='test',
|
||||
launch_index=1,
|
||||
|
@ -300,6 +302,7 @@ class MetadataTestCase(test.TestCase):
|
|||
network_info=network_info)
|
||||
|
||||
def test_InstanceMetadata_invoke_metadata_for_config_drive(self):
|
||||
fakes.stub_out_key_pair_funcs(self.stubs)
|
||||
inst = self.instance.obj_clone()
|
||||
inst_md = base.InstanceMetadata(inst)
|
||||
for (path, value) in inst_md.metadata_for_config_drive():
|
||||
|
@ -396,6 +399,7 @@ class OpenStackMetadataTestCase(test.TestCase):
|
|||
grizzly_supported_apis)
|
||||
|
||||
def test_metadata_json(self):
|
||||
fakes.stub_out_key_pair_funcs(self.stubs)
|
||||
inst = self.instance.obj_clone()
|
||||
content = [
|
||||
('/etc/my.conf', "content of my.conf"),
|
||||
|
@ -429,8 +433,25 @@ class OpenStackMetadataTestCase(test.TestCase):
|
|||
found = mdinst.lookup("/openstack%s" % fent['content_path'])
|
||||
self.assertEqual(found, content)
|
||||
|
||||
def test_x509_keypair(self):
|
||||
# check if the x509 content is set, if the keypair type is x509.
|
||||
fakes.stub_out_key_pair_funcs(self.stubs, type='x509')
|
||||
inst = self.instance.obj_clone()
|
||||
mdinst = fake_InstanceMetadata(self.stubs, inst)
|
||||
|
||||
mdjson = mdinst.lookup("/openstack/2012-08-10/meta_data.json")
|
||||
mddict = jsonutils.loads(mdjson)
|
||||
|
||||
# keypair is stubbed-out, so it's public_key is 'public_key'.
|
||||
expected = {'name': self.instance['key_name'],
|
||||
'type': 'x509',
|
||||
'data': 'public_key'}
|
||||
|
||||
self.assertEqual([expected], mddict['keys'])
|
||||
|
||||
def test_extra_md(self):
|
||||
# make sure extra_md makes it through to metadata
|
||||
fakes.stub_out_key_pair_funcs(self.stubs)
|
||||
inst = self.instance.obj_clone()
|
||||
extra = {'foo': 'bar', 'mylist': [1, 2, 3],
|
||||
'mydict': {"one": 1, "two": 2}}
|
||||
|
@ -470,6 +491,7 @@ class OpenStackMetadataTestCase(test.TestCase):
|
|||
mdinst.lookup, "/openstack/2012-08-10/user_data")
|
||||
|
||||
def test_random_seed(self):
|
||||
fakes.stub_out_key_pair_funcs(self.stubs)
|
||||
inst = self.instance.obj_clone()
|
||||
mdinst = fake_InstanceMetadata(self.stubs, inst)
|
||||
|
||||
|
@ -486,6 +508,7 @@ class OpenStackMetadataTestCase(test.TestCase):
|
|||
|
||||
def test_no_dashes_in_metadata(self):
|
||||
# top level entries in meta_data should not contain '-' in their name
|
||||
fakes.stub_out_key_pair_funcs(self.stubs)
|
||||
inst = self.instance.obj_clone()
|
||||
mdinst = fake_InstanceMetadata(self.stubs, inst)
|
||||
mdjson = jsonutils.loads(
|
||||
|
@ -592,6 +615,7 @@ class MetadataHandlerTestCase(test.TestCase):
|
|||
self.assertEqual(response.status_int, 404)
|
||||
|
||||
def test_json_data(self):
|
||||
fakes.stub_out_key_pair_funcs(self.stubs)
|
||||
response = fake_request(self.stubs, self.mdinst,
|
||||
"/openstack/latest/meta_data.json")
|
||||
response_ctype = response.headers['Content-Type']
|
||||
|
|
Loading…
Reference in New Issue