From bf7fa0ee12702000175079a4b314c7b33cc2c9db Mon Sep 17 00:00:00 2001 From: Vonteddu Chaithra Date: Thu, 26 Feb 2026 14:58:00 +0530 Subject: [PATCH 1/2] 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 030ffb41..e63ed8c6 100644 --- a/docs/appendix.rst +++ b/docs/appendix.rst @@ -539,14 +539,28 @@ 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 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 virtualized because they belong to a :term:`Partition`. Therefore, the diff --git a/docs/resources.rst b/docs/resources.rst index 12cdfe69..9f5e0a62 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 ef38c069..dc30ade2 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 363c3ffa..ee5c2fee 100644 --- a/zhmcclient/_console.py +++ b/zhmcclient/_console.py @@ -1642,6 +1642,7 @@ def dump(self): "sso_server_definitions": [...], "unmanaged_cpcs": [...], "storage_groups": [...], + "tape_libraries": [...], } Returns: @@ -1680,6 +1681,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 2787acb2..cb198c9a 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 @@ -360,9 +361,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 83ae0530..de8d957d 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', @@ -3819,6 +3821,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 @@ -3832,6 +3972,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 e788247b..55bc58cb 100644 --- a/zhmcclient/mock/_urihandler.py +++ b/zhmcclient/mock/_urihandler.py @@ -5712,6 +5712,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. @@ -7039,6 +7358,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), From 2fd8eb7a5c372acc8e75d514d899765062e972fd Mon Sep 17 00:00:00 2001 From: Vonteddu Chaithra Date: Thu, 9 Apr 2026 12:05:36 +0530 Subject: [PATCH 2/2] Added support for tape link and virtual tape resource Signed-off-by: Vonteddu Chaithra git commit -m "Added support for tape link and virtual tape resource Signed-off-by: Vonteddu Chaithra " --- docs/appendix.rst | 19 +- tests/end2end/mocked_hmc_z16.yaml | 109 ++--- tests/end2end/test_tape_link.py | 283 ++++------- tests/end2end/test_virtual_tape_resource.py | 446 ++++++++---------- tests/unit/zhmcclient/test_tape_link.py | 199 +++----- .../zhmcclient/test_virtual_tape_resource.py | 12 +- zhmcclient/_console.py | 17 + zhmcclient/_tape_library.py | 13 - zhmcclient/_tape_link.py | 367 ++++++++------ zhmcclient/_virtual_tape_resource.py | 70 +-- zhmcclient/mock/_hmc.py | 125 ++--- zhmcclient/mock/_session.py | 11 +- zhmcclient/mock/_urihandler.py | 216 ++++----- 13 files changed, 852 insertions(+), 1035 deletions(-) diff --git a/docs/appendix.rst b/docs/appendix.rst index e63ed8c6..7d0b032d 100644 --- a/docs/appendix.rst +++ b/docs/appendix.rst @@ -547,11 +547,20 @@ Resources scoped to CPCs in DPM mode can see the 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`. + Tape Link + A Tape Link represents a single tape link associated with a DPM-enabled + :term:`CPC`. Tape links define pathways to :term:`Tape Library` storage + that can be attached to :term:`Partitions `. + + When a tape link is attached to a partition, its fulfilled resources are + virtualized and the partition's view of them is represented by + :term:`Virtual Tape Resources `. + + Tape Link resources are top-level resources whose conceptual parent is + the :term:`Console`; a linked tape library is identified by the + ``tape-library-uri`` property of the tape link. + + For details, see section :ref:`Tape Links`.. Virtual Tape Resource A representation of a tape-related z/Architecture device in a :term:`Partition`. diff --git a/tests/end2end/mocked_hmc_z16.yaml b/tests/end2end/mocked_hmc_z16.yaml index dc30ade2..571a0156 100644 --- a/tests/end2end/mocked_hmc_z16.yaml +++ b/tests/end2end/mocked_hmc_z16.yaml @@ -355,7 +355,7 @@ hmc_definition: adapter-port-uri: /api/adapters/fcp1 partition-uri: /api/partitions/part1 - tape_library: + tape_libraries: - properties: # class : created automatically # parent : created automatically @@ -364,69 +364,70 @@ hmc_definition: name : Tape Library description : Tape Library resource 1 cpc-uri : /api/cpcs/cpc_dpm - tape_links: + + tape_links: + - properties: + # class: created automatically + # parent: created automatically + # object-uri: created automatically + object-id: tlink1 + name: "Tape Link 1" + cpc-uri: /api/cpcs/cpc_dpm + description: "Tape link connecting Tape Library to 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: tlink1 - name: "Tape Link 1" - description: "Tape link connecting Tape Library to PART1" + 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 - 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: [] + world-wide-port-name-info: + status: validated + world-wide-port-name: "c05076ffe8000002" + degraded-reasons: [] + - properties: + # class: created automatically + # parent: created automatically + # object-uri: created automatically + object-id: tlink2 + name: "Tape Link 2" + cpc-uri: /api/cpcs/cpc_dpm + description: "Tape link for testing" + tape-library-uri: /api/tape-libraries/tl1 + virtual_tape_resources: - properties: # class: created automatically # parent: created automatically # element-uri: created automatically - element-id: tlink2 - name: "Tape Link 2" - description: "Tape link for testing" + 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 - 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: [] + 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 index b9cc0f00..05919674 100644 --- a/tests/end2end/test_tape_link.py +++ b/tests/end2end/test_tape_link.py @@ -29,11 +29,10 @@ urllib3.disable_warnings() # Properties in minimalistic Tape Link objects (e.g. find_by_name()) -TLINK_MINIMAL_PROPS = ['element-uri', 'name'] +TLINK_MINIMAL_PROPS = ['object-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'] +TLINK_LIST_PROPS = ['object-uri', 'name'] # Properties whose values can change between retrievals of Tape Link objects TLINK_VOLATILE_PROPS = [] @@ -47,29 +46,19 @@ def test_tlink_find_list(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) + # Get tape links from console + tlink_list = console.tape_links.list() + if not tlink_list: + skip_warn(f"No Tape Links defined on HMC {hd.host}") - for tl in tl_list: - print(f"Testing with Tape Library {tl.name!r}") + tlink_list = pick_test_resources(tlink_list) - # 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) + for tlink in tlink_list: + print(f"Testing with Tape Link {tlink.name!r}") + runtest_find_list( + hmc_session, console.tape_links, tlink.name, + 'name', 'object-uri', TLINK_VOLATILE_PROPS, + TLINK_MINIMAL_PROPS, TLINK_LIST_PROPS) def test_tlink_property(hmc_session): @@ -80,30 +69,20 @@ def test_tlink_property(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 from console + tlink_list = console.tape_links.list() + if not tlink_list: + skip_warn(f"No Tape Links defined on HMC {hd.host}") - # 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) - tlink_list = pick_test_resources(tlink_list) + for tlink in tlink_list: + print(f"Testing with Tape Link {tlink.name!r}") - 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' - # Select a property that is not returned by list() - non_list_prop = 'class' - - runtest_get_properties(tlink.manager, non_list_prop) + runtest_get_properties(tlink.manager, non_list_prop) def test_tlink_crud(hmc_session): @@ -114,17 +93,13 @@ def test_tlink_crud(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}") + # Pick a CPC to test with + cpc_list = client.cpcs.list() + if not cpc_list: + skip_warn(f"No CPCs defined on HMC {hd.host}") + cpc = cpc_list[0] - # Get the CPC for this tape library - cpc_uri = tl.get_property('cpc-uri') - cpc = client.cpcs.find(**{'object-uri': cpc_uri}) + print(f"Testing with CPC {cpc.name!r}") # Pick a partition to test with part_list = cpc.partitions.list() @@ -139,23 +114,22 @@ def test_tlink_crud(hmc_session): tlink_props = { 'name': tlink_name, 'description': 'Test tape link for end-to-end testing', - 'partition-uri': partition.uri, + 'cpc-uri': cpc.uri, } # Clean up any existing test tape link try: - existing_tlink = tl.tape_links.find(name=tlink_name) + existing_tlink = console.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) + tlink = console.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 @@ -178,7 +152,7 @@ def test_tlink_crud(hmc_session): assert tlink.properties['name'] == new_name with pytest.raises(zhmcclient.NotFound): - tl.tape_links.find(name=tlink_name) + console.tape_links.find(name=tlink_name) print(f"Renamed Tape Link to {new_name!r}") finally: @@ -198,40 +172,30 @@ def test_tlink_get_partitions(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) + # Get tape links from console + tlink_list = console.tape_links.list() + if not tlink_list: + skip_warn(f"No Tape Links defined on HMC {hd.host}") - for tl in tl_list: - print(f"Testing with Tape Library {tl.name!r}") + tlink_list = pick_test_resources(tlink_list) - # 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 + for tlink in tlink_list: + print(f"Testing with Tape Link {tlink.name!r}") - tlink_list = pick_test_resources(tlink_list) + # The code to be tested + partitions = tlink.get_partitions() - for tlink in tlink_list: - print(f"Testing with Tape Link {tlink.name!r}") + assert isinstance(partitions, list) + print(f"Tape Link {tlink.name!r} has {len(partitions)} " + f"partition(s)") - # 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']}") + 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): @@ -242,36 +206,26 @@ def test_tlink_get_histories(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 from console + tlink_list = console.tape_links.list() + if not tlink_list: + skip_warn(f"No Tape Links defined on HMC {hd.host}") - # 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) - tlink_list = pick_test_resources(tlink_list) + for tlink in tlink_list: + print(f"Testing with Tape Link {tlink.name!r}") - for tlink in tlink_list: - print(f"Testing with Tape Link {tlink.name!r}") + # The code to be tested + histories = tlink.get_histories() - # The code to be tested - histories = tlink.get_histories() + assert isinstance(histories, dict) + print(f"Retrieved histories for Tape Link {tlink.name!r}") - 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)") + # 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): @@ -282,84 +236,37 @@ def test_tlink_environment_report(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) + # Get tape links from console + tlink_list = console.tape_links.list() + if not tlink_list: + skip_warn(f"No Tape Links defined on HMC {hd.host}") - for tl in tl_list: - print(f"Testing with Tape Library {tl.name!r}") + tlink_list = pick_test_resources(tlink_list) - # 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 + for tlink in tlink_list: + print(f"Testing with Tape Link {tlink.name!r}") - tlink_list = pick_test_resources(tlink_list) + # The code to be tested: Get environment report + report = tlink.get_environment_report() - for tlink in tlink_list: - print(f"Testing with Tape Link {tlink.name!r}") + assert isinstance(report, dict) + print(f"Retrieved environment report for Tape Link {tlink.name!r}") - # The code to be tested - partition = tlink.partition + # 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' + } - 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}") + 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 diff --git a/tests/end2end/test_virtual_tape_resource.py b/tests/end2end/test_virtual_tape_resource.py index 13464e17..856d0880 100644 --- a/tests/end2end/test_virtual_tape_resource.py +++ b/tests/end2end/test_virtual_tape_resource.py @@ -48,42 +48,32 @@ def test_vtr_find_list(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 + # Get tape links from console + tlink_list = console.tape_links.list() + if not tlink_list: + skip_warn(f"No Tape Links defined on HMC {hd.host}") - tlink_list = pick_test_resources(tlink_list) + tlink_list = pick_test_resources(tlink_list) - for tlink in tlink_list: - print(f"Testing with Tape Link {tlink.name!r}") + 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 + # 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) + 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) + 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): @@ -94,43 +84,33 @@ def test_vtr_property(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) + # Get tape links from console + tlink_list = console.tape_links.list() + if not tlink_list: + skip_warn(f"No Tape Links defined on HMC {hd.host}") - 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) - tlink_list = pick_test_resources(tlink_list) + for tlink in tlink_list: + print(f"Testing with Tape Link {tlink.name!r}") - 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 + # 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) + vtr_list = pick_test_resources(vtr_list) - for vtr in vtr_list: - print(f"Testing with Virtual Tape Resource " - f"{vtr.name!r}") + 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' + # Select a property that is not returned by list() + non_list_prop = 'class' - runtest_get_properties(vtr.manager, non_list_prop) + runtest_get_properties(vtr.manager, non_list_prop) def test_vtr_update_properties(hmc_session): @@ -141,66 +121,56 @@ def test_vtr_update_properties(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) + # Get tape links from console + tlink_list = console.tape_links.list() + if not tlink_list: + skip_warn(f"No Tape Links defined on HMC {hd.host}") - for tl in tl_list: - print(f"Testing with Tape Library {tl.name!r}") + tlink_list = pick_test_resources(tlink_list) - # 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 + for tlink in tlink_list: + print(f"Testing with Tape Link {tlink.name!r}") - 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 + # 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) + vtr_list = pick_test_resources(vtr_list) - for vtr in vtr_list: - print(f"Testing with Virtual Tape Resource " - f"{vtr.name!r}") + 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') + # 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}) + # 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}" + # 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}) + # 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}" + # 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}") + print(f"Successfully tested update_properties() for " + f"Virtual Tape Resource {vtr.name!r}") def test_vtr_attached_partition(hmc_session): @@ -211,49 +181,39 @@ def test_vtr_attached_partition(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) + # Get tape links from console + tlink_list = console.tape_links.list() + if not tlink_list: + skip_warn(f"No Tape Links defined on HMC {hd.host}") - for tl in tl_list: - print(f"Testing with Tape Library {tl.name!r}") + tlink_list = pick_test_resources(tlink_list) - # 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}") - 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 + # 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) + vtr_list = pick_test_resources(vtr_list) - for vtr in vtr_list: - print(f"Testing with Virtual Tape Resource " - f"{vtr.name!r}") + 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" + # 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}" + # 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): @@ -264,55 +224,45 @@ def test_vtr_adapter_port(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 from console + tlink_list = console.tape_links.list() + if not tlink_list: + skip_warn(f"No Tape Links defined on HMC {hd.host}") - # 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) - tlink_list = pick_test_resources(tlink_list) + for tlink in tlink_list: + print(f"Testing with Tape Link {tlink.name!r}") - 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 + # 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) + vtr_list = pick_test_resources(vtr_list) - for vtr in vtr_list: - print(f"Testing with Virtual Tape Resource " - f"{vtr.name!r}") + 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') + # 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 + 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" + # 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}" + # 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): @@ -323,50 +273,40 @@ def test_vtr_filter_by_device_number(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) + # Get tape links from console + tlink_list = console.tape_links.list() + if not tlink_list: + skip_warn(f"No Tape Links defined on HMC {hd.host}") - 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) - tlink_list = pick_test_resources(tlink_list) + for tlink in tlink_list: + print(f"Testing with Tape Link {tlink.name!r}") - 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 + # 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') + # 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}") + 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}) + # 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}" + 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) + # 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): @@ -377,47 +317,37 @@ def test_vtr_filter_by_partition(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 from console + tlink_list = console.tape_links.list() + if not tlink_list: + skip_warn(f"No Tape Links defined on HMC {hd.host}") - # 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) + tlink_list = pick_test_resources(tlink_list) - for tlink in tlink_list: - print(f"Testing with Tape Link {tlink.name!r}") + 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 + # 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') + # 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}") + 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}) + # 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}" + 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) + # 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 index d828da39..8c6c5174 100644 --- a/tests/unit/zhmcclient/test_tape_link.py +++ b/tests/unit/zhmcclient/test_tape_link.py @@ -30,15 +30,10 @@ # 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' +TLINK1_OID = 'tl1-oid' +TLINK1_NAME = 'tl 1' +TLINK2_OID = 'tl2-oid' +TLINK2_NAME = 'tl 2' class TestTapeLink: @@ -63,27 +58,18 @@ def setup_method(self): 'parent': None, 'class': 'cpc', 'name': 'fake-cpc1-name', - 'description': 'CPC #1 (DPM mode)', + 'description': 'CPC #1 (DPM mode,storage mgmt feature enabled)', 'status': 'active', 'dpm-enabled': True, 'is-ensemble-member': False, 'iml-mode': 'dpm', + 'available-features-list': [ + dict(name='dpm-storage-management', state=True), + ], }) 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 @@ -95,69 +81,61 @@ def setup_method(self): }) 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 + faked_tape_link = self.faked_console.tape_links.add({ + 'object-id': TLINK1_OID, + # object-uri will be automatically set # parent will be automatically set # class will be automatically set + 'cpc-uri': CPC_URI, 'name': TLINK1_NAME, 'description': 'Tape Link #1', - 'partition-uri': PARTITION_URI, + 'max-partitions': 2, + 'fulfillment-state': 'complete', + 'connectivity': 4, }) 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 + faked_tape_link = self.faked_console.tape_links.add({ + 'object-id': TLINK2_OID, + # object-uri will be automatically set # parent will be automatically set # class will be automatically set + 'cpc-uri': CPC_URI, 'name': TLINK2_NAME, 'description': 'Tape Link #2', - 'partition-uri': PARTITION_URI, + 'max-partitions': 2, + 'fulfillment-state': 'complete', + 'connectivity': 4, }) return faked_tape_link def test_tlm_initial_attrs(self): """Test initial attributes of TapeLinkManager.""" - tape_link_mgr = self.tape_library.tape_links + tape_link_mgr = self.console.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 + assert tape_link_mgr.parent == self.console + assert tape_link_mgr.console == self.console # TODO: Test for TapeLinkManager.__repr__() testcases_tlm_list_full_properties = ( "full_properties_kwargs, prop_names", [ ({}, - ['element-uri', 'name', 'partition-uri']), + ['object-uri', 'cpc-uri', 'name', 'fulfillment-state']), (dict(full_properties=False), - ['element-uri', 'name', 'partition-uri']), + ['object-uri', 'cpc-uri', 'name', 'fulfillment-state']), ] ) @@ -173,7 +151,7 @@ def test_tlm_list_full_properties( 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 + tape_link_mgr = self.console.tape_links # Execute the code to be tested tape_links = tape_link_mgr.list(**full_properties_kwargs) @@ -182,19 +160,19 @@ def test_tlm_list_full_properties( testcases_tlm_list_filter_args = ( "filter_args, exp_names", [ - ({'element-id': TLINK1_OID}, + ({'object-id': TLINK1_OID}, [TLINK1_NAME]), - ({'element-id': TLINK2_OID}, + ({'object-id': TLINK2_OID}, [TLINK2_NAME]), - ({'element-id': [TLINK1_OID, TLINK2_OID]}, + ({'object-id': [TLINK1_OID, TLINK2_OID]}, [TLINK1_NAME, TLINK2_NAME]), - ({'element-id': [TLINK1_OID, TLINK1_OID]}, + ({'object-id': [TLINK1_OID, TLINK1_OID]}, [TLINK1_NAME]), - ({'element-id': TLINK1_OID + 'foo'}, + ({'object-id': TLINK1_OID + 'foo'}, []), - ({'element-id': [TLINK1_OID, TLINK2_OID + 'foo']}, + ({'object-id': [TLINK1_OID, TLINK2_OID + 'foo']}, [TLINK1_NAME]), - ({'element-id': [TLINK2_OID + 'foo', TLINK1_OID]}, + ({'object-id': [TLINK2_OID + 'foo', TLINK1_OID]}, [TLINK1_NAME]), ({'name': TLINK1_NAME}, [TLINK1_NAME]), @@ -210,31 +188,31 @@ def test_tlm_list_full_properties( [TLINK1_NAME]), ({'name': [TLINK1_NAME, TLINK1_NAME]}, [TLINK1_NAME]), - ({'name': '.*tape link 1'}, + ({'name': '.*tl 1'}, [TLINK1_NAME]), - ({'name': 'tape link 1.*'}, + ({'name': 'tl 1.*'}, [TLINK1_NAME]), - ({'name': 'tape link .'}, + ({'name': 'tl .'}, [TLINK1_NAME, TLINK2_NAME]), - ({'name': '.ape link 1'}, + ({'name': '.l 1'}, [TLINK1_NAME]), ({'name': '.+'}, [TLINK1_NAME, TLINK2_NAME]), - ({'name': 'tape link 1.+'}, + ({'name': 'tl 1.+'}, []), - ({'name': '.+tape link 1'}, + ({'name': '.+tl 1'}, []), ({'name': TLINK1_NAME, - 'element-id': TLINK1_OID}, + 'object-id': TLINK1_OID}, [TLINK1_NAME]), ({'name': TLINK1_NAME, - 'element-id': TLINK1_OID + 'foo'}, + 'object-id': TLINK1_OID + 'foo'}, []), ({'name': TLINK1_NAME + 'foo', - 'element-id': TLINK1_OID}, + 'object-id': TLINK1_OID}, []), ({'name': TLINK1_NAME + 'foo', - 'element-id': TLINK1_OID + 'foo'}, + 'object-id': TLINK1_OID + 'foo'}, []), ] ) @@ -250,7 +228,7 @@ def test_tlm_list_filter_args( self.add_tape_link1() self.add_tape_link2() - tape_link_mgr = self.tape_library.tape_links + tape_link_mgr = self.console.tape_links # Execute the code to be tested tape_links = tape_link_mgr.list(filter_args=filter_args) @@ -272,8 +250,8 @@ def test_tlm_list_filter_args( None, HTTPError({'http-status': 400, 'reason': 5})), ({'name': 'fake-tlink-x', - 'partition-uri': PARTITION_URI}, - ['element-uri', 'name', 'partition-uri'], + 'cpc-uri': CPC_URI}, + ['object-uri', 'name', 'cpc-uri'], None), ] ) @@ -285,7 +263,7 @@ def test_tlm_create( self, input_props, exp_prop_names, exp_exc): """Test TapeLinkManager.create().""" - tape_link_mgr = self.tape_library.tape_links + tape_link_mgr = self.console.tape_links if exp_exc is not None: @@ -303,7 +281,7 @@ def test_tlm_create( # Execute the code to be tested. # Note: the TapeLink object returned by TapeLink.create() - # has the input properties plus 'element-uri'. + # has the input properties plus 'object-uri'. tape_link = tape_link_mgr.create(properties=input_props) # Check the resource for consistency within itself @@ -312,7 +290,7 @@ def test_tlm_create( 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'] + exp_tape_link_uri = tape_link.properties['object-uri'] assert tape_link_uri == exp_tape_link_uri # Check the properties against the expected names and values @@ -335,21 +313,21 @@ def test_tlm_resource_object(self): faked_tape_link = self.add_tape_link1() tape_link_oid = faked_tape_link.oid - tape_link_mgr = self.tape_library.tape_links + tape_link_mgr = self.console.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}" + tape_link_uri = f'/api/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['object-uri'] == tape_link_uri + assert tape_link.properties['object-id'] == tape_link_oid assert tape_link.properties['class'] == 'tape-link' - assert tape_link.properties['parent'] == TL_URI + assert tape_link.properties['parent'] == self.console.uri def test_tl_repr(self): """Test TapeLink.__repr__().""" @@ -357,7 +335,7 @@ def test_tl_repr(self): # Add a faked tape link faked_tape_link = self.add_tape_link1() - tape_link_mgr = self.tape_library.tape_links + tape_link_mgr = self.console.tape_links tape_link = tape_link_mgr.find(name=faked_tape_link.name) # Execute the code to be tested @@ -377,7 +355,7 @@ def test_tl_delete(self): faked_tape_link = self.add_tape_link1() self.add_tape_link2() - tape_link_mgr = self.tape_library.tape_links + tape_link_mgr = self.console.tape_links tape_link = tape_link_mgr.find(name=faked_tape_link.name) @@ -400,7 +378,7 @@ def test_tl_delete_create_same(self): tl3_props = copy.deepcopy(faked_tape_link.properties) tl3_props['description'] = 'Third tape link' - tape_link_mgr = self.tape_library.tape_links + tape_link_mgr = self.console.tape_links tape_link = tape_link_mgr.find(name=tape_link_name) # Execute the deletion code to be tested. @@ -446,7 +424,7 @@ def test_tl_update_properties( self.add_tape_link1() self.add_tape_link2() - tape_link_mgr = self.tape_library.tape_links + tape_link_mgr = self.console.tape_links tape_link = tape_link_mgr.find(name=tape_link_name) tape_link.pull_full_properties() @@ -487,7 +465,7 @@ def test_tl_update_name(self): faked_tape_link = self.add_tape_link1() tape_link_name = faked_tape_link.name - tape_link_mgr = self.tape_library.tape_links + tape_link_mgr = self.console.tape_links tape_link = tape_link_mgr.find(name=tape_link_name) new_tape_link_name = "new-" + tape_link_name @@ -530,46 +508,13 @@ def test_tl_update_name(self): 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_mgr = self.console.tape_links tape_link = tape_link_mgr.find(name=faked_tape_link.name) # Execute the code to be tested @@ -586,7 +531,7 @@ def test_tl_get_environment_report(self): # Add a faked tape link faked_tape_link = self.add_tape_link1() - tape_link_mgr = self.tape_library.tape_links + tape_link_mgr = self.console.tape_links tape_link = tape_link_mgr.find(name=faked_tape_link.name) # Execute the code to be tested @@ -603,7 +548,7 @@ def test_tl_update_environment_report(self): # Add a faked tape link faked_tape_link = self.add_tape_link1() - tape_link_mgr = self.tape_library.tape_links + tape_link_mgr = self.console.tape_links tape_link = tape_link_mgr.find(name=faked_tape_link.name) # Prepare update properties @@ -619,19 +564,3 @@ def test_tl_update_environment_report(self): 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 index 8c6e0d6b..9d7813dc 100644 --- a/tests/unit/zhmcclient/test_virtual_tape_resource.py +++ b/tests/unit/zhmcclient/test_virtual_tape_resource.py @@ -40,7 +40,7 @@ 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}' +TLINK_URI = f'/api/tape-links/{TLINK_OID}' VTR1_OID = 'vtr1-oid' VTR1_NAME = 'virtual tape resource 1' VTR2_OID = 'vtr2-oid' @@ -131,17 +131,18 @@ def setup_method(self): 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 + self.faked_tape_link = self.faked_console.tape_links.add({ + 'object-id': TLINK_OID, + # object-uri will be automatically set # parent will be automatically set # class will be automatically set + 'cpc-uri': CPC_URI, '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) + self.tape_link = self.console.tape_links.find(name=TLINK_NAME) def add_vtr1(self): """Add virtual tape resource 1.""" @@ -337,6 +338,7 @@ def test_vtrm_resource_object(self): assert vtr.properties['element-uri'] == vtr_uri assert vtr.properties['element-id'] == vtr_oid assert vtr.properties['class'] == 'virtual-tape-resource' + # Parent is the tape link URI assert vtr.properties['parent'] == TLINK_URI def test_vtr_repr(self): diff --git a/zhmcclient/_console.py b/zhmcclient/_console.py index ee5c2fee..6e5d37e2 100644 --- a/zhmcclient/_console.py +++ b/zhmcclient/_console.py @@ -31,6 +31,7 @@ from ._storage_group import StorageGroupManager from ._storage_group_template import StorageGroupTemplateManager from ._tape_library import TapeLibraryManager +from ._tape_link import TapeLinkManager from ._user import UserManager from ._user_role import UserRoleManager from ._user_pattern import UserPatternManager @@ -213,6 +214,7 @@ def __init__(self, manager, uri, name=None, properties=None): self._storage_groups = None self._storage_group_templates = None self._tape_library = None + self._tape_links = None self._partition_links = None self._users = None self._user_roles = None @@ -262,6 +264,17 @@ def tape_library(self): self._tape_library = TapeLibraryManager(self) return self._tape_library + @property + def tape_links(self): + """ + :class:`~zhmcclient.StorageGroupManager`: + Manager object for the Storage Groups in scope of this Console. + """ + # We do here some lazy loading. + if not self._tape_links: + self._tape_links = TapeLinkManager(self) + return self._tape_links + @property def partition_links(self): """ @@ -1643,6 +1656,7 @@ def dump(self): "unmanaged_cpcs": [...], "storage_groups": [...], "tape_libraries": [...], + "tape_links": [...], } Returns: @@ -1684,6 +1698,9 @@ def dump(self): tape_libraries = self.tape_library.dump() if tape_libraries: resource_dict['tape_libraries'] = tape_libraries + tape_links = self.tape_links.dump() + if tape_links: + resource_dict['tape_links'] = tape_links # 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 cb198c9a..2787acb2 100644 --- a/zhmcclient/_tape_library.py +++ b/zhmcclient/_tape_library.py @@ -37,7 +37,6 @@ 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 @@ -361,21 +360,9 @@ 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 index f205f235..0df6aa0a 100644 --- a/zhmcclient/_tape_link.py +++ b/zhmcclient/_tape_link.py @@ -14,22 +14,35 @@ """ 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. +introduced for DPM mode. + +A :term:`tape link` object represents a single tape link associated with a +DPM-enabled :term:`CPC`. Tape links define pathways to tape library storage +that can be attached to partitions. When a tape link is attached to a +partition, its fulfilled resources are virtualized and the partition view of +them is represented by a set of +:term:`virtual tape resources `. + +Tape links are top-level resources whose conceptual parent is the +:term:`Console`. In the zhmcclient, the :class:`~zhmcclient.TapeLink` objects +are accessible via the :attr:`~zhmcclient.Console.tape_links` property of a +:class:`~zhmcclient.Console` object. + +Tape links can be listed, created, deleted, and updated. They also support +querying and updating selected properties of their virtual tape resources. + +Tape link resources have a lifecycle that is reflected in their +``fulfillment-state`` property. Creating or modifying a tape link can require +subsequent SAN configuration changes by a storage administrator before the tape +link resources become usable by a partition. Fulfillment is auto-detected by +the HMC and can transition through states such as ``pending``, ``complete``, +``pending-with-mismatches``, and ``incomplete``. + +The canonical URI of a tape link is ``/api/tape-links/{tape-link-id}``, and +its ``parent`` property identifies the owning Console. The ``cpc-uri`` +property identifies the CPC associated with the tape link, and the +``tape-library-uri`` property identifies the linked tape library once it is +specified or discovered. Tape links can only be managed on CPCs that support the tape library management feature (SE version >= 2.15.0). @@ -41,6 +54,7 @@ from ._manager import BaseManager from ._resource import BaseResource +from ._virtual_tape_resource import VirtualTapeResourceManager from ._logging import logged_api_call from ._utils import RC_TAPE_LINK, append_query_parms @@ -49,8 +63,8 @@ class TapeLinkManager(BaseManager): """ - Manager providing access to the :term:`tape links ` of a - :term:`tape library`. + Manager providing access to the :term:`tape links ` of the + :term:`Console`. Derived from :class:`~zhmcclient.BaseManager`; see there for common methods and attributes. @@ -58,53 +72,57 @@ class TapeLinkManager(BaseManager): 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. + * :attr:`~zhmcclient.Console.tape_links` of a + :class:`~zhmcclient.Console` object. + + The tape links managed by this class are associated with DPM-enabled CPCs + and can be filtered by properties such as ``cpc-uri``, ``name``, and + ``fulfillment-state``. HMC/SE version requirements: * SE version >= 2.15.0 """ - def __init__(self, tape_library): + def __init__(self, console): # This function should not go into the docs. # Parameters: - # tape_library (:class:`~zhmcclient.TapeLibrary`): - # Tape library defining the scope for this manager. + # console (:class:`~zhmcclient.Console`): + # Console 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 = [ + 'cpc-uri', 'name', - 'partition-uri', + 'fulfillment-state', ] 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', + session=console.manager.session, + parent=console, + base_uri='/api/tape-links', + oid_prop='object-id', + uri_prop='object-uri', name_prop='name', query_props=query_props) - self._tape_library = tape_library + self._console = console @property - def tape_library(self): + def console(self): """ - :class:`~zhmcclient.TapeLibrary`: The :term:`tape library` defining - the scope for this manager. + :class:`~zhmcclient.Console`: The Console object representing the HMC. """ - return self._tape_library + return self._console @logged_api_call def list(self, full_properties=False, filter_args=None): """ - List the tape links defined in this tape library. + List the tape links defined on this Console. Tape links for which the authenticated user does not have object-access permission are not included. @@ -135,7 +153,7 @@ def list(self, full_properties=False, filter_args=None): Authorization requirements: - * Object-access permission to this tape library. + * Object-access permission to the Console. * Object-access permission to any tape links to be included in the result. @@ -143,8 +161,8 @@ def list(self, full_properties=False, filter_args=None): 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". + tape link is being retrieved, vs. only a short set of properties + returned by the HMC for tape link list operations. filter_args (dict): Filter arguments that narrow the list of returned resources to @@ -173,10 +191,13 @@ def list(self, full_properties=False, filter_args=None): @logged_api_call def create(self, properties): """ - Create a tape link in this tape library. + Create a tape link on this Console. - The new tape link establishes a connection between this tape library - and a partition, enabling the partition to access tape storage devices. + The new tape link establishes a pathway to tape library storage for a + partition on a DPM-enabled CPC. Depending on the requested properties + and the SAN environment, the new tape link may initially enter the + ``pending`` fulfillment state until its requested resources are + fulfilled. HMC/SE version requirements: @@ -184,9 +205,9 @@ def create(self, properties): Authorization requirements: - * Object-access permission to this tape library. - * Object-access permission to the partition specified in the - 'partition-uri' property. + * Object-access permission to the Console. + * Object-access permission to the CPC and any explicitly referenced + resources such as the tape library or adapter ports. * Task permission to the "Configure Storage - System Programmer" task. Parameters: @@ -195,14 +216,17 @@ def create(self, properties): 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. + Typical input properties include ``name``, ``description``, + ``cpc-uri``, ``connectivity``, ``max-partitions``, + ``tape-library-uri``, and ``adapter-port-uris``. If + ``tape-library-uri`` or enough adapter ports are not specified, the + remaining assignment may be deferred to the storage administrator. 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 object will have its ``object-uri`` property set as returned by the HMC, and will also have the input properties set. Raises: @@ -228,6 +252,10 @@ class TapeLink(BaseResource): """ Representation of a :term:`tape link`. + A tape link is associated with a CPC and conceptually parented by the + Console. It can link to a specific tape library or allow the storage + administrator to choose one during fulfillment. + Derived from :class:`~zhmcclient.BaseResource`; see there for common methods and attributes. @@ -255,27 +283,8 @@ def __init__(self, manager, uri, name=None, properties=None): 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 + self._cpc = None @property def virtual_tape_resources(self): @@ -286,16 +295,98 @@ def virtual_tape_resources(self): """ # 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 + @property + def cpc(self): + """ + :class:`~zhmcclient.Cpc`: The :term:`CPC` to which this tape link + is associated. + + The returned :class:`~zhmcclient.Cpc` has only a minimal set of + properties populated. + """ + # We do here some lazy loading. + if not self._cpc: + cpc_uri = self.get_property('cpc-uri') + cpc_mgr = self.manager.console.manager.client.cpcs + self._cpc = cpc_mgr.resource_object(cpc_uri) + return self._cpc + + @logged_api_call + def get_partitions(self, name=None, status=None): + """ + Return the partitions to which this tape link is currently + attached, optionally filtered by partition name and status. + + If a returned partition is active, the tape link's fulfilled resources + are dynamically available in that partition as virtual tape resources. + + HMC/SE version requirements: + + * SE version >= 2.15.0 + + Authorization requirements: + + * Object-access permission to this tape link. + + 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 to which this tape link is currently attached, + 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}' + + tl_cpc = self.cpc + part_mgr = tl_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 delete(self): + def delete(self, email_to_addresses=None, email_cc_addresses=None, + email_insert=None): """ - Delete this tape link and remove the connection between the tape - library and the partition. + Delete this tape link. + + The tape link must be detached from all partitions before it can be + deleted. If email recipients are specified, the HMC can notify storage + administrators about resource deletion or cleanup actions. HMC/SE version requirements: @@ -304,10 +395,26 @@ def delete(self): 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: + + email_to_addresses (:term:`iterable` of :term:`string`): Email + addresses of one or more storage administrator to be notified. + If `None` or empty, no email will be sent. + + email_cc_addresses (:term:`iterable` of :term:`string`): Email + addresses of one or more storage administrator to be copied + on the notification email. + If `None` or empty, nobody will be copied on the email. + Must be `None` or empty if `email_to_addresses` is `None` or empty. + + email_insert (:term:`string`): Additional text to be inserted in the + notification email. + The text can include HTML formatting tags. + If `None`, no additional text will be inserted. + Must be `None` or empty if `email_to_addresses` is `None` or empty. + Raises: :exc:`~zhmcclient.HTTPError` @@ -315,8 +422,27 @@ def delete(self): :exc:`~zhmcclient.AuthError` :exc:`~zhmcclient.ConnectionError` """ + + body = {} + + if email_to_addresses: + body['email-to-addresses'] = email_to_addresses + if email_cc_addresses: + body['email-cc-addresses'] = email_cc_addresses + if email_insert: + body['email-insert'] = email_insert + else: + if email_cc_addresses: + raise ValueError( + "email_cc_addresses must not be specified if there is no " + f"email_to_addresses: {email_cc_addresses!r}") + if email_insert: + raise ValueError( + "email_insert must not be specified if there is no " + f"email_to_addresses: {email_insert!r}") + self.manager.session.post( - uri=self.uri + '/operations/delete', resource=self, body={}) + uri=self.uri + '/operations/delete', resource=self, body=body) # pylint: disable=protected-access self.manager._name_uri_cache.delete( self.get_properties_local(self.manager._name_prop, None)) @@ -337,8 +463,6 @@ def update_properties(self, properties): 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: @@ -357,7 +481,8 @@ def update_properties(self, properties): :exc:`~zhmcclient.ConnectionError` """ # pylint: disable=protected-access - self.manager.session.post(self.uri, resource=self, body=properties) + uri = f'{self.uri}/operations/modify' + self.manager.session.post(uri, resource=self, body=properties) is_rename = self.manager._name_prop in properties if is_rename: # Delete the old name from the cache @@ -367,70 +492,6 @@ def update_properties(self, properties): # 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): """ @@ -438,9 +499,9 @@ def get_histories(self): 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. + This operation retrieves historical information about the tape link. + For details about the returned content, see the corresponding HMC API + operation. HMC/SE version requirements: @@ -477,9 +538,8 @@ def get_environment_report(self): 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. + The environment report provides information about the current tape link + environment as defined by the HMC API for this operation. HMC/SE version requirements: @@ -519,9 +579,8 @@ def update_environment_report(self, properties): 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. + This operation updates selected fields in the tape link environment + report as supported by the HMC API. HMC/SE version requirements: @@ -714,9 +773,9 @@ def list_adapter_ports(self, full_properties=False): :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')}) + cpc = self.cpc + if not cpc: + return [] adapter_mgr = cpc.adapters port_list = [] port_uris = self.get_property('adapter-port-uris') diff --git a/zhmcclient/_virtual_tape_resource.py b/zhmcclient/_virtual_tape_resource.py index c8c1b532..384f4ba2 100644 --- a/zhmcclient/_virtual_tape_resource.py +++ b/zhmcclient/_virtual_tape_resource.py @@ -13,18 +13,20 @@ # 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`. +A :term:`virtual tape resource` defines the virtualized view of a storage +adapter that is backing a :term:`tape link` as seen by a partition. -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 element resources of a tape link. The +canonical URI of a virtual tape resource is of the form +``/api/tape-links/{tape-link-id}/virtual-tape-resources/`` +``{virtual-tape-resource-id}``, where the owning tape link remains the parent +element and the tape link itself is a top-level resource whose conceptual +parent is the :term:`Console`. -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. +A virtual tape resource is created as tape link resources are fulfilled and +made available to attached partitions. It provides the partition-specific view +of the backing adapter port, allocated WWPN information, and the assigned +device number. Tape links and virtual tape resources can only be managed on CPCs that support the tape library management feature (SE version >= 2.15.0). @@ -45,7 +47,7 @@ class VirtualTapeResourceManager(BaseManager): """ Manager providing access to the :term:`virtual tape resources - ` in a particular :term:`tape link`. + ` of a particular :term:`tape link`. Derived from :class:`~zhmcclient.BaseManager`; see there for common methods and attributes. @@ -100,7 +102,7 @@ def tape_link(self): @logged_api_call def list(self, full_properties=False, filter_args=None): """ - List the virtual tape resources in this tape link. + List the virtual tape resources of this tape link. Any resource property may be specified in a filter argument. For details about filter arguments, see :ref:`Filtering`. @@ -134,9 +136,9 @@ def list(self, full_properties=False, filter_args=None): 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". + virtual tape resource is being retrieved, vs. only a short set of + properties returned by the HMC for virtual tape resource list + operations. filter_args (dict): Filter arguments that narrow the list of returned resources to @@ -168,6 +170,10 @@ class VirtualTapeResource(BaseResource): """ Representation of a :term:`virtual tape resource`. + A virtual tape resource is an element resource of a tape link and + represents the partition-visible view of the backing adapter port for tape + access. Its parent is the owning tape link. + Derived from :class:`~zhmcclient.BaseResource`; see there for common methods and attributes. @@ -205,6 +211,8 @@ def attached_partition(self): :class:`~zhmcclient.Partition`: The partition to which this virtual tape resource is attached. + This is the partition identified by the ``partition-uri`` property. + The returned partition object has only a minimal set of properties set ('object-id', 'object-uri', 'class', 'parent'). @@ -228,11 +236,7 @@ def attached_partition(self): :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_mgr = self.manager.tape_link.cpc.partitions part = part_mgr.resource_object(self.get_property('partition-uri')) self._attached_partition = part return self._attached_partition @@ -240,9 +244,13 @@ def attached_partition(self): @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. + :class:`~zhmcclient.Port`: The backing adapter port associated with + this virtual tape resource. + + This is the port identified by the ``adapter-port-uri`` property. Its + value can change when adapter ports are added, removed, or replaced for + the owning tape link. The property value can be `None` if an adapter + has not yet been discovered to back this virtual tape resource. The returned adapter port object has only a minimal set of properties set ('object-id', 'object-uri', 'class', 'parent'). @@ -270,11 +278,7 @@ def adapter_port(self): 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 + adapter_mgr = self.manager.tape_link.cpc.adapters filter_args = {'object-uri': adapter_uri} adapter = adapter_mgr.find(**filter_args) port_mgr = adapter.ports @@ -287,6 +291,10 @@ def update_properties(self, properties): """ Update writeable properties of this virtual tape resource. + This method updates the writeable properties defined by the HMC API for + virtual tape resource element objects, such as ``name``, + ``description``, and ``device-number``. + This method serializes with other methods that access or change properties on the same Python object. @@ -304,9 +312,9 @@ def update_properties(self, properties): 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. + Allowable properties are the properties with qualifier ``(w)`` + in the data model for the Virtual Tape Resource element object in + the :term:`HMC API` book. Raises: diff --git a/zhmcclient/mock/_hmc.py b/zhmcclient/mock/_hmc.py index de8d957d..0c8858dd 100644 --- a/zhmcclient/mock/_hmc.py +++ b/zhmcclient/mock/_hmc.py @@ -1144,6 +1144,8 @@ def __init__(self, manager, properties): hmc=manager.hmc, console=self) self._tape_library = FakedTapeLibraryManager( hmc=manager.hmc, console=self) + self._tape_links = FakedTapeLinkManager( + hmc=manager.hmc, console=self) self._users = FakedUserManager(hmc=manager.hmc, console=self) self._user_roles = FakedUserRoleManager(hmc=manager.hmc, console=self) self._user_patterns = FakedUserPatternManager( @@ -1220,6 +1222,14 @@ def tape_library(self): """ return self._tape_library + @property + def tape_links(self): + """ + :class:`~zhmcclient.mock.FakedTapeLibraryManager`: Access to + the faked Storage Group Template resources of this Console. + """ + return self._tape_links + @property def users(self): """ @@ -3821,6 +3831,21 @@ def add(self, properties): return new_tlib +class FakedTapeLibrary(FakedBaseResource): + """ + A faked Tape Library resource within a faked HMC (see + :class:`zhmcclient.mock.FakedHmc`). + + 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 FakedTapeLinkManager(FakedBaseManager): """ A manager for faked TapeLink resources within a faked TapeLibrary (see @@ -3830,43 +3855,41 @@ class FakedTapeLinkManager(FakedBaseManager): common methods and attributes. """ - def __init__(self, hmc, tape_library): + def __init__(self, hmc, console): super().__init__( hmc=hmc, - parent=tape_library, + parent=console, resource_class=FakedTapeLink, - base_uri=tape_library.uri + '/tape-links', - oid_prop='element-id', - uri_prop='element-uri', + base_uri=self.api_root + '/tape-links', + oid_prop='object-id', + uri_prop='object-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: +class FakedTapeLink(FakedBaseResource): + """ + A faked TapeLink resource within a faked TapeLibrary (see + :class:`zhmcclient.mock.FakedTapeLibrary`). - * '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. + Derived from :class:`zhmcclient.mock.FakedBaseResource`, see there for + common methods and attributes. + """ - Returns: + def __init__(self, manager, properties): + super().__init__( + manager=manager, + properties=properties) + self._virtual_tape_resources = FakedVirtualTapeResourceManager( + hmc=manager.hmc, tape_link=self) - :class:`~zhmcclient.mock.FakedTapeLink`: The faked TapeLink resource. + @property + def virtual_tape_resources(self): """ - return super().add(properties) + :class:`~zhmcclient.mock.FakedVirtualTapeResourceManager`: Access to + the faked VirtualTapeResource resources of this TapeLink. + """ + return self._virtual_tape_resources class FakedVirtualTapeResourceManager(FakedBaseManager): @@ -3934,56 +3957,6 @@ def __init__(self, manager, properties): 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 - :class:`zhmcclient.mock.FakedHmc`). - - 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._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): """ A manager for faked CapacityGroup resources within a faked Cpc (see diff --git a/zhmcclient/mock/_session.py b/zhmcclient/mock/_session.py index 9161e553..3cc88450 100644 --- a/zhmcclient/mock/_session.py +++ b/zhmcclient/mock/_session.py @@ -225,13 +225,20 @@ "$ref": "#/definitions/StorageGroup" }, }, - "tape_library": { - "description": "The Tape Library defined on this HMC", + "tape_libraries": { + "description": "The Tape Libraries defined on this HMC", "type": "array", "items": { "$ref": "#/definitions/TapeLibrary" }, }, + "tape_links": { + "description": "The tape links defined on this HMC", + "type": "array", + "items": { + "$ref": "#/definitions/TapeLink" + }, + }, "hw_messages": { "description": "The hardware mesages for this Console", "type": "array", diff --git a/zhmcclient/mock/_urihandler.py b/zhmcclient/mock/_urihandler.py index 55bc58cb..829c3133 100644 --- a/zhmcclient/mock/_urihandler.py +++ b/zhmcclient/mock/_urihandler.py @@ -5717,10 +5717,9 @@ class TapeLinksHandler: Handler class for HTTP methods on set of TapeLink resources. """ - valid_query_parms_get = ['tape-library-uri', 'partition-uri', 'name'] + valid_query_parms_get = ['cpc-uri', 'name', 'fulfillment-state'] - returned_props = ['element-uri', 'tape-library-uri', 'partition-uri', - 'name', 'description'] + returned_props = ['object-uri', 'cpc-uri', 'name', 'fulfillment-state'] @classmethod def get(cls, method, hmc, uri, uri_parms, logon_required): @@ -5730,21 +5729,10 @@ def get(cls, method, hmc, uri, uri_parms, logon_required): 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): + for tl in hmc.consoles.console.tape_links.list(filter_args): result_tl = {} for prop in cls.returned_props: result_tl[prop] = prop_copy(tl.properties.get(prop)) @@ -5757,39 +5745,24 @@ def post(method, hmc, uri, uri_parms, body, logon_required, # 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'] + check_required_fields(method, uri, body, ['name', 'cpc-uri']) + cpc_uri = body['cpc-uri'] try: - hmc.lookup_by_uri(partition_uri) + cpc = hmc.lookup_by_uri(cpc_uri) except KeyError: new_exc = InvalidResourceError(method, uri) new_exc.__cause__ = None raise new_exc # zhmcclient.mock.InvalidResourceError + if not cpc.dpm_enabled: + raise CpcNotInDpmError(method, uri, cpc) + check_valid_cpc_status(method, uri, cpc) - # 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) - + body2['fulfillment-state'] = 'pending' + new_tape_link = hmc.consoles.console.tape_links.add(body2) return { - 'element-uri': new_tape_link.uri, + 'object-uri': new_tape_link.uri } @@ -5813,43 +5786,35 @@ def post(method, hmc, uri, uri_parms, body, logon_required, """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}') - + tape_link_oid = uri_parms[0] + tape_link_uri = '/api/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) + # TODO: Check that the Tape Link is detached from any partitions + # Reflect the result of deleting the tape_link + tape_link.manager.remove(tape_link.oid) -class TapeLinkGetPartitionsHandler: + +class TapeLinkUpdateHandler: """ - Handler class for operation: Get Partitions for a Tape Link. + Handler class for operation: Delete Tape Link. """ @staticmethod - def get(method, hmc, uri, uri_parms, logon_required): + def post(method, hmc, uri, uri_parms, body, logon_required, + wait_for_completion): # 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}') + """Operation: Delete Tape Link.""" + assert wait_for_completion is True # async not supported yet + tape_link_oid = uri_parms[0] + tape_link_uri = '/api/tape-links/' + tape_link_oid try: tape_link = hmc.lookup_by_uri(tape_link_uri) except KeyError: @@ -5857,24 +5822,8 @@ def get(method, hmc, uri, uri_parms, logon_required): 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]} + body2 = body.copy() + tape_link.update(body2) class TapeLinkGetHistoriesHandler: @@ -5887,12 +5836,9 @@ 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}') + # Extract tape link OID from URI + tape_link_oid = uri_parms[0] + tape_link_uri = f'/api/tape-links/{tape_link_oid}' try: hmc.lookup_by_uri(tape_link_uri) @@ -5908,6 +5854,54 @@ def get(method, hmc, uri, uri_parms, logon_required): } +class TapeLinkGetPartitionsHandler: + """ + Handler class for operation: Get Partitions attached to Tape Link. + """ + + @staticmethod + def get(method, hmc, uri, uri_parms, logon_required): + # pylint: disable=unused-argument + """Operation: Get Partitions attached to Tape Link.""" + + # Extract tape link OID from URI + tape_link_oid = uri_parms[0] + tape_link_uri = f'/api/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 CPC for this tape link + cpc_uri = tape_link.properties.get('cpc-uri') + if not cpc_uri: + # Return empty list if no CPC is associated + return {'partitions': []} + + try: + cpc = hmc.lookup_by_uri(cpc_uri) + except KeyError: + # Return empty list if CPC not found + return {'partitions': []} + + # Return all partitions from the CPC + # In a real implementation, this would filter based on actual + # attachments + result_partitions = [] + for partition in cpc.partitions.list(): + result_partition = { + 'object-uri': partition.uri, + 'name': partition.properties.get('name'), + 'status': partition.properties.get('status', 'stopped') + } + result_partitions.append(result_partition) + + return {'partitions': result_partitions} + + class TapeLinkGetEnvironmentReportHandler: """ Handler class for operation: Get Tape Link Environment Report. @@ -5918,12 +5912,9 @@ 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}') + # Extract tape link OID from URI + tape_link_oid = uri_parms[0] + tape_link_uri = f'/api/tape-links/{tape_link_oid}' try: tape_link = hmc.lookup_by_uri(tape_link_uri) @@ -5957,12 +5948,9 @@ def post(method, hmc, uri, uri_parms, body, logon_required, """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}') + # Extract tape link OID from URI + tape_link_oid = uri_parms[0] + tape_link_uri = f'/api/tape-links/{tape_link_oid}' try: tape_link = hmc.lookup_by_uri(tape_link_uri) @@ -5998,12 +5986,9 @@ def get(cls, method, hmc, uri, uri_parms, logon_required): 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}') + # Extract tape link OID from URI + tape_link_oid = uri_parms[0] + tape_link_uri = f'/api/tape-links/{tape_link_oid}' try: tape_link = hmc.lookup_by_uri(tape_link_uri) @@ -7358,30 +7343,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(?:\?(.*))?', + (r'/api/tape-links(?:\?(.*))?', TapeLinksHandler), - (r'/api/tape-libraries/([^/]+)/tape-links/([^?/]+)(?:\?(.*))?', + (r'/api/tape-links/([^?/]+)(?:\?(.*))?', TapeLinkHandler), - (r'/api/tape-libraries/([^/]+)/tape-links/([^/]+)/' + (r'/api/tape-links/([^/]+)/' r'operations/delete', TapeLinkDeleteHandler), - (r'/api/tape-libraries/([^/]+)/tape-links/([^/]+)/' - r'operations/get-partitions(?:\?(.*))?', - TapeLinkGetPartitionsHandler), - (r'/api/tape-libraries/([^/]+)/tape-links/([^/]+)/' + (r'/api/tape-links/([^/]+)/' + r'operations/modify', + TapeLinkUpdateHandler), + (r'/api/tape-links/([^/]+)/' r'operations/get-tape-link-histories(?:\?(.*))?', TapeLinkGetHistoriesHandler), - (r'/api/tape-libraries/([^/]+)/tape-links/([^/]+)/' + (r'/api/tape-links/([^/]+)/' + r'operations/get-partitions(?:\?(.*))?', + TapeLinkGetPartitionsHandler), + (r'/api/tape-links/([^/]+)/' r'operations/get-tape-link-environment-report(?:\?(.*))?', TapeLinkGetEnvironmentReportHandler), - (r'/api/tape-libraries/([^/]+)/tape-links/([^/]+)/' + (r'/api/tape-links/([^/]+)/' r'operations/update-tape-link-environment-report(?:\?(.*))?', TapeLinkUpdateEnvironmentReportHandler), - (r'/api/tape-libraries/([^/]+)/tape-links/([^/]+)/' + (r'/api/tape-links/([^/]+)/' r'virtual-tape-resources(?:\?(.*))?', VirtualTapeResourcesHandler), - (r'/api/tape-libraries/([^/]+)/tape-links/([^/]+)/' + (r'/api/tape-links/([^/]+)/' r'virtual-tape-resources/([^?/]+)(?:\?(.*))?', VirtualTapeResourceHandler),