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:
parent
5d44a0a016
commit
721f9ba62f
@ -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:
|
||||
|
@ -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.
|
||||
|
@ -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.'
|
||||
|
@ -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":
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user