From 5f136811d69432aff85a8356b14bd2153ec7f76e Mon Sep 17 00:00:00 2001 From: Oliver Walsh Date: Thu, 23 Mar 2017 17:31:51 +0000 Subject: [PATCH] Add MigrationSshKey to generated passwords This reuses the existing password generation mistral action to generate an ssh keypair to be used for nova cold migration Paramiko is used to generate the ssh key, based on the existing approach in the nova keypair api. Also update validation ssh key generation to reuse the same method. Change-Id: I9e7a1862911312ad942233ac8fc828f4e1be1dcf --- .../migration_ssh_key-6e772d18d4d24485.yaml | 4 +++ requirements.txt | 1 + tripleo_common/actions/validations.py | 23 +++-------------- tripleo_common/constants.py | 1 + .../tests/actions/test_parameters.py | 14 +++++++++++ .../tests/actions/test_validations.py | 25 +++++++------------ tripleo_common/tests/utils/test_passwords.py | 6 +++++ .../tests/utils/test_validations.py | 7 ------ tripleo_common/utils/passwords.py | 19 +++++++++++++- tripleo_common/utils/validations.py | 9 +------ 10 files changed, 58 insertions(+), 51 deletions(-) create mode 100644 releasenotes/notes/migration_ssh_key-6e772d18d4d24485.yaml 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 671925661..6bf6bca94 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,3 +18,4 @@ Jinja2!=2.9.0,!=2.9.1,!=2.9.2,!=2.9.3,!=2.9.4,>=2.8 # BSD License (3 clause) python-novaclient>=7.1.0 # Apache-2.0 passlib>=1.7.0 # BSD netifaces>=0.10.4 # MIT +paramiko>=2.0 # LGPLv2.1+ diff --git a/tripleo_common/actions/validations.py b/tripleo_common/actions/validations.py index a8c6adf31..9f37ffe7f 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 @@ -33,25 +30,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 625b18a42..88d8a5e1b 100644 --- a/tripleo_common/constants.py +++ b/tripleo_common/constants.py @@ -87,6 +87,7 @@ PASSWORD_PARAMETER_NAMES = ( 'NeutronMetadataProxySharedSecret', 'NeutronPassword', 'NovaPassword', + 'MigrationSshKey', 'OctaviaHeartbeatKey', 'OctaviaPassword', 'PacemakerRemoteAuthkey', diff --git a/tripleo_common/tests/actions/test_parameters.py b/tripleo_common/tests/actions/test_parameters.py index ff17a05e9..cdcd8c920 100644 --- a/tripleo_common/tests/actions/test_parameters.py +++ b/tripleo_common/tests/actions/test_parameters.py @@ -128,6 +128,10 @@ _EXISTING_PASSWORDS = { 'QttkuxyeQTgHupKNaZF6y7rDyf7mbNR9DaPXpBQuZ7un6KDj2Dfh7yvfhPk8cHG7n9pb' 'KEKD3sgbbKnQ8d9MsGhUtCQVed7dtjpYKsmGJmbYMvZjpGpqsfsHQfFRdCgJHnW3FdQ6' 'sGhUtCQVed7dtj12', + 'MigrationSshKey': { + 'private_key': 'private_key', + 'public_key': 'public_key' + }, } @@ -323,6 +327,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.' @@ -330,9 +336,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() @@ -361,6 +370,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.' @@ -368,9 +379,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 7c4685ca7..b32a19708 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 1d3e3ac57..bdab8244f 100644 --- a/tripleo_common/tests/utils/test_passwords.py +++ b/tripleo_common/tests/utils/test_passwords.py @@ -69,3 +69,9 @@ class TestPasswords(base.TestCase): self.assertNotEqual(value['KeystoneCredential0'], value['KeystoneCredential1']) + + 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 c57b76184..13504c193 100644 --- a/tripleo_common/tests/utils/test_validations.py +++ b/tripleo_common/tests/utils/test_validations.py @@ -89,13 +89,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 e19dbb772..9ac9f1f17 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 @@ -42,7 +43,6 @@ def generate_passwords(mistralclient=None, 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', {}): @@ -68,6 +68,8 @@ def generate_passwords(mistralclient=None, stack_env=None): elif name in ('KeystoneCredential0', 'KeystoneCredential1', 'KeystoneFernetKey0', 'KeystoneFernetKey1'): passwords[name] = create_keystone_credential() + elif name == 'MigrationSshKey': + passwords[name] = create_ssh_keypair() else: passwords[name] = passutils.generate_password( size=_MIN_PASSWORD_SIZE) @@ -93,3 +95,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 6dcdb244d..c26a6aaef 100644 --- a/tripleo_common/utils/validations.py +++ b/tripleo_common/utils/validations.py @@ -93,13 +93,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_') @@ -112,7 +105,7 @@ 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)