diff --git a/README-automountmap.md b/README-automountmap.md index 208dd53deb5e6b7ddba8a42d85204577889c7ce9..77e6af092038fee06e214e67a9a1816887a50eca 100644 --- a/README-automountmap.md +++ b/README-automountmap.md @@ -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 diff --git a/playbooks/automount/automount-map-indirect-map.yml b/playbooks/automount/automount-map-indirect-map.yml new file mode 100644 index 0000000000000000000000000000000000000000..38359e553154e1a6272907f900db856702735764 --- /dev/null +++ b/playbooks/automount/automount-map-indirect-map.yml @@ -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 diff --git a/plugins/modules/ipaautomountmap.py b/plugins/modules/ipaautomountmap.py index 1590ebb69518366b476cc13599a04ccb8f475c19..668e887a313a5936101e787bf6fa1cff0e925d81 100644 --- a/plugins/modules/ipaautomountmap.py +++ b/plugins/modules/ipaautomountmap.py @@ -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 diff --git a/tests/automount/test_automountmap_indirect.yml b/tests/automount/test_automountmap_indirect.yml new file mode 100644 index 0000000000000000000000000000000000000000..200fe1ef4784057747e0879e04226c2dd908c3c6 --- /dev/null +++ b/tests/automount/test_automountmap_indirect.yml @@ -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