library: Add radosgw_caps to manage capabilities

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>
pull/7052/head
Mathias Chapelain 2022-01-18 16:04:16 +01:00 committed by Guillaume Abrioux
parent 055326be7c
commit 65003f4ff9
1 changed files with 378 additions and 0 deletions

View File

@ -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()