Skip to content
Snippets Groups Projects
Commit ba4a3605 authored by Thomas Woerner's avatar Thomas Woerner
Browse files

New idview management module.

There is a new idview management module placed in the plugins folder:

    plugins/modules/ipaidview.py

The idview module allows to ensure presence and absence of idviews and
idview host members.

Here is the documentation for the module:

    README-idview.md

New example playbooks have been added:

    playbooks/idview/idview-absent.yml
    playbooks/idview/idview-host-applied.yml
    playbooks/idview/idview-host-unapplied.yml
    playbooks/idview/idview-present.yml

New tests for the module can be found at:

    tests/idview/test_idview.yml
    tests/idview/test_idview_client_context.yml
parent cf779e43
Branches
Tags
No related merge requests found
Idview module
============
Description
-----------
The idview module allows to ensure presence and absence of idviews and idview host members.
Use Cases
---------
With ID views it is possible to override user or group attributes for users stored in the LDAP server. For example the login name, home directory, certificate for authentication or SSH keys. An ID view is client-side and specifies new values for user or group attributes and also the client host or hosts on which the values apply.
The ID view and the applied hosts are managed with idview, the user attributes are managed with idoverrideuser and the group attributes with idoverridegroup.
Features
--------
* Idview management
Supported FreeIPA Versions
--------------------------
FreeIPA versions 4.4.0 and up are supported by the ipaidview module.
Requirements
------------
**Controller**
* Ansible version: 2.13
**Node**
* Supported FreeIPA version (see above)
Usage
=====
Example inventory file
```ini
[ipaserver]
ipaserver.test.local
```
Example playbook to make sure idview "test_idview" is present:
```yaml
---
- name: Playbook to manage IPA idview.
hosts: ipaserver
become: false
tasks:
- ipaidview:
ipaadmin_password: SomeADMINpassword
name: test_idview
```
Example playbook to make sure idview "test_idview" member host "testhost.example.com" is present:
```yaml
---
- name: Playbook to manage IPA idview host member.
hosts: ipaserver
become: false
tasks:
- ipaidview:
ipaadmin_password: SomeADMINpassword
name: test_idview
host: testhost.example.com
action: member
```
Example playbook to make sure idview "test_idview" member host "testhost.example.com" is absent:
```yaml
---
- name: Playbook to manage IPA idview host member.
hosts: ipaserver
become: false
tasks:
- ipaidview:
ipaadmin_password: SomeADMINpassword
name: test_idview
host: testhost.example.com
action: member
state: absent
```
Example playbook to make sure idview "test_idview" is present with domain_resolution_order for "ad.example.com:ipa.example.com":
```yaml
---
- name: Playbook to manage IPA idview host member.
hosts: ipaserver
become: false
tasks:
- ipaidview:
ipaadmin_password: SomeADMINpassword
name: test_idview
domain_resolution_order: "ad.example.com:ipa.example.com"
```
Example playbook to make sure idview "test_idview" is absent:
```yaml
---
- name: Playbook to manage IPA idview.
hosts: ipaserver
become: false
tasks:
- ipaidview:
ipaadmin_password: SomeADMINpassword
name: test_idview
state: absent
```
Variables
---------
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
`ipaapi_context` | The context in which the module will execute. Executing in a server context is preferred. If not provided context will be determined by the execution environment. Valid values are `server` and `client`. | no
`ipaapi_ldap_cache` | Use LDAP cache for IPA connection. The bool setting defaults to true. (bool) | no
`name` \| `cn` | The list of idview name strings. | yes
`description` \| `desc` | The description string of the idview. | no
`domain_resolution_order` \| `ipadomainresolutionorder` | Colon-separated list of domains used for short name qualification. | no
`host` \| `hosts` | List of hosts to apply the ID View to. A host can only be applied to a single idview at any time. Applying a host that is already applied to a different idview will change the idview the host is applied to to the new one. | no
`rename` \| `new_name` | Rename the ID view object to the new name string. Only usable with `state: renamed`. | no
`delete_continue` \| `continue` | Continuous mode. Don't stop on errors. Valid only if `state` is `absent`. | no
`action` | Work on idview or member level. It can be on of `member` or `idview` and defaults to `idview`. | no
`state` | The state to ensure. It can be one of `present`, `absent` and `renamed`, default: `present`. | no
Authors
=======
Thomas Woerner
......@@ -31,6 +31,7 @@ Features
* Modules for host management
* Modules for hostgroup management
* Modules for idrange management
* Modules for idview management
* Modules for location management
* Modules for netgroup management
* Modules for permission management
......@@ -451,6 +452,7 @@ Modules in plugin/modules
* [ipahost](README-host.md)
* [ipahostgroup](README-hostgroup.md)
* [idrange](README-idrange.md)
* [idview](README-idview.md)
* [ipalocation](README-location.md)
* [ipanetgroup](README-netgroup.md)
* [ipapermission](README-permission.md)
......
---
- name: Idview absent example
hosts: ipaserver
become: no
tasks:
- name: Ensure idview test_idview is absent
ipaidview:
ipaadmin_password: SomeADMINpassword
name: test_idview
state: absent
---
- name: Idview host member applied example
hosts: ipaserver
become: no
tasks:
- name: Ensure host testhost.example.com is applied to idview test_idview
ipaidview:
ipaadmin_password: SomeADMINpassword
name: test_idview
host: testhost.example.com
action: member
---
- name: Idview host member unapplied example
hosts: ipaserver
become: no
tasks:
- name: Ensure host testhost.example.com is not applied to idview test_idview
ipaidview:
ipaadmin_password: SomeADMINpassword
name: test_idview
host: testhost.example.com
action: member
state: absent
---
- name: Idview present example
hosts: ipaserver
become: no
tasks:
- name: Ensure idview test_idview is present
ipaidview:
ipaadmin_password: SomeADMINpassword
name: test_idview
# -*- coding: utf-8 -*-
# Authors:
# Thomas Woerner <twoerner@redhat.com>
#
# Copyright (C) 2023 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/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {
"metadata_version": "1.0",
"supported_by": "community",
"status": ["preview"],
}
DOCUMENTATION = """
---
module: ipaidview
short_description: Manage FreeIPA idview
description: Manage FreeIPA idview and idview host members
extends_documentation_fragment:
- ipamodule_base_docs
options:
name:
description: The list of idview name strings.
required: true
type: list
elements: str
aliases: ["cn"]
description:
description: Description
required: False
type: str
aliases: ["desc"]
domain_resolution_order:
description: |
Colon-separated list of domains used for short name qualification
required: False
type: str
aliases: ["ipadomainresolutionorder"]
host:
description: Hosts to apply the ID View to
required: False
type: list
elements: str
aliases: ["hosts"]
rename:
description: Rename the ID view object
required: False
type: str
aliases: ["new_name"]
delete_continue:
description: |
Continuous mode. Don't stop on errors.
Valid only if `state` is `absent`.
required: false
type: bool
aliases: ["continue"]
action:
description: Work on idview or member level.
choices: ["idview", "member"]
default: idview
type: str
state:
description: The state to ensure.
choices: ["present", "absent", "renamed"]
default: present
type: str
author:
- Thomas Woerner (@t-woerner)
"""
EXAMPLES = """
# Ensure idview test_idview is present
- ipaidview:
ipaadmin_password: SomeADMINpassword
name: test_idview
# name: Ensure host testhost.example.com is applied to idview test_idview
- ipaidview:
ipaadmin_password: SomeADMINpassword
name: test_idview
host: testhost.example.com
action: member
# Ensure host testhost.example.com is not applied to idview test_idview
- ipaidview:
ipaadmin_password: SomeADMINpassword
name: test_idview
host: testhost.example.com
action: member
state: absent
# Ensure idview "test_idview" is present with domain_resolution_order for
# "ad.example.com:ipa.example.com"
- ipaidview:
ipaadmin_password: SomeADMINpassword
name: test_idview
domain_resolution_order: "ad.example.com:ipa.example.com"
# Ensure idview test_idview is absent
- ipaidview:
ipaadmin_password: SomeADMINpassword
name: test_idview
state: absent
"""
RETURN = """
"""
from ansible.module_utils.ansible_freeipa_module import \
IPAAnsibleModule, compare_args_ipa, gen_add_del_lists, gen_add_list, \
gen_intersection_list
from ansible.module_utils import six
if six.PY3:
unicode = str
def find_idview(module, name):
"""Find if a idview with the given name already exist."""
try:
_result = module.ipa_command("idview_show", name, {"all": True})
except Exception: # pylint: disable=broad-except
# An exception is raised if idview name is not found.
return None
else:
return _result["result"]
def gen_args(description, domain_resolution_order):
_args = {}
if description is not None:
_args["description"] = description
if domain_resolution_order is not None:
_args["ipadomainresolutionorder"] = domain_resolution_order
return _args
def gen_member_args(host):
_args = {}
if host is not None:
_args["host"] = host
return _args
def main():
ansible_module = IPAAnsibleModule(
argument_spec=dict(
# general
name=dict(type="list", elements="str", required=True,
aliases=["cn"]),
# present
description=dict(type="str", required=False, aliases=["desc"]),
domain_resolution_order=dict(type="str", required=False,
aliases=["ipadomainresolutionorder"]),
host=dict(type="list", elements="str", required=False,
aliases=["hosts"], default=None),
rename=dict(type="str", required=False, aliases=["new_name"]),
delete_continue=dict(type="bool", required=False,
aliases=['continue'], default=None),
# action
action=dict(type="str", default="idview",
choices=["member", "idview"]),
# state
state=dict(type="str", default="present",
choices=["present", "absent", "renamed"]),
),
supports_check_mode=True,
)
ansible_module._ansible_debug = True
# Get parameters
# general
names = ansible_module.params_get("name")
# present
description = ansible_module.params_get("description")
domain_resolution_order = ansible_module.params_get(
"domain_resolution_order")
host = ansible_module.params_get("host")
rename = ansible_module.params_get("rename")
action = ansible_module.params_get("action")
# absent
delete_continue = ansible_module.params_get("delete_continue")
# state
state = ansible_module.params_get("state")
# Check parameters
invalid = []
if state == "present":
if len(names) != 1:
ansible_module.fail_json(
msg="Only one idview can be added at a time.")
invalid = ["delete_continue", "rename"]
if action == "member":
invalid += ["description", "domain_resolution_order"]
if state == "renamed":
if len(names) != 1:
ansible_module.fail_json(
msg="Only one idoverridegroup can be renamed at a time.")
if not rename:
ansible_module.fail_json(
msg="Rename is required for state: renamed.")
if action == "member":
ansible_module.fail_json(
msg="Action member can not be used with state: renamed.")
invalid = ["description", "domain_resolution_order", "host",
"delete_continue"]
if state == "absent":
if len(names) < 1:
ansible_module.fail_json(msg="No name given.")
invalid = ["description", "domain_resolution_order", "rename"]
if action == "idview":
invalid += ["host"]
ansible_module.params_fail_used_invalid(invalid, state, action)
# Init
changed = False
exit_args = {}
# Connect to IPA API
with ansible_module.ipa_connect():
commands = []
for name in names:
# Make sure idview exists
res_find = find_idview(ansible_module, name)
# add/del lists
host_add, host_del = [], []
# Create command
if state == "present":
# Generate args
args = gen_args(description, domain_resolution_order)
if action == "idview":
# Found the idview
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, "idview_mod", args])
else:
commands.append([name, "idview_add", args])
res_find = {}
member_args = gen_member_args(host)
if not compare_args_ipa(ansible_module, member_args,
res_find):
# Generate addition and removal lists
host_add, host_del = gen_add_del_lists(
host, res_find.get("appliedtohosts"))
elif action == "member":
if res_find is None:
ansible_module.fail_json(
msg="No idview '%s'" % name)
# Reduce add lists for host
# to new entries only that are not in res_find.
if host is not None:
host_add = gen_add_list(
host, res_find.get("appliedtohosts"))
elif state == "absent":
if action == "idview":
if res_find is not None:
commands.append(
[name, "idview_del",
{"continue": delete_continue or False}]
)
elif action == "member":
if res_find is None:
ansible_module.fail_json(
msg="No idview '%s'" % name)
# Reduce del lists of member_host
# to the entries only that are in res_find.
if host is not None:
host_del = gen_intersection_list(
host, res_find.get("appliedtohosts"))
elif state == "renamed":
if res_find is None:
ansible_module.fail_json(msg="No idview '%s'" % name)
else:
commands.append([name, 'idview_mod', {"rename": rename}])
else:
ansible_module.fail_json(msg="Unkown state '%s'" % state)
# Member management
# Add members
if host_add:
commands.append([name, "idview_apply", {"host": host_add}])
# Remove members
if host_del:
# idview_unapply does not have the idview name (cn) as an arg.
# It is removing the host from any idview it is applied to.
# But as we create the intersection with the list of hosts of
# the idview, we emulate the correct behaviour. But this means
# that there is no general idview_unapply like in the cli.
commands.append([None, "idview_unapply", {"host": host_del}])
# Execute commands
changed = ansible_module.execute_ipa_commands(commands)
# Done
ansible_module.exit_json(changed=changed, **exit_args)
if __name__ == "__main__":
main()
---
- name: Test idview
hosts: "{{ ipa_test_host | default('ipaserver') }}"
# It is normally not needed to set "become" to "true" for a module test.
# Only set it to true if it is needed to execute commands as root.
become: false
# Enable "gather_facts" only if "ansible_facts" variable needs to be used.
gather_facts: false
module_defaults:
ipahost:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
ipaidview:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
tasks:
- name: Get Domain from server name
ansible.builtin.set_fact:
ipaserver_domain: "{{ ansible_facts['fqdn'].split('.')[1:] | join('.') }}"
when: ipaserver_domain is not defined
- name: Set host1_fqdn .. host2_fqdn
ansible.builtin.set_fact:
host1_fqdn: "{{ 'host1.' + ipaserver_domain }}"
host2_fqdn: "{{ 'host2.' + ipaserver_domain }}"
# CLEANUP TEST ITEMS
- name: Hosts "{{ host1_fqdn }}" and "{{ host2_fqdn }}" are absent
ipahost:
hosts:
- name: "{{ host1_fqdn }}"
- name: "{{ host2_fqdn }}"
state: absent
- name: Ensure idview test1_idview, test2_idview and renamed_idview are absent
ipaidview:
name:
- test1_idview
- test2_idview
- renamed_idview
state: absent
# CREATE TEST ITEMS
- name: Hosts "{{ host1_fqdn }}" and "{{ host2_fqdn }}" are present
ipahost:
hosts:
- name: "{{ host1_fqdn }}"
force: true
- name: "{{ host2_fqdn }}"
force: true
register: result
failed_when: not result.changed or result.failed
# TESTS
- name: Ensure idview test1_idview is present
ipaidview:
name: test1_idview
register: result
failed_when: not result.changed or result.failed
- name: Ensure idview test1_idview is present again
ipaidview:
name: test1_idview
# Add needed parameters here
register: result
failed_when: result.changed or result.failed
- name: Ensure idview test2_idview is present
ipaidview:
name: test2_idview
register: result
failed_when: not result.changed or result.failed
- name: Ensure idview test2_idview is present again
ipaidview:
name: test2_idview
# Add needed parameters here
register: result
failed_when: result.changed or result.failed
- name: Rename test1_idview to renamed_idview
ipaidview:
name: test1_idview
rename: renamed_idview
state: renamed
register: result
failed_when: not result.changed or result.failed
# This task will fail as there is no idview to be renamed
- name: Rename test1_idview to renamed_idview, again
ipaidview:
name: test1_idview
rename: renamed_idview
state: renamed
register: result
failed_when: result.changed or (not result.failed and "No idview 'test1_idview'" not in result.msg)
- name: Rename renamed_idview back to to test1_idview
ipaidview:
name: renamed_idview
rename: test1_idview
state: renamed
register: result
failed_when: not result.changed or result.failed
- name: Ensure idview test1_idview is present with description
ipaidview:
name: test1_idview
description: "Test IDView"
register: result
failed_when: not result.changed or result.failed
- name: Ensure idview test1_idview is present with description, again
ipaidview:
name: test1_idview
description: "Test IDView"
register: result
failed_when: result.changed or result.failed
- name: Ensure idview test1_idview is present with empty description
ipaidview:
name: test1_idview
description: ""
register: result
failed_when: not result.changed or result.failed
- name: Ensure idview test1_idview is present with empty description, again
ipaidview:
name: test1_idview
description: ""
register: result
failed_when: result.changed or result.failed
- name: Ensure idview test1_idview is present with domain reolution order "{{ ipaserver_domain }}"
ipaidview:
name: test1_idview
domain_resolution_order: "{{ ipaserver_domain }}"
register: result
failed_when: not result.changed or result.failed
- name: Ensure idview test1_idview is present with domain reolution order "{{ ipaserver_domain }}", again
ipaidview:
name: test1_idview
domain_resolution_order: "{{ ipaserver_domain }}"
register: result
failed_when: result.changed or result.failed
- name: Ensure idview test1_idview is present with empty domain reolution order
ipaidview:
name: test1_idview
domain_resolution_order: ""
register: result
failed_when: not result.changed or result.failed
- name: Ensure idview test1_idview is present with empty domain reolution order, again
ipaidview:
name: test1_idview
domain_resolution_order: ""
register: result
failed_when: result.changed or result.failed
- name: Ensure host "{{ host1_fqdn }}" is applied to idview test1_idview
ipaidview:
name: test1_idview
host:
- "{{ host1_fqdn }}"
action: member
register: result
failed_when: not result.changed or result.failed
- name: Ensure host "{{ host1_fqdn }}" is applied to idview test1_idview, again
ipaidview:
name: test1_idview
host:
- "{{ host1_fqdn }}"
action: member
register: result
failed_when: result.changed or result.failed
- name: Ensure host "{{ host2_fqdn }}" is applied to idview test1_idview
ipaidview:
name: test1_idview
host:
- "{{ host2_fqdn }}"
action: member
register: result
failed_when: not result.changed or result.failed
- name: Ensure host "{{ host2_fqdn }}" is applied to idview test1_idview, again
ipaidview:
name: test1_idview
host:
- "{{ host2_fqdn }}"
action: member
register: result
failed_when: result.changed or result.failed
- name: Ensure hosts "{{ host1_fqdn }}" and "{{ host1_fqdn }}" are applied to idview test1_idview
ipaidview:
name: test1_idview
host:
- "{{ host1_fqdn }}"
- "{{ host2_fqdn }}"
action: member
register: result
failed_when: result.changed or result.failed
- name: Ensure hosts "{{ host1_fqdn }}" and "{{ host1_fqdn }}" are not applied to idview test1_idview
ipaidview:
name: test1_idview
host:
- "{{ host1_fqdn }}"
- "{{ host2_fqdn }}"
action: member
state: absent
register: result
failed_when: not result.changed or result.failed
- name: Ensure hosts "{{ host1_fqdn }}" and "{{ host1_fqdn }}" are not applied to idview test1_idview, again
ipaidview:
name: test1_idview
host:
- "{{ host1_fqdn }}"
- "{{ host2_fqdn }}"
action: member
state: absent
register: result
failed_when: result.changed or result.failed
- name: Ensure host "{{ host1_fqdn }}" is applied to idview test1_idview
ipaidview:
name: test1_idview
host:
- "{{ host1_fqdn }}"
action: member
register: result
failed_when: not result.changed or result.failed
- name: Ensure host "{{ host1_fqdn }}" is applied to idview test1_idview, again
ipaidview:
name: test1_idview
host:
- "{{ host1_fqdn }}"
action: member
register: result
failed_when: result.changed or result.failed
- name: Ensure host "{{ host1_fqdn }}" is applied to idview test2_idview
ipaidview:
name: test2_idview
host:
- "{{ host1_fqdn }}"
action: member
register: result
failed_when: not result.changed or result.failed
- name: Ensure host "{{ host1_fqdn }}" is applied to idview test2_idview, again
ipaidview:
name: test2_idview
host:
- "{{ host1_fqdn }}"
action: member
register: result
failed_when: result.changed or result.failed
- name: Ensure host "{{ host1_fqdn }}" is not applied to idview test1_idview anymore
ipaidview:
name: test1_idview
host:
- "{{ host1_fqdn }}"
action: member
state: absent
register: result
failed_when: result.changed or result.failed
- name: Ensure host "{{ host1_fqdn }}" is not applied to idview test2_idview
ipaidview:
name: test2_idview
host:
- "{{ host1_fqdn }}"
action: member
state: absent
register: result
failed_when: not result.changed or result.failed
- name: Ensure host "{{ host1_fqdn }}" is not applied to idview test2_idview, again
ipaidview:
name: test2_idview
host:
- "{{ host1_fqdn }}"
action: member
state: absent
register: result
failed_when: result.changed or result.failed
# CLEANUP TEST ITEMS
- name: Hosts "{{ host1_fqdn }}" and "{{ host2_fqdn }}" absent
ipahost:
hosts:
- name: "{{ host1_fqdn }}"
- name: "{{ host2_fqdn }}"
state: absent
- name: Ensure idview test1_idview, test2_idview and renamed_idview are absent
ipaidview:
name:
- test1_idview
- test2_idview
- renamed_idview
state: absent
---
- name: Test idview
hosts: ipaclients, ipaserver
# It is normally not needed to set "become" to "true" for a module test.
# Only set it to true if it is needed to execute commands as root.
become: false
# Enable "gather_facts" only if "ansible_facts" variable needs to be used.
gather_facts: false
tasks:
- name: Include FreeIPA facts.
ansible.builtin.include_tasks: ../env_freeipa_facts.yml
# Test will only be executed if host is not a server.
- name: Execute with server context in the client.
ipaidview:
ipaadmin_password: SomeADMINpassword
ipaapi_context: server
name: ThisShouldNotWork
register: result
failed_when: not (result.failed and result.msg is regex("No module named '*ipaserver'*"))
when: ipa_host_is_client
# Import basic module tests, and execute with ipa_context set to 'client'.
# If ipaclients is set, it will be executed using the client, if not,
# ipaserver will be used.
#
# With this setup, tests can be executed against an IPA client, against
# an IPA server using "client" context, and ensure that tests are executed
# in upstream CI.
- name: Test idview using client context, in client host.
import_playbook: test_idview.yml
when: groups['ipaclients']
vars:
ipa_test_host: ipaclients
- name: Test idview using client context, in server host.
import_playbook: test_idview.yml
when: groups['ipaclients'] is not defined or not groups['ipaclients']
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment