deep_scrub_choked
This commit is contained in:
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.
Binary file not shown.
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user