diff options
-rwxr-xr-x | ur_upgrade.py | 307 |
1 files changed, 175 insertions, 132 deletions
diff --git a/ur_upgrade.py b/ur_upgrade.py index f0e3879..ff23a10 100755 --- a/ur_upgrade.py +++ b/ur_upgrade.py @@ -1,26 +1,26 @@ #!/usr/bin/env python -#Copyright (c) 2014, Joseph Hunkeler <jhunkeler at gmail.com> -#All rights reserved. +# Copyright (c) 2014, Joseph Hunkeler <jhunkeler at gmail.com> +# All rights reserved. # -#Redistribution and use in source and binary forms, with or without -#modification, are permitted provided that the following conditions are met: +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -#1. Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -#2. Redistributions in binary form must reproduce the above copyright notice, +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # -#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -#ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -#WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -#DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -#ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -#(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -#LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -#ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -#(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -#SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from __future__ import division import argparse @@ -40,7 +40,7 @@ from itertools import chain DEFAULT_MIRROR = "http://ssb.stsci.edu/ureka" class Ureka(object): - ''' + ''' ''' def __init__(self, basepath): self.path = basepath @@ -50,7 +50,6 @@ class Ureka(object): self.info = self._info() else: self.info = {} - def _info(self): data = [] @@ -62,29 +61,29 @@ class Ureka(object): except: item = '' data.append(item) - + ureka_info = namedtuple('ureka_info', self._files) return ureka_info._make(data) - + def __getitem__(self, key): info = self.info._asdict() if key not in info: return None return info[key] - + def __iter__(self): for item in self.info._asdict().iteritems(): yield item def ur_getenv(ur_dir): - ''' Evaluate environment variables produced by ur-setup-real + ''' Evaluates environment variables produced by ur-setup-real ''' path = os.path.join(ur_dir, 'bin') command = '{}/ur-setup-real -sh'.format(path).split() output = subprocess.check_output(command, shell=True, stderr=open(os.path.devnull)) output = output.split(os.linesep) output_env = [] - + # Generate environment keypairs for line in output: if not line: @@ -96,82 +95,163 @@ def ur_getenv(ur_dir): line = line.replace(';', '') line = line.replace('"', '') output_env.append(line.partition("=")[::2]) - + output_env_temp = dict(output_env) - + # Perform shell expansion of Ureka's environment variables for k, v in output_env_temp.items(): template = Template(v) v = template.safe_substitute(output_env_temp) output_env_temp[k] = v - + # Merge Ureka's environment with existing system environment output_env_temp = dict(chain(os.environ.items(), output_env_temp.items())) - + # Assign expanded variables output_env = output_env_temp - + return output_env def ur_check_version(urobj, vers): - if StrictVersion(urobj['version']) > vers: + if StrictVersion(urobj['version']) > StrictVersion(vers): return False return True -def get_archive(url): - print("Downloading archive... {}".format(url)) - try: - req_data = urllib2.urlopen(url) - except Exception as ex: - print("Error {}, {}: {}".format(ex.code, ex.msg, url)) - exit(1) - - remote_size = float(req_data.headers['content-length']) - total = 0 - with open(os.path.join(UPGRADE_TEMP, UREKA_TARBALL), 'wb') as installer: - for chunk in iter(lambda: req_data.read(16*1024), ''): - print("\r{:.2f}% [{:.2f}/{:.2f} MB]".format((total / remote_size * 100), (total / 1024 ** 2), (remote_size / 1024 ** 2))), - total += float(len(chunk)) - installer.write(chunk) - print("") - -def upgrade(srcobj, destobj): - print("Upgrading distribution from {} to {}...".format(srcobj['version'], destobj['version'])) - proc = subprocess.Popen('rsync -a -ureka_dist_original {} {}'.format(destobj.path, os.path.dirname(srcobj.path)).split()) - proc.wait() - -def unpack_archive(): - installer = tarfile.open(os.path.join(UPGRADE_TEMP, UREKA_TARBALL), 'r') - print("Preparing to unpack upgrade... (please wait)") - files_total = len(installer.getmembers()) - - print("Unpacking upgrade...") - for index, member in enumerate(installer, start=1): - print("\r{:.2f}% [{}/{}]".format((index / files_total * 100), index, files_total)), - installer.extract(member, path=TEMP_INSTALL_PATH) - print("") - -def pre_upgrade(): - with open(os.path.join(TEMP_INSTALL_PATH, 'Ureka', 'misc', 'name'), 'w+') as fp: - fp.write(ureka_dist_original['name'] + os.linesep) - -def post_upgrade(urobj): - print("Performing upgrade maintenance tasks...") - ur_env = ur_getenv(urobj.path) - proc = subprocess.Popen('ur_normalize -n -i -x'.split(), env=ur_env) - proc.wait() - - print("Purging temporary data...") - shutil.rmtree(UPGRADE_TEMP) - - -def cleanup(sig, stack): - print('') - print('Received signal {}!'.format(sig)) - print('Cleaning up...') - shutil.rmtree(UPGRADE_TEMP) - exit(sig) - +class VersionError(Exception): + pass + +class Upgrade(object): + def __init__(self, ur_dir, to_version, mirror=DEFAULT_MIRROR, **kwargs): + self.mirror = mirror + self.ureka = Ureka(ur_dir) + self.ureka_next = None + self.to_version = to_version + self.tmp = tempfile.mkdtemp(prefix="upgrade") + self.tmp_dist = os.path.join(self.tmp, self.to_version) + self.archive_ext = '.tar.gz' + + if 'archive_ext' in kwargs: + self.archive_ext = kwargs['archive_ext'] + + self.archive = "Ureka_{}_{}_{}{}".format(self.ureka['os'], + self.ureka['bits'], + self.to_version, + self.archive_ext) + self.archive_url = "{}/{}/{}".format(self.mirror, + self.to_version, + self.archive) + self.archive_path = os.path.abspath(os.path.join(self.tmp, self.archive)) + + if not ur_check_version(self.ureka, self.to_version): + self._cleanup() + raise VersionError("Refusing to downgrade from {} to {}".format(self.ureka['version'], self.to_version)) + exit(1) + + signal.signal(signal.SIGINT, self._cleanup_on_signal) + signal.signal(signal.SIGTERM, self._cleanup_on_signal) + + if not which('rsync'): + print('++ rsync not found in PATH. Please install it.') + exit(1) + + def run(self): + self._get_archive() + ureka_next_path = self._unpack_archive() + self._pre() + + # Populate temporary Ureka upgrade object + self.ureka_next = Ureka(ureka_next_path) + + # Sync data + if not self._upgrade(self.ureka_next, self.ureka): + self._cleanup() + print("++ Upgrade failed!") + return 1 + + # Regenerate original Ureka object + self.ureka = Ureka(self.ureka.path) + + if not self._post(self.ureka): + self._cleanup() + print("++ Post-installation failed!") + return 1 + + self._cleanup() + + def _cleanup(self): + shutil.rmtree(self.tmp) + + def _cleanup_on_signal(self, sig, stack): + print('') + print('++ Received signal {}...'.format(sig)) + self._cleanup() + exit(sig) + + def _get_archive(self): + print("+ Downloading archive... {}".format(self.archive_url)) + try: + req_data = urllib2.urlopen(self.archive_url) + except Exception as ex: + print("Error {}, {}: {}".format(ex.code, ex.msg, self.archive_url)) + exit(1) + + remote_size = float(req_data.headers['content-length']) + + total = 0 + with open(self.archive_path, 'wb') as installer: + for chunk in iter(lambda: req_data.read(16 * 1024), ''): + total += float(len(chunk)) + installer.write(chunk) + print("\r{:.2f}% [{:.2f}/{:.2f} MB]".format((total / remote_size * 100), + (total / 1024 ** 2), + (remote_size / 1024 ** 2))), + print("") + + def _unpack_archive(self): + print('+ Preparing to unpack (please wait)...'), + installer = tarfile.open(self.archive_path, 'r') + files_total = len(installer.getmembers()) + print('done') + print("+ Unpacking...") + for index, member in enumerate(installer, start=1): + print("\r{:.2f}% [{}/{}]".format((index / files_total * 100), + index, + files_total)), + installer.extract(member, path=self.tmp_dist) + print("") + + return os.path.join(self.tmp_dist, 'Ureka') + + def _pre(self): + print("+ Executing pre-upgrade tasks...") + misc = os.path.join(self.tmp_dist, 'Ureka', 'misc', 'name') + with open(misc, 'w+') as fp: + fp.write(self.ureka['name'] + os.linesep) + + def _upgrade(self, src, dest): + print("+ Upgrade in progress ({} to {})...".format(src['version'], + dest['version'])) + command = 'rsync -a -u {} {}'.format(dest.path, + os.path.dirname(src.path)).split() + proc = subprocess.Popen(command) + proc.wait() + + if proc.returncode: + return False + return True + + def _post(self, ur): + print("+ Executing post-upgrade tasks...") + ur_env = ur_getenv(ur.path) + + command = 'ur_normalize -n -i -x'.split() + proc = subprocess.Popen(command, env=ur_env) + proc.wait() + + if proc.returncode: + return False + return True + if __name__ == "__main__": parser = argparse.ArgumentParser(description='Ureka Upgrade Utility') @@ -180,63 +260,26 @@ if __name__ == "__main__": parser.add_argument('--request', type=str, help='Upgrade to a specific version') parser.add_argument('--mirror', type=str, help='Use a Ureka download mirror') args = parser.parse_args() - + if args.latest and args.request: print("--latest and --request are mutually exclusive options.") exit(1) - + if args.latest: # Need to work out a deal with SSB. - # "LATEST-STABLE" and "LATEST-DEVEL" links could be useful + # "LATEST-STABLE" and "LATEST-DEVEL" links could be useful print("--latest is not implemented yet.") exit(1) - + if not args.request: print("No version requested. Use --request (e.g. 1.4.1)") exit(1) - - UPGRADE_TEMP = tempfile.mkdtemp(prefix="upgrade") - CURRENT = args.UR_DIR - - # Register signal handlers AFTER temporary directory is created - signal.signal(signal.SIGINT, cleanup) - signal.signal(signal.SIGTERM, cleanup) - - if not which('rsync'): - print('rsync not found in PATH. Please install it.') - exit(1) - - ureka_dist_original = Ureka(CURRENT) - - if not ur_check_version(ureka_dist_original, args.request): - print("Refusing to downgrade from {} to {}".format(ureka_dist_original['version'], args.request)) - exit(1) - - TEMP_INSTALL_PATH = os.path.join(UPGRADE_TEMP, args.request) - UREKA_TARBALL = "Ureka_{}_{}_{}.tar.gz".format(ureka_dist_original['os'], ureka_dist_original['bits'], args.request) - url = "{}/{}/{}".format(DEFAULT_MIRROR, args.request, UREKA_TARBALL) + mirror = DEFAULT_MIRROR if args.mirror: - url = "{}/{}/{}".format(args.mirror, args.request, UREKA_TARBALL) - - # Download requested archive - get_archive(url) - - # Unpack archive to temporary storage - unpack_archive() - - # Generate upgrade Ureka object - ureka_dist_new = Ureka(os.path.join(TEMP_INSTALL_PATH, 'Ureka')) - - # Perform tasks required before upgrading - pre_upgrade() - - # Perform upgrade - upgrade(ureka_dist_original, ureka_dist_new) - - # Regenerate upgraded ureka installation target - ureka_dist_original = Ureka(CURRENT) - - # Perform tasks required after upgrade is completed - post_upgrade(ureka_dist_original) - + mirror = args.mirror + + upgrade = Upgrade(args.UR_DIR, args.request, mirror) + exit_code = upgrade.run() + + exit(exit_code) |