mirror of
https://github.com/freeipa/ansible-freeipa.git
synced 2026-08-05 06:14:42 +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"
|
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:
|
Example playbook to ensure auto.DMZi is absent:
|
||||||
|
|
||||||
```yaml
|
```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
|
`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
|
`name` \| `mapname` \| `map` \| `automountmapname` | Name of the map to manage | yes
|
||||||
`location` \| `automountlocation` \| `automountlocationcn` | Location name. | 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
|
`desc` \| `description` | Description of the map | yes
|
||||||
`state` | The state to ensure. It can be one of `present`, or `absent`, default: `present`. | no
|
`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
|
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:
|
author:
|
||||||
- Chris Procter (@chr15p)
|
- Chris Procter (@chr15p)
|
||||||
- Thomas Woerner (@t-woerner)
|
- Thomas Woerner (@t-woerner)
|
||||||
|
- Rafael Jeffman (@rjeffman)
|
||||||
short_description: Manage FreeIPA autommount map
|
short_description: Manage FreeIPA autommount map
|
||||||
description:
|
description:
|
||||||
- Add, delete, and modify an IPA automount map
|
- Add, delete, and modify an IPA automount map
|
||||||
@@ -59,6 +60,16 @@ options:
|
|||||||
type: str
|
type: str
|
||||||
aliases: ["description"]
|
aliases: ["description"]
|
||||||
required: false
|
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:
|
state:
|
||||||
description: State to ensure
|
description: State to ensure
|
||||||
type: str
|
type: str
|
||||||
@@ -75,6 +86,14 @@ EXAMPLES = '''
|
|||||||
location: DMZ
|
location: DMZ
|
||||||
desc: "this is a map for servers in the 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
|
- name: remove a map named auto.DMZ in location DMZ if it exists
|
||||||
ipaautomountmap:
|
ipaautomountmap:
|
||||||
ipaadmin_password: SomeADMINpassword
|
ipaadmin_password: SomeADMINpassword
|
||||||
@@ -110,6 +129,35 @@ class AutomountMap(IPAAnsibleModule):
|
|||||||
else:
|
else:
|
||||||
return response["result"]
|
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):
|
def check_ipa_params(self):
|
||||||
invalid = []
|
invalid = []
|
||||||
name = self.params_get("name")
|
name = self.params_get("name")
|
||||||
@@ -118,15 +166,27 @@ class AutomountMap(IPAAnsibleModule):
|
|||||||
if len(name) != 1:
|
if len(name) != 1:
|
||||||
self.fail_json(msg="Exactly one name must be provided for"
|
self.fail_json(msg="Exactly one name must be provided for"
|
||||||
" 'state: present'.")
|
" '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 state == "absent":
|
||||||
if len(name) == 0:
|
if len(name) == 0:
|
||||||
self.fail_json(msg="At least one 'name' must be provided for"
|
self.fail_json(msg="At least one 'name' must be provided for"
|
||||||
" 'state: absent'")
|
" 'state: absent'")
|
||||||
invalid = ["desc"]
|
invalid = ["desc", "parentmap", "mount"]
|
||||||
|
|
||||||
self.params_fail_used_invalid(invalid, state)
|
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.
|
# automountmapname is required for all automountmap operations.
|
||||||
if not mapname:
|
if not mapname:
|
||||||
self.fail_json(msg="automountmapname cannot be None or empty.")
|
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.
|
# An empty string is valid and will clear the attribute.
|
||||||
if desc is not None:
|
if desc is not None:
|
||||||
_args["description"] = desc
|
_args["description"] = desc
|
||||||
|
# indirect map attributes
|
||||||
|
if parentmap is not None:
|
||||||
|
_args["parentmap"] = parentmap
|
||||||
|
if mount is not None:
|
||||||
|
_args["key"] = mount
|
||||||
return _args
|
return _args
|
||||||
|
|
||||||
def define_ipa_commands(self):
|
def define_ipa_commands(self):
|
||||||
@@ -141,28 +206,102 @@ class AutomountMap(IPAAnsibleModule):
|
|||||||
state = self.params_get("state")
|
state = self.params_get("state")
|
||||||
location = self.params_get("location")
|
location = self.params_get("location")
|
||||||
desc = self.params_get("desc")
|
desc = self.params_get("desc")
|
||||||
|
mount = self.params_get("mount")
|
||||||
|
parentmap = self.params_get("parentmap")
|
||||||
|
|
||||||
for mapname in name:
|
for mapname in name:
|
||||||
automountmap = self.get_automountmap(location, mapname)
|
automountmap = self.get_automountmap(location, mapname)
|
||||||
|
|
||||||
|
is_indirect_map = any([parentmap, mount])
|
||||||
|
|
||||||
if state == "present":
|
if state == "present":
|
||||||
args = self.get_args(mapname, desc)
|
args = self.get_args(mapname, desc, parentmap, mount)
|
||||||
if automountmap is None:
|
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:
|
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(
|
self.commands.append(
|
||||||
[location, "automountmap_mod", args]
|
[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:
|
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([
|
self.commands.append([
|
||||||
location,
|
location,
|
||||||
"automountmap_del",
|
"automountmap_del",
|
||||||
{"automountmapname": [mapname]}
|
{"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():
|
def main():
|
||||||
ipa_module = AutomountMap(
|
ipa_module = AutomountMap(
|
||||||
@@ -184,6 +323,10 @@ def main():
|
|||||||
required=False,
|
required=False,
|
||||||
default=None
|
default=None
|
||||||
),
|
),
|
||||||
|
parentmap=dict(
|
||||||
|
type="str", required=False, default=None
|
||||||
|
),
|
||||||
|
mount=dict(type="str", required=False, default=None),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
changed = False
|
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