diff --git a/README-sudocmdgroup.md b/README-sudocmdgroup.md new file mode 100644 index 0000000000000000000000000000000000000000..f24b2cc4ee7bfcd7ec7ac8e53a6b632b18d9f25a --- /dev/null +++ b/README-sudocmdgroup.md @@ -0,0 +1,137 @@ +Sudocmdgroup module +=================== + +Description +----------- + +The sudocmdgroup module allows to ensure presence and absence of sudocmdgroups and members of sudocmdgroups. + +The sudocmdgroup module is as compatible as possible to the Ansible upstream `ipa_sudocmdgroup` module, but additionally offers to make sure that sudocmds are present or absent in a sudocmdgroup. + + +Features +-------- +* Sudocmdgroup management + + +Supported FreeIPA Versions +-------------------------- + +FreeIPA versions 4.4.0 and up are supported by the ipasudocmdgroup module. + + +Requirements +------------ + +**Controller** +* Ansible version: 2.8+ + +**Node** +* Supported FreeIPA version (see above) + + +Usage +===== + +Example inventory file + +```ini +[ipaserver] +ipaserver.test.local +``` + + +Example playbook to make sure sudocmdgroup is present: + +```yaml +--- +- name: Playbook to handle sudocmdgroups + hosts: ipaserver + become: true + + tasks: + # Ensure sudocmdgroup is present + - ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: group01 + description: Group of important commands +``` + +Example playbook to make sure that a sudo command and sudocmdgroups are present in existing sudocmdgroup: + +```yaml +--- +- name: Playbook to handle sudocmdgroups + hosts: ipaserver + become: true + + tasks: + # Ensure sudo commands are present in existing sudocmdgroup + - ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: group01 + sudocmd: + - /usr/bin/su + - /usr/bin/less + action: member +``` +`action` controls if the sudocmdgroup or member will be handled. To add or remove members, set `action` to `member`. + +Example playbook to make sure that a sudo command and sudocmdgroups are absent in sudocmdgroup: + +```yaml +--- +- name: Playbook to handle sudocmdgroups + hosts: ipaserver + become: true + + tasks: + # Ensure sudocmds are absent in existing sudocmdgroup + - ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: group01 + sudocmd: + - /usr/bin/su + - /usr/bin/less + action: member + state: absent +``` + +Example playbook to make sure sudocmdgroup is absent: + +```yaml +--- +- name: Playbook to handle sudocmdgroups + hosts: ipaserver + become: true + + tasks: + # Ensure sudocmdgroup is absent + - ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: group01 + state: absent +``` + +Variables +========= + +ipasudocmdgroup +------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no +`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no +`name` \| `cn` | The list of sudocmdgroup name strings. | no +`description` | The sudocmdgroup description string. | no +`nomembers` | Suppress processing of membership attributes. (bool) | no +`sudocmd` | List of sudocmdgroup name strings assigned to this sudocmdgroup. | no +`action` | Work on sudocmdgroup or member level. It can be on of `member` or `sudocmdgroup` and defaults to `sudocmdgroup`. | no +`state` | The state to ensure. It can be one of `present` or `absent`, default: `present`. | no + + +Authors +======= + +Rafael Guterres Jeffman diff --git a/README.md b/README.md index aa07e288a7a6f6f8f7875bbf8925bfe72346c4ff..bc54c23cbd41773593ef0900eb513c91873bca58 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Features * Modules for host management * Modules for hostgroup management * Modules for sudocmd management +* Modules for sudocmdgroup management * Modules for topology management * Modules for user management @@ -393,6 +394,7 @@ Modules in plugin/modules * [ipahost](README-host.md) * [ipahostgroup](README-hostgroup.md) * [ipasudocmd](README-sudocmd.md) +* [ipasudocmdgroup](README-sudocmdgroup.md) * [ipatopologysegment](README-topology.md) * [ipatopologysuffix](README-topology.md) * [ipauser](README-user.md) diff --git a/playbooks/sudocmdgroup/ensure-sudocmd-are-absent-in-sudocmdgroup.yml b/playbooks/sudocmdgroup/ensure-sudocmd-are-absent-in-sudocmdgroup.yml new file mode 100644 index 0000000000000000000000000000000000000000..bde823e54ae63e31047bfcda7a92bf28b5267cc9 --- /dev/null +++ b/playbooks/sudocmdgroup/ensure-sudocmd-are-absent-in-sudocmdgroup.yml @@ -0,0 +1,15 @@ +--- +- name: Playbook to handle sudocmdgroups + hosts: ipaserver + become: true + + tasks: + # Ensure sudocmds are absent in sudocmdgroup + - ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + action: member + state: absent diff --git a/playbooks/sudocmdgroup/ensure-sudocmd-are-present-in-sudocmdgroup.yml b/playbooks/sudocmdgroup/ensure-sudocmd-are-present-in-sudocmdgroup.yml new file mode 100644 index 0000000000000000000000000000000000000000..c415695b0b27bae02d36311f9a727195a6d0db2f --- /dev/null +++ b/playbooks/sudocmdgroup/ensure-sudocmd-are-present-in-sudocmdgroup.yml @@ -0,0 +1,22 @@ +--- +- name: Playbook to handle sudocmdgroups + hosts: ipaserver + become: true + + tasks: + # Ensure sudo commands are present + - ipasudocmd: + ipaadmin_password: MyPassword123 + name: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + state: present + + # Ensure sudo commands are present in existing sudocmdgroup + - ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + action: member diff --git a/playbooks/sudocmdgroup/ensure-sudocmdgroup-is-absent.yml b/playbooks/sudocmdgroup/ensure-sudocmdgroup-is-absent.yml new file mode 100644 index 0000000000000000000000000000000000000000..7674e5decfd9d8fe0584777e6aed8872fff6b585 --- /dev/null +++ b/playbooks/sudocmdgroup/ensure-sudocmdgroup-is-absent.yml @@ -0,0 +1,12 @@ +--- +- name: Playbook to handle sudocmdgroups + hosts: ipaserver + become: true + + tasks: + # Ensure sudocmdgroup is absent + - ipasudocmdgroup: + ipaadmin_password: pass1234 + name: network + state: absent + action: sudocmdgroup diff --git a/playbooks/sudocmdgroup/ensure-sudocmdgroup-is-present.yml b/playbooks/sudocmdgroup/ensure-sudocmdgroup-is-present.yml new file mode 100644 index 0000000000000000000000000000000000000000..6809080c93a689eec29dc9c6ee56f50a73a3056f --- /dev/null +++ b/playbooks/sudocmdgroup/ensure-sudocmdgroup-is-present.yml @@ -0,0 +1,15 @@ +--- +- name: Playbook to handle sudocmdgroups + hosts: ipaserver + become: true + + tasks: + # Ensure sudocmdgroup sudocmds are present + - ipasudocmdgroup: + ipaadmin_password: pass1234 + name: network + description: Group of important commands. + sudocmd: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + state: present diff --git a/plugins/modules/ipasudocmdgroup.py b/plugins/modules/ipasudocmdgroup.py new file mode 100644 index 0000000000000000000000000000000000000000..bfa01300199d28f9ea48443baefc6e6db80727ce --- /dev/null +++ b/plugins/modules/ipasudocmdgroup.py @@ -0,0 +1,355 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Rafael Guterres Jeffman <rjeffman@redhat.com> +# +# Copyright (C) 2019 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +ANSIBLE_METADATA = { + "metadata_version": "1.0", + "supported_by": "community", + "status": ["preview"], +} + + +DOCUMENTATION = """ +--- +module: ipasudocmdgroup +short description: Manage FreeIPA sudocmd groups +description: Manage FreeIPA sudocmd groups +options: + ipaadmin_principal: + description: The admin principal + default: admin + ipaadmin_password: + description: The admin password + required: false + name: + description: The sudocmodgroup name + required: false + aliases: ["cn"] + description: + description: The sudocmdgroup description + required: false + nomembers: + description: Suppress processing of membership attributes + required: false + type: bool + sudocmdgroup: + description: List of sudocmdgroup names assigned to this sudocmdgroup. + required: false + type: list + sudocmd: + description: List of sudocmds assigned to this sudocmdgroup. + required: false + type: list + action: + description: Work on sudocmdgroup or member level + default: hostgroup + choices: ["member", "sudocmdgroup"] + state: + description: State to ensure + default: present + choices: ["present", "absent"] +author: + - Rafael Guterres Jeffman +""" + +EXAMPLES = """ +# Ensure sudocmd-group 'network' is present +- ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + state: present + +# Ensure sudocmdgroup and sudocmd are present in 'network' sudocmdgroup +- ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + action: member + +# Ensure sudocmdgroup and sudocmd are absent in 'network' sudocmdgroup +- ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + action: member + state: absent + +# Ensure sudocmd-group 'network' is absent +- ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + action: member + state: absent +""" + +RETURN = """ +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_text +from ansible.module_utils.ansible_freeipa_module import temp_kinit, \ + temp_kdestroy, valid_creds, api_connect, api_command, compare_args_ipa + + +def find_sudocmdgroup(module, name): + _args = { + "all": True, + "cn": to_text(name), + } + + _result = api_command(module, "sudocmdgroup_find", to_text(name), _args) + + if len(_result["result"]) > 1: + module.fail_json( + msg="There is more than one sudocmdgroup '%s'" % (name)) + elif len(_result["result"]) == 1: + return _result["result"][0] + else: + return None + + +def gen_args(description, nomembers): + _args = {} + if description is not None: + _args["description"] = to_text(description) + if nomembers is not None: + _args["nomembers"] = nomembers + + return _args + + +def gen_member_args(sudocmdgroup): + _args = {} + if sudocmdgroup is not None: + _args["member_sudocmdgroup"] = sudocmdgroup + + return _args + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # general + ipaadmin_principal=dict(type="str", default="admin"), + ipaadmin_password=dict(type="str", required=False, no_log=True), + + name=dict(type="list", aliases=["cn"], default=None, + required=True), + # present + description=dict(type="str", default=None), + nomembers=dict(required=False, type='bool', default=None), + sudocmdgroup=dict(required=False, type='list', default=None), + sudocmd=dict(required=False, type='list', default=None), + action=dict(type="str", default="sudocmdgroup", + choices=["member", "sudocmdgroup"]), + # state + state=dict(type="str", default="present", + choices=["present", "absent"]), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + + # Get parameters + + # general + ipaadmin_principal = ansible_module.params.get("ipaadmin_principal") + ipaadmin_password = ansible_module.params.get("ipaadmin_password") + names = ansible_module.params.get("name") + + # present + description = ansible_module.params.get("description") + nomembers = ansible_module.params.get("nomembers") + sudocmdgroup = ansible_module.params.get("sudocmdgroup") + sudocmd = ansible_module.params.get("sudocmd") + action = ansible_module.params.get("action") + # state + state = ansible_module.params.get("state") + + # Check parameters + + if state == "present": + if len(names) != 1: + ansible_module.fail_json( + msg="Only one sudocmdgroup can be added at a time.") + if action == "member": + invalid = ["description", "nomembers"] + for x in invalid: + if vars()[x] is not None: + ansible_module.fail_json( + msg="Argument '%s' can not be used with action " + "'%s'" % (x, action)) + + if state == "absent": + if len(names) < 1: + ansible_module.fail_json( + msg="No name given.") + invalid = ["description", "nomembers"] + if action == "sudocmdgroup": + invalid.extend(["sudocmd"]) + for x in invalid: + if vars()[x] is not None: + ansible_module.fail_json( + msg="Argument '%s' can not be used with state '%s'" % + (x, state)) + + # Init + + changed = False + exit_args = {} + ccache_dir = None + ccache_name = None + try: + if not valid_creds(ansible_module, ipaadmin_principal): + ccache_dir, ccache_name = temp_kinit(ipaadmin_principal, + ipaadmin_password) + api_connect() + + commands = [] + + for name in names: + # Make sure hostgroup exists + res_find = find_sudocmdgroup(ansible_module, name) + + # Create command + if state == "present": + # Generate args + args = gen_args(description, nomembers) + + if action == "sudocmdgroup": + # Found the hostgroup + if res_find is not None: + # For all settings is args, check if there are + # different settings in the find result. + # If yes: modify + if not compare_args_ipa(ansible_module, args, + res_find): + commands.append([name, "sudocmdgroup_mod", args]) + else: + commands.append([name, "sudocmdgroup_add", args]) + # Set res_find to empty dict for next step + res_find = {} + + member_args = gen_member_args(sudocmd) + if not compare_args_ipa(ansible_module, member_args, + res_find): + # Generate addition and removal lists + sudocmdgroup_add = list( + set(sudocmdgroup or []) - + set(res_find.get("member_sudocmdgroup", []))) + sudocmdgroup_del = list( + set(res_find.get("member_sudocmdgroup", [])) - + set(sudocmdgroup or [])) + + # Add members + if len(sudocmdgroup_add) > 0: + commands.append([name, "sudocmdgroup_add_member", + { + "sudocmd": [to_text(c) + for c in + sudocmdgroup_add] + } + ]) + # Remove members + if len(sudocmdgroup_del) > 0: + commands.append([name, + "sudocmdgroup_remove_member", + { + "sudocmd": [to_text(c) + for c in + sudocmdgroup_del] + } + ]) + elif action == "member": + if res_find is None: + ansible_module.fail_json( + msg="No sudocmdgroup '%s'" % name) + + # Ensure members are present + commands.append([name, "sudocmdgroup_add_member", + {"sudocmd": [to_text(c) for c in sudocmd]} + ]) + elif state == "absent": + if action == "sudocmdgroup": + if res_find is not None: + commands.append([name, "sudocmdgroup_del", {}]) + + elif action == "member": + if res_find is None: + ansible_module.fail_json( + msg="No sudocmdgroup '%s'" % name) + + # Ensure members are absent + commands.append([name, "sudocmdgroup_remove_member", + {"sudocmd": [to_text(c) for c in sudocmd]} + ]) + else: + ansible_module.fail_json(msg="Unkown state '%s'" % state) + + # Execute commands + for name, command, args in commands: + try: + result = api_command(ansible_module, command, to_text(name), + args) + if action == "member": + if "completed" in result and result["completed"] > 0: + changed = True + else: + if command == "sudocmdgroup_del": + changed |= "Deleted" in result['summary'] + elif command == "sudocmdgroup_add": + changed |= "Added" in result['summary'] + except Exception as e: + ansible_module.fail_json(msg="%s: %s: %s" % (command, name, + str(e))) + # Get all errors + # All "already a member" and "not a member" failures in the + # result are ignored. All others are reported. + errors = [] + if "failed" in result and "member" in result["failed"]: + failed = result["failed"]["member"] + for member_type in failed: + for member, failure in failed[member_type]: + if "already a member" not in failure \ + and "not a member" not in failure: + errors.append("%s: %s %s: %s" % ( + command, member_type, member, failure)) + if len(errors) > 0: + ansible_module.fail_json(msg=", ".join(errors)) + + except Exception as e: + ansible_module.fail_json(msg=str(e)) + + finally: + temp_kdestroy(ccache_dir, ccache_name) + + # Done + + ansible_module.exit_json(changed=changed, **exit_args) + + +if __name__ == "__main__": + main() diff --git a/tests/sudocmdgroup/test_sudocmdgroup.yml b/tests/sudocmdgroup/test_sudocmdgroup.yml new file mode 100644 index 0000000000000000000000000000000000000000..226e986f65e58bcde5fbef07096cbbb63800c592 --- /dev/null +++ b/tests/sudocmdgroup/test_sudocmdgroup.yml @@ -0,0 +1,164 @@ +--- + +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Ensure sudocmds are present + ipasudocmd: + ipaadmin_password: MyPassword123 + name: + - /usr/bin/su + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + state: present + + - name: Ensure sudocmdgroup is absent + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + state: absent + + - name: Ensure sudocmdgroup is present + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + state: present + register: result + failed_when: not result.changed + + - name: Ensure sudocmdgroup is present again + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + state: present + register: result + failed_when: result.changed + + - name: Ensure sudocmdgroup is absent + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + state: absent + register: result + failed_when: not result.changed + + - name: Ensure sudocmdgroup is absent again + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + state: absent + register: result + failed_when: result.changed + + - name: Ensure testing sudocmdgroup is present + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + state: present + register: result + failed_when: not result.changed + + - name: Ensure sudo commands are present in existing sudocmdgroup + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + action: member + register: result + failed_when: not result.changed + + - name: Ensure sudo commands are present in existing sudocmdgroup, again + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + action: member + register: result + failed_when: result.changed + + - name: Ensure sudo commands are absent in existing sudocmdgroup + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Ensure sudo commands are absent in existing sudocmdgroup, again + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + action: member + state: absent + register: result + failed_when: result.changed + + - name: Ensure sudo commands are present in sudocmdgroup + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + action: member + state: present + register: result + failed_when: not result.changed + + - name: Ensure one sudo command is not present in sudocmdgroup + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/ifconfig + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Ensure one sudo command is present in sudocmdgroup + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/ifconfig + action: member + state: present + register: result + failed_when: not result.changed + + - name: Ensure the other sudo command is not present in sudocmdgroup + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/iwlist + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Ensure the other sudo commandsis not present in sudocmdgroup, again + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/iwlist + action: member + state: absent + register: result + failed_when: result.changed