Skip to content
Snippets Groups Projects
Unverified Commit 5d082145 authored by Thomas Woerner's avatar Thomas Woerner Committed by GitHub
Browse files

Merge pull request #1075 from rjeffman/automount_indirect_maps

ipaautomountmap: add support for indirect maps
parents fef1bdcf a33fcf45
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: ...@@ -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 ...@@ -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
---
- 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 ...@@ -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: ...@@ -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 = ''' ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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(): ...@@ -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
......
---
- 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.
Finish editing this message first!
Please register or to comment