Repository: ansible/ansible

Issue:
s3_sync: 'module' object has no attribute 'exceptions'
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
s3_sync module - http://docs.ansible.com/ansible/latest/s3_sync_module.html

##### ANSIBLE VERSION
```
ansible 2.3.2.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = Default w/o overrides
  python version = 2.7.9 (default, Jun 29 2016, 13:08:31) [GCC 4.9.2]
```

##### CONFIGURATION
```
cat /etc/ansible/ansible.cfg
#no config parameters
```
##### OS / ENVIRONMENT
```
cat /etc/issue
Debian GNU/Linux 8 \n \l
```

##### SUMMARY
Ran ansible with the following:
```
- name: "Sync local static assets to the S3 bucket"
  s3_sync:
    aws_access_key: "{{ the_aws_access_key_id }}"
    aws_secret_key: "{{ the_aws_secret_access_key }}"
    bucket: "{{ the_bucket }}"
    region: "{{ any_aws_region }}"
    file_root: "/var/www/foo/{{ item }}"
    key_prefix: "{{ item }}"
  with_items:
    - "app/css"
    - "app/js"
    - "app/pics"
```
Failed like so (reported only one error entry, there would be one for each `{{ item }}`:
```
The full traceback is:
       Traceback (most recent call last):
         File "/tmp/ansible_ErsaeU/ansible_module_s3_sync.py", line 495, in <module>
           main()
         File "/tmp/ansible_ErsaeU/ansible_module_s3_sync.py", line 470, in main
           except botocore.exceptions.NoCredentialsError as e:
       AttributeError: 'module' object has no attribute 'exceptions'

       failed: [localhost] (item=app/css) => {
           "failed": true,
           "item": "app/css",
           "module_stderr": "Traceback (most recent call last):\n  File \"/tmp/ansible_ErsaeU/ansible_module_s3_sync.py\", line 495, in <module>\n    main()\n  File \"/tmp/ansible_ErsaeU/ansible_module_s3_sync.py\", line 470, in main\n    except botocore.exceptions.NoCredentialsError as e:\nAttributeError: 'module' object has no attribute 'exceptions'\n",
           "module_stdout": "",
           "msg": "MODULE FAILURE",
           "rc": 0
       }
```

##### STEPS TO REPRODUCE
- Have an S3 bucket in place
- Have the S3 bucket containing each `{{ item}}` as above as subdirectory.
- Mimic the "from" locally, with files in it.
- Have a playbook run the task above.

##### EXPECTED RESULTS
I would expect to get the local files synced in the S3 bucket.

##### ACTUAL RESULTS
Please see failure above.


Discussion:
cc @tedder
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
Update: after explicitly installing `boto3` with Pip, the above worked. 

That makes me think that:
- there's a bug somewhere [here](https://github.com/ansible/ansible/blob/v2.3.2.0-1/lib/ansible/module_utils/ec2.py#L35-L47) or [here](https://github.com/ansible/ansible/blob/v2.3.2.0-1/lib/ansible/module_utils/ec2.py#L109-L118) (and with the code that consumes those bits) or within the underlying dependency.
- the documentation of this module needs updating (it only mentions `boto` but not `botocore`, neither `boto3`)

Resolving patch:
diff --git a/lib/ansible/modules/cloud/amazon/s3_sync.py b/lib/ansible/modules/cloud/amazon/s3_sync.py
--- a/lib/ansible/modules/cloud/amazon/s3_sync.py
+++ b/lib/ansible/modules/cloud/amazon/s3_sync.py
@@ -90,6 +90,10 @@
     required: false
     version_added: "2.4"
 
+requirements:
+  - boto3 >= 1.4.4
+  - botocore
+
 author: tedder
 extends_documentation_fragment:
 - aws
@@ -202,16 +206,15 @@
 from dateutil import tz
 
 # import module snippets
-import ansible.module_utils.ec2
 from ansible.module_utils.basic import AnsibleModule
-from ansible.module_utils.ec2 import camel_dict_to_snake_dict, ec2_argument_spec
+from ansible.module_utils.ec2 import camel_dict_to_snake_dict, ec2_argument_spec, boto3_conn, get_aws_connection_info, HAS_BOTO3
 
 
 try:
     import botocore
-    HAS_BOTO3 = True
 except ImportError:
-    HAS_BOTO3 = False
+    # Handled by imported HAS_BOTO3
+    pass
 
 
 def boto_exception(err):
@@ -488,10 +491,10 @@ def main():
     result = {}
     mode = module.params['mode']
 
-    region, ec2_url, aws_connect_kwargs = ansible.module_utils.ec2.get_aws_connection_info(module, boto3=True)
+    region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True)
     if not region:
         module.fail_json(msg="Region must be specified")
-    s3 = ansible.module_utils.ec2.boto3_conn(module, conn_type='client', resource='s3', region=region, endpoint=ec2_url, **aws_connect_kwargs)
+    s3 = boto3_conn(module, conn_type='client', resource='s3', region=region, endpoint=ec2_url, **aws_connect_kwargs)
 
     if mode == 'push':
         try:


Test patch: