Add mistral job to rotate passwords on the overcloud

This commit adds worflows to allow deployers to change
passwords post-install.  There are several options:

rotate_passwords: generates new passwords for all passwords
    except those which need to be handled specially
rotate_passwords + password_list: generates only the
    specified passwords

All data is stored in the plan.  To be propagated, the
overcloud must then be re-deployed.

Change-Id: I0ef8be542c3e4969e1bd3193e2e4bf7d4be73f55
This commit is contained in:
Ade Lee 2018-06-12 10:21:59 -04:00
parent 5d44a0a016
commit 721f9ba62f
5 changed files with 239 additions and 7 deletions

View File

@ -232,13 +232,24 @@ class GeneratePasswordsAction(base.TripleOAction):
"""Generates passwords needed for Overcloud deployment
This method generates passwords and ensures they are stored in the
plan environment. This method respects previously generated passwords and
adds new passwords as necessary.
plan environment. By default, this method respects previously
generated passwords and adds new passwords as necessary.
If rotate_passwords is set to True, then passwords will be replaced as
follows:
- if password names are specified in the rotate_pw_list, then only those
passwords will be replaced.
- otherwise, all passwords not in the DO_NOT_ROTATE list (as they require
special handling, like KEKs and Fernet keys) will be replaced.
"""
def __init__(self, container=constants.DEFAULT_CONTAINER_NAME):
def __init__(self, container=constants.DEFAULT_CONTAINER_NAME,
rotate_passwords=False,
rotate_pw_list=[]):
super(GeneratePasswordsAction, self).__init__()
self.container = container
self.rotate_passwords = rotate_passwords
self.rotate_pw_list = rotate_pw_list
def run(self, context):
heat = self.get_orchestration_client(context)
@ -271,7 +282,11 @@ class GeneratePasswordsAction(base.TripleOAction):
except heat_exc.HTTPNotFound:
stack_env = None
passwords = password_utils.generate_passwords(mistral, stack_env)
passwords = password_utils.generate_passwords(
mistralclient=mistral,
stack_env=stack_env,
rotate_passwords=self.rotate_passwords
)
# if passwords don't yet exist in plan environment
if 'passwords' not in env:
@ -290,6 +305,15 @@ class GeneratePasswordsAction(base.TripleOAction):
if name not in env['passwords']:
env['passwords'][name] = password
if self.rotate_passwords:
if len(self.rotate_pw_list) > 0:
for name in self.rotate_pw_list:
env['passwords'][name] = passwords[name]
else:
for name, password in passwords.items():
if name not in constants.DO_NOT_ROTATE_LIST:
env['passwords'][name] = password
try:
plan_utils.put_env(swift, env)
except swiftexceptions.ClientException as err:

View File

@ -136,6 +136,17 @@ LEGACY_HEAT_PASSWORD_RESOURCE_NAMES = (
'RabbitCookie',
)
# List of passwords that should not be rotated by default using the
# GeneratePasswordAction because they require some special handling
DO_NOT_ROTATE_LIST = (
'BarbicanSimpleCryptoKek',
'KeystoneCredential0',
'KeystoneCredential1',
'KeystoneFernetKey0',
'KeystoneFernetKey1',
'KeystoneFernetKeys',
)
PLAN_NAME_PATTERN = '^[a-zA-Z0-9-]+$'
# The default version of the Image API to set in overcloudrc.

View File

