mirror of
https://github.com/freeipa/ansible-freeipa.git
synced 2026-05-06 13:23:14 +00:00
Merge pull request #1075 from rjeffman/automount_indirect_maps
ipaautomountmap: add support for indirect maps
This commit is contained in:
@@ -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
|
||||
|
||||
14
playbooks/automount/automount-map-indirect-map.yml
Normal file
14
playbooks/automount/automount-map-indirect-map.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
- 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
|
||||
|
||||
143
tests/automount/test_automountmap_indirect.yml
Normal file
143
tests/automount/test_automountmap_indirect.yml
Normal file
@@ -0,0 +1,143 @@
|
||||
---
|
||||
- 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
|
||||
Reference in New Issue
Block a user