1# -*- coding: utf-8 -*- 2# Copyright 2013 Google Inc. All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15"""This module provides the notification command to gsutil.""" 16 17from __future__ import absolute_import 18 19import getopt 20import uuid 21 22from gslib.cloud_api import AccessDeniedException 23from gslib.command import Command 24from gslib.command import NO_MAX 25from gslib.command_argument import CommandArgument 26from gslib.cs_api_map import ApiSelector 27from gslib.exception import CommandException 28from gslib.help_provider import CreateHelpText 29from gslib.storage_url import StorageUrlFromString 30 31 32_WATCHBUCKET_SYNOPSIS = """ 33 gsutil notification watchbucket [-i id] [-t token] app_url bucket_url... 34""" 35 36_STOPCHANNEL_SYNOPSIS = """ 37 gsutil notification stopchannel channel_id resource_id 38""" 39 40_SYNOPSIS = _WATCHBUCKET_SYNOPSIS + _STOPCHANNEL_SYNOPSIS.lstrip('\n') 41 42_WATCHBUCKET_DESCRIPTION = """ 43<B>WATCHBUCKET</B> 44 The watchbucket sub-command can be used to watch a bucket for object changes. 45 A service account must be used when running this command. 46 47 The app_url parameter must be an HTTPS URL to an application that will be 48 notified of changes to any object in the bucket. The URL endpoint must be 49 a verified domain on your project. See 50 `Notification Authorization <https://developers.google.com/storage/docs/object-change-notification#_Authorization>`_ 51 for details. 52 53 The optional id parameter can be used to assign a unique identifier to the 54 created notification channel. If not provided, a random UUID string will be 55 generated. 56 57 The optional token parameter can be used to validate notifications events. 58 To do this, set this custom token and store it to later verify that 59 notification events contain the client token you expect. 60 61""" 62 63_STOPCHANNEL_DESCRIPTION = """ 64<B>STOPCHANNEL</B> 65 The stopchannel sub-command can be used to stop sending change events to a 66 notification channel. 67 68 The channel_id and resource_id parameters should match the values from the 69 response of a bucket watch request. 70 71""" 72 73_DESCRIPTION = """ 74 The notification command can be used to configure notifications. 75 For more information on the Object Change Notification feature, please see: 76 https://developers.google.com/storage/docs/object-change-notification 77 78 The notification command has two sub-commands: 79""" + _WATCHBUCKET_DESCRIPTION + _STOPCHANNEL_DESCRIPTION + """ 80 81<B>EXAMPLES</B> 82 83 Watch the bucket example-bucket for changes and send notifications to an 84 application server running at example.com: 85 86 gsutil notification watchbucket https://example.com/notify \\ 87 gs://example-bucket 88 89 Assign identifier my-channel-id to the created notification channel: 90 91 gsutil notification watchbucket -i my-channel-id \\ 92 https://example.com/notify gs://example-bucket 93 94 Set a custom client token that will be included with each notification event: 95 96 gsutil notification watchbucket -t my-client-token \\ 97 https://example.com/notify gs://example-bucket 98 99 Stop the notification event channel with channel identifier channel1 and 100 resource identifier SoGqan08XDIFWr1Fv_nGpRJBHh8: 101 102 gsutil notification stopchannel channel1 SoGqan08XDIFWr1Fv_nGpRJBHh8 103 104<B>NOTIFICATIONS AND PARALLEL COMPOSITE UPLOADS</B> 105 106 By default, gsutil enables parallel composite uploads for large files (see 107 "gsutil help cp"), which means that an upload of a large object can result 108 in multiple temporary component objects being uploaded before the actual 109 intended object is created. Any subscriber to notifications for this bucket 110 will then see a notification for each of these components being created and 111 deleted. If this is a concern for you, note that parallel composite uploads 112 can be disabled by setting "parallel_composite_upload_threshold = 0" in your 113 boto config file. 114 115""" 116 117NOTIFICATION_AUTHORIZATION_FAILED_MESSAGE = """ 118Watch bucket attempt failed: 119 {watch_error} 120 121You attempted to watch a bucket with an application URL of: 122 123 {watch_url} 124 125which is not authorized for your project. Please ensure that you are using 126Service Account authentication and that the Service Account's project is 127authorized for the application URL. Notification endpoint URLs must also be 128whitelisted in your Cloud Console project. To do that, the domain must also be 129verified using Google Webmaster Tools. For instructions, please see: 130 131 https://developers.google.com/storage/docs/object-change-notification#_Authorization 132""" 133 134_DETAILED_HELP_TEXT = CreateHelpText(_SYNOPSIS, _DESCRIPTION) 135 136_watchbucket_help_text = ( 137 CreateHelpText(_WATCHBUCKET_SYNOPSIS, _WATCHBUCKET_DESCRIPTION)) 138_stopchannel_help_text = ( 139 CreateHelpText(_STOPCHANNEL_SYNOPSIS, _STOPCHANNEL_DESCRIPTION)) 140 141 142class NotificationCommand(Command): 143 """Implementation of gsutil notification command.""" 144 145 # Command specification. See base class for documentation. 146 command_spec = Command.CreateCommandSpec( 147 'notification', 148 command_name_aliases=[ 149 'notify', 'notifyconfig', 'notifications', 'notif'], 150 usage_synopsis=_SYNOPSIS, 151 min_args=3, 152 max_args=NO_MAX, 153 supported_sub_args='i:t:', 154 file_url_ok=False, 155 provider_url_ok=False, 156 urls_start_arg=1, 157 gs_api_support=[ApiSelector.JSON], 158 gs_default_api=ApiSelector.JSON, 159 argparse_arguments={ 160 'watchbucket': [ 161 CommandArgument.MakeFreeTextArgument(), 162 CommandArgument.MakeZeroOrMoreCloudBucketURLsArgument() 163 ], 164 'stopchannel': [] 165 } 166 ) 167 # Help specification. See help_provider.py for documentation. 168 help_spec = Command.HelpSpec( 169 help_name='notification', 170 help_name_aliases=['watchbucket', 'stopchannel', 'notifyconfig'], 171 help_type='command_help', 172 help_one_line_summary='Configure object change notification', 173 help_text=_DETAILED_HELP_TEXT, 174 subcommand_help_text={'watchbucket': _watchbucket_help_text, 175 'stopchannel': _stopchannel_help_text}, 176 ) 177 178 def _WatchBucket(self): 179 """Creates a watch on a bucket given in self.args.""" 180 self.CheckArguments() 181 identifier = None 182 client_token = None 183 if self.sub_opts: 184 for o, a in self.sub_opts: 185 if o == '-i': 186 identifier = a 187 if o == '-t': 188 client_token = a 189 190 identifier = identifier or str(uuid.uuid4()) 191 watch_url = self.args[0] 192 bucket_arg = self.args[-1] 193 194 if not watch_url.lower().startswith('https://'): 195 raise CommandException('The application URL must be an https:// URL.') 196 197 bucket_url = StorageUrlFromString(bucket_arg) 198 if not (bucket_url.IsBucket() and bucket_url.scheme == 'gs'): 199 raise CommandException( 200 'The %s command can only be used with gs:// bucket URLs.' % 201 self.command_name) 202 if not bucket_url.IsBucket(): 203 raise CommandException('URL must name a bucket for the %s command.' % 204 self.command_name) 205 206 self.logger.info('Watching bucket %s with application URL %s ...', 207 bucket_url, watch_url) 208 209 try: 210 channel = self.gsutil_api.WatchBucket( 211 bucket_url.bucket_name, watch_url, identifier, token=client_token, 212 provider=bucket_url.scheme) 213 except AccessDeniedException, e: 214 self.logger.warn(NOTIFICATION_AUTHORIZATION_FAILED_MESSAGE.format( 215 watch_error=str(e), watch_url=watch_url)) 216 raise 217 218 channel_id = channel.id 219 resource_id = channel.resourceId 220 client_token = channel.token 221 self.logger.info('Successfully created watch notification channel.') 222 self.logger.info('Watch channel identifier: %s', channel_id) 223 self.logger.info('Canonicalized resource identifier: %s', resource_id) 224 self.logger.info('Client state token: %s', client_token) 225 226 return 0 227 228 def _StopChannel(self): 229 channel_id = self.args[0] 230 resource_id = self.args[1] 231 232 self.logger.info('Removing channel %s with resource identifier %s ...', 233 channel_id, resource_id) 234 self.gsutil_api.StopChannel(channel_id, resource_id, provider='gs') 235 self.logger.info('Succesfully removed channel.') 236 237 return 0 238 239 def _RunSubCommand(self, func): 240 try: 241 (self.sub_opts, self.args) = getopt.getopt( 242 self.args, self.command_spec.supported_sub_args) 243 return func() 244 except getopt.GetoptError, e: 245 self.RaiseInvalidArgumentException() 246 247 def RunCommand(self): 248 """Command entry point for the notification command.""" 249 subcommand = self.args.pop(0) 250 251 if subcommand == 'watchbucket': 252 return self._RunSubCommand(self._WatchBucket) 253 elif subcommand == 'stopchannel': 254 return self._RunSubCommand(self._StopChannel) 255 else: 256 raise CommandException('Invalid subcommand "%s" for the %s command.' % 257 (subcommand, self.command_name)) 258