| #!/usr/bin/python3 |
| |
| # Copyright 2018, 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 |
| |
| import base64 |
| import datetime |
| import json |
| import os |
| import socket |
| import struct |
| import time |
| |
| from ansible.module_utils.basic import AnsibleModule |
| from ansible_collections.vexxhost.atmosphere.plugins.module_utils.ca_common import ( |
| container_exec, |
| fatal, |
| generate_ceph_cmd, |
| is_containerized, |
| ) |
| |
| ANSIBLE_METADATA = { |
| "metadata_version": "1.1", |
| "status": ["preview"], |
| "supported_by": "community", |
| } |
| |
| DOCUMENTATION = """ |
| --- |
| module: ceph_key |
| |
| author: Sebastien Han <seb@redhat.com> |
| |
| short_description: Manage Cephx key(s) |
| |
| version_added: "2.6" |
| |
| description: |
| - Manage CephX creation, deletion and updates. |
| It can also list and get information about keyring(s). |
| options: |
| cluster: |
| description: |
| - The ceph cluster name. |
| required: false |
| default: ceph |
| name: |
| description: |
| - name of the CephX key |
| required: true |
| user: |
| description: |
| - entity used to perform operation. |
| It corresponds to the -n option (--name) |
| required: false |
| user_key: |
| description: |
| - the path to the keyring corresponding to the |
| user being used. |
| It corresponds to the -k option (--keyring) |
| state: |
| description: |
| - If 'present' is used, the module creates a keyring |
| with the associated capabilities. |
| If 'present' is used and a secret is provided the module |
| will always add the key. Which means it will update |
| the keyring if the secret changes, the same goes for |
| the capabilities. |
| If 'absent' is used, the module will simply delete the keyring. |
| If 'list' is used, the module will list all the keys and will |
| return a json output. |
| If 'info' is used, the module will return in a json format the |
| description of a given keyring. |
| If 'generate_secret' is used, the module will simply output a cephx keyring. |
| required: false |
| choices: ['present', 'update', 'absent', 'list', 'info', 'fetch_initial_keys', 'generate_secret'] |
| default: present |
| caps: |
| description: |
| - CephX key capabilities |
| default: None |
| required: false |
| secret: |
| description: |
| - keyring's secret value |
| required: false |
| default: None |
| import_key: |
| description: |
| - Wether or not to import the created keyring into Ceph. |
| This can be useful for someone that only wants to generate keyrings |
| but not add them into Ceph. |
| required: false |
| default: True |
| dest: |
| description: |
| - Destination to write the keyring, can a file or a directory |
| required: false |
| default: /etc/ceph/ |
| fetch_initial_keys: |
| description: |
| - Fetch client.admin and bootstrap key. |
| This is only needed for Nautilus and above. |
| Writes down to the filesystem the initial keys generated by the monitor. # noqa: E501 |
| This command can ONLY run from a monitor node. |
| required: false |
| default: false |
| output_format: |
| description: |
| - The key output format when retrieving the information of an |
| entity. |
| required: false |
| default: json |
| """ |
| |
| EXAMPLES = """ |
| |
| keys_to_create: |
| - { name: client.key, key: "AQAin8tUUK84ExAA/QgBtI7gEMWdmnvKBzlXdQ==", caps: { mon: "allow rwx", mds: "allow *" } , mode: "0600" } # noqa: E501 |
| - { name: client.cle, caps: { mon: "allow r", osd: "allow *" } , mode: "0600" } # noqa: E501 |
| |
| caps: |
| mon: "allow rwx" |
| mds: "allow *" |
| |
| - name: create ceph admin key |
| ceph_key: |
| name: client.admin |
| state: present |
| secret: AQAin8tU2DsKFBAAFIAzVTzkL3+gtAjjpQiomw== |
| caps: |
| mon: allow * |
| osd: allow * |
| mgr: allow * |
| mds: allow |
| mode: 0400 |
| import_key: False |
| |
| - name: create monitor initial keyring |
| ceph_key: |
| name: mon. |
| state: present |
| secret: AQAin8tUMICVFBAALRHNrV0Z4MXupRw4v9JQ6Q== |
| caps: |
| mon: allow * |
| dest: "/var/lib/ceph/tmp/" |
| import_key: False |
| |
| - name: create cephx key |
| ceph_key: |
| name: "{{ keys_to_create }}" |
| user: client.bootstrap-rgw |
| user_key: /var/lib/ceph/bootstrap-rgw/ceph.keyring |
| state: present |
| caps: "{{ caps }}" |
| |
| - name: create cephx key but don't import it in Ceph |
| ceph_key: |
| name: "{{ keys_to_create }}" |
| state: present |
| caps: "{{ caps }}" |
| import_key: False |
| |
| - name: delete cephx key |
| ceph_key: |
| name: "my_key" |
| state: absent |
| |
| - name: info cephx key |
| ceph_key: |
| name: "my_key"" |
| state: info |
| |
| - name: info cephx admin key (plain) |
| ceph_key: |
| name: client.admin |
| output_format: plain |
| state: info |
| register: client_admin_key |
| |
| - name: list cephx keys |
| ceph_key: |
| state: list |
| |
| - name: fetch cephx keys |
| ceph_key: |
| state: fetch_initial_keys |
| """ |
| |
| RETURN = """# """ |
| |
| |
| CEPH_INITIAL_KEYS = [ |
| "client.admin", |
| "client.bootstrap-mds", |
| "client.bootstrap-mgr", # noqa: E501 |
| "client.bootstrap-osd", |
| "client.bootstrap-rbd", |
| "client.bootstrap-rbd-mirror", |
| "client.bootstrap-rgw", |
| ] # noqa: E501 |
| |
| |
| def str_to_bool(val): |
| try: |
| val = val.lower() |
| except AttributeError: |
| val = str(val).lower() |
| if val == "true": |
| return True |
| elif val == "false": |
| return False |
| else: |
| raise ValueError("Invalid input value: %s" % val) |
| |
| |
| def generate_secret(): |
| """ |
| Generate a CephX secret |
| """ |
| |
| key = os.urandom(16) |
| header = struct.pack("<hiih", 1, int(time.time()), 0, len(key)) |
| secret = base64.b64encode(header + key) |
| |
| return secret |
| |
| |
| def generate_caps(_type, caps): |
| """ |
| Generate CephX capabilities list |
| """ |
| |
| caps_cli = [] |
| |
| for k, v in caps.items(): |
| # makes sure someone didn't pass an empty var, |
| # we don't want to add an empty cap |
| if len(k) == 0: |
| continue |
| if _type == "ceph-authtool": |
| caps_cli.extend(["--cap"]) |
| caps_cli.extend([k, v]) |
| |
| return caps_cli |
| |
| |
| def generate_ceph_authtool_cmd( |
| cluster, name, secret, caps, dest, container_image=None |
| ): # noqa: E501 |
| """ |
| Generate 'ceph-authtool' command line to execute |
| """ |
| |
| if container_image: |
| binary = "ceph-authtool" |
| cmd = container_exec(binary, container_image) |
| else: |
| binary = ["ceph-authtool"] |
| cmd = binary |
| |
| base_cmd = [ |
| "--create-keyring", |
| dest, |
| "--name", |
| name, |
| "--add-key", |
| secret, |
| ] |
| |
| cmd.extend(base_cmd) |
| cmd.extend(generate_caps("ceph-authtool", caps)) |
| |
| return cmd |
| |
| |
| def create_key( |
| module, |
| result, |
| cluster, |
| user, |
| user_key, |
| name, |
| secret, |
| caps, |
| import_key, |
| dest, |
| container_image=None, |
| ): # noqa: E501 |
| """ |
| Create a CephX key |
| """ |
| |
| cmd_list = [] |
| if not secret: |
| secret = generate_secret() |
| |
| if user == "client.admin": |
| args = ["import", "-i", dest] |
| else: |
| args = ["get-or-create", name] |
| args.extend(generate_caps(None, caps)) |
| args.extend(["-o", dest]) |
| |
| cmd_list.append( |
| generate_ceph_authtool_cmd(cluster, name, secret, caps, dest, container_image) |
| ) |
| |
| if import_key or user != "client.admin": |
| cmd_list.append( |
| generate_ceph_cmd( |
| sub_cmd=["auth"], |
| args=args, |
| cluster=cluster, |
| user=user, |
| user_key=user_key, |
| container_image=container_image, |
| ) |
| ) |
| |
| return cmd_list |
| |
| |
| def delete_key(cluster, user, user_key, name, container_image=None): |
| """ |
| Delete a CephX key |
| """ |
| |
| cmd_list = [] |
| |
| args = [ |
| "del", |
| name, |
| ] |
| |
| cmd_list.append( |
| generate_ceph_cmd( |
| sub_cmd=["auth"], |
| args=args, |
| cluster=cluster, |
| user=user, |
| user_key=user_key, |
| container_image=container_image, |
| ) |
| ) |
| |
| return cmd_list |
| |
| |
| def get_key(cluster, user, user_key, name, dest, container_image=None): |
| """ |
| Get a CephX key (write on the filesystem) |
| """ |
| |
| cmd_list = [] |
| |
| args = [ |
| "get", |
| name, |
| "-o", |
| dest, |
| ] |
| |
| cmd_list.append( |
| generate_ceph_cmd( |
| sub_cmd=["auth"], |
| args=args, |
| cluster=cluster, |
| user=user, |
| user_key=user_key, |
| container_image=container_image, |
| ) |
| ) |
| |
| return cmd_list |
| |
| |
| def info_key( |
| cluster, name, user, user_key, output_format, container_image=None |
| ): # noqa: E501 |
| """ |
| Get information about a CephX key |
| """ |
| |
| cmd_list = [] |
| |
| args = [ |
| "get", |
| name, |
| "-f", |
| output_format, |
| ] |
| |
| cmd_list.append( |
| generate_ceph_cmd( |
| sub_cmd=["auth"], |
| args=args, |
| cluster=cluster, |
| user=user, |
| user_key=user_key, |
| container_image=container_image, |
| ) |
| ) |
| |
| return cmd_list |
| |
| |
| def list_keys(cluster, user, user_key, container_image=None): |
| """ |
| List all CephX keys |
| """ |
| |
| cmd_list = [] |
| |
| args = [ |
| "ls", |
| "-f", |
| "json", |
| ] |
| |
| cmd_list.append( |
| generate_ceph_cmd( |
| sub_cmd=["auth"], |
| args=args, |
| cluster=cluster, |
| user=user, |
| user_key=user_key, |
| container_image=container_image, |
| ) |
| ) |
| |
| return cmd_list |
| |
| |
| def exec_commands(module, cmd_list): |
| """ |
| Execute command(s) |
| """ |
| |
| for cmd in cmd_list: |
| rc, out, err = module.run_command(cmd) |
| if rc != 0: |
| return rc, cmd, out, err |
| |
| return rc, cmd, out, err |
| |
| |
| def lookup_ceph_initial_entities(module, out): |
| """ |
| Lookup Ceph initial keys entries in the auth map |
| """ |
| |
| # convert out to json, ansible returns a string... |
| try: |
| out_dict = json.loads(out) |
| except ValueError as e: |
| fatal( |
| "Could not decode 'ceph auth list' json output: {}".format(e), module |
| ) # noqa: E501 |
| |
| entities = [] |
| if "auth_dump" in out_dict: |
| for key in out_dict["auth_dump"]: |
| for k, v in key.items(): |
| if k == "entity": |
| if v in CEPH_INITIAL_KEYS: |
| entities.append(v) |
| else: |
| fatal("'auth_dump' key not present in json output:", module) # noqa: E501 |
| |
| if len(entities) != len(CEPH_INITIAL_KEYS) and not str_to_bool( |
| os.environ.get("CEPH_ROLLING_UPDATE", False) |
| ): # noqa: E501 |
| # must be missing in auth_dump, as if it were in CEPH_INITIAL_KEYS |
| # it'd be in entities from the above test. Report what's missing. |
| missing = [] |
| for e in CEPH_INITIAL_KEYS: |
| if e not in entities: |
| missing.append(e) |
| fatal( |
| "initial keyring does not contain keys: " + " ".join(missing), module |
| ) # noqa: E501 |
| return entities |
| |
| |
| def build_key_path(cluster, entity): |
| """ |
| Build key path depending on the key type |
| """ |
| |
| if "admin" in entity: |
| path = "/etc/ceph" |
| keyring_filename = cluster + "." + entity + ".keyring" |
| key_path = os.path.join(path, keyring_filename) |
| elif "bootstrap" in entity: |
| path = "/var/lib/ceph" |
| # bootstrap keys show up as 'client.boostrap-osd' |
| # however the directory is called '/var/lib/ceph/bootstrap-osd' |
| # so we need to substring 'client.' |
| entity_split = entity.split(".")[1] |
| keyring_filename = cluster + ".keyring" |
| key_path = os.path.join(path, entity_split, keyring_filename) |
| else: |
| return None |
| |
| return key_path |
| |
| |
| def run_module(): |
| module_args = dict( |
| cluster=dict(type="str", required=False, default="ceph"), |
| name=dict(type="str", required=False), |
| state=dict( |
| type="str", |
| required=False, |
| default="present", |
| choices=[ |
| "present", |
| "update", |
| "absent", # noqa: E501 |
| "list", |
| "info", |
| "fetch_initial_keys", |
| "generate_secret", |
| ], |
| ), # noqa: E501 |
| caps=dict(type="dict", required=False, default=None), |
| secret=dict(type="str", required=False, default=None, no_log=True), |
| import_key=dict(type="bool", required=False, default=True), |
| dest=dict(type="str", required=False, default="/etc/ceph/"), |
| user=dict(type="str", required=False, default="client.admin"), |
| user_key=dict(type="str", required=False, default=None), |
| output_format=dict( |
| type="str", |
| required=False, |
| default="json", |
| choices=["json", "plain", "xml", "yaml"], |
| ), # noqa: E501 |
| ) |
| |
| module = AnsibleModule( |
| argument_spec=module_args, |
| supports_check_mode=True, |
| add_file_common_args=True, |
| ) |
| |
| file_args = module.load_file_common_arguments(module.params) |
| |
| # Gather module parameters in variables |
| state = module.params["state"] |
| name = module.params.get("name") |
| cluster = module.params.get("cluster") |
| caps = module.params.get("caps") |
| secret = module.params.get("secret") |
| import_key = module.params.get("import_key") |
| dest = module.params.get("dest") |
| user = module.params.get("user") |
| user_key = module.params.get("user_key") |
| output_format = module.params.get("output_format") |
| |
| changed = False |
| |
| result = dict( |
| changed=changed, |
| stdout="", |
| stderr="", |
| rc=0, |
| start="", |
| end="", |
| delta="", |
| ) |
| |
| if module.check_mode and state != "info": |
| module.exit_json(**result) |
| |
| startd = datetime.datetime.now() |
| |
| # will return either the image name or None |
| container_image = is_containerized() |
| |
| # Test if the key exists, if it does we skip its creation |
| # We only want to run this check when a key needs to be added |
| # There is no guarantee that any cluster is running and we don't need one |
| _secret = secret |
| _caps = caps |
| key_exist = 1 |
| |
| if not user_key: |
| user_key_filename = "{}.{}.keyring".format(cluster, user) |
| user_key_dir = "/etc/ceph" |
| user_key_path = os.path.join(user_key_dir, user_key_filename) |
| else: |
| user_key_path = user_key |
| |
| if state in ["present", "update"]: |
| # if dest is not a directory, the user wants to change the file's name |
| # (e,g: /etc/ceph/ceph.mgr.ceph-mon2.keyring) |
| if not os.path.isdir(dest): |
| file_path = dest |
| else: |
| if "bootstrap" in dest: |
| # Build a different path for bootstrap keys as there are stored |
| # as /var/lib/ceph/bootstrap-rbd/ceph.keyring |
| keyring_filename = cluster + ".keyring" |
| else: |
| keyring_filename = cluster + "." + name + ".keyring" |
| file_path = os.path.join(dest, keyring_filename) |
| |
| file_args["path"] = file_path |
| |
| if import_key: |
| _info_key = [] |
| rc, cmd, out, err = exec_commands( |
| module, |
| info_key( |
| cluster, name, user, user_key_path, output_format, container_image |
| ), |
| ) # noqa: E501 |
| key_exist = rc |
| if not caps and key_exist != 0: |
| fatal( |
| "Capabilities must be provided when state is 'present'", module |
| ) # noqa: E501 |
| if key_exist != 0 and secret is None and caps is None: |
| fatal( |
| "Keyring doesn't exist, you must provide 'secret' and 'caps'", |
| module, |
| ) # noqa: E501 |
| if key_exist == 0: |
| _info_key = json.loads(out) |
| if not secret: |
| secret = _info_key[0]["key"] |
| _secret = _info_key[0]["key"] |
| if not caps: |
| caps = _info_key[0]["caps"] |
| _caps = _info_key[0]["caps"] |
| if secret == _secret and caps == _caps: |
| if not os.path.isfile(file_path): |
| rc, cmd, out, err = exec_commands( |
| module, |
| get_key( |
| cluster, |
| user, |
| user_key_path, |
| name, |
| file_path, |
| container_image, |
| ), |
| ) # noqa: E501 |
| result["rc"] = rc |
| if rc != 0: |
| result[ |
| "stdout" |
| ] = "Couldn't fetch the key {0} at {1}.".format( |
| name, file_path |
| ) # noqa: E501 |
| module.exit_json(**result) |
| result["stdout"] = "fetched the key {0} at {1}.".format( |
| name, file_path |
| ) # noqa: E501 |
| |
| result[ |
| "stdout" |
| ] = "{0} already exists and doesn't need to be updated.".format( |
| name |
| ) # noqa: E501 |
| result["rc"] = 0 |
| module.set_fs_attributes_if_different(file_args, False) |
| module.exit_json(**result) |
| else: |
| if os.path.isfile(file_path) and not secret or not caps: |
| result[ |
| "stdout" |
| ] = "{0} already exists in {1} you must provide secret *and* caps when import_key is {2}".format( |
| name, dest, import_key |
| ) # noqa: E501 |
| result["rc"] = 0 |
| module.exit_json(**result) |
| if ( |
| key_exist == 0 and (secret != _secret or caps != _caps) |
| ) or key_exist != 0: # noqa: E501 |
| rc, cmd, out, err = exec_commands( |
| module, |
| create_key( |
| module, |
| result, |
| cluster, |
| user, |
| user_key_path, |
| name, |
| secret, |
| caps, |
| import_key, |
| file_path, |
| container_image, |
| ), |
| ) # noqa: E501 |
| if rc != 0: |
| result["stdout"] = "Couldn't create or update {0}".format(name) |
| result["stderr"] = err |
| module.exit_json(**result) |
| module.set_fs_attributes_if_different(file_args, False) |
| changed = True |
| |
| elif state == "absent": |
| if key_exist == 0: |
| rc, cmd, out, err = exec_commands( |
| module, delete_key(cluster, user, user_key_path, name, container_image) |
| ) # noqa: E501 |
| if rc == 0: |
| changed = True |
| else: |
| rc = 0 |
| |
| elif state == "info": |
| rc, cmd, out, err = exec_commands( |
| module, |
| info_key( |
| cluster, name, user, user_key_path, output_format, container_image |
| ), |
| ) # noqa: E501 |
| |
| elif state == "list": |
| rc, cmd, out, err = exec_commands( |
| module, list_keys(cluster, user, user_key_path, container_image) |
| ) |
| |
| elif state == "fetch_initial_keys": |
| hostname = socket.gethostname().split(".", 1)[0] |
| user = "mon." |
| keyring_filename = cluster + "-" + hostname + "/keyring" |
| user_key_path = os.path.join("/var/lib/ceph/mon/", keyring_filename) |
| rc, cmd, out, err = exec_commands( |
| module, list_keys(cluster, user, user_key_path, container_image) |
| ) |
| if rc != 0: |
| result["stdout"] = "failed to retrieve ceph keys" |
| result["sdterr"] = err |
| result["rc"] = 0 |
| module.exit_json(**result) |
| |
| entities = lookup_ceph_initial_entities(module, out) |
| |
| output_format = "plain" |
| for entity in entities: |
| key_path = build_key_path(cluster, entity) |
| if key_path is None: |
| fatal("Failed to build key path, no entity yet?", module) |
| elif os.path.isfile(key_path): |
| # if the key is already on the filesystem |
| # there is no need to fetch it again |
| continue |
| |
| extra_args = [ |
| "-o", |
| key_path, |
| ] |
| |
| info_cmd = info_key( |
| cluster, entity, user, user_key_path, output_format, container_image |
| ) |
| # we use info_cmd[0] because info_cmd is an array made of an array |
| info_cmd[0].extend(extra_args) |
| rc, cmd, out, err = exec_commands(module, info_cmd) # noqa: E501 |
| |
| file_args = module.load_file_common_arguments(module.params) |
| file_args["path"] = key_path |
| module.set_fs_attributes_if_different(file_args, False) |
| elif state == "generate_secret": |
| out = generate_secret().decode() |
| cmd = "" |
| rc = 0 |
| err = "" |
| changed = True |
| |
| endd = datetime.datetime.now() |
| delta = endd - startd |
| |
| result = dict( |
| cmd=cmd, |
| start=str(startd), |
| end=str(endd), |
| delta=str(delta), |
| rc=rc, |
| stdout=out.rstrip("\r\n"), |
| stderr=err.rstrip("\r\n"), |
| changed=changed, |
| ) |
| |
| if rc != 0: |
| module.fail_json(msg="non-zero return code", **result) |
| |
| module.exit_json(**result) |
| |
| |
| def main(): |
| run_module() |
| |
| |
| if __name__ == "__main__": |
| main() |