Add rotate-admin-password action

This action allows the user to easily rotate the admin user's
password by replacing it with a randomly generated one.

Change-Id: I6ce69be15b11b00f804d3143d835ec3ce6515865
Related-Bug: #1927280
Func-Test-PR: https://github.com/openstack-charmers/zaza-openstack-tests/pull/720
This commit is contained in:
Pedro Castillo
2022-03-08 15:36:29 +00:00
parent 4949830cea
commit ae178d7471
6 changed files with 102 additions and 2 deletions

View File

@@ -9,6 +9,11 @@ resume:
Resume keystone services. Resume keystone services.
If the keystone deployment is clustered using the hacluster charm, the If the keystone deployment is clustered using the hacluster charm, the
corresponding hacluster unit on the node must be resumed as well. corresponding hacluster unit on the node must be resumed as well.
rotate-admin-password:
description: |
Rotate the admin user's password.
The current password is replaced with a randomly generated password. The
new password is stored in the leader's admin_passwd bucket.
openstack-upgrade: openstack-upgrade:
description: | description: |
Perform openstack upgrades. Config option action-managed-upgrade must be Perform openstack upgrades. Config option action-managed-upgrade must be

View File

@@ -33,12 +33,21 @@ _add_path(_root)
from charmhelpers.core.hookenv import action_fail from charmhelpers.core.hookenv import action_fail
from keystone_utils import ( from keystone_utils import (
rotate_admin_passwd,
pause_unit_helper, pause_unit_helper,
resume_unit_helper, resume_unit_helper,
register_configs, register_configs,
) )
def rotate_admin_password(args):
"""Rotate the admin user's password.
@raises Exception if keystone client cannot update the password
"""
rotate_admin_passwd()
def pause(args): def pause(args):
"""Pause all the Keystone services. """Pause all the Keystone services.
@@ -57,7 +66,11 @@ def resume(args):
# A dictionary of all the defined actions to callables (which take # A dictionary of all the defined actions to callables (which take
# parsed arguments). # parsed arguments).
ACTIONS = {"pause": pause, "resume": resume} ACTIONS = {
"rotate-admin-password": rotate_admin_password,
"pause": pause,
"resume": resume,
}
def main(args): def main(args):

View File

@@ -0,0 +1 @@
actions.py

View File

@@ -1500,6 +1500,22 @@ def set_admin_passwd(passwd, user=None):
_leader_set_secret({'{}_passwd'.format(user): passwd}) _leader_set_secret({'{}_passwd'.format(user): passwd})
def rotate_admin_passwd():
if not is_leader():
raise RuntimeError("This unit is not the leader and therefore can't "
"rotate the admin password.")
admin_passwd = config('admin-password')
if admin_passwd and admin_passwd.strip().lower() != 'none':
raise RuntimeError(
"The 'admin-password' config is present, so the action will be "
"aborted. To allow randomly generated passwords, unset the "
"config value.")
user = config('admin-user')
new_passwd = pwgen(16)
update_user_password(user, new_passwd, ADMIN_DOMAIN)
leader_set({'admin_passwd': new_passwd})
def get_api_version(): def get_api_version():
api_version = config('preferred-api-version') api_version = config('preferred-api-version')
cmp_release = CompareOpenStackReleases( cmp_release = CompareOpenStackReleases(

View File

@@ -25,6 +25,17 @@ with patch('charmhelpers.contrib.openstack.utils.'
import actions.actions import actions.actions
class ChangeAdminPasswordTestCase(CharmTestCase):
def setUp(self):
super(ChangeAdminPasswordTestCase, self).setUp(
actions.actions, ["rotate_admin_passwd"])
def test_rotate_admin_password(self):
actions.actions.rotate_admin_password([])
self.rotate_admin_passwd.assert_called_once()
class PauseTestCase(CharmTestCase): class PauseTestCase(CharmTestCase):
def setUp(self): def setUp(self):

View File

@@ -2131,3 +2131,57 @@ class TestKeystoneUtils(CharmTestCase):
self.assertEqual( self.assertEqual(
utils.get_add_role_to_admin({}), utils.get_add_role_to_admin({}),
[]) [])
@patch.object(utils, 'update_user_password')
@patch.object(utils, 'leader_set')
@patch.object(utils, 'pwgen')
@patch.object(utils, 'is_leader')
def test_rotate_admin_password_without_config(
self, is_leader, pwgen, leader_set, update_user_password):
user = 'test-user'
password = 'password'
is_leader.return_value = True
pwgen.return_value = password
self.test_config.set('admin-user', user)
self.test_config.set('admin-password', '')
utils.rotate_admin_passwd()
pwgen.assert_called_once()
update_user_password.assert_called_once_with(
user, password, utils.ADMIN_DOMAIN)
leader_set.assert_called_once_with({'admin_passwd': password})
@patch.object(utils, 'update_user_password')
@patch.object(utils, 'leader_set')
@patch.object(utils, 'pwgen')
@patch.object(utils, 'is_leader')
def test_rotate_admin_password_with_config(
self, is_leader, pwgen, leader_set, update_user_password):
user = 'test-user'
password = 'password'
is_leader.return_value = True
self.test_config.set('admin-user', user)
self.test_config.set('admin-password', password)
with self.assertRaises(RuntimeError):
utils.rotate_admin_passwd()
pwgen.assert_not_called()
update_user_password.assert_not_called()
leader_set.assert_not_called()
@patch.object(utils, 'update_user_password')
@patch.object(utils, 'leader_set')
@patch.object(utils, 'pwgen')
@patch.object(utils, 'is_leader')
def test_rotate_admin_password_outside_leader(
self, is_leader, pwgen, leader_set, update_user_password):
is_leader.return_value = False
with self.assertRaises(RuntimeError):
utils.rotate_admin_passwd()
pwgen.assert_not_called()
update_user_password.assert_not_called()
leader_set.assert_not_called()