diff --git a/tripleo_common/actions/parameters.py b/tripleo_common/actions/parameters.py index 68b0e70c4..eb2b9af2b 100644 --- a/tripleo_common/actions/parameters.py +++ b/tripleo_common/actions/parameters.py @@ -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: diff --git a/tripleo_common/constants.py b/tripleo_common/constants.py index 7b6d7a9b5..abb963eed 100644 --- a/tripleo_common/constants.py +++ b/tripleo_common/constants.py @@ -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. diff --git a/tripleo_common/tests/actions/test_parameters.py b/tripleo_common/tests/actions/test_parameters.py index c74ee1765..75cf22536 100644 --- a/tripleo_common/tests/actions/test_parameters.py +++ b/tripleo_common/tests/actions/test_parameters.py @@ -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.' diff --git a/tripleo_common/utils/passwords.py b/tripleo_common/utils/passwords.py index 18bedb691..82bb3428b 100644 --- a/tripleo_common/utils/passwords.py +++ b/tripleo_common/utils/passwords.py @@ -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": diff --git a/workbooks/plan_management.yaml b/workbooks/plan_management.yaml index 5190c3c69..a22173641 100644 --- a/workbooks/plan_management.yaml +++ b/workbooks/plan_management.yaml @@ -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: