deep_scrub_choked

This commit is contained in:
a.pivkin
2026-04-06 07:08:17 +03:00
commit 9149dc1d7f
840 changed files with 52471 additions and 0 deletions
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+571
View File
@@ -0,0 +1,571 @@
#!/usr/bin/python
# Copyright 2018 Daniel Pivonka <dpivonka@redhat.com>
# Copyright 2018 Red Hat, Inc.
#
# GNU General Public License v3.0+
from ansible.module_utils.basic import AnsibleModule
from socket import error as socket_error
import boto
import radosgw
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: ceph_add_users_buckets
short_description: bulk create user and buckets
description:
- Bulk create Ceph Object Storage users and buckets
option:
rgw_host:
description:
- a radosgw host in the ceph cluster
required: true
port:
description:
- tcp port of the radosgw host
required: true
is_secure:
description:
- boolean indicating whether the instance is running over https
required: false
default: false
admin_access_key:
description:
- radosgw admin user's access key
required: true
admin_secret_key:
description:
- radosgw admin user's secret key
required: true
users:
description:
- list of users to be created containing sub options
required: false
sub_options:
username:
description:
- username for new user
required: true
fullname:
description:
- fullname for new user
required: true
email:
description:
- email for new user
required: false
maxbucket:
description:
- max bucket for new user
required: false
default: 1000
suspend:
description:
- suspend a new user apon creation
required: false
default: false
autogenkey:
description:
- auto generate keys for new user
required: false
default: true
accesskey:
description:
- access key for new user
required: false
secretkey:
description:
- secret key for new user
required: false
userquota:
description:
- enable/disable user quota for new user
required: false
default: false
usermaxsize:
description:
- with user quota enabled specify quota size in kb
required: false
default: unlimited
usermaxobjects:
description:
- with user quota enabled specify maximum number of objects
required: false
default: unlimited
bucketquota:
description:
- enable/disable bucket quota for new user
required: false
default: false
bucketmaxsize:
description:
- with bucket quota enabled specify bucket size in kb
required: false
default: unlimited
bucketmaxobjects:
description:
- with bucket quota enabled specify maximum number of objects # noqa: E501
required: false
default: unlimited
buckets:
description:
- list of buckets to be created containing sub options
required: false
sub_options:
bucket:
description:
- name for new bucket
required: true
user:
description:
- user new bucket will be linked too
required: true
requirements: ['radosgw', 'boto']
author:
- 'Daniel Pivonka'
'''
EXAMPLES = '''
# single basic user
- name: single basic user
ceph_add_users_buckets:
rgw_host: '172.16.0.12'
port: 8080
admin_access_key: 'N61I8625V4XTWGDTLBLL'
admin_secret_key: 'HZrkuHHO9usUurDWBQHTeLIjO325bIULaC7DxcoV'
users:
- username: 'test1'
fullname: 'tester'
# single complex user
- name: single complex user
ceph_add_users_buckets:
rgw_host: '172.16.0.12'
port: 8080
admin_access_key: 'N61I8625V4XTWGDTLBLL'
admin_secret_key: 'HZrkuHHO9usUurDWBQHTeLIjO325bIULaC7DxcoV'
users:
- username: 'test1'
fullname: 'tester'
email: 'dan@email.com'
maxbucket: 666
suspend: true
autogenkey: true
accesskey: 'B3AR4Q33L59YV56A9A2F'
secretkey: 'd84BRnMysnVGSyZiRlYUMduVgIarQWiNMdKzrF76'
userquota: true
usermaxsize: '1000'
usermaxobjects: 3
bucketquota: true
bucketmaxsize: '1000'
bucketmaxobjects: 3
# multi user
- name: multi user
ceph_add_users_buckets:
rgw_host: '172.16.0.12'
port: 8080
admin_access_key: 'N61I8625V4XTWGDTLBLL'
admin_secret_key: 'HZrkuHHO9usUurDWBQHTeLIjO325bIULaC7DxcoV'
users:
- username: 'test1'
fullname: 'tester'
email: 'dan@email.com'
maxbucket: 666
suspend: true
autogenkey: true
accesskey: 'B3AR4Q33L59YV56A9A2F'
secretkey: 'd84BRnMysnVGSyZiRlYUMduVgIarQWiNMdKzrF76'
userquota: true
usermaxsize: '1000K'
usermaxobjects: 3
bucketquota: true
bucketmaxsize: '1000K'
bucketmaxobjects: 3
- username: 'test2'
fullname: 'tester'
# single bucket
- name: single basic user
ceph_add_users_buckets:
rgw_host: '172.16.0.12'
port: 8080
admin_access_key: 'N61I8625V4XTWGDTLBLL'
admin_secret_key: 'HZrkuHHO9usUurDWBQHTeLIjO325bIULaC7DxcoV'
buckets:
- bucket: 'heyimabucket1'
user: 'test1'
# multi bucket
- name: single basic user
ceph_add_users_buckets:
rgw_host: '172.16.0.12'
port: 8080
admin_access_key: 'N61I8625V4XTWGDTLBLL'
admin_secret_key: 'HZrkuHHO9usUurDWBQHTeLIjO325bIULaC7DxcoV'
buckets:
- bucket: 'heyimabucket1'
user: 'test1'
- bucket: 'heyimabucket2'
user: 'test2'
- bucket: 'heyimabucket3'
user: 'test2'
# buckets and users
- name: single basic user
ceph_add_users_buckets:
rgw_host: '172.16.0.12'
port: 8080
admin_access_key: 'N61I8625V4XTWGDTLBLL'
admin_secret_key: 'HZrkuHHO9usUurDWBQHTeLIjO325bIULaC7DxcoV'
users:
- username: 'test1'
fullname: 'tester'
email: 'dan@email.com'
maxbucket: 666
- username: 'test2'
fullname: 'tester'
email: 'dan1@email.com'
accesskey: 'B3AR4Q33L59YV56A9A2F'
secretkey: 'd84BRnMysnVGSyZiRlYUMduVgIarQWiNMdKzrF76'
userquota: true
usermaxsize: '1000'
usermaxobjects: 3
bucketquota: true
bucketmaxsize: '1000'
bucketmaxobjects: 3
buckets:
- bucket: 'heyimabucket1'
user: 'test1'
- bucket: 'heyimabucket2'
user: 'test2'
- bucket: 'heyimabucket3'
user: 'test2'
'''
RETURN = '''
error_messages:
description: error for failed user or bucket.
returned: always
type: list
sample: [
"test2: could not modify user: unable to modify user, cannot add duplicate email\n" # noqa: E501
]
failed_users:
description: users that were not created.
returned: always
type: str
sample: "test2"
added_users:
description: users that were created.
returned: always
type: str
sample: "test1"
failed_buckets:
description: buckets that were not created.
returned: always
type: str
sample: "heyimabucket3"
added_buckets:
description: buckets that were created.
returned: always
type: str
sample: "heyimabucket1, heyimabucket2"
'''
def create_users(rgw, users, result):
added_users = []
failed_users = []
for user in users:
# get info
username = user['username']
fullname = user['fullname']
email = user['email']
maxbucket = user['maxbucket']
suspend = user['suspend']
autogenkey = user['autogenkey']
accesskey = user['accesskey']
secretkey = user['secretkey']
userquota = user['userquota']
usermaxsize = user['usermaxsize']
usermaxobjects = user['usermaxobjects']
bucketquota = user['bucketquota']
bucketmaxsize = user['bucketmaxsize']
bucketmaxobjects = user['bucketmaxobjects']
fail_flag = False
# check if user exists
try:
user_info = rgw.get_user(uid=username)
except radosgw.exception.RadosGWAdminError:
# it doesnt exist
user_info = None
# user exists can not create
if user_info:
result['error_messages'].append(username + ' UserExists')
failed_users.append(username)
else:
# user doesnt exist create it
if email:
if autogenkey:
try:
rgw.create_user(username, fullname, email=email, key_type='s3', # noqa: E501
generate_key=autogenkey,
max_buckets=maxbucket, suspended=suspend) # noqa: E501
except radosgw.exception.RadosGWAdminError as e:
result['error_messages'].append(username + ' ' + e.get_code()) # noqa: E501
fail_flag = True
else:
try:
rgw.create_user(username, fullname, email=email, key_type='s3', # noqa: E501
access_key=accesskey, secret_key=secretkey, # noqa: E501
max_buckets=maxbucket, suspended=suspend) # noqa: E501
except radosgw.exception.RadosGWAdminError as e:
result['error_messages'].append(username + ' ' + e.get_code()) # noqa: E501
fail_flag = True
else:
if autogenkey:
try:
rgw.create_user(username, fullname, key_type='s3',
generate_key=autogenkey,
max_buckets=maxbucket, suspended=suspend) # noqa: E501
except radosgw.exception.RadosGWAdminError as e:
result['error_messages'].append(username + ' ' + e.get_code()) # noqa: E501
fail_flag = True
else:
try:
rgw.create_user(username, fullname, key_type='s3',
access_key=accesskey, secret_key=secretkey, # noqa: E501
max_buckets=maxbucket, suspended=suspend) # noqa: E501
except radosgw.exception.RadosGWAdminError as e:
result['error_messages'].append(username + ' ' + e.get_code()) # noqa: E501
fail_flag = True
if not fail_flag and userquota:
try:
rgw.set_quota(username, 'user', max_objects=usermaxobjects,
max_size_kb=usermaxsize, enabled=True)
except radosgw.exception.RadosGWAdminError as e:
result['error_messages'].append(username + ' ' + e.get_code()) # noqa: E501
fail_flag = True
if not fail_flag and bucketquota:
try:
rgw.set_quota(username, 'bucket', max_objects=bucketmaxobjects, # noqa: E501
max_size_kb=bucketmaxsize, enabled=True)
except radosgw.exception.RadosGWAdminError as e:
result['error_messages'].append(username + ' ' + e.get_code()) # noqa: E501
fail_flag = True
if fail_flag:
try:
rgw.delete_user(username)
except radosgw.exception.RadosGWAdminError:
pass
failed_users.append(username)
else:
added_users.append(username)
result['added_users'] = ", ".join(added_users)
result['failed_users'] = ", ".join(failed_users)
def create_buckets(rgw, buckets, result):
added_buckets = []
failed_buckets = []
for bucket_info in buckets:
bucket = bucket_info['bucket']
user = bucket_info['user']
# check if bucket exists
try:
bucket_info = rgw.get_bucket(bucket_name=bucket)
except TypeError:
# it doesnt exist
bucket_info = None
# if it exists add to failed list
if bucket_info:
failed_buckets.append(bucket)
result['error_messages'].append(bucket + ' BucketExists')
else:
# bucket doesn't exist, so we need to create it
bucket_info = create_bucket(rgw, bucket)
if bucket_info:
# bucket created ok, link to user
# check if user exists
try:
user_info = rgw.get_user(uid=user)
except radosgw.exception.RadosGWAdminError:
# it doesnt exist
user_info = None
# user exists, link
if user_info:
try:
rgw.link_bucket(bucket_name=bucket,
bucket_id=bucket_info.id,
uid=user)
added_buckets.append(bucket)
except radosgw.exception.RadosGWAdminError as e:
result['error_messages'].append(bucket + e.get_code())
try:
rgw.delete_bucket(bucket, purge_objects=True)
except radosgw.exception.RadosGWAdminError:
pass
failed_buckets.append(bucket)
else:
# user doesnt exist cant be link delete bucket
try:
rgw.delete_bucket(bucket, purge_objects=True)
except radosgw.exception.RadosGWAdminError:
pass
failed_buckets.append(bucket)
result['error_messages'].append(bucket + ' could not be linked' + ', NoSuchUser ' + user) # noqa: E501
else:
# something went wrong
failed_buckets.append(bucket)
result['error_messages'].append(bucket + ' could not be created') # noqa: E501
result['added_buckets'] = ", ".join(added_buckets)
result['failed_buckets'] = ", ".join(failed_buckets)
def create_bucket(rgw, bucket):
conn = boto.connect_s3(aws_access_key_id=rgw.provider._access_key,
aws_secret_access_key=rgw.provider._secret_key,
host=rgw._connection[0],
port=rgw.port,
is_secure=rgw.is_secure,
calling_format=boto.s3.connection.OrdinaryCallingFormat(), # noqa: E501
)
try:
conn.create_bucket(bucket_name=bucket)
bucket_info = rgw.get_bucket(bucket_name=bucket)
except boto.exception.S3ResponseError:
return None
else:
return bucket_info
def main():
# arguments/parameters that a user can pass to the module
fields = dict(rgw_host=dict(type='str', required=True),
port=dict(type='int', required=True),
is_secure=dict(type='bool',
required=False,
default=False),
admin_access_key=dict(type='str', required=True),
admin_secret_key=dict(type='str', required=True),
buckets=dict(type='list', required=False, elements='dict',
options=dict(bucket=dict(type='str', required=True), # noqa: E501
user=dict(type='str', required=True))), # noqa: E501
users=dict(type='list', required=False, elements='dict',
options=dict(username=dict(type='str', required=True), # noqa: E501
fullname=dict(type='str', required=True), # noqa: E501
email=dict(type='str', required=False), # noqa: E501
maxbucket=dict(type='int', required=False, default=1000), # noqa: E501
suspend=dict(type='bool', required=False, default=False), # noqa: E501
autogenkey=dict(type='bool', required=False, default=True), # noqa: E501
accesskey=dict(type='str', required=False), # noqa: E501
secretkey=dict(type='str', required=False), # noqa: E501
userquota=dict(type='bool', required=False, default=False), # noqa: E501
usermaxsize=dict(type='str', required=False, default='-1'), # noqa: E501
usermaxobjects=dict(type='int', required=False, default=-1), # noqa: E501
bucketquota=dict(type='bool', required=False, default=False), # noqa: E501
bucketmaxsize=dict(type='str', required=False, default='-1'), # noqa: E501
bucketmaxobjects=dict(type='int', required=False, default=-1)))) # noqa: E501
# the AnsibleModule object
module = AnsibleModule(argument_spec=fields,
supports_check_mode=False)
# get vars
rgw_host = module.params.get('rgw_host')
port = module.params.get('port')
is_secure = module.params.get('is_secure')
admin_access_key = module.params.get('admin_access_key')
admin_secret_key = module.params.get('admin_secret_key')
users = module.params['users']
buckets = module.params.get('buckets')
# seed the result dict in the object
result = dict(
changed=False,
error_messages=[],
added_users='',
failed_users='',
added_buckets='',
failed_buckets='',
)
# radosgw connection
rgw = radosgw.connection.RadosGWAdminConnection(host=rgw_host,
port=port,
access_key=admin_access_key, # noqa: E501
secret_key=admin_secret_key, # noqa: E501
aws_signature='AWS4',
is_secure=is_secure)
# test connection
connected = True
try:
rgw.get_usage()
except radosgw.exception.RadosGWAdminError as e:
connected = False
result['error_messages'] = e.get_code()
except socket_error as e:
connected = False
result['error_messages'] = str(e)
if connected and users:
create_users(rgw, users, result)
if connected and buckets:
create_buckets(rgw, buckets, result)
if result['added_users'] != '' or result['added_buckets'] != '':
result['changed'] = True
# conditional state caused a failure
if result['added_users'] == '' and result['added_buckets'] == '':
module.fail_json(msg='No users or buckets were added successfully',
**result)
# EXIT
module.exit_json(**result)
if __name__ == '__main__':
main()
+131
View File
@@ -0,0 +1,131 @@
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 container_exec, \
is_containerized
except ImportError:
from module_utils.ca_common import container_exec, \
is_containerized
import datetime
import os
class KeyringExists(Exception):
pass
def build_cmd(create_keyring=False,
gen_key=False,
add_key=False,
import_keyring=None,
caps={},
name=None,
path=None,
container_image=None,
**a):
auth_tool_binary: str = 'ceph-authtool'
if container_image:
c = container_exec(auth_tool_binary,
container_image)
else:
c = [auth_tool_binary]
if name:
c.extend(['-n', name])
if create_keyring:
if os.path.exists(path):
raise KeyringExists
c.append('-C')
if gen_key:
c.append('-g')
if caps:
for k, v in caps.items():
c.extend(['--cap'] + [k] + [v])
c.append(path)
if import_keyring:
c.extend(['--import-keyring', import_keyring])
return c
def run_module():
module_args = dict(
name=dict(type='str', required=False),
create_keyring=dict(type='bool', required=False, default=False),
gen_key=dict(type='bool', required=False, default=False),
add_key=dict(type='str', required=False, default=None),
import_keyring=dict(type='str', required=False, default=None),
caps=dict(type='dict', required=False, default=None),
path=dict(type='str', required=True)
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True,
add_file_common_args=True,
)
cmd = []
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()
try:
cmd = build_cmd(**module.params, container_image=container_image)
except KeyringExists:
rc = 0
out = f"{module.params['path']} already exists. Skipping"
err = ""
else:
rc, out, err = module.run_command(cmd)
if rc == 0:
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)
# file_args = module.load_file_common_arguments(module.params)
# module.set_fs_attributes_if_different(file_args, False)
module.exit_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()
+208
View File
@@ -0,0 +1,208 @@
# Copyright Red Hat
# SPDX-License-Identifier: Apache-2.0
# Author: Guillaume Abrioux <gabrioux@redhat.com>
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible.module_utils.basic import AnsibleModule # type: ignore
try:
from ansible.module_utils.ca_common import exit_module, generate_cmd, fatal, is_containerized # type: ignore
except ImportError:
from module_utils.ca_common import exit_module, generate_cmd, fatal, is_containerized # type: ignore
import datetime
import json
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: ceph_config
short_description: set ceph config
version_added: "2.10"
description:
- Set Ceph config options.
options:
fsid:
description:
- the fsid of the Ceph cluster to interact with.
required: false
image:
description:
- The Ceph container image to use.
required: false
action:
description:
- whether to get or set the parameter specified in 'option'
required: false
default: 'set'
who:
description:
- which daemon the configuration should be set to
required: true
option:
description:
- name of the parameter to be set
required: true
value:
description:
- value of the parameter
required: true if action is 'set'
author:
- Guillaume Abrioux <gabrioux@redhat.com>
'''
EXAMPLES = '''
- name: set osd_memory_target for osd.0
ceph_config:
action: set
who: osd.0
option: osd_memory_target
value: 5368709120
- name: set osd_memory_target for host ceph-osd-02
ceph_config:
action: set
who: osd/host:ceph-osd-02
option: osd_memory_target
value: 5368709120
- name: get osd_pool_default_size value
ceph_config:
action: get
who: global
option: osd_pool_default_size
value: 1
'''
RETURN = '''# '''
def set_option(module,
who,
option,
value,
container_image=None):
args = []
args.extend([who, option, value])
cmd = generate_cmd(sub_cmd=['config', 'set'],
args=args,
cluster=module.params.get('cluster'),
container_image=container_image)
rc, out, err = module.run_command(cmd)
return rc, cmd, out.strip(), err
def rm_option(module,
who,
option,
container_image=None):
args = []
args.extend([who, option])
cmd = generate_cmd(sub_cmd=['config', 'rm'],
args=args,
cluster=module.params.get('cluster'),
container_image=container_image)
rc, out, err = module.run_command(cmd)
return rc, cmd, out.strip(), err
def get_config_dump(module, container_image=None):
cmd = generate_cmd(sub_cmd=['config', 'dump', '--format', 'json'],
args=[],
cluster=module.params.get('cluster'),
container_image=container_image)
rc, out, err = module.run_command(cmd)
if rc:
fatal(message=f"Can't get current configuration via `ceph config dump`.Error:\n{err}", module=module)
out = out.strip()
return rc, cmd, out, err
def get_current_value(who, option, config_dump):
for config in config_dump:
if config['section'] == who and config['name'] == option:
return config['value']
return None
def main() -> None:
module = AnsibleModule(
argument_spec=dict(
who=dict(type='str', required=True),
action=dict(type='str', required=False, choices=['get', 'set', 'rm'], default='set'),
option=dict(type='str', required=True),
value=dict(type='str', required=False),
fsid=dict(type='str', required=False),
image=dict(type='str', required=False),
cluster=dict(type='str', required=False, default='ceph')
),
supports_check_mode=True,
required_if=[['action', 'set', ['value']]]
)
# Gather module parameters in variables
who = module.params.get('who')
option = module.params.get('option')
value = module.params.get('value')
action = module.params.get('action')
container_image = is_containerized()
if module.check_mode:
module.exit_json(
changed=False,
stdout='',
cmd=[],
stderr='',
rc=0,
start='',
end='',
delta='',
)
startd = datetime.datetime.now()
changed = False
rc, cmd, out, err = get_config_dump(module, container_image=container_image)
config_dump = json.loads(out)
current_value = get_current_value(who, option, config_dump)
if action == 'set':
if current_value and value.lower() == current_value.lower():
out = 'who={} option={} value={} already set. Skipping.'.format(who, option, value)
else:
rc, cmd, out, err = set_option(module, who, option, value, container_image=container_image)
changed = True
elif action == 'get':
if current_value is None:
out = ''
err = 'No value found for who={} option={}'.format(who, option)
else:
out = current_value
elif action == 'rm':
if current_value:
rc, cmd, out, err = rm_option(module, who, option, container_image=container_image)
changed = True
exit_module(module=module, out=out, rc=rc,
cmd=cmd, err=err, startd=startd,
changed=changed)
if __name__ == '__main__':
main()
+245
View File
@@ -0,0 +1,245 @@
#!/usr/bin/python
# Copyright (c) 2018 Red Hat, Inc.
#
# GNU General Public License v3.0+
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 fatal
except ImportError:
from module_utils.ca_common import fatal
import datetime
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: ceph_crush
author: Sebastien Han <seb@redhat.com>
short_description: Create Ceph CRUSH hierarchy
version_added: "2.6"
description:
- By using the hostvar variable 'osd_crush_location'
ceph_crush creates buckets and places them in the right CRUSH hierarchy
options:
cluster:
description:
- The ceph cluster name.
required: false
default: ceph
location:
description:
- osd_crush_location dict from the inventory file. It contains
the placement of each host in the CRUSH map.
required: true
containerized:
description:
- Weither or not this is a containerized cluster. The value is
assigned or not depending on how the playbook runs.
required: false
default: None
'''
EXAMPLES = '''
- name: configure crush hierarchy
ceph_crush:
cluster: "{{ cluster }}"
location: "{{ hostvars[item]['osd_crush_location'] }}"
containerized: "{{ container_exec_cmd }}"
with_items: "{{ groups[osd_group_name] }}"
when: crush_rule_config | bool
'''
RETURN = '''# '''
def generate_cmd(cluster, subcommand, bucket, bucket_type, containerized=None):
'''
Generate command line to execute
'''
cmd = [
'ceph',
'--cluster',
cluster,
'osd',
'crush',
subcommand,
bucket,
bucket_type,
]
if containerized:
cmd = containerized.split() + cmd
return cmd
def sort_osd_crush_location(location, module):
'''
Sort location tuple
'''
if len(location) < 2:
fatal("You must specify at least 2 buckets.", module)
if not any(item for item in location if item[0] == "host"):
fatal("You must specify a 'host' bucket.", module)
try:
crush_bucket_types = [
"host",
"chassis",
"rack",
"row",
"pdu",
"pod",
"room",
"datacenter",
"region",
"root",
]
return sorted(location, key=lambda crush: crush_bucket_types.index(crush[0])) # noqa: E501
except ValueError as error:
fatal("{} is not a valid CRUSH bucket, valid bucket types are {}".format(error.args[0].split()[0], crush_bucket_types), module) # noqa: E501
def get_crush_tree(module, cluster, containerized=None):
'''
Get the CRUSH map
'''
cmd = [
'ceph',
'--cluster',
cluster,
'osd',
'crush',
'tree',
'--format',
'json',
]
if containerized:
cmd = containerized.split() + cmd
rc, out, err = module.run_command(cmd)
return rc, cmd, out, err
def create_and_move_buckets_list(cluster, location, crush_map, containerized=None): # noqa: E501
'''
Creates Ceph CRUSH buckets and arrange the hierarchy
'''
def bucket_exists(bucket_name, bucket_type):
for item in crush_map['nodes']:
if item['name'] == bucket_name and item['type'] == bucket_type:
return True
return False
def bucket_in_place(bucket_name, target_bucket_name, target_bucket_type): # noqa: E501
bucket_id = None
target_bucket = None
for item in crush_map['nodes']:
if item['name'] == bucket_name:
bucket_id = item['id']
if item['name'] == target_bucket_name and item['type'] == target_bucket_type: # noqa: E501
target_bucket = item
if not bucket_id or not target_bucket:
return False
return bucket_id in target_bucket['children']
previous_bucket = None
cmd_list = []
for item in location:
bucket_type, bucket_name = item
# ceph osd crush add-bucket maroot root
if not bucket_exists(bucket_name, bucket_type):
cmd_list.append(generate_cmd(cluster, "add-bucket", bucket_name, bucket_type, containerized)) # noqa: E501
if previous_bucket:
# ceph osd crush move monrack root=maroot
if not bucket_in_place(previous_bucket, bucket_name, bucket_type): # noqa: E501
cmd_list.append(generate_cmd(cluster, "move", previous_bucket, "%s=%s" % (bucket_type, bucket_name), containerized)) # noqa: E501
previous_bucket = item[1]
return cmd_list
def exec_commands(module, cmd_list):
'''
Creates Ceph commands
'''
for cmd in cmd_list:
rc, out, err = module.run_command(cmd)
return rc, cmd, out, err
def main():
module = AnsibleModule(
argument_spec=dict(
cluster=dict(type='str', required=False, default='ceph'),
location=dict(type='dict', required=True),
containerized=dict(type='str', required=False, default=None),
),
supports_check_mode=True,
)
cluster = module.params['cluster']
location_dict = module.params['location']
location = sort_osd_crush_location(tuple(location_dict.items()), module)
containerized = module.params['containerized']
diff = dict(before="", after="")
startd = datetime.datetime.now()
# get the CRUSH map
rc, cmd, out, err = get_crush_tree(module, cluster, containerized)
if rc != 0 and not module.check_mode:
module.fail_json(msg='non-zero return code', rc=rc, stdout=out, stderr=err) # noqa: E501
# parse the JSON output
if rc == 0:
crush_map = module.from_json(out)
else:
crush_map = {"nodes": []}
# run the Ceph command to add buckets
cmd_list = create_and_move_buckets_list(cluster, location, crush_map, containerized) # noqa: E501
changed = len(cmd_list) > 0
if changed:
diff['after'] = module.jsonify(cmd_list)
if not module.check_mode:
rc, cmd, out, err = exec_commands(module, cmd_list) # noqa: E501
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,
diff=diff
)
if rc != 0:
module.fail_json(msg='non-zero return code', **result)
module.exit_json(**result)
if __name__ == '__main__':
main()
+256
View File
@@ -0,0 +1,256 @@
# 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.
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, \
generate_cmd, \
is_containerized, \
exec_command
except ImportError:
from module_utils.ca_common import exit_module, \
generate_cmd, \
is_containerized, \
exec_command
import datetime
import json
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: ceph_crush_rule
short_description: Manage Ceph Crush Replicated/Erasure Rule
version_added: "2.8"
description:
- Manage Ceph Crush rule(s) creation, deletion and updates.
options:
name:
description:
- name of the Ceph Crush rule. If state is 'info' - empty string
can be provided as a value to get all crush rules
required: true
cluster:
description:
- The ceph cluster name.
required: false
default: ceph
state:
description:
If 'present' is used, the module creates a rule if it doesn't
exist or update it if it already exists.
If 'absent' is used, the module will simply delete the rule.
required: false
choices: ['present', 'absent']
default: present
rule_type:
description:
- The ceph CRUSH rule type.
required: false
choices: ['replicated', 'erasure']
required: false
bucket_root:
description:
- The ceph bucket root for replicated rule.
required: false
bucket_type:
description:
- The ceph bucket type for replicated rule.
required: false
choices: ['osd', 'host', 'chassis', 'rack', 'row', 'pdu', 'pod',
'room', 'datacenter', 'zone', 'region', 'root']
device_class:
description:
- The ceph device class for replicated rule.
required: false
profile:
description:
- The ceph erasure profile for erasure rule.
required: false
author:
- Dimitri Savineau <dsavinea@redhat.com>
'''
EXAMPLES = '''
- name: create a Ceph Crush replicated rule
ceph_crush_rule:
name: foo
bucket_root: default
bucket_type: host
device_class: ssd
rule_type: replicated
- name: create a Ceph Crush erasure rule
ceph_crush_rule:
name: foo
profile: bar
rule_type: erasure
- name: get a Ceph Crush rule information
ceph_crush_rule:
name: foo
state: info
- name: delete a Ceph Crush rule
ceph_crush_rule:
name: foo
state: absent
'''
RETURN = '''# '''
def create_rule(module, container_image=None):
'''
Create a new crush replicated/erasure rule
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
rule_type = module.params.get('rule_type')
bucket_root = module.params.get('bucket_root')
bucket_type = module.params.get('bucket_type')
device_class = module.params.get('device_class')
profile = module.params.get('profile')
if rule_type == 'replicated':
args = ['create-replicated', name, bucket_root, bucket_type]
if device_class:
args.append(device_class)
else:
args = ['create-erasure', name]
if profile:
args.append(profile)
cmd = generate_cmd(sub_cmd=['osd', 'crush', 'rule'],
args=args,
cluster=cluster,
container_image=container_image)
return cmd
def get_rule(module, container_image=None):
'''
Get existing crush rule
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
args = ['dump', name, '--format=json']
cmd = generate_cmd(sub_cmd=['osd', 'crush', 'rule'],
args=args,
cluster=cluster,
container_image=container_image)
return cmd
def remove_rule(module, container_image=None):
'''
Remove a crush rule
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
args = ['rm', name]
cmd = generate_cmd(sub_cmd=['osd', 'crush', 'rule'],
args=args,
cluster=cluster,
container_image=container_image)
return cmd
def main():
module = AnsibleModule(
argument_spec=dict(
name=dict(type='str', required=False),
cluster=dict(type='str', required=False, default='ceph'),
state=dict(type='str', required=False, choices=['present', 'absent'], default='present'), # noqa: E501
rule_type=dict(type='str', required=False, choices=['replicated', 'erasure']), # noqa: E501
bucket_root=dict(type='str', required=False),
bucket_type=dict(type='str', required=False, choices=['osd', 'host', 'chassis', 'rack', 'row', 'pdu', 'pod', # noqa: E501
'room', 'datacenter', 'zone', 'region', 'root']), # noqa: E501
device_class=dict(type='str', required=False),
profile=dict(type='str', required=False)
),
supports_check_mode=True,
required_if=[
('state', 'present', ['rule_type']),
('state', 'present', ['name']),
('state', 'absent', ['name']),
('rule_type', 'replicated', ['bucket_root', 'bucket_type']),
('rule_type', 'erasure', ['profile'])
]
)
# Gather module parameters in variables
name = module.params.get('name')
state = module.params.get('state')
rule_type = module.params.get('rule_type')
if module.check_mode:
module.exit_json(
changed=False,
stdout='',
stderr='',
rc=0,
start='',
end='',
delta='',
)
startd = datetime.datetime.now()
changed = False
# will return either the image name or None
container_image = is_containerized()
rc, cmd, out, err = exec_command(module, get_rule(module, container_image=container_image)) # noqa: E501
if state == "present":
if rc != 0:
rc, cmd, out, err = exec_command(module, create_rule(module, container_image=container_image)) # noqa: E501
changed = True
else:
rule = json.loads(out)
if (rule['type'] == 1 and rule_type == 'erasure') or (rule['type'] == 3 and rule_type == 'replicated'): # noqa: E501
module.fail_json(msg="Can not convert crush rule {} to {}".format(name, rule_type), changed=False, rc=1) # noqa: E501
elif state == "absent":
if rc == 0:
rc, cmd, out, err = exec_command(module, remove_rule(module, container_image=container_image)) # noqa: E501
changed = True
else:
rc = 0
out = "Crush Rule {} doesn't exist".format(name)
else:
pass
exit_module(module=module, out=out, rc=rc, cmd=cmd, err=err, startd=startd, changed=changed) # noqa: E501
if __name__ == '__main__':
main()
+119
View File
@@ -0,0 +1,119 @@
# 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.
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, \
generate_cmd, \
is_containerized, \
exec_command
except ImportError:
from module_utils.ca_common import exit_module, \
generate_cmd, \
is_containerized, \
exec_command
import datetime
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: ceph_crush_rule_info
short_description: Lists Ceph Crush Replicated/Erasure Rules
version_added: "2.8"
description:
- Retrieces Ceph Crush rule(s).
options:
name:
description:
- name of the Ceph Crush rule. If state is 'info' - empty string
can be provided as a value to get all crush rules
required: true
cluster:
description:
- The ceph cluster name.
required: false
default: ceph
author:
- Teoman ONAY <@asm0deuz>
'''
EXAMPLES = '''
- name: get a Ceph Crush rule information
ceph_crush_rule_info:
name: foo
'''
RETURN = '''# '''
def get_rule(module, container_image=None):
'''
Get existing crush rule
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
args = ['dump', name, '--format=json']
cmd = generate_cmd(sub_cmd=['osd', 'crush', 'rule'],
args=args,
cluster=cluster,
container_image=container_image)
return cmd
def main():
module = AnsibleModule(
argument_spec=dict(
name=dict(type='str', required=False),
cluster=dict(type='str', required=False, default='ceph'),
),
supports_check_mode=True,
)
if module.check_mode:
module.exit_json(
changed=False,
stdout='',
stderr='',
rc=0,
start='',
end='',
delta='',
)
startd = datetime.datetime.now()
changed = False
# will return either the image name or None
container_image = is_containerized()
rc, cmd, out, err = exec_command(module, get_rule(module, container_image=container_image)) # noqa: E501
exit_module(module=module, out=out, rc=rc, cmd=cmd, err=err, startd=startd, changed=changed) # noqa: E501
if __name__ == '__main__':
main()
+289
View File
@@ -0,0 +1,289 @@
# 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.
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 generate_cmd, \
is_containerized, \
exec_command, \
exit_module, \
fatal
except ImportError:
from module_utils.ca_common import generate_cmd, is_containerized, exec_command, exit_module, fatal # noqa: E501
import datetime
import json
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: ceph_dashboard_user
short_description: Manage Ceph Dashboard User
version_added: "2.8"
description:
- Manage Ceph Dashboard user(s) creation, deletion and updates.
options:
cluster:
description:
- The ceph cluster name.
required: false
default: ceph
name:
description:
- name of the Ceph Dashboard user.
required: true
state:
description:
If 'present' is used, the module creates a user if it doesn't
exist or update it if it already exists.
If 'absent' is used, the module will simply delete the user.
If 'info' is used, the module will return all details about the
existing user (json formatted).
required: false
choices: ['present', 'absent', 'info']
default: present
password:
description:
- password of the Ceph Dashboard user.
required: false
roles:
description:
- roles of the Ceph Dashboard user.
required: false
default: []
author:
- Dimitri Savineau <dsavinea@redhat.com>
'''
EXAMPLES = '''
- name: create a Ceph Dashboard user
ceph_dashboard_user:
name: foo
password: bar
- name: create a read-only/block-manager Ceph Dashboard user
ceph_dashboard_user:
name: foo
password: bar
roles:
- 'read-only'
- 'block-manager'
- name: create a Ceph Dashboard admin user
ceph_dashboard_user:
name: foo
password: bar
roles: ['administrator']
- name: get a Ceph Dashboard user information
ceph_dashboard_user:
name: foo
state: info
- name: delete a Ceph Dashboard user
ceph_dashboard_user:
name: foo
state: absent
'''
RETURN = '''# '''
def create_user(module, container_image=None):
'''
Create a new user
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
args = ['ac-user-create', '-i', '-', name]
cmd = generate_cmd(sub_cmd=['dashboard'],
args=args,
cluster=cluster,
container_image=container_image,
interactive=True)
return cmd
def set_roles(module, container_image=None):
'''
Set user roles
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
roles = module.params.get('roles')
args = ['ac-user-set-roles', name]
args.extend(roles)
cmd = generate_cmd(sub_cmd=['dashboard'],
args=args,
cluster=cluster,
container_image=container_image)
return cmd
def set_password(module, container_image=None):
'''
Set user password
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
args = ['ac-user-set-password', '-i', '-', name]
cmd = generate_cmd(sub_cmd=['dashboard'],
args=args,
cluster=cluster,
container_image=container_image,
interactive=True)
return cmd
def get_user(module, container_image=None):
'''
Get existing user
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
args = ['ac-user-show', name, '--format=json']
cmd = generate_cmd(sub_cmd=['dashboard'],
args=args,
cluster=cluster,
container_image=container_image)
return cmd
def remove_user(module, container_image=None):
'''
Remove a user
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
args = ['ac-user-delete', name]
cmd = generate_cmd(sub_cmd=['dashboard'],
args=args,
cluster=cluster,
container_image=container_image)
return cmd
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', 'info'], default='present'), # noqa: E501
password=dict(type='str', required=False, no_log=True),
roles=dict(type='list',
required=False,
choices=['administrator', 'read-only', 'block-manager', 'rgw-manager', 'cluster-manager', 'pool-manager', 'cephfs-manager'], # noqa: E501
default=[]),
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True,
required_if=[['state', 'present', ['password']]]
)
# Gather module parameters in variables
name = module.params.get('name')
state = module.params.get('state')
roles = module.params.get('roles')
password = module.params.get('password')
if module.check_mode:
module.exit_json(
changed=False,
stdout='',
stderr='',
rc=0,
start='',
end='',
delta='',
)
startd = datetime.datetime.now()
changed = False
# will return either the image name or None
container_image = is_containerized()
if state == "present":
rc, cmd, out, err = exec_command(module, get_user(module, container_image=container_image)) # noqa: E501
if rc == 0:
user = json.loads(out)
user['roles'].sort()
roles.sort()
if user['roles'] != roles:
rc, cmd, out, err = exec_command(module, set_roles(module, container_image=container_image)) # noqa: E501
changed = True
rc, cmd, out, err = exec_command(module, set_password(module, container_image=container_image), stdin=password) # noqa: E501
else:
rc, cmd, out, err = exec_command(module, create_user(module, container_image=container_image), stdin=password) # noqa: E501
if rc != 0:
fatal(err, module)
rc, cmd, out, err = exec_command(module, set_roles(module, container_image=container_image)) # noqa: E501
changed = True
elif state == "absent":
rc, cmd, out, err = exec_command(module, get_user(module, container_image=container_image)) # noqa: E501
if rc == 0:
rc, cmd, out, err = exec_command(module, remove_user(module, container_image=container_image)) # noqa: E501
changed = True
else:
rc = 0
out = "Dashboard User {} doesn't exist".format(name)
elif state == "info":
rc, cmd, out, err = exec_command(module, get_user(module, container_image=container_image)) # noqa: E501
exit_module(module=module, out=out, rc=rc, cmd=cmd, err=err, startd=startd, changed=changed) # noqa: E501
def main():
run_module()
if __name__ == '__main__':
main()
+256
View File
@@ -0,0 +1,256 @@
# 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.
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 is_containerized, \
generate_cmd, \
exec_command, \
exit_module
except ImportError:
from module_utils.ca_common import is_containerized, \
generate_cmd, \
exec_command, \
exit_module
import datetime
import json
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: ceph_ec_profile
short_description: Manage Ceph Erasure Code profile
version_added: "2.8"
description:
- Manage Ceph Erasure Code profile
options:
cluster:
description:
- The ceph cluster name.
required: false
default: ceph
name:
description:
- name of the profile.
required: true
state:
description:
If 'present' is used, the module creates a profile.
If 'absent' is used, the module will delete the profile.
required: false
choices: ['present', 'absent', 'info']
default: present
stripe_unit:
description:
- The amount of data in a data chunk, per stripe.
required: false
k:
description:
- Number of data-chunks the object will be split in
required: true
m:
description:
- Compute coding chunks for each object and store them on different
OSDs.
required: true
crush_device_class:
description:
- Restrict placement to devices of a specific class (hdd/ssd)
required: false
author:
- Guillaume Abrioux <gabrioux@redhat.com>
'''
EXAMPLES = '''
- name: create an erasure code profile
ceph_ec_profile:
name: foo
k: 4
m: 2
- name: delete an erassure code profile
ceph_ec_profile:
name: foo
state: absent
'''
RETURN = '''# '''
def get_profile(name, cluster='ceph', container_image=None):
'''
Get existing profile
'''
args = ['get', name, '--format=json']
cmd = generate_cmd(sub_cmd=['osd', 'erasure-code-profile'],
args=args,
cluster=cluster,
container_image=container_image)
return cmd
def create_profile(name, user_profile, force, cluster='ceph', container_image=None): # noqa: E501
'''
Create a profile
'''
args = ['set', name]
for key, value in user_profile.items():
args.append('{}={}'.format(key, value))
if force:
args.append('--force')
cmd = generate_cmd(sub_cmd=['osd', 'erasure-code-profile'],
args=args,
cluster=cluster,
container_image=container_image)
return cmd
def delete_profile(name, cluster='ceph', container_image=None):
'''
Delete a profile
'''
args = ['rm', name]
cmd = generate_cmd(sub_cmd=['osd', 'erasure-code-profile'],
args=args,
cluster=cluster,
container_image=container_image)
return cmd
def parse_user_profile(module):
profile_keys = ['plugin',
'k', 'm', 'd', 'l', 'c',
'stripe_unit', 'scalar_mds', 'technique',
'crush-root', 'crush-device-class', 'crush-failure-domain']
profile = {}
for key in profile_keys:
ansible_lookup_key = key.replace('-', '_')
value = module.params.get(ansible_lookup_key)
if value:
profile[key] = value
return profile
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'),
stripe_unit=dict(type='str', required=False),
plugin=dict(type='str', required=False, default='jerasure'),
k=dict(type='str', required=False),
m=dict(type='str', required=False),
d=dict(type='str', required=False),
l=dict(type='str', required=False),
c=dict(type='str', required=False),
scalar_mds=dict(type='str', required=False),
technique=dict(type='str', required=False),
crush_root=dict(type='str', required=False),
crush_failure_domain=dict(type='str', required=False),
crush_device_class=dict(type='str', required=False),
force=dict(type='bool', required=False, default=False),
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True,
required_if=[['state', 'present', ['k', 'm']]],
)
# Gather module parameters in variables
name = module.params.get('name')
cluster = module.params.get('cluster')
state = module.params.get('state')
force = module.params.get('force')
user_profile = parse_user_profile(module)
if module.check_mode:
module.exit_json(
changed=False,
stdout='',
stderr='',
rc=0,
start='',
end='',
delta='',
)
startd = datetime.datetime.now()
diff = dict(before="", after="")
changed = False
# will return either the image name or None
container_image = is_containerized()
if state == "present":
rc, cmd, out, err = exec_command(module, get_profile(name, cluster, container_image=container_image)) # noqa: E501
current_profile = {}
if rc == 0:
current_profile = json.loads(out)
changed = current_profile != user_profile
if changed:
diff['before'] = json.dumps(current_profile)
diff['after'] = json.dumps(user_profile)
rc, cmd, out, err = exec_command(module,
create_profile(name,
user_profile,
force,
cluster,
container_image=container_image), # noqa: E501
check_rc=True)
elif state == "absent":
rc, cmd, out, err = exec_command(module, delete_profile(name, cluster, container_image=container_image)) # noqa: E501
if not err:
out = 'Profile {} removed.'.format(name)
changed = True
else:
rc = 0
out = "Skipping, the profile {} doesn't exist".format(name)
exit_module(module=module, out=out, rc=rc, cmd=cmd, err=err, startd=startd, changed=changed, diff=diff) # noqa: E501
def main():
run_module()
if __name__ == '__main__':
main()
+278
View File
@@ -0,0 +1,278 @@
# 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.
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 is_containerized, \
exec_command, \
generate_cmd, \
exit_module
except ImportError:
from module_utils.ca_common import is_containerized, \
exec_command, \
generate_cmd, \
exit_module
import datetime
import json
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: ceph_fs
short_description: Manage Ceph File System
version_added: "2.8"
description:
- Manage Ceph File System(s) creation, deletion and updates.
options:
cluster:
description:
- The ceph cluster name.
required: false
default: ceph
name:
description:
- name of the Ceph File System.
required: true
state:
description:
If 'present' is used, the module creates a filesystem if it
doesn't exist or update it if it already exists.
If 'absent' is used, the module will simply delete the filesystem.
If 'info' is used, the module will return all details about the
existing filesystem (json formatted).
required: false
choices: ['present', 'absent', 'info']
default: present
data:
description:
- name of the data pool.
required: false
metadata:
description:
- name of the metadata pool.
required: false
max_mds:
description:
- name of the max_mds attribute.
required: false
author:
- Dimitri Savineau <dsavinea@redhat.com>
'''
EXAMPLES = '''
- name: create a Ceph File System
ceph_fs:
name: foo
data: bar_data
metadata: bar_metadata
max_mds: 2
- name: get a Ceph File System information
ceph_fs:
name: foo
state: info
- name: delete a Ceph File System
ceph_fs:
name: foo
state: absent
'''
RETURN = '''# '''
def create_fs(module, container_image=None):
'''
Create a new fs
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
data = module.params.get('data')
metadata = module.params.get('metadata')
args = ['new', name, metadata, data]
cmd = generate_cmd(sub_cmd=['fs'],
args=args,
cluster=cluster,
container_image=container_image)
return cmd
def get_fs(module, container_image=None):
'''
Get existing fs
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
args = ['get', name, '--format=json']
cmd = generate_cmd(sub_cmd=['fs'],
args=args,
cluster=cluster,
container_image=container_image)
return cmd
def remove_fs(module, container_image=None):
'''
Remove a fs
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
args = ['rm', name, '--yes-i-really-mean-it']
cmd = generate_cmd(sub_cmd=['fs'],
args=args,
cluster=cluster,
container_image=container_image)
return cmd
def fail_fs(module, container_image=None):
'''
Fail a fs
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
args = ['fail', name]
cmd = generate_cmd(sub_cmd=['fs'],
args=args,
cluster=cluster,
container_image=container_image)
return cmd
def set_fs(module, container_image=None):
'''
Set parameter to a fs
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
max_mds = module.params.get('max_mds')
args = ['set', name, 'max_mds', str(max_mds)]
cmd = generate_cmd(sub_cmd=['fs'],
args=args,
cluster=cluster,
container_image=container_image)
return cmd
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', 'info'], default='present'), # noqa: E501
data=dict(type='str', required=False),
metadata=dict(type='str', required=False),
max_mds=dict(type='int', required=False),
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True,
required_if=[['state', 'present', ['data', 'metadata']]],
)
# Gather module parameters in variables
name = module.params.get('name')
state = module.params.get('state')
max_mds = module.params.get('max_mds')
if module.check_mode:
module.exit_json(
changed=False,
stdout='',
stderr='',
rc=0,
start='',
end='',
delta='',
)
startd = datetime.datetime.now()
changed = False
# will return either the image name or None
container_image = is_containerized()
if state == "present":
rc, cmd, out, err = exec_command(module, get_fs(module, container_image=container_image)) # noqa: E501
if rc == 0:
fs = json.loads(out)
if max_mds and fs["mdsmap"]["max_mds"] != max_mds:
rc, cmd, out, err = exec_command(module, set_fs(module, container_image=container_image)) # noqa: E501
if rc == 0:
changed = True
else:
rc, cmd, out, err = exec_command(module, create_fs(module, container_image=container_image)) # noqa: E501
if max_mds and max_mds > 1:
exec_command(module, set_fs(module, container_image=container_image)) # noqa: E501
if rc == 0:
changed = True
elif state == "absent":
rc, cmd, out, err = exec_command(module, get_fs(module, container_image=container_image)) # noqa: E501
if rc == 0:
exec_command(module, fail_fs(module, container_image=container_image)) # noqa: E501
rc, cmd, out, err = exec_command(module, remove_fs(module, container_image=container_image)) # noqa: E501
if rc == 0:
changed = True
else:
rc = 0
out = "Ceph File System {} doesn't exist".format(name)
elif state == "info":
rc, cmd, out, err = exec_command(module, get_fs(module, container_image=container_image)) # noqa: E501
exit_module(module=module, out=out, rc=rc, cmd=cmd, err=err, startd=startd, changed=changed) # noqa: E501
def main():
run_module()
if __name__ == '__main__':
main()
+683
View File
@@ -0,0 +1,683 @@
# 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
from ansible.module_utils.basic import AnsibleModule
try:
from ansible.module_utils.ca_common import generate_cmd, \
is_containerized, \
container_exec, \
fatal
except ImportError:
from module_utils.ca_common import generate_cmd, \
is_containerized, \
container_exec, \
fatal
import datetime
import json
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"
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 'generate_secret' is used, the module will simply output a cephx keyring.
required: false
choices: ['present', 'update', 'absent', '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: 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,
cluster,
user,
user_key,
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_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_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_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_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_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
'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')
# Can't use required_if with 'name' for some reason...
if state in ['present', 'absent', 'update'] and not name:
fatal(f'"state" is "{state}" but "name" is not defined.', module)
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)) # 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, 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":
rc, cmd, out, err = exec_commands(
module, info_key(cluster, name, user, user_key_path, output_format, container_image)) # noqa: E501
if rc == 0:
rc, cmd, out, err = exec_commands(
module, delete_key(cluster, user, user_key_path, name, container_image)) # noqa: E501
changed = True
else:
rc = 0
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()
+265
View File
@@ -0,0 +1,265 @@
# 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
from ansible.module_utils.basic import AnsibleModule
try:
from ansible.module_utils.ca_common import generate_cmd, \
is_containerized, \
fatal
except ImportError:
from module_utils.ca_common import generate_cmd, \
is_containerized, \
fatal
import datetime
import os
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: ceph_key_info
author: Teoman ONAY <@asM0deuz>
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 '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.
required: false
choices: ['list', 'info']
default: list
output_format:
description:
- The key output format when retrieving the information of an
entity.
required: false
default: json
'''
EXAMPLES = '''
- 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
'''
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 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_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_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 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='info', choices=['list', 'info']), # noqa: E501
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,
)
# Gather module parameters in variables
state = module.params['state']
name = module.params.get('name')
cluster = module.params.get('cluster')
user = module.params.get('user')
user_key = module.params.get('user_key')
output_format = module.params.get('output_format')
# Can't use required_if with 'name' for some reason...
if state in ['info'] and not name:
fatal(f'"state" is "{state}" but "name" is not defined.', module)
changed = False
cmd = ''
rc = 0
out = ''
err = ''
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()
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 == "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))
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()
+133
View File
@@ -0,0 +1,133 @@
# 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.
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, \
generate_cmd, \
is_containerized
except ImportError:
from module_utils.ca_common import exit_module, \
generate_cmd, \
is_containerized
import datetime
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: ceph_mgr_module
short_description: Manage Ceph MGR module
version_added: "2.8"
description:
- Manage Ceph MGR module
options:
name:
description:
- name of the ceph MGR module.
required: true
cluster:
description:
- The ceph cluster name.
required: false
default: ceph
state:
description:
- If 'enable' is used, the module enables the MGR module.
If 'absent' is used, the module disables the MGR module.
required: false
choices: ['enable', 'disable']
default: enable
author:
- Dimitri Savineau <dsavinea@redhat.com>
'''
EXAMPLES = '''
- name: enable dashboard mgr module
ceph_mgr_module:
name: dashboard
state: enable
- name: disable multiple mgr modules
ceph_mgr_module:
name: '{{ item }}'
state: disable
loop:
- 'dashboard'
- 'prometheus'
'''
RETURN = '''# '''
def main():
module = AnsibleModule(
argument_spec=dict(
name=dict(type='str', required=True),
cluster=dict(type='str', required=False, default='ceph'),
state=dict(type='str', required=False, default='enable', choices=['enable', 'disable']), # noqa: E501
),
supports_check_mode=True,
)
name = module.params.get('name')
cluster = module.params.get('cluster')
state = module.params.get('state')
startd = datetime.datetime.now()
container_image = is_containerized()
cmd = generate_cmd(sub_cmd=['mgr', 'module'],
args=[state, name],
cluster=cluster,
container_image=container_image)
if module.check_mode:
exit_module(
module=module,
out='',
rc=0,
cmd=cmd,
err='',
startd=startd,
changed=False
)
else:
rc, out, err = module.run_command(cmd)
if 'is already enabled' in err:
changed = False
else:
changed = True
exit_module(
module=module,
out=out,
rc=rc,
cmd=cmd,
err=err,
startd=startd,
changed=changed
)
if __name__ == '__main__':
main()
+202
View File
@@ -0,0 +1,202 @@
# Copyright Red Hat
# SPDX-License-Identifier: Apache-2.0
# Author: Guillaume Abrioux <gabrioux@redhat.com>
#
# 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
from typing import List, Tuple, Dict
__metaclass__ = type
import datetime
import yaml
from ansible.module_utils.basic import AnsibleModule # type: ignore
try:
from ansible.module_utils.ca_common import exit_module, build_base_cmd # type: ignore
except ImportError:
from module_utils.ca_common import exit_module, build_base_cmd
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: ceph_orch_apply
short_description: apply service spec
version_added: "2.9"
description:
- apply a service spec
options:
fsid:
description:
- the fsid of the Ceph cluster to interact with.
required: false
image:
description:
- The Ceph container image to use.
required: false
cluster:
description:
- The Ceph cluster name. Defaults to ceph
required: false
spec:
description:
- The service spec to apply
required: true
author:
- Guillaume Abrioux <gabrioux@redhat.com>
'''
EXAMPLES = '''
- name: apply osd spec
ceph_orch_apply:
spec: |
service_type: osd
service_id: osd
placement:
label: osds
spec:
data_devices:
all: true
'''
def parse_spec(spec: str) -> Dict:
""" parse spec string to yaml """
yaml_spec = yaml.safe_load(spec)
return yaml_spec
def retrieve_current_spec(module: AnsibleModule, expected_spec: Dict) -> Dict:
""" retrieve current config of the service """
service: str = expected_spec["service_type"]
cmd = build_base_cmd(module)
cluster = module.params.get('cluster')
if cluster != 'ceph':
conf_path = f"/etc/ceph/{cluster}.conf"
keyring_path = f"/etc/ceph/{cluster}.client.admin.keyring"
cmd.extend(['--config', conf_path, '--keyring', keyring_path])
cmd.extend(['ceph', 'orch', 'ls', service])
if 'service_name' in expected_spec:
cmd.extend([expected_spec["service_name"]])
else:
cmd.extend([expected_spec["service_type"] + "." + expected_spec["service_id"]])
cmd.extend(['--format=yaml'])
out = module.run_command(cmd)
if isinstance(out, str):
# if there is no existing service, cephadm returns the string 'No services reported'
return {}
else:
return yaml.safe_load(out[1])
def apply_spec(module: "AnsibleModule",
data: str) -> Tuple[int, List[str], str, str]:
cmd = build_base_cmd(module)
cluster = module.params.get('cluster')
if cluster != 'ceph':
conf_path = f"/etc/ceph/{cluster}.conf"
keyring_path = f"/etc/ceph/{cluster}.client.admin.keyring"
cmd.extend(['--config', conf_path, '--keyring', keyring_path])
cmd.extend(['ceph', 'orch', 'apply', '-i', '-'])
rc, out, err = module.run_command(cmd, data=data)
if rc:
raise RuntimeError(err)
return rc, cmd, out, err
def change_required(current: Dict, expected: Dict) -> bool:
""" checks if the current config differs from what is expected """
if not current:
return True
for key, value in expected.items():
if key in current:
if current[key] != value:
return True
continue
else:
return True
return False
def run_module() -> None:
module_args = dict(
spec=dict(type='str', required=True),
fsid=dict(type='str', required=False),
docker=dict(type=bool,
required=False,
default=False),
image=dict(type='str', required=False),
cluster=dict(type='str', required=False,
default='ceph')
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True
)
startd = datetime.datetime.now()
spec = module.params.get('spec')
if module.check_mode:
exit_module(
module=module,
out='',
rc=0,
cmd=[],
err='',
startd=startd,
changed=False
)
# Idempotency check
expected = parse_spec(module.params.get('spec'))
current_spec = retrieve_current_spec(module, expected)
if change_required(current_spec, expected):
rc, cmd, out, err = apply_spec(module, spec)
changed = True
else:
rc = 0
cmd = []
out = ''
err = ''
changed = False
exit_module(
module=module,
out=out,
rc=rc,
cmd=cmd,
err=err,
startd=startd,
changed=changed
)
def main() -> None:
run_module()
if __name__ == '__main__':
main()
+146
View File
@@ -0,0 +1,146 @@
# 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.
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, generate_cmd, is_containerized # noqa: E501
except ImportError:
from module_utils.ca_common import exit_module, generate_cmd, is_containerized # noqa: E501
import datetime
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: ceph_osd
short_description: Manage Ceph OSD state
version_added: "2.8"
description:
- Manage Ceph OSD state
options:
ids:
description:
- The ceph OSD id(s).
required: true
cluster:
description:
- The ceph cluster name.
required: false
default: ceph
state:
description:
- The ceph OSD state.
required: true
choices: ['destroy', 'down', 'in', 'out', 'purge', 'rm']
author:
- Dimitri Savineau <dsavinea@redhat.com>
'''
EXAMPLES = '''
- name: destroy OSD 42
ceph_osd:
ids: 42
state: destroy
- name: set multiple OSDs down
ceph_osd:
ids: [0, 1, 3]
state: down
- name: set OSD 42 in
ceph_osd:
ids: 42
state: in
- name: set OSD 42 out
ceph_osd:
ids: 42
state: out
- name: purge OSD 42
ceph_osd:
ids: 42
state: purge
- name: rm OSD 42
ceph_osd:
ids: 42
state: rm
'''
RETURN = '''# '''
def main():
module = AnsibleModule(
argument_spec=dict(
ids=dict(type='list', required=True),
cluster=dict(type='str', required=False, default='ceph'),
state=dict(type='str', required=True, choices=['destroy', 'down', 'in', 'out', 'purge', 'rm']), # noqa: E501
),
supports_check_mode=True,
)
ids = module.params.get('ids')
cluster = module.params.get('cluster')
state = module.params.get('state')
if state in ['destroy', 'purge'] and len(ids) > 1:
module.fail_json(msg='destroy and purge only support one OSD at at time', rc=1) # noqa: E501
startd = datetime.datetime.now()
container_image = is_containerized()
cmd = generate_cmd(sub_cmd=['osd', state], args=ids, cluster=cluster, container_image=container_image) # noqa: E501
if state in ['destroy', 'purge']:
cmd.append('--yes-i-really-mean-it')
if module.check_mode:
exit_module(
module=module,
out='',
rc=0,
cmd=cmd,
err='',
startd=startd,
changed=False
)
else:
rc, out, err = module.run_command(cmd)
changed = True
if state in ['down', 'in', 'out'] and 'marked' not in err:
changed = False
exit_module(
module=module,
out=out,
rc=rc,
cmd=cmd,
err=err,
startd=startd,
changed=changed
)
if __name__ == '__main__':
main()
+130
View File
@@ -0,0 +1,130 @@
# 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.
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, \
generate_cmd, \
is_containerized
except ImportError:
from module_utils.ca_common import exit_module, \
generate_cmd, \
is_containerized
import datetime
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: ceph_osd_flag
short_description: Manage Ceph OSD flag
version_added: "2.8"
description:
- Manage Ceph OSD flag
options:
name:
description:
- name of the ceph OSD flag.
required: true
choices: ['noup', 'nodown', 'noout', 'nobackfill', 'norebalance',
'norecover', 'noscrub', 'nodeep-scrub']
cluster:
description:
- The ceph cluster name.
required: false
default: ceph
state:
description:
- If 'present' is used, the module sets the OSD flag.
If 'absent' is used, the module will unset the OSD flag.
required: false
choices: ['present', 'absent']
default: present
author:
- Dimitri Savineau <dsavinea@redhat.com>
'''
EXAMPLES = '''
- name: set noup OSD flag
ceph_osd_flag:
name: noup
- name: unset multiple OSD flags
ceph_osd_flag:
name: '{{ item }}'
state: absent
loop:
- 'noup'
- 'norebalance'
'''
RETURN = '''# '''
def main():
module = AnsibleModule(
argument_spec=dict(
name=dict(type='str', required=True, choices=['noup', 'nodown', 'noout', 'nobackfill', 'norebalance', 'norecover', 'noscrub', 'nodeep-scrub', 'noautoscale']), # noqa: E501
cluster=dict(type='str', required=False, default='ceph'),
state=dict(type='str', required=False, default='present', choices=['present', 'absent']), # noqa: E501
),
supports_check_mode=True,
)
name = module.params.get('name')
cluster = module.params.get('cluster')
state = module.params.get('state')
startd = datetime.datetime.now()
container_image = is_containerized()
if state == 'present':
cmd = generate_cmd(sub_cmd=['osd', 'set'], args=[name], cluster=cluster, container_image=container_image) # noqa: E501
else:
cmd = generate_cmd(sub_cmd=['osd', 'unset'], args=[name], cluster=cluster, container_image=container_image) # noqa: E501
if module.check_mode:
exit_module(
module=module,
out='',
rc=0,
cmd=cmd,
err='',
startd=startd,
changed=False
)
else:
rc, out, err = module.run_command(cmd)
exit_module(
module=module,
out=out,
rc=rc,
cmd=cmd,
err=err,
startd=startd,
changed=True
)
if __name__ == '__main__':
main()
+739
View File
@@ -0,0 +1,739 @@
# 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.
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 generate_cmd, \
pre_generate_cmd, \
is_containerized, \
exec_command, \
exit_module
except ImportError:
from module_utils.ca_common import generate_cmd, \
pre_generate_cmd, \
is_containerized, \
exec_command, \
exit_module
import datetime
import json
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
name:
description:
- name of the Ceph pool
required: true
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
choices: ['present', 'absent', 'list']
default: present
size:
description:
- set the replica size of the pool.
required: false
default: 3
min_size:
description:
- set the min_size parameter of the pool.
required: false
default: default to `osd_pool_default_min_size` (ceph)
pg_num:
description:
- set the pg_num of the pool.
required: false
default: default to `osd_pool_default_pg_num` (ceph)
pgp_num:
description:
- set the pgp_num of the pool.
required: false
default: default to `osd_pool_default_pgp_num` (ceph)
pg_autoscale_mode:
description:
- set the pg autoscaler on the pool.
required: false
default: 'on'
target_size_ratio:
description:
- set the target_size_ratio on the pool
required: false
default: None
pool_type:
description:
- set the pool type, either 'replicated' or 'erasure'
required: false
default: 'replicated'
erasure_profile:
description:
- When pool_type = 'erasure', set the erasure profile of the pool
required: false
default: 'default'
rule_name:
description:
- Set the crush rule name assigned to the pool
required: false
default: 'replicated_rule' when pool_type is 'erasure' else None
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
default: None
'''
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 = '''# '''
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_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_cmd('ceph', 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_cmd(sub_cmd=['osd', 'pool'],
args=args,
cluster=cluster,
user=user,
user_key=user_key,
container_image=container_image)
return cmd
def get_crush_rule_pool(cluster,
name,
user,
user_key,
output_format='json',
container_image=None):
'''
Get crush rule type on a given pool
'''
args = ['get', name, 'crush_rule', '-f', output_format]
cmd = generate_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_cmd(sub_cmd=['osd', 'pool'],
args=args,
cluster=cluster,
user=user,
user_key=user_key,
container_image=container_image)
return cmd
def init_rbd_pool(cluster,
name,
user,
user_key,
container_image=None):
'''
Initialize a rbd pool
'''
args = [name]
cmd = generate_cmd(cmd='rbd',
sub_cmd=['pool', 'init'],
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_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_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
_rc, _cmd, crush_rule, _err = exec_command(module,
get_crush_rule_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]
out['crush_rule'] = json.loads(crush_rule.strip())['crush_rule']
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',
'crush_rule']
for key in filter_keys:
if (str(running_pool_details[key]) != user_pool_config[key]['value'] and # noqa: E501
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_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,
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']]
pg_autoscale_mode = user_pool_config['pg_autoscale_mode']['value']
if pg_autoscale_mode in ['off', 'warn']:
args.extend(['--pg_num',
user_pool_config['pg_num']['value'],
'--pgp_num',
user_pool_config['pgp_num']['value'] or
user_pool_config['pg_num']['value']])
if pg_autoscale_mode in ['on', 'warn'] and 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_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_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_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:
return rc, cmd, out, err
else:
rc, cmd, out, err = exec_command(module, disable_application_pool(cluster, name, delta['application']['old_application'], user, user_key, container_image=container_image)) # noqa: E501
if rc != 0:
return rc, cmd, out, err
rc, cmd, out, err = exec_command(module, enable_application_pool(cluster, name, delta['application']['new_application'], user, user_key, container_image=container_image)) # noqa: E501
if rc != 0:
return rc, cmd, out, err
report = report + "\n" + "{} has been updated: {} is now {}".format(name, key, delta[key]['value']) # noqa: E501
out = report
return rc, cmd, out, err
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, default='present',
choices=['present', 'absent', 'list']),
details=dict(type='bool', required=False, default=False),
size=dict(type='str', required=False),
min_size=dict(type='str', required=False),
pg_num=dict(type='str', required=False),
pgp_num=dict(type='str', required=False),
pg_autoscale_mode=dict(type='str', required=False, default='on'),
target_size_ratio=dict(type='str', required=False, default=None),
pool_type=dict(type='str', required=False, default='replicated',
choices=['replicated', 'erasure', '1', '3']),
erasure_profile=dict(type='str', required=False, default='default'),
rule_name=dict(type='str', required=False, default=None),
expected_num_objects=dict(type='str', required=False, default="0"),
application=dict(type='str', required=False, default=None),
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True
)
# Gather module parameters in variables
cluster = module.params.get('cluster')
name = module.params.get('name')
state = module.params.get('state')
details = module.params.get('details')
size = module.params.get('size')
min_size = module.params.get('min_size')
pg_num = module.params.get('pg_num')
pgp_num = module.params.get('pgp_num')
pg_autoscale_mode = module.params.get('pg_autoscale_mode')
target_size_ratio = module.params.get('target_size_ratio')
application = module.params.get('application')
if (module.params.get('pg_autoscale_mode').lower() in
['true', 'on', 'yes']):
pg_autoscale_mode = 'on'
elif (module.params.get('pg_autoscale_mode').lower() in
['false', 'off', 'no']):
pg_autoscale_mode = 'off'
else:
pg_autoscale_mode = 'warn'
if module.params.get('pool_type') == '1':
pool_type = 'replicated'
elif module.params.get('pool_type') == '3':
pool_type = 'erasure'
else:
pool_type = module.params.get('pool_type')
if not module.params.get('rule_name'):
rule_name = 'replicated_rule' if pool_type == 'replicated' else None
else:
rule_name = module.params.get('rule_name')
erasure_profile = module.params.get('erasure_profile')
expected_num_objects = module.params.get('expected_num_objects')
user_pool_config = {
'pool_name': {'value': name},
'pg_num': {'value': pg_num, 'cli_set_opt': 'pg_num'},
'pgp_num': {'value': pgp_num, 'cli_set_opt': 'pgp_num'},
'pg_autoscale_mode': {'value': pg_autoscale_mode,
'cli_set_opt': 'pg_autoscale_mode'},
'target_size_ratio': {'value': target_size_ratio,
'cli_set_opt': 'target_size_ratio'},
'application': {'value': application},
'type': {'value': pool_type},
'erasure_profile': {'value': erasure_profile},
'crush_rule': {'value': rule_name, 'cli_set_opt': 'crush_rule'},
'expected_num_objects': {'value': expected_num_objects},
'size': {'value': size, 'cli_set_opt': 'size'},
'min_size': {'value': min_size}
}
startd = datetime.datetime.now()
changed = False
# will return either the image name or None
container_image = is_containerized()
user = "client.admin"
keyring_filename = cluster + '.' + user + '.keyring'
user_key = os.path.join("/etc/ceph/", keyring_filename)
diff = dict(before="", after="")
if state == "present":
rc, cmd, out, err = exec_command(module,
check_pool_exist(cluster,
name,
user,
user_key,
container_image=container_image)) # noqa: E501
changed = rc != 0
if not changed:
running_pool_details = get_pool_details(module,
cluster,
name,
user,
user_key,
container_image=container_image) # noqa: E501
user_pool_config['pg_placement_num'] = {'value': str(running_pool_details[2]['pg_placement_num']), 'cli_set_opt': 'pgp_num'} # noqa: E501
delta = compare_pool_config(user_pool_config,
running_pool_details[2])
if len(delta) > 0:
keys = list(delta.keys())
details = running_pool_details[2]
if details['erasure_code_profile'] and 'size' in keys:
del delta['size']
if details['pg_autoscale_mode'] == 'on':
delta.pop('pg_num', None)
delta.pop('pgp_num', None)
if not module.params.get('rule_name'):
delta.pop('crush_rule', None)
for key in delta.keys():
diff['before'] += "{}: {}\n".format(key, details[key])
diff['after'] += "{}: {}\n".format(key, delta[key]['value'])
changed = len(delta) > 0
if changed and not module.check_mode:
rc, cmd, out, err = update_pool(module,
cluster,
name,
user,
user_key,
delta,
container_image=container_image) # noqa: E501
elif not module.check_mode:
rc, cmd, out, err = exec_command(module,
create_pool(cluster,
user,
user_key,
user_pool_config=user_pool_config, # noqa: E501
container_image=container_image)) # noqa: E501
if user_pool_config['application']['value']:
rc, _, _, _ = exec_command(module,
enable_application_pool(cluster,
name,
user_pool_config['application']['value'], # noqa: E501
user,
user_key,
container_image=container_image)) # noqa: E501
if rc == 0 and user_pool_config['application']['value'] == 'rbd': # noqa: E501
rc, cmd, out, err = exec_command(module,
init_rbd_pool(cluster,
user_pool_config['pool_name']['value'], # noqa: E501
user,
user_key,
container_image=container_image)) # noqa: E501
if user_pool_config['min_size']['value']:
# not implemented yet
pass
elif state == "list":
rc, cmd, out, err = exec_command(module,
list_pools(cluster,
name, user,
user_key,
details,
container_image=container_image)) # noqa: E501
if rc != 0:
out = "Couldn't list pool(s) present on the cluster"
elif state == "absent":
rc, cmd, out, err = exec_command(module,
check_pool_exist(cluster,
name, user,
user_key,
container_image=container_image)) # noqa: E501
changed = rc == 0
if changed and not module.check_mode:
rc, cmd, out, err = exec_command(module,
remove_pool(cluster,
name,
user,
user_key,
container_image=container_image)) # noqa: E501
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()
+732
View File
@@ -0,0 +1,732 @@
#!/usr/bin/python
from ansible.module_utils.basic import AnsibleModule
try:
from ansible.module_utils.ca_common import exec_command, \
is_containerized, \
fatal
except ImportError:
from module_utils.ca_common import exec_command, \
is_containerized, \
fatal
import datetime
import copy
import json
import os
import re
ANSIBLE_METADATA = {
'metadata_version': '1.0',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: ceph_volume
short_description: Create ceph OSDs with ceph-volume
description:
- Using the ceph-volume utility available in Ceph this module
can be used to create ceph OSDs that are backed by logical volumes.
- Only available in ceph versions luminous or greater.
options:
cluster:
description:
- The ceph cluster name.
required: false
default: ceph
objectstore:
description:
- The objectstore of the OSD.
required: false
choices: ['bluestore']
default: bluestore
action:
description:
- The action to take. Creating OSDs and zapping or querying devices.
required: true
choices: ['create', 'zap', 'batch', 'prepare', 'activate', 'list', 'inventory']
default: create
data:
description:
- The logical volume name or device to use for the OSD data.
required: true
data_vg:
description:
- If data is a lv, this must be the name of the volume group it belongs to.
required: false
osd_fsid:
description:
- The OSD FSID
required: false
osd_id:
description:
- The OSD ID
required: false
db:
description:
- A partition or logical volume name to use for block.db.
required: false
db_vg:
description:
- If db is a lv, this must be the name of the volume group it belongs to. # noqa: E501
required: false
wal:
description:
- A partition or logical volume name to use for block.wal.
required: false
wal_vg:
description:
- If wal is a lv, this must be the name of the volume group it belongs to. # noqa: E501
required: false
crush_device_class:
description:
- Will set the crush device class for the OSD.
required: false
dmcrypt:
description:
- If set to True the OSD will be encrypted with dmcrypt.
required: false
batch_devices:
description:
- A list of devices to pass to the 'ceph-volume lvm batch' subcommand.
- Only applicable if action is 'batch'.
required: false
osds_per_device:
description:
- The number of OSDs to create per device.
- Only applicable if action is 'batch'.
required: false
default: 1
block_db_size:
description:
- The size in bytes of bluestore block db lvs.
- The default of -1 means to create them as big as possible.
- Only applicable if action is 'batch'.
required: false
default: -1
block_db_devices:
description:
- A list of devices for bluestore block db to pass to the 'ceph-volume lvm batch' subcommand.
- Only applicable if action is 'batch'.
required: false
wal_devices:
description:
- A list of devices for bluestore block wal to pass to the 'ceph-volume lvm batch' subcommand.
- Only applicable if action is 'batch'.
required: false
report:
description:
- If provided the --report flag will be passed to 'ceph-volume lvm batch'.
- No OSDs will be created.
- Results will be returned in json format.
- Only applicable if action is 'batch'.
required: false
list:
description:
- List potential Ceph LVM metadata on a device
required: false
inventory:
description:
- List storage device inventory.
required: false
author:
- Andrew Schoen (@andrewschoen)
- Sebastien Han <seb@redhat.com>
'''
EXAMPLES = '''
- name: set up a bluestore osd with a raw device for data
ceph_volume:
objectstore: bluestore
data: /dev/sdc
action: create
- name: set up a bluestore osd with an lv for data and partitions for block.wal and block.db # noqa: E501
ceph_volume:
objectstore: bluestore
data: data-lv
data_vg: data-vg
db: /dev/sdc1
wal: /dev/sdc2
action: create
'''
def container_exec(binary, container_image, mounts=None):
'''
Build the docker CLI to run a command inside a container
'''
_mounts = {}
_mounts['/run/lock/lvm'] = '/run/lock/lvm:z'
_mounts['/var/run/udev'] = '/var/run/udev:z'
_mounts['/dev'] = '/dev'
_mounts['/etc/ceph'] = '/etc/ceph:z'
_mounts['/run/lvm'] = '/run/lvm'
_mounts['/var/lib/ceph'] = '/var/lib/ceph:z'
_mounts['/var/log/ceph'] = '/var/log/ceph:z'
if mounts is None:
mounts = _mounts
else:
_mounts.update(mounts)
volumes = sum(
[['-v', '{}:{}'.format(src_dir, dst_dir)]
for src_dir, dst_dir in _mounts.items()], [])
container_binary = os.getenv('CEPH_CONTAINER_BINARY')
command_exec = [container_binary, 'run',
'--rm',
'--privileged',
'--net=host',
'--ipc=host'] + volumes + \
['--entrypoint=' + binary, container_image]
return command_exec
def build_cmd(action, container_image,
cluster='ceph',
binary='ceph-volume', mounts=None):
'''
Build the ceph-volume command
'''
_binary = binary
if container_image:
cmd = container_exec(
binary, container_image, mounts=mounts)
else:
binary = [binary]
cmd = binary
if _binary == 'ceph-volume':
cmd.extend(['--cluster', cluster])
cmd.extend(action)
return cmd
def get_data(data, data_vg):
if data_vg:
data = '{0}/{1}'.format(data_vg, data)
return data
def get_journal(journal, journal_vg):
if journal_vg:
journal = '{0}/{1}'.format(journal_vg, journal)
return journal
def get_db(db, db_vg):
if db_vg:
db = '{0}/{1}'.format(db_vg, db)
return db
def get_wal(wal, wal_vg):
if wal_vg:
wal = '{0}/{1}'.format(wal_vg, wal)
return wal
def batch(module, container_image, report=None):
'''
Batch prepare OSD devices
'''
# get module variables
cluster = module.params['cluster']
objectstore = module.params['objectstore']
batch_devices = module.params.get('batch_devices', None)
crush_device_class = module.params.get('crush_device_class', None)
block_db_size = module.params.get('block_db_size', None)
block_db_devices = module.params.get('block_db_devices', None)
wal_devices = module.params.get('wal_devices', None)
dmcrypt = module.params.get('dmcrypt', None)
osds_per_device = module.params.get('osds_per_device', 1)
if not osds_per_device:
fatal('osds_per_device must be provided if action is "batch"', module)
if osds_per_device < 1:
fatal('osds_per_device must be greater than 0 if action is "batch"', module) # noqa: E501
if not batch_devices:
fatal('batch_devices must be provided if action is "batch"', module)
# Build the CLI
action = ['lvm', 'batch']
cmd = build_cmd(action, container_image, cluster)
cmd.extend(['--%s' % objectstore])
if not report:
cmd.append('--yes')
if container_image:
cmd.append('--prepare')
if crush_device_class:
cmd.extend(['--crush-device-class', crush_device_class])
if dmcrypt:
cmd.append('--dmcrypt')
if osds_per_device > 1:
cmd.extend(['--osds-per-device', str(osds_per_device)])
if objectstore == 'bluestore' and block_db_size != '-1':
cmd.extend(['--block-db-size', block_db_size])
cmd.extend(batch_devices)
if block_db_devices and objectstore == 'bluestore':
cmd.append('--db-devices')
cmd.extend(block_db_devices)
if wal_devices and objectstore == 'bluestore':
cmd.append('--wal-devices')
cmd.extend(wal_devices)
return cmd
def ceph_volume_cmd(subcommand, container_image, cluster=None):
'''
Build ceph-volume initial command
'''
if container_image:
binary = 'ceph-volume'
cmd = container_exec(
binary, container_image)
else:
binary = ['ceph-volume']
cmd = binary
if cluster:
cmd.extend(['--cluster', cluster])
cmd.append('lvm')
cmd.append(subcommand)
return cmd
def prepare_or_create_osd(module, action, container_image):
'''
Prepare or create OSD devices
'''
# get module variables
cluster = module.params['cluster']
objectstore = module.params['objectstore']
data = module.params['data']
data_vg = module.params.get('data_vg', None)
data = get_data(data, data_vg)
db = module.params.get('db', None)
db_vg = module.params.get('db_vg', None)
wal = module.params.get('wal', None)
wal_vg = module.params.get('wal_vg', None)
crush_device_class = module.params.get('crush_device_class', None)
dmcrypt = module.params.get('dmcrypt', None)
# Build the CLI
action = ['lvm', action]
cmd = build_cmd(action, container_image, cluster)
cmd.extend(['--%s' % objectstore])
cmd.append('--data')
cmd.append(data)
if db and objectstore == 'bluestore':
db = get_db(db, db_vg)
cmd.extend(['--block.db', db])
if wal and objectstore == 'bluestore':
wal = get_wal(wal, wal_vg)
cmd.extend(['--block.wal', wal])
if crush_device_class:
cmd.extend(['--crush-device-class', crush_device_class])
if dmcrypt:
cmd.append('--dmcrypt')
return cmd
def list_osd(module, container_image):
'''
List will detect wether or not a device has Ceph LVM Metadata
'''
# get module variables
cluster = module.params['cluster']
data = module.params.get('data', None)
data_vg = module.params.get('data_vg', None)
data = get_data(data, data_vg)
# Build the CLI
action = ['lvm', 'list']
cmd = build_cmd(action,
container_image,
cluster,
mounts={'/var/lib/ceph': '/var/lib/ceph:ro'})
if data:
cmd.append(data)
cmd.append('--format=json')
return cmd
def list_storage_inventory(module, container_image):
'''
List storage inventory.
'''
action = ['inventory']
cmd = build_cmd(action, container_image)
cmd.append('--format=json')
return cmd
def activate_osd():
'''
Activate all the OSDs on a machine
'''
# build the CLI
action = ['lvm', 'activate']
container_image = None
cmd = build_cmd(action, container_image)
cmd.append('--all')
return cmd
def is_lv(module, vg, lv, container_image):
'''
Check if an LV exists
'''
args = ['--noheadings', '--reportformat', 'json', '--select', 'lv_name={},vg_name={}'.format(lv, vg)] # noqa: E501
cmd = build_cmd(args, container_image, binary='lvs')
rc, cmd, out, err = exec_command(module, cmd)
if rc == 0:
result = json.loads(out)['report'][0]['lv']
if len(result) > 0:
return True
return False
def zap_devices(module, container_image):
'''
Will run 'ceph-volume lvm zap' on all devices, lvs and partitions
used to create the OSD. The --destroy flag is always passed so that
if an OSD was originally created with a raw device or partition for
'data' then any lvs that were created by ceph-volume are removed.
'''
# get module variables
data = module.params.get('data', None)
data_vg = module.params.get('data_vg', None)
db = module.params.get('db', None)
db_vg = module.params.get('db_vg', None)
wal = module.params.get('wal', None)
wal_vg = module.params.get('wal_vg', None)
osd_fsid = module.params.get('osd_fsid', None)
osd_id = module.params.get('osd_id', None)
destroy = module.params.get('destroy', True)
# build the CLI
action = ['lvm', 'zap']
cmd = build_cmd(action, container_image)
if destroy:
cmd.append('--destroy')
if osd_fsid:
cmd.extend(['--osd-fsid', osd_fsid])
if osd_id:
cmd.extend(['--osd-id', osd_id])
if data:
data = get_data(data, data_vg)
cmd.append(data)
if db:
db = get_db(db, db_vg)
cmd.extend([db])
if wal:
wal = get_wal(wal, wal_vg)
cmd.extend([wal])
return cmd
def allowed_in_check_mode(module):
'''
Check if the action is allowed in check mode
'''
action = module.params['action']
report = module.params.get('report', False)
# batch is allowed in check mode if report is set
if action == 'batch' and report:
return True
allowed_actions = ['list', 'inventory']
return action in allowed_actions
def run_module():
module_args = dict(
cluster=dict(type='str', required=False, default='ceph'),
objectstore=dict(type='str', required=False, choices=[
'bluestore'], default='bluestore'),
action=dict(type='str', required=False, choices=[
'create', 'zap', 'batch', 'prepare', 'activate', 'list',
'inventory'], default='create'), # noqa: 4502
data=dict(type='str', required=False),
data_vg=dict(type='str', required=False),
db=dict(type='str', required=False),
db_vg=dict(type='str', required=False),
wal=dict(type='str', required=False),
wal_vg=dict(type='str', required=False),
crush_device_class=dict(type='str', required=False),
dmcrypt=dict(type='bool', required=False, default=False),
batch_devices=dict(type='list', required=False, default=[]),
osds_per_device=dict(type='int', required=False, default=1),
block_db_size=dict(type='str', required=False, default='-1'),
block_db_devices=dict(type='list', required=False, default=[]),
wal_devices=dict(type='list', required=False, default=[]),
report=dict(type='bool', required=False, default=False),
osd_fsid=dict(type='str', required=False),
osd_id=dict(type='str', required=False),
destroy=dict(type='bool', required=False, default=True),
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True,
mutually_exclusive=[
('data', 'osd_fsid', 'osd_id'),
],
required_if=[
('action', 'zap', ('data', 'osd_fsid', 'osd_id'), True)
]
)
result = dict(
changed=False,
stdout='',
stderr='',
rc=0,
start='',
end='',
delta='',
)
if module.check_mode and not allowed_in_check_mode(module):
module.exit_json(**result)
# start execution
startd = datetime.datetime.now()
# get the desired action
action = module.params['action']
# will return either the image name or None
container_image = is_containerized()
# Assume the task's status will be 'changed'
changed = True
if action == 'create' or action == 'prepare':
# First test if the device has Ceph LVM Metadata
rc, cmd, out, err = exec_command(
module, list_osd(module, container_image))
# list_osd returns a dict, if the dict is empty this means
# we can not check the return code since it's not consistent
# with the plain output
# see: http://tracker.ceph.com/issues/36329
# FIXME: it's probably less confusing to check for rc
# convert out to json, ansible returns a string...
try:
out_dict = json.loads(out)
except ValueError:
fatal("Could not decode json output: {} from the command {}".format(out, cmd), module) # noqa: E501
if out_dict:
data = module.params['data']
result['stdout'] = 'skipped, since {0} is already used for an osd'.format(data) # noqa: E501
result['rc'] = 0
module.exit_json(**result)
# Prepare or create the OSD
rc, cmd, out, err = exec_command(
module, prepare_or_create_osd(module, action, container_image))
err = re.sub('[a-zA-Z0-9+/]{38}==', '*' * 8, err)
elif action == 'activate':
if container_image:
fatal(
"This is not how container's activation happens, nothing to activate", module) # noqa: E501
# Activate the OSD
rc, cmd, out, err = exec_command(
module, activate_osd())
elif action == 'zap':
# Zap the OSD
skip = []
for device_type in ['journal', 'data', 'db', 'wal']:
# 1/ if we passed vg/lv
if module.params.get('{}_vg'.format(device_type), None) and module.params.get(device_type, None): # noqa: E501
# 2/ check this is an actual lv/vg
ret = is_lv(module, module.params['{}_vg'.format(device_type)], module.params[device_type], container_image) # noqa: E501
skip.append(ret)
# 3/ This isn't a lv/vg device
if not ret:
module.params['{}_vg'.format(device_type)] = False
module.params[device_type] = False
# 4/ no journal|data|db|wal|_vg was passed, so it must be a raw device # noqa: E501
elif not module.params.get('{}_vg'.format(device_type), None) and module.params.get(device_type, None): # noqa: E501
skip.append(True)
cmd = zap_devices(module, container_image)
if any(skip) or module.params.get('osd_fsid', None) \
or module.params.get('osd_id', None):
rc, cmd, out, err = exec_command(
module, cmd)
for scan_cmd in ['vgscan', 'lvscan']:
module.run_command([scan_cmd, '--cache'])
else:
out = 'Skipped, nothing to zap'
err = ''
changed = False
rc = 0
elif action == 'list':
# List Ceph LVM Metadata on a device
changed = False
rc, cmd, out, err = exec_command(
module, list_osd(module, container_image))
elif action == 'inventory':
# List storage device inventory.
changed = False
rc, cmd, out, err = exec_command(
module, list_storage_inventory(module, container_image))
elif action == 'batch':
# Batch prepare AND activate OSDs
report = module.params.get('report', None)
# Add --report flag for the idempotency test
report_flags = [
'--report',
'--format=json',
]
cmd = batch(module, container_image, report=True)
batch_report_cmd = copy.copy(cmd)
batch_report_cmd.extend(report_flags)
# Run batch --report to see what's going to happen
# Do not run the batch command if there is nothing to do
rc, cmd, out, err = exec_command(
module, batch_report_cmd)
try:
if not out:
out = '{}'
report_result = json.loads(out)
except ValueError:
strategy_changed_in_out = "strategy changed" in out
strategy_changed_in_err = "strategy changed" in err
strategy_changed = strategy_changed_in_out or \
strategy_changed_in_err
if strategy_changed:
if strategy_changed_in_out:
out = json.dumps({"changed": False,
"stdout": out.rstrip("\r\n")})
elif strategy_changed_in_err:
out = json.dumps({"changed": False,
"stderr": err.rstrip("\r\n")})
rc = 0
changed = False
else:
out = out.rstrip("\r\n")
result = dict(
cmd=cmd,
stdout=out.rstrip('\r\n'),
stderr=err.rstrip('\r\n'),
rc=rc,
changed=changed,
)
if strategy_changed:
module.exit_json(**result)
module.fail_json(msg='non-zero return code', **result)
if not report:
if 'changed' in report_result:
# we have the old batch implementation
# if not asking for a report, let's just run the batch command
changed = report_result['changed']
if changed:
# Batch prepare the OSD
rc, cmd, out, err = exec_command(
module, batch(module, container_image))
err = re.sub('[a-zA-Z0-9+/]{38}==', '*' * 8, err)
else:
# we have the refactored batch, its idempotent so lets just
# run it
rc, cmd, out, err = exec_command(
module, batch(module, container_image))
err = re.sub('[a-zA-Z0-9+/]{38}==', '*' * 8, err)
else:
cmd = batch_report_cmd
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()
+190
View File
@@ -0,0 +1,190 @@
# 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.
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
except ImportError:
from module_utils.ca_common import exit_module
import datetime
import os
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: ceph_volume_simple_activate
short_description: Activate legacy OSD with ceph-volume
version_added: "2.8"
description:
- Activate legacy OSD with ceph-volume by providing the JSON file from
the scan operation or by passing the OSD ID and OSD FSID.
options:
cluster:
description:
- The ceph cluster name.
required: false
default: ceph
path:
description:
- The OSD metadata as JSON file in /etc/ceph/osd directory, it
must exist.
required: false
osd_id:
description:
- The legacy OSD ID.
required: false
osd_fsid:
description:
- The legacy OSD FSID.
required: false
osd_all:
description:
- Activate all legacy OSDs.
required: false
systemd:
description:
- Using systemd unit during the OSD activation.
required: false
default: true
author:
- Dimitri Savineau <dsavinea@redhat.com>
'''
EXAMPLES = '''
- name: activate all legacy OSDs
ceph_volume_simple_activate:
cluster: ceph
osd_all: true
- name: activate a legacy OSD via OSD ID and OSD FSID
ceph_volume_simple_activate:
cluster: ceph
osd_id: 3
osd_fsid: 0c4a7eca-0c2a-4c12-beff-08a80f064c52
- name: activate a legacy OSD via the JSON file
ceph_volume_simple_activate:
cluster: ceph
path: /etc/ceph/osd/3-0c4a7eca-0c2a-4c12-beff-08a80f064c52.json
- name: activate a legacy OSD via the JSON file without systemd
ceph_volume_simple_activate:
cluster: ceph
path: /etc/ceph/osd/3-0c4a7eca-0c2a-4c12-beff-08a80f064c52.json
systemd: false
'''
RETURN = '''# '''
def main():
module = AnsibleModule(
argument_spec=dict(
cluster=dict(type='str', required=False, default='ceph'),
path=dict(type='path', required=False),
systemd=dict(type='bool', required=False, default=True),
osd_id=dict(type='str', required=False),
osd_fsid=dict(type='str', required=False),
osd_all=dict(type='bool', required=False),
),
supports_check_mode=True,
mutually_exclusive=[
('osd_all', 'osd_id'),
('osd_all', 'osd_fsid'),
('path', 'osd_id'),
('path', 'osd_fsid'),
],
required_together=[
('osd_id', 'osd_fsid')
],
required_one_of=[
('path', 'osd_id', 'osd_all'),
('path', 'osd_fsid', 'osd_all'),
],
)
path = module.params.get('path')
cluster = module.params.get('cluster')
systemd = module.params.get('systemd')
osd_id = module.params.get('osd_id')
osd_fsid = module.params.get('osd_fsid')
osd_all = module.params.get('osd_all')
if path and not os.path.exists(path):
module.fail_json(msg='{} does not exist'.format(path), rc=1)
startd = datetime.datetime.now()
container_image = os.getenv('CEPH_CONTAINER_IMAGE')
container_binary = os.getenv('CEPH_CONTAINER_BINARY')
if container_binary and container_image:
cmd = [container_binary,
'run', '--rm', '--privileged',
'--ipc=host', '--net=host',
'-v', '/etc/ceph:/etc/ceph:z',
'-v', '/var/lib/ceph/:/var/lib/ceph/:z',
'-v', '/var/log/ceph/:/var/log/ceph/:z',
'-v', '/run/lvm/:/run/lvm/',
'-v', '/run/lock/lvm/:/run/lock/lvm/',
'--entrypoint=ceph-volume', container_image]
else:
cmd = ['ceph-volume']
cmd.extend(['--cluster', cluster, 'simple', 'activate'])
if osd_all:
cmd.append('--all')
else:
if path:
cmd.extend(['--file', path])
else:
cmd.extend([osd_id, osd_fsid])
if not systemd:
cmd.append('--no-systemd')
if module.check_mode:
exit_module(
module=module,
out='',
rc=0,
cmd=cmd,
err='',
startd=startd,
changed=False
)
else:
rc, out, err = module.run_command(cmd)
exit_module(
module=module,
out=out,
rc=rc,
cmd=cmd,
err=err,
startd=startd,
changed=True
)
if __name__ == '__main__':
main()
+163
View File
@@ -0,0 +1,163 @@
# 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.
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
except ImportError:
from module_utils.ca_common import exit_module
import datetime
import os
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: ceph_volume_simple_scan
short_description: Scan legacy OSD with ceph-volume
version_added: "2.8"
description:
- Scan legacy OSD with ceph-volume and store the output as JSON file
in /etc/ceph/osd directory with {OSD_ID}-{OSD_FSID}.json format.
options:
cluster:
description:
- The ceph cluster name.
required: false
default: ceph
path:
description:
- The OSD directory or metadata partition. The directory or
partition must exist.
required: false
force:
description:
- Force re-scanning an OSD and overwriting the JSON content.
required: false
default: false
stdout:
description:
- Do not store the output to JSON file but stdout instead.
required: false
default: false
author:
- Dimitri Savineau <dsavinea@redhat.com>
'''
EXAMPLES = '''
- name: scan all running OSDs
ceph_volume_simple_scan:
cluster: ceph
- name: scan an OSD with the directory
ceph_volume_simple_scan:
cluster: ceph
path: /var/lib/ceph/osd/ceph-3
- name: scan an OSD with the partition
ceph_volume_simple_scan:
cluster: ceph
path: /dev/sdb1
- name: rescan an OSD and print the result on stdout
ceph_volume_simple_scan:
cluster: ceph
path: /dev/nvme0n1p1
force: true
stdout: true
'''
RETURN = '''# '''
def main():
module = AnsibleModule(
argument_spec=dict(
cluster=dict(type='str', required=False, default='ceph'),
path=dict(type='path', required=False),
force=dict(type='bool', required=False, default=False),
stdout=dict(type='bool', required=False, default=False),
),
supports_check_mode=True,
)
path = module.params.get('path')
cluster = module.params.get('cluster')
force = module.params.get('force')
stdout = module.params.get('stdout')
if path and not os.path.exists(path):
module.fail_json(msg='{} does not exist'.format(path), rc=1)
startd = datetime.datetime.now()
container_image = os.getenv('CEPH_CONTAINER_IMAGE')
container_binary = os.getenv('CEPH_CONTAINER_BINARY')
if container_binary and container_image:
cmd = [container_binary,
'run', '--rm', '--privileged',
'--ipc=host', '--net=host',
'-v', '/etc/ceph:/etc/ceph:z',
'-v', '/var/lib/ceph/:/var/lib/ceph/:z',
'-v', '/var/log/ceph/:/var/log/ceph/:z',
'-v', '/run/lvm/:/run/lvm/',
'-v', '/run/lock/lvm/:/run/lock/lvm/',
'--entrypoint=ceph-volume', container_image]
else:
cmd = ['ceph-volume']
cmd.extend(['--cluster', cluster, 'simple', 'scan'])
if force:
cmd.append('--force')
if stdout:
cmd.append('--stdout')
if path:
cmd.append(path)
if module.check_mode:
exit_module(
module=module,
out='',
rc=0,
cmd=cmd,
err='',
startd=startd,
changed=False
)
else:
rc, out, err = module.run_command(cmd)
exit_module(
module=module,
out=out,
rc=rc,
cmd=cmd,
err=err,
startd=startd,
changed=True
)
if __name__ == '__main__':
main()
+184
View File
@@ -0,0 +1,184 @@
# 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.
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
except ImportError:
from module_utils.ca_common import exit_module
import datetime
import json
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: cephadm_adopt
short_description: Adopt a Ceph cluster with cephadm
version_added: "2.8"
description:
- Adopt a Ceph cluster with cephadm
options:
name:
description:
- The ceph daemon name.
required: true
cluster:
description:
- The ceph cluster name.
required: false
default: ceph
style:
description:
- Cep deployment style.
required: false
default: legacy
image:
description:
- Ceph container image.
required: false
docker:
description:
- Use docker instead of podman.
required: false
pull:
description:
- Pull the Ceph container image.
required: false
default: true
firewalld:
description:
- Manage firewall rules with firewalld.
required: false
default: true
author:
- Dimitri Savineau <dsavinea@redhat.com>
'''
EXAMPLES = '''
- name: adopt a ceph monitor with cephadm (default values)
cephadm_adopt:
name: mon.foo
style: legacy
- name: adopt a ceph monitor with cephadm (with custom values)
cephadm_adopt:
name: mon.foo
style: legacy
image: quay.io/ceph/ceph:v19
pull: false
firewalld: false
- name: adopt a ceph monitor with cephadm with custom image via env var
cephadm_adopt:
name: mon.foo
style: legacy
environment:
CEPHADM_IMAGE: quay.io/ceph/ceph:v19
'''
RETURN = '''# '''
def main():
module = AnsibleModule(
argument_spec=dict(
name=dict(type='str', required=True),
cluster=dict(type='str', required=False, default='ceph'),
style=dict(type='str', required=False, default='legacy'),
image=dict(type='str', required=False),
docker=dict(type='bool', required=False, default=False),
pull=dict(type='bool', required=False, default=True),
firewalld=dict(type='bool', required=False, default=True),
),
supports_check_mode=True,
)
name = module.params.get('name')
cluster = module.params.get('cluster')
style = module.params.get('style')
docker = module.params.get('docker')
image = module.params.get('image')
pull = module.params.get('pull')
firewalld = module.params.get('firewalld')
startd = datetime.datetime.now()
cmd = ['cephadm', 'ls', '--no-detail']
if module.check_mode:
exit_module(
module=module,
out='',
rc=0,
cmd=cmd,
err='',
startd=startd,
changed=False
)
else:
rc, out, err = module.run_command(cmd)
if rc == 0:
if name in [x["name"] for x in json.loads(out) if x["style"] == "cephadm:v1"]: # noqa: E501
exit_module(
module=module,
out='{} is already adopted'.format(name),
rc=0,
cmd=cmd,
err='',
startd=startd,
changed=False
)
else:
module.fail_json(msg=err, rc=rc)
cmd = ['cephadm']
if docker:
cmd.append('--docker')
if image:
cmd.extend(['--image', image])
cmd.extend(['adopt', '--cluster', cluster, '--name', name, '--style', style]) # noqa: E501
if not pull:
cmd.append('--skip-pull')
if not firewalld:
cmd.append('--skip-firewalld')
rc, out, err = module.run_command(cmd)
exit_module(
module=module,
out=out,
rc=rc,
cmd=cmd,
err=err,
startd=startd,
changed=True
)
if __name__ == '__main__':
main()
+265
View File
@@ -0,0 +1,265 @@
# 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.
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
except ImportError:
from module_utils.ca_common import exit_module
import datetime
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: cephadm_bootstrap
short_description: Bootstrap a Ceph cluster via cephadm
version_added: "2.8"
description:
- Bootstrap a Ceph cluster via cephadm
options:
mon_ip:
description:
- Ceph monitor IP address.
required: true
image:
description:
- Ceph container image.
required: false
docker:
description:
- Use docker instead of podman.
required: false
fsid:
description:
- Ceph FSID.
required: false
pull:
description:
- Pull the Ceph container image.
required: false
default: true
dashboard:
description:
- Deploy the Ceph dashboard.
required: false
default: true
dashboard_user:
description:
- Ceph dashboard user.
required: false
dashboard_password:
description:
- Ceph dashboard password.
required: false
monitoring:
description:
- Deploy the monitoring stack.
required: false
default: true
firewalld:
description:
- Manage firewall rules with firewalld.
required: false
default: true
allow_overwrite:
description:
- allow overwrite of existing –output-* config/keyring/ssh files.
required: false
default: false
registry_url:
description:
- URL for custom registry.
required: false
registry_username:
description:
- Username for custom registry.
required: false
registry_password:
description:
- Password for custom registry.
required: false
registry_json:
description:
- JSON file with custom registry login info (URL,
username, password).
required: false
ssh_user:
description:
- SSH user used for cephadm ssh to the hosts.
required: false
ssh_config:
description:
- SSH config file path for cephadm ssh client.
required: false
author:
- Dimitri Savineau <dsavinea@redhat.com>
'''
EXAMPLES = '''
- name: bootstrap a cluster via cephadm (with default values)
cephadm_bootstrap:
mon_ip: 192.168.42.1
- name: bootstrap a cluster via cephadm (with custom values)
cephadm_bootstrap:
mon_ip: 192.168.42.1
fsid: 3c9ba63a-c7df-4476-a1e7-317dfc711f82
image: quay.io/ceph/ceph:v19
dashboard: false
monitoring: false
firewalld: false
- name: bootstrap a cluster via cephadm with custom image via env var
cephadm_bootstrap:
mon_ip: 192.168.42.1
environment:
CEPHADM_IMAGE: quay.io/ceph/ceph:v19
'''
RETURN = '''# '''
def main():
module = AnsibleModule(
argument_spec=dict(
mon_ip=dict(type='str', required=True),
image=dict(type='str', required=False),
docker=dict(type='bool', required=False, default=False),
fsid=dict(type='str', required=False),
pull=dict(type='bool', required=False, default=True),
dashboard=dict(type='bool', required=False, default=True),
dashboard_user=dict(type='str', required=False),
dashboard_password=dict(type='str', required=False, no_log=True),
monitoring=dict(type='bool', required=False, default=True),
firewalld=dict(type='bool', required=False, default=True),
allow_overwrite=dict(type='bool', required=False, default=False),
registry_url=dict(type='str', require=False),
registry_username=dict(type='str', require=False),
registry_password=dict(type='str', require=False, no_log=True),
registry_json=dict(type='path', require=False),
ssh_user=dict(type='str', required=False),
ssh_config=dict(type='str', required=False),
),
supports_check_mode=True,
mutually_exclusive=[
('registry_json', 'registry_url'),
('registry_json', 'registry_username'),
('registry_json', 'registry_password'),
],
required_together=[
('registry_url', 'registry_username', 'registry_password')
],
)
mon_ip = module.params.get('mon_ip')
docker = module.params.get('docker')
image = module.params.get('image')
fsid = module.params.get('fsid')
pull = module.params.get('pull')
dashboard = module.params.get('dashboard')
dashboard_user = module.params.get('dashboard_user')
dashboard_password = module.params.get('dashboard_password')
monitoring = module.params.get('monitoring')
firewalld = module.params.get('firewalld')
allow_overwrite = module.params.get('allow_overwrite')
registry_url = module.params.get('registry_url')
registry_username = module.params.get('registry_username')
registry_password = module.params.get('registry_password')
registry_json = module.params.get('registry_json')
ssh_user = module.params.get('ssh_user')
ssh_config = module.params.get('ssh_config')
startd = datetime.datetime.now()
cmd = ['cephadm']
if docker:
cmd.append('--docker')
if image:
cmd.extend(['--image', image])
cmd.extend(['bootstrap', '--mon-ip', mon_ip])
if fsid:
cmd.extend(['--fsid', fsid])
if not pull:
cmd.append('--skip-pull')
if dashboard:
if dashboard_user:
cmd.extend(['--initial-dashboard-user', dashboard_user])
if dashboard_password:
cmd.extend(['--initial-dashboard-password', dashboard_password])
else:
cmd.append('--skip-dashboard')
if not monitoring:
cmd.append('--skip-monitoring-stack')
if not firewalld:
cmd.append('--skip-firewalld')
if allow_overwrite:
cmd.append('--allow-overwrite')
if registry_url and registry_username and registry_password:
cmd.extend(['--registry-url', registry_url,
'--registry-username', registry_username,
'--registry-password', registry_password])
if registry_json:
cmd.extend(['--registry-json', registry_json])
if ssh_user:
cmd.extend(['--ssh-user', ssh_user])
if ssh_config:
cmd.extend(['--ssh-config', ssh_config])
if module.check_mode:
exit_module(
module=module,
out='',
rc=0,
cmd=cmd,
err='',
startd=startd,
changed=False
)
else:
rc, out, err = module.run_command(cmd)
exit_module(
module=module,
out=out,
rc=rc,
cmd=cmd,
err=err,
startd=startd,
changed=True
)
if __name__ == '__main__':
main()
+378
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()
+339
View File
@@ -0,0 +1,339 @@
# 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.
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible.module_utils.basic import AnsibleModule
import datetime
import os
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: radosgw_realm
short_description: Manage RADOS Gateway Realm
version_added: "2.8"
description:
- Manage RADOS Gateway realm(s) creation, deletion and updates.
options:
cluster:
description:
- The ceph cluster name.
required: false
default: ceph
name:
description:
- name of the RADOS Gateway realm.
required: true
state:
description:
If 'present' is used, the module creates a realm if it doesn't
exist or update it if it already exists.
If 'absent' is used, the module will simply delete the realm.
If 'info' is used, the module will return all details about the
existing realm (json formatted).
required: false
choices: ['present', 'absent', 'info']
default: present
default:
description:
- set the default flag on the realm.
required: false
default: false
url:
description:
- URL to the master RADOS Gateway zone.
required: false
access_key:
description:
- S3 access key of the master RADOS Gateway zone.
required: false
secret_key:
description:
- S3 secret key of the master RADOS Gateway zone.
required: false
author:
- Dimitri Savineau <dsavinea@redhat.com>
'''
EXAMPLES = '''
- name: create a RADOS Gateway default realm
radosgw_realm:
name: foo
default: true
- name: get a RADOS Gateway realm information
radosgw_realm:
name: foo
state: info
- name: delete a RADOS Gateway realm
radosgw_realm:
name: foo
state: absent
'''
RETURN = '''# '''
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=' + 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_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,
'realm'
]
cmd.extend(base_cmd + args)
return cmd
def exec_commands(module, cmd):
'''
Execute command(s)
'''
rc, out, err = module.run_command(cmd)
return rc, cmd, out, err
def create_realm(module, container_image=None):
'''
Create a new realm
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
default = module.params.get('default', False)
args = ['create', '--rgw-realm=' + name]
if default:
args.append('--default')
cmd = generate_radosgw_cmd(cluster=cluster,
args=args,
container_image=container_image)
return cmd
def get_realm(module, container_image=None):
'''
Get existing realm
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
args = ['get', '--rgw-realm=' + name, '--format=json']
cmd = generate_radosgw_cmd(cluster=cluster,
args=args,
container_image=container_image)
return cmd
def remove_realm(module, container_image=None):
'''
Remove a realm
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
args = ['delete', '--rgw-realm=' + name]
cmd = generate_radosgw_cmd(cluster=cluster,
args=args,
container_image=container_image)
return cmd
def pull_realm(module, container_image=None):
'''
Pull a realm
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
url = module.params.get('url')
access_key = module.params.get('access_key')
secret_key = module.params.get('secret_key')
default = module.params.get('default', False)
args = [
'pull',
'--rgw-realm=' + name,
'--url=' + url,
'--access-key=' + access_key,
'--secret=' + secret_key
]
if default:
args.append('--default')
cmd = generate_radosgw_cmd(cluster=cluster,
args=args,
container_image=container_image)
return cmd
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)
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', 'info', 'pull'], default='present'), # noqa: E501
default=dict(type='bool', required=False, default=False),
url=dict(type='str', required=False),
access_key=dict(type='str', required=False),
secret_key=dict(type='str', required=False),
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True,
required_if=[['state', 'pull', ['url', 'access_key', 'secret_key']]],
)
# Gather module parameters in variables
name = module.params.get('name')
state = module.params.get('state')
if module.check_mode:
module.exit_json(
changed=False,
stdout='',
stderr='',
rc=0,
start='',
end='',
delta='',
)
startd = datetime.datetime.now()
changed = False
# will return either the image name or None
container_image = is_containerized()
if state == "present":
rc, cmd, out, err = exec_commands(module, get_realm(module, container_image=container_image)) # noqa: E501
if rc != 0:
rc, cmd, out, err = exec_commands(module, create_realm(module, container_image=container_image)) # noqa: E501
changed = True
elif state == "absent":
rc, cmd, out, err = exec_commands(module, get_realm(module, container_image=container_image)) # noqa: E501
if rc == 0:
rc, cmd, out, err = exec_commands(module, remove_realm(module, container_image=container_image)) # noqa: E501
changed = True
else:
rc = 0
out = "Realm {} doesn't exist".format(name)
elif state == "info":
rc, cmd, out, err = exec_commands(module, get_realm(module, container_image=container_image)) # noqa: E501
elif state == "pull":
rc, cmd, out, err = exec_commands(module, pull_realm(module, container_image=container_image)) # noqa: E501
exit_module(module=module, out=out, rc=rc, cmd=cmd, err=err, startd=startd, changed=changed) # noqa: E501
def main():
run_module()
if __name__ == '__main__':
main()
+581
View File
@@ -0,0 +1,581 @@
# 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.
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible.module_utils.basic import AnsibleModule
import datetime
import json
import os
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: radosgw_user
short_description: Manage RADOS Gateway User
version_added: "2.8"
description:
- Manage RADOS Gateway user(s) creation, deletion and updates.
options:
cluster:
description:
- The ceph cluster name.
required: false
default: ceph
name:
description:
- name of the RADOS Gateway user (uid).
required: true
state:
description:
If 'present' is used, the module creates a user if it doesn't
exist or update it if it already exists.
If 'absent' is used, the module will simply delete the user.
If 'info' is used, the module will return all details about the
existing user (json formatted).
required: false
choices: ['present', 'absent', 'info']
default: present
display_name:
description:
- set the display name of the user.
required: false
default: None
email:
description:
- set the email of the user.
required: false
default: None
access_key:
description:
- set the S3 access key of the user.
required: false
default: None
secret_key:
description:
- set the S3 secret key of the user.
required: false
default: None
realm:
description:
- set the realm of the user.
required: false
default: None
zonegroup:
description:
- set the zonegroup of the user.
required: false
default: None
zone:
description:
- set the zone of the user.
required: false
default: None
system:
description:
- set the system flag on the user.
required: false
default: false
admin:
description:
- set the admin flag on the user.
required: false
default: false
author:
- Dimitri Savineau <dsavinea@redhat.com>
'''
EXAMPLES = '''
- name: create a RADOS Gateway sytem user
radosgw_user:
name: foo
system: true
- name: modify a RADOS Gateway user
radosgw_user:
name: foo
email: foo@bar.io
access_key: LbwDPp2BBo2Sdlts89Um
secret_key: FavL6ueQWcWuWn0YXyQ3TnJ3mT3Uj5SGVHCUXC5K
state: present
- name: get a RADOS Gateway user information
radosgw_user:
name: foo
state: info
- name: delete a RADOS Gateway user
radosgw_user:
name: foo
state: absent
'''
RETURN = '''# '''
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=' + 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_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,
'user'
]
cmd.extend(base_cmd + args)
return cmd
def generate_caps_cmd(cluster, args, container_image=None):
'''
Generate 'radosgw' command line to execute for caps
'''
cmd = pre_generate_radosgw_cmd(container_image=container_image)
base_cmd = [
'--cluster',
cluster,
'caps'
]
cmd.extend(base_cmd + args)
return cmd
def exec_commands(module, cmd):
'''
Execute command(s)
'''
rc, out, err = module.run_command(cmd)
return rc, cmd, out, err
def create_user(module, container_image=None):
'''
Create a new user
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
display_name = module.params.get('display_name')
if not display_name:
display_name = name
email = module.params.get('email', None)
access_key = module.params.get('access_key', None)
secret_key = module.params.get('secret_key', None)
realm = module.params.get('realm', None)
zonegroup = module.params.get('zonegroup', None)
zone = module.params.get('zone', None)
system = module.params.get('system', False)
admin = module.params.get('admin', False)
caps = module.params.get('caps')
args = ['create', '--uid=' + name, '--display_name=' + display_name]
if email:
args.extend(['--email=' + email])
if access_key:
args.extend(['--access-key=' + access_key])
if secret_key:
args.extend(['--secret-key=' + secret_key])
if realm:
args.extend(['--rgw-realm=' + realm])
if zonegroup:
args.extend(['--rgw-zonegroup=' + zonegroup])
if zone:
args.extend(['--rgw-zone=' + zone])
if system:
args.append('--system')
if admin:
args.append('--admin')
if caps:
caps_args = [f"{cap['type']}={cap['perm']}" for cap in caps]
args.extend(['--caps=' + ';'.join(caps_args)])
cmd = generate_radosgw_cmd(cluster=cluster,
args=args,
container_image=container_image)
return cmd
def caps_add(module, caps, container_image=None):
'''
Create a new user
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
realm = module.params.get('realm', None)
zonegroup = module.params.get('zonegroup', None)
zone = module.params.get('zone', None)
args = ['add', '--uid=' + name]
if realm:
args.extend(['--rgw-realm=' + realm])
if zonegroup:
args.extend(['--rgw-zonegroup=' + zonegroup])
if zone:
args.extend(['--rgw-zone=' + zone])
caps_args = [f"{cap['type']}={cap['perm']}" for cap in caps]
args.extend(['--caps=' + ';'.join(caps_args)])
cmd = generate_caps_cmd(cluster=cluster,
args=args,
container_image=container_image)
return cmd
def caps_rm(module, caps, container_image=None):
'''
Create a new user
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
realm = module.params.get('realm', None)
zonegroup = module.params.get('zonegroup', None)
zone = module.params.get('zone', None)
args = ['rm', '--uid=' + name]
if realm:
args.extend(['--rgw-realm=' + realm])
if zonegroup:
args.extend(['--rgw-zonegroup=' + zonegroup])
if zone:
args.extend(['--rgw-zone=' + zone])
caps_args = [f"{cap['type']}={cap['perm']}" for cap in caps]
args.extend(['--caps=' + ';'.join(caps_args)])
cmd = generate_caps_cmd(cluster=cluster,
args=args,
container_image=container_image)
return cmd
def modify_user(module, container_image=None):
'''
Modify an existing user
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
display_name = module.params.get('display_name')
email = module.params.get('email', None)
access_key = module.params.get('access_key', None)
secret_key = module.params.get('secret_key', None)
realm = module.params.get('realm', None)
zonegroup = module.params.get('zonegroup', None)
zone = module.params.get('zone', None)
system = module.params.get('system', False)
admin = module.params.get('admin', False)
args = ['modify', '--uid=' + name]
if display_name:
args.extend(['--display_name=' + display_name])
if email:
args.extend(['--email=' + email])
if access_key:
args.extend(['--access-key=' + access_key])
if secret_key:
args.extend(['--secret-key=' + secret_key])
if realm:
args.extend(['--rgw-realm=' + realm])
if zonegroup:
args.extend(['--rgw-zonegroup=' + zonegroup])
if zone:
args.extend(['--rgw-zone=' + zone])
if system:
args.append('--system')
if admin:
args.append('--admin')
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')
realm = module.params.get('realm', None)
zonegroup = module.params.get('zonegroup', None)
zone = module.params.get('zone', None)
args = ['info', '--uid=' + name, '--format=json']
if realm:
args.extend(['--rgw-realm=' + realm])
if zonegroup:
args.extend(['--rgw-zonegroup=' + zonegroup])
if zone:
args.extend(['--rgw-zone=' + zone])
cmd = generate_radosgw_cmd(cluster=cluster,
args=args,
container_image=container_image)
return cmd
def remove_user(module, container_image=None):
'''
Remove a user
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
realm = module.params.get('realm', None)
zonegroup = module.params.get('zonegroup', None)
zone = module.params.get('zone', None)
args = ['rm', '--uid=' + name]
if realm:
args.extend(['--rgw-realm=' + realm])
if zonegroup:
args.extend(['--rgw-zonegroup=' + zonegroup])
if zone:
args.extend(['--rgw-zone=' + zone])
cmd = generate_radosgw_cmd(cluster=cluster,
args=args,
container_image=container_image)
return cmd
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)
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', 'info'], default='present'), # noqa: E501
display_name=dict(type='str', required=False),
email=dict(type='str', required=False),
access_key=dict(type='str', required=False, no_log=True),
secret_key=dict(type='str', required=False, no_log=True),
realm=dict(type='str', required=False),
zonegroup=dict(type='str', required=False),
zone=dict(type='str', required=False),
system=dict(type='bool', required=False, default=False),
admin=dict(type='bool', required=False, default=False),
caps=dict(type='list', required=False),
)
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')
display_name = module.params.get('display_name')
if not display_name:
display_name = name
email = module.params.get('email')
access_key = module.params.get('access_key')
secret_key = module.params.get('secret_key')
system = module.params.get('system')
admin = module.params.get('admin')
caps = module.params.get('caps')
startd = datetime.datetime.now()
changed = False
# will return either the image name or None
container_image = is_containerized()
rc, cmd, out, err = exec_commands(module, get_user(module, container_image=container_image)) # noqa: E501
if state == "present":
if rc == 0:
user = json.loads(out)
current = {
'display_name': user['display_name'],
'system': user.get('system', False),
'admin': user.get('admin', False),
}
asked = {
'display_name': display_name,
'system': system,
'admin': admin,
}
if email:
current['email'] = user['email']
asked['email'] = email
if caps:
current['caps'] = user['caps']
asked['caps'] = caps
if access_key and secret_key:
asked['access_key'] = access_key
asked['secret_key'] = secret_key
for key in user['keys']:
if key['access_key'] == access_key and key['secret_key'] == secret_key: # noqa: E501
del asked['access_key']
del asked['secret_key']
break
changed = current != asked
if changed and not module.check_mode:
rc, cmd, out, err = exec_commands(module, modify_user(module, container_image=container_image))
if caps:
missing_caps = [cap for cap in asked['caps'] if cap not in current['caps']]
extra_caps = [cap for cap in current['caps'] if cap not in asked['caps']]
if extra_caps:
rc, cmd, out, err = exec_commands(module, caps_rm(module, extra_caps, container_image=container_image))
if missing_caps:
rc, cmd, out, err = exec_commands(module, caps_add(module, missing_caps, container_image=container_image))
else:
changed = True
if not module.check_mode:
rc, cmd, out, err = exec_commands(module, create_user(module, container_image=container_image)) # noqa: E501
else:
rc = 0
elif state == "absent":
if rc == 0:
if not module.check_mode:
rc, cmd, out, err = exec_commands(module, remove_user(module, container_image=container_image)) # noqa: E501
changed = True
else:
rc = 0
out = "User {} doesn't exist".format(name)
exit_module(module=module, out=out, rc=rc, cmd=cmd, err=err, startd=startd, changed=changed) # noqa: E501
def main():
run_module()
if __name__ == '__main__':
main()
+543
View File
@@ -0,0 +1,543 @@
# 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.
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 fatal
except ImportError:
from module_utils.ca_common import fatal
import datetime
import json
import os
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: radosgw_zone
short_description: Manage RADOS Gateway Zone
version_added: "2.8"
description:
- Manage RADOS Gateway zone(s) creation, deletion and updates.
options:
cluster:
description:
- The ceph cluster name.
required: false
default: ceph
name:
description:
- name of the RADOS Gateway zone.
required: true
state:
description:
If 'present' is used, the module creates a zone if it doesn't
exist or update it if it already exists.
If 'absent' is used, the module will simply delete the zone.
If 'info' is used, the module will return all details about the
existing zone (json formatted).
required: false
choices: ['present', 'absent', 'info']
default: present
realm:
description:
- name of the RADOS Gateway realm.
required: true
zonegroup:
description:
- name of the RADOS Gateway zonegroup.
required: true
endpoints:
description:
- endpoints of the RADOS Gateway zone.
required: false
default: []
access_key:
description:
- set the S3 access key of the user.
required: false
default: None
secret_key:
description:
- set the S3 secret key of the user.
required: false
default: None
default:
description:
- set the default flag on the zone.
required: false
default: false
master:
description:
- set the master flag on the zone.
required: false
default: false
author:
- Dimitri Savineau <dsavinea@redhat.com>
'''
EXAMPLES = '''
- name: create a RADOS Gateway default zone
radosgw_zone:
name: z1
realm: foo
zonegroup: bar
endpoints:
- http://192.168.1.10:8080
- http://192.168.1.11:8080
default: true
- name: get a RADOS Gateway zone information
radosgw_zone:
name: z1
state: info
- name: delete a RADOS Gateway zone
radosgw_zone:
name: z1
state: absent
'''
RETURN = '''# '''
def container_exec(binary, container_image, container_args=[]):
'''
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']
command_exec.extend(container_args)
command_exec.extend([
'-v', '/etc/ceph:/etc/ceph:z',
'-v', '/var/lib/ceph/:/var/lib/ceph/:z',
'-v', '/var/log/ceph/:/var/log/ceph/:z',
'--entrypoint=' + 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_radosgw_cmd(container_image=None, container_args=[]):
'''
Generate radosgw-admin prefix comaand
'''
if container_image:
cmd = container_exec('radosgw-admin', container_image, container_args)
else:
cmd = ['radosgw-admin']
return cmd
def generate_radosgw_cmd(cluster, args, container_image=None, container_args=[]):
'''
Generate 'radosgw' command line to execute
'''
cmd = pre_generate_radosgw_cmd(container_image=container_image, container_args=container_args) # noqa: E501
base_cmd = [
'--cluster',
cluster,
'zone'
]
cmd.extend(base_cmd + args)
return cmd
def exec_commands(module, cmd):
'''
Execute command(s)
'''
rc, out, err = module.run_command(cmd)
return rc, cmd, out, err
def create_zone(module, container_image=None):
'''
Create a new zone
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
realm = module.params.get('realm')
zonegroup = module.params.get('zonegroup')
endpoints = module.params.get('endpoints')
access_key = module.params.get('access_key')
secret_key = module.params.get('secret_key')
default = module.params.get('default')
master = module.params.get('master')
args = [
'create',
'--rgw-realm=' + realm,
'--rgw-zonegroup=' + zonegroup,
'--rgw-zone=' + name
]
if endpoints:
args.extend(['--endpoints=' + ','.join(endpoints)])
if access_key:
args.extend(['--access-key=' + access_key])
if secret_key:
args.extend(['--secret-key=' + secret_key])
if default:
args.append('--default')
if master:
args.append('--master')
cmd = generate_radosgw_cmd(cluster=cluster,
args=args,
container_image=container_image)
return cmd
def modify_zone(module, container_image=None):
'''
Modify a new zone
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
realm = module.params.get('realm')
zonegroup = module.params.get('zonegroup')
endpoints = module.params.get('endpoints')
access_key = module.params.get('access_key')
secret_key = module.params.get('secret_key')
default = module.params.get('default')
master = module.params.get('master')
args = [
'modify',
'--rgw-realm=' + realm,
'--rgw-zonegroup=' + zonegroup,
'--rgw-zone=' + name
]
if endpoints:
args.extend(['--endpoints=' + ','.join(endpoints)])
if access_key:
args.extend(['--access-key=' + access_key])
if secret_key:
args.extend(['--secret-key=' + secret_key])
if default:
args.append('--default')
if master:
args.append('--master')
cmd = generate_radosgw_cmd(cluster=cluster,
args=args,
container_image=container_image)
return cmd
def get_zone(module, container_image=None):
'''
Get existing zone
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
realm = module.params.get('realm')
zonegroup = module.params.get('zonegroup')
args = [
'get',
'--rgw-realm=' + realm,
'--rgw-zonegroup=' + zonegroup,
'--rgw-zone=' + name,
'--format=json'
]
cmd = generate_radosgw_cmd(cluster=cluster,
args=args,
container_image=container_image)
return cmd
def get_zonegroup(module, container_image=None):
'''
Get existing zonegroup
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
realm = module.params.get('realm')
zonegroup = module.params.get('zonegroup')
cmd = pre_generate_radosgw_cmd(container_image=container_image)
args = [
'--cluster',
cluster,
'zonegroup',
'get',
'--rgw-zone=' + name,
'--rgw-realm=' + realm,
'--rgw-zonegroup=' + zonegroup,
'--format=json'
]
cmd.extend(args)
return cmd
def get_realm(module, container_image=None):
'''
Get existing realm
'''
cluster = module.params.get('cluster')
realm = module.params.get('realm')
cmd = pre_generate_radosgw_cmd(container_image=container_image)
args = [
'--cluster',
cluster,
'realm',
'get',
'--rgw-realm=' + realm,
'--format=json'
]
cmd.extend(args)
return cmd
def remove_zone(module, container_image=None):
'''
Remove a zone
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
realm = module.params.get('realm')
zonegroup = module.params.get('zonegroup')
args = [
'delete',
'--rgw-realm=' + realm,
'--rgw-zonegroup=' + zonegroup,
'--rgw-zone=' + name
]
cmd = generate_radosgw_cmd(cluster=cluster,
args=args,
container_image=container_image)
return cmd
def set_zone(module, container_image=None):
'''
Set a zone
'''
cluster = module.params.get('cluster')
realm = module.params.get('realm')
zone_doc = module.params.get('zone_doc')
# store the zone_doc in a file
filename = module.tmpdir + 'zone_doc.json'
with open(filename, 'w') as f:
json.dump(zone_doc, f)
container_args = [
'-v', filename + ':' + filename + ':ro'
]
args = [
'set',
'--rgw-realm=' + realm,
'--infile=' + filename,
]
cmd = generate_radosgw_cmd(cluster=cluster,
args=args,
container_image=container_image,
container_args=container_args)
return cmd
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)
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', 'info', 'set'], default='present'), # noqa: E501
realm=dict(type='str', require=True),
zonegroup=dict(type='str', require=True),
endpoints=dict(type='list', require=False, default=[]),
access_key=dict(type='str', required=False, no_log=True),
secret_key=dict(type='str', required=False, no_log=True),
default=dict(type='bool', required=False, default=False),
master=dict(type='bool', required=False, default=False),
zone_doc=dict(type='dict', required=False, default={})
)
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')
endpoints = module.params.get('endpoints')
access_key = module.params.get('access_key')
secret_key = module.params.get('secret_key')
if module.check_mode:
module.exit_json(
changed=False,
stdout='',
stderr='',
rc=0,
start='',
end='',
delta='',
)
startd = datetime.datetime.now()
changed = False
# will return either the image name or None
container_image = is_containerized()
rc, cmd, out, err = exec_commands(module, get_zone(module, container_image=container_image)) # noqa: E501
if state == "set":
zone = json.loads(out) if rc == 0 else {}
zone_doc = module.params.get('zone_doc')
if not zone_doc:
fatal("zone_doc is required when state is set", module)
changed = zone_doc != zone
if changed:
rc, cmd, out, err = exec_commands(module, set_zone(module, container_image=container_image))
if state == "present":
if rc == 0:
zone = json.loads(out)
_rc, _cmd, _out, _err = exec_commands(module, get_realm(module, container_image=container_image)) # noqa: E501
if _rc != 0:
fatal(_err, module)
realm = json.loads(_out)
_rc, _cmd, _out, _err = exec_commands(module, get_zonegroup(module, container_image=container_image)) # noqa: E501
if _rc != 0:
fatal(_err, module)
zonegroup = json.loads(_out)
if not access_key:
access_key = ''
if not secret_key:
secret_key = ''
current = {
'endpoints': next(zone['endpoints'] for zone in zonegroup['zones'] if zone['name'] == name), # noqa: E501
'access_key': zone['system_key']['access_key'],
'secret_key': zone['system_key']['secret_key'],
'realm_id': zone['realm_id']
}
asked = {
'endpoints': endpoints,
'access_key': access_key,
'secret_key': secret_key,
'realm_id': realm['id']
}
if current != asked:
rc, cmd, out, err = exec_commands(module, modify_zone(module, container_image=container_image)) # noqa: E501
changed = True
else:
rc, cmd, out, err = exec_commands(module, create_zone(module, container_image=container_image)) # noqa: E501
changed = True
elif state == "absent":
if rc == 0:
rc, cmd, out, err = exec_commands(module, remove_zone(module, container_image=container_image)) # noqa: E501
changed = True
else:
rc = 0
out = "Zone {} doesn't exist".format(name)
exit_module(module=module, out=out, rc=rc, cmd=cmd, err=err, startd=startd, changed=changed) # noqa: E501
def main():
run_module()
if __name__ == '__main__':
main()
+397
View File
@@ -0,0 +1,397 @@
# 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.
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 fatal
except ImportError:
from module_utils.ca_common import fatal
import datetime
import json
import os
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: radosgw_zonegroup
short_description: Manage RADOS Gateway Zonegroup
version_added: "2.8"
description:
- Manage RADOS Gateway zonegroup(s) creation, deletion and updates.
options:
cluster:
description:
- The ceph cluster name.
required: false
default: ceph
name:
description:
- name of the RADOS Gateway zonegroup.
required: true
state:
description:
If 'present' is used, the module creates a zonegroup if it doesn't
exist or update it if it already exists.
If 'absent' is used, the module will simply delete the zonegroup.
If 'info' is used, the module will return all details about the
existing zonegroup (json formatted).
required: false
choices: ['present', 'absent', 'info']
default: present
realm:
description:
- name of the RADOS Gateway realm.
required: true
endpoints:
description:
- endpoints of the RADOS Gateway zonegroup.
required: false
default: []
default:
description:
- set the default flag on the zonegroup.
required: false
default: false
master:
description:
- set the master flag on the zonegroup.
required: false
default: false
author:
- Dimitri Savineau <dsavinea@redhat.com>
'''
EXAMPLES = '''
- name: create a RADOS Gateway default zonegroup
radosgw_zonegroup:
name: foo
realm: bar
endpoints:
- http://192.168.1.10:8080
- http://192.168.1.11:8080
default: true
- name: get a RADOS Gateway zonegroup information
radosgw_zonegroup:
name: foo
realm: bar
state: info
- name: delete a RADOS Gateway zonegroup
radosgw_zonegroup:
name: foo
realm: bar
state: absent
'''
RETURN = '''# '''
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=' + 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_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,
'zonegroup'
]
cmd.extend(base_cmd + args)
return cmd
def exec_commands(module, cmd):
'''
Execute command(s)
'''
rc, out, err = module.run_command(cmd)
return rc, cmd, out, err
def create_zonegroup(module, container_image=None):
'''
Create a new zonegroup
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
realm = module.params.get('realm')
endpoints = module.params.get('endpoints')
default = module.params.get('default')
master = module.params.get('master')
args = ['create', '--rgw-realm=' + realm, '--rgw-zonegroup=' + name]
if endpoints:
args.extend(['--endpoints=' + ','.join(endpoints)])
if default:
args.append('--default')
if master:
args.append('--master')
cmd = generate_radosgw_cmd(cluster=cluster,
args=args,
container_image=container_image)
return cmd
def modify_zonegroup(module, container_image=None):
'''
Modify a new zonegroup
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
realm = module.params.get('realm')
endpoints = module.params.get('endpoints')
default = module.params.get('default')
master = module.params.get('master')
args = ['modify', '--rgw-realm=' + realm, '--rgw-zonegroup=' + name]
if endpoints:
args.extend(['--endpoints=' + ','.join(endpoints)])
if default:
args.append('--default')
if master:
args.append('--master')
cmd = generate_radosgw_cmd(cluster=cluster,
args=args,
container_image=container_image)
return cmd
def get_zonegroup(module, container_image=None):
'''
Get existing zonegroup
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
realm = module.params.get('realm')
args = [
'get',
'--rgw-realm=' + realm,
'--rgw-zonegroup=' + name,
'--format=json'
]
cmd = generate_radosgw_cmd(cluster=cluster,
args=args,
container_image=container_image)
return cmd
def get_realm(module, container_image=None):
'''
Get existing realm
'''
cluster = module.params.get('cluster')
realm = module.params.get('realm')
cmd = pre_generate_radosgw_cmd(container_image=container_image)
args = [
'--cluster',
cluster,
'realm',
'get',
'--rgw-realm=' + realm,
'--format=json'
]
cmd.extend(args)
return cmd
def remove_zonegroup(module, container_image=None):
'''
Remove a zonegroup
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
realm = module.params.get('realm')
args = ['delete', '--rgw-realm=' + realm, '--rgw-zonegroup=' + name]
cmd = generate_radosgw_cmd(cluster=cluster,
args=args,
container_image=container_image)
return cmd
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)
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', 'info'], default='present'), # noqa: E501
realm=dict(type='str', require=True),
endpoints=dict(type='list', require=False, default=[]),
default=dict(type='bool', required=False, default=False),
master=dict(type='bool', required=False, default=False),
)
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')
endpoints = module.params.get('endpoints')
master = module.params.get('master')
startd = datetime.datetime.now()
changed = False
# will return either the image name or None
container_image = is_containerized()
rc, cmd, out, err = exec_commands(module, get_zonegroup(module, container_image=container_image)) # noqa: E501
if state == "present":
if rc == 0:
zonegroup = json.loads(out)
_rc, _cmd, _out, _err = exec_commands(module, get_realm(module, container_image=container_image)) # noqa: E501
if _rc != 0:
fatal(_err, module)
realm = json.loads(_out)
current = {
'endpoints': zonegroup['endpoints'],
'master': zonegroup.get('is_master', False),
'realm_id': zonegroup['realm_id']
}
asked = {
'endpoints': endpoints,
'master': master,
'realm_id': realm['id']
}
changed = current != asked
if changed and not module.check_mode:
rc, cmd, out, err = exec_commands(module, modify_zonegroup(module, container_image=container_image)) # noqa: E501
else:
if not module.check_mode:
rc, cmd, out, err = exec_commands(module, create_zonegroup(module, container_image=container_image)) # noqa: E501
changed = True
elif state == "absent":
if rc == 0:
if not module.check_mode:
rc, cmd, out, err = exec_commands(module, remove_zonegroup(module, container_image=container_image)) # noqa: E501
changed = True
else:
rc = 0
out = "Zonegroup {} doesn't exist".format(name)
exit_module(module=module, out=out, rc=rc, cmd=cmd, err=err, startd=startd, changed=changed) # noqa: E501
def main():
run_module()
if __name__ == '__main__':
main()