From 90edd76212765802077ced20f4b1e4857789548a Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Wed, 9 Jul 2025 11:30:17 -0600 Subject: [PATCH 01/30] Add skeleton --- plugins/modules/zos_user.py | 109 ++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 plugins/modules/zos_user.py diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py new file mode 100644 index 0000000000..30f11794cd --- /dev/null +++ b/plugins/modules/zos_user.py @@ -0,0 +1,109 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) IBM Corporation 2025 +# 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 + +DOCUMENTATION = r""" +--- +module: zos_user +version_added: '2.0.0' +author: + - "Alex Moreno (@rexemin)" +short_description: +description: + - The L(zos_user,./zos_user.html) +options: + +attributes: + action: + support: none + description: Indicates this has a corresponding action plugin so some parts of the options can be executed on the controller. + async: + support: full + description: Supports being used with the ``async`` keyword. + check_mode: + support: full + description: Can run in check_mode and return changed status prediction without modifying target. If not supported, the action will be skipped. + +notes: + +seealso: +""" + +EXAMPLES = r""" +""" + +RETURN = r""" +""" + +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.ibm.ibm_zos_core.plugins.module_utils import ( + better_arg_parser +) + +def run_module(): + """ + """ + module = AnsibleModule( + argument_spec={ + 'name': { + 'type': 'str', + 'required': True, + 'aliases': ['src'] + }, + 'operation': { + 'type': 'choice', + 'required': True, + 'choices': ['create', 'list', 'update', 'delete', 'purge', 'connect', 'remove'] + }, + 'scope': { + 'type': 'choice', + 'required': True, + 'choices': ['user', 'group'] + } + }, + supports_check_mode=True + ) + + args_def = { + 'name': { + 'arg_type': 'str', + 'required': True, + 'aliases': ['src'] + }, + 'operation': { + 'type': 'str', + 'required': True + }, + 'scope': { + 'type': 'str', + 'required': True + } + } + + try: + parser = better_arg_parser.BetterArgParser(args_def) + parsed_args = parser.parse_args(module.params) + module.params = parsed_args + except ValueError as err: + module.fail_json( + msg='Parameter verification failed.', + stderr=str(err) + ) + +if __name__ == '__main__': + run_module() From 206f5092a6c64eed68e506575c19859c4387828e Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Mon, 14 Jul 2025 16:35:06 -0600 Subject: [PATCH 02/30] Add group creation --- plugins/modules/zos_user.py | 458 +++++++++++++++++++++++++++++++++++- 1 file changed, 445 insertions(+), 13 deletions(-) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index 30f11794cd..19451392cc 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -55,8 +55,247 @@ better_arg_parser ) -def run_module(): + +def dynamic_dict(contents, dependencies): + """Validates options that are YAML dictionaries created by a user in a task. + + Parameters + ---------- + contents: dict + Content of the option provided by the user. + dependencies: dict + Any dependencies the option has. + + Raises + ------ + ValueError: When something other than a dictionary gets provided. + + Returns + ------- + dict: Dictionary containing information provided by the user. """ + if contents is None: + return None + if not isinstance(contents, dict): + raise ValueError(f'Invalid value {contents}. The option needs a dictionary.') + for key in contents: + if isinstance(contents[key], dict): + raise ValueError(f'Invalid value {contents[key]}. The option does not accept subdictionaries.') + return contents + + +def are_blocks_defined(module_params): + """Checks that there's at least one block of information accompanying an operation. + It's possible to call the module without actually giving it information about what it needs + to change, since most options are not required and trying to tell the argument parser to ask + for at least one block will become too unwieldy with how many operations this module needs + to handle. + + Parameters + ---------- + module_params: dict + All the module parameters specified in the task. + + Returns + ------- + bool: Whether the module actually has something to do or if it should exit with no changes. + """ + # All the lists with only 'name' on them indicate an operation that doesn't require any other + # block to make sense. + valid_blocks = { + 'group': { + 'create': ['name'] + } + } + + scope = module_params['scope'] + operation = module_params['operation'] + + for block in valid_blocks[scope][operation]: + if module_params[block] is not None: + return True + + return False + + +class RACFHandler(): + """Small class that handles executing RACF TSO commands and RACF utilities. + """ + + def __init__(self, module, module_params): + """Initializes a new handler with all the context needed to execute RACF + commands. + + Parameters + ---------- + module: AnsibleModule + Object with all the task's context. + module_params: dict + Module options specified in the task. + """ + self.module = module + self.name = module_params['name'] + self.general = module_params['general'] + self.group = module_params['group'] + self.dfp = module_params['dfp'] + self.omvs = module_params['omvs'] + self.ovm = module_params['ovm'] + + self.cmd = None + self.num_entities_modified = 0 + self.entities_modified = [] + self.database_dumped = False + self.dump_kept = False + self.dump_name = None + + def execute_operation(self, operation, scope): + """Given the operation and scope, it executes a RACF command. + + Parameters + ---------- + operation: str + One of 'create', 'list', 'update', 'delete', 'purge', 'connect' + or 'remove'. + scope: str + One of 'user' or 'group'. + + Returns + ------- + tuple: Return code, standard output and standard error from the command. + """ + if scope == 'group': + if operation == 'create': + self.cmd = self._create_group() + + rc, stdout, stderr = self.module.run_command(f""" tsocmd "{self.cmd}" """) + return rc, stdout, stderr + + def _create_group(self): + """Builds an ADDGROUP command. + + Returns + ------- + str: ADDGROUP command. + """ + cmd = f'ADDGROUP ({self.name})' + + cmd = f'{cmd} {self._make_general_string()}'.strip() + cmd = f'{cmd} {self._make_dfp_substring()}'.strip() + cmd = f'{cmd} {self._make_group_string()}'.strip() + + # OMVS and OVM blocks won't use the string options since a group only uses one option + # from both blocks. + if self.omvs is not None: + if self.omvs['uid'] == 'auto': + cmd = f'{cmd} OMVS(AUTOGID)' + elif self.omvs['uid'] != 'none': + cmd = f"{cmd} OMVS(GID({self.omvs['custom_uid']})" + if self.omvs['uid'] == 'shared': + cmd = f'{cmd}SHARED' + cmd = f'{cmd})' + + if self.ovm is not None: + cmd = f"{cmd} OVM(GID({self.ovm['uid']}))" + + return cmd + + def _make_general_string(self): + """Creates a string that defines various common parameters of a profile. + + Returns + ------- + str: A portion of the parameters of a RACF command. + """ + cmd = "" + + if self.general is not None: + if self.general.get('custom_fields') is not None: + if self.general['custom_fields'].get('add') is not None: + custom_fields = self.general['custom_fields']['add'] + cmd = f'{cmd}CSDATA( ' + for field in custom_fields: + cmd = f'{cmd}{field}({custom_fields[field]}) ' + cmd = f'{cmd}) ' + elif self.general['custom_fields'].get('delete') is not None: + custom_fields = self.general['custom_fields']['delete'] + cmd = f'{cmd}CSDATA( ' + for field in custom_fields: + cmd = f'{cmd}NO{field.upper()} ' + cmd = f'{cmd}) ' + if self.general.get('installation_data') is not None: + cmd = f"{cmd}DATA('{self.general['installation_data']}') " + if self.general.get('model') is not None: + cmd = f"{cmd}MODEL({self.general['model']}) " + if self.general.get('owner') is not None: + cmd = f"{cmd}OWNER({self.general['owner']}) " + + return cmd + + def _make_group_string(self): + """Creates a string that defines the GROUP parameters of a profile. + + Returns + ------- + str: GROUP parameters of a RACF command. + """ + cmd = "" + + if self.group is not None: + if self.group.get('superior_group') is not None: + cmd = f"{cmd}SUPGROUP({self.group['superior_group']}) " + if self.group.get('terminal_access') is not None: + terminal_access = 'TERMUACC' if self.group['terminal_access'] else 'NOTERMUACC' + cmd = f"{cmd}{terminal_access} " + if self.group.get('universal_group', False): + cmd = f"{cmd}UNIVERSAL " + + return cmd + + def _make_dfp_substring(self): + """Creates a string that defines the DFP segment of a profile. + + Returns + ------- + str: DFP segment of a RACF command. + """ + cmd = "" + + if self.dfp is not None: + cmd = f"{cmd}DFP(" + + if self.dfp.get('data_app_id') is not None: + cmd = f"{cmd} DATAAPPL({self.dfp['data_app_id']})" + if self.dfp.get('data_class') is not None: + cmd = f"{cmd} DATACLAS({self.dfp['data_class']})" + if self.dfp.get('management_class') is not None: + cmd = f"{cmd} MGMTCLAS({self.dfp['management_class']})" + if self.dfp.get('storage_class') is not None: + cmd = f"{cmd} STORCLAS({self.dfp['storage_class']})" + + cmd = f"{cmd} )" + + return cmd + + def get_extra_data(self): + """Returns all ancilliary information computed while executing a command. + + Returns + ------- + dict: Dictionary containing cmd, num_entities_modified, entities_modified, + database_dumped, dump_kept and dump_name. + """ + return { + 'racf_command': self.cmd, + 'num_entities_modified': self.num_entities_modified, + 'entities_modified': self.entities_modified, + 'database_dumped': self.database_dumped, + 'dump_kept': self.dump_kept, + 'dump_name': self.dump_name, + } + + +def run_module(): + """Parses the module's options and runs a RACF command. """ module = AnsibleModule( argument_spec={ @@ -66,32 +305,187 @@ def run_module(): 'aliases': ['src'] }, 'operation': { - 'type': 'choice', + 'type': 'str', 'required': True, 'choices': ['create', 'list', 'update', 'delete', 'purge', 'connect', 'remove'] }, 'scope': { - 'type': 'choice', + 'type': 'str', 'required': True, 'choices': ['user', 'group'] + }, + 'general': { + 'type': 'dict', + 'required': False, + 'options': { + 'model': { + 'type': 'str', + 'required': False + }, + 'owner': { + 'type': 'str', + 'required': False + }, + 'installation_data': { + 'type': 'str', + 'required': False + }, + 'custom_fields': { + 'type': 'dict', + 'required': False, + 'mutually_exclusive': [('add', 'delete')], + 'options': { + 'add': { + 'type': 'dict', + 'required': False + }, + 'delete': { + 'type': 'list', + 'elements': 'str', + 'required': False + } + } + } + } + }, + 'group': { + 'type': 'dict', + 'required': False, + 'options': { + 'superior_group': { + 'type': 'str', + 'required': False + }, + 'terminal_access': { + 'type': 'bool', + 'required': False + }, + 'universal_group': { + 'type': 'bool', + 'required': False + } + } + }, + 'dfp': { + 'type': 'dict', + 'required': False, + 'mutually_exclusive': [ + ('data_app_id', 'delete'), + ('data_class', 'delete'), + ('management_class', 'delete'), + ('storage_class', 'delete') + ], + 'options': { + 'data_app_id': { + 'type': 'str', + 'required': False + }, + 'data_class': { + 'type': 'str', + 'required': False + }, + 'management_class': { + 'type': 'str', + 'required': False + }, + 'storage_class': { + 'type': 'str', + 'required': False + }, + 'delete': { + 'type': 'bool', + 'required': False + } + } + }, + 'omvs': { + 'type': 'dict', + 'required': False, + 'required_if': [ + ('uid', 'custom', ('custom_uid',)), + ('uid', 'shared', ('custom_uid',)) + ], + 'options': { + 'uid': { + 'type': 'str', + 'required': False, + 'choices': ['auto', 'custom', 'shared', 'none'] + }, + 'custom_uid': { + 'type': 'int', + 'required': False + } + } + }, + 'ovm': { + 'type': 'dict', + 'required': False, + 'options': { + 'uid': { + 'type': 'int', + 'required': False + } + } } }, supports_check_mode=True ) args_def = { - 'name': { - 'arg_type': 'str', - 'required': True, - 'aliases': ['src'] + 'name': {'arg_type': 'str', 'required': True, 'aliases': ['src']}, + 'operation': {'arg_type': 'str', 'required': True}, + 'scope': {'arg_type': 'str', 'required': True}, + 'general': { + 'arg_type': 'dict', + 'required': False, + 'options': { + 'model': {'arg_type': 'str', 'required': False}, + 'owner': {'arg_type': 'str', 'required': False}, + 'installation_data': {'arg_type': 'str', 'required': False}, + 'custom_fields': { + 'arg_type': 'dict', + 'required': False, + 'options': { + 'add': {'arg_type': dynamic_dict, 'required': False}, + 'delete': {'arg_type': 'list', 'elements': 'str', 'required': False} + } + } + } }, - 'operation': { - 'type': 'str', - 'required': True + 'group': { + 'arg_type': 'dict', + 'required': False, + 'options': { + 'superior_group': {'arg_type': 'str', 'required': False}, + 'terminal_access': {'arg_type': 'bool', 'required': False}, + 'universal_group': {'arg_type': 'bool', 'required': False} + } + }, + 'dfp': { + 'arg_type': 'dict', + 'required': False, + 'options': { + 'data_app_id': {'arg_type': 'str', 'required': False}, + 'data_class': {'arg_type': 'str', 'required': False}, + 'management_class': {'arg_type': 'str', 'required': False}, + 'storage_class': {'arg_type': 'str', 'required': False}, + 'delete': {'arg_type': 'bool', 'required': False} + } + }, + 'omvs': { + 'arg_type': 'dict', + 'required': False, + 'options': { + 'uid': {'arg_type': 'str', 'required': False}, + 'custom_uid': {'arg_type': 'int', 'required': False} + } }, - 'scope': { - 'type': 'str', - 'required': True + 'ovm': { + 'arg_type': 'dict', + 'required': False, + 'options': { + 'uid': {'arg_type': 'int', 'required': False} + } } } @@ -105,5 +499,43 @@ def run_module(): stderr=str(err) ) + result = { + 'changed': False, + 'operation': module.params['operation'], + 'racf_command': None, + 'num_entities_modified': 0, + 'entities_modified': [], + 'database_dumped': False, + 'dump_kept': False, + 'dump_name': None + } + + if not are_blocks_defined(module.params): + result['msg'] = 'No profile blocks were provided with this operation, no changes made.' + module.exit_json(**result) + + operation = module.params['operation'] + scope = module.params['scope'] + racf_handler = RACFHandler(module, module.params) + + rc, stdout, stderr = racf_handler.execute_operation(operation, scope) + result['rc'] = rc + result['stdout'] = stdout + result['stdout_lines'] = stdout.split('\n') + result['stderr'] = stderr + result['stderr_lines'] = stderr.split('\n') + result.update(racf_handler.get_extra_data()) + + if rc == 0: + result['changed'] = True + else: + module.fail_json(**result) + + module.exit_json(**result) + + # TODO: check that fields are ignored (deleted from the class object) when needed. + # TODO: add 'delete_block' to custom_fields + # TODO: add support for check_mode. + if __name__ == '__main__': run_module() From d22ba409725b2a5a915f6694fc3b31d6ab14e16f Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Tue, 15 Jul 2025 13:05:25 -0600 Subject: [PATCH 03/30] Update return fields when creating a group --- plugins/modules/zos_user.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index 19451392cc..deec9ccc55 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -165,17 +165,17 @@ def execute_operation(self, operation, scope): """ if scope == 'group': if operation == 'create': - self.cmd = self._create_group() + rc, stdout, stderr, cmd = self._create_group() - rc, stdout, stderr = self.module.run_command(f""" tsocmd "{self.cmd}" """) + self.cmd = cmd return rc, stdout, stderr def _create_group(self): - """Builds an ADDGROUP command. + """Builds and execute an ADDGROUP command. Returns ------- - str: ADDGROUP command. + tuple: RC, stdout and stderr from the RACF command, and the ADDGROUP command. """ cmd = f'ADDGROUP ({self.name})' @@ -197,7 +197,13 @@ def _create_group(self): if self.ovm is not None: cmd = f"{cmd} OVM(GID({self.ovm['uid']}))" - return cmd + rc, stdout, stderr = self.module.run_command(f""" tsocmd "{cmd}" """) + + if rc == 0: + self.num_entities_modified = 1 + self.entities_modified = [self.name] + + return rc, stdout, stderr, cmd def _make_general_string(self): """Creates a string that defines various common parameters of a profile. From e3cbd137d5b8f6e462177e2be26d1f20cc45d200 Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Tue, 15 Jul 2025 15:56:10 -0600 Subject: [PATCH 04/30] Add clean_blocks function --- plugins/modules/zos_user.py | 40 +++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index deec9ccc55..53b6ada089 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -84,6 +84,38 @@ def dynamic_dict(contents, dependencies): return contents +def clean_blocks(module_params, operation, scope): + """Deletes unnecessary fields and blocks when an operation doesn't + need them. + + Parameters + ---------- + module_params: dict + All parameters specified for this task. + operation: str + One of 'create', 'list', 'update', 'delete', 'purge', 'connect' + or 'remove'. + scope: str + One of 'user' or 'group'. + """ + if scope == 'group': + if operation == 'create': + # Groups only accept a UID from the OMVS segment. + if module_params['omvs'] is not None: + omvs = dict() + if module_params['omvs'].get('uid') is not None: + omvs['uid'] = module_params['omvs']['uid'] + if module_params['omvs'].get('custom_uid') is not None: + omvs['custom_uid'] = module_params['omvs']['custom_uid'] + module_params['omvs'] = omvs if omvs else None + # Groups only accept UID from the OVM segment. + if module_params['ovm'] is not None: + ovm = dict() + if module_params['ovm'].get('uid') is not None: + ovm['uid'] = module_params['ovm']['uid'] + module_params['ovm'] = ovm if ovm else None + + def are_blocks_defined(module_params): """Checks that there's at least one block of information accompanying an operation. It's possible to call the module without actually giving it information about what it needs @@ -516,14 +548,15 @@ def run_module(): 'dump_name': None } + operation = module.params['operation'] + scope = module.params['scope'] + clean_blocks(module.params, operation, scope) + if not are_blocks_defined(module.params): result['msg'] = 'No profile blocks were provided with this operation, no changes made.' module.exit_json(**result) - operation = module.params['operation'] - scope = module.params['scope'] racf_handler = RACFHandler(module, module.params) - rc, stdout, stderr = racf_handler.execute_operation(operation, scope) result['rc'] = rc result['stdout'] = stdout @@ -539,7 +572,6 @@ def run_module(): module.exit_json(**result) - # TODO: check that fields are ignored (deleted from the class object) when needed. # TODO: add 'delete_block' to custom_fields # TODO: add support for check_mode. From d1e69368e213d1d431d4e58d2a998eaddc46202a Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Wed, 27 Aug 2025 11:42:59 -0600 Subject: [PATCH 05/30] Refactor and add group updates --- plugins/modules/zos_user.py | 676 ++++++++++++++++++++++++++---------- 1 file changed, 492 insertions(+), 184 deletions(-) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index 53b6ada089..fb55326a08 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -48,6 +48,8 @@ RETURN = r""" """ +import copy +import re import traceback from ansible.module_utils.basic import AnsibleModule @@ -84,75 +86,52 @@ def dynamic_dict(contents, dependencies): return contents -def clean_blocks(module_params, operation, scope): - """Deletes unnecessary fields and blocks when an operation doesn't - need them. - - Parameters - ---------- - module_params: dict - All parameters specified for this task. - operation: str - One of 'create', 'list', 'update', 'delete', 'purge', 'connect' - or 'remove'. - scope: str - One of 'user' or 'group'. - """ - if scope == 'group': - if operation == 'create': - # Groups only accept a UID from the OMVS segment. - if module_params['omvs'] is not None: - omvs = dict() - if module_params['omvs'].get('uid') is not None: - omvs['uid'] = module_params['omvs']['uid'] - if module_params['omvs'].get('custom_uid') is not None: - omvs['custom_uid'] = module_params['omvs']['custom_uid'] - module_params['omvs'] = omvs if omvs else None - # Groups only accept UID from the OVM segment. - if module_params['ovm'] is not None: - ovm = dict() - if module_params['ovm'].get('uid') is not None: - ovm['uid'] = module_params['ovm']['uid'] - module_params['ovm'] = ovm if ovm else None - - -def are_blocks_defined(module_params): - """Checks that there's at least one block of information accompanying an operation. - It's possible to call the module without actually giving it information about what it needs - to change, since most options are not required and trying to tell the argument parser to ask - for at least one block will become too unwieldy with how many operations this module needs - to handle. - - Parameters - ---------- - module_params: dict - All the module parameters specified in the task. - - Returns - ------- - bool: Whether the module actually has something to do or if it should exit with no changes. +class RACFHandler(): + """Parent class for group and user RACF operations. """ - # All the lists with only 'name' on them indicate an operation that doesn't require any other - # block to make sense. - valid_blocks = { - 'group': { - 'create': ['name'] + # These next 4 fields should be overwritten by every subclass. Values here + # are just an example of the format expected. + # self.filters has information about what suboptions and options are valid + # for a certain operation, generally to ignore the rest of the options since + # they are not needed. + filters = { + 'operation': { + 'nested': [], + 'flat': [] } } - scope = module_params['scope'] - operation = module_params['operation'] - - for block in valid_blocks[scope][operation]: - if module_params[block] is not None: - return True - - return False - + # self.valid_blocks has information about what blocks are needed so an operation + # makes sense. + valid_blocks = { + 'operation': [] + } -class RACFHandler(): - """Small class that handles executing RACF TSO commands and RACF utilities. - """ + # self.should_remove_empty_strings tells whether or not deleting entire blocks + # (what an empty string means in this module) makes sense in an operation, and + # whether options with this value should be removed. + should_remove_empty_strings = {} + + # self.validations has information on valid ranges or values for all options + # that should be validated before running a RACF command. + # First tuple defines the option/suboption that should be validated. + # Second element defines the type of validation needed, there is 'format', + # 'length' and 'range'. + # Third element (and second tuple) is an argument tuple for the validation. + validations = [ + # Format validation verifies that a string comes in a specific format. + # If multiple formats are valid, specify them inside the arg tuple. + # (('block', 'option'), 'format', ('format1', 'format2')), + # Length validation verifies that a string contains a certain number + # of characters. If multiple length ranges are valid, specify each one + # as a 2-item tuple inside the arg tuple. + # (('block', 'option', 'suboption'), 'length', ((min, max), (max, max))), + # Range validation verifies that an integer has a value inside a specific + # range. A third value in the arg tuple defines a special value that + # represents deleting the field/block from a profile during update + # operations. + # (('block', 'option'), 'range', (min, max, deletion)) + ] def __init__(self, module, module_params): """Initializes a new handler with all the context needed to execute RACF @@ -165,22 +144,322 @@ def __init__(self, module, module_params): module_params: dict Module options specified in the task. """ - self.module = module + # Standalone params. self.name = module_params['name'] - self.general = module_params['general'] - self.group = module_params['group'] - self.dfp = module_params['dfp'] - self.omvs = module_params['omvs'] - self.ovm = module_params['ovm'] - + self.operation = module_params['operation'] + self.scope = module_params['scope'] + # Nested params. + params_copy = copy.deepcopy(module_params) + del params_copy['name'] + del params_copy['operation'] + del params_copy['scope'] + self.params = params_copy + # Execution data. self.cmd = None self.num_entities_modified = 0 self.entities_modified = [] self.database_dumped = False self.dump_kept = False self.dump_name = None + self.module = module + + def get_state(self): + """Returns the current values of most fields. + + Returns + ------- + dict: Dictionary with all fields. + """ + return { + 'cmd': self.cmd, + 'num_entities_modified': self.num_entities_modified, + 'entities_modified': self.entities_modified, + 'database_dumped': self.database_dumped, + 'dump_kept': self.dump_kept, + 'dump_name': self.dump_name + } + + def clean_input(self): + """Removes empty strings and unnecessary options when needed. Modifies self.params. + """ + if self.should_remove_empty_strings.get(self.operation, False): + self.remove_empty_strings() + self.clean_blocks() + + def remove_empty_strings(self): + """Removes every field that has an empty space from self.params. Empty strings + in the context of the module cause an entire block from a profile to get deleted. + This behavior only applies to certain operations, see the values of + self.should_remove_empty_strings in each subclass. + """ + for block in self.params: + for option in self.params[block]: + if self.params[block][option] == "": + del self.params[block][option] + elif isinstance(self.params[block][option], dict): + for suboption in self.params[block][option]: + if self.params[block][option][suboption] == "": + del self.params[block][option][suboption] + + def filter_block(self, block, allowed_options): + """Returns a new dictionary with values from only the allowed + options for a specific block (group of related suboptions for a profile). + + Parameters + ---------- + block: dict + Dictionary containing the suboptions for a specific set of + attributes a RACF profile should have. + allowed_options: list + Suboptions from block that are allowed by an operation. + + Returns + ------- + dict: Filtered dictionary containing only allowed options. + """ + if block is not None: + params = {} + for option in allowed_options: + if block.get(option) is not None: + params[option] = block[option] + return params + + return None + + def clean_blocks(self): + """Deletes unnecessary fields and blocks when an operation doesn't + need them by using the information in self.filters. + """ + for block in self.filters[self.operation].get('nested', {}): + if self.params.get(block[0], {}).get(block[1]) is not None: + filtered_params = self.filter_block(self.params[block[0]][block[1]], block[2]) + self.params[block[0]][block[1]] = filtered_params if filtered_params else None + + for block in self.filters[self.operation].get('flat', {}): + if self.params.get(block[0]) is not None: + filtered_params = self.filter_block(self.params[block[0]], block[1]) + self.params[block[0]] = filtered_params if filtered_params else None + + # Removing empty dictionaries from the parameters. + for block in self.params: + if isinstance(self.params[block], dict) and not self.params[block]: + del self.params[block] + + def are_blocks_defined(self): + """Checks that there's at least one block of information accompanying an operation. + It's possible to call the module without actually giving it information about what it needs + to change, since most options are not required and trying to tell the argument parser to ask + for at least one block will become too unwieldy with how many operations this module needs + to handle. + + Returns + ------- + bool: Whether the module actually has something to do or if it should exit with no changes. + """ + if len(self.valid_blocks[self.operation]) == 0: + return True + + for block in self.valid_blocks[self.operation]: + if self.params[block] is not None: + return True + + return False + + def validate_params(self): + """Uses self.validations to validate that all parameters are withing valid ranges + or have valid values. + + Raises + ------ + ValueError: When a parameter has an invalid value. + """ + def validate_format(value, formats): + for str_format in formats: + if re.fullmatch(str_format, value) is not None: + return True + + return False + + def validate_length(value, lengths): + for (min_length, max_length) in lengths: + if len(value) >= min_length and len(value) <= max_length: + return True + + return False + + def validate_range(value, valid_range): + min_value = valid_range[0] + max_value = valid_range[1] + deletion_value = valid_range[2] + + if (min_value <= value <= max_value) or value == deletion_value: + return True + + return False + + for validation in self.validations: + option = validation[0] + if len(option) == 2: + value = self.params.get(option[0], {}).get(option[1]) + option_name = f'{option[0]}.{option[1]}' + else: + value = self.params.get(option[0], {}).get(option[1], {}).get(option[2]) + option_name = f'{option[0]}.{option[1]}.{option[2]}' + + if value is None: + continue + + if validation[1] == 'format': + if validate_format(value, validation[2]): + continue + elif validation[1] == 'length': + if validate_length(value, validation[2]): + continue + elif validation[1] == 'range': + if validate_range(value, validation[2]): + continue + + # When having valid option values, we should never reach this line. + raise ValueError(f'Option {option_name} has an invalid value, please check your task.') + + def execute_operation(self): + """This method should handle all calls to RACF operations inside a subclass. + + Returns + ------- + dict: Dictionary containing RC, stdout, stderr from the RACF command executed, + as well as the command string, self.num_entities_modified, self.entities_modified, + self.database_dumped, self.dump_kept and self.dump_name. + """ + return self.get_state() + + def _make_general_string(self): + """Creates a string that defines various common parameters of a profile. + + Returns + ------- + str: A portion of the parameters of a RACF command. + """ + cmd = "" + general = self.params.get('general') + + if general is not None: + if general.get('custom_fields') is not None: + if general['custom_fields'].get('add') is not None: + custom_fields = general['custom_fields']['add'] + cmd = f'{cmd}CSDATA( ' + for field in custom_fields: + cmd = f'{cmd}{field}({custom_fields[field]}) ' + cmd = f'{cmd}) ' + elif general['custom_fields'].get('delete') is not None: + custom_fields = general['custom_fields']['delete'] + cmd = f'{cmd}CSDATA( ' + for field in custom_fields: + cmd = f'{cmd}NO{field.upper()} ' + cmd = f'{cmd}) ' + elif general['custom_fields'].get('delete_block') is not None: + cmd = f'{cmd}NOCSDATA ' + if general.get('installation_data') is not None: + if general.get('installation_data') != "": + cmd = f"{cmd}DATA('{general['installation_data']}') " + else: + cmd = f"{cmd}NODATA " + if general.get('model') is not None: + if general.get('model') != "": + cmd = f"{cmd}MODEL({general['model']}) " + else: + cmd = f"{cmd}NOMODEL " + if general.get('owner') is not None and general.get('owner') != "": + cmd = f"{cmd}OWNER({general['owner']}) " + + return cmd + + def _make_dfp_substring(self): + """Creates a string that defines the DFP segment of a profile. + + Returns + ------- + str: DFP segment of a RACF command. + """ + cmd = "" + dfp = self.params.get('dfp') + + if dfp is not None: + cmd = f"{cmd}DFP(" + + if dfp.get('data_app_id') is not None: + if dfp.get('data_app_id') != "": + cmd = f"{cmd} DATAAPPL({dfp['data_app_id']})" + else: + cmd = f"{cmd} NODATAAPPL" + if dfp.get('data_class') is not None: + if dfp.get('data_class') != "": + cmd = f"{cmd} DATACLAS({dfp['data_class']})" + else: + cmd = f"{cmd} NODATACLAS" + if dfp.get('management_class') is not None: + if dfp.get('management_class') != "": + cmd = f"{cmd} MGMTCLAS({dfp['management_class']})" + else: + cmd = f"{cmd} NOMGMTCLAS" + if dfp.get('storage_class') is not None: + if dfp.get('storage_class') != "": + cmd = f"{cmd} STORCLAS({dfp['storage_class']})" + else: + cmd = f"{cmd} NOSTORCLAS" + + cmd = f"{cmd} )" + + return cmd + + +class GroupHandler(RACFHandler): + """Subclass containing all information needed to clean, validate and execute + RACF commands affecting group profiles. + """ + filters = { + 'create': { + 'nested': [ + ('general', 'custom_fields', ('add',)) + ], + 'flat': [ + ('omvs', ('uid', 'custom_uid')), + ('ovm', ('uid',)) + ] + }, + 'update': { + 'flat': [ + ('omvs', ('uid', 'custom_uid')), + ('ovm', ('uid',)) + ] + } + } - def execute_operation(self, operation, scope): + should_remove_empty_strings = { + 'create': True, + 'update': False + } + + # All empty lists indicate an operation that doesn't require any other + # block to make sense. + valid_blocks = { + 'create': [], + 'update': ['general', 'group', 'dfp', 'omvs', 'ovm'] + } + + validations = [ + (('general', 'installation_data'), 'length', ((0, 255),)), + (('dfp', 'data_app_id'), 'length', ((0, 8),)), + (('dfp', 'data_class'), 'length', ((0, 8),)), + (('dfp', 'management_class'), 'length', ((0, 8),)), + (('dfp', 'storage_class'), 'length', ((0, 8),)), + (('omvs', 'custom_uid'), 'range', (0, 2_147_483_647, 0)), + (('ovm', 'root'), 'length', ((0, 1023),)), + (('ovm', 'home'), 'length', ((0, 1023),)), + (('ovm', 'uid'), 'range', (0, 2_147_483_647, -1)) + ] + + def execute_operation(self): """Given the operation and scope, it executes a RACF command. Parameters @@ -195,12 +474,19 @@ def execute_operation(self, operation, scope): ------- tuple: Return code, standard output and standard error from the command. """ - if scope == 'group': - if operation == 'create': + if self.scope == 'group': + if self.operation == 'create': rc, stdout, stderr, cmd = self._create_group() + if self.operation == 'update': + rc, stdout, stderr, cmd = self._update_group() self.cmd = cmd - return rc, stdout, stderr + # Getting the base dictionary. + result = super().execute_operation() + result['rc'] = rc + result['stdout'] = stdout + result['stderr'] = stderr + return result def _create_group(self): """Builds and execute an ADDGROUP command. @@ -215,19 +501,21 @@ def _create_group(self): cmd = f'{cmd} {self._make_dfp_substring()}'.strip() cmd = f'{cmd} {self._make_group_string()}'.strip() - # OMVS and OVM blocks won't use the string options since a group only uses one option + # OMVS and OVM blocks won't use the string methods since a group only uses one option # from both blocks. - if self.omvs is not None: - if self.omvs['uid'] == 'auto': + omvs = self.params.get('omvs') + if omvs is not None: + if omvs.get('uid') == 'auto': cmd = f'{cmd} OMVS(AUTOGID)' - elif self.omvs['uid'] != 'none': - cmd = f"{cmd} OMVS(GID({self.omvs['custom_uid']})" - if self.omvs['uid'] == 'shared': + elif omvs.get('uid') != 'none': + cmd = f"{cmd} OMVS(GID({omvs['custom_uid']})" + if omvs['uid'] == 'shared': cmd = f'{cmd}SHARED' cmd = f'{cmd})' - if self.ovm is not None: - cmd = f"{cmd} OVM(GID({self.ovm['uid']}))" + ovm = self.params.get('ovm') + if ovm is not None and ovm.get('uid') != -1: + cmd = f"{cmd} OVM(GID({ovm['uid']}))" rc, stdout, stderr = self.module.run_command(f""" tsocmd "{cmd}" """) @@ -237,37 +525,53 @@ def _create_group(self): return rc, stdout, stderr, cmd - def _make_general_string(self): - """Creates a string that defines various common parameters of a profile. + def _update_group(self): + """Builds and execute an ALTGROUP command. Returns ------- - str: A portion of the parameters of a RACF command. + tuple: RC, stdout and stderr from the RACF command, and the ALTGROUP command. """ - cmd = "" + cmd = f'ALTGROUP ({self.name})' - if self.general is not None: - if self.general.get('custom_fields') is not None: - if self.general['custom_fields'].get('add') is not None: - custom_fields = self.general['custom_fields']['add'] - cmd = f'{cmd}CSDATA( ' - for field in custom_fields: - cmd = f'{cmd}{field}({custom_fields[field]}) ' - cmd = f'{cmd}) ' - elif self.general['custom_fields'].get('delete') is not None: - custom_fields = self.general['custom_fields']['delete'] - cmd = f'{cmd}CSDATA( ' - for field in custom_fields: - cmd = f'{cmd}NO{field.upper()} ' - cmd = f'{cmd}) ' - if self.general.get('installation_data') is not None: - cmd = f"{cmd}DATA('{self.general['installation_data']}') " - if self.general.get('model') is not None: - cmd = f"{cmd}MODEL({self.general['model']}) " - if self.general.get('owner') is not None: - cmd = f"{cmd}OWNER({self.general['owner']}) " + cmd = f'{cmd} {self._make_general_string()}'.strip() + cmd = f'{cmd} {self._make_dfp_substring()}'.strip() + cmd = f'{cmd} {self._make_group_string()}'.strip() - return cmd + # OMVS and OVM blocks won't use the string options since a group only uses one option + # from both blocks. + omvs = self.params.get('omvs') + if omvs is not None: + if omvs.get('delete'): + cmd = f'{cmd} NOOMVS' + + if omvs.get('uid') == 'auto': + cmd = f'{cmd} OMVS(AUTOGID)' + elif omvs.get('uid') != 'none': + cmd = f"{cmd} OMVS(GID({omvs['custom_uid']})" + if omvs['uid'] == 'shared': + cmd = f'{cmd}SHARED' + cmd = f'{cmd})' + else: + cmd = f'{cmd} OMVS(NOGID)' + + ovm = self.params.get('ovm') + if ovm is not None: + if ovm.get('delete'): + cmd = f'{cmd} NOOVM' + + if ovm.get('uid') != -1: + cmd = f"{cmd} OVM(GID({ovm['uid']}))" + else: + cmd = f"{cmd} OVM(NOGID)" + + rc, stdout, stderr = self.module.run_command(f""" tsocmd "{cmd}" """) + + if rc == 0: + self.num_entities_modified = 1 + self.entities_modified = [self.name] + + return rc, stdout, stderr, cmd def _make_group_string(self): """Creates a string that defines the GROUP parameters of a profile. @@ -277,59 +581,39 @@ def _make_group_string(self): str: GROUP parameters of a RACF command. """ cmd = "" + group = self.params.get('group') - if self.group is not None: - if self.group.get('superior_group') is not None: - cmd = f"{cmd}SUPGROUP({self.group['superior_group']}) " - if self.group.get('terminal_access') is not None: - terminal_access = 'TERMUACC' if self.group['terminal_access'] else 'NOTERMUACC' + if group is not None: + if group.get('superior_group') is not None: + cmd = f"{cmd}SUPGROUP({group['superior_group']}) " + if group.get('terminal_access') is not None: + terminal_access = 'TERMUACC' if group['terminal_access'] else 'NOTERMUACC' cmd = f"{cmd}{terminal_access} " - if self.group.get('universal_group', False): + if group.get('universal_group', False): cmd = f"{cmd}UNIVERSAL " return cmd - def _make_dfp_substring(self): - """Creates a string that defines the DFP segment of a profile. - - Returns - ------- - str: DFP segment of a RACF command. - """ - cmd = "" - - if self.dfp is not None: - cmd = f"{cmd}DFP(" - - if self.dfp.get('data_app_id') is not None: - cmd = f"{cmd} DATAAPPL({self.dfp['data_app_id']})" - if self.dfp.get('data_class') is not None: - cmd = f"{cmd} DATACLAS({self.dfp['data_class']})" - if self.dfp.get('management_class') is not None: - cmd = f"{cmd} MGMTCLAS({self.dfp['management_class']})" - if self.dfp.get('storage_class') is not None: - cmd = f"{cmd} STORCLAS({self.dfp['storage_class']})" - cmd = f"{cmd} )" +def get_racf_handler(module, module_params): + """Returns the correct handler needed for the scope and operation given in a task. - return cmd - - def get_extra_data(self): - """Returns all ancilliary information computed while executing a command. + Parameters + ---------- + module: AnsibleModule + Object with all the task's context. + module_params: dict + Module options specified in the task. - Returns - ------- - dict: Dictionary containing cmd, num_entities_modified, entities_modified, - database_dumped, dump_kept and dump_name. - """ - return { - 'racf_command': self.cmd, - 'num_entities_modified': self.num_entities_modified, - 'entities_modified': self.entities_modified, - 'database_dumped': self.database_dumped, - 'dump_kept': self.dump_kept, - 'dump_name': self.dump_name, - } + Returns + ------- + RACFHandler: Object with the necessary context to execute a RACF command. + """ + if module_params['scope'] == 'group': + return GroupHandler(module, module_params) + elif module_params['scope'] == 'user': + pass + # return UserHandler(module, module_params) def run_module(): @@ -371,7 +655,11 @@ def run_module(): 'custom_fields': { 'type': 'dict', 'required': False, - 'mutually_exclusive': [('add', 'delete')], + 'mutually_exclusive': [ + ('add', 'delete'), + ('add', 'delete_block'), + ('delete', 'delete_block') + ], 'options': { 'add': { 'type': 'dict', @@ -381,6 +669,10 @@ def run_module(): 'type': 'list', 'elements': 'str', 'required': False + }, + 'delete_block': { + 'type': 'bool', + 'required': False } } } @@ -402,7 +694,7 @@ def run_module(): 'type': 'bool', 'required': False } - } + } }, 'dfp': { 'type': 'dict', @@ -439,6 +731,10 @@ def run_module(): 'omvs': { 'type': 'dict', 'required': False, + 'mutually_exclusive': [ + ('uid', 'delete'), + ('custom_uid', 'delete') + ], 'required_if': [ ('uid', 'custom', ('custom_uid',)), ('uid', 'shared', ('custom_uid',)) @@ -452,16 +748,37 @@ def run_module(): 'custom_uid': { 'type': 'int', 'required': False + }, + 'delete': { + 'type': 'bool', + 'required': False } } }, 'ovm': { 'type': 'dict', 'required': False, + 'mutually_exclusive': [ + ('root', 'delete'), + ('home', 'delete'), + ('uid', 'delete'), + ], 'options': { + 'root': { + 'type': 'str', + 'required': False + }, + 'home': { + 'type': 'str', + 'required': False + }, 'uid': { 'type': 'int', 'required': False + }, + 'delete': { + 'type': 'bool', + 'required': False } } } @@ -485,7 +802,8 @@ def run_module(): 'required': False, 'options': { 'add': {'arg_type': dynamic_dict, 'required': False}, - 'delete': {'arg_type': 'list', 'elements': 'str', 'required': False} + 'delete': {'arg_type': 'list', 'elements': 'str', 'required': False}, + 'delete_block': {'arg_type': 'bool', 'required': False} } } } @@ -522,7 +840,10 @@ def run_module(): 'arg_type': 'dict', 'required': False, 'options': { - 'uid': {'arg_type': 'int', 'required': False} + 'root': {'arg_type': 'str', 'required': False}, + 'home': {'arg_type': 'str', 'required': False}, + 'uid': {'arg_type': 'int', 'required': False}, + 'delete': {'arg_type': 'bool', 'required': False} } } } @@ -537,43 +858,30 @@ def run_module(): stderr=str(err) ) - result = { - 'changed': False, - 'operation': module.params['operation'], - 'racf_command': None, - 'num_entities_modified': 0, - 'entities_modified': [], - 'database_dumped': False, - 'dump_kept': False, - 'dump_name': None - } - - operation = module.params['operation'] - scope = module.params['scope'] - clean_blocks(module.params, operation, scope) - - if not are_blocks_defined(module.params): + # TODO: add support for check_mode. + operation_handler = get_racf_handler(module, module.params) + operation_handler.clean_input() + if not operation_handler.are_blocks_defined(): + result = operation_handler.get_state() result['msg'] = 'No profile blocks were provided with this operation, no changes made.' module.exit_json(**result) - racf_handler = RACFHandler(module, module.params) - rc, stdout, stderr = racf_handler.execute_operation(operation, scope) - result['rc'] = rc - result['stdout'] = stdout - result['stdout_lines'] = stdout.split('\n') - result['stderr'] = stderr - result['stderr_lines'] = stderr.split('\n') - result.update(racf_handler.get_extra_data()) + try: + operation_handler.validate_params() + except ValueError as err: + result['msg'] = str(err) + module.fail_json(**result) - if rc == 0: + result = operation_handler.execute_operation() + result['stdout_lines'] = result['stdout'].split('\n') + result['stderr_lines'] = result['stderr'].split('\n') + + if result['rc'] == 0: result['changed'] = True else: module.fail_json(**result) module.exit_json(**result) - # TODO: add 'delete_block' to custom_fields - # TODO: add support for check_mode. - if __name__ == '__main__': run_module() From ab86a2257aed0b24059e92064b3a7df666abf43e Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Thu, 28 Aug 2025 15:17:43 -0600 Subject: [PATCH 06/30] Start module doc, fix validations --- plugins/modules/zos_user.py | 265 +++++++++++++++++++++++++++--------- 1 file changed, 200 insertions(+), 65 deletions(-) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index fb55326a08..bf7e8036a6 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -21,10 +21,188 @@ version_added: '2.0.0' author: - "Alex Moreno (@rexemin)" -short_description: +short_description: Manage user and group profiles in RACF description: - - The L(zos_user,./zos_user.html) + - The L(zos_user,./zos_user.html) module executes RACF TSO commands that can manage + user and group RACF profiles. + - The module can create, update and delete RACF profiles, as well as list information + about them. options: + name: + description: + - Name of the RACF profile the module will operate on. + type: str + required: true + aliases: + - src + operation: + description: + - RACF command that will be executed. + - Group profiles can be created, updated, listed, deleted and purged. + - User profiles can use any of the choices. + - C(delete) will run a RACF C(DELGROUP) or a C(DELUSER) TSO command. This will + remove the profile but not every reference in the RACF database. + - C(purge) will execute the RACF utility IRRDBU00, thereby removing all references + of a profile from the RACF database. + - C(connect) will add a given user profile to a group. C(remove) will remove the + user from a group. + type: str + required: true + choices: + - create + - update + - list + - delete + - purge + - connect + - remove + scope: + description: + - Whether commands should affect a user or a group profile. + type: str + required: true + choices: + - user + - group + general: + description: + - Options that change common attributes in a RACF profile. + required: false + type: dict + suboptions: + model: + description: + - RACF profile that will be used as a model for the profile being changed. + - An empty string will delete this field from the profile. + type: str + required: false + owner: + description: + - Owner of the profile that is being changed. + - It can be a user or a group profile. + type: str + required: false + installation_data: + description: + - Installation-defined data that will be stored in the profile. + - Maximum length of 255 characters. + - The module will automatically enclose the contents in single quotation + marks. + - An empty string will delete this field from the profile. + type: str + required: false + custom_fields: + description: + - Custom fields that will be stored with the profile. + type: dict + required: false + suboptions: + add: + description: + - Adds custom fields to this profile. + - Each custom field should be a C(key: value) pair. + type: dict + required: false + delete: + description: + - Deletes each custom field listed. + type: list + elements: str + required: false + delete_block: + description: + - Delete the whole custom fields block from the profile. + - This option is only valid when updating profiles, it will be ignored + when creating one. + - This option is mutually exclusive with C(add) and C(delete). + type: bool + required: required + group: + description: + - Options that change group-specific attributes in a RACF profile. + - Only valid when changing a group profile, ignored for user profiles. + required: false + type: dict + suboptions: + superior_group: + description: + - Superior group that will be assigned to the profile. + type: str + required: false + terminal_access: + description: + - Whether to allow the use of the universal access authority for a + terminal during authorization checking. + type: bool + required: false + universal_group: + description: + - Whether the group should be allowed to have an unlimited number of + users. + type: bool + required: false + dfp: + description: + - Options that set DFP attributes from the Storage Management Subsytem. + required: false + type: dict + suboptions: + data_app_id: + description: Name of a DFP data application. + type: str + required: false + data_class: + description: Default data class for data set allocation. + type: str + required: false + management_class: + description: Default management class for data set migration and backup. + type: str + required: false + storage_class: + description: Default storage class for data set space, device and volume. + type: str + required: false + delete: + description: + - Delete the whole DFP block from the profile. + - This option is only valid when updating profiles, it will be ignored + when creating one. + - This option is mutually exclusive with every other option in this section. + type: bool + required: false + omvs: + description: + - Attributes for how Unix System Services should work under a profile. + required: false + type: dict + suboptions: + uid: + description: + - How RACF should assign a user its UID. + - C(none) will be ignored when creating a profile. + - C(custom) and C(shared) require C(custom_uid) too. + type: str + required: false + choices: + - auto + - custom + - shared + - none + custom_uid: + description: + - Specifies the profile's UID. + - A number between 0 and 2,147,483,647. + type: int + required: false + delete: + description: + - Delete the whole OMVS block from the profile. + - This option is only valid when updating profiles, it will be ignored + when creating one. + - This option is mutually exclusive with every other option in this section. + type: bool + required: false attributes: action: @@ -40,6 +218,7 @@ notes: seealso: + - module: zos_tso_command """ EXAMPLES = r""" @@ -193,6 +372,9 @@ def remove_empty_strings(self): self.should_remove_empty_strings in each subclass. """ for block in self.params: + if self.params[block] is None: + continue + for option in self.params[block]: if self.params[block][option] == "": del self.params[block][option] @@ -241,9 +423,17 @@ def clean_blocks(self): self.params[block[0]] = filtered_params if filtered_params else None # Removing empty dictionaries from the parameters. + clean_params = copy.deepcopy(self.params) for block in self.params: - if isinstance(self.params[block], dict) and not self.params[block]: - del self.params[block] + if self.params[block] is None: + del clean_params[block] + continue + + for option in self.params[block]: + if self.params[block][option] is None: + del clean_params[block][option] + + self.params = clean_params def are_blocks_defined(self): """Checks that there's at least one block of information accompanying an operation. @@ -424,13 +614,11 @@ class GroupHandler(RACFHandler): ], 'flat': [ ('omvs', ('uid', 'custom_uid')), - ('ovm', ('uid',)) ] }, 'update': { 'flat': [ ('omvs', ('uid', 'custom_uid')), - ('ovm', ('uid',)) ] } } @@ -444,7 +632,7 @@ class GroupHandler(RACFHandler): # block to make sense. valid_blocks = { 'create': [], - 'update': ['general', 'group', 'dfp', 'omvs', 'ovm'] + 'update': ['general', 'group', 'dfp', 'omvs'] } validations = [ @@ -454,9 +642,6 @@ class GroupHandler(RACFHandler): (('dfp', 'management_class'), 'length', ((0, 8),)), (('dfp', 'storage_class'), 'length', ((0, 8),)), (('omvs', 'custom_uid'), 'range', (0, 2_147_483_647, 0)), - (('ovm', 'root'), 'length', ((0, 1023),)), - (('ovm', 'home'), 'length', ((0, 1023),)), - (('ovm', 'uid'), 'range', (0, 2_147_483_647, -1)) ] def execute_operation(self): @@ -501,8 +686,8 @@ def _create_group(self): cmd = f'{cmd} {self._make_dfp_substring()}'.strip() cmd = f'{cmd} {self._make_group_string()}'.strip() - # OMVS and OVM blocks won't use the string methods since a group only uses one option - # from both blocks. + # The OMVS block won't use the string methods since a group only uses one option + # from it. omvs = self.params.get('omvs') if omvs is not None: if omvs.get('uid') == 'auto': @@ -513,10 +698,6 @@ def _create_group(self): cmd = f'{cmd}SHARED' cmd = f'{cmd})' - ovm = self.params.get('ovm') - if ovm is not None and ovm.get('uid') != -1: - cmd = f"{cmd} OVM(GID({ovm['uid']}))" - rc, stdout, stderr = self.module.run_command(f""" tsocmd "{cmd}" """) if rc == 0: @@ -538,8 +719,8 @@ def _update_group(self): cmd = f'{cmd} {self._make_dfp_substring()}'.strip() cmd = f'{cmd} {self._make_group_string()}'.strip() - # OMVS and OVM blocks won't use the string options since a group only uses one option - # from both blocks. + # The OMVS block won't use the string options since a group only uses one option + # from it. omvs = self.params.get('omvs') if omvs is not None: if omvs.get('delete'): @@ -555,16 +736,6 @@ def _update_group(self): else: cmd = f'{cmd} OMVS(NOGID)' - ovm = self.params.get('ovm') - if ovm is not None: - if ovm.get('delete'): - cmd = f'{cmd} NOOVM' - - if ovm.get('uid') != -1: - cmd = f"{cmd} OVM(GID({ovm['uid']}))" - else: - cmd = f"{cmd} OVM(NOGID)" - rc, stdout, stderr = self.module.run_command(f""" tsocmd "{cmd}" """) if rc == 0: @@ -754,33 +925,6 @@ def run_module(): 'required': False } } - }, - 'ovm': { - 'type': 'dict', - 'required': False, - 'mutually_exclusive': [ - ('root', 'delete'), - ('home', 'delete'), - ('uid', 'delete'), - ], - 'options': { - 'root': { - 'type': 'str', - 'required': False - }, - 'home': { - 'type': 'str', - 'required': False - }, - 'uid': { - 'type': 'int', - 'required': False - }, - 'delete': { - 'type': 'bool', - 'required': False - } - } } }, supports_check_mode=True @@ -835,16 +979,6 @@ def run_module(): 'uid': {'arg_type': 'str', 'required': False}, 'custom_uid': {'arg_type': 'int', 'required': False} } - }, - 'ovm': { - 'arg_type': 'dict', - 'required': False, - 'options': { - 'root': {'arg_type': 'str', 'required': False}, - 'home': {'arg_type': 'str', 'required': False}, - 'uid': {'arg_type': 'int', 'required': False}, - 'delete': {'arg_type': 'bool', 'required': False} - } } } @@ -879,6 +1013,7 @@ def run_module(): if result['rc'] == 0: result['changed'] = True else: + result['msg'] = 'An error ocurred while executing the RACF command.' module.fail_json(**result) module.exit_json(**result) From c5c609c6988b9c13c59da269e4b08b7663df0945 Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Fri, 29 Aug 2025 10:33:23 -0600 Subject: [PATCH 07/30] Add group deletion --- plugins/modules/zos_user.py | 51 ++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index bf7e8036a6..500edb5ea0 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -620,19 +620,28 @@ class GroupHandler(RACFHandler): 'flat': [ ('omvs', ('uid', 'custom_uid')), ] - } + }, + 'delete': {}, + 'purge': {}, + 'list': {} } should_remove_empty_strings = { 'create': True, - 'update': False + 'update': False, + 'delete': False, + 'purge': False, + 'list': False } # All empty lists indicate an operation that doesn't require any other # block to make sense. valid_blocks = { 'create': [], - 'update': ['general', 'group', 'dfp', 'omvs'] + 'update': ['general', 'group', 'dfp', 'omvs'], + 'delete': [], + 'purge': [], + 'list': [] } validations = [ @@ -644,6 +653,24 @@ class GroupHandler(RACFHandler): (('omvs', 'custom_uid'), 'range', (0, 2_147_483_647, 0)), ] + def __init__(self, module, module_params): + """Initializes a new handler with all the context needed to execute RACF + commands. + + Parameters + ---------- + module: AnsibleModule + Object with all the task's context. + module_params: dict + Module options specified in the task. + """ + super().__init__(module, module_params) + + # Removing all block params since these operations only need the + # name. + if self.operation in ['delete', 'purge', 'list']: + self.params = {} + def execute_operation(self): """Given the operation and scope, it executes a RACF command. @@ -664,6 +691,8 @@ def execute_operation(self): rc, stdout, stderr, cmd = self._create_group() if self.operation == 'update': rc, stdout, stderr, cmd = self._update_group() + if self.operation == 'delete': + rc, stdout, stderr, cmd = self._delete_group() self.cmd = cmd # Getting the base dictionary. @@ -744,6 +773,22 @@ def _update_group(self): return rc, stdout, stderr, cmd + def _delete_group(self): + """Builds and execute a DELGROUP command. + + Returns + ------- + tuple: RC, stdout and stderr from the RACF command, and the DELGROUP command. + """ + cmd = f'DELGROUP ({self.name})' + rc, stdout, stderr = self.module.run_command(f""" tsocmd "{cmd}" """) + + if rc == 0: + self.num_entities_modified = 1 + self.entities_modified = [self.name] + + return rc, stdout, stderr, cmd + def _make_group_string(self): """Creates a string that defines the GROUP parameters of a profile. From fe6bdf4225af95cea3685911992f1d376c10ecdb Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Fri, 29 Aug 2025 13:47:25 -0600 Subject: [PATCH 08/30] Add user handler and language options --- plugins/modules/zos_user.py | 195 ++++++++++++++++++++++++++++++++---- 1 file changed, 178 insertions(+), 17 deletions(-) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index 500edb5ea0..f824800426 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -614,6 +614,7 @@ class GroupHandler(RACFHandler): ], 'flat': [ ('omvs', ('uid', 'custom_uid')), + ('dfp', ('data_app_id', 'data_class', 'storage_class', 'management_class')) ] }, 'update': { @@ -674,25 +675,16 @@ def __init__(self, module, module_params): def execute_operation(self): """Given the operation and scope, it executes a RACF command. - Parameters - ---------- - operation: str - One of 'create', 'list', 'update', 'delete', 'purge', 'connect' - or 'remove'. - scope: str - One of 'user' or 'group'. - Returns ------- tuple: Return code, standard output and standard error from the command. """ - if self.scope == 'group': - if self.operation == 'create': - rc, stdout, stderr, cmd = self._create_group() - if self.operation == 'update': - rc, stdout, stderr, cmd = self._update_group() - if self.operation == 'delete': - rc, stdout, stderr, cmd = self._delete_group() + if self.operation == 'create': + rc, stdout, stderr, cmd = self._create_group() + if self.operation == 'update': + rc, stdout, stderr, cmd = self._update_group() + if self.operation == 'delete': + rc, stdout, stderr, cmd = self._delete_group() self.cmd = cmd # Getting the base dictionary. @@ -811,6 +803,154 @@ def _make_group_string(self): return cmd +class UserHandler(RACFHandler): + """Subclass containing all information needed to clean, validate and execute + RACF commands affecting user profiles. + """ + filters = { + 'create': { + 'nested': [ + ('general', 'custom_fields', ('add',)) + ], + 'flat': [ + ('dfp', ('data_app_id', 'data_class', 'storage_class', 'management_class')), + ('language', ('primary', 'secondary')) + ] + }, + 'update': {}, + 'delete': {}, + 'purge': {}, + 'list': {} + } + + should_remove_empty_strings = { + 'create': True, + 'update': False, + 'delete': False, + 'purge': False, + 'list': False + } + + # All empty lists indicate an operation that doesn't require any other + # block to make sense. + valid_blocks = { + 'create': [], + 'update': ['general', 'group', 'dfp', 'language', 'omvs'], + 'delete': [], + 'purge': [], + 'list': [] + } + + validations = [ + (('general', 'installation_data'), 'length', ((0, 255),)), + (('dfp', 'data_app_id'), 'length', ((0, 8),)), + (('dfp', 'data_class'), 'length', ((0, 8),)), + (('dfp', 'management_class'), 'length', ((0, 8),)), + (('dfp', 'storage_class'), 'length', ((0, 8),)), + (('language', 'primary'), 'format', ('[a-zA-Z]{3}', '[a-zA-Z]{0, 24}')), + (('language', 'secondary'), 'format', ('[a-zA-Z]{3}', '[a-zA-Z]{0, 24}')), + (('omvs', 'custom_uid'), 'range', (0, 2_147_483_647, 0)) + ] + + def __init__(self, module, module_params): + """Initializes a new handler with all the context needed to execute RACF + commands. + + Parameters + ---------- + module: AnsibleModule + Object with all the task's context. + module_params: dict + Module options specified in the task. + """ + super().__init__(module, module_params) + + # Removing all block params since these operations only need the + # name. + if self.operation in ['delete', 'purge', 'list']: + self.params = {} + + def execute_operation(self): + """Given the operation and scope, it executes a RACF command. + + Returns + ------- + tuple: Return code, standard output and standard error from the command. + """ + if self.operation == 'create': + rc, stdout, stderr, cmd = self._create_user() + if self.operation == 'update': + rc, stdout, stderr, cmd = self._update_user() + if self.operation == 'delete': + rc, stdout, stderr, cmd = self._delete_user() + + self.cmd = cmd + # Getting the base dictionary. + result = super().execute_operation() + result['rc'] = rc + result['stdout'] = stdout + result['stderr'] = stderr + return result + + def _create_user(self): + """Builds and execute an ADDUSER command. + + Returns + ------- + tuple: RC, stdout and stderr from the RACF command, and the ADDUSER command. + """ + cmd = f'ADDUSER ({self.name})' + + # TODO: add language + cmd = f'{cmd} {self._make_general_string()}'.strip() + cmd = f'{cmd} {self._make_dfp_substring()}'.strip() + + # The OMVS block won't use the string methods since a group only uses one option + # from it. + omvs = self.params.get('omvs') + if omvs is not None: + if omvs.get('uid') == 'auto': + cmd = f'{cmd} OMVS(AUTOGID)' + elif omvs.get('uid') != 'none': + cmd = f"{cmd} OMVS(GID({omvs['custom_uid']})" + if omvs['uid'] == 'shared': + cmd = f'{cmd}SHARED' + cmd = f'{cmd})' + + rc, stdout, stderr = self.module.run_command(f""" tsocmd "{cmd}" """) + + if rc == 0: + self.num_entities_modified = 1 + self.entities_modified = [self.name] + + return rc, stdout, stderr, cmd + + def _update_user(self): + """Builds and execute an ALTUSER command. + + Returns + ------- + tuple: RC, stdout and stderr from the RACF command, and the ALTUSER command. + """ + pass + + def _delete_user(self): + """Builds and execute a DELUSER command. + + Returns + ------- + tuple: RC, stdout and stderr from the RACF command, and the DELUSER command. + """ + cmd = f'DELUSER ({self.name})' + rc, stdout, stderr = self.module.run_command(f""" tsocmd "{cmd}" """) + + if rc == 0: + self.num_entities_modified = 1 + self.entities_modified = [self.name] + + return rc, stdout, stderr, cmd + + def get_racf_handler(module, module_params): """Returns the correct handler needed for the scope and operation given in a task. @@ -828,8 +968,7 @@ def get_racf_handler(module, module_params): if module_params['scope'] == 'group': return GroupHandler(module, module_params) elif module_params['scope'] == 'user': - pass - # return UserHandler(module, module_params) + return UserHandler(module, module_params) def run_module(): @@ -944,6 +1083,28 @@ def run_module(): } } }, + 'language': { + 'type': 'dict', + 'required': False, + 'mutually_exclusive': [ + ('primary', 'delete'), + ('secondary', 'delete') + ], + 'options': { + 'primary': { + 'type': 'str', + 'required': False + }, + 'secondary': { + 'type': 'str', + 'required': False + }, + 'delete': { + 'type': 'bool', + 'required': False + } + } + }, 'omvs': { 'type': 'dict', 'required': False, From 776f1184bf7bafafba03020fda0ef0a50898424d Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Mon, 1 Sep 2025 13:08:23 -0600 Subject: [PATCH 09/30] Add the rest of options to the OMVS block --- plugins/modules/zos_user.py | 86 +++++++++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 4 deletions(-) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index f824800426..2201773574 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -814,7 +814,8 @@ class UserHandler(RACFHandler): ], 'flat': [ ('dfp', ('data_app_id', 'data_class', 'storage_class', 'management_class')), - ('language', ('primary', 'secondary')) + ('language', ('primary', 'secondary')), + ('omvs', ('uid', 'custom_uid', 'home', 'program', 'nonshared_size', 'shared_size', 'addr_space_size', 'map_size', 'max_procs', 'max_threads', 'max_cpu_time', 'max_files')) ] }, 'update': {}, @@ -849,7 +850,15 @@ class UserHandler(RACFHandler): (('dfp', 'storage_class'), 'length', ((0, 8),)), (('language', 'primary'), 'format', ('[a-zA-Z]{3}', '[a-zA-Z]{0, 24}')), (('language', 'secondary'), 'format', ('[a-zA-Z]{3}', '[a-zA-Z]{0, 24}')), - (('omvs', 'custom_uid'), 'range', (0, 2_147_483_647, 0)) + (('omvs', 'custom_uid'), 'range', (0, 2_147_483_647, 0)), + (('omvs', 'home'), 'length', ((0, 1023),)), + (('omvs', 'program'), 'length', ((0, 1023),)), + (('omvs', 'addr_space_size'), 'range', (10_485_760, 2_147_483_647, 0)), + (('omvs', 'map_size'), 'range', (1, 16_777_216, 0)), + (('omvs', 'max_procs'), 'range', (3, 32_767, 0)), + (('omvs', 'max_threads'), 'range', (0, 100_000, -1)), + (('omvs', 'max_cpu_time'), 'range', (7, 2_147_483_647, 0)), + (('omvs', 'max_files'), 'range', (3, 524_287, 0)) ] def __init__(self, module, module_params): @@ -1109,8 +1118,14 @@ def run_module(): 'type': 'dict', 'required': False, 'mutually_exclusive': [ + ('addr_space_size', 'delete'), ('uid', 'delete'), - ('custom_uid', 'delete') + ('custom_uid', 'delete'), + ('max_cpu_time', 'delete'), + ('max_files', 'delete'), + ('home', 'delete'), + ('nonshared_size', 'delete'), + ('map_size', 'delete'), ], 'required_if': [ ('uid', 'custom', ('custom_uid',)), @@ -1126,6 +1141,48 @@ def run_module(): 'type': 'int', 'required': False }, + 'home': { + 'type': 'str', + 'required': False + }, + 'program': { + 'type': 'int', + 'required': False + }, + # TODO: add validation for this one + 'nonshared_size': { + 'type': 'str', + 'required': False + }, + # TODO: add validation for this one + 'shared_size': { + 'type': 'str', + 'required': False + }, + 'addr_space_size': { + 'type': 'int', + 'required': False + }, + 'map_size': { + 'type': 'int', + 'required': False + }, + 'max_procs': { + 'type': 'int', + 'required': False + }, + 'max_threads': { + 'type': 'int', + 'required': False + }, + 'max_cpu_time': { + 'type': 'int', + 'required': False + }, + 'max_files': { + 'type': 'int', + 'required': False + }, 'delete': { 'type': 'bool', 'required': False @@ -1136,6 +1193,7 @@ def run_module(): supports_check_mode=True ) + # TODO: update this args_def = { 'name': {'arg_type': 'str', 'required': True, 'aliases': ['src']}, 'operation': {'arg_type': 'str', 'required': True}, @@ -1178,12 +1236,32 @@ def run_module(): 'delete': {'arg_type': 'bool', 'required': False} } }, + 'language': { + 'arg_type': 'dict', + 'required': False, + 'options': { + 'primary': {'arg_type': 'str', 'required': False}, + 'secondary': {'arg_type': 'str', 'required': False}, + 'delete': {'arg_type': 'bool', 'required': False} + } + }, 'omvs': { 'arg_type': 'dict', 'required': False, 'options': { 'uid': {'arg_type': 'str', 'required': False}, - 'custom_uid': {'arg_type': 'int', 'required': False} + 'custom_uid': {'arg_type': 'int', 'required': False}, + 'home': {'arg_type': 'path', 'required': False}, + 'program': {'arg_type': 'path', 'required': False}, + 'nonshared_size': {'arg_type': 'str', 'required': False}, + 'shared_size': {'arg_type': 'str', 'required': False}, + 'addr_space_size': {'arg_type': 'int', 'required': False}, + 'map_size': {'arg_type': 'int', 'required': False}, + 'max_procs': {'arg_type': 'int', 'required': False}, + 'max_threads': {'arg_type': 'int', 'required': False}, + 'max_cpu_time': {'arg_type': 'int', 'required': False}, + 'max_files': {'arg_type': 'int', 'required': False}, + 'delete': {'arg_type': 'bool', 'required': False} } } } From 5f8e669cce01b999f5c7e89941b225423ac99ff9 Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Mon, 1 Sep 2025 15:29:19 -0600 Subject: [PATCH 10/30] Add TSO options --- plugins/modules/zos_user.py | 125 ++++++++++++++++++++++++++++++++++-- 1 file changed, 119 insertions(+), 6 deletions(-) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index 2201773574..ec49c6a76a 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -815,7 +815,8 @@ class UserHandler(RACFHandler): 'flat': [ ('dfp', ('data_app_id', 'data_class', 'storage_class', 'management_class')), ('language', ('primary', 'secondary')), - ('omvs', ('uid', 'custom_uid', 'home', 'program', 'nonshared_size', 'shared_size', 'addr_space_size', 'map_size', 'max_procs', 'max_threads', 'max_cpu_time', 'max_files')) + ('omvs', ('uid', 'custom_uid', 'home', 'program', 'nonshared_size', 'shared_size', 'addr_space_size', 'map_size', 'max_procs', 'max_threads', 'max_cpu_time', 'max_files')), + ('tso', ('account_num', 'logon_cmd', 'logon_proc', 'dest_id', 'hold_class', 'job_class', 'msg_class', 'sysout_class', 'region_size', 'max_region_size', 'security_label', 'unit_name', 'user_data')) ] }, 'update': {}, @@ -858,7 +859,19 @@ class UserHandler(RACFHandler): (('omvs', 'max_procs'), 'range', (3, 32_767, 0)), (('omvs', 'max_threads'), 'range', (0, 100_000, -1)), (('omvs', 'max_cpu_time'), 'range', (7, 2_147_483_647, 0)), - (('omvs', 'max_files'), 'range', (3, 524_287, 0)) + (('omvs', 'max_files'), 'range', (3, 524_287, 0)), + (('tso', 'account_num'), 'length', ((0, 39),)), + (('tso', 'logon_cmd'), 'length', ((0, 80),)), + (('tso', 'logon_proc'), 'length', ((0, 8),)), + (('tso', 'dest_id'), 'length', ((0, 7),)), + (('tso', 'hold_class'), 'length', ((0, 1),)), + (('tso', 'job_class'), 'length', ((0, 1),)), + (('tso', 'msg_class'), 'length', ((0, 1),)), + (('tso', 'sysout_class'), 'length', ((0, 1),)), + (('tso', 'region_size'), 'range', (0, 2_096_128, -1)), + (('tso', 'max_region_size'), 'range', (0, 2_096_128, -1)), + (('tso', 'unit_name'), 'length', ((0, 8),)), + (('tso', 'user_data'), 'format', ('[^\s*$]|[0-9a-zA-Z]{4}',)), ] def __init__(self, module, module_params): @@ -1118,14 +1131,18 @@ def run_module(): 'type': 'dict', 'required': False, 'mutually_exclusive': [ - ('addr_space_size', 'delete'), ('uid', 'delete'), ('custom_uid', 'delete'), - ('max_cpu_time', 'delete'), - ('max_files', 'delete'), ('home', 'delete'), + ('program', 'delete'), ('nonshared_size', 'delete'), + ('shared_size', 'delete'), + ('addr_space_size', 'delete'), ('map_size', 'delete'), + ('max_procs', 'delete'), + ('max_threads', 'delete'), + ('max_cpu_time', 'delete'), + ('max_files', 'delete') ], 'required_if': [ ('uid', 'custom', ('custom_uid',)), @@ -1188,12 +1205,88 @@ def run_module(): 'required': False } } + }, + 'tso': { + 'type': 'dict', + 'required': False, + 'mutually_exclusive': [ + ('account_num', 'delete'), + ('logon_cmd', 'delete'), + ('logon_proc', 'delete'), + ('dest_id', 'delete'), + ('hold_class', 'delete'), + ('job_class', 'delete'), + ('msg_class', 'delete'), + ('sysout_class', 'delete'), + ('region_size', 'delete'), + ('max_region_size', 'delete'), + ('security_label', 'delete'), + ('unit_name', 'delete'), + ('user_data', 'delete'), + ], + 'options': { + 'account_num': { + 'type': 'str', + 'required': False + }, + 'logon_cmd': { + 'type': 'str', + 'required': False + }, + 'logon_proc': { + 'type': 'str', + 'required': False + }, + 'dest_id': { + 'type': 'str', + 'required': False + }, + 'hold_class': { + 'type': 'str', + 'required': False + }, + 'job_class': { + 'type': 'str', + 'required': False + }, + 'msg_class': { + 'type': 'str', + 'required': False + }, + 'sysout_class': { + 'type': 'str', + 'required': False + }, + 'region_size': { + 'type': 'int', + 'required': False + }, + 'max_region_size': { + 'type': 'int', + 'required': False + }, + 'security_label': { + 'type': 'str', + 'required': False + }, + 'unit_name': { + 'type': 'str', + 'required': False + }, + 'user_data': { + 'type': 'str', + 'required': False + }, + 'delete': { + 'type': 'bool', + 'required': False + } + } } }, supports_check_mode=True ) - # TODO: update this args_def = { 'name': {'arg_type': 'str', 'required': True, 'aliases': ['src']}, 'operation': {'arg_type': 'str', 'required': True}, @@ -1263,6 +1356,26 @@ def run_module(): 'max_files': {'arg_type': 'int', 'required': False}, 'delete': {'arg_type': 'bool', 'required': False} } + }, + 'tso': { + 'arg_type': 'dict', + 'required': False, + 'options': { + 'account_num': {'arg_type': 'str', 'required': False}, + 'logon_cmd': {'arg_type': 'str', 'required': False}, + 'logon_proc': {'arg_type': 'str', 'required': False}, + 'dest_id': {'arg_type': 'str', 'required': False}, + 'hold_class': {'arg_type': 'str', 'required': False}, + 'job_class': {'arg_type': 'str', 'required': False}, + 'msg_class': {'arg_type': 'str', 'required': False}, + 'sysout_class': {'arg_type': 'str', 'required': False}, + 'region_size': {'arg_type': 'int', 'required': False}, + 'max_region_size': {'arg_type': 'int', 'required': False}, + 'security_label': {'arg_type': 'str', 'required': False}, + 'unit_name': {'arg_type': 'str', 'required': False}, + 'user_data': {'arg_type': 'str', 'required': False}, + 'delete': {'arg_type': 'bool', 'required': False} + } } } From 0265c3f34ffbf6497cc38583fb118f4d35a529f4 Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Wed, 10 Sep 2025 13:18:39 -0600 Subject: [PATCH 11/30] Add access and operator options --- plugins/modules/zos_user.py | 359 +++++++++++++++++++++++++++++++++++- 1 file changed, 356 insertions(+), 3 deletions(-) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index ec49c6a76a..0815ea5148 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -816,7 +816,9 @@ class UserHandler(RACFHandler): ('dfp', ('data_app_id', 'data_class', 'storage_class', 'management_class')), ('language', ('primary', 'secondary')), ('omvs', ('uid', 'custom_uid', 'home', 'program', 'nonshared_size', 'shared_size', 'addr_space_size', 'map_size', 'max_procs', 'max_threads', 'max_cpu_time', 'max_files')), - ('tso', ('account_num', 'logon_cmd', 'logon_proc', 'dest_id', 'hold_class', 'job_class', 'msg_class', 'sysout_class', 'region_size', 'max_region_size', 'security_label', 'unit_name', 'user_data')) + ('tso', ('account_num', 'logon_cmd', 'logon_proc', 'dest_id', 'hold_class', 'job_class', 'msg_class', 'sysout_class', 'region_size', 'max_region_size', 'security_label', 'unit_name', 'user_data')), + ('access', ('authority', 'universal_access', 'group_name', 'group_account', 'group_operations', 'default_group', 'clauth', 'auditor', 'roaudit', 'adsp_attribute', 'category', 'operator_card', 'maintenance_access', 'restricted', 'security_label', 'security_level', 'special')), + ('operator', ('alt_group', 'authority', 'cmd_system', 'search_key', 'migration_id', 'display', 'msg_level', 'msg_format', 'msg_storage', 'msg_scope', 'automated_msgs', 'del_msgs', 'hardcopy_msgs', 'internal_msgs', 'routing_msgs', 'undelivered_msgs', 'unknown_msgs', 'responses')) ] }, 'update': {}, @@ -837,7 +839,7 @@ class UserHandler(RACFHandler): # block to make sense. valid_blocks = { 'create': [], - 'update': ['general', 'group', 'dfp', 'language', 'omvs'], + 'update': ['general', 'dfp', 'language', 'omvs', 'tso', 'access', 'operator'], 'delete': [], 'purge': [], 'list': [] @@ -872,6 +874,10 @@ class UserHandler(RACFHandler): (('tso', 'max_region_size'), 'range', (0, 2_096_128, -1)), (('tso', 'unit_name'), 'length', ((0, 8),)), (('tso', 'user_data'), 'format', ('[^\s*$]|[0-9a-zA-Z]{4}',)), + (('operator', 'alt_group'), 'length', ((0, 8),)), + (('operator', 'cmd_system'), 'length', ((0, 8),)), + (('operator', 'search_key'), 'length', ((0, 8),)), + (('operator', 'msg_storage'), 'range', (1, 2000, 0)), ] def __init__(self, module, module_params): @@ -1222,7 +1228,7 @@ def run_module(): ('max_region_size', 'delete'), ('security_label', 'delete'), ('unit_name', 'delete'), - ('user_data', 'delete'), + ('user_data', 'delete') ], 'options': { 'account_num': { @@ -1282,6 +1288,282 @@ def run_module(): 'required': False } } + }, + 'access': { + 'type': 'dict', + 'required': False, + 'mutually_exclusive': [ + ('authority', 'delete'), + ('universal_access', 'delete'), + ('group_name', 'delete'), + ('group_account', 'delete'), + ('group_operations', 'delete'), + ('default_group', 'delete'), + ('clauth', 'delete'), + ('auditor', 'delete'), + ('roaudit', 'delete'), + ('adsp_attribute', 'delete'), + ('category', 'delete'), + ('operator_card', 'delete'), + ('maintenance_access', 'delete'), + ('restricted', 'delete'), + ('security_label', 'delete'), + ('security_level', 'delete'), + ('special', 'delete') + ], + 'options': { + 'authority': { + 'type': 'str', + 'required': False, + 'choices': ['use', 'create', 'connect', 'join'] + }, + 'universal_access': { + 'type': 'str', + 'required': False, + 'choices': ['alter', 'control', 'update', 'read', 'none'] + }, + 'group_name': { + 'type': 'str', + 'required': False + }, + 'group_account': { + 'type': 'bool', + 'required': False, + 'default': False + }, + 'group_operations': { + 'type': 'bool', + 'required': False, + 'default': False + }, + 'default_group': { + 'type': 'str', + 'required': False + }, + 'clauth': { + 'type': 'dict', + 'required': False, + 'mutually_exclusive': [ + ('add', 'delete') + ], + 'options': { + 'add': { + 'type': 'list', + 'elements': 'str', + 'required': False + }, + 'delete': { + 'type': 'list', + 'elements': 'str', + 'required': False + }, + } + }, + 'auditor': { + 'type': 'bool', + 'required': False, + 'default': False + }, + 'roaudit': { + 'type': 'bool', + 'required': False, + 'default': False + }, + 'adsp_attribute': { + 'type': 'bool', + 'required': False, + 'default': False + }, + 'category': { + 'type': 'dict', + 'required': False, + 'mutually_exclusive': [ + ('add', 'delete') + ], + 'options': { + 'add': { + 'type': 'list', + 'elements': 'str', + 'required': False + }, + 'delete': { + 'type': 'list', + 'elements': 'str', + 'required': False + }, + } + }, + 'operator_card': { + 'type': 'bool', + 'required': False, + 'default': False + }, + 'maintenance_access': { + 'type': 'bool', + 'required': False, + 'default': False + }, + 'restricted': { + 'type': 'bool', + 'required': False, + 'default': False + }, + 'security_label': { + 'type': 'str', + 'required': False + }, + 'security_level': { + 'type': 'str', + 'required': False + }, + 'special': { + 'type': 'bool', + 'required': False, + 'default': False + }, + 'delete': { + 'type': 'bool', + 'required': False + } + } + }, + 'operator': { + 'type': 'dict', + 'required': False, + 'mutually_exclusive': [ + ('alt_group', 'delete'), + ('authority', 'delete'), + ('cmd_system', 'delete'), + ('search_key', 'delete'), + ('migration_id', 'delete'), + ('display', 'delete'), + ('msg_level', 'delete'), + ('msg_format', 'delete'), + ('msg_storage', 'delete'), + ('msg_scope', 'delete'), + ('automated_msgs', 'delete'), + ('del_msgs', 'delete'), + ('hardcopy_msgs', 'delete'), + ('internal_msgs', 'delete'), + ('routing_msgs', 'delete'), + ('undelivered_msgs', 'delete'), + ('unknown_msgs', 'delete'), + ('responses', 'delete') + ], + 'options': { + 'alt_group': { + 'type': 'str', + 'required': False + }, + 'authority': { + 'type': 'str', + 'required': False, + 'choices': ['master', 'all', 'info', 'cons', 'io', 'sys', 'delete'] + }, + 'cmd_system': { + 'type': 'str', + 'required': False + }, + 'search_key': { + 'type': 'str', + 'required': False + }, + 'migration_id': { + 'type': 'bool', + 'required': False, + 'default': False + }, + # TODO: allow multiple choices + # TODO: default should be ['jobnames', 'sess'] + 'display': { + 'type': 'str', + 'required': False, + 'choices': ['jobnames', 'jobnamest', 'sess', 'sesst', 'status', 'delete'] + }, + 'msg_level': { + 'type': 'str', + 'required': False, + 'choices': ['nb', 'all', 'r', 'i', 'ce', 'e', 'in', 'delete'] + }, + 'msg_format': { + 'type': 'str', + 'required': False, + 'choices': ['j', 'm', 's', 't', 'x', 'delete'] + }, + 'msg_storage': { + 'type': 'int', + 'required': False + }, + 'msg_scope': { + 'type': 'dict', + 'required': False, + 'mutually_exclusive': [ + ('add', 'remove'), + ('add', 'delete'), + ('remove', 'delete'), + ], + 'options': { + 'add': { + 'type': 'list', + 'elements': 'str', + 'required': False + }, + 'remove': { + 'type': 'list', + 'elements': 'str', + 'required': False + }, + 'delete': { + 'type': 'bool', + 'required': False + } + } + }, + 'automated_msgs': { + 'type': 'bool', + 'required': False, + 'default': False + }, + 'del_msgs': { + 'type': 'str', + 'required': False, + 'choices': ['normal', 'all', 'none', 'delete'] + }, + 'hardcopy_msgs': { + 'type': 'bool', + 'required': False, + 'default': False + }, + 'internal_msgs': { + 'type': 'bool', + 'required': False, + 'default': False + }, + 'routing_msgs': { + 'type': 'list', + 'required': False, + 'elements': 'str' + }, + 'undelivered_msgs': { + 'type': 'bool', + 'required': False, + 'default': False + }, + 'unknown_msgs': { + 'type': 'bool', + 'required': False, + 'default': False + }, + 'responses': { + 'type': 'bool', + 'required': False, + 'default': True + }, + 'delete': { + 'type': 'bool', + 'required': False + } + } } }, supports_check_mode=True @@ -1376,6 +1658,77 @@ def run_module(): 'user_data': {'arg_type': 'str', 'required': False}, 'delete': {'arg_type': 'bool', 'required': False} } + }, + 'access': { + 'arg_type': 'dict', + 'required': False, + 'options': { + 'authority': {'arg_type': 'str', 'required': False}, + 'universal_access': {'arg_type': 'str', 'required': False}, + 'group_name': {'arg_type': 'str', 'required': False}, + 'group_account': {'arg_type': 'bool', 'required': False}, + 'group_operations': {'arg_type': 'bool', 'required': False}, + 'default_group': {'arg_type': 'str', 'required': False}, + 'clauth': { + 'arg_type': 'dict', + 'required': False, + 'options': { + 'add': {'arg_type': 'list', 'elements': 'str', 'required': False}, + 'delete': {'arg_type': 'list', 'elements': 'str', 'required': False} + } + }, + 'auditor': {'arg_type': 'bool', 'required': False}, + 'roaudit': {'arg_type': 'bool', 'required': False}, + 'adsp_attribute': {'arg_type': 'bool', 'required': False}, + 'category': { + 'arg_type': 'dict', + 'required': False, + 'options': { + 'add': {'arg_type': 'list', 'elements': 'str', 'required': False}, + 'delete': {'arg_type': 'list', 'elements': 'str', 'required': False} + } + }, + 'operator_card': {'arg_type': 'bool', 'required': False}, + 'maintenance_access': {'arg_type': 'bool', 'required': False}, + 'restricted': {'arg_type': 'bool', 'required': False}, + 'security_label': {'arg_type': 'str', 'required': False}, + 'security_level': {'arg_type': 'str', 'required': False}, + 'special': {'arg_type': 'bool', 'required': False}, + 'delete': {'arg_type': 'bool', 'required': False} + } + }, + 'operator': { + 'arg_type': 'dict', + 'required': False, + 'options': { + 'alt_group': {'arg_type': 'str', 'required': False}, + 'authority': {'arg_type': 'str', 'required': False}, + 'cmd_system': {'arg_type': 'str', 'required': False}, + 'search_key': {'arg_type': 'str', 'required': False}, + 'migration_id': {'arg_type': 'bool', 'required': False}, + 'display': {'arg_type': 'str', 'required': False}, + 'msg_level': {'arg_type': 'str', 'required': False}, + 'msg_format': {'arg_type': 'str', 'required': False}, + 'msg_storage': {'arg_type': 'int', 'required': False}, + 'msg_scope': { + 'arg_type': 'dict', + 'required': False, + 'options': { + 'add': {'arg_type': 'list', 'elements': 'str', 'required': False}, + 'remove': {'arg_type': 'list', 'elements': 'str', 'required': False}, + 'delete': {'arg_type': 'bool', 'required': False} + } + }, + 'automated_msgs': {'arg_type': 'bool', 'required': False}, + 'del_msgs': {'arg_type': 'str', 'required': False}, + 'hardcopy_msgs': {'arg_type': 'bool', 'required': False}, + 'internal_msgs': {'arg_type': 'bool', 'required': False}, + 'routing_msgs': {'arg_type': 'list', 'elements': 'str', 'required': False}, + 'undelivered_msgs': {'arg_type': 'bool', 'required': False}, + 'unknown_msgs': {'arg_type': 'bool', 'required': False}, + 'responses': {'arg_type': 'bool', 'required': False}, + 'delete': {'arg_type': 'bool', 'required': False} + } } } From f0a9a182cfae8bfccf55b4ad07279643ba3d9de2 Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Wed, 17 Sep 2025 13:00:54 -0600 Subject: [PATCH 12/30] Add rest of the options --- plugins/modules/zos_user.py | 58 +++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index 0815ea5148..4cf074a7b4 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -818,7 +818,8 @@ class UserHandler(RACFHandler): ('omvs', ('uid', 'custom_uid', 'home', 'program', 'nonshared_size', 'shared_size', 'addr_space_size', 'map_size', 'max_procs', 'max_threads', 'max_cpu_time', 'max_files')), ('tso', ('account_num', 'logon_cmd', 'logon_proc', 'dest_id', 'hold_class', 'job_class', 'msg_class', 'sysout_class', 'region_size', 'max_region_size', 'security_label', 'unit_name', 'user_data')), ('access', ('authority', 'universal_access', 'group_name', 'group_account', 'group_operations', 'default_group', 'clauth', 'auditor', 'roaudit', 'adsp_attribute', 'category', 'operator_card', 'maintenance_access', 'restricted', 'security_label', 'security_level', 'special')), - ('operator', ('alt_group', 'authority', 'cmd_system', 'search_key', 'migration_id', 'display', 'msg_level', 'msg_format', 'msg_storage', 'msg_scope', 'automated_msgs', 'del_msgs', 'hardcopy_msgs', 'internal_msgs', 'routing_msgs', 'undelivered_msgs', 'unknown_msgs', 'responses')) + ('operator', ('alt_group', 'authority', 'cmd_system', 'search_key', 'migration_id', 'display', 'msg_level', 'msg_format', 'msg_storage', 'msg_scope', 'automated_msgs', 'del_msgs', 'hardcopy_msgs', 'internal_msgs', 'routing_msgs', 'undelivered_msgs', 'unknown_msgs', 'responses')), + ('restrictions', ('days', 'time', 'resume', 'revoke')) ] }, 'update': {}, @@ -839,7 +840,7 @@ class UserHandler(RACFHandler): # block to make sense. valid_blocks = { 'create': [], - 'update': ['general', 'dfp', 'language', 'omvs', 'tso', 'access', 'operator'], + 'update': ['general', 'dfp', 'language', 'omvs', 'tso', 'access', 'operator', 'restrictions'], 'delete': [], 'purge': [], 'list': [] @@ -878,6 +879,9 @@ class UserHandler(RACFHandler): (('operator', 'cmd_system'), 'length', ((0, 8),)), (('operator', 'search_key'), 'length', ((0, 8),)), (('operator', 'msg_storage'), 'range', (1, 2000, 0)), + (('restrictions', 'time'), 'format', ('^([01]?[0-9]|2[0-3]):[0-5][0-9]-([01]?[0-9]|2[0-3]):[0-5][0-9]$', 'anytime')), + (('restrictions', 'resume'), 'format', ('^([0]?[1-9]|1[0-2])/([0-2]?[1-9]|3[0-1])/([0-9]{4})$',)), + (('restrictions', 'revoke'), 'format', ('^([0]?[1-9]|1[0-2])/([0-2]?[1-9]|3[0-1])/([0-9]{4})$',)), ] def __init__(self, module, module_params): @@ -1564,6 +1568,44 @@ def run_module(): 'required': False } } + }, + 'restrictions': { + 'type': 'dict', + 'required': False, + 'mutually_exclusive': [ + ('resume', 'delete_resume'), + ('revoke', 'delete_revoke') + ], + 'options': { + # TODO: allow multiple + 'days': { + 'type': 'str', + 'required': False, + 'choices': ['anyday', 'weekdays', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'], + 'default': 'anyday' + }, + 'time': { + 'type': 'str', + 'required': False, + 'default': 'anytime' + }, + 'resume': { + 'type': 'str', + 'required': False + }, + 'delete_resume': { + 'type': 'bool', + 'required': False + }, + 'revoke': { + 'type': 'str', + 'required': False + }, + 'delete_revoke': { + 'type': 'bool', + 'required': False + }, + } } }, supports_check_mode=True @@ -1729,6 +1771,18 @@ def run_module(): 'responses': {'arg_type': 'bool', 'required': False}, 'delete': {'arg_type': 'bool', 'required': False} } + }, + 'restrictions': { + 'arg_type': 'dict', + 'required': False, + 'options': { + 'days': {'arg_type': 'str', 'required': False}, + 'time': {'arg_type': 'str', 'required': False}, + 'resume': {'arg_type': 'str', 'required': False}, + 'delete_resume': {'arg_type': 'bool', 'required': False}, + 'revoke': {'arg_type': 'str', 'required': False}, + 'delete_revoke': {'arg_type': 'bool', 'required': False} + } } } From cd0f48b88d04c60a9bab5b8949ada4cb3ad59b9e Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Fri, 19 Sep 2025 09:50:42 -0600 Subject: [PATCH 13/30] Add user creation and update --- plugins/modules/zos_user.py | 522 +++++++++++++++++++++++++++++++++--- 1 file changed, 482 insertions(+), 40 deletions(-) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index 4cf074a7b4..25edcb9ef5 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -575,6 +575,9 @@ def _make_dfp_substring(self): dfp = self.params.get('dfp') if dfp is not None: + if dfp.get('delete', False): + return "NODFP" + cmd = f"{cmd}DFP(" if dfp.get('data_app_id') is not None: @@ -879,9 +882,9 @@ class UserHandler(RACFHandler): (('operator', 'cmd_system'), 'length', ((0, 8),)), (('operator', 'search_key'), 'length', ((0, 8),)), (('operator', 'msg_storage'), 'range', (1, 2000, 0)), - (('restrictions', 'time'), 'format', ('^([01]?[0-9]|2[0-3]):[0-5][0-9]-([01]?[0-9]|2[0-3]):[0-5][0-9]$', 'anytime')), - (('restrictions', 'resume'), 'format', ('^([0]?[1-9]|1[0-2])/([0-2]?[1-9]|3[0-1])/([0-9]{4})$',)), - (('restrictions', 'revoke'), 'format', ('^([0]?[1-9]|1[0-2])/([0-2]?[1-9]|3[0-1])/([0-9]{4})$',)), + (('restrictions', 'time'), 'format', ('^([01]?[0-9]|2[0-3])[0-5][0-9]:([01]?[0-9]|2[0-3])[0-5][0-9]$', 'anytime')), + (('restrictions', 'resume'), 'format', ('^([0]?[1-9]|1[0-2])/([0-2]?[1-9]|3[0-1])/([0-9]{2})$',)), + (('restrictions', 'revoke'), 'format', ('^([0]?[1-9]|1[0-2])/([0-2]?[1-9]|3[0-1])/([0-9]{2})$',)), ] def __init__(self, module, module_params): @@ -933,21 +936,14 @@ def _create_user(self): """ cmd = f'ADDUSER ({self.name})' - # TODO: add language cmd = f'{cmd} {self._make_general_string()}'.strip() cmd = f'{cmd} {self._make_dfp_substring()}'.strip() - - # The OMVS block won't use the string methods since a group only uses one option - # from it. - omvs = self.params.get('omvs') - if omvs is not None: - if omvs.get('uid') == 'auto': - cmd = f'{cmd} OMVS(AUTOGID)' - elif omvs.get('uid') != 'none': - cmd = f"{cmd} OMVS(GID({omvs['custom_uid']})" - if omvs['uid'] == 'shared': - cmd = f'{cmd}SHARED' - cmd = f'{cmd})' + cmd = f'{cmd} {self._make_language_substring()}'.strip() + cmd = f'{cmd} {self._make_tso_substring()}'.strip() + cmd = f'{cmd} {self._make_omvs_substring()}'.strip() + cmd = f'{cmd} {self._make_access_substring_creation()}'.strip() + cmd = f'{cmd} {self._make_restrictions_substring()}'.strip() + cmd = f'{cmd} {self._make_operator_substring()}'.strip() rc, stdout, stderr = self.module.run_command(f""" tsocmd "{cmd}" """) @@ -964,7 +960,24 @@ def _update_user(self): ------- tuple: RC, stdout and stderr from the RACF command, and the ALTUSER command. """ - pass + cmd = f'ALTUSER ({self.name})' + + cmd = f'{cmd} {self._make_general_string()}'.strip() + cmd = f'{cmd} {self._make_dfp_substring()}'.strip() + cmd = f'{cmd} {self._make_language_substring()}'.strip() + cmd = f'{cmd} {self._make_tso_substring()}'.strip() + cmd = f'{cmd} {self._make_omvs_substring()}'.strip() + cmd = f'{cmd} {self._make_access_substring_creation()}'.strip() + cmd = f'{cmd} {self._make_restrictions_substring()}'.strip() + cmd = f'{cmd} {self._make_operator_substring()}'.strip() + + rc, stdout, stderr = self.module.run_command(f""" tsocmd "{cmd}" """) + + if rc == 0: + self.num_entities_modified = 1 + self.entities_modified = [self.name] + + return rc, stdout, stderr, cmd def _delete_user(self): """Builds and execute a DELUSER command. @@ -982,6 +995,458 @@ def _delete_user(self): return rc, stdout, stderr, cmd + def _make_language_substring(self): + """Creates a string that defines the LANGUAGE block of a user profile. + + Returns + ------- + str: LANGUAGE parameters of a RACF command. + """ + cmd = "" + language = self.params.get('language') + + if language is not None: + if language.get('delete', False): + return "NOLANGUAGE" + + cmd = f"{cmd}LANGUAGE(" + + if language.get('primary') is not None: + if language.get('primary') != "": + cmd = f"{cmd} PRIMARY({language['primary']})" + else: + cmd = f"{cmd} NOPRIMARY" + if language.get('secondary') is not None: + if language.get('secondary') != "": + cmd = f"{cmd} SECONDARY({language['secondary']})" + else: + cmd = f"{cmd} NOSECONDARY" + + cmd = f"{cmd} )" + + return cmd + + def _make_omvs_substring(self): + """Creates a string that defines the OMVS (Unix System Services) block of + a user profile. + + Returns + ------- + str: OMVS parameters of a RACF command. + """ + cmd = "" + omvs = self.params.get('omvs') + + if omvs is not None: + if omvs.get('delete', False): + return "NOOMVS" + + cmd = f"{cmd}OMVS(" + + if omvs.get('uid') == 'auto': + cmd = f'{cmd} AUTOUID' + elif omvs.get('uid') != 'none': + cmd = f"{cmd} UID({omvs['custom_uid']})" + if omvs['uid'] == 'shared': + cmd = f'{cmd}SHARED' + else: + cmd = f'{cmd} NOUID' + + if omvs.get('home') is not None: + if omvs.get('home') != "": + cmd = f"{cmd} HOME({omvs['home']})" + else: + cmd = f"{cmd} NOHOME" + if omvs.get('program') is not None: + if omvs.get('program') != "": + cmd = f"{cmd} PROGRAM({omvs['program']})" + else: + cmd = f"{cmd} NOPROGRAM" + if omvs.get('nonshared_size') is not None: + if omvs.get('nonshared_size') != "": + cmd = f"{cmd} MEMLIMIT({omvs['nonshared_size']})" + else: + cmd = f"{cmd} NOMEMLIMIT" + if omvs.get('shared_size') is not None: + if omvs.get('shared_size') != "": + cmd = f"{cmd} SHMEMMAX({omvs['shared_size']})" + else: + cmd = f"{cmd} NOSHMEMMAX" + if omvs.get('addr_space_size') is not None: + if omvs.get('addr_space_size') != 0: + cmd = f"{cmd} ASSIZEMAX({omvs['addr_space_size']})" + else: + cmd = f"{cmd} NOASSIZEMAX" + if omvs.get('map_size') is not None: + if omvs.get('map_size') != 0: + cmd = f"{cmd} MMAPAREAMAX({omvs['map_size']})" + else: + cmd = f"{cmd} NOMMAPAREAMAX" + if omvs.get('max_procs') is not None: + if omvs.get('max_procs') != 0: + cmd = f"{cmd} PROCUSERMAX({omvs['max_procs']})" + else: + cmd = f"{cmd} NOPROCUSERMAX" + if omvs.get('max_threads') is not None: + if omvs.get('max_threads') != -1: + cmd = f"{cmd} THREADSMAX({omvs['max_threads']})" + else: + cmd = f"{cmd} NOTHREADSMAX" + if omvs.get('max_cpu_time') is not None: + if omvs.get('max_cpu_time') != 0: + cmd = f"{cmd} CPUTIMEMAX({omvs['max_cpu_time']})" + else: + cmd = f"{cmd} NOCPUTIMEMAX" + if omvs.get('max_files') is not None: + if omvs.get('max_files') != 0: + cmd = f"{cmd} FILEPROCMAX({omvs['max_files']})" + else: + cmd = f"{cmd} NOFILEPROCMAX" + + cmd = f"{cmd} )" + + return cmd + + def _make_tso_substring(self): + """Creates a string that defines the TSO block of a user profile. + + Returns + ------- + str: TSO parameters of a RACF command. + """ + cmd = "" + tso = self.params.get('tso') + + if tso is not None: + if tso.get('delete', False): + return "NOTSO" + + cmd = f"{cmd}TSO(" + + if tso.get('account_num') is not None: + if tso.get('account_num') != "": + cmd = f"{cmd} ACCTNUM({tso['account_num']})" + else: + cmd = f"{cmd} NOACCTNUM" + if tso.get('logon_cmd') is not None: + if tso.get('logon_cmd') != "": + cmd = f"{cmd} COMMAND({tso['logon_cmd']})" + else: + cmd = f"{cmd} NOCOMMAND" + if tso.get('dest_id') is not None: + if tso.get('dest_id') != "": + cmd = f"{cmd} DEST({tso['dest_id']})" + else: + cmd = f"{cmd} NODEST" + if tso.get('hold_class') is not None: + if tso.get('hold_class') != "": + cmd = f"{cmd} HOLDCLASS({tso['hold_class']})" + else: + cmd = f"{cmd} NOHOLDCLASS" + if tso.get('job_class') is not None: + if tso.get('job_class') != "": + cmd = f"{cmd} JOBCLASS({tso['job_class']})" + else: + cmd = f"{cmd} NOJOBCLASS" + if tso.get('msg_class') is not None: + if tso.get('msg_class') != "": + cmd = f"{cmd} MSGCLASS({tso['msg_class']})" + else: + cmd = f"{cmd} NOMSGCLASS" + if tso.get('sysout_class') is not None: + if tso.get('sysout_class') != "": + cmd = f"{cmd} SYS({tso['sysout_class']})" + else: + cmd = f"{cmd} NOSYS" + if tso.get('region_size') is not None: + if tso.get('region_size') != -1: + cmd = f"{cmd} SIZE({tso['region_size']})" + else: + cmd = f"{cmd} NOSIZE" + if tso.get('max_region_size') is not None: + if tso.get('max_region_size') != -1: + cmd = f"{cmd} MAXSIZE({tso['max_region_size']})" + else: + cmd = f"{cmd} NOMAXSIZE" + if tso.get('logon_proc') is not None: + if tso.get('logon_proc') != "": + cmd = f"{cmd} PROC({tso['logon_proc']})" + else: + cmd = f"{cmd} NOPROC" + if tso.get('security_label') is not None: + if tso.get('security_label') != "": + cmd = f"{cmd} SECLABEL({tso['security_label']})" + else: + cmd = f"{cmd} NOSECLABEL" + if tso.get('unit_name') is not None: + if tso.get('unit_name') != "": + cmd = f"{cmd} UNIT({tso['unit_name']})" + else: + cmd = f"{cmd} NOUNIT" + if tso.get('user_data') is not None: + if tso.get('user_data') != "": + cmd = f"{cmd} USERDATA({tso['user_data']})" + else: + cmd = f"{cmd} NOUSERDATA" + + cmd = f"{cmd} )" + + return cmd + + def _make_operator_substring(self): + """Creates a string that defines the OPERATOR block of a user profile. + + Returns + ------- + str: OPERATOR parameters of a RACF command. + """ + cmd = "" + operator = self.params.get('operator') + + if operator is not None: + if operator.get('delete', False): + return "NOOPERPARM" + + cmd = f"{cmd}OPERPARM(" + + if operator.get('alt_group') is not None: + if operator.get('alt_group') != "": + cmd = f"{cmd} ALTGRP({operator['account_num']})" + else: + cmd = f"{cmd} NOALTGRP" + if operator.get('authority') is not None: + if operator.get('authority') != "delete": + cmd = f"{cmd} AUTH({operator['authority']})" + else: + cmd = f"{cmd} NOAUTH" + if operator.get('cmd_system') is not None: + if operator.get('cmd_system') != "": + cmd = f"{cmd} CMDSYS({tso['cmd_system']})" + else: + cmd = f"{cmd} NOCMDSYS" + if operator.get('search_key') is not None: + if operator.get('search_key') != "": + cmd = f"{cmd} KEY({operator['search_key']})" + else: + cmd = f"{cmd} NOKEY" + if operator.get('migration_id', False): + cmd = f"{cmd} MIGID(YES)" + else: + cmd = f"{cmd} MIGID(NO)" + # TODO: allow multiple choices + if operator.get('display') is not None: + if operator.get('display') != "delete": + cmd = f"{cmd} MONITOR({operator['operator']})" + else: + cmd = f"{cmd} NOMONITOR" + if operator.get('msg_level') is not None: + if operator.get('msg_level') != "delete": + cmd = f"{cmd} LEVEL({operator['msg_level']})" + else: + cmd = f"{cmd} NOLEVEL" + if operator.get('msg_format') is not None: + if operator.get('msg_format') != 'delete': + cmd = f"{cmd} MFORM({operator['msg_format']})" + else: + cmd = f"{cmd} NOMFORM" + if operator.get('msg_storage') is not None: + if operator.get('msg_storage') != 0: + cmd = f"{cmd} STORAGE({operator['msg_storage']})" + else: + cmd = f"{cmd} NOSTORAGE" + if operator.get('msg_scope') is not None: + if operator['msg_scope'].get('add') is not None: + scopes = operator['msg_scope']['add'] + cmd = f'{cmd}ADDMSCOPE( ' + for scope in scopes: + cmd = f'{cmd}{scope} ' + cmd = f'{cmd}) ' + elif operator['msg_scope'].get('add') is not None: + categories = access['category']['delete'] + cmd = f'{cmd}DELMSCOPE( ' + for scope in scopes: + cmd = f'{cmd}{scope} ' + cmd = f'{cmd}) ' + else: + cmd = f'{cmd}NOMSCOPE' + if operator.get('automated_msgs', False): + cmd = f"{cmd} AUTO(YES)" + else: + cmd = f"{cmd} AUTO(NO)" + if operator.get('del_msgs') is not None: + if operator.get('del_msgs') != 'delete': + cmd = f"{cmd} DOM({operator['del_msgs']})" + else: + cmd = f"{cmd} NODOM" + if operator.get('hardcopy_msgs', False): + cmd = f"{cmd} HC(YES)" + else: + cmd = f"{cmd} HC(NO)" + if operator.get('internal_msgs', False): + cmd = f"{cmd} INTIDS(YES)" + else: + cmd = f"{cmd} INTIDS(NO)" + if operator.get('routing_msgs') is not None: + routes = operator['routing_msgs'] + cmd = f'{cmd}ROUTCODE( ' + for route in routes: + cmd = f'{cmd}{route} ' + cmd = f'{cmd}) ' + if operator.get('undelivered_msgs', False): + cmd = f"{cmd} UD(YES)" + else: + cmd = f"{cmd} UD(NO)" + if operator.get('unknown_msgs', False): + cmd = f"{cmd} UNKNIDS(YES)" + else: + cmd = f"{cmd} UNKNIDS(NO)" + if operator.get('responses', False): + cmd = f"{cmd} LOGCMDRESP(YES)" + else: + cmd = f"{cmd} LOGCMDRESP(NO)" + + cmd = f"{cmd} )" + + return cmd + + def _make_access_substring_creation(self): + """Creates a string that defines various parameters for a user profile. + + Returns + ------- + str: User create/alter parameters of a RACF command. + """ + cmd = "" + access = self.params.get('access') + + if access is not None: + if access.get('default_group') is not None: + cmd = f"{cmd}DFLTGRP({access['default_group']}) " + if access.get('clauth') is not None: + if access['clauth'].get('add') is not None: + clauth = access['clauth']['add'] + cmd = f'{cmd}CLAUTH( ' + for class in clauth: + cmd = f'{cmd}{class} ' + cmd = f'{cmd}) ' + elif access['clauth'].get('delete') is not None: + clauth = access['clauth']['delete'] + cmd = f'{cmd}NOCLAUTH( ' + for class in clauth: + cmd = f'{cmd}{class} ' + cmd = f'{cmd}) ' + if access.get('roaudit') is not None: + roaudit = "ROAUDIT" if access['roaudit'] else "NOROAUDIT" + cmd = f'{cmd}{roaudit} ' + if access.get('category') is not None: + if access['category'].get('add') is not None: + categories = access['category']['add'] + cmd = f'{cmd}ADDCATEGORY( ' + for category in categories: + cmd = f'{cmd}{category} ' + cmd = f'{cmd}) ' + elif access['category'].get('delete') is not None: + categories = access['category']['delete'] + cmd = f'{cmd}DELCATEGORY( ' + for category in categories: + cmd = f'{cmd}{category} ' + cmd = f'{cmd}) ' + if access.get('operator_card') is not None: + op_card = "OIDCARD" if access['operator_card'] else "NOOIDCARD" + cmd = f'{cmd}{op_card} ' + if access.get('maintenance_access') is not None: + operations = "OPERATIONS" if access['maintenance_access'] else "NOOPERATIONS" + cmd = f'{cmd}{operations} ' + if access.get('restricted') is not None: + restricted = "RESTRICTED" if access['restricted'] else "NORESTRICTED" + cmd = f'{cmd}{restricted} ' + if access.get('security_label') is not None: + if access.get('security_label') != "": + cmd = f"{cmd}SECLABEL('{access['security_label']}') " + else: + cmd = f"{cmd}NOSECLABEL " + if access.get('security_level') is not None: + if access.get('security_level') != "": + cmd = f"{cmd}SECLEVEL('{access['security_level']}') " + else: + cmd = f"{cmd}NOSECLEVEL " + + return cmd + + def _make_restrictions_substring(self): + """Creates a string that defines various parameters for how a user can + access a system. + + Returns + ------- + str: User parameters of a RACF command. + """ + cmd = "" + restrictions = self.params.get('restrictions') + + if restrictions is not None: + if restrictions.get('days') is not None or restrictions.get('time') is not None: + cmd = f"{cmd}WHEN( " + # TODO: change to allow multiple choices + if restrictions.get('days') is not None: + cmd = f"{cmd}DAYS({restrictions['days']}) " + if restrictions.get('time') is not None: + cmd = f"{cmd}TIME({restrictions['time']}) " + cmd = f"{cmd}) " + + if restrictions.get('resume') is not None: + cmd = f"{cmd}RESUME({restrictions['resume']})" + elif restrictions.get('delete_resume', False): + cmd = f"{cmd}NORESUME " + + if restrictions.get('revoke') is not None: + cmd = f"{cmd}REVOKE({restrictions['revoke']})" + elif restrictions.get('delete_revoke', False): + cmd = f"{cmd}NOREVOKE " + + return cmd + + # def _make_access_substring_connect(self): + # """ + # """ + # cmd = "" + # access = self.params.get('access') + # + # if access is not None: + # if access.get('authority') is not None: + # cmd = f"{cmd}AUTHORITY({access['authority']}) " + # if access.get('universal_access') is not None: + # cmd = f"{cmd}UACC({access['universal_access']}) " + # + # if access['authority'].get('add') is not None: + # custom_fields = general['custom_fields']['add'] + # cmd = f'{cmd}CSDATA( ' + # for field in custom_fields: + # cmd = f'{cmd}{field}({custom_fields[field]}) ' + # cmd = f'{cmd}) ' + # elif general['custom_fields'].get('delete') is not None: + # custom_fields = general['custom_fields']['delete'] + # cmd = f'{cmd}CSDATA( ' + # for field in custom_fields: + # cmd = f'{cmd}NO{field.upper()} ' + # cmd = f'{cmd}) ' + # elif general['custom_fields'].get('delete_block') is not None: + # cmd = f'{cmd}NOCSDATA ' + # if general.get('installation_data') is not None: + # if general.get('installation_data') != "": + # cmd = f"{cmd}DATA('{general['installation_data']}') " + # else: + # cmd = f"{cmd}NODATA " + # if general.get('model') is not None: + # if general.get('model') != "": + # cmd = f"{cmd}MODEL({general['model']}) " + # else: + # cmd = f"{cmd}NOMODEL " + # if general.get('owner') is not None and general.get('owner') != "": + # cmd = f"{cmd}OWNER({general['owner']}) " + # + # return cmd + def get_racf_handler(module, module_params): """Returns the correct handler needed for the scope and operation given in a task. @@ -1296,25 +1761,6 @@ def run_module(): 'access': { 'type': 'dict', 'required': False, - 'mutually_exclusive': [ - ('authority', 'delete'), - ('universal_access', 'delete'), - ('group_name', 'delete'), - ('group_account', 'delete'), - ('group_operations', 'delete'), - ('default_group', 'delete'), - ('clauth', 'delete'), - ('auditor', 'delete'), - ('roaudit', 'delete'), - ('adsp_attribute', 'delete'), - ('category', 'delete'), - ('operator_card', 'delete'), - ('maintenance_access', 'delete'), - ('restricted', 'delete'), - ('security_label', 'delete'), - ('security_level', 'delete'), - ('special', 'delete') - ], 'options': { 'authority': { 'type': 'str', @@ -1424,10 +1870,6 @@ def run_module(): 'type': 'bool', 'required': False, 'default': False - }, - 'delete': { - 'type': 'bool', - 'required': False } } }, From a853aeeceb7d7feebd1b917b9714521be8539607 Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Fri, 19 Sep 2025 11:51:52 -0600 Subject: [PATCH 14/30] Add user connection and removal from a group --- plugins/modules/zos_user.py | 211 ++++++++++++++++++++++++------------ 1 file changed, 144 insertions(+), 67 deletions(-) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index 25edcb9ef5..33a97d4891 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -813,14 +813,17 @@ class UserHandler(RACFHandler): filters = { 'create': { 'nested': [ - ('general', 'custom_fields', ('add',)) + ('general', 'custom_fields', ('add',)), + ('access', 'clauth', ('add',)), + ('access', 'category', ('add',)), + ('operator', 'msg_scope', ('add',)) ], 'flat': [ ('dfp', ('data_app_id', 'data_class', 'storage_class', 'management_class')), ('language', ('primary', 'secondary')), ('omvs', ('uid', 'custom_uid', 'home', 'program', 'nonshared_size', 'shared_size', 'addr_space_size', 'map_size', 'max_procs', 'max_threads', 'max_cpu_time', 'max_files')), ('tso', ('account_num', 'logon_cmd', 'logon_proc', 'dest_id', 'hold_class', 'job_class', 'msg_class', 'sysout_class', 'region_size', 'max_region_size', 'security_label', 'unit_name', 'user_data')), - ('access', ('authority', 'universal_access', 'group_name', 'group_account', 'group_operations', 'default_group', 'clauth', 'auditor', 'roaudit', 'adsp_attribute', 'category', 'operator_card', 'maintenance_access', 'restricted', 'security_label', 'security_level', 'special')), + ('access', ('default_group', 'clauth', 'roaudit', 'category', 'operator_card', 'maintenance_access', 'restricted', 'security_label', 'security_level')), ('operator', ('alt_group', 'authority', 'cmd_system', 'search_key', 'migration_id', 'display', 'msg_level', 'msg_format', 'msg_storage', 'msg_scope', 'automated_msgs', 'del_msgs', 'hardcopy_msgs', 'internal_msgs', 'routing_msgs', 'undelivered_msgs', 'unknown_msgs', 'responses')), ('restrictions', ('days', 'time', 'resume', 'revoke')) ] @@ -828,7 +831,9 @@ class UserHandler(RACFHandler): 'update': {}, 'delete': {}, 'purge': {}, - 'list': {} + 'list': {}, + 'connect': {}, + 'remove': {} } should_remove_empty_strings = { @@ -836,7 +841,9 @@ class UserHandler(RACFHandler): 'update': False, 'delete': False, 'purge': False, - 'list': False + 'list': False, + 'connect': False, + 'remove': False } # All empty lists indicate an operation that doesn't require any other @@ -846,7 +853,9 @@ class UserHandler(RACFHandler): 'update': ['general', 'dfp', 'language', 'omvs', 'tso', 'access', 'operator', 'restrictions'], 'delete': [], 'purge': [], - 'list': [] + 'list': [], + 'connect': ['connect'], + 'remove': ['connect'] } validations = [ @@ -918,6 +927,10 @@ def execute_operation(self): rc, stdout, stderr, cmd = self._update_user() if self.operation == 'delete': rc, stdout, stderr, cmd = self._delete_user() + if self.operation == 'connect': + rc, stdout, stderr, cmd = self._connect_user() + if self.operation == 'remove': + rc, stdout, stderr, cmd = self._remove_user() self.cmd = cmd # Getting the base dictionary. @@ -995,6 +1008,99 @@ def _delete_user(self): return rc, stdout, stderr, cmd + def _connect_user(self): + """Builds and execute a CONNECT command. + + Returns + ------- + tuple: RC, stdout and stderr from the RACF command, and the CONNECT command. + """ + cmd = f'CONNECT ({self.name}) ' + + connect = self.params.get('connect') + + if connect.get('group_name') is not None: + cmd = f"{cmd} GROUP({connect['group_name']}) " + else: + return 1, "", "No group was provided for a connect operation.", cmd + + if connect.get('authority') is not None: + cmd = f"{cmd}AUTHORITY({connect['authority']}) " + if connect.get('universal_access') is not None: + cmd = f"{cmd}UACC({connect['universal_access']}) " + if connect.get('group_account', False): + cmd = f"{cmd}GRPACC " + else: + cmd = f"{cmd}NOGRPACC " + if connect.get('group_operations', False): + cmd = f"{cmd}OPERATIONS " + else: + cmd = f"{cmd}NOOPERATIONS " + if connect.get('auditor', False): + cmd = f"{cmd}AUDITOR " + else: + cmd = f"{cmd}NOAUDITOR " + if connect.get('adsp_attribute', False): + cmd = f"{cmd}ADSP " + else: + cmd = f"{cmd}NOADSP " + if connect.get('special', False): + cmd = f"{cmd}SPECIAL " + else: + cmd = f"{cmd}NOSPECIAL " + + if self.params.get('general') is not None: + if self.params['general'].get('owner') is not None: + cmd = f"{cmd} OWNER({self.params['general']['owner']})" + + if self.params.get('restrictions') is not None: + restrictions = self.params['restrictions'] + if restrictions.get('resume') is not None: + cmd = f"{cmd}RESUME({restrictions['resume']})" + elif restrictions.get('delete_resume', False): + cmd = f"{cmd}NORESUME " + + if restrictions.get('revoke') is not None: + cmd = f"{cmd}REVOKE({restrictions['revoke']})" + elif restrictions.get('delete_revoke', False): + cmd = f"{cmd}NOREVOKE " + + rc, stdout, stderr = self.module.run_command(f""" tsocmd "{cmd}" """) + + if rc == 0: + self.num_entities_modified = 1 + self.entities_modified = [self.name] + + return rc, stdout, stderr, cmd + + def _remove_user(self): + """Builds and execute a REMOVE command. + + Returns + ------- + tuple: RC, stdout and stderr from the RACF command, and the REMOVE command. + """ + cmd = f'REMOVE ({self.name}) ' + + connect = self.params.get('connect') + + if connect.get('group_name') is not None: + cmd = f"{cmd} GROUP({connect['group_name']}) " + else: + return 1, "", "No group was provided for a remove operation.", cmd + + if self.params.get('general') is not None: + if self.params['general'].get('owner') is not None: + cmd = f"{cmd} OWNER({self.params['general']['owner']})" + + rc, stdout, stderr = self.module.run_command(f""" tsocmd "{cmd}" """) + + if rc == 0: + self.num_entities_modified = 1 + self.entities_modified = [self.name] + + return rc, stdout, stderr, cmd + def _make_language_substring(self): """Creates a string that defines the LANGUAGE block of a user profile. @@ -1406,47 +1512,6 @@ def _make_restrictions_substring(self): return cmd - # def _make_access_substring_connect(self): - # """ - # """ - # cmd = "" - # access = self.params.get('access') - # - # if access is not None: - # if access.get('authority') is not None: - # cmd = f"{cmd}AUTHORITY({access['authority']}) " - # if access.get('universal_access') is not None: - # cmd = f"{cmd}UACC({access['universal_access']}) " - # - # if access['authority'].get('add') is not None: - # custom_fields = general['custom_fields']['add'] - # cmd = f'{cmd}CSDATA( ' - # for field in custom_fields: - # cmd = f'{cmd}{field}({custom_fields[field]}) ' - # cmd = f'{cmd}) ' - # elif general['custom_fields'].get('delete') is not None: - # custom_fields = general['custom_fields']['delete'] - # cmd = f'{cmd}CSDATA( ' - # for field in custom_fields: - # cmd = f'{cmd}NO{field.upper()} ' - # cmd = f'{cmd}) ' - # elif general['custom_fields'].get('delete_block') is not None: - # cmd = f'{cmd}NOCSDATA ' - # if general.get('installation_data') is not None: - # if general.get('installation_data') != "": - # cmd = f"{cmd}DATA('{general['installation_data']}') " - # else: - # cmd = f"{cmd}NODATA " - # if general.get('model') is not None: - # if general.get('model') != "": - # cmd = f"{cmd}MODEL({general['model']}) " - # else: - # cmd = f"{cmd}NOMODEL " - # if general.get('owner') is not None and general.get('owner') != "": - # cmd = f"{cmd}OWNER({general['owner']}) " - # - # return cmd - def get_racf_handler(module, module_params): """Returns the correct handler needed for the scope and operation given in a task. @@ -1758,7 +1823,7 @@ def run_module(): } } }, - 'access': { + 'connect': { 'type': 'dict', 'required': False, 'options': { @@ -1786,6 +1851,28 @@ def run_module(): 'required': False, 'default': False }, + 'auditor': { + 'type': 'bool', + 'required': False, + 'default': False + }, + 'adsp_attribute': { + 'type': 'bool', + 'required': False, + 'default': False + }, + 'special': { + 'type': 'bool', + 'required': False, + 'default': False + } + } + + }, + 'access': { + 'type': 'dict', + 'required': False, + 'options': { 'default_group': { 'type': 'str', 'required': False @@ -1809,21 +1896,11 @@ def run_module(): }, } }, - 'auditor': { - 'type': 'bool', - 'required': False, - 'default': False - }, 'roaudit': { 'type': 'bool', 'required': False, 'default': False }, - 'adsp_attribute': { - 'type': 'bool', - 'required': False, - 'default': False - }, 'category': { 'type': 'dict', 'required': False, @@ -1866,11 +1943,6 @@ def run_module(): 'type': 'str', 'required': False }, - 'special': { - 'type': 'bool', - 'required': False, - 'default': False - } } }, 'operator': { @@ -2143,7 +2215,7 @@ def run_module(): 'delete': {'arg_type': 'bool', 'required': False} } }, - 'access': { + 'connect': { 'arg_type': 'dict', 'required': False, 'options': { @@ -2152,6 +2224,15 @@ def run_module(): 'group_name': {'arg_type': 'str', 'required': False}, 'group_account': {'arg_type': 'bool', 'required': False}, 'group_operations': {'arg_type': 'bool', 'required': False}, + 'auditor': {'arg_type': 'bool', 'required': False}, + 'adsp_attribute': {'arg_type': 'bool', 'required': False}, + 'special': {'arg_type': 'bool', 'required': False}, + } + }, + 'access': { + 'arg_type': 'dict', + 'required': False, + 'options': { 'default_group': {'arg_type': 'str', 'required': False}, 'clauth': { 'arg_type': 'dict', @@ -2161,9 +2242,7 @@ def run_module(): 'delete': {'arg_type': 'list', 'elements': 'str', 'required': False} } }, - 'auditor': {'arg_type': 'bool', 'required': False}, 'roaudit': {'arg_type': 'bool', 'required': False}, - 'adsp_attribute': {'arg_type': 'bool', 'required': False}, 'category': { 'arg_type': 'dict', 'required': False, @@ -2177,8 +2256,6 @@ def run_module(): 'restricted': {'arg_type': 'bool', 'required': False}, 'security_label': {'arg_type': 'str', 'required': False}, 'security_level': {'arg_type': 'str', 'required': False}, - 'special': {'arg_type': 'bool', 'required': False}, - 'delete': {'arg_type': 'bool', 'required': False} } }, 'operator': { From 2dcf5bd052006ad74474e62c6e661f35e88f6afa Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Fri, 19 Sep 2025 14:07:12 -0600 Subject: [PATCH 15/30] Add validations for omvs parameters --- plugins/modules/zos_user.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index 33a97d4891..eabf63a5f6 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -869,6 +869,8 @@ class UserHandler(RACFHandler): (('omvs', 'custom_uid'), 'range', (0, 2_147_483_647, 0)), (('omvs', 'home'), 'length', ((0, 1023),)), (('omvs', 'program'), 'length', ((0, 1023),)), + (('omvs', 'nonshared_size'), 'format', ('[0-9]{1,8}[MGTP]',)), + (('omvs', 'shared_size'), 'format', ('[0-9]{1,8}[MGTP]',)), (('omvs', 'addr_space_size'), 'range', (10_485_760, 2_147_483_647, 0)), (('omvs', 'map_size'), 'range', (1, 16_777_216, 0)), (('omvs', 'max_procs'), 'range', (3, 32_767, 0)), @@ -914,6 +916,28 @@ def __init__(self, module, module_params): if self.operation in ['delete', 'purge', 'list']: self.params = {} + def validate_params(self): + """Adds a couple of validations for omvs.nonshared_size and omvs.shared_size. + + Raises + ------ + ValueError: When a parameter has an invalid value. + """ + super().validate_params() + + if self.params.get('omvs') is not None: + if self.params['omvs'].get('nonshared_size') is not None: + nonshared_size = self.params['omvs']['nonshared_size'] + nonshared_size = int(nonshared_size[:len(nonshared_size)-1]) + if nonshared_size < 0 or nonshared_size > 16_777_215: + raise ValueError('Value of omvs.nonshared_size is outside of its range.') + + if self.params['omvs'].get('shared_size') is not None: + shared_size = self.params['omvs']['shared_size'] + shared_size = int(shared_size[:len(shared_size)-1]) + if shared_size < 1 or shared_size > 16_777_215: + raise ValueError('Value of omvs.shared_size is outside of its range.') + def execute_operation(self): """Given the operation and scope, it executes a RACF command. @@ -1706,12 +1730,10 @@ def run_module(): 'type': 'int', 'required': False }, - # TODO: add validation for this one 'nonshared_size': { 'type': 'str', 'required': False }, - # TODO: add validation for this one 'shared_size': { 'type': 'str', 'required': False From 985f090056691cc4ac6e00581d1583b9d58e80d7 Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Fri, 19 Sep 2025 16:18:48 -0600 Subject: [PATCH 16/30] Add multiple choices to 2 options --- plugins/modules/zos_user.py | 110 ++++++++++++++++++++++++++++++++---- 1 file changed, 98 insertions(+), 12 deletions(-) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index eabf63a5f6..91ae392240 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -265,6 +265,89 @@ def dynamic_dict(contents, dependencies): return contents +def multiple_choice_display(contents, dependencies): + """Validates multiple choices for the operator.display option. + + Parameters + ---------- + contents: Union[str, list[str]] + The contents of the choice argument. + dependencies: dict + Any arguments this argument is dependent on. + + Raises + ------- + ValueError: str + When an invalid argument provided. + + Returns + ------- + list: List containing each choice selected. + """ + allowed_values = { + 'jobnames', + 'jobnamest', + 'sess', + 'sesst', + 'status', + 'delete' + } + + if not contents: + return None + if not isinstance(contents, list): + contents = [contents] + for value in contents: + if value not in allowed_values: + raise ValueError(f'Invalid argument "{value}" for option operator.display.') + if 'delete' in contents and len(contents) > 1: + raise ValueError(f'Cannot specify "delete" with other values for option operator.display.') + return contents + + +def multiple_choice_days(contents, dependencies): + """Validates multiple choices for the restrictions.days option. + + Parameters + ---------- + contents: Union[str, list[str]] + The contents of the choice argument. + dependencies: dict + Any arguments this argument is dependent on. + + Raises + ------- + ValueError: str + When an invalid argument provided. + + Returns + ------- + list: List containing each choice selected. + """ + allowed_values = { + 'anyday', + 'weekdays', + 'monday', + 'tuesday', + 'wednesday', + 'thursday', + 'friday', + 'saturday', + 'sunday' + } + + if not contents: + return None + if not isinstance(contents, list): + contents = [contents] + for value in contents: + if value not in allowed_values: + raise ValueError(f'Invalid argument "{value}" for option restrictions.days.') + if 'anyday' in contents and len(contents) > 1: + raise ValueError(f'Cannot specify "anyday" with other values for option restrictions.days.') + return contents + + class RACFHandler(): """Parent class for group and user RACF operations. """ @@ -1363,10 +1446,13 @@ def _make_operator_substring(self): cmd = f"{cmd} MIGID(YES)" else: cmd = f"{cmd} MIGID(NO)" - # TODO: allow multiple choices if operator.get('display') is not None: - if operator.get('display') != "delete": - cmd = f"{cmd} MONITOR({operator['operator']})" + if "delete" not in operator['display']: + options = operator['display'] + cmd = f'{cmd}MONITOR( ' + for option in options: + cmd = f'{cmd}{option} ' + cmd = f'{cmd}) ' else: cmd = f"{cmd} NOMONITOR" if operator.get('msg_level') is not None: @@ -1517,9 +1603,11 @@ def _make_restrictions_substring(self): if restrictions is not None: if restrictions.get('days') is not None or restrictions.get('time') is not None: cmd = f"{cmd}WHEN( " - # TODO: change to allow multiple choices if restrictions.get('days') is not None: - cmd = f"{cmd}DAYS({restrictions['days']}) " + cmd = f"{cmd}DAYS( " + for day in restrictions['days']: + cmd = f"{cmd}{day} " + cmd = f"{cmd}) " if restrictions.get('time') is not None: cmd = f"{cmd}TIME({restrictions['time']}) " cmd = f"{cmd}) " @@ -2013,12 +2101,11 @@ def run_module(): 'required': False, 'default': False }, - # TODO: allow multiple choices - # TODO: default should be ['jobnames', 'sess'] 'display': { - 'type': 'str', + 'type': 'raw', 'required': False, - 'choices': ['jobnames', 'jobnamest', 'sess', 'sesst', 'status', 'delete'] + 'default': ['jobnames', 'sess'] + # 'choices': ['jobnames', 'jobnamest', 'sess', 'sesst', 'status', 'delete'] }, 'msg_level': { 'type': 'str', @@ -2113,7 +2200,6 @@ def run_module(): ('revoke', 'delete_revoke') ], 'options': { - # TODO: allow multiple 'days': { 'type': 'str', 'required': False, @@ -2289,7 +2375,7 @@ def run_module(): 'cmd_system': {'arg_type': 'str', 'required': False}, 'search_key': {'arg_type': 'str', 'required': False}, 'migration_id': {'arg_type': 'bool', 'required': False}, - 'display': {'arg_type': 'str', 'required': False}, + 'display': {'arg_type': multiple_choice_display, 'required': False}, 'msg_level': {'arg_type': 'str', 'required': False}, 'msg_format': {'arg_type': 'str', 'required': False}, 'msg_storage': {'arg_type': 'int', 'required': False}, @@ -2317,7 +2403,7 @@ def run_module(): 'arg_type': 'dict', 'required': False, 'options': { - 'days': {'arg_type': 'str', 'required': False}, + 'days': {'arg_type': multiple_choice_days, 'required': False}, 'time': {'arg_type': 'str', 'required': False}, 'resume': {'arg_type': 'str', 'required': False}, 'delete_resume': {'arg_type': 'bool', 'required': False}, From d1be8df8a4499b037de2cea4867f5693e54b5fbd Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Wed, 24 Sep 2025 17:45:49 -0600 Subject: [PATCH 17/30] Fix syntax --- plugins/modules/zos_user.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index 91ae392240..59d46117df 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -1542,14 +1542,14 @@ def _make_access_substring_creation(self): if access['clauth'].get('add') is not None: clauth = access['clauth']['add'] cmd = f'{cmd}CLAUTH( ' - for class in clauth: - cmd = f'{cmd}{class} ' + for auth_class in clauth: + cmd = f'{cmd}{auth_class} ' cmd = f'{cmd}) ' elif access['clauth'].get('delete') is not None: clauth = access['clauth']['delete'] cmd = f'{cmd}NOCLAUTH( ' - for class in clauth: - cmd = f'{cmd}{class} ' + for auth_class in clauth: + cmd = f'{cmd}{auth_class} ' cmd = f'{cmd}) ' if access.get('roaudit') is not None: roaudit = "ROAUDIT" if access['roaudit'] else "NOROAUDIT" From 4d9ab84f497c976c72ed907ed41c3c213dc3b085 Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Wed, 24 Sep 2025 18:17:45 -0600 Subject: [PATCH 18/30] Clean up strings --- plugins/modules/zos_user.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index 59d46117df..f91b3699e5 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -1170,7 +1170,7 @@ def _connect_user(self): if restrictions.get('revoke') is not None: cmd = f"{cmd}REVOKE({restrictions['revoke']})" elif restrictions.get('delete_revoke', False): - cmd = f"{cmd}NOREVOKE " + cmd = f"{cmd}NOREVOKE" rc, stdout, stderr = self.module.run_command(f""" tsocmd "{cmd}" """) @@ -1192,7 +1192,7 @@ def _remove_user(self): connect = self.params.get('connect') if connect.get('group_name') is not None: - cmd = f"{cmd} GROUP({connect['group_name']}) " + cmd = f"{cmd}GROUP({connect['group_name']})" else: return 1, "", "No group was provided for a remove operation.", cmd From 37bf23e238085c249617653683f2b8f7b7240111 Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Mon, 29 Sep 2025 08:27:50 -0600 Subject: [PATCH 19/30] Fix validation issues --- plugins/modules/zos_user.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index f91b3699e5..4df573d974 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -952,8 +952,8 @@ class UserHandler(RACFHandler): (('omvs', 'custom_uid'), 'range', (0, 2_147_483_647, 0)), (('omvs', 'home'), 'length', ((0, 1023),)), (('omvs', 'program'), 'length', ((0, 1023),)), - (('omvs', 'nonshared_size'), 'format', ('[0-9]{1,8}[MGTP]',)), - (('omvs', 'shared_size'), 'format', ('[0-9]{1,8}[MGTP]',)), + (('omvs', 'nonshared_size'), 'format', ('[0-9]{1,8}[mgtp]',)), + (('omvs', 'shared_size'), 'format', ('[0-9]{1,8}[mgtp]',)), (('omvs', 'addr_space_size'), 'range', (10_485_760, 2_147_483_647, 0)), (('omvs', 'map_size'), 'range', (1, 16_777_216, 0)), (('omvs', 'max_procs'), 'range', (3, 32_767, 0)), @@ -1424,7 +1424,7 @@ def _make_operator_substring(self): if operator.get('alt_group') is not None: if operator.get('alt_group') != "": - cmd = f"{cmd} ALTGRP({operator['account_num']})" + cmd = f"{cmd} ALTGRP({operator['alt_group']})" else: cmd = f"{cmd} NOALTGRP" if operator.get('authority') is not None: @@ -1434,7 +1434,7 @@ def _make_operator_substring(self): cmd = f"{cmd} NOAUTH" if operator.get('cmd_system') is not None: if operator.get('cmd_system') != "": - cmd = f"{cmd} CMDSYS({tso['cmd_system']})" + cmd = f"{cmd} CMDSYS({operator['cmd_system']})" else: cmd = f"{cmd} NOCMDSYS" if operator.get('search_key') is not None: @@ -1449,7 +1449,7 @@ def _make_operator_substring(self): if operator.get('display') is not None: if "delete" not in operator['display']: options = operator['display'] - cmd = f'{cmd}MONITOR( ' + cmd = f'{cmd} MONITOR( ' for option in options: cmd = f'{cmd}{option} ' cmd = f'{cmd}) ' @@ -1504,7 +1504,7 @@ def _make_operator_substring(self): cmd = f"{cmd} INTIDS(NO)" if operator.get('routing_msgs') is not None: routes = operator['routing_msgs'] - cmd = f'{cmd}ROUTCODE( ' + cmd = f'{cmd} ROUTCODE( ' for route in routes: cmd = f'{cmd}{route} ' cmd = f'{cmd}) ' @@ -1517,7 +1517,7 @@ def _make_operator_substring(self): else: cmd = f"{cmd} UNKNIDS(NO)" if operator.get('responses', False): - cmd = f"{cmd} LOGCMDRESP(YES)" + cmd = f"{cmd} LOGCMDRESP(SYSTEM)" else: cmd = f"{cmd} LOGCMDRESP(NO)" @@ -1815,7 +1815,7 @@ def run_module(): 'required': False }, 'program': { - 'type': 'int', + 'type': 'str', 'required': False }, 'nonshared_size': { @@ -2201,9 +2201,8 @@ def run_module(): ], 'options': { 'days': { - 'type': 'str', + 'type': 'raw', 'required': False, - 'choices': ['anyday', 'weekdays', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'], 'default': 'anyday' }, 'time': { @@ -2434,6 +2433,7 @@ def run_module(): try: operation_handler.validate_params() except ValueError as err: + result = operation_handler.get_state() result['msg'] = str(err) module.fail_json(**result) From f7068519778e9c5cdc8a4ba819e888ac97356141 Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Wed, 1 Oct 2025 17:25:26 -0600 Subject: [PATCH 20/30] Add language and omvs documentation --- plugins/modules/zos_user.py | 113 ++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index 4df573d974..4dda8f72fb 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -171,6 +171,37 @@ - This option is mutually exclusive with every other option in this section. type: bool required: false + language: + description: + - Options that set the preferred national languages for a user profile. + - These options will override the system-wide defaults. + required: false + type: dict + suboptions: + primary: + description: + - User's primary language. + - Value should be either a 3 character-long language code or an + installation-defined name of up to 24 characters. + - An empty string will delete this field from the profile. + type: str + required: false + secondary: + description: + - User's secondary language. + - Value should be either a 3 character-long language code or an + installation-defined name of up to 24 characters. + - An empty string will delete this field from the profile. + type: str + required: false + delete: + description: + - Delete the whole LANGUAGE block from the profile. + - This option is only valid when updating user profiles, it will be ignored + when creating one. + - This option is mutually exclusive with every other option in this section. + type: bool + required: false omvs: description: - Attributes for how Unix System Services should work under a profile. @@ -195,6 +226,88 @@ - A number between 0 and 2,147,483,647. type: int required: false + home: + description: + - Path name for the z/OS Unix System Services home directory. + - Maximum length of 1023 characters. + - An empty string will delete this field from the profile. + type: str + required: false + program: + description: + - Path of the shell program to use when the user logs in. + - Maximum length of 1023 characters. + - An empty string will delete this field from the profile. + type: str + required: false + nonshared_size: + description: + - Maximum number of bytes of nonshared memory that can be allocated + by the user. + - Must be a number between 0 and 16,777,215 subfixed by one of the + following units: m (megabytes), g (gigabytes), t (terabytes) or + p (petabytes). + - An empty string will delete the current limit set. + type: str + required: false + shared_size: + description: + - Maximum number of bytes of shared memory that can be allocated + by the user. + - Must be a number between 1 and 16,777,215 subfixed by one of the + following units: m (megabytes), g (gigabytes), t (terabytes) or + p (petabytes). + - An empty string will delete the current limit set. + type: str + required: false + addr_space_size: + description: + - Address space region size in bytes. + - Value between 10,485,760 and 2,147,483,647. + - A value of 0 will delete this field from the profile. + type: int + required: false + map_size: + description: + - Maximum amount of data space storage that can be allocated by + the user. + - This option represents the number of memory pages, not bytes, + available. + - Value between 1 and 16,777,216. + - A value of 0 will delete this field from the profile. + type: int + required: false + max_procs: + description: + - Maximum number of processes the user is allowed to have active + at the same time. + - Value between 3 and 32,767. + - A value of 0 will delete this field from the profile. + type: int + required: false + max_threads: + description: + - Maximum number of threads the user can have concurrently active. + - Value between 0 and 100,000. + - A value of -1 will delete this field from the profile. + type: int + required: false + max_cpu_time: + description: + - Specifies the RLIMIT_CPU hard limit. Indicates the cpu-time that a + user process is allowed to use. + - Value between 7 and 2,147,483,647 seconds. + - A value of 0 will delete this field from the profile. + type: int + required: false + max_files: + description: + - Maximum number of files the user is allowed to have concurrently + active or open. + - Value between 3 and 524,287. + - A value of 0 will delete this field from the profile. + type: int + required: false delete: description: - Delete the whole OMVS block from the profile. From e1414bc4df045ac4ec93ec2d581ff628a4b2def8 Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Thu, 2 Oct 2025 17:51:12 -0600 Subject: [PATCH 21/30] Add return block and access options --- plugins/modules/zos_user.py | 141 ++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index 4dda8f72fb..96a195e4e2 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -316,6 +316,111 @@ - This option is mutually exclusive with every other option in this section. type: bool required: false + tso: + description: + - + required: false + type: dict + suboptions: + connect: + description: + - + required: false + type: dict + suboptions: + access: + description: + - Options that set different security attributes in a user profile. + required: false + type: dict + suboptions: + default_group: + description: + - RACF's default group for the user profile. + type: str + required: false + clauth: + description: + - Classes in which a user is allowed to define profiles to RACF for protection. + type: dict + required: false + suboptions: + add: + description: + - Adds classes to the profile. + type: list + elements: str + required: false + delete: + description: + - Removes classes from the profile. + type: list + elements: str + required: false + roaudit: + description: + - Whether a user should have full responsibility for auditing the use of system + resources. + type: bool + required: false + category: + description: + - Security categories that the profile should have. + type: dict + required: false + suboptions: + add: + description: + - Adds security categories to the profile. + type: list + elements: str + required: false + delete: + description: + - Removes security categories from the profile. + type: list + elements: str + required: false + operator_card: + description: + - Whether a user must supply an operator identification card when logging in. + type: bool + required: false + maintenance_access: + description: + - Whether the user has authorization to do maintenance operations on all + RACF-protected DASD data sets, tape volumes, and DASD volumes. + type: bool + required: false + restricted: + description: + - Whether to give the profile the RESTRICTED attribute. + type: bool + required: false + security_label: + description: + - Security label applied to the profile. + - Empty value deletes this field. + type: str + required: false + security_level: + description: + - Security level applied to the profile. + - Empty value deletes this field. + type: str + required: false + operator: + description: + - + required: false + type: dict + suboptions: + restrictions: + description: + - + required: false + type: dict + suboptions: attributes: action: @@ -338,6 +443,42 @@ """ RETURN = r""" +operation: + description: Operation that was performed by the module. + returned: always + type: str + sample: create +racf_command: + description: Full command string that was executed with tsocmd. + returned: success + type: str + sample: "DELUSER (user)" +num_entities_modified: + description: Number of profiles and references modified by the operation. + returned: always + type: int + sample: 1 +entities_modified: + description: List of all profiles and references modified by the operation. + returned: success + type: list + elements: str + sample: ['user'] +database_dumped: + description: Whether the module used IRRRID00 to dump the RACF database. + returned: always + type: bool + sample: false +dump_kept: + description: Whether the RACF database dump was kept on the managed node. + returned: always + type: bool + sample: false +dump_name: + description: Name of the database containing the output from the IRRRID00 utility. + returned: success + type: str + sample: USER.BACKUP.RACF.DATABASE """ import copy From 319ffd984a68294a31c37abd9755635380a79df1 Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Fri, 3 Oct 2025 13:31:09 -0600 Subject: [PATCH 22/30] Add the operator block --- plugins/modules/zos_user.py | 184 +++++++++++++++++++++++++++++++++++- 1 file changed, 183 insertions(+), 1 deletion(-) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index 96a195e4e2..92df01a304 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -411,10 +411,192 @@ required: false operator: description: - - + - Attributes used when a user establishes an extended MCS + console session. required: false type: dict suboptions: + alt_group: + description: + - Console group used in recovery. + - Must be between 1 and 8 characters in length. + - Empty value deletes this field. + type: str + required: false + authority: + description: + - Console's authority to issue operator commands. + - C(delete) will remove the field from the profile. + type: str + required: false + choices: + - master + - all + - info + - cons + - io + - sys + - delete + cmd_system: + description: + - System to which commands from this console are to + be sent. + - Must be between 1 and 8 characters in length. + - Empty value deletes this field. + type: str + required: false + search_key: + description: + - Name used to display information for all consoles + with the specified key by using the MVS command + C(DISPLAY CONSOLES,KEY). + - Must be between 1 and 8 characters in length. + - Empty value deletes this field. + type: str + required: false + migration_id: + description: + - Whether a 1-byte migration ID should be assigned to + this console. + type: bool + required: false + display: + description: + - Which information should be displayed when monitoring + jobs, TSO sessions, or data set status. + - Possible values are C(jobnames), C(jobnamest), C(sess), + C(sesst), C(status) and C(delete). + - Multiple choices are allowed. + - C(delete) will remove this field from the profile. + type: str + required: false + default: ['jobnames', 'sess'] + msg_level: + description: + - Specifies the messages that this console is to receive. + - C(delete) will remove this field from the profile. + type: str + required: false + choices: + - nb + - all + - r + - i + - ce + - e + - in + - delete + msg_format: + description: + - Format in which messages are displayed at the console. + - C(delete) will remove this field from the profile. + type: str + required: false + choices: + - j + - m + - s + - t + - x + - delete + msg_storage: + description: + - Specifies the amount of storage in the TSO/E user's address + space that can be used for message queuing to the console. + - Its value can be a number between 1 and 2,000. + - A value of 0 deletes this field. + type: int + required: false + msg_scope: + description: + - Systems from which this console can receive messages that + are not directed to a specific console. + type: dict + required: false + suboptions: + add: + description: + - Add new systems to this field. + type: list + elements: str + required: false + remove: + description: + - Removes systems from this field. + type: list + elements: str + required: false + delete: + description: + - Deletes this field from the profile. + - Mutually exclusive with the rest of the options + in this section. + type: bool + required: false + automated_msgs: + description: + - Whether the extended console can receive messages + that have been automated by the MFP. + type: bool + required: false + del_msgs: + description: + - Which delete operator message (DOM) requests the + console can receive. + - C(delete) will remove the field from the profile. + type: str + required: false + choices: + - normal + - all + - none + - delete + hardcopy_msgs: + description: + - Whether the console should receive all messages + that are directed to hardcopy. + type: bool + required: false + internal_msgs: + description: + - Whether the console should receive messages that + are directed to console ID zero. + type: bool + required: false + routing_msgs: + description: + - Specifies the routing codes of messages this + operator is to receive. + - C(ALL) can be specified to receive all codes. Conversely, + C(NONE) can be used to receive none. + type: list + elements: str + required: false + undelivered_msgs: + description: + - Whether the console should receive undelivered + messages. + type: bool + required: false + unknown_msgs: + description: + - Whether the console should receive messages that + are directed to unknown console IDs. + type: bool + required: false + responses: + description: + - Whether command responses should be logged. + type: bool + required: false + delete: + description: + - Delete the whole OPERPARM block from the profile. + - This option is only valid when updating profiles, it will be ignored + when creating one. + - This option is mutually exclusive with every other option in this section. + type: bool + required: false restrictions: description: - From 30a3e775c280841a825662d8ac433abd463413e7 Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Fri, 3 Oct 2025 13:50:52 -0600 Subject: [PATCH 23/30] Add restrictions docs --- plugins/modules/zos_user.py | 49 ++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index 92df01a304..dd4d705cee 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -599,10 +599,57 @@ required: false restrictions: description: - - + - Attributes that determine the days and times a user is + allowed to login. required: false type: dict suboptions: + days: + description: + - Days of the week that a user is allowed to login. + - Multiple choices are allowed. + - Valid values are C(anyday), C(weekdays), C(monday), C(tuesday), + C(wednesday), C(thursday), C(friday), C(saturday) and C(sunday). + type: list + elements: str + required: false + time: + description: + - Daily time period when the user is allowed to login. + - The value for this option must be in the format HHMM:HHMM. + - This field uses a 24-hour format. + - This field also accepts the value C(anytime) to indicate a + user is free to login at any time of the day. + type: str + required: false + resume: + description: + - Date when the user is allowed access to a system again. + - The value for this option must be in the format MM/DD/YY, + where C(YY) are the last two digits of the year. + type: str + required: false + delete_resume: + description: + - Delete the resume field from the profile. + - This option is only valid when connecting a user to a group. + - This option is mutually exclusive with I(resume). + type: bool + required: false + revoke: + description: + - Date when the user is forbidden access to a system. + - The value for this option must be in the format MM/DD/YY, + where C(YY) are the last two digits of the year. + type: str + required: false + delete_revoke: + description: + - Delete the revoke field from the profile. + - This option is only valid when connecting a user to a group. + - This option is mutually exclusive with I(revoke). + type: bool + required: false attributes: action: From 2fc472cd8f339be7fa32d9e36f332159cadf1939 Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Mon, 6 Oct 2025 12:41:20 -0600 Subject: [PATCH 24/30] Add examples --- plugins/modules/zos_user.py | 159 +++++++++++++++++++++++++++++++++++- 1 file changed, 158 insertions(+), 1 deletion(-) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index dd4d705cee..c009c63e3c 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -318,10 +318,25 @@ required: false tso: description: - - + - Attributes for how TSO should handle a user profile. required: false type: dict suboptions: + account_num: + description: + - User's default TSO account number when logging in. + - Value between 3 and 524,287. + - A value of 0 will delete this field from the profile. + type: int + required: false + delete: + description: + - Delete the whole TSO block from the profile. + - This option is only valid when updating profiles, it will be ignored + when creating one. + - This option is mutually exclusive with every other option in this section. + type: bool + required: false connect: description: - @@ -669,6 +684,148 @@ """ EXAMPLES = r""" +- name: Create a new group profile using RACF defaults. + zos_user: + name: newgrp + operation: create + scope: group + +- name: Create a new group profile using another group as a model and setting its owner. + zos_user: + name: newgrp + operation: create + scope: group + general: + model: oldgrp + owner: admin + +- name: Create a new group profile and set group attributes. + zos_user: + name: newgrp + operation: create + scope: group + group: + superior_group: sys1 + terminal_access: true + universal_group: false + +- name: Update a group profile to change its installation data and remove custom fields. + zos_user: + name: usergrp + operation: update + scope: group + general: + installation_data: New installation data + custom_fields: + delete_block: true + +- name: Create a user using RACF defaults. + zos_user: + name: newuser + operation: create + scope: user + +- name: Create a user using another profile as a model. + zos_user: + name: newuser + operation: create + scope: user + general: + model: olduser + +- name: Create a user and set how Unix System Services should behave when it logs in. + zos_user: + name: newuser + operation: create + scope: user + omvs: + uid: auto + home: /u/newuser + program: /bin/sh + nonshared_size: '10g' + shared_size: '10g' + addr_space_size: 10485760 + map_size: 2056 + max_procs: 16 + max_threads: 150 + max_cpu_time: 4096 + max_files: 4096 + +- name: Create a user and set access permissions to it. + zos_user: + name: newuser + operation: create + scope: user + access: + default_group: usergrp + roaudit: true + operator_card: false + maintenance_access: true + restricted: false + restrictions: + days: + - monday + - tuesday + - wednesday + time: anytime + +- name: Update a user profile to change its TSO attributes and owner. + zos_user: + name: user + operation: create + scope: user + general: + owner: admin + tso: + hold_class: K + job_class: K + msg_class: K + sysout_class: K + region_size: 2048 + max_region_size: 4096 + +- name: Connect a user to a group using RACF defaults. + zos_user: + name: user + operation: connect + scope: user + connect: + group_name: usergrp + +- name: Connect a user to a group and give it special permissions. + zos_user: + name: user + operation: connect + scope: user + connect: + group_name: usergrp + authority: connect + universal_access: alter + group_account: true + group_operations: true + auditor: true + adsp_attribute: true + special: true + +- name: Remove a user from a group. + zos_user: + name: user + operation: remove + scope: user + connect: + group_name: usergrp + +- name: Delete a user from the RACF database. + zos_user: + name: user + operation: delete + scope: user + +- name: Delete group from the RACF database. + zos_user: + name: usergrp + operation: delete + scope: group """ RETURN = r""" From d2fbc37da621a24c8b07377328073badd129152d Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Mon, 6 Oct 2025 13:00:17 -0600 Subject: [PATCH 25/30] Add TSO options --- plugins/modules/zos_user.py | 86 +++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index c009c63e3c..d3c3c13e9e 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -329,6 +329,92 @@ - A value of 0 will delete this field from the profile. type: int required: false + logon_cmd: + description: + - Command that needs to be run during TSO/E logon. + - Maximum length of 80 characters. + - This option keeps case. + - An empty value deletes this field. + type: str + required: false + logon_proc: + description: + - User's default logon procedure. + - The value for this field is 1 to 8 alphanumeric characters. + - An empty value deletes this field. + type: str + required: false + dest_id: + description: + - Default destination to which the user can route dynamically allocated SYSOUT + data sets. + - The value for this field is 1 to 7 alphanumeric characters. + - An empty value deletes this field. + type: str + required: false + hold_class: + description: + - User's default hold class. + - This option consists of 1 alphanumeric character. + - An empty value deletes this field. + type: str + required: false + job_class: + description: + - User's default job class. + - This option consists of 1 alphanumeric character. + - An empty value deletes this field. + type: str + required: false + msg_class: + description: + - User's default message class. + - This option consists of 1 alphanumeric character. + - An empty value deletes this field. + type: str + required: false + sysout_class: + description: + - User's default SYSOUT class. + - This option consists of 1 alphanumeric character. + - An empty value deletes this field. + type: str + required: false + region_size: + description: + - Minimum region size if the user does not request a region size at logon. + - A value between 0 and 2,096,128. + - A value of -1 deletes this field. + type: int + required: false + max_region_size: + description: + - Maximum region size that the user can request at logon. + - A value between 0 and 2,096,128. + - A value of -1 deletes this field. + type: int + required: false + security_label: + description: + - User's security label if the user specifies one on the TSO logon panel. + - An empty value deletes this field. + type: str + required: false + unit_name: + description: + - Default name of a device or group of devices that a procedure uses for + allocations. + - The value for this field is 1 to 8 alphanumeric characters. + - An empty value deletes this field. + type: str + required: false + user_data: + description: + - Optional installation data defined for the user profile. + - Must be 4 EBCDIC characters. + - An empty value deletes this field. + type: str + required: false delete: description: - Delete the whole TSO block from the profile. From 0eac25fc3fa72ed1678c9c8627a32b870ae0ea7f Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Mon, 6 Oct 2025 13:21:17 -0600 Subject: [PATCH 26/30] Add connect options --- plugins/modules/zos_user.py | 60 ++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index d3c3c13e9e..21d531891e 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -425,10 +425,68 @@ required: false connect: description: - - + - Options that configure what a user can do inside a group that is connected to. + - These options are only used when C(operation=connect) and they are ignored + otherwise. required: false type: dict suboptions: + authority: + description: + - Level of group authority given to a user profile. + type: str + required: false + choices: + - use + - create + - connect + - join + universal_access: + description: + - Level of universal access authority given to a user profile. + type: str + required: false + choices: + - alter + - control + - update + - read + - none + group_name: + description: + - Group to which the user will be connected to. + - The rest of the options in this block will affect this group. + - If not supplied, RACF will use a default group. It is recommended to specify + this option when trying to connect a user to a group. + type: str + required: false + group_account: + description: + - Whether the user's protected data sets are accessible to other users in the group. + type: bool + required: false + group_operations: + description: + - Whether a user should have the group-OPERATIONS attribute when connected to a group. + type: bool + required: false + auditor: + description: + - Whether a user should have auditor privileges for the group it is connected to. + type: bool + required: false + adsp_attribute: + description: + - Whether to give a user the ADSP attribute, which tells RACF to automatically protect + data sets it creates with discrete profiles. + type: bool + required: false + special: + description: + - Whether to give a user profile the SPECIAL attribute. + - This attribute lets a user change attributes of other profiles. Use with caution. + type: bool + required: false access: description: - Options that set different security attributes in a user profile. From c8beca578160d602e4d167cc48c020622655c292 Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Tue, 7 Oct 2025 15:05:14 -0600 Subject: [PATCH 27/30] Create RST file --- docs/source/modules/zos_user.rst | 909 +++++++++++++++++++++++++++++++ plugins/modules/zos_user.py | 12 +- 2 files changed, 915 insertions(+), 6 deletions(-) create mode 100644 docs/source/modules/zos_user.rst diff --git a/docs/source/modules/zos_user.rst b/docs/source/modules/zos_user.rst new file mode 100644 index 0000000000..748c82f68e --- /dev/null +++ b/docs/source/modules/zos_user.rst @@ -0,0 +1,909 @@ +.. _zos_user_module: + + +zos_user -- Manage user and group profiles in RACF +================================================== + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- + +The \ `zos\_user <./zos_user.html>`__ module executes RACF TSO commands that can manage user and group RACF profiles. + +The module can create, update and delete RACF profiles, as well as list information about them. + + + + + + +Parameters +---------- + + name (True, str, None) + Name of the RACF profile the module will operate on. + + + operation (True, str, None) + RACF command that will be executed. + + Group profiles can be created, updated, listed, deleted and purged. + + User profiles can use any of the choices. + + :literal:`delete` will run a RACF :literal:`DELGROUP` or a :literal:`DELUSER` TSO command. This will remove the profile but not every reference in the RACF database. + + :literal:`purge` will execute the RACF utility IRRDBU00, thereby removing all references of a profile from the RACF database. + + :literal:`connect` will add a given user profile to a group. :literal:`remove` will remove the user from a group. + + + scope (True, str, None) + Whether commands should affect a user or a group profile. + + + general (False, dict, None) + Options that change common attributes in a RACF profile. + + + model (False, str, None) + RACF profile that will be used as a model for the profile being changed. + + An empty string will delete this field from the profile. + + + owner (False, str, None) + Owner of the profile that is being changed. + + It can be a user or a group profile. + + + installation_data (False, str, None) + Installation-defined data that will be stored in the profile. + + Maximum length of 255 characters. + + The module will automatically enclose the contents in single quotation marks. + + An empty string will delete this field from the profile. + + + custom_fields (False, dict, None) + Custom fields that will be stored with the profile. + + + add (False, dict, None) + Adds custom fields to this profile. + + Each custom field should be a :strong:`ERROR while parsing`\ : While parsing "C(key" at index 31 of paragraph 1: Cannot find closing ")" after last parameter + + + delete (False, list, None) + Deletes each custom field listed. + + + delete_block (required, bool, None) + Delete the whole custom fields block from the profile. + + This option is only valid when updating profiles, it will be ignored when creating one. + + This option is mutually exclusive with :literal:`add` and :literal:`delete`. + + + + + group (False, dict, None) + Options that change group-specific attributes in a RACF profile. + + Only valid when changing a group profile, ignored for user profiles. + + + superior_group (False, str, None) + Superior group that will be assigned to the profile. + + + terminal_access (False, bool, None) + Whether to allow the use of the universal access authority for a terminal during authorization checking. + + + universal_group (False, bool, None) + Whether the group should be allowed to have an unlimited number of users. + + + + dfp (False, dict, None) + Options that set DFP attributes from the Storage Management Subsytem. + + + data_app_id (False, str, None) + Name of a DFP data application. + + + data_class (False, str, None) + Default data class for data set allocation. + + + management_class (False, str, None) + Default management class for data set migration and backup. + + + storage_class (False, str, None) + Default storage class for data set space, device and volume. + + + delete (False, bool, None) + Delete the whole DFP block from the profile. + + This option is only valid when updating profiles, it will be ignored when creating one. + + This option is mutually exclusive with every other option in this section. + + + + language (False, dict, None) + Options that set the preferred national languages for a user profile. + + These options will override the system-wide defaults. + + + primary (False, str, None) + User's primary language. + + Value should be either a 3 character-long language code or an installation-defined name of up to 24 characters. + + An empty string will delete this field from the profile. + + + secondary (False, str, None) + User's secondary language. + + Value should be either a 3 character-long language code or an installation-defined name of up to 24 characters. + + An empty string will delete this field from the profile. + + + delete (False, bool, None) + Delete the whole LANGUAGE block from the profile. + + This option is only valid when updating user profiles, it will be ignored when creating one. + + This option is mutually exclusive with every other option in this section. + + + + omvs (False, dict, None) + Attributes for how Unix System Services should work under a profile. + + + uid (False, str, None) + How RACF should assign a user its UID. + + :literal:`none` will be ignored when creating a profile. + + :literal:`custom` and :literal:`shared` require :literal:`custom\_uid` too. + + + custom_uid (False, int, None) + Specifies the profile's UID. + + A number between 0 and 2,147,483,647. + + + home (False, str, None) + Path name for the z/OS Unix System Services home directory. + + Maximum length of 1023 characters. + + An empty string will delete this field from the profile. + + + program (False, str, None) + Path of the shell program to use when the user logs in. + + Maximum length of 1023 characters. + + An empty string will delete this field from the profile. + + + nonshared_size (False, str, None) + Maximum number of bytes of nonshared memory that can be allocated by the user. + + Must be a number between 0 and 16,777,215 subfixed by a unit. + + Valid units are m (megabytes), g (gigabytes), t (terabytes) or p (petabytes). + + An empty string will delete the current limit set. + + + shared_size (False, str, None) + Maximum number of bytes of shared memory that can be allocated by the user. + + Must be a number between 1 and 16,777,215 subfixed by a unit. + + Valid units are m (megabytes), g (gigabytes), t (terabytes) or p (petabytes). + + An empty string will delete the current limit set. + + + addr_space_size (False, int, None) + Address space region size in bytes. + + Value between 10,485,760 and 2,147,483,647. + + A value of 0 will delete this field from the profile. + + + map_size (False, int, None) + Maximum amount of data space storage that can be allocated by the user. + + This option represents the number of memory pages, not bytes, available. + + Value between 1 and 16,777,216. + + A value of 0 will delete this field from the profile. + + + max_procs (False, int, None) + Maximum number of processes the user is allowed to have active at the same time. + + Value between 3 and 32,767. + + A value of 0 will delete this field from the profile. + + + max_threads (False, int, None) + Maximum number of threads the user can have concurrently active. + + Value between 0 and 100,000. + + A value of -1 will delete this field from the profile. + + + max_cpu_time (False, int, None) + Specifies the RLIMIT\_CPU hard limit. Indicates the cpu-time that a user process is allowed to use. + + Value between 7 and 2,147,483,647 seconds. + + A value of 0 will delete this field from the profile. + + + max_files (False, int, None) + Maximum number of files the user is allowed to have concurrently active or open. + + Value between 3 and 524,287. + + A value of 0 will delete this field from the profile. + + + delete (False, bool, None) + Delete the whole OMVS block from the profile. + + This option is only valid when updating profiles, it will be ignored when creating one. + + This option is mutually exclusive with every other option in this section. + + + + tso (False, dict, None) + Attributes for how TSO should handle a user profile. + + + account_num (False, int, None) + User's default TSO account number when logging in. + + Value between 3 and 524,287. + + A value of 0 will delete this field from the profile. + + + logon_cmd (False, str, None) + Command that needs to be run during TSO/E logon. + + Maximum length of 80 characters. + + This option keeps case. + + An empty value deletes this field. + + + logon_proc (False, str, None) + User's default logon procedure. + + The value for this field is 1 to 8 alphanumeric characters. + + An empty value deletes this field. + + + dest_id (False, str, None) + Default destination to which the user can route dynamically allocated SYSOUT data sets. + + The value for this field is 1 to 7 alphanumeric characters. + + An empty value deletes this field. + + + hold_class (False, str, None) + User's default hold class. + + This option consists of 1 alphanumeric character. + + An empty value deletes this field. + + + job_class (False, str, None) + User's default job class. + + This option consists of 1 alphanumeric character. + + An empty value deletes this field. + + + msg_class (False, str, None) + User's default message class. + + This option consists of 1 alphanumeric character. + + An empty value deletes this field. + + + sysout_class (False, str, None) + User's default SYSOUT class. + + This option consists of 1 alphanumeric character. + + An empty value deletes this field. + + + region_size (False, int, None) + Minimum region size if the user does not request a region size at logon. + + A value between 0 and 2,096,128. + + A value of -1 deletes this field. + + + max_region_size (False, int, None) + Maximum region size that the user can request at logon. + + A value between 0 and 2,096,128. + + A value of -1 deletes this field. + + + security_label (False, str, None) + User's security label if the user specifies one on the TSO logon panel. + + An empty value deletes this field. + + + unit_name (False, str, None) + Default name of a device or group of devices that a procedure uses for allocations. + + The value for this field is 1 to 8 alphanumeric characters. + + An empty value deletes this field. + + + user_data (False, str, None) + Optional installation data defined for the user profile. + + Must be 4 EBCDIC characters. + + An empty value deletes this field. + + + delete (False, bool, None) + Delete the whole TSO block from the profile. + + This option is only valid when updating profiles, it will be ignored when creating one. + + This option is mutually exclusive with every other option in this section. + + + + connect (False, dict, None) + Options that configure what a user can do inside a group that is connected to. + + These options are only used when :literal:`operation=connect` and they are ignored otherwise. + + + authority (False, str, None) + Level of group authority given to a user profile. + + + universal_access (False, str, None) + Level of universal access authority given to a user profile. + + + group_name (False, str, None) + Group to which the user will be connected to. + + The rest of the options in this block will affect this group. + + If not supplied, RACF will use a default group. It is recommended to specify this option when trying to connect a user to a group. + + + group_account (False, bool, None) + Whether the user's protected data sets are accessible to other users in the group. + + + group_operations (False, bool, None) + Whether a user should have the group-OPERATIONS attribute when connected to a group. + + + auditor (False, bool, None) + Whether a user should have auditor privileges for the group it is connected to. + + + adsp_attribute (False, bool, None) + Whether to give a user the ADSP attribute, which tells RACF to automatically protect data sets it creates with discrete profiles. + + + special (False, bool, None) + Whether to give a user profile the SPECIAL attribute. + + This attribute lets a user change attributes of other profiles. Use with caution. + + + + access (False, dict, None) + Options that set different security attributes in a user profile. + + + default_group (False, str, None) + RACF's default group for the user profile. + + + clauth (False, dict, None) + Classes in which a user is allowed to define profiles to RACF for protection. + + + add (False, list, None) + Adds classes to the profile. + + + delete (False, list, None) + Removes classes from the profile. + + + + roaudit (False, bool, None) + Whether a user should have full responsibility for auditing the use of system resources. + + + category (False, dict, None) + Security categories that the profile should have. + + + add (False, list, None) + Adds security categories to the profile. + + + delete (False, list, None) + Removes security categories from the profile. + + + + operator_card (False, bool, None) + Whether a user must supply an operator identification card when logging in. + + + maintenance_access (False, bool, None) + Whether the user has authorization to do maintenance operations on all RACF-protected DASD data sets, tape volumes, and DASD volumes. + + + restricted (False, bool, None) + Whether to give the profile the RESTRICTED attribute. + + + security_label (False, str, None) + Security label applied to the profile. + + Empty value deletes this field. + + + security_level (False, str, None) + Security level applied to the profile. + + Empty value deletes this field. + + + + operator (False, dict, None) + Attributes used when a user establishes an extended MCS console session. + + + alt_group (False, str, None) + Console group used in recovery. + + Must be between 1 and 8 characters in length. + + Empty value deletes this field. + + + authority (False, str, None) + Console's authority to issue operator commands. + + :literal:`delete` will remove the field from the profile. + + + cmd_system (False, str, None) + System to which commands from this console are to be sent. + + Must be between 1 and 8 characters in length. + + Empty value deletes this field. + + + search_key (False, str, None) + Name used to display information for all consoles with the specified key by using the MVS command :literal:`DISPLAY CONSOLES,KEY`. + + Must be between 1 and 8 characters in length. + + Empty value deletes this field. + + + migration_id (False, bool, None) + Whether a 1-byte migration ID should be assigned to this console. + + + display (False, str, ['jobnames', 'sess']) + Which information should be displayed when monitoring jobs, TSO sessions, or data set status. + + Possible values are :literal:`jobnames`\ , :literal:`jobnamest`\ , :literal:`sess`\ , :literal:`sesst`\ , :literal:`status` and :literal:`delete`. + + Multiple choices are allowed. + + :literal:`delete` will remove this field from the profile. + + + msg_level (False, str, None) + Specifies the messages that this console is to receive. + + :literal:`delete` will remove this field from the profile. + + + msg_format (False, str, None) + Format in which messages are displayed at the console. + + :literal:`delete` will remove this field from the profile. + + + msg_storage (False, int, None) + Specifies the amount of storage in the TSO/E user's address space that can be used for message queuing to the console. + + Its value can be a number between 1 and 2,000. + + A value of 0 deletes this field. + + + msg_scope (False, dict, None) + Systems from which this console can receive messages that are not directed to a specific console. + + + add (False, list, None) + Add new systems to this field. + + + remove (False, list, None) + Removes systems from this field. + + + delete (False, bool, None) + Deletes this field from the profile. + + Mutually exclusive with the rest of the options in this section. + + + + automated_msgs (False, bool, None) + Whether the extended console can receive messages that have been automated by the MFP. + + + del_msgs (False, str, None) + Which delete operator message (DOM) requests the console can receive. + + :literal:`delete` will remove the field from the profile. + + + hardcopy_msgs (False, bool, None) + Whether the console should receive all messages that are directed to hardcopy. + + + internal_msgs (False, bool, None) + Whether the console should receive messages that are directed to console ID zero. + + + routing_msgs (False, list, None) + Specifies the routing codes of messages this operator is to receive. + + :literal:`ALL` can be specified to receive all codes. Conversely, :literal:`NONE` can be used to receive none. + + + undelivered_msgs (False, bool, None) + Whether the console should receive undelivered messages. + + + unknown_msgs (False, bool, None) + Whether the console should receive messages that are directed to unknown console IDs. + + + responses (False, bool, None) + Whether command responses should be logged. + + + delete (False, bool, None) + Delete the whole OPERPARM block from the profile. + + This option is only valid when updating profiles, it will be ignored when creating one. + + This option is mutually exclusive with every other option in this section. + + + + restrictions (False, dict, None) + Attributes that determine the days and times a user is allowed to login. + + + days (False, list, None) + Days of the week that a user is allowed to login. + + Multiple choices are allowed. + + Valid values are :literal:`anyday`\ , :literal:`weekdays`\ , :literal:`monday`\ , :literal:`tuesday`\ , :literal:`wednesday`\ , :literal:`thursday`\ , :literal:`friday`\ , :literal:`saturday` and :literal:`sunday`. + + + time (False, str, None) + Daily time period when the user is allowed to login. + + The value for this option must be in the format HHMM:HHMM. + + This field uses a 24-hour format. + + This field also accepts the value :literal:`anytime` to indicate a user is free to login at any time of the day. + + + resume (False, str, None) + Date when the user is allowed access to a system again. + + The value for this option must be in the format MM/DD/YY, where :literal:`YY` are the last two digits of the year. + + + delete_resume (False, bool, None) + Delete the resume field from the profile. + + This option is only valid when connecting a user to a group. + + This option is mutually exclusive with :emphasis:`resume`. + + + revoke (False, str, None) + Date when the user is forbidden access to a system. + + The value for this option must be in the format MM/DD/YY, where :literal:`YY` are the last two digits of the year. + + + delete_revoke (False, bool, None) + Delete the revoke field from the profile. + + This option is only valid when connecting a user to a group. + + This option is mutually exclusive with :emphasis:`revoke`. + + + + + + + + +See Also +-------- + +.. seealso:: + + :ref:`zos_tso_command_module` + The official documentation on the **zos_tso_command** module. + + +Examples +-------- + +.. code-block:: yaml+jinja + + + - name: Create a new group profile using RACF defaults. + zos_user: + name: newgrp + operation: create + scope: group + + - name: Create a new group profile using another group as a model and setting its owner. + zos_user: + name: newgrp + operation: create + scope: group + general: + model: oldgrp + owner: admin + + - name: Create a new group profile and set group attributes. + zos_user: + name: newgrp + operation: create + scope: group + group: + superior_group: sys1 + terminal_access: true + universal_group: false + + - name: Update a group profile to change its installation data and remove custom fields. + zos_user: + name: usergrp + operation: update + scope: group + general: + installation_data: New installation data + custom_fields: + delete_block: true + + - name: Create a user using RACF defaults. + zos_user: + name: newuser + operation: create + scope: user + + - name: Create a user using another profile as a model. + zos_user: + name: newuser + operation: create + scope: user + general: + model: olduser + + - name: Create a user and set how Unix System Services should behave when it logs in. + zos_user: + name: newuser + operation: create + scope: user + omvs: + uid: auto + home: /u/newuser + program: /bin/sh + nonshared_size: '10g' + shared_size: '10g' + addr_space_size: 10485760 + map_size: 2056 + max_procs: 16 + max_threads: 150 + max_cpu_time: 4096 + max_files: 4096 + + - name: Create a user and set access permissions to it. + zos_user: + name: newuser + operation: create + scope: user + access: + default_group: usergrp + roaudit: true + operator_card: false + maintenance_access: true + restricted: false + restrictions: + days: + - monday + - tuesday + - wednesday + time: anytime + + - name: Update a user profile to change its TSO attributes and owner. + zos_user: + name: user + operation: create + scope: user + general: + owner: admin + tso: + hold_class: K + job_class: K + msg_class: K + sysout_class: K + region_size: 2048 + max_region_size: 4096 + + - name: Connect a user to a group using RACF defaults. + zos_user: + name: user + operation: connect + scope: user + connect: + group_name: usergrp + + - name: Connect a user to a group and give it special permissions. + zos_user: + name: user + operation: connect + scope: user + connect: + group_name: usergrp + authority: connect + universal_access: alter + group_account: true + group_operations: true + auditor: true + adsp_attribute: true + special: true + + - name: Remove a user from a group. + zos_user: + name: user + operation: remove + scope: user + connect: + group_name: usergrp + + - name: Delete a user from the RACF database. + zos_user: + name: user + operation: delete + scope: user + + - name: Delete group from the RACF database. + zos_user: + name: usergrp + operation: delete + scope: group + + + +Return Values +------------- + +operation (always, str, create) + Operation that was performed by the module. + + +racf_command (success, str, DELUSER (user)) + Full command string that was executed with tsocmd. + + +num_entities_modified (always, int, 1) + Number of profiles and references modified by the operation. + + +entities_modified (success, list, ['user']) + List of all profiles and references modified by the operation. + + +database_dumped (always, bool, False) + Whether the module used IRRRID00 to dump the RACF database. + + +dump_kept (always, bool, False) + Whether the RACF database dump was kept on the managed node. + + +dump_name (success, str, USER.BACKUP.RACF.DATABASE) + Name of the database containing the output from the IRRRID00 utility. + + + + + +Status +------ + + + + + +Authors +~~~~~~~ + +- Alex Moreno (@rexemin) + diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index 21d531891e..c3fd3012c7 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -244,8 +244,8 @@ description: - Maximum number of bytes of nonshared memory that can be allocated by the user. - - Must be a number between 0 and 16,777,215 subfixed by one of the - following units: m (megabytes), g (gigabytes), t (terabytes) or + - Must be a number between 0 and 16,777,215 subfixed by a unit. + - Valid units are m (megabytes), g (gigabytes), t (terabytes) or p (petabytes). - An empty string will delete the current limit set. type: str @@ -254,8 +254,8 @@ description: - Maximum number of bytes of shared memory that can be allocated by the user. - - Must be a number between 1 and 16,777,215 subfixed by one of the - following units: m (megabytes), g (gigabytes), t (terabytes) or + - Must be a number between 1 and 16,777,215 subfixed by a unit. + - Valid units are m (megabytes), g (gigabytes), t (terabytes) or p (petabytes). - An empty string will delete the current limit set. type: str @@ -821,8 +821,6 @@ support: full description: Can run in check_mode and return changed status prediction without modifying target. If not supported, the action will be skipped. -notes: - seealso: - module: zos_tso_command """ @@ -1958,6 +1956,7 @@ def _connect_user(self): rc, stdout, stderr = self.module.run_command(f""" tsocmd "{cmd}" """) + # TODO: change this to include group. if rc == 0: self.num_entities_modified = 1 self.entities_modified = [self.name] @@ -1986,6 +1985,7 @@ def _remove_user(self): rc, stdout, stderr = self.module.run_command(f""" tsocmd "{cmd}" """) + # TODO: change this to include group. if rc == 0: self.num_entities_modified = 1 self.entities_modified = [self.name] From 533ef4866921605212009cd50101e8e1bf667eb5 Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Wed, 22 Oct 2025 17:46:13 -0600 Subject: [PATCH 28/30] Add purge operation --- plugins/modules/zos_user.py | 210 +++++++++++++++++++++++++++++++++++- 1 file changed, 205 insertions(+), 5 deletions(-) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index c3fd3012c7..b9fb299884 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -25,8 +25,7 @@ description: - The L(zos_user,./zos_user.html) module executes RACF TSO commands that can manage user and group RACF profiles. - - The module can create, update and delete RACF profiles, as well as list information - about them. + - The module can create, update and delete RACF profiles about them. options: name: description: @@ -38,7 +37,7 @@ operation: description: - RACF command that will be executed. - - Group profiles can be created, updated, listed, deleted and purged. + - Group profiles can be created, updated, deleted and purged. - User profiles can use any of the choices. - C(delete) will run a RACF C(DELGROUP) or a C(DELUSER) TSO command. This will remove the profile but not every reference in the RACF database. @@ -51,7 +50,6 @@ choices: - create - update - - list - delete - purge - connect @@ -1010,7 +1008,10 @@ """ import copy +import math +import os import re +import tempfile import traceback from ansible.module_utils.basic import AnsibleModule @@ -1018,6 +1019,15 @@ better_arg_parser ) +from ansible_collections.ibm.ibm_zos_core.plugins.module_utils.import_handler import \ + ZOAUImportError + +try: + from zoautil_py import datasets, mvscmd, ztypes +except Exception: + datasets = ZOAUImportError(traceback.format_exc()) + mvscmd = ZOAUImportError(traceback.format_exc()) + ztypes = ZOAUImportError(traceback.format_exc()) def dynamic_dict(contents, dependencies): """Validates options that are YAML dictionaries created by a user in a task. @@ -1192,6 +1202,9 @@ def __init__(self, module, module_params): self.name = module_params['name'] self.operation = module_params['operation'] self.scope = module_params['scope'] + self.database = module_params['database'] + self.keep_dump = module_params['keep_dump'] + self.optimize_dump = module_params['optimize_dump'] # Nested params. params_copy = copy.deepcopy(module_params) del params_copy['name'] @@ -1470,6 +1483,172 @@ def _make_dfp_substring(self): return cmd + def purge_profile(self): + # First step: run the IRRUT200 utility. + # Getting the total allocation for the database. + # TODO: put inside a try/catch block. + database_listing = datasets.list_datasets(self.database)[0] + database_total_space = database_listing.total_space + + # Putting the input commands for IRRUT200 inside a text file. + sysin_file = tempfile.mkstemp() + with open(sysin_file[0], mode='w', encoding='cp1047') as filepath: + filepath.write("MAP\nEND") + + # TODO: use a temp HLQ. + backup_name = datasets.tmp_name() + + irrut200_dds = [ + ztypes.DDStatement('SYSRACF', ztypes.DatasetDefinition( + self.database, + disposition='SHR' + )), + ztypes.DDStatement('SYSUT1', ztypes.DatasetDefinition( + backup_name, + type='SEQ', + disposition='NEW', + normal_disposition='DELETE', + abnormal_disposition='DELETE', + device_unit='SYSDA', + primary=10, + primary_unit='CYL', + secondary=0, + secondary_unit='CYL', + record_format='F', + record_length=4096, + block_size=20480 + )), + ztypes.DDStatement('SYSUT2', '*'), + ztypes.DDStatement('SYSPRINT', '*'), + ztypes.DDStatement('SYSIN', ztypes.FileDefinition(sysin_file[1])) + ] + irrut200_response = mvscmd.execute_authorized('IRRUT200', dds=irrut200_dds) + percent_used_search = re.search( + r'(RACF DATA SET IS\s*)(\d+)(\s*PERCENT FULL)', + irrut200_response.stdout_response + ) + percent_used = int(percent_used_search[2]) + database_used_space = math.ceil((database_total_space * percent_used) / 100) + + # Cleaning up. + os.remove(sysin_file[1]) + datasets.delete(backup_name) + + # Second step: run the IRRDBU00 utility. + # TODO: put inside a try/catch block. + dump_data_set = datasets.tmp_name() + irrdbu00_dds = [ + ztypes.DDStatement('SYSPRINT', '*'), + ztypes.DDStatement('INDD1', ztypes.DatasetDefinition( + self.database, + disposition='SHR' + )), + # TODO: change size attributes + ztypes.DDStatement('OUTDD', ztypes.DatasetDefinition( + dump_data_set, + type='SEQ', + disposition='NEW', + normal_disposition='CATALOG', + abnormal_disposition='DELETE', + device_unit='SYSDA', + primary=150, + primary_unit='CYL', + secondary=50, + secondary_unit='CYL', + record_format='VB', + record_length=4096, + block_size=20480 + )) + ] + lock_input = 'NOLOCKINPUT' if self.optimize_dump else 'LOCKINPUT' + irrdbu00_response = mvscmd.execute_authorized('IRRDBU00', lock_input, dds=irrdbu00_dds) + + # Third step: run IRRRID00. + # TODO: put inside a try/catch block. + # Putting the profile we want to search for in a text file. + sysin_name = datasets.tmp_name() + sysin_data_set = datasets.create( + sysin_name, + 'SEQ', + record_format='FB', + record_length=80 + ) + datasets.write(sysin_name, self.name, append=False) + + clist = datasets.tmp_name() + irrrid00_dds = [ + ztypes.DDStatement('SYSPRINT', '*'), + ztypes.DDStatement('SYSOUT', '*'), + ztypes.DDStatement('SORTOUT', ztypes.DatasetDefinition( + datasets.tmp_name(), + type='SEQ', + disposition='NEW', + normal_disposition='DELETE', + abnormal_disposition='DELETE', + device_unit='SYSDA', + primary=5, + primary_unit='CYL', + secondary=5, + secondary_unit='CYL', + record_format='VB', + record_length=4096, + block_size=20480 + )), + ztypes.DDStatement('SYSUT1', ztypes.DatasetDefinition( + datasets.tmp_name(), + type='SEQ', + disposition='NEW', + normal_disposition='DELETE', + abnormal_disposition='DELETE', + device_unit='SYSDA', + primary=3, + primary_unit='CYL', + secondary=5, + secondary_unit='CYL' + )), + ztypes.DDStatement('INDD', ztypes.DatasetDefinition( + dump_data_set, + disposition='OLD' + )), + ztypes.DDStatement('OUTDD', ztypes.DatasetDefinition( + clist, + type='SEQ', + disposition='NEW', + normal_disposition='CATALOG', + abnormal_disposition='DELETE', + device_unit='SYSDA', + primary=150, + primary_unit='CYL', + secondary=50, + secondary_unit='CYL', + record_format='VB', + record_length=259, + block_size=1036 + )), + ztypes.DDStatement('SYSIN', ztypes.DatasetDefinition( + sysin_name, + disposition='SHR' + )) + ] + irrrid00_response = mvscmd.execute('IRRRID00', dds=irrrid00_dds) + + # TODO: update entitied modified + cmd = f"EXEC '{clist}'" + rc, stdout, stderr = self.module.run_command(f"""tsocmd "{cmd}" """) + + # Cleaning up. + datasets.delete(sysin_name) + if not self.keep_dump: + datasets.delete(clist) + datasets.delete(dump_data_set) + + self.database_dumped = True + self.dump_kept = self.keep_dump + self.dump_name = dump_data_set + + return rc, stdout, stderr, cmd + + class GroupHandler(RACFHandler): """Subclass containing all information needed to clean, validate and execute @@ -1553,6 +1732,8 @@ def execute_operation(self): rc, stdout, stderr, cmd = self._update_group() if self.operation == 'delete': rc, stdout, stderr, cmd = self._delete_group() + if self.operation == 'purge': + rc, stdout, stderr, cmd = self.purge_profile() self.cmd = cmd # Getting the base dictionary. @@ -1816,6 +1997,8 @@ def execute_operation(self): rc, stdout, stderr, cmd = self._update_user() if self.operation == 'delete': rc, stdout, stderr, cmd = self._delete_user() + if self.operation == 'purge': + rc, stdout, stderr, cmd = self.purge_profile() if self.operation == 'connect': rc, stdout, stderr, cmd = self._connect_user() if self.operation == 'remove': @@ -2442,13 +2625,25 @@ def run_module(): 'operation': { 'type': 'str', 'required': True, - 'choices': ['create', 'list', 'update', 'delete', 'purge', 'connect', 'remove'] + 'choices': ['create', 'update', 'delete', 'purge', 'connect', 'remove'] }, 'scope': { 'type': 'str', 'required': True, 'choices': ['user', 'group'] }, + 'database': { + 'type': 'str', + 'required': False + }, + 'keep_dump': { + 'type': 'bool', + 'default': False + }, + 'optimize_dump': { + 'type': 'bool', + 'default': True + }, 'general': { 'type': 'dict', 'required': False, @@ -3013,6 +3208,8 @@ def run_module(): } } }, + # Require database when operation=purge. + required_if=[('operation', 'purge', ('database',))], supports_check_mode=True ) @@ -3020,6 +3217,9 @@ def run_module(): 'name': {'arg_type': 'str', 'required': True, 'aliases': ['src']}, 'operation': {'arg_type': 'str', 'required': True}, 'scope': {'arg_type': 'str', 'required': True}, + 'database': {'arg_type': 'str', 'required': False}, + 'keep_dump': {'arg_type': 'bool', 'required': True}, + 'optimize_dump': {'arg_type': 'bool', 'required': True}, 'general': { 'arg_type': 'dict', 'required': False, From fd7cb2994ef3c8554c573bdfed856bf8420f5aca Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Wed, 29 Oct 2025 09:29:46 -0600 Subject: [PATCH 29/30] Modify purge operation --- plugins/modules/zos_user.py | 323 +++++++++++++++++++----------------- 1 file changed, 172 insertions(+), 151 deletions(-) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index b9fb299884..ba460d41fb 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -1015,6 +1015,7 @@ import traceback from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_text from ansible_collections.ibm.ibm_zos_core.plugins.module_utils import ( better_arg_parser ) @@ -1486,168 +1487,188 @@ def _make_dfp_substring(self): def purge_profile(self): # First step: run the IRRUT200 utility. # Getting the total allocation for the database. - # TODO: put inside a try/catch block. - database_listing = datasets.list_datasets(self.database)[0] - database_total_space = database_listing.total_space - - # Putting the input commands for IRRUT200 inside a text file. - sysin_file = tempfile.mkstemp() - with open(sysin_file[0], mode='w', encoding='cp1047') as filepath: - filepath.write("MAP\nEND") - - # TODO: use a temp HLQ. - backup_name = datasets.tmp_name() - - irrut200_dds = [ - ztypes.DDStatement('SYSRACF', ztypes.DatasetDefinition( - self.database, - disposition='SHR' - )), - ztypes.DDStatement('SYSUT1', ztypes.DatasetDefinition( - backup_name, - type='SEQ', - disposition='NEW', - normal_disposition='DELETE', - abnormal_disposition='DELETE', - device_unit='SYSDA', - primary=10, - primary_unit='CYL', - secondary=0, - secondary_unit='CYL', - record_format='F', - record_length=4096, - block_size=20480 - )), - ztypes.DDStatement('SYSUT2', '*'), - ztypes.DDStatement('SYSPRINT', '*'), - ztypes.DDStatement('SYSIN', ztypes.FileDefinition(sysin_file[1])) - ] - irrut200_response = mvscmd.execute_authorized('IRRUT200', dds=irrut200_dds) - percent_used_search = re.search( - r'(RACF DATA SET IS\s*)(\d+)(\s*PERCENT FULL)', - irrut200_response.stdout_response - ) - percent_used = int(percent_used_search[2]) - database_used_space = math.ceil((database_total_space * percent_used) / 100) - - # Cleaning up. - os.remove(sysin_file[1]) - datasets.delete(backup_name) + try: + # TODO: use a temp HLQ. + backup_name = datasets.tmp_name() + sysin_file = None + + if not datasets.exists(self.database): + return 1, "", f"The RACF database {self.database} does not exist. No purge was not performed.", None + database_listing = datasets.list_datasets(self.database)[0] + database_total_space = math.ceil(database_listing.total_space / 1000) + + # Putting the input commands for IRRUT200 inside a text file. + sysin_file = tempfile.mkstemp() + with open(sysin_file[0], mode='w', encoding='cp1047') as filepath: + filepath.write("MAP\nEND") + + irrut200_dds = [ + ztypes.DDStatement('SYSRACF', ztypes.DatasetDefinition( + self.database, + disposition='SHR' + )), + ztypes.DDStatement('SYSUT1', ztypes.DatasetDefinition( + backup_name, + type='SEQ', + disposition='NEW', + normal_disposition='DELETE', + abnormal_disposition='DELETE', + device_unit='SYSDA', + primary=database_total_space, + primary_unit='KB', + secondary=0, + secondary_unit='KB', + record_format='F', + record_length=4096, + block_size=20480 + )), + ztypes.DDStatement('SYSUT2', '*'), + ztypes.DDStatement('SYSPRINT', '*'), + ztypes.DDStatement('SYSIN', ztypes.FileDefinition(sysin_file[1])) + ] + irrut200_response = mvscmd.execute_authorized('IRRUT200', dds=irrut200_dds) + percent_used_search = re.search( + r'(RACF DATA SET IS\s*)(\d+)(\s*PERCENT FULL)', + irrut200_response.stdout_response + ) + percent_used = int(percent_used_search[2]) + database_used_space = math.ceil((database_total_space * percent_used) / 100) + except Exception as err: + return 1, "", f"An error ocurred while running the IRRUT200 utility: {traceback.format_exc()}", None + finally: + # Cleaning up. + if sysin_file and os.path.exists(sysin_file[1]): + os.remove(sysin_file[1]) + if datasets.exists(backup_name): + datasets.delete(backup_name) # Second step: run the IRRDBU00 utility. - # TODO: put inside a try/catch block. - dump_data_set = datasets.tmp_name() - irrdbu00_dds = [ - ztypes.DDStatement('SYSPRINT', '*'), - ztypes.DDStatement('INDD1', ztypes.DatasetDefinition( - self.database, - disposition='SHR' - )), - # TODO: change size attributes - ztypes.DDStatement('OUTDD', ztypes.DatasetDefinition( - dump_data_set, - type='SEQ', - disposition='NEW', - normal_disposition='CATALOG', - abnormal_disposition='DELETE', - device_unit='SYSDA', - primary=150, - primary_unit='CYL', - secondary=50, - secondary_unit='CYL', - record_format='VB', - record_length=4096, - block_size=20480 - )) - ] - lock_input = 'NOLOCKINPUT' if self.optimize_dump else 'LOCKINPUT' - irrdbu00_response = mvscmd.execute_authorized('IRRDBU00', lock_input, dds=irrdbu00_dds) + try: + # TODO: use a temp HLQ. + dump_data_set = datasets.tmp_name() + irrdbu00_dds = [ + ztypes.DDStatement('SYSPRINT', '*'), + ztypes.DDStatement('INDD1', ztypes.DatasetDefinition( + self.database, + disposition='SHR' + )), + ztypes.DDStatement('OUTDD', ztypes.DatasetDefinition( + dump_data_set, + type='SEQ', + disposition='NEW', + normal_disposition='CATALOG', + abnormal_disposition='DELETE', + device_unit='SYSDA', + primary=database_total_space, + primary_unit='KB', + secondary=math.ceil(database_total_space / 2), + secondary_unit='KB', + record_format='VB', + record_length=4096, + block_size=20480 + )) + ] + lock_input = 'NOLOCKINPUT' if self.optimize_dump else 'LOCKINPUT' + irrdbu00_response = mvscmd.execute_authorized('IRRDBU00', lock_input, dds=irrdbu00_dds) + except Exception as err: + return 1, "", f"An error ocurred while running the IRRDBU00 utility: {traceback.format_exc()}", None # Third step: run IRRRID00. - # TODO: put inside a try/catch block. # Putting the profile we want to search for in a text file. - sysin_name = datasets.tmp_name() - sysin_data_set = datasets.create( - sysin_name, - 'SEQ', - record_format='FB', - record_length=80 - ) - datasets.write(sysin_name, self.name, append=False) - - clist = datasets.tmp_name() - irrrid00_dds = [ - ztypes.DDStatement('SYSPRINT', '*'), - ztypes.DDStatement('SYSOUT', '*'), - ztypes.DDStatement('SORTOUT', ztypes.DatasetDefinition( - datasets.tmp_name(), - type='SEQ', - disposition='NEW', - normal_disposition='DELETE', - abnormal_disposition='DELETE', - device_unit='SYSDA', - primary=5, - primary_unit='CYL', - secondary=5, - secondary_unit='CYL', - record_format='VB', - record_length=4096, - block_size=20480 - )), - ztypes.DDStatement('SYSUT1', ztypes.DatasetDefinition( - datasets.tmp_name(), - type='SEQ', - disposition='NEW', - normal_disposition='DELETE', - abnormal_disposition='DELETE', - device_unit='SYSDA', - primary=3, - primary_unit='CYL', - secondary=5, - secondary_unit='CYL' - )), - ztypes.DDStatement('INDD', ztypes.DatasetDefinition( - dump_data_set, - disposition='OLD' - )), - ztypes.DDStatement('OUTDD', ztypes.DatasetDefinition( - clist, - type='SEQ', - disposition='NEW', - normal_disposition='CATALOG', - abnormal_disposition='DELETE', - device_unit='SYSDA', - primary=150, - primary_unit='CYL', - secondary=50, - secondary_unit='CYL', - record_format='VB', - record_length=259, - block_size=1036 - )), - ztypes.DDStatement('SYSIN', ztypes.DatasetDefinition( + try: + # TODO: use a temp HLQ. + sysin_name = datasets.tmp_name() + sysin_data_set = datasets.create( sysin_name, - disposition='SHR' - )) - ] - irrrid00_response = mvscmd.execute('IRRRID00', dds=irrrid00_dds) - - # TODO: update entitied modified - cmd = f"EXEC '{clist}'" - rc, stdout, stderr = self.module.run_command(f"""tsocmd "{cmd}" """) - - # Cleaning up. - datasets.delete(sysin_name) - if not self.keep_dump: - datasets.delete(clist) - datasets.delete(dump_data_set) + 'SEQ', + record_format='FB', + record_length=80, + device_unit='SYSDA', + primary=1, + primary_unit='KB', + secondary=0, + secondary_unit='KB', + ) + datasets.write(sysin_name, self.name, append=False) + + clist = datasets.tmp_name() + irrrid00_dds = [ + ztypes.DDStatement('SYSPRINT', '*'), + ztypes.DDStatement('SYSOUT', '*'), + ztypes.DDStatement('SORTOUT', ztypes.DatasetDefinition( + datasets.tmp_name(), + type='SEQ', + disposition='NEW', + normal_disposition='DELETE', + abnormal_disposition='DELETE', + device_unit='SYSDA', + primary=database_total_space, + primary_unit='KB', + secondary=math.ceil(database_total_space / 2), + secondary_unit='KB', + record_format='VB', + record_length=4096, + block_size=20480 + )), + ztypes.DDStatement('SYSUT1', ztypes.DatasetDefinition( + datasets.tmp_name(), + type='SEQ', + disposition='NEW', + normal_disposition='DELETE', + abnormal_disposition='DELETE', + device_unit='SYSDA', + primary=database_total_space, + primary_unit='KB', + secondary=math.ceil(database_total_space / 2), + secondary_unit='KB', + )), + ztypes.DDStatement('INDD', ztypes.DatasetDefinition( + dump_data_set, + disposition='OLD' + )), + ztypes.DDStatement('OUTDD', ztypes.DatasetDefinition( + clist, + type='SEQ', + disposition='NEW', + normal_disposition='CATALOG', + abnormal_disposition='DELETE', + device_unit='SYSDA', + primary=database_total_space, + primary_unit='KB', + secondary=math.ceil(database_total_space / 2), + secondary_unit='KB', + record_format='VB', + record_length=259, + block_size=1036 + )), + ztypes.DDStatement('SYSIN', ztypes.DatasetDefinition( + sysin_name, + disposition='SHR' + )) + ] + irrrid00_response = mvscmd.execute('IRRRID00', dds=irrrid00_dds) + + # TODO: update entities modified + # TODO: add noexec option to not execute the CLIST + cmd = f"EXEC '{clist}'" + # rc, stdout, stderr = self.module.run_command(f"""tsocmd "{cmd}" """) + except Exception as err: + return 1, "", f"An error ocurred while running the IRRRID00 utility: {traceback.format_exc()}", None + finally: + # TODO: fix clean up + # Cleaning up. + if datasets.exists(sysin_name): + datasets.delete(sysin_name) + # if not self.keep_dump: + # datasets.delete(clist) + # datasets.delete(dump_data_set) self.database_dumped = True self.dump_kept = self.keep_dump self.dump_name = dump_data_set - return rc, stdout, stderr, cmd - + # return rc, stdout, stderr, cmd + return 0, f"{dump_data_set}, {clist}", "", cmd class GroupHandler(RACFHandler): From 19ea2f96e938d41da78a360031a58aa6780640b0 Mon Sep 17 00:00:00 2001 From: Ivan Moreno Date: Wed, 29 Oct 2025 09:44:52 -0600 Subject: [PATCH 30/30] Print CLIST --- plugins/modules/zos_user.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/modules/zos_user.py b/plugins/modules/zos_user.py index ba460d41fb..fa97e615c0 100644 --- a/plugins/modules/zos_user.py +++ b/plugins/modules/zos_user.py @@ -1667,8 +1667,10 @@ def purge_profile(self): self.dump_kept = self.keep_dump self.dump_name = dump_data_set + rc, out, err = self.module.run_command(f"dcat {clist}") + # return rc, stdout, stderr, cmd - return 0, f"{dump_data_set}, {clist}", "", cmd + return 0, f"{dump_data_set}, {clist}: {out}", "", cmd class GroupHandler(RACFHandler):