Skip to content

Commit

Permalink
Mock: Added partial support for session IDs
Browse files Browse the repository at this point in the history
Note: Removal of session IDs in the mocked Logoff operation and validation
      of session IDs in any mocked operations that require to be logged on
      has not been implemented in this change. That would require passing
      the HTTP header fields to the mock support, which is not the case today
      and would require larger changes.

      However, since addition/removal/validation of session IDs is implemented
      for the FakedHmc class, this should be sufficient to provide the session
      ID support required for zhmcclient/zhmccli#413.

Details:

* Added support for dynamically creating a session ID in the
  mocked Logon operation (i.e. _urihandler.SessionsHandler).

* The new session ID is stored in the zhmcclient_mock.FakedHmc
  object in new support for adding, removing and validating
  session IDs.

* Added unit test cases for the session ID support in the
  zhmcclient_mock.FakedHmc class.

Signed-off-by: Andreas Maier <[email protected]>
  • Loading branch information
andy-maier committed Mar 12, 2024
1 parent 25d2aec commit cb29032
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 2 deletions.
95 changes: 95 additions & 0 deletions design/fakedsession.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Support for multiple sessions in zhmcclient mock support

Issue https://github.com/zhmcclient/python-zhmcclient/issues/1437 asks for
adding support for unique session IDs in the zhmcclient mock support.

There are two aspects to this:
1. Generating a unique session ID
2. Supporting multiple FakedSession objects for the same FakedHmc object

The first aspect can easily be implemented with the current design.

The difficulty for the second aspect is that the current design makes the
FakedHmc object a data item within the FakedSession object that is automatically
generated, whereas a proper session support would work such that the FakedHmc
object exists independent of the FakedSession object, and multiple FakedSession
objects could be created against the same FakedHmc object.

This document describes the relevant parts of the current design, to have a
basis for a future support for multiple FakedSession objects for the same
FakedHmc object.

## Current design

class FakedHmc(FakedBaseResource):

init parms:
session, hmc_name, hmc_version, api_version
attributes:
super(FakedHmc, self).__init__(
manager=None, properties=None)
self._session = session # FakedSession object
self.hmc_name = hmc_name
self.hmc_version = hmc_version
self.api_version = api_version

self.cpcs = FakedCpcManager(...)
self.metrics_contexts = FakedMetricsContextManager(...)
self.consoles = FakedConsoleManager(...)

self._metric_groups
self._metric_values

self.all_resources
self._enabled

class FakedSession(zhmcclient.Session):

init parms:
host, hmc_name, hmc_version, api_version, userid=None, password=None
attributes:
super(FakedSession, self).__init__(
host, userid=userid, password=password)
self._hmc = FakedHmc(self, hmc_name, hmc_version, api_version)
self._urihandler = UriHandler(URIS)
self._object_topic = 'faked-notification-topic'
self._job_topic = 'faked-job-notification-topic'

Handler for "Logon" (POST /api/sessions):

def post(method, hmc, uri, uri_parms, body, logon_required,
wait_for_completion):
assert wait_for_completion is True # synchronous operation
check_required_fields(method, uri, body, ['userid', 'password'])
result = {
'api-session': 'fake-session-id',
'notification-topic': 'fake-topic-1',
'job-notification-topic': 'fake-topic-2',
'api-major-version': 4,
'api-minor-version': 40,
'password-expires': -1,
# 'shared-secret-key' not included
'session-credential': uuid.uuid4().hex,
}
return result

Handler for "Logoff" (DELETE /api/sessions/this-session):

def delete(method, hmc, uri, uri_parms, logon_required):
pass

Typical use:

session = FakedSession('fake-host', 'fake-hmc', '2.13.1', '1.8')
session.hmc.consoles.add({
'object-id': None,
# object-uri will be automatically set
'parent': None,
'class': 'console',
'name': 'fake-console1',
'description': 'Console #1',
})

# From here on, normal zhmcclient classes/methods are used:
client = Client(session)
console = client.consoles.find(name=...)
26 changes: 26 additions & 0 deletions tests/unit/zhmcclient_mock/test_hmc.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from datetime import datetime
from dateutil import tz
import pytz
import pytest

from zhmcclient_mock._session import FakedSession
from zhmcclient_mock._hmc import \
Expand Down Expand Up @@ -241,6 +242,31 @@ def test_hmc_add_resources(self):
assert port1.properties == port1_out_props
assert port1.manager == adapter1.ports

def test_hmc_session_id(self):
"""Test FakedHmc.validate/add/remove_session_id()."""

hmc = self.hmc

session_id = 'valid_id'

hmc.add_session_id(session_id)

result = hmc.validate_session_id(session_id)
assert result is True

# Adding an already valid ID fails
with pytest.raises(ValueError):
hmc.add_session_id(session_id)

hmc.remove_session_id(session_id)

result = hmc.validate_session_id(session_id)
assert result is False

# Removing an invalid ID fails
with pytest.raises(ValueError):
hmc.remove_session_id(session_id)


class TestFakedBase(object):
"""All tests for the FakedBaseManager and FakedBaseResource classes."""
Expand Down
56 changes: 56 additions & 0 deletions zhmcclient_mock/_hmc.py
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,7 @@ def __init__(self, session, hmc_name, hmc_version, api_version):
self.hmc_name = hmc_name
self.hmc_version = hmc_version
self.api_version = api_version
self._valid_session_ids = set()

self._metric_groups = {} # by metric group name
for mg_dict in METRIC_GROUPS:
Expand Down Expand Up @@ -926,6 +927,7 @@ def __repr__(self):
" metrics_contexts = {metrics_contexts}\n"
" consoles = {consoles}\n"
" all_resources (keys only) = {all_resource_keys}\n"
" valid_session_ids = {valid_session_ids}\n"
")".format(
classname=self.__class__.__name__,
id=id(self),
Expand All @@ -941,6 +943,7 @@ def __repr__(self):
consoles=repr_manager(self.consoles, indent=2),
all_resource_keys=repr_list(self.all_resources.keys(),
indent=2),
valid_session_ids=self._valid_session_ids,
))
return ret

Expand Down Expand Up @@ -999,6 +1002,59 @@ def disable(self):
"""
self._enabled = False

def validate_session_id(self, session_id):
"""
Return boolean indicating whether a session ID is valid.
Parameters:
session_id (string):
The session ID to be validated.
Returns:
bool: Whether the session ID is valid.
"""
return session_id in self._valid_session_ids

def add_session_id(self, session_id):
"""
Add a session ID to the valid session IDs.
If the session ID already exists, ValueError is raised.
Parameters:
session_id (string):
The session ID to be added.
Raises:
ValueError: Session ID already exists.
"""
if session_id in self._valid_session_ids:
raise ValueError(
"Session ID {} already exists".format(session_id))

self._valid_session_ids.add(session_id)

def remove_session_id(self, session_id):
"""
Remove a session ID from the valid session IDs.
If the session ID does not exist, ValueError is raised.
Parameters:
session_id (string):
The session ID to be removed.
Raises:
ValueError: Session ID does not exist.
"""
try:
self._valid_session_ids.remove(session_id)
except KeyError:
raise ValueError("Session ID {} does not exist".format(session_id))

def lookup_by_uri(self, uri):
"""
Look up a faked resource by its object URI, within this faked HMC.
Expand Down
10 changes: 8 additions & 2 deletions zhmcclient_mock/_urihandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -620,15 +620,18 @@ def post(method, hmc, uri, uri_parms, body, logon_required,
"""Operation: Logon."""
assert wait_for_completion is True # synchronous operation
check_required_fields(method, uri, body, ['userid', 'password'])
session_id = uuid.uuid4().hex
session_cred = session_id + '-cred'
hmc.add_session_id(session_id)
result = {
'api-session': 'fake-session-id',
'api-session': session_id,
'notification-topic': 'fake-topic-1',
'job-notification-topic': 'fake-topic-2',
'api-major-version': 4,
'api-minor-version': 40,
'password-expires': -1,
# 'shared-secret-key' not included
'session-credential': uuid.uuid4().hex,
'session-credential': session_cred,
}
return result

Expand All @@ -642,6 +645,9 @@ class ThisSessionHandler(object):
def delete(method, hmc, uri, uri_parms, logon_required):
# pylint: disable=unused-argument
"""Operation: Logoff."""
# TODO: Add support for removing the current session ID. This requires
# passing the request headers, which requires changing the
# FakedSession class to get control at the HTTP level.
pass


Expand Down

0 comments on commit cb29032

Please sign in to comment.