[aws] Support custom KMS keys in aws_s3
module (#35761)
* Allow the use of 'aws:kms' as an encryption method * Allow the use of a non standard KMS key * Deduce whether AWS Signature Version 4 is required rather than specifying with a parameter
This commit is contained in:
parent
146cc2dd9c
commit
f61164406e
2 changed files with 483 additions and 304 deletions
|
@ -52,6 +52,14 @@ options:
|
||||||
- When set for PUT mode, asks for server-side encryption.
|
- When set for PUT mode, asks for server-side encryption.
|
||||||
default: True
|
default: True
|
||||||
version_added: "2.0"
|
version_added: "2.0"
|
||||||
|
encryption_mode:
|
||||||
|
description:
|
||||||
|
- What encryption mode to use if C(encrypt) is set
|
||||||
|
default: AES256
|
||||||
|
choices:
|
||||||
|
- AES256
|
||||||
|
- aws:kms
|
||||||
|
version_added: "2.7"
|
||||||
expiration:
|
expiration:
|
||||||
description:
|
description:
|
||||||
- Time limit (in seconds) for the URL generated and returned by S3/Walrus when performing a mode=put or mode=geturl operation.
|
- Time limit (in seconds) for the URL generated and returned by S3/Walrus when performing a mode=put or mode=geturl operation.
|
||||||
|
@ -140,6 +148,10 @@ options:
|
||||||
GetObject permission but no other permissions. In this case using the option mode: get will fail without specifying
|
GetObject permission but no other permissions. In this case using the option mode: get will fail without specifying
|
||||||
ignore_nonexistent_bucket: True."
|
ignore_nonexistent_bucket: True."
|
||||||
version_added: "2.3"
|
version_added: "2.3"
|
||||||
|
encryption_kms_key_id:
|
||||||
|
description:
|
||||||
|
- KMS key id to use when encrypting objects using C(aws:kms) encryption. Ignored if encryption is not C(aws:kms)
|
||||||
|
version_added: "2.7"
|
||||||
|
|
||||||
requirements: [ "boto3", "botocore" ]
|
requirements: [ "boto3", "botocore" ]
|
||||||
author:
|
author:
|
||||||
|
@ -290,6 +302,10 @@ except ImportError:
|
||||||
pass # will be detected by imported HAS_BOTO3
|
pass # will be detected by imported HAS_BOTO3
|
||||||
|
|
||||||
|
|
||||||
|
class Sigv4Required(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def key_check(module, s3, bucket, obj, version=None, validate=True):
|
def key_check(module, s3, bucket, obj, version=None, validate=True):
|
||||||
exists = True
|
exists = True
|
||||||
try:
|
try:
|
||||||
|
@ -443,7 +459,9 @@ def create_dirkey(module, s3, bucket, obj, encrypt):
|
||||||
try:
|
try:
|
||||||
params = {'Bucket': bucket, 'Key': obj, 'Body': b''}
|
params = {'Bucket': bucket, 'Key': obj, 'Body': b''}
|
||||||
if encrypt:
|
if encrypt:
|
||||||
params['ServerSideEncryption'] = 'AES256'
|
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)
|
s3.put_object(**params)
|
||||||
for acl in module.params.get('permission'):
|
for acl in module.params.get('permission'):
|
||||||
|
@ -481,7 +499,9 @@ def upload_s3file(module, s3, bucket, obj, src, expiry, metadata, encrypt, heade
|
||||||
try:
|
try:
|
||||||
extra = {}
|
extra = {}
|
||||||
if encrypt:
|
if encrypt:
|
||||||
extra['ServerSideEncryption'] = 'AES256'
|
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:
|
if metadata:
|
||||||
extra['Metadata'] = {}
|
extra['Metadata'] = {}
|
||||||
|
|
||||||
|
@ -522,7 +542,9 @@ def download_s3file(module, s3, bucket, obj, dest, retries, version=None):
|
||||||
else:
|
else:
|
||||||
key = s3.get_object(Bucket=bucket, Key=obj)
|
key = s3.get_object(Bucket=bucket, Key=obj)
|
||||||
except botocore.exceptions.ClientError as e:
|
except botocore.exceptions.ClientError as e:
|
||||||
if e.response['Error']['Code'] != "404":
|
if e.response['Error']['Code'] == 'InvalidArgument' and 'require AWS Signature Version 4' in to_text(e):
|
||||||
|
raise Sigv4Required()
|
||||||
|
elif e.response['Error']['Code'] != "404":
|
||||||
module.fail_json(msg="Could not find the key %s." % obj, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
|
module.fail_json(msg="Could not find the key %s." % obj, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
|
||||||
|
|
||||||
for x in range(0, retries + 1):
|
for x in range(0, retries + 1):
|
||||||
|
@ -551,8 +573,11 @@ def download_s3str(module, s3, bucket, obj, version=None, validate=True):
|
||||||
contents = to_native(s3.get_object(Bucket=bucket, Key=obj)["Body"].read())
|
contents = to_native(s3.get_object(Bucket=bucket, Key=obj)["Body"].read())
|
||||||
module.exit_json(msg="GET operation complete", contents=contents, changed=True)
|
module.exit_json(msg="GET operation complete", contents=contents, changed=True)
|
||||||
except botocore.exceptions.ClientError as e:
|
except botocore.exceptions.ClientError as e:
|
||||||
module.fail_json(msg="Failed while getting contents of object %s as a string." % obj,
|
if e.response['Error']['Code'] == 'InvalidArgument' and 'require AWS Signature Version 4' in to_text(e):
|
||||||
exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
|
raise Sigv4Required()
|
||||||
|
else:
|
||||||
|
module.fail_json(msg="Failed while getting contents of object %s as a string." % obj,
|
||||||
|
exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
|
||||||
|
|
||||||
|
|
||||||
def get_download_url(module, s3, bucket, obj, expiry, changed=True):
|
def get_download_url(module, s3, bucket, obj, expiry, changed=True):
|
||||||
|
@ -584,7 +609,7 @@ def is_walrus(s3_url):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_s3_connection(module, aws_connect_kwargs, location, rgw, s3_url):
|
def get_s3_connection(module, aws_connect_kwargs, location, rgw, s3_url, sig_4=False):
|
||||||
if s3_url and rgw: # TODO - test this
|
if s3_url and rgw: # TODO - test this
|
||||||
rgw = urlparse(s3_url)
|
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)
|
params = dict(module=module, conn_type='client', resource='s3', use_ssl=rgw.scheme == 'https', region=location, endpoint=s3_url, **aws_connect_kwargs)
|
||||||
|
@ -607,6 +632,10 @@ def get_s3_connection(module, aws_connect_kwargs, location, rgw, s3_url):
|
||||||
params = dict(module=module, conn_type='client', resource='s3', region=location, endpoint=walrus, **aws_connect_kwargs)
|
params = dict(module=module, conn_type='client', resource='s3', region=location, endpoint=walrus, **aws_connect_kwargs)
|
||||||
else:
|
else:
|
||||||
params = dict(module=module, conn_type='client', resource='s3', region=location, endpoint=s3_url, **aws_connect_kwargs)
|
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')
|
||||||
return boto3_conn(**params)
|
return boto3_conn(**params)
|
||||||
|
|
||||||
|
|
||||||
|
@ -617,6 +646,7 @@ def main():
|
||||||
bucket=dict(required=True),
|
bucket=dict(required=True),
|
||||||
dest=dict(default=None, type='path'),
|
dest=dict(default=None, type='path'),
|
||||||
encrypt=dict(default=True, type='bool'),
|
encrypt=dict(default=True, type='bool'),
|
||||||
|
encryption_mode=dict(choices=['AES256', 'aws:kms'], default='AES256'),
|
||||||
expiry=dict(default=600, type='int', aliases=['expiration']),
|
expiry=dict(default=600, type='int', aliases=['expiration']),
|
||||||
headers=dict(type='dict'),
|
headers=dict(type='dict'),
|
||||||
marker=dict(default=""),
|
marker=dict(default=""),
|
||||||
|
@ -632,7 +662,8 @@ def main():
|
||||||
s3_url=dict(aliases=['S3_URL']),
|
s3_url=dict(aliases=['S3_URL']),
|
||||||
rgw=dict(default='no', type='bool'),
|
rgw=dict(default='no', type='bool'),
|
||||||
src=dict(),
|
src=dict(),
|
||||||
ignore_nonexistent_bucket=dict(default=False, type='bool')
|
ignore_nonexistent_bucket=dict(default=False, type='bool'),
|
||||||
|
encryption_kms_key_id=dict()
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
module = AnsibleModule(
|
module = AnsibleModule(
|
||||||
|
@ -746,18 +777,30 @@ def main():
|
||||||
if keysum_compare(module, dest, s3, bucket, obj, version=version):
|
if keysum_compare(module, dest, s3, bucket, obj, version=version):
|
||||||
sum_matches = True
|
sum_matches = True
|
||||||
if overwrite == 'always':
|
if overwrite == 'always':
|
||||||
download_s3file(module, s3, bucket, obj, dest, retries, version=version)
|
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)
|
||||||
else:
|
else:
|
||||||
module.exit_json(msg="Local and remote object are identical, ignoring. Use overwrite=always parameter to force.", changed=False)
|
module.exit_json(msg="Local and remote object are identical, ignoring. Use overwrite=always parameter to force.", changed=False)
|
||||||
else:
|
else:
|
||||||
sum_matches = False
|
sum_matches = False
|
||||||
|
|
||||||
if overwrite in ('always', 'different'):
|
if overwrite in ('always', 'different'):
|
||||||
download_s3file(module, s3, bucket, obj, dest, retries, version=version)
|
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)
|
||||||
else:
|
else:
|
||||||
module.exit_json(msg="WARNING: Checksums do not match. Use overwrite parameter to force download.")
|
module.exit_json(msg="WARNING: Checksums do not match. Use overwrite parameter to force download.")
|
||||||
else:
|
else:
|
||||||
download_s3file(module, s3, bucket, obj, dest, retries, version=version)
|
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 our mode is a PUT operation (upload), go through the procedure as appropriate ...
|
# if our mode is a PUT operation (upload), go through the procedure as appropriate ...
|
||||||
if mode == 'put':
|
if mode == 'put':
|
||||||
|
@ -887,7 +930,11 @@ def main():
|
||||||
if bucket and obj:
|
if bucket and obj:
|
||||||
keyrtn = key_check(module, s3, bucket, obj, version=version, validate=validate)
|
keyrtn = key_check(module, s3, bucket, obj, version=version, validate=validate)
|
||||||
if keyrtn:
|
if keyrtn:
|
||||||
download_s3str(module, s3, bucket, obj, version=version)
|
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:
|
elif version is not None:
|
||||||
module.fail_json(msg="Key %s with version id %s does not exist." % (obj, version))
|
module.fail_json(msg="Key %s with version id %s does not exist." % (obj, version))
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
# tasks file for test_s3
|
# tasks file for test_s3
|
||||||
# ============================================================
|
|
||||||
- name: set up aws connection info
|
- name: set up aws connection info
|
||||||
set_fact:
|
set_fact:
|
||||||
aws_connection_info: &aws_connection_info
|
aws_connection_info: &aws_connection_info
|
||||||
|
@ -9,333 +9,465 @@
|
||||||
security_token: "{{ security_token }}"
|
security_token: "{{ security_token }}"
|
||||||
region: "{{ aws_region }}"
|
region: "{{ aws_region }}"
|
||||||
no_log: yes
|
no_log: yes
|
||||||
# ============================================================
|
|
||||||
- name: test create bucket
|
|
||||||
aws_s3:
|
|
||||||
bucket: "{{ bucket_name }}"
|
|
||||||
mode: create
|
|
||||||
<<: *aws_connection_info
|
|
||||||
register: result
|
|
||||||
- name: assert changed is True
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- result.changed == True
|
|
||||||
# ============================================================
|
|
||||||
- name: trying to create a bucket name that already exists
|
|
||||||
aws_s3:
|
|
||||||
bucket: "{{ bucket_name }}"
|
|
||||||
mode: create
|
|
||||||
<<: *aws_connection_info
|
|
||||||
register: result
|
|
||||||
- name: assert changed is False since the bucket already exists
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- result.changed == False
|
|
||||||
# ============================================================
|
|
||||||
- name: create temporary file object to put in a bucket
|
|
||||||
tempfile:
|
|
||||||
register: tmp1
|
|
||||||
- name: make random contents
|
|
||||||
set_fact:
|
|
||||||
content: "{{ lookup('password', '/dev/null chars=ascii_letters,digits,hexdigits,punctuation') }}"
|
|
||||||
|
|
||||||
- name: give temporary file data
|
- block:
|
||||||
copy:
|
- name: test create bucket
|
||||||
content: "{{ content }}"
|
|
||||||
dest: "{{ tmp1.path }}"
|
|
||||||
- name: get the stat of the file
|
|
||||||
stat:
|
|
||||||
path: "{{ tmp1.path }}"
|
|
||||||
get_checksum: yes
|
|
||||||
register: file1stat
|
|
||||||
# ============================================================
|
|
||||||
- name: test putting an object in the bucket
|
|
||||||
aws_s3:
|
|
||||||
bucket: "{{ bucket_name }}"
|
|
||||||
mode: put
|
|
||||||
src: "{{ tmp1.path }}"
|
|
||||||
object: delete.txt
|
|
||||||
<<: *aws_connection_info
|
|
||||||
retries: 3
|
|
||||||
delay: 3
|
|
||||||
register: result
|
|
||||||
- name: assert object exists
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- result.changed == True
|
|
||||||
- result.msg == "PUT operation complete"
|
|
||||||
# ============================================================
|
|
||||||
- name: check that roles file lookups work as expected
|
|
||||||
aws_s3:
|
|
||||||
bucket: "{{ bucket_name }}"
|
|
||||||
mode: put
|
|
||||||
src: hello.txt
|
|
||||||
object: hello.txt
|
|
||||||
<<: *aws_connection_info
|
|
||||||
retries: 3
|
|
||||||
delay: 3
|
|
||||||
register: result
|
|
||||||
- name: assert object exists
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- result.changed == True
|
|
||||||
- result.msg == "PUT operation complete"
|
|
||||||
- name: remove hello.txt (deletion tests are later)
|
|
||||||
aws_s3:
|
|
||||||
bucket: "{{ bucket_name }}"
|
|
||||||
mode: delobj
|
|
||||||
object: hello.txt
|
|
||||||
<<: *aws_connection_info
|
|
||||||
retries: 3
|
|
||||||
delay: 3
|
|
||||||
register: result
|
|
||||||
# ============================================================
|
|
||||||
- name: create a second temp file to download the object from the bucket
|
|
||||||
tempfile:
|
|
||||||
register: tmp2
|
|
||||||
- name: test get object
|
|
||||||
aws_s3:
|
|
||||||
bucket: "{{ bucket_name }}"
|
|
||||||
mode: get
|
|
||||||
dest: "{{ tmp2.path }}"
|
|
||||||
object: delete.txt
|
|
||||||
<<: *aws_connection_info
|
|
||||||
retries: 3
|
|
||||||
delay: 3
|
|
||||||
register: result
|
|
||||||
until: "result.msg == 'GET operation complete'"
|
|
||||||
- name: get the stat of the file so we can compare the checksums
|
|
||||||
stat:
|
|
||||||
path: "{{ tmp2.path }}"
|
|
||||||
get_checksum: yes
|
|
||||||
register: file2stat
|
|
||||||
- name: assert checksums are the same
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- file1stat.stat.checksum == file2stat.stat.checksum
|
|
||||||
# ============================================================
|
|
||||||
- name: test geturl of the object
|
|
||||||
aws_s3:
|
|
||||||
bucket: "{{ bucket_name }}"
|
|
||||||
mode: geturl
|
|
||||||
object: delete.txt
|
|
||||||
<<: *aws_connection_info
|
|
||||||
retries: 3
|
|
||||||
delay: 3
|
|
||||||
register: result
|
|
||||||
until: result.changed
|
|
||||||
- name: assert we have the object's url
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "'Download url:' in result.msg"
|
|
||||||
- result.changed == True
|
|
||||||
# ============================================================
|
|
||||||
- name: test getstr of the object
|
|
||||||
aws_s3:
|
|
||||||
bucket: "{{ bucket_name }}"
|
|
||||||
mode: getstr
|
|
||||||
object: delete.txt
|
|
||||||
<<: *aws_connection_info
|
|
||||||
retries: 3
|
|
||||||
delay: 3
|
|
||||||
register: result
|
|
||||||
- name: assert that we have the object's contents
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- result.msg == "GET operation complete"
|
|
||||||
- result.contents == content
|
|
||||||
# ============================================================
|
|
||||||
- name: test list to get all objects in the bucket
|
|
||||||
aws_s3:
|
|
||||||
bucket: "{{ bucket_name }}"
|
|
||||||
mode: list
|
|
||||||
<<: *aws_connection_info
|
|
||||||
retries: 3
|
|
||||||
delay: 3
|
|
||||||
register: result
|
|
||||||
- name: assert that the keys are correct
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "'delete.txt' in result.s3_keys"
|
|
||||||
- result.msg == "LIST operation complete"
|
|
||||||
# ============================================================
|
|
||||||
- name: test delobj to just delete an object in the bucket
|
|
||||||
aws_s3:
|
|
||||||
bucket: "{{ bucket_name }}"
|
|
||||||
mode: delobj
|
|
||||||
object: delete.txt
|
|
||||||
<<: *aws_connection_info
|
|
||||||
retries: 3
|
|
||||||
delay: 3
|
|
||||||
register: result
|
|
||||||
- name: assert that delete.txt is no longer an object in the bucket deleteme
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "'Object deleted from bucket' in result.msg"
|
|
||||||
- result.changed == True
|
|
||||||
- name: assert that delete.txt is no longer an object in the bucket deleteme
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "'Object deleted from bucket' in result.msg"
|
|
||||||
- result.changed == True
|
|
||||||
# ============================================================
|
|
||||||
- name: test creation of empty path
|
|
||||||
aws_s3:
|
|
||||||
bucket: "{{ bucket_name }}"
|
|
||||||
mode: create
|
|
||||||
object: foo/bar/baz/
|
|
||||||
<<: *aws_connection_info
|
|
||||||
retries: 3
|
|
||||||
delay: 3
|
|
||||||
register: result
|
|
||||||
- name: assert that empty path is created
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- "'Virtual directory foo/bar/baz/ created' in result.msg"
|
|
||||||
- result.changed == True
|
|
||||||
- name: test deletion of empty path
|
|
||||||
aws_s3:
|
|
||||||
bucket: "{{ bucket_name }}"
|
|
||||||
mode: delobj
|
|
||||||
object: foo/bar/baz/
|
|
||||||
<<: *aws_connection_info
|
|
||||||
retries: 3
|
|
||||||
delay: 3
|
|
||||||
# ============================================================
|
|
||||||
- name: test delete bucket
|
|
||||||
aws_s3:
|
|
||||||
bucket: "{{ bucket_name }}"
|
|
||||||
mode: delete
|
|
||||||
<<: *aws_connection_info
|
|
||||||
register: result
|
|
||||||
retries: 3
|
|
||||||
delay: 3
|
|
||||||
until: result.changed
|
|
||||||
- name: assert that changed is True
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- result.changed == True
|
|
||||||
# ============================================================
|
|
||||||
- name: delete temporary file 1
|
|
||||||
file:
|
|
||||||
state: absent
|
|
||||||
path: "{{ tmp1.path }}"
|
|
||||||
- name: delete temporary file 2
|
|
||||||
file:
|
|
||||||
state: absent
|
|
||||||
path: "{{ tmp2.path }}"
|
|
||||||
# ============================================================
|
|
||||||
- name: test create a bucket with a dot in the name
|
|
||||||
aws_s3:
|
|
||||||
bucket: "{{ bucket_name + '.bucket' }}"
|
|
||||||
mode: create
|
|
||||||
<<: *aws_connection_info
|
|
||||||
register: result
|
|
||||||
- name: assert that changed is True
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- result.changed == True
|
|
||||||
# ============================================================
|
|
||||||
- name: test delete a bucket with a dot in the name
|
|
||||||
aws_s3:
|
|
||||||
bucket: "{{ bucket_name + '.bucket' }}"
|
|
||||||
mode: delete
|
|
||||||
<<: *aws_connection_info
|
|
||||||
register: result
|
|
||||||
- name: assert that changed is True
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- result.changed == True
|
|
||||||
# ============================================================
|
|
||||||
- name: test delete a nonexistent bucket
|
|
||||||
aws_s3:
|
|
||||||
bucket: "{{ bucket_name + '.bucket' }}"
|
|
||||||
mode: delete
|
|
||||||
<<: *aws_connection_info
|
|
||||||
register: result
|
|
||||||
- name: assert that changed is False
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- result.changed == False
|
|
||||||
# ============================================================
|
|
||||||
- name: create a tempfile for the path
|
|
||||||
tempfile:
|
|
||||||
register: tmp1
|
|
||||||
|
|
||||||
- name: make tempfile 4 GB for OSX
|
|
||||||
command:
|
|
||||||
_raw_params: "dd if=/dev/zero of={{ tmp1.path }} bs=1m count=4096"
|
|
||||||
when: ansible_distribution == 'MacOSX'
|
|
||||||
|
|
||||||
- name: make tempfile 4 GB for linux
|
|
||||||
command:
|
|
||||||
_raw_params: "dd if=/dev/zero of={{ tmp1.path }} bs=1M count=4096"
|
|
||||||
when: ansible_distribution == 'Linux'
|
|
||||||
|
|
||||||
- name: test multipart download - platform specific
|
|
||||||
block:
|
|
||||||
- name: make a bucket to upload the file
|
|
||||||
aws_s3:
|
aws_s3:
|
||||||
bucket: "{{ bucket_name }}"
|
bucket: "{{ bucket_name }}"
|
||||||
mode: create
|
mode: create
|
||||||
<<: *aws_connection_info
|
<<: *aws_connection_info
|
||||||
|
register: result
|
||||||
|
- name: assert changed is True
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result.changed == True
|
||||||
|
|
||||||
- name: upload the file to the bucket
|
- name: trying to create a bucket name that already exists
|
||||||
|
aws_s3:
|
||||||
|
bucket: "{{ bucket_name }}"
|
||||||
|
mode: create
|
||||||
|
<<: *aws_connection_info
|
||||||
|
register: result
|
||||||
|
- name: assert changed is False since the bucket already exists
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result.changed == False
|
||||||
|
|
||||||
|
- name: create temporary file object to put in a bucket
|
||||||
|
tempfile:
|
||||||
|
register: tmp1
|
||||||
|
- name: make random contents
|
||||||
|
set_fact:
|
||||||
|
content: "{{ lookup('password', '/dev/null chars=ascii_letters,digits,hexdigits,punctuation') }}"
|
||||||
|
|
||||||
|
- name: give temporary file data
|
||||||
|
copy:
|
||||||
|
content: "{{ content }}"
|
||||||
|
dest: "{{ tmp1.path }}"
|
||||||
|
- name: get the stat of the file
|
||||||
|
stat:
|
||||||
|
path: "{{ tmp1.path }}"
|
||||||
|
get_checksum: yes
|
||||||
|
register: file1stat
|
||||||
|
|
||||||
|
- name: test putting an object in the bucket
|
||||||
aws_s3:
|
aws_s3:
|
||||||
bucket: "{{ bucket_name }}"
|
bucket: "{{ bucket_name }}"
|
||||||
mode: put
|
mode: put
|
||||||
src: "{{ tmp1.path }}"
|
src: "{{ tmp1.path }}"
|
||||||
object: multipart.txt
|
object: delete.txt
|
||||||
<<: *aws_connection_info
|
<<: *aws_connection_info
|
||||||
|
retries: 3
|
||||||
|
delay: 3
|
||||||
|
register: result
|
||||||
|
- name: assert object exists
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result.changed == True
|
||||||
|
- result.msg == "PUT operation complete"
|
||||||
|
|
||||||
- name: download file once
|
- name: check that roles file lookups work as expected
|
||||||
|
aws_s3:
|
||||||
|
bucket: "{{ bucket_name }}"
|
||||||
|
mode: put
|
||||||
|
src: hello.txt
|
||||||
|
object: hello.txt
|
||||||
|
<<: *aws_connection_info
|
||||||
|
retries: 3
|
||||||
|
delay: 3
|
||||||
|
register: result
|
||||||
|
- name: assert object exists
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result.changed == True
|
||||||
|
- result.msg == "PUT operation complete"
|
||||||
|
- name: remove hello.txt (deletion tests are later)
|
||||||
|
aws_s3:
|
||||||
|
bucket: "{{ bucket_name }}"
|
||||||
|
mode: delobj
|
||||||
|
object: hello.txt
|
||||||
|
<<: *aws_connection_info
|
||||||
|
retries: 3
|
||||||
|
delay: 3
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: create a second temp file to download the object from the bucket
|
||||||
|
tempfile:
|
||||||
|
register: tmp2
|
||||||
|
|
||||||
|
- name: test get object
|
||||||
aws_s3:
|
aws_s3:
|
||||||
bucket: "{{ bucket_name }}"
|
bucket: "{{ bucket_name }}"
|
||||||
mode: get
|
mode: get
|
||||||
dest: /tmp/multipart_download.txt
|
dest: "{{ tmp2.path }}"
|
||||||
object: multipart.txt
|
object: delete.txt
|
||||||
overwrite: different
|
<<: *aws_connection_info
|
||||||
|
retries: 3
|
||||||
|
delay: 3
|
||||||
|
register: result
|
||||||
|
until: "result.msg == 'GET operation complete'"
|
||||||
|
- name: get the stat of the file so we can compare the checksums
|
||||||
|
stat:
|
||||||
|
path: "{{ tmp2.path }}"
|
||||||
|
get_checksum: yes
|
||||||
|
register: file2stat
|
||||||
|
- name: assert checksums are the same
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- file1stat.stat.checksum == file2stat.stat.checksum
|
||||||
|
|
||||||
|
- name: test geturl of the object
|
||||||
|
aws_s3:
|
||||||
|
bucket: "{{ bucket_name }}"
|
||||||
|
mode: geturl
|
||||||
|
object: delete.txt
|
||||||
|
<<: *aws_connection_info
|
||||||
|
retries: 3
|
||||||
|
delay: 3
|
||||||
|
register: result
|
||||||
|
until: result.changed
|
||||||
|
- name: assert we have the object's url
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "'Download url:' in result.msg"
|
||||||
|
- result.changed == True
|
||||||
|
|
||||||
|
- name: test getstr of the object
|
||||||
|
aws_s3:
|
||||||
|
bucket: "{{ bucket_name }}"
|
||||||
|
mode: getstr
|
||||||
|
object: delete.txt
|
||||||
|
<<: *aws_connection_info
|
||||||
|
retries: 3
|
||||||
|
delay: 3
|
||||||
|
register: result
|
||||||
|
- name: assert that we have the object's contents
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result.msg == "GET operation complete"
|
||||||
|
- result.contents == content
|
||||||
|
|
||||||
|
- name: test list to get all objects in the bucket
|
||||||
|
aws_s3:
|
||||||
|
bucket: "{{ bucket_name }}"
|
||||||
|
mode: list
|
||||||
|
<<: *aws_connection_info
|
||||||
|
retries: 3
|
||||||
|
delay: 3
|
||||||
|
register: result
|
||||||
|
- name: assert that the keys are correct
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "'delete.txt' in result.s3_keys"
|
||||||
|
- result.msg == "LIST operation complete"
|
||||||
|
|
||||||
|
- name: test delobj to just delete an object in the bucket
|
||||||
|
aws_s3:
|
||||||
|
bucket: "{{ bucket_name }}"
|
||||||
|
mode: delobj
|
||||||
|
object: delete.txt
|
||||||
|
<<: *aws_connection_info
|
||||||
|
retries: 3
|
||||||
|
delay: 3
|
||||||
|
register: result
|
||||||
|
- name: assert that delete.txt is no longer an object in the bucket deleteme
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "'Object deleted from bucket' in result.msg"
|
||||||
|
- result.changed == True
|
||||||
|
- name: assert that delete.txt is no longer an object in the bucket deleteme
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "'Object deleted from bucket' in result.msg"
|
||||||
|
- result.changed == True
|
||||||
|
- name: clean up temp file
|
||||||
|
file:
|
||||||
|
path: "{{ tmp2.path }}"
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: test putting an encrypted object in the bucket
|
||||||
|
aws_s3:
|
||||||
|
bucket: "{{ bucket_name }}"
|
||||||
|
mode: put
|
||||||
|
src: "{{ tmp1.path }}"
|
||||||
|
encrypt: yes
|
||||||
|
object: delete_encrypt.txt
|
||||||
|
<<: *aws_connection_info
|
||||||
|
retries: 3
|
||||||
|
delay: 3
|
||||||
|
register: result
|
||||||
|
- name: assert object exists
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result.changed == True
|
||||||
|
- result.msg == "PUT operation complete"
|
||||||
|
|
||||||
|
- name: create a second temp file to download the object from the bucket
|
||||||
|
tempfile:
|
||||||
|
register: tmp2
|
||||||
|
- name: test get encrypted object
|
||||||
|
aws_s3:
|
||||||
|
bucket: "{{ bucket_name }}"
|
||||||
|
mode: get
|
||||||
|
dest: "{{ tmp2.path }}"
|
||||||
|
object: delete_encrypt.txt
|
||||||
|
<<: *aws_connection_info
|
||||||
|
retries: 3
|
||||||
|
delay: 3
|
||||||
|
register: result
|
||||||
|
until: "result.msg == 'GET operation complete'"
|
||||||
|
- name: get the stat of the file so we can compare the checksums
|
||||||
|
stat:
|
||||||
|
path: "{{ tmp2.path }}"
|
||||||
|
get_checksum: yes
|
||||||
|
register: file2stat
|
||||||
|
- name: assert checksums are the same
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- file1stat.stat.checksum == file2stat.stat.checksum
|
||||||
|
- name: delete encrypted file
|
||||||
|
aws_s3:
|
||||||
|
bucket: "{{ bucket_name }}"
|
||||||
|
mode: delobj
|
||||||
|
object: delete_encrypt.txt
|
||||||
|
<<: *aws_connection_info
|
||||||
|
retries: 3
|
||||||
|
delay: 3
|
||||||
|
- name: clean up temp file
|
||||||
|
file:
|
||||||
|
path: "{{ tmp2.path }}"
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: test putting an aws:kms encrypted object in the bucket
|
||||||
|
aws_s3:
|
||||||
|
bucket: "{{ bucket_name }}"
|
||||||
|
mode: put
|
||||||
|
src: "{{ tmp1.path }}"
|
||||||
|
encrypt: yes
|
||||||
|
encryption_mode: aws:kms
|
||||||
|
object: delete_encrypt_kms.txt
|
||||||
|
<<: *aws_connection_info
|
||||||
|
retries: 3
|
||||||
|
delay: 3
|
||||||
|
register: result
|
||||||
|
- name: assert object exists
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result.changed == True
|
||||||
|
- result.msg == "PUT operation complete"
|
||||||
|
|
||||||
|
- name: create a second temp file to download the object from the bucket
|
||||||
|
tempfile:
|
||||||
|
register: tmp2
|
||||||
|
- name: test get KMS encrypted object
|
||||||
|
aws_s3:
|
||||||
|
bucket: "{{ bucket_name }}"
|
||||||
|
mode: get
|
||||||
|
dest: "{{ tmp2.path }}"
|
||||||
|
object: delete_encrypt_kms.txt
|
||||||
|
<<: *aws_connection_info
|
||||||
|
retries: 3
|
||||||
|
delay: 3
|
||||||
|
register: result
|
||||||
|
until: "result.msg == 'GET operation complete'"
|
||||||
|
- name: get the stat of the file so we can compare the checksums
|
||||||
|
stat:
|
||||||
|
path: "{{ tmp2.path }}"
|
||||||
|
get_checksum: yes
|
||||||
|
register: file2stat
|
||||||
|
- name: assert checksums are the same
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- file1stat.stat.checksum == file2stat.stat.checksum
|
||||||
|
# FIXME - could use a test that checks uploaded file is *actually* aws:kms encrypted
|
||||||
|
- name: test get KMS encrypted object using v4 signature
|
||||||
|
aws_s3:
|
||||||
|
bucket: "{{ bucket_name }}"
|
||||||
|
mode: get
|
||||||
|
dest: "{{ tmp2.path }}"
|
||||||
|
object: delete_encrypt_kms.txt
|
||||||
<<: *aws_connection_info
|
<<: *aws_connection_info
|
||||||
retries: 3
|
retries: 3
|
||||||
delay: 3
|
delay: 3
|
||||||
until: "result.msg == 'GET operation complete'"
|
until: "result.msg == 'GET operation complete'"
|
||||||
register: result
|
- name: delete KMS encrypted file
|
||||||
|
|
||||||
- name: assert the file was downloaded once
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- result.changed
|
|
||||||
|
|
||||||
- name: download file again
|
|
||||||
aws_s3:
|
aws_s3:
|
||||||
bucket: "{{ bucket_name }}"
|
bucket: "{{ bucket_name }}"
|
||||||
mode: get
|
mode: delobj
|
||||||
dest: /tmp/multipart_download.txt
|
object: delete_encrypt_kms.txt
|
||||||
object: multipart.txt
|
|
||||||
overwrite: different
|
|
||||||
<<: *aws_connection_info
|
<<: *aws_connection_info
|
||||||
register: result
|
retries: 3
|
||||||
|
delay: 3
|
||||||
|
- name: clean up temp file
|
||||||
|
file:
|
||||||
|
path: "{{ tmp2.path }}"
|
||||||
|
state: absent
|
||||||
|
|
||||||
- name: assert the file was not redownloaded
|
# FIXME: could use a test that checks non standard KMS key
|
||||||
|
# but that would require ability to create and remove such keys.
|
||||||
|
# PRs exist for that, but propose deferring until after merge.
|
||||||
|
|
||||||
|
- name: test creation of empty path
|
||||||
|
aws_s3:
|
||||||
|
bucket: "{{ bucket_name }}"
|
||||||
|
mode: create
|
||||||
|
object: foo/bar/baz/
|
||||||
|
<<: *aws_connection_info
|
||||||
|
retries: 3
|
||||||
|
delay: 3
|
||||||
|
register: result
|
||||||
|
- name: assert that empty path is created
|
||||||
assert:
|
assert:
|
||||||
that:
|
that:
|
||||||
- not result.changed
|
- "'Virtual directory foo/bar/baz/ created' in result.msg"
|
||||||
|
- result.changed == True
|
||||||
|
- name: test deletion of empty path
|
||||||
|
aws_s3:
|
||||||
|
bucket: "{{ bucket_name }}"
|
||||||
|
mode: delobj
|
||||||
|
object: foo/bar/baz/
|
||||||
|
<<: *aws_connection_info
|
||||||
|
retries: 3
|
||||||
|
delay: 3
|
||||||
|
|
||||||
- name: delete file used for upload
|
- name: test delete bucket
|
||||||
|
aws_s3:
|
||||||
|
bucket: "{{ bucket_name }}"
|
||||||
|
mode: delete
|
||||||
|
<<: *aws_connection_info
|
||||||
|
register: result
|
||||||
|
retries: 3
|
||||||
|
delay: 3
|
||||||
|
until: result.changed
|
||||||
|
- name: assert that changed is True
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result.changed == True
|
||||||
|
|
||||||
|
- name: test create a bucket with a dot in the name
|
||||||
|
aws_s3:
|
||||||
|
bucket: "{{ bucket_name + '.bucket' }}"
|
||||||
|
mode: create
|
||||||
|
<<: *aws_connection_info
|
||||||
|
register: result
|
||||||
|
- name: assert that changed is True
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result.changed == True
|
||||||
|
|
||||||
|
- name: test delete a bucket with a dot in the name
|
||||||
|
aws_s3:
|
||||||
|
bucket: "{{ bucket_name + '.bucket' }}"
|
||||||
|
mode: delete
|
||||||
|
<<: *aws_connection_info
|
||||||
|
register: result
|
||||||
|
- name: assert that changed is True
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result.changed == True
|
||||||
|
|
||||||
|
- name: test delete a nonexistent bucket
|
||||||
|
aws_s3:
|
||||||
|
bucket: "{{ bucket_name + '.bucket' }}"
|
||||||
|
mode: delete
|
||||||
|
<<: *aws_connection_info
|
||||||
|
register: result
|
||||||
|
- name: assert that changed is False
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result.changed == False
|
||||||
|
|
||||||
|
- name: make tempfile 4 GB for OSX
|
||||||
|
command:
|
||||||
|
_raw_params: "dd if=/dev/zero of={{ tmp1.path }} bs=1m count=4096"
|
||||||
|
when: ansible_distribution == 'MacOSX'
|
||||||
|
|
||||||
|
- name: make tempfile 4 GB for linux
|
||||||
|
command:
|
||||||
|
_raw_params: "dd if=/dev/zero of={{ tmp1.path }} bs=1M count=4096"
|
||||||
|
when: ansible_system == 'Linux'
|
||||||
|
|
||||||
|
- name: test multipart download - platform specific
|
||||||
|
block:
|
||||||
|
- name: make a bucket to upload the file
|
||||||
|
aws_s3:
|
||||||
|
bucket: "{{ bucket_name }}"
|
||||||
|
mode: create
|
||||||
|
<<: *aws_connection_info
|
||||||
|
|
||||||
|
- name: upload the file to the bucket
|
||||||
|
aws_s3:
|
||||||
|
bucket: "{{ bucket_name }}"
|
||||||
|
mode: put
|
||||||
|
src: "{{ tmp1.path }}"
|
||||||
|
object: multipart.txt
|
||||||
|
<<: *aws_connection_info
|
||||||
|
|
||||||
|
- name: download file once
|
||||||
|
aws_s3:
|
||||||
|
bucket: "{{ bucket_name }}"
|
||||||
|
mode: get
|
||||||
|
dest: "{{ tmp2.path }}"
|
||||||
|
object: multipart.txt
|
||||||
|
overwrite: different
|
||||||
|
<<: *aws_connection_info
|
||||||
|
retries: 3
|
||||||
|
delay: 3
|
||||||
|
until: "result.msg == 'GET operation complete'"
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert the file was downloaded once
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result.changed
|
||||||
|
|
||||||
|
- name: download file again
|
||||||
|
aws_s3:
|
||||||
|
bucket: "{{ bucket_name }}"
|
||||||
|
mode: get
|
||||||
|
dest: "{{ tmp2.path }}"
|
||||||
|
object: multipart.txt
|
||||||
|
overwrite: different
|
||||||
|
<<: *aws_connection_info
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: assert the file was not redownloaded
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- not result.changed
|
||||||
|
when: ansible_system == 'Linux' or ansible_distribution == 'MacOSX'
|
||||||
|
|
||||||
|
always:
|
||||||
|
###### TEARDOWN STARTS HERE ######
|
||||||
|
|
||||||
|
- name: remove uploaded files
|
||||||
|
aws_s3:
|
||||||
|
bucket: "{{ bucket_name }}"
|
||||||
|
mode: delobj
|
||||||
|
object: "{{ item }}"
|
||||||
|
<<: *aws_connection_info
|
||||||
|
with_items:
|
||||||
|
- hello.txt
|
||||||
|
- delete.txt
|
||||||
|
- delete_encrypt.txt
|
||||||
|
- delete_encrypt_kms.txt
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
|
- name: delete temporary file 1
|
||||||
file:
|
file:
|
||||||
state: absent
|
state: absent
|
||||||
path: "{{ tmp1.path }}"
|
path: "{{ tmp1.path }}"
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
- name: delete downloaded file
|
- name: delete temporary file 2
|
||||||
file:
|
file:
|
||||||
state: absent
|
state: absent
|
||||||
path: /tmp/multipart_download.txt
|
path: "{{ tmp2.path }}"
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
- name: delete the bucket
|
- name: delete the bucket
|
||||||
aws_s3:
|
aws_s3:
|
||||||
bucket: "{{ bucket_name }}"
|
bucket: "{{ bucket_name }}"
|
||||||
mode: delete
|
mode: delete
|
||||||
<<: *aws_connection_info
|
<<: *aws_connection_info
|
||||||
|
ignore_errors: yes
|
||||||
when: ansible_distribution in ['MacOSX', 'Linux']
|
|
||||||
# ============================================================
|
|
||||||
|
|
Loading…
Reference in a new issue