]> git.apps.os.sepia.ceph.com Git - ceph-ansible.git/commitdiff
library: Add radosgw_caps to manage capabilities
authorMathias Chapelain <mathias.chapelain@proton.ch>
Tue, 18 Jan 2022 15:04:16 +0000 (16:04 +0100)
committerGuillaume Abrioux <gabrioux@redhat.com>
Wed, 16 Feb 2022 08:44:36 +0000 (09:44 +0100)
This commit add `radosgw_caps` module to be able to manage RadosGW users
capabilities.

Usage from module's documentation:
```YAML
- name: add users read write and all buckets capabilities
  radosgw_caps:
    name: foo
    state: present
    caps:
      - users=read,write
      - buckets=*
- name: remove usage write capabilities
  radosgw_caps:
    name: foo
    state: absent
    caps:
      - usage=write
```

This module support check mode by simulating the original `radosgw-admin`
behavior when adding capabilities.

Signed-off-by: Mathias Chapelain <mathias.chapelain@proton.ch>
library/radosgw_caps.py [new file with mode: 0644]

diff --git a/library/radosgw_caps.py b/library/radosgw_caps.py
new file mode 100644 (file)
index 0000000..1d5f6b4
--- /dev/null
@@ -0,0 +1,378 @@
+# Copyright 2022, Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+
+try:
+    from ansible.module_utils.ca_common import (
+        exit_module,
+        exec_command,
+        is_containerized,
+        container_exec,
+    )
+except ImportError:
+    from module_utils.ca_common import (
+        exit_module,
+        exec_command,
+        is_containerized,
+        container_exec,
+    )
+import datetime
+import json
+import re
+from enum import IntFlag
+
+
+ANSIBLE_METADATA = {
+    "metadata_version": "1.1",
+    "status": ["preview"],
+    "supported_by": "community",
+}
+
+DOCUMENTATION = """
+---
+module: radosgw_caps
+
+short_description: Manage RADOS Gateway Admin capabilities
+
+version_added: "2.10"
+
+description:
+    - Manage RADOS Gateway capabilities addition and deletion.
+options:
+    cluster:
+        description:
+            - The ceph cluster name.
+        required: false
+        default: ceph
+        type: str
+    name:
+        description:
+            - name of the RADOS Gateway user (uid).
+        required: true
+        type: str
+    state:
+        description:
+            If 'present' is used, the module will assign capabilities
+            defined in `caps`.
+            If 'absent' is used, the module will remove the capabilities.
+        required: false
+        choices: ['present', 'absent']
+        default: present
+        type: str
+    caps:
+        description:
+            - The set of capabilities to assign or remove.
+        required: true
+        type: list
+        elements: str
+
+author:
+    - Mathias Chapelain <mathias.chapelain@proton.ch>
+"""
+
+EXAMPLES = """
+- name: add users read capabilties to a user
+  radosgw_caps:
+    name: foo
+    state: present
+    caps:
+      - users=read
+
+- name: add users read write and all buckets capabilities
+  radosgw_caps:
+    name: foo
+    state: present
+    caps:
+      - users=read,write
+      - buckets=*
+
+- name: remove usage write capabilities
+  radosgw_caps:
+    name: foo
+    state: absent
+    caps:
+      - usage=write
+"""
+
+RETURN = """
+---
+cmd:
+  description: The radosgw-admin command being run by the module to apply caps settings.
+  returned: always
+  type: str
+start:
+  description: Timestamp of module execution start.
+  returned: always
+  type: str
+end:
+  description: Timestamp of module execution end.
+  returned: always
+  type: str
+delta:
+  description: Time of module execution between start and end.
+  returned: always
+  type: str
+diff:
+  description: Dict containing the user capabilities before and after modifications.
+  returned: always
+  type: dict
+  contains:
+    before:
+      description: Contains user capabilities, json-formatted, as returned by `radosgw-admin user info`.
+      returned: always
+      type: str
+    after:
+      description: Contains user capabilities, json-formatted, as returned by `radosgw-admin caps add/rm`.
+      returned: success
+      type: str
+rc:
+  description: Return code of the module command executed, see `cmd` return value.
+  returned: always
+  type: int
+stdout:
+  description: Output of the executed command.
+  returned: always
+  type: str
+stderr:
+  description: Error output of the executed command.
+  returned: always
+  type: str
+changed:
+  description: Specify if user capabilities has been changed during module execution.
+  returned: always
+  type: bool
+"""
+
+
+def pre_generate_radosgw_cmd(container_image=None):
+    """
+    Generate radosgw-admin prefix comaand
+    """
+    if container_image:
+        cmd = container_exec("radosgw-admin", container_image)
+    else:
+        cmd = ["radosgw-admin"]
+
+    return cmd
+
+
+def generate_radosgw_cmd(cluster, args, container_image=None):
+    """
+    Generate 'radosgw' command line to execute
+    """
+
+    cmd = pre_generate_radosgw_cmd(container_image=container_image)
+
+    base_cmd = ["--cluster", cluster, "caps"]
+
+    cmd.extend(base_cmd + args)
+
+    return cmd
+
+
+def add_caps(module, container_image=None):
+    """
+    Add capabilities
+    """
+
+    cluster = module.params.get("cluster")
+    name = module.params.get("name")
+    caps = module.params.get("caps")
+
+    args = ["add", "--uid=" + name, "--caps=" + ";".join(caps)]
+
+    cmd = generate_radosgw_cmd(
+        cluster=cluster, args=args, container_image=container_image
+    )
+
+    return cmd
+
+
+def remove_caps(module, container_image=None):
+    """
+    Remove capabilities
+    """
+
+    cluster = module.params.get("cluster")
+    name = module.params.get("name")
+    caps = module.params.get("caps")
+
+    args = ["rm", "--uid=" + name, "--caps=" + ";".join(caps)]
+
+    cmd = generate_radosgw_cmd(
+        cluster=cluster, args=args, container_image=container_image
+    )
+
+    return cmd
+
+
+def get_user(module, container_image=None):
+    """
+    Get existing user
+    """
+
+    cluster = module.params.get("cluster")
+    name = module.params.get("name")
+
+    args = ["info", "--uid=" + name, "--format=json"]
+
+    cmd = pre_generate_radosgw_cmd(container_image=container_image)
+
+    base_cmd = ["--cluster", cluster, "user"]
+
+    cmd.extend(base_cmd + args)
+
+    return cmd
+
+
+class RGWUserCaps(IntFlag):
+    INVALID = 0x0
+    READ = 0x1
+    WRITE = 0x2
+    ALL = READ | WRITE
+
+
+def perm_string_to_flag(perm):
+    splitted = re.split(",|=| |\t", perm)
+    if ("read" in splitted and "write" in splitted) or "*" in splitted:
+        return RGWUserCaps.ALL
+    elif "read" in splitted:
+        return RGWUserCaps.READ
+    elif "write" in splitted:
+        return RGWUserCaps.WRITE
+    return RGWUserCaps.INVALID
+
+
+def perm_flag_to_string(perm):
+    if perm == RGWUserCaps.ALL:
+        return "*"
+    elif perm == RGWUserCaps.READ:
+        return "read"
+    elif perm == RGWUserCaps.WRITE:
+        return "write"
+    else:
+        return "invalid"
+
+
+def params_to_caps_output(current_caps, params, deletion=False):
+    out_caps = current_caps
+    for param in params:
+        splitted = param.split("=", maxsplit=1)
+        cap = splitted[0]
+
+        new_perm = perm_string_to_flag(splitted[1])
+        current = next((item for item in out_caps if item["type"] == cap), None)
+
+        if not current:
+            if not deletion:
+                out_caps.append(dict(type=cap, perm=perm_flag_to_string(new_perm)))
+            continue
+
+        current_perm = perm_string_to_flag(current["perm"])
+
+        new_perm = current_perm & ~new_perm if deletion else new_perm | current_perm
+
+        if new_perm == 0x0:
+            out_caps.remove(current)
+
+        current["perm"] = perm_flag_to_string(new_perm)
+
+    return out_caps
+
+
+def run_module():
+    module_args = dict(
+        cluster=dict(type="str", required=False, default="ceph"),
+        name=dict(type="str", required=True),
+        state=dict(
+            type="str", required=False, choices=["present", "absent"], default="present"
+        ),
+        caps=dict(type="list", required=True),
+    )
+
+    module = AnsibleModule(
+        argument_spec=module_args,
+        supports_check_mode=True,
+    )
+
+    # Gather module parameters in variables
+    name = module.params.get("name")
+    state = module.params.get("state")
+    caps = module.params.get("caps")
+
+    startd = datetime.datetime.now()
+    changed = False
+
+    # will return either the image name or None
+    container_image = is_containerized()
+
+    diff = dict(before="", after="")
+
+    # get user infos for diff
+    rc, cmd, out, err = exec_command(
+        module, get_user(module, container_image=container_image)
+    )
+
+    if rc == 0:
+        before_user = json.loads(out)
+        before_caps = sorted(before_user["caps"], key=lambda d: d["type"])
+        diff["before"] = json.dumps(before_caps, indent=4)
+
+        out = ""
+        err = ""
+
+        if state == "present":
+            cmd = add_caps(module, container_image=container_image)
+        elif state == "absent":
+            cmd = remove_caps(module, container_image=container_image)
+
+        if not module.check_mode:
+            rc, cmd, out, err = exec_command(module, cmd)
+        else:
+            out_caps = params_to_caps_output(
+                before_user["caps"], caps, deletion=(state == "absent")
+            )
+            out = json.dumps(dict(caps=out_caps))
+
+        if rc == 0:
+            after_user = json.loads(out)["caps"]
+            after_user = sorted(after_user, key=lambda d: d["type"])
+            diff["after"] = json.dumps(after_user, indent=4)
+            changed = diff["before"] != diff["after"]
+    else:
+        out = "User {} doesn't exist".format(name)
+
+    exit_module(
+        module=module,
+        out=out,
+        rc=rc,
+        cmd=cmd,
+        err=err,
+        startd=startd,
+        changed=changed,
+        diff=diff,
+    )
+
+
+def main():
+    run_module()
+
+
+if __name__ == "__main__":
+    main()