1 # -*- coding: utf-8 -*-
2 # Copyright 2011 Google Inc. All Rights Reserved.
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
8 # http://www.apache.org/licenses/LICENSE-2.0
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 """Implementation of update command for updating gsutil."""
17 from __future__
import absolute_import
28 from gslib
.command
import Command
29 from gslib
.cs_api_map
import ApiSelector
30 from gslib
.exception
import CommandException
31 from gslib
.sig_handling
import RegisterSignalHandler
32 from gslib
.util
import CERTIFICATE_VALIDATION_ENABLED
33 from gslib
.util
import CompareVersions
34 from gslib
.util
import GetBotoConfigFileList
35 from gslib
.util
import GSUTIL_PUB_TARBALL
36 from gslib
.util
import IS_CYGWIN
37 from gslib
.util
import IS_WINDOWS
38 from gslib
.util
import LookUpGsutilVersion
39 from gslib
.util
import RELEASE_NOTES_URL
43 gsutil update [-f] [-n] [url]
46 _DETAILED_HELP_TEXT
= ("""
52 The gsutil update command downloads the latest gsutil release, checks its
53 version, and offers to let you update to it if it differs from the version
54 you're currently running.
56 Once you say "Y" to the prompt of whether to install the update, the gsutil
57 update command locates where the running copy of gsutil is installed,
58 unpacks the new version into an adjacent directory, moves the previous version
59 aside, moves the new version to where the previous version was installed,
60 and removes the moved-aside old version. Because of this, users are cautioned
61 not to store data in the gsutil directory, since that data will be lost
62 when you update gsutil. (Some users change directories into the gsutil
63 directory to run the command. We advise against doing that, for this reason.)
64 Note also that the gsutil update command will refuse to run if it finds user
65 data in the gsutil directory.
67 By default gsutil update will retrieve the new code from
68 %s, but you can optionally specify a URL to use
69 instead. This is primarily used for distributing pre-release versions of
70 the code to a small group of early test users.
72 Note: gsutil periodically checks whether a more recent software update is
73 available. By default this check is performed every 30 days; you can change
74 (or disable) this check by editing the software_update_check_period variable
75 in the .boto config file. Note also that gsutil will only check for software
76 updates if stdin, stdout, and stderr are all connected to a TTY, to avoid
77 interfering with cron jobs, streaming transfers, and other cases where gsutil
78 input or output are redirected from/to files or pipes. Software update
79 periodic checks are also disabled by the gsutil -q option (see
80 'gsutil help options')
84 -f Forces the update command to offer to let you update, even if you
85 have the most current copy already. This can be useful if you have
86 a corrupted local copy.
88 -n Causes update command to run without prompting [Y/n] whether to
89 continue if an update is available.
90 """ % GSUTIL_PUB_TARBALL
)
93 class UpdateCommand(Command
):
94 """Implementation of gsutil update command."""
96 # Command specification. See base class for documentation.
97 command_spec
= Command
.CreateCommandSpec(
99 command_name_aliases
=['refresh'],
100 usage_synopsis
=_SYNOPSIS
,
103 supported_sub_args
='fn',
105 provider_url_ok
=False,
107 gs_api_support
=[ApiSelector
.XML
, ApiSelector
.JSON
],
108 gs_default_api
=ApiSelector
.JSON
,
110 # Help specification. See help_provider.py for documentation.
111 help_spec
= Command
.HelpSpec(
113 help_name_aliases
=['refresh'],
114 help_type
='command_help',
115 help_one_line_summary
='Update to the latest gsutil release',
116 help_text
=_DETAILED_HELP_TEXT
,
117 subcommand_help_text
={},
120 def _DisallowUpdataIfDataInGsutilDir(self
):
121 """Disallows the update command if files not in the gsutil distro are found.
123 This prevents users from losing data if they are in the habit of running
124 gsutil from the gsutil directory and leaving data in that directory.
126 This will also detect someone attempting to run gsutil update from a git
127 repo, since the top-level directory will contain git files and dirs (like
128 .git) that are not distributed with gsutil.
131 CommandException: if files other than those distributed with gsutil found.
133 # Manifest includes recursive-includes of gslib. Directly add
134 # those to the list here so we will skip them in os.listdir() loop without
135 # having to build deeper handling of the MANIFEST file here. Also include
136 # 'third_party', which isn't present in manifest but gets added to the
137 # gsutil distro by the gsutil submodule configuration; and the MANIFEST.in
138 # and CHANGES.md files.
139 manifest_lines
= ['gslib', 'third_party', 'MANIFEST.in', 'CHANGES.md']
142 with
open(os
.path
.join(gslib
.GSUTIL_DIR
, 'MANIFEST.in'), 'r') as fp
:
144 if line
.startswith('include '):
145 manifest_lines
.append(line
.split()[-1])
147 self
.logger
.warn('MANIFEST.in not found in %s.\nSkipping user data '
148 'check.\n', gslib
.GSUTIL_DIR
)
151 # Look just at top-level directory. We don't try to catch data dropped into
152 # subdirs (like gslib) because that would require deeper parsing of
153 # MANFFEST.in, and most users who drop data into gsutil dir do so at the top
155 for filename
in os
.listdir(gslib
.GSUTIL_DIR
):
156 if filename
.endswith('.pyc'):
157 # Ignore compiled code.
159 if filename
not in manifest_lines
:
160 raise CommandException('\n'.join(textwrap
.wrap(
161 'A file (%s) that is not distributed with gsutil was found in '
162 'the gsutil directory. The update command cannot run with user '
163 'data in the gsutil directory.' %
164 os
.path
.join(gslib
.GSUTIL_DIR
, filename
))))
166 def _ExplainIfSudoNeeded(self
, tf
, dirs_to_remove
):
167 """Explains what to do if sudo needed to update gsutil software.
169 Happens if gsutil was previously installed by a different user (typically if
170 someone originally installed in a shared file system location, using sudo).
174 dirs_to_remove: List of directories to remove.
177 CommandException: if errors encountered.
179 # If running under Windows or Cygwin we don't need (or have) sudo.
180 if IS_CYGWIN
or IS_WINDOWS
:
183 user_id
= os
.getuid()
184 if os
.stat(gslib
.GSUTIL_DIR
).st_uid
== user_id
:
187 # Won't fail - this command runs after main startup code that insists on
188 # having a config file.
189 config_file_list
= GetBotoConfigFileList()
190 config_files
= ' '.join(config_file_list
)
191 self
._CleanUpUpdateCommand
(tf
, dirs_to_remove
)
193 # Pick current protection of each boto config file for command that restores
194 # protection (rather than fixing at 600) to support use cases like how GCE
195 # installs a service account with an /etc/boto.cfg file protected to 644.
197 for config_file
in config_file_list
:
198 mode
= oct(stat
.S_IMODE((os
.stat(config_file
)[stat
.ST_MODE
])))
199 chmod_cmds
.append('\n\tsudo chmod %s %s' % (mode
, config_file
))
201 raise CommandException('\n'.join(textwrap
.wrap(
202 'Since it was installed by a different user previously, you will need '
203 'to update using the following commands. You will be prompted for your '
204 'password, and the install will run as "root". If you\'re unsure what '
205 'this means please ask your system administrator for help:')) + (
206 '\n\tsudo chmod 0644 %s\n\tsudo env BOTO_CONFIG="%s" %s update'
207 '%s') % (config_files
, config_files
, self
.gsutil_path
,
208 ' '.join(chmod_cmds
)), informational
=True)
210 # This list is checked during gsutil update by doing a lowercased
211 # slash-left-stripped check. For example "/Dev" would match the "dev" entry.
212 unsafe_update_dirs
= [
213 'applications', 'auto', 'bin', 'boot', 'desktop', 'dev',
214 'documents and settings', 'etc', 'export', 'home', 'kernel', 'lib',
215 'lib32', 'library', 'lost+found', 'mach_kernel', 'media', 'mnt', 'net',
216 'null', 'network', 'opt', 'private', 'proc', 'program files', 'python',
217 'root', 'sbin', 'scripts', 'srv', 'sys', 'system', 'tmp', 'users', 'usr',
218 'var', 'volumes', 'win', 'win32', 'windows', 'winnt',
221 def _EnsureDirsSafeForUpdate(self
, dirs
):
222 """Raises Exception if any of dirs is known to be unsafe for gsutil update.
224 This provides a fail-safe check to ensure we don't try to overwrite
225 or delete any important directories. (That shouldn't happen given the
226 way we construct tmp dirs, etc., but since the gsutil update cleanup
227 uses shutil.rmtree() it's prudent to add extra checks.)
230 dirs: List of directories to check.
233 CommandException: If unsafe directory encountered.
238 if d
.lstrip(os
.sep
).lower() in self
.unsafe_update_dirs
:
239 raise CommandException('EnsureDirsSafeForUpdate: encountered unsafe '
240 'directory (%s); aborting update' % d
)
242 def _CleanUpUpdateCommand(self
, tf
, dirs_to_remove
):
243 """Cleans up temp files etc. from running update command.
246 tf: Opened TarFile, or None if none currently open.
247 dirs_to_remove: List of directories to remove.
252 self
._EnsureDirsSafeForUpdate
(dirs_to_remove
)
253 for directory
in dirs_to_remove
:
255 shutil
.rmtree(directory
)
257 # Ignore errors while attempting to remove old dirs under Windows. They
258 # happen because of Windows exclusive file locking, and the update
259 # actually succeeds but just leaves the old versions around in the
264 def RunCommand(self
):
265 """Command entry point for the update command."""
267 if gslib
.IS_PACKAGE_INSTALL
:
268 raise CommandException(
269 'The update command is only available for gsutil installed from a '
270 'tarball. If you installed gsutil via another method, use the same '
271 'method to update it.')
273 if os
.environ
.get('CLOUDSDK_WRAPPER') == '1':
274 raise CommandException(
275 'The update command is disabled for Cloud SDK installs. Please run '
276 '"gcloud components update" to update it. Note: the Cloud SDK '
277 'incorporates updates to the underlying tools approximately every 2 '
278 'weeks, so if you are attempting to update to a recently created '
279 'release / pre-release of gsutil it may not yet be available via '
282 https_validate_certificates
= CERTIFICATE_VALIDATION_ENABLED
283 if not https_validate_certificates
:
284 raise CommandException(
285 'Your boto configuration has https_validate_certificates = False.\n'
286 'The update command cannot be run this way, for security reasons.')
288 self
._DisallowUpdataIfDataInGsutilDir
()
293 for o
, unused_a
in self
.sub_opts
:
300 tmp_dir
= tempfile
.mkdtemp()
301 dirs_to_remove
.append(tmp_dir
)
305 self
.logger
.info('Checking for software update...')
307 update_from_url_str
= self
.args
[0]
308 if not update_from_url_str
.endswith('.tar.gz'):
309 raise CommandException(
310 'The update command only works with tar.gz files.')
311 for i
, result
in enumerate(self
.WildcardIterator(update_from_url_str
)):
313 raise CommandException(
314 'Invalid update URL. Must name a single .tar.gz file.')
315 storage_url
= result
.storage_url
316 if storage_url
.IsFileUrl() and not storage_url
.IsDirectory():
318 raise CommandException(
319 ('"update" command does not support "file://" URLs without the '
321 elif not (storage_url
.IsCloudUrl() and storage_url
.IsObject()):
322 raise CommandException(
323 'Invalid update object URL. Must name a single .tar.gz file.')
325 update_from_url_str
= GSUTIL_PUB_TARBALL
327 # Try to retrieve version info from tarball metadata; failing that; download
328 # the tarball and extract the VERSION file. The version lookup will fail
329 # when running the update system test, because it retrieves the tarball from
330 # a temp file rather than a cloud URL (files lack the version metadata).
331 tarball_version
= LookUpGsutilVersion(self
.gsutil_api
, update_from_url_str
)
335 tf
= self
._FetchAndOpenGsutilTarball
(update_from_url_str
)
337 with
open(os
.path
.join('gsutil', 'VERSION'), 'r') as ver_file
:
338 tarball_version
= ver_file
.read().strip()
340 if not force_update
and gslib
.VERSION
== tarball_version
:
341 self
._CleanUpUpdateCommand
(tf
, dirs_to_remove
)
343 raise CommandException('You already have %s installed.' %
344 update_from_url_str
, informational
=True)
346 raise CommandException('You already have the latest gsutil release '
347 'installed.', informational
=True)
350 (_
, major
) = CompareVersions(tarball_version
, gslib
.VERSION
)
352 print('\n'.join(textwrap
.wrap(
353 'This command will update to the "%s" version of gsutil at %s. '
354 'NOTE: This a major new version, so it is strongly recommended '
355 'that you review the release note details at %s before updating to '
356 'this version, especially if you use gsutil in scripts.'
357 % (tarball_version
, gslib
.GSUTIL_DIR
, RELEASE_NOTES_URL
))))
359 print('This command will update to the "%s" version of\ngsutil at %s'
360 % (tarball_version
, gslib
.GSUTIL_DIR
))
361 self
._ExplainIfSudoNeeded
(tf
, dirs_to_remove
)
366 answer
= raw_input('Proceed? [y/N] ')
367 if not answer
or answer
.lower()[0] != 'y':
368 self
._CleanUpUpdateCommand
(tf
, dirs_to_remove
)
369 raise CommandException('Not running update.', informational
=True)
372 tf
= self
._FetchAndOpenGsutilTarball
(update_from_url_str
)
374 # Ignore keyboard interrupts during the update to reduce the chance someone
375 # hitting ^C leaves gsutil in a broken state.
376 RegisterSignalHandler(signal
.SIGINT
, signal
.SIG_IGN
)
378 # gslib.GSUTIL_DIR lists the path where the code should end up (like
379 # /usr/local/gsutil), which is one level down from the relative path in the
380 # tarball (since the latter creates files in ./gsutil). So, we need to
381 # extract at the parent directory level.
382 gsutil_bin_parent_dir
= os
.path
.normpath(
383 os
.path
.join(gslib
.GSUTIL_DIR
, '..'))
385 # Extract tarball to a temporary directory in a sibling to GSUTIL_DIR.
386 old_dir
= tempfile
.mkdtemp(dir=gsutil_bin_parent_dir
)
387 new_dir
= tempfile
.mkdtemp(dir=gsutil_bin_parent_dir
)
388 dirs_to_remove
.append(old_dir
)
389 dirs_to_remove
.append(new_dir
)
390 self
._EnsureDirsSafeForUpdate
(dirs_to_remove
)
392 tf
.extractall(path
=new_dir
)
394 self
._CleanUpUpdateCommand
(tf
, dirs_to_remove
)
395 raise CommandException('Update failed: %s.' % e
)
397 # For enterprise mode (shared/central) installation, users with
398 # different user/group than the installation user/group must be
399 # able to run gsutil so we need to do some permissions adjustments
400 # here. Since enterprise mode is not not supported for Windows
401 # users, we can skip this step when running on Windows, which
402 # avoids the problem that Windows has no find or xargs command.
404 # Make all files and dirs in updated area owner-RW and world-R, and make
405 # all directories owner-RWX and world-RX.
406 for dirname
, subdirs
, filenames
in os
.walk(new_dir
):
407 for filename
in filenames
:
408 fd
= os
.open(os
.path
.join(dirname
, filename
), os
.O_RDONLY
)
409 os
.fchmod(fd
, stat
.S_IWRITE | stat
.S_IRUSR |
410 stat
.S_IRGRP | stat
.S_IROTH
)
412 for subdir
in subdirs
:
413 fd
= os
.open(os
.path
.join(dirname
, subdir
), os
.O_RDONLY
)
414 os
.fchmod(fd
, stat
.S_IRWXU | stat
.S_IXGRP | stat
.S_IXOTH |
415 stat
.S_IRGRP | stat
.S_IROTH
)
418 # Make main gsutil script owner-RWX and world-RX.
419 fd
= os
.open(os
.path
.join(new_dir
, 'gsutil', 'gsutil'), os
.O_RDONLY
)
420 os
.fchmod(fd
, stat
.S_IRWXU | stat
.S_IRGRP | stat
.S_IXGRP |
421 stat
.S_IROTH | stat
.S_IXOTH
)
424 # Move old installation aside and new into place.
425 os
.rename(gslib
.GSUTIL_DIR
, os
.path
.join(old_dir
, 'old'))
426 os
.rename(os
.path
.join(new_dir
, 'gsutil'), gslib
.GSUTIL_DIR
)
427 self
._CleanUpUpdateCommand
(tf
, dirs_to_remove
)
428 RegisterSignalHandler(signal
.SIGINT
, signal
.SIG_DFL
)
429 self
.logger
.info('Update complete.')
432 def _FetchAndOpenGsutilTarball(self
, update_from_url_str
):
433 self
.command_runner
.RunNamedCommand(
434 'cp', [update_from_url_str
, 'file://gsutil.tar.gz'], self
.headers
,
435 self
.debug
, skip_update_check
=True)
436 # Note: tf is closed in _CleanUpUpdateCommand.
437 tf
= tarfile
.open('gsutil.tar.gz')
438 tf
.errorlevel
= 1 # So fatal tarball unpack errors raise exceptions.