--- /dev/null
+# 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()