s3 17.9 KB
Newer Older
1
#!/usr/bin/python
lwade committed
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
# This file is part of Ansible
#
# Ansible 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.
#
# Ansible 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 Ansible.  If not, see <http://www.gnu.org/licenses/>.

DOCUMENTATION = '''
---
module: s3
20
short_description: idempotent S3 module putting a file into S3. 
lwade committed
21
description:
22
    - This module allows the user to dictate the presence of a given file in an S3 bucket. If or once the key (file) exists in the bucket, it returns a time-expired download URL. This module has a dependency on python-boto.
lwade committed
23 24 25 26
version_added: "1.1"
options:
  bucket:
    description:
27
      - Bucket name. 
lwade committed
28 29 30
    required: true
    default: null 
    aliases: []
31
  object:
lwade committed
32
    description:
33
      - Keyname of the object inside the bucket. Can be used to create "virtual directories", see examples.
lwade committed
34 35 36
    required: false
    default: null
    aliases: []
37 38
    version_added: "1.3"
  src:
39
    description:
40
      - The source file path when performing a PUT operation.
41 42 43
    required: false
    default: null
    aliases: []
44 45
    version_added: "1.3"
  dest:
lwade committed
46
    description:
47
      - The destination file path when downloading an object/key with a GET operation.
lwade committed
48 49 50
    required: false
    default: 600
    aliases: []
51
    version_added: "1.3"
52 53
  overwrite:
    description:
54
      - Force overwrite either locally on the filesystem or remotely with the object/key. Used with PUT and GET operations.
55 56
    required: false
    default: false
57
    version_added: "1.2"
58 59
  mode:
    description:
60
      - Switches the module behaviour between put (upload), get (download), geturl (return download url (Ansible 1.3+), getstr (download object as string (1.3+)), create (bucket) and delete (bucket). 
61 62 63 64 65 66 67 68 69
    required: true
    default: null
    aliases: []
  expiry:
    description:
      - Time limit (in seconds) for the URL generated and returned by S3/Walrus when performing a mode=put or mode=geturl operation. 
    required: false
    default: null
    aliases: []
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
  s3_url:
    description:
        - S3 URL endpoint. If not specified then the S3_URL environment variable is used, if that variable is defined.
    default: null
    aliases: [ S3_URL ]
  ec2_access_key:
    description:
      - EC2 access key. If not specified then the EC2_ACCESS_KEY environment variable is used.
    required: false
    default: null
    aliases: [ EC2_ACCESS_KEY ]
  ec2_secret_key:
    description:
      - EC2 secret key. If not specified then the EC2_SECRET_KEY environment variable is used.
    required: false
    default: null
    aliases: [ EC2_SECRET_KEY ]
lwade committed
87
requirements: [ "boto" ]
88
author: Lester Wade, Ralph Tice
lwade committed
89 90
'''

91 92
EXAMPLES = '''
# Simple PUT operation
93 94 95 96 97 98 99
- s3: bucket=mybucket object=/my/desired/key.txt src=/usr/local/myfile.txt mode=put
# Simple GET operation
- s3: bucket=mybucket object=/my/desired/key.txt dest=/usr/local/myfile.txt mode=get
# GET/download and overwrite local file (trust remote)
- s3: bucket=mybucket object=/my/desired/key.txt dest=/usr/local/myfile.txt mode=get overwrite=true
# PUT/upload and overwrite remote file (trust local)
- s3: bucket=mybucket object=/my/desired/key.txt src=/usr/local/myfile.txt mode=put overwrite=true
100 101
# Download an object as a string to use else where in your playbook
- s3: bucket=mybucket object=/my/desired/key.txt src=/usr/local/myfile.txt mode=getstr
102 103
# Create an empty bucket
- s3: bucket=mybucket mode=create
104 105
# Create a bucket with key as directory
- s3: bucket=mybucket object=/my/directory/path mode=create
106 107
# Delete a bucket and all contents
- s3: bucket=mybucket mode=delete
108 109
'''

lwade committed
110 111 112
import sys
import os
import urlparse
113
import hashlib
lwade committed
114 115 116 117 118 119 120

try:
    import boto
except ImportError:
    print "failed=True msg='boto required for this module'"
    sys.exit(1)

121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
def key_check(module, s3, bucket, obj):
    try:
        bucket = s3.lookup(bucket)
        key_check = bucket.get_key(obj)
    except s3.provider.storage_response_error, e:
        module.fail_json(msg= str(e))
    if key_check:
        return True
    else:
        return False

