summaryrefslogtreecommitdiff
path: root/test/support
diff options
context:
space:
mode:
authorMartin Krizek <martin.krizek@gmail.com>2021-10-15 18:48:14 +0200
committerGitHub <noreply@github.com>2021-10-15 09:48:14 -0700
commit80f4b67f436876eb396ccf265af9d96b95716a59 (patch)
tree50116c443c00d0ec0bb29be95dff0ae1e54ef01e /test/support
parent817e1045bb82cc44332329c3b87e35ddcb1b9862 (diff)
downloadansible-80f4b67f436876eb396ccf265af9d96b95716a59.tar.gz
Removed unused test/support modules (#76048)
Diffstat (limited to 'test/support')
-rw-r--r--test/support/integration/plugins/modules/aws_s3.py925
-rw-r--r--test/support/integration/plugins/modules/locale_gen.py237
-rw-r--r--test/support/integration/plugins/modules/lvg.py295
-rw-r--r--test/support/integration/plugins/modules/mongodb_parameter.py223
-rw-r--r--test/support/integration/plugins/modules/mongodb_user.py474
-rw-r--r--test/support/integration/plugins/modules/pids.py89
-rw-r--r--test/support/integration/plugins/modules/postgresql_db.py657
-rw-r--r--test/support/integration/plugins/modules/postgresql_privs.py1097
-rw-r--r--test/support/integration/plugins/modules/postgresql_query.py364
-rw-r--r--test/support/integration/plugins/modules/postgresql_set.py434
-rw-r--r--test/support/integration/plugins/modules/postgresql_table.py601
-rw-r--r--test/support/integration/plugins/modules/postgresql_user.py927
-rw-r--r--test/support/integration/plugins/modules/rabbitmq_plugin.py180
-rw-r--r--test/support/integration/plugins/modules/rabbitmq_queue.py257
-rw-r--r--test/support/integration/plugins/modules/s3_bucket.py740
-rw-r--r--test/support/integration/plugins/modules/selogin.py260
-rw-r--r--test/support/integration/plugins/modules/x509_crl.py783
-rw-r--r--test/support/integration/plugins/modules/x509_crl_info.py281
18 files changed, 0 insertions, 8824 deletions
diff --git a/test/support/integration/plugins/modules/aws_s3.py b/test/support/integration/plugins/modules/aws_s3.py
deleted file mode 100644
index 54874f05ce..0000000000
--- a/test/support/integration/plugins/modules/aws_s3.py
+++ /dev/null
@@ -1,925 +0,0 @@
-#!/usr/bin/python
-# This file is part of Ansible
-# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-
-from __future__ import (absolute_import, division, print_function)
-__metaclass__ = type
-
-ANSIBLE_METADATA = {'metadata_version': '1.1',
- 'status': ['stableinterface'],
- 'supported_by': 'core'}
-
-
-DOCUMENTATION = '''
----
-module: aws_s3
-short_description: manage objects in S3.
-description:
- - This module allows the user to manage S3 buckets and the objects within them. Includes support for creating and
- deleting both objects and buckets, retrieving objects as files or strings and generating download links.
- This module has a dependency on boto3 and botocore.
-notes:
- - In 2.4, this module has been renamed from C(s3) into M(aws_s3).
-version_added: "1.1"
-options:
- bucket:
- description:
- - Bucket name.
- required: true
- type: str
- dest:
- description:
- - The destination file path when downloading an object/key with a GET operation.
- version_added: "1.3"
- type: path
- encrypt:
- description:
- - When set for PUT mode, asks for server-side encryption.
- default: true
- version_added: "2.0"
- type: bool
- encryption_mode:
- description:
- - What encryption mode to use if I(encrypt=true).
- default: AES256
- choices:
- - AES256
- - aws:kms
- version_added: "2.7"
- type: str
- expiry:
- description:
- - Time limit (in seconds) for the URL generated and returned by S3/Walrus when performing a I(mode=put) or I(mode=geturl) operation.
- default: 600
- aliases: ['expiration']
- type: int
- headers:
- description:
- - Custom headers for PUT operation, as a dictionary of 'key=value' and 'key=value,key=value'.
- version_added: "2.0"
- type: dict
- marker:
- description:
- - Specifies the key to start with when using list mode. Object keys are returned in alphabetical order, starting with key after the marker in order.
- version_added: "2.0"
- type: str
- max_keys:
- description:
- - Max number of results to return in list mode, set this if you want to retrieve fewer than the default 1000 keys.
- default: 1000
- version_added: "2.0"
- type: int
- metadata:
- description:
- - Metadata for PUT operation, as a dictionary of 'key=value' and 'key=value,key=value'.
- version_added: "1.6"
- type: dict
- mode:
- description:
- - Switches the module behaviour between put (upload), get (download), geturl (return download url, Ansible 1.3+),
- getstr (download object as string (1.3+)), list (list keys, Ansible 2.0+), create (bucket), delete (bucket),
- and delobj (delete object, Ansible 2.0+).
- required: true
- choices: ['get', 'put', 'delete', 'create', 'geturl', 'getstr', 'delobj', 'list']
- type: str
- object:
- description:
- - Keyname of the object inside the bucket. Can be used to create "virtual directories", see examples.
- type: str
- permission:
- description:
- - This option lets the user set the canned permissions on the object/bucket that are created.
- The permissions that can be set are C(private), C(public-read), C(public-read-write), C(authenticated-read) for a bucket or
- C(private), C(public-read), C(public-read-write), C(aws-exec-read), C(authenticated-read), C(bucket-owner-read),
- C(bucket-owner-full-control) for an object. Multiple permissions can be specified as a list.
- default: ['private']
- version_added: "2.0"
- type: list
- elements: str
- prefix:
- description:
- - Limits the response to keys that begin with the specified prefix for list mode.
- default: ""
- version_added: "2.0"
- type: str
- version:
- description:
- - Version ID of the object inside the bucket. Can be used to get a specific version of a file if versioning is enabled in the target bucket.
- version_added: "2.0"
- type: str
- overwrite:
- description:
- - Force overwrite either locally on the filesystem or remotely with the object/key. Used with PUT and GET operations.
- Boolean or one of [always, never, different], true is equal to 'always' and false is equal to 'never', new in 2.0.
- When this is set to 'different', the md5 sum of the local file is compared with the 'ETag' of the object/key in S3.
- The ETag may or may not be an MD5 digest of the object data. See the ETag response header here
- U(https://docs.aws.amazon.com/AmazonS3/latest/API/RESTCommonResponseHeaders.html)
- default: 'always'
- aliases: ['force']
- version_added: "1.2"
- type: str
- retries:
- description:
- - On recoverable failure, how many times to retry before actually failing.
- default: 0
- version_added: "2.0"
- type: int
- aliases: ['retry']
- s3_url:
- description:
- - S3 URL endpoint for usage with Ceph, Eucalyptus and fakes3 etc. Otherwise assumes AWS.
- aliases: [ S3_URL ]
- type: str
- dualstack:
- description:
- - Enables Amazon S3 Dual-Stack Endpoints, allowing S3 communications using both IPv4 and IPv6.
- - Requires at least botocore version 1.4.45.
- type: bool
- default: false
- version_added: "2.7"
- rgw:
- description:
- - Enable Ceph RGW S3 support. This option requires an explicit url via I(s3_url).
- default: false
- version_added: "2.2"
- type: bool
- src:
- description:
- - The source file path when performing a PUT operation.
- version_added: "1.3"
- type: str
- ignore_nonexistent_bucket:
- description:
- - "Overrides initial bucket lookups in case bucket or iam policies are restrictive. Example: a user may have the
- GetObject permission but no other permissions. In this case using the option mode: get will fail without specifying
- I(ignore_nonexistent_bucket=true)."
- version_added: "2.3"
- type: bool
- encryption_kms_key_id:
- description:
- - KMS key id to use when encrypting objects using I(encrypting=aws:kms). Ignored if I(encryption) is not C(aws:kms)
- version_added: "2.7"
- type: str
-requirements: [ "boto3", "botocore" ]
-author:
- - "Lester Wade (@lwade)"
- - "Sloane Hertel (@s-hertel)"
-extends_documentation_fragment:
- - aws
- - ec2
-'''
-
-EXAMPLES = '''
-- name: Simple PUT operation
- aws_s3:
- bucket: mybucket
- object: /my/desired/key.txt
- src: /usr/local/myfile.txt
- mode: put
-
-- name: Simple PUT operation in Ceph RGW S3
- aws_s3:
- bucket: mybucket
- object: /my/desired/key.txt
- src: /usr/local/myfile.txt
- mode: put
- rgw: true
- s3_url: "http://localhost:8000"
-
-- name: Simple GET operation
- aws_s3:
- bucket: mybucket
- object: /my/desired/key.txt
- dest: /usr/local/myfile.txt
- mode: get
-
-- name: Get a specific version of an object.
- aws_s3:
- bucket: mybucket
- object: /my/desired/key.txt
- version: 48c9ee5131af7a716edc22df9772aa6f
- dest: /usr/local/myfile.txt
- mode: get
-
-- name: PUT/upload with metadata
- aws_s3:
- bucket: mybucket
- object: /my/desired/key.txt
- src: /usr/local/myfile.txt
- mode: put
- metadata: 'Content-Encoding=gzip,Cache-Control=no-cache'
-
-- name: PUT/upload with custom headers
- aws_s3:
- bucket: mybucket
- object: /my/desired/key.txt
- src: /usr/local/myfile.txt
- mode: put
- headers: 'x-amz-grant-full-control=emailAddress=owner@example.com'
-
-- name: List keys simple
- aws_s3:
- bucket: mybucket
- mode: list
-
-- name: List keys all options
- aws_s3:
- bucket: mybucket
- mode: list
- prefix: /my/desired/
- marker: /my/desired/0023.txt
- max_keys: 472
-
-- name: Create an empty bucket
- aws_s3:
- bucket: mybucket
- mode: create
- permission: public-read
-
-- name: Create a bucket with key as directory, in the EU region
- aws_s3:
- bucket: mybucket
- object: /my/directory/path
- mode: create
- region: eu-west-1
-
-- name: Delete a bucket and all contents
- aws_s3:
- bucket: mybucket
- mode: delete
-
-- name: GET an object but don't download if the file checksums match. New in 2.0
- aws_s3:
- bucket: mybucket
- object: /my/desired/key.txt
- dest: /usr/local/myfile.txt
- mode: get
- overwrite: different
-
-- name: Delete an object from a bucket
- aws_s3:
- bucket: mybucket
- object: /my/desired/key.txt
- mode: delobj
-'''
-
-RETURN = '''
-msg:
- description: Message indicating the status of the operation.
- returned: always
- type: str
- sample: PUT operation complete
-url:
- description: URL of the object.
- returned: (for put and geturl operations)
- type: str
- sample: https://my-bucket.s3.amazonaws.com/my-key.txt?AWSAccessKeyId=<access-key>&Expires=1506888865&Signature=<signature>
-expiry:
- description: Number of seconds the presigned url is valid for.
- returned: (for geturl operation)
- type: int
- sample: 600
-contents:
- description: Contents of the object as string.
- returned: (for getstr operation)
- type: str
- sample: "Hello, world!"
-s3_keys:
- description: List of object keys.
- returned: (for list operation)
- type: list
- elements: str
- sample:
- - prefix1/
- - prefix1/key1
- - prefix1/key2
-'''
-
-import mimetypes
-import os
-from ansible.module_utils.six.moves.urllib.parse import urlparse
-from ssl import SSLError
-from ansible.module_utils.basic import to_text, to_native
-from ansible.module_utils.aws.core import AnsibleAWSModule
-from ansible.module_utils.aws.s3 import calculate_etag, HAS_MD5
-from ansible.module_utils.ec2 import get_aws_connection_info, boto3_conn
-
-try:
- import botocore
-except ImportError:
- pass # will be detected by imported AnsibleAWSModule
-
-IGNORE_S3_DROP_IN_EXCEPTIONS = ['XNotImplemented', 'NotImplemented']
-
-
-class Sigv4Required(Exception):
- pass
-
-
-def key_check(module, s3, bucket, obj, version=None, validate=True):
- exists = True
- try:
- if version:
- s3.head_object(Bucket=bucket, Key=obj, VersionId=version)
- else:
- s3.head_object(Bucket=bucket, Key=obj)
- except botocore.exceptions.ClientError as e:
- # if a client error is thrown, check if it's a 404 error
- # if it's a 404 error, then the object does not exist
- error_code = int(e.response['Error']['Code'])
- if error_code == 404:
- exists = False
- elif error_code == 403 and validate is False:
- pass
- else:
- module.fail_json_aws(e, msg="Failed while looking up object (during key check) %s." % obj)
- except botocore.exceptions.BotoCoreError as e:
- module.fail_json_aws(e, msg="Failed while looking up object (during key check) %s." % obj)
- return exists
-
-
-def etag_compare(module, local_file, s3, bucket, obj, version=None):
- s3_etag = get_etag(s3, bucket, obj, version=version)
- local_etag = calculate_etag(module, local_file, s3_etag, s3, bucket, obj, version)
-
- return s3_etag == local_etag
-
-
-def get_etag(s3, bucket, obj, version=None):
- if version:
- key_check = s3.head_object(Bucket=bucket, Key=obj, VersionId=version)
- else:
- key_check = s3.head_object(Bucket=bucket, Key=obj)
- if not key_check:
- return None
- return key_check['ETag']
-
-
-def bucket_check(module, s3, bucket, validate=True):
- exists = True
- try:
- s3.head_bucket(Bucket=bucket)
- except botocore.exceptions.ClientError as e:
- # If a client error is thrown, then check that it was a 404 error.
- # If it was a 404 error, then the bucket does not exist.
- error_code = int(e.response['Error']['Code'])
- if error_code == 404:
- exists = False
- elif error_code == 403 and validate is False:
- pass
- else:
- module.fail_json_aws(e, msg="Failed while looking up bucket (during bucket_check) %s." % bucket)
- except botocore.exceptions.EndpointConnectionError as e:
- module.fail_json_aws(e, msg="Invalid endpoint provided")
- except botocore.exceptions.BotoCoreError as e:
- module.fail_json_aws(e, msg="Failed while looking up bucket (during bucket_check) %s." % bucket)
- return exists
-
-
-def create_bucket(module, s3, bucket, location=None):
- if module.check_mode:
- module.exit_json(msg="CREATE operation skipped - running in check mode", changed=True)
- configuration = {}
- if location not in ('us-east-1', None):
- configuration['LocationConstraint'] = location
- try:
- if len(configuration) > 0:
- s3.create_bucket(Bucket=bucket, CreateBucketConfiguration=configuration)
- else:
- s3.create_bucket(Bucket=bucket)
- if module.params.get('permission'):
- # Wait for the bucket to exist before setting ACLs
- s3.get_waiter('bucket_exists').wait(Bucket=bucket)
- for acl in module.params.get('permission'):
- s3.put_bucket_acl(ACL=acl, Bucket=bucket)
- except botocore.exceptions.ClientError as e:
- if e.response['Error']['Code'] in IGNORE_S3_DROP_IN_EXCEPTIONS:
- module.warn("PutBucketAcl is not implemented by your storage provider. Set the permission parameters to the empty list to avoid this warning")
- else:
- module.fail_json_aws(e, msg="Failed while creating bucket or setting acl (check that you have CreateBucket and PutBucketAcl permission).")
- except botocore.exceptions.BotoCoreError as e:
- module.fail_json_aws(e, msg="Failed while creating bucket or setting acl (check that you have CreateBucket and PutBucketAcl permission).")
-
- if bucket:
- return True
-
-
-def paginated_list(s3, **pagination_params):
- pg = s3.get_paginator('list_objects_v2')
- for page in pg.paginate(**pagination_params):
- yield [data['Key'] for data in page.get('Contents', [])]
-
-
-def paginated_versioned_list_with_fallback(s3, **pagination_params):
- try:
- versioned_pg = s3.get_paginator('list_object_versions')
- for page in versioned_pg.paginate(**pagination_params):
- delete_markers = [{'Key': data['Key'], 'VersionId': data['VersionId']} for data in page.get('DeleteMarkers', [])]
- current_objects = [{'Key': data['Key'], 'VersionId': data['VersionId']} for data in page.get('Versions', [])]
- yield delete_markers + current_objects
- except botocore.exceptions.ClientError as e:
- if to_text(e.response['Error']['Code']) in IGNORE_S3_DROP_IN_EXCEPTIONS + ['AccessDenied']:
- for page in paginated_list(s3, **pagination_params):
- yield [{'Key': data['Key']} for data in page]
-
-
-def list_keys(module, s3, bucket, prefix, marker, max_keys):
- pagination_params = {'Bucket': bucket}
- for param_name, param_value in (('Prefix', prefix), ('StartAfter', marker), ('MaxKeys', max_keys)):
- pagination_params[param_name] = param_value
- try:
- keys = sum(paginated_list(s3, **pagination_params), [])
- module.exit_json(msg="LIST operation complete", s3_keys=keys)
- except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
- module.fail_json_aws(e, msg="Failed while listing the keys in the bucket {0}".format(bucket))
-
-
-def delete_bucket(module, s3, bucket):
- if module.check_mode:
- module.exit_json(msg="DELETE operation skipped - running in check mode", changed=True)
- try:
- exists = bucket_check(module, s3, bucket)
- if exists is False:
- return False
- # if there are contents then we need to delete them before we can delete the bucket
- for keys in paginated_versioned_list_with_fallback(s3, Bucket=bucket):
- if keys:
- s3.delete_objects(Bucket=bucket, Delete={'Objects': keys})
- s3.delete_bucket(Bucket=bucket)
- return True
- except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
- module.fail_json_aws(e, msg="Failed while deleting bucket %s." % bucket)
-
-
-def delete_key(module, s3, bucket, obj):
- if module.check_mode:
- module.exit_json(msg="DELETE operation skipped - running in check mode", changed=True)
- try:
- s3.delete_object(Bucket=bucket, Key=obj)
- module.exit_json(msg="Object deleted from bucket %s." % (bucket), changed=True)
- except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
- module.fail_json_aws(e, msg="Failed while trying to delete %s." % obj)
-
-
-def create_dirkey(module, s3, bucket, obj, encrypt):
- if module.check_mode:
- module.exit_json(msg="PUT operation skipped - running in check mode", changed=True)
- try:
- params = {'Bucket': bucket, 'Key': obj, 'Body': b''}
- if encrypt:
- params['ServerSideEncryption'] = module.params['encryption_mode']
- if module.params['encryption_kms_key_id'] and module.params['encryption_mode'] == 'aws:kms':
- params['SSEKMSKeyId'] = module.params['encryption_kms_key_id']
-
- s3.put_object(**params)
- for acl in module.params.get('permission'):
- s3.put_object_acl(ACL=acl, Bucket=bucket, Key=obj)
- except botocore.exceptions.ClientError as e:
- if e.response['Error']['Code'] in IGNORE_S3_DROP_IN_EXCEPTIONS:
- module.warn("PutObjectAcl is not implemented by your storage provider. Set the permissions parameters to the empty list to avoid this warning")
- else:
- module.fail_json_aws(e, msg="Failed while creating object %s." % obj)
- except botocore.exceptions.BotoCoreError as e:
- module.fail_json_aws(e, msg="Failed while creating object %s." % obj)
- module.exit_json(msg="Virtual directory %s created in bucket %s" % (obj, bucket), changed=True)
-
-
-def path_check(path):
- if os.path.exists(path):
- return True
- else:
- return False
-
-
-def option_in_extra_args(option):
- temp_option = option.replace('-', '').lower()
-
- allowed_extra_args = {'acl': 'ACL', 'cachecontrol': 'CacheControl', 'contentdisposition': 'ContentDisposition',
- 'contentencoding': 'ContentEncoding', 'contentlanguage': 'ContentLanguage',
- 'contenttype': 'ContentType', 'expires': 'Expires', 'grantfullcontrol': 'GrantFullControl',
- 'grantread': 'GrantRead', 'grantreadacp': 'GrantReadACP', 'grantwriteacp': 'GrantWriteACP',
- 'metadata': 'Metadata', 'requestpayer': 'RequestPayer', 'serversideencryption': 'ServerSideEncryption',
- 'storageclass': 'StorageClass', 'ssecustomeralgorithm': 'SSECustomerAlgorithm', 'ssecustomerkey': 'SSECustomerKey',
- 'ssecustomerkeymd5': 'SSECustomerKeyMD5', 'ssekmskeyid': 'SSEKMSKeyId', 'websiteredirectlocation': 'WebsiteRedirectLocation'}
-
- if temp_option in allowed_extra_args:
- return allowed_extra_args[temp_option]
-
-
-def upload_s3file(module, s3, bucket, obj, src, expiry, metadata, encrypt, headers):
- if module.check_mode:
- module.exit_json(msg="PUT operation skipped - running in check mode", changed=True)
- try:
- extra = {}
- if encrypt:
- extra['ServerSideEncryption'] = module.params['encryption_mode']
- if module.params['encryption_kms_key_id'] and module.params['encryption_mode'] == 'aws:kms':
- extra['SSEKMSKeyId'] = module.params['encryption_kms_key_id']
- if metadata:
- extra['Metadata'] = {}
-
- # determine object metadata and extra arguments
- for option in metadata:
- extra_args_option = option_in_extra_args(option)
- if extra_args_option is not None:
- extra[extra_args_option] = metadata[option]
- else:
- extra['Metadata'][option] = metadata[option]
-
- if 'ContentType' not in extra:
- content_type = mimetypes.guess_type(src)[0]
- if content_type is None:
- # s3 default content type
- content_type = 'binary/octet-stream'
- extra['ContentType'] = content_type
-
- s3.upload_file(Filename=src, Bucket=bucket, Key=obj, ExtraArgs=extra)
- except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
- module.fail_json_aws(e, msg="Unable to complete PUT operation.")
- try:
- for acl in module.params.get('permission'):
- s3.put_object_acl(ACL=acl, Bucket=bucket, Key=obj)
- except botocore.exceptions.ClientError as e:
- if e.response['Error']['Code'] in IGNORE_S3_DROP_IN_EXCEPTIONS:
- module.warn("PutObjectAcl is not implemented by your storage provider. Set the permission parameters to the empty list to avoid this warning")
- else:
- module.fail_json_aws(e, msg="Unable to set object ACL")
- except botocore.exceptions.BotoCoreError as e:
- module.fail_json_aws(e, msg="Unable to set object ACL")
- try:
- url = s3.generate_presigned_url(ClientMethod='put_object',
- Params={'Bucket': bucket, 'Key': obj},
- ExpiresIn=expiry)
- except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
- module.fail_json_aws(e, msg="Unable to generate presigned URL")
- module.exit_json(msg="PUT operation complete", url=url, changed=True)
-
-
-def download_s3file(module, s3, bucket, obj, dest, retries, version=None):
- if module.check_mode:
- module.exit_json(msg="GET operation skipped - running in check mode", changed=True)
- # retries is the number of loops; range/xrange needs to be one
- # more to get that count of loops.
- try:
- if version:
- key = s3.get_object(Bucket=bucket, Key=obj, VersionId=version)
- else:
- key = s3.get_object(Bucket=bucket, Key=obj)
- except botocore.exceptions.ClientError as e:
- if e.response['Error']['Code'] == 'InvalidArgument' and 'require AWS Signature Version 4' in to_text(e):
- raise Sigv4Required()
- elif e.response['Error']['Code'] not in ("403", "404"):
- # AccessDenied errors may be triggered if 1) file does not exist or 2) file exists but
- # user does not have the s3:GetObject permission. 404 errors are handled by download_file().
- module.fail_json_aws(e, msg="Could not find the key %s." % obj)
- except botocore.exceptions.BotoCoreError as e:
- module.fail_json_aws(e, msg="Could not find the key %s." % obj)
-
- optional_kwargs = {'ExtraArgs': {'VersionId': version}} if version else {}
- for x in range(0, retries + 1):
- try:
- s3.download_file(bucket, obj, dest, **optional_kwargs)
- module.exit_json(msg="GET operation complete", changed=True)
- except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
- # actually fail on last pass through the loop.
- if x >= retries:
- module.fail_json_aws(e, msg="Failed while downloading %s." % obj)
- # otherwise, try again, this may be a transient timeout.
- except SSLError as e: # will ClientError catch SSLError?
- # actually fail on last pass through the loop.
- if x >= retries:
- module.fail_json_aws(e, msg="s3 download failed")
- # otherwise, try again, this may be a transient timeout.
-
-
-def download_s3str(module, s3, bucket, obj, version=None, validate=True):
- if module.check_mode:
- module.exit_json(msg="GET operation skipped - running in check mode", changed=True)
- try:
- if version:
- contents = to_native(s3.get_object(Bucket=bucket, Key=obj, VersionId=version)["Body"].read())
- else:
- contents = to_native(s3.get_object(Bucket=bucket, Key=obj)["Body"].read())
- module.exit_json(msg="GET operation complete", contents=contents, changed=True)
- except botocore.exceptions.ClientError as e:
- if e.response['Error']['Code'] == 'InvalidArgument' and 'require AWS Signature Version 4' in to_text(e):
- raise Sigv4Required()
- else:
- module.fail_json_aws(e, msg="Failed while getting contents of object %s as a string." % obj)
- except botocore.exceptions.BotoCoreError as e:
- module.fail_json_aws(e, msg="Failed while getting contents of object %s as a string." % obj)
-
-
-def get_download_url(module, s3, bucket, obj, expiry, changed=True):
- try:
- url = s3.generate_presigned_url(ClientMethod='get_object',
- Params={'Bucket': bucket, 'Key': obj},
- ExpiresIn=expiry)
- module.exit_json(msg="Download url:", url=url, expiry=expiry, changed=changed)
- except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
- module.fail_json_aws(e, msg="Failed while getting download url.")
-
-
-def is_fakes3(s3_url):
- """ Return True if s3_url has scheme fakes3:// """
- if s3_url is not None:
- return urlparse(s3_url).scheme in ('fakes3', 'fakes3s')
- else:
- return False
-
-
-def get_s3_connection(module, aws_connect_kwargs, location, rgw, s3_url, sig_4=False):
- if s3_url and rgw: # TODO - test this
- rgw = urlparse(s3_url)
- params = dict(module=module, conn_type='client', resource='s3', use_ssl=rgw.scheme == 'https', region=location, endpoint=s3_url, **aws_connect_kwargs)
- elif is_fakes3(s3_url):
- fakes3 = urlparse(s3_url)
- port = fakes3.port
- if fakes3.scheme == 'fakes3s':
- protocol = "https"
- if port is None:
- port = 443
- else:
- protocol = "http"
- if port is None:
- port = 80
- params = dict(module=module, conn_type='client', resource='s3', region=location,
- endpoint="%s://%s:%s" % (protocol, fakes3.hostname, to_text(port)),
- use_ssl=fakes3.scheme == 'fakes3s', **aws_connect_kwargs)
- else:
- params = dict(module=module, conn_type='client', resource='s3', region=location, endpoint=s3_url, **aws_connect_kwargs)
- if module.params['mode'] == 'put' and module.params['encryption_mode'] == 'aws:kms':
- params['config'] = botocore.client.Config(signature_version='s3v4')
- elif module.params['mode'] in ('get', 'getstr') and sig_4:
- params['config'] = botocore.client.Config(signature_version='s3v4')
- if module.params['dualstack']:
- dualconf = botocore.client.Config(s3={'use_dualstack_endpoint': True})
- if 'config' in params:
- params['config'] = params['config'].merge(dualconf)
- else:
- params['config'] = dualconf
- return boto3_conn(**params)
-
-
-def main():
- argument_spec = dict(
- bucket=dict(required=True),
- dest=dict(default=None, type='path'),
- encrypt=dict(default=True, type='bool'),
- encryption_mode=dict(choices=['AES256', 'aws:kms'], default='AES256'),
- expiry=dict(default=600, type='int', aliases=['expiration']),
- headers=dict(type='dict'),
- marker=dict(default=""),
- max_keys=dict(default=1000, type='int'),
- metadata=dict(type='dict'),
- mode=dict(choices=['get', 'put', 'delete', 'create', 'geturl', 'getstr', 'delobj', 'list'], required=True),
- object=dict(),
- permission=dict(type='list', default=['private']),
- version=dict(default=None),
- overwrite=dict(aliases=['force'], default='always'),
- prefix=dict(default=""),
- retries=dict(aliases=['retry'], type='int', default=0),
- s3_url=dict(aliases=['S3_URL']),
- dualstack=dict(default='no', type='bool'),
- rgw=dict(default='no', type='bool'),
- src=dict(),
- ignore_nonexistent_bucket=dict(default=False, type='bool'),
- encryption_kms_key_id=dict()
- )
- module = AnsibleAWSModule(
- argument_spec=argument_spec,
- supports_check_mode=True,
- required_if=[['mode', 'put', ['src', 'object']],
- ['mode', 'get', ['dest', 'object']],
- ['mode', 'getstr', ['object']],
- ['mode', 'geturl', ['object']]],
- )
-
- bucket = module.params.get('bucket')
- encrypt = module.params.get('encrypt')
- expiry = module.params.get('expiry')
- dest = module.params.get('dest', '')
- headers = module.params.get('headers')
- marker = module.params.get('marker')
- max_keys = module.params.get('max_keys')
- metadata = module.params.get('metadata')
- mode = module.params.get('mode')
- obj = module.params.get('object')
- version = module.params.get('version')
- overwrite = module.params.get('overwrite')
- prefix = module.params.get('prefix')
- retries = module.params.get('retries')
- s3_url = module.params.get('s3_url')
- dualstack = module.params.get('dualstack')
- rgw = module.params.get('rgw')
- src = module.params.get('src')
- ignore_nonexistent_bucket = module.params.get('ignore_nonexistent_bucket')
-
- object_canned_acl = ["private", "public-read", "public-read-write", "aws-exec-read", "authenticated-read", "bucket-owner-read", "bucket-owner-full-control"]
- bucket_canned_acl = ["private", "public-read", "public-read-write", "authenticated-read"]
-
- if overwrite not in ['always', 'never', 'different']:
- if module.boolean(overwrite):
- overwrite = 'always'
- else:
- overwrite = 'never'
-
- if overwrite == 'different' and not HAS_MD5:
- module.fail_json(msg='overwrite=different is unavailable: ETag calculation requires MD5 support')
-
- region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True)
-
- if region in ('us-east-1', '', None):
- # default to US Standard region
- location = 'us-east-1'
- else:
- # Boto uses symbolic names for locations but region strings will
- # actually work fine for everything except us-east-1 (US Standard)
- location = region
-
- if module.params.get('object'):
- obj = module.params['object']
- # If there is a top level object, do nothing - if the object starts with /
- # remove the leading character to maintain compatibility with Ansible versions < 2.4
- if obj.startswith('/'):
- obj = obj[1:]
-
- # Bucket deletion does not require obj. Prevents ambiguity with delobj.
- if obj and mode == "delete":
- module.fail_json(msg='Parameter obj cannot be used with mode=delete')
-
- # allow eucarc environment variables to be used if ansible vars aren't set
- if not s3_url and 'S3_URL' in os.environ:
- s3_url = os.environ['S3_URL']
-
- if dualstack and s3_url is not None and 'amazonaws.com' not in s3_url:
- module.fail_json(msg='dualstack only applies to AWS S3')
-
- if dualstack and not module.botocore_at_least('1.4.45'):
- module.fail_json(msg='dualstack requires botocore >= 1.4.45')
-
- # rgw requires an explicit url
- if rgw and not s3_url:
- module.fail_json(msg='rgw flavour requires s3_url')
-
- # Look at s3_url and tweak connection settings
- # if connecting to RGW, Walrus or fakes3
- if s3_url:
- for key in ['validate_certs', 'security_token', 'profile_name']:
- aws_connect_kwargs.pop(key, None)
- s3 = get_s3_connection(module, aws_connect_kwargs, location, rgw, s3_url)
-
- validate = not ignore_nonexistent_bucket
-
- # separate types of ACLs
- bucket_acl = [acl for acl in module.params.get('permission') if acl in bucket_canned_acl]
- object_acl = [acl for acl in module.params.get('permission') if acl in object_canned_acl]
- error_acl = [acl for acl in module.params.get('permission') if acl not in bucket_canned_acl and acl not in object_canned_acl]
- if error_acl:
- module.fail_json(msg='Unknown permission specified: %s' % error_acl)
-
- # First, we check to see if the bucket exists, we get "bucket" returned.
- bucketrtn = bucket_check(module, s3, bucket, validate=validate)
-
- if validate and mode not in ('create', 'put', 'delete') and not bucketrtn:
- module.fail_json(msg="Source bucket cannot be found.")
-
- if mode == 'get':
- keyrtn = key_check(module, s3, bucket, obj, version=version, validate=validate)
- if keyrtn is False:
- if version:
- module.fail_json(msg="Key %s with version id %s does not exist." % (obj, version))
- else:
- module.fail_json(msg="Key %s does not exist." % obj)
-
- if path_check(dest) and overwrite != 'always':
- if overwrite == 'never':
- module.exit_json(msg="Local object already exists and overwrite is disabled.", changed=False)
- if etag_compare(module, dest, s3, bucket, obj, version=version):
- module.exit_json(msg="Local and remote object are identical, ignoring. Use overwrite=always parameter to force.", changed=False)
-
- try:
- download_s3file(module, s3, bucket, obj, dest, retries, version=version)
- except Sigv4Required:
- s3 = get_s3_connection(module, aws_connect_kwargs, location, rgw, s3_url, sig_4=True)
- download_s3file(module, s3, bucket, obj, dest, retries, version=version)
-
- if mode == 'put':
-
- # if putting an object in a bucket yet to be created, acls for the bucket and/or the object may be specified
- # these were separated into the variables bucket_acl and object_acl above
-
- if not path_check(src):
- module.fail_json(msg="Local object for PUT does not exist")
-
- if bucketrtn:
- keyrtn = key_check(module, s3, bucket, obj, version=version, validate=validate)
- else:
- # If the bucket doesn't exist we should create it.
- # only use valid bucket acls for create_bucket function
- module.params['permission'] = bucket_acl
- create_bucket(module, s3, bucket, location)
-
- if keyrtn and overwrite != 'always':
- if overwrite == 'never' or etag_compare(module, src, s3, bucket, obj):
- # Return the download URL for the existing object
- get_download_url(module, s3, bucket, obj, expiry, changed=False)
-
- # only use valid object acls for the upload_s3file function
- module.params['permission'] = object_acl
- upload_s3file(module, s3, bucket, obj, src, expiry, metadata, encrypt, headers)
-
- # Delete an object from a bucket, not the entire bucket
- if mode == 'delobj':
- if obj is None:
- module.fail_json(msg="object parameter is required")
- if bucket:
- deletertn = delete_key(module, s3, bucket, obj)
- if deletertn is True:
- module.exit_json(msg="Object deleted from bucket %s." % bucket, changed=True)
- else:
- module.fail_json(msg="Bucket parameter is required.")
-
- # Delete an entire bucket, including all objects in the bucket
- if mode == 'delete':
- if bucket:
- deletertn = delete_bucket(module, s3, bucket)
- if deletertn is True:
- module.exit_json(msg="Bucket %s and all keys have been deleted." % bucket, changed=True)
- else:
- module.fail_json(msg="Bucket parameter is required.")
-
- # Support for listing a set of keys
- if mode == 'list':
- exists = bucket_check(module, s3, bucket)
-
- # If the bucket does not exist then bail out
- if not exists:
- module.fail_json(msg="Target bucket (%s) cannot be found" % bucket)
-
- list_keys(module, s3, bucket, prefix, marker, max_keys)
-
- # Need to research how to create directories without "populating" a key, so this should just do bucket creation for now.
- # WE SHOULD ENABLE SOME WAY OF CREATING AN EMPTY KEY TO CREATE "DIRECTORY" STRUCTURE, AWS CONSOLE DOES THIS.
- if mode == 'create':
-
- # if both creating a bucket and putting an object in it, acls for the bucket and/or the object may be specified
- # these were separated above into the variables bucket_acl and object_acl
-
- if bucket and not obj:
- if bucketrtn:
- module.exit_json(msg="Bucket already exists.", changed=False)
- else:
- # only use valid bucket acls when creating the bucket
- module.params['permission'] = bucket_acl
- module.exit_json(msg="Bucket created successfully", changed=create_bucket(module, s3, bucket, location))
- if bucket and obj:
- if obj.endswith('/'):
- dirobj = obj
- else:
- dirobj = obj + "/"
- if bucketrtn:
- if key_check(module, s3, bucket, dirobj):
- module.exit_json(msg="Bucket %s and key %s already exists." % (bucket, obj), changed=False)
- else:
- # setting valid object acls for the create_dirkey function
- module.params['permission'] = object_acl
- create_dirkey(module, s3, bucket, dirobj, encrypt)
- else:
- # only use valid bucket acls for the create_bucket function
- module.params['permission'] = bucket_acl
- created = create_bucket(module, s3, bucket, location)
- # only use valid object acls for the create_dirkey function
- module.params['permission'] = object_acl
- create_dirkey(module, s3, bucket, dirobj, encrypt)
-
- # Support for grabbing the time-expired URL for an object in S3/Walrus.
- if mode == 'geturl':
- if not bucket and not obj:
- module.fail_json(msg="Bucket and Object parameters must be set")
-
- keyrtn = key_check(module, s3, bucket, obj, version=version, validate=validate)
- if keyrtn:
- get_download_url(module, s3, bucket, obj, expiry)
- else:
- module.fail_json(msg="Key %s does not exist." % obj)
-
- if mode == 'getstr':
- if bucket and obj:
- keyrtn = key_check(module, s3, bucket, obj, version=version, validate=validate)
- if keyrtn:
- try:
- download_s3str(module, s3, bucket, obj, version=version)
- except Sigv4Required:
- s3 = get_s3_connection(module, aws_connect_kwargs, location, rgw, s3_url, sig_4=True)
- download_s3str(module, s3, bucket, obj, version=version)
- elif version is not None:
- module.fail_json(msg="Key %s with version id %s does not exist." % (obj, version))
- else:
- module.fail_json(msg="Key %s does not exist." % obj)
-
- module.exit_json(failed=False)
-
-
-if __name__ == '__main__':
- main()
diff --git a/test/support/integration/plugins/modules/locale_gen.py b/test/support/integration/plugins/modules/locale_gen.py
deleted file mode 100644
index 4968b834af..0000000000
--- a/test/support/integration/plugins/modules/locale_gen.py
+++ /dev/null
@@ -1,237 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-ANSIBLE_METADATA = {'metadata_version': '1.1',
- 'status': ['preview'],
- 'supported_by': 'community'}
-
-DOCUMENTATION = '''
----
-module: locale_gen
-short_description: Creates or removes locales
-description:
- - Manages locales by editing /etc/locale.gen and invoking locale-gen.
-version_added: "1.6"
-author:
-- Augustus Kling (@AugustusKling)
-options:
- name:
- description:
- - Name and encoding of the locale, such as "en_GB.UTF-8".
- required: true
- state:
- description:
- - Whether the locale shall be present.
- choices: [ absent, present ]
- default: present
-'''
-
-EXAMPLES = '''
-- name: Ensure a locale exists
- locale_gen:
- name: de_CH.UTF-8
- state: present
-'''
-
-import os
-import re
-from subprocess import Popen, PIPE, call
-
-from ansible.module_utils.basic import AnsibleModule
-from ansible.module_utils._text import to_native
-
-LOCALE_NORMALIZATION = {
- ".utf8": ".UTF-8",
- ".eucjp": ".EUC-JP",
- ".iso885915": ".ISO-8859-15",
- ".cp1251": ".CP1251",
- ".koi8r": ".KOI8-R",
- ".armscii8": ".ARMSCII-8",
- ".euckr": ".EUC-KR",
- ".gbk": ".GBK",
- ".gb18030": ".GB18030",
- ".euctw": ".EUC-TW",
-}
-
-
-# ===========================================
-# location module specific support methods.
-#
-
-def is_available(name, ubuntuMode):
- """Check if the given locale is available on the system. This is done by
- checking either :
- * if the locale is present in /etc/locales.gen
- * or if the locale is present in /usr/share/i18n/SUPPORTED"""
- if ubuntuMode:
- __regexp = r'^(?P<locale>\S+_\S+) (?P<charset>\S+)\s*$'
- __locales_available = '/usr/share/i18n/SUPPORTED'
- else:
- __regexp = r'^#{0,1}\s*(?P<locale>\S+_\S+) (?P<charset>\S+)\s*$'
- __locales_available = '/etc/locale.gen'
-
- re_compiled = re.compile(__regexp)
- fd = open(__locales_available, 'r')
- for line in fd:
- result = re_compiled.match(line)
- if result and result.group('locale') == name:
- return True
- fd.close()
- return False
-
-
-def is_present(name):
- """Checks if the given locale is currently installed."""
- output = Popen(["locale", "-a"], stdout=PIPE).communicate()[0]
- output = to_native(output)
- return any(fix_case(name) == fix_case(line) for line in output.splitlines())
-
-
-def fix_case(name):
- """locale -a might return the encoding in either lower or upper case.
- Passing through this function makes them uniform for comparisons."""
- for s, r in LOCALE_NORMALIZATION.items():
- name = name.replace(s, r)
- return name
-
-
-def replace_line(existing_line, new_line):
- """Replaces lines in /etc/locale.gen"""
- try:
- f = open("/etc/locale.gen", "r")
- lines = [line.replace(existing_line, new_line) for line in f]
- finally:
- f.close()
- try:
- f = open("/etc/locale.gen", "w")
- f.write("".join(lines))
- finally:
- f.close()
-
-
-def set_locale(name, enabled=True):
- """ Sets the state of the locale. Defaults to enabled. """
- search_string = r'#{0,1}\s*%s (?P<charset>.+)' % name
- if enabled:
- new_string = r'%s \g<charset>' % (name)
- else:
- new_string = r'# %s \g<charset>' % (name)
- try:
- f = open("/etc/locale.gen", "r")
- lines = [re.sub(search_string, new_string, line) for line in f]
- finally:
- f.close()
- try:
- f = open("/etc/locale.gen", "w")
- f.write("".join(lines))
- finally:
- f.close()
-
-
-def apply_change(targetState, name):
- """Create or remove locale.
-
- Keyword arguments:
- targetState -- Desired state, either present or absent.
- name -- Name including encoding such as de_CH.UTF-8.
- """
- if targetState == "present":
- # Create locale.
- set_locale(name, enabled=True)
- else:
- # Delete locale.
- set_locale(name, enabled=False)
-
- localeGenExitValue = call("locale-gen")
- if localeGenExitValue != 0:
- raise EnvironmentError(localeGenExitValue, "locale.gen failed to execute, it returned " + str(localeGenExitValue))
-
-
-def apply_change_ubuntu(targetState, name):
- """Create or remove locale.
-
- Keyword arguments:
- targetState -- Desired state, either present or absent.
- name -- Name including encoding such as de_CH.UTF-8.
- """
- if targetState == "present":
- # Create locale.
- # Ubuntu's patched locale-gen automatically adds the new locale to /var/lib/locales/supported.d/local
- localeGenExitValue = call(["locale-gen", name])
- else:
- # Delete locale involves discarding the locale from /var/lib/locales/supported.d/local and regenerating all locales.
- try:
- f = open("/var/lib/locales/supported.d/local", "r")
- content = f.readlines()
- finally:
- f.close()
- try:
- f = open("/var/lib/locales/supported.d/local", "w")
- for line in content:
- locale, charset = line.split(' ')
- if locale != name:
- f.write(line)
- finally:
- f.close()
- # Purge locales and regenerate.
- # Please provide a patch if you know how to avoid regenerating the locales to keep!
- localeGenExitValue = call(["locale-gen", "--purge"])
-
- if localeGenExitValue != 0:
- raise EnvironmentError(localeGenExitValue, "locale.gen failed to execute, it returned " + str(localeGenExitValue))
-
-
-def main():
- module = AnsibleModule(
- argument_spec=dict(
- name=dict(type='str', required=True),
- state=dict(type='str', default='present', choices=['absent', 'present']),
- ),
- supports_check_mode=True,
- )
-
- name = module.params['name']
- state = module.params['state']
-
- if not os.path.exists("/etc/locale.gen"):
- if os.path.exists("/var/lib/locales/supported.d/"):
- # Ubuntu created its own system to manage locales.
- ubuntuMode = True
- else:
- module.fail_json(msg="/etc/locale.gen and /var/lib/locales/supported.d/local are missing. Is the package \"locales\" installed?")
- else:
- # We found the common way to manage locales.
- ubuntuMode = False
-
- if not is_available(name, ubuntuMode):
- module.fail_json(msg="The locale you've entered is not available "
- "on your system.")
-
- if is_present(name):
- prev_state = "present"
- else:
- prev_state = "absent"
- changed = (prev_state != state)
-
- if module.check_mode:
- module.exit_json(changed=changed)
- else:
- if changed:
- try:
- if ubuntuMode is False:
- apply_change(state, name)
- else:
- apply_change_ubuntu(state, name)
- except EnvironmentError as e:
- module.fail_json(msg=to_native(e), exitValue=e.errno)
-
- module.exit_json(name=name, changed=changed, msg="OK")
-
-
-if __name__ == '__main__':
- main()
diff --git a/test/support/integration/plugins/modules/lvg.py b/test/support/integration/plugins/modules/lvg.py
deleted file mode 100644
index e2035f688d..0000000000
--- a/test/support/integration/plugins/modules/lvg.py
+++ /dev/null
@@ -1,295 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Copyright: (c) 2013, Alexander Bulimov <lazywolf0@gmail.com>
-# Based on lvol module by Jeroen Hoekx <jeroen.hoekx@dsquare.be>
-# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-ANSIBLE_METADATA = {'metadata_version': '1.1',
- 'status': ['preview'],
- 'supported_by': 'community'}
-
-DOCUMENTATION = r'''
----
-author:
-- Alexander Bulimov (@abulimov)
-module: lvg
-short_description: Configure LVM volume groups
-description:
- - This module creates, removes or resizes volume groups.
-version_added: "1.1"
-options:
- vg:
- description:
- - The name of the volume group.
- type: str
- required: true
- pvs:
- description:
- - List of comma-separated devices to use as physical devices in this volume group.
- - Required when creating or resizing volume group.
- - The module will take care of running pvcreate if needed.
- type: list
- pesize:
- description:
- - "The size of the physical extent. I(pesize) must be a power of 2 of at least 1 sector
- (where the sector size is the largest sector size of the PVs currently used in the VG),
- or at least 128KiB."
- - Since Ansible 2.6, pesize can be optionally suffixed by a UNIT (k/K/m/M/g/G), default unit is megabyte.
- type: str
- default: "4"
- pv_options:
- description:
- - Additional options to pass to C(pvcreate) when creating the volume group.
- type: str
- version_added: "2.4"
- vg_options:
- description:
- - Additional options to pass to C(vgcreate) when creating the volume group.
- type: str
- version_added: "1.6"
- state:
- description:
- - Control if the volume group exists.
- type: str
- choices: [ absent, present ]
- default: present
- force:
- description:
- - If C(yes), allows to remove volume group with logical volumes.
- type: bool
- default: no
-seealso:
-- module: filesystem
-- module: lvol
-- module: parted
-notes:
- - This module does not modify PE size for already present volume group.
-'''
-
-EXAMPLES = r'''
-- name: Create a volume group on top of /dev/sda1 with physical extent size = 32MB
- lvg:
- vg: vg.services
- pvs: /dev/sda1
- pesize: 32
-
-- name: Create a volume group on top of /dev/sdb with physical extent size = 128KiB
- lvg:
- vg: vg.services
- pvs: /dev/sdb
- pesize: 128K
-
-# If, for example, we already have VG vg.services on top of /dev/sdb1,
-# this VG will be extended by /dev/sdc5. Or if vg.services was created on
-# top of /dev/sda5, we first extend it with /dev/sdb1 and /dev/sdc5,
-# and then reduce by /dev/sda5.
-- name: Create or resize a volume group on top of /dev/sdb1 and /dev/sdc5.
- lvg:
- vg: vg.services
- pvs: /dev/sdb1,/dev/sdc5
-
-- name: Remove a volume group with name vg.services
- lvg:
- vg: vg.services
- state: absent
-'''
-
-import itertools
-import os
-
-from ansible.module_utils.basic import AnsibleModule
-
-
-def parse_vgs(data):
- vgs = []
- for line in data.splitlines():
- parts = line.strip().split(';')
- vgs.append({
- 'name': parts[0],
- 'pv_count': int(parts[1]),
- 'lv_count': int(parts[2]),
- })
- return vgs
-
-
-def find_mapper_device_name(module, dm_device):
- dmsetup_cmd = module.get_bin_path('dmsetup', True)
- mapper_prefix = '/dev/mapper/'
- rc, dm_name, err = module.run_command("%s info -C --noheadings -o name %s" % (dmsetup_cmd, dm_device))
- if rc != 0:
- module.fail_json(msg="Failed executing dmsetup command.", rc=rc, err=err)
- mapper_device = mapper_prefix + dm_name.rstrip()
- return mapper_device
-
-
-def parse_pvs(module, data):
- pvs = []
- dm_prefix = '/dev/dm-'
- for line in data.splitlines():
- parts = line.strip().split(';')
- if parts[0].startswith(dm_prefix):
- parts[0] = find_mapper_device_name(module, parts[0])
- pvs.append({
- 'name': parts[0],
- 'vg_name': parts[1],
- })
- return pvs
-
-
-def main():
- module = AnsibleModule(
- argument_spec=dict(
- vg=dict(type='str', required=True),
- pvs=dict(type='list'),
- pesize=dict(type='str', default='4'),
- pv_options=dict(type='str', default=''),
- vg_options=dict(type='str', default=''),
- state=dict(type='str', default='present', choices=['absent', 'present']),
- force=dict(type='bool', default=False),
- ),
- supports_check_mode=True,
- )
-
- vg = module.params['vg']
- state = module.params['state']
- force = module.boolean(module.params['force'])
- pesize = module.params['pesize']
- pvoptions = module.params['pv_options'].split()
- vgoptions = module.params['vg_options'].split()
-
- dev_list = []
- if module.params['pvs']:
- dev_list = list(module.params['pvs'])
- elif state == 'present':
- module.fail_json(msg="No physical volumes given.")
-
- # LVM always uses real paths not symlinks so replace symlinks with actual path
- for idx, dev in enumerate(dev_list):
- dev_list[idx] = os.path.realpath(dev)
-
- if state == 'present':
- # check given devices
- for test_dev in dev_list:
- if not os.path.exists(test_dev):
- module.fail_json(msg="Device %s not found." % test_dev)
-
- # get pv list
- pvs_cmd = module.get_bin_path('pvs', True)
- if dev_list:
- pvs_filter_pv_name = ' || '.join(
- 'pv_name = {0}'.format(x)
- for x in itertools.chain(dev_list, module.params['pvs'])
- )
- pvs_filter_vg_name = 'vg_name = {0}'.format(vg)
- pvs_filter = "--select '{0} || {1}' ".format(pvs_filter_pv_name, pvs_filter_vg_name)
- else:
- pvs_filter = ''
- rc, current_pvs, err = module.run_command("%s --noheadings -o pv_name,vg_name --separator ';' %s" % (pvs_cmd, pvs_filter))
- if rc != 0:
- module.fail_json(msg="Failed executing pvs command.", rc=rc, err=err)
-
- # check pv for devices
- pvs = parse_pvs(module, current_pvs)
- used_pvs = [pv for pv in pvs if pv['name'] in dev_list and pv['vg_name'] and pv['vg_name'] != vg]
- if used_pvs:
- module.fail_json(msg="Device %s is already in %s volume group." % (used_pvs[0]['name'], used_pvs[0]['vg_name']))
-
- vgs_cmd = module.get_bin_path('vgs', True)
- rc, current_vgs, err = module.run_command("%s --noheadings -o vg_name,pv_count,lv_count --separator ';'" % vgs_cmd)
-
- if rc != 0:
- module.fail_json(msg="Failed executing vgs command.", rc=rc, err=err)
-
- changed = False
-
- vgs = parse_vgs(current_vgs)
-
- for test_vg in vgs:
- if test_vg['name'] == vg:
- this_vg = test_vg
- break
- else:
- this_vg = None
-
- if this_vg is None:
- if state == 'present':
- # create VG
- if module.check_mode:
- changed = True
- else:
- # create PV
- pvcreate_cmd = module.get_bin_path('pvcreate', True)
- for current_dev in dev_list:
- rc, _, err = module.run_command([pvcreate_cmd] + pvoptions + ['-f', str(current_dev)])
- if rc == 0:
- changed = True
- else:
- module.fail_json(msg="Creating physical volume '%s' failed" % current_dev, rc=rc, err=err)
- vgcreate_cmd = module.get_bin_path('vgcreate')
- rc, _, err = module.run_command([vgcreate_cmd] + vgoptions + ['-s', pesize, vg] + dev_list)
- if rc == 0:
- changed = True
- else:
- module.fail_json(msg="Creating volume group '%s' failed" % vg, rc=rc, err=err)
- else:
- if state == 'absent':
- if module.check_mode:
- module.exit_json(changed=True)
- else:
- if this_vg['lv_count'] == 0 or force:
- # remove VG
- vgremove_cmd = module.get_bin_path('vgremove', True)
- rc, _, err = module.run_command("%s --force %s" % (vgremove_cmd, vg))
- if rc == 0:
- module.exit_json(changed=True)
- else:
- module.fail_json(msg="Failed to remove volume group %s" % (vg), rc=rc, err=err)
- else:
- module.fail_json(msg="Refuse to remove non-empty volume group %s without force=yes" % (vg))
-
- # resize VG
- current_devs = [os.path.realpath(pv['name']) for pv in pvs if pv['vg_name'] == vg]
- devs_to_remove = list(set(current_devs) - set(dev_list))
- devs_to_add = list(set(dev_list) - set(current_devs))
-
- if devs_to_add or devs_to_remove:
- if module.check_mode:
- changed = True
- else:
- if devs_to_add:
- devs_to_add_string = ' '.join(devs_to_add)
- # create PV
- pvcreate_cmd = module.get_bin_path('pvcreate', True)
- for current_dev in devs_to_add:
- rc, _, err = module.run_command([pvcreate_cmd] + pvoptions + ['-f', str(current_dev)])
- if rc == 0:
- changed = True
- else:
- module.fail_json(msg="Creating physical volume '%s' failed" % current_dev, rc=rc, err=err)
- # add PV to our VG
- vgextend_cmd = module.get_bin_path('vgextend', True)
- rc, _, err = module.run_command("%s %s %s" % (vgextend_cmd, vg, devs_to_add_string))
- if rc == 0:
- changed = True
- else:
- module.fail_json(msg="Unable to extend %s by %s." % (vg, devs_to_add_string), rc=rc, err=err)
-
- # remove some PV from our VG
- if devs_to_remove:
- devs_to_remove_string = ' '.join(devs_to_remove)
- vgreduce_cmd = module.get_bin_path('vgreduce', True)
- rc, _, err = module.run_command("%s --force %s %s" % (vgreduce_cmd, vg, devs_to_remove_string))
- if rc == 0:
- changed = True
- else:
- module.fail_json(msg="Unable to reduce %s by %s." % (vg, devs_to_remove_string), rc=rc, err=err)
-
- module.exit_json(changed=changed)
-
-
-if __name__ == '__main__':
- main()
diff --git a/test/support/integration/plugins/modules/mongodb_parameter.py b/test/support/integration/plugins/modules/mongodb_parameter.py
deleted file mode 100644
index 05de42b2ea..0000000000
--- a/test/support/integration/plugins/modules/mongodb_parameter.py
+++ /dev/null
@@ -1,223 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# (c) 2016, Loic Blot <loic.blot@unix-experience.fr>
-# Sponsored by Infopro Digital. http://www.infopro-digital.com/
-# Sponsored by E.T.A.I. http://www.etai.fr/
-#
-# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-
-ANSIBLE_METADATA = {'metadata_version': '1.1',
- 'status': ['preview'],
- 'supported_by': 'community'}
-
-
-DOCUMENTATION = r'''
----
-module: mongodb_parameter
-short_description: Change an administrative parameter on a MongoDB server
-description:
- - Change an administrative parameter on a MongoDB server.
-version_added: "2.1"
-options:
- login_user:
- description:
- - The MongoDB username used to authenticate with.
- type: str
- login_password:
- description:
- - The login user's password used to authenticate with.
- type: str
- login_host:
- description:
- - The host running the database.
- type: str
- default: localhost
- login_port:
- description:
- - The MongoDB port to connect to.
- default: 27017
- type: int
- login_database:
- description:
- - The database where login credentials are stored.
- type: str
- replica_set:
- description:
- - Replica set to connect to (automatically connects to primary for writes).
- type: str
- ssl:
- description:
- - Whether to use an SSL connection when connecting to the database.
- type: bool
- default: no
- param:
- description:
- - MongoDB administrative parameter to modify.
- type: str
- required: true
- value:
- description:
- - MongoDB administrative parameter value to set.
- type: str
- required: true
- param_type:
- description:
- - Define the type of parameter value.
- default: str
- type: str
- choices: [int, str]
-
-notes:
- - Requires the pymongo Python package on the remote host, version 2.4.2+.
- - This can be installed using pip or the OS package manager.
- - See also U(http://api.mongodb.org/python/current/installation.html)
-requirements: [ "pymongo" ]
-author: "Loic Blot (@nerzhul)"
-'''
-
-EXAMPLES = r'''
-- name: Set MongoDB syncdelay to 60 (this is an int)
- mongodb_parameter:
- param: syncdelay
- value: 60
- param_type: int
-'''
-
-RETURN = r'''
-before:
- description: value before modification
- returned: success
- type: str
-after:
- description: value after modification
- returned: success
- type: str
-'''
-
-import os
-import traceback
-
-try:
- from pymongo.errors import ConnectionFailure
- from pymongo.errors import OperationFailure
- from pymongo import version as PyMongoVersion
- from pymongo import MongoClient
-except ImportError:
- try: # for older PyMongo 2.2
- from pymongo import Connection as MongoClient
- except ImportError:
- pymongo_found = False
- else:
- pymongo_found = True
-else:
- pymongo_found = True
-
-from ansible.module_utils.basic import AnsibleModule, missing_required_lib
-from ansible.module_utils.six.moves import configparser
-from ansible.module_utils._text import to_native
-
-
-# =========================================
-# MongoDB module specific support methods.
-#
-
-def load_mongocnf():
- config = configparser.RawConfigParser()
- mongocnf = os.path.expanduser('~/.mongodb.cnf')
-
- try:
- config.readfp(open(mongocnf))
- creds = dict(
- user=config.get('client', 'user'),
- password=config.get('client', 'pass')
- )
- except (configparser.NoOptionError, IOError):
- return False
-
- return creds
-
-
-# =========================================
-# Module execution.
-#
-
-def main():
- module = AnsibleModule(
- argument_spec=dict(
- login_user=dict(default=None),
- login_password=dict(default=None, no_log=True),
- login_host=dict(default='localhost'),
- login_port=dict(default=27017, type='int'),
- login_database=dict(default=None),
- replica_set=dict(default=None),
- param=dict(required=True),
- value=dict(required=True),
- param_type=dict(default="str", choices=['str', 'int']),
- ssl=dict(default=False, type='bool'),
- )
- )
-
- if not pymongo_found:
- module.fail_json(msg=missing_required_lib('pymongo'))
-
- login_user = module.params['login_user']
- login_password = module.params['login_password']
- login_host = module.params['login_host']
- login_port = module.params['login_port']
- login_database = module.params['login_database']
-
- replica_set = module.params['replica_set']
- ssl = module.params['ssl']
-
- param = module.params['param']
- param_type = module.params['param_type']
- value = module.params['value']
-
- # Verify parameter is coherent with specified type
- try:
- if param_type == 'int':
- value = int(value)
- except ValueError:
- module.fail_json(msg="value '%s' is not %s" % (value, param_type))
-
- try:
- if replica_set:
- client = MongoClient(login_host, int(login_port), replicaset=replica_set, ssl=ssl)
- else:
- client = MongoClient(login_host, int(login_port), ssl=ssl)
-
- if login_user is None and login_password is None:
- mongocnf_creds = load_mongocnf()
- if mongocnf_creds is not False:
- login_user = mongocnf_creds['user']
- login_password = mongocnf_creds['password']
- elif login_password is None or login_user is None:
- module.fail_json(msg='when supplying login arguments, both login_user and login_password must be provided')
-
- if login_user is not None and login_password is not None:
- client.admin.authenticate(login_user, login_password, source=login_database)
-
- except ConnectionFailure as e:
- module.fail_json(msg='unable to connect to database: %s' % to_native(e), exception=traceback.format_exc())
-
- db = client.admin
-
- try:
- after_value = db.command("setParameter", **{param: value})
- except OperationFailure as e:
- module.fail_json(msg="unable to change parameter: %s" % to_native(e), exception=traceback.format_exc())
-
- if "was" not in after_value:
- module.exit_json(changed=True, msg="Unable to determine old value, assume it changed.")
- else:
- module.exit_json(changed=(value != after_value["was"]), before=after_value["was"],
- after=value)
-
-
-if __name__ == '__main__':
- main()
diff --git a/test/support/integration/plugins/modules/mongodb_user.py b/test/support/integration/plugins/modules/mongodb_user.py
deleted file mode 100644
index 7a18b15908..0000000000
--- a/test/support/integration/plugins/modules/mongodb_user.py
+++ /dev/null
@@ -1,474 +0,0 @@
-#!/usr/bin/python
-
-# (c) 2012, Elliott Foster <elliott@fourkitchens.com>
-# Sponsored by Four Kitchens http://fourkitchens.com.
-# (c) 2014, Epic Games, Inc.
-#
-# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-
-ANSIBLE_METADATA = {'metadata_version': '1.1',
- 'status': ['preview'],
- 'supported_by': 'community'}
-
-
-DOCUMENTATION = '''
----
-module: mongodb_user
-short_description: Adds or removes a user from a MongoDB database
-description:
- - Adds or removes a user from a MongoDB database.
-version_added: "1.1"
-options:
- login_user:
- description:
- - The MongoDB username used to authenticate with.
- type: str
- login_password:
- description:
- - The login user's password used to authenticate with.
- type: str
- login_host:
- description:
- - The host running the database.
- default: localhost
- type: str
- login_port:
- description:
- - The MongoDB port to connect to.
- default: '27017'
- type: str
- login_database:
- version_added: "2.0"
- description:
- - The database where login credentials are stored.
- type: str
- replica_set:
- version_added: "1.6"
- description:
- - Replica set to connect to (automatically connects to primary for writes).
- type: str
- database:
- description:
- - The name of the database to add/remove the user from.
- required: true
- type: str
- aliases: [db]
- name:
- description:
- - The name of the user to add or remove.
- required: true
- aliases: [user]
- type: str
- password:
- description:
- - The password to use for the user.
- type: str
- aliases: [pass]
- ssl:
- version_added: "1.8"
- description:
- - Whether to use an SSL connection when connecting to the database.
- type: bool
- ssl_cert_reqs:
- version_added: "2.2"
- description:
- - Specifies whether a certificate is required from the other side of the connection,
- and whether it will be validated if provided.
- default: CERT_REQUIRED
- choices: [CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED]
- type: str
- roles:
- version_added: "1.3"
- type: list
- elements: raw
- description:
- - >
- The database user roles valid values could either be one or more of the following strings:
- 'read', 'readWrite', 'dbAdmin', 'userAdmin', 'clusterAdmin', 'readAnyDatabase', 'readWriteAnyDatabase', 'userAdminAnyDatabase',
- 'dbAdminAnyDatabase'
- - "Or the following dictionary '{ db: DATABASE_NAME, role: ROLE_NAME }'."
- - "This param requires pymongo 2.5+. If it is a string, mongodb 2.4+ is also required. If it is a dictionary, mongo 2.6+ is required."
- state:
- description:
- - The database user state.
- default: present
- choices: [absent, present]
- type: str
- update_password:
- default: always
- choices: [always, on_create]
- version_added: "2.1"
- description:
- - C(always) will update passwords if they differ.
- - C(on_create) will only set the password for newly created users.
- type: str
-
-notes:
- - Requires the pymongo Python package on the remote host, version 2.4.2+. This
- can be installed using pip or the OS package manager. @see http://api.mongodb.org/python/current/installation.html
-requirements: [ "pymongo" ]
-author:
- - "Elliott Foster (@elliotttf)"
- - "Julien Thebault (@Lujeni)"
-'''
-
-EXAMPLES = '''
-- name: Create 'burgers' database user with name 'bob' and password '12345'.
- mongodb_user:
- database: burgers
- name: bob
- password: 12345
- state: present
-
-- name: Create a database user via SSL (MongoDB must be compiled with the SSL option and configured properly)
- mongodb_user:
- database: burgers
- name: bob
- password: 12345
- state: present
- ssl: True
-
-- name: Delete 'burgers' database user with name 'bob'.
- mongodb_user:
- database: burgers
- name: bob
- state: absent
-
-- name: Define more users with various specific roles (if not defined, no roles is assigned, and the user will be added via pre mongo 2.2 style)
- mongodb_user:
- database: burgers
- name: ben
- password: 12345
- roles: read
- state: present
-
-- name: Define roles
- mongodb_user:
- database: burgers
- name: jim
- password: 12345
- roles: readWrite,dbAdmin,userAdmin
- state: present
-
-- name: Define roles
- mongodb_user:
- database: burgers
- name: joe
- password: 12345
- roles: readWriteAnyDatabase
- state: present
-
-- name: Add a user to database in a replica set, the primary server is automatically discovered and written to
- mongodb_user:
- database: burgers
- name: bob
- replica_set: belcher
- password: 12345
- roles: readWriteAnyDatabase
- state: present
-
-# add a user 'oplog_reader' with read only access to the 'local' database on the replica_set 'belcher'. This is useful for oplog access (MONGO_OPLOG_URL).
-# please notice the credentials must be added to the 'admin' database because the 'local' database is not synchronized and can't receive user credentials
-# To login with such user, the connection string should be MONGO_OPLOG_URL="mongodb://oplog_reader:oplog_reader_password@server1,server2/local?authSource=admin"
-# This syntax requires mongodb 2.6+ and pymongo 2.5+
-- name: Roles as a dictionary
- mongodb_user:
- login_user: root
- login_password: root_password
- database: admin
- user: oplog_reader
- password: oplog_reader_password
- state: present
- replica_set: belcher
- roles:
- - db: local
- role: read
-
-'''
-
-RETURN = '''
-user:
- description: The name of the user to add or remove.
- returned: success
- type: str
-'''
-
-import os
-import ssl as ssl_lib
-import traceback
-from ansible.module_utils.compat.version import LooseVersion
-from operator import itemgetter
-
-try:
- from pymongo.errors import ConnectionFailure
- from pymongo.errors import OperationFailure
- from pymongo import version as PyMongoVersion
- from pymongo import MongoClient
-except ImportError:
- try: # for older PyMongo 2.2
- from pymongo import Connection as MongoClient
- except ImportError:
- pymongo_found = False
- else:
- pymongo_found = True
-else:
- pymongo_found = True
-
-from ansible.module_utils.basic import AnsibleModule, missing_required_lib
-from ansible.module_utils.six import binary_type, text_type
-from ansible.module_utils.six.moves import configparser
-from ansible.module_utils._text import to_native
-
-
-# =========================================
-# MongoDB module specific support methods.
-#
-
-def check_compatibility(module, client):
- """Check the compatibility between the driver and the database.
-
- See: https://docs.mongodb.com/ecosystem/drivers/driver-compatibility-reference/#python-driver-compatibility
-
- Args:
- module: Ansible module.
- client (cursor): Mongodb cursor on admin database.
- """
- loose_srv_version = LooseVersion(client.server_info()['version'])
- loose_driver_version = LooseVersion(PyMongoVersion)
-
- if loose_srv_version >= LooseVersion('3.2') and loose_driver_version < LooseVersion('3.2'):
- module.fail_json(msg=' (Note: you must use pymongo 3.2+ with MongoDB >= 3.2)')
-
- elif loose_srv_version >= LooseVersion('3.0') and loose_driver_version <= LooseVersion('2.8'):
- module.fail_json(msg=' (Note: you must use pymongo 2.8+ with MongoDB 3.0)')
-
- elif loose_srv_version >= LooseVersion('2.6') and loose_driver_version <= LooseVersion('2.7'):
- module.fail_json(msg=' (Note: you must use pymongo 2.7+ with MongoDB 2.6)')
-
- elif LooseVersion(PyMongoVersion) <= LooseVersion('2.5'):
- module.fail_json(msg=' (Note: you must be on mongodb 2.4+ and pymongo 2.5+ to use the roles param)')
-
-
-def user_find(client, user, db_name):
- """Check if the user exists.
-
- Args:
- client (cursor): Mongodb cursor on admin database.
- user (str): User to check.
- db_name (str): User's database.
-
- Returns:
- dict: when user exists, False otherwise.
- """
- for mongo_user in client["admin"].system.users.find():
- if mongo_user['user'] == user:
- # NOTE: there is no 'db' field in mongo 2.4.
- if 'db' not in mongo_user:
- return mongo_user
-
- if mongo_user["db"] == db_name:
- return mongo_user
- return False
-
-
-def user_add(module, client, db_name, user, password, roles):
- # pymongo's user_add is a _create_or_update_user so we won't know if it was changed or updated
- # without reproducing a lot of the logic in database.py of pymongo
- db = client[db_name]
-
- if roles is None:
- db.add_user(user, password, False)
- else:
- db.add_user(user, password, None, roles=roles)
-
-
-def user_remove(module, client, db_name, user):
- exists = user_find(client, user, db_name)
- if exists:
- if module.check_mode:
- module.exit_json(changed=True, user=user)
- db = client[db_name]
- db.remove_user(user)
- else:
- module.exit_json(changed=False, user=user)
-
-
-def load_mongocnf():
- config = configparser.RawConfigParser()
- mongocnf = os.path.expanduser('~/.mongodb.cnf')
-
- try:
- config.readfp(open(mongocnf))
- creds = dict(
- user=config.get('client', 'user'),
- password=config.get('client', 'pass')
- )
- except (configparser.NoOptionError, IOError):
- return False
-
- return creds
-
-
-def check_if_roles_changed(uinfo, roles, db_name):
- # We must be aware of users which can read the oplog on a replicaset
- # Such users must have access to the local DB, but since this DB does not store users credentials
- # and is not synchronized among replica sets, the user must be stored on the admin db
- # Therefore their structure is the following :
- # {
- # "_id" : "admin.oplog_reader",
- # "user" : "oplog_reader",
- # "db" : "admin", # <-- admin DB
- # "roles" : [
- # {
- # "role" : "read",
- # "db" : "local" # <-- local DB
- # }
- # ]
- # }
-
- def make_sure_roles_are_a_list_of_dict(roles, db_name):
- output = list()
- for role in roles:
- if isinstance(role, (binary_type, text_type)):
- new_role = {"role": role, "db": db_name}
- output.append(new_role)
- else:
- output.append(role)
- return output
-
- roles_as_list_of_dict = make_sure_roles_are_a_list_of_dict(roles, db_name)
- uinfo_roles = uinfo.get('roles', [])
-
- if sorted(roles_as_list_of_dict, key=itemgetter('db')) == sorted(uinfo_roles, key=itemgetter('db')):
- return False
- return True
-
-
-# =========================================
-# Module execution.
-#
-
-def main():
- module = AnsibleModule(
- argument_spec=dict(
- login_user=dict(default=None),
- login_password=dict(default=None, no_log=True),
- login_host=dict(default='localhost'),
- login_port=dict(default='27017'),
- login_database=dict(default=None),
- replica_set=dict(default=None),
- database=dict(required=True, aliases=['db']),
- name=dict(required=True, aliases=['user']),
- password=dict(aliases=['pass'], no_log=True),
- ssl=dict(default=False, type='bool'),
- roles=dict(default=None, type='list', elements='raw'),
- state=dict(default='present', choices=['absent', 'present']),
- update_password=dict(default="always", choices=["always", "on_create"]),
- ssl_cert_reqs=dict(default='CERT_REQUIRED', choices=['CERT_NONE', 'CERT_OPTIONAL', 'CERT_REQUIRED']),
- ),
- supports_check_mode=True
- )
-
- if not pymongo_found:
- module.fail_json(msg=missing_required_lib('pymongo'))
-
- login_user = module.params['login_user']
- login_password = module.params['login_password']
- login_host = module.params['login_host']
- login_port = module.params['login_port']
- login_database = module.params['login_database']
-
- replica_set = module.params['replica_set']
- db_name = module.params['database']
- user = module.params['name']
- password = module.params['password']
- ssl = module.params['ssl']
- roles = module.params['roles'] or []
- state = module.params['state']
- update_password = module.params['update_password']
-
- try:
- connection_params = {
- "host": login_host,
- "port": int(login_port),
- }
-
- if replica_set:
- connection_params["replicaset"] = replica_set
-
- if ssl:
- connection_params["ssl"] = ssl
- connection_params["ssl_cert_reqs"] = getattr(ssl_lib, module.params['ssl_cert_reqs'])
-
- client = MongoClient(**connection_params)
-
- # NOTE: this check must be done ASAP.
- # We doesn't need to be authenticated (this ability has lost in PyMongo 3.6)
- if LooseVersion(PyMongoVersion) <= LooseVersion('3.5'):
- check_compatibility(module, client)
-
- if login_user is None and login_password is None:
- mongocnf_creds = load_mongocnf()
- if mongocnf_creds is not False:
- login_user = mongocnf_creds['user']
- login_password = mongocnf_creds['password']
- elif login_password is None or login_user is None:
- module.fail_json(msg='when supplying login arguments, both login_user and login_password must be provided')
-
- if login_user is not None and login_password is not None:
- client.admin.authenticate(login_user, login_password, source=login_database)
- elif LooseVersion(PyMongoVersion) >= LooseVersion('3.0'):
- if db_name != "admin":
- module.fail_json(msg='The localhost login exception only allows the first admin account to be created')
- # else: this has to be the first admin user added
-
- except Exception as e:
- module.fail_json(msg='unable to connect to database: %s' % to_native(e), exception=traceback.format_exc())
-
- if state == 'present':
- if password is None and update_password == 'always':
- module.fail_json(msg='password parameter required when adding a user unless update_password is set to on_create')
-
- try:
- if update_password != 'always':
- uinfo = user_find(client, user, db_name)
- if uinfo:
- password = None
- if not check_if_roles_changed(uinfo, roles, db_name):
- module.exit_json(changed=False, user=user)
-
- if module.check_mode:
- module.exit_json(changed=True, user=user)
-
- user_add(module, client, db_name, user, password, roles)
- except Exception as e:
- module.fail_json(msg='Unable to add or update user: %s' % to_native(e), exception=traceback.format_exc())
- finally:
- try:
- client.close()
- except Exception:
- pass
- # Here we can check password change if mongo provide a query for that : https://jira.mongodb.org/browse/SERVER-22848
- # newuinfo = user_find(client, user, db_name)
- # if uinfo['role'] == newuinfo['role'] and CheckPasswordHere:
- # module.exit_json(changed=False, user=user)
-
- elif state == 'absent':
- try:
- user_remove(module, client, db_name, user)
- except Exception as e:
- module.fail_json(msg='Unable to remove user: %s' % to_native(e), exception=traceback.format_exc())
- finally:
- try:
- client.close()
- except Exception:
- pass
- module.exit_json(changed=True, user=user)
-
-
-if __name__ == '__main__':
- main()
diff --git a/test/support/integration/plugins/modules/pids.py b/test/support/integration/plugins/modules/pids.py
deleted file mode 100644
index 4cbf45a969..0000000000
--- a/test/support/integration/plugins/modules/pids.py
+++ /dev/null
@@ -1,89 +0,0 @@
-#!/usr/bin/python
-# Copyright: (c) 2019, Saranya Sridharan
-# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-from __future__ import (absolute_import, division, print_function)
-__metaclass__ = type
-
-ANSIBLE_METADATA = {'metadata_version': '1.1',
- 'status': ['preview'],
- 'supported_by': 'community'}
-
-DOCUMENTATION = '''
-module: pids
-version_added: 2.8
-description: "Retrieves a list of PIDs of given process name in Ansible controller/controlled machines.Returns an empty list if no process in that name exists."
-short_description: "Retrieves process IDs list if the process is running otherwise return empty list"
-author:
- - Saranya Sridharan (@saranyasridharan)
-requirements:
- - psutil(python module)
-options:
- name:
- description: the name of the process you want to get PID for.
- required: true
- type: str
-'''
-
-EXAMPLES = '''
-# Pass the process name
-- name: Getting process IDs of the process
- pids:
- name: python
- register: pids_of_python
-
-- name: Printing the process IDs obtained
- debug:
- msg: "PIDS of python:{{pids_of_python.pids|join(',')}}"
-'''
-
-RETURN = '''
-pids:
- description: Process IDs of the given process
- returned: list of none, one, or more process IDs
- type: list
- sample: [100,200]
-'''
-
-from ansible.module_utils.basic import AnsibleModule
-try:
- import psutil
- HAS_PSUTIL = True
-except ImportError:
- HAS_PSUTIL = False
-
-
-def compare_lower(a, b):
- if a is None or b is None:
- # this could just be "return False" but would lead to surprising behavior if both a and b are None
- return a == b
-
- return a.lower() == b.lower()
-
-
-def get_pid(name):
- pids = []
-
- for proc in psutil.process_iter(attrs=['name', 'cmdline']):
- if compare_lower(proc.info['name'], name) or \
- proc.info['cmdline'] and compare_lower(proc.info['cmdline'][0], name):
- pids.append(proc.pid)
-
- return pids
-
-
-def main():
- module = AnsibleModule(
- argument_spec=dict(
- name=dict(required=True, type="str"),
- ),
- supports_check_mode=True,
- )
- if not HAS_PSUTIL:
- module.fail_json(msg="Missing required 'psutil' python module. Try installing it with: pip install psutil")
- name = module.params["name"]
- response = dict(pids=get_pid(name))
- module.exit_json(**response)
-
-
-if __name__ == '__main__':
- main()
diff --git a/test/support/integration/plugins/modules/postgresql_db.py b/test/support/integration/plugins/modules/postgresql_db.py
deleted file mode 100644
index 40858d9974..0000000000
--- a/test/support/integration/plugins/modules/postgresql_db.py
+++ /dev/null
@@ -1,657 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Copyright: Ansible Project
-# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-ANSIBLE_METADATA = {'metadata_version': '1.1',
- 'status': ['stableinterface'],
- 'supported_by': 'community'}
-
-DOCUMENTATION = r'''
----
-module: postgresql_db
-short_description: Add or remove PostgreSQL databases from a remote host.
-description:
- - Add or remove PostgreSQL databases from a remote host.
-version_added: '0.6'
-options:
- name:
- description:
- - Name of the database to add or remove
- type: str
- required: true
- aliases: [ db ]
- port:
- description:
- - Database port to connect (if needed)
- type: int
- default: 5432
- aliases:
- - login_port
- owner:
- description:
- - Name of the role to set as owner of the database
- type: str
- template:
- description:
- - Template used to create the database
- type: str
- encoding:
- description:
- - Encoding of the database
- type: str
- lc_collate:
- description:
- - Collation order (LC_COLLATE) to use in the database. Must match collation order of template database unless C(template0) is used as template.
- type: str
- lc_ctype:
- description:
- - Character classification (LC_CTYPE) to use in the database (e.g. lower, upper, ...) Must match LC_CTYPE of template database unless C(template0)
- is used as template.
- type: str
- session_role:
- description:
- - Switch to session_role after connecting. The specified session_role must be a role that the current login_user is a member of.
- - Permissions checking for SQL commands is carried out as though the session_role were the one that had logged in originally.
- type: str
- version_added: '2.8'
- state:
- description:
- - The database state.
- - C(present) implies that the database should be created if necessary.
- - C(absent) implies that the database should be removed if present.
- - C(dump) requires a target definition to which the database will be backed up. (Added in Ansible 2.4)
- Note that in some PostgreSQL versions of pg_dump, which is an embedded PostgreSQL utility and is used by the module,
- returns rc 0 even when errors occurred (e.g. the connection is forbidden by pg_hba.conf, etc.),
- so the module returns changed=True but the dump has not actually been done. Please, be sure that your version of
- pg_dump returns rc 1 in this case.
- - C(restore) also requires a target definition from which the database will be restored. (Added in Ansible 2.4)
- - The format of the backup will be detected based on the target name.
- - Supported compression formats for dump and restore include C(.pgc), C(.bz2), C(.gz) and C(.xz)
- - Supported formats for dump and restore include C(.sql) and C(.tar)
- type: str
- choices: [ absent, dump, present, restore ]
- default: present
- target:
- description:
- - File to back up or restore from.
- - Used when I(state) is C(dump) or C(restore).
- type: path
- version_added: '2.4'
- target_opts:
- description:
- - Further arguments for pg_dump or pg_restore.
- - Used when I(state) is C(dump) or C(restore).
- type: str
- version_added: '2.4'
- maintenance_db:
- description:
- - The value specifies the initial database (which is also called as maintenance DB) that Ansible connects to.
- type: str
- default: postgres
- version_added: '2.5'
- conn_limit:
- description:
- - Specifies the database connection limit.
- type: str
- version_added: '2.8'
- tablespace:
- description:
- - The tablespace to set for the database
- U(https://www.postgresql.org/docs/current/sql-alterdatabase.html).
- - If you want to move the database back to the default tablespace,
- explicitly set this to pg_default.
- type: path
- version_added: '2.9'
- dump_extra_args:
- description:
- - Provides additional arguments when I(state) is C(dump).
- - Cannot be used with dump-file-format-related arguments like ``--format=d``.
- type: str
- version_added: '2.10'
-seealso:
-- name: CREATE DATABASE reference
- description: Complete reference of the CREATE DATABASE command documentation.
- link: https://www.postgresql.org/docs/current/sql-createdatabase.html
-- name: DROP DATABASE reference
- description: Complete reference of the DROP DATABASE command documentation.
- link: https://www.postgresql.org/docs/current/sql-dropdatabase.html
-- name: pg_dump reference
- description: Complete reference of pg_dump documentation.
- link: https://www.postgresql.org/docs/current/app-pgdump.html
-- name: pg_restore reference
- description: Complete reference of pg_restore documentation.
- link: https://www.postgresql.org/docs/current/app-pgrestore.html
-- module: postgresql_tablespace
-- module: postgresql_info
-- module: postgresql_ping
-notes:
-- State C(dump) and C(restore) don't require I(psycopg2) since version 2.8.
-author: "Ansible Core Team"
-extends_documentation_fragment:
-- postgres
-'''
-
-EXAMPLES = r'''
-- name: Create a new database with name "acme"
- postgresql_db:
- name: acme
-
-# Note: If a template different from "template0" is specified, encoding and locale settings must match those of the template.
-- name: Create a new database with name "acme" and specific encoding and locale # settings.
- postgresql_db:
- name: acme
- encoding: UTF-8
- lc_collate: de_DE.UTF-8
- lc_ctype: de_DE.UTF-8
- template: template0
-
-# Note: Default limit for the number of concurrent connections to a specific database is "-1", which means "unlimited"
-- name: Create a new database with name "acme" which has a limit of 100 concurrent connections
- postgresql_db:
- name: acme
- conn_limit: "100"
-
-- name: Dump an existing database to a file
- postgresql_db:
- name: acme
- state: dump
- target: /tmp/acme.sql
-
-- name: Dump an existing database to a file excluding the test table
- postgresql_db:
- name: acme
- state: dump
- target: /tmp/acme.sql
- dump_extra_args: --exclude-table=test
-
-- name: Dump an existing database to a file (with compression)
- postgresql_db:
- name: acme
- state: dump
- target: /tmp/acme.sql.gz
-
-- name: Dump a single schema for an existing database
- postgresql_db:
- name: acme
- state: dump
- target: /tmp/acme.sql
- target_opts: "-n public"
-
-# Note: In the example below, if database foo exists and has another tablespace
-# the tablespace will be changed to foo. Access to the database will be locked
-# until the copying of database files is finished.
-- name: Create a new database called foo in tablespace bar
- postgresql_db:
- name: foo
- tablespace: bar
-'''
-
-RETURN = r'''
-executed_commands:
- description: List of commands which tried to run.
- returned: always
- type: list
- sample: ["CREATE DATABASE acme"]
- version_added: '2.10'
-'''
-
-
-import os
-import subprocess
-import traceback
-
-try:
- import psycopg2
- import psycopg2.extras
-except ImportError:
- HAS_PSYCOPG2 = False
-else:
- HAS_PSYCOPG2 = True
-
-import ansible.module_utils.postgres as pgutils
-from ansible.module_utils.basic import AnsibleModule
-from ansible.module_utils.database import SQLParseError, pg_quote_identifier
-from ansible.module_utils.six import iteritems
-from ansible.module_utils.six.moves import shlex_quote
-from ansible.module_utils._text import to_native
-
-executed_commands = []
-
-
-class NotSupportedError(Exception):
- pass
-
-# ===========================================
-# PostgreSQL module specific support methods.
-#
-
-
-def set_owner(cursor, db, owner):
- query = 'ALTER DATABASE %s OWNER TO "%s"' % (
- pg_quote_identifier(db, 'database'),
- owner)
- executed_commands.append(query)
- cursor.execute(query)
- return True
-
-
-def set_conn_limit(cursor, db, conn_limit):
- query = "ALTER DATABASE %s CONNECTION LIMIT %s" % (
- pg_quote_identifier(db, 'database'),
- conn_limit)
- executed_commands.append(query)
- cursor.execute(query)
- return True
-
-
-def get_encoding_id(cursor, encoding):
- query = "SELECT pg_char_to_encoding(%(encoding)s) AS encoding_id;"
- cursor.execute(query, {'encoding': encoding})
- return cursor.fetchone()['encoding_id']
-
-
-def get_db_info(cursor, db):
- query = """
- SELECT rolname AS owner,
- pg_encoding_to_char(encoding) AS encoding, encoding AS encoding_id,
- datcollate AS lc_collate, datctype AS lc_ctype, pg_database.datconnlimit AS conn_limit,
- spcname AS tablespace
- FROM pg_database
- JOIN pg_roles ON pg_roles.oid = pg_database.datdba
- JOIN pg_tablespace ON pg_tablespace.oid = pg_database.dattablespace
- WHERE datname = %(db)s
- """
- cursor.execute(query, {'db': db})
- return cursor.fetchone()
-
-
-def db_exists(cursor, db):
- query = "SELECT * FROM pg_database WHERE datname=%(db)s"
- cursor.execute(query, {'db': db})
- return cursor.rowcount == 1
-
-
-def db_delete(cursor, db):
- if db_exists(cursor, db):
- query = "DROP DATABASE %s" % pg_quote_identifier(db, 'database')
- executed_commands.append(query)
- cursor.execute(query)
- return True
- else:
- return False
-
-
-def db_create(cursor, db, owner, template, encoding, lc_collate, lc_ctype, conn_limit, tablespace):
- params = dict(enc=encoding, collate=lc_collate, ctype=lc_ctype, conn_limit=conn_limit, tablespace=tablespace)
- if not db_exists(cursor, db):
- query_fragments = ['CREATE DATABASE %s' % pg_quote_identifier(db, 'database')]
- if owner:
- query_fragments.append('OWNER "%s"' % owner)
- if template:
- query_fragments.append('TEMPLATE %s' % pg_quote_identifier(template, 'database'))
- if encoding:
- query_fragments.append('ENCODING %(enc)s')
- if lc_collate:
- query_fragments.append('LC_COLLATE %(collate)s')
- if lc_ctype:
- query_fragments.append('LC_CTYPE %(ctype)s')
- if tablespace:
- query_fragments.append('TABLESPACE %s' % pg_quote_identifier(tablespace, 'tablespace'))
- if conn_limit:
- query_fragments.append("CONNECTION LIMIT %(conn_limit)s" % {"conn_limit": conn_limit})
- query = ' '.join(query_fragments)
- executed_commands.append(cursor.mogrify(query, params))
- cursor.execute(query, params)
- return True
- else:
- db_info = get_db_info(cursor, db)
- if (encoding and get_encoding_id(cursor, encoding) != db_info['encoding_id']):
- raise NotSupportedError(
- 'Changing database encoding is not supported. '
- 'Current encoding: %s' % db_info['encoding']
- )
- elif lc_collate and lc_collate != db_info['lc_collate']:
- raise NotSupportedError(
- 'Changing LC_COLLATE is not supported. '
- 'Current LC_COLLATE: %s' % db_info['lc_collate']
- )
- elif lc_ctype and lc_ctype != db_info['lc_ctype']:
- raise NotSupportedError(
- 'Changing LC_CTYPE is not supported.'
- 'Current LC_CTYPE: %s' % db_info['lc_ctype']
- )
- else:
- changed = False
-
- if owner and owner != db_info['owner']:
- changed = set_owner(cursor, db, owner)
-
- if conn_limit and conn_limit != str(db_info['conn_limit']):
- changed = set_conn_limit(cursor, db, conn_limit)
-
- if tablespace and tablespace != db_info['tablespace']:
- changed = set_tablespace(cursor, db, tablespace)
-
- return changed
-
-
-def db_matches(cursor, db, owner, template, encoding, lc_collate, lc_ctype, conn_limit, tablespace):
- if not db_exists(cursor, db):
- return False
- else:
- db_info = get_db_info(cursor, db)
- if (encoding and get_encoding_id(cursor, encoding) != db_info['encoding_id']):
- return False
- elif lc_collate and lc_collate != db_info['lc_collate']:
- return False
- elif lc_ctype and lc_ctype != db_info['lc_ctype']:
- return False
- elif owner and owner != db_info['owner']:
- return False
- elif conn_limit and conn_limit != str(db_info['conn_limit']):
- return False
- elif tablespace and tablespace != db_info['tablespace']:
- return False
- else:
- return True
-
-
-def db_dump(module, target, target_opts="",
- db=None,
- dump_extra_args=None,
- user=None,
- password=None,
- host=None,
- port=None,
- **kw):
-
- flags = login_flags(db, host, port, user, db_prefix=False)
- cmd = module.get_bin_path('pg_dump', True)
- comp_prog_path = None
-
- if os.path.splitext(target)[-1] == '.tar':
- flags.append(' --format=t')
- elif os.path.splitext(target)[-1] == '.pgc':
- flags.append(' --format=c')
- if os.path.splitext(target)[-1] == '.gz':
- if module.get_bin_path('pigz'):
- comp_prog_path = module.get_bin_path('pigz', True)
- else:
- comp_prog_path = module.get_bin_path('gzip', True)
- elif os.path.splitext(target)[-1] == '.bz2':
- comp_prog_path = module.get_bin_path('bzip2', True)
- elif os.path.splitext(target)[-1] == '.xz':
- comp_prog_path = module.get_bin_path('xz', True)
-
- cmd += "".join(flags)
-
- if dump_extra_args:
- cmd += " {0} ".format(dump_extra_args)
-
- if target_opts:
- cmd += " {0} ".format(target_opts)
-
- if comp_prog_path:
- # Use a fifo to be notified of an error in pg_dump
- # Using shell pipe has no way to return the code of the first command
- # in a portable way.
- fifo = os.path.join(module.tmpdir, 'pg_fifo')
- os.mkfifo(fifo)
- cmd = '{1} <{3} > {2} & {0} >{3}'.format(cmd, comp_prog_path, shlex_quote(target), fifo)
- else:
- cmd = '{0} > {1}'.format(cmd, shlex_quote(target))
-
- return do_with_password(module, cmd, password)
-
-
-def db_restore(module, target, target_opts="",
- db=None,
- user=None,
- password=None,
- host=None,
- port=None,
- **kw):
-
- flags = login_flags(db, host, port, user)
- comp_prog_path = None
- cmd = module.get_bin_path('psql', True)
-
- if os.path.splitext(target)[-1] == '.sql':
- flags.append(' --file={0}'.format(target))
-
- elif os.path.splitext(target)[-1] == '.tar':
- flags.append(' --format=Tar')
- cmd = module.get_bin_path('pg_restore', True)
-
- elif os.path.splitext(target)[-1] == '.pgc':
- flags.append(' --format=Custom')
- cmd = module.get_bin_path('pg_restore', True)
-
- elif os.path.splitext(target)[-1] == '.gz':
- comp_prog_path = module.get_bin_path('zcat', True)
-
- elif os.path.splitext(target)[-1] == '.bz2':
- comp_prog_path = module.get_bin_path('bzcat', True)
-
- elif os.path.splitext(target)[-1] == '.xz':
- comp_prog_path = module.get_bin_path('xzcat', True)
-
- cmd += "".join(flags)
- if target_opts:
- cmd += " {0} ".format(target_opts)
-
- if comp_prog_path:
- env = os.environ.copy()
- if password:
- env = {"PGPASSWORD": password}
- p1 = subprocess.Popen([comp_prog_path, target], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- p2 = subprocess.Popen(cmd, stdin=p1.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, env=env)
- (stdout2, stderr2) = p2.communicate()
- p1.stdout.close()
- p1.wait()
- if p1.returncode != 0:
- stderr1 = p1.stderr.read()
- return p1.returncode, '', stderr1, 'cmd: ****'
- else:
- return p2.returncode, '', stderr2, 'cmd: ****'
- else:
- cmd = '{0} < {1}'.format(cmd, shlex_quote(target))
-
- return do_with_password(module, cmd, password)
-
-
-def login_flags(db, host, port, user, db_prefix=True):
- """
- returns a list of connection argument strings each prefixed
- with a space and quoted where necessary to later be combined
- in a single shell string with `"".join(rv)`
-
- db_prefix determines if "--dbname" is prefixed to the db argument,
- since the argument was introduced in 9.3.
- """
- flags = []
- if db:
- if db_prefix:
- flags.append(' --dbname={0}'.format(shlex_quote(db)))
- else:
- flags.append(' {0}'.format(shlex_quote(db)))
- if host:
- flags.append(' --host={0}'.format(host))
- if port:
- flags.append(' --port={0}'.format(port))
- if user:
- flags.append(' --username={0}'.format(user))
- return flags
-
-
-def do_with_password(module, cmd, password):
- env = {}
- if password:
- env = {"PGPASSWORD": password}
- executed_commands.append(cmd)
- rc, stderr, stdout = module.run_command(cmd, use_unsafe_shell=True, environ_update=env)
- return rc, stderr, stdout, cmd
-
-
-def set_tablespace(cursor, db, tablespace):
- query = "ALTER DATABASE %s SET TABLESPACE %s" % (
- pg_quote_identifier(db, 'database'),
- pg_quote_identifier(tablespace, 'tablespace'))
- executed_commands.append(query)
- cursor.execute(query)
- return True
-
-# ===========================================
-# Module execution.
-#
-
-
-def main():
- argument_spec = pgutils.postgres_common_argument_spec()
- argument_spec.update(
- db=dict(type='str', required=True, aliases=['name']),
- owner=dict(type='str', default=''),
- template=dict(type='str', default=''),
- encoding=dict(type='str', default=''),
- lc_collate=dict(type='str', default=''),
- lc_ctype=dict(type='str', default=''),
- state=dict(type='str', default='present', choices=['absent', 'dump', 'present', 'restore']),
- target=dict(type='path', default=''),
- target_opts=dict(type='str', default=''),
- maintenance_db=dict(type='str', default="postgres"),
- session_role=dict(type='str'),
- conn_limit=dict(type='str', default=''),
- tablespace=dict(type='path', default=''),
- dump_extra_args=dict(type='str', default=None),
- )
-
- module = AnsibleModule(
- argument_spec=argument_spec,
- supports_check_mode=True
- )
-
- db = module.params["db"]
- owner = module.params["owner"]
- template = module.params["template"]
- encoding = module.params["encoding"]
- lc_collate = module.params["lc_collate"]
- lc_ctype = module.params["lc_ctype"]
- target = module.params["target"]
- target_opts = module.params["target_opts"]
- state = module.params["state"]
- changed = False
- maintenance_db = module.params['maintenance_db']
- session_role = module.params["session_role"]
- conn_limit = module.params['conn_limit']
- tablespace = module.params['tablespace']
- dump_extra_args = module.params['dump_extra_args']
-
- raw_connection = state in ("dump", "restore")
-
- if not raw_connection:
- pgutils.ensure_required_libs(module)
-
- # To use defaults values, keyword arguments must be absent, so
- # check which values are empty and don't include in the **kw
- # dictionary
- params_map = {
- "login_host": "host",
- "login_user": "user",
- "login_password": "password",
- "port": "port",
- "ssl_mode": "sslmode",
- "ca_cert": "sslrootcert"
- }
- kw = dict((params_map[k], v) for (k, v) in iteritems(module.params)
- if k in params_map and v != '' and v is not None)
-
- # If a login_unix_socket is specified, incorporate it here.
- is_localhost = "host" not in kw or kw["host"] == "" or kw["host"] == "localhost"
-
- if is_localhost and module.params["login_unix_socket"] != "":
- kw["host"] = module.params["login_unix_socket"]
-
- if target == "":
- target = "{0}/{1}.sql".format(os.getcwd(), db)
- target = os.path.expanduser(target)
-
- if not raw_connection:
- try:
- db_connection = psycopg2.connect(database=maintenance_db, **kw)
-
- # Enable autocommit so we can create databases
- if psycopg2.__version__ >= '2.4.2':
- db_connection.autocommit = True
- else:
- db_connection.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
- cursor = db_connection.cursor(cursor_factory=psycopg2.extras.DictCursor)
-
- except TypeError as e:
- if 'sslrootcert' in e.args[0]:
- module.fail_json(msg='Postgresql server must be at least version 8.4 to support sslrootcert. Exception: {0}'.format(to_native(e)),
- exception=traceback.format_exc())
- module.fail_json(msg="unable to connect to database: %s" % to_native(e), exception=traceback.format_exc())
-
- except Exception as e:
- module.fail_json(msg="unable to connect to database: %s" % to_native(e), exception=traceback.format_exc())
-
- if session_role:
- try:
- cursor.execute('SET ROLE "%s"' % session_role)
- except Exception as e:
- module.fail_json(msg="Could not switch role: %s" % to_native(e), exception=traceback.format_exc())
-
- try:
- if module.check_mode:
- if state == "absent":
- changed = db_exists(cursor, db)
- elif state == "present":
- changed = not db_matches(cursor, db, owner, template, encoding, lc_collate, lc_ctype, conn_limit, tablespace)
- module.exit_json(changed=changed, db=db, executed_commands=executed_commands)
-
- if state == "absent":
- try:
- changed = db_delete(cursor, db)
- except SQLParseError as e:
- module.fail_json(msg=to_native(e), exception=traceback.format_exc())
-
- elif state == "present":
- try:
- changed = db_create(cursor, db, owner, template, encoding, lc_collate, lc_ctype, conn_limit, tablespace)
- except SQLParseError as e:
- module.fail_json(msg=to_native(e), exception=traceback.format_exc())
-
- elif state in ("dump", "restore"):
- method = state == "dump" and db_dump or db_restore
- try:
- if state == 'dump':
- rc, stdout, stderr, cmd = method(module, target, target_opts, db, dump_extra_args, **kw)
- else:
- rc, stdout, stderr, cmd = method(module, target, target_opts, db, **kw)
-
- if rc != 0:
- module.fail_json(msg=stderr, stdout=stdout, rc=rc, cmd=cmd)
- else:
- module.exit_json(changed=True, msg=stdout, stderr=stderr, rc=rc, cmd=cmd,
- executed_commands=executed_commands)
- except SQLParseError as e:
- module.fail_json(msg=to_native(e), exception=traceback.format_exc())
-
- except NotSupportedError as e:
- module.fail_json(msg=to_native(e), exception=traceback.format_exc())
- except SystemExit:
- # Avoid catching this on Python 2.4
- raise
- except Exception as e:
- module.fail_json(msg="Database query failed: %s" % to_native(e), exception=traceback.format_exc())
-
- module.exit_json(changed=changed, db=db, executed_commands=executed_commands)
-
-
-if __name__ == '__main__':
- main()
diff --git a/test/support/integration/plugins/modules/postgresql_privs.py b/test/support/integration/plugins/modules/postgresql_privs.py
deleted file mode 100644
index ba8324dde6..0000000000
--- a/test/support/integration/plugins/modules/postgresql_privs.py
+++ /dev/null
@@ -1,1097 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Copyright: Ansible Project
-# Copyright: (c) 2019, Tobias Birkefeld (@tcraxs) <t@craxs.de>
-# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-ANSIBLE_METADATA = {'metadata_version': '1.1',
- 'status': ['stableinterface'],
- 'supported_by': 'community'}
-
-DOCUMENTATION = r'''
----
-module: postgresql_privs
-version_added: '1.2'
-short_description: Grant or revoke privileges on PostgreSQL database objects
-description:
-- Grant or revoke privileges on PostgreSQL database objects.
-- This module is basically a wrapper around most of the functionality of
- PostgreSQL's GRANT and REVOKE statements with detection of changes
- (GRANT/REVOKE I(privs) ON I(type) I(objs) TO/FROM I(roles)).
-options:
- database:
- description:
- - Name of database to connect to.
- required: yes
- type: str
- aliases:
- - db
- - login_db
- state:
- description:
- - If C(present), the specified privileges are granted, if C(absent) they are revoked.
- type: str
- default: present
- choices: [ absent, present ]
- privs:
- description:
- - Comma separated list of privileges to grant/revoke.
- type: str
- aliases:
- - priv
- type:
- description:
- - Type of database object to set privileges on.
- - The C(default_privs) choice is available starting at version 2.7.
- - The C(foreign_data_wrapper) and C(foreign_server) object types are available from Ansible version '2.8'.
- - The C(type) choice is available from Ansible version '2.10'.
- type: str
- default: table
- choices: [ database, default_privs, foreign_data_wrapper, foreign_server, function,
- group, language, table, tablespace, schema, sequence, type ]
- objs:
- description:
- - Comma separated list of database objects to set privileges on.
- - If I(type) is C(table), C(partition table), C(sequence) or C(function),
- the special valueC(ALL_IN_SCHEMA) can be provided instead to specify all
- database objects of type I(type) in the schema specified via I(schema).
- (This also works with PostgreSQL < 9.0.) (C(ALL_IN_SCHEMA) is available
- for C(function) and C(partition table) from version 2.8)
- - If I(type) is C(database), this parameter can be omitted, in which case
- privileges are set for the database specified via I(database).
- - 'If I(type) is I(function), colons (":") in object names will be
- replaced with commas (needed to specify function signatures, see examples)'
- type: str
- aliases:
- - obj
- schema:
- description:
- - Schema that contains the database objects specified via I(objs).
- - May only be provided if I(type) is C(table), C(sequence), C(function), C(type),
- or C(default_privs). Defaults to C(public) in these cases.
- - Pay attention, for embedded types when I(type=type)
- I(schema) can be C(pg_catalog) or C(information_schema) respectively.
- type: str
- roles:
- description:
- - Comma separated list of role (user/group) names to set permissions for.
- - The special value C(PUBLIC) can be provided instead to set permissions
- for the implicitly defined PUBLIC group.
- type: str
- required: yes
- aliases:
- - role
- fail_on_role:
- version_added: '2.8'
- description:
- - If C(yes), fail when target role (for whom privs need to be granted) does not exist.
- Otherwise just warn and continue.
- default: yes
- type: bool
- session_role:
- version_added: '2.8'
- description:
- - Switch to session_role after connecting.
- - The specified session_role must be a role that the current login_user is a member of.
- - Permissions checking for SQL commands is carried out as though the session_role were the one that had logged in originally.
- type: str
- target_roles:
- description:
- - A list of existing role (user/group) names to set as the
- default permissions for database objects subsequently created by them.
- - Parameter I(target_roles) is only available with C(type=default_privs).
- type: str
- version_added: '2.8'
- grant_option:
- description:
- - Whether C(role) may grant/revoke the specified privileges/group memberships to others.
- - Set to C(no) to revoke GRANT OPTION, leave unspecified to make no changes.
- - I(grant_option) only has an effect if I(state) is C(present).
- type: bool
- aliases:
- - admin_option
- host:
- description:
- - Database host address. If unspecified, connect via Unix socket.
- type: str
- aliases:
- - login_host
- port:
- description:
- - Database port to connect to.
- type: int
- default: 5432
- aliases:
- - login_port
- unix_socket:
- description:
- - Path to a Unix domain socket for local connections.
- type: str
- aliases:
- - login_unix_socket
- login:
- description:
- - The username to authenticate with.
- type: str
- default: postgres
- aliases:
- - login_user
- password:
- description:
- - The password to authenticate with.
- type: str
- aliases:
- - login_password
- ssl_mode:
- description:
- - Determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server.
- - See https://www.postgresql.org/docs/current/static/libpq-ssl.html for more information on the modes.
- - Default of C(prefer) matches libpq default.
- type: str
- default: prefer
- choices: [ allow, disable, prefer, require, verify-ca, verify-full ]
- version_added: '2.3'
- ca_cert:
- description:
- - Specifies the name of a file containing SSL certificate authority (CA) certificate(s).
- - If the file exists, the server's certificate will be verified to be signed by one of these authorities.
- version_added: '2.3'
- type: str
- aliases:
- - ssl_rootcert
-
-notes:
-- Parameters that accept comma separated lists (I(privs), I(objs), I(roles))
- have singular alias names (I(priv), I(obj), I(role)).
-- To revoke only C(GRANT OPTION) for a specific object, set I(state) to
- C(present) and I(grant_option) to C(no) (see examples).
-- Note that when revoking privileges from a role R, this role may still have
- access via privileges granted to any role R is a member of including C(PUBLIC).
-- Note that when revoking privileges from a role R, you do so as the user
- specified via I(login). If R has been granted the same privileges by
- another user also, R can still access database objects via these privileges.
-- When revoking privileges, C(RESTRICT) is assumed (see PostgreSQL docs).
-
-seealso:
-- module: postgresql_user
-- module: postgresql_owner
-- module: postgresql_membership
-- name: PostgreSQL privileges
- description: General information about PostgreSQL privileges.
- link: https://www.postgresql.org/docs/current/ddl-priv.html
-- name: PostgreSQL GRANT command reference
- description: Complete reference of the PostgreSQL GRANT command documentation.
- link: https://www.postgresql.org/docs/current/sql-grant.html
-- name: PostgreSQL REVOKE command reference
- description: Complete reference of the PostgreSQL REVOKE command documentation.
- link: https://www.postgresql.org/docs/current/sql-revoke.html
-
-extends_documentation_fragment:
-- postgres
-
-author:
-- Bernhard Weitzhofer (@b6d)
-- Tobias Birkefeld (@tcraxs)
-'''
-
-EXAMPLES = r'''
-# On database "library":
-# GRANT SELECT, INSERT, UPDATE ON TABLE public.books, public.authors
-# TO librarian, reader WITH GRANT OPTION
-- name: Grant privs to librarian and reader on database library
- postgresql_privs:
- database: library
- state: present
- privs: SELECT,INSERT,UPDATE
- type: table
- objs: books,authors
- schema: public
- roles: librarian,reader
- grant_option: yes
-
-- name: Same as above leveraging default values
- postgresql_privs:
- db: library
- privs: SELECT,INSERT,UPDATE
- objs: books,authors
- roles: librarian,reader
- grant_option: yes
-
-# REVOKE GRANT OPTION FOR INSERT ON TABLE books FROM reader
-# Note that role "reader" will be *granted* INSERT privilege itself if this
-# isn't already the case (since state: present).
-- name: Revoke privs from reader
- postgresql_privs:
- db: library
- state: present
- priv: INSERT
- obj: books
- role: reader
- grant_option: no
-
-# "public" is the default schema. This also works for PostgreSQL 8.x.
-- name: REVOKE INSERT, UPDATE ON ALL TABLES IN SCHEMA public FROM reader
- postgresql_privs:
- db: library
- state: absent
- privs: INSERT,UPDATE
- objs: ALL_IN_SCHEMA
- role: reader
-
-- name: GRANT ALL PRIVILEGES ON SCHEMA public, math TO librarian
- postgresql_privs:
- db: library
- privs: ALL
- type: schema
- objs: public,math
- role: librarian
-
-# Note the separation of arguments with colons.
-- name: GRANT ALL PRIVILEGES ON FUNCTION math.add(int, int) TO librarian, reader
- postgresql_privs:
- db: library
- privs: ALL
- type: function
- obj: add(int:int)
- schema: math
- roles: librarian,reader
-
-# Note that group role memberships apply cluster-wide and therefore are not
-# restricted to database "library" here.
-- name: GRANT librarian, reader TO alice, bob WITH ADMIN OPTION
- postgresql_privs:
- db: library
- type: group
- objs: librarian,reader
- roles: alice,bob
- admin_option: yes
-
-# Note that here "db: postgres" specifies the database to connect to, not the
-# database to grant privileges on (which is specified via the "objs" param)
-- name: GRANT ALL PRIVILEGES ON DATABASE library TO librarian
- postgresql_privs:
- db: postgres
- privs: ALL
- type: database
- obj: library
- role: librarian
-
-# If objs is omitted for type "database", it defaults to the database
-# to which the connection is established
-- name: GRANT ALL PRIVILEGES ON DATABASE library TO librarian
- postgresql_privs:
- db: library
- privs: ALL
- type: database
- role: librarian
-
-# Available since version 2.7
-# Objs must be set, ALL_DEFAULT to TABLES/SEQUENCES/TYPES/FUNCTIONS
-# ALL_DEFAULT works only with privs=ALL
-# For specific
-- name: ALTER DEFAULT PRIVILEGES ON DATABASE library TO librarian
- postgresql_privs:
- db: library
- objs: ALL_DEFAULT
- privs: ALL
- type: default_privs
- role: librarian
- grant_option: yes
-
-# Available since version 2.7
-# Objs must be set, ALL_DEFAULT to TABLES/SEQUENCES/TYPES/FUNCTIONS
-# ALL_DEFAULT works only with privs=ALL
-# For specific
-- name: ALTER DEFAULT PRIVILEGES ON DATABASE library TO reader, step 1
- postgresql_privs:
- db: library
- objs: TABLES,SEQUENCES
- privs: SELECT
- type: default_privs
- role: reader
-
-- name: ALTER DEFAULT PRIVILEGES ON DATABASE library TO reader, step 2
- postgresql_privs:
- db: library
- objs: TYPES
- privs: USAGE
- type: default_privs
- role: reader
-
-# Available since version 2.8
-- name: GRANT ALL PRIVILEGES ON FOREIGN DATA WRAPPER fdw TO reader
- postgresql_privs:
- db: test
- objs: fdw
- privs: ALL
- type: foreign_data_wrapper
- role: reader
-
-# Available since version 2.10
-- name: GRANT ALL PRIVILEGES ON TYPE customtype TO reader
- postgresql_privs:
- db: test
- objs: customtype
- privs: ALL
- type: type
- role: reader
-
-# Available since version 2.8
-- name: GRANT ALL PRIVILEGES ON FOREIGN SERVER fdw_server TO reader
- postgresql_privs:
- db: test
- objs: fdw_server
- privs: ALL
- type: foreign_server
- role: reader
-
-# Available since version 2.8
-# Grant 'execute' permissions on all functions in schema 'common' to role 'caller'
-- name: GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA common TO caller
- postgresql_privs:
- type: function
- state: present
- privs: EXECUTE
- roles: caller
- objs: ALL_IN_SCHEMA
- schema: common
-
-# Available since version 2.8
-# ALTER DEFAULT PRIVILEGES FOR ROLE librarian IN SCHEMA library GRANT SELECT ON TABLES TO reader
-# GRANT SELECT privileges for new TABLES objects created by librarian as
-# default to the role reader.
-# For specific
-- name: ALTER privs
- postgresql_privs:
- db: library
- schema: library
- objs: TABLES
- privs: SELECT
- type: default_privs
- role: reader
- target_roles: librarian
-
-# Available since version 2.8
-# ALTER DEFAULT PRIVILEGES FOR ROLE librarian IN SCHEMA library REVOKE SELECT ON TABLES FROM reader
-# REVOKE SELECT privileges for new TABLES objects created by librarian as
-# default from the role reader.
-# For specific
-- name: ALTER privs
- postgresql_privs:
- db: library
- state: absent
- schema: library
- objs: TABLES
- privs: SELECT
- type: default_privs
- role: reader
- target_roles: librarian
-
-# Available since version 2.10
-- name: Grant type privileges for pg_catalog.numeric type to alice
- postgresql_privs:
- type: type
- roles: alice
- privs: ALL
- objs: numeric
- schema: pg_catalog
- db: acme
-'''
-
-RETURN = r'''
-queries:
- description: List of executed queries.
- returned: always
- type: list
- sample: ['REVOKE GRANT OPTION FOR INSERT ON TABLE "books" FROM "reader";']
- version_added: '2.8'
-'''
-
-import traceback
-
-PSYCOPG2_IMP_ERR = None
-try:
- import psycopg2
- import psycopg2.extensions
-except ImportError:
- PSYCOPG2_IMP_ERR = traceback.format_exc()
- psycopg2 = None
-
-# import module snippets
-from ansible.module_utils.basic import AnsibleModule, missing_required_lib
-from ansible.module_utils.database import pg_quote_identifier
-from ansible.module_utils.postgres import postgres_common_argument_spec
-from ansible.module_utils._text import to_native
-
-VALID_PRIVS = frozenset(('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'TRUNCATE',
- 'REFERENCES', 'TRIGGER', 'CREATE', 'CONNECT',
- 'TEMPORARY', 'TEMP', 'EXECUTE', 'USAGE', 'ALL', 'USAGE'))
-VALID_DEFAULT_OBJS = {'TABLES': ('ALL', 'SELECT', 'INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'REFERENCES', 'TRIGGER'),
- 'SEQUENCES': ('ALL', 'SELECT', 'UPDATE', 'USAGE'),
- 'FUNCTIONS': ('ALL', 'EXECUTE'),
- 'TYPES': ('ALL', 'USAGE')}
-
-executed_queries = []
-
-
-class Error(Exception):
- pass
-
-
-def role_exists(module, cursor, rolname):
- """Check user exists or not"""
- query = "SELECT 1 FROM pg_roles WHERE rolname = '%s'" % rolname
- try:
- cursor.execute(query)
- return cursor.rowcount > 0
-
- except Exception as e:
- module.fail_json(msg="Cannot execute SQL '%s': %s" % (query, to_native(e)))
-
- return False
-
-
-# We don't have functools.partial in Python < 2.5
-def partial(f, *args, **kwargs):
- """Partial function application"""
-
- def g(*g_args, **g_kwargs):
- new_kwargs = kwargs.copy()
- new_kwargs.update(g_kwargs)
- return f(*(args + g_args), **g_kwargs)
-
- g.f = f
- g.args = args
- g.kwargs = kwargs
- return g
-
-
-class Connection(object):
- """Wrapper around a psycopg2 connection with some convenience methods"""
-
- def __init__(self, params, module):
- self.database = params.database
- self.module = module
- # To use defaults values, keyword arguments must be absent, so
- # check which values are empty and don't include in the **kw
- # dictionary
- params_map = {
- "host": "host",
- "login": "user",
- "password": "password",
- "port": "port",
- "database": "database",
- "ssl_mode": "sslmode",
- "ca_cert": "sslrootcert"
- }
-
- kw = dict((params_map[k], getattr(params, k)) for k in params_map
- if getattr(params, k) != '' and getattr(params, k) is not None)
-
- # If a unix_socket is specified, incorporate it here.
- is_localhost = "host" not in kw or kw["host"] == "" or kw["host"] == "localhost"
- if is_localhost and params.unix_socket != "":
- kw["host"] = params.unix_socket
-
- sslrootcert = params.ca_cert
- if psycopg2.__version__ < '2.4.3' and sslrootcert is not None:
- raise ValueError('psycopg2 must be at least 2.4.3 in order to user the ca_cert parameter')
-
- self.connection = psycopg2.connect(**kw)
- self.cursor = self.connection.cursor()
-
- def commit(self):
- self.connection.commit()
-
- def rollback(self):
- self.connection.rollback()
-
- @property
- def encoding(self):
- """Connection encoding in Python-compatible form"""
- return psycopg2.extensions.encodings[self.connection.encoding]
-
- # Methods for querying database objects
-
- # PostgreSQL < 9.0 doesn't support "ALL TABLES IN SCHEMA schema"-like
- # phrases in GRANT or REVOKE statements, therefore alternative methods are
- # provided here.
-
- def schema_exists(self, schema):
- query = """SELECT count(*)
- FROM pg_catalog.pg_namespace WHERE nspname = %s"""
- self.cursor.execute(query, (schema,))
- return self.cursor.fetchone()[0] > 0
-
- def get_all_tables_in_schema(self, schema):
- if not self.schema_exists(schema):
- raise Error('Schema "%s" does not exist.' % schema)
- query = """SELECT relname
- FROM pg_catalog.pg_class c
- JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
- WHERE nspname = %s AND relkind in ('r', 'v', 'm', 'p')"""
- self.cursor.execute(query, (schema,))
- return [t[0] for t in self.cursor.fetchall()]
-
- def get_all_sequences_in_schema(self, schema):
- if not self.schema_exists(schema):
- raise Error('Schema "%s" does not exist.' % schema)
- query = """SELECT relname
- FROM pg_catalog.pg_class c
- JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
- WHERE nspname = %s AND relkind = 'S'"""
- self.cursor.execute(query, (schema,))
- return [t[0] for t in self.cursor.fetchall()]
-
- def get_all_functions_in_schema(self, schema):
- if not self.schema_exists(schema):
- raise Error('Schema "%s" does not exist.' % schema)
- query = """SELECT p.proname, oidvectortypes(p.proargtypes)
- FROM pg_catalog.pg_proc p
- JOIN pg_namespace n ON n.oid = p.pronamespace
- WHERE nspname = %s"""
- self.cursor.execute(query, (schema,))
- return ["%s(%s)" % (t[0], t[1]) for t in self.cursor.fetchall()]
-
- # Methods for getting access control lists and group membership info
-
- # To determine whether anything has changed after granting/revoking
- # privileges, we compare the access control lists of the specified database
- # objects before and afterwards. Python's list/string comparison should
- # suffice for change detection, we should not actually have to parse ACLs.
- # The same should apply to group membership information.
-
- def get_table_acls(self, schema, tables):
- query = """SELECT relacl
- FROM pg_catalog.pg_class c
- JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
- WHERE nspname = %s AND relkind in ('r','p','v','m') AND relname = ANY (%s)
- ORDER BY relname"""
- self.cursor.execute(query, (schema, tables))
- return [t[0] for t in self.cursor.fetchall()]
-
- def get_sequence_acls(self, schema, sequences):
- query = """SELECT relacl
- FROM pg_catalog.pg_class c
- JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
- WHERE nspname = %s AND relkind = 'S' AND relname = ANY (%s)
- ORDER BY relname"""
- self.cursor.execute(query, (schema, sequences))
- return [t[0] for t in self.cursor.fetchall()]
-
- def get_function_acls(self, schema, function_signatures):
- funcnames = [f.split('(', 1)[0] for f in function_signatures]
- query = """SELECT proacl
- FROM pg_catalog.pg_proc p
- JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
- WHERE nspname = %s AND proname = ANY (%s)
- ORDER BY proname, proargtypes"""
- self.cursor.execute(query, (schema, funcnames))
- return [t[0] for t in self.cursor.fetchall()]
-
- def get_schema_acls(self, schemas):
- query = """SELECT nspacl FROM pg_catalog.pg_namespace
- WHERE nspname = ANY (%s) ORDER BY nspname"""
- self.cursor.execute(query, (schemas,))
- return [t[0] for t in self.cursor.fetchall()]
-
- def get_language_acls(self, languages):
- query = """SELECT lanacl FROM pg_catalog.pg_language
- WHERE lanname = ANY (%s) ORDER BY lanname"""
- self.cursor.execute(query, (languages,))
- return [t[0] for t in self.cursor.fetchall()]
-
- def get_tablespace_acls(self, tablespaces):
- query = """SELECT spcacl FROM pg_catalog.pg_tablespace
- WHERE spcname = ANY (%s) ORDER BY spcname"""
- self.cursor.execute(query, (tablespaces,))
- return [t[0] for t in self.cursor.fetchall()]
-
- def get_database_acls(self, databases):
- query = """SELECT datacl FROM pg_catalog.pg_database
- WHERE datname = ANY (%s) ORDER BY datname"""
- self.cursor.execute(query, (databases,))
- return [t[0] for t in self.cursor.fetchall()]
-
- def get_group_memberships(self, groups):
- query = """SELECT roleid, grantor, member, admin_option
- FROM pg_catalog.pg_auth_members am
- JOIN pg_catalog.pg_roles r ON r.oid = am.roleid
- WHERE r.rolname = ANY(%s)
- ORDER BY roleid, grantor, member"""
- self.cursor.execute(query, (groups,))
- return self.cursor.fetchall()
-
- def get_default_privs(self, schema, *args):
- query = """SELECT defaclacl
- FROM pg_default_acl a
- JOIN pg_namespace b ON a.defaclnamespace=b.oid
- WHERE b.nspname = %s;"""
- self.cursor.execute(query, (schema,))
- return [t[0] for t in self.cursor.fetchall()]
-
- def get_foreign_data_wrapper_acls(self, fdws):
- query = """SELECT fdwacl FROM pg_catalog.pg_foreign_data_wrapper
- WHERE fdwname = ANY (%s) ORDER BY fdwname"""
- self.cursor.execute(query, (fdws,))
- return [t[0] for t in self.cursor.fetchall()]
-
- def get_foreign_server_acls(self, fs):
- query = """SELECT srvacl FROM pg_catalog.pg_foreign_server
- WHERE srvname = ANY (%s) ORDER BY srvname"""
- self.cursor.execute(query, (fs,))
- return [t[0] for t in self.cursor.fetchall()]
-
- def get_type_acls(self, schema, types):
- query = """SELECT t.typacl FROM pg_catalog.pg_type t
- JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
- WHERE n.nspname = %s AND t.typname = ANY (%s) ORDER BY typname"""
- self.cursor.execute(query, (schema, types))
- return [t[0] for t in self.cursor.fetchall()]
-
- # Manipulating privileges
-
- def manipulate_privs(self, obj_type, privs, objs, roles, target_roles,
- state, grant_option, schema_qualifier=None, fail_on_role=True):
- """Manipulate database object privileges.
-
- :param obj_type: Type of database object to grant/revoke
- privileges for.
- :param privs: Either a list of privileges to grant/revoke
- or None if type is "group".
- :param objs: List of database objects to grant/revoke
- privileges for.
- :param roles: Either a list of role names or "PUBLIC"
- for the implicitly defined "PUBLIC" group
- :param target_roles: List of role names to grant/revoke
- default privileges as.
- :param state: "present" to grant privileges, "absent" to revoke.
- :param grant_option: Only for state "present": If True, set
- grant/admin option. If False, revoke it.
- If None, don't change grant option.
- :param schema_qualifier: Some object types ("TABLE", "SEQUENCE",
- "FUNCTION") must be qualified by schema.
- Ignored for other Types.
- """
- # get_status: function to get current status
- if obj_type == 'table':
- get_status = partial(self.get_table_acls, schema_qualifier)
- elif obj_type == 'sequence':
- get_status = partial(self.get_sequence_acls, schema_qualifier)
- elif obj_type == 'function':
- get_status = partial(self.get_function_acls, schema_qualifier)
- elif obj_type == 'schema':
- get_status = self.get_schema_acls
- elif obj_type == 'language':
- get_status = self.get_language_acls
- elif obj_type == 'tablespace':
- get_status = self.get_tablespace_acls
- elif obj_type == 'database':
- get_status = self.get_database_acls
- elif obj_type == 'group':
- get_status = self.get_group_memberships
- elif obj_type == 'default_privs':
- get_status = partial(self.get_default_privs, schema_qualifier)
- elif obj_type == 'foreign_data_wrapper':
- get_status = self.get_foreign_data_wrapper_acls
- elif obj_type == 'foreign_server':
- get_status = self.get_foreign_server_acls
- elif obj_type == 'type':
- get_status = partial(self.get_type_acls, schema_qualifier)
- else:
- raise Error('Unsupported database object type "%s".' % obj_type)
-
- # Return False (nothing has changed) if there are no objs to work on.
- if not objs:
- return False
-
- # obj_ids: quoted db object identifiers (sometimes schema-qualified)
- if obj_type == 'function':
- obj_ids = []
- for obj in objs:
- try:
- f, args = obj.split('(', 1)
- except Exception:
- raise Error('Illegal function signature: "%s".' % obj)
- obj_ids.append('"%s"."%s"(%s' % (schema_qualifier, f, args))
- elif obj_type in ['table', 'sequence', 'type']:
- obj_ids = ['"%s"."%s"' % (schema_qualifier, o) for o in objs]
- else:
- obj_ids = ['"%s"' % o for o in objs]
-
- # set_what: SQL-fragment specifying what to set for the target roles:
- # Either group membership or privileges on objects of a certain type
- if obj_type == 'group':
- set_what = ','.join('"%s"' % i for i in obj_ids)
- elif obj_type == 'default_privs':
- # We don't want privs to be quoted here
- set_what = ','.join(privs)
- else:
- # function types are already quoted above
- if obj_type != 'function':
- obj_ids = [pg_quote_identifier(i, 'table') for i in obj_ids]
- # Note: obj_type has been checked against a set of string literals
- # and privs was escaped when it was parsed
- # Note: Underscores are replaced with spaces to support multi-word obj_type
- set_what = '%s ON %s %s' % (','.join(privs), obj_type.replace('_', ' '),
- ','.join(obj_ids))
-
- # for_whom: SQL-fragment specifying for whom to set the above
- if roles == 'PUBLIC':
- for_whom = 'PUBLIC'
- else:
- for_whom = []
- for r in roles:
- if not role_exists(self.module, self.cursor, r):
- if fail_on_role:
- self.module.fail_json(msg="Role '%s' does not exist" % r.strip())
-
- else:
- self.module.warn("Role '%s' does not exist, pass it" % r.strip())
- else:
- for_whom.append('"%s"' % r)
-
- if not for_whom:
- return False
-
- for_whom = ','.join(for_whom)
-
- # as_who:
- as_who = None
- if target_roles:
- as_who = ','.join('"%s"' % r for r in target_roles)
-
- status_before = get_status(objs)
-
- query = QueryBuilder(state) \
- .for_objtype(obj_type) \
- .with_grant_option(grant_option) \
- .for_whom(for_whom) \
- .as_who(as_who) \
- .for_schema(schema_qualifier) \
- .set_what(set_what) \
- .for_objs(objs) \
- .build()
-
- executed_queries.append(query)
- self.cursor.execute(query)
- status_after = get_status(objs)
-
- def nonesorted(e):
- # For python 3+ that can fail trying
- # to compare NoneType elements by sort method.
- if e is None:
- return ''
- return e
-
- status_before.sort(key=nonesorted)
- status_after.sort(key=nonesorted)
- return status_before != status_after
-
-
-class QueryBuilder(object):
- def __init__(self, state):
- self._grant_option = None
- self._for_whom = None
- self._as_who = None
- self._set_what = None
- self._obj_type = None
- self._state = state
- self._schema = None
- self._objs = None
- self.query = []
-
- def for_objs(self, objs):
- self._objs = objs
- return self
-
- def for_schema(self, schema):
- self._schema = schema
- return self
-
- def with_grant_option(self, option):
- self._grant_option = option
- return self
-
- def for_whom(self, who):
- self._for_whom = who
- return self
-
- def as_who(self, target_roles):
- self._as_who = target_roles
- return self
-
- def set_what(self, what):
- self._set_what = what
- return self
-
- def for_objtype(self, objtype):
- self._obj_type = objtype
- return self
-
- def build(self):
- if self._state == 'present':
- self.build_present()
- elif self._state == 'absent':
- self.build_absent()
- else:
- self.build_absent()
- return '\n'.join(self.query)
-
- def add_default_revoke(self):
- for obj in self._objs:
- if self._as_who:
- self.query.append(
- 'ALTER DEFAULT PRIVILEGES FOR ROLE {0} IN SCHEMA {1} REVOKE ALL ON {2} FROM {3};'.format(self._as_who,
- self._schema, obj,
- self._for_whom))
- else:
- self.query.append(
- 'ALTER DEFAULT PRIVILEGES IN SCHEMA {0} REVOKE ALL ON {1} FROM {2};'.format(self._schema, obj,
- self._for_whom))
-
- def add_grant_option(self):
- if self._grant_option:
- if self._obj_type == 'group':
- self.query[-1] += ' WITH ADMIN OPTION;'
- else:
- self.query[-1] += ' WITH GRANT OPTION;'
- else:
- self.query[-1] += ';'
- if self._obj_type == 'group':
- self.query.append('REVOKE ADMIN OPTION FOR {0} FROM {1};'.format(self._set_what, self._for_whom))
- elif not self._obj_type == 'default_privs':
- self.query.append('REVOKE GRANT OPTION FOR {0} FROM {1};'.format(self._set_what, self._for_whom))
-
- def add_default_priv(self):
- for obj in self._objs:
- if self._as_who:
- self.query.append(
- 'ALTER DEFAULT PRIVILEGES FOR ROLE {0} IN SCHEMA {1} GRANT {2} ON {3} TO {4}'.format(self._as_who,
- self._schema,
- self._set_what,
- obj,
- self._for_whom))
- else:
- self.query.append(
- 'ALTER DEFAULT PRIVILEGES IN SCHEMA {0} GRANT {1} ON {2} TO {3}'.format(self._schema,
- self._set_what,
- obj,
- self._for_whom))
- self.add_grant_option()
- if self._as_who:
- self.query.append(
- 'ALTER DEFAULT PRIVILEGES FOR ROLE {0} IN SCHEMA {1} GRANT USAGE ON TYPES TO {2}'.format(self._as_who,
- self._schema,
- self._for_whom))
- else:
- self.query.append(
- 'ALTER DEFAULT PRIVILEGES IN SCHEMA {0} GRANT USAGE ON TYPES TO {1}'.format(self._schema, self._for_whom))
- self.add_grant_option()
-
- def build_present(self):
- if self._obj_type == 'default_privs':
- self.add_default_revoke()
- self.add_default_priv()
- else:
- self.query.append('GRANT {0} TO {1}'.format(self._set_what, self._for_whom))
- self.add_grant_option()
-
- def build_absent(self):
- if self._obj_type == 'default_privs':
- self.query = []
- for obj in ['TABLES', 'SEQUENCES', 'TYPES']:
- if self._as_who:
- self.query.append(
- 'ALTER DEFAULT PRIVILEGES FOR ROLE {0} IN SCHEMA {1} REVOKE ALL ON {2} FROM {3};'.format(self._as_who,
- self._schema, obj,
- self._for_whom))
- else:
- self.query.append(
- 'ALTER DEFAULT PRIVILEGES IN SCHEMA {0} REVOKE ALL ON {1} FROM {2};'.format(self._schema, obj,
- self._for_whom))
- else:
- self.query.append('REVOKE {0} FROM {1};'.format(self._set_what, self._for_whom))
-
-
-def main():
- argument_spec = postgres_common_argument_spec()
- argument_spec.update(
- database=dict(required=True, aliases=['db', 'login_db']),
- state=dict(default='present', choices=['present', 'absent']),
- privs=dict(required=False, aliases=['priv']),
- type=dict(default='table',
- choices=['table',
- 'sequence',
- 'function',
- 'database',
- 'schema',
- 'language',
- 'tablespace',
- 'group',
- 'default_privs',
- 'foreign_data_wrapper',
- 'foreign_server',
- 'type', ]),
- objs=dict(required=False, aliases=['obj']),
- schema=dict(required=False),
- roles=dict(required=True, aliases=['role']),
- session_role=dict(required=False),
- target_roles=dict(required=False),
- grant_option=dict(required=False, type='bool',
- aliases=['admin_option']),
- host=dict(default='', aliases=['login_host']),
- unix_socket=dict(default='', aliases=['login_unix_socket']),
- login=dict(default='postgres', aliases=['login_user']),
- password=dict(default='', aliases=['login_password'], no_log=True),
- fail_on_role=dict(type='bool', default=True),
- )
-
- module = AnsibleModule(
- argument_spec=argument_spec,
- supports_check_mode=True,
- )
-
- fail_on_role = module.params['fail_on_role']
-
- # Create type object as namespace for module params
- p = type('Params', (), module.params)
- # param "schema": default, allowed depends on param "type"
- if p.type in ['table', 'sequence', 'function', 'type', 'default_privs']:
- p.schema = p.schema or 'public'
- elif p.schema:
- module.fail_json(msg='Argument "schema" is not allowed '
- 'for type "%s".' % p.type)
-
- # param "objs": default, required depends on param "type"
- if p.type == 'database':
- p.objs = p.objs or p.database
- elif not p.objs:
- module.fail_json(msg='Argument "objs" is required '
- 'for type "%s".' % p.type)
-
- # param "privs": allowed, required depends on param "type"
- if p.type == 'group':
- if p.privs:
- module.fail_json(msg='Argument "privs" is not allowed '
- 'for type "group".')
- elif not p.privs:
- module.fail_json(msg='Argument "privs" is required '
- 'for type "%s".' % p.type)
-
- # Connect to Database
- if not psycopg2:
- module.fail_json(msg=missing_required_lib('psycopg2'), exception=PSYCOPG2_IMP_ERR)
- try:
- conn = Connection(p, module)
- except psycopg2.Error as e:
- module.fail_json(msg='Could not connect to database: %s' % to_native(e), exception=traceback.format_exc())
- except TypeError as e:
- if 'sslrootcert' in e.args[0]:
- module.fail_json(msg='Postgresql server must be at least version 8.4 to support sslrootcert')
- module.fail_json(msg="unable to connect to database: %s" % to_native(e), exception=traceback.format_exc())
- except ValueError as e:
- # We raise this when the psycopg library is too old
- module.fail_json(msg=to_native(e))
-
- if p.session_role:
- try:
- conn.cursor.execute('SET ROLE "%s"' % p.session_role)
- except Exception as e:
- module.fail_json(msg="Could not switch to role %s: %s" % (p.session_role, to_native(e)), exception=traceback.format_exc())
-
- try:
- # privs
- if p.privs:
- privs = frozenset(pr.upper() for pr in p.privs.split(','))
- if not privs.issubset(VALID_PRIVS):
- module.fail_json(msg='Invalid privileges specified: %s' % privs.difference(VALID_PRIVS))
- else:
- privs = None
- # objs:
- if p.type == 'table' and p.objs == 'ALL_IN_SCHEMA':
- objs = conn.get_all_tables_in_schema(p.schema)
- elif p.type == 'sequence' and p.objs == 'ALL_IN_SCHEMA':
- objs = conn.get_all_sequences_in_schema(p.schema)
- elif p.type == 'function' and p.objs == 'ALL_IN_SCHEMA':
- objs = conn.get_all_functions_in_schema(p.schema)
- elif p.type == 'default_privs':
- if p.objs == 'ALL_DEFAULT':
- objs = frozenset(VALID_DEFAULT_OBJS.keys())
- else:
- objs = frozenset(obj.upper() for obj in p.objs.split(','))
- if not objs.issubset(VALID_DEFAULT_OBJS):
- module.fail_json(
- msg='Invalid Object set specified: %s' % objs.difference(VALID_DEFAULT_OBJS.keys()))
- # Again, do we have valid privs specified for object type:
- valid_objects_for_priv = frozenset(obj for obj in objs if privs.issubset(VALID_DEFAULT_OBJS[obj]))
- if not valid_objects_for_priv == objs:
- module.fail_json(
- msg='Invalid priv specified. Valid object for priv: {0}. Objects: {1}'.format(
- valid_objects_for_priv, objs))
- else:
- objs = p.objs.split(',')
-
- # function signatures are encoded using ':' to separate args
- if p.type == 'function':
- objs = [obj.replace(':', ',') for obj in objs]
-
- # roles
- if p.roles == 'PUBLIC':
- roles = 'PUBLIC'
- else:
- roles = p.roles.split(',')
-
- if len(roles) == 1 and not role_exists(module, conn.cursor, roles[0]):
- module.exit_json(changed=False)
-
- if fail_on_role:
- module.fail_json(msg="Role '%s' does not exist" % roles[0].strip())
-
- else:
- module.warn("Role '%s' does not exist, nothing to do" % roles[0].strip())
-
- # check if target_roles is set with type: default_privs
- if p.target_roles and not p.type == 'default_privs':
- module.warn('"target_roles" will be ignored '
- 'Argument "type: default_privs" is required for usage of "target_roles".')
-
- # target roles
- if p.target_roles:
- target_roles = p.target_roles.split(',')
- else:
- target_roles = None
-
- changed = conn.manipulate_privs(
- obj_type=p.type,
- privs=privs,
- objs=objs,
- roles=roles,
- target_roles=target_roles,
- state=p.state,
- grant_option=p.grant_option,
- schema_qualifier=p.schema,
- fail_on_role=fail_on_role,
- )
-
- except Error as e:
- conn.rollback()
- module.fail_json(msg=e.message, exception=traceback.format_exc())
-
- except psycopg2.Error as e:
- conn.rollback()
- module.fail_json(msg=to_native(e.message))
-
- if module.check_mode:
- conn.rollback()
- else:
- conn.commit()
- module.exit_json(changed=changed, queries=executed_queries)
-
-
-if __name__ == '__main__':
- main()
diff --git a/test/support/integration/plugins/modules/postgresql_query.py b/test/support/integration/plugins/modules/postgresql_query.py
deleted file mode 100644
index 18d63e332a..0000000000
--- a/test/support/integration/plugins/modules/postgresql_query.py
+++ /dev/null
@@ -1,364 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Copyright: (c) 2017, Felix Archambault
-# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru>
-# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-
-from __future__ import (absolute_import, division, print_function)
-__metaclass__ = type
-
-ANSIBLE_METADATA = {
- 'metadata_version': '1.1',
- 'supported_by': 'community',
- 'status': ['preview']
-}
-
-DOCUMENTATION = r'''
----
-module: postgresql_query
-short_description: Run PostgreSQL queries
-description:
-- Runs arbitrary PostgreSQL queries.
-- Can run queries from SQL script files.
-- Does not run against backup files. Use M(postgresql_db) with I(state=restore)
- to run queries on files made by pg_dump/pg_dumpall utilities.
-version_added: '2.8'
-options:
- query:
- description:
- - SQL query to run. Variables can be escaped with psycopg2 syntax
- U(http://initd.org/psycopg/docs/usage.html).
- type: str
- positional_args:
- description:
- - List of values to be passed as positional arguments to the query.
- When the value is a list, it will be converted to PostgreSQL array.
- - Mutually exclusive with I(named_args).
- type: list
- elements: raw
- named_args:
- description:
- - Dictionary of key-value arguments to pass to the query.
- When the value is a list, it will be converted to PostgreSQL array.
- - Mutually exclusive with I(positional_args).
- type: dict
- path_to_script:
- description:
- - Path to SQL script on the remote host.
- - Returns result of the last query in the script.
- - Mutually exclusive with I(query).
- type: path
- session_role:
- description:
- - Switch to session_role after connecting. The specified session_role must
- be a role that the current login_user is a member of.
- - Permissions checking for SQL commands is carried out as though
- the session_role were the one that had logged in originally.
- type: str
- db:
- description:
- - Name of database to connect to and run queries against.
- type: str
- aliases:
- - login_db
- autocommit:
- description:
- - Execute in autocommit mode when the query can't be run inside a transaction block
- (e.g., VACUUM).
- - Mutually exclusive with I(check_mode).
- type: bool
- default: no
- version_added: '2.9'
- encoding:
- description:
- - Set the client encoding for the current session (e.g. C(UTF-8)).
- - The default is the encoding defined by the database.
- type: str
- version_added: '2.10'
-seealso:
-- module: postgresql_db
-author:
-- Felix Archambault (@archf)
-- Andrew Klychkov (@Andersson007)
-- Will Rouesnel (@wrouesnel)
-extends_documentation_fragment: postgres
-'''
-
-EXAMPLES = r'''
-- name: Simple select query to acme db
- postgresql_query:
- db: acme
- query: SELECT version()
-
-- name: Select query to db acme with positional arguments and non-default credentials
- postgresql_query:
- db: acme
- login_user: django
- login_password: mysecretpass
- query: SELECT * FROM acme WHERE id = %s AND story = %s
- positional_args:
- - 1
- - test
-
-- name: Select query to test_db with named_args
- postgresql_query:
- db: test_db
- query: SELECT * FROM test WHERE id = %(id_val)s AND story = %(story_val)s
- named_args:
- id_val: 1
- story_val: test
-
-- name: Insert query to test_table in db test_db
- postgresql_query:
- db: test_db
- query: INSERT INTO test_table (id, story) VALUES (2, 'my_long_story')
-
-- name: Run queries from SQL script using UTF-8 client encoding for session
- postgresql_query:
- db: test_db
- path_to_script: /var/lib/pgsql/test.sql
- positional_args:
- - 1
- encoding: UTF-8
-
-- name: Example of using autocommit parameter
- postgresql_query:
- db: test_db
- query: VACUUM
- autocommit: yes
-
-- name: >
- Insert data to the column of array type using positional_args.
- Note that we use quotes here, the same as for passing JSON, etc.
- postgresql_query:
- query: INSERT INTO test_table (array_column) VALUES (%s)
- positional_args:
- - '{1,2,3}'
-
-# Pass list and string vars as positional_args
-- name: Set vars
- set_fact:
- my_list:
- - 1
- - 2
- - 3
- my_arr: '{1, 2, 3}'
-
-- name: Select from test table by passing positional_args as arrays
- postgresql_query:
- query: SELECT * FROM test_array_table WHERE arr_col1 = %s AND arr_col2 = %s
- positional_args:
- - '{{ my_list }}'
- - '{{ my_arr|string }}'
-'''
-
-RETURN = r'''
-query:
- description: Query that was tried to be executed.
- returned: always
- type: str
- sample: 'SELECT * FROM bar'
-statusmessage:
- description: Attribute containing the message returned by the command.
- returned: always
- type: str
- sample: 'INSERT 0 1'
-query_result:
- description:
- - List of dictionaries in column:value form representing returned rows.
- returned: changed
- type: list
- sample: [{"Column": "Value1"},{"Column": "Value2"}]
-rowcount:
- description: Number of affected rows.
- returned: changed
- type: int
- sample: 5
-'''
-
-try:
- from psycopg2 import ProgrammingError as Psycopg2ProgrammingError
- from psycopg2.extras import DictCursor
-except ImportError:
- # it is needed for checking 'no result to fetch' in main(),
- # psycopg2 availability will be checked by connect_to_db() into
- # ansible.module_utils.postgres
- pass
-
-from ansible.module_utils.basic import AnsibleModule
-from ansible.module_utils.postgres import (
- connect_to_db,
- get_conn_params,
- postgres_common_argument_spec,
-)
-from ansible.module_utils._text import to_native
-from ansible.module_utils.six import iteritems
-
-
-# ===========================================
-# Module execution.
-#
-
-def list_to_pg_array(elem):
- """Convert the passed list to PostgreSQL array
- represented as a string.
-
- Args:
- elem (list): List that needs to be converted.
-
- Returns:
- elem (str): String representation of PostgreSQL array.
- """
- elem = str(elem).strip('[]')
- elem = '{' + elem + '}'
- return elem
-
-
-def convert_elements_to_pg_arrays(obj):
- """Convert list elements of the passed object
- to PostgreSQL arrays represented as strings.
-
- Args:
- obj (dict or list): Object whose elements need to be converted.
-
- Returns:
- obj (dict or list): Object with converted elements.
- """
- if isinstance(obj, dict):
- for (key, elem) in iteritems(obj):
- if isinstance(elem, list):
- obj[key] = list_to_pg_array(elem)
-
- elif isinstance(obj, list):
- for i, elem in enumerate(obj):
- if isinstance(elem, list):
- obj[i] = list_to_pg_array(elem)
-
- return obj
-
-
-def main():
- argument_spec = postgres_common_argument_spec()
- argument_spec.update(
- query=dict(type='str'),
- db=dict(type='str', aliases=['login_db']),
- positional_args=dict(type='list', elements='raw'),
- named_args=dict(type='dict'),
- session_role=dict(type='str'),
- path_to_script=dict(type='path'),
- autocommit=dict(type='bool', default=False),
- encoding=dict(type='str'),
- )
-
- module = AnsibleModule(
- argument_spec=argument_spec,
- mutually_exclusive=(('positional_args', 'named_args'),),
- supports_check_mode=True,
- )
-
- query = module.params["query"]
- positional_args = module.params["positional_args"]
- named_args = module.params["named_args"]
- path_to_script = module.params["path_to_script"]
- autocommit = module.params["autocommit"]
- encoding = module.params["encoding"]
-
- if autocommit and module.check_mode:
- module.fail_json(msg="Using autocommit is mutually exclusive with check_mode")
-
- if path_to_script and query:
- module.fail_json(msg="path_to_script is mutually exclusive with query")
-
- if positional_args:
- positional_args = convert_elements_to_pg_arrays(positional_args)
-
- elif named_args:
- named_args = convert_elements_to_pg_arrays(named_args)
-
- if path_to_script:
- try:
- with open(path_to_script, 'rb') as f:
- query = to_native(f.read())
- except Exception as e:
- module.fail_json(msg="Cannot read file '%s' : %s" % (path_to_script, to_native(e)))
-
- conn_params = get_conn_params(module, module.params)
- db_connection = connect_to_db(module, conn_params, autocommit=autocommit)
- if encoding is not None:
- db_connection.set_client_encoding(encoding)
- cursor = db_connection.cursor(cursor_factory=DictCursor)
-
- # Prepare args:
- if module.params.get("positional_args"):
- arguments = module.params["positional_args"]
- elif module.params.get("named_args"):
- arguments = module.params["named_args"]
- else:
- arguments = None
-
- # Set defaults:
- changed = False
-
- # Execute query:
- try:
- cursor.execute(query, arguments)
- except Exception as e:
- if not autocommit:
- db_connection.rollback()
-
- cursor.close()
- db_connection.close()
- module.fail_json(msg="Cannot execute SQL '%s' %s: %s" % (query, arguments, to_native(e)))
-
- statusmessage = cursor.statusmessage
- rowcount = cursor.rowcount
-
- try:
- query_result = [dict(row) for row in cursor.fetchall()]
- except Psycopg2ProgrammingError as e:
- if to_native(e) == 'no results to fetch':
- query_result = {}
-
- except Exception as e:
- module.fail_json(msg="Cannot fetch rows from cursor: %s" % to_native(e))
-
- if 'SELECT' not in statusmessage:
- if 'UPDATE' in statusmessage or 'INSERT' in statusmessage or 'DELETE' in statusmessage:
- s = statusmessage.split()
- if len(s) == 3:
- if statusmessage.split()[2] != '0':
- changed = True
-
- elif len(s) == 2:
- if statusmessage.split()[1] != '0':
- changed = True
-
- else:
- changed = True
-
- else:
- changed = True
-
- if module.check_mode:
- db_connection.rollback()
- else:
- if not autocommit:
- db_connection.commit()
-
- kw = dict(
- changed=changed,
- query=cursor.query,
- statusmessage=statusmessage,
- query_result=query_result,
- rowcount=rowcount if rowcount >= 0 else 0,
- )
-
- cursor.close()
- db_connection.close()
-
- module.exit_json(**kw)
-
-
-if __name__ == '__main__':
- main()
diff --git a/test/support/integration/plugins/modules/postgresql_set.py b/test/support/integration/plugins/modules/postgresql_set.py
deleted file mode 100644
index cfbdae642b..0000000000
--- a/test/support/integration/plugins/modules/postgresql_set.py
+++ /dev/null
@@ -1,434 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Copyright: (c) 2018, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru>
-# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-ANSIBLE_METADATA = {
- 'metadata_version': '1.1',
- 'status': ['preview'],
- 'supported_by': 'community'
-}
-
-DOCUMENTATION = r'''
----
-module: postgresql_set
-short_description: Change a PostgreSQL server configuration parameter
-description:
- - Allows to change a PostgreSQL server configuration parameter.
- - The module uses ALTER SYSTEM command and applies changes by reload server configuration.
- - ALTER SYSTEM is used for changing server configuration parameters across the entire database cluster.
- - It can be more convenient and safe than the traditional method of manually editing the postgresql.conf file.
- - ALTER SYSTEM writes the given parameter setting to the $PGDATA/postgresql.auto.conf file,
- which is read in addition to postgresql.conf.
- - The module allows to reset parameter to boot_val (cluster initial value) by I(reset=yes) or remove parameter
- string from postgresql.auto.conf and reload I(value=default) (for settings with postmaster context restart is required).
- - After change you can see in the ansible output the previous and
- the new parameter value and other information using returned values and M(debug) module.
-version_added: '2.8'
-options:
- name:
- description:
- - Name of PostgreSQL server parameter.
- type: str
- required: true
- value:
- description:
- - Parameter value to set.
- - To remove parameter string from postgresql.auto.conf and
- reload the server configuration you must pass I(value=default).
- With I(value=default) the playbook always returns changed is true.
- type: str
- reset:
- description:
- - Restore parameter to initial state (boot_val). Mutually exclusive with I(value).
- type: bool
- default: false
- session_role:
- description:
- - Switch to session_role after connecting. The specified session_role must
- be a role that the current login_user is a member of.
- - Permissions checking for SQL commands is carried out as though
- the session_role were the one that had logged in originally.
- type: str
- db:
- description:
- - Name of database to connect.
- type: str
- aliases:
- - login_db
-notes:
-- Supported version of PostgreSQL is 9.4 and later.
-- Pay attention, change setting with 'postmaster' context can return changed is true
- when actually nothing changes because the same value may be presented in
- several different form, for example, 1024MB, 1GB, etc. However in pg_settings
- system view it can be defined like 131072 number of 8kB pages.
- The final check of the parameter value cannot compare it because the server was
- not restarted and the value in pg_settings is not updated yet.
-- For some parameters restart of PostgreSQL server is required.
- See official documentation U(https://www.postgresql.org/docs/current/view-pg-settings.html).
-seealso:
-- module: postgresql_info
-- name: PostgreSQL server configuration
- description: General information about PostgreSQL server configuration.
- link: https://www.postgresql.org/docs/current/runtime-config.html
-- name: PostgreSQL view pg_settings reference
- description: Complete reference of the pg_settings view documentation.
- link: https://www.postgresql.org/docs/current/view-pg-settings.html
-- name: PostgreSQL ALTER SYSTEM command reference
- description: Complete reference of the ALTER SYSTEM command documentation.
- link: https://www.postgresql.org/docs/current/sql-altersystem.html
-author:
-- Andrew Klychkov (@Andersson007)
-extends_documentation_fragment: postgres
-'''
-
-EXAMPLES = r'''
-- name: Restore wal_keep_segments parameter to initial state
- postgresql_set:
- name: wal_keep_segments
- reset: yes
-
-# Set work_mem parameter to 32MB and show what's been changed and restart is required or not
-# (output example: "msg": "work_mem 4MB >> 64MB restart_req: False")
-- name: Set work mem parameter
- postgresql_set:
- name: work_mem
- value: 32mb
- register: set
-
-- debug:
- msg: "{{ set.name }} {{ set.prev_val_pretty }} >> {{ set.value_pretty }} restart_req: {{ set.restart_required }}"
- when: set.changed
-# Ensure that the restart of PostgreSQL server must be required for some parameters.
-# In this situation you see the same parameter in prev_val and value_prettyue, but 'changed=True'
-# (If you passed the value that was different from the current server setting).
-
-- name: Set log_min_duration_statement parameter to 1 second
- postgresql_set:
- name: log_min_duration_statement
- value: 1s
-
-- name: Set wal_log_hints parameter to default value (remove parameter from postgresql.auto.conf)
- postgresql_set:
- name: wal_log_hints
- value: default
-'''
-
-RETURN = r'''
-name:
- description: Name of PostgreSQL server parameter.
- returned: always
- type: str
- sample: 'shared_buffers'
-restart_required:
- description: Information about parameter current state.
- returned: always
- type: bool
- sample: true
-prev_val_pretty:
- description: Information about previous state of the parameter.
- returned: always
- type: str
- sample: '4MB'
-value_pretty:
- description: Information about current state of the parameter.
- returned: always
- type: str
- sample: '64MB'
-value:
- description:
- - Dictionary that contains the current parameter value (at the time of playbook finish).
- - Pay attention that for real change some parameters restart of PostgreSQL server is required.
- - Returns the current value in the check mode.
- returned: always
- type: dict
- sample: { "value": 67108864, "unit": "b" }
-context:
- description:
- - PostgreSQL setting context.
- returned: always
- type: str
- sample: user
-'''
-
-try:
- from psycopg2.extras import DictCursor
-except Exception:
- # psycopg2 is checked by connect_to_db()
- # from ansible.module_utils.postgres
- pass
-
-from copy import deepcopy
-
-from ansible.module_utils.basic import AnsibleModule
-from ansible.module_utils.postgres import (
- connect_to_db,
- get_conn_params,
- postgres_common_argument_spec,
-)
-from ansible.module_utils._text import to_native
-
-PG_REQ_VER = 90400
-
-# To allow to set value like 1mb instead of 1MB, etc:
-POSSIBLE_SIZE_UNITS = ("mb", "gb", "tb")
-
-# ===========================================
-# PostgreSQL module specific support methods.
-#
-
-
-def param_get(cursor, module, name):
- query = ("SELECT name, setting, unit, context, boot_val "
- "FROM pg_settings WHERE name = %(name)s")
- try:
- cursor.execute(query, {'name': name})
- info = cursor.fetchall()
- cursor.execute("SHOW %s" % name)
- val = cursor.fetchone()
-
- except Exception as e:
- module.fail_json(msg="Unable to get %s value due to : %s" % (name, to_native(e)))
-
- raw_val = info[0][1]
- unit = info[0][2]
- context = info[0][3]
- boot_val = info[0][4]
-
- if val[0] == 'True':
- val[0] = 'on'
- elif val[0] == 'False':
- val[0] = 'off'
-
- if unit == 'kB':
- if int(raw_val) > 0:
- raw_val = int(raw_val) * 1024
- if int(boot_val) > 0:
- boot_val = int(boot_val) * 1024
-
- unit = 'b'
-
- elif unit == 'MB':
- if int(raw_val) > 0:
- raw_val = int(raw_val) * 1024 * 1024
- if int(boot_val) > 0:
- boot_val = int(boot_val) * 1024 * 1024
-
- unit = 'b'
-
- return (val[0], raw_val, unit, boot_val, context)
-
-
-def pretty_to_bytes(pretty_val):
- # The function returns a value in bytes
- # if the value contains 'B', 'kB', 'MB', 'GB', 'TB'.
- # Otherwise it returns the passed argument.
-
- val_in_bytes = None
-
- if 'kB' in pretty_val:
- num_part = int(''.join(d for d in pretty_val if d.isdigit()))
- val_in_bytes = num_part * 1024
-
- elif 'MB' in pretty_val.upper():
- num_part = int(''.join(d for d in pretty_val if d.isdigit()))
- val_in_bytes = num_part * 1024 * 1024
-
- elif 'GB' in pretty_val.upper():
- num_part = int(''.join(d for d in pretty_val if d.isdigit()))
- val_in_bytes = num_part * 1024 * 1024 * 1024
-
- elif 'TB' in pretty_val.upper():
- num_part = int(''.join(d for d in pretty_val if d.isdigit()))
- val_in_bytes = num_part * 1024 * 1024 * 1024 * 1024
-
- elif 'B' in pretty_val.upper():
- num_part = int(''.join(d for d in pretty_val if d.isdigit()))
- val_in_bytes = num_part
-
- else:
- return pretty_val
-
- return val_in_bytes
-
-
-def param_set(cursor, module, name, value, context):
- try:
- if str(value).lower() == 'default':
- query = "ALTER SYSTEM SET %s = DEFAULT" % name
- else:
- query = "ALTER SYSTEM SET %s = '%s'" % (name, value)
- cursor.execute(query)
-
- if context != 'postmaster':
- cursor.execute("SELECT pg_reload_conf()")
-
- except Exception as e:
- module.fail_json(msg="Unable to get %s value due to : %s" % (name, to_native(e)))
-
- return True
-
-
-# ===========================================
-# Module execution.
-#
-
-
-def main():
- argument_spec = postgres_common_argument_spec()
- argument_spec.update(
- name=dict(type='str', required=True),
- db=dict(type='str', aliases=['login_db']),
- value=dict(type='str'),
- reset=dict(type='bool'),
- session_role=dict(type='str'),
- )
- module = AnsibleModule(
- argument_spec=argument_spec,
- supports_check_mode=True,
- )
-
- name = module.params["name"]
- value = module.params["value"]
- reset = module.params["reset"]
-
- # Allow to pass values like 1mb instead of 1MB, etc:
- if value:
- for unit in POSSIBLE_SIZE_UNITS:
- if value[:-2].isdigit() and unit in value[-2:]:
- value = value.upper()
-
- if value and reset:
- module.fail_json(msg="%s: value and reset params are mutually exclusive" % name)
-
- if not value and not reset:
- module.fail_json(msg="%s: at least one of value or reset param must be specified" % name)
-
- conn_params = get_conn_params(module, module.params, warn_db_default=False)
- db_connection = connect_to_db(module, conn_params, autocommit=True)
- cursor = db_connection.cursor(cursor_factory=DictCursor)
-
- kw = {}
- # Check server version (needs 9.4 or later):
- ver = db_connection.server_version
- if ver < PG_REQ_VER:
- module.warn("PostgreSQL is %s version but %s or later is required" % (ver, PG_REQ_VER))
- kw = dict(
- changed=False,
- restart_required=False,
- value_pretty="",
- prev_val_pretty="",
- value={"value": "", "unit": ""},
- )
- kw['name'] = name
- db_connection.close()
- module.exit_json(**kw)
-
- # Set default returned values:
- restart_required = False
- changed = False
- kw['name'] = name
- kw['restart_required'] = False
-
- # Get info about param state:
- res = param_get(cursor, module, name)
- current_value = res[0]
- raw_val = res[1]
- unit = res[2]
- boot_val = res[3]
- context = res[4]
-
- if value == 'True':
- value = 'on'
- elif value == 'False':
- value = 'off'
-
- kw['prev_val_pretty'] = current_value
- kw['value_pretty'] = deepcopy(kw['prev_val_pretty'])
- kw['context'] = context
-
- # Do job
- if context == "internal":
- module.fail_json(msg="%s: cannot be changed (internal context). See "
- "https://www.postgresql.org/docs/current/runtime-config-preset.html" % name)
-
- if context == "postmaster":
- restart_required = True
-
- # If check_mode, just compare and exit:
- if module.check_mode:
- if pretty_to_bytes(value) == pretty_to_bytes(current_value):
- kw['changed'] = False
-
- else:
- kw['value_pretty'] = value
- kw['changed'] = True
-
- # Anyway returns current raw value in the check_mode:
- kw['value'] = dict(
- value=raw_val,
- unit=unit,
- )
- kw['restart_required'] = restart_required
- module.exit_json(**kw)
-
- # Set param:
- if value and value != current_value:
- changed = param_set(cursor, module, name, value, context)
-
- kw['value_pretty'] = value
-
- # Reset param:
- elif reset:
- if raw_val == boot_val:
- # nothing to change, exit:
- kw['value'] = dict(
- value=raw_val,
- unit=unit,
- )
- module.exit_json(**kw)
-
- changed = param_set(cursor, module, name, boot_val, context)
-
- if restart_required:
- module.warn("Restart of PostgreSQL is required for setting %s" % name)
-
- cursor.close()
- db_connection.close()
-
- # Reconnect and recheck current value:
- if context in ('sighup', 'superuser-backend', 'backend', 'superuser', 'user'):
- db_connection = connect_to_db(module, conn_params, autocommit=True)
- cursor = db_connection.cursor(cursor_factory=DictCursor)
-
- res = param_get(cursor, module, name)
- # f_ means 'final'
- f_value = res[0]
- f_raw_val = res[1]
-
- if raw_val == f_raw_val:
- changed = False
-
- else:
- changed = True
-
- kw['value_pretty'] = f_value
- kw['value'] = dict(
- value=f_raw_val,
- unit=unit,
- )
-
- cursor.close()
- db_connection.close()
-
- kw['changed'] = changed
- kw['restart_required'] = restart_required
- module.exit_json(**kw)
-
-
-if __name__ == '__main__':
- main()
diff --git a/test/support/integration/plugins/modules/postgresql_table.py b/test/support/integration/plugins/modules/postgresql_table.py
deleted file mode 100644
index 3bef03b08f..0000000000
--- a/test/support/integration/plugins/modules/postgresql_table.py
+++ /dev/null
@@ -1,601 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru>
-# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-ANSIBLE_METADATA = {
- 'metadata_version': '1.1',
- 'status': ['preview'],
- 'supported_by': 'community'
-}
-
-DOCUMENTATION = r'''
----
-module: postgresql_table
-short_description: Create, drop, or modify a PostgreSQL table
-description:
-- Allows to create, drop, rename, truncate a table, or change some table attributes.
-version_added: '2.8'
-options:
- table:
- description:
- - Table name.
- required: true
- aliases:
- - name
- type: str
- state:
- description:
- - The table state. I(state=absent) is mutually exclusive with I(tablespace), I(owner), I(unlogged),
- I(like), I(including), I(columns), I(truncate), I(storage_params) and, I(rename).
- type: str
- default: present
- choices: [ absent, present ]
- tablespace:
- description:
- - Set a tablespace for the table.
- required: false
- type: str
- owner:
- description:
- - Set a table owner.
- type: str
- unlogged:
- description:
- - Create an unlogged table.
- type: bool
- default: no
- like:
- description:
- - Create a table like another table (with similar DDL).
- Mutually exclusive with I(columns), I(rename), and I(truncate).
- type: str
- including:
- description:
- - Keywords that are used with like parameter, may be DEFAULTS, CONSTRAINTS, INDEXES, STORAGE, COMMENTS or ALL.
- Needs I(like) specified. Mutually exclusive with I(columns), I(rename), and I(truncate).
- type: str
- columns:
- description:
- - Columns that are needed.
- type: list
- elements: str
- rename:
- description:
- - New table name. Mutually exclusive with I(tablespace), I(owner),
- I(unlogged), I(like), I(including), I(columns), I(truncate), and I(storage_params).
- type: str
- truncate:
- description:
- - Truncate a table. Mutually exclusive with I(tablespace), I(owner), I(unlogged),
- I(like), I(including), I(columns), I(rename), and I(storage_params).
- type: bool
- default: no
- storage_params:
- description:
- - Storage parameters like fillfactor, autovacuum_vacuum_treshold, etc.
- Mutually exclusive with I(rename) and I(truncate).
- type: list
- elements: str
- db:
- description:
- - Name of database to connect and where the table will be created.
- type: str
- aliases:
- - login_db
- session_role:
- description:
- - Switch to session_role after connecting.
- The specified session_role must be a role that the current login_user is a member of.
- - Permissions checking for SQL commands is carried out as though
- the session_role were the one that had logged in originally.
- type: str
- cascade:
- description:
- - Automatically drop objects that depend on the table (such as views).
- Used with I(state=absent) only.
- type: bool
- default: no
- version_added: '2.9'
-notes:
-- If you do not pass db parameter, tables will be created in the database
- named postgres.
-- PostgreSQL allows to create columnless table, so columns param is optional.
-- Unlogged tables are available from PostgreSQL server version 9.1.
-seealso:
-- module: postgresql_sequence
-- module: postgresql_idx
-- module: postgresql_info
-- module: postgresql_tablespace
-- module: postgresql_owner
-- module: postgresql_privs
-- module: postgresql_copy
-- name: CREATE TABLE reference
- description: Complete reference of the CREATE TABLE command documentation.
- link: https://www.postgresql.org/docs/current/sql-createtable.html
-- name: ALTER TABLE reference
- description: Complete reference of the ALTER TABLE command documentation.
- link: https://www.postgresql.org/docs/current/sql-altertable.html
-- name: DROP TABLE reference
- description: Complete reference of the DROP TABLE command documentation.
- link: https://www.postgresql.org/docs/current/sql-droptable.html
-- name: PostgreSQL data types
- description: Complete reference of the PostgreSQL data types documentation.
- link: https://www.postgresql.org/docs/current/datatype.html
-author:
-- Andrei Klychkov (@Andersson007)
-extends_documentation_fragment: postgres
-'''
-
-EXAMPLES = r'''
-- name: Create tbl2 in the acme database with the DDL like tbl1 with testuser as an owner
- postgresql_table:
- db: acme
- name: tbl2
- like: tbl1
- owner: testuser
-
-- name: Create tbl2 in the acme database and tablespace ssd with the DDL like tbl1 including comments and indexes
- postgresql_table:
- db: acme
- table: tbl2
- like: tbl1
- including: comments, indexes
- tablespace: ssd
-
-- name: Create test_table with several columns in ssd tablespace with fillfactor=10 and autovacuum_analyze_threshold=1
- postgresql_table:
- name: test_table
- columns:
- - id bigserial primary key
- - num bigint
- - stories text
- tablespace: ssd
- storage_params:
- - fillfactor=10
- - autovacuum_analyze_threshold=1
-
-- name: Create an unlogged table in schema acme
- postgresql_table:
- name: acme.useless_data
- columns: waste_id int
- unlogged: true
-
-- name: Rename table foo to bar
- postgresql_table:
- table: foo
- rename: bar
-
-- name: Rename table foo from schema acme to bar
- postgresql_table:
- name: acme.foo
- rename: bar
-
-- name: Set owner to someuser
- postgresql_table:
- name: foo
- owner: someuser
-
-- name: Change tablespace of foo table to new_tablespace and set owner to new_user
- postgresql_table:
- name: foo
- tablespace: new_tablespace
- owner: new_user
-
-- name: Truncate table foo
- postgresql_table:
- name: foo
- truncate: yes
-
-- name: Drop table foo from schema acme
- postgresql_table:
- name: acme.foo
- state: absent
-
-- name: Drop table bar cascade
- postgresql_table:
- name: bar
- state: absent
- cascade: yes
-'''
-
-RETURN = r'''
-table:
- description: Name of a table.
- returned: always
- type: str
- sample: 'foo'
-state:
- description: Table state.
- returned: always
- type: str
- sample: 'present'
-owner:
- description: Table owner.
- returned: always
- type: str
- sample: 'postgres'
-tablespace:
- description: Tablespace.
- returned: always
- type: str
- sample: 'ssd_tablespace'
-queries:
- description: List of executed queries.
- returned: always
- type: str
- sample: [ 'CREATE TABLE "test_table" (id bigint)' ]
-storage_params:
- description: Storage parameters.
- returned: always
- type: list
- sample: [ "fillfactor=100", "autovacuum_analyze_threshold=1" ]
-'''
-
-try:
- from psycopg2.extras import DictCursor
-except ImportError:
- # psycopg2 is checked by connect_to_db()
- # from ansible.module_utils.postgres
- pass
-
-from ansible.module_utils.basic import AnsibleModule
-from ansible.module_utils.database import pg_quote_identifier
-from ansible.module_utils.postgres import (
- connect_to_db,
- exec_sql,
- get_conn_params,
- postgres_common_argument_spec,
-)
-
-
-# ===========================================
-# PostgreSQL module specific support methods.
-#
-
-class Table(object):
- def __init__(self, name, module, cursor):
- self.name = name
- self.module = module
- self.cursor = cursor
- self.info = {
- 'owner': '',
- 'tblspace': '',
- 'storage_params': [],
- }
- self.exists = False
- self.__exists_in_db()
- self.executed_queries = []
-
- def get_info(self):
- """Getter to refresh and get table info"""
- self.__exists_in_db()
-
- def __exists_in_db(self):
- """Check table exists and refresh info"""
- if "." in self.name:
- schema = self.name.split('.')[-2]
- tblname = self.name.split('.')[-1]
- else:
- schema = 'public'
- tblname = self.name
-
- query = ("SELECT t.tableowner, t.tablespace, c.reloptions "
- "FROM pg_tables AS t "
- "INNER JOIN pg_class AS c ON c.relname = t.tablename "
- "INNER JOIN pg_namespace AS n ON c.relnamespace = n.oid "
- "WHERE t.tablename = %(tblname)s "
- "AND n.nspname = %(schema)s")
- res = exec_sql(self, query, query_params={'tblname': tblname, 'schema': schema},
- add_to_executed=False)
- if res:
- self.exists = True
- self.info = dict(
- owner=res[0][0],
- tblspace=res[0][1] if res[0][1] else '',
- storage_params=res[0][2] if res[0][2] else [],
- )
-
- return True
- else:
- self.exists = False
- return False
-
- def create(self, columns='', params='', tblspace='',
- unlogged=False, owner=''):
- """
- Create table.
- If table exists, check passed args (params, tblspace, owner) and,
- if they're different from current, change them.
- Arguments:
- params - storage params (passed by "WITH (...)" in SQL),
- comma separated.
- tblspace - tablespace.
- owner - table owner.
- unlogged - create unlogged table.
- columns - column string (comma separated).
- """
- name = pg_quote_identifier(self.name, 'table')
-
- changed = False
-
- if self.exists:
- if tblspace == 'pg_default' and self.info['tblspace'] is None:
- pass # Because they have the same meaning
- elif tblspace and self.info['tblspace'] != tblspace:
- self.set_tblspace(tblspace)
- changed = True
-
- if owner and self.info['owner'] != owner:
- self.set_owner(owner)
- changed = True
-
- if params:
- param_list = [p.strip(' ') for p in params.split(',')]
-
- new_param = False
- for p in param_list:
- if p not in self.info['storage_params']:
- new_param = True
-
- if new_param:
- self.set_stor_params(params)
- changed = True
-
- if changed:
- return True
- return False
-
- query = "CREATE"
- if unlogged:
- query += " UNLOGGED TABLE %s" % name
- else:
- query += " TABLE %s" % name
-
- if columns:
- query += " (%s)" % columns
- else:
- query += " ()"
-
- if params:
- query += " WITH (%s)" % params
-
- if tblspace:
- query += " TABLESPACE %s" % pg_quote_identifier(tblspace, 'database')
-
- if exec_sql(self, query, ddl=True):
- changed = True
-
- if owner:
- changed = self.set_owner(owner)
-
- return changed
-
- def create_like(self, src_table, including='', tblspace='',
- unlogged=False, params='', owner=''):
- """
- Create table like another table (with similar DDL).
- Arguments:
- src_table - source table.
- including - corresponds to optional INCLUDING expression
- in CREATE TABLE ... LIKE statement.
- params - storage params (passed by "WITH (...)" in SQL),
- comma separated.
- tblspace - tablespace.
- owner - table owner.
- unlogged - create unlogged table.
- """
- changed = False
-
- name = pg_quote_identifier(self.name, 'table')
-
- query = "CREATE"
- if unlogged:
- query += " UNLOGGED TABLE %s" % name
- else:
- query += " TABLE %s" % name
-
- query += " (LIKE %s" % pg_quote_identifier(src_table, 'table')
-
- if including:
- including = including.split(',')
- for i in including:
- query += " INCLUDING %s" % i
-
- query += ')'
-
- if params:
- query += " WITH (%s)" % params
-
- if tblspace:
- query += " TABLESPACE %s" % pg_quote_identifier(tblspace, 'database')
-
- if exec_sql(self, query, ddl=True):
- changed = True
-
- if owner:
- changed = self.set_owner(owner)
-
- return changed
-
- def truncate(self):
- query = "TRUNCATE TABLE %s" % pg_quote_identifier(self.name, 'table')
- return exec_sql(self, query, ddl=True)
-
- def rename(self, newname):
- query = "ALTER TABLE %s RENAME TO %s" % (pg_quote_identifier(self.name, 'table'),
- pg_quote_identifier(newname, 'table'))
- return exec_sql(self, query, ddl=True)
-
- def set_owner(self, username):
- query = "ALTER TABLE %s OWNER TO %s" % (pg_quote_identifier(self.name, 'table'),
- pg_quote_identifier(username, 'role'))
- return exec_sql(self, query, ddl=True)
-
- def drop(self, cascade=False):
- if not self.exists:
- return False
-
- query = "DROP TABLE %s" % pg_quote_identifier(self.name, 'table')
- if cascade:
- query += " CASCADE"
- return exec_sql(self, query, ddl=True)
-
- def set_tblspace(self, tblspace):
- query = "ALTER TABLE %s SET TABLESPACE %s" % (pg_quote_identifier(self.name, 'table'),
- pg_quote_identifier(tblspace, 'database'))
- return exec_sql(self, query, ddl=True)
-
- def set_stor_params(self, params):
- query = "ALTER TABLE %s SET (%s)" % (pg_quote_identifier(self.name, 'table'), params)
- return exec_sql(self, query, ddl=True)
-
-
-# ===========================================
-# Module execution.
-#
-
-
-def main():
- argument_spec = postgres_common_argument_spec()
- argument_spec.update(
- table=dict(type='str', required=True, aliases=['name']),
- state=dict(type='str', default="present", choices=["absent", "present"]),
- db=dict(type='str', default='', aliases=['login_db']),
- tablespace=dict(type='str'),
- owner=dict(type='str'),
- unlogged=dict(type='bool', default=False),
- like=dict(type='str'),
- including=dict(type='str'),
- rename=dict(type='str'),
- truncate=dict(type='bool', default=False),
- columns=dict(type='list', elements='str'),
- storage_params=dict(type='list', elements='str'),
- session_role=dict(type='str'),
- cascade=dict(type='bool', default=False),
- )
- module = AnsibleModule(
- argument_spec=argument_spec,
- supports_check_mode=True,
- )
-
- table = module.params["table"]
- state = module.params["state"]
- tablespace = module.params["tablespace"]
- owner = module.params["owner"]
- unlogged = module.params["unlogged"]
- like = module.params["like"]
- including = module.params["including"]
- newname = module.params["rename"]
- storage_params = module.params["storage_params"]
- truncate = module.params["truncate"]
- columns = module.params["columns"]
- cascade = module.params["cascade"]
-
- if state == 'present' and cascade:
- module.warn("cascade=true is ignored when state=present")
-
- # Check mutual exclusive parameters:
- if state == 'absent' and (truncate or newname or columns or tablespace or like or storage_params or unlogged or owner or including):
- module.fail_json(msg="%s: state=absent is mutually exclusive with: "
- "truncate, rename, columns, tablespace, "
- "including, like, storage_params, unlogged, owner" % table)
-
- if truncate and (newname or columns or like or unlogged or storage_params or owner or tablespace or including):
- module.fail_json(msg="%s: truncate is mutually exclusive with: "
- "rename, columns, like, unlogged, including, "
- "storage_params, owner, tablespace" % table)
-
- if newname and (columns or like or unlogged or storage_params or owner or tablespace or including):
- module.fail_json(msg="%s: rename is mutually exclusive with: "
- "columns, like, unlogged, including, "
- "storage_params, owner, tablespace" % table)
-
- if like and columns:
- module.fail_json(msg="%s: like and columns params are mutually exclusive" % table)
- if including and not like:
- module.fail_json(msg="%s: including param needs like param specified" % table)
-
- conn_params = get_conn_params(module, module.params)
- db_connection = connect_to_db(module, conn_params, autocommit=False)
- cursor = db_connection.cursor(cursor_factory=DictCursor)
-
- if storage_params:
- storage_params = ','.join(storage_params)
-
- if columns:
- columns = ','.join(columns)
-
- ##############
- # Do main job:
- table_obj = Table(table, module, cursor)
-
- # Set default returned values:
- changed = False
- kw = {}
- kw['table'] = table
- kw['state'] = ''
- if table_obj.exists:
- kw = dict(
- table=table,
- state='present',
- owner=table_obj.info['owner'],
- tablespace=table_obj.info['tblspace'],
- storage_params=table_obj.info['storage_params'],
- )
-
- if state == 'absent':
- changed = table_obj.drop(cascade=cascade)
-
- elif truncate:
- changed = table_obj.truncate()
-
- elif newname:
- changed = table_obj.rename(newname)
- q = table_obj.executed_queries
- table_obj = Table(newname, module, cursor)
- table_obj.executed_queries = q
-
- elif state == 'present' and not like:
- changed = table_obj.create(columns, storage_params,
- tablespace, unlogged, owner)
-
- elif state == 'present' and like:
- changed = table_obj.create_like(like, including, tablespace,
- unlogged, storage_params)
-
- if changed:
- if module.check_mode:
- db_connection.rollback()
- else:
- db_connection.commit()
-
- # Refresh table info for RETURN.
- # Note, if table has been renamed, it gets info by newname:
- table_obj.get_info()
- db_connection.commit()
- if table_obj.exists:
- kw = dict(
- table=table,
- state='present',
- owner=table_obj.info['owner'],
- tablespace=table_obj.info['tblspace'],
- storage_params=table_obj.info['storage_params'],
- )
- else:
- # We just change the table state here
- # to keep other information about the dropped table:
- kw['state'] = 'absent'
-
- kw['queries'] = table_obj.executed_queries
- kw['changed'] = changed
- db_connection.close()
- module.exit_json(**kw)
-
-
-if __name__ == '__main__':
- main()
diff --git a/test/support/integration/plugins/modules/postgresql_user.py b/test/support/integration/plugins/modules/postgresql_user.py
deleted file mode 100644
index 10afd0a0d8..0000000000
--- a/test/support/integration/plugins/modules/postgresql_user.py
+++ /dev/null
@@ -1,927 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Copyright: Ansible Project
-# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-ANSIBLE_METADATA = {
- 'metadata_version': '1.1',
- 'status': ['stableinterface'],
- 'supported_by': 'community'
-}
-
-DOCUMENTATION = r'''
----
-module: postgresql_user
-short_description: Add or remove a user (role) from a PostgreSQL server instance
-description:
-- Adds or removes a user (role) from a PostgreSQL server instance
- ("cluster" in PostgreSQL terminology) and, optionally,
- grants the user access to an existing database or tables.
-- A user is a role with login privilege.
-- The fundamental function of the module is to create, or delete, users from
- a PostgreSQL instances. Privilege assignment, or removal, is an optional
- step, which works on one database at a time. This allows for the module to
- be called several times in the same module to modify the permissions on
- different databases, or to grant permissions to already existing users.
-- A user cannot be removed until all the privileges have been stripped from
- the user. In such situation, if the module tries to remove the user it
- will fail. To avoid this from happening the fail_on_user option signals
- the module to try to remove the user, but if not possible keep going; the
- module will report if changes happened and separately if the user was
- removed or not.
-version_added: '0.6'
-options:
- name:
- description:
- - Name of the user (role) to add or remove.
- type: str
- required: true
- aliases:
- - user
- password:
- description:
- - Set the user's password, before 1.4 this was required.
- - Password can be passed unhashed or hashed (MD5-hashed).
- - Unhashed password will automatically be hashed when saved into the
- database if C(encrypted) parameter is set, otherwise it will be save in
- plain text format.
- - When passing a hashed password it must be generated with the format
- C('str["md5"] + md5[ password + username ]'), resulting in a total of
- 35 characters. An easy way to do this is C(echo "md5$(echo -n
- 'verysecretpasswordJOE' | md5sum | awk '{print $1}')").
- - Note that if the provided password string is already in MD5-hashed
- format, then it is used as-is, regardless of C(encrypted) parameter.
- type: str
- db:
- description:
- - Name of database to connect to and where user's permissions will be granted.
- type: str
- aliases:
- - login_db
- fail_on_user:
- description:
- - If C(yes), fail when user (role) can't be removed. Otherwise just log and continue.
- default: 'yes'
- type: bool
- aliases:
- - fail_on_role
- priv:
- description:
- - "Slash-separated PostgreSQL privileges string: C(priv1/priv2), where
- privileges can be defined for database ( allowed options - 'CREATE',
- 'CONNECT', 'TEMPORARY', 'TEMP', 'ALL'. For example C(CONNECT) ) or
- for table ( allowed options - 'SELECT', 'INSERT', 'UPDATE', 'DELETE',
- 'TRUNCATE', 'REFERENCES', 'TRIGGER', 'ALL'. For example
- C(table:SELECT) ). Mixed example of this string:
- C(CONNECT/CREATE/table1:SELECT/table2:INSERT)."
- type: str
- role_attr_flags:
- description:
- - "PostgreSQL user attributes string in the format: CREATEDB,CREATEROLE,SUPERUSER."
- - Note that '[NO]CREATEUSER' is deprecated.
- - To create a simple role for using it like a group, use C(NOLOGIN) flag.
- type: str
- choices: [ '[NO]SUPERUSER', '[NO]CREATEROLE', '[NO]CREATEDB',
- '[NO]INHERIT', '[NO]LOGIN', '[NO]REPLICATION', '[NO]BYPASSRLS' ]
- session_role:
- version_added: '2.8'
- description:
- - Switch to session_role after connecting.
- - The specified session_role must be a role that the current login_user is a member of.
- - Permissions checking for SQL commands is carried out as though the session_role were the one that had logged in originally.
- type: str
- state:
- description:
- - The user (role) state.
- type: str
- default: present
- choices: [ absent, present ]
- encrypted:
- description:
- - Whether the password is stored hashed in the database.
- - Passwords can be passed already hashed or unhashed, and postgresql
- ensures the stored password is hashed when C(encrypted) is set.
- - "Note: Postgresql 10 and newer doesn't support unhashed passwords."
- - Previous to Ansible 2.6, this was C(no) by default.
- default: 'yes'
- type: bool
- version_added: '1.4'
- expires:
- description:
- - The date at which the user's password is to expire.
- - If set to C('infinity'), user's password never expire.
- - Note that this value should be a valid SQL date and time type.
- type: str
- version_added: '1.4'
- no_password_changes:
- description:
- - If C(yes), don't inspect database for password changes. Effective when
- C(pg_authid) is not accessible (such as AWS RDS). Otherwise, make
- password changes as necessary.
- default: 'no'
- type: bool
- version_added: '2.0'
- conn_limit:
- description:
- - Specifies the user (role) connection limit.
- type: int
- version_added: '2.4'
- ssl_mode:
- description:
- - Determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server.
- - See https://www.postgresql.org/docs/current/static/libpq-ssl.html for more information on the modes.
- - Default of C(prefer) matches libpq default.
- type: str
- default: prefer
- choices: [ allow, disable, prefer, require, verify-ca, verify-full ]
- version_added: '2.3'
- ca_cert:
- description:
- - Specifies the name of a file containing SSL certificate authority (CA) certificate(s).
- - If the file exists, the server's certificate will be verified to be signed by one of these authorities.
- type: str
- aliases: [ ssl_rootcert ]
- version_added: '2.3'
- groups:
- description:
- - The list of groups (roles) that need to be granted to the user.
- type: list
- elements: str
- version_added: '2.9'
- comment:
- description:
- - Add a comment on the user (equal to the COMMENT ON ROLE statement result).
- type: str
- version_added: '2.10'
-notes:
-- The module creates a user (role) with login privilege by default.
- Use NOLOGIN role_attr_flags to change this behaviour.
-- If you specify PUBLIC as the user (role), then the privilege changes will apply to all users (roles).
- You may not specify password or role_attr_flags when the PUBLIC user is specified.
-seealso:
-- module: postgresql_privs
-- module: postgresql_membership
-- module: postgresql_owner
-- name: PostgreSQL database roles
- description: Complete reference of the PostgreSQL database roles documentation.
- link: https://www.postgresql.org/docs/current/user-manag.html
-author:
-- Ansible Core Team
-extends_documentation_fragment: postgres
-'''
-
-EXAMPLES = r'''
-- name: Connect to acme database, create django user, and grant access to database and products table
- postgresql_user:
- db: acme
- name: django
- password: ceec4eif7ya
- priv: "CONNECT/products:ALL"
- expires: "Jan 31 2020"
-
-- name: Add a comment on django user
- postgresql_user:
- db: acme
- name: django
- comment: This is a test user
-
-# Connect to default database, create rails user, set its password (MD5-hashed),
-# and grant privilege to create other databases and demote rails from super user status if user exists
-- name: Create rails user, set MD5-hashed password, grant privs
- postgresql_user:
- name: rails
- password: md59543f1d82624df2b31672ec0f7050460
- role_attr_flags: CREATEDB,NOSUPERUSER
-
-- name: Connect to acme database and remove test user privileges from there
- postgresql_user:
- db: acme
- name: test
- priv: "ALL/products:ALL"
- state: absent
- fail_on_user: no
-
-- name: Connect to test database, remove test user from cluster
- postgresql_user:
- db: test
- name: test
- priv: ALL
- state: absent
-
-- name: Connect to acme database and set user's password with no expire date
- postgresql_user:
- db: acme
- name: django
- password: mysupersecretword
- priv: "CONNECT/products:ALL"
- expires: infinity
-
-# Example privileges string format
-# INSERT,UPDATE/table:SELECT/anothertable:ALL
-
-- name: Connect to test database and remove an existing user's password
- postgresql_user:
- db: test
- user: test
- password: ""
-
-- name: Create user test and grant group user_ro and user_rw to it
- postgresql_user:
- name: test
- groups:
- - user_ro
- - user_rw
-'''
-
-RETURN = r'''
-queries:
- description: List of executed queries.
- returned: always
- type: list
- sample: ['CREATE USER "alice"', 'GRANT CONNECT ON DATABASE "acme" TO "alice"']
- version_added: '2.8'
-'''
-
-import itertools
-import re
-import traceback
-from hashlib import md5
-
-try:
- import psycopg2
- from psycopg2.extras import DictCursor
-except ImportError:
- # psycopg2 is checked by connect_to_db()
- # from ansible.module_utils.postgres
- pass
-
-from ansible.module_utils.basic import AnsibleModule
-from ansible.module_utils.database import pg_quote_identifier, SQLParseError
-from ansible.module_utils.postgres import (
- connect_to_db,
- get_conn_params,
- PgMembership,
- postgres_common_argument_spec,
-)
-from ansible.module_utils._text import to_bytes, to_native
-from ansible.module_utils.six import iteritems
-
-
-FLAGS = ('SUPERUSER', 'CREATEROLE', 'CREATEDB', 'INHERIT', 'LOGIN', 'REPLICATION')
-FLAGS_BY_VERSION = {'BYPASSRLS': 90500}
-
-VALID_PRIVS = dict(table=frozenset(('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'REFERENCES', 'TRIGGER', 'ALL')),
- database=frozenset(
- ('CREATE', 'CONNECT', 'TEMPORARY', 'TEMP', 'ALL')),
- )
-
-# map to cope with idiosyncracies of SUPERUSER and LOGIN
-PRIV_TO_AUTHID_COLUMN = dict(SUPERUSER='rolsuper', CREATEROLE='rolcreaterole',
- CREATEDB='rolcreatedb', INHERIT='rolinherit', LOGIN='rolcanlogin',
- REPLICATION='rolreplication', BYPASSRLS='rolbypassrls')
-
-executed_queries = []
-
-
-class InvalidFlagsError(Exception):
- pass
-
-
-class InvalidPrivsError(Exception):
- pass
-
-# ===========================================
-# PostgreSQL module specific support methods.
-#
-
-
-def user_exists(cursor, user):
- # The PUBLIC user is a special case that is always there
- if user == 'PUBLIC':
- return True
- query = "SELECT rolname FROM pg_roles WHERE rolname=%(user)s"
- cursor.execute(query, {'user': user})
- return cursor.rowcount > 0
-
-
-def user_add(cursor, user, password, role_attr_flags, encrypted, expires, conn_limit):
- """Create a new database user (role)."""
- # Note: role_attr_flags escaped by parse_role_attrs and encrypted is a
- # literal
- query_password_data = dict(password=password, expires=expires)
- query = ['CREATE USER "%(user)s"' %
- {"user": user}]
- if password is not None and password != '':
- query.append("WITH %(crypt)s" % {"crypt": encrypted})
- query.append("PASSWORD %(password)s")
- if expires is not None:
- query.append("VALID UNTIL %(expires)s")
- if conn_limit is not None:
- query.append("CONNECTION LIMIT %(conn_limit)s" % {"conn_limit": conn_limit})
- query.append(role_attr_flags)
- query = ' '.join(query)
- executed_queries.append(query)
- cursor.execute(query, query_password_data)
- return True
-
-
-def user_should_we_change_password(current_role_attrs, user, password, encrypted):
- """Check if we should change the user's password.
-
- Compare the proposed password with the existing one, comparing
- hashes if encrypted. If we can't access it assume yes.
- """
-
- if current_role_attrs is None:
- # on some databases, E.g. AWS RDS instances, there is no access to
- # the pg_authid relation to check the pre-existing password, so we
- # just assume password is different
- return True
-
- # Do we actually need to do anything?
- pwchanging = False
- if password is not None:
- # Empty password means that the role shouldn't have a password, which
- # means we need to check if the current password is None.
- if password == '':
- if current_role_attrs['rolpassword'] is not None:
- pwchanging = True
- # 32: MD5 hashes are represented as a sequence of 32 hexadecimal digits
- # 3: The size of the 'md5' prefix
- # When the provided password looks like a MD5-hash, value of
- # 'encrypted' is ignored.
- elif (password.startswith('md5') and len(password) == 32 + 3) or encrypted == 'UNENCRYPTED':
- if password != current_role_attrs['rolpassword']:
- pwchanging = True
- elif encrypted == 'ENCRYPTED':
- hashed_password = 'md5{0}'.format(md5(to_bytes(password) + to_bytes(user)).hexdigest())
- if hashed_password != current_role_attrs['rolpassword']:
- pwchanging = True
-
- return pwchanging
-
-
-def user_alter(db_connection, module, user, password, role_attr_flags, encrypted, expires, no_password_changes, conn_limit):
- """Change user password and/or attributes. Return True if changed, False otherwise."""
- changed = False
-
- cursor = db_connection.cursor(cursor_factory=DictCursor)
- # Note: role_attr_flags escaped by parse_role_attrs and encrypted is a
- # literal
- if user == 'PUBLIC':
- if password is not None:
- module.fail_json(msg="cannot change the password for PUBLIC user")
- elif role_attr_flags != '':
- module.fail_json(msg="cannot change the role_attr_flags for PUBLIC user")
- else:
- return False
-
- # Handle passwords.
- if not no_password_changes and (password is not None or role_attr_flags != '' or expires is not None or conn_limit is not None):
- # Select password and all flag-like columns in order to verify changes.
- try:
- select = "SELECT * FROM pg_authid where rolname=%(user)s"
- cursor.execute(select, {"user": user})
- # Grab current role attributes.
- current_role_attrs = cursor.fetchone()
- except psycopg2.ProgrammingError:
- current_role_attrs = None
- db_connection.rollback()
-
- pwchanging = user_should_we_change_password(current_role_attrs, user, password, encrypted)
-
- if current_role_attrs is None:
- try:
- # AWS RDS instances does not allow user to access pg_authid
- # so try to get current_role_attrs from pg_roles tables
- select = "SELECT * FROM pg_roles where rolname=%(user)s"
- cursor.execute(select, {"user": user})
- # Grab current role attributes from pg_roles
- current_role_attrs = cursor.fetchone()
- except psycopg2.ProgrammingError as e:
- db_connection.rollback()
- module.fail_json(msg="Failed to get role details for current user %s: %s" % (user, e))
-
- role_attr_flags_changing = False
- if role_attr_flags:
- role_attr_flags_dict = {}
- for r in role_attr_flags.split(' '):
- if r.startswith('NO'):
- role_attr_flags_dict[r.replace('NO', '', 1)] = False
- else:
- role_attr_flags_dict[r] = True
-
- for role_attr_name, role_attr_value in role_attr_flags_dict.items():
- if current_role_attrs[PRIV_TO_AUTHID_COLUMN[role_attr_name]] != role_attr_value:
- role_attr_flags_changing = True
-
- if expires is not None:
- cursor.execute("SELECT %s::timestamptz;", (expires,))
- expires_with_tz = cursor.fetchone()[0]
- expires_changing = expires_with_tz != current_role_attrs.get('rolvaliduntil')
- else:
- expires_changing = False
-
- conn_limit_changing = (conn_limit is not None and conn_limit != current_role_attrs['rolconnlimit'])
-
- if not pwchanging and not role_attr_flags_changing and not expires_changing and not conn_limit_changing:
- return False
-
- alter = ['ALTER USER "%(user)s"' % {"user": user}]
- if pwchanging:
- if password != '':
- alter.append("WITH %(crypt)s" % {"crypt": encrypted})
- alter.append("PASSWORD %(password)s")
- else:
- alter.append("WITH PASSWORD NULL")
- alter.append(role_attr_flags)
- elif role_attr_flags:
- alter.append('WITH %s' % role_attr_flags)
- if expires is not None:
- alter.append("VALID UNTIL %(expires)s")
- if conn_limit is not None:
- alter.append("CONNECTION LIMIT %(conn_limit)s" % {"conn_limit": conn_limit})
-
- query_password_data = dict(password=password, expires=expires)
- try:
- cursor.execute(' '.join(alter), query_password_data)
- changed = True
- except psycopg2.InternalError as e:
- if e.pgcode == '25006':
- # Handle errors due to read-only transactions indicated by pgcode 25006
- # ERROR: cannot execute ALTER ROLE in a read-only transaction
- changed = False
- module.fail_json(msg=e.pgerror, exception=traceback.format_exc())
- return changed
- else:
- raise psycopg2.InternalError(e)
- except psycopg2.NotSupportedError as e:
- module.fail_json(msg=e.pgerror, exception=traceback.format_exc())
-
- elif no_password_changes and role_attr_flags != '':
- # Grab role information from pg_roles instead of pg_authid
- select = "SELECT * FROM pg_roles where rolname=%(user)s"
- cursor.execute(select, {"user": user})
- # Grab current role attributes.
- current_role_attrs = cursor.fetchone()
-
- role_attr_flags_changing = False
-
- if role_attr_flags:
- role_attr_flags_dict = {}
- for r in role_attr_flags.split(' '):
- if r.startswith('NO'):
- role_attr_flags_dict[r.replace('NO', '', 1)] = False
- else:
- role_attr_flags_dict[r] = True
-
- for role_attr_name, role_attr_value in role_attr_flags_dict.items():
- if current_role_attrs[PRIV_TO_AUTHID_COLUMN[role_attr_name]] != role_attr_value:
- role_attr_flags_changing = True
-
- if not role_attr_flags_changing:
- return False
-
- alter = ['ALTER USER "%(user)s"' %
- {"user": user}]
- if role_attr_flags:
- alter.append('WITH %s' % role_attr_flags)
-
- try:
- cursor.execute(' '.join(alter))
- except psycopg2.InternalError as e:
- if e.pgcode == '25006':
- # Handle errors due to read-only transactions indicated by pgcode 25006
- # ERROR: cannot execute ALTER ROLE in a read-only transaction
- changed = False
- module.fail_json(msg=e.pgerror, exception=traceback.format_exc())
- return changed
- else:
- raise psycopg2.InternalError(e)
-
- # Grab new role attributes.
- cursor.execute(select, {"user": user})
- new_role_attrs = cursor.fetchone()
-
- # Detect any differences between current_ and new_role_attrs.
- changed = current_role_attrs != new_role_attrs
-
- return changed
-
-
-def user_delete(cursor, user):
- """Try to remove a user. Returns True if successful otherwise False"""
- cursor.execute("SAVEPOINT ansible_pgsql_user_delete")
- try:
- query = 'DROP USER "%s"' % user
- executed_queries.append(query)
- cursor.execute(query)
- except Exception:
- cursor.execute("ROLLBACK TO SAVEPOINT ansible_pgsql_user_delete")
- cursor.execute("RELEASE SAVEPOINT ansible_pgsql_user_delete")
- return False
-
- cursor.execute("RELEASE SAVEPOINT ansible_pgsql_user_delete")
- return True
-
-
-def has_table_privileges(cursor, user, table, privs):
- """
- Return the difference between the privileges that a user already has and
- the privileges that they desire to have.
-
- :returns: tuple of:
- * privileges that they have and were requested
- * privileges they currently hold but were not requested
- * privileges requested that they do not hold
- """
- cur_privs = get_table_privileges(cursor, user, table)
- have_currently = cur_privs.intersection(privs)
- other_current = cur_privs.difference(privs)
- desired = privs.difference(cur_privs)
- return (have_currently, other_current, desired)
-
-
-def get_table_privileges(cursor, user, table):
- if '.' in table:
- schema, table = table.split('.', 1)
- else:
- schema = 'public'
- query = ("SELECT privilege_type FROM information_schema.role_table_grants "
- "WHERE grantee=%(user)s AND table_name=%(table)s AND table_schema=%(schema)s")
- cursor.execute(query, {'user': user, 'table': table, 'schema': schema})
- return frozenset([x[0] for x in cursor.fetchall()])
-
-
-def grant_table_privileges(cursor, user, table, privs):
- # Note: priv escaped by parse_privs
- privs = ', '.join(privs)
- query = 'GRANT %s ON TABLE %s TO "%s"' % (
- privs, pg_quote_identifier(table, 'table'), user)
- executed_queries.append(query)
- cursor.execute(query)
-
-
-def revoke_table_privileges(cursor, user, table, privs):
- # Note: priv escaped by parse_privs
- privs = ', '.join(privs)
- query = 'REVOKE %s ON TABLE %s FROM "%s"' % (
- privs, pg_quote_identifier(table, 'table'), user)
- executed_queries.append(query)
- cursor.execute(query)
-
-
-def get_database_privileges(cursor, user, db):
- priv_map = {
- 'C': 'CREATE',
- 'T': 'TEMPORARY',
- 'c': 'CONNECT',
- }
- query = 'SELECT datacl FROM pg_database WHERE datname = %s'
- cursor.execute(query, (db,))
- datacl = cursor.fetchone()[0]
- if datacl is None:
- return set()
- r = re.search(r'%s\\?"?=(C?T?c?)/[^,]+,?' % user, datacl)
- if r is None:
- return set()
- o = set()
- for v in r.group(1):
- o.add(priv_map[v])
- return normalize_privileges(o, 'database')
-
-
-def has_database_privileges(cursor, user, db, privs):
- """
- Return the difference between the privileges that a user already has and
- the privileges that they desire to have.
-
- :returns: tuple of:
- * privileges that they have and were requested
- * privileges they currently hold but were not requested
- * privileges requested that they do not hold
- """
- cur_privs = get_database_privileges(cursor, user, db)
- have_currently = cur_privs.intersection(privs)
- other_current = cur_privs.difference(privs)
- desired = privs.difference(cur_privs)
- return (have_currently, other_current, desired)
-
-
-def grant_database_privileges(cursor, user, db, privs):
- # Note: priv escaped by parse_privs
- privs = ', '.join(privs)
- if user == "PUBLIC":
- query = 'GRANT %s ON DATABASE %s TO PUBLIC' % (
- privs, pg_quote_identifier(db, 'database'))
- else:
- query = 'GRANT %s ON DATABASE %s TO "%s"' % (
- privs, pg_quote_identifier(db, 'database'), user)
-
- executed_queries.append(query)
- cursor.execute(query)
-
-
-def revoke_database_privileges(cursor, user, db, privs):
- # Note: priv escaped by parse_privs
- privs = ', '.join(privs)
- if user == "PUBLIC":
- query = 'REVOKE %s ON DATABASE %s FROM PUBLIC' % (
- privs, pg_quote_identifier(db, 'database'))
- else:
- query = 'REVOKE %s ON DATABASE %s FROM "%s"' % (
- privs, pg_quote_identifier(db, 'database'), user)
-
- executed_queries.append(query)
- cursor.execute(query)
-
-
-def revoke_privileges(cursor, user, privs):
- if privs is None:
- return False
-
- revoke_funcs = dict(table=revoke_table_privileges,
- database=revoke_database_privileges)
- check_funcs = dict(table=has_table_privileges,
- database=has_database_privileges)
-
- changed = False
- for type_ in privs:
- for name, privileges in iteritems(privs[type_]):
- # Check that any of the privileges requested to be removed are
- # currently granted to the user
- differences = check_funcs[type_](cursor, user, name, privileges)
- if differences[0]:
- revoke_funcs[type_](cursor, user, name, privileges)
- changed = True
- return changed
-
-
-def grant_privileges(cursor, user, privs):
- if privs is None:
- return False
-
- grant_funcs = dict(table=grant_table_privileges,
- database=grant_database_privileges)
- check_funcs = dict(table=has_table_privileges,
- database=has_database_privileges)
-
- changed = False
- for type_ in privs:
- for name, privileges in iteritems(privs[type_]):
- # Check that any of the privileges requested for the user are
- # currently missing
- differences = check_funcs[type_](cursor, user, name, privileges)
- if differences[2]:
- grant_funcs[type_](cursor, user, name, privileges)
- changed = True
- return changed
-
-
-def parse_role_attrs(cursor, role_attr_flags):
- """
- Parse role attributes string for user creation.
- Format:
-
- attributes[,attributes,...]
-
- Where:
-
- attributes := CREATEDB,CREATEROLE,NOSUPERUSER,...
- [ "[NO]SUPERUSER","[NO]CREATEROLE", "[NO]CREATEDB",
- "[NO]INHERIT", "[NO]LOGIN", "[NO]REPLICATION",
- "[NO]BYPASSRLS" ]
-
- Note: "[NO]BYPASSRLS" role attribute introduced in 9.5
- Note: "[NO]CREATEUSER" role attribute is deprecated.
-
- """
- flags = frozenset(role.upper() for role in role_attr_flags.split(',') if role)
-
- valid_flags = frozenset(itertools.chain(FLAGS, get_valid_flags_by_version(cursor)))
- valid_flags = frozenset(itertools.chain(valid_flags, ('NO%s' % flag for flag in valid_flags)))
-
- if not flags.issubset(valid_flags):
- raise InvalidFlagsError('Invalid role_attr_flags specified: %s' %
- ' '.join(flags.difference(valid_flags)))
-
- return ' '.join(flags)
-
-
-def normalize_privileges(privs, type_):
- new_privs = set(privs)
- if 'ALL' in new_privs:
- new_privs.update(VALID_PRIVS[type_])
- new_privs.remove('ALL')
- if 'TEMP' in new_privs:
- new_privs.add('TEMPORARY')
- new_privs.remove('TEMP')
-
- return new_privs
-
-
-def parse_privs(privs, db):
- """
- Parse privilege string to determine permissions for database db.
- Format:
-
- privileges[/privileges/...]
-
- Where:
-
- privileges := DATABASE_PRIVILEGES[,DATABASE_PRIVILEGES,...] |
- TABLE_NAME:TABLE_PRIVILEGES[,TABLE_PRIVILEGES,...]
- """
- if privs is None:
- return privs
-
- o_privs = {
- 'database': {},
- 'table': {}
- }
- for token in privs.split('/'):
- if ':' not in token:
- type_ = 'database'
- name = db
- priv_set = frozenset(x.strip().upper()
- for x in token.split(',') if x.strip())
- else:
- type_ = 'table'
- name, privileges = token.split(':', 1)
- priv_set = frozenset(x.strip().upper()
- for x in privileges.split(',') if x.strip())
-
- if not priv_set.issubset(VALID_PRIVS[type_]):
- raise InvalidPrivsError('Invalid privs specified for %s: %s' %
- (type_, ' '.join(priv_set.difference(VALID_PRIVS[type_]))))
-
- priv_set = normalize_privileges(priv_set, type_)
- o_privs[type_][name] = priv_set
-
- return o_privs
-
-
-def get_valid_flags_by_version(cursor):
- """
- Some role attributes were introduced after certain versions. We want to
- compile a list of valid flags against the current Postgres version.
- """
- current_version = cursor.connection.server_version
-
- return [
- flag
- for flag, version_introduced in FLAGS_BY_VERSION.items()
- if current_version >= version_introduced
- ]
-
-
-def get_comment(cursor, user):
- """Get user's comment."""
- query = ("SELECT pg_catalog.shobj_description(r.oid, 'pg_authid') "
- "FROM pg_catalog.pg_roles r "
- "WHERE r.rolname = %(user)s")
- cursor.execute(query, {'user': user})
- return cursor.fetchone()[0]
-
-
-def add_comment(cursor, user, comment):
- """Add comment on user."""
- if comment != get_comment(cursor, user):
- query = 'COMMENT ON ROLE "%s" IS ' % user
- cursor.execute(query + '%(comment)s', {'comment': comment})
- executed_queries.append(cursor.mogrify(query + '%(comment)s', {'comment': comment}))
- return True
- else:
- return False
-
-
-# ===========================================
-# Module execution.
-#
-
-def main():
- argument_spec = postgres_common_argument_spec()
- argument_spec.update(
- user=dict(type='str', required=True, aliases=['name']),
- password=dict(type='str', default=None, no_log=True),
- state=dict(type='str', default='present', choices=['absent', 'present']),
- priv=dict(type='str', default=None),
- db=dict(type='str', default='', aliases=['login_db']),
- fail_on_user=dict(type='bool', default='yes', aliases=['fail_on_role']),
- role_attr_flags=dict(type='str', default=''),
- encrypted=dict(type='bool', default='yes'),
- no_password_changes=dict(type='bool', default='no'),
- expires=dict(type='str', default=None),
- conn_limit=dict(type='int', default=None),
- session_role=dict(type='str'),
- groups=dict(type='list', elements='str'),
- comment=dict(type='str', default=None),
- )
- module = AnsibleModule(
- argument_spec=argument_spec,
- supports_check_mode=True
- )
-
- user = module.params["user"]
- password = module.params["password"]
- state = module.params["state"]
- fail_on_user = module.params["fail_on_user"]
- if module.params['db'] == '' and module.params["priv"] is not None:
- module.fail_json(msg="privileges require a database to be specified")
- privs = parse_privs(module.params["priv"], module.params["db"])
- no_password_changes = module.params["no_password_changes"]
- if module.params["encrypted"]:
- encrypted = "ENCRYPTED"
- else:
- encrypted = "UNENCRYPTED"
- expires = module.params["expires"]
- conn_limit = module.params["conn_limit"]
- role_attr_flags = module.params["role_attr_flags"]
- groups = module.params["groups"]
- if groups:
- groups = [e.strip() for e in groups]
- comment = module.params["comment"]
-
- conn_params = get_conn_params(module, module.params, warn_db_default=False)
- db_connection = connect_to_db(module, conn_params)
- cursor = db_connection.cursor(cursor_factory=DictCursor)
-
- try:
- role_attr_flags = parse_role_attrs(cursor, role_attr_flags)
- except InvalidFlagsError as e:
- module.fail_json(msg=to_native(e), exception=traceback.format_exc())
-
- kw = dict(user=user)
- changed = False
- user_removed = False
-
- if state == "present":
- if user_exists(cursor, user):
- try:
- changed = user_alter(db_connection, module, user, password,
- role_attr_flags, encrypted, expires, no_password_changes, conn_limit)
- except SQLParseError as e:
- module.fail_json(msg=to_native(e), exception=traceback.format_exc())
- else:
- try:
- changed = user_add(cursor, user, password,
- role_attr_flags, encrypted, expires, conn_limit)
- except psycopg2.ProgrammingError as e:
- module.fail_json(msg="Unable to add user with given requirement "
- "due to : %s" % to_native(e),
- exception=traceback.format_exc())
- except SQLParseError as e:
- module.fail_json(msg=to_native(e), exception=traceback.format_exc())
- try:
- changed = grant_privileges(cursor, user, privs) or changed
- except SQLParseError as e:
- module.fail_json(msg=to_native(e), exception=traceback.format_exc())
-
- if groups:
- target_roles = []
- target_roles.append(user)
- pg_membership = PgMembership(module, cursor, groups, target_roles)
- changed = pg_membership.grant() or changed
- executed_queries.extend(pg_membership.executed_queries)
-
- if comment is not None:
- try:
- changed = add_comment(cursor, user, comment) or changed
- except Exception as e:
- module.fail_json(msg='Unable to add comment on role: %s' % to_native(e),
- exception=traceback.format_exc())
-
- else:
- if user_exists(cursor, user):
- if module.check_mode:
- changed = True
- kw['user_removed'] = True
- else:
- try:
- changed = revoke_privileges(cursor, user, privs)
- user_removed = user_delete(cursor, user)
- except SQLParseError as e:
- module.fail_json(msg=to_native(e), exception=traceback.format_exc())
- changed = changed or user_removed
- if fail_on_user and not user_removed:
- msg = "Unable to remove user"
- module.fail_json(msg=msg)
- kw['user_removed'] = user_removed
-
- if changed:
- if module.check_mode:
- db_connection.rollback()
- else:
- db_connection.commit()
-
- kw['changed'] = changed
- kw['queries'] = executed_queries
- module.exit_json(**kw)
-
-
-if __name__ == '__main__':
- main()
diff --git a/test/support/integration/plugins/modules/rabbitmq_plugin.py b/test/support/integration/plugins/modules/rabbitmq_plugin.py
deleted file mode 100644
index 301bbfe282..0000000000
--- a/test/support/integration/plugins/modules/rabbitmq_plugin.py
+++ /dev/null
@@ -1,180 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Copyright: (c) 2013, Chatham Financial <oss@chathamfinancial.com>
-# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-
-ANSIBLE_METADATA = {
- 'metadata_version': '1.1',
- 'status': ['preview'],
- 'supported_by': 'community'
-}
-
-
-DOCUMENTATION = '''
----
-module: rabbitmq_plugin
-short_description: Manage RabbitMQ plugins
-description:
- - This module can be used to enable or disable RabbitMQ plugins.
-version_added: "1.1"
-author:
- - Chris Hoffman (@chrishoffman)
-options:
- names:
- description:
- - Comma-separated list of plugin names. Also, accepts plugin name.
- required: true
- aliases: [name]
- new_only:
- description:
- - Only enable missing plugins.
- - Does not disable plugins that are not in the names list.
- type: bool
- default: "no"
- state:
- description:
- - Specify if plugins are to be enabled or disabled.
- default: enabled
- choices: [enabled, disabled]
- prefix:
- description:
- - Specify a custom install prefix to a Rabbit.
- version_added: "1.3"
-'''
-
-EXAMPLES = '''
-- name: Enables the rabbitmq_management plugin
- rabbitmq_plugin:
- names: rabbitmq_management
- state: enabled
-
-- name: Enable multiple rabbitmq plugins
- rabbitmq_plugin:
- names: rabbitmq_management,rabbitmq_management_visualiser
- state: enabled
-
-- name: Disable plugin
- rabbitmq_plugin:
- names: rabbitmq_management
- state: disabled
-
-- name: Enable every plugin in list with existing plugins
- rabbitmq_plugin:
- names: rabbitmq_management,rabbitmq_management_visualiser,rabbitmq_shovel,rabbitmq_shovel_management
- state: enabled
- new_only: 'yes'
-'''
-
-RETURN = '''
-enabled:
- description: list of plugins enabled during task run
- returned: always
- type: list
- sample: ["rabbitmq_management"]
-disabled:
- description: list of plugins disabled during task run
- returned: always
- type: list
- sample: ["rabbitmq_management"]
-'''
-
-import os
-from ansible.module_utils.basic import AnsibleModule
-
-
-class RabbitMqPlugins(object):
-
- def __init__(self, module):
- self.module = module
- bin_path = ''
- if module.params['prefix']:
- if os.path.isdir(os.path.join(module.params['prefix'], 'bin')):
- bin_path = os.path.join(module.params['prefix'], 'bin')
- elif os.path.isdir(os.path.join(module.params['prefix'], 'sbin')):
- bin_path = os.path.join(module.params['prefix'], 'sbin')
- else:
- # No such path exists.
- module.fail_json(msg="No binary folder in prefix %s" % module.params['prefix'])
-
- self._rabbitmq_plugins = os.path.join(bin_path, "rabbitmq-plugins")
- else:
- self._rabbitmq_plugins = module.get_bin_path('rabbitmq-plugins', True)
-
- def _exec(self, args, run_in_check_mode=False):
- if not self.module.check_mode or (self.module.check_mode and run_in_check_mode):
- cmd = [self._rabbitmq_plugins]
- rc, out, err = self.module.run_command(cmd + args, check_rc=True)
- return out.splitlines()
- return list()
-
- def get_all(self):
- list_output = self._exec(['list', '-E', '-m'], True)
- plugins = []
- for plugin in list_output:
- if not plugin:
- break
- plugins.append(plugin)
-
- return plugins
-
- def enable(self, name):
- self._exec(['enable', name])
-
- def disable(self, name):
- self._exec(['disable', name])
-
-
-def main():
- arg_spec = dict(
- names=dict(required=True, aliases=['name']),
- new_only=dict(default='no', type='bool'),
- state=dict(default='enabled', choices=['enabled', 'disabled']),
- prefix=dict(required=False, default=None)
- )
- module = AnsibleModule(
- argument_spec=arg_spec,
- supports_check_mode=True
- )
-
- result = dict()
- names = module.params['names'].split(',')
- new_only = module.params['new_only']
- state = module.params['state']
-
- rabbitmq_plugins = RabbitMqPlugins(module)
- enabled_plugins = rabbitmq_plugins.get_all()
-
- enabled = []
- disabled = []
- if state == 'enabled':
- if not new_only:
- for plugin in enabled_plugins:
- if " " in plugin:
- continue
- if plugin not in names:
- rabbitmq_plugins.disable(plugin)
- disabled.append(plugin)
-
- for name in names:
- if name not in enabled_plugins:
- rabbitmq_plugins.enable(name)
- enabled.append(name)
- else:
- for plugin in enabled_plugins:
- if plugin in names:
- rabbitmq_plugins.disable(plugin)
- disabled.append(plugin)
-
- result['changed'] = len(enabled) > 0 or len(disabled) > 0
- result['enabled'] = enabled
- result['disabled'] = disabled
- module.exit_json(**result)
-
-
-if __name__ == '__main__':
- main()
diff --git a/test/support/integration/plugins/modules/rabbitmq_queue.py b/test/support/integration/plugins/modules/rabbitmq_queue.py
deleted file mode 100644
index 567ec8130d..0000000000
--- a/test/support/integration/plugins/modules/rabbitmq_queue.py
+++ /dev/null
@@ -1,257 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Copyright: (c) 2015, Manuel Sousa <manuel.sousa@gmail.com>
-# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-
-ANSIBLE_METADATA = {'metadata_version': '1.1',
- 'status': ['preview'],
- 'supported_by': 'community'}
-
-
-DOCUMENTATION = '''
----
-module: rabbitmq_queue
-author: Manuel Sousa (@manuel-sousa)
-version_added: "2.0"
-
-short_description: Manage rabbitMQ queues
-description:
- - This module uses rabbitMQ Rest API to create/delete queues
-requirements: [ "requests >= 1.0.0" ]
-options:
- name:
- description:
- - Name of the queue
- required: true
- state:
- description:
- - Whether the queue should be present or absent
- choices: [ "present", "absent" ]
- default: present
- durable:
- description:
- - whether queue is durable or not
- type: bool
- default: 'yes'
- auto_delete:
- description:
- - if the queue should delete itself after all queues/queues unbound from it
- type: bool
- default: 'no'
- message_ttl:
- description:
- - How long a message can live in queue before it is discarded (milliseconds)
- default: forever
- auto_expires:
- description:
- - How long a queue can be unused before it is automatically deleted (milliseconds)
- default: forever
- max_length:
- description:
- - How many messages can the queue contain before it starts rejecting
- default: no limit
- dead_letter_exchange:
- description:
- - Optional name of an exchange to which messages will be republished if they
- - are rejected or expire
- dead_letter_routing_key:
- description:
- - Optional replacement routing key to use when a message is dead-lettered.
- - Original routing key will be used if unset
- max_priority:
- description:
- - Maximum number of priority levels for the queue to support.
- - If not set, the queue will not support message priorities.
- - Larger numbers indicate higher priority.
- version_added: "2.4"
- arguments:
- description:
- - extra arguments for queue. If defined this argument is a key/value dictionary
- default: {}
-extends_documentation_fragment:
- - rabbitmq
-'''
-
-EXAMPLES = '''
-# Create a queue
-- rabbitmq_queue:
- name: myQueue
-
-# Create a queue on remote host
-- rabbitmq_queue:
- name: myRemoteQueue
- login_user: user
- login_password: secret
- login_host: remote.example.org
-'''
-
-import json
-import traceback
-
-REQUESTS_IMP_ERR = None
-try:
- import requests
- HAS_REQUESTS = True
-except ImportError:
- REQUESTS_IMP_ERR = traceback.format_exc()
- HAS_REQUESTS = False
-
-from ansible.module_utils.basic import AnsibleModule, missing_required_lib
-from ansible.module_utils.six.moves.urllib import parse as urllib_parse
-from ansible.module_utils.rabbitmq import rabbitmq_argument_spec
-
-
-def main():
-
- argument_spec = rabbitmq_argument_spec()
- argument_spec.update(
- dict(
- state=dict(default='present', choices=['present', 'absent'], type='str'),
- name=dict(required=True, type='str'),
- durable=dict(default=True, type='bool'),
- auto_delete=dict(default=False, type='bool'),
- message_ttl=dict(default=None, type='int'),
- auto_expires=dict(default=None, type='int'),
- max_length=dict(default=None, type='int'),
- dead_letter_exchange=dict(default=None, type='str'),
- dead_letter_routing_key=dict(default=None, type='str'),
- arguments=dict(default=dict(), type='dict'),
- max_priority=dict(default=None, type='int')
- )
- )
- module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
-
- url = "%s://%s:%s/api/queues/%s/%s" % (
- module.params['login_protocol'],
- module.params['login_host'],
- module.params['login_port'],
- urllib_parse.quote(module.params['vhost'], ''),
- module.params['name']
- )
-
- if not HAS_REQUESTS:
- module.fail_json(msg=missing_required_lib("requests"), exception=REQUESTS_IMP_ERR)
-
- result = dict(changed=False, name=module.params['name'])
-
- # Check if queue already exists
- r = requests.get(url, auth=(module.params['login_user'], module.params['login_password']),
- verify=module.params['ca_cert'], cert=(module.params['client_cert'], module.params['client_key']))
-
- if r.status_code == 200:
- queue_exists = True
- response = r.json()
- elif r.status_code == 404:
- queue_exists = False
- response = r.text
- else:
- module.fail_json(
- msg="Invalid response from RESTAPI when trying to check if queue exists",
- details=r.text
- )
-
- if module.params['state'] == 'present':
- change_required = not queue_exists
- else:
- change_required = queue_exists
-
- # Check if attributes change on existing queue
- if not change_required and r.status_code == 200 and module.params['state'] == 'present':
- if not (
- response['durable'] == module.params['durable'] and
- response['auto_delete'] == module.params['auto_delete'] and
- (
- ('x-message-ttl' in response['arguments'] and response['arguments']['x-message-ttl'] == module.params['message_ttl']) or
- ('x-message-ttl' not in response['arguments'] and module.params['message_ttl'] is None)
- ) and
- (
- ('x-expires' in response['arguments'] and response['arguments']['x-expires'] == module.params['auto_expires']) or
- ('x-expires' not in response['arguments'] and module.params['auto_expires'] is None)
- ) and
- (
- ('x-max-length' in response['arguments'] and response['arguments']['x-max-length'] == module.params['max_length']) or
- ('x-max-length' not in response['arguments'] and module.params['max_length'] is None)
- ) and
- (
- ('x-dead-letter-exchange' in response['arguments'] and
- response['arguments']['x-dead-letter-exchange'] == module.params['dead_letter_exchange']) or
- ('x-dead-letter-exchange' not in response['arguments'] and module.params['dead_letter_exchange'] is None)
- ) and
- (
- ('x-dead-letter-routing-key' in response['arguments'] and
- response['arguments']['x-dead-letter-routing-key'] == module.params['dead_letter_routing_key']) or
- ('x-dead-letter-routing-key' not in response['arguments'] and module.params['dead_letter_routing_key'] is None)
- ) and
- (
- ('x-max-priority' in response['arguments'] and
- response['arguments']['x-max-priority'] == module.params['max_priority']) or
- ('x-max-priority' not in response['arguments'] and module.params['max_priority'] is None)
- )
- ):
- module.fail_json(
- msg="RabbitMQ RESTAPI doesn't support attribute changes for existing queues",
- )
-
- # Copy parameters to arguments as used by RabbitMQ
- for k, v in {
- 'message_ttl': 'x-message-ttl',
- 'auto_expires': 'x-expires',
- 'max_length': 'x-max-length',
- 'dead_letter_exchange': 'x-dead-letter-exchange',
- 'dead_letter_routing_key': 'x-dead-letter-routing-key',
- 'max_priority': 'x-max-priority'
- }.items():
- if module.params[k] is not None:
- module.params['arguments'][v] = module.params[k]
-
- # Exit if check_mode
- if module.check_mode:
- result['changed'] = change_required
- result['details'] = response
- result['arguments'] = module.params['arguments']
- module.exit_json(**result)
-
- # Do changes
- if change_required:
- if module.params['state'] == 'present':
- r = requests.put(
- url,
- auth=(module.params['login_user'], module.params['login_password']),
- headers={"content-type": "application/json"},
- data=json.dumps({
- "durable": module.params['durable'],
- "auto_delete": module.params['auto_delete'],
- "arguments": module.params['arguments']
- }),
- verify=module.params['ca_cert'],
- cert=(module.params['client_cert'], module.params['client_key'])
- )
- elif module.params['state'] == 'absent':
- r = requests.delete(url, auth=(module.params['login_user'], module.params['login_password']),
- verify=module.params['ca_cert'], cert=(module.params['client_cert'], module.params['client_key']))
-
- # RabbitMQ 3.6.7 changed this response code from 204 to 201
- if r.status_code == 204 or r.status_code == 201:
- result['changed'] = True
- module.exit_json(**result)
- else:
- module.fail_json(
- msg="Error creating queue",
- status=r.status_code,
- details=r.text
- )
-
- else:
- module.exit_json(
- changed=False,
- name=module.params['name']
- )
-
-
-if __name__ == '__main__':
- main()
diff --git a/test/support/integration/plugins/modules/s3_bucket.py b/test/support/integration/plugins/modules/s3_bucket.py
deleted file mode 100644
index f35cf53b5e..0000000000
--- a/test/support/integration/plugins/modules/s3_bucket.py
+++ /dev/null
@@ -1,740 +0,0 @@
-#!/usr/bin/python
-#
-# This is a free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This Ansible library is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this library. If not, see <http://www.gnu.org/licenses/>.
-
-from __future__ import (absolute_import, division, print_function)
-__metaclass__ = type
-
-ANSIBLE_METADATA = {'metadata_version': '1.1',
- 'status': ['stableinterface'],
- 'supported_by': 'core'}
-
-
-DOCUMENTATION = '''
----
-module: s3_bucket
-short_description: Manage S3 buckets in AWS, DigitalOcean, Ceph, Walrus, FakeS3 and StorageGRID
-description:
- - Manage S3 buckets in AWS, DigitalOcean, Ceph, Walrus, FakeS3 and StorageGRID
-version_added: "2.0"
-requirements: [ boto3 ]
-author: "Rob White (@wimnat)"
-options:
- force:
- description:
- - When trying to delete a bucket, delete all keys (including versions and delete markers)
- in the bucket first (an s3 bucket must be empty for a successful deletion)
- type: bool
- default: 'no'
- name:
- description:
- - Name of the s3 bucket
- required: true
- type: str
- policy:
- description:
- - The JSON policy as a string.
- type: json
- s3_url:
- description:
- - S3 URL endpoint for usage with DigitalOcean, Ceph, Eucalyptus and fakes3 etc.
- - Assumes AWS if not specified.
- - For Walrus, use FQDN of the endpoint without scheme nor path.
- aliases: [ S3_URL ]
- type: str
- ceph:
- description:
- - Enable API compatibility with Ceph. It takes into account the S3 API subset working
- with Ceph in order to provide the same module behaviour where possible.
- type: bool
- version_added: "2.2"
- requester_pays:
- description:
- - With Requester Pays buckets, the requester instead of the bucket owner pays the cost
- of the request and the data download from the bucket.
- type: bool
- default: False
- state:
- description:
- - Create or remove the s3 bucket
- required: false
- default: present
- choices: [ 'present', 'absent' ]
- type: str
- tags:
- description:
- - tags dict to apply to bucket
- type: dict
- purge_tags:
- description:
- - whether to remove tags that aren't present in the C(tags) parameter
- type: bool
- default: True
- version_added: "2.9"
- versioning:
- description:
- - Whether versioning is enabled or disabled (note that once versioning is enabled, it can only be suspended)
- type: bool
- encryption:
- description:
- - Describes the default server-side encryption to apply to new objects in the bucket.
- In order to remove the server-side encryption, the encryption needs to be set to 'none' explicitly.
- choices: [ 'none', 'AES256', 'aws:kms' ]
- version_added: "2.9"
- type: str
- encryption_key_id:
- description: KMS master key ID to use for the default encryption. This parameter is allowed if encryption is aws:kms. If
- not specified then it will default to the AWS provided KMS key.
- version_added: "2.9"
- type: str
-extends_documentation_fragment:
- - aws
- - ec2
-notes:
- - If C(requestPayment), C(policy), C(tagging) or C(versioning)
- operations/API aren't implemented by the endpoint, module doesn't fail
- if each parameter satisfies the following condition.
- I(requester_pays) is C(False), I(policy), I(tags), and I(versioning) are C(None).
-'''
-
-EXAMPLES = '''
-# Note: These examples do not set authentication details, see the AWS Guide for details.
-
-# Create a simple s3 bucket
-- s3_bucket:
- name: mys3bucket
- state: present
-
-# Create a simple s3 bucket on Ceph Rados Gateway
-- s3_bucket:
- name: mys3bucket
- s3_url: http://your-ceph-rados-gateway-server.xxx
- ceph: true
-
-# Remove an s3 bucket and any keys it contains
-- s3_bucket:
- name: mys3bucket
- state: absent
- force: yes
-
-# Create a bucket, add a policy from a file, enable requester pays, enable versioning and tag
-- s3_bucket:
- name: mys3bucket
- policy: "{{ lookup('file','policy.json') }}"
- requester_pays: yes
- versioning: yes
- tags:
- example: tag1
- another: tag2
-
-# Create a simple DigitalOcean Spaces bucket using their provided regional endpoint
-- s3_bucket:
- name: mydobucket
- s3_url: 'https://nyc3.digitaloceanspaces.com'
-
-# Create a bucket with AES256 encryption
-- s3_bucket:
- name: mys3bucket
- state: present
- encryption: "AES256"
-
-# Create a bucket with aws:kms encryption, KMS key
-- s3_bucket:
- name: mys3bucket
- state: present
- encryption: "aws:kms"
- encryption_key_id: "arn:aws:kms:us-east-1:1234/5678example"
-
-# Create a bucket with aws:kms encryption, default key
-- s3_bucket:
- name: mys3bucket
- state: present
- encryption: "aws:kms"
-'''
-
-import json
-import os
-import time
-
-from ansible.module_utils.six.moves.urllib.parse import urlparse
-from ansible.module_utils.six import string_types
-from ansible.module_utils.basic import to_text
-from ansible.module_utils.aws.core import AnsibleAWSModule, is_boto3_error_code
-from ansible.module_utils.ec2 import compare_policies, ec2_argument_spec, boto3_tag_list_to_ansible_dict, ansible_dict_to_boto3_tag_list
-from ansible.module_utils.ec2 import get_aws_connection_info, boto3_conn, AWSRetry
-
-try:
- from botocore.exceptions import BotoCoreError, ClientError, EndpointConnectionError, WaiterError
-except ImportError:
- pass # handled by AnsibleAWSModule
-
-
-def create_or_update_bucket(s3_client, module, location):
-
- policy = module.params.get("policy")
- name = module.params.get("name")
- requester_pays = module.params.get("requester_pays")
- tags = module.params.get("tags")
- purge_tags = module.params.get("purge_tags")
- versioning = module.params.get("versioning")
- encryption = module.params.get("encryption")
- encryption_key_id = module.params.get("encryption_key_id")
- changed = False
- result = {}
-
- try:
- bucket_is_present = bucket_exists(s3_client, name)
- except EndpointConnectionError as e:
- module.fail_json_aws(e, msg="Invalid endpoint provided: %s" % to_text(e))
- except (BotoCoreError, ClientError) as e:
- module.fail_json_aws(e, msg="Failed to check bucket presence")
-
- if not bucket_is_present:
- try:
- bucket_changed = create_bucket(s3_client, name, location)
- s3_client.get_waiter('bucket_exists').wait(Bucket=name)
- changed = changed or bucket_changed
- except WaiterError as e:
- module.fail_json_aws(e, msg='An error occurred waiting for the bucket to become available')
- except (BotoCoreError, ClientError) as e:
- module.fail_json_aws(e, msg="Failed while creating bucket")
-
- # Versioning
- try:
- versioning_status = get_bucket_versioning(s3_client, name)
- except BotoCoreError as exp:
- module.fail_json_aws(exp, msg="Failed to get bucket versioning")
- except ClientError as exp:
- if exp.response['Error']['Code'] != 'NotImplemented' or versioning is not None:
- module.fail_json_aws(exp, msg="Failed to get bucket versioning")
- else:
- if versioning is not None:
- required_versioning = None
- if versioning and versioning_status.get('Status') != "Enabled":
- required_versioning = 'Enabled'
- elif not versioning and versioning_status.get('Status') == "Enabled":
- required_versioning = 'Suspended'
-
- if required_versioning:
- try:
- put_bucket_versioning(s3_client, name, required_versioning)
- changed = True
- except (BotoCoreError, ClientError) as e:
- module.fail_json_aws(e, msg="Failed to update bucket versioning")
-
- versioning_status = wait_versioning_is_applied(module, s3_client, name, required_versioning)
-
- # This output format is there to ensure compatibility with previous versions of the module
- result['versioning'] = {
- 'Versioning': versioning_status.get('Status', 'Disabled'),
- 'MfaDelete': versioning_status.get('MFADelete', 'Disabled'),
- }
-
- # Requester pays
- try:
- requester_pays_status = get_bucket_request_payment(s3_client, name)
- except BotoCoreError as exp:
- module.fail_json_aws(exp, msg="Failed to get bucket request payment")
- except ClientError as exp:
- if exp.response['Error']['Code'] not in ('NotImplemented', 'XNotImplemented') or requester_pays:
- module.fail_json_aws(exp, msg="Failed to get bucket request payment")
- else:
- if requester_pays:
- payer = 'Requester' if requester_pays else 'BucketOwner'
- if requester_pays_status != payer:
- put_bucket_request_payment(s3_client, name, payer)
- requester_pays_status = wait_payer_is_applied(module, s3_client, name, payer, should_fail=False)
- if requester_pays_status is None:
- # We have seen that it happens quite a lot of times that the put request was not taken into
- # account, so we retry one more time
- put_bucket_request_payment(s3_client, name, payer)
- requester_pays_status = wait_payer_is_applied(module, s3_client, name, payer, should_fail=True)
- changed = True
-
- result['requester_pays'] = requester_pays
-
- # Policy
- try:
- current_policy = get_bucket_policy(s3_client, name)
- except BotoCoreError as exp:
- module.fail_json_aws(exp, msg="Failed to get bucket policy")
- except ClientError as exp:
- if exp.response['Error']['Code'] != 'NotImplemented' or policy is not None:
- module.fail_json_aws(exp, msg="Failed to get bucket policy")
- else:
- if policy is not None:
- if isinstance(policy, string_types):
- policy = json.loads(policy)
-
- if not policy and current_policy:
- try:
- delete_bucket_policy(s3_client, name)
- except (BotoCoreError, ClientError) as e:
- module.fail_json_aws(e, msg="Failed to delete bucket policy")
- current_policy = wait_policy_is_applied(module, s3_client, name, policy)
- changed = True
- elif compare_policies(current_policy, policy):
- try:
- put_bucket_policy(s3_client, name, policy)
- except (BotoCoreError, ClientError) as e:
- module.fail_json_aws(e, msg="Failed to update bucket policy")
- current_policy = wait_policy_is_applied(module, s3_client, name, policy, should_fail=False)
- if current_policy is None:
- # As for request payement, it happens quite a lot of times that the put request was not taken into
- # account, so we retry one more time
- put_bucket_policy(s3_client, name, policy)
- current_policy = wait_policy_is_applied(module, s3_client, name, policy, should_fail=True)
- changed = True
-
- result['policy'] = current_policy
-
- # Tags
- try:
- current_tags_dict = get_current_bucket_tags_dict(s3_client, name)
- except BotoCoreError as exp:
- module.fail_json_aws(exp, msg="Failed to get bucket tags")
- except ClientError as exp:
- if exp.response['Error']['Code'] not in ('NotImplemented', 'XNotImplemented') or tags is not None:
- module.fail_json_aws(exp, msg="Failed to get bucket tags")
- else:
- if tags is not None:
- # Tags are always returned as text
- tags = dict((to_text(k), to_text(v)) for k, v in tags.items())
- if not purge_tags:
- # Ensure existing tags that aren't updated by desired tags remain
- current_copy = current_tags_dict.copy()
- current_copy.update(tags)
- tags = current_copy
- if current_tags_dict != tags:
- if tags:
- try:
- put_bucket_tagging(s3_client, name, tags)
- except (BotoCoreError, ClientError) as e:
- module.fail_json_aws(e, msg="Failed to update bucket tags")
- else:
- if purge_tags:
- try:
- delete_bucket_tagging(s3_client, name)
- except (BotoCoreError, ClientError) as e:
- module.fail_json_aws(e, msg="Failed to delete bucket tags")
- current_tags_dict = wait_tags_are_applied(module, s3_client, name, tags)
- changed = True
-
- result['tags'] = current_tags_dict
-
- # Encryption
- if hasattr(s3_client, "get_bucket_encryption"):
- try:
- current_encryption = get_bucket_encryption(s3_client, name)
- except (ClientError, BotoCoreError) as e:
- module.fail_json_aws(e, msg="Failed to get bucket encryption")
- elif encryption is not None:
- module.fail_json(msg="Using bucket encryption requires botocore version >= 1.7.41")
-
- if encryption is not None:
- current_encryption_algorithm = current_encryption.get('SSEAlgorithm') if current_encryption else None
- current_encryption_key = current_encryption.get('KMSMasterKeyID') if current_encryption else None
- if encryption == 'none' and current_encryption_algorithm is not None:
- try:
- delete_bucket_encryption(s3_client, name)
- except (BotoCoreError, ClientError) as e:
- module.fail_json_aws(e, msg="Failed to delete bucket encryption")
- current_encryption = wait_encryption_is_applied(module, s3_client, name, None)
- changed = True
- elif encryption != 'none' and (encryption != current_encryption_algorithm) or (encryption == 'aws:kms' and current_encryption_key != encryption_key_id):
- expected_encryption = {'SSEAlgorithm': encryption}
- if encryption == 'aws:kms' and encryption_key_id is not None:
- expected_encryption.update({'KMSMasterKeyID': encryption_key_id})
- try:
- put_bucket_encryption(s3_client, name, expected_encryption)
- except (BotoCoreError, ClientError) as e:
- module.fail_json_aws(e, msg="Failed to set bucket encryption")
- current_encryption = wait_encryption_is_applied(module, s3_client, name, expected_encryption)
- changed = True
-
- result['encryption'] = current_encryption
-
- module.exit_json(changed=changed, name=name, **result)
-
-
-def bucket_exists(s3_client, bucket_name):
- # head_bucket appeared to be really inconsistent, so we use list_buckets instead,
- # and loop over all the buckets, even if we know it's less performant :(
- all_buckets = s3_client.list_buckets(Bucket=bucket_name)['Buckets']
- return any(bucket['Name'] == bucket_name for bucket in all_buckets)
-
-
-@AWSRetry.exponential_backoff(max_delay=120)
-def create_bucket(s3_client, bucket_name, location):
- try:
- configuration = {}
- if location not in ('us-east-1', None):
- configuration['LocationConstraint'] = location
- if len(configuration) > 0:
- s3_client.create_bucket(Bucket=bucket_name, CreateBucketConfiguration=configuration)
- else:
- s3_client.create_bucket(Bucket=bucket_name)
- return True
- except ClientError as e:
- error_code = e.response['Error']['Code']
- if error_code == 'BucketAlreadyOwnedByYou':
- # We should never get there since we check the bucket presence before calling the create_or_update_bucket
- # method. However, the AWS Api sometimes fails to report bucket presence, so we catch this exception
- return False
- else:
- raise e
-
-
-@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket'])
-def put_bucket_tagging(s3_client, bucket_name, tags):
- s3_client.put_bucket_tagging(Bucket=bucket_name, Tagging={'TagSet': ansible_dict_to_boto3_tag_list(tags)})
-
-
-@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket'])
-def put_bucket_policy(s3_client, bucket_name, policy):
- s3_client.put_bucket_policy(Bucket=bucket_name, Policy=json.dumps(policy))
-
-
-@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket'])
-def delete_bucket_policy(s3_client, bucket_name):
- s3_client.delete_bucket_policy(Bucket=bucket_name)
-
-
-@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket'])
-def get_bucket_policy(s3_client, bucket_name):
- try:
- current_policy = json.loads(s3_client.get_bucket_policy(Bucket=bucket_name).get('Policy'))
- except ClientError as e:
- if e.response['Error']['Code'] == 'NoSuchBucketPolicy':
- current_policy = None
- else:
- raise e
- return current_policy
-
-
-@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket'])
-def put_bucket_request_payment(s3_client, bucket_name, payer):
- s3_client.put_bucket_request_payment(Bucket=bucket_name, RequestPaymentConfiguration={'Payer': payer})
-
-
-@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket'])
-def get_bucket_request_payment(s3_client, bucket_name):
- return s3_client.get_bucket_request_payment(Bucket=bucket_name).get('Payer')
-
-
-@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket'])
-def get_bucket_versioning(s3_client, bucket_name):
- return s3_client.get_bucket_versioning(Bucket=bucket_name)
-
-
-@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket'])
-def put_bucket_versioning(s3_client, bucket_name, required_versioning):
- s3_client.put_bucket_versioning(Bucket=bucket_name, VersioningConfiguration={'Status': required_versioning})
-
-
-@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket'])
-def get_bucket_encryption(s3_client, bucket_name):
- try:
- result = s3_client.get_bucket_encryption(Bucket=bucket_name)
- return result.get('ServerSideEncryptionConfiguration', {}).get('Rules', [])[0].get('ApplyServerSideEncryptionByDefault')
- except ClientError as e:
- if e.response['Error']['Code'] == 'ServerSideEncryptionConfigurationNotFoundError':
- return None
- else:
- raise e
- except (IndexError, KeyError):
- return None
-
-
-@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket'])
-def put_bucket_encryption(s3_client, bucket_name, encryption):
- server_side_encryption_configuration = {'Rules': [{'ApplyServerSideEncryptionByDefault': encryption}]}
- s3_client.put_bucket_encryption(Bucket=bucket_name, ServerSideEncryptionConfiguration=server_side_encryption_configuration)
-
-
-@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket'])
-def delete_bucket_tagging(s3_client, bucket_name):
- s3_client.delete_bucket_tagging(Bucket=bucket_name)
-
-
-@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket'])
-def delete_bucket_encryption(s3_client, bucket_name):
- s3_client.delete_bucket_encryption(Bucket=bucket_name)
-
-
-@AWSRetry.exponential_backoff(max_delay=120)
-def delete_bucket(s3_client, bucket_name):
- try:
- s3_client.delete_bucket(Bucket=bucket_name)
- except ClientError as e:
- if e.response['Error']['Code'] == 'NoSuchBucket':
- # This means bucket should have been in a deleting state when we checked it existence
- # We just ignore the error
- pass
- else:
- raise e
-
-
-def wait_policy_is_applied(module, s3_client, bucket_name, expected_policy, should_fail=True):
- for dummy in range(0, 12):
- try:
- current_policy = get_bucket_policy(s3_client, bucket_name)
- except (ClientError, BotoCoreError) as e:
- module.fail_json_aws(e, msg="Failed to get bucket policy")
-
- if compare_policies(current_policy, expected_policy):
- time.sleep(5)
- else:
- return current_policy
- if should_fail:
- module.fail_json(msg="Bucket policy failed to apply in the expected time")
- else:
- return None
-
-
-def wait_payer_is_applied(module, s3_client, bucket_name, expected_payer, should_fail=True):
- for dummy in range(0, 12):
- try:
- requester_pays_status = get_bucket_request_payment(s3_client, bucket_name)
- except (BotoCoreError, ClientError) as e:
- module.fail_json_aws(e, msg="Failed to get bucket request payment")
- if requester_pays_status != expected_payer:
- time.sleep(5)
- else:
- return requester_pays_status
- if should_fail:
- module.fail_json(msg="Bucket request payment failed to apply in the expected time")
- else:
- return None
-
-
-def wait_encryption_is_applied(module, s3_client, bucket_name, expected_encryption):
- for dummy in range(0, 12):
- try:
- encryption = get_bucket_encryption(s3_client, bucket_name)
- except (BotoCoreError, ClientError) as e:
- module.fail_json_aws(e, msg="Failed to get updated encryption for bucket")
- if encryption != expected_encryption:
- time.sleep(5)
- else:
- return encryption
- module.fail_json(msg="Bucket encryption failed to apply in the expected time")
-
-
-def wait_versioning_is_applied(module, s3_client, bucket_name, required_versioning):
- for dummy in range(0, 24):
- try:
- versioning_status = get_bucket_versioning(s3_client, bucket_name)
- except (BotoCoreError, ClientError) as e:
- module.fail_json_aws(e, msg="Failed to get updated versioning for bucket")
- if versioning_status.get('Status') != required_versioning:
- time.sleep(8)
- else:
- return versioning_status
- module.fail_json(msg="Bucket versioning failed to apply in the expected time")
-
-
-def wait_tags_are_applied(module, s3_client, bucket_name, expected_tags_dict):
- for dummy in range(0, 12):
- try:
- current_tags_dict = get_current_bucket_tags_dict(s3_client, bucket_name)
- except (ClientError, BotoCoreError) as e:
- module.fail_json_aws(e, msg="Failed to get bucket policy")
- if current_tags_dict != expected_tags_dict:
- time.sleep(5)
- else:
- return current_tags_dict
- module.fail_json(msg="Bucket tags failed to apply in the expected time")
-
-
-def get_current_bucket_tags_dict(s3_client, bucket_name):
- try:
- current_tags = s3_client.get_bucket_tagging(Bucket=bucket_name).get('TagSet')
- except ClientError as e:
- if e.response['Error']['Code'] == 'NoSuchTagSet':
- return {}
- raise e
-
- return boto3_tag_list_to_ansible_dict(current_tags)
-
-
-def paginated_list(s3_client, **pagination_params):
- pg = s3_client.get_paginator('list_objects_v2')
- for page in pg.paginate(**pagination_params):
- yield [data['Key'] for data in page.get('Contents', [])]
-
-
-def paginated_versions_list(s3_client, **pagination_params):
- try:
- pg = s3_client.get_paginator('list_object_versions')
- for page in pg.paginate(**pagination_params):
- # We have to merge the Versions and DeleteMarker lists here, as DeleteMarkers can still prevent a bucket deletion
- yield [(data['Key'], data['VersionId']) for data in (page.get('Versions', []) + page.get('DeleteMarkers', []))]
- except is_boto3_error_code('NoSuchBucket'):
- yield []
-
-
-def destroy_bucket(s3_client, module):
-
- force = module.params.get("force")
- name = module.params.get("name")
- try:
- bucket_is_present = bucket_exists(s3_client, name)
- except EndpointConnectionError as e:
- module.fail_json_aws(e, msg="Invalid endpoint provided: %s" % to_text(e))
- except (BotoCoreError, ClientError) as e:
- module.fail_json_aws(e, msg="Failed to check bucket presence")
-
- if not bucket_is_present:
- module.exit_json(changed=False)
-
- if force:
- # if there are contents then we need to delete them (including versions) before we can delete the bucket
- try:
- for key_version_pairs in paginated_versions_list(s3_client, Bucket=name):
- formatted_keys = [{'Key': key, 'VersionId': version} for key, version in key_version_pairs]
- for fk in formatted_keys:
- # remove VersionId from cases where they are `None` so that
- # unversioned objects are deleted using `DeleteObject`
- # rather than `DeleteObjectVersion`, improving backwards
- # compatibility with older IAM policies.
- if not fk.get('VersionId'):
- fk.pop('VersionId')
-
- if formatted_keys:
- resp = s3_client.delete_objects(Bucket=name, Delete={'Objects': formatted_keys})
- if resp.get('Errors'):
- module.fail_json(
- msg='Could not empty bucket before deleting. Could not delete objects: {0}'.format(
- ', '.join([k['Key'] for k in resp['Errors']])
- ),
- errors=resp['Errors'], response=resp
- )
- except (BotoCoreError, ClientError) as e:
- module.fail_json_aws(e, msg="Failed while deleting bucket")
-
- try:
- delete_bucket(s3_client, name)
- s3_client.get_waiter('bucket_not_exists').wait(Bucket=name, WaiterConfig=dict(Delay=5, MaxAttempts=60))
- except WaiterError as e:
- module.fail_json_aws(e, msg='An error occurred waiting for the bucket to be deleted.')
- except (BotoCoreError, ClientError) as e:
- module.fail_json_aws(e, msg="Failed to delete bucket")
-
- module.exit_json(changed=True)
-
-
-def is_fakes3(s3_url):
- """ Return True if s3_url has scheme fakes3:// """
- if s3_url is not None:
- return urlparse(s3_url).scheme in ('fakes3', 'fakes3s')
- else:
- return False
-
-
-def get_s3_client(module, aws_connect_kwargs, location, ceph, s3_url):
- if s3_url and ceph: # TODO - test this
- ceph = urlparse(s3_url)
- params = dict(module=module, conn_type='client', resource='s3', use_ssl=ceph.scheme == 'https', region=location, endpoint=s3_url, **aws_connect_kwargs)
- elif is_fakes3(s3_url):
- fakes3 = urlparse(s3_url)
- port = fakes3.port
- if fakes3.scheme == 'fakes3s':
- protocol = "https"
- if port is None:
- port = 443
- else:
- protocol = "http"
- if port is None:
- port = 80
- params = dict(module=module, conn_type='client', resource='s3', region=location,
- endpoint="%s://%s:%s" % (protocol, fakes3.hostname, to_text(port)),
- use_ssl=fakes3.scheme == 'fakes3s', **aws_connect_kwargs)
- else:
- params = dict(module=module, conn_type='client', resource='s3', region=location, endpoint=s3_url, **aws_connect_kwargs)
- return boto3_conn(**params)
-
-
-def main():
-
- argument_spec = ec2_argument_spec()
- argument_spec.update(
- dict(
- force=dict(default=False, type='bool'),
- policy=dict(type='json'),
- name=dict(required=True),
- requester_pays=dict(default=False, type='bool'),
- s3_url=dict(aliases=['S3_URL']),
- state=dict(default='present', choices=['present', 'absent']),
- tags=dict(type='dict'),
- purge_tags=dict(type='bool', default=True),
- versioning=dict(type='bool'),
- ceph=dict(default=False, type='bool'),
- encryption=dict(choices=['none', 'AES256', 'aws:kms']),
- encryption_key_id=dict()
- )
- )
-
- module = AnsibleAWSModule(
- argument_spec=argument_spec,
- )
-
- region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True)
-
- if region in ('us-east-1', '', None):
- # default to US Standard region
- location = 'us-east-1'
- else:
- # Boto uses symbolic names for locations but region strings will
- # actually work fine for everything except us-east-1 (US Standard)
- location = region
-
- s3_url = module.params.get('s3_url')
- ceph = module.params.get('ceph')
-
- # allow eucarc environment variables to be used if ansible vars aren't set
- if not s3_url and 'S3_URL' in os.environ:
- s3_url = os.environ['S3_URL']
-
- if ceph and not s3_url:
- module.fail_json(msg='ceph flavour requires s3_url')
-
- # Look at s3_url and tweak connection settings
- # if connecting to Ceph RGW, Walrus or fakes3
- if s3_url:
- for key in ['validate_certs', 'security_token', 'profile_name']:
- aws_connect_kwargs.pop(key, None)
- s3_client = get_s3_client(module, aws_connect_kwargs, location, ceph, s3_url)
-
- if s3_client is None: # this should never happen
- module.fail_json(msg='Unknown error, failed to create s3 connection, no information from boto.')
-
- state = module.params.get("state")
- encryption = module.params.get("encryption")
- encryption_key_id = module.params.get("encryption_key_id")
-
- # Parameter validation
- if encryption_key_id is not None and encryption is None:
- module.fail_json(msg="You must specify encryption parameter along with encryption_key_id.")
- elif encryption_key_id is not None and encryption != 'aws:kms':
- module.fail_json(msg="Only 'aws:kms' is a valid option for encryption parameter when you specify encryption_key_id.")
-
- if state == 'present':
- create_or_update_bucket(s3_client, module, location)
- elif state == 'absent':
- destroy_bucket(s3_client, module)
-
-
-if __name__ == '__main__':
- main()
diff --git a/test/support/integration/plugins/modules/selogin.py b/test/support/integration/plugins/modules/selogin.py
deleted file mode 100644
index 6429ef36e1..0000000000
--- a/test/support/integration/plugins/modules/selogin.py
+++ /dev/null
@@ -1,260 +0,0 @@
-#!/usr/bin/python
-
-# (c) 2017, Petr Lautrbach <plautrba@redhat.com>
-# Based on seport.py module (c) 2014, Dan Keder <dan.keder@gmail.com>
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-ANSIBLE_METADATA = {'metadata_version': '1.1',
- 'status': ['preview'],
- 'supported_by': 'community'}
-
-DOCUMENTATION = '''
----
-module: selogin
-short_description: Manages linux user to SELinux user mapping
-description:
- - Manages linux user to SELinux user mapping
-version_added: "2.8"
-options:
- login:
- description:
- - a Linux user
- required: true
- seuser:
- description:
- - SELinux user name
- required: true
- selevel:
- aliases: [ serange ]
- description:
- - MLS/MCS Security Range (MLS/MCS Systems only) SELinux Range for SELinux login mapping defaults to the SELinux user record range.
- default: s0
- state:
- description:
- - Desired mapping value.
- required: true
- default: present
- choices: [ 'present', 'absent' ]
- reload:
- description:
- - Reload SELinux policy after commit.
- default: yes
- ignore_selinux_state:
- description:
- - Run independent of selinux runtime state
- type: bool
- default: false
-notes:
- - The changes are persistent across reboots
- - Not tested on any debian based system
-requirements: [ 'libselinux', 'policycoreutils' ]
-author:
-- Dan Keder (@dankeder)
-- Petr Lautrbach (@bachradsusi)
-- James Cassell (@jamescassell)
-'''
-
-EXAMPLES = '''
-# Modify the default user on the system to the guest_u user
-- selogin:
- login: __default__
- seuser: guest_u
- state: present
-
-# Assign gijoe user on an MLS machine a range and to the staff_u user
-- selogin:
- login: gijoe
- seuser: staff_u
- serange: SystemLow-Secret
- state: present
-
-# Assign all users in the engineering group to the staff_u user
-- selogin:
- login: '%engineering'
- seuser: staff_u
- state: present
-'''
-
-RETURN = r'''
-# Default return values
-'''
-
-
-import traceback
-
-SELINUX_IMP_ERR = None
-try:
- import selinux
- HAVE_SELINUX = True
-except ImportError:
- SELINUX_IMP_ERR = traceback.format_exc()
- HAVE_SELINUX = False
-
-SEOBJECT_IMP_ERR = None
-try:
- import seobject
- HAVE_SEOBJECT = True
-except ImportError:
- SEOBJECT_IMP_ERR = traceback.format_exc()
- HAVE_SEOBJECT = False
-
-
-from ansible.module_utils.basic import AnsibleModule, missing_required_lib
-from ansible.module_utils._text import to_native
-
-
-def semanage_login_add(module, login, seuser, do_reload, serange='s0', sestore=''):
- """ Add linux user to SELinux user mapping
-
- :type module: AnsibleModule
- :param module: Ansible module
-
- :type login: str
- :param login: a Linux User or a Linux group if it begins with %
-
- :type seuser: str
- :param proto: An SELinux user ('__default__', 'unconfined_u', 'staff_u', ...), see 'semanage login -l'
-
- :type serange: str
- :param serange: SELinux MLS/MCS range (defaults to 's0')
-
- :type do_reload: bool
- :param do_reload: Whether to reload SELinux policy after commit
-
- :type sestore: str
- :param sestore: SELinux store
-
- :rtype: bool
- :return: True if the policy was changed, otherwise False
- """
- try:
- selogin = seobject.loginRecords(sestore)
- selogin.set_reload(do_reload)
- change = False
- all_logins = selogin.get_all()
- # module.fail_json(msg="%s: %s %s" % (all_logins, login, sestore))
- # for local_login in all_logins:
- if login not in all_logins.keys():
- change = True
- if not module.check_mode:
- selogin.add(login, seuser, serange)
- else:
- if all_logins[login][0] != seuser or all_logins[login][1] != serange:
- change = True
- if not module.check_mode:
- selogin.modify(login, seuser, serange)
-
- except (ValueError, KeyError, OSError, RuntimeError) as e:
- module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, to_native(e)), exception=traceback.format_exc())
-
- return change
-
-
-def semanage_login_del(module, login, seuser, do_reload, sestore=''):
- """ Delete linux user to SELinux user mapping
-
- :type module: AnsibleModule
- :param module: Ansible module
-
- :type login: str
- :param login: a Linux User or a Linux group if it begins with %
-
- :type seuser: str
- :param proto: An SELinux user ('__default__', 'unconfined_u', 'staff_u', ...), see 'semanage login -l'
-
- :type do_reload: bool
- :param do_reload: Whether to reload SELinux policy after commit
-
- :type sestore: str
- :param sestore: SELinux store
-
- :rtype: bool
- :return: True if the policy was changed, otherwise False
- """
- try:
- selogin = seobject.loginRecords(sestore)
- selogin.set_reload(do_reload)
- change = False
- all_logins = selogin.get_all()
- # module.fail_json(msg="%s: %s %s" % (all_logins, login, sestore))
- if login in all_logins.keys():
- change = True
- if not module.check_mode:
- selogin.delete(login)
-
- except (ValueError, KeyError, OSError, RuntimeError) as e:
- module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, to_native(e)), exception=traceback.format_exc())
-
- return change
-
-
-def get_runtime_status(ignore_selinux_state=False):
- return True if ignore_selinux_state is True else selinux.is_selinux_enabled()
-
-
-def main():
- module = AnsibleModule(
- argument_spec=dict(
- ignore_selinux_state=dict(type='bool', default=False),
- login=dict(type='str', required=True),
- seuser=dict(type='str'),
- selevel=dict(type='str', aliases=['serange'], default='s0'),
- state=dict(type='str', default='present', choices=['absent', 'present']),
- reload=dict(type='bool', default=True),
- ),
- required_if=[
- ["state", "present", ["seuser"]]
- ],
- supports_check_mode=True
- )
- if not HAVE_SELINUX:
- module.fail_json(msg=missing_required_lib("libselinux"), exception=SELINUX_IMP_ERR)
-
- if not HAVE_SEOBJECT:
- module.fail_json(msg=missing_required_lib("seobject from policycoreutils"), exception=SEOBJECT_IMP_ERR)
-
- ignore_selinux_state = module.params['ignore_selinux_state']
-
- if not get_runtime_status(ignore_selinux_state):
- module.fail_json(msg="SELinux is disabled on this host.")
-
- login = module.params['login']
- seuser = module.params['seuser']
- serange = module.params['selevel']
- state = module.params['state']
- do_reload = module.params['reload']
-
- result = {
- 'login': login,
- 'seuser': seuser,
- 'serange': serange,
- 'state': state,
- }
-
- if state == 'present':
- result['changed'] = semanage_login_add(module, login, seuser, do_reload, serange)
- elif state == 'absent':
- result['changed'] = semanage_login_del(module, login, seuser, do_reload)
- else:
- module.fail_json(msg='Invalid value of argument "state": {0}'.format(state))
-
- module.exit_json(**result)
-
-
-if __name__ == '__main__':
- main()
diff --git a/test/support/integration/plugins/modules/x509_crl.py b/test/support/integration/plugins/modules/x509_crl.py
deleted file mode 100644
index 9bb83a5b9f..0000000000
--- a/test/support/integration/plugins/modules/x509_crl.py
+++ /dev/null
@@ -1,783 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Copyright: (c) 2019, Felix Fontein <felix@fontein.de>
-# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-ANSIBLE_METADATA = {'metadata_version': '1.1',
- 'status': ['preview'],
- 'supported_by': 'community'}
-
-DOCUMENTATION = r'''
----
-module: x509_crl
-version_added: "2.10"
-short_description: Generate Certificate Revocation Lists (CRLs)
-description:
- - This module allows one to (re)generate or update Certificate Revocation Lists (CRLs).
- - Certificates on the revocation list can be either specified via serial number and (optionally) their issuer,
- or as a path to a certificate file in PEM format.
-requirements:
- - cryptography >= 1.2
-author:
- - Felix Fontein (@felixfontein)
-options:
- state:
- description:
- - Whether the CRL file should exist or not, taking action if the state is different from what is stated.
- type: str
- default: present
- choices: [ absent, present ]
-
- mode:
- description:
- - Defines how to process entries of existing CRLs.
- - If set to C(generate), makes sure that the CRL has the exact set of revoked certificates
- as specified in I(revoked_certificates).
- - If set to C(update), makes sure that the CRL contains the revoked certificates from
- I(revoked_certificates), but can also contain other revoked certificates. If the CRL file
- already exists, all entries from the existing CRL will also be included in the new CRL.
- When using C(update), you might be interested in setting I(ignore_timestamps) to C(yes).
- type: str
- default: generate
- choices: [ generate, update ]
-
- force:
- description:
- - Should the CRL be forced to be regenerated.
- type: bool
- default: no
-
- backup:
- description:
- - Create a backup file including a timestamp so you can get the original
- CRL back if you overwrote it with a new one by accident.
- type: bool
- default: no
-
- path:
- description:
- - Remote absolute path where the generated CRL file should be created or is already located.
- type: path
- required: yes
-
- privatekey_path:
- description:
- - Path to the CA's private key to use when signing the CRL.
- - Either I(privatekey_path) or I(privatekey_content) must be specified if I(state) is C(present), but not both.
- type: path
-
- privatekey_content:
- description:
- - The content of the CA's private key to use when signing the CRL.
- - Either I(privatekey_path) or I(privatekey_content) must be specified if I(state) is C(present), but not both.
- type: str
-
- privatekey_passphrase:
- description:
- - The passphrase for the I(privatekey_path).
- - This is required if the private key is password protected.
- type: str
-
- issuer:
- description:
- - Key/value pairs that will be present in the issuer name field of the CRL.
- - If you need to specify more than one value with the same key, use a list as value.
- - Required if I(state) is C(present).
- type: dict
-
- last_update:
- description:
- - The point in time from which this CRL can be trusted.
- - Time can be specified either as relative time or as absolute timestamp.
- - Time will always be interpreted as UTC.
- - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
- + C([w | d | h | m | s]) (e.g. C(+32w1d2h).
- - Note that if using relative time this module is NOT idempotent, except when
- I(ignore_timestamps) is set to C(yes).
- type: str
- default: "+0s"
-
- next_update:
- description:
- - "The absolute latest point in time by which this I(issuer) is expected to have issued
- another CRL. Many clients will treat a CRL as expired once I(next_update) occurs."
- - Time can be specified either as relative time or as absolute timestamp.
- - Time will always be interpreted as UTC.
- - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
- + C([w | d | h | m | s]) (e.g. C(+32w1d2h).
- - Note that if using relative time this module is NOT idempotent, except when
- I(ignore_timestamps) is set to C(yes).
- - Required if I(state) is C(present).
- type: str
-
- digest:
- description:
- - Digest algorithm to be used when signing the CRL.
- type: str
- default: sha256
-
- revoked_certificates:
- description:
- - List of certificates to be revoked.
- - Required if I(state) is C(present).
- type: list
- elements: dict
- suboptions:
- path:
- description:
- - Path to a certificate in PEM format.
- - The serial number and issuer will be extracted from the certificate.
- - Mutually exclusive with I(content) and I(serial_number). One of these three options
- must be specified.
- type: path
- content:
- description:
- - Content of a certificate in PEM format.
- - The serial number and issuer will be extracted from the certificate.
- - Mutually exclusive with I(path) and I(serial_number). One of these three options
- must be specified.
- type: str
- serial_number:
- description:
- - Serial number of the certificate.
- - Mutually exclusive with I(path) and I(content). One of these three options must
- be specified.
- type: int
- revocation_date:
- description:
- - The point in time the certificate was revoked.
- - Time can be specified either as relative time or as absolute timestamp.
- - Time will always be interpreted as UTC.
- - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
- + C([w | d | h | m | s]) (e.g. C(+32w1d2h).
- - Note that if using relative time this module is NOT idempotent, except when
- I(ignore_timestamps) is set to C(yes).
- type: str
- default: "+0s"
- issuer:
- description:
- - The certificate's issuer.
- - "Example: C(DNS:ca.example.org)"
- type: list
- elements: str
- issuer_critical:
- description:
- - Whether the certificate issuer extension should be critical.
- type: bool
- default: no
- reason:
- description:
- - The value for the revocation reason extension.
- type: str
- choices:
- - unspecified
- - key_compromise
- - ca_compromise
- - affiliation_changed
- - superseded
- - cessation_of_operation
- - certificate_hold
- - privilege_withdrawn
- - aa_compromise
- - remove_from_crl
- reason_critical:
- description:
- - Whether the revocation reason extension should be critical.
- type: bool
- default: no
- invalidity_date:
- description:
- - The point in time it was known/suspected that the private key was compromised
- or that the certificate otherwise became invalid.
- - Time can be specified either as relative time or as absolute timestamp.
- - Time will always be interpreted as UTC.
- - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
- + C([w | d | h | m | s]) (e.g. C(+32w1d2h).
- - Note that if using relative time this module is NOT idempotent. This will NOT
- change when I(ignore_timestamps) is set to C(yes).
- type: str
- invalidity_date_critical:
- description:
- - Whether the invalidity date extension should be critical.
- type: bool
- default: no
-
- ignore_timestamps:
- description:
- - Whether the timestamps I(last_update), I(next_update) and I(revocation_date) (in
- I(revoked_certificates)) should be ignored for idempotency checks. The timestamp
- I(invalidity_date) in I(revoked_certificates) will never be ignored.
- - Use this in combination with relative timestamps for these values to get idempotency.
- type: bool
- default: no
-
- return_content:
- description:
- - If set to C(yes), will return the (current or generated) CRL's content as I(crl).
- type: bool
- default: no
-
-extends_documentation_fragment:
- - files
-
-notes:
- - All ASN.1 TIME values should be specified following the YYYYMMDDHHMMSSZ pattern.
- - Date specified should be UTC. Minutes and seconds are mandatory.
-'''
-
-EXAMPLES = r'''
-- name: Generate a CRL
- x509_crl:
- path: /etc/ssl/my-ca.crl
- privatekey_path: /etc/ssl/private/my-ca.pem
- issuer:
- CN: My CA
- last_update: "+0s"
- next_update: "+7d"
- revoked_certificates:
- - serial_number: 1234
- revocation_date: 20190331202428Z
- issuer:
- CN: My CA
- - serial_number: 2345
- revocation_date: 20191013152910Z
- reason: affiliation_changed
- invalidity_date: 20191001000000Z
- - path: /etc/ssl/crt/revoked-cert.pem
- revocation_date: 20191010010203Z
-'''
-
-RETURN = r'''
-filename:
- description: Path to the generated CRL
- returned: changed or success
- type: str
- sample: /path/to/my-ca.crl
-backup_file:
- description: Name of backup file created.
- returned: changed and if I(backup) is C(yes)
- type: str
- sample: /path/to/my-ca.crl.2019-03-09@11:22~
-privatekey:
- description: Path to the private CA key
- returned: changed or success
- type: str
- sample: /path/to/my-ca.pem
-issuer:
- description:
- - The CRL's issuer.
- - Note that for repeated values, only the last one will be returned.
- returned: success
- type: dict
- sample: '{"organizationName": "Ansible", "commonName": "ca.example.com"}'
-issuer_ordered:
- description: The CRL's issuer as an ordered list of tuples.
- returned: success
- type: list
- elements: list
- sample: '[["organizationName", "Ansible"], ["commonName": "ca.example.com"]]'
-last_update:
- description: The point in time from which this CRL can be trusted as ASN.1 TIME.
- returned: success
- type: str
- sample: 20190413202428Z
-next_update:
- description: The point in time from which a new CRL will be issued and the client has to check for it as ASN.1 TIME.
- returned: success
- type: str
- sample: 20190413202428Z
-digest:
- description: The signature algorithm used to sign the CRL.
- returned: success
- type: str
- sample: sha256WithRSAEncryption
-revoked_certificates:
- description: List of certificates to be revoked.
- returned: success
- type: list
- elements: dict
- contains:
- serial_number:
- description: Serial number of the certificate.
- type: int
- sample: 1234
- revocation_date:
- description: The point in time the certificate was revoked as ASN.1 TIME.
- type: str
- sample: 20190413202428Z
- issuer:
- description: The certificate's issuer.
- type: list
- elements: str
- sample: '["DNS:ca.example.org"]'
- issuer_critical:
- description: Whether the certificate issuer extension is critical.
- type: bool
- sample: no
- reason:
- description:
- - The value for the revocation reason extension.
- - One of C(unspecified), C(key_compromise), C(ca_compromise), C(affiliation_changed), C(superseded),
- C(cessation_of_operation), C(certificate_hold), C(privilege_withdrawn), C(aa_compromise), and
- C(remove_from_crl).
- type: str
- sample: key_compromise
- reason_critical:
- description: Whether the revocation reason extension is critical.
- type: bool
- sample: no
- invalidity_date:
- description: |
- The point in time it was known/suspected that the private key was compromised
- or that the certificate otherwise became invalid as ASN.1 TIME.
- type: str
- sample: 20190413202428Z
- invalidity_date_critical:
- description: Whether the invalidity date extension is critical.
- type: bool
- sample: no
-crl:
- description: The (current or generated) CRL's content.
- returned: if I(state) is C(present) and I(return_content) is C(yes)
- type: str
-'''
-
-
-import os
-import traceback
-from ansible.module_utils.compat.version import LooseVersion
-
-from ansible.module_utils import crypto as crypto_utils
-from ansible.module_utils._text import to_native, to_text
-from ansible.module_utils.basic import AnsibleModule, missing_required_lib
-
-MINIMAL_CRYPTOGRAPHY_VERSION = '1.2'
-
-CRYPTOGRAPHY_IMP_ERR = None
-try:
- import cryptography
- from cryptography import x509
- from cryptography.hazmat.backends import default_backend
- from cryptography.hazmat.primitives.serialization import Encoding
- from cryptography.x509 import (
- CertificateRevocationListBuilder,
- RevokedCertificateBuilder,
- NameAttribute,
- Name,
- )
- CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
-except ImportError:
- CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
- CRYPTOGRAPHY_FOUND = False
-else:
- CRYPTOGRAPHY_FOUND = True
-
-
-TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
-
-
-class CRLError(crypto_utils.OpenSSLObjectError):
- pass
-
-
-class CRL(crypto_utils.OpenSSLObject):
-
- def __init__(self, module):
- super(CRL, self).__init__(
- module.params['path'],
- module.params['state'],
- module.params['force'],
- module.check_mode
- )
-
- self.update = module.params['mode'] == 'update'
- self.ignore_timestamps = module.params['ignore_timestamps']
- self.return_content = module.params['return_content']
- self.crl_content = None
-
- self.privatekey_path = module.params['privatekey_path']
- self.privatekey_content = module.params['privatekey_content']
- if self.privatekey_content is not None:
- self.privatekey_content = self.privatekey_content.encode('utf-8')
- self.privatekey_passphrase = module.params['privatekey_passphrase']
-
- self.issuer = crypto_utils.parse_name_field(module.params['issuer'])
- self.issuer = [(entry[0], entry[1]) for entry in self.issuer if entry[1]]
-
- self.last_update = crypto_utils.get_relative_time_option(module.params['last_update'], 'last_update')
- self.next_update = crypto_utils.get_relative_time_option(module.params['next_update'], 'next_update')
-
- self.digest = crypto_utils.select_message_digest(module.params['digest'])
- if self.digest is None:
- raise CRLError('The digest "{0}" is not supported'.format(module.params['digest']))
-
- self.revoked_certificates = []
- for i, rc in enumerate(module.params['revoked_certificates']):
- result = {
- 'serial_number': None,
- 'revocation_date': None,
- 'issuer': None,
- 'issuer_critical': False,
- 'reason': None,
- 'reason_critical': False,
- 'invalidity_date': None,
- 'invalidity_date_critical': False,
- }
- path_prefix = 'revoked_certificates[{0}].'.format(i)
- if rc['path'] is not None or rc['content'] is not None:
- # Load certificate from file or content
- try:
- if rc['content'] is not None:
- rc['content'] = rc['content'].encode('utf-8')
- cert = crypto_utils.load_certificate(rc['path'], content=rc['content'], backend='cryptography')
- try:
- result['serial_number'] = cert.serial_number
- except AttributeError:
- # The property was called "serial" before cryptography 1.4
- result['serial_number'] = cert.serial
- except crypto_utils.OpenSSLObjectError as e:
- if rc['content'] is not None:
- module.fail_json(
- msg='Cannot parse certificate from {0}content: {1}'.format(path_prefix, to_native(e))
- )
- else:
- module.fail_json(
- msg='Cannot read certificate "{1}" from {0}path: {2}'.format(path_prefix, rc['path'], to_native(e))
- )
- else:
- # Specify serial_number (and potentially issuer) directly
- result['serial_number'] = rc['serial_number']
- # All other options
- if rc['issuer']:
- result['issuer'] = [crypto_utils.cryptography_get_name(issuer) for issuer in rc['issuer']]
- result['issuer_critical'] = rc['issuer_critical']
- result['revocation_date'] = crypto_utils.get_relative_time_option(
- rc['revocation_date'],
- path_prefix + 'revocation_date'
- )
- if rc['reason']:
- result['reason'] = crypto_utils.REVOCATION_REASON_MAP[rc['reason']]
- result['reason_critical'] = rc['reason_critical']
- if rc['invalidity_date']:
- result['invalidity_date'] = crypto_utils.get_relative_time_option(
- rc['invalidity_date'],
- path_prefix + 'invalidity_date'
- )
- result['invalidity_date_critical'] = rc['invalidity_date_critical']
- self.revoked_certificates.append(result)
-
- self.module = module
-
- self.backup = module.params['backup']
- self.backup_file = None
-
- try:
- self.privatekey = crypto_utils.load_privatekey(
- path=self.privatekey_path,
- content=self.privatekey_content,
- passphrase=self.privatekey_passphrase,
- backend='cryptography'
- )
- except crypto_utils.OpenSSLBadPassphraseError as exc:
- raise CRLError(exc)
-
- self.crl = None
- try:
- with open(self.path, 'rb') as f:
- data = f.read()
- self.crl = x509.load_pem_x509_crl(data, default_backend())
- if self.return_content:
- self.crl_content = data
- except Exception as dummy:
- self.crl_content = None
-
- def remove(self):
- if self.backup:
- self.backup_file = self.module.backup_local(self.path)
- super(CRL, self).remove(self.module)
-
- def _compress_entry(self, entry):
- if self.ignore_timestamps:
- # Throw out revocation_date
- return (
- entry['serial_number'],
- tuple(entry['issuer']) if entry['issuer'] is not None else None,
- entry['issuer_critical'],
- entry['reason'],
- entry['reason_critical'],
- entry['invalidity_date'],
- entry['invalidity_date_critical'],
- )
- else:
- return (
- entry['serial_number'],
- entry['revocation_date'],
- tuple(entry['issuer']) if entry['issuer'] is not None else None,
- entry['issuer_critical'],
- entry['reason'],
- entry['reason_critical'],
- entry['invalidity_date'],
- entry['invalidity_date_critical'],
- )
-
- def check(self, perms_required=True):
- """Ensure the resource is in its desired state."""
-
- state_and_perms = super(CRL, self).check(self.module, perms_required)
-
- if not state_and_perms:
- return False
-
- if self.crl is None:
- return False
-
- if self.last_update != self.crl.last_update and not self.ignore_timestamps:
- return False
- if self.next_update != self.crl.next_update and not self.ignore_timestamps:
- return False
- if self.digest.name != self.crl.signature_hash_algorithm.name:
- return False
-
- want_issuer = [(crypto_utils.cryptography_name_to_oid(entry[0]), entry[1]) for entry in self.issuer]
- if want_issuer != [(sub.oid, sub.value) for sub in self.crl.issuer]:
- return False
-
- old_entries = [self._compress_entry(crypto_utils.cryptography_decode_revoked_certificate(cert)) for cert in self.crl]
- new_entries = [self._compress_entry(cert) for cert in self.revoked_certificates]
- if self.update:
- # We don't simply use a set so that duplicate entries are treated correctly
- for entry in new_entries:
- try:
- old_entries.remove(entry)
- except ValueError:
- return False
- else:
- if old_entries != new_entries:
- return False
-
- return True
-
- def _generate_crl(self):
- backend = default_backend()
- crl = CertificateRevocationListBuilder()
-
- try:
- crl = crl.issuer_name(Name([
- NameAttribute(crypto_utils.cryptography_name_to_oid(entry[0]), to_text(entry[1]))
- for entry in self.issuer
- ]))
- except ValueError as e:
- raise CRLError(e)
-
- crl = crl.last_update(self.last_update)
- crl = crl.next_update(self.next_update)
-
- if self.update and self.crl:
- new_entries = set([self._compress_entry(entry) for entry in self.revoked_certificates])
- for entry in self.crl:
- decoded_entry = self._compress_entry(crypto_utils.cryptography_decode_revoked_certificate(entry))
- if decoded_entry not in new_entries:
- crl = crl.add_revoked_certificate(entry)
- for entry in self.revoked_certificates:
- revoked_cert = RevokedCertificateBuilder()
- revoked_cert = revoked_cert.serial_number(entry['serial_number'])
- revoked_cert = revoked_cert.revocation_date(entry['revocation_date'])
- if entry['issuer'] is not None:
- revoked_cert = revoked_cert.add_extension(
- x509.CertificateIssuer([
- crypto_utils.cryptography_get_name(name) for name in self.entry['issuer']
- ]),
- entry['issuer_critical']
- )
- if entry['reason'] is not None:
- revoked_cert = revoked_cert.add_extension(
- x509.CRLReason(entry['reason']),
- entry['reason_critical']
- )
- if entry['invalidity_date'] is not None:
- revoked_cert = revoked_cert.add_extension(
- x509.InvalidityDate(entry['invalidity_date']),
- entry['invalidity_date_critical']
- )
- crl = crl.add_revoked_certificate(revoked_cert.build(backend))
-
- self.crl = crl.sign(self.privatekey, self.digest, backend=backend)
- return self.crl.public_bytes(Encoding.PEM)
-
- def generate(self):
- if not self.check(perms_required=False) or self.force:
- result = self._generate_crl()
- if self.return_content:
- self.crl_content = result
- if self.backup:
- self.backup_file = self.module.backup_local(self.path)
- crypto_utils.write_file(self.module, result)
- self.changed = True
-
- file_args = self.module.load_file_common_arguments(self.module.params)
- if self.module.set_fs_attributes_if_different(file_args, False):
- self.changed = True
-
- def _dump_revoked(self, entry):
- return {
- 'serial_number': entry['serial_number'],
- 'revocation_date': entry['revocation_date'].strftime(TIMESTAMP_FORMAT),
- 'issuer':
- [crypto_utils.cryptography_decode_name(issuer) for issuer in entry['issuer']]
- if entry['issuer'] is not None else None,
- 'issuer_critical': entry['issuer_critical'],
- 'reason': crypto_utils.REVOCATION_REASON_MAP_INVERSE.get(entry['reason']) if entry['reason'] is not None else None,
- 'reason_critical': entry['reason_critical'],
- 'invalidity_date':
- entry['invalidity_date'].strftime(TIMESTAMP_FORMAT)
- if entry['invalidity_date'] is not None else None,
- 'invalidity_date_critical': entry['invalidity_date_critical'],
- }
-
- def dump(self, check_mode=False):
- result = {
- 'changed': self.changed,
- 'filename': self.path,
- 'privatekey': self.privatekey_path,
- 'last_update': None,
- 'next_update': None,
- 'digest': None,
- 'issuer_ordered': None,
- 'issuer': None,
- 'revoked_certificates': [],
- }
- if self.backup_file:
- result['backup_file'] = self.backup_file
-
- if check_mode:
- result['last_update'] = self.last_update.strftime(TIMESTAMP_FORMAT)
- result['next_update'] = self.next_update.strftime(TIMESTAMP_FORMAT)
- # result['digest'] = crypto_utils.cryptography_oid_to_name(self.crl.signature_algorithm_oid)
- result['digest'] = self.module.params['digest']
- result['issuer_ordered'] = self.issuer
- result['issuer'] = {}
- for k, v in self.issuer:
- result['issuer'][k] = v
- result['revoked_certificates'] = []
- for entry in self.revoked_certificates:
- result['revoked_certificates'].append(self._dump_revoked(entry))
- elif self.crl:
- result['last_update'] = self.crl.last_update.strftime(TIMESTAMP_FORMAT)
- result['next_update'] = self.crl.next_update.strftime(TIMESTAMP_FORMAT)
- try:
- result['digest'] = crypto_utils.cryptography_oid_to_name(self.crl.signature_algorithm_oid)
- except AttributeError:
- # Older cryptography versions don't have signature_algorithm_oid yet
- dotted = crypto_utils._obj2txt(
- self.crl._backend._lib,
- self.crl._backend._ffi,
- self.crl._x509_crl.sig_alg.algorithm
- )
- oid = x509.oid.ObjectIdentifier(dotted)
- result['digest'] = crypto_utils.cryptography_oid_to_name(oid)
- issuer = []
- for attribute in self.crl.issuer:
- issuer.append([crypto_utils.cryptography_oid_to_name(attribute.oid), attribute.value])
- result['issuer_ordered'] = issuer
- result['issuer'] = {}
- for k, v in issuer:
- result['issuer'][k] = v
- result['revoked_certificates'] = []
- for cert in self.crl:
- entry = crypto_utils.cryptography_decode_revoked_certificate(cert)
- result['revoked_certificates'].append(self._dump_revoked(entry))
-
- if self.return_content:
- result['crl'] = self.crl_content
-
- return result
-
-
-def main():
- module = AnsibleModule(
- argument_spec=dict(
- state=dict(type='str', default='present', choices=['present', 'absent']),
- mode=dict(type='str', default='generate', choices=['generate', 'update']),
- force=dict(type='bool', default=False),
- backup=dict(type='bool', default=False),
- path=dict(type='path', required=True),
- privatekey_path=dict(type='path'),
- privatekey_content=dict(type='str'),
- privatekey_passphrase=dict(type='str', no_log=True),
- issuer=dict(type='dict'),
- last_update=dict(type='str', default='+0s'),
- next_update=dict(type='str'),
- digest=dict(type='str', default='sha256'),
- ignore_timestamps=dict(type='bool', default=False),
- return_content=dict(type='bool', default=False),
- revoked_certificates=dict(
- type='list',
- elements='dict',
- options=dict(
- path=dict(type='path'),
- content=dict(type='str'),
- serial_number=dict(type='int'),
- revocation_date=dict(type='str', default='+0s'),
- issuer=dict(type='list', elements='str'),
- issuer_critical=dict(type='bool', default=False),
- reason=dict(
- type='str',
- choices=[
- 'unspecified', 'key_compromise', 'ca_compromise', 'affiliation_changed',
- 'superseded', 'cessation_of_operation', 'certificate_hold',
- 'privilege_withdrawn', 'aa_compromise', 'remove_from_crl'
- ]
- ),
- reason_critical=dict(type='bool', default=False),
- invalidity_date=dict(type='str'),
- invalidity_date_critical=dict(type='bool', default=False),
- ),
- required_one_of=[['path', 'content', 'serial_number']],
- mutually_exclusive=[['path', 'content', 'serial_number']],
- ),
- ),
- required_if=[
- ('state', 'present', ['privatekey_path', 'privatekey_content'], True),
- ('state', 'present', ['issuer', 'next_update', 'revoked_certificates'], False),
- ],
- mutually_exclusive=(
- ['privatekey_path', 'privatekey_content'],
- ),
- supports_check_mode=True,
- add_file_common_args=True,
- )
-
- if not CRYPTOGRAPHY_FOUND:
- module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
- exception=CRYPTOGRAPHY_IMP_ERR)
-
- try:
- crl = CRL(module)
-
- if module.params['state'] == 'present':
- if module.check_mode:
- result = crl.dump(check_mode=True)
- result['changed'] = module.params['force'] or not crl.check()
- module.exit_json(**result)
-
- crl.generate()
- else:
- if module.check_mode:
- result = crl.dump(check_mode=True)
- result['changed'] = os.path.exists(module.params['path'])
- module.exit_json(**result)
-
- crl.remove()
-
- result = crl.dump()
- module.exit_json(**result)
- except crypto_utils.OpenSSLObjectError as exc:
- module.fail_json(msg=to_native(exc))
-
-
-if __name__ == "__main__":
- main()
diff --git a/test/support/integration/plugins/modules/x509_crl_info.py b/test/support/integration/plugins/modules/x509_crl_info.py
deleted file mode 100644
index b6d3632074..0000000000
--- a/test/support/integration/plugins/modules/x509_crl_info.py
+++ /dev/null
@@ -1,281 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Copyright: (c) 2020, Felix Fontein <felix@fontein.de>
-# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-ANSIBLE_METADATA = {'metadata_version': '1.1',
- 'status': ['preview'],
- 'supported_by': 'community'}
-
-DOCUMENTATION = r'''
----
-module: x509_crl_info
-version_added: "2.10"
-short_description: Retrieve information on Certificate Revocation Lists (CRLs)
-description:
- - This module allows one to retrieve information on Certificate Revocation Lists (CRLs).
-requirements:
- - cryptography >= 1.2
-author:
- - Felix Fontein (@felixfontein)
-options:
- path:
- description:
- - Remote absolute path where the generated CRL file should be created or is already located.
- - Either I(path) or I(content) must be specified, but not both.
- type: path
- content:
- description:
- - Content of the X.509 certificate in PEM format.
- - Either I(path) or I(content) must be specified, but not both.
- type: str
-
-notes:
- - All timestamp values are provided in ASN.1 TIME format, i.e. following the C(YYYYMMDDHHMMSSZ) pattern.
- They are all in UTC.
-seealso:
- - module: x509_crl
-'''
-
-EXAMPLES = r'''
-- name: Get information on CRL
- x509_crl_info:
- path: /etc/ssl/my-ca.crl
- register: result
-
-- debug:
- msg: "{{ result }}"
-'''
-
-RETURN = r'''
-issuer:
- description:
- - The CRL's issuer.
- - Note that for repeated values, only the last one will be returned.
- returned: success
- type: dict
- sample: '{"organizationName": "Ansible", "commonName": "ca.example.com"}'
-issuer_ordered:
- description: The CRL's issuer as an ordered list of tuples.
- returned: success
- type: list
- elements: list
- sample: '[["organizationName", "Ansible"], ["commonName": "ca.example.com"]]'
-last_update:
- description: The point in time from which this CRL can be trusted as ASN.1 TIME.
- returned: success
- type: str
- sample: 20190413202428Z
-next_update:
- description: The point in time from which a new CRL will be issued and the client has to check for it as ASN.1 TIME.
- returned: success
- type: str
- sample: 20190413202428Z
-digest:
- description: The signature algorithm used to sign the CRL.
- returned: success
- type: str
- sample: sha256WithRSAEncryption
-revoked_certificates:
- description: List of certificates to be revoked.
- returned: success
- type: list
- elements: dict
- contains:
- serial_number:
- description: Serial number of the certificate.
- type: int
- sample: 1234
- revocation_date:
- description: The point in time the certificate was revoked as ASN.1 TIME.
- type: str
- sample: 20190413202428Z
- issuer:
- description: The certificate's issuer.
- type: list
- elements: str
- sample: '["DNS:ca.example.org"]'
- issuer_critical:
- description: Whether the certificate issuer extension is critical.
- type: bool
- sample: no
- reason:
- description:
- - The value for the revocation reason extension.
- - One of C(unspecified), C(key_compromise), C(ca_compromise), C(affiliation_changed), C(superseded),
- C(cessation_of_operation), C(certificate_hold), C(privilege_withdrawn), C(aa_compromise), and
- C(remove_from_crl).
- type: str
- sample: key_compromise
- reason_critical:
- description: Whether the revocation reason extension is critical.
- type: bool
- sample: no
- invalidity_date:
- description: |
- The point in time it was known/suspected that the private key was compromised
- or that the certificate otherwise became invalid as ASN.1 TIME.
- type: str
- sample: 20190413202428Z
- invalidity_date_critical:
- description: Whether the invalidity date extension is critical.
- type: bool
- sample: no
-'''
-
-
-import traceback
-from ansible.module_utils.compat.version import LooseVersion
-
-from ansible.module_utils import crypto as crypto_utils
-from ansible.module_utils._text import to_native
-from ansible.module_utils.basic import AnsibleModule, missing_required_lib
-
-MINIMAL_CRYPTOGRAPHY_VERSION = '1.2'
-
-CRYPTOGRAPHY_IMP_ERR = None
-try:
- import cryptography
- from cryptography import x509
- from cryptography.hazmat.backends import default_backend
- CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
-except ImportError:
- CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
- CRYPTOGRAPHY_FOUND = False
-else:
- CRYPTOGRAPHY_FOUND = True
-
-
-TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
-
-
-class CRLError(crypto_utils.OpenSSLObjectError):
- pass
-
-
-class CRLInfo(crypto_utils.OpenSSLObject):
- """The main module implementation."""
-
- def __init__(self, module):
- super(CRLInfo, self).__init__(
- module.params['path'] or '',
- 'present',
- False,
- module.check_mode
- )
-
- self.content = module.params['content']
-
- self.module = module
-
- self.crl = None
- if self.content is None:
- try:
- with open(self.path, 'rb') as f:
- data = f.read()
- except Exception as e:
- self.module.fail_json(msg='Error while reading CRL file from disk: {0}'.format(e))
- else:
- data = self.content.encode('utf-8')
-
- try:
- self.crl = x509.load_pem_x509_crl(data, default_backend())
- except Exception as e:
- self.module.fail_json(msg='Error while decoding CRL: {0}'.format(e))
-
- def _dump_revoked(self, entry):
- return {
- 'serial_number': entry['serial_number'],
- 'revocation_date': entry['revocation_date'].strftime(TIMESTAMP_FORMAT),
- 'issuer':
- [crypto_utils.cryptography_decode_name(issuer) for issuer in entry['issuer']]
- if entry['issuer'] is not None else None,
- 'issuer_critical': entry['issuer_critical'],
- 'reason': crypto_utils.REVOCATION_REASON_MAP_INVERSE.get(entry['reason']) if entry['reason'] is not None else None,
- 'reason_critical': entry['reason_critical'],
- 'invalidity_date':
- entry['invalidity_date'].strftime(TIMESTAMP_FORMAT)
- if entry['invalidity_date'] is not None else None,
- 'invalidity_date_critical': entry['invalidity_date_critical'],
- }
-
- def get_info(self):
- result = {
- 'changed': False,
- 'last_update': None,
- 'next_update': None,
- 'digest': None,
- 'issuer_ordered': None,
- 'issuer': None,
- 'revoked_certificates': [],
- }
-
- result['last_update'] = self.crl.last_update.strftime(TIMESTAMP_FORMAT)
- result['next_update'] = self.crl.next_update.strftime(TIMESTAMP_FORMAT)
- try:
- result['digest'] = crypto_utils.cryptography_oid_to_name(self.crl.signature_algorithm_oid)
- except AttributeError:
- # Older cryptography versions don't have signature_algorithm_oid yet
- dotted = crypto_utils._obj2txt(
- self.crl._backend._lib,
- self.crl._backend._ffi,
- self.crl._x509_crl.sig_alg.algorithm
- )
- oid = x509.oid.ObjectIdentifier(dotted)
- result['digest'] = crypto_utils.cryptography_oid_to_name(oid)
- issuer = []
- for attribute in self.crl.issuer:
- issuer.append([crypto_utils.cryptography_oid_to_name(attribute.oid), attribute.value])
- result['issuer_ordered'] = issuer
- result['issuer'] = {}
- for k, v in issuer:
- result['issuer'][k] = v
- result['revoked_certificates'] = []
- for cert in self.crl:
- entry = crypto_utils.cryptography_decode_revoked_certificate(cert)
- result['revoked_certificates'].append(self._dump_revoked(entry))
-
- return result
-
- def generate(self):
- # Empty method because crypto_utils.OpenSSLObject wants this
- pass
-
- def dump(self):
- # Empty method because crypto_utils.OpenSSLObject wants this
- pass
-
-
-def main():
- module = AnsibleModule(
- argument_spec=dict(
- path=dict(type='path'),
- content=dict(type='str'),
- ),
- required_one_of=(
- ['path', 'content'],
- ),
- mutually_exclusive=(
- ['path', 'content'],
- ),
- supports_check_mode=True,
- )
-
- if not CRYPTOGRAPHY_FOUND:
- module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
- exception=CRYPTOGRAPHY_IMP_ERR)
-
- try:
- crl = CRLInfo(module)
- result = crl.get_info()
- module.exit_json(**result)
- except crypto_utils.OpenSSLObjectError as e:
- module.fail_json(msg=to_native(e))
-
-
-if __name__ == "__main__":
- main()