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