def keysum(module, s3, bucket, obj):
    bucket = s3.lookup(bucket)
    key_check = bucket.get_key(obj)
    if key_check:
        md5_remote = key_check.etag[1:-1]
        etag_multipart = md5_remote.find('-')!=-1 #Check for multipart, etag is not md5
        if etag_multipart is True:
            module.fail_json(msg="Files uploaded with multipart of s3 are not supported with checksum, unable to compute checksum.")
            sys.exit(0)
    return md5_remote

def bucket_check(module, s3, bucket):
    try:
        result = s3.lookup(bucket)
    except s3.provider.storage_response_error, e:
        module.fail_json(msg= str(e))
    if result:
        return True
    else:
        return False

def create_bucket(module, s3, bucket):
    try:
        bucket = s3.create_bucket(bucket)
    except s3.provider.storage_response_error, e:
        module.fail_json(msg= str(e))
    if bucket:
        return True

def delete_bucket(module, s3, bucket):
    try:
        bucket = s3.lookup(bucket)
        bucket_contents = bucket.list()
        bucket.delete_keys([key.name for key in bucket_contents])
        bucket.delete()
        return True
    except s3.provider.storage_response_error, e:
        module.fail_json(msg= str(e))

def delete_key(module, s3, bucket, obj):
    try:
        bucket = s3.lookup(bucket)
        bucket.delete_key(obj)
        module.exit_json(msg="Object deleted from bucket %s"%bucket, changed=True)
    except s3.provider.storage_response_error, e:
        module.fail_json(msg= str(e))
 
179
def create_dirkey(module, s3, bucket, obj):
180 181
    try:
        bucket = s3.lookup(bucket)
182 183 184
        key = bucket.new_key(obj)
        key.set_contents_from_string('')
        module.exit_json(msg="Virtual directory %s created in bucket %s" % (obj, bucket.name), changed=True)
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
    except s3.provider.storage_response_error, e:
        module.fail_json(msg= str(e))

def upload_file_check(src):
    if os.path.exists(src):
        file_exists is True
    else:
        file_exists is False
    if os.path.isdir(src):
        module.fail_json(msg="Specifying a directory is not a valid source for upload.", failed=True)
        sys.exit(0)
    return file_exists

def path_check(path):
    if os.path.exists(path):
        return True 
    else:
        return False

def upload_s3file(module, s3, bucket, obj, src, expiry):
205
    try:
206 207 208
        bucket = s3.lookup(bucket)
        key = bucket.new_key(obj)  
        key.set_contents_from_filename(src)
209
        url = key.generate_url(expiry)
210 211 212 213 214 215 216 217 218 219 220
        module.exit_json(msg="PUT operation complete", url=url, changed=True)
        sys.exit(0)
    except s3.provider.storage_copy_error, e:
        module.fail_json(msg= str(e))

def download_s3file(module, s3, bucket, obj, dest):
    try:
        bucket = s3.lookup(bucket)
        key = bucket.lookup(obj)
        key.get_contents_to_filename(dest)
        module.exit_json(msg="GET operation complete", changed=True)
221 222 223 224
        sys.exit(0)
    except s3.provider.storage_copy_error, e:
        module.fail_json(msg= str(e))

225 226 227 228 229 230 231 232 233 234
def download_s3str(module, s3, bucket, obj):
    try:
        bucket = s3.lookup(bucket)
        key = bucket.lookup(obj)
        contents = key.get_contents_as_string()
        module.exit_json(msg="GET operation complete", contents=contents, changed=True)
        sys.exit(0)
    except s3.provider.storage_copy_error, e:
        module.fail_json(msg= str(e))

235 236 237 238 239 240 241 242 243 244
def get_download_url(module, s3, bucket, obj, expiry):
    try:
        bucket = s3.lookup(bucket)
        key = bucket.lookup(obj)
        url = key.generate_url(expiry)
        module.exit_json(msg="Download url:", url=url, expiry=expiry, changed=True)
        sys.exit(0)
    except s3.provider.storage_response_error, e:
        module.fail_json(msg= str(e))

lwade committed
245 246 247
def main():
    module = AnsibleModule(
        argument_spec = dict(
248 249 250
            bucket         = dict(required=True),
            object         = dict(),
            src            = dict(),
251
            dest           = dict(),
252
            mode           = dict(choices=['get', 'put', 'delete', 'create', 'geturl', 'getstr'], required=True),
253 254
            expiry         = dict(default=600, aliases=['expiration']),
            s3_url         = dict(aliases=['S3_URL']),
lwade committed
255 256
            ec2_secret_key = dict(aliases=['EC2_SECRET_KEY']),
            ec2_access_key = dict(aliases=['EC2_ACCESS_KEY']),
257
            overwrite      = dict(default=False, type='bool'),
lwade committed
258 259 260
        ),
    )

261 262 263
    bucket = module.params.get('bucket')
    obj = module.params.get('object')
    src = module.params.get('src')
264
    dest = module.params.get('dest')
265
    mode = module.params.get('mode')
lwade committed
266 267 268 269
    expiry = int(module.params['expiry'])
    s3_url = module.params.get('s3_url')
    ec2_secret_key = module.params.get('ec2_secret_key')
    ec2_access_key = module.params.get('ec2_access_key')
270
    overwrite = module.params.get('overwrite')
271 272 273
    
    if module.params.get('object'):
        obj = os.path.expanduser(module.params['object'])
lwade committed
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294

    # 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 not ec2_secret_key and 'EC2_SECRET_KEY' in os.environ:
        ec2_secret_key = os.environ['EC2_SECRET_KEY']
    if not ec2_access_key and 'EC2_ACCESS_KEY' in os.environ:
        ec2_access_key = os.environ['EC2_ACCESS_KEY']

    # If we have an S3_URL env var set, this is likely to be Walrus, so change connection method
    if 'S3_URL' in os.environ:
        try:
            walrus = urlparse.urlparse(s3_url).hostname
            s3 = boto.connect_walrus(walrus, ec2_access_key, ec2_secret_key)
        except boto.exception.NoAuthHandlerFound, e:
            module.fail_json(msg = str(e))
    else:
        try:
            s3 = boto.connect_s3(ec2_access_key, ec2_secret_key)
        except boto.exception.NoAuthHandlerFound, e:
            module.fail_json(msg = str(e))
295 296 297
 
    # If our mode is a GET operation (download), go through the procedure as appropriate ...
    if mode == 'get':
lwade committed
298
    
299 300 301 302 303
        # First, we check to see if the bucket exists, we get "bucket" returned.
        bucketrtn = bucket_check(module, s3, bucket)
        if bucketrtn is False:
            module.fail_json(msg="Target bucket cannot be found", failed=True)
            sys.exit(0)
lwade committed
304

305 306 307 308
        # Next, we check to see if the key in the bucket exists. If it exists, it also returns key_matches md5sum check.
        keyrtn = key_check(module, s3, bucket, obj)    
        if keyrtn is False:
            module.fail_json(msg="Target key cannot be found", failed=True)
lwade committed
309 310
            sys.exit(0)

311 312 313 314
        # If the destination path doesn't exist, no need to md5um etag check, so just download.
        pathrtn = path_check(dest)
        if pathrtn is False:
            download_s3file(module, s3, bucket, obj, dest)
315

316 317 318 319 320 321 322 323 324 325
        # Compare the remote MD5 sum of the object with the local dest md5sum, if it already exists. 
        if pathrtn is True:
            md5_remote = keysum(module, s3, bucket, obj)
            md5_local = hashlib.md5(open(dest, 'rb').read()).hexdigest()
            if md5_local == md5_remote:
                sum_matches = True
                if overwrite is True:
                    download_s3file(module, s3, bucket, obj, dest)
                else:
                    module.exit_json(msg="Local and remote object are identical, ignoring. Use overwrite parameter to force.", changed=False)
lwade committed
326
            else:
327 328 329 330 331
                sum_matches = False
                if overwrite is True:
                    download_s3file(module, s3, bucket, obj, dest)
                else:
                    module.fail_json(msg="WARNING: Checksums do not match. Use overwrite parameter to force download.", failed=True)
332
        
333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348
        # If destination file doesn't already exist we can go ahead and download.
        if pathrtn is False:
            download_s3file(module, s3, bucket, obj, dest)
   
        # Firstly, if key_matches is TRUE and overwrite is not enabled, we EXIT with a helpful message. 
        if sum_matches is True and overwrite is False:
            module.exit_json(msg="Local and remote object are identical, ignoring. Use overwrite parameter to force.", changed=False)

        # At this point explicitly define the overwrite condition.
        if sum_matches is True and pathrtn is True and overwrite is True:
            download_s3file(module, s3, bucket, obj, dest)

        # If sum does not match but the destination exists, we 
               
    # if our mode is a PUT operation (upload), go through the procedure as appropriate ...        
    if mode == 'put':
349

350 351 352
        # Use this snippet to debug through conditionals:
#       module.exit_json(msg="Bucket return %s"%bucketrtn)
#       sys.exit(0)
353

354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384
        # Lets check the src path.
        pathrtn = path_check(src)
        if pathrtn is False:
            module.fail_json(msg="Local object for PUT does not exist", failed=True)
            sys.exit(0)
        
        # Lets check to see if bucket exists to get ground truth.
        bucketrtn = bucket_check(module, s3, bucket)
        keyrtn = key_check(module, s3, bucket, obj)

        # Lets check key state. Does it exist and if it does, compute the etag md5sum.
        if bucketrtn is True and keyrtn is True:
                md5_remote = keysum(module, s3, bucket, obj)
                md5_local = hashlib.md5(open(src, 'rb').read()).hexdigest()
                if md5_local == md5_remote:
                    sum_matches = True
                    if overwrite is True:
                        upload_s3file(module, s3, bucket, obj, src, expiry)
                    else:
                        module.exit_json(msg="Local and remote object are identical, ignoring. Use overwrite parameter to force.", changed=False)
                else:
                    sum_matches = False
                    if overwrite is True:
                        upload_s3file(module, s3, bucket, obj, src, expiry)
                    else:
                        module.exit_json(msg="WARNING: Checksums do not match. Use overwrite parameter to force upload.", failed=True)
                                                                                                            
        # If neither exist (based on bucket existence), we can create both.
        if bucketrtn is False and pathrtn is True:      
            create_bucket(module, s3, bucket)
            upload_s3file(module, s3, bucket, obj, src, expiry)
lwade committed
385

386 387 388
        # If bucket exists but key doesn't, just upload.
        if bucketrtn is True and pathrtn is True and keyrtn is False:
            upload_s3file(module, s3, bucket, obj, src, expiry)
lwade committed
389
    
390 391 392 393 394 395 396 397 398 399
    # Support for deleting an object if we have both params.  
    if mode == 'delete':
        if bucket:
            bucketrtn = bucket_check(module, s3, bucket)
            if bucketrtn is True:
                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 does not exist.", failed=True)
lwade committed
400
        else:
401 402 403 404 405
            module.fail_json(msg="Bucket parameter is required.", failed=True)
 
    # 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':
406
        if bucket and not obj: 
407 408 409 410 411 412
            bucketrtn = bucket_check(module, s3, bucket)
            if bucketrtn is True:
                module.exit_json(msg="Bucket already exists.", changed=False)
            else:
                created = create_bucket(module, s3, bucket)
        if bucket and obj:
413 414 415 416 417 418 419 420 421 422 423 424 425 426
            bucketrtn = bucket_check(module, s3, bucket)
            if obj.endswith('/'):
                dirobj = obj
            else:
                dirobj = obj + "/"
            if bucketrtn is True:
                keyrtn = key_check(module, s3, bucket, dirobj)
                if keyrtn is True: 
                    module.exit_json(msg="Bucket %s and key %s already exists."% (bucket, obj), changed=False)
                else:      
                    create_dirkey(module, s3, bucket, dirobj)
            if bucketrtn is False:
                created = create_bucket(module, s3, bucket)
                create_dirkey(module, s3, bucket, dirobj)
427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442

    # Support for grabbing the time-expired URL for an object in S3/Walrus.
    if mode == 'geturl':
        if bucket and obj:
            bucketrtn = bucket_check(module, s3, bucket)
            if bucketrtn is False:
                module.fail_json(msg="Bucket %s does not exist."%bucket, failed=True)
            else:
                keyrtn = key_check(module, s3, bucket, obj)
                if keyrtn is True:
                    get_download_url(module, s3, bucket, obj, expiry)
                else:
                    module.fail_json(msg="Key %s does not exist."%obj, failed=True)
        else:
            module.fail_json(msg="Bucket and Object parameters must be set", failed=True)
            sys.exit(0)
443

444 445 446 447 448 449 450 451 452 453 454 455
    if mode == 'getstr':
        if bucket and obj:
            bucketrtn = bucket_check(module, s3, bucket)
            if bucketrtn is False:
                module.fail_json(msg="Bucket %s does not exist."%bucket, failed=True)
            else:
                keyrtn = key_check(module, s3, bucket, obj)
                if keyrtn is True:
                    download_s3str(module, s3, bucket, obj)
                else:
                    module.fail_json(msg="Key %s does not exist."%obj, failed=True)

lwade committed
456 457 458 459 460 461
    sys.exit(0)

# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>

main()