Listen and update keystone service users' passwords

This commit adds listeners to monitor the change of keystone
service users' passwords, apply puppet runtime manifest to
update the service configuration and restart the related
services.

Tests passed:
1 Update keyring users' password
2 Change keystone users' passwords with OpenStack CLI
3 Verified the configuration updated
4 Verified the service w/o auth failure
5 No host swact during the apply, no FM alarm created at the
end of the process

Note:
1 the password synchronization between keyring and keystone
is not included in this review.
2. the update of the secure static hieradata is not included
in this change due to upgrade concerns, users need to update
the hieradata manually. E.g. the subcloud_rehome playbook
will add a task to migrate the passwords in the hieradata
during subcloud rehoming.
3. the unit tests will be delivered by another task in this
story.

Depends-On: https://review.opendev.org/c/starlingx/stx-puppet/+/853708
Story: 2010230
Task: 46074

Signed-off-by: Yuxing Jiang <Yuxing.Jiang@windriver.com>
Change-Id: I1a2dbc8b1e0bd03c2086895818729b2283b0fb96
This commit is contained in:
Yuxing Jiang 2022-08-22 17:07:01 -04:00
parent 7782f877a0
commit 5b5fe7ec7c
5 changed files with 72 additions and 38 deletions

View File

@ -2,7 +2,7 @@
# SPDX-License-Identifier: Apache-2.0
#
# Copyright (C) 2019 Intel Corporation
# Copyright (c) 2021 Wind River Systems, Inc.
# Copyright (c) 2021-2022 Wind River Systems, Inc.
#
"""
Sysinv Keystone notification listener.
@ -36,21 +36,23 @@ class NotificationEndpoint(object):
filter_rule = oslo_messaging.NotificationFilter(
event_type='identity.user.updated')
def __init__(self, callback, context):
def __init__(self, callback, context, user):
self.callback_func = callback
self.context = context
self.username = user
self.dbapi = dbapi.get_instance()
self._openstack = openstack.OpenStackOperator(self.dbapi)
def info(self, ctxt, publisher_id, event_type, payload, metadata):
"""Receives notification at info level."""
user = self._openstack.get_platform_keystone_user(self.context.user)
user = self._openstack.get_platform_keystone_user(self.username)
if payload['eventType'] == 'activity' and \
payload['action'] == 'updated.user' and \
payload['outcome'] == 'success' and \
payload['resource_info'] == user.id:
self.callback_func(self.context)
self.callback_func(self.context, self.username)
return oslo_messaging.NotificationResult.HANDLED
@ -92,8 +94,9 @@ def start_keystone_listener(callback_endpoints):
endpoints = []
for callback_endpoint in callback_endpoints:
endpoint = NotificationEndpoint(callback_endpoint.get('func'),
callback_endpoint.get('ctx'))
endpoint = NotificationEndpoint(callback_endpoint.get('function'),
callback_endpoint.get('context'),
callback_endpoint.get('user'))
endpoints.append(endpoint)
pool = "sysinv-keystone-listener-workers"

View File

@ -1132,7 +1132,7 @@ class AppOperator(object):
except Exception as e:
LOG.exception(e)
def audit_local_registry_secrets(self, context):
def audit_local_registry_secrets(self, context, username=None):
"""
local registry uses admin's username&password for authentication.
K8s stores the authentication info in secrets in order to access

View File

