Skip to content
Snippets Groups Projects
Commit a33fcf45 authored by Rafael Guterres Jeffman's avatar Rafael Guterres Jeffman
Browse files

ipaautomountmap: add support for indirect maps

Indirect maps were not supported by ansible-freeipa ipaautomountmap.
This patch adds support for adding indirect automount maps using the
"parent" and "mount" parameters, if the map do not yet exist. An
existing map cannot be modified.

The "parent" parameter must match an existing automount map, and the
"mount" parameter is required if "parent" is used.

A new example playbook can be found at:

    playbooks/automount/automount-map-indirect-map.yml

A new test playbook was added to test the feature:

    tests/automount/test_automountmap_indirect.yml
parent fef1bdcf
No related branches found
No related tags found
No related merge requests found
......@@ -54,6 +54,21 @@ Example playbook to ensure presence of an automount map:
desc: "this is a map for servers in the DMZ"
```
Automount maps can contain a submount key, which defines a mount location within the map the references another map. On FreeIPA, this is known as an indirect map. An indirect automount map is equivalent to adding a proper automount key to a map, referencyng another map (this second map is the indirect map). Use `parent` and `mount` parameters to create an indirect automount map with ansible-freeipa, without the need to directly manage the automount keys.
Example playbook to ensure an indirect automount map is present:
```yaml
---
- name: Playbook to add an indirect automount map
ipaautomountmap:
ipaadmin_password: SomeADMINpassword
name: auto.indirect
location: DMZ
parent: auto.DMZ
mount: dmz_indirect
```
Example playbook to ensure auto.DMZi is absent:
```yaml
......@@ -81,16 +96,14 @@ Variable | Description | Required
`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no
`name` \| `mapname` \| `map` \| `automountmapname` | Name of the map to manage | yes
`location` \| `automountlocation` \| `automountlocationcn` | Location name. | yes
`parentmap` | Parent map of the indirect map. Can only be used when creating new maps. Default: auto.master | no
`mount` | Indirect map mount point, relative to parent map. | yes, if `parent` is used.
`desc` \| `description` | Description of the map | yes
`state` | The state to ensure. It can be one of `present`, or `absent`, default: `present`. | no
Notes
=====
Creation of indirect mount points are not supported.
Authors
=======
Chris Procter
- Chris Procter
- Rafael Jeffman
---
- name: Managed automount maps
hosts: ipaserver
become: false
gather_facts: false
tasks:
- name: Playbook to add an indirect automount map
ipaautomountmap:
ipaadmin_password: SomeADMINpassword
name: auto.indirect
location: DMZ
parent: auto.DMZ
mount: dmz_indirect
......@@ -37,6 +37,7 @@ module: ipaautomountmap
author:
- Chris Procter (@chr15p)
- Thomas Woerner (@t-woerner)
- Rafael Jeffman (@rjeffman)
short_description: Manage FreeIPA autommount map
description:
- Add, delete, and modify an IPA automount map
......@@ -59,6 +60,16 @@ options:
type: str
aliases: ["description"]
required: false
parentmap:
description: |
Parent map of the indirect map. Can only be used when creating
new maps.
type: str
required: false
mount:
description: Indirect map mount point, relative to parent map.
type: str
required: false
state:
description: State to ensure
type: str
......@@ -75,6 +86,14 @@ EXAMPLES = '''
location: DMZ
desc: "this is a map for servers in the DMZ"
- name: ensure indirect map exists
ipaautomountmap:
ipaadmin_password: SomeADMINpassword
name: auto.INDIRECT
location: DMZ
parentmap: auto.DMZ
mount: indirect
- name: remove a map named auto.DMZ in location DMZ if it exists
ipaautomountmap:
ipaadmin_password: SomeADMINpassword
......@@ -110,6 +129,35 @@ class AutomountMap(IPAAnsibleModule):
else:
return response["result"]
def get_indirect_map_keys(self, location, name):
"""Check if 'name' is an indirect map for 'parentmap'."""
try:
maps = self.ipa_command("automountmap_find", location, {})
except Exception: # pylint: disable=broad-except
return []
result = []
for check_map in maps.get("result", []):
_mapname = check_map['automountmapname'][0]
keys = self.ipa_command(
"automountkey_find",
location,
{
"automountmapautomountmapname": _mapname,
"all": True
}
)
cmp_value = (
name if _mapname == "auto.master" else "ldap:{0}".format(name)
)
result.extend([
(location, _mapname, key.get("automountkey")[0])
for key in keys.get("result", [])
for mount_info in key.get("automountinformation", [])
if cmp_value in mount_info
])
return result
def check_ipa_params(self):
invalid = []
name = self.params_get("name")
......@@ -118,15 +166,27 @@ class AutomountMap(IPAAnsibleModule):
if len(name) != 1:
self.fail_json(msg="Exactly one name must be provided for"
" 'state: present'.")
mount = self.params_get("mount") or False
parentmap = self.params_get("parentmap")
if parentmap:
if not mount:
self.fail_json(
msg="Must provide 'mount' parameter for indirect map."
)
elif parentmap != "auto.master" and mount[0] == "/":
self.fail_json(
msg="mount point is relative to parent map, "
"cannot begin with '/'"
)
if state == "absent":
if len(name) == 0:
self.fail_json(msg="At least one 'name' must be provided for"
" 'state: absent'")
invalid = ["desc"]
invalid = ["desc", "parentmap", "mount"]
self.params_fail_used_invalid(invalid, state)
def get_args(self, mapname, desc):
def get_args(self, mapname, desc, parentmap, mount):
# automountmapname is required for all automountmap operations.
if not mapname:
self.fail_json(msg="automountmapname cannot be None or empty.")
......@@ -134,6 +194,11 @@ class AutomountMap(IPAAnsibleModule):
# An empty string is valid and will clear the attribute.
if desc is not None:
_args["description"] = desc
# indirect map attributes
if parentmap is not None:
_args["parentmap"] = parentmap
if mount is not None:
_args["key"] = mount
return _args
def define_ipa_commands(self):
......@@ -141,28 +206,102 @@ class AutomountMap(IPAAnsibleModule):
state = self.params_get("state")
location = self.params_get("location")
desc = self.params_get("desc")
mount = self.params_get("mount")
parentmap = self.params_get("parentmap")
for mapname in name:
automountmap = self.get_automountmap(location, mapname)
is_indirect_map = any([parentmap, mount])
if state == "present":
args = self.get_args(mapname, desc)
args = self.get_args(mapname, desc, parentmap, mount)
if automountmap is None:
self.commands.append([location, "automountmap_add", args])
if is_indirect_map:
if (
parentmap and
self.get_automountmap(location, parentmap) is None
):
self.fail_json(msg="Parent map does not exist.")
self.commands.append(
[location, "automountmap_add_indirect", args]
)
else:
self.commands.append(
[location, "automountmap_add", args]
)
else:
if not compare_args_ipa(self, args, automountmap):
has_changes = not compare_args_ipa(
self, args, automountmap, ['parentmap', 'key']
)
if is_indirect_map:
map_config = (
location, parentmap or "auto.master", mount
)
indirects = self.get_indirect_map_keys(
location, mapname
)
if map_config not in indirects or has_changes:
self.fail_json(
msg="Indirect maps can only be created, "
"not modified."
)
elif has_changes:
self.commands.append(
[location, "automountmap_mod", args]
)
if state == "absent":
elif state == "absent":
def find_keys(parent_loc, parent_map, parent_key):
return self.ipa_command(
"automountkey_show",
parent_loc,
{
"automountmapautomountmapname": parent_map,
"automountkey": parent_key,
}
).get("result")
if automountmap is not None:
indirects = self.get_indirect_map_keys(location, mapname)
# Remove indirect map configurations for this map
self.commands.extend([
(
ploc,
"automountkey_del",
{
"automountmapautomountmapname": pmap,
"automountkey": pkey,
}
)
for ploc, pmap, pkey in indirects
if find_keys(ploc, pmap, pkey)
])
# Remove map
self.commands.append([
location,
"automountmap_del",
{"automountmapname": [mapname]}
])
# ensure commands are unique and automountkey commands are
# executed first in the list
def hashable_dict(dictionaire):
return tuple(
(k, tuple(v) if isinstance(v, (list, tuple)) else v)
for k, v in dictionaire.items()
)
cmds = [
(name, cmd, hashable_dict(args))
for name, cmd, args in self.commands
]
self.commands = [
(name, cmd, dict(args))
for name, cmd, args in
sorted(set(cmds), key=lambda cmd: cmd[1])
]
def main():
ipa_module = AutomountMap(
......@@ -184,6 +323,10 @@ def main():
required=False,
default=None
),
parentmap=dict(
type="str", required=False, default=None
),
mount=dict(type="str", required=False, default=None),
),
)
changed = False
......
---
- name: Test automountmap
hosts: "{{ ipa_test_host | default('ipaserver') }}"
become: no
gather_facts: no
tasks:
# setup environment
- name: Ensure test maps are absent
ipaautomountmap:
ipaadmin_password: SomeADMINpassword
location: TestIndirect
name:
- DirectMap
- IndirectMap
- IndirectMapDefault
- IndirectMapDefaultAbsolute
state: absent
- name: Ensure test location is present
ipaautomountlocation:
ipaadmin_password: SomeADMINpassword
name: TestIndirect
- name: Ensure parent map is present
ipaautomountmap:
ipaadmin_password: SomeADMINpassword
location: TestIndirect
name: DirectMap
# TESTS
- name: Mount point cannot start with '/' if parentmap is not 'auto.master'
ipaautomountmap:
ipaadmin_password: SomeADMINpassword
location: TestIndirect
name: IndirectMap
parentmap: DirectMap
mount: '/absolute/path/will/fail'
register: result
failed_when: not result.failed or 'mount point is relative to parent map' not in result.msg
- name: Ensure indirect map is present
ipaautomountmap:
ipaadmin_password: SomeADMINpassword
location: TestIndirect
name: IndirectMap
parentmap: DirectMap
mount: indirect
register: result
failed_when: result.failed or not result.changed
- name: Ensure indirect map is present, again
ipaautomountmap:
ipaadmin_password: SomeADMINpassword
location: TestIndirect
name: IndirectMap
parentmap: DirectMap
mount: indirect
register: result
failed_when: result.failed or result.changed
- name: Ensure indirect map is absent
ipaautomountmap:
ipaadmin_password: SomeADMINpassword
location: TestIndirect
name: IndirectMap
state: absent
register: result
failed_when: result.failed or not result.changed
- name: Ensure indirect map is absent, again
ipaautomountmap:
ipaadmin_password: SomeADMINpassword
location: TestIndirect
name: IndirectMap
state: absent
register: result
failed_when: result.failed or result.changed
- name: Ensure indirect map is present, after being deleted
ipaautomountmap:
ipaadmin_password: SomeADMINpassword
location: TestIndirect
name: IndirectMap
parentmap: DirectMap
mount: indirect
register: result
failed_when: result.failed or not result.changed
- name: Ensure indirect map is present, after being deleted, again
ipaautomountmap:
ipaadmin_password: SomeADMINpassword
location: TestIndirect
name: IndirectMap
parentmap: DirectMap
mount: indirect
register: result
failed_when: result.failed or result.changed
- name: Ensure indirect map is present with default parent (auto.master)
ipaautomountmap:
ipaadmin_password: SomeADMINpassword
location: TestIndirect
name: IndirectMapDefault
mount: indirect_with_default
register: result
failed_when: result.failed or not result.changed
- name: Ensure indirect map is present with default parent (auto.master), again
ipaautomountmap:
ipaadmin_password: SomeADMINpassword
location: TestIndirect
name: IndirectMapDefault
mount: indirect_with_default
register: result
failed_when: result.failed or result.changed
- name: Absolute paths must workd with 'auto.master'
ipaautomountmap:
ipaadmin_password: SomeADMINpassword
location: TestIndirect
name: IndirectMapDefaultAbsolute
mount: /valid/path/indirect_with_default
register: result
failed_when: result.failed or not result.changed
# Cleanup
- name: Ensure test maps are absent
ipaautomountmap:
ipaadmin_password: SomeADMINpassword
location: TestIndirect
name:
- DirectMap
- IndirectMap
- IndirectMapDefault
- IndirectMapDefaultAbsolute
state: absent
- name: Ensure test location is absent
ipaautomountlocation:
ipaadmin_password: SomeADMINpassword
name: TestIndirect
state: absent
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment