diff --git a/releasenotes/notes/migration_ssh_key-6e772d18d4d24485.yaml b/releasenotes/notes/migration_ssh_key-6e772d18d4d24485.yaml new file mode 100644 index 000000000..54c09ee69 --- /dev/null +++ b/releasenotes/notes/migration_ssh_key-6e772d18d4d24485.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add MigrationSshKey to generated passwords. This ssh key-pair is used by + nova cold-migration and libvirt live-migration unless TLS is enabled. diff --git a/requirements.txt b/requirements.txt index ae2d440f0..9899647c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,3 +17,4 @@ python-ironic-inspector-client>=1.5.0 # Apache-2.0 Jinja2>=2.8 # BSD License (3 clause) python-novaclient!=2.33.0,>=2.29.0 # Apache-2.0 passlib>=1.6 # BSD +paramiko>=2.0 # LGPLv2.1+ diff --git a/tripleo_common/actions/validations.py b/tripleo_common/actions/validations.py index cb35cf814..bd97e1ffd 100644 --- a/tripleo_common/actions/validations.py +++ b/tripleo_common/actions/validations.py @@ -12,16 +12,13 @@ # 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 os -import shutil -import tempfile - from mistral.workflow import utils as mistral_workflow_utils from mistralclient.api import base as mistralclient_api from oslo_concurrency.processutils import ProcessExecutionError from tripleo_common.actions import base from tripleo_common import constants +from tripleo_common.utils import passwords as password_utils from tripleo_common.utils import validations as utils @@ -35,25 +32,13 @@ class GetPubkeyAction(base.TripleOAction): env = mc.environments.get('ssh_keys') public_key = env.variables['public_key'] except Exception: - tmp_dir = tempfile.mkdtemp() - private_key_path = os.path.join(tmp_dir, 'id_rsa') - public_key_path = private_key_path + '.pub' - utils.create_ssh_keypair(private_key_path) - - with open(private_key_path, 'r') as f: - private_key = f.read().strip() - with open(public_key_path, 'r') as f: - public_key = f.read().strip() - - shutil.rmtree(tmp_dir) + ssh_key = password_utils.create_ssh_keypair() + public_key = ssh_key['public_key'] workflow_env = { 'name': 'ssh_keys', 'description': 'SSH keys for TripleO validations', - 'variables': { - 'public_key': public_key, - 'private_key': private_key, - } + 'variables': ssh_key } mc.environments.create(**workflow_env) diff --git a/tripleo_common/constants.py b/tripleo_common/constants.py index 77acf2d17..68341fc7c 100644 --- a/tripleo_common/constants.py +++ b/tripleo_common/constants.py @@ -81,6 +81,7 @@ PASSWORD_PARAMETER_NAMES = ( 'NeutronMetadataProxySharedSecret', 'NeutronPassword', 'NovaPassword', + 'MigrationSshKey', 'RabbitPassword', 'RedisPassword', 'SaharaPassword', diff --git a/tripleo_common/tests/actions/test_parameters.py b/tripleo_common/tests/actions/test_parameters.py index 356ac0dc0..9e32eae0a 100644 --- a/tripleo_common/tests/actions/test_parameters.py +++ b/tripleo_common/tests/actions/test_parameters.py @@ -51,7 +51,11 @@ _EXISTING_PASSWORDS = { 'HeatPassword': 'bREnsXtMHKTHxt8XW6NXAYr48', 'MysqlClustercheckPassword': 'jN4RMMWWJ4sycaRwh7UvrAtfX', 'CephClientKey': b'AQCQXtlXAAAAABAAKyc+8St8i9onHyu2mPk+vg==', - 'NeutronPassword': 'ZxAjdU2UXCV4GM3WyPKrzAZXD' + 'NeutronPassword': 'ZxAjdU2UXCV4GM3WyPKrzAZXD', + 'MigrationSshKey': { + 'private_key': 'private_key', + 'public_key': 'public_key' + }, } @@ -247,6 +251,8 @@ class GeneratePasswordsActionTest(base.TestCase): @mock.patch('tripleo_common.actions.base.TripleOAction.' '_get_orchestration_client') + @mock.patch('tripleo_common.utils.passwords.' + 'create_ssh_keypair') @mock.patch('tripleo_common.utils.passwords.' 'get_snmpd_readonly_user_password') @mock.patch('tripleo_common.actions.base.TripleOAction.' @@ -254,9 +260,12 @@ class GeneratePasswordsActionTest(base.TestCase): @mock.patch('mistral.context.ctx') def test_run_passwords_exist(self, mock_ctx, mock_get_workflow_client, mock_get_snmpd_readonly_user_password, + mock_create_ssh_keypair, mock_get_orchestration_client): mock_get_snmpd_readonly_user_password.return_value = "TestPassword" + mock_create_ssh_keypair.return_value = {'public_key': 'Foo', + 'private_key': 'Bar'} mock_ctx.return_value = mock.MagicMock() mock_mistral = mock.MagicMock() @@ -285,6 +294,8 @@ class GeneratePasswordsActionTest(base.TestCase): @mock.patch('tripleo_common.actions.base.TripleOAction.' '_get_orchestration_client') + @mock.patch('tripleo_common.utils.passwords.' + 'create_ssh_keypair') @mock.patch('tripleo_common.utils.passwords.' 'get_snmpd_readonly_user_password') @mock.patch('tripleo_common.actions.base.TripleOAction.' @@ -292,9 +303,12 @@ class GeneratePasswordsActionTest(base.TestCase): @mock.patch('mistral.context.ctx') def test_passwords_exist_in_heat(self, mock_ctx, mock_get_workflow_client, mock_get_snmpd_readonly_user_password, + mock_create_ssh_keypair, mock_get_orchestration_client): mock_get_snmpd_readonly_user_password.return_value = "TestPassword" + mock_create_ssh_keypair.return_value = {'public_key': 'Foo', + 'private_key': 'Bar'} existing_passwords = _EXISTING_PASSWORDS.copy() existing_passwords.pop("AdminPassword") diff --git a/tripleo_common/tests/actions/test_validations.py b/tripleo_common/tests/actions/test_validations.py index 02319fc1c..9acfa95a3 100644 --- a/tripleo_common/tests/actions/test_validations.py +++ b/tripleo_common/tests/actions/test_validations.py @@ -40,26 +40,19 @@ class GetPubkeyActionTest(base.TestCase): @mock.patch( 'tripleo_common.actions.base.TripleOAction._get_workflow_client') - @mock.patch('tripleo_common.utils.validations.create_ssh_keypair') - @mock.patch('tempfile.mkdtemp') - @mock.patch('shutil.rmtree') - def test_run_no_pubkey(self, mock_rmtree, mock_mkdtemp, - mock_create_keypair, get_workflow_client_mock): + @mock.patch('tripleo_common.utils.passwords.create_ssh_keypair') + def test_run_no_pubkey(self, mock_create_keypair, + get_workflow_client_mock): mistral = mock.MagicMock() get_workflow_client_mock.return_value = mistral mistral.environments.get.side_effect = 'nope, sorry' - mock_mkdtemp.return_value = '/tmp_path' + mock_create_keypair.return_value = { + 'public_key': 'public_key', + 'private_key': 'private_key', + } - mock_open_context = mock.mock_open() - mock_open_context().read.side_effect = ['private_key', 'public_key'] - - with mock.patch('six.moves.builtins.open', mock_open_context): - action = validations.GetPubkeyAction() - self.assertEqual('public_key', action.run()) - - mock_mkdtemp.assert_called_once() - mock_create_keypair.assert_called_once_with('/tmp_path/id_rsa') - mock_rmtree.asser_called_once_with('/tmp_path') + action = validations.GetPubkeyAction() + self.assertEqual('public_key', action.run()) class Enabled(base.TestCase): diff --git a/tripleo_common/tests/utils/test_passwords.py b/tripleo_common/tests/utils/test_passwords.py index 47ca41755..ca674614c 100644 --- a/tripleo_common/tests/utils/test_passwords.py +++ b/tripleo_common/tests/utils/test_passwords.py @@ -34,3 +34,9 @@ class TestPasswords(base.TestCase): value = password_utils.get_snmpd_readonly_user_password(mock_mistral) self.assertEqual(value, "78cbc32b858718267c355d4") + + def test_create_ssh_keypair(self): + + value = password_utils.create_ssh_keypair(comment="Foo") + self.assertEqual('ssh-rsa', value['public_key'][:7]) + self.assertEqual('Foo', value['public_key'][-3:]) diff --git a/tripleo_common/tests/utils/test_validations.py b/tripleo_common/tests/utils/test_validations.py index 83d775f6d..0c717bdc3 100644 --- a/tripleo_common/tests/utils/test_validations.py +++ b/tripleo_common/tests/utils/test_validations.py @@ -88,13 +88,6 @@ VALIDATION_GROUPS_1_2_PARSED = { class ValidationsKeyTest(base.TestCase): - @mock.patch("oslo_concurrency.processutils.execute") - def test_create_ssh_keypair(self, mock_execute): - validations.create_ssh_keypair('/path/to/key') - mock_execute.assert_called_once_with( - '/usr/bin/ssh-keygen', '-t', 'rsa', '-N', '', - '-f', '/path/to/key', '-C', 'tripleo-validations') - @mock.patch("oslo_concurrency.processutils.execute") @mock.patch('tempfile.mkstemp') def test_write_identity_file(self, mock_mkstemp, mock_execute): diff --git a/tripleo_common/utils/passwords.py b/tripleo_common/utils/passwords.py index 4f9e0454e..05dce6241 100644 --- a/tripleo_common/utils/passwords.py +++ b/tripleo_common/utils/passwords.py @@ -15,6 +15,7 @@ import base64 import logging import os +import paramiko import struct import time import uuid @@ -39,7 +40,6 @@ def generate_overcloud_passwords(mistralclient, stack_env=None): passwords = {} for name in constants.PASSWORD_PARAMETER_NAMES: - # Support users upgrading from Mitaka or otherwise creating a plan for # a Heat stack that already exists. if stack_env and name in stack_env.get('parameter_defaults', {}): @@ -56,6 +56,8 @@ def generate_overcloud_passwords(mistralclient, stack_env=None): passwords[name] = get_snmpd_readonly_user_password(mistralclient) elif name in ('KeystoneCredential0', 'KeystoneCredential1'): passwords[name] = create_keystone_credential() + elif name == 'MigrationSshKey': + passwords[name] = create_ssh_keypair() else: passwords[name] = passutils.generate_password( size=_MIN_PASSWORD_SIZE) @@ -81,3 +83,18 @@ def get_snmpd_readonly_user_password(mistralclient): def create_keystone_credential(): return base64.urlsafe_b64encode(os.urandom(32)) + + +def create_ssh_keypair(comment=None, bits=2048): + """Generate an ssh keypair for use on the overcloud""" + if comment is None: + comment = "Generated by TripleO" + key = paramiko.RSAKey.generate(bits) + keyout = six.StringIO() + key.write_private_key(keyout) + private_key = keyout.getvalue() + public_key = '{} {} {}'.format(key.get_name(), key.get_base64(), comment) + return { + 'private_key': private_key, + 'public_key': public_key, + } diff --git a/tripleo_common/utils/validations.py b/tripleo_common/utils/validations.py index fa16f9253..391dac78f 100644 --- a/tripleo_common/utils/validations.py +++ b/tripleo_common/utils/validations.py @@ -92,13 +92,6 @@ def run_validation(validation, identity_file, plan): ) -def create_ssh_keypair(key_path): - """Create SSH keypair""" - LOG.debug('Creating SSH keypair at %s', key_path) - processutils.execute('/usr/bin/ssh-keygen', '-t', 'rsa', '-N', '', - '-f', key_path, '-C', 'tripleo-validations') - - def write_identity_file(key): """Write the SSH private key to disk""" fd, path = tempfile.mkstemp(prefix='validations_identity_') @@ -111,6 +104,6 @@ def write_identity_file(key): def cleanup_identity_file(path): - """Write the SSH private key to disk""" + """Remove the SSH private key from disk""" LOG.debug('Cleaning up identity file at %s', path) processutils.execute('/usr/bin/sudo', '/usr/bin/rm', '-f', path)