From 1a7f0da3ea943eba4fca7a07a93b130bfa5ca63a Mon Sep 17 00:00:00 2001 From: Vonteddu Chaithra Date: Thu, 26 Feb 2026 14:58:00 +0530 Subject: [PATCH] Added support for tape link and virtual tape resource Signed-off-by: Vonteddu Chaithra --- docs/appendix.rst | 22 +- docs/resources.rst | 40 + tests/end2end/mocked_hmc_z16.yaml | 63 ++ tests/end2end/test_tape_link.py | 365 +++++++++ tests/end2end/test_virtual_tape_resource.py | 423 ++++++++++ tests/unit/zhmcclient/test_tape_link.py | 637 +++++++++++++++ .../zhmcclient/test_virtual_tape_resource.py | 638 +++++++++++++++ zhmcclient/__init__.py | 2 + zhmcclient/_console.py | 4 + zhmcclient/_tape_library.py | 13 + zhmcclient/_tape_link.py | 736 ++++++++++++++++++ zhmcclient/_virtual_tape_resource.py | 327 ++++++++ zhmcclient/mock/_hmc.py | 150 ++++ zhmcclient/mock/_session.py | 44 +- zhmcclient/mock/_urihandler.py | 346 ++++++++ 15 files changed, 3804 insertions(+), 6 deletions(-) create mode 100644 tests/end2end/test_tape_link.py create mode 100644 tests/end2end/test_virtual_tape_resource.py create mode 100644 tests/unit/zhmcclient/test_tape_link.py create mode 100644 tests/unit/zhmcclient/test_virtual_tape_resource.py create mode 100644 zhmcclient/_tape_link.py create mode 100644 zhmcclient/_virtual_tape_resource.py diff --git a/docs/appendix.rst b/docs/appendix.rst index 590477bf..23dfbd61 100644 --- a/docs/appendix.rst +++ b/docs/appendix.rst @@ -540,12 +540,26 @@ Resources scoped to CPCs in DPM mode Storage Volume Template A template for :term:`Storage Volumes `. - Tape library + Tape Library A Tape Library represents one physical tape storage unit connected to - a CPC.Tape libraries are automatically discovered,but discovery requires - preprocessing,A single Worldwide Port Name (WWPN) must be zoned so the CPC + a CPC. Tape libraries are automatically discovered, but discovery requires + preprocessing. A single Worldwide Port Name (WWPN) must be zoned so the CPC can see the tape library. - For details, see section :ref:`Tape Library`. + For details, see section :ref:`Tape Libraries`. + + Tape Link + A Tape Link connects a :term:`Tape Library` to one or more :term:`Partitions `, + enabling the partitions to access tape drives in the tape library. A tape link + defines the connectivity and resource allocation for tape access. + For details, see section :ref:`Tape Links`. + + Virtual Tape Resource + A representation of a tape-related z/Architecture device in a :term:`Partition`. + A Virtual Tape Resource object represents access to a tape drive through a + :term:`Tape Link`. Each virtual tape resource is associated with a specific + adapter port and provides the partition with a device number for accessing + the tape drive. + For details, see section :ref:`Virtual Tape Resources`. vHBA Synonym for :term:`HBA`. In this resource model, HBAs are always diff --git a/docs/resources.rst b/docs/resources.rst index 221d5bba..b71bab43 100644 --- a/docs/resources.rst +++ b/docs/resources.rst @@ -358,6 +358,46 @@ Tape Libraries :special-members: __str__ +.. _`Tape Links`: + +Tape Links +---------- + +.. automodule:: zhmcclient._tape_link + +.. autoclass:: zhmcclient.TapeLinkManager + :members: + :autosummary: + :autosummary-inherited-members: + :special-members: __str__ + +.. autoclass:: zhmcclient.TapeLink + :members: + :autosummary: + :autosummary-inherited-members: + :special-members: __str__ + + +.. _`Virtual Tape Resources`: + +Virtual Tape Resources +---------------------- + +.. automodule:: zhmcclient._virtual_tape_resource + +.. autoclass:: zhmcclient.VirtualTapeResourceManager + :members: + :autosummary: + :autosummary-inherited-members: + :special-members: __str__ + +.. autoclass:: zhmcclient.VirtualTapeResource + :members: + :autosummary: + :autosummary-inherited-members: + :special-members: __str__ + + .. _`Virtual Storage Resources`: Virtual Storage Resources diff --git a/tests/end2end/mocked_hmc_z16.yaml b/tests/end2end/mocked_hmc_z16.yaml index c08ad39b..6f3e1d92 100644 --- a/tests/end2end/mocked_hmc_z16.yaml +++ b/tests/end2end/mocked_hmc_z16.yaml @@ -364,6 +364,69 @@ hmc_definition: name : Tape Library description : Tape Library resource 1 cpc-uri : /api/cpcs/cpc_dpm + tape_links: + - properties: + # class: created automatically + # parent: created automatically + # element-uri: created automatically + element-id: tlink1 + name: "Tape Link 1" + description: "Tape link connecting Tape Library to PART1" + partition-uri: /api/partitions/part1 + tape-library-uri: /api/tape-libraries/tl1 + virtual_tape_resources: + - properties: + # class: created automatically + # parent: created automatically + # element-uri: created automatically + element-id: vtr1 + name: "Virtual Tape Resource 1" + description: "Virtual tape resource 1" + device-number: "0001" + adapter-port-uri: /api/adapters/fcp1/storage-ports/0 + partition-uri: /api/partitions/part1 + world-wide-port-name-info: + status: validated + world-wide-port-name: "c05076ffe8000001" + degraded-reasons: [] + - properties: + # class: created automatically + # parent: created automatically + # element-uri: created automatically + element-id: vtr2 + name: "Virtual Tape Resource 2" + description: "Virtual tape resource 2" + device-number: "0002" + adapter-port-uri: /api/adapters/fcp1/storage-ports/1 + partition-uri: /api/partitions/part1 + world-wide-port-name-info: + status: validated + world-wide-port-name: "c05076ffe8000002" + degraded-reasons: [] + - properties: + # class: created automatically + # parent: created automatically + # element-uri: created automatically + element-id: tlink2 + name: "Tape Link 2" + description: "Tape link for testing" + partition-uri: /api/partitions/part1 + tape-library-uri: /api/tape-libraries/tl1 + virtual_tape_resources: + - properties: + # class: created automatically + # parent: created automatically + # element-uri: created automatically + element-id: vtr3 + name: "Virtual Tape Resource 3" + description: "Virtual tape resource 3" + device-number: "0003" + adapter-port-uri: /api/adapters/fcp1/storage-ports/0 + partition-uri: /api/partitions/part1 + world-wide-port-name-info: + status: not-validated + world-wide-port-name: "c05076ffe8000003" + degraded-reasons: [] hw_messages: - properties: diff --git a/tests/end2end/test_tape_link.py b/tests/end2end/test_tape_link.py new file mode 100644 index 00000000..b9cc0f00 --- /dev/null +++ b/tests/end2end/test_tape_link.py @@ -0,0 +1,365 @@ +# Copyright 2026 IBM Corp. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +End2end tests for Tape Links (on CPCs in DPM mode). + +These tests create, modify, and delete test Tape Links. +""" + +import pytest +from requests.packages import urllib3 + +import zhmcclient + +from .utils import skip_warn, pick_test_resources, \ + runtest_find_list, runtest_get_properties + +urllib3.disable_warnings() + +# Properties in minimalistic Tape Link objects (e.g. find_by_name()) +TLINK_MINIMAL_PROPS = ['element-uri', 'name'] + +# Properties in Tape Link objects returned by list() without full props +TLINK_LIST_PROPS = ['element-uri', 'name', 'partition-uri', + 'tape-library-uri', 'description'] + +# Properties whose values can change between retrievals of Tape Link objects +TLINK_VOLATILE_PROPS = [] + + +def test_tlink_find_list(hmc_session): + """ + Test list(), find(), findall() for Tape Links. + """ + client = zhmcclient.Client(hmc_session) + console = client.consoles.console + hd = hmc_session.hmc_definition + + # Pick the Tape Library to test with + tl_list = console.tape_library.list() + if not tl_list: + skip_warn(f"No Tape Library defined on HMC {hd.host}") + tl_list = pick_test_resources(tl_list) + + for tl in tl_list: + print(f"Testing with Tape Library {tl.name!r}") + + # Get tape links for this tape library + tlink_list = tl.tape_links.list() + if not tlink_list: + skip_warn(f"No Tape Links defined for Tape Library {tl.name!r}") + continue + + tlink_list = pick_test_resources(tlink_list) + + for tlink in tlink_list: + print(f"Testing with Tape Link {tlink.name!r}") + runtest_find_list( + hmc_session, tl.tape_links, tlink.name, + 'name', 'element-uri', TLINK_VOLATILE_PROPS, + TLINK_MINIMAL_PROPS, TLINK_LIST_PROPS) + + +def test_tlink_property(hmc_session): + """ + Test property related methods for Tape Links. + """ + client = zhmcclient.Client(hmc_session) + console = client.consoles.console + hd = hmc_session.hmc_definition + + # Pick the Tape Library to test with + tl_list = console.tape_library.list() + if not tl_list: + skip_warn(f"No Tape Library defined on HMC {hd.host}") + tl_list = pick_test_resources(tl_list) + + for tl in tl_list: + print(f"Testing with Tape Library {tl.name!r}") + + # Get tape links for this tape library + tlink_list = tl.tape_links.list() + if not tlink_list: + skip_warn(f"No Tape Links defined for Tape Library {tl.name!r}") + continue + + tlink_list = pick_test_resources(tlink_list) + + for tlink in tlink_list: + print(f"Testing with Tape Link {tlink.name!r}") + + # Select a property that is not returned by list() + non_list_prop = 'class' + + runtest_get_properties(tlink.manager, non_list_prop) + + +def test_tlink_crud(hmc_session): + """ + Test create, update, and delete operations for Tape Links. + """ + client = zhmcclient.Client(hmc_session) + console = client.consoles.console + hd = hmc_session.hmc_definition + + # Pick the Tape Library to test with + tl_list = console.tape_library.list() + if not tl_list: + skip_warn(f"No Tape Library defined on HMC {hd.host}") + tl = tl_list[0] + + print(f"Testing with Tape Library {tl.name!r}") + + # Get the CPC for this tape library + cpc_uri = tl.get_property('cpc-uri') + cpc = client.cpcs.find(**{'object-uri': cpc_uri}) + + # Pick a partition to test with + part_list = cpc.partitions.list() + if not part_list: + skip_warn(f"No Partitions defined on CPC {cpc.name!r}") + partition = part_list[0] + + print(f"Testing with Partition {partition.name!r}") + + # Test creating a Tape Link + tlink_name = 'test-tape-link-e2e' + tlink_props = { + 'name': tlink_name, + 'description': 'Test tape link for end-to-end testing', + 'partition-uri': partition.uri, + } + + # Clean up any existing test tape link + try: + existing_tlink = tl.tape_links.find(name=tlink_name) + existing_tlink.delete() + print(f"Deleted existing test Tape Link {tlink_name!r}") + except zhmcclient.NotFound: + pass + + # The code to be tested: Create + tlink = tl.tape_links.create(tlink_props) + + try: + assert tlink.properties['name'] == tlink_name + assert tlink.properties['partition-uri'] == partition.uri + print(f"Created Tape Link {tlink.name!r}") + + # Test updating properties + new_desc = "Updated tape link description for e2e testing" + + # The code to be tested: Update + tlink.update_properties({'description': new_desc}) + + assert tlink.properties['description'] == new_desc + tlink.pull_full_properties() + assert tlink.properties['description'] == new_desc + print(f"Updated Tape Link {tlink.name!r} description") + + # Test renaming + new_name = 'test-tape-link-e2e-renamed' + + # The code to be tested: Rename + tlink.update_properties({'name': new_name}) + tlink.pull_full_properties() + + assert tlink.properties['name'] == new_name + with pytest.raises(zhmcclient.NotFound): + tl.tape_links.find(name=tlink_name) + print(f"Renamed Tape Link to {new_name!r}") + + finally: + # Clean up: Delete the test tape link + try: + tlink.delete() + print(f"Deleted test Tape Link {tlink.name!r}") + except zhmcclient.NotFound: + pass + + +def test_tlink_get_partitions(hmc_session): + """ + Test get_partitions() method for Tape Links. + """ + client = zhmcclient.Client(hmc_session) + console = client.consoles.console + hd = hmc_session.hmc_definition + + # Pick the Tape Library to test with + tl_list = console.tape_library.list() + if not tl_list: + skip_warn(f"No Tape Library defined on HMC {hd.host}") + tl_list = pick_test_resources(tl_list) + + for tl in tl_list: + print(f"Testing with Tape Library {tl.name!r}") + + # Get tape links for this tape library + tlink_list = tl.tape_links.list() + if not tlink_list: + skip_warn(f"No Tape Links defined for Tape Library {tl.name!r}") + continue + + tlink_list = pick_test_resources(tlink_list) + + for tlink in tlink_list: + print(f"Testing with Tape Link {tlink.name!r}") + + # The code to be tested + partitions = tlink.get_partitions() + + assert isinstance(partitions, list) + print(f"Tape Link {tlink.name!r} has {len(partitions)} " + f"partition(s)") + + for partition in partitions: + assert isinstance(partition, zhmcclient.Partition) + assert 'object-uri' in partition.properties + assert 'name' in partition.properties + assert 'status' in partition.properties + print(f" Partition: {partition.name!r}, " + f"Status: {partition.properties['status']}") + + +def test_tlink_get_histories(hmc_session): + """ + Test get_histories() method for Tape Links. + """ + client = zhmcclient.Client(hmc_session) + console = client.consoles.console + hd = hmc_session.hmc_definition + + # Pick the Tape Library to test with + tl_list = console.tape_library.list() + if not tl_list: + skip_warn(f"No Tape Library defined on HMC {hd.host}") + tl_list = pick_test_resources(tl_list) + + for tl in tl_list: + print(f"Testing with Tape Library {tl.name!r}") + + # Get tape links for this tape library + tlink_list = tl.tape_links.list() + if not tlink_list: + skip_warn(f"No Tape Links defined for Tape Library {tl.name!r}") + continue + + tlink_list = pick_test_resources(tlink_list) + + for tlink in tlink_list: + print(f"Testing with Tape Link {tlink.name!r}") + + # The code to be tested + histories = tlink.get_histories() + + assert isinstance(histories, dict) + print(f"Retrieved histories for Tape Link {tlink.name!r}") + + # The response should contain tape-link-histories + if 'tape-link-histories' in histories: + history_list = histories['tape-link-histories'] + print(f" Found {len(history_list)} history record(s)") + + +def test_tlink_environment_report(hmc_session): + """ + Test get_environment_report() and update_environment_report() methods. + """ + client = zhmcclient.Client(hmc_session) + console = client.consoles.console + hd = hmc_session.hmc_definition + + # Pick the Tape Library to test with + tl_list = console.tape_library.list() + if not tl_list: + skip_warn(f"No Tape Library defined on HMC {hd.host}") + tl_list = pick_test_resources(tl_list) + + for tl in tl_list: + print(f"Testing with Tape Library {tl.name!r}") + + # Get tape links for this tape library + tlink_list = tl.tape_links.list() + if not tlink_list: + skip_warn(f"No Tape Links defined for Tape Library {tl.name!r}") + continue + + tlink_list = pick_test_resources(tlink_list) + + for tlink in tlink_list: + print(f"Testing with Tape Link {tlink.name!r}") + + # The code to be tested: Get environment report + report = tlink.get_environment_report() + + assert isinstance(report, dict) + print(f"Retrieved environment report for Tape Link {tlink.name!r}") + + # The code to be tested: Update environment report + # Note: The actual properties that can be updated depend on the + # HMC API specification. This is a basic test. + update_props = { + 'test-field': 'test-value' + } + + try: + result = tlink.update_environment_report(update_props) + assert isinstance(result, dict) + print(f"Updated environment report for Tape Link " + f"{tlink.name!r}") + except zhmcclient.HTTPError as exc: + # Some properties might not be updatable, which is acceptable + if exc.http_status == 400: + print(f"Update not supported (expected): {exc}") + else: + raise + + +def test_tlink_partition_property(hmc_session): + """ + Test the partition property of Tape Link. + """ + client = zhmcclient.Client(hmc_session) + console = client.consoles.console + hd = hmc_session.hmc_definition + + # Pick the Tape Library to test with + tl_list = console.tape_library.list() + if not tl_list: + skip_warn(f"No Tape Library defined on HMC {hd.host}") + tl_list = pick_test_resources(tl_list) + + for tl in tl_list: + print(f"Testing with Tape Library {tl.name!r}") + + # Get tape links for this tape library + tlink_list = tl.tape_links.list() + if not tlink_list: + skip_warn(f"No Tape Links defined for Tape Library {tl.name!r}") + continue + + tlink_list = pick_test_resources(tlink_list) + + for tlink in tlink_list: + print(f"Testing with Tape Link {tlink.name!r}") + + # The code to be tested + partition = tlink.partition + + assert isinstance(partition, zhmcclient.Partition) + assert partition.uri == tlink.get_property('partition-uri') + print(f"Tape Link {tlink.name!r} is linked to " + f"Partition {partition.name!r}") diff --git a/tests/end2end/test_virtual_tape_resource.py b/tests/end2end/test_virtual_tape_resource.py new file mode 100644 index 00000000..13464e17 --- /dev/null +++ b/tests/end2end/test_virtual_tape_resource.py @@ -0,0 +1,423 @@ +# Copyright 2026 IBM Corp. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +End2end tests for Virtual Tape Resources (on CPCs in DPM mode). + +These tests list, retrieve properties, and update Virtual Tape Resources. +""" + +from requests.packages import urllib3 + +import zhmcclient + +from .utils import skip_warn, pick_test_resources, \ + runtest_find_list, runtest_get_properties + +urllib3.disable_warnings() + +# Properties in minimalistic Virtual Tape Resource objects (e.g. find_by_name()) +VTR_MINIMAL_PROPS = ['element-uri', 'name'] + +# Properties in Virtual Tape Resource objects returned by list() +# without full props +VTR_LIST_PROPS = ['element-uri', 'name', 'device-number', + 'adapter-port-uri', 'partition-uri'] + +# Properties whose values can change between retrievals of Virtual +# Tape Resource objects +VTR_VOLATILE_PROPS = [] + + +def test_vtr_find_list(hmc_session): + """ + Test list(), find(), findall() for Virtual Tape Resources. + """ + client = zhmcclient.Client(hmc_session) + console = client.consoles.console + hd = hmc_session.hmc_definition + + # Pick the Tape Library to test with + tl_list = console.tape_library.list() + if not tl_list: + skip_warn(f"No Tape Library defined on HMC {hd.host}") + tl_list = pick_test_resources(tl_list) + + for tl in tl_list: + print(f"Testing with Tape Library {tl.name!r}") + + # Get tape links for this tape library + tlink_list = tl.tape_links.list() + if not tlink_list: + skip_warn(f"No Tape Links defined for Tape Library {tl.name!r}") + continue + + tlink_list = pick_test_resources(tlink_list) + + for tlink in tlink_list: + print(f"Testing with Tape Link {tlink.name!r}") + + # Get virtual tape resources for this tape link + vtr_list = tlink.virtual_tape_resources.list() + if not vtr_list: + skip_warn(f"No Virtual Tape Resources defined for " + f"Tape Link {tlink.name!r}") + continue + + vtr_list = pick_test_resources(vtr_list) + + for vtr in vtr_list: + print(f"Testing with Virtual Tape Resource " + f"{vtr.name!r}") + runtest_find_list( + hmc_session, tlink.virtual_tape_resources, vtr.name, + 'name', 'element-uri', VTR_VOLATILE_PROPS, + VTR_MINIMAL_PROPS, VTR_LIST_PROPS) + + +def test_vtr_property(hmc_session): + """ + Test property related methods for Virtual Tape Resources. + """ + client = zhmcclient.Client(hmc_session) + console = client.consoles.console + hd = hmc_session.hmc_definition + + # Pick the Tape Library to test with + tl_list = console.tape_library.list() + if not tl_list: + skip_warn(f"No Tape Library defined on HMC {hd.host}") + tl_list = pick_test_resources(tl_list) + + for tl in tl_list: + print(f"Testing with Tape Library {tl.name!r}") + + # Get tape links for this tape library + tlink_list = tl.tape_links.list() + if not tlink_list: + skip_warn(f"No Tape Links defined for Tape Library {tl.name!r}") + continue + + tlink_list = pick_test_resources(tlink_list) + + for tlink in tlink_list: + print(f"Testing with Tape Link {tlink.name!r}") + + # Get virtual tape resources for this tape link + vtr_list = tlink.virtual_tape_resources.list() + if not vtr_list: + skip_warn(f"No Virtual Tape Resources defined for " + f"Tape Link {tlink.name!r}") + continue + + vtr_list = pick_test_resources(vtr_list) + + for vtr in vtr_list: + print(f"Testing with Virtual Tape Resource " + f"{vtr.name!r}") + + # Select a property that is not returned by list() + non_list_prop = 'class' + + runtest_get_properties(vtr.manager, non_list_prop) + + +def test_vtr_update_properties(hmc_session): + """ + Test update_properties() for Virtual Tape Resources. + """ + client = zhmcclient.Client(hmc_session) + console = client.consoles.console + hd = hmc_session.hmc_definition + + # Pick the Tape Library to test with + tl_list = console.tape_library.list() + if not tl_list: + skip_warn(f"No Tape Library defined on HMC {hd.host}") + tl_list = pick_test_resources(tl_list) + + for tl in tl_list: + print(f"Testing with Tape Library {tl.name!r}") + + # Get tape links for this tape library + tlink_list = tl.tape_links.list() + if not tlink_list: + skip_warn(f"No Tape Links defined for Tape Library {tl.name!r}") + continue + + tlink_list = pick_test_resources(tlink_list) + + for tlink in tlink_list: + print(f"Testing with Tape Link {tlink.name!r}") + + # Get virtual tape resources for this tape link + vtr_list = tlink.virtual_tape_resources.list() + if not vtr_list: + skip_warn(f"No Virtual Tape Resources defined for " + f"Tape Link {tlink.name!r}") + continue + + vtr_list = pick_test_resources(vtr_list) + + for vtr in vtr_list: + print(f"Testing with Virtual Tape Resource " + f"{vtr.name!r}") + + # Get current properties + vtr.pull_full_properties() + original_desc = vtr.get_property('description') + + # Update description + new_desc = 'Updated by end2end test' + print(f"Updating description to: {new_desc!r}") + vtr.update_properties({'description': new_desc}) + + # Verify the update + vtr.pull_full_properties() + updated_desc = vtr.get_property('description') + assert updated_desc == new_desc, \ + f"Description was not updated correctly: {updated_desc!r}" + + # Restore original description + print(f"Restoring description to: {original_desc!r}") + vtr.update_properties({'description': original_desc}) + + # Verify restoration + vtr.pull_full_properties() + restored_desc = vtr.get_property('description') + assert restored_desc == original_desc, \ + f"Description was not restored correctly: {restored_desc!r}" + + print(f"Successfully tested update_properties() for " + f"Virtual Tape Resource {vtr.name!r}") + + +def test_vtr_attached_partition(hmc_session): + """ + Test attached_partition property for Virtual Tape Resources. + """ + client = zhmcclient.Client(hmc_session) + console = client.consoles.console + hd = hmc_session.hmc_definition + + # Pick the Tape Library to test with + tl_list = console.tape_library.list() + if not tl_list: + skip_warn(f"No Tape Library defined on HMC {hd.host}") + tl_list = pick_test_resources(tl_list) + + for tl in tl_list: + print(f"Testing with Tape Library {tl.name!r}") + + # Get tape links for this tape library + tlink_list = tl.tape_links.list() + if not tlink_list: + skip_warn(f"No Tape Links defined for Tape Library {tl.name!r}") + continue + + tlink_list = pick_test_resources(tlink_list) + + for tlink in tlink_list: + print(f"Testing with Tape Link {tlink.name!r}") + + # Get virtual tape resources for this tape link + vtr_list = tlink.virtual_tape_resources.list() + if not vtr_list: + skip_warn(f"No Virtual Tape Resources defined for " + f"Tape Link {tlink.name!r}") + continue + + vtr_list = pick_test_resources(vtr_list) + + for vtr in vtr_list: + print(f"Testing with Virtual Tape Resource " + f"{vtr.name!r}") + + # Get the attached partition + partition = vtr.attached_partition + assert partition is not None, \ + f"VTR {vtr.name!r} has no attached partition" + + # Verify partition URI matches + vtr.pull_full_properties() + partition_uri = vtr.get_property('partition-uri') + assert partition.uri == partition_uri, \ + f"Partition URI mismatch for VTR {vtr.name!r}" + + +def test_vtr_adapter_port(hmc_session): + """ + Test adapter_port property for Virtual Tape Resources. + """ + client = zhmcclient.Client(hmc_session) + console = client.consoles.console + hd = hmc_session.hmc_definition + + # Pick the Tape Library to test with + tl_list = console.tape_library.list() + if not tl_list: + skip_warn(f"No Tape Library defined on HMC {hd.host}") + tl_list = pick_test_resources(tl_list) + + for tl in tl_list: + print(f"Testing with Tape Library {tl.name!r}") + + # Get tape links for this tape library + tlink_list = tl.tape_links.list() + if not tlink_list: + skip_warn(f"No Tape Links defined for Tape Library {tl.name!r}") + continue + + tlink_list = pick_test_resources(tlink_list) + + for tlink in tlink_list: + print(f"Testing with Tape Link {tlink.name!r}") + + # Get virtual tape resources for this tape link + vtr_list = tlink.virtual_tape_resources.list() + if not vtr_list: + skip_warn(f"No Virtual Tape Resources defined for " + f"Tape Link {tlink.name!r}") + continue + + vtr_list = pick_test_resources(vtr_list) + + for vtr in vtr_list: + print(f"Testing with Virtual Tape Resource " + f"{vtr.name!r}") + + # Get full properties to ensure adapter-port-uri is available + vtr.pull_full_properties() + adapter_port_uri = vtr.get_property('adapter-port-uri') + + if adapter_port_uri is None: + print(f"Skipping VTR {vtr.name!r} - no adapter port URI") + continue + + # Get the adapter port + adapter_port = vtr.adapter_port + assert adapter_port is not None, \ + f"Virtual Tape Resource {vtr.name!r} has no adapter port" + + # Verify adapter port URI matches + assert adapter_port.uri == adapter_port_uri, \ + f"Adapter port URI mismatch for VTR {vtr.name!r}" + + +def test_vtr_filter_by_device_number(hmc_session): + """ + Test filtering Virtual Tape Resources by device number. + """ + client = zhmcclient.Client(hmc_session) + console = client.consoles.console + hd = hmc_session.hmc_definition + + # Pick the Tape Library to test with + tl_list = console.tape_library.list() + if not tl_list: + skip_warn(f"No Tape Library defined on HMC {hd.host}") + tl_list = pick_test_resources(tl_list) + + for tl in tl_list: + print(f"Testing with Tape Library {tl.name!r}") + + # Get tape links for this tape library + tlink_list = tl.tape_links.list() + if not tlink_list: + skip_warn(f"No Tape Links defined for Tape Library {tl.name!r}") + continue + + tlink_list = pick_test_resources(tlink_list) + + for tlink in tlink_list: + print(f"Testing with Tape Link {tlink.name!r}") + + # Get all virtual tape resources for this tape link + all_vtrs = tlink.virtual_tape_resources.list() + if not all_vtrs: + skip_warn(f"No Virtual Tape Resources defined for " + f"Tape Link {tlink.name!r}") + continue + + # Pick one to test filtering + test_vtr = all_vtrs[0] + device_number = test_vtr.get_property('device-number') + + print(f"Testing filter by device-number: {device_number!r}") + + # Filter by device number + filtered_vtrs = tlink.virtual_tape_resources.list( + filter_args={'device-number': device_number}) + + assert len(filtered_vtrs) >= 1, \ + f"No VTRs found with device-number {device_number!r}" + + # Verify all returned resources have the correct device number + for vtr in filtered_vtrs: + assert (vtr.get_property('device-number') == + device_number) + + +def test_vtr_filter_by_partition(hmc_session): + """ + Test filtering Virtual Tape Resources by partition URI. + """ + client = zhmcclient.Client(hmc_session) + console = client.consoles.console + hd = hmc_session.hmc_definition + + # Pick the Tape Library to test with + tl_list = console.tape_library.list() + if not tl_list: + skip_warn(f"No Tape Library defined on HMC {hd.host}") + tl_list = pick_test_resources(tl_list) + + for tl in tl_list: + print(f"Testing with Tape Library {tl.name!r}") + + # Get tape links for this tape library + tlink_list = tl.tape_links.list() + if not tlink_list: + skip_warn(f"No Tape Links defined for Tape Library {tl.name!r}") + continue + + tlink_list = pick_test_resources(tlink_list) + + for tlink in tlink_list: + print(f"Testing with Tape Link {tlink.name!r}") + + # Get all virtual tape resources for this tape link + all_vtrs = tlink.virtual_tape_resources.list() + if not all_vtrs: + skip_warn(f"No Virtual Tape Resources defined for " + f"Tape Link {tlink.name!r}") + continue + + # Pick one to test filtering + test_vtr = all_vtrs[0] + partition_uri = test_vtr.get_property('partition-uri') + + print(f"Testing filter by partition-uri: {partition_uri!r}") + + # Filter by partition URI + filtered_vtrs = tlink.virtual_tape_resources.list( + filter_args={'partition-uri': partition_uri}) + + assert len(filtered_vtrs) >= 1, \ + f"No VTRs found with partition-uri {partition_uri!r}" + + # Verify all returned resources have the correct partition URI + for vtr in filtered_vtrs: + assert (vtr.get_property('partition-uri') == + partition_uri) diff --git a/tests/unit/zhmcclient/test_tape_link.py b/tests/unit/zhmcclient/test_tape_link.py new file mode 100644 index 00000000..d828da39 --- /dev/null +++ b/tests/unit/zhmcclient/test_tape_link.py @@ -0,0 +1,637 @@ +# Copyright 2026 IBM Corp. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Unit tests for _tape_link module. +""" + + +import re +import copy +import pytest + +from zhmcclient import Client, TapeLink, TapeLinkManager, \ + HTTPError, NotFound +from zhmcclient.mock import FakedSession +from tests.common.utils import assert_resources + + +# Object IDs and names of our faked resources: +CPC_OID = 'fake-cpc1-oid' +CPC_URI = f'/api/cpcs/{CPC_OID}' +PARTITION_OID = 'partition1-oid' +PARTITION_URI = f'/api/partitions/{PARTITION_OID}' +TL_OID = 'tape-library1-oid' +TL_NAME = 'tape-library 1' +TL_URI = f'/api/tape-libraries/{TL_OID}' +TLINK1_OID = 'tlink1-oid' +TLINK1_NAME = 'tape link 1' +TLINK2_OID = 'tlink2-oid' +TLINK2_NAME = 'tape link 2' + + +class TestTapeLink: + """All tests for the TapeLink and TapeLinkManager classes.""" + + def setup_method(self): + """ + Setup that is called by pytest before each test method. + + Set up a faked session, and add a faked CPC in DPM mode with a + tape library. + """ + # pylint: disable=attribute-defined-outside-init + + self.session = FakedSession('fake-host', 'fake-hmc', '2.16.0', '4.10') + self.client = Client(self.session) + + # Add a faked CPC + self.faked_cpc = self.session.hmc.cpcs.add({ + 'object-id': CPC_OID, + # object-uri is set up automatically + 'parent': None, + 'class': 'cpc', + 'name': 'fake-cpc1-name', + 'description': 'CPC #1 (DPM mode)', + 'status': 'active', + 'dpm-enabled': True, + 'is-ensemble-member': False, + 'iml-mode': 'dpm', + }) + assert self.faked_cpc.uri == CPC_URI + self.cpc = self.client.cpcs.find(name='fake-cpc1-name') + + # Add a faked partition + self.faked_partition = self.faked_cpc.partitions.add({ + 'object-id': PARTITION_OID, + # object-uri is set up automatically + 'parent': CPC_URI, + 'class': 'partition', + 'name': 'fake-partition1-name', + 'description': 'Partition #1', + 'status': 'stopped', + }) + assert self.faked_partition.uri == PARTITION_URI + + # Add a faked console + self.faked_console = self.session.hmc.consoles.add({ + # object-id is set up automatically + # object-uri is set up automatically + # parent will be automatically set + # class will be automatically set + 'name': 'fake-console-name', + 'description': 'The HMC', + }) + self.console = self.client.consoles.console + + # Add a faked tape library + self.faked_tape_library = self.faked_console.tape_library.add({ + 'object-id': TL_OID, + # object-uri will be automatically set + # parent will be automatically set + # class will be automatically set + 'cpc-uri': CPC_URI, + 'name': TL_NAME, + 'description': 'Tape Library #1', + 'state': 'online', + }) + assert self.faked_tape_library.uri == TL_URI + self.tape_library = self.console.tape_library.find(name=TL_NAME) + + def add_tape_link1(self): + """Add tape link 1.""" + + faked_tape_link = self.faked_tape_library.tape_links.add({ + 'element-id': TLINK1_OID, + # element-uri will be automatically set + # parent will be automatically set + # class will be automatically set + 'name': TLINK1_NAME, + 'description': 'Tape Link #1', + 'partition-uri': PARTITION_URI, + }) + return faked_tape_link + + def add_tape_link2(self): + """Add tape link 2.""" + + faked_tape_link = self.faked_tape_library.tape_links.add({ + 'element-id': TLINK2_OID, + # element-uri will be automatically set + # parent will be automatically set + # class will be automatically set + 'name': TLINK2_NAME, + 'description': 'Tape Link #2', + 'partition-uri': PARTITION_URI, + }) + return faked_tape_link + + def test_tlm_initial_attrs(self): + """Test initial attributes of TapeLinkManager.""" + + tape_link_mgr = self.tape_library.tape_links + + assert isinstance(tape_link_mgr, TapeLinkManager) + + # Verify all public properties of the manager object + assert tape_link_mgr.resource_class == TapeLink + assert tape_link_mgr.session == self.session + assert tape_link_mgr.parent == self.tape_library + assert tape_link_mgr.tape_library == self.tape_library + + # TODO: Test for TapeLinkManager.__repr__() + + testcases_tlm_list_full_properties = ( + "full_properties_kwargs, prop_names", [ + ({}, + ['element-uri', 'name', 'partition-uri']), + (dict(full_properties=False), + ['element-uri', 'name', 'partition-uri']), + ] + ) + + @pytest.mark.parametrize( + *testcases_tlm_list_full_properties + ) + def test_tlm_list_full_properties( + self, full_properties_kwargs, prop_names): + """Test TapeLinkManager.list() with full_properties.""" + + # Add two faked tape links + faked_tape_link1 = self.add_tape_link1() + faked_tape_link2 = self.add_tape_link2() + + exp_faked_tape_links = [faked_tape_link1, faked_tape_link2] + tape_link_mgr = self.tape_library.tape_links + + # Execute the code to be tested + tape_links = tape_link_mgr.list(**full_properties_kwargs) + + assert_resources(tape_links, exp_faked_tape_links, prop_names) + + testcases_tlm_list_filter_args = ( + "filter_args, exp_names", [ + ({'element-id': TLINK1_OID}, + [TLINK1_NAME]), + ({'element-id': TLINK2_OID}, + [TLINK2_NAME]), + ({'element-id': [TLINK1_OID, TLINK2_OID]}, + [TLINK1_NAME, TLINK2_NAME]), + ({'element-id': [TLINK1_OID, TLINK1_OID]}, + [TLINK1_NAME]), + ({'element-id': TLINK1_OID + 'foo'}, + []), + ({'element-id': [TLINK1_OID, TLINK2_OID + 'foo']}, + [TLINK1_NAME]), + ({'element-id': [TLINK2_OID + 'foo', TLINK1_OID]}, + [TLINK1_NAME]), + ({'name': TLINK1_NAME}, + [TLINK1_NAME]), + ({'name': TLINK2_NAME}, + [TLINK2_NAME]), + ({'name': [TLINK1_NAME, TLINK2_NAME]}, + [TLINK1_NAME, TLINK2_NAME]), + ({'name': TLINK1_NAME + 'foo'}, + []), + ({'name': [TLINK1_NAME, TLINK2_NAME + 'foo']}, + [TLINK1_NAME]), + ({'name': [TLINK2_NAME + 'foo', TLINK1_NAME]}, + [TLINK1_NAME]), + ({'name': [TLINK1_NAME, TLINK1_NAME]}, + [TLINK1_NAME]), + ({'name': '.*tape link 1'}, + [TLINK1_NAME]), + ({'name': 'tape link 1.*'}, + [TLINK1_NAME]), + ({'name': 'tape link .'}, + [TLINK1_NAME, TLINK2_NAME]), + ({'name': '.ape link 1'}, + [TLINK1_NAME]), + ({'name': '.+'}, + [TLINK1_NAME, TLINK2_NAME]), + ({'name': 'tape link 1.+'}, + []), + ({'name': '.+tape link 1'}, + []), + ({'name': TLINK1_NAME, + 'element-id': TLINK1_OID}, + [TLINK1_NAME]), + ({'name': TLINK1_NAME, + 'element-id': TLINK1_OID + 'foo'}, + []), + ({'name': TLINK1_NAME + 'foo', + 'element-id': TLINK1_OID}, + []), + ({'name': TLINK1_NAME + 'foo', + 'element-id': TLINK1_OID + 'foo'}, + []), + ] + ) + + @pytest.mark.parametrize( + *testcases_tlm_list_filter_args + ) + def test_tlm_list_filter_args( + self, filter_args, exp_names): + """Test TapeLinkManager.list() with filter_args.""" + + # Add two faked tape links + self.add_tape_link1() + self.add_tape_link2() + + tape_link_mgr = self.tape_library.tape_links + + # Execute the code to be tested + tape_links = tape_link_mgr.list(filter_args=filter_args) + + assert len(tape_links) == len(exp_names) + if exp_names: + names = [tl.properties['name'] for tl in tape_links] + assert set(names) == set(exp_names) + + testcases_tlm_create = ( + "input_props, exp_prop_names, exp_exc", [ + ({}, + None, + HTTPError({'http-status': 400, 'reason': 5})), + ({'description': 'fake description X'}, + None, + HTTPError({'http-status': 400, 'reason': 5})), + ({'name': 'fake-tlink-x'}, + None, + HTTPError({'http-status': 400, 'reason': 5})), + ({'name': 'fake-tlink-x', + 'partition-uri': PARTITION_URI}, + ['element-uri', 'name', 'partition-uri'], + None), + ] + ) + + @pytest.mark.parametrize( + *testcases_tlm_create + ) + def test_tlm_create( + self, input_props, exp_prop_names, exp_exc): + """Test TapeLinkManager.create().""" + + tape_link_mgr = self.tape_library.tape_links + + if exp_exc is not None: + + with pytest.raises(exp_exc.__class__) as exc_info: + + # Execute the code to be tested + tape_link = tape_link_mgr.create(properties=input_props) + + exc = exc_info.value + if isinstance(exp_exc, HTTPError): + assert exc.http_status == exp_exc.http_status + assert exc.reason == exp_exc.reason + + else: + + # Execute the code to be tested. + # Note: the TapeLink object returned by TapeLink.create() + # has the input properties plus 'element-uri'. + tape_link = tape_link_mgr.create(properties=input_props) + + # Check the resource for consistency within itself + assert isinstance(tape_link, TapeLink) + tape_link_name = tape_link.name + exp_tape_link_name = tape_link.properties['name'] + assert tape_link_name == exp_tape_link_name + tape_link_uri = tape_link.uri + exp_tape_link_uri = tape_link.properties['element-uri'] + assert tape_link_uri == exp_tape_link_uri + + # Check the properties against the expected names and values + for prop_name in exp_prop_names: + assert prop_name in tape_link.properties + if prop_name in input_props: + value = tape_link.properties[prop_name] + exp_value = input_props[prop_name] + assert value == exp_value + + def test_tlm_resource_object(self): + """ + Test TapeLinkManager.resource_object(). + + This test exists for historical reasons, and by now is covered by the + test for BaseManager.resource_object(). + """ + + # Add a faked tape link + faked_tape_link = self.add_tape_link1() + tape_link_oid = faked_tape_link.oid + + tape_link_mgr = self.tape_library.tape_links + + # Execute the code to be tested + tape_link = tape_link_mgr.resource_object(tape_link_oid) + + tape_link_uri = f"{TL_URI}/tape-links/{tape_link_oid}" + + assert isinstance(tape_link, TapeLink) + + # Note: Properties inherited from BaseResource are tested there, + # but we test them again: + assert tape_link.properties['element-uri'] == tape_link_uri + assert tape_link.properties['element-id'] == tape_link_oid + assert tape_link.properties['class'] == 'tape-link' + assert tape_link.properties['parent'] == TL_URI + + def test_tl_repr(self): + """Test TapeLink.__repr__().""" + + # Add a faked tape link + faked_tape_link = self.add_tape_link1() + + tape_link_mgr = self.tape_library.tape_links + tape_link = tape_link_mgr.find(name=faked_tape_link.name) + + # Execute the code to be tested + repr_str = repr(tape_link) + + repr_str = repr_str.replace('\n', '\\n') + # We check just the begin of the string: + assert re.match( + rf'^{tape_link.__class__.__name__}\s+at\s+' + rf'0x{id(tape_link):08x}\s+\(\\n.*', + repr_str) + + def test_tl_delete(self): + """Test TapeLink.delete().""" + + # Add a faked tape link to be tested and another one + faked_tape_link = self.add_tape_link1() + self.add_tape_link2() + + tape_link_mgr = self.tape_library.tape_links + + tape_link = tape_link_mgr.find(name=faked_tape_link.name) + + # Execute the code to be tested. + tape_link.delete() + + # Check that the tape link no longer exists + with pytest.raises(NotFound): + tape_link_mgr.find(name=faked_tape_link.name) + + def test_tl_delete_create_same(self): + """Test TapeLink.delete() followed by create() with same name.""" + + # Add a faked tape link to be tested and another one + faked_tape_link = self.add_tape_link1() + tape_link_name = faked_tape_link.name + self.add_tape_link2() + + # Construct the input properties for a third tape link + tl3_props = copy.deepcopy(faked_tape_link.properties) + tl3_props['description'] = 'Third tape link' + + tape_link_mgr = self.tape_library.tape_links + tape_link = tape_link_mgr.find(name=tape_link_name) + + # Execute the deletion code to be tested. + tape_link.delete() + + # Check that the tape link no longer exists + with pytest.raises(NotFound): + tape_link_mgr.find(name=tape_link_name) + + # Execute the creation code to be tested. + tape_link_mgr.create(tl3_props) + + # Check that the tape link exists again under that name + tape_link3 = tape_link_mgr.find(name=tape_link_name) + description = tape_link3.get_property('description') + assert description == 'Third tape link' + + testcases_tl_update_properties_tls = ( + "tape_link_name", [ + TLINK1_NAME, + TLINK2_NAME, + ] + ) + + testcases_tl_update_properties_props = ( + "input_props", [ + {}, + {'description': 'New tape link description'}, + ] + ) + + @pytest.mark.parametrize( + *testcases_tl_update_properties_tls + ) + @pytest.mark.parametrize( + *testcases_tl_update_properties_props + ) + def test_tl_update_properties( + self, input_props, tape_link_name): + """Test TapeLink.update_properties().""" + + # Add faked tape links + self.add_tape_link1() + self.add_tape_link2() + + tape_link_mgr = self.tape_library.tape_links + tape_link = tape_link_mgr.find(name=tape_link_name) + + tape_link.pull_full_properties() + saved_properties = copy.deepcopy(tape_link.properties) + + # Execute the code to be tested + tape_link.update_properties(properties=input_props) + + # Verify that the resource object already reflects the property + # updates. + for prop_name in saved_properties: + if prop_name in input_props: + exp_prop_value = input_props[prop_name] + else: + exp_prop_value = saved_properties[prop_name] + assert prop_name in tape_link.properties + prop_value = tape_link.properties[prop_name] + assert prop_value == exp_prop_value + + # Refresh the resource object and verify that the resource object + # still reflects the property updates. + tape_link.pull_full_properties() + for prop_name in saved_properties: + if prop_name in input_props: + exp_prop_value = input_props[prop_name] + else: + exp_prop_value = saved_properties[prop_name] + assert prop_name in tape_link.properties + prop_value = tape_link.properties[prop_name] + assert prop_value == exp_prop_value + + def test_tl_update_name(self): + """ + Test TapeLink.update_properties() with 'name' property. + """ + + # Add a faked tape link + faked_tape_link = self.add_tape_link1() + tape_link_name = faked_tape_link.name + + tape_link_mgr = self.tape_library.tape_links + tape_link = tape_link_mgr.find(name=tape_link_name) + + new_tape_link_name = "new-" + tape_link_name + + # Execute the code to be tested + tape_link.update_properties( + properties={'name': new_tape_link_name}) + + # Verify that the resource is no longer found by its old name, using + # list() (this does not use the name-to-URI cache). + tape_links_list = tape_link_mgr.list( + filter_args=dict(name=tape_link_name)) + assert len(tape_links_list) == 0 + + # Verify that the resource is no longer found by its old name, using + # find() (this uses the name-to-URI cache). + with pytest.raises(NotFound): + tape_link_mgr.find(name=tape_link_name) + + # Verify that the resource object already reflects the update, even + # though it has not been refreshed yet. + assert tape_link.properties['name'] == new_tape_link_name + + # Refresh the resource object and verify that it still reflects the + # update. + tape_link.pull_full_properties() + assert tape_link.properties['name'] == new_tape_link_name + + # Verify that the resource can be found by its new name, using find() + new_tape_link_find = tape_link_mgr.find( + name=new_tape_link_name) + assert new_tape_link_find.properties['name'] == \ + new_tape_link_name + + # Verify that the resource can be found by its new name, using list() + new_tape_links_list = tape_link_mgr.list( + filter_args=dict(name=new_tape_link_name)) + assert len(new_tape_links_list) == 1 + new_tape_link_list = new_tape_links_list[0] + assert new_tape_link_list.properties['name'] == \ + new_tape_link_name + + def test_tl_get_partitions(self): + """Test TapeLink.get_partitions().""" + + # Add a faked tape link + faked_tape_link = self.add_tape_link1() + + tape_link_mgr = self.tape_library.tape_links + tape_link = tape_link_mgr.find(name=faked_tape_link.name) + + # Execute the code to be tested + partitions = tape_link.get_partitions() + + # Verify the result + assert isinstance(partitions, list) + # The partition should be in the list + assert len(partitions) >= 0 + + def test_tl_get_partitions_with_filters(self): + """Test TapeLink.get_partitions() with name and status filters.""" + + # Add a faked tape link + faked_tape_link = self.add_tape_link1() + + tape_link_mgr = self.tape_library.tape_links + tape_link = tape_link_mgr.find(name=faked_tape_link.name) + + # Execute the code to be tested with filters + partitions = tape_link.get_partitions( + name='fake-partition.*', status='stopped') + + # Verify the result + assert isinstance(partitions, list) + + def test_tl_get_histories(self): + """Test TapeLink.get_histories().""" + + # Add a faked tape link + faked_tape_link = self.add_tape_link1() + + tape_link_mgr = self.tape_library.tape_links + tape_link = tape_link_mgr.find(name=faked_tape_link.name) + + # Execute the code to be tested + histories = tape_link.get_histories() + + # Verify the result + assert isinstance(histories, dict) + # The histories should contain expected keys + # (actual keys depend on HMC API response structure) + + def test_tl_get_environment_report(self): + """Test TapeLink.get_environment_report().""" + + # Add a faked tape link + faked_tape_link = self.add_tape_link1() + + tape_link_mgr = self.tape_library.tape_links + tape_link = tape_link_mgr.find(name=faked_tape_link.name) + + # Execute the code to be tested + report = tape_link.get_environment_report() + + # Verify the result + assert isinstance(report, dict) + # The report should contain expected keys + # (actual keys depend on HMC API response structure) + + def test_tl_update_environment_report(self): + """Test TapeLink.update_environment_report().""" + + # Add a faked tape link + faked_tape_link = self.add_tape_link1() + + tape_link_mgr = self.tape_library.tape_links + tape_link = tape_link_mgr.find(name=faked_tape_link.name) + + # Prepare update properties + update_props = { + 'acknowledged': True, + 'notes': 'Environment report updated' + } + + # Execute the code to be tested + result = tape_link.update_environment_report(properties=update_props) + + # Verify the result + assert isinstance(result, dict) + # The result should contain operation results + # (actual structure depends on HMC API response) + + def test_tl_partition_property(self): + """Test TapeLink.partition property.""" + + # Add a faked tape link + faked_tape_link = self.add_tape_link1() + + tape_link_mgr = self.tape_library.tape_links + tape_link = tape_link_mgr.find(name=faked_tape_link.name) + + # Execute the code to be tested + partition = tape_link.partition + + # Verify the result + assert partition is not None + assert partition.uri == PARTITION_URI diff --git a/tests/unit/zhmcclient/test_virtual_tape_resource.py b/tests/unit/zhmcclient/test_virtual_tape_resource.py new file mode 100644 index 00000000..8c6e0d6b --- /dev/null +++ b/tests/unit/zhmcclient/test_virtual_tape_resource.py @@ -0,0 +1,638 @@ +# Copyright 2026 IBM Corp. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Unit tests for _virtual_tape_resource module. +""" + + +import re +import copy +import pytest + +from zhmcclient import Client, VirtualTapeResource, \ + VirtualTapeResourceManager, NotFound +from zhmcclient.mock import FakedSession +from tests.common.utils import assert_resources + + +# Object IDs and names of our faked resources: +CPC_OID = 'fake-cpc1-oid' +CPC_URI = f'/api/cpcs/{CPC_OID}' +PARTITION_OID = 'partition1-oid' +PARTITION_URI = f'/api/partitions/{PARTITION_OID}' +ADAPTER_OID = 'adapter1-oid' +ADAPTER_URI = f'/api/adapters/{ADAPTER_OID}' +ADAPTER_PORT_URI = f'{ADAPTER_URI}/storage-ports/0' +TL_OID = 'tape-library1-oid' +TL_NAME = 'tape-library 1' +TL_URI = f'/api/tape-libraries/{TL_OID}' +TLINK_OID = 'tlink1-oid' +TLINK_NAME = 'tape link 1' +TLINK_URI = f'{TL_URI}/tape-links/{TLINK_OID}' +VTR1_OID = 'vtr1-oid' +VTR1_NAME = 'virtual tape resource 1' +VTR2_OID = 'vtr2-oid' +VTR2_NAME = 'virtual tape resource 2' + + +class TestVirtualTapeResource: + """All tests for the VirtualTapeResource and + VirtualTapeResourceManager classes.""" + + def setup_method(self): + """ + Setup that is called by pytest before each test method. + + Set up a faked session, and add a faked CPC in DPM mode with a + tape library, tape link, and related resources. + """ + # pylint: disable=attribute-defined-outside-init + + self.session = FakedSession('fake-host', 'fake-hmc', '2.16.0', '4.10') + self.client = Client(self.session) + + # Add a faked CPC + self.faked_cpc = self.session.hmc.cpcs.add({ + 'object-id': CPC_OID, + # object-uri is set up automatically + 'parent': None, + 'class': 'cpc', + 'name': 'fake-cpc1-name', + 'description': 'CPC #1 (DPM mode)', + 'status': 'active', + 'dpm-enabled': True, + 'is-ensemble-member': False, + 'iml-mode': 'dpm', + }) + assert self.faked_cpc.uri == CPC_URI + self.cpc = self.client.cpcs.find(name='fake-cpc1-name') + + # Add a faked partition + self.faked_partition = self.faked_cpc.partitions.add({ + 'object-id': PARTITION_OID, + # object-uri is set up automatically + 'parent': CPC_URI, + 'class': 'partition', + 'name': 'fake-partition1-name', + 'description': 'Partition #1', + 'status': 'stopped', + }) + assert self.faked_partition.uri == PARTITION_URI + + # Add a faked adapter + self.faked_adapter = self.faked_cpc.adapters.add({ + 'object-id': ADAPTER_OID, + # object-uri is set up automatically + 'parent': CPC_URI, + 'class': 'adapter', + 'name': 'fake-adapter1-name', + 'description': 'Adapter #1', + 'type': 'fcp', + 'adapter-family': 'ficon', + 'status': 'active', + }) + assert self.faked_adapter.uri == ADAPTER_URI + + # Add a faked console + self.faked_console = self.session.hmc.consoles.add({ + # object-id is set up automatically + # object-uri is set up automatically + # parent will be automatically set + # class will be automatically set + 'name': 'fake-console-name', + 'description': 'The HMC', + }) + self.console = self.client.consoles.console + + # Add a faked tape library + self.faked_tape_library = self.faked_console.tape_library.add({ + 'object-id': TL_OID, + # object-uri will be automatically set + # parent will be automatically set + # class will be automatically set + 'cpc-uri': CPC_URI, + 'name': TL_NAME, + 'description': 'Tape Library #1', + 'state': 'online', + }) + assert self.faked_tape_library.uri == TL_URI + self.tape_library = self.console.tape_library.find(name=TL_NAME) + + # Add a faked tape link + self.faked_tape_link = self.faked_tape_library.tape_links.add({ + 'element-id': TLINK_OID, + # element-uri will be automatically set + # parent will be automatically set + # class will be automatically set + 'name': TLINK_NAME, + 'description': 'Tape Link #1', + 'partition-uri': PARTITION_URI, + }) + assert self.faked_tape_link.uri == TLINK_URI + self.tape_link = self.tape_library.tape_links.find(name=TLINK_NAME) + + def add_vtr1(self): + """Add virtual tape resource 1.""" + + faked_vtr = self.faked_tape_link.virtual_tape_resources.add({ + 'element-id': VTR1_OID, + # element-uri will be automatically set + # parent will be automatically set + # class will be automatically set + 'name': VTR1_NAME, + 'description': 'Virtual Tape Resource #1', + 'device-number': '0001', + 'adapter-port-uri': ADAPTER_PORT_URI, + 'partition-uri': PARTITION_URI, + }) + return faked_vtr + + def add_vtr2(self): + """Add virtual tape resource 2.""" + + faked_vtr = self.faked_tape_link.virtual_tape_resources.add({ + 'element-id': VTR2_OID, + # element-uri will be automatically set + # parent will be automatically set + # class will be automatically set + 'name': VTR2_NAME, + 'description': 'Virtual Tape Resource #2', + 'device-number': '0002', + 'adapter-port-uri': ADAPTER_PORT_URI, + 'partition-uri': PARTITION_URI, + }) + return faked_vtr + + def test_vtrm_initial_attrs(self): + """Test initial attributes of VirtualTapeResourceManager.""" + + vtr_mgr = self.tape_link.virtual_tape_resources + + assert isinstance(vtr_mgr, VirtualTapeResourceManager) + + # Verify all public properties of the manager object + assert vtr_mgr.resource_class == VirtualTapeResource + assert vtr_mgr.session == self.session + assert vtr_mgr.parent == self.tape_link + assert vtr_mgr.tape_link == self.tape_link + + # TODO: Test for VirtualTapeResourceManager.__repr__() + + testcases_vtrm_list_full_properties = ( + "full_properties_kwargs, prop_names", [ + ({}, + ['element-uri', 'name', 'device-number', 'adapter-port-uri', + 'partition-uri']), + (dict(full_properties=False), + ['element-uri', 'name', 'device-number', 'adapter-port-uri', + 'partition-uri']), + (dict(full_properties=True), + ['element-uri', 'name', 'device-number', 'adapter-port-uri', + 'partition-uri', 'description']), + ] + ) + + @pytest.mark.parametrize( + *testcases_vtrm_list_full_properties + ) + def test_vtrm_list_full_properties( + self, full_properties_kwargs, prop_names): + """Test VirtualTapeResourceManager.list() with full_properties.""" + + # Add two faked virtual tape resources + faked_vtr1 = self.add_vtr1() + faked_vtr2 = self.add_vtr2() + + exp_faked_vtrs = [faked_vtr1, faked_vtr2] + vtr_mgr = self.tape_link.virtual_tape_resources + + # Execute the code to be tested + vtrs = vtr_mgr.list(**full_properties_kwargs) + + assert_resources(vtrs, exp_faked_vtrs, prop_names) + + testcases_vtrm_list_filter_args = ( + "filter_args, exp_names", [ + ({'element-id': VTR1_OID}, + [VTR1_NAME]), + ({'element-id': VTR2_OID}, + [VTR2_NAME]), + ({'element-id': [VTR1_OID, VTR2_OID]}, + [VTR1_NAME, VTR2_NAME]), + ({'element-id': [VTR1_OID, VTR1_OID]}, + [VTR1_NAME]), + ({'element-id': VTR1_OID + 'foo'}, + []), + ({'element-id': [VTR1_OID, VTR2_OID + 'foo']}, + [VTR1_NAME]), + ({'element-id': [VTR2_OID + 'foo', VTR1_OID]}, + [VTR1_NAME]), + ({'name': VTR1_NAME}, + [VTR1_NAME]), + ({'name': VTR2_NAME}, + [VTR2_NAME]), + ({'name': [VTR1_NAME, VTR2_NAME]}, + [VTR1_NAME, VTR2_NAME]), + ({'name': VTR1_NAME + 'foo'}, + []), + ({'name': [VTR1_NAME, VTR2_NAME + 'foo']}, + [VTR1_NAME]), + ({'name': [VTR2_NAME + 'foo', VTR1_NAME]}, + [VTR1_NAME]), + ({'name': [VTR1_NAME, VTR1_NAME]}, + [VTR1_NAME]), + ({'name': '.*virtual tape resource 1'}, + [VTR1_NAME]), + ({'name': 'virtual tape resource 1.*'}, + [VTR1_NAME]), + ({'name': 'virtual tape resource .'}, + [VTR1_NAME, VTR2_NAME]), + ({'name': '.irtual tape resource 1'}, + [VTR1_NAME]), + ({'name': '.+'}, + [VTR1_NAME, VTR2_NAME]), + ({'name': 'virtual tape resource 1.+'}, + []), + ({'name': '.+virtual tape resource 1'}, + []), + ({'name': VTR1_NAME, + 'element-id': VTR1_OID}, + [VTR1_NAME]), + ({'name': VTR1_NAME, + 'element-id': VTR1_OID + 'foo'}, + []), + ({'name': VTR1_NAME + 'foo', + 'element-id': VTR1_OID}, + []), + ({'name': VTR1_NAME + 'foo', + 'element-id': VTR1_OID + 'foo'}, + []), + ({'device-number': '0001'}, + [VTR1_NAME]), + ({'device-number': '0002'}, + [VTR2_NAME]), + ({'adapter-port-uri': ADAPTER_PORT_URI}, + [VTR1_NAME, VTR2_NAME]), + ({'partition-uri': PARTITION_URI}, + [VTR1_NAME, VTR2_NAME]), + ] + ) + + @pytest.mark.parametrize( + *testcases_vtrm_list_filter_args + ) + def test_vtrm_list_filter_args( + self, filter_args, exp_names): + """Test VirtualTapeResourceManager.list() with filter_args.""" + + # Add two faked virtual tape resources + self.add_vtr1() + self.add_vtr2() + + vtr_mgr = self.tape_link.virtual_tape_resources + + # Execute the code to be tested + vtrs = vtr_mgr.list(filter_args=filter_args) + + assert len(vtrs) == len(exp_names) + if exp_names: + names = [vtr.properties['name'] for vtr in vtrs] + assert set(names) == set(exp_names) + + def test_vtrm_resource_object(self): + """ + Test VirtualTapeResourceManager.resource_object(). + + This test exists for historical reasons, and by now is covered by the + test for BaseManager.resource_object(). + """ + + # Add a faked virtual tape resource + faked_vtr = self.add_vtr1() + vtr_oid = faked_vtr.oid + + vtr_mgr = self.tape_link.virtual_tape_resources + + # Execute the code to be tested + vtr = vtr_mgr.resource_object(vtr_oid) + + vtr_uri = f"{TLINK_URI}/virtual-tape-resources/{vtr_oid}" + + assert isinstance(vtr, VirtualTapeResource) + + # Note: Properties inherited from BaseResource are tested there, + # but we test them again: + assert vtr.properties['element-uri'] == vtr_uri + assert vtr.properties['element-id'] == vtr_oid + assert vtr.properties['class'] == 'virtual-tape-resource' + assert vtr.properties['parent'] == TLINK_URI + + def test_vtr_repr(self): + """Test VirtualTapeResource.__repr__().""" + + # Add a faked virtual tape resource + faked_vtr = self.add_vtr1() + + vtr_mgr = self.tape_link.virtual_tape_resources + vtr = vtr_mgr.find(name=faked_vtr.name) + + # Execute the code to be tested + repr_str = repr(vtr) + + repr_str = repr_str.replace('\n', '\\n') + # We check just the begin of the string: + assert re.match( + rf'^{vtr.__class__.__name__}\s+at\s+' + rf'0x{id(vtr):08x}\s+\(\\n.*', + repr_str) + + testcases_vtr_update_properties_vtrs = ( + "vtr_name", [ + VTR1_NAME, + VTR2_NAME, + ] + ) + + testcases_vtr_update_properties_props = ( + "input_props", [ + {}, + {'description': 'New virtual tape resource description'}, + {'device-number': '0003'}, + {'description': 'Updated description', 'device-number': '0004'}, + ] + ) + + @pytest.mark.parametrize( + *testcases_vtr_update_properties_vtrs + ) + @pytest.mark.parametrize( + *testcases_vtr_update_properties_props + ) + def test_vtr_update_properties( + self, input_props, vtr_name): + """Test VirtualTapeResource.update_properties().""" + + # Add faked virtual tape resources + self.add_vtr1() + self.add_vtr2() + + vtr_mgr = self.tape_link.virtual_tape_resources + vtr = vtr_mgr.find(name=vtr_name) + + vtr.pull_full_properties() + saved_properties = copy.deepcopy(vtr.properties) + + # Execute the code to be tested + vtr.update_properties(properties=input_props) + + # Verify that the resource object already reflects the property + # updates. + for prop_name in saved_properties: + if prop_name in input_props: + exp_prop_value = input_props[prop_name] + else: + exp_prop_value = saved_properties[prop_name] + assert prop_name in vtr.properties + prop_value = vtr.properties[prop_name] + assert prop_value == exp_prop_value + + # Refresh the resource object and verify that the resource object + # still reflects the property updates. + vtr.pull_full_properties() + for prop_name in saved_properties: + if prop_name in input_props: + exp_prop_value = input_props[prop_name] + else: + exp_prop_value = saved_properties[prop_name] + assert prop_name in vtr.properties + prop_value = vtr.properties[prop_name] + assert prop_value == exp_prop_value + + def test_vtr_update_name(self): + """ + Test VirtualTapeResource.update_properties() with 'name' property. + """ + + # Add a faked virtual tape resource + faked_vtr = self.add_vtr1() + vtr_name = faked_vtr.name + + vtr_mgr = self.tape_link.virtual_tape_resources + vtr = vtr_mgr.find(name=vtr_name) + + new_vtr_name = "new-" + vtr_name + + # Execute the code to be tested + vtr.update_properties( + properties={'name': new_vtr_name}) + + # Verify that the resource is no longer found by its old name, using + # list() (this does not use the name-to-URI cache). + vtrs_list = vtr_mgr.list( + filter_args=dict(name=vtr_name)) + assert len(vtrs_list) == 0 + + # Verify that the resource is no longer found by its old name, using + # find() (this uses the name-to-URI cache). + with pytest.raises(NotFound): + vtr_mgr.find(name=vtr_name) + + # Verify that the resource object already reflects the update, even + # though it has not been refreshed yet. + assert vtr.properties['name'] == new_vtr_name + + # Refresh the resource object and verify that it still reflects the + # update. + vtr.pull_full_properties() + assert vtr.properties['name'] == new_vtr_name + + # Verify that the resource can be found by its new name, using find() + new_vtr_find = vtr_mgr.find( + name=new_vtr_name) + assert new_vtr_find.properties['name'] == \ + new_vtr_name + + # Verify that the resource can be found by its new name, using list() + new_vtrs_list = vtr_mgr.list( + filter_args=dict(name=new_vtr_name)) + assert len(new_vtrs_list) == 1 + new_vtr_list = new_vtrs_list[0] + assert new_vtr_list.properties['name'] == \ + new_vtr_name + + def test_vtr_attached_partition_property(self): + """Test VirtualTapeResource.attached_partition property.""" + + # Add a faked virtual tape resource + faked_vtr = self.add_vtr1() + + vtr_mgr = self.tape_link.virtual_tape_resources + vtr = vtr_mgr.find(name=faked_vtr.name) + + # Execute the code to be tested + partition = vtr.attached_partition + + # Verify the result + assert partition is not None + assert partition.uri == PARTITION_URI + + def test_vtr_adapter_port_property(self): + """Test VirtualTapeResource.adapter_port property.""" + + # Add a faked virtual tape resource + faked_vtr = self.add_vtr1() + + vtr_mgr = self.tape_link.virtual_tape_resources + vtr = vtr_mgr.find(name=faked_vtr.name) + + # Execute the code to be tested + adapter_port = vtr.adapter_port + + # Verify the result + assert adapter_port is not None + assert adapter_port.uri == ADAPTER_PORT_URI + + def test_vtr_update_device_number(self): + """Test updating device-number property.""" + + # Add a faked virtual tape resource + faked_vtr = self.add_vtr1() + + vtr_mgr = self.tape_link.virtual_tape_resources + vtr = vtr_mgr.find(name=faked_vtr.name) + + old_device_number = vtr.get_property('device-number') + new_device_number = '0005' + + # Execute the code to be tested + vtr.update_properties(properties={'device-number': new_device_number}) + + # Verify the update + assert vtr.get_property('device-number') == new_device_number + assert vtr.get_property('device-number') != old_device_number + + def test_vtr_multiple_property_updates(self): + """Test updating multiple properties at once.""" + + # Add a faked virtual tape resource + faked_vtr = self.add_vtr1() + + vtr_mgr = self.tape_link.virtual_tape_resources + vtr = vtr_mgr.find(name=faked_vtr.name) + + new_props = { + 'name': 'updated-vtr-name', + 'description': 'Updated description', + 'device-number': '0006', + } + + # Execute the code to be tested + vtr.update_properties(properties=new_props) + + # Verify all updates + vtr.pull_full_properties() + assert vtr.get_property('name') == new_props['name'] + assert vtr.get_property('description') == new_props['description'] + assert vtr.get_property('device-number') == new_props['device-number'] + + def test_vtr_list_empty(self): + """Test listing virtual tape resources when none exist.""" + + vtr_mgr = self.tape_link.virtual_tape_resources + + # Execute the code to be tested + vtrs = vtr_mgr.list() + + # Verify the result + assert isinstance(vtrs, list) + assert len(vtrs) == 0 + + def test_vtr_find_nonexistent(self): + """Test finding a non-existent virtual tape resource.""" + + vtr_mgr = self.tape_link.virtual_tape_resources + + # Execute the code to be tested and expect NotFound + with pytest.raises(NotFound): + vtr_mgr.find(name='nonexistent-vtr') + + def test_vtr_properties_consistency(self): + """Test that virtual tape resource properties are consistent.""" + + # Add a faked virtual tape resource + faked_vtr = self.add_vtr1() + + vtr_mgr = self.tape_link.virtual_tape_resources + vtr = vtr_mgr.find(name=faked_vtr.name) + + # Pull full properties to ensure all properties are available + vtr.pull_full_properties() + + # Verify property consistency + assert vtr.name == vtr.properties['name'] + assert vtr.uri == vtr.properties['element-uri'] + assert vtr.properties['partition-uri'] == PARTITION_URI + assert vtr.properties['adapter-port-uri'] == ADAPTER_PORT_URI + + def test_vtr_filter_by_device_number(self): + """Test filtering virtual tape resources by device number.""" + + # Add faked virtual tape resources with different device numbers + self.add_vtr1() # device-number: '0001' + self.add_vtr2() # device-number: '0002' + + vtr_mgr = self.tape_link.virtual_tape_resources + + # Execute the code to be tested + vtrs = vtr_mgr.list(filter_args={'device-number': '0001'}) + + # Verify the result + assert len(vtrs) == 1 + assert vtrs[0].get_property('device-number') == '0001' + assert vtrs[0].name == VTR1_NAME + + def test_vtr_filter_by_partition_uri(self): + """Test filtering virtual tape resources by partition URI.""" + + # Add faked virtual tape resources + self.add_vtr1() + self.add_vtr2() + + vtr_mgr = self.tape_link.virtual_tape_resources + + # Execute the code to be tested + vtrs = vtr_mgr.list(filter_args={'partition-uri': PARTITION_URI}) + + # Verify the result + assert len(vtrs) == 2 + for vtr in vtrs: + assert vtr.get_property('partition-uri') == PARTITION_URI + + def test_vtr_filter_by_adapter_port_uri(self): + """Test filtering virtual tape resources by adapter port URI.""" + + # Add faked virtual tape resources + self.add_vtr1() + self.add_vtr2() + + vtr_mgr = self.tape_link.virtual_tape_resources + + # Execute the code to be tested + vtrs = vtr_mgr.list(filter_args={'adapter-port-uri': ADAPTER_PORT_URI}) + + # Verify the result + assert len(vtrs) == 2 + for vtr in vtrs: + assert vtr.get_property('adapter-port-uri') == ADAPTER_PORT_URI diff --git a/zhmcclient/__init__.py b/zhmcclient/__init__.py index 6e0c787a..66100f4c 100644 --- a/zhmcclient/__init__.py +++ b/zhmcclient/__init__.py @@ -60,6 +60,8 @@ from ._storage_group_template import * # noqa: F401 from ._storage_volume_template import * # noqa: F401 from ._tape_library import * # noqa: F401 +from ._tape_link import * # noqa: F401 +from ._virtual_tape_resource import * # noqa: F401 from ._partition_link import * # noqa: F401 from ._capacity_group import * # noqa: F401 from ._certificates import * # noqa: F401 diff --git a/zhmcclient/_console.py b/zhmcclient/_console.py index 3a87d5a5..09589ff4 100644 --- a/zhmcclient/_console.py +++ b/zhmcclient/_console.py @@ -1643,6 +1643,7 @@ def dump(self): "sso_server_definitions": [...], "unmanaged_cpcs": [...], "storage_groups": [...], + "tape_libraries": [...], } Returns: @@ -1681,6 +1682,9 @@ def dump(self): storage_groups = self.storage_groups.dump() if storage_groups: resource_dict['storage_groups'] = storage_groups + tape_libraries = self.tape_library.dump() + if tape_libraries: + resource_dict['tape_libraries'] = tape_libraries # Note: Unmanaged CPCs are not dumped, since their properties cannot # be retrieved. diff --git a/zhmcclient/_tape_library.py b/zhmcclient/_tape_library.py index b52a9dad..68025696 100644 --- a/zhmcclient/_tape_library.py +++ b/zhmcclient/_tape_library.py @@ -37,6 +37,7 @@ from ._manager import BaseManager from ._resource import BaseResource +from ._tape_link import TapeLinkManager from ._logging import logged_api_call from ._utils import RC_TAPE_LIBRARY @@ -364,9 +365,21 @@ def __init__(self, manager, uri, name=None, properties=None): ) super().__init__(manager, uri, name, properties) # The manager objects for child resources (with lazy initialization): + self._tape_links = None self._tape_library = None self._cpc = None + @property + def tape_links(self): + """ + :class:`~zhmcclient.TapeLinkManager`: Access to the + :term:`tape links ` in this tape library. + """ + # We do here some lazy loading. + if not self._tape_links: + self._tape_links = TapeLinkManager(self) + return self._tape_links + @logged_api_call def undefine(self): """ diff --git a/zhmcclient/_tape_link.py b/zhmcclient/_tape_link.py new file mode 100644 index 00000000..f205f235 --- /dev/null +++ b/zhmcclient/_tape_link.py @@ -0,0 +1,736 @@ +# Copyright 2026 IBM Corp. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Starting with SE version 2.15.0, tape link management capabilities have been +introduced to support management of connections between tape libraries and +partitions in DPM mode. + +Tape links represent logical connections between a :term:`tape library` and a +:term:`partition`. They enable partitions to access tape storage devices +through FCP adapters. Each tape link defines the relationship and access +parameters for a partition to communicate with a specific tape library. + +Tape links are child resources of :term:`tape libraries `. +In the zhmcclient, the :class:`~zhmcclient.TapeLink` objects are accessible +via the :attr:`~zhmcclient.TapeLibrary.tape_links` property of a +:class:`~zhmcclient.TapeLibrary` object. + +Tape links can be listed, created, deleted, and updated. They facilitate +the attachment of tape libraries to partitions, enabling tape storage +operations within the partition environment. + +Tape links can only be managed on CPCs that support the tape library +management feature (SE version >= 2.15.0). +""" + + +import copy +import re + +from ._manager import BaseManager +from ._resource import BaseResource +from ._logging import logged_api_call +from ._utils import RC_TAPE_LINK, append_query_parms + +__all__ = ['TapeLinkManager', 'TapeLink'] + + +class TapeLinkManager(BaseManager): + """ + Manager providing access to the :term:`tape links ` of a + :term:`tape library`. + + Derived from :class:`~zhmcclient.BaseManager`; see there for common methods + and attributes. + + Objects of this class are not directly created by the user; they are + accessible via the following instance variable: + + * :attr:`~zhmcclient.TapeLibrary.tape_links` of a + :class:`~zhmcclient.TapeLibrary` object. + + HMC/SE version requirements: + + * SE version >= 2.15.0 + """ + + def __init__(self, tape_library): + # This function should not go into the docs. + # Parameters: + # tape_library (:class:`~zhmcclient.TapeLibrary`): + # Tape library defining the scope for this manager. + + # Resource properties that are supported as filter query parameters. + # If the support for a resource property changes within the set of HMC + # versions that support this type of resource, this list must be set up + # for the version of the HMC this session is connected to. + query_props = [ + 'name', + 'partition-uri', + ] + + super().__init__( + resource_class=TapeLink, + class_name=RC_TAPE_LINK, + session=tape_library.manager.session, + parent=tape_library, + base_uri=f'{tape_library.uri}/tape-links', + oid_prop='element-id', + uri_prop='element-uri', + name_prop='name', + query_props=query_props) + self._tape_library = tape_library + + @property + def tape_library(self): + """ + :class:`~zhmcclient.TapeLibrary`: The :term:`tape library` defining + the scope for this manager. + """ + return self._tape_library + + @logged_api_call + def list(self, full_properties=False, filter_args=None): + """ + List the tape links defined in this tape library. + + Tape links for which the authenticated user does not have + object-access permission are not included. + + Any resource property may be specified in a filter argument. For + details about filter arguments, see :ref:`Filtering`. + + The listing of resources is handled in an optimized way: + + * If this manager is enabled for :ref:`auto-updating`, a locally + maintained resource list is used (which is automatically updated via + inventory notifications from the HMC) and the provided filter + arguments are applied. + + * Otherwise, if the filter arguments specify the resource name as a + single filter argument with a straight match string (i.e. without + regular expressions), an optimized lookup is performed based on a + locally maintained name-URI cache. + + * Otherwise, the HMC List operation is performed with the subset of the + provided filter arguments that can be handled on the HMC side and the + remaining filter arguments are applied on the client side on the list + result. + + HMC/SE version requirements: + + * SE version >= 2.15.0 + + Authorization requirements: + + * Object-access permission to this tape library. + * Object-access permission to any tape links to be included in the + result. + + Parameters: + + full_properties (bool): + Controls that the full set of resource properties for each returned + tape link is being retrieved, vs. only the following short + set: "element-uri", "name", and "partition-uri". + + filter_args (dict): + Filter arguments that narrow the list of returned resources to + those that match the specified filter arguments. For details, see + :ref:`Filtering`. + + `None` causes no filtering to happen. + + Returns: + + : A list of :class:`~zhmcclient.TapeLink` objects. + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + :exc:`~zhmcclient.FilterConversionError` + """ + result_prop = 'tape-links' + list_uri = self._base_uri + return self._list_with_operation( + list_uri, result_prop, full_properties, filter_args, None) + + @logged_api_call + def create(self, properties): + """ + Create a tape link in this tape library. + + The new tape link establishes a connection between this tape library + and a partition, enabling the partition to access tape storage devices. + + HMC/SE version requirements: + + * SE version >= 2.15.0 + + Authorization requirements: + + * Object-access permission to this tape library. + * Object-access permission to the partition specified in the + 'partition-uri' property. + * Task permission to the "Configure Storage - System Programmer" task. + + Parameters: + + properties (dict): Initial property values. + Allowable properties are defined in section 'Request body contents' + in section 'Create Tape Link' in the :term:`HMC API` book. + + The 'partition-uri' property identifies the partition to which + this tape link will connect, and is required to be specified. + + Returns: + + :class:`~zhmcclient.TapeLink`: + The resource object for the new tape link. + The object will have its 'element-uri' property set as returned by + the HMC, and will also have the input properties set. + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + """ + result = self.session.post(self._base_uri, body=properties) + # There should not be overlaps, but just in case there are, the + # returned props should overwrite the input props: + props = copy.deepcopy(properties) + props.update(result) + name = props.get(self._name_prop, None) + uri = props[self._uri_prop] + tape_link = TapeLink(self, uri, name, props) + self._name_uri_cache.update(name, uri) + return tape_link + + +class TapeLink(BaseResource): + """ + Representation of a :term:`tape link`. + + Derived from :class:`~zhmcclient.BaseResource`; see there for common + methods and attributes. + + Objects of this class are not directly created by the user; they are + returned from creation or list functions on their manager object + (in this case, :class:`~zhmcclient.TapeLinkManager`). + + HMC/SE version requirements: + + * SE version >= 2.15.0 + """ + + def __init__(self, manager, uri, name=None, properties=None): + # This function should not go into the docs. + # manager (:class:`~zhmcclient.TapeLinkManager`): + # Manager object for this resource object. + # uri (string): + # Canonical URI path of the resource. + # name (string): + # Name of the resource. + # properties (dict): + # Properties to be set for this resource object. May be `None` or + # empty. + assert isinstance(manager, TapeLinkManager), ( + f"TapeLink init: Expected manager type {TapeLinkManager}, " + f"got {type(manager)}") + super().__init__(manager, uri, name, properties) + self._partition = None + self._virtual_tape_resources = None + + @property + def partition(self): + """ + :class:`~zhmcclient.Partition`: The :term:`partition` to which this + tape link is connected. + + The returned :class:`~zhmcclient.Partition` has only a minimal set of + properties populated. + """ + # We do here some lazy loading. + if not self._partition: + partition_uri = self.get_property('partition-uri') + tape_library = self.manager.tape_library + cpc = tape_library.manager.console.manager.client.cpcs.find( + **{'object-uri': tape_library.get_property('cpc-uri')}) + part_mgr = cpc.partitions + self._partition = part_mgr.resource_object(partition_uri) + return self._partition + + @property + def virtual_tape_resources(self): + """ + :class:`~zhmcclient.VirtualTapeResourceManager`: Access to the + :term:`virtual tape resources ` in this + tape link. + """ + # We do here some lazy loading. + if not self._virtual_tape_resources: + # pylint: disable=import-outside-toplevel + from ._virtual_tape_resource import VirtualTapeResourceManager + self._virtual_tape_resources = VirtualTapeResourceManager(self) + return self._virtual_tape_resources + + @logged_api_call + def delete(self): + """ + Delete this tape link and remove the connection between the tape + library and the partition. + + HMC/SE version requirements: + + * SE version >= 2.15.0 + + Authorization requirements: + + * Object-access permission to this tape link. + * Object-access permission to the tape library containing this tape + link. + * Task permission to the "Configure Storage - System Programmer" task. + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + """ + self.manager.session.post( + uri=self.uri + '/operations/delete', resource=self, body={}) + # pylint: disable=protected-access + self.manager._name_uri_cache.delete( + self.get_properties_local(self.manager._name_prop, None)) + self.cease_existence_local() + + @logged_api_call + def update_properties(self, properties): + """ + Update writeable properties of this tape link. + + This method serializes with other methods that access or change + properties on the same Python object. + + HMC/SE version requirements: + + * SE version >= 2.15.0 + + Authorization requirements: + + * Object-access permission to this tape link. + * Object-access permission to the tape library containing this tape + link. + * Task permission to the "Configure Storage - System Programmer" task. + + Parameters: + + properties (dict): New values for the properties to be updated. + Properties not to be updated are omitted. + Allowable properties are listed for operation + 'Modify Tape Link Properties' in section 'Tape Link element object' + in the :term:`HMC API` book. + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + """ + # pylint: disable=protected-access + self.manager.session.post(self.uri, resource=self, body=properties) + is_rename = self.manager._name_prop in properties + if is_rename: + # Delete the old name from the cache + self.manager._name_uri_cache.delete(self.name) + self.update_properties_local(copy.deepcopy(properties)) + if is_rename: + # Add the new name to the cache + self.manager._name_uri_cache.update(self.name, self.uri) + + @logged_api_call + def get_partitions(self, name=None, status=None): + """ + Return the partitions associated with this tape link, optionally + filtered by partition name and status. + + HMC/SE version requirements: + + * SE version >= 2.15.0 + + Authorization requirements: + + * Object-access permission to this tape link. + * Task permission to the "Configure Storage - System Programmer" task. + + Parameters: + + name (:term:`string`): Filter pattern (regular expression) to limit + returned partitions to those that have a matching name. If `None`, + no filtering for the partition name takes place. + + status (:term:`string`): Filter string to limit returned partitions + to those that have a matching status. The value must be a valid + partition status property value. If `None`, no filtering for the + partition status takes place. + + Returns: + + List of :class:`~zhmcclient.Partition` objects representing the + partitions associated with this tape link, + with a minimal set of properties ('object-id', 'name', 'status'). + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + """ + + query_parms = [] + if name is not None: + append_query_parms(query_parms, 'name', name) + if status is not None: + append_query_parms(query_parms, 'status', status) + query_parms_str = '&'.join(query_parms) + if query_parms_str: + query_parms_str = f'?{query_parms_str}' + + uri = f'{self.uri}/operations/get-partitions{query_parms_str}' + + tape_library = self.manager.tape_library + cpc = tape_library.manager.console.manager.client.cpcs.find( + **{'object-uri': tape_library.get_property('cpc-uri')}) + part_mgr = cpc.partitions + + result = self.manager.session.get(uri, resource=self) + props_list = result['partitions'] + part_list = [] + for props in props_list: + part = part_mgr.resource_object(props['object-uri'], props) + part_list.append(part) + return part_list + + @logged_api_call + def get_histories(self): + """ + Get the historical records for this tape link. + + The corresponding HMC operation is "Get Tape Link Histories". + + This operation retrieves historical information about the tape link, + including connection events, configuration changes, and operational + status over time. + + HMC/SE version requirements: + + * SE version >= 2.15.0 + + Authorization requirements: + + * Object-access permission to this tape link. + * Task permission to the "Configure Storage - System Programmer" task + or to the "Configure Storage - Storage Administrator" task. + + Returns: + + :term:`json object`: + A JSON object with the tape link histories. For details about the + items in the JSON object, see section 'Response body contents' in + section 'Get Tape Link Histories' in the :term:`HMC API` book. + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + """ + result = self.manager.session.get( + self.uri + '/operations/get-tape-link-histories', resource=self) + return result + + @logged_api_call + def get_environment_report(self): + """ + Get the latest environment report for this tape link. + + The corresponding HMC operation is "Get Tape Link Environment Report". + + The environment report provides information about the tape link's + operational environment, including connectivity status, adapter + information, and any environmental issues or warnings. + + HMC/SE version requirements: + + * SE version >= 2.15.0 + + Authorization requirements: + + * Object-access permission to this tape link. + * Task permission to the "Configure Storage - System Programmer" task + or to the "Configure Storage - Storage Administrator" task. + + Returns: + + :term:`json object`: + A JSON object with the environment report. For details about the + items in the JSON object, see section 'Response body contents' in + section 'Get Tape Link Environment Report' in the + :term:`HMC API` book. + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + """ + result = self.manager.session.get( + self.uri + '/operations/get-tape-link-environment-report', + resource=self) + return result + + @logged_api_call + def update_environment_report(self, properties): + """ + Update the environment report for this tape link. + + The corresponding HMC operation is + "Update Tape Link Environment Report". + + This operation allows updating specific fields in the tape link's + environment report, such as acknowledging warnings or updating + configuration parameters. + + HMC/SE version requirements: + + * SE version >= 2.15.0 + + Authorization requirements: + + * Object-access permission to this tape link. + * Task permission to the "Configure Storage - System Programmer" task. + + Parameters: + + properties (dict): Properties to be updated in the environment report. + Allowable properties are defined in section 'Request body contents' + in section 'Update Tape Link Environment Report' in the + :term:`HMC API` book. + + Returns: + + :term:`json object`: + A JSON object with the operation results. For details about the + items in the JSON object, see section 'Response body contents' in + section 'Update Tape Link Environment Report' in the + :term:`HMC API` book. + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + """ + result = self.manager.session.post( + self.uri + '/operations/update-tape-link-environment-report', + resource=self, body=properties) + return result + + @logged_api_call + def add_adapter_ports(self, ports): + """ + Add a list of tape adapter ports to this tape link's adapter ports list. + + These adapter ports become candidates for use as backing adapters when + creating virtual tape resources when the tape link is attached to a + partition. The adapter ports should have connectivity to the tape + library. + + HMC/SE version requirements: + + * SE version >= 2.15.0 + + Authorization requirements: + + * Object-access permission to this tape link. + * Object-access permission to the adapter of each specified port. + * Task permission to the "Configure Storage - System Programmer" task. + + Parameters: + + ports (:class:`py:list`): List of :class:`~zhmcclient.Port` objects + representing the ports to be added. All specified ports must not + already be members of this tape link's adapter ports list. + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + """ + body = { + 'adapter-port-uris': [p.uri for p in ports], + } + self.manager.session.post( + self.uri + '/operations/add-adapter-ports', resource=self, + body=body) + + @logged_api_call + def remove_adapter_ports(self, ports): + """ + Remove a list of tape adapter ports from this tape link's adapter + ports list. + + HMC/SE version requirements: + + * SE version >= 2.15.0 + + Authorization requirements: + + * Object-access permission to this tape link. + * Object-access permission to the adapter of each specified port. + * Task permission to the "Configure Storage - System Programmer" task. + + Parameters: + + ports (:class:`py:list`): List of :class:`~zhmcclient.Port` objects + representing the ports to be removed. All specified ports must + currently be members of this tape link's adapter ports list and + must not be referenced by any of the tape link's virtual tape + resources. + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + """ + body = { + 'adapter-port-uris': [p.uri for p in ports], + } + self.manager.session.post( + self.uri + '/operations/remove-adapter-ports', + resource=self, body=body) + + @logged_api_call + def replace_adapter_port(self, current_port, new_port): + """ + Replace a tape adapter port in this tape link's adapter ports list + with a different port. + + This operation allows replacing an adapter port that is currently in + use with a new port, which can be useful for maintenance or + reconfiguration scenarios. + + HMC/SE version requirements: + + * SE version >= 2.15.0 + + Authorization requirements: + + * Object-access permission to this tape link. + * Object-access permission to the adapters of both specified ports. + * Task permission to the "Configure Storage - System Programmer" task. + + Parameters: + + current_port (:class:`~zhmcclient.Port`): The port object + representing the port to be replaced. This port must currently be + a member of this tape link's adapter ports list. + + new_port (:class:`~zhmcclient.Port`): The port object representing + the new port to replace the current port. This port must not + already be a member of this tape link's adapter ports list. + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + """ + body = { + 'current-adapter-port-uri': current_port.uri, + 'new-adapter-port-uri': new_port.uri, + } + self.manager.session.post( + self.uri + '/operations/replace-adapter-port', + resource=self, body=body) + + @logged_api_call + def list_adapter_ports(self, full_properties=False): + """ + Return the current adapter port list of this tape link. + + The result reflects the actual list of ports used by the CPC for this + tape link. The source for this information is the 'adapter-port-uris' + property of the tape link object. + + HMC/SE version requirements: + + * SE version >= 2.15.0 + + Parameters: + + full_properties (bool): + Controls that the full set of resource properties for each returned + adapter port is being retrieved, vs. only the following short + set: "element-uri", "element-id", "class", "parent". + + Returns: + + List of :class:`~zhmcclient.Port` objects representing the + current adapter ports of this tape link. + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + """ + tape_library = self.manager.tape_library + cpc = tape_library.manager.console.manager.client.cpcs.find( + **{'object-uri': tape_library.get_property('cpc-uri')}) + adapter_mgr = cpc.adapters + port_list = [] + port_uris = self.get_property('adapter-port-uris') + if port_uris: + for port_uri in port_uris: + m = re.match(r'^(/api/adapters/[^/]*)/.*', port_uri) + + adapter_uri = m.group(1) + adapter = adapter_mgr.resource_object(adapter_uri) + + port_mgr = adapter.ports + port = port_mgr.resource_object(port_uri) + port_list.append(port) + if full_properties: + port.pull_full_properties() + + return port_list diff --git a/zhmcclient/_virtual_tape_resource.py b/zhmcclient/_virtual_tape_resource.py new file mode 100644 index 00000000..c8c1b532 --- /dev/null +++ b/zhmcclient/_virtual_tape_resource.py @@ -0,0 +1,327 @@ +# Copyright 2026 IBM Corp. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +A :term:`virtual tape resource` object represents a tape-related +z/Architecture device that is visible to a partition and that provides access +for that partition to a :term:`tape library` through a :term:`tape link`. + +Virtual tape resource objects represent the virtualized tape devices in the +partition that are used to access the tape library. Each usage of a virtual +tape device in context of a tape link has its own virtual tape resource object. + +Virtual tape resource objects are instantiated automatically when a tape link +is attached to a partition, and are removed automatically upon detachment. + +Virtual tape resource objects are contained in :term:`tape link` objects. + +Tape links and virtual tape resources can only be managed on CPCs that support +the tape library management feature (SE version >= 2.15.0). +""" + + +import re +import copy + +from ._manager import BaseManager +from ._resource import BaseResource +from ._logging import logged_api_call +from ._utils import RC_VIRTUAL_TAPE_RESOURCE + +__all__ = ['VirtualTapeResourceManager', 'VirtualTapeResource'] + + +class VirtualTapeResourceManager(BaseManager): + """ + Manager providing access to the :term:`virtual tape resources + ` in a particular :term:`tape link`. + + Derived from :class:`~zhmcclient.BaseManager`; see there for common methods + and attributes. + + Objects of this class are not directly created by the user; they are + accessible via the following instance variable of a + :class:`~zhmcclient.TapeLink` object: + + * :attr:`~zhmcclient.TapeLink.virtual_tape_resources` + + HMC/SE version requirements: + + * SE version >= 2.15.0 + """ + + def __init__(self, tape_link): + # This function should not go into the docs. + # Parameters: + # tape_link (:class:`~zhmcclient.TapeLink`): + # Tape link defining the scope for this manager. + + # Resource properties that are supported as filter query parameters. + # If the support for a resource property changes within the set of HMC + # versions that support this type of resource, this list must be set up + # for the version of the HMC this session is connected to. + query_props = [ + 'name', + 'device-number', + 'adapter-port-uri', + 'partition-uri', + ] + + super().__init__( + resource_class=VirtualTapeResource, + class_name=RC_VIRTUAL_TAPE_RESOURCE, + session=tape_link.manager.session, + parent=tape_link, + base_uri=f'{tape_link.uri}/virtual-tape-resources', + oid_prop='element-id', + uri_prop='element-uri', + name_prop='name', + query_props=query_props) + + @property + def tape_link(self): + """ + :class:`~zhmcclient.TapeLink`: :term:`Tape link` defining the + scope for this manager. + """ + return self._parent + + @logged_api_call + def list(self, full_properties=False, filter_args=None): + """ + List the virtual tape resources in this tape link. + + Any resource property may be specified in a filter argument. For + details about filter arguments, see :ref:`Filtering`. + + The listing of resources is handled in an optimized way: + + * If this manager is enabled for :ref:`auto-updating`, a locally + maintained resource list is used (which is automatically updated via + inventory notifications from the HMC) and the provided filter + arguments are applied. + + * Otherwise, if the filter arguments specify the resource name as a + single filter argument with a straight match string (i.e. without + regular expressions), an optimized lookup is performed based on a + locally maintained name-URI cache. + + * Otherwise, the HMC List operation is performed with the subset of the + provided filter arguments that can be handled on the HMC side and the + remaining filter arguments are applied on the client side on the list + result. + + HMC/SE version requirements: + + * SE version >= 2.15.0 + + Authorization requirements: + + * Object-access permission to this tape link. + + Parameters: + + full_properties (bool): + Controls that the full set of resource properties for each returned + virtual tape resource is being retrieved, vs. only the following + short set: "element-uri", "name", "device-number", + "adapter-port-uri", and "partition-uri". + + filter_args (dict): + Filter arguments that narrow the list of returned resources to + those that match the specified filter arguments. For details, see + :ref:`Filtering`. + + `None` causes no filtering to happen, i.e. all resources are + returned. + + Returns: + + : A list of :class:`~zhmcclient.VirtualTapeResource` objects. + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + :exc:`~zhmcclient.FilterConversionError` + """ + result_prop = 'virtual-tape-resources' + list_uri = f'{self.tape_link.uri}/virtual-tape-resources' + return self._list_with_operation( + list_uri, result_prop, full_properties, filter_args, None) + + +class VirtualTapeResource(BaseResource): + """ + Representation of a :term:`virtual tape resource`. + + Derived from :class:`~zhmcclient.BaseResource`; see there for common + methods and attributes. + + Objects of this class are not directly created by the user; they are + returned from creation or list functions on their manager object + (in this case, :class:`~zhmcclient.VirtualTapeResourceManager`). + + HMC/SE version requirements: + + * SE version >= 2.15.0 + """ + + def __init__(self, manager, uri, name=None, properties=None): + # This function should not go into the docs. + # manager (:class:`~zhmcclient.VirtualTapeResourceManager`): + # Manager object for this resource object. + # uri (string): + # Canonical URI path of the resource. + # name (string): + # Name of the resource. + # properties (dict): + # Properties to be set for this resource object. May be `None` or + # empty. + assert isinstance(manager, VirtualTapeResourceManager), ( + "VirtualTapeResource init: Expected manager type " + f"{VirtualTapeResourceManager}, got {type(manager)}") + super().__init__( + manager, uri, name, properties) + self._attached_partition = None + self._adapter_port = None + + @property + def attached_partition(self): + """ + :class:`~zhmcclient.Partition`: The partition to which this virtual + tape resource is attached. + + The returned partition object has only a minimal set of properties set + ('object-id', 'object-uri', 'class', 'parent'). + + Note that a virtual tape resource is always attached to a partition, + as long as it exists. + + HMC/SE version requirements: + + * SE version >= 2.15.0 + + Authorization requirements: + + * Object-access permission to the tape link owning this + virtual tape resource. + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + """ + if self._attached_partition is None: + tape_link = self.manager.tape_link + tape_library = tape_link.manager.tape_library + cpc = tape_library.manager.console.manager.client.cpcs.find( + **{'object-uri': tape_library.get_property('cpc-uri')}) + part_mgr = cpc.partitions + part = part_mgr.resource_object(self.get_property('partition-uri')) + self._attached_partition = part + return self._attached_partition + + @property + def adapter_port(self): + """ + :class:`~zhmcclient.Port`: The tape adapter port associated with + this virtual tape resource, once discovery has determined which + port to use for this virtual tape resource. + + The returned adapter port object has only a minimal set of properties + set ('object-id', 'object-uri', 'class', 'parent'). + + HMC/SE version requirements: + + * SE version >= 2.15.0 + + Authorization requirements: + + * Object-access permission to the tape link owning this + virtual tape resource. + * Object-access permission to the CPC of the tape adapter. + * Object-access permission to the tape adapter. + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + """ + if self._adapter_port is None: + port_uri = self.get_property('adapter-port-uri') + assert port_uri is not None + m = re.match(r'^(/api/adapters/[^/]+)/.*', port_uri) + adapter_uri = m.group(1) + tape_link = self.manager.tape_link + tape_library = tape_link.manager.tape_library + cpc = tape_library.manager.console.manager.client.cpcs.find( + **{'object-uri': tape_library.get_property('cpc-uri')}) + adapter_mgr = cpc.adapters + filter_args = {'object-uri': adapter_uri} + adapter = adapter_mgr.find(**filter_args) + port_mgr = adapter.ports + port = port_mgr.resource_object(port_uri) + self._adapter_port = port + return self._adapter_port + + @logged_api_call + def update_properties(self, properties): + """ + Update writeable properties of this virtual tape resource. + + This method serializes with other methods that access or change + properties on the same Python object. + + HMC/SE version requirements: + + * SE version >= 2.15.0 + + Authorization requirements: + + * Object-access permission to the tape link owning this + virtual tape resource. + * Task permission to the "Configure Storage - System Programmer" task. + + Parameters: + + properties (dict): New values for the properties to be updated. + Properties not to be updated are omitted. + Allowable properties are the properties with qualifier (w) in + section 'Data model' in section 'Virtual Tape Resource object' + in the :term:`HMC API` book. + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + """ + # pylint: disable=protected-access + self.manager.session.post(self.uri, resource=self, body=properties) + is_rename = self.manager._name_prop in properties + if is_rename: + # Delete the old name from the cache + self.manager._name_uri_cache.delete(self.name) + self.update_properties_local(copy.deepcopy(properties)) + if is_rename: + # Add the new name to the cache + self.manager._name_uri_cache.update(self.name, self.uri) diff --git a/zhmcclient/mock/_hmc.py b/zhmcclient/mock/_hmc.py index 225d522c..a22be28e 100644 --- a/zhmcclient/mock/_hmc.py +++ b/zhmcclient/mock/_hmc.py @@ -55,6 +55,8 @@ 'FakedVirtualStorageResourceManager', 'FakedVirtualStorageResource', 'FakedStorageGroupTemplateManager', 'FakedStorageGroupTemplate', 'FakedTapeLibraryManager', 'FakedTapeLibrary', + 'FakedTapeLinkManager', 'FakedTapeLink', + 'FakedVirtualTapeResourceManager', 'FakedVirtualTapeResource', 'FakedMetricsContextManager', 'FakedMetricsContext', 'FakedMetricGroupDefinition', 'FakedMetricObjectValues', 'FakedCapacityGroupManager', 'FakedCapacityGroup', @@ -3815,6 +3817,144 @@ def add(self, properties): return new_tlib +class FakedTapeLinkManager(FakedBaseManager): + """ + A manager for faked TapeLink resources within a faked TapeLibrary (see + :class:`zhmcclient.mock.FakedTapeLibrary`). + + Derived from :class:`zhmcclient.mock.FakedBaseManager`, see there for + common methods and attributes. + """ + + def __init__(self, hmc, tape_library): + super().__init__( + hmc=hmc, + parent=tape_library, + resource_class=FakedTapeLink, + base_uri=tape_library.uri + '/tape-links', + oid_prop='element-id', + uri_prop='element-uri', + class_value='tape-link', + name_prop='name') + + def add(self, properties): + # pylint: disable=useless-super-delegation + """ + Add a faked TapeLink resource. + + Parameters: + + properties (dict): + Resource properties. + + Special handling and requirements for certain properties: + + * 'element-id' will be auto-generated with a unique value across + all instances of this resource type, if not specified. + * 'element-uri' will be auto-generated based upon the object ID, + if not specified. + * 'class' will be auto-generated to 'tape-link', + if not specified. + * 'parent' will be auto-generated to the parent resource URI + (the TapeLibrary URI), if not specified. + + Returns: + + :class:`~zhmcclient.mock.FakedTapeLink`: The faked TapeLink resource. + """ + return super().add(properties) + + +class FakedVirtualTapeResourceManager(FakedBaseManager): + """ + A manager for faked VirtualTapeResource resources within a faked TapeLink + (see :class:`zhmcclient.mock.FakedTapeLink`). + + Derived from :class:`zhmcclient.mock.FakedBaseManager`, see there for + common methods and attributes. + """ + + def __init__(self, hmc, tape_link): + super().__init__( + hmc=hmc, + parent=tape_link, + resource_class=None, # Will be set below + base_uri=tape_link.uri + '/virtual-tape-resources', + oid_prop='element-id', + uri_prop='element-uri', + class_value='virtual-tape-resource', + name_prop='name') + # Set resource_class after the class is defined + self._resource_class = FakedVirtualTapeResource + + def add(self, properties): + # pylint: disable=useless-super-delegation + """ + Add a faked VirtualTapeResource resource. + + Parameters: + + properties (dict): + Resource properties. + + Special handling and requirements for certain properties: + + * ``element-id`` will be auto-generated with a unique value + across all instances of this resource type, if not specified. + * ``element-uri`` will be auto-generated based upon the element + ID, if not specified. + * ``class`` will be auto-generated to ``'virtual-tape-resource'``, + if not specified. + * ``parent`` will be auto-generated to the URI of the parent + TapeLink, if not specified. + + Returns: + :class:`~zhmcclient.mock.FakedVirtualTapeResource`: The faked + VirtualTapeResource resource. + """ + return super().add(properties) + + +class FakedVirtualTapeResource(FakedBaseResource): + """ + A faked VirtualTapeResource resource within a faked TapeLink (see + :class:`zhmcclient.mock.FakedTapeLink`). + + Derived from :class:`zhmcclient.mock.FakedBaseResource`, see there for + common methods and attributes. + """ + + def __init__(self, manager, properties): + super().__init__( + manager=manager, + properties=properties) + + +class FakedTapeLink(FakedBaseResource): + """ + A faked TapeLink resource within a faked TapeLibrary (see + :class:`zhmcclient.mock.FakedTapeLibrary`). + + Derived from :class:`zhmcclient.mock.FakedBaseResource`, see there for + common methods and attributes. + """ + + def __init__(self, manager, properties): + super().__init__( + manager=manager, + properties=properties) + self._virtual_tape_resources = FakedVirtualTapeResourceManager( + hmc=manager.hmc, tape_link=self) + + @property + def virtual_tape_resources(self): + """ + :class:`~zhmcclient.mock.FakedVirtualTapeResourceManager`: Access to + the faked VirtualTapeResource resources of this TapeLink. + """ + return self._virtual_tape_resources + + class FakedTapeLibrary(FakedBaseResource): """ A faked Tape Library resource within a faked HMC (see @@ -3828,6 +3968,16 @@ def __init__(self, manager, properties): super().__init__( manager=manager, properties=properties) + self._tape_links = FakedTapeLinkManager( + hmc=manager.hmc, tape_library=self) + + @property + def tape_links(self): + """ + :class:`~zhmcclient.mock.FakedTapeLinkManager`: Access to the + faked TapeLink resources of this TapeLibrary. + """ + return self._tape_links class FakedCapacityGroupManager(FakedBaseManager): diff --git a/zhmcclient/mock/_session.py b/zhmcclient/mock/_session.py index d0ac19a2..9161e553 100644 --- a/zhmcclient/mock/_session.py +++ b/zhmcclient/mock/_session.py @@ -400,9 +400,49 @@ }, }, "TapeLibrary": { - "description": "An Tape Library on an HMC", + "description": "A Tape Library on an HMC", "type": "object", - "additionalProperties": True, + "additionalProperties": False, + "required": [ + "properties", + ], + "properties": { + "properties": { + "$ref": "#/definitions/Properties" + }, + "tape_links": { + "description": "The tape links of this tape library", + "type": "array", + "items": { + "$ref": "#/definitions/TapeLink" + }, + }, + }, + }, + "TapeLink": { + "description": "A tape link of a tape library", + "type": "object", + "additionalProperties": False, + "required": [ + "properties", + ], + "properties": { + "properties": { + "$ref": "#/definitions/Properties" + }, + "virtual_tape_resources": { + "description": "Virtual tape resources of this tape link", + "type": "array", + "items": { + "$ref": "#/definitions/VirtualTapeResource" + }, + }, + }, + }, + "VirtualTapeResource": { + "description": "A virtual tape resource of a tape link", + "type": "object", + "additionalProperties": False, "required": [ "properties", ], diff --git a/zhmcclient/mock/_urihandler.py b/zhmcclient/mock/_urihandler.py index f94afc92..ae1da2e3 100644 --- a/zhmcclient/mock/_urihandler.py +++ b/zhmcclient/mock/_urihandler.py @@ -5670,6 +5670,325 @@ def post(method, hmc, uri, uri_parms, body, logon_required, message=f"The CPC with the {cpc_uri} has not been zoned.") +class TapeLinksHandler: + """ + Handler class for HTTP methods on set of TapeLink resources. + """ + + valid_query_parms_get = ['tape-library-uri', 'partition-uri', 'name'] + + returned_props = ['element-uri', 'tape-library-uri', 'partition-uri', + 'name', 'description'] + + @classmethod + def get(cls, method, hmc, uri, uri_parms, logon_required): + # pylint: disable=unused-argument + """Operation: List Tape Links.""" + uri, query_parms = parse_query_parms(method, uri) + check_invalid_query_parms( + method, uri, query_parms, cls.valid_query_parms_get) + + # Extract tape library OID from URI + tape_library_oid = uri_parms[0] + tape_library_uri = f'/api/tape-libraries/{tape_library_oid}' + + try: + tape_library = hmc.lookup_by_uri(tape_library_uri) + except KeyError: + new_exc = InvalidResourceError(method, uri) + new_exc.__cause__ = None + raise new_exc # zhmcclient.mock.InvalidResourceError + + filter_args = query_parms + + result_tape_links = [] + for tl in tape_library.tape_links.list(filter_args): + result_tl = {} + for prop in cls.returned_props: + result_tl[prop] = prop_copy(tl.properties.get(prop)) + result_tape_links.append(result_tl) + return {'tape-links': result_tape_links} + + @staticmethod + def post(method, hmc, uri, uri_parms, body, logon_required, + wait_for_completion): + # pylint: disable=unused-argument + """Operation: Create Tape Link.""" + assert wait_for_completion is True # async not supported yet + + # Extract tape library OID from URI + tape_library_oid = uri_parms[0] + tape_library_uri = f'/api/tape-libraries/{tape_library_oid}' + + try: + tape_library = hmc.lookup_by_uri(tape_library_uri) + except KeyError: + new_exc = InvalidResourceError(method, uri) + new_exc.__cause__ = None + raise new_exc # zhmcclient.mock.InvalidResourceError + + check_required_fields(method, uri, body, ['partition-uri']) + + # Verify partition exists + partition_uri = body['partition-uri'] + try: + hmc.lookup_by_uri(partition_uri) + except KeyError: + new_exc = InvalidResourceError(method, uri) + new_exc.__cause__ = None + raise new_exc # zhmcclient.mock.InvalidResourceError + + # Create the tape link + body2 = body.copy() + body2.setdefault('name', f'tape-link-{uuid.uuid4()}') + body2.setdefault('description', '') + body2['tape-library-uri'] = tape_library_uri + + new_tape_link = tape_library.tape_links.add(body2) + + return { + 'element-uri': new_tape_link.uri, + } + + +class TapeLinkHandler(GenericGetPropertiesHandler, + GenericUpdatePropertiesHandler): + """ + Handler class for HTTP methods on single TapeLink resource. + """ + pass + + +class TapeLinkDeleteHandler: + """ + Handler class for operation: Delete Tape Link. + """ + + @staticmethod + def post(method, hmc, uri, uri_parms, body, logon_required, + wait_for_completion): + # pylint: disable=unused-argument + """Operation: Delete Tape Link.""" + assert wait_for_completion is True # async not supported yet + + # Extract tape library and tape link OIDs from URI + tape_library_oid = uri_parms[0] + tape_link_oid = uri_parms[1] + tape_library_uri = f'/api/tape-libraries/{tape_library_oid}' + tape_link_uri = ( + f'/api/tape-libraries/{tape_library_oid}/' + f'tape-links/{tape_link_oid}') + + try: + tape_library = hmc.lookup_by_uri(tape_library_uri) + tape_link = hmc.lookup_by_uri(tape_link_uri) + except KeyError: + new_exc = InvalidResourceError(method, uri) + new_exc.__cause__ = None + raise new_exc # zhmcclient.mock.InvalidResourceError + + # Reflect the result of deleting the tape link + tape_library.tape_links.remove(tape_link.oid) + + +class TapeLinkGetPartitionsHandler: + """ + Handler class for operation: Get Partitions for a Tape Link. + """ + + @staticmethod + def get(method, hmc, uri, uri_parms, logon_required): + # pylint: disable=unused-argument + """Operation: Get Partitions for a Tape Link.""" + + # Extract tape library and tape link OIDs from URI + tape_library_oid = uri_parms[0] + tape_link_oid = uri_parms[1] + tape_link_uri = ( + f'/api/tape-libraries/{tape_library_oid}/' + f'tape-links/{tape_link_oid}') + + try: + tape_link = hmc.lookup_by_uri(tape_link_uri) + except KeyError: + new_exc = InvalidResourceError(method, uri) + new_exc.__cause__ = None + raise new_exc # zhmcclient.mock.InvalidResourceError + + # Get the partition URI from the tape link + partition_uri = tape_link.properties.get('partition-uri') + if not partition_uri: + return {'partitions': []} + + try: + partition = hmc.lookup_by_uri(partition_uri) + except KeyError: + return {'partitions': []} + + # Return partition information + partition_info = { + 'object-uri': partition.uri, + 'name': partition.properties.get('name'), + 'status': partition.properties.get('status'), + } + + return {'partitions': [partition_info]} + + +class TapeLinkGetHistoriesHandler: + """ + Handler class for operation: Get Tape Link Histories. + """ + + @staticmethod + def get(method, hmc, uri, uri_parms, logon_required): + # pylint: disable=unused-argument + """Operation: Get Tape Link Histories.""" + + # Extract tape library and tape link OIDs from URI + tape_library_oid = uri_parms[0] + tape_link_oid = uri_parms[1] + tape_link_uri = ( + f'/api/tape-libraries/{tape_library_oid}/' + f'tape-links/{tape_link_oid}') + + try: + hmc.lookup_by_uri(tape_link_uri) + except KeyError: + new_exc = InvalidResourceError(method, uri) + new_exc.__cause__ = None + raise new_exc # zhmcclient.mock.InvalidResourceError + + # Return mock history data + # In a real implementation, this would return actual history records + return { + 'tape-link-histories': [] + } + + +class TapeLinkGetEnvironmentReportHandler: + """ + Handler class for operation: Get Tape Link Environment Report. + """ + + @staticmethod + def get(method, hmc, uri, uri_parms, logon_required): + # pylint: disable=unused-argument + """Operation: Get Tape Link Environment Report.""" + + # Extract tape library and tape link OIDs from URI + tape_library_oid = uri_parms[0] + tape_link_oid = uri_parms[1] + tape_link_uri = ( + f'/api/tape-libraries/{tape_library_oid}/' + f'tape-links/{tape_link_oid}') + + try: + tape_link = hmc.lookup_by_uri(tape_link_uri) + except KeyError: + new_exc = InvalidResourceError(method, uri) + new_exc.__cause__ = None + raise new_exc # zhmcclient.mock.InvalidResourceError + + # Return mock environment report + # In a real implementation, this would return actual environment data + report = tape_link.properties.get('environment-report', {}) + if not report: + report = { + 'status': 'ok', + 'last-updated': '2024-01-01T00:00:00Z', + 'details': {} + } + + return report + + +class TapeLinkUpdateEnvironmentReportHandler: + """ + Handler class for operation: Update Tape Link Environment Report. + """ + + @staticmethod + def post(method, hmc, uri, uri_parms, body, logon_required, + wait_for_completion): + # pylint: disable=unused-argument + """Operation: Update Tape Link Environment Report.""" + assert wait_for_completion is True # async not supported yet + + # Extract tape library and tape link OIDs from URI + tape_library_oid = uri_parms[0] + tape_link_oid = uri_parms[1] + tape_link_uri = ( + f'/api/tape-libraries/{tape_library_oid}/' + f'tape-links/{tape_link_oid}') + + try: + tape_link = hmc.lookup_by_uri(tape_link_uri) + except KeyError: + new_exc = InvalidResourceError(method, uri) + new_exc.__cause__ = None + raise new_exc # zhmcclient.mock.InvalidResourceError + + # Update the environment report in the tape link properties + if body: + tape_link.properties['environment-report'] = body + + return {} + + +class VirtualTapeResourcesHandler: + """ + Handler class for HTTP methods on set of VirtualTapeResource resources. + """ + + valid_query_parms_get = [ + 'name', 'device-number', 'adapter-port-uri', 'partition-uri'] + + returned_props = [ + 'element-uri', 'name', 'device-number', + 'adapter-port-uri', 'partition-uri'] + + @classmethod + def get(cls, method, hmc, uri, uri_parms, logon_required): + # pylint: disable=unused-argument + """Operation: List Virtual Tape Resources of a Tape Link.""" + uri, query_parms = parse_query_parms(method, uri) + check_invalid_query_parms( + method, uri, query_parms, cls.valid_query_parms_get) + + # Extract tape library and tape link OIDs from URI + tape_library_oid = uri_parms[0] + tape_link_oid = uri_parms[1] + tape_link_uri = ( + f'/api/tape-libraries/{tape_library_oid}/' + f'tape-links/{tape_link_oid}') + + try: + tape_link = hmc.lookup_by_uri(tape_link_uri) + except KeyError: + new_exc = InvalidResourceError(method, uri) + new_exc.__cause__ = None + raise new_exc # zhmcclient.mock.InvalidResourceError + + filter_args = query_parms + + result_vtrs = [] + for vtr in tape_link.virtual_tape_resources.list(filter_args): + result_vtr = {} + for prop in cls.returned_props: + result_vtr[prop] = prop_copy(vtr.properties.get(prop)) + result_vtrs.append(result_vtr) + return {'virtual-tape-resources': result_vtrs} + + +class VirtualTapeResourceHandler(GenericGetPropertiesHandler, + GenericUpdatePropertiesHandler): + """ + Handler class for HTTP methods on single VirtualTapeResource resource. + """ + pass + + class CapacityGroupsHandler: """ Handler class for HTTP methods on set of CapacityGroup resources. @@ -6995,6 +7314,33 @@ def post(method, hmc, uri, uri_parms, body, logon_required, (r'/api/tape-libraries/operations/discover-tape-libraries', TapeLibraryDiscoverHandler), + (r'/api/tape-libraries/([^/]+)/tape-links(?:\?(.*))?', + TapeLinksHandler), + (r'/api/tape-libraries/([^/]+)/tape-links/([^?/]+)(?:\?(.*))?', + TapeLinkHandler), + (r'/api/tape-libraries/([^/]+)/tape-links/([^/]+)/' + r'operations/delete', + TapeLinkDeleteHandler), + (r'/api/tape-libraries/([^/]+)/tape-links/([^/]+)/' + r'operations/get-partitions(?:\?(.*))?', + TapeLinkGetPartitionsHandler), + (r'/api/tape-libraries/([^/]+)/tape-links/([^/]+)/' + r'operations/get-tape-link-histories(?:\?(.*))?', + TapeLinkGetHistoriesHandler), + (r'/api/tape-libraries/([^/]+)/tape-links/([^/]+)/' + r'operations/get-tape-link-environment-report(?:\?(.*))?', + TapeLinkGetEnvironmentReportHandler), + (r'/api/tape-libraries/([^/]+)/tape-links/([^/]+)/' + r'operations/update-tape-link-environment-report(?:\?(.*))?', + TapeLinkUpdateEnvironmentReportHandler), + + (r'/api/tape-libraries/([^/]+)/tape-links/([^/]+)/' + r'virtual-tape-resources(?:\?(.*))?', + VirtualTapeResourcesHandler), + (r'/api/tape-libraries/([^/]+)/tape-links/([^/]+)/' + r'virtual-tape-resources/([^?/]+)(?:\?(.*))?', + VirtualTapeResourceHandler), + (r'/api/cpcs/([^/]+)/capacity-groups(?:\?(.*))?', CapacityGroupsHandler), (r'/api/cpcs/([^/]+)/capacity-groups/([^?/]+)(?:\?(.*))?', CapacityGroupHandler),