From 5beb8182a689811f71f5a63ea3c1dc6541a57a2a Mon Sep 17 00:00:00 2001 From: Andreas Maier Date: Mon, 31 Jul 2023 00:38:36 +0200 Subject: [PATCH] Support for LPAR Load from FTP Details: * Added support for the 'Load LPAR from FTP' operation by adding a method Lpar.load_from_ftp(). * Extended the HTTP-level mock support by a new fixture http_mocked_lpar. * Added unit tests for Lpar.load_from_ftp() based on HTTP-level mocking. Signed-off-by: Andreas Maier --- docs/changes.rst | 3 + tests/common/http_mocked_fixtures.py | 33 ++++++++ tests/unit/zhmcclient/test_lpar.py | 56 ++++++++++++- zhmcclient/_lpar.py | 120 +++++++++++++++++++++++++++ 4 files changed, 211 insertions(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index 491ee2a5..3cbc6210 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -49,6 +49,9 @@ Released: not yet * Test: Added pytest fixtures for mocking at the HTTP level for unit tests in cases where zhmcclient mock support is not implemented. +* Added support for LPAR Load from FTP via a new Lpar.load_from_ftp() + method. (issue #1048) + **Cleanup:** * Fixed new issue reported by flake8 6.1.0. diff --git a/tests/common/http_mocked_fixtures.py b/tests/common/http_mocked_fixtures.py index db10a30c..d9af2f54 100644 --- a/tests/common/http_mocked_fixtures.py +++ b/tests/common/http_mocked_fixtures.py @@ -157,6 +157,39 @@ def http_mocked_cpc_classic(request, http_mocked_session): # noqa: F811 return cpc +@pytest.fixture( + scope='module' +) +def http_mocked_lpar(request, http_mocked_cpc_classic): # noqa: F811 + # pylint: disable=unused-argument,redefined-outer-name + """ + Pytest fixture representing a HTTP-mocked LPAR on a CPC in classic mode. + + Its CPC object can be accessed as the parent object. + + A test function parameter using this fixture resolves to a + :class:`~zhmcclient.Lpar` object that has only a + minimal set of properties in the object (those that are returned with the + 'List Logical Partitions of CPC' operation). + """ + + with requests_mock.mock() as m: + uri = http_mocked_cpc_classic.uri + '/logical-partitions' + m.get(uri, status_code=200, json={ + 'logical-partitions': [ + { + 'object-uri': '/api/logical-partitions/lpar-id-1', + 'name': 'LPAR1', + 'status': 'operating', + } + ] + }) + lpars = http_mocked_cpc_classic.lpars.list() + lpar = lpars[0] + + return lpar + + @pytest.fixture( scope='module' ) diff --git a/tests/unit/zhmcclient/test_lpar.py b/tests/unit/zhmcclient/test_lpar.py index e3411e51..57f5b88f 100644 --- a/tests/unit/zhmcclient/test_lpar.py +++ b/tests/unit/zhmcclient/test_lpar.py @@ -22,12 +22,19 @@ import copy import mock import pytest +import requests_mock -from zhmcclient import Client, Lpar, HTTPError, StatusTimeout +from zhmcclient import Client, Lpar, HTTPError, StatusTimeout, Job from zhmcclient_mock import FakedSession, LparActivateHandler, \ LparDeactivateHandler, LparLoadHandler from tests.common.utils import assert_resources +# pylint: disable=unused-import,line-too-long +from tests.common.http_mocked_fixtures import http_mocked_session # noqa: F401 +from tests.common.http_mocked_fixtures import http_mocked_cpc_classic # noqa: F401,E501 +from tests.common.http_mocked_fixtures import http_mocked_lpar # noqa: F401 +# pylint: enable=unused-import,line-too-long + # Object IDs and names of our faked LPARs: LPAR1_OID = 'lpar1-oid' @@ -1403,3 +1410,50 @@ def test_console_list_permitted_lpars(self, filter_args, exp_names): # TODO: Test for Lpar.psw_restart() # TODO: Test for Lpar.reset_clear() # TODO: Test for Lpar.reset_normal() + + +def test_lpar_load_from_ftp(http_mocked_lpar): # noqa: F811 + # pylint: disable=redefined-outer-name,unused-argument + """ + Test function for Lpar.load_from_ftp() + """ + session = http_mocked_lpar.manager.session + uri = http_mocked_lpar.uri + '/operations/load-from-ftp' + + # Define the input parameters for the test call + host = 'test-ftp-host-1' + username = 'test-user' + password = 'test-pwd' + load_file = '/images/load1.img' + protocol = 'sftp' + + job_uri = '/api/jobs/job-1' + + exp_request_body = { + 'host-name': host, + 'user-name': username, + 'password': password, + 'file-path': load_file, + 'protocol': protocol, + } + exp_status_code = 202 + result_body = { + 'job-uri': job_uri, + } + exp_result_job = Job(session, job_uri, 'POST', uri) + + rm_adapter = requests_mock.Adapter(case_sensitive=True) + with requests_mock.mock(adapter=rm_adapter) as m: + + m.post(uri, status_code=exp_status_code, json=result_body) + + result_job = http_mocked_lpar.load_from_ftp( + host, username, password, load_file, protocol, + wait_for_completion=False) + + assert rm_adapter.called + request_body = rm_adapter.last_request.json() + assert request_body == exp_request_body + assert result_job.uri == exp_result_job.uri + assert result_job.op_method == exp_result_job.op_method + assert result_job.op_uri == exp_result_job.op_uri diff --git a/zhmcclient/_lpar.py b/zhmcclient/_lpar.py index 09e5b417..a774497c 100644 --- a/zhmcclient/_lpar.py +++ b/zhmcclient/_lpar.py @@ -1258,6 +1258,126 @@ def load(self, load_address=None, load_parameter=None, self.wait_for_status(statuses, status_timeout) return result + @logged_api_call + def load_from_ftp( + self, host, username, password, load_file, protocol='ftp', + wait_for_completion=True, operation_timeout=None, + status_timeout=None, allow_status_exceptions=False): + """ + Load (boot) this LPAR from an FTP server, using the HMC operation + "Load Logical Partition from FTP". + + This operation is not permitted for an LPAR whose 'activation-mode' + property is "zaware" or "ssc". + + This HMC operation has deferred status behavior: If the asynchronous + job on the HMC is complete, it takes a few seconds until the LPAR + status has reached the desired value. If `wait_for_completion=True`, + this method repeatedly checks the status of the LPAR after the HMC + operation has completed, and waits until the status is in the desired + state "operating", or if `allow_status_exceptions` was + set additionally in the state "exceptions". + + Authorization requirements: + + * Object-access permission to this LPAR. + * Before HMC API version 3.6 in an update to HMC 2.15.0: Object-access + permission to the CPC of this LPAR. + * Task permission for the "Load from Removable Media or Server" task. + + Parameters: + + host (string): Host name or IP address of the FTP server. + + username (string): User name for the account on the FTP server. + + password (string): Password that is associated with the user name on + the FTP server. + + load_file (string): Path name of the file to be read from the FTP + server and loaded into the LPAR. + + protocol (string): Network protocol for transferring files. Must be + one of: + + * "ftp" - File Transfer Protocol + * "ftps" - FTP Secure + * "sftp" - SSH File Transfer Protocol + + Default: "ftp" + + wait_for_completion (bool): + Boolean controlling whether this method should wait for completion + of the requested asynchronous HMC operation, as follows: + + * If `True`, this method will wait for completion of the + asynchronous job performing the operation, and for the status + becoming "operating" (or in addition "exceptions", if + `allow_status_exceptions` was set. + + * If `False`, this method will return immediately once the HMC has + accepted the request to perform the operation. + + operation_timeout (:term:`number`): + Timeout in seconds, for waiting for completion of the asynchronous + job performing the operation. The special value 0 means that no + timeout is set. `None` means that the default async operation + timeout of the session is used. If the timeout expires when + `wait_for_completion=True`, a + :exc:`~zhmcclient.OperationTimeout` is raised. + + status_timeout (:term:`number`): + Timeout in seconds, for waiting that the status of the LPAR has + reached the desired status, after the HMC operation has completed. + The special value 0 means that no timeout is set. `None` means that + the default async operation timeout of the session is used. + If the timeout expires when `wait_for_completion=True`, a + :exc:`~zhmcclient.StatusTimeout` is raised. + + allow_status_exceptions (bool): + Boolean controlling whether LPAR status "exceptions" is considered + an additional acceptable end status when `wait_for_completion` is + set. + + Returns: + + `None` or :class:`~zhmcclient.Job`: + + If `wait_for_completion` is `True`, returns `None`. + + If `wait_for_completion` is `False`, returns a + :class:`~zhmcclient.Job` object representing the asynchronously + executing job on the HMC. + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + :exc:`~zhmcclient.OperationTimeout`: The timeout expired while + waiting for completion of the operation. + :exc:`~zhmcclient.StatusTimeout`: The timeout expired while + waiting for the desired LPAR status. + """ + body = { + 'host-name': host, + 'user-name': username, + 'password': password, + 'file-path': load_file, + 'protocol': protocol, + } + result = self.manager.session.post( + self.uri + '/operations/load-from-ftp', body=body, + wait_for_completion=wait_for_completion, + operation_timeout=operation_timeout) + if wait_for_completion: + statuses = ["operating"] + if allow_status_exceptions: + statuses.append("exceptions") + self.wait_for_status(statuses, status_timeout) + return result + @logged_api_call def stop(self, wait_for_completion=True, operation_timeout=None, status_timeout=None, allow_status_exceptions=False):