Merge "compute: Generate SSH keypairs ourselves"
This commit is contained in:
commit
43f0e1090a
@ -15,11 +15,13 @@
|
|||||||
|
|
||||||
"""Keypair action implementations"""
|
"""Keypair action implementations"""
|
||||||
|
|
||||||
|
import collections
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
|
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
from openstack import utils as sdk_utils
|
from openstack import utils as sdk_utils
|
||||||
from osc_lib.command import command
|
from osc_lib.command import command
|
||||||
from osc_lib import exceptions
|
from osc_lib import exceptions
|
||||||
@ -30,6 +32,27 @@ from openstackclient.identity import common as identity_common
|
|||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
Keypair = collections.namedtuple('Keypair', 'private_key public_key')
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_keypair():
|
||||||
|
"""Generate a Ed25519 keypair in OpenSSH format.
|
||||||
|
|
||||||
|
:returns: A `Keypair` named tuple with the generated private and public
|
||||||
|
keys.
|
||||||
|
"""
|
||||||
|
key = ed25519.Ed25519PrivateKey.generate()
|
||||||
|
private_key = key.private_bytes(
|
||||||
|
serialization.Encoding.PEM,
|
||||||
|
serialization.PrivateFormat.OpenSSH,
|
||||||
|
serialization.NoEncryption()
|
||||||
|
).decode()
|
||||||
|
public_key = key.public_key().public_bytes(
|
||||||
|
serialization.Encoding.OpenSSH,
|
||||||
|
serialization.PublicFormat.OpenSSH
|
||||||
|
).decode()
|
||||||
|
|
||||||
|
return Keypair(private_key, public_key)
|
||||||
|
|
||||||
|
|
||||||
def _get_keypair_columns(item, hide_pub_key=False, hide_priv_key=False):
|
def _get_keypair_columns(item, hide_pub_key=False, hide_priv_key=False):
|
||||||
@ -59,30 +82,37 @@ class CreateKeypair(command.ShowOne):
|
|||||||
key_group.add_argument(
|
key_group.add_argument(
|
||||||
'--public-key',
|
'--public-key',
|
||||||
metavar='<file>',
|
metavar='<file>',
|
||||||
help=_("Filename for public key to add. If not used, "
|
help=_(
|
||||||
"creates a private key.")
|
"Filename for public key to add. "
|
||||||
|
"If not used, generates a private key in ssh-ed25519 format. "
|
||||||
|
"To generate keys in other formats, including the legacy "
|
||||||
|
"ssh-rsa format, you must use an external tool such as "
|
||||||
|
"ssh-keygen and specify this argument."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
key_group.add_argument(
|
key_group.add_argument(
|
||||||
'--private-key',
|
'--private-key',
|
||||||
metavar='<file>',
|
metavar='<file>',
|
||||||
help=_("Filename for private key to save. If not used, "
|
help=_(
|
||||||
"print private key in console.")
|
"Filename for private key to save. "
|
||||||
|
"If not used, print private key in console."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--type',
|
'--type',
|
||||||
metavar='<type>',
|
metavar='<type>',
|
||||||
choices=['ssh', 'x509'],
|
choices=['ssh', 'x509'],
|
||||||
help=_(
|
help=_(
|
||||||
"Keypair type. Can be ssh or x509. "
|
'Keypair type '
|
||||||
"(Supported by API versions '2.2' - '2.latest')"
|
'(supported by --os-compute-api-version 2.2 or above)'
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--user',
|
'--user',
|
||||||
metavar='<user>',
|
metavar='<user>',
|
||||||
help=_(
|
help=_(
|
||||||
'The owner of the keypair. (admin only) (name or ID). '
|
'The owner of the keypair (admin only) (name or ID) '
|
||||||
'Requires ``--os-compute-api-version`` 2.10 or greater.'
|
'(supported by --os-compute-api-version 2.10 or above)'
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
identity_common.add_user_domain_option_to_parser(parser)
|
identity_common.add_user_domain_option_to_parser(parser)
|
||||||
@ -96,19 +126,43 @@ class CreateKeypair(command.ShowOne):
|
|||||||
'name': parsed_args.name
|
'name': parsed_args.name
|
||||||
}
|
}
|
||||||
|
|
||||||
public_key = parsed_args.public_key
|
if parsed_args.public_key:
|
||||||
if public_key:
|
generated_keypair = None
|
||||||
try:
|
try:
|
||||||
with io.open(os.path.expanduser(parsed_args.public_key)) as p:
|
with io.open(os.path.expanduser(parsed_args.public_key)) as p:
|
||||||
public_key = p.read()
|
public_key = p.read()
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
msg = _("Key file %(public_key)s not found: %(exception)s")
|
msg = _("Key file %(public_key)s not found: %(exception)s")
|
||||||
raise exceptions.CommandError(
|
raise exceptions.CommandError(
|
||||||
msg % {"public_key": parsed_args.public_key,
|
msg % {
|
||||||
"exception": e}
|
"public_key": parsed_args.public_key,
|
||||||
|
"exception": e,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
kwargs['public_key'] = public_key
|
kwargs['public_key'] = public_key
|
||||||
|
else:
|
||||||
|
generated_keypair = _generate_keypair()
|
||||||
|
kwargs['public_key'] = generated_keypair.public_key
|
||||||
|
|
||||||
|
# If user have us a file, save private key into specified file
|
||||||
|
if parsed_args.private_key:
|
||||||
|
try:
|
||||||
|
with io.open(
|
||||||
|
os.path.expanduser(parsed_args.private_key), 'w+'
|
||||||
|
) as p:
|
||||||
|
p.write(generated_keypair.private_key)
|
||||||
|
except IOError as e:
|
||||||
|
msg = _(
|
||||||
|
"Key file %(private_key)s can not be saved: "
|
||||||
|
"%(exception)s"
|
||||||
|
)
|
||||||
|
raise exceptions.CommandError(
|
||||||
|
msg % {
|
||||||
|
"private_key": parsed_args.private_key,
|
||||||
|
"exception": e,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if parsed_args.type:
|
if parsed_args.type:
|
||||||
if not sdk_utils.supports_microversion(compute_client, '2.2'):
|
if not sdk_utils.supports_microversion(compute_client, '2.2'):
|
||||||
@ -136,32 +190,17 @@ class CreateKeypair(command.ShowOne):
|
|||||||
|
|
||||||
keypair = compute_client.create_keypair(**kwargs)
|
keypair = compute_client.create_keypair(**kwargs)
|
||||||
|
|
||||||
private_key = parsed_args.private_key
|
|
||||||
# Save private key into specified file
|
|
||||||
if private_key:
|
|
||||||
try:
|
|
||||||
with io.open(
|
|
||||||
os.path.expanduser(parsed_args.private_key), 'w+'
|
|
||||||
) as p:
|
|
||||||
p.write(keypair.private_key)
|
|
||||||
except IOError as e:
|
|
||||||
msg = _("Key file %(private_key)s can not be saved: "
|
|
||||||
"%(exception)s")
|
|
||||||
raise exceptions.CommandError(
|
|
||||||
msg % {"private_key": parsed_args.private_key,
|
|
||||||
"exception": e}
|
|
||||||
)
|
|
||||||
# NOTE(dtroyer): how do we want to handle the display of the private
|
# NOTE(dtroyer): how do we want to handle the display of the private
|
||||||
# key when it needs to be communicated back to the user
|
# key when it needs to be communicated back to the user
|
||||||
# For now, duplicate nova keypair-add command output
|
# For now, duplicate nova keypair-add command output
|
||||||
if public_key or private_key:
|
if parsed_args.public_key or parsed_args.private_key:
|
||||||
display_columns, columns = _get_keypair_columns(
|
display_columns, columns = _get_keypair_columns(
|
||||||
keypair, hide_pub_key=True, hide_priv_key=True)
|
keypair, hide_pub_key=True, hide_priv_key=True)
|
||||||
data = utils.get_item_properties(keypair, columns)
|
data = utils.get_item_properties(keypair, columns)
|
||||||
|
|
||||||
return (display_columns, data)
|
return (display_columns, data)
|
||||||
else:
|
else:
|
||||||
sys.stdout.write(keypair.private_key)
|
self.app.stdout.write(generated_keypair.private_key)
|
||||||
return ({}, {})
|
return ({}, {})
|
||||||
|
|
||||||
|
|
||||||
@ -405,5 +444,5 @@ class ShowKeypair(command.ShowOne):
|
|||||||
data = utils.get_item_properties(keypair, columns)
|
data = utils.get_item_properties(keypair, columns)
|
||||||
return (display_columns, data)
|
return (display_columns, data)
|
||||||
else:
|
else:
|
||||||
sys.stdout.write(keypair.public_key)
|
self.app.stdout.write(keypair.public_key)
|
||||||
return ({}, {})
|
return ({}, {})
|
||||||
|
@ -117,24 +117,28 @@ class KeypairTests(KeypairBase):
|
|||||||
self.assertIsNotNone(cmd_output.get('user_id'))
|
self.assertIsNotNone(cmd_output.get('user_id'))
|
||||||
self.assertIsNotNone(cmd_output.get('fingerprint'))
|
self.assertIsNotNone(cmd_output.get('fingerprint'))
|
||||||
pk_content = f.read()
|
pk_content = f.read()
|
||||||
self.assertInOutput('-----BEGIN RSA PRIVATE KEY-----', pk_content)
|
self.assertInOutput(
|
||||||
|
'-----BEGIN OPENSSH PRIVATE KEY-----', pk_content,
|
||||||
|
)
|
||||||
self.assertRegex(pk_content, "[0-9A-Za-z+/]+[=]{0,3}\n")
|
self.assertRegex(pk_content, "[0-9A-Za-z+/]+[=]{0,3}\n")
|
||||||
self.assertInOutput('-----END RSA PRIVATE KEY-----', pk_content)
|
self.assertInOutput(
|
||||||
|
'-----END OPENSSH PRIVATE KEY-----', pk_content,
|
||||||
|
)
|
||||||
|
|
||||||
def test_keypair_create(self):
|
def test_keypair_create(self):
|
||||||
"""Test keypair create command.
|
"""Test keypair create command.
|
||||||
|
|
||||||
Test steps:
|
Test steps:
|
||||||
1) Create keypair in setUp
|
1) Create keypair in setUp
|
||||||
2) Check RSA private key in output
|
2) Check Ed25519 private key in output
|
||||||
3) Check for new keypair in keypairs list
|
3) Check for new keypair in keypairs list
|
||||||
"""
|
"""
|
||||||
NewName = data_utils.rand_name('TestKeyPairCreated')
|
NewName = data_utils.rand_name('TestKeyPairCreated')
|
||||||
raw_output = self.openstack('keypair create ' + NewName)
|
raw_output = self.openstack('keypair create ' + NewName)
|
||||||
self.addCleanup(self.openstack, 'keypair delete ' + NewName)
|
self.addCleanup(self.openstack, 'keypair delete ' + NewName)
|
||||||
self.assertInOutput('-----BEGIN RSA PRIVATE KEY-----', raw_output)
|
self.assertInOutput('-----BEGIN OPENSSH PRIVATE KEY-----', raw_output)
|
||||||
self.assertRegex(raw_output, "[0-9A-Za-z+/]+[=]{0,3}\n")
|
self.assertRegex(raw_output, "[0-9A-Za-z+/]+[=]{0,3}\n")
|
||||||
self.assertInOutput('-----END RSA PRIVATE KEY-----', raw_output)
|
self.assertInOutput('-----END OPENSSH PRIVATE KEY-----', raw_output)
|
||||||
self.assertIn(NewName, self.keypair_list())
|
self.assertIn(NewName, self.keypair_list())
|
||||||
|
|
||||||
def test_keypair_delete_not_existing(self):
|
def test_keypair_delete_not_existing(self):
|
||||||
|
@ -793,7 +793,7 @@ class FakeKeypair(object):
|
|||||||
"""Fake one or more keypairs."""
|
"""Fake one or more keypairs."""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_one_keypair(attrs=None, no_pri=False):
|
def create_one_keypair(attrs=None):
|
||||||
"""Create a fake keypair
|
"""Create a fake keypair
|
||||||
|
|
||||||
:param dict attrs:
|
:param dict attrs:
|
||||||
@ -811,8 +811,6 @@ class FakeKeypair(object):
|
|||||||
'public_key': 'dummy',
|
'public_key': 'dummy',
|
||||||
'user_id': 'user'
|
'user_id': 'user'
|
||||||
}
|
}
|
||||||
if not no_pri:
|
|
||||||
keypair_info['private_key'] = 'private_key'
|
|
||||||
|
|
||||||
# Overwrite default attributes.
|
# Overwrite default attributes.
|
||||||
keypair_info.update(attrs)
|
keypair_info.update(attrs)
|
||||||
|
@ -54,10 +54,10 @@ class TestKeypair(compute_fakes.TestComputev2):
|
|||||||
|
|
||||||
class TestKeypairCreate(TestKeypair):
|
class TestKeypairCreate(TestKeypair):
|
||||||
|
|
||||||
keypair = compute_fakes.FakeKeypair.create_one_keypair()
|
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestKeypairCreate, self).setUp()
|
super().setUp()
|
||||||
|
|
||||||
|
self.keypair = compute_fakes.FakeKeypair.create_one_keypair()
|
||||||
|
|
||||||
self.columns = (
|
self.columns = (
|
||||||
'fingerprint',
|
'fingerprint',
|
||||||
@ -77,8 +77,11 @@ class TestKeypairCreate(TestKeypair):
|
|||||||
|
|
||||||
self.sdk_client.create_keypair.return_value = self.keypair
|
self.sdk_client.create_keypair.return_value = self.keypair
|
||||||
|
|
||||||
def test_key_pair_create_no_options(self):
|
@mock.patch.object(
|
||||||
|
keypair, '_generate_keypair',
|
||||||
|
return_value=keypair.Keypair('private', 'public'),
|
||||||
|
)
|
||||||
|
def test_key_pair_create_no_options(self, mock_generate):
|
||||||
arglist = [
|
arglist = [
|
||||||
self.keypair.name,
|
self.keypair.name,
|
||||||
]
|
]
|
||||||
@ -90,18 +93,14 @@ class TestKeypairCreate(TestKeypair):
|
|||||||
columns, data = self.cmd.take_action(parsed_args)
|
columns, data = self.cmd.take_action(parsed_args)
|
||||||
|
|
||||||
self.sdk_client.create_keypair.assert_called_with(
|
self.sdk_client.create_keypair.assert_called_with(
|
||||||
name=self.keypair.name
|
name=self.keypair.name,
|
||||||
|
public_key=mock_generate.return_value.public_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual({}, columns)
|
self.assertEqual({}, columns)
|
||||||
self.assertEqual({}, data)
|
self.assertEqual({}, data)
|
||||||
|
|
||||||
def test_keypair_create_public_key(self):
|
def test_keypair_create_public_key(self):
|
||||||
# overwrite the setup one because we want to omit private_key
|
|
||||||
self.keypair = compute_fakes.FakeKeypair.create_one_keypair(
|
|
||||||
no_pri=True)
|
|
||||||
self.sdk_client.create_keypair.return_value = self.keypair
|
|
||||||
|
|
||||||
self.data = (
|
self.data = (
|
||||||
self.keypair.fingerprint,
|
self.keypair.fingerprint,
|
||||||
self.keypair.name,
|
self.keypair.name,
|
||||||
@ -135,7 +134,11 @@ class TestKeypairCreate(TestKeypair):
|
|||||||
self.assertEqual(self.columns, columns)
|
self.assertEqual(self.columns, columns)
|
||||||
self.assertEqual(self.data, data)
|
self.assertEqual(self.data, data)
|
||||||
|
|
||||||
def test_keypair_create_private_key(self):
|
@mock.patch.object(
|
||||||
|
keypair, '_generate_keypair',
|
||||||
|
return_value=keypair.Keypair('private', 'public'),
|
||||||
|
)
|
||||||
|
def test_keypair_create_private_key(self, mock_generate):
|
||||||
tmp_pk_file = '/tmp/kp-file-' + uuid.uuid4().hex
|
tmp_pk_file = '/tmp/kp-file-' + uuid.uuid4().hex
|
||||||
arglist = [
|
arglist = [
|
||||||
'--private-key', tmp_pk_file,
|
'--private-key', tmp_pk_file,
|
||||||
@ -156,10 +159,13 @@ class TestKeypairCreate(TestKeypair):
|
|||||||
|
|
||||||
self.sdk_client.create_keypair.assert_called_with(
|
self.sdk_client.create_keypair.assert_called_with(
|
||||||
name=self.keypair.name,
|
name=self.keypair.name,
|
||||||
|
public_key=mock_generate.return_value.public_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_open.assert_called_once_with(tmp_pk_file, 'w+')
|
mock_open.assert_called_once_with(tmp_pk_file, 'w+')
|
||||||
m_file.write.assert_called_once_with(self.keypair.private_key)
|
m_file.write.assert_called_once_with(
|
||||||
|
mock_generate.return_value.private_key,
|
||||||
|
)
|
||||||
|
|
||||||
self.assertEqual(self.columns, columns)
|
self.assertEqual(self.columns, columns)
|
||||||
self.assertEqual(self.data, data)
|
self.assertEqual(self.data, data)
|
||||||
@ -167,8 +173,6 @@ class TestKeypairCreate(TestKeypair):
|
|||||||
@mock.patch.object(sdk_utils, 'supports_microversion', return_value=True)
|
@mock.patch.object(sdk_utils, 'supports_microversion', return_value=True)
|
||||||
def test_keypair_create_with_key_type(self, sm_mock):
|
def test_keypair_create_with_key_type(self, sm_mock):
|
||||||
for key_type in ['x509', 'ssh']:
|
for key_type in ['x509', 'ssh']:
|
||||||
self.keypair = compute_fakes.FakeKeypair.create_one_keypair(
|
|
||||||
no_pri=True)
|
|
||||||
self.sdk_client.create_keypair.return_value = self.keypair
|
self.sdk_client.create_keypair.return_value = self.keypair
|
||||||
|
|
||||||
self.data = (
|
self.data = (
|
||||||
@ -233,8 +237,12 @@ class TestKeypairCreate(TestKeypair):
|
|||||||
'--os-compute-api-version 2.2 or greater is required',
|
'--os-compute-api-version 2.2 or greater is required',
|
||||||
str(ex))
|
str(ex))
|
||||||
|
|
||||||
|
@mock.patch.object(
|
||||||
|
keypair, '_generate_keypair',
|
||||||
|
return_value=keypair.Keypair('private', 'public'),
|
||||||
|
)
|
||||||
@mock.patch.object(sdk_utils, 'supports_microversion', return_value=True)
|
@mock.patch.object(sdk_utils, 'supports_microversion', return_value=True)
|
||||||
def test_key_pair_create_with_user(self, sm_mock):
|
def test_key_pair_create_with_user(self, sm_mock, mock_generate):
|
||||||
arglist = [
|
arglist = [
|
||||||
'--user', identity_fakes.user_name,
|
'--user', identity_fakes.user_name,
|
||||||
self.keypair.name,
|
self.keypair.name,
|
||||||
@ -250,6 +258,7 @@ class TestKeypairCreate(TestKeypair):
|
|||||||
self.sdk_client.create_keypair.assert_called_with(
|
self.sdk_client.create_keypair.assert_called_with(
|
||||||
name=self.keypair.name,
|
name=self.keypair.name,
|
||||||
user_id=identity_fakes.user_id,
|
user_id=identity_fakes.user_id,
|
||||||
|
public_key=mock_generate.return_value.public_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual({}, columns)
|
self.assertEqual({}, columns)
|
||||||
@ -673,9 +682,6 @@ class TestKeypairShow(TestKeypair):
|
|||||||
self.cmd, arglist, verifylist)
|
self.cmd, arglist, verifylist)
|
||||||
|
|
||||||
def test_keypair_show(self):
|
def test_keypair_show(self):
|
||||||
# overwrite the setup one because we want to omit private_key
|
|
||||||
self.keypair = compute_fakes.FakeKeypair.create_one_keypair(
|
|
||||||
no_pri=True)
|
|
||||||
self.sdk_client.find_keypair.return_value = self.keypair
|
self.sdk_client.find_keypair.return_value = self.keypair
|
||||||
|
|
||||||
self.data = (
|
self.data = (
|
||||||
@ -704,7 +710,6 @@ class TestKeypairShow(TestKeypair):
|
|||||||
self.assertEqual(self.data, data)
|
self.assertEqual(self.data, data)
|
||||||
|
|
||||||
def test_keypair_show_public(self):
|
def test_keypair_show_public(self):
|
||||||
|
|
||||||
arglist = [
|
arglist = [
|
||||||
'--public-key',
|
'--public-key',
|
||||||
self.keypair.name
|
self.keypair.name
|
||||||
@ -723,10 +728,6 @@ class TestKeypairShow(TestKeypair):
|
|||||||
|
|
||||||
@mock.patch.object(sdk_utils, 'supports_microversion', return_value=True)
|
@mock.patch.object(sdk_utils, 'supports_microversion', return_value=True)
|
||||||
def test_keypair_show_with_user(self, sm_mock):
|
def test_keypair_show_with_user(self, sm_mock):
|
||||||
|
|
||||||
# overwrite the setup one because we want to omit private_key
|
|
||||||
self.keypair = compute_fakes.FakeKeypair.create_one_keypair(
|
|
||||||
no_pri=True)
|
|
||||||
self.sdk_client.find_keypair.return_value = self.keypair
|
self.sdk_client.find_keypair.return_value = self.keypair
|
||||||
|
|
||||||
self.data = (
|
self.data = (
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
The ``openstack keypair create`` command will now generate keypairs on the
|
||||||
|
client side in ssh-ed25519 format. The Compute service no longer supports
|
||||||
|
server-side key generation starting with ``--os-compute-api-version 2.92``
|
||||||
|
while the use of ssh-ed25519 is necessary as support for ssh-rsa has been
|
||||||
|
disabled by default starting in OpenSSH 8.8, which prevents its use in
|
||||||
|
guests using this version of OpenSSH in the default configuration.
|
||||||
|
ssh-ed25519 support is widespread and is supported by OpenSSH 6.5 or later
|
||||||
|
and Dropbear 2020.79 or later.
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
||||||
|
|
||||||
|
cryptography>=2.7 # BSD/Apache-2.0
|
||||||
cliff>=3.5.0 # Apache-2.0
|
cliff>=3.5.0 # Apache-2.0
|
||||||
iso8601>=0.1.11 # MIT
|
iso8601>=0.1.11 # MIT
|
||||||
openstacksdk>=0.103.0 # Apache-2.0
|
openstacksdk>=0.103.0 # Apache-2.0
|
||||||
|
Loading…
x
Reference in New Issue
Block a user