@ -194,6 +194,21 @@ LOCK_NAME_UPDATE_CONFIG = 'update_config_'
LOCK_APP_AUTO_MANAGE = 'AppAutoManageLock'
LOCK_RUNTIME_CONFIG_CHECK = 'runtime_config_check'
# Keystone users whose passwords change are monitored by keystone listener, and
# the puppet classes to update the service after the passwords change.
# TODO(yuxing): there are still several keystone users are not covered by this
# dictionary, e.g. dcorch,dcdbsync, smapi and sysinv etc. Need to consider to
# create puppet class to reload the related service in case their passwords
# are changed in keystone and keyring.
KEYSTONE_USER_PASSWORD_UPDATE = {
"admin": "openstack::keystone::password::runtime",
"barbican": "openstack::keystone::barbican::password::runtime",
"dcmanager": "openstack::keystone::dcmanager::password::runtime",
"fm": "openstack::keystone::fm::password::runtime",
"mtce": "platform::mtce::runtime",
"patching": "openstack::keystone::patching::password::runtime",
"vim": "openstack::keystone::nfv::password::runtime"
}
AppTarBall = namedtuple(
'AppTarBall',
@ -280,16 +295,10 @@ class ConductorManager(service.PeriodicService):
# monitor keystone user update event to check whether admin password is
# changed or not. If changed, then sync it to kubernetes's secret info,
# and restart impacted services.
admin_context = ctx.RequestContext(user='admin', tenant='admin',
is_admin=True)
callback_endpoints = \
[{'func': self._app.audit_local_registry_secrets,
'ctx': admin_context},
{'func': self.update_keystone_password,
'ctx': admin_context}]
# The thread to monitor keystone admin password change
callback_endpoints = self._get_keystone_callback_endpoints()
greenthread.spawn(keystone_listener.start_keystone_listener,
callback_endpoints)
# Monitor ceph to become responsive
if StorageBackendConfig.has_backend_configured(
self.dbapi,
@ -352,6 +361,23 @@ class ConductorManager(service.PeriodicService):
else:
return None
def _get_keystone_callback_endpoints(self):
""" Get call back endpoints for keystone listener"""
callback_endpoints = []
context = ctx.RequestContext(user='admin', tenant='admin',
is_admin=True)
for username in KEYSTONE_USER_PASSWORD_UPDATE.keys():
if username == 'admin':
callback_endpoints.append(
{'function': self._app.audit_local_registry_secrets,
'context': context,
'user': username})
callback_endpoints.append({'function': self._update_keystone_password,
'context': context,
'user': username})
return callback_endpoints
def _initialize_active_controller_reboot_config(self):
# initialize host_reboot_config for active controller in case
# process has been restarted
@ -1724,16 +1750,17 @@ class ConductorManager(service.PeriodicService):
config_dict=config_dict)
return True
def update_keystone_password(self, context):
"""This method calls a puppet class
'openstack::keystone::password::runtime'
on keystone password change"""
def _update_keystone_password(self, context, username):
"""This method calls a puppet class to update the service config and
reload the related service on keystone password change"""
LOG.info("Updating service config for keystone user: %s" % username)
personalities = [constants.CONTROLLER]
config_uuid = self._config_update_hosts(context, personalities)
config_dict = {
"personalities": personalities,
"classes": ["openstack::keystone::password::runtime"]
"classes": [KEYSTONE_USER_PASSWORD_UPDATE[username]],
}
self._config_apply_runtime_manifest(context, config_uuid, config_dict)
@ -10837,9 +10864,7 @@ class ConductorManager(service.PeriodicService):
LOG.info("SYS_I Clear system config alarm: %s target config %s" %
(ihost_obj.hostname, ihost_obj.config_target))
self.fm_api.clear_fault(
fm_constants.FM_ALARM_ID_SYSCONFIG_OUT_OF_DATE,
entity_instance_id)
self._clear_config_out_of_date_alarm(entity_instance_id)
self._clear_runtime_class_apply_in_progress(host_uuids=[ihost_obj.uuid])
@ -10848,6 +10873,20 @@ class ConductorManager(service.PeriodicService):
ihost_obj.config_status = None
ihost_obj.save(context)
@retry(retry_on_result=lambda x: x is False, wait_fixed=5000,
stop_max_attempt_number=5)
def _clear_config_out_of_date_alarm(self, entity_instance_id):
"""Apply a new config may result a temporary failure to clear the
config-out-of-date alarm, retry it until success
"""
if self.fm_api.clear_fault(
fm_constants.FM_ALARM_ID_SYSCONFIG_OUT_OF_DATE,
entity_instance_id):
return True
else:
return False
@staticmethod
def _config_set_reboot_required(config_uuid):
"""Set the reboot required flag for the supplied UUID

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2013-2019 Wind River Systems, Inc.
# Copyright (c) 2013-2022 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@ -345,7 +345,7 @@ class OpenStackOperator(object):
########################
# keystone user methods
########################
def _get_keystone_users(self, service_config):
def get_keystone_users(self, service_config=PLATFORM_CONFIG):
"""Get a list of all users in keystone otherwise an empty list."""
user_list = []
@ -356,21 +356,9 @@ class OpenStackOperator(object):
return user_list
def get_platform_keystone_admin_user(self):
"""Return platform keystone admin user otherwise None."""
users = self._get_keystone_users(PLATFORM_CONFIG)
try:
return [user for user in users if user.name == 'admin'][0]
except Exception as e:
LOG.error("Failed to get platform keystone admin user:\n%s"
% str(e))
return None
def get_platform_keystone_user(self, username):
"""Return platform keystone user otherwise None."""
users = self._get_keystone_users(PLATFORM_CONFIG)
users = self.get_keystone_users()
try:
return [user for user in users if user.name == username][0]

View File

@ -1658,6 +1658,10 @@ class ManagerTestCase(base.DbTestCase):
def _clear_alarm(self, fm_id, fm_instance):
self.alarm_raised = False
# Need to return a value to simulate the return value of
# fm_api.clear_fault, as we rely on it to see whether the the clear_fault
# is succeeded and if a retry is required.
return True
def _get_faults_by_id(self, alarm_id):
return None