Merge "Redfish: Consider password part of the session cache"

This commit is contained in:
Zuul 2022-09-05 09:26:57 +00:00 committed by Gerrit Code Review
commit 7f933a1bed
4 changed files with 111 additions and 23 deletions

View File

@ -87,8 +87,18 @@ field:
The "auto" mode first tries "session" and falls back
to "basic" if session authentication is not supported
by the Redfish BMC. Default is set in ironic config
as ``[redfish]auth_type``.
as ``[redfish]auth_type``. Most operators should not
need to leverage this setting. Session based
authentication should generally be used in most
cases as it prevents re-authentication every time
a background task checks in with the BMC.
.. note::
The ``redfish_address``, ``redfish_username``, ``redfish_password``,
and ``redfish_verify_ca`` fields, if changed, will trigger a new session
to be establsihed and cached with the BMC. The ``redfish_auth_type`` field
will only be used for the creation of a new cached session, or should
one be rejected by the BMC.
The ``baremetal node create`` command can be used to enroll
a node with the ``redfish`` driver. For example:
@ -620,6 +630,44 @@ Eject Virtual Media
"boot_device (optional)", "body", "string", "Type of the device to eject (all devices by default)"
Internal Session Cache
======================
The ``redfish`` hardware type, and derived interfaces, utilizes a built-in
session cache which prevents Ironic from re-authenticating every time
Ironic attempts to connect to the BMC for any reason.
This consists of cached connectors objects which are used and tracked by
a unique consideration of ``redfish_username``, ``redfish_password``,
``redfish_verify_ca``, and finally ``redfish_address``. Changing any one
of those values will trigger a new session to be created.
The ``redfish_system_id`` value is explicitly not considered as Redfish
has a model of use of one BMC to many systems, which is also a model
Ironic supports.
The session cache default size is ``1000`` sessions per conductor.
If you are operating a deployment with a larger number of Redfish
BMCs, it is advised that you do appropriately tune that number.
This can be tuned via the API service configuration file,
``[redfish]connection_cache_size``.
Session Cache Expiration
~~~~~~~~~~~~~~~~~~~~~~~~
By default, sessions remain cached for as long as possible in
memory, as long as they have not experienced an authentication,
connection, or other unexplained error.
Under normal circumstances, the sessions will only be rolled out
of the cache in order of oldest first when the cache becomes full.
There is no time based expiration to entries in the session cache.
Of course, the cache is only in memory, and restarting the
``ironic-conductor`` will also cause the cache to be rebuilt
from scratch. If this is due to any persistent connectivity issue,
this may be sign of an unexpected condition, and please consider
contacting the Ironic developer community for assistance.
.. _Redfish: http://redfish.dmtf.org/
.. _Sushy: https://opendev.org/openstack/sushy
.. _TLS: https://en.wikipedia.org/wiki/Transport_Layer_Security

View File

@ -15,6 +15,7 @@
# under the License.
import collections
import hashlib
import os
from urllib import parse as urlparse
@ -198,43 +199,59 @@ class SessionCache(object):
_sessions = collections.OrderedDict()
def __init__(self, driver_info):
# Hash the password in the data structure, so we can
# include it in the session key.
# NOTE(TheJulia): Multiplying the address by 4, to ensure
# we meet a minimum of 16 bytes for salt.
pw_hash = hashlib.pbkdf2_hmac(
'sha512',
driver_info.get('password').encode('utf-8'),
str(driver_info.get('address') * 4).encode('utf-8'), 40)
self._driver_info = driver_info
# Assemble the session key and append the hashed password to it,
# which forces new sessions to be established when the saved password
# is changed, just like the username, or address.
self._session_key = tuple(
self._driver_info.get(key)
for key in ('address', 'username', 'verify_ca')
)
) + (pw_hash.hex(),)
def __enter__(self):
try:
return self.__class__._sessions[self._session_key]
except KeyError:
auth_type = self._driver_info['auth_type']
LOG.debug('A cached redfish session for Redfish endpoint '
'%(endpoint)s was not detected, initiating a session.',
{'endpoint': self._driver_info['address']})
auth_class = self.AUTH_CLASSES[auth_type]
auth_type = self._driver_info['auth_type']
authenticator = auth_class(
username=self._driver_info['username'],
password=self._driver_info['password']
)
auth_class = self.AUTH_CLASSES[auth_type]
sushy_params = {'verify': self._driver_info['verify_ca'],
'auth': authenticator}
if 'root_prefix' in self._driver_info:
sushy_params['root_prefix'] = self._driver_info['root_prefix']
conn = sushy.Sushy(
self._driver_info['address'],
**sushy_params
)
authenticator = auth_class(
username=self._driver_info['username'],
password=self._driver_info['password']
)
if CONF.redfish.connection_cache_size:
self.__class__._sessions[self._session_key] = conn
sushy_params = {'verify': self._driver_info['verify_ca'],
'auth': authenticator}
if 'root_prefix' in self._driver_info:
sushy_params['root_prefix'] = self._driver_info['root_prefix']
conn = sushy.Sushy(
self._driver_info['address'],
**sushy_params
)
if (len(self.__class__._sessions)
> CONF.redfish.connection_cache_size):
self._expire_oldest_session()
if CONF.redfish.connection_cache_size:
self.__class__._sessions[self._session_key] = conn
# Save a secure hash of the password into memory, so if we
# observe it change, we can detect the session is no longer valid.
return conn
if (len(self.__class__._sessions)
> CONF.redfish.connection_cache_size):
self._expire_oldest_session()
return conn
def __exit__(self, exc_type, exc_val, exc_tb):
# NOTE(etingof): perhaps this session token is no good

View File

@ -252,6 +252,7 @@ class RedfishUtilsAuthTestCase(db_base.DbTestCase):
redfish_utils.get_system(self.node)
redfish_utils.get_system(self.node)
self.assertEqual(1, mock_sushy.call_count)
self.assertEqual(len(redfish_utils.SessionCache._sessions), 1)
@mock.patch.object(sushy, 'Sushy', autospec=True)
def test_ensure_new_session_address(self, mock_sushy):
@ -269,6 +270,21 @@ class RedfishUtilsAuthTestCase(db_base.DbTestCase):
redfish_utils.get_system(self.node)
self.assertEqual(2, mock_sushy.call_count)
@mock.patch.object(sushy, 'Sushy', autospec=True)
def test_ensure_new_session_password(self, mock_sushy):
d_info = self.node.driver_info
d_info['redfish_username'] = 'foo'
d_info['redfish_password'] = 'bar'
self.node.driver_info = d_info
self.node.save()
redfish_utils.get_system(self.node)
d_info['redfish_password'] = 'foo'
self.node.driver_info = d_info
self.node.save()
redfish_utils.SessionCache._sessions = collections.OrderedDict()
redfish_utils.get_system(self.node)
self.assertEqual(2, mock_sushy.call_count)
@mock.patch.object(sushy, 'Sushy', autospec=True)
@mock.patch('ironic.drivers.modules.redfish.utils.'
'SessionCache.AUTH_CLASSES', autospec=True)

View File

@ -0,0 +1,7 @@
---
fixes:
- |
Fixes an issue where the Redfish session cache would continue using an
old session when a password for a Redfish BMC was changed. Now the old
session will not be found in this case, and a new session will be created
with the latest credential information available.