diff --git a/docs/source/modules.rst b/docs/source/modules.rst index 2192f01a9..2b5fd4074 100644 --- a/docs/source/modules.rst +++ b/docs/source/modules.rst @@ -75,6 +75,7 @@ Modules supported only with CPCs in DPM operational mode: modules/zhmc_nic modules/zhmc_nic_list modules/zhmc_partition + modules/zhmc_partition_command modules/zhmc_partition_list modules/zhmc_partition_messages modules/zhmc_storage_group @@ -89,6 +90,7 @@ Modules supported only with CPCs in classic operational mode: :glob: modules/zhmc_lpar + modules/zhmc_lpar_command modules/zhmc_lpar_list modules/zhmc_lpar_messages diff --git a/docs/source/modules/zhmc_lpar_command.rst b/docs/source/modules/zhmc_lpar_command.rst new file mode 100644 index 000000000..5d5f8949b --- /dev/null +++ b/docs/source/modules/zhmc_lpar_command.rst @@ -0,0 +1,195 @@ + +:github_url: https://github.com/ansible-collections/ibm_zos_core/blob/dev/plugins/modules/zhmc_lpar_command.py + +.. _zhmc_lpar_command_module: + + +zhmc_lpar_command -- Execute OS console command in an LPAR (classic mode) +========================================================================= + + + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Execute a command in the console of the OS running in an LPAR and get back the command output. + + +Requirements +------------ + +- The targeted CPC must be in the classic operational mode. +- The targeted LPAR must be loaded (i.e. running an operating system). +- The HMC userid must have these task permissions: 'Operating System Messages' (view-only is sufficient) +- The HMC userid must have object-access permissions to these objects: Target CPC, target LPAR. + + + + +Parameters +---------- + + +hmc_host + The hostnames or IP addresses of a single HMC or of a list of redundant HMCs. A single HMC can be specified as a string type or as an HMC list with one item. An HMC list can be specified as a list type or as a string type containing a Python list representation. + + The first available HMC of a list of redundant HMCs is used for the entire execution of the module. + + | **required**: True + | **type**: raw + + +hmc_auth + The authentication credentials for the HMC. + + | **required**: True + | **type**: dict + + + userid + The userid (username) for authenticating with the HMC. This is mutually exclusive with providing \ :literal:`hmc\_auth.session\_id`\ . + + | **required**: False + | **type**: str + + + password + The password for authenticating with the HMC. This is mutually exclusive with providing \ :literal:`hmc\_auth.session\_id`\ . + + | **required**: False + | **type**: str + + + session_id + HMC session ID to be used. This is mutually exclusive with providing \ :literal:`hmc\_auth.userid`\ and \ :literal:`hmc\_auth.password`\ and can be created as described in the \ :ref:`zhmc\_session module `\ . + + | **required**: False + | **type**: str + + + ca_certs + Path name of certificate file or certificate directory to be used for verifying the HMC certificate. If null (default), the path name in the \ :envvar:`REQUESTS\_CA\_BUNDLE`\ environment variable or the path name in the \ :envvar:`CURL\_CA\_BUNDLE`\ environment variable is used, or if neither of these variables is set, the certificates in the Mozilla CA Certificate List provided by the 'certifi' Python package are used for verifying the HMC certificate. + + | **required**: False + | **type**: str + + + verify + If True (default), verify the HMC certificate as specified in the \ :literal:`hmc\_auth.ca\_certs`\ parameter. If False, ignore what is specified in the \ :literal:`hmc\_auth.ca\_certs`\ parameter and do not verify the HMC certificate. + + | **required**: False + | **type**: bool + | **default**: True + + + +cpc_name + The name of the CPC with the target LPAR. + + | **required**: True + | **type**: str + + +name + The name of the target LPAR. + + | **required**: True + | **type**: str + + +command + The OS console command to be executed. + + | **required**: True + | **type**: str + + +is_priority + Controls whether the command is executed as a priority command. + + | **required**: False + | **type**: bool + + +log_file + File path of a log file to which the logic flow of this module as well as interactions with the HMC are logged. If null, logging will be propagated to the Python root logger. + + | **required**: False + | **type**: str + + + + +Examples +-------- + +.. code-block:: yaml+jinja + + + --- + # Note: The following examples assume that some variables named 'my_*' are set. + + - name: Get z/OS system time via OS console command + zhmc_lpar_command: + hmc_host: "{{ my_hmc_host }}" + hmc_auth: "{{ my_hmc_auth }}" + cpc_name: "{{ my_cpc_name }}" + name: "{{ my_lpar_name }}" + command: "D T" + register: zos_time_output + + + + + + + + + + +Return Values +------------- + + +changed + Indicates if any change has been made by the module. + + This will always be true, because it is not clear whether the command has performed a change. Note that a playbook using this module with a command that does not perform a change can override that by specifying \ :literal:`changed\_when: false`\ . + + | **returned**: always + | **type**: bool + +msg + An error message that describes the failure. + + | **returned**: failure + | **type**: str + +output + The command and its output, as one item per line, without any trailing newlines. + + The format of each message text depends on the type of OS. Typical formats are, showing the message with the command: + + z/VM: \ :literal:`04:30:02 Q CPLEVEL`\ + + z/OS: \ :literal:`D T`\ + + Linux: \ :literal:`uname -a`\ + + | **returned**: success + | **type**: list + | **elements**: str + | **sample**: + + .. code-block:: json + + [ + "D T", + "RESPONSE=GR1 IEE136I LOCAL: TIME=09.25.08 DATE=2024.194 UTC:", + "RESPONSE=TIME=07.25.08 DATE=2024.194" + ] + diff --git a/docs/source/modules/zhmc_partition_command.rst b/docs/source/modules/zhmc_partition_command.rst new file mode 100644 index 000000000..2b3500c0b --- /dev/null +++ b/docs/source/modules/zhmc_partition_command.rst @@ -0,0 +1,194 @@ + +:github_url: https://github.com/ansible-collections/ibm_zos_core/blob/dev/plugins/modules/zhmc_partition_command.py + +.. _zhmc_partition_command_module: + + +zhmc_partition_command -- Execute OS console command in a partition (DPM mode) +============================================================================== + + + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Execute a command in the console of the OS running in a partition and get back the command output. + + +Requirements +------------ + +- The targeted CPC must be in the DPM operational mode. +- The targeted partition must be active (i.e. running an operating system). +- The HMC userid must have these task permissions: 'Operating System Messages' (view-only is sufficient) +- The HMC userid must have object-access permissions to these objects: Target CPC, target partition. + + + + +Parameters +---------- + + +hmc_host + The hostnames or IP addresses of a single HMC or of a list of redundant HMCs. A single HMC can be specified as a string type or as an HMC list with one item. An HMC list can be specified as a list type or as a string type containing a Python list representation. + + The first available HMC of a list of redundant HMCs is used for the entire execution of the module. + + | **required**: True + | **type**: raw + + +hmc_auth + The authentication credentials for the HMC. + + | **required**: True + | **type**: dict + + + userid + The userid (username) for authenticating with the HMC. This is mutually exclusive with providing \ :literal:`hmc\_auth.session\_id`\ . + + | **required**: False + | **type**: str + + + password + The password for authenticating with the HMC. This is mutually exclusive with providing \ :literal:`hmc\_auth.session\_id`\ . + + | **required**: False + | **type**: str + + + session_id + HMC session ID to be used. This is mutually exclusive with providing \ :literal:`hmc\_auth.userid`\ and \ :literal:`hmc\_auth.password`\ and can be created as described in the \ :ref:`zhmc\_session module `\ . + + | **required**: False + | **type**: str + + + ca_certs + Path name of certificate file or certificate directory to be used for verifying the HMC certificate. If null (default), the path name in the \ :envvar:`REQUESTS\_CA\_BUNDLE`\ environment variable or the path name in the \ :envvar:`CURL\_CA\_BUNDLE`\ environment variable is used, or if neither of these variables is set, the certificates in the Mozilla CA Certificate List provided by the 'certifi' Python package are used for verifying the HMC certificate. + + | **required**: False + | **type**: str + + + verify + If True (default), verify the HMC certificate as specified in the \ :literal:`hmc\_auth.ca\_certs`\ parameter. If False, ignore what is specified in the \ :literal:`hmc\_auth.ca\_certs`\ parameter and do not verify the HMC certificate. + + | **required**: False + | **type**: bool + | **default**: True + + + +cpc_name + The name of the CPC with the target partition. + + | **required**: True + | **type**: str + + +name + The name of the target partition. + + | **required**: True + | **type**: str + + +command + The OS console command to be executed. + + | **required**: True + | **type**: str + + +is_priority + Controls whether the command is executed as a priority command. + + | **required**: False + | **type**: bool + + +log_file + File path of a log file to which the logic flow of this module as well as interactions with the HMC are logged. If null, logging will be propagated to the Python root logger. + + | **required**: False + | **type**: str + + + + +Examples +-------- + +.. code-block:: yaml+jinja + + + --- + # Note: The following examples assume that some variables named 'my_*' are set. + + - name: Get z/VM CP level via OS console command + zhmc_partition_command: + hmc_host: "{{ my_hmc_host }}" + hmc_auth: "{{ my_hmc_auth }}" + cpc_name: "{{ my_cpc_name }}" + name: "{{ my_partition_name }}" + command: "Q CPLEVEL" + register: zvm_cplevel_output + + + + + + + + + + +Return Values +------------- + + +changed + Indicates if any change has been made by the module. + + This will always be true, because it is not clear whether the command has performed a change. Note that a playbook using this module with a command that does not perform a change can override that by specifying \ :literal:`changed\_when: false`\ . + + | **returned**: always + | **type**: bool + +msg + An error message that describes the failure. + + | **returned**: failure + | **type**: str + +output + The command and its output, as one item per line, without any trailing newlines. + + The format of each message text depends on the type of OS. Typical formats are, showing the message with the command: + + z/VM: \ :literal:`04:30:02 Q CPLEVEL`\ + + Linux: \ :literal:`uname -a`\ + + | **returned**: success + | **type**: list + | **elements**: str + | **sample**: + + .. code-block:: json + + [ + "04:30:02 Q CPLEVEL", + "04:30:02 z/VM Version 7 Release 2.0, service level 2101 (64-bit)", + "04:30:02 Generated at 05/19/21 10:00:00 CES", + "04:30:02 IPL at 06/04/24 19:18:57 CES" + ] + diff --git a/docs/source/release_notes.rst b/docs/source/release_notes.rst index 93e0f2181..a44fa825b 100644 --- a/docs/source/release_notes.rst +++ b/docs/source/release_notes.rst @@ -129,6 +129,13 @@ Availability: `AutomationHub`_, `Galaxy`_, `GitHub`_ * Added a new Ansible module 'zhmc_cpc_capacity' for managing the temporary processor capacity of a CPC. (issue #243) +* Added new modules 'zhmc_lpar_command' and 'zhmc_partition_command' for + executing an OS consosle command in the OS running in an LPAR or + partition. (issue #938) + +* Enabled notification logging via the 'zhmcclient.jms' Python logger. + (related to issue #938) + **Cleanup:** * Modernized the code to match the minimum Python version 3.8 (use of f-strings, diff --git a/plugins/module_utils/common.py b/plugins/module_utils/common.py index 4f0b16872..ec51d77cb 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -21,6 +21,7 @@ import logging import traceback +import threading import platform import sys import re @@ -1184,6 +1185,11 @@ def log_init(logger_name, log_file=None): if handler: ensure_one_handler(logger, handler) + logger = logging.getLogger('zhmcclient.jms') + logger.setLevel(logging.DEBUG) + if handler: + ensure_one_handler(logger, handler) + def ensure_one_handler(logger, handler): """ @@ -1334,3 +1340,71 @@ def hyphen_properties(prop_dict): pvalue = hyphen_properties(pvalue) hyphen_prop_dict[pname_hyphen] = pvalue return hyphen_prop_dict + + +class NotificationThread(threading.Thread): + """ + A thread class derived from :class:`py:threading.Thread` that is designed + for running threads that receive zhmcclient notifications. + + Capabilities: + + * handles exceptions that are raised in the started thread, by re-raising + them in the thread that joins the started thread. + + * can be stopped by calling stop(). The thread function must regularly + check for whether it should stop by calling need_to_stop(). + + * can indicate an arbitrary readiness condition to the calling thread. + + The thread function needs to be specified with the 'target' init argument. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._exc_info = None + self._stop_event = threading.Event() + self._ready_event = threading.Event() + + def run(self): + """ + Call inherited run() and save exception info. + """ + try: + super().run() + except Exception: # noqa: E722 pylint: disable=broad-except + self._exc_info = sys.exc_info() + + def join(self, timeout=None): + """ + Call inherited join() and reraise exception if exception info was saved. + """ + super().join(timeout) + if self._exc_info: + raise self._exc_info.value + + def stop(self): + """ + In the code that started the thread, request the thread to stop. + """ + self._stop_event.set() + + def need_to_stop(self): + """ + In the thread function, check whether a stop has been requested. + """ + return self._stop_event.is_set() + + def ready(self): + """ + In the thread function, indicate readiness. + """ + self._ready_event.set() + + def wait_ready(self, timeout=None): + """ + In the code that started the thread, wait for readiness. + + The timeout is an int or float in seconds. + """ + return self._ready_event.wait(timeout) diff --git a/plugins/modules/zhmc_lpar_command.py b/plugins/modules/zhmc_lpar_command.py new file mode 100644 index 000000000..ababc480a --- /dev/null +++ b/plugins/modules/zhmc_lpar_command.py @@ -0,0 +1,440 @@ +#!/usr/bin/python +# Copyright 2024 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. + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +# For information on the format of the ANSIBLE_METADATA, DOCUMENTATION, +# EXAMPLES, and RETURN strings, see +# http://docs.ansible.com/ansible/dev_guide/developing_modules_documenting.html + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['stableinterface'], + 'supported_by': 'community', + 'shipped_by': 'other', + 'other_repo_url': 'https://github.com/zhmcclient/zhmc-ansible-modules' +} + +DOCUMENTATION = """ +--- +module: zhmc_lpar_command +version_added: "2.15.0" +short_description: Execute OS console command in an LPAR (classic mode) +description: + - Execute a command in the console of the OS running in an LPAR and + get back the command output. +author: + - Andreas Maier (@andy-maier) +requirements: + - The targeted CPC must be in the classic operational mode. + - The targeted LPAR must be loaded (i.e. running an operating system). + - "The HMC userid must have these task permissions: + 'Operating System Messages' (view-only is sufficient)" + - "The HMC userid must have object-access permissions to these objects: + Target CPC, target LPAR." +options: + hmc_host: + description: + - The hostnames or IP addresses of a single HMC or of a list of redundant + HMCs. A single HMC can be specified as a string type or as an HMC list + with one item. An HMC list can be specified as a list type or as a + string type containing a Python list representation. + - The first available HMC of a list of redundant HMCs is used for the + entire execution of the module. + type: raw + required: true + hmc_auth: + description: + - The authentication credentials for the HMC. + type: dict + required: true + suboptions: + userid: + description: + - The userid (username) for authenticating with the HMC. + This is mutually exclusive with providing O(hmc_auth.session_id). + type: str + required: false + default: null + password: + description: + - The password for authenticating with the HMC. + This is mutually exclusive with providing O(hmc_auth.session_id). + type: str + required: false + default: null + session_id: + description: + - HMC session ID to be used. + This is mutually exclusive with providing O(hmc_auth.userid) and + O(hmc_auth.password) and can be created as described in the + R(zhmc_session module,zhmc_session_module). + type: str + required: false + default: null + ca_certs: + description: + - Path name of certificate file or certificate directory to be used + for verifying the HMC certificate. If null (default), the path name + in the E(REQUESTS_CA_BUNDLE) environment variable or the path name + in the E(CURL_CA_BUNDLE) environment variable is used, or if neither + of these variables is set, the certificates in the Mozilla CA + Certificate List provided by the 'certifi' Python package are used + for verifying the HMC certificate. + type: str + required: false + default: null + verify: + description: + - If True (default), verify the HMC certificate as specified in the + O(hmc_auth.ca_certs) parameter. If False, ignore what is specified in the + O(hmc_auth.ca_certs) parameter and do not verify the HMC certificate. + type: bool + required: false + default: true + cpc_name: + description: + - The name of the CPC with the target LPAR. + type: str + required: true + name: + description: + - The name of the target LPAR. + type: str + required: true + command: + description: + - "The OS console command to be executed." + type: str + required: true + is_priority: + description: + - "Controls whether the command is executed as a priority command." + type: bool + required: false + default: false + log_file: + description: + - "File path of a log file to which the logic flow of this module as well + as interactions with the HMC are logged. If null, logging will be + propagated to the Python root logger." + type: str + required: false + default: null + _faked_session: + description: + - "An internal parameter used for testing the module." + type: raw + required: false + default: null +""" + +EXAMPLES = """ +--- +# Note: The following examples assume that some variables named 'my_*' are set. + +- name: Get z/OS system time via OS console command + zhmc_lpar_command: + hmc_host: "{{ my_hmc_host }}" + hmc_auth: "{{ my_hmc_auth }}" + cpc_name: "{{ my_cpc_name }}" + name: "{{ my_lpar_name }}" + command: "D T" + register: zos_time_output +""" + +RETURN = """ +changed: + description: + - "Indicates if any change has been made by the module." + - "This will always be true, because it is not clear whether the command + has performed a change. Note that a playbook using this module with + a command that does not perform a change can override that by specifying + C(changed_when: false)." + returned: always + type: bool +msg: + description: An error message that describes the failure. + returned: failure + type: str +output: + description: + - "The command and its output, as one item per line, without any trailing + newlines." + - "The format of each message text depends on the type of OS. + Typical formats are, showing the message with the command:" + - "z/VM: C(04:30:02 Q CPLEVEL)" + - "z/OS: C(D T)" + - "Linux: C(uname -a)" + returned: success + type: list + elements: str + sample: + [ + "D T", + "RESPONSE=GR1 IEE136I LOCAL: TIME=09.25.08 DATE=2024.194 UTC:", + "RESPONSE=TIME=07.25.08 DATE=2024.194", + ] +""" + +import logging # noqa: E402 +import traceback # noqa: E402 +import threading # noqa: E402 +import queue # noqa: E402 +from ansible.module_utils.basic import AnsibleModule # noqa: E402 + +from ..module_utils.common import log_init, open_session, close_session, \ + hmc_auth_parameter, Error, NotificationThread, missing_required_lib, \ + common_fail_on_import_errors, parse_hmc_host # noqa: E402 + +try: + import urllib3 + IMP_URLLIB3_ERR = None +except ImportError: + IMP_URLLIB3_ERR = traceback.format_exc() + +try: + import zhmcclient + IMP_ZHMCCLIENT_ERR = None +except ImportError: + IMP_ZHMCCLIENT_ERR = traceback.format_exc() + +# Python logger name for this module +LOGGER_NAME = 'zhmc_lpar_command' + +LOGGER = logging.getLogger(LOGGER_NAME) + + +class NoCommandFoundError(Error): + """ + Indicates that the command was not found in the OS console messages. + """ + pass + + +def find_lpar(client, cpc_name, lpar_name): + """ + Find the specified LPAR in the specified CPC. + + The "List Permitted Logical Partitions" operation is used when available. + + Returns: + zhmcclient.Lpar + + Raises: + zhmcclient.NotFound: LPAR does not exist. + """ + # The "List Permitted Logical Partitions" operation was added in HMC + # version 2.14.0. The operation depends only on the HMC version and not + # on the SE/CPC version, so it is supported e.g. for a 2.14 HMC managing + # a z13 CPC. + hmc_version = client.query_api_version()['hmc-version'] + hmc_version_info = [int(x) for x in hmc_version.split('.')] + if hmc_version_info < [2, 14, 0]: + # Find the LPAR in the traditional way + cpc = client.cpcs.find(name=cpc_name) + lpar = cpc.lpars.find(name=lpar_name) + else: + # Find the LPAR using the new operation + filter_args = {'cpc-name': cpc_name, 'name': lpar_name} + lpars = client.consoles.console.list_permitted_lpars( + filter_args=filter_args) + try: + lpar = lpars[0] + except IndexError: + raise zhmcclient.NotFound( + message=f"Could not find LPAR {lpar_name!r} in permitted " + f"LPARs of CPC {cpc_name!r}") + return lpar + + +def add_messages(receiver, msg_queue): + """ + Receive the OS message notifications in the specified receiver and + add them to the specified result_list. The function returns when the + receiver is exhausted (which happens when it is closed). + """ + this_thread = threading.current_thread() + LOGGER.debug("Message thread: Receiving messages") + # pylint: disable=unused-variable + for header, message in receiver.notifications(): + + # Indicate we are ready for command execution + this_thread.ready() + + for msg_info in message['os-messages']: + msg_txt = msg_info['message-text'] + msg_txt = msg_txt.rstrip('\n') + LOGGER.debug("Message thread: Got message: %r", msg_txt) + msg_queue.put(msg_txt) + if this_thread.need_to_stop(): + LOGGER.debug("Message thread: Stop requested") + return + + LOGGER.warning("Message thread: Unexpected end of notification loop") + + +def perform_command(params): + """ + Send a command to the OS console, and return the command output as a module + result. + + This is done by setting up a notification receiver for OS console messages, + that receives OS messages in a separate thread, while the OS console + command is executed. + + The resulting messages are then examined to find the command itself, which + determines the starting point for returning the messages. + + The end point is reached when there are no more OS messages coming in + for some seconds. + + Raises: + zhmcclient.Error: Any zhmcclient exception can happen. + """ + + hmc_host = params['hmc_host'] + hmc_auth = params['hmc_auth'] + cpc_name = params['cpc_name'] + lpar_name = params['name'] + command = params['command'] + is_priority = params['is_priority'] + + session, logoff = open_session(params) + try: + client = zhmcclient.Client(session) + + lpar = find_lpar(client, cpc_name, lpar_name) + + LOGGER.debug("Opening message channel to LPAR %r", lpar.name) + topic = lpar.open_os_message_channel(include_refresh_messages=False) + + receiver = zhmcclient.NotificationReceiver( + topic, hmc_host, hmc_auth['userid'], hmc_auth['password']) + + try: + + LOGGER.debug("Starting message thread") + msg_queue = queue.Queue() + msg_thread = NotificationThread( + target=add_messages, args=(receiver, msg_queue)) + msg_thread.start() + + readiness_timeout = 2 + LOGGER.debug("Waiting for message thread readiness (timeout: %d)", + readiness_timeout) + msg_thread.wait_ready(timeout=readiness_timeout) + + LOGGER.debug("Executing command: %r", command) + lpar.send_os_command(command, is_priority) + + # Process the messages being received + LOGGER.debug("Processing received messages") + result = [] + command_upper = command.upper() + command_found = False + no_more_messages_timeout = 2 + while True: + try: + msg = msg_queue.get(timeout=no_more_messages_timeout) + except queue.Empty: + LOGGER.debug("Found no more messages for %d s", + no_more_messages_timeout) + break + + if not command_found: + if command_upper in msg.upper(): + LOGGER.debug("Found command in message: %r", msg) + result.append(msg) + command_found = True + continue + + result.append(msg) + + finally: + LOGGER.debug("Closing receiver") + receiver.close() + + return result + + finally: + close_session(session, logoff) + + +def main(): + """Main function""" + + # The following definition of module input parameters must match the + # description of the options in the DOCUMENTATION string. + argument_spec = dict( + hmc_host=dict(required=True, type='raw'), + hmc_auth=hmc_auth_parameter(), + cpc_name=dict(required=True, type='str'), + name=dict(required=True, type='str'), + command=dict(required=True, type='str'), + is_priority=dict(required=False, type='bool', default=False), + log_file=dict(required=False, type='str', default=None), + _faked_session=dict(required=False, type='raw'), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True) + + if IMP_URLLIB3_ERR is not None: + module.fail_json(msg=missing_required_lib("requests"), + exception=IMP_URLLIB3_ERR) + + urllib3.disable_warnings() + + if IMP_ZHMCCLIENT_ERR is not None: + module.fail_json(msg=missing_required_lib("zhmcclient"), + exception=IMP_ZHMCCLIENT_ERR) + + common_fail_on_import_errors(module) + + log_file = module.params['log_file'] + log_init(LOGGER_NAME, log_file) + + module.params['hmc_host'] = parse_hmc_host(module.params['hmc_host']) + + _params = dict(module.params) + del _params['hmc_auth'] + LOGGER.debug("Module entry: params: %r", _params) + + changed = True + try: + + result = perform_command(module.params) + + except (Error, zhmcclient.Error) as exc: + # These exceptions are considered errors in the environment or in user + # input. They have a proper message that stands on its own, so we + # simply pass that message on and will not need a traceback. + msg = f"{exc.__class__.__name__}: {exc}" + LOGGER.debug( + "Module exit (failure): msg: %s", msg) + module.fail_json(msg=msg) + # Other exceptions are considered module errors and are handled by Ansible + # by showing the traceback. + + LOGGER.debug( + "Module exit (success): changed: %r, output: %r", changed, result) + module.exit_json(changed=changed, output=result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/zhmc_partition_command.py b/plugins/modules/zhmc_partition_command.py new file mode 100644 index 000000000..1e91e3722 --- /dev/null +++ b/plugins/modules/zhmc_partition_command.py @@ -0,0 +1,441 @@ +#!/usr/bin/python +# Copyright 2024 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. + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +# For information on the format of the ANSIBLE_METADATA, DOCUMENTATION, +# EXAMPLES, and RETURN strings, see +# http://docs.ansible.com/ansible/dev_guide/developing_modules_documenting.html + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['stableinterface'], + 'supported_by': 'community', + 'shipped_by': 'other', + 'other_repo_url': 'https://github.com/zhmcclient/zhmc-ansible-modules' +} + +DOCUMENTATION = """ +--- +module: zhmc_partition_command +version_added: "2.15.0" +short_description: Execute OS console command in a partition (DPM mode) +description: + - Execute a command in the console of the OS running in a partition and + get back the command output. +author: + - Andreas Maier (@andy-maier) +requirements: + - The targeted CPC must be in the DPM operational mode. + - The targeted partition must be active (i.e. running an operating system). + - "The HMC userid must have these task permissions: + 'Operating System Messages' (view-only is sufficient)" + - "The HMC userid must have object-access permissions to these objects: + Target CPC, target partition." +options: + hmc_host: + description: + - The hostnames or IP addresses of a single HMC or of a list of redundant + HMCs. A single HMC can be specified as a string type or as an HMC list + with one item. An HMC list can be specified as a list type or as a + string type containing a Python list representation. + - The first available HMC of a list of redundant HMCs is used for the + entire execution of the module. + type: raw + required: true + hmc_auth: + description: + - The authentication credentials for the HMC. + type: dict + required: true + suboptions: + userid: + description: + - The userid (username) for authenticating with the HMC. + This is mutually exclusive with providing O(hmc_auth.session_id). + type: str + required: false + default: null + password: + description: + - The password for authenticating with the HMC. + This is mutually exclusive with providing O(hmc_auth.session_id). + type: str + required: false + default: null + session_id: + description: + - HMC session ID to be used. + This is mutually exclusive with providing O(hmc_auth.userid) and + O(hmc_auth.password) and can be created as described in the + R(zhmc_session module,zhmc_session_module). + type: str + required: false + default: null + ca_certs: + description: + - Path name of certificate file or certificate directory to be used + for verifying the HMC certificate. If null (default), the path name + in the E(REQUESTS_CA_BUNDLE) environment variable or the path name + in the E(CURL_CA_BUNDLE) environment variable is used, or if neither + of these variables is set, the certificates in the Mozilla CA + Certificate List provided by the 'certifi' Python package are used + for verifying the HMC certificate. + type: str + required: false + default: null + verify: + description: + - If True (default), verify the HMC certificate as specified in the + O(hmc_auth.ca_certs) parameter. If False, ignore what is specified in the + O(hmc_auth.ca_certs) parameter and do not verify the HMC certificate. + type: bool + required: false + default: true + cpc_name: + description: + - The name of the CPC with the target partition. + type: str + required: true + name: + description: + - The name of the target partition. + type: str + required: true + command: + description: + - "The OS console command to be executed." + type: str + required: true + is_priority: + description: + - "Controls whether the command is executed as a priority command." + type: bool + required: false + default: false + log_file: + description: + - "File path of a log file to which the logic flow of this module as well + as interactions with the HMC are logged. If null, logging will be + propagated to the Python root logger." + type: str + required: false + default: null + _faked_session: + description: + - "An internal parameter used for testing the module." + type: raw + required: false + default: null +""" + +EXAMPLES = """ +--- +# Note: The following examples assume that some variables named 'my_*' are set. + +- name: Get z/VM CP level via OS console command + zhmc_partition_command: + hmc_host: "{{ my_hmc_host }}" + hmc_auth: "{{ my_hmc_auth }}" + cpc_name: "{{ my_cpc_name }}" + name: "{{ my_partition_name }}" + command: "Q CPLEVEL" + register: zvm_cplevel_output +""" + +RETURN = """ +changed: + description: + - "Indicates if any change has been made by the module." + - "This will always be true, because it is not clear whether the command + has performed a change. Note that a playbook using this module with + a command that does not perform a change can override that by specifying + C(changed_when: false)." + returned: always + type: bool +msg: + description: An error message that describes the failure. + returned: failure + type: str +output: + description: + - "The command and its output, as one item per line, without any trailing + newlines." + - "The format of each message text depends on the type of OS. + Typical formats are, showing the message with the command:" + - "z/VM: C(04:30:02 Q CPLEVEL)" + - "Linux: C(uname -a)" + returned: success + type: list + elements: str + sample: + [ + "04:30:02 Q CPLEVEL", + "04:30:02 z/VM Version 7 Release 2.0, service level 2101 (64-bit)", + "04:30:02 Generated at 05/19/21 10:00:00 CES", + "04:30:02 IPL at 06/04/24 19:18:57 CES" + ] +""" + +import logging # noqa: E402 +import traceback # noqa: E402 +import threading # noqa: E402 +import queue # noqa: E402 +from ansible.module_utils.basic import AnsibleModule # noqa: E402 + +from ..module_utils.common import log_init, open_session, close_session, \ + hmc_auth_parameter, Error, NotificationThread, missing_required_lib, \ + common_fail_on_import_errors, parse_hmc_host # noqa: E402 + +try: + import urllib3 + IMP_URLLIB3_ERR = None +except ImportError: + IMP_URLLIB3_ERR = traceback.format_exc() + +try: + import zhmcclient + IMP_ZHMCCLIENT_ERR = None +except ImportError: + IMP_ZHMCCLIENT_ERR = traceback.format_exc() + +# Python logger name for this module +LOGGER_NAME = 'zhmc_partition_command' + +LOGGER = logging.getLogger(LOGGER_NAME) + + +class NoCommandFoundError(Error): + """ + Indicates that the command was not found in the OS console messages. + """ + pass + + +def find_partition(client, cpc_name, partition_name): + """ + Find the specified partition in the specified CPC. + + The "List Permitted Logical Partitions" operation is used when available. + + Returns: + zhmcclient.Partition + + Raises: + zhmcclient.NotFound: partition does not exist. + """ + # The "List Permitted Logical Partitions" operation was added in HMC + # version 2.14.0. The operation depends only on the HMC version and not + # on the SE/CPC version, so it is supported e.g. for a 2.14 HMC managing + # a z13 CPC. + hmc_version = client.query_api_version()['hmc-version'] + hmc_version_info = [int(x) for x in hmc_version.split('.')] + if hmc_version_info < [2, 14, 0]: + # Find the partition in the traditional way + cpc = client.cpcs.find(name=cpc_name) + partition = cpc.partitions.find(name=partition_name) + else: + # Find the partition using the new operation + filter_args = {'cpc-name': cpc_name, 'name': partition_name} + partitions = client.consoles.console.list_permitted_partitions( + filter_args=filter_args) + try: + partition = partitions[0] + except IndexError: + raise zhmcclient.NotFound( + message=f"Could not find partition {partition_name!r} in " + f"permitted partitions of CPC {cpc_name!r}") + return partition + + +def add_messages(receiver, msg_queue): + """ + Receive the OS message notifications in the specified receiver and + put them to the specified messsage queue. The function returns when the + receiver is exhausted (which happens when it is closed). + """ + this_thread = threading.current_thread() + LOGGER.debug("Message thread: Receiving messages") + # pylint: disable=unused-variable + for header, message in receiver.notifications(): + + # Indicate we are ready for command execution + this_thread.ready() + + for msg_info in message['os-messages']: + msg_txt = msg_info['message-text'] + msg_txt = msg_txt.rstrip('\n') + LOGGER.debug("Message thread: Got message: %r", msg_txt) + msg_queue.put(msg_txt) + if this_thread.need_to_stop(): + LOGGER.debug("Message thread: Stop requested") + return + + LOGGER.warning("Message thread: Unexpected end of notification loop") + + +def perform_command(params): + """ + Send a command to the OS console, and return the command output as a module + result. + + This is done by setting up a notification receiver for OS console messages, + that receives OS messages in a separate thread, while the OS console + command is executed. + + The resulting messages are then examined to find the command itself, which + determines the starting point for returning the messages. + + The end point is reached when there are no more OS messages coming in + for some seconds. + + Raises: + zhmcclient.Error: Any zhmcclient exception can happen. + """ + + hmc_host = params['hmc_host'] + hmc_auth = params['hmc_auth'] + cpc_name = params['cpc_name'] + partition_name = params['name'] + command = params['command'] + is_priority = params['is_priority'] + + session, logoff = open_session(params) + try: + client = zhmcclient.Client(session) + + partition = find_partition(client, cpc_name, partition_name) + + LOGGER.debug("Opening message channel to partition %r", partition.name) + topic = partition.open_os_message_channel( + include_refresh_messages=False) + + receiver = zhmcclient.NotificationReceiver( + topic, hmc_host, hmc_auth['userid'], hmc_auth['password']) + + try: + + LOGGER.debug("Starting message thread") + msg_queue = queue.Queue() + msg_thread = NotificationThread( + target=add_messages, args=(receiver, msg_queue)) + msg_thread.start() + + readiness_timeout = 2 + LOGGER.debug("Waiting for message thread readiness (timeout: %d)", + readiness_timeout) + msg_thread.wait_ready(timeout=readiness_timeout) + + LOGGER.debug("Executing command: %r", command) + partition.send_os_command(command, is_priority) + + # Process the messages being received + LOGGER.debug("Processing received messages") + result = [] + command_upper = command.upper() + command_found = False + no_more_messages_timeout = 2 + while True: + try: + msg = msg_queue.get(timeout=no_more_messages_timeout) + except queue.Empty: + LOGGER.debug("Found no more messages for %d s", + no_more_messages_timeout) + break + + if not command_found: + if command_upper in msg.upper(): + LOGGER.debug("Found command in message: %r", msg) + result.append(msg) + command_found = True + continue + + result.append(msg) + + finally: + LOGGER.debug("Closing receiver") + receiver.close() + + return result + + finally: + close_session(session, logoff) + + +def main(): + """Main function""" + + # The following definition of module input parameters must match the + # description of the options in the DOCUMENTATION string. + argument_spec = dict( + hmc_host=dict(required=True, type='raw'), + hmc_auth=hmc_auth_parameter(), + cpc_name=dict(required=True, type='str'), + name=dict(required=True, type='str'), + command=dict(required=True, type='str'), + is_priority=dict(required=False, type='bool', default=False), + log_file=dict(required=False, type='str', default=None), + _faked_session=dict(required=False, type='raw'), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True) + + if IMP_URLLIB3_ERR is not None: + module.fail_json(msg=missing_required_lib("requests"), + exception=IMP_URLLIB3_ERR) + + urllib3.disable_warnings() + + if IMP_ZHMCCLIENT_ERR is not None: + module.fail_json(msg=missing_required_lib("zhmcclient"), + exception=IMP_ZHMCCLIENT_ERR) + + common_fail_on_import_errors(module) + + log_file = module.params['log_file'] + log_init(LOGGER_NAME, log_file) + + module.params['hmc_host'] = parse_hmc_host(module.params['hmc_host']) + + _params = dict(module.params) + del _params['hmc_auth'] + LOGGER.debug("Module entry: params: %r", _params) + + changed = True + try: + + result = perform_command(module.params) + + except (Error, zhmcclient.Error) as exc: + # These exceptions are considered errors in the environment or in user + # input. They have a proper message that stands on its own, so we + # simply pass that message on and will not need a traceback. + msg = f"{exc.__class__.__name__}: {exc}" + LOGGER.debug( + "Module exit (failure): msg: %s", msg) + module.fail_json(msg=msg) + # Other exceptions are considered module errors and are handled by Ansible + # by showing the traceback. + + LOGGER.debug( + "Module exit (success): changed: %r, output: %r", changed, result) + module.exit_json(changed=changed, output=result) + + +if __name__ == '__main__': + main() diff --git a/tests/end2end/test_zhmc_lpar_command.py b/tests/end2end/test_zhmc_lpar_command.py new file mode 100644 index 000000000..868871126 --- /dev/null +++ b/tests/end2end/test_zhmc_lpar_command.py @@ -0,0 +1,218 @@ +# Copyright 2024 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 zhmc_lpar_command module. +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import random +import re +from unittest import mock +import pytest +import urllib3 +# pylint: disable=line-too-long,unused-import +from zhmcclient.testutils import hmc_definition, hmc_session # noqa: F401, E501 +from zhmcclient.testutils import classic_mode_cpcs # noqa: F401, E501 +# pylint: enable=line-too-long,unused-import + +from plugins.modules import zhmc_lpar_command +from .utils import mock_ansible_module, get_failure_msg + +urllib3.disable_warnings() + +LOGGING = False +LOG_FILE = 'zhmc_lpar_command.log' if LOGGING else None + + +def get_module_output(mod_obj): + """ + Return the module output as a tuple (changed, messages) (i.e. + the arguments of the call to exit_json()). + If the module failed, return None. + """ + + def func(changed, output): + return changed, output + + if not mod_obj.exit_json.called: + return None + call_args = mod_obj.exit_json.call_args + + # The following makes sure we get the arguments regardless of whether they + # were specified as positional or keyword arguments: + return func(*call_args[0], **call_args[1]) + + +TESTCASES_ZHMC_LPAR_COMMAND = [ + # Testcases for test_zhmc_lpar_command(), each with these items: + # * desc (str): Testcase description + # * in_params (dict): Input parameters passed to module, except for + # hmc_host, hmc_auth, cpc, name, command, log_file. + # * act_mode (str): LPAR activation-mode property, for selecting the LPAR + # * os_type (str): LPAR os-type property, for selecting the LPAR + # * command (str): Command to be executed + # * exp_output (list): Expected command output lines, as a list of regexp + + ( + "Simple z/VM command", + { + 'is_priority': False, + }, + "zvm", + "z/VM", + "Q CPLEVEL", + [ + ".* Q CPLEVEL", + ".* z/VM Version .* Release .* service level .*", + ".* Generated at .*", + ".* IPL at .*", + ] + ), + ( + "Simple z/OS command", + { + 'is_priority': False, + }, + "esa390", + "z/OS", + "D T", + [ + "D T", + "RESPONSE=GR1 .* TIME=.*", + "RESPONSE=TIME=.* DATE=.*", + ] + ), + # TODO: Find out how to deal with Linux password prompt + # ( + # "Simple Linux command", + # { + # 'is_priority': False, + # }, + # "linux", + # "Linux", + # "uname -a", + # [ + # "uname -a", + # "Kernel Version .* s390x", + # ] + # ), +] + + +@pytest.mark.parametrize( + "desc, in_params, act_mode, os_type, command, exp_output", + TESTCASES_ZHMC_LPAR_COMMAND +) +@pytest.mark.parametrize( + "check_mode", [ + pytest.param(False, id="check_mode=False"), + pytest.param(True, id="check_mode=True"), + ] +) +@mock.patch("plugins.modules.zhmc_lpar_command.AnsibleModule", autospec=True) +def test_zhmc_lpar_command( + ansible_mod_cls, check_mode, + desc, in_params, act_mode, os_type, command, exp_output, + classic_mode_cpcs): # noqa: F811, E501 + # pylint: disable=redefined-outer-name,unused-argument + """ + Test the zhmc_lpar_command module with classic mode CPCs. + """ + if not classic_mode_cpcs: + pytest.skip("HMC definition does not include any CPCs in classic mode") + + for cpc in classic_mode_cpcs: + assert not cpc.dpm_enabled + + session = cpc.manager.session + hd = session.hmc_definition + hmc_host = hd.host + hmc_auth = dict(userid=hd.userid, password=hd.password, + ca_certs=hd.ca_certs, verify=hd.verify) + faked_session = session if hd.mock_file else None + console = cpc.manager.console + + # Requires HMC 2.14.0 or later + mode_lpars = console.list_permitted_lpars( + filter_args={'cpc-name': cpc.name, 'activation-mode': act_mode}, + additional_properties=['os-type']) + + if not mode_lpars: + pytest.skip(f"CPC {cpc.name} does not have any LPARs with " + f"activation mode {act_mode}") + + loaded_lpars = [] + for lpar in mode_lpars: + lpar_status = lpar.get_property('status') + if lpar_status in ('operating', 'exceptions'): + loaded_lpars.append(lpar) + + if not loaded_lpars: + pytest.skip(f"CPC {cpc.name} does not have any LPARs with " + f"activation mode {act_mode} in a loaded state") + + test_lpars = [] + for lpar in loaded_lpars: + lpar_os_type = lpar.get_property('os-type') + if lpar_os_type == os_type: + test_lpars.append(lpar) + + test_lpar_names = [lpar.name for lpar in test_lpars] + lpar = random.choice(test_lpars) + print(f"Using LPAR {lpar.name} (from {', '.join(test_lpar_names)})") + + # Prepare module input parameters (must be all required + optional) + params = { + 'hmc_host': hmc_host, + 'hmc_auth': hmc_auth, + 'cpc_name': cpc.name, + 'name': lpar.name, + 'command': command, + 'log_file': LOG_FILE, + '_faked_session': faked_session, + } + params.update(in_params) + + # Prepare mocks for AnsibleModule object + mod_obj = mock_ansible_module(ansible_mod_cls, params, check_mode) + + # Exercise the code to be tested + with pytest.raises(SystemExit) as exc_info: + zhmc_lpar_command.main() + exit_code = exc_info.value.args[0] + + # Assert module exit code + assert exit_code == 0, \ + f"Module failed with exit code {exit_code} and message:\n" \ + f"{get_failure_msg(mod_obj)}" + + # Assert module output + changed, output = get_module_output(mod_obj) + assert changed is True + + assert len(output) == len(exp_output), ( + "Unexpected number of lines in command output:\n" + f" Actual output lines:\n{output}\n" + f" Expected output lines (regexp):\n{exp_output}\n" + ) + for i, line in enumerate(output): + exp_regexp = f"^{exp_output[i]}$" + assert re.match(exp_regexp, line), ( + f"Output line #{i} does not match expected regexp:\n" + f" Actual output line: {line}\n" + f" Expected output line (regexp): {exp_regexp}\n" + ) diff --git a/tests/end2end/test_zhmc_partition_command.py b/tests/end2end/test_zhmc_partition_command.py new file mode 100644 index 000000000..d1c6a829e --- /dev/null +++ b/tests/end2end/test_zhmc_partition_command.py @@ -0,0 +1,203 @@ +# Copyright 2024 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 zhmc_partition_command module. +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import random +import re +from unittest import mock +import pytest +import urllib3 +# pylint: disable=line-too-long,unused-import +from zhmcclient.testutils import hmc_definition, hmc_session # noqa: F401, E501 +from zhmcclient.testutils import dpm_mode_cpcs # noqa: F401, E501 +# pylint: enable=line-too-long,unused-import + +from plugins.modules import zhmc_partition_command +from .utils import mock_ansible_module, get_failure_msg + +urllib3.disable_warnings() + +LOGGING = False +LOG_FILE = 'zhmc_partition_command.log' if LOGGING else None + + +def get_module_output(mod_obj): + """ + Return the module output as a tuple (changed, messages) (i.e. + the arguments of the call to exit_json()). + If the module failed, return None. + """ + + def func(changed, output): + return changed, output + + if not mod_obj.exit_json.called: + return None + call_args = mod_obj.exit_json.call_args + + # The following makes sure we get the arguments regardless of whether they + # were specified as positional or keyword arguments: + return func(*call_args[0], **call_args[1]) + + +TESTCASES_ZHMC_PARTITION_COMMAND = [ + # Testcases for test_zhmc_partition_command(), each with these items: + # * desc (str): Testcase description + # * in_params (dict): Input parameters passed to module, except for + # hmc_host, hmc_auth, cpc, name, command, log_file. + # * type (str): partition type property, for selecting the partition + # * os_type (str): partition os-type property, for selecting the partition + # * command (str): Command to be executed + # * exp_output (list): Expected command output lines, as a list of regexp + + ( + "Simple z/VM command", + { + 'is_priority': False, + }, + "zvm", + "z/VM", + "Q CPLEVEL", + [ + ".* Q CPLEVEL", + ".* z/VM Version .* Release .* service level .*", + ".* Generated at .*", + ".* IPL at .*", + ] + ), + # TODO: Find out how to deal with Linux password prompt + # ( + # "Simple Linux command", + # { + # 'is_priority': False, + # }, + # "linux", + # "Linux", + # "uname -a", + # [ + # "uname -a", + # "Kernel Version .* s390x", + # ] + # ), +] + + +@pytest.mark.parametrize( + "desc, in_params, type, os_type, command, exp_output", + TESTCASES_ZHMC_PARTITION_COMMAND +) +@pytest.mark.parametrize( + "check_mode", [ + pytest.param(False, id="check_mode=False"), + pytest.param(True, id="check_mode=True"), + ] +) +@mock.patch("plugins.modules.zhmc_partition_command.AnsibleModule", autospec=True) +def test_zhmc_partition_command( + ansible_mod_cls, check_mode, + desc, in_params, type, os_type, command, exp_output, + dpm_mode_cpcs): # noqa: F811, E501 + # pylint: disable=redefined-outer-name,unused-argument + """ + Test the zhmc_partition_command module with DPM mode CPCs. + """ + if not dpm_mode_cpcs: + pytest.skip("HMC definition does not include any CPCs in DPM mode") + + for cpc in dpm_mode_cpcs: + assert cpc.dpm_enabled + + session = cpc.manager.session + hd = session.hmc_definition + hmc_host = hd.host + hmc_auth = dict(userid=hd.userid, password=hd.password, + ca_certs=hd.ca_certs, verify=hd.verify) + faked_session = session if hd.mock_file else None + console = cpc.manager.console + + # Requires HMC 2.14.0 or later + mode_partitions = console.list_permitted_partitions( + filter_args={'cpc-name': cpc.name, 'type': type}) + + if not mode_partitions: + pytest.skip(f"CPC {cpc.name} does not have any partitions with " + f"type {type}") + + loaded_partitions = [] + for partition in mode_partitions: + partition_status = partition.get_property('status') + if partition_status in ('active', 'degraded'): + loaded_partitions.append(partition) + + if not loaded_partitions: + pytest.skip(f"CPC {cpc.name} does not have any partitions with " + f"type {type} in an active state") + + test_partitions = [] + for partition in loaded_partitions: + partition_os_type = partition.get_property('os-type') + if partition_os_type == os_type: + test_partitions.append(partition) + + test_partition_names = [partition.name for partition in test_partitions] + partition = random.choice(test_partitions) + print(f"Using partition {partition.name} (from {', '.join(test_partition_names)})") + + # Prepare module input parameters (must be all required + optional) + params = { + 'hmc_host': hmc_host, + 'hmc_auth': hmc_auth, + 'cpc_name': cpc.name, + 'name': partition.name, + 'command': command, + 'log_file': LOG_FILE, + '_faked_session': faked_session, + } + params.update(in_params) + + # Prepare mocks for AnsibleModule object + mod_obj = mock_ansible_module(ansible_mod_cls, params, check_mode) + + # Exercise the code to be tested + with pytest.raises(SystemExit) as exc_info: + zhmc_partition_command.main() + exit_code = exc_info.value.args[0] + + # Assert module exit code + assert exit_code == 0, \ + f"Module failed with exit code {exit_code} and message:\n" \ + f"{get_failure_msg(mod_obj)}" + + # Assert module output + changed, output = get_module_output(mod_obj) + assert changed is True + + assert len(output) == len(exp_output), ( + "Unexpected number of lines in command output:\n" + f" Actual output lines:\n{output}\n" + f" Expected output lines (regexp):\n{exp_output}\n" + ) + for i, line in enumerate(output): + exp_regexp = f"^{exp_output[i]}$" + assert re.match(exp_regexp, line), ( + f"Output line #{i} does not match expected regexp:\n" + f" Actual output line: {line}\n" + f" Expected output line (regexp): {exp_regexp}\n" + ) diff --git a/tests/sanity/ignore-2.14.txt b/tests/sanity/ignore-2.14.txt index c8ce18b91..1a178abf3 100644 --- a/tests/sanity/ignore-2.14.txt +++ b/tests/sanity/ignore-2.14.txt @@ -8,11 +8,13 @@ plugins/modules/zhmc_crypto_attachment.py validate-modules:missing-gplv3-license plugins/modules/zhmc_hba.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_ldap_server_definition_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_ldap_server_definition.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/zhmc_lpar_command.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_lpar_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_lpar_messages.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_lpar.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_nic_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_nic.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/zhmc_partition_command.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_partition_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_partition_messages.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_partition.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 diff --git a/tests/sanity/ignore-2.15.txt b/tests/sanity/ignore-2.15.txt index c8ce18b91..1a178abf3 100644 --- a/tests/sanity/ignore-2.15.txt +++ b/tests/sanity/ignore-2.15.txt @@ -8,11 +8,13 @@ plugins/modules/zhmc_crypto_attachment.py validate-modules:missing-gplv3-license plugins/modules/zhmc_hba.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_ldap_server_definition_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_ldap_server_definition.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/zhmc_lpar_command.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_lpar_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_lpar_messages.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_lpar.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_nic_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_nic.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/zhmc_partition_command.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_partition_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_partition_messages.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_partition.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 diff --git a/tests/sanity/ignore-2.16.txt b/tests/sanity/ignore-2.16.txt index c8ce18b91..1a178abf3 100644 --- a/tests/sanity/ignore-2.16.txt +++ b/tests/sanity/ignore-2.16.txt @@ -8,11 +8,13 @@ plugins/modules/zhmc_crypto_attachment.py validate-modules:missing-gplv3-license plugins/modules/zhmc_hba.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_ldap_server_definition_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_ldap_server_definition.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/zhmc_lpar_command.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_lpar_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_lpar_messages.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_lpar.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_nic_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_nic.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/zhmc_partition_command.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_partition_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_partition_messages.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_partition.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 diff --git a/tests/sanity/ignore-2.17.txt b/tests/sanity/ignore-2.17.txt index c8ce18b91..1a178abf3 100644 --- a/tests/sanity/ignore-2.17.txt +++ b/tests/sanity/ignore-2.17.txt @@ -8,11 +8,13 @@ plugins/modules/zhmc_crypto_attachment.py validate-modules:missing-gplv3-license plugins/modules/zhmc_hba.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_ldap_server_definition_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_ldap_server_definition.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/zhmc_lpar_command.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_lpar_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_lpar_messages.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_lpar.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_nic_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_nic.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/zhmc_partition_command.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_partition_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_partition_messages.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_partition.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0