diff --git a/docs/source/modules.rst b/docs/source/modules.rst index 4217aaf4e..4e63a972a 100644 --- a/docs/source/modules.rst +++ b/docs/source/modules.rst @@ -32,6 +32,8 @@ Modules targeting the HMC (i.e. not a specific CPC): :glob: modules/zhmc_user + modules/zhmc_password_rule + modules/zhmc_password_rule_list Modules supported with CPCs in any operational mode: diff --git a/docs/source/modules/zhmc_password_rule.rst b/docs/source/modules/zhmc_password_rule.rst new file mode 100644 index 000000000..5910c219f --- /dev/null +++ b/docs/source/modules/zhmc_password_rule.rst @@ -0,0 +1,247 @@ + +:github_url: https://github.com/ansible-collections/ibm_zos_core/blob/dev/plugins/modules/zhmc_password_rule.py + +.. _zhmc_password_rule_module: + + +zhmc_password_rule -- Create HMC password rules +=============================================== + + + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Gather facts about a password rule on an HMC of a Z system. +- Create, delete, or update a password rule on an HMC. + + +Requirements +------------ + +- Access to the WS API of the HMC of the targeted Z system (see :term:`HMC API`). +- The targeted Z system can be in any operational mode (classic, DPM) + + + + +Parameters +---------- + + +hmc_host + The hostname or IP address of the HMC. + + | **required**: True + | **type**: str + + +hmc_auth + The authentication credentials for the HMC. + + | **required**: True + | **type**: dict + + + userid + The userid (username) for authenticating with the HMC. + + | **required**: True + | **type**: str + + + password + The password for authenticating with the HMC. + + | **required**: True + | **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 'REQUESTS_CA_BUNDLE' environment variable or the path name in the '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 ``ca_certs`` parameter. If False, ignore what is specified in the ``ca_certs`` parameter and do not verify the HMC certificate. + + | **required**: False + | **type**: bool + | **default**: True + + + +name + The name of the target password rule. + + | **required**: True + | **type**: str + + +state + The desired state for the HMC password rule. All states are fully idempotent within the limits of the properties that can be changed: + + * ``absent``: Ensures that the password rule does not exist. + + * ``present``: Ensures that the password rule exists and has the specified properties. + + * ``facts``: Returns the password rule properties. + + | **required**: True + | **type**: str + | **choices**: absent, present, facts + + +properties + Dictionary with desired properties for the password rule. Used for ``state=present``; ignored for ``state=absent|facts``. Dictionary key is the property name with underscores instead of hyphens, and dictionary value is the property value in YAML syntax. Integer properties may also be provided as decimal strings. + + The possible input properties in this dictionary are the properties defined as writeable in the data model for Password Rule resources (where the property names contain underscores instead of hyphens), with the following exceptions: + + * ``name``: Cannot be specified because the name has already been specified in the ``name`` module parameter. + + Properties omitted in this dictionary will remain unchanged when the password rule already exists, and will get the default value defined in the data model for password rules in the :term:`HMC API` when the password rule is being created. + + | **required**: False + | **type**: dict + + +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: Gather facts about a password rule + zhmc_password_rule: + hmc_host: "{{ my_hmc_host }}" + hmc_auth: "{{ my_hmc_auth }}" + name: "{{ my_password_rule_name }}" + state: facts + register: rule1 + + - name: Ensure the password rule does not exist + zhmc_password_rule: + hmc_host: "{{ my_hmc_host }}" + hmc_auth: "{{ my_hmc_auth }}" + name: "{{ my_password_rule_name }}" + state: absent + + - name: Ensure the password rule exists and has certain properties + zhmc_password_rule: + hmc_host: "{{ my_hmc_host }}" + hmc_auth: "{{ my_hmc_auth }}" + name: "{{ my_password_rule_name }}" + state: present + properties: + description: "Example password rule 1" + register: rule1 + + + + + + + + + + + +Return Values +------------- + + +changed + Indicates if any change has been made by the module. For ``state=facts``, always will be false. + + | **returned**: always + | **type**: bool + +msg + An error message that describes the failure. + + | **returned**: failure + | **type**: str + +password_rule + For ``state=absent``, an empty dictionary. + + For ``state=present|facts``, a dictionary with the resource properties of the target password rule. + + | **returned**: success + | **type**: dict + | **sample**: + + .. code-block:: json + + { + "case-sensitive": false, + "character-rules": [ + { + "alphabetic": "allowed", + "custom-character-sets": [], + "max-characters": 1, + "min-characters": 1, + "numeric": "not-allowed", + "special": "allowed" + }, + { + "alphabetic": "required", + "custom-character-sets": [], + "max-characters": 28, + "min-characters": 4, + "numeric": "allowed", + "special": "allowed" + }, + { + "alphabetic": "allowed", + "custom-character-sets": [], + "max-characters": 1, + "min-characters": 1, + "numeric": "not-allowed", + "special": "allowed" + } + ], + "class": "password-rule", + "consecutive-characters": 2, + "description": "Standard password rule definition", + "element-id": "520c0138-4a7e-11e9-8bb3-bdfeb245fc36", + "element-uri": "/api/console/password-rules/520c0138-4a7e-11e9-8bb3-bdfeb245fc36", + "expiration": 186, + "history-count": 4, + "max-length": 30, + "min-length": 6, + "name": "Standard", + "parent": "/api/console", + "replication-overwrite-possible": false, + "similarity-count": 0, + "type": "system-defined" + } + + name + Password rule name + + | **type**: str + + {property} + Additional properties of the password rule, as described in the data model of the 'Password Rule' object in the :term:`HMC API` book. The property names have hyphens (-) as described in that book. + + + diff --git a/docs/source/modules/zhmc_password_rule_list.rst b/docs/source/modules/zhmc_password_rule_list.rst new file mode 100644 index 000000000..675271b40 --- /dev/null +++ b/docs/source/modules/zhmc_password_rule_list.rst @@ -0,0 +1,151 @@ + +:github_url: https://github.com/ansible-collections/ibm_zos_core/blob/dev/plugins/modules/zhmc_password_rule_list.py + +.. _zhmc_password_rule_list_module: + + +zhmc_password_rule_list -- List Password Rules +============================================== + + + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- List Password Rules on the HMC. + + +Requirements +------------ + +- Access to the WS API of the HMC (see :term:`HMC API`). + + + + +Parameters +---------- + + +hmc_host + The hostname or IP address of the HMC. + + | **required**: True + | **type**: str + + +hmc_auth + The authentication credentials for the HMC. + + | **required**: True + | **type**: dict + + + userid + The userid (username) for authenticating with the HMC. + + | **required**: True + | **type**: str + + + password + The password for authenticating with the HMC. + + | **required**: True + | **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 'REQUESTS_CA_BUNDLE' environment variable or the path name in the '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 ``ca_certs`` parameter. If False, ignore what is specified in the ``ca_certs`` parameter and do not verify the HMC certificate. + + | **required**: False + | **type**: bool + | **default**: True + + + +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: List Password Rules + zhmc_password_rule_list: + hmc_host: "{{ my_hmc_host }}" + hmc_auth: "{{ my_hmc_auth }}" + register: pwrule_list + + + + + + + + + + +Return Values +------------- + + +changed + Indicates if any change has been made by the module. This will always be false. + + | **returned**: always + | **type**: bool + +msg + An error message that describes the failure. + + | **returned**: failure + | **type**: str + +password_rules + The list of Password Rules, with a subset of their properties. + + | **returned**: success + | **type**: list + | **elements**: dict + | **sample**: + + .. code-block:: json + + [ + { + "name": "Basic" + }, + { + "name": "Standard" + } + ] + + name + Password rule name + + | **type**: str + + diff --git a/docs/source/release_notes.rst b/docs/source/release_notes.rst index e62104e99..045092fde 100644 --- a/docs/source/release_notes.rst +++ b/docs/source/release_notes.rst @@ -53,6 +53,12 @@ Availability: `AutomationHub`_, `Galaxy`_, `GitHub`_ * Increased the minimum version of zhmcclient to 1.3.1, in order to pick up fixes. (part of issue #396) +* Added a new module 'zhmc_password_rule' that supports creating/updating, + deleting, and gathering facts of a password rule on the HMC. (issue #363) + +* Added a new module 'zhmc_password_rule_list' that supports listing the names + of password rules on the HMC. (issue #363) + **Cleanup:** **Known issues:** diff --git a/minimum-constraints.txt b/minimum-constraints.txt index 61c36cb34..0b73d512c 100644 --- a/minimum-constraints.txt +++ b/minimum-constraints.txt @@ -91,7 +91,8 @@ ansible==2.9.0.0 requests==2.22.0; python_version <= '3.9' requests==2.25.0; python_version >= '3.10' -zhmcclient==1.3.1 +# TODO: Enable once zhmcclient 1.4.0 has been released +# zhmcclient==1.4.0 # Indirect dependencies for installation (must be consistent with requirements.txt) diff --git a/plugins/modules/zhmc_password_rule.py b/plugins/modules/zhmc_password_rule.py new file mode 100644 index 000000000..8ebbea882 --- /dev/null +++ b/plugins/modules/zhmc_password_rule.py @@ -0,0 +1,700 @@ +#!/usr/bin/python +# Copyright 2022 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_password_rule +version_added: "2.9.0" +short_description: Create HMC password rules +description: + - Gather facts about a password rule on an HMC of a Z system. + - Create, delete, or update a password rule on an HMC. +author: + - Andreas Maier (@andy-maier) +requirements: + - Access to the WS API of the HMC of the targeted Z system + (see :term:`HMC API`). + - The targeted Z system can be in any operational mode (classic, DPM) +options: + hmc_host: + description: + - The hostname or IP address of the HMC. + type: str + 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. + type: str + required: true + password: + description: + - The password for authenticating with the HMC. + type: str + required: true + 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 'REQUESTS_CA_BUNDLE' environment variable or the path name + in the '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 + C(ca_certs) parameter. If False, ignore what is specified in the + C(ca_certs) parameter and do not verify the HMC certificate. + type: bool + required: false + default: true + name: + description: + - The name of the target password rule. + type: str + required: true + state: + description: + - "The desired state for the HMC password rule. All states are fully + idempotent within the limits of the properties that can be changed:" + - "* C(absent): Ensures that the password rule does not exist." + - "* C(present): Ensures that the password rule exists and has the + specified properties." + - "* C(facts): Returns the password rule properties." + type: str + required: true + choices: ['absent', 'present', 'facts'] + properties: + description: + - "Dictionary with desired properties for the password rule. + Used for C(state=present); ignored for C(state=absent|facts). + Dictionary key is the property name with underscores instead + of hyphens, and dictionary value is the property value in YAML syntax. + Integer properties may also be provided as decimal strings." + - "The possible input properties in this dictionary are the properties + defined as writeable in the data model for Password Rule resources + (where the property names contain underscores instead of hyphens), + with the following exceptions:" + - "* C(name): Cannot be specified because the name has already been + specified in the C(name) module parameter." + - "Properties omitted in this dictionary will remain unchanged when the + password rule already exists, and will get the default value defined + in the data model for password rules in the :term:`HMC API` when the + password rule is being created." + type: dict + required: false + default: null + 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." + required: false + type: raw + default: null +""" + +EXAMPLES = """ +--- +# Note: The following examples assume that some variables named 'my_*' are set. + +- name: Gather facts about a password rule + zhmc_password_rule: + hmc_host: "{{ my_hmc_host }}" + hmc_auth: "{{ my_hmc_auth }}" + name: "{{ my_password_rule_name }}" + state: facts + register: rule1 + +- name: Ensure the password rule does not exist + zhmc_password_rule: + hmc_host: "{{ my_hmc_host }}" + hmc_auth: "{{ my_hmc_auth }}" + name: "{{ my_password_rule_name }}" + state: absent + +- name: Ensure the password rule exists and has certain properties + zhmc_password_rule: + hmc_host: "{{ my_hmc_host }}" + hmc_auth: "{{ my_hmc_auth }}" + name: "{{ my_password_rule_name }}" + state: present + properties: + description: "Example password rule 1" + register: rule1 + +""" + +RETURN = """ +changed: + description: Indicates if any change has been made by the module. + For C(state=facts), always will be false. + returned: always + type: bool +msg: + description: An error message that describes the failure. + returned: failure + type: str +password_rule: + description: + - "For C(state=absent), an empty dictionary." + - "For C(state=present|facts), a + dictionary with the resource properties of the target password rule." + returned: success + type: dict + contains: + name: + description: "Password rule name" + type: str + "{property}": + description: "Additional properties of the password rule, as described + in the data model of the 'Password Rule' object in the :term:`HMC API` + book. The property names have hyphens (-) as described in that book." + sample: + { + "case-sensitive": false, + "character-rules": [ + { + "special": "allowed", + "alphabetic": "allowed", + "min-characters": 1, + "numeric": "not-allowed", + "max-characters": 1, + "custom-character-sets": [] + }, + { + "special": "allowed", + "alphabetic": "required", + "min-characters": 4, + "numeric": "allowed", + "max-characters": 28, + "custom-character-sets": [] + }, + { + "special": "allowed", + "alphabetic": "allowed", + "min-characters": 1, + "numeric": "not-allowed", + "max-characters": 1, + "custom-character-sets": [] + } + ], + "class": "password-rule", + "consecutive-characters": 2, + "description": "Standard password rule definition", + "element-id": "520c0138-4a7e-11e9-8bb3-bdfeb245fc36", + "element-uri": "/api/console/password-rules/520c0138-4a7e-11e9-8bb3-bdfeb245fc36", + "expiration": 186, + "history-count": 4, + "max-length": 30, + "min-length": 6, + "name": "Standard", + "parent": "/api/console", + "replication-overwrite-possible": false, + "similarity-count": 0, + "type": "system-defined" + } +""" + +import uuid # noqa: E402 +import logging # noqa: E402 +import traceback # noqa: E402 +from ansible.module_utils.basic import AnsibleModule # noqa: E402 + +from ..module_utils.common import log_init, Error, ParameterError, \ + get_hmc_auth, get_session, to_unicode, process_normal_property, \ + missing_required_lib # noqa: E402 + +try: + import requests.packages.urllib3 + IMP_URLLIB3 = True +except ImportError: + IMP_URLLIB3 = False + IMP_URLLIB3_ERR = traceback.format_exc() + +try: + import zhmcclient + IMP_ZHMCCLIENT = True +except ImportError: + IMP_ZHMCCLIENT = False + IMP_ZHMCCLIENT_ERR = traceback.format_exc() + +# Python logger name for this module +LOGGER_NAME = 'zhmc_password_rule' + +LOGGER = logging.getLogger(LOGGER_NAME) + +# Dictionary of properties of password rule resources, in this format: +# name: (allowed, create, update, eq_func, type_cast) +# where: +# name: Name of the property according to the data model, with hyphens +# replaced by underscores (this is how it is or would be specified in +# the 'properties' module parameter). +# allowed: Indicates whether it is allowed in the 'properties' module +# parameter. +# create: Indicates whether it can be specified for the "Create Password Rule" +# operation. +# update: Indicates whether it can be specified for the "Modify Password Rule +# Properties" operation (at all). +# update_while_active: Not used for this module. +# eq_func: Equality test function for two values of the property; None means +# to use Python equality. +# type_cast: Type cast function for an input value of the property; None +# means to use it directly. This can be used for example to convert +# integers provided as strings by Ansible back into integers (that is a +# current deficiency of Ansible). +ZHMC_PASSWORD_RULE_PROPERTIES = { + + # create+update properties: + 'name': (False, True, True, True, None, None), + # name: provided in 'name' module parm + 'description': (True, True, True, True, None, to_unicode), + 'expiration': (True, True, True, True, None, int), + 'min_length': (True, True, True, True, None, int), + 'max_length': (True, True, True, True, None, int), + 'consecutive_characters': (True, True, True, True, None, int), + 'similarity_count': (True, True, True, True, None, int), + 'history_count': (True, True, True, True, None, int), + 'case_sensitive': (True, True, True, True, None, bool), + 'character_rules': (True, True, True, True, None, None), + # character_rules is an array of character-rule objects: + # character-rule object: + # min_characters: Integer + # max_characters: Integer + # alphabetic: StringEnum ("allowed","not-allowed","required") + # numeric: StringEnum ("allowed","not-allowed","required") + # special: StringEnum ("allowed","not-allowed","required") + # custom_character_sets: Array of custom-character-set objects + # custom-character-set object: + # character_set: String + # inclusion: StringEnum ("allowed","not-allowed","required") + + # read-only properties: + 'element_uri': (False, False, False, True, None, None), + 'element_id': (False, False, False, True, None, None), + 'parent': (False, False, False, True, None, None), + 'class': (False, False, False, True, None, None), + 'type': (False, False, False, True, None, None), + 'replication_overwrite_possible': (False, False, False, True, None, bool), +} + + +def process_properties(console, pwrule, params): + """ + Process the properties specified in the 'properties' module parameter, + and return two dictionaries (create_props, update_props) that contain + the properties that can be created, and the properties that can be updated, + respectively. If the resource exists, the input property values are + compared with the existing resource property values and the returned set + of properties is the minimal set of properties that need to be changed. + + - Underscores in the property names are translated into hyphens. + - The presence of read-only properties, invalid properties (i.e. not + defined in the data model for password rules), and properties that are + not allowed because of restrictions or because they are auto-created from + an artificial property is surfaced by raising ParameterError. + + Parameters: + + pwrule (zhmcclient.PasswordRule): Password Rule object to be updated with + the full set of current properties, or `None` if it did not previously + exist. + + params (dict): Module input parameters. + + Returns: + tuple of (create_props, update_props), + where: + * create_props: dict of properties for + zhmcclient.PasswordRuleManager.create() + * update_props: dict of properties for + zhmcclient.PasswordRule.update_properties() + + Raises: + ParameterError: An issue with the module parameters. + """ + create_props = {} + update_props = {} + + # handle 'name' property + pwrule_name = to_unicode(params['name']) + if pwrule is None: + # Password Rule does not exist yet. + create_props['name'] = pwrule_name + else: + # Password Rule does already exist. + # We looked up the password rule by name, so we will never have to + # update the password rule name. + pass + + # handle the other properties + input_props = params.get('properties', None) + if input_props is None: + input_props = {} + + for prop_name in input_props: + + if prop_name not in ZHMC_PASSWORD_RULE_PROPERTIES: + raise ParameterError( + "Property {0!r} is not defined in the data model for " + "password rules.".format(prop_name)) + + allowed, create, update, update_while_active, eq_func, type_cast = \ + ZHMC_PASSWORD_RULE_PROPERTIES[prop_name] + + if not allowed: + raise ParameterError( + "Property {0!r} is not allowed in the 'properties' module " + "parameter.".format(prop_name)) + + # Process a normal (= non-artificial) property + _create_props, _update_props, _stop = process_normal_property( + prop_name, ZHMC_PASSWORD_RULE_PROPERTIES, input_props, pwrule) + create_props.update(_create_props) + update_props.update(_update_props) + if _stop: + raise AssertionError() + return create_props, update_props + + +def create_check_mode_pwrule(console, create_props, update_props): + """ + Create and return a fake local Password Rule object. + + This is used when a password rule needs to be created in check mode. + + This function must be consistent with the behavior of the "Create Password + Rule" operation on the HMC. HTTP errors the HMC would return are indicated + by raising zhmcclient.HTTPError. + """ + + input_props = {} + input_props.update(create_props) + input_props.update(update_props) + + # Check required input properties + missing_props = [] + for pname in ('name',): + if pname not in input_props: + missing_props.append(pname) + if missing_props: + raise zhmcclient.HTTPError({ + 'http-status': 400, + 'reason': 4, + 'message': "Required input properties missing for Create Password Rule: {p}". + format(p=missing_props), + }) + + # Defaults for properties + props = { + # createable/updateable + 'description': '', + 'expiration': 0, + 'min-length': 8, + 'max-length': 256, + 'consecutive-characters': 0, + 'similarity-count': 0, + 'history-count': 0, + 'case-sensitive': False, + 'character-rules': [], + # read-only + 'type': 'user-defined', + 'replication-overwrite-possible': True, + } + + # Apply specified input properties on top of the defaults + props.update(input_props) + + pwrule_oid = 'fake-{0}'.format(uuid.uuid4()) + pwrule = console.password_rules.resource_object(pwrule_oid, props=props) + + return pwrule + + +def ensure_present(params, check_mode): + """ + Ensure that the password rule exists and has the specified properties. + + Raises: + ParameterError: An issue with the module parameters. + zhmcclient.Error: Any zhmcclient exception can happen. + """ + + host = params['hmc_host'] + userid, password, ca_certs, verify = get_hmc_auth(params['hmc_auth']) + pwrule_name = params['name'] + _faked_session = params.get('_faked_session', None) + + changed = False + result = {} + + session = get_session( + _faked_session, host, userid, password, ca_certs, verify) + try: + client = zhmcclient.Client(session) + console = client.consoles.console + # The default exception handling is sufficient for the above. + + try: + pwrule = console.password_rules.find(name=pwrule_name) + except zhmcclient.NotFound: + pwrule = None + + if pwrule is None: + # It does not exist. Create it and update it if there are + # update-only properties. + create_props, update_props = \ + process_properties(console, pwrule, params) + update2_props = {} + for name, value in update_props.items(): + if name not in create_props: + update2_props[name] = value + if not check_mode: + pwrule = console.password_rules.create(create_props) + if update2_props: + pwrule.update_properties(update2_props) + # We refresh the properties after the update, in case an + # input property value gets changed. + pwrule.pull_full_properties() + else: + # Create a Password Rule object locally + pwrule = create_check_mode_pwrule( + console, create_props, update2_props) + result = dict(pwrule.properties) + changed = True + else: + # It exists. Update its properties. + pwrule.pull_full_properties() + result = dict(pwrule.properties) + create_props, update_props = \ + process_properties(console, pwrule, params) + if create_props: + raise AssertionError("Unexpected " + "create_props: %r" % create_props) + if update_props: + LOGGER.debug( + "Existing password rule %r needs to get properties " + "updated: %r", pwrule_name, update_props) + if not check_mode: + pwrule.update_properties(update_props) + # We refresh the properties after the update, in case an + # input property value gets changed. + pwrule.pull_full_properties() + result = dict(pwrule.properties) + else: + # Update the local Password Rule object's properties + result.update(update_props) + changed = True + + if not pwrule: + raise AssertionError() + + return changed, result + + finally: + session.logoff() + + +def ensure_absent(params, check_mode): + """ + Ensure that the password rule does not exist. + + Raises: + ParameterError: An issue with the module parameters. + zhmcclient.Error: Any zhmcclient exception can happen. + """ + + host = params['hmc_host'] + userid, password, ca_certs, verify = get_hmc_auth(params['hmc_auth']) + pwrule_name = params['name'] + _faked_session = params.get('_faked_session', None) + + changed = False + result = {} + + session = get_session( + _faked_session, host, userid, password, ca_certs, verify) + try: + client = zhmcclient.Client(session) + console = client.consoles.console + # The default exception handling is sufficient for the above. + + try: + pwrule = console.password_rules.find(name=pwrule_name) + except zhmcclient.NotFound: + return changed, result + + if not check_mode: + pwrule.delete() + changed = True + + return changed, result + + finally: + session.logoff() + + +def facts(params, check_mode): + """ + Return facts about a password rule. + + Raises: + ParameterError: An issue with the module parameters. + zhmcclient.Error: Any zhmcclient exception can happen. + """ + + host = params['hmc_host'] + userid, password, ca_certs, verify = get_hmc_auth(params['hmc_auth']) + pwrule_name = params['name'] + _faked_session = params.get('_faked_session', None) + + changed = False + result = {} + + session = get_session( + _faked_session, host, userid, password, ca_certs, verify) + try: + # The default exception handling is sufficient for this code + client = zhmcclient.Client(session) + console = client.consoles.console + + pwrule = console.password_rules.find(name=pwrule_name) + pwrule.pull_full_properties() + + result = dict(pwrule.properties) + return changed, result + + finally: + session.logoff() + + +def perform_task(params, check_mode): + """ + Perform the task for this module, dependent on the 'state' module + parameter. + + If check_mode is True, check whether changes would occur, but don't + actually perform any changes. + + Raises: + ParameterError: An issue with the module parameters. + zhmcclient.Error: Any zhmcclient exception can happen. + """ + actions = { + "absent": ensure_absent, + "present": ensure_present, + "facts": facts, + } + return actions[params['state']](params, check_mode) + + +def main(): + + # 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='str'), + hmc_auth=dict( + required=True, + type='dict', + options=dict( + userid=dict(required=True, type='str'), + password=dict(required=True, type='str', no_log=True), + ca_certs=dict(required=False, type='str', default=None), + verify=dict(required=False, type='bool', default=True), + ), + ), + name=dict(required=True, type='str'), + state=dict(required=True, type='str', + choices=['absent', 'present', 'facts']), + properties=dict(required=False, type='dict', default={}), + 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 not IMP_URLLIB3: + module.fail_json(msg=missing_required_lib("requests"), + exception=IMP_URLLIB3_ERR) + + requests.packages.urllib3.disable_warnings() + + if not IMP_ZHMCCLIENT: + module.fail_json(msg=missing_required_lib("zhmcclient"), + exception=IMP_ZHMCCLIENT_ERR) + + log_file = module.params['log_file'] + log_init(LOGGER_NAME, log_file) + + _params = dict(module.params) + del _params['hmc_auth'] + LOGGER.debug("Module entry: params: %r", _params) + + try: + + changed, result = perform_task(module.params, module.check_mode) + + 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 = "{0}: {1}".format(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, password_rule: %r", + changed, result) + module.exit_json(changed=changed, password_rule=result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/zhmc_password_rule_list.py b/plugins/modules/zhmc_password_rule_list.py new file mode 100644 index 000000000..242837fa8 --- /dev/null +++ b/plugins/modules/zhmc_password_rule_list.py @@ -0,0 +1,266 @@ +#!/usr/bin/python +# Copyright 2022 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_password_rule_list +version_added: "2.9.0" +short_description: List Password Rules +description: + - List Password Rules on the HMC. +author: + - Andreas Maier (@andy-maier) +requirements: + - Access to the WS API of the HMC (see :term:`HMC API`). +options: + hmc_host: + description: + - The hostname or IP address of the HMC. + type: str + 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. + type: str + required: true + password: + description: + - The password for authenticating with the HMC. + type: str + required: true + 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 'REQUESTS_CA_BUNDLE' environment variable or the path name + in the '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 + C(ca_certs) parameter. If False, ignore what is specified in the + C(ca_certs) parameter and do not verify the HMC certificate. + type: bool + required: false + default: true + 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." + required: false + type: raw + default: null +""" + +EXAMPLES = """ +--- +# Note: The following examples assume that some variables named 'my_*' are set. + +- name: List Password Rules + zhmc_password_rule_list: + hmc_host: "{{ my_hmc_host }}" + hmc_auth: "{{ my_hmc_auth }}" + register: pwrule_list +""" + +RETURN = """ +changed: + description: Indicates if any change has been made by the module. + This will always be false. + returned: always + type: bool +msg: + description: An error message that describes the failure. + returned: failure + type: str +password_rules: + description: The list of Password Rules, with a subset of their properties. + returned: success + type: list + elements: dict + contains: + name: + description: "Password rule name" + type: str + sample: + [ + { + "name": "Basic", + }, + { + "name": "Standard", + } + ] +""" + +import logging # noqa: E402 +import traceback # noqa: E402 +from ansible.module_utils.basic import AnsibleModule # noqa: E402 + +from ..module_utils.common import log_init, Error, get_hmc_auth, get_session, \ + missing_required_lib # noqa: E402 + +try: + import requests.packages.urllib3 + IMP_URLLIB3 = True +except ImportError: + IMP_URLLIB3 = False + IMP_URLLIB3_ERR = traceback.format_exc() + +try: + import zhmcclient + IMP_ZHMCCLIENT = True +except ImportError: + IMP_ZHMCCLIENT = False + IMP_ZHMCCLIENT_ERR = traceback.format_exc() + +# Python logger name for this module +LOGGER_NAME = 'zhmc_password_rule_list' + +LOGGER = logging.getLogger(LOGGER_NAME) + + +def perform_list(params): + """ + List the managed Password Rules and return a subset of properties. + + Raises: + ParameterError: An issue with the module parameters. + zhmcclient.Error: Any zhmcclient exception can happen. + """ + + host = params['hmc_host'] + userid, password, ca_certs, verify = get_hmc_auth(params['hmc_auth']) + _faked_session = params.get('_faked_session', None) # No default specified + + session = get_session( + _faked_session, host, userid, password, ca_certs, verify) + try: + client = zhmcclient.Client(session) + console = client.consoles.console + + pwrule_list = [] + + # List the Password Rules + pwrules = console.password_rules.list() + # The default exception handling is sufficient for the above. + for pwrule in pwrules: + pwrule_properties = { + "name": pwrule.name, + } + pwrule_list.append(pwrule_properties) + + return pwrule_list + + finally: + session.logoff() + + +def main(): + + # 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='str'), + hmc_auth=dict( + required=True, + type='dict', + options=dict( + userid=dict(required=True, type='str'), + password=dict(required=True, type='str', no_log=True), + ca_certs=dict(required=False, type='str', default=None), + verify=dict(required=False, type='bool', default=True), + ), + ), + 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 not IMP_URLLIB3: + module.fail_json(msg=missing_required_lib("requests"), + exception=IMP_URLLIB3_ERR) + + requests.packages.urllib3.disable_warnings() + + if not IMP_ZHMCCLIENT: + module.fail_json(msg=missing_required_lib("zhmcclient"), + exception=IMP_ZHMCCLIENT_ERR) + + log_file = module.params['log_file'] + log_init(LOGGER_NAME, log_file) + + _params = dict(module.params) + del _params['hmc_auth'] + LOGGER.debug("Module entry: params: %r", _params) + + changed = False + try: + + result_list = perform_list(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 = "{0}: {1}".format(exc.__class__.__name__, exc) + LOGGER.debug("Module exit (failure): msg: %r", 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: %s, result: %r", + changed, result_list) + module.exit_json(changed=changed, password_rules=result_list) + + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt index 655c1a0b5..ae565f757 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,8 +19,9 @@ ansible>=2.9.0.0 requests>=2.22.0; python_version <= '3.9' requests>=2.25.0; python_version >= '3.10' -# git+https://github.com/zhmcclient/python-zhmcclient@master#egg=zhmcclient -zhmcclient>=1.3.1 +# TODO: Enable once zhmcclient 1.4.0 has been released +git+https://github.com/zhmcclient/python-zhmcclient@master#egg=zhmcclient +# zhmcclient>=1.4.0 # Indirect dependencies are not specified in this file, unless needed to solve versioning issues: diff --git a/tests/end2end/mocked_z14_classic.yaml b/tests/end2end/mocked_z14_classic.yaml index c87823a51..e0a349382 100644 --- a/tests/end2end/mocked_z14_classic.yaml +++ b/tests/end2end/mocked_z14_classic.yaml @@ -187,9 +187,31 @@ hmc_definition: - properties: element-id: passwordrule1 name: Password rule 1 + type: "user-defined" + description: "Password rule 1" + expiration: 186 + min-length: 6 + max-length: 30 + consecutive-characters: 2 + history-count: 4 + similarity-count: 0 + case-sensitive: false + character-rules: [] + replication-overwrite-possible: false - properties: - element-id: Basic - name: Basic + element-id: Standard + name: Standard + type: "system-defined" + description: "Standard password rule definition" + expiration: 90 + min-length: 16 + max-length: 32 + consecutive-characters: 0 + history-count: 2 + similarity-count: 2 + case-sensitive: true + character-rules: [] + replication-overwrite-possible: true tasks: - properties: diff --git a/tests/end2end/mocked_z14_dpm.yaml b/tests/end2end/mocked_z14_dpm.yaml index ac80cb172..43927e008 100644 --- a/tests/end2end/mocked_z14_dpm.yaml +++ b/tests/end2end/mocked_z14_dpm.yaml @@ -187,9 +187,31 @@ hmc_definition: - properties: element-id: passwordrule1 name: Password rule 1 + type: "user-defined" + description: "Password rule 1" + expiration: 186 + min-length: 6 + max-length: 30 + consecutive-characters: 2 + history-count: 4 + similarity-count: 0 + case-sensitive: false + character-rules: [] + replication-overwrite-possible: false - properties: - element-id: Basic - name: Basic + element-id: Standard + name: Standard + type: "system-defined" + description: "Standard password rule definition" + expiration: 90 + min-length: 16 + max-length: 32 + consecutive-characters: 0 + history-count: 2 + similarity-count: 2 + case-sensitive: true + character-rules: [] + replication-overwrite-possible: true tasks: - properties: diff --git a/tests/end2end/test_zhmc_password_rule.py b/tests/end2end/test_zhmc_password_rule.py new file mode 100644 index 000000000..338a460a0 --- /dev/null +++ b/tests/end2end/test_zhmc_password_rule.py @@ -0,0 +1,358 @@ +# Copyright 2022 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_password_rule module. +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import uuid +import pytest +import mock +import random +import requests.packages.urllib3 +from collections import OrderedDict +from pprint import pformat +import zhmcclient +# pylint: disable=line-too-long,unused-import +from zhmcclient.testutils import hmc_definition, hmc_session # noqa: F401, E501 +# pylint: enable=line-too-long,unused-import + +from plugins.modules import zhmc_password_rule +from .utils import mock_ansible_module, get_failure_msg + +requests.packages.urllib3.disable_warnings() + +# Print debug messages +DEBUG = False + +LOG_FILE = 'zhmc_password_rule.log' if DEBUG else None + +# Properties in the returned password rule facts that are not always present, but +# only under certain conditions. This includes artificial properties whose base +# properties are not always present. The +# comments state the condition under which the property is present. +PWRULE_CONDITIONAL_PROPS = ( +) + + +# A standard test password rule, as specified for the 'properties' module input +# parm +STD_PWRULE_INPUT_PROPERTIES = { + # 'name': provided in separate module input parameter + 'description': "zhmc test password rule", + 'expiration': 90, + 'min_length': 16, + 'max_length': 32, + 'consecutive_characters': 0, + 'similarity_count': 2, + 'history_count': 2, + 'case_sensitive': True, + 'character_rules': [], +} + + +# A standard test password rule consistent with STD_PWRULE_INPUT_PROPERTIES, but +# specified with HMC properties. +STD_PWRULE_PROPERTIES = { + # 'name': updated upon use + # 'default-group-uri': no default group + 'description': STD_PWRULE_INPUT_PROPERTIES['description'], + 'expiration': STD_PWRULE_INPUT_PROPERTIES['expiration'], + 'min-length': STD_PWRULE_INPUT_PROPERTIES['min_length'], + 'max-length': STD_PWRULE_INPUT_PROPERTIES['max_length'], + 'consecutive-characters': STD_PWRULE_INPUT_PROPERTIES['consecutive_characters'], + 'similarity-count': STD_PWRULE_INPUT_PROPERTIES['similarity_count'], + 'history-count': STD_PWRULE_INPUT_PROPERTIES['history_count'], + 'character-rules': STD_PWRULE_INPUT_PROPERTIES['character_rules'], +} + + +def updated_copy(dict1, dict2): + dict1c = dict1.copy() + dict1c.update(dict2) + return dict1c + + +def new_pwrule_name(): + pwrule_name = 'test_{0}'.format(uuid.uuid4()) + return pwrule_name + + +def get_module_output(mod_obj): + """ + Return the module output as a tuple (changed, user_properties) (i.e. + the arguments of the call to exit_json()). + If the module failed, return None. + """ + + def func(changed, password_rule): + return changed, password_rule + + 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]) + + +def assert_pwrule_props(pwrule_props, where): + """ + Assert the output object of the zhmc_password_rule module + """ + assert isinstance(pwrule_props, dict), where # Dict of Password rule props + + # Assert presence of normal properties in the output + for prop_name in zhmc_password_rule.ZHMC_PASSWORD_RULE_PROPERTIES: + prop_name_hmc = prop_name.replace('_', '-') + if prop_name_hmc in PWRULE_CONDITIONAL_PROPS: + continue + assert prop_name_hmc in pwrule_props, where + + +@pytest.mark.parametrize( + "check_mode", [ + pytest.param(False, id="check_mode=False"), + pytest.param(True, id="check_mode=True"), + ] +) +@mock.patch("plugins.modules.zhmc_password_rule.AnsibleModule", autospec=True) +def test_user_pwrule_facts( + ansible_mod_cls, check_mode, hmc_session): # noqa: F811, E501 + """ + Test fact gathering on all password rules of the HMC. + """ + + hd = hmc_session.hmc_definition + hmc_host = hd.host + hmc_auth = dict(userid=hd.userid, password=hd.password, + ca_certs=hd.ca_certs, verify=hd.verify) + + client = zhmcclient.Client(hmc_session) + console = client.consoles.console + + faked_session = hmc_session if hd.mock_file else None + + # Determine a random existing password rule of the desired type to test. + pwrules = console.password_rules.list() + pwrule = random.choice(pwrules) + + where = "password rule '{u}'".format(u=pwrule.name) + + # Prepare module input parameters (must be all required + optional) + params = { + 'hmc_host': hmc_host, + 'hmc_auth': hmc_auth, + 'name': pwrule.name, + 'state': 'facts', + 'properties': {}, + 'log_file': LOG_FILE, + '_faked_session': faked_session, + } + + # 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_password_rule.main() + exit_code = exc_info.value.args[0] + + # Assert module exit code + assert exit_code == 0, \ + "{w}: Module failed with exit code {e} and message:\n{m}". \ + format(w=where, e=exit_code, m=get_failure_msg(mod_obj)) + + # Assert module output + changed, pwrule_props = get_module_output(mod_obj) + assert changed is False, where + assert_pwrule_props(pwrule_props, where) + + +PWRULE_ABSENT_PRESENT_TESTCASES = [ + # The list items are tuples with the following items: + # - desc (string): description of the testcase. + # - initial_pwrule_props (dict): HMC-formatted properties for initial + # password rule, in addition to STD_PWRULE_PROPERTIES, or None for no + # initial password rule. + # - input_props (dict): 'properties' input parameter for zhmc_password_rule + # module. + # - exp_pwrule_props (dict): HMC-formatted properties for expected + # properties of created password rule. + # - exp_changed (bool): Boolean for expected 'changed' flag. + + ( + "Present with non-existing password rule", + None, + 'present', + STD_PWRULE_INPUT_PROPERTIES, + STD_PWRULE_PROPERTIES, + True, + ), + ( + "Present with existing password rule, no properties changed", + {}, + 'present', + STD_PWRULE_INPUT_PROPERTIES, + STD_PWRULE_PROPERTIES, + True, # due to password + ), + ( + "Present with existing password rule, some properties changed", + { + 'session-timeout': 30, + }, + 'present', + STD_PWRULE_INPUT_PROPERTIES, + STD_PWRULE_PROPERTIES, + True, + ), + ( + "Absent with existing password rule", + {}, + 'absent', + None, + None, + True, + ), + ( + "Absent with non-existing password rule", + None, + 'absent', + None, + None, + False, + ), +] + + +@pytest.mark.parametrize( + "check_mode", [ + pytest.param(False, id="check_mode=False"), + pytest.param(True, id="check_mode=True"), + ] +) +@pytest.mark.parametrize( + "desc, initial_pwrule_props, input_state, " + "input_props, exp_pwrule_props, exp_changed", + PWRULE_ABSENT_PRESENT_TESTCASES) +@mock.patch("plugins.modules.zhmc_password_rule.AnsibleModule", autospec=True) +def test_user_pwrule_absent_present( + ansible_mod_cls, + desc, initial_pwrule_props, input_state, + input_props, exp_pwrule_props, exp_changed, + check_mode, + hmc_session): # noqa: F811, E501 + """ + Test the zhmc_password_rule module with all combinations of absent & + present state. + """ + + hd = hmc_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 = hmc_session if hd.mock_file else None + + client = zhmcclient.Client(hmc_session) + console = client.consoles.console + + # Create a password rule name that does not exist + pwrule_name = new_pwrule_name() + + where = "password rule '{u}'".format(u=pwrule_name) + + # Create initial password rule, if specified so + if initial_pwrule_props is not None: + pwrule_props = STD_PWRULE_PROPERTIES.copy() + pwrule_props.update(initial_pwrule_props) + pwrule_props['name'] = pwrule_name + try: + console.password_rules.create(pwrule_props) + except zhmcclient.HTTPError as exc: + if exc.http_status == 403 and exc.reason == 1: + # User is not permitted to create password rules + pytest.skip("HMC user '{u}' is not permitted to create " + "initial test password rule". + format(u=hd.userid)) + else: + pwrule_props = None + + try: + + # Prepare module input parameters (must be all required + optional) + params = { + 'hmc_host': hmc_host, + 'hmc_auth': hmc_auth, + 'name': pwrule_name, + 'state': input_state, + 'log_file': LOG_FILE, + '_faked_session': faked_session, + } + if input_props is not None: + params['properties'] = input_props + else: + params['properties'] = {} + + 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_password_rule.main() + exit_code = exc_info.value.args[0] + + assert exit_code == 0, \ + "{w}: Module failed with exit code {e} and message:\n{m}". \ + format(w=where, e=exit_code, m=get_failure_msg(mod_obj)) + + changed, output_props = get_module_output(mod_obj) + if changed != exp_changed: + pwrule_props_sorted = \ + OrderedDict(sorted(pwrule_props.items(), key=lambda x: x[0])) \ + if pwrule_props is not None else None + input_props_sorted = \ + OrderedDict(sorted(input_props.items(), key=lambda x: x[0])) \ + if input_props is not None else None + output_props_sorted = \ + OrderedDict(sorted(output_props.items(), key=lambda x: x[0])) \ + if output_props is not None else None + raise AssertionError( + "Unexpected change flag returned: actual: {0}, expected: {1}\n" + "Initial password rule properties:\n{2}\n" + "Module input properties:\n{3}\n" + "Resulting password rule properties:\n{4}". + format(changed, exp_changed, + pformat(pwrule_props_sorted.items(), indent=2), + pformat(input_props_sorted.items(), indent=2), + pformat(output_props_sorted.items(), indent=2))) + if input_state == 'present': + assert_pwrule_props(output_props, where) + + finally: + # Delete password rule, if it exists + try: + # We invalidate the name cache of our client, because the password rule + # was possibly deleted by the Ansible module and not through our + # client instance. + console.password_rules.invalidate_cache() + pwrule = console.password_rules.find_by_name(pwrule_name) + except zhmcclient.NotFound: + pwrule = None + if pwrule: + pwrule.delete() diff --git a/tests/end2end/test_zhmc_password_rule_list.py b/tests/end2end/test_zhmc_password_rule_list.py new file mode 100644 index 000000000..dda3b2481 --- /dev/null +++ b/tests/end2end/test_zhmc_password_rule_list.py @@ -0,0 +1,140 @@ +# Copyright 2022 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_password_rule_list module. +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest +import mock +import requests.packages.urllib3 +import zhmcclient +# pylint: disable=line-too-long,unused-import +from zhmcclient.testutils import hmc_definition, hmc_session # noqa: F401, E501 +# pylint: enable=line-too-long,unused-import + +from plugins.modules import zhmc_password_rule_list +from .utils import mock_ansible_module, get_failure_msg + +requests.packages.urllib3.disable_warnings() + +# Print debug messages +DEBUG = False + +LOG_FILE = 'zhmc_password_rule_list.log' if DEBUG else None + + +def get_module_output(mod_obj): + """ + Return the module output as a tuple (changed, user_properties) (i.e. + the arguments of the call to exit_json()). + If the module failed, return None. + """ + + def func(changed, password_rules): + return changed, password_rules + + 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]) + + +def assert_pwrule_list(pwrule_list, exp_pwrule_dict): + """ + Assert the output of the zhmc_password_rule_list module + """ + assert isinstance(pwrule_list, list) + assert len(pwrule_list) == len(exp_pwrule_dict) + for pwrule_item in pwrule_list: + assert 'name' in pwrule_item, \ + "Returned password rule {ri!r} does not have a 'name' property". \ + format(ri=pwrule_item) + pwrule_name = pwrule_item['name'] + assert pwrule_name in exp_pwrule_dict, \ + "Unexpected returned password rule {rn!r}". \ + format(rn=pwrule_name) + exp_pwrule = exp_pwrule_dict[pwrule_name] + for pname, pvalue in pwrule_item.items(): + assert pname in exp_pwrule.properties, \ + "Unexpected property {pn!r} in password rule {rn!r}". \ + format(pn=pname, rn=pwrule_name) + exp_value = exp_pwrule.properties[pname] + assert pvalue == exp_value, \ + "Incorrect value for property {pn!r} of password rule {rn!r}". \ + format(pn=pname, rn=pwrule_name) + + +@pytest.mark.parametrize( + "check_mode", [ + pytest.param(False, id="check_mode=False"), + pytest.param(True, id="check_mode=True"), + ] +) +@mock.patch("plugins.modules.zhmc_password_rule_list.AnsibleModule", + autospec=True) +def test_user_pwrule_list( + ansible_mod_cls, check_mode, hmc_session): # noqa: F811, E501 + """ + Test listing of password rules of the HMC. + """ + + hd = hmc_session.hmc_definition + hmc_host = hd.host + hmc_auth = dict(userid=hd.userid, password=hd.password, + ca_certs=hd.ca_certs, verify=hd.verify) + + client = zhmcclient.Client(hmc_session) + console = client.consoles.console + + faked_session = hmc_session if hd.mock_file else None + + # Determine the actual list of password rules on the HMC. + act_pwrules = console.password_rules.list() + act_pwrules_dict = {} + for r in act_pwrules: + act_pwrules_dict[r.name] = r + + # Prepare module input parameters (must be all required + optional) + params = { + 'hmc_host': hmc_host, + 'hmc_auth': hmc_auth, + 'log_file': LOG_FILE, + '_faked_session': faked_session, + } + + # 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_password_rule_list.main() + exit_code = exc_info.value.args[0] + + # Assert module exit code + assert exit_code == 0, \ + "Module failed with exit code {e} and message:\n{m}". \ + format(e=exit_code, m=get_failure_msg(mod_obj)) + + # Assert module output + changed, pwrule_list = get_module_output(mod_obj) + assert changed is False + + assert_pwrule_list(pwrule_list, act_pwrules_dict) diff --git a/tests/end2end/test_zhmc_user.py b/tests/end2end/test_zhmc_user.py index ea9d52578..e2f1893eb 100644 --- a/tests/end2end/test_zhmc_user.py +++ b/tests/end2end/test_zhmc_user.py @@ -85,7 +85,7 @@ 'disabled': False, 'user_role_names': ['hmc-all-system-managed-objects'], # (artificial prop) 'authentication_type': 'local', - 'password_rule_name': 'Basic', # (artificial property) + 'password_rule_name': 'Standard', # (artificial property) 'password': 'Bumeran9', 'force_password_change': True, # 'ldap_server_definition_name': no LDAP (artificial property) @@ -330,7 +330,7 @@ def test_user_facts( "Present with existing user, no properties changed", {}, { - 'password_rule_name': 'Basic', + 'password_rule_name': 'Standard', }, 'present', STD_USER_INPUT_PROPERTIES, @@ -343,7 +343,7 @@ def test_user_facts( 'session-timeout': 30, }, { - 'password_rule_name': 'Basic', + 'password_rule_name': 'Standard', }, 'present', STD_USER_INPUT_PROPERTIES, @@ -354,7 +354,7 @@ def test_user_facts( "Absent with existing user", {}, { - 'password_rule_name': 'Basic', + 'password_rule_name': 'Standard', }, 'absent', None, diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt index e5d7c358c..900c74dc2 100644 --- a/tests/sanity/ignore-2.10.txt +++ b/tests/sanity/ignore-2.10.txt @@ -8,6 +8,8 @@ plugins/modules/zhmc_partition.py validate-modules:missing-gplv3-license # Licen plugins/modules/zhmc_partition_list.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_lpar_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/zhmc_password_rule.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/zhmc_password_rule_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_storage_group.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_storage_group_attachment.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_storage_volume.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 @@ -26,6 +28,7 @@ plugins/modules/zhmc_hba.py validate-modules:return-syntax-error # Missing type plugins/modules/zhmc_nic.py validate-modules:return-syntax-error # Missing type on generic {property} plugins/modules/zhmc_partition.py validate-modules:return-syntax-error # Missing type on generic {property} plugins/modules/zhmc_lpar.py validate-modules:return-syntax-error # Missing type on generic {property} +plugins/modules/zhmc_password_rule.py validate-modules:return-syntax-error # Missing type on generic {property} plugins/modules/zhmc_storage_group.py validate-modules:return-syntax-error # Missing type on generic {property} plugins/modules/zhmc_storage_volume.py validate-modules:return-syntax-error # Missing type on generic {property} plugins/modules/zhmc_user.py validate-modules:return-syntax-error # Missing type on generic {property} diff --git a/tests/sanity/ignore-2.11.txt b/tests/sanity/ignore-2.11.txt index e8e79f6cc..305fcd94c 100644 --- a/tests/sanity/ignore-2.11.txt +++ b/tests/sanity/ignore-2.11.txt @@ -8,6 +8,8 @@ plugins/modules/zhmc_partition.py validate-modules:missing-gplv3-license # Licen plugins/modules/zhmc_partition_list.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_lpar_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/zhmc_password_rule.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/zhmc_password_rule_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_storage_group.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_storage_group_attachment.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_storage_volume.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 @@ -20,6 +22,7 @@ plugins/modules/zhmc_hba.py validate-modules:return-syntax-error # Missing type plugins/modules/zhmc_nic.py validate-modules:return-syntax-error # Missing type on generic {property} plugins/modules/zhmc_partition.py validate-modules:return-syntax-error # Missing type on generic {property} plugins/modules/zhmc_lpar.py validate-modules:return-syntax-error # Missing type on generic {property} +plugins/modules/zhmc_password_rule.py validate-modules:return-syntax-error # Missing type on generic {property} plugins/modules/zhmc_storage_group.py validate-modules:return-syntax-error # Missing type on generic {property} plugins/modules/zhmc_storage_volume.py validate-modules:return-syntax-error # Missing type on generic {property} plugins/modules/zhmc_user.py validate-modules:return-syntax-error # Missing type on generic {property} diff --git a/tests/sanity/ignore-2.12.txt b/tests/sanity/ignore-2.12.txt index e8e79f6cc..305fcd94c 100644 --- a/tests/sanity/ignore-2.12.txt +++ b/tests/sanity/ignore-2.12.txt @@ -8,6 +8,8 @@ plugins/modules/zhmc_partition.py validate-modules:missing-gplv3-license # Licen plugins/modules/zhmc_partition_list.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_lpar_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/zhmc_password_rule.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/zhmc_password_rule_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_storage_group.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_storage_group_attachment.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_storage_volume.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 @@ -20,6 +22,7 @@ plugins/modules/zhmc_hba.py validate-modules:return-syntax-error # Missing type plugins/modules/zhmc_nic.py validate-modules:return-syntax-error # Missing type on generic {property} plugins/modules/zhmc_partition.py validate-modules:return-syntax-error # Missing type on generic {property} plugins/modules/zhmc_lpar.py validate-modules:return-syntax-error # Missing type on generic {property} +plugins/modules/zhmc_password_rule.py validate-modules:return-syntax-error # Missing type on generic {property} plugins/modules/zhmc_storage_group.py validate-modules:return-syntax-error # Missing type on generic {property} plugins/modules/zhmc_storage_volume.py validate-modules:return-syntax-error # Missing type on generic {property} plugins/modules/zhmc_user.py validate-modules:return-syntax-error # Missing type on generic {property} diff --git a/tests/sanity/ignore-2.13.txt b/tests/sanity/ignore-2.13.txt index e8e79f6cc..305fcd94c 100644 --- a/tests/sanity/ignore-2.13.txt +++ b/tests/sanity/ignore-2.13.txt @@ -8,6 +8,8 @@ plugins/modules/zhmc_partition.py validate-modules:missing-gplv3-license # Licen plugins/modules/zhmc_partition_list.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_lpar_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/zhmc_password_rule.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/zhmc_password_rule_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_storage_group.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_storage_group_attachment.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_storage_volume.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 @@ -20,6 +22,7 @@ plugins/modules/zhmc_hba.py validate-modules:return-syntax-error # Missing type plugins/modules/zhmc_nic.py validate-modules:return-syntax-error # Missing type on generic {property} plugins/modules/zhmc_partition.py validate-modules:return-syntax-error # Missing type on generic {property} plugins/modules/zhmc_lpar.py validate-modules:return-syntax-error # Missing type on generic {property} +plugins/modules/zhmc_password_rule.py validate-modules:return-syntax-error # Missing type on generic {property} plugins/modules/zhmc_storage_group.py validate-modules:return-syntax-error # Missing type on generic {property} plugins/modules/zhmc_storage_volume.py validate-modules:return-syntax-error # Missing type on generic {property} plugins/modules/zhmc_user.py validate-modules:return-syntax-error # Missing type on generic {property} diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt index 9c81c9ca6..1346d4674 100644 --- a/tests/sanity/ignore-2.9.txt +++ b/tests/sanity/ignore-2.9.txt @@ -8,6 +8,8 @@ plugins/modules/zhmc_partition.py validate-modules:missing-gplv3-license # Licen plugins/modules/zhmc_partition_list.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_lpar_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/zhmc_password_rule.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/zhmc_password_rule_list.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_storage_group.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_storage_group_attachment.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/zhmc_storage_volume.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 @@ -45,6 +47,8 @@ plugins/modules/zhmc_partition.py validate-modules:import-error # Was broken in plugins/modules/zhmc_partition_list.py validate-modules:import-error # Was broken in Ansible 2.9, fixed in 2.10 via PR 63932 plugins/modules/zhmc_lpar.py validate-modules:import-error # Was broken in Ansible 2.9, fixed in 2.10 via PR 63932 plugins/modules/zhmc_lpar_list.py validate-modules:import-error # Was broken in Ansible 2.9, fixed in 2.10 via PR 63932 +plugins/modules/zhmc_password_rule.py validate-modules:import-error # Was broken in Ansible 2.9, fixed in 2.10 via PR 63932 +plugins/modules/zhmc_password_rule_list.py validate-modules:import-error # Was broken in Ansible 2.9, fixed in 2.10 via PR 63932 plugins/modules/zhmc_storage_group.py validate-modules:import-error # Was broken in Ansible 2.9, fixed in 2.10 via PR 63932 plugins/modules/zhmc_storage_group_attachment.py validate-modules:import-error # Was broken in Ansible 2.9, fixed in 2.10 via PR 63932 plugins/modules/zhmc_storage_volume.py validate-modules:import-error # Was broken in Ansible 2.9, fixed in 2.10 via PR 63932 @@ -53,3 +57,4 @@ plugins/modules/zhmc_virtual_function.py validate-modules:import-error # Was bro plugins/modules/zhmc_cpc_list.py validate-modules:return-syntax-error # Was broken in Ansible 2.9, fixed in 2.10 plugins/modules/zhmc_partition_list.py validate-modules:return-syntax-error # Was broken in Ansible 2.9, fixed in 2.10 plugins/modules/zhmc_lpar_list.py validate-modules:return-syntax-error # Was broken in Ansible 2.9, fixed in 2.10 +plugins/modules/zhmc_password_rule_list.py validate-modules:return-syntax-error # Was broken in Ansible 2.9, fixed in 2.10