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