From a33fcf45f83dabf5b6da70b200571695e3917a75 Mon Sep 17 00:00:00 2001 From: Rafael Guterres Jeffman <rjeffman@redhat.com> Date: Mon, 10 Apr 2023 20:36:26 -0300 Subject: [PATCH] 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 --- README-automountmap.md | 25 ++- .../automount/automount-map-indirect-map.yml | 14 ++ plugins/modules/ipaautomountmap.py | 155 +++++++++++++++++- .../automount/test_automountmap_indirect.yml | 143 ++++++++++++++++ 4 files changed, 325 insertions(+), 12 deletions(-) create mode 100644 playbooks/automount/automount-map-indirect-map.yml create mode 100644 tests/automount/test_automountmap_indirect.yml diff --git a/README-automountmap.md b/README-automountmap.md index 208dd53d..77e6af09 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 00000000..38359e55 --- /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 1590ebb6..668e887a 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 00000000..200fe1ef --- /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 -- GitLab