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
(cherry picked from commit 5f136811d6)
This commit is contained in:
Oliver Walsh 2017-03-23 17:31:51 +00:00
parent 570c5acc9e
commit c49755c72b
10 changed files with 59 additions and 52 deletions

View File

@ -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.

View File

@ -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+

View File

@ -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)

View File

@ -81,6 +81,7 @@ PASSWORD_PARAMETER_NAMES = (
'NeutronMetadataProxySharedSecret',
'NeutronPassword',
'NovaPassword',
'MigrationSshKey',
'RabbitPassword',
'RedisPassword',
'SaharaPassword',

View File

@ -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")

View File

@ -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):

View File

@ -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:])

View File

@ -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):

View File

@ -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,
}

View File

@ -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)