@ -704,6 +704,158 @@ class GeneratePasswordsActionTest(base.TestCase):
"tripleo.parameters.get"
)
@mock.patch('tripleo_common.actions.base.TripleOAction.'
'cache_delete')
@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.'
'create_fernet_keys_repo_structure_and_keys')
@mock.patch('tripleo_common.utils.passwords.'
'get_snmpd_readonly_user_password')
@mock.patch('tripleo_common.actions.base.TripleOAction.'
'get_workflow_client')
@mock.patch('tripleo_common.actions.base.TripleOAction.get_object_client')
def test_run_rotate_no_rotate_list(self, mock_get_object_client,
mock_get_workflow_client,
mock_get_snmpd_readonly_user_password,
mock_fernet_keys_setup,
mock_create_ssh_keypair,
mock_get_orchestration_client,
mock_cache):
mock_get_snmpd_readonly_user_password.return_value = "TestPassword"
mock_create_ssh_keypair.return_value = {'public_key': 'Foo',
'private_key': 'Bar'}
mock_fernet_keys_setup.return_value = {'/tmp/foo': {'content': 'Foo'},
'/tmp/bar': {'content': 'Bar'}}
mock_ctx = mock.MagicMock()
swift = mock.MagicMock(url="http://test.com")
mock_env = yaml.safe_dump({
'name': constants.DEFAULT_CONTAINER_NAME,
'temp_environment': 'temp_environment',
'template': 'template',
'environments': [{u'path': u'environments/test.yaml'}],
'passwords': _EXISTING_PASSWORDS.copy()
}, default_flow_style=False)
swift.get_object.return_value = ({}, mock_env)
mock_get_object_client.return_value = swift
mock_orchestration = mock.MagicMock()
mock_orchestration.stacks.environment.return_value = {
'parameter_defaults': {}
}
mock_resource = mock.MagicMock()
mock_resource.attributes = {
'value': 'existing_value'
}
mock_orchestration.resources.get.return_value = mock_resource
mock_get_orchestration_client.return_value = mock_orchestration
action = parameters.GeneratePasswordsAction(rotate_passwords=True)
result = action.run(mock_ctx)
# ensure passwords in the DO_NOT_ROTATE_LIST are not modified
for name in constants.DO_NOT_ROTATE_LIST:
self.assertEqual(_EXISTING_PASSWORDS[name], result[name])
# ensure all passwords are generated
for name in constants.PASSWORD_PARAMETER_NAMES:
self.assertTrue(name in result, "%s is not in %s" % (name, result))
# ensure new passwords have been generated
self.assertNotEqual(_EXISTING_PASSWORDS, result)
mock_cache.assert_called_once_with(
mock_ctx,
"overcloud",
"tripleo.parameters.get"
)
@mock.patch('tripleo_common.actions.base.TripleOAction.'
'cache_delete')
@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.'
'create_fernet_keys_repo_structure_and_keys')
@mock.patch('tripleo_common.utils.passwords.'
'get_snmpd_readonly_user_password')
@mock.patch('tripleo_common.actions.base.TripleOAction.'
'get_workflow_client')
@mock.patch('tripleo_common.actions.base.TripleOAction.get_object_client')
def test_run_rotate_with_rotate_list(self, mock_get_object_client,
mock_get_workflow_client,
mock_get_snmpd_readonly_user_password,
mock_fernet_keys_setup,
mock_create_ssh_keypair,
mock_get_orchestration_client,
mock_cache):
mock_get_snmpd_readonly_user_password.return_value = "TestPassword"
mock_create_ssh_keypair.return_value = {'public_key': 'Foo',
'private_key': 'Bar'}
mock_fernet_keys_setup.return_value = {'/tmp/foo': {'content': 'Foo'},
'/tmp/bar': {'content': 'Bar'}}
mock_ctx = mock.MagicMock()
swift = mock.MagicMock(url="http://test.com")
mock_env = yaml.safe_dump({
'name': constants.DEFAULT_CONTAINER_NAME,
'temp_environment': 'temp_environment',
'template': 'template',
'environments': [{u'path': u'environments/test.yaml'}],
'passwords': _EXISTING_PASSWORDS.copy()
}, default_flow_style=False)
swift.get_object.return_value = ({}, mock_env)
mock_get_object_client.return_value = swift
mock_orchestration = mock.MagicMock()
mock_orchestration.stacks.environment.return_value = {
'parameter_defaults': {}
}
mock_resource = mock.MagicMock()
mock_resource.attributes = {
'value': 'existing_value'
}
mock_orchestration.resources.get.return_value = mock_resource
mock_get_orchestration_client.return_value = mock_orchestration
rotate_list = [
'MistralPassword',
'BarbicanPassword',
'AdminPassword',
'CeilometerMeteringSecret',
'ZaqarPassword',
'NovaPassword',
'MysqlRootPassword'
]
action = parameters.GeneratePasswordsAction(
rotate_passwords=True,
rotate_pw_list=rotate_list
)
result = action.run(mock_ctx)
# ensure only specified passwords are regenerated
for name in constants.PASSWORD_PARAMETER_NAMES:
self.assertTrue(name in result, "%s is not in %s" % (name, result))
if name in rotate_list:
self.assertNotEqual(_EXISTING_PASSWORDS[name], result[name])
else:
self.assertEqual(_EXISTING_PASSWORDS[name], result[name])
mock_cache.assert_called_once_with(
mock_ctx,
"overcloud",
"tripleo.parameters.get"
)
@mock.patch('tripleo_common.actions.base.TripleOAction.'
'cache_delete')
@mock.patch('tripleo_common.actions.base.TripleOAction.'

View File

@ -31,7 +31,8 @@ KEYSTONE_FERNET_REPO = '/etc/keystone/fernet-keys/'
LOG = logging.getLogger(__name__)
def generate_passwords(mistralclient=None, stack_env=None):
def generate_passwords(mistralclient=None, stack_env=None,
rotate_passwords=False):
"""Create the passwords needed for deploying OpenStack via t-h-t.
This will create the set of passwords required by the undercloud and
@ -46,7 +47,8 @@ def generate_passwords(mistralclient=None, stack_env=None):
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', {}):
if (stack_env and name in stack_env.get('parameter_defaults', {}) and
not rotate_passwords):
passwords[name] = stack_env['parameter_defaults'][name]
elif name.startswith("Ceph"):
if name == "CephClusterFSID":

View File

@ -391,7 +391,6 @@ workflows:
plan_name: <% $.container %>
message: <% $.get('message', '') %>
get_passwords:
description: Retrieves passwords for a given plan
input:
@ -441,6 +440,50 @@ workflows:
plan_name: <% $.container %>
message: <% $.get('message', '') %>
rotate_passwords:
description: Rotate passwords for a given plan
input:
- container
- queue_name: tripleo
- password_list: []
tags:
- tripleo-common-managed
tasks:
verify_container_exists:
action: swift.head_container container=<% $.container %>
publish-on-error:
status: FAILED
message: <% task().result %>
on-success: rotate_environment_passwords
on-error: send_message
rotate_environment_passwords:
action: tripleo.parameters.generate_passwords
input:
container: <% $.container %>
rotate_passwords: true
rotate_pw_list: <% $.password_list %>
publish:
status: SUCCESS
message: <% task().result %>
publish-on-error:
status: FAILED
message: <% task().result %>
on-complete: send_message
send_message:
workflow: tripleo.messaging.v1.send
input:
queue_name: <% $.queue_name %>
type: <% execution().name %>
status: <% $.status %>
execution: <% execution() %>
plan_name: <% $.container %>
message: <% $.get('message', '') %>
export_deployment_plan:
description: Creates an export tarball for a given plan
input: