1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3#
4# Copyright (C) 2017 Google
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6# ----------------------------------------------------------------------------
7#
8#     ***     AUTO GENERATED CODE    ***    AUTO GENERATED CODE     ***
9#
10# ----------------------------------------------------------------------------
11#
12#     This file is automatically generated by Magic Modules and manual
13#     changes will be clobbered when the file is regenerated.
14#
15#     Please read more about how to change this file at
16#     https://www.github.com/GoogleCloudPlatform/magic-modules
17#
18# ----------------------------------------------------------------------------
19
20from __future__ import absolute_import, division, print_function
21
22__metaclass__ = type
23
24################################################################################
25# Documentation
26################################################################################
27
28ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ["preview"], 'supported_by': 'community'}
29
30DOCUMENTATION = '''
31---
32module: gcp_storage_object
33description:
34- Upload or download a file from a GCS bucket.
35short_description: Creates a GCP Object
36version_added: 2.8
37author: Google Inc. (@googlecloudplatform)
38requirements:
39- python >= 2.6
40- requests >= 2.18.4
41- google-auth >= 1.3.0
42options:
43  state:
44    description:
45    - Whether the given object should exist in GCP
46    choices:
47    - present
48    - absent
49    default: present
50    type: str
51  action:
52    description:
53    - Upload or download from the bucket.
54    - 'Some valid choices include: "download", "upload"'
55    required: false
56    type: str
57  overwrite:
58    description:
59    - "'Overwrite the file on the bucket/local machine. If overwrite is false and
60      a difference exists between GCS + local, module will fail with error' ."
61    required: false
62    type: bool
63  src:
64    description:
65    - Source location of file (may be local machine or cloud depending on action).
66    required: false
67    type: path
68  dest:
69    description:
70    - Destination location of file (may be local machine or cloud depending on action).
71    required: false
72    type: path
73  bucket:
74    description:
75    - The name of the bucket.
76    required: false
77    type: str
78extends_documentation_fragment: gcp
79'''
80
81EXAMPLES = '''
82- name: create a object
83  gcp_storage_object:
84    action: download
85    bucket: ansible-bucket
86    src: modules.zip
87    dest: "~/modules.zip"
88    project: test_project
89    auth_kind: serviceaccount
90    service_account_file: "/tmp/auth.pem"
91    state: present
92'''
93
94RETURN = '''
95action:
96  description:
97  - Upload or download from the bucket.
98  returned: success
99  type: str
100overwrite:
101  description:
102  - "'Overwrite the file on the bucket/local machine. If overwrite is false and a
103    difference exists between GCS + local, module will fail with error' ."
104  returned: success
105  type: bool
106src:
107  description:
108  - Source location of file (may be local machine or cloud depending on action).
109  returned: success
110  type: str
111dest:
112  description:
113  - Destination location of file (may be local machine or cloud depending on action).
114  returned: success
115  type: str
116bucket:
117  description:
118  - The name of the bucket.
119  returned: success
120  type: str
121'''
122
123################################################################################
124# Imports
125################################################################################
126
127from ansible.module_utils.gcp_utils import navigate_hash, GcpSession, GcpModule, GcpRequest, replace_resource_dict
128import json
129import os
130import mimetypes
131import hashlib
132import base64
133
134################################################################################
135# Main
136################################################################################
137
138
139def main():
140    """Main function"""
141
142    module = GcpModule(
143        argument_spec=dict(
144            state=dict(default='present', choices=['present', 'absent'], type='str'),
145            action=dict(type='str'),
146            overwrite=dict(type='bool'),
147            src=dict(type='path'),
148            dest=dict(type='path'),
149            bucket=dict(type='str'),
150        )
151    )
152
153    if not module.params['scopes']:
154        module.params['scopes'] = ['https://www.googleapis.com/auth/devstorage.full_control']
155
156    remote_object = fetch_resource(module, self_link(module))
157    local_file_exists = os.path.isfile(local_file_path(module))
158
159    # Check if files exist.
160    if module.params['action'] == 'download' and not remote_object:
161        module.fail_json(msg="File does not exist in bucket")
162
163    if module.params['action'] == 'upload' and not local_file_exists:
164        module.fail_json(msg="File does not exist on disk")
165
166    # Check if we'll be overwriting files.
167    if not module.params['overwrite']:
168        remote_object['changed'] = False
169        if module.params['action'] == 'download' and local_file_exists:
170            # If files differ, throw an error
171            if get_md5_local(local_file_path(module)) != remote_object['md5Hash']:
172                module.fail_json(msg="Local file is different than remote file")
173            # If files are the same, module is done running.
174            else:
175                module.exit_json(**remote_object)
176
177        elif module.params['action'] == 'upload' and remote_object:
178            # If files differ, throw an error
179            if get_md5_local(local_file_path(module)) != remote_object['md5Hash']:
180                module.fail_json(msg="Local file is different than remote file")
181            # If files are the same, module is done running.
182            else:
183                module.exit_json(**remote_object)
184
185    # Upload/download the files
186    auth = GcpSession(module, 'storage')
187    if module.params['action'] == 'download':
188        results = download_file(module)
189    else:
190        results = upload_file(module)
191
192    module.exit_json(**results)
193
194
195def download_file(module):
196    auth = GcpSession(module, 'storage')
197    data = auth.get(media_link(module))
198    with open(module.params['dest'], 'w') as f:
199        f.write(data.text.encode('utf8'))
200    return fetch_resource(module, self_link(module))
201
202
203def upload_file(module):
204    auth = GcpSession(module, 'storage')
205    with open(module.params['src'], 'r') as f:
206        results = return_if_object(module, auth.post_contents(upload_link(module), f, object_headers(module)))
207    results['changed'] = True
208    return results
209
210
211def get_md5_local(path):
212    md5 = hashlib.md5()
213    with open(path, "rb") as f:
214        for chunk in iter(lambda: f.read(4096), b""):
215            md5.update(chunk)
216    return base64.b64encode(md5.digest())
217
218
219def get_md5_remote(module):
220    resource = fetch_resource(module, self_link(module))
221    return resource.get('md5Hash')
222
223
224def fetch_resource(module, link, allow_not_found=True):
225    auth = GcpSession(module, 'storage')
226    return return_if_object(module, auth.get(link), allow_not_found)
227
228
229def self_link(module):
230    if module.params['action'] == 'download':
231        return "https://www.googleapis.com/storage/v1/b/{bucket}/o/{src}".format(**module.params)
232    else:
233        return "https://www.googleapis.com/storage/v1/b/{bucket}/o/{dest}".format(**module.params)
234
235
236def local_file_path(module):
237    if module.params['action'] == 'download':
238        return module.params['dest']
239    else:
240        return module.params['src']
241
242
243def media_link(module):
244    if module.params['action'] == 'download':
245        return "https://www.googleapis.com/storage/v1/b/{bucket}/o/{src}?alt=media".format(**module.params)
246    else:
247        return "https://www.googleapis.com/storage/v1/b/{bucket}/o/{dest}?alt=media".format(**module.params)
248
249
250def upload_link(module):
251    return "https://www.googleapis.com/upload/storage/v1/b/{bucket}/o?uploadType=media&name={dest}".format(**module.params)
252
253
254def return_if_object(module, response, allow_not_found=False):
255    # If not found, return nothing.
256    if allow_not_found and response.status_code == 404:
257        return None
258
259    # If no content, return nothing.
260    if response.status_code == 204:
261        return None
262
263    try:
264        module.raise_for_status(response)
265        result = response.json()
266    except getattr(json.decoder, 'JSONDecodeError', ValueError) as inst:
267        module.fail_json(msg="Invalid JSON response with error: %s" % inst)
268
269    if navigate_hash(result, ['error', 'errors']):
270        module.fail_json(msg=navigate_hash(result, ['error', 'errors']))
271
272    return result
273
274
275# Remove unnecessary properties from the response.
276# This is for doing comparisons with Ansible's current parameters.
277def object_headers(module):
278    return {
279        "name": module.params['dest'],
280        "Content-Type": mimetypes.guess_type(module.params['src'])[0],
281        "Content-Length": str(os.path.getsize(module.params['src'])),
282    }
283
284
285if __name__ == '__main__':
286    main()
287