diff --git a/docs/changes.rst b/docs/changes.rst index 3cbc6210..f7d71801 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -52,6 +52,10 @@ Released: not yet * Added support for LPAR Load from FTP via a new Lpar.load_from_ftp() method. (issue #1048) +* Added support for STP configuration of CPCs via new operations of + zhmcclient.Cpc: swap_current_time_server(), set_stp_config(), + change_stp_id(), join_ctn(), leave_ctn(). (issue #750) + **Cleanup:** * Fixed new issue reported by flake8 6.1.0. diff --git a/docs/resources.rst b/docs/resources.rst index 373b1251..0aa08912 100644 --- a/docs/resources.rst +++ b/docs/resources.rst @@ -71,6 +71,13 @@ CPCs :special-members: __str__ +.. autoclass:: zhmcclient.STPNode + :members: + :autosummary: + :autosummary-inherited-members: + :special-members: __str__ + + .. _`Unmanaged CPCs`: Unmanaged CPCs @@ -569,4 +576,3 @@ Certificates :autosummary: :autosummary-inherited-members: :special-members: __str__ - diff --git a/tests/unit/zhmcclient/test_cpc.py b/tests/unit/zhmcclient/test_cpc.py index 1e337047..e1b7faac 100644 --- a/tests/unit/zhmcclient/test_cpc.py +++ b/tests/unit/zhmcclient/test_cpc.py @@ -21,11 +21,16 @@ import re import copy import pytest +import requests_mock -from zhmcclient import Client, Cpc, HTTPError +from zhmcclient import Client, Cpc, HTTPError, STPNode from zhmcclient_mock import FakedSession from tests.common.utils import assert_resources +# pylint: disable=unused-import +from tests.common.http_mocked_fixtures import http_mocked_session # noqa: F401 +from tests.common.http_mocked_fixtures import http_mocked_cpc_dpm # noqa: F401 +# pylint: enable=unused-import # Object IDs and names of our faked CPCs: CPC1_NAME = 'cpc 1' # z13s in DPM mode @@ -1767,3 +1772,157 @@ def test_cpc_set_auto_start_list( # TODO: Test for Cpc.list_associated_storage_groups() # TODO: Test for Cpc.validate_lun_path() + + +def test_cpc_swap_cts(http_mocked_cpc_dpm): # noqa: F811 + # pylint: disable=redefined-outer-name,unused-argument + """ + Test function for Cpc.swap_current_time_server() + """ + uri = http_mocked_cpc_dpm.uri + '/operations/swap-cts' + + # Define the input parameters for the test call + stp_id = 'stp-1' + + exp_request_body = { + 'stp-id': stp_id, + } + exp_status_code = 204 + + rm_adapter = requests_mock.Adapter(case_sensitive=True) + with requests_mock.mock(adapter=rm_adapter) as m: + + m.post(uri, status_code=exp_status_code) + + result = http_mocked_cpc_dpm.swap_current_time_server(stp_id) + + assert rm_adapter.called + request_body = rm_adapter.last_request.json() + assert request_body == exp_request_body + assert result is None + + +def test_cpc_set_stp_config(http_mocked_cpc_dpm): # noqa: F811 + # pylint: disable=redefined-outer-name,unused-argument + """ + Test function for Cpc.set_stp_config() + """ + uri = http_mocked_cpc_dpm.uri + '/operations/set-stp-config' + + # Define the input parameters for the test call + stp_id = 'stp-1' + new_stp_id = None + force = True + preferred_time_server = STPNode( + object_uri=http_mocked_cpc_dpm.uri, + type='', model='', manuf='', po_manuf='', seq_num='', + node_name='' + ) + backup_time_server = None + arbiter = None + current_time_server = 'preferred' + + exp_request_body = { + 'stp-id': stp_id, + 'force': force, + 'preferred-time-server': preferred_time_server.json(), + 'current-time-server': current_time_server, + } + if new_stp_id: + exp_request_body['new-stp-id'] = new_stp_id + if backup_time_server: + exp_request_body['backup-time-server'] = backup_time_server.json() + if arbiter: + exp_request_body['arbiter'] = arbiter.json() + exp_status_code = 204 + + rm_adapter = requests_mock.Adapter(case_sensitive=True) + with requests_mock.mock(adapter=rm_adapter) as m: + + m.post(uri, status_code=exp_status_code) + + result = http_mocked_cpc_dpm.set_stp_config( + stp_id, new_stp_id, force, preferred_time_server, + backup_time_server, arbiter, current_time_server) + + assert rm_adapter.called + request_body = rm_adapter.last_request.json() + assert request_body == exp_request_body + assert result is None + + +def test_cpc_change_stp_id(http_mocked_cpc_dpm): # noqa: F811 + # pylint: disable=redefined-outer-name,unused-argument + """ + Test function for Cpc.change_stp_id() + """ + uri = http_mocked_cpc_dpm.uri + '/operations/change-stponly-ctn' + + # Define the input parameters for the test call + stp_id = 'stp-1' + + exp_request_body = { + 'stp-id': stp_id, + } + exp_status_code = 204 + + rm_adapter = requests_mock.Adapter(case_sensitive=True) + with requests_mock.mock(adapter=rm_adapter) as m: + + m.post(uri, status_code=exp_status_code) + + result = http_mocked_cpc_dpm.change_stp_id(stp_id) + + assert rm_adapter.called + request_body = rm_adapter.last_request.json() + assert request_body == exp_request_body + assert result is None + + +def test_cpc_join_ctn(http_mocked_cpc_dpm): # noqa: F811 + # pylint: disable=redefined-outer-name,unused-argument + """ + Test function for Cpc.join_ctn() + """ + uri = http_mocked_cpc_dpm.uri + '/operations/join-stponly-ctn' + + # Define the input parameters for the test call + stp_id = 'stp-1' + + exp_request_body = { + 'stp-id': stp_id, + } + exp_status_code = 204 + + rm_adapter = requests_mock.Adapter(case_sensitive=True) + with requests_mock.mock(adapter=rm_adapter) as m: + + m.post(uri, status_code=exp_status_code) + + result = http_mocked_cpc_dpm.join_ctn(stp_id) + + assert rm_adapter.called + request_body = rm_adapter.last_request.json() + assert request_body == exp_request_body + assert result is None + + +def test_cpc_leave_ctn(http_mocked_cpc_dpm): # noqa: F811 + # pylint: disable=redefined-outer-name,unused-argument + """ + Test function for Cpc.leave_ctn() + """ + uri = http_mocked_cpc_dpm.uri + '/operations/leave-stponly-ctn' + + exp_status_code = 204 + + rm_adapter = requests_mock.Adapter(case_sensitive=True) + with requests_mock.mock(adapter=rm_adapter) as m: + + m.post(uri, status_code=exp_status_code) + + result = http_mocked_cpc_dpm.leave_ctn() + + assert rm_adapter.called + assert rm_adapter.last_request.text is None + assert result is None diff --git a/zhmcclient/_cpc.py b/zhmcclient/_cpc.py index 54401500..f62ef21b 100644 --- a/zhmcclient/_cpc.py +++ b/zhmcclient/_cpc.py @@ -50,6 +50,7 @@ import warnings import copy +import json try: from collections import OrderedDict except ImportError: @@ -74,7 +75,63 @@ RC_STORAGE_PATH, RC_STORAGE_CONTROL_UNIT, RC_VIRTUAL_TAPE_RESOURCE, \ RC_TAPE_LINK, RC_TAPE_LIBRARY, RC_CERTIFICATE -__all__ = ['CpcManager', 'Cpc'] +__all__ = ['STPNode', 'CpcManager', 'Cpc'] + + +class STPNode(object): + # pylint: disable=too-few-public-methods + """ + Data structure defining a CPC that is referenced by an STP configuration. + + That CPC does not need to be managed by the HMC. + + This object is used in the following methods: + + * :meth:`zhmcclient.Cpc.set_stp_config` + """ + + def __init__( + self, object_uri, type, model, manuf, po_manuf, seq_num, node_name): + # pylint: disable=redefined-builtin + """ + Parameters: + object_uri (string): The object-uri of the CPC, if the CPC is managed + by the HMC. Otherwise, `None`. + type (string): Machine type of the CPC (6 chars left padded with + zeros), or an empty string. + model (string): Machine model of the CPC (3 chars), or an empty + string. + manuf (string): Manufacturer of the CPC (3 chars), or an empty + string. + po_manuf (string): Plant code of the manufacturer of the CPC + (2 chars), or an empty string. + seq_num (string): Sequence number of the CPC (12 chars), or an empty + string. + node_name (string): Name of the CPC (1-8 chars). + """ + self.object_uri = object_uri + self.type = type + self.model = model + self.manuf = manuf + self.po_manuf = po_manuf + self.seq_num = seq_num + self.node_name = node_name + + def json(self): + """ + Returns this data structure as a JSON string suitable for being used as + an argument in methods that use this data structure. + """ + ret_dict = { + 'object-uri': self.object_uri, + 'type': self.type, + 'model': self.model, + 'manuf': self.manuf, + 'po-manuf': self.po_manuf, + 'seq-num': self.seq_num, + 'node-name': self.node_name, + } + return json.dumps(ret_dict) class CpcManager(BaseManager): @@ -2181,6 +2238,211 @@ def single_step_install( operation_timeout=operation_timeout) return result + @logged_api_call + def swap_current_time_server(self, stp_id): + """ + Makes this CPC the current time server of an STP-only Coordinated Timing + Network (CTN). + + This is done by performing the "Swap Current Time Server" operation. + + The CTN must be STP-only; mixed CTNs will be rejected. + + Authorization requirements: + + * Object-access permission to this CPC. + * Task permission to the "Manage System Time" and "Modify Assigned + Server Roles" tasks. + + Parameters: + + stp_id (string): STP identifier of the CTN. + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + """ + body = { + 'stp-id': stp_id, + } + self.manager.session.post( + self.uri + '/operations/swap-cts', body=body) + + @logged_api_call + def set_stp_config( + self, stp_id, new_stp_id, force, preferred_time_server, + backup_time_server, arbiter, current_time_server): + """ + Sets the configuration of the STP-only Coordinated Timing Network + (CTN) whose current time server is this CPC. + + This is done by performing the "Set STP Configuration" operation. + + The CTN must be STP-only; mixed CTNs will be rejected. + + Authorization requirements: + + * Object-access permission to this CPC. + * Task permission to the "Manage System Time" and "Modify Assigned + Server Roles" tasks. + + Parameters: + + stp_id (string): Current STP identifier of the CTN to be updated. + This CPC must be the current time server of the CTN. + + new_stp_id (string): The new STP identifier for the CTN. + If `None`, the STP identifier of the CTN is not changed. + + force (bool): Required. Indicates whether a disruptive operation is + allowed (`True`) or rejected (`False`). Must not be `None`. + + preferred_time_server (zhmcclient.STPNode): Identifies the CPC to be + the preferred time server of the CTN. Must not be `None`. + + backup_time_server (zhmcclient.STPNode): Identifies the CPC to be the + backup time server of the CTN. If `None`, the CTN will have no + backup time server. + + arbiter (zhmcclient.STPNode): Identifies the CPC to be the arbiter for + the CTN. If `None`, the CTN will have no arbiter. + + current_time_server (string): Identifies which time server takes on + the role of the current time server. Must be one of: + + * "preferred" - the preferred time server is the current time server + * "backup" - the backup time server is the current time server + + Must not be `None`. + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + """ + body = { + 'stp-id': stp_id, + 'force': force, + 'preferred-time-server': preferred_time_server.json(), + 'current-time-server': current_time_server, + } + if new_stp_id: + body['new-stp-id'] = new_stp_id + if backup_time_server: + body['backup-time-server'] = backup_time_server.json() + if arbiter: + body['arbiter'] = arbiter.json() + self.manager.session.post( + self.uri + '/operations/set-stp-config', body=body) + + @logged_api_call + def change_stp_id(self, stp_id): + """ + Changes the STP identifier of the STP-only Coordinated Timing Network + (CTN) whose current time server is this CPC. + + This is done by performing the "Change STP-only Coordinated Timing + Network" operation. + + The CTN must be STP-only; mixed CTNs will be rejected. + + Authorization requirements: + + * Object-access permission to this CPC. + * Task permission to the "Manage System Time" and "Rename CTN" tasks. + + Parameters: + + stp_id (string): STP identifier of the CTN to be updated. + This CPC must be the current time server of the CTN. + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + """ + body = { + 'stp-id': stp_id, + } + self.manager.session.post( + self.uri + '/operations/change-stponly-ctn', body=body) + + @logged_api_call + def join_ctn(self, stp_id): + """ + Causes this CPC to join an STP-only Coordinated Timing Network (CTN). + + This is done by performing the "Join STP-only Coordinated Timing + Network" operation. + + If the CPC is already a member of a different CTN but not in the role + of the current time server, it is removed from that CTN. + + If the CPC object has an ETR ID, the ETR ID is removed. + + The CPC must not be the current time server of a different CTN. + + The CTN must be STP-only; mixed CTNs will be rejected. + + Authorization requirements: + + * Object-access permission to this CPC. + * Task permission to the "Manage System Time" and "Add Systems to CTN" + tasks. + + Parameters: + + stp_id (string): STP identifier of the CTN to be joined. + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + """ + body = { + 'stp-id': stp_id, + } + self.manager.session.post( + self.uri + '/operations/join-stponly-ctn', body=body) + + @logged_api_call + def leave_ctn(self): + """ + Causes this CPC to leave its current STP-only Coordinated Timing + Network (CTN). + + This is done by performing the "Leave STP-only Coordinated Timing + Network" operation. + + The CTN must be STP-only; mixed CTNs will be rejected. + + The CPC must not be the current time server of its current CTN. + + Authorization requirements: + + * Object-access permission to this CPC. + * Task permission to the "Manage System Time" and "Remove Systems from + CTN" tasks. + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + """ + self.manager.session.post( + self.uri + '/operations/leave-stponly-ctn') + def _convert_to_config(self, inventory_list, include_unused_adapters): """ Convert the inventory list to a DPM configuration dict.