Browse Source
This role is provided as part of the implementation of the tripleo-ceph spec. It is an Ansible wrapper to call the cephadm Ceph tool and it contains the Ansible modules ceph_key and ceph_pool from ceph-ansible for managing cephx keys and ceph pools. Implements: blueprint tripleo-ceph Change-Id: I60d6857b888ef97242c4f4bbf20fbc62de5ef29fchanges/74/770674/59
27 changed files with 2724 additions and 11 deletions
@ -0,0 +1,117 @@
|
||||
====================== |
||||
Role - tripleo_cephadm |
||||
====================== |
||||
|
||||
.. ansibleautoplugin:: |
||||
:role: tripleo_ansible/roles/tripleo_cephadm |
||||
|
||||
About |
||||
~~~~~ |
||||
|
||||
An Ansible role for TripleO integration with Ceph clusters deployed with |
||||
`cephadm`_ and managed with Ceph `orchestrator`_. |
||||
|
||||
This role is provided as part of the implementation of the `tripleo_ceph_spec`_. |
||||
It is an Ansible wrapper to call the Ceph tools `cephadm`_ and `orchestrator`_ |
||||
and it contains the Ansible module `ceph_key`_ from `ceph-ansible`_. |
||||
|
||||
Assumptions |
||||
~~~~~~~~~~~ |
||||
|
||||
- This role assumes it has an inventory with a single host, known as the |
||||
`bootstrap_host`. An inventory genereated by `tripleo-ansible-inventory` |
||||
will have a `mons` group so the first node in this group is a good |
||||
candidate for this host. |
||||
|
||||
- The `cephadm`_ binary must be installed on the `bootstrap_host`. |
||||
|
||||
- Though there only needs to be one Ceph node in the inventory `cephadm`_ |
||||
will configure the other servers with SSH. Thus, the following playbook |
||||
should be run before one which uses this role to configure the `ceph-admin` |
||||
user on the overcloud with the SSH keys that `cephadm`_ requires. |
||||
|
||||
.. code-block:: bash |
||||
|
||||
ansible-playbook -i $INV \ |
||||
tripleo-ansible/tripleo_ansible/playbooks/cli-enable-ssh-admin.yaml \ |
||||
-e @ceph-admin.yml |
||||
|
||||
Where `ceph-admin.yml` contains something like the following: |
||||
|
||||
.. code-block:: YAML |
||||
|
||||
--- |
||||
tripleo_admin_user: ceph-admin |
||||
ssh_servers: "{{ groups['mons'] }}" |
||||
distribute_private_key: true |
||||
|
||||
The `ssh_servers` variable should be expanded to contain another other nodes |
||||
hosting Ceph, e.g. `osds`. |
||||
|
||||
- A `cephadm-spec`_ file should be provided which references the Ceph services |
||||
to be run on the other `ssh_hosts`. The path to this file can be set with |
||||
the `ceph_spec` variable. |
||||
|
||||
Usage |
||||
~~~~~ |
||||
|
||||
Here is an example of a playbook which bootstraps the first Ceph monitor |
||||
and then applies a spec file to add other hosts. It then creates RBD pools |
||||
for Nova, Cinder, and Glance and a cephx keyring called `openstack` to access |
||||
those pools. It then creates a file which can be passed as input to the role |
||||
`tripleo_ceph_client` so that an overcloud can be configured to use the deployed |
||||
Ceph cluster. |
||||
|
||||
.. code-block:: YAML |
||||
|
||||
- name: Deploy Ceph with cephadm |
||||
hosts: mons[0] |
||||
vars: |
||||
bootstrap_host: "{{ groups['mons'][0] }}" |
||||
tripleo_cephadm_spec_on_bootstrap: false |
||||
pools: |
||||
- vms |
||||
- volumes |
||||
- images |
||||
tasks: |
||||
- name: Satisfy Ceph prerequisites |
||||
import_role: |
||||
role: tripleo_cephadm |
||||
tasks_from: pre |
||||
|
||||
- name: Bootstrap Ceph |
||||
import_role: |
||||
role: tripleo_cephadm |
||||
tasks_from: bootstrap |
||||
|
||||
- name: Apply Ceph spec |
||||
import_role: |
||||
role: tripleo_cephadm |
||||
tasks_from: apply_spec |
||||
when: not tripleo_cephadm_spec_on_bootstrap |
||||
|
||||
- name: Create Pools |
||||
import_role: |
||||
role: tripleo_cephadm |
||||
tasks_from: pools |
||||
|
||||
- name: Create Keys |
||||
import_role: |
||||
role: tripleo_cephadm |
||||
tasks_from: keys |
||||
|
||||
- name: Export configuration for tripleo_ceph_client |
||||
import_role: |
||||
role: tripleo_cephadm |
||||
tasks_from: export |
||||
vars: |
||||
cephx_keys: |
||||
- client.openstack |
||||
|
||||
|
||||
.. _tripleo_ceph_spec: https://specs.openstack.org/openstack/tripleo-specs/specs/wallaby/tripleo-ceph.html |
||||
.. _cephadm: https://docs.ceph.com/en/latest/cephadm/ |
||||
.. _orchestrator: https://docs.ceph.com/en/latest/mgr/orchestrator/ |
||||
.. _ceph_key: https://github.com/ceph/ceph-ansible/blob/master/library/ceph_key.py |
||||
.. _ceph-ansible: https://github.com/ceph/ceph-ansible/ |
||||
.. _cephadm-spec: https://tracker.ceph.com/issues/44205 |
@ -0,0 +1,748 @@
|
||||
#!/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. |
||||
# Included from: https://github.com/ceph/ceph-ansible/blob/master/library/ceph_key.py |
||||
|
||||
from __future__ import absolute_import, division, print_function |
||||
__metaclass__ = type |
||||
|
||||
from ansible.module_utils.basic import AnsibleModule |
||||
import datetime |
||||
import json |
||||
import yaml |
||||
import os |
||||
import struct |
||||
import time |
||||
import base64 |
||||
import socket |
||||
|
||||
|
||||
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" |
||||
notes: [] |
||||
description: |
||||
- Manage CephX creation, deletion and updates. |
||||
It can also list and get information about keyring(s). |
||||
requirements: |
||||
- None |
||||
options: |
||||
cluster: |
||||
description: |
||||
- The ceph cluster name. |
||||
required: false |
||||
type: str |
||||
default: ceph |
||||
name: |
||||
description: |
||||
- name of the CephX key |
||||
type: str |
||||
required: true |
||||
user: |
||||
description: |
||||
- entity used to perform operation. |
||||
It corresponds to the -n option (--name) |
||||
type: str |
||||
required: false |
||||
default: client.admin |
||||
user_key: |
||||
description: |
||||
- the path to the keyring corresponding to the |
||||
user being used. It corresponds to the -k |
||||
option (--keyring) |
||||
type: str |
||||
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 |
||||
type: str |
||||
choices: ['present', 'update', 'absent', 'list', 'info', 'fetch_initial_keys', 'generate_secret'] |
||||
default: 'present' |
||||
caps: |
||||
description: |
||||
- CephX key capabilities |
||||
type: dict |
||||
required: false |
||||
secret: |
||||
description: |
||||
- keyring's secret value |
||||
required: false |
||||
type: str |
||||
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 |
||||
type: bool |
||||
default: true |
||||
dest: |
||||
description: |
||||
- Destination to write the keyring, can a file or a directory |
||||
required: false |
||||
type: str |
||||
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. |
||||
This command can ONLY run from a monitor node. |
||||
required: false |
||||
type: str |
||||
default: 'false' |
||||
output_format: |
||||
description: |
||||
- The key output format when retrieving the information of an |
||||
entity. |
||||
required: false |
||||
type: str |
||||
default: 'json' |
||||
""" |
||||
|
||||
EXAMPLES = ''' |
||||
|
||||
keys_to_create: |
||||
- { name: client.key, key: "AQAin8tUUK84ExAA/QgBtI7gEMWdmnvKBzlXdQ==", \ |
||||
caps: { mon: "allow rwx", mds: "allow *" } , mode: "0600" } |
||||
- { name: client.cle, caps: { mon: "allow r", osd: "allow *" } , mode: "0600" } |
||||
|
||||
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', |
||||
'client.bootstrap-osd', 'client.bootstrap-rbd', |
||||
'client.bootstrap-rbd-mirror', 'client.bootstrap-rgw'] |
||||
|
||||
|
||||
def fatal(message, module): |
||||
''' |
||||
Report a fatal error and exit |
||||
''' |
||||
|
||||
if module: |
||||
module.fail_json(msg=message, rc=1) |
||||
else: |
||||
raise(Exception(message)) |
||||
|
||||
|
||||
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_cmd(cluster, args, user, user_key_path, container_image=None): |
||||
''' |
||||
Generate 'ceph' command line to execute |
||||
''' |
||||
|
||||
if container_image: |
||||
binary = 'ceph' |
||||
cmd = container_exec( |
||||
binary, container_image) |
||||
else: |
||||
binary = ['ceph'] |
||||
cmd = binary |
||||
|
||||
base_cmd = [ |
||||
'-n', |
||||
user, |
||||
'-k', |
||||
user_key_path, |
||||
'--cluster', |
||||
cluster, |
||||
'auth', |
||||
] |
||||
|
||||
cmd.extend(base_cmd + args) |
||||
|
||||
return cmd |
||||
|
||||
# Start TripleO change |
||||
# Tripleo only needs ca_common module_utils for this module. |
||||
# Rather than add to tripleo-ansible's module_utils, insert 6 functions here |
||||
# https://github.com/ceph/ceph-ansible/blob/master/module_utils/ca_common.py |
||||
|
||||
|
||||
def container_exec(binary, container_image): |
||||
''' |
||||
Build the docker CLI to run a command inside a container |
||||
''' |
||||
|
||||
container_binary = os.getenv('CEPH_CONTAINER_BINARY') |
||||
command_exec = [container_binary, |
||||
'run', |
||||
'--rm', |
||||
'--net=host', |
||||
'-v', '/etc/ceph:/etc/ceph:z', |
||||
'-v', '/var/lib/ceph/:/var/lib/ceph/:z', |
||||
'-v', '/var/log/ceph/:/var/log/ceph/:z', |
||||
'--entrypoint={}'.format(binary), container_image] |
||||
return command_exec |
||||
|
||||
|
||||
def is_containerized(): |
||||
''' |
||||
Check if we are running on a containerized cluster |
||||
''' |
||||
|
||||
if 'CEPH_CONTAINER_IMAGE' in os.environ: |
||||
container_image = os.getenv('CEPH_CONTAINER_IMAGE') |
||||
else: |
||||
container_image = None |
||||
|
||||
return container_image |
||||
|
||||
# End TripleO change |
||||
|
||||
|
||||
def generate_ceph_authtool_cmd(cluster, name, secret, caps, dest, container_image=None): |
||||
''' |
||||
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_path, name, secret, caps, |
||||
import_key, dest, container_image=None): |
||||
''' |
||||
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( |
||||
cluster, args, user, user_key_path, container_image)) |
||||
|
||||
return cmd_list |
||||
|
||||
|
||||
def delete_key(cluster, user, user_key_path, name, container_image=None): |
||||
''' |
||||
Delete a CephX key |
||||
''' |
||||
|
||||
cmd_list = [] |
||||
|
||||
args = [ |
||||
'del', |
||||
name, |
||||
] |
||||
|
||||
cmd_list.append(generate_ceph_cmd( |
||||
cluster, args, user, user_key_path, container_image)) |
||||
|
||||
return cmd_list |
||||
|
||||
|
||||
def get_key(cluster, user, user_key_path, 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( |
||||
cluster, args, user, user_key_path, container_image)) |
||||
|
||||
return cmd_list |
||||
|
||||
|
||||
def info_key(cluster, name, user, user_key_path, output_format, |
||||
container_image=None): |
||||
''' |
||||
Get information about a CephX key |
||||
''' |
||||
|
||||
cmd_list = [] |
||||
|
||||
args = [ |
||||
'get', |
||||
name, |
||||
'-f', |
||||
output_format, |
||||
] |
||||
|
||||
cmd_list.append(generate_ceph_cmd( |
||||
cluster, args, user, user_key_path, container_image)) |
||||
|
||||
return cmd_list |
||||
|
||||
|
||||
def list_keys(cluster, user, user_key_path, container_image=None): |
||||
''' |
||||
List all CephX keys |
||||
''' |
||||
|
||||
cmd_list = [] |
||||
|
||||
args = [ |
||||
'ls', |
||||
'-f', |
||||
'json', |
||||
] |
||||
|
||||
cmd_list.append(generate_ceph_cmd( |
||||
cluster, args, user, user_key_path, 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) |
||||
|
||||
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) |
||||
|
||||
if len(entities) != len(CEPH_INITIAL_KEYS): |
||||
# 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) |
||||
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 = AnsibleModule( |
||||
argument_spec=yaml.safe_load(DOCUMENTATION)['options'], |
||||
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: |
||||
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)) |
||||
key_exist = rc |
||||
if not caps and key_exist != 0: |
||||
fatal("Capabilities must be provided when state " |
||||
"is 'present'", module) |
||||
if key_exist != 0 and secret is None and caps is None: |
||||
fatal("Keyring doesn't exist, you must provide " |
||||
"'secret' and 'caps'", module) |
||||
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)) |
||||
result["rc"] = rc |
||||
if rc != 0: |
||||
result["stdout"] = "Couldn't fetch the key {0} at " \ |
||||
"{1}.".format(name, file_path) |
||||
module.exit_json(**result) |
||||
result["stdout"] = "fetched the key {0} at " \ |
||||
"{1}.".format(name, file_path) |
||||
|
||||
result["stdout"] = "{0} already exists and doesn't " \ |
||||
"need to be updated.".format(name) |
||||
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) |
||||
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)) |
||||
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)) |
||||
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)) |
||||
if rc != 0: |
||||
result["stdout"] = "skipped, since {0} does not exist".format(name) |
||||
result['rc'] = 0 |
||||
module.exit_json(**result) |
||||
|
||||
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) |
||||
|
||||
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() |
@ -0,0 +1,766 @@
|
||||
#!/usr/bin/python3 |
||||
|
||||
# Copyright 2020, 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. |
||||
# Included from: https://github.com/ceph/ceph-ansible/blob/master/library/ceph_pool.py |
||||
|
||||
from __future__ import absolute_import, division, print_function |
||||
__metaclass__ = type |
||||
|
||||
from ansible.module_utils.basic import AnsibleModule |
||||
|
||||
import datetime |
||||
import json |
||||
import yaml |
||||
import os |
||||
|
||||
|
||||
ANSIBLE_METADATA = { |
||||
'metadata_version': '1.1', |
||||
'status': ['preview'], |
||||
'supported_by': 'community' |
||||
} |
||||
|
||||
DOCUMENTATION = """ |
||||
--- |
||||
module: ceph_pool |
||||
author: Guillaume Abrioux <gabrioux@redhat.com> |
||||
short_description: Manage Ceph Pools |
||||
version_added: "2.8" |
||||
description: |
||||
- Manage Ceph pool(s) creation, deletion and updates. |
||||
options: |
||||
cluster: |
||||
description: |
||||
- The ceph cluster name. |
||||
required: false |
||||
default: ceph |
||||
type: str |
||||
name: |
||||
description: |
||||
- name of the Ceph pool |
||||
required: true |
||||
type: str |
||||
state: |
||||
description: |
||||
If 'present' is used, the module creates a pool if it doesn't exist |
||||
or update it if it already exists. |
||||
If 'absent' is used, the module will simply delete the pool. |
||||
If 'list' is used, the module will return all details about the |
||||
existing pools. (json formatted). |
||||
required: false |
||||
type: str |
||||
choices: ['present', 'absent', 'list'] |
||||
default: present |
||||
size: |
||||
description: |
||||
- set the replica size of the pool. |
||||
required: false |
||||
type: str |
||||
min_size: |
||||
description: |
||||
- set the min_size parameter of the pool. |
||||
required: false |
||||
type: str |
||||
pg_num: |
||||
description: |
||||
- set the pg_num of the pool. |
||||
required: false |
||||
type: str |
||||
pgp_num: |
||||
description: |
||||
- set the pgp_num of the pool. |
||||
required: false |
||||
type: str |
||||
pg_autoscale_mode: |
||||
description: |
||||
- set the pg autoscaler on the pool. |
||||
required: false |
||||
default: 'on' |
||||
type: str |
||||
target_size_ratio: |
||||
description: |
||||
- set the target_size_ratio on the pool |
||||
required: false |
||||
type: str |
||||
pool_type: |
||||
description: |
||||
- set the pool type, either 'replicated' or 'erasure' |
||||
required: false |
||||
default: 'replicated' |
||||
type: str |
||||
erasure_profile: |
||||
description: |
||||
- When pool_type = 'erasure', set the erasure profile of the pool |
||||
required: false |
||||
default: 'default' |
||||
type: str |
||||
rule_name: |
||||
description: |
||||
- Set the crush rule name assigned to the pool |
||||
required: false |
||||
default: 'replicated_rule' |
||||
type: str |
||||
expected_num_objects: |
||||
description: |
||||
- Set the expected_num_objects parameter of the pool. |
||||
required: false |
||||
default: '0' |
||||
application: |
||||
description: |
||||
- Set the pool application on the pool. |
||||
required: false |
||||
type: str |
||||
""" |
||||
|
||||
EXAMPLES = ''' |
||||
|
||||
pools: |
||||
- { name: foo, size: 3, application: rbd, pool_type: 'replicated', |
||||
pg_autoscale_mode: 'on' } |
||||
|
||||
- hosts: all |
||||
become: true |
||||
tasks: |
||||
- name: create a pool |
||||
ceph_pool: |
||||
name: "{{ item.name }}" |
||||
state: present |
||||
size: "{{ item.size }}" |
||||
application: "{{ item.application }}" |
||||
pool_type: "{{ item.pool_type }}" |
||||
pg_autoscale_mode: "{{ item.pg_autoscale_mode }}" |
||||
with_items: "{{ pools }}" |
||||
''' |
||||
|
||||
RETURN = '''# ''' |
||||
|
||||
# Start TripleO change |
||||
# Tripleo only needs ca_common module_utils for this module. |
||||
# Rather than add to tripleo-ansible's module_utils, insert 6 functions here |
||||
# https://github.com/ceph/ceph-ansible/blob/master/module_utils/ca_common.py |
||||
|
||||
|
||||
def generate_ceph_cmd(sub_cmd, args, user_key=None, |
||||
cluster='ceph', user='client.admin', |
||||
container_image=None, interactive=False): |
||||
''' |
||||
Generate 'ceph' command line to execute |
||||
''' |
||||
|
||||
if not user_key: |
||||
user_key = '/etc/ceph/{}.{}.keyring'.format(cluster, user) |
||||
|
||||
cmd = pre_generate_ceph_cmd(container_image=container_image, interactive=interactive) |
||||
|
||||
base_cmd = [ |
||||
'-n', |
||||
user, |
||||
'-k', |
||||
user_key, |
||||
'--cluster', |
||||
cluster |
||||
] |
||||
base_cmd.extend(sub_cmd) |
||||
cmd.extend(base_cmd + args) |
||||
|
||||
return cmd |
||||
|
||||
|
||||
def container_exec(binary, container_image, interactive=False): |
||||
''' |
||||
Build the docker CLI to run a command inside a container |
||||
''' |
||||
|
||||
container_binary = os.getenv('CEPH_CONTAINER_BINARY') |
||||
command_exec = [container_binary, 'run'] |
||||
|
||||
if interactive: |
||||
command_exec.extend(['--interactive']) |
||||
|
||||
command_exec.extend(['--rm', |
||||
'--net=host', |
||||
'-v', '/etc/ceph:/etc/ceph:z', |
||||
'-v', '/var/lib/ceph/:/var/lib/ceph/:z', |
||||
'-v', '/var/log/ceph/:/var/log/ceph/:z', |
||||
'--entrypoint={}'.format(binary), container_image]) |
||||
return command_exec |
||||
|
||||
|
||||
def is_containerized(): |
||||
''' |
||||
Check if we are running on a containerized cluster |
||||
''' |
||||
|
||||
if 'CEPH_CONTAINER_IMAGE' in os.environ: |
||||
container_image = os.getenv('CEPH_CONTAINER_IMAGE') |
||||
else: |
||||
container_image = None |
||||
|
||||
return container_image |
||||
|
||||
|
||||
def pre_generate_ceph_cmd(container_image=None, interactive=False): |
||||
''' |
||||
Generate ceph prefix comaand |
||||
''' |
||||
if container_image: |
||||
cmd = container_exec('ceph', container_image, interactive=interactive) |
||||
else: |
||||
cmd = ['ceph'] |
||||
|
||||
return cmd |
||||
|
||||
|
||||
def exec_command(module, cmd, stdin=None): |
||||
''' |
||||
Execute command(s) |
||||
''' |
||||
|
||||
binary_data = False |
||||
if stdin: |
||||
binary_data = True |
||||
rc, out, err = module.run_command(cmd, data=stdin, binary_data=binary_data) |
||||
|
||||
return rc, cmd, out, err |
||||
|
||||
|
||||
def exit_module(module, out, rc, cmd, err, startd, changed=False): |
||||
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, |
||||
) |
||||
module.exit_json(**result) |
||||
# End TripleO change |
||||
|
||||
|
||||
def check_pool_exist(cluster, |
||||
name, |
||||
user, |
||||
user_key, |
||||
output_format='json', |
||||
container_image=None): |
||||
''' |
||||
Check if a given pool exists |
||||
''' |
||||
|
||||
args = ['stats', name, '-f', output_format] |
||||
|
||||
cmd = generate_ceph_cmd(sub_cmd=['osd', 'pool'], |
||||
args=args, |
||||
cluster=cluster, |
||||
user=user, |
||||
user_key=user_key, |
||||
container_image=container_image) |
||||
|
||||
return cmd |
||||
|
||||
|
||||
def generate_get_config_cmd(param, |
||||
cluster, |
||||
user, |
||||
user_key, |
||||
container_image=None): |
||||
_cmd = pre_generate_ceph_cmd(container_image=container_image) |
||||
args = [ |
||||
'-n', |
||||
user, |
||||
'-k', |
||||
user_key, |
||||
'--cluster', |
||||
cluster, |
||||
'config', |
||||
'get', |
||||
'mon.*', |
||||
param |
||||
] |
||||
cmd = _cmd + args |
||||
return cmd |
||||
|
||||
|
||||
def get_application_pool(cluster, |
||||
name, |
||||
user, |
||||
user_key, |
||||
output_format='json', |
||||
container_image=None): |
||||
''' |
||||
Get application type enabled on a given pool |
||||
''' |
||||
|
||||
args = ['application', 'get', name, '-f', output_format] |
||||
|
||||
cmd = generate_ceph_cmd(sub_cmd=['osd', 'pool'], |
||||
args=args, |
||||
cluster=cluster, |
||||
user=user, |
||||
user_key=user_key, |
||||
container_image=container_image) |
||||
|
||||
return cmd |
||||
|
||||
|
||||
def enable_application_pool(cluster, |
||||
name, |
||||
application, |
||||
user, |
||||
user_key, |
||||
container_image=None): |
||||
''' |
||||
Enable application on a given pool |
||||
''' |
||||
|
||||
args = ['application', 'enable', name, application] |
||||
|
||||
cmd = generate_ceph_cmd(sub_cmd=['osd', 'pool'], |
||||
args=args, |
||||
cluster=cluster, |
||||
user=user, |
||||
user_key=user_key, |
||||
container_image=container_image) |
||||
|
||||
return cmd |
||||
|
||||
|
||||
def disable_application_pool(cluster, |
||||
name, |
||||
application, |
||||
user, |
||||
user_key, |
||||
container_image=None): |
||||
''' |
||||
Disable application on a given pool |
||||
''' |
||||
|
||||
args = ['application', 'disable', name, |
||||
application, '--yes-i-really-mean-it'] |
||||
|
||||
cmd = generate_ceph_cmd(sub_cmd=['osd', 'pool'], |
||||
args=args, |
||||
cluster=cluster, |
||||
user=user, |
||||
user_key=user_key, |
||||
container_image=container_image) |
||||
|
||||
return cmd |
||||
|
||||
|
||||
def get_pool_details(module, |
||||
cluster, |
||||
name, |
||||
user, |
||||
user_key, |
||||
output_format='json', |
||||
container_image=None): |
||||
''' |
||||
Get details about a given pool |
||||
''' |
||||
|
||||
args = ['ls', 'detail', '-f', output_format] |
||||
|
||||
cmd = generate_ceph_cmd(sub_cmd=['osd', 'pool'], |
||||
args=args, |
||||
cluster=cluster, |
||||
user=user, |
||||
user_key=user_key, |
||||
container_image=container_image) |
||||
|
||||
rc, cmd, out, err = exec_command(module, cmd) |
||||
|
||||
if rc == 0: |
||||
out = [p for p in json.loads(out.strip()) if p['pool_name'] == name][0] |
||||
|
||||
_rc, _cmd, application_pool, _err = exec_command(module, |
||||
get_application_pool(cluster, # noqa: E501 |
||||
name, # noqa: E501 |
||||
user, # noqa: E501 |
||||
user_key, # noqa: E501 |
||||
container_image=container_image)) # noqa: E501 |
||||
|
||||
# This is a trick because "target_size_ratio" isn't present at the same level in the dict |
||||
# ie: |
||||
# { |
||||
# 'pg_num': 8, |
||||
# 'pgp_num': 8, |
||||
# 'pg_autoscale_mode': 'on', |
||||
# 'options': { |
||||
# 'target_size_ratio': 0.1 |
||||
# } |
||||
# } |
||||
# If 'target_size_ratio' is present in 'options', we set it, this way we end up |
||||
# with a dict containing all needed keys at the same level. |
||||
if 'target_size_ratio' in out['options'].keys(): |
||||
out['target_size_ratio'] = out['options']['target_size_ratio'] |
||||
else: |
||||
out['target_size_ratio'] = None |
||||
|
||||
application = list(json.loads(application_pool.strip()).keys()) |
||||
|
||||
if len(application) == 0: |
||||
out['application'] = '' |
||||
else: |
||||
out['application'] = application[0] |
||||
|
||||
return rc, cmd, out, err |
||||
|
||||
|
||||
def compare_pool_config(user_pool_config, running_pool_details): |
||||
''' |
||||
Compare user input config pool details with current running pool details |
||||
''' |
||||
|
||||
delta = {} |
||||
filter_keys = ['pg_num', 'pg_placement_num', 'size', |
||||
'pg_autoscale_mode', 'target_size_ratio'] |
||||
for key in filter_keys: |
||||
if (str(running_pool_details[key]) != user_pool_config[key]['value'] and user_pool_config[key]['value']): |
||||
delta[key] = user_pool_config[key] |
||||
|
||||
if (running_pool_details['application'] != user_pool_config['application']['value'] and user_pool_config['application']['value']): |
||||
delta['application'] = {} |
||||
delta['application']['new_application'] = user_pool_config['application']['value'] # noqa: E501 |
||||
# to be improved (for update_pools()...) |
||||
delta['application']['value'] = delta['application']['new_application'] |
||||
delta['application']['old_application'] = running_pool_details['application'] # noqa: E501 |
||||
|
||||
return delta |
||||
|
||||
|
||||
def list_pools(cluster, |
||||
user, |
||||
user_key, |
||||
details, |
||||
output_format='json', |
||||
container_image=None): |
||||
''' |
||||
List existing pools |
||||
''' |
||||
|
||||
args = ['ls'] |
||||
|
||||
if details: |
||||
args.append('detail') |
||||
|
||||
args.extend(['-f', output_format]) |
||||
|
||||
cmd = generate_ceph_cmd(sub_cmd=['osd', 'pool'], |
||||
args=args, |
||||
cluster=cluster, |
||||
user=user, |
||||
user_key=user_key, |
||||
container_image=container_image) |
||||
|
||||
return cmd |
||||
|
||||
|
||||
def create_pool(cluster, |
||||
name, |
||||
user, |
||||
user_key, |
||||
user_pool_config, |
||||
container_image=None): |
||||
''' |
||||
Create a new pool |
||||
''' |
||||
|
||||
args = ['create', user_pool_config['pool_name']['value'], |
||||
user_pool_config['type']['value']] |
||||
|
||||
if user_pool_config['pg_autoscale_mode']['value'] != 'on': |
||||
args.extend(['--pg_num', |
||||
user_pool_config['pg_num']['value'], |
||||
'--pgp_num', |
||||
user_pool_config['pgp_num']['value']]) |
||||
elif user_pool_config['target_size_ratio']['value']: |
||||
args.extend(['--target_size_ratio', |
||||
user_pool_config['target_size_ratio']['value']]) |
||||
|
||||
if user_pool_config['type']['value'] == 'replicated': |
||||
args.extend([user_pool_config['crush_rule']['value'], |
||||
'--expected_num_objects', |
||||
user_pool_config['expected_num_objects']['value'], |
||||
'--autoscale-mode', |
||||
user_pool_config['pg_autoscale_mode']['value']]) |
||||
|
||||
if (user_pool_config['size']['value'] and user_pool_config['type']['value'] == "replicated"): |
||||
args.extend(['--size', user_pool_config['size']['value']]) |
||||
|
||||
elif user_pool_config['type']['value'] == 'erasure': |
||||
args.extend([user_pool_config['erasure_profile']['value']]) |
||||
|
||||
if user_pool_config['crush_rule']['value']: |
||||
args.extend([user_pool_config['crush_rule']['value']]) |
||||
|
||||
args.extend(['--expected_num_objects', |
||||
user_pool_config['expected_num_objects']['value'], |
||||
'--autoscale-mode', |
||||
user_pool_config['pg_autoscale_mode']['value']]) |
||||
|
||||
cmd = generate_ceph_cmd(sub_cmd=['osd', 'pool'], |
||||
args=args, |
||||
cluster=cluster, |
||||
user=user, |
||||
user_key=user_key, |
||||
container_image=container_image) |
||||
|
||||
return cmd |
||||
|
||||
|
||||
def remove_pool(cluster, name, user, user_key, container_image=None): |
||||
''' |
||||
Remove a pool |
||||
''' |
||||
|
||||
args = ['rm', name, name, '--yes-i-really-really-mean-it'] |
||||
|
||||
cmd = generate_ceph_cmd(sub_cmd=['osd', 'pool'], |
||||
args=args, |
||||
cluster=cluster, |
||||
user=user, |
||||
user_key=user_key, |
||||
container_image=container_image) |
||||
|
||||
return cmd |
||||
|
||||
|
||||
def update_pool(module, cluster, name, |
||||
user, user_key, delta, container_image=None): |
||||
''' |
||||
Update an existing pool |
||||
''' |
||||
|
||||
report = "" |
||||
|
||||
for key in delta.keys(): |
||||
if key != 'application': |
||||
args = ['set', |
||||
name, |
||||
delta[key]['cli_set_opt'], |
||||
delta[key]['value']] |
||||
|
||||
cmd = generate_ceph_cmd(sub_cmd=['osd', 'pool'], |
||||
args=args, |
||||
cluster=cluster, |
||||
user=user, |
||||
user_key=user_key, |
||||
container_image=container_image) |
||||
|
||||
|