| OLD | NEW |
| 1 # -*- coding: utf-8 -*- |
| 1 # Copyright 2013 Google Inc. All Rights Reserved. | 2 # Copyright 2013 Google Inc. All Rights Reserved. |
| 2 # | 3 # |
| 3 # Licensed under the Apache License, Version 2.0 (the "License"); | 4 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 # you may not use this file except in compliance with the License. | 5 # you may not use this file except in compliance with the License. |
| 5 # You may obtain a copy of the License at | 6 # You may obtain a copy of the License at |
| 6 # | 7 # |
| 7 # http://www.apache.org/licenses/LICENSE-2.0 | 8 # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 # | 9 # |
| 9 # Unless required by applicable law or agreed to in writing, software | 10 # Unless required by applicable law or agreed to in writing, software |
| 10 # distributed under the License is distributed on an "AS IS" BASIS, | 11 # distributed under the License is distributed on an "AS IS" BASIS, |
| 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 # See the License for the specific language governing permissions and | 13 # See the License for the specific language governing permissions and |
| 13 # limitations under the License. | 14 # limitations under the License. |
| 14 """This module provides the notification command to gsutil.""" | 15 """This module provides the notification command to gsutil.""" |
| 15 | 16 |
| 17 from __future__ import absolute_import |
| 18 |
| 16 import getopt | 19 import getopt |
| 17 import uuid | 20 import uuid |
| 18 | 21 |
| 19 from apiclient import discovery | 22 from gslib.cloud_api import AccessDeniedException |
| 20 from apiclient import errors as apiclient_errors | |
| 21 import boto | |
| 22 | |
| 23 from gslib.command import Command | 23 from gslib.command import Command |
| 24 from gslib.command import COMMAND_NAME | |
| 25 from gslib.command import COMMAND_NAME_ALIASES | |
| 26 from gslib.command import FILE_URIS_OK | |
| 27 from gslib.command import MAX_ARGS | |
| 28 from gslib.command import MIN_ARGS | |
| 29 from gslib.command import NO_MAX | 24 from gslib.command import NO_MAX |
| 30 from gslib.command import PROVIDER_URIS_OK | 25 from gslib.cs_api_map import ApiSelector |
| 31 from gslib.command import SUPPORTED_SUB_ARGS | |
| 32 from gslib.command import URIS_START_ARG | |
| 33 from gslib.exception import CommandException | 26 from gslib.exception import CommandException |
| 34 from gslib.help_provider import CreateHelpText | 27 from gslib.help_provider import CreateHelpText |
| 35 from gslib.help_provider import HELP_NAME | 28 from gslib.storage_url import StorageUrlFromString |
| 36 from gslib.help_provider import HELP_NAME_ALIASES | |
| 37 from gslib.help_provider import HELP_ONE_LINE_SUMMARY | |
| 38 from gslib.help_provider import HELP_TEXT | |
| 39 from gslib.help_provider import HELP_TYPE | |
| 40 from gslib.help_provider import HelpType | |
| 41 from gslib.help_provider import SUBCOMMAND_HELP_TEXT | |
| 42 | 29 |
| 43 | 30 |
| 44 _WATCHBUCKET_SYNOPSIS = """ | 31 _WATCHBUCKET_SYNOPSIS = """ |
| 45 gsutil notification watchbucket [-i id] [-t token] app_url bucket_uri... | 32 gsutil notification watchbucket [-i id] [-t token] app_url bucket_url... |
| 46 """ | 33 """ |
| 47 | 34 |
| 48 _STOPCHANNEL_SYNOPSIS = """ | 35 _STOPCHANNEL_SYNOPSIS = """ |
| 49 gsutil notification stopchannel channel_id resource_id | 36 gsutil notification stopchannel channel_id resource_id |
| 50 """ | 37 """ |
| 51 | 38 |
| 52 _SYNOPSIS = _WATCHBUCKET_SYNOPSIS + _STOPCHANNEL_SYNOPSIS.lstrip('\n') | 39 _SYNOPSIS = _WATCHBUCKET_SYNOPSIS + _STOPCHANNEL_SYNOPSIS.lstrip('\n') |
| 53 | 40 |
| 54 _WATCHBUCKET_DESCRIPTION = """ | 41 _WATCHBUCKET_DESCRIPTION = """ |
| 55 <B>WATCHBUCKET</B> | 42 <B>WATCHBUCKET</B> |
| 56 The watchbucket sub-command can be used to watch a bucket for object | 43 The watchbucket sub-command can be used to watch a bucket for object changes. |
| 57 changes. | 44 A service account must be used when running this command. |
| 58 | 45 |
| 59 The app_url parameter must be an HTTPS URL to an application that will be | 46 The app_url parameter must be an HTTPS URL to an application that will be |
| 60 notified of changes to any object in the bucket. The URL endpoint must be | 47 notified of changes to any object in the bucket. The URL endpoint must be |
| 61 a verified domain on your project. See | 48 a verified domain on your project. See |
| 62 `Notification Authorization <https://developers.google.com/storage/docs/object
-change-notification#_Authorization>`_ | 49 `Notification Authorization <https://developers.google.com/storage/docs/object
-change-notification#_Authorization>`_ |
| 63 for details. | 50 for details. |
| 64 | 51 |
| 65 The optional id parameter can be used to assign a unique identifier to the | 52 The optional id parameter can be used to assign a unique identifier to the |
| 66 created notification channel. If not provided, a random UUID string will be | 53 created notification channel. If not provided, a random UUID string will be |
| 67 generated. | 54 generated. |
| (...skipping 57 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 125 boto config file. | 112 boto config file. |
| 126 | 113 |
| 127 """ | 114 """ |
| 128 | 115 |
| 129 NOTIFICATION_AUTHORIZATION_FAILED_MESSAGE = """ | 116 NOTIFICATION_AUTHORIZATION_FAILED_MESSAGE = """ |
| 130 Watch bucket attempt failed: | 117 Watch bucket attempt failed: |
| 131 {watch_error} | 118 {watch_error} |
| 132 | 119 |
| 133 You attempted to watch a bucket with an application URL of: | 120 You attempted to watch a bucket with an application URL of: |
| 134 | 121 |
| 135 {watch_uri} | 122 {watch_url} |
| 136 | 123 |
| 137 which is not authorized for your project. Notification endpoint URLs must be | 124 which is not authorized for your project. Please ensure that you are using |
| 125 Service Account authentication and that the Service Account's project is |
| 126 authorized for the application URL. Notification endpoint URLs must also be |
| 138 whitelisted in your Cloud Console project. To do that, the domain must also be | 127 whitelisted in your Cloud Console project. To do that, the domain must also be |
| 139 verified using Google Webmaster Tools. For instructions, please see: | 128 verified using Google Webmaster Tools. For instructions, please see: |
| 140 | 129 |
| 141 https://developers.google.com/storage/docs/object-change-notification#_Authori
zation | 130 https://developers.google.com/storage/docs/object-change-notification#_Authori
zation |
| 142 """ | 131 """ |
| 143 | 132 |
| 144 _detailed_help_text = CreateHelpText(_SYNOPSIS, _DESCRIPTION) | 133 _DETAILED_HELP_TEXT = CreateHelpText(_SYNOPSIS, _DESCRIPTION) |
| 145 | 134 |
| 146 _watchbucket_help_text = ( | 135 _watchbucket_help_text = ( |
| 147 CreateHelpText(_WATCHBUCKET_SYNOPSIS, _WATCHBUCKET_DESCRIPTION)) | 136 CreateHelpText(_WATCHBUCKET_SYNOPSIS, _WATCHBUCKET_DESCRIPTION)) |
| 148 _stopchannel_help_text = ( | 137 _stopchannel_help_text = ( |
| 149 CreateHelpText(_STOPCHANNEL_SYNOPSIS, _STOPCHANNEL_DESCRIPTION)) | 138 CreateHelpText(_STOPCHANNEL_SYNOPSIS, _STOPCHANNEL_DESCRIPTION)) |
| 150 | 139 |
| 151 DISCOVERY_SERVICE_URL = boto.config.get_value( | |
| 152 'GSUtil', 'discovery_service_url', None) | |
| 153 JSON_API_VERSION = boto.config.get_value( | |
| 154 'GSUtil', 'json_api_version', 'v1beta2') | |
| 155 | |
| 156 | 140 |
| 157 class NotificationCommand(Command): | 141 class NotificationCommand(Command): |
| 158 """Implementation of gsutil notification command.""" | 142 """Implementation of gsutil notification command.""" |
| 159 | 143 |
| 160 # Command specification (processed by parent class). | 144 # Command specification. See base class for documentation. |
| 161 command_spec = { | 145 command_spec = Command.CreateCommandSpec( |
| 162 # Name of command. | 146 'notification', |
| 163 COMMAND_NAME: 'notification', | 147 command_name_aliases=[ |
| 164 # List of command name aliases. | 148 'notify', 'notifyconfig', 'notifications', 'notif'], |
| 165 COMMAND_NAME_ALIASES: ['notify', 'notifyconfig', 'notifications', | 149 min_args=3, |
| 166 'notif'], | 150 max_args=NO_MAX, |
| 167 # Min number of args required by this command. | 151 supported_sub_args='i:t:', |
| 168 MIN_ARGS: 3, | 152 file_url_ok=False, |
| 169 # Max number of args required by this command, or NO_MAX. | 153 provider_url_ok=False, |
| 170 MAX_ARGS: NO_MAX, | 154 urls_start_arg=1, |
| 171 # Getopt-style string specifying acceptable sub args. | 155 gs_api_support=[ApiSelector.JSON], |
| 172 SUPPORTED_SUB_ARGS: 'i:t:', | 156 gs_default_api=ApiSelector.JSON, |
| 173 # True if file URIs acceptable for this command. | 157 ) |
| 174 FILE_URIS_OK: True, | 158 # Help specification. See help_provider.py for documentation. |
| 175 # True if provider-only URIs acceptable for this command. | 159 help_spec = Command.HelpSpec( |
| 176 PROVIDER_URIS_OK: False, | 160 help_name='notification', |
| 177 # Index in args of first URI arg. | 161 help_name_aliases=['watchbucket', 'stopchannel', 'notifyconfig'], |
| 178 URIS_START_ARG: 1, | 162 help_type='command_help', |
| 179 } | 163 help_one_line_summary='Configure object change notification', |
| 180 help_spec = { | 164 help_text=_DETAILED_HELP_TEXT, |
| 181 # Name of command or auxiliary help info for which this help applies. | 165 subcommand_help_text={'watchbucket': _watchbucket_help_text, |
| 182 HELP_NAME: 'notification', | 166 'stopchannel': _stopchannel_help_text}, |
| 183 # List of help name aliases. | 167 ) |
| 184 HELP_NAME_ALIASES: ['watchbucket', 'stopchannel', 'notifyconfig'], | |
| 185 # Type of help: | |
| 186 HELP_TYPE: HelpType.COMMAND_HELP, | |
| 187 # One line summary of this help. | |
| 188 HELP_ONE_LINE_SUMMARY: 'Configure object change notification', | |
| 189 # The full help text. | |
| 190 HELP_TEXT: _detailed_help_text, | |
| 191 # Help text for sub-commands. | |
| 192 SUBCOMMAND_HELP_TEXT : {'watchbucket' : _watchbucket_help_text, | |
| 193 'stopchannel' : _stopchannel_help_text}, | |
| 194 } | |
| 195 | 168 |
| 196 def _WatchBucket(self): | 169 def _WatchBucket(self): |
| 170 """Creates a watch on a bucket given in self.args.""" |
| 171 self.CheckArguments() |
| 197 identifier = None | 172 identifier = None |
| 198 client_token = None | 173 client_token = None |
| 199 if self.sub_opts: | 174 if self.sub_opts: |
| 200 for o, a in self.sub_opts: | 175 for o, a in self.sub_opts: |
| 201 if o == '-i': | 176 if o == '-i': |
| 202 identifier = a | 177 identifier = a |
| 203 if o == '-t': | 178 if o == '-t': |
| 204 client_token = a | 179 client_token = a |
| 205 | 180 |
| 206 identifier = identifier or str(uuid.uuid4()) | 181 identifier = identifier or str(uuid.uuid4()) |
| 207 watch_uri = self.args[0] | 182 watch_url = self.args[0] |
| 208 bucket_arg = self.args[-1] | 183 bucket_arg = self.args[-1] |
| 209 | 184 |
| 210 if not watch_uri.lower().startswith('https://'): | 185 if not watch_url.lower().startswith('https://'): |
| 211 raise CommandException('The application URL must be an https:// URL.') | 186 raise CommandException('The application URL must be an https:// URL.') |
| 212 | 187 |
| 213 bucket_uri = self.suri_builder.StorageUri(bucket_arg) | 188 bucket_url = StorageUrlFromString(bucket_arg) |
| 214 if bucket_uri.get_provider().name != 'google': | 189 if not (bucket_url.IsBucket() and bucket_url.scheme == 'gs'): |
| 215 raise CommandException( | 190 raise CommandException( |
| 216 'The %s command can only be used with gs:// bucket URIs.' % | 191 'The %s command can only be used with gs:// bucket URLs.' % |
| 217 self.command_name) | 192 self.command_name) |
| 218 if not bucket_uri.names_bucket(): | 193 if not bucket_url.IsBucket(): |
| 219 raise CommandException('URI must name a bucket for the %s command.' % | 194 raise CommandException('URL must name a bucket for the %s command.' % |
| 220 self.command_name) | 195 self.command_name) |
| 221 | 196 |
| 222 self.logger.info('Watching bucket %s with application URL %s ...', | 197 self.logger.info('Watching bucket %s with application URL %s ...', |
| 223 bucket_uri, watch_uri) | 198 bucket_url, watch_url) |
| 224 | 199 |
| 225 bucket = bucket_uri.get_bucket() | 200 try: |
| 226 auth_handler = bucket.connection._auth_handler | 201 channel = self.gsutil_api.WatchBucket( |
| 227 oauth2_client = getattr(auth_handler, 'oauth2_client', None) | 202 bucket_url.bucket_name, watch_url, identifier, token=client_token, |
| 228 if not oauth2_client: | 203 provider=bucket_url.scheme) |
| 229 raise CommandException( | 204 except AccessDeniedException, e: |
| 230 'The %s command requires using OAuth credentials.' % | 205 self.logger.warn(NOTIFICATION_AUTHORIZATION_FAILED_MESSAGE.format( |
| 231 self.command_name) | 206 watch_error=str(e), watch_url=watch_url)) |
| 207 raise |
| 232 | 208 |
| 233 http = oauth2_client.CreateHttpRequest() | 209 channel_id = channel.id |
| 234 kwargs = {'http': http} | 210 resource_id = channel.resourceId |
| 235 if DISCOVERY_SERVICE_URL: | 211 client_token = channel.token |
| 236 kwargs['discoveryServiceUrl'] = DISCOVERY_SERVICE_URL | |
| 237 service = discovery.build( | |
| 238 'storage', JSON_API_VERSION, **kwargs) | |
| 239 | |
| 240 body = {'type': 'WEB_HOOK', | |
| 241 'address': watch_uri, | |
| 242 'id': identifier} | |
| 243 if client_token: | |
| 244 body['token'] = client_token | |
| 245 request = service.objects().watchAll(body=body, bucket=bucket.name) | |
| 246 request.headers['authorization'] = oauth2_client.GetAuthorizationHeader() | |
| 247 try: | |
| 248 response = request.execute() | |
| 249 except apiclient_errors.HttpError, e: | |
| 250 if e.resp.status == 401 and 'Unauthorized' in str(e): | |
| 251 self.logger.warn(NOTIFICATION_AUTHORIZATION_FAILED_MESSAGE.format( | |
| 252 watch_error=str(e), watch_uri=watch_uri)) | |
| 253 return 1 | |
| 254 else: | |
| 255 raise | |
| 256 | |
| 257 channel_id = response['id'] | |
| 258 resource_id = response['resourceId'] | |
| 259 client_token = response.get('token', '') | |
| 260 self.logger.info('Successfully created watch notification channel.') | 212 self.logger.info('Successfully created watch notification channel.') |
| 261 self.logger.info('Watch channel identifier: %s', channel_id) | 213 self.logger.info('Watch channel identifier: %s', channel_id) |
| 262 self.logger.info('Canonicalized resource identifier: %s', resource_id) | 214 self.logger.info('Canonicalized resource identifier: %s', resource_id) |
| 263 self.logger.info('Client state token: %s', client_token) | 215 self.logger.info('Client state token: %s', client_token) |
| 264 | 216 |
| 265 return 0 | 217 return 0 |
| 266 | 218 |
| 267 def _StopChannel(self): | 219 def _StopChannel(self): |
| 268 channel_id = self.args[0] | 220 channel_id = self.args[0] |
| 269 resource_id = self.args[1] | 221 resource_id = self.args[1] |
| 270 | 222 |
| 271 uri = self.suri_builder.StorageUri('gs://') | |
| 272 self.logger.info('Removing channel %s with resource identifier %s ...', | 223 self.logger.info('Removing channel %s with resource identifier %s ...', |
| 273 channel_id, resource_id) | 224 channel_id, resource_id) |
| 274 | 225 self.gsutil_api.StopChannel(channel_id, resource_id, provider='gs') |
| 275 auth_handler = uri.connect()._auth_handler | |
| 276 oauth2_client = getattr(auth_handler, 'oauth2_client', None) | |
| 277 if not oauth2_client: | |
| 278 raise CommandException( | |
| 279 'The %s command requires using OAuth credentials.' % | |
| 280 self.command_name) | |
| 281 | |
| 282 http = oauth2_client.CreateHttpRequest() | |
| 283 kwargs = {'http': http} | |
| 284 if DISCOVERY_SERVICE_URL: | |
| 285 kwargs['discoveryServiceUrl'] = DISCOVERY_SERVICE_URL | |
| 286 service = discovery.build( | |
| 287 'storage', JSON_API_VERSION, **kwargs) | |
| 288 | |
| 289 body = {'id': channel_id, | |
| 290 'resourceId': resource_id} | |
| 291 request = service.channels().stop(body=body) | |
| 292 request.headers['authorization'] = oauth2_client.GetAuthorizationHeader() | |
| 293 request.execute() | |
| 294 self.logger.info('Succesfully removed channel.') | 226 self.logger.info('Succesfully removed channel.') |
| 295 | 227 |
| 296 return 0 | 228 return 0 |
| 297 | 229 |
| 298 def _RunSubCommand(self, func): | 230 def _RunSubCommand(self, func): |
| 299 try: | 231 try: |
| 300 (self.sub_opts, self.args) = getopt.getopt( | 232 (self.sub_opts, self.args) = getopt.getopt( |
| 301 self.args, self.command_spec[SUPPORTED_SUB_ARGS]) | 233 self.args, self.command_spec.supported_sub_args) |
| 302 return func() | 234 return func() |
| 303 except getopt.GetoptError, e: | 235 except getopt.GetoptError, e: |
| 304 raise CommandException('%s for "%s" command.' % (e.msg, | 236 raise CommandException('%s for "%s" command.' % (e.msg, |
| 305 self.command_name)) | 237 self.command_name)) |
| 306 | 238 |
| 307 def RunCommand(self): | 239 def RunCommand(self): |
| 240 """Command entry point for the notification command.""" |
| 308 subcommand = self.args.pop(0) | 241 subcommand = self.args.pop(0) |
| 242 |
| 309 if subcommand == 'watchbucket': | 243 if subcommand == 'watchbucket': |
| 310 return self._RunSubCommand(self._WatchBucket) | 244 return self._RunSubCommand(self._WatchBucket) |
| 311 elif subcommand == 'stopchannel': | 245 elif subcommand == 'stopchannel': |
| 312 return self._RunSubCommand(self._StopChannel) | 246 return self._RunSubCommand(self._StopChannel) |
| 313 else: | 247 else: |
| 314 raise CommandException('Invalid subcommand "%s" for the %s command.' % | 248 raise CommandException('Invalid subcommand "%s" for the %s command.' % |
| 315 (subcommand, self.command_name)) | 249 (subcommand, self.command_name)) |
| OLD | NEW |