#!/usr/bin/python -u # -*- coding: utf-8 -*- # ########################################################################## # Maj-Auto - Manage automatique update of EOLE server # Copyright © 2015 Pôle de compétences EOLE # # License CeCILL: # * in french: http://www.cecill.info/licences/Licence_CeCILL_V2-fr.html # * in english http://www.cecill.info/licences/Licence_CeCILL_V2-en.html ########################################################################## import sys import warnings import apt import atexit from argparse import ArgumentParser import locale from pyeole.i18n import i18n from pyeole import lock from pyeole.process import system_code from pyeole.ihm import print_title, print_red, print_green from pyeole.ihm import question_ouinon from creole.config import configeoldir from os import system from os import access from os import R_OK from os import mkdir from os import unlink from os.path import join from os.path import isfile from os.path import isdir import time import re from shutil import copytree from shutil import copy from pyeole.pkg import EolePkg, _configure_sources_mirror from creole.fonctionseole import controle_kernel, zephir from creole.config import eoledir from creole.config import templatedir from creole.config import vareole from creole.template import CreoleTemplateEngine from creole.client import CreoleClient from glob import glob import hashlib from urllib2 import urlopen from urllib2 import ProxyHandler from urllib2 import build_opener from HTMLParser import HTMLParser warnings.filterwarnings("ignore", "apt API not stable yet", FutureWarning) _ = i18n('update-manager') if not isfile(join(configeoldir, '.upgrade_available')): print "" print_red(_(u'Upgrade to a newer major version is not available')) print _(u'Use Maj-Release script to upgrade to next minoir version') print "" sys.exit(1) from UpdateManager.Core.MetaRelease import MetaReleaseCore from UpdateManager.Core.DistUpgradeFetcherCore import DistUpgradeFetcherCore from DistUpgrade.utils import init_proxy quit_re = re.compile(r'q|(quit)|(exit)|(abort)', re.I) tmp_dir = '/tmp/Upgrade-Auto' def release_lock(): if lock.is_locked('upgrade-auto', level='system'): lock.release('upgrade-auto', level='system') class EOLEDistUpgradeFetcherCore(DistUpgradeFetcherCore): def verifyDistUprader(self, *args, **kwargs): copy(join(tmp_dir, 'DistUpgradeViewEOLE.py'), self.tmpdir) return super(EOLEDistUpgradeFetcherCore, self).verifyDistUprader(*args, **kwargs) def cli_choice(alternatives, prompt, title=None, guess=False): """ Display choices in terminal and return chosen one :param alternatives: choices proposed to user :type alternatives: list :param guess: wether to guess choice :type guess: boolean """ def default_input(alt_mapping, prompt, title=None, guess=False): choices = "\n".join(["[{0}] {1}".format(alt[0], alt[1]) for alt in alt_mapping.items()]) choices = _(u"Available choices:\n{}\n").format(choices) if title is not None: print_green(title) print choices if guess is True and len(alt_mapping) < 2: print_green(_(u"Automatically selected first choice: {}\n").format(alt_mapping[1])) choice = "1" else: try: prompt = prompt + _(u"\n[1]: ") choice = raw_input(prompt) except KeyboardInterrupt, EOFError: print _("\nUpgrade aborted by user") sys.exit(0) if choice == '': choice = "1" return choice alt_mapping = {num + 1: choice for num, choice in enumerate(alternatives)} choice = default_input(alt_mapping, prompt, title=title, guess=guess) if choice not in alt_mapping.values(): try: choice = alt_mapping[int(choice)] except KeyError: print _("Choice {} not available\n").format(choice) choice = cli_choice(alternatives, prompt, guess=guess) except ValueError: if quit_re.match(choice): print _("Upgrade cancelled by user") sys.exit(0) else: print _("Invalid input: {}\n").format(choice) choice = cli_choice(alternatives, prompt, guess=guess) return choice def upgrade_container_source(container): """Edit source list in container :param container: container name :type container: str """ print 'changement des sources.list pour le conteneur ', container source_list = '/opt/lxc/{}/rootfs/etc/apt/sources.list'.format(container) with open(source_list) as old_source: sources = old_source.read() with open(source_list, 'w') as new_source: new_source.write(sources.replace('precise', 'trusty')) cmd = ['apt-get', 'update'] code = system_code(cmd, container=container) return code ALTERNATIVES = ("2.5.1", ) RUNPARTS_CMD = u'/bin/run-parts --exit-on-error -v {directory}' PRE_DOWNLOAD = 'pre_download' UPGRADE_DIR = join(eoledir, 'upgrade') ISO_DIR = join(vareole, 'iso') ISO_URL_BASE = 'http://eole.ac-dijon.fr/pub/iso' z_proc = "UPGRADE" class ExtractEOLEVersions(HTMLParser): """Extrat stable EOLE versions from HTML page Gathered versions are stored in ``self.versions``. """ def __init__(self, version): HTMLParser.__init__(self) self.version = version self.versions = [] self.process_a = False def handle_starttag(self, tag, attrs): if tag != 'a': self.process_a = False return self.process_a = True def handle_data(self, data): if not self.process_a: return # Strip not matching and pre stable versions if data.lower().startswith(self.version) and '-' not in data: self.versions.append(data.rstrip('/')) def get_most_recent_version(match, url, proxy=None): """Get the most recent matching version """ if proxy is not None: proxy_handler = ProxyHandler({'http': proxy, 'https': proxy}) else: proxy_handler = ProxyHandler({}) opener = build_opener(proxy_handler) http_request = opener.open(url) html_parser = ExtractEOLEVersions(match) html_parser.feed(http_request.read()) html_parser.versions.sort() return html_parser.versions[-1] def build_iso_name(version): """Build ISO name for version """ arch = apt.apt_pkg.config.get('APT::Architecture') iso_name = 'eole-{version}-alternate-{arch}.iso'.format(version=version, arch=arch) return iso_name def build_release_url(version, proxy=None): """Build the URL of latest release of a version """ version_url = ISO_URL_BASE + '/EOLE-' + '.'.join(version.split('.')[0:2]) latest_version = get_most_recent_version(version, version_url, proxy) release_url = '{base}/{release}'.format(base=version_url, release=latest_version) return release_url def check_iso(iso_name, path, release_url, proxy=None): """Verify checksum and signature """ sha256_file = join(ISO_DIR, 'SHA256SUMS') sha256_url = release_url + '/SHA256SUMS' sha256_gpg_file = join(ISO_DIR, 'SHA256SUMS.gpg') sha256_gpg_url = sha256_url + '.gpg' sha256 = hashlib.sha256() iso_ok = False print _(u"Verifying ISO image {iso}").format(iso=path) if not isfile(sha256_file): print _(u"Download SHA256SUMS file") download_with_wget(sha256_file, sha256_url, proxy) if not isfile(sha256_gpg_file): print _(u"Download SHA256SUMS.gpg file") download_with_wget(sha256_gpg_file, sha256_gpg_url, proxy) print _(u"Check SHA256SUMS file signature") gpg_cmd = ['gpgv', '-q', '--keyring', '/etc/apt/trusted.gpg.d/eole-archive-keyring.gpg', sha256_gpg_file, sha256_file] ret = system_code(gpg_cmd) if ret == 0: print _(u"Check ISO SHA256..."), sha_fh = open(sha256_file, 'r') for line in sha_fh: sha, filename = line.split() if filename != '*{iso_name}'.format(iso_name=iso_name): continue with open(path, 'rb') as iso_fh: while True: block = iso_fh.read(2**10) if not block: break sha256.update(block) iso_ok = sha == sha256.hexdigest() if iso_ok: print _(u'OK') return True print _(u'Error') # Default return False def download_with_wget(out_file, url, proxy=None, limit_rate="0"): """Use wget to download a file with a progress bar and rate limit """ wcmd = ['wget', '-c', '--progress', 'dot:giga'] if limit_rate != '0': wcmd.extend(['--limit-rate', limit_rate]) wcmd.extend(['-O', out_file, url]) if proxy is not None: env = {'http_proxy': proxy, 'https_proxy': proxy} else: env = {} if not system_code(wcmd, env=env) == 0: err_msg = _("Error downloading {file} with wget from {url}") err_msg = err_msg.format(file=out_file, url=url) zephir("ERR", err_msg, z_proc) raise SystemError(err_msg) def download_iso_with_zsync(iso_file, iso_url): """Use zsync to download the ISO """ zcmd = ['zsync', '-o', iso_file, iso_url + '.zsync'] if not system_code(zcmd) == 0: err_msg = _("Error downloading the image with zsync") zephir("ERR", err_msg, z_proc) raise SystemError(err_msg) def clean_iso_dir(iso_file=None): """Clean ISO directory If :data:`iso_file` is not `None`, it's keept """ # Remove any file other than targeted release ISO # This permit to resume download for filename in glob('{iso_dir}/*'.format(iso_dir=ISO_DIR)): if filename == iso_file: continue unlink(filename) def get_cdrom_device(): device = None client = CreoleClient() mount_point = '/media/cdrom' mounted = False if not isdir(mount_point): mkdir(mount_point) for cdrom in client.get_creole('cdrom_devices'): cmd = ['/bin/mount', cdrom, mount_point, '-o', 'ro'] if system_code(cmd) != 0: continue mounted = True if isdir('{0}/dists'.format(mount_point)): device = cdrom cmd = ['/bin/umount', mount_point] if system_code(cmd) != 0: err_msg = _("Unable to umount {cdrom}").format(cdrom=cdrom) zephir("ERR", err_msg, z_proc) raise SystemError(err_msg) if device is not None: break return device def download_iso(args): """Download ISO image Download an ISO image from internet or copy one from :data:`path`. :parameter path: path of an existing ISO image :type path: `str` """ proxy = None client = CreoleClient() if client.get_creole('activer_proxy_client') == u'oui': address = client.get_creole('proxy_client_adresse') port = client.get_creole('proxy_client_port') proxy = 'http://{address}:{port}'.format(address=address, port=port) release_url = build_release_url(args.release, proxy) iso_name = build_iso_name(args.release) iso_file = join(ISO_DIR, iso_name) iso_url = '{url}/{iso}'.format(url=release_url, iso=iso_name) print_title(_(u"Downloading ISO image for {release}").format(release=args.release)) if not isdir(ISO_DIR): mkdir(ISO_DIR) if isfile(iso_file) and check_iso(iso_name, iso_file, release_url, proxy): # ISO is downloaded and verified return True # Remove SHA265SUMS* files # Keep ISO to resume download if possible clean_iso_dir(iso_file) err_msg = None if args.cdrom or args.iso is not None: if args.cdrom: path = get_cdrom_device() if path is None: err_msg = _("No CDROM found") else: path = args.iso if not isfile(path): err_msg = _("No such file: {iso}").format(iso=path) elif not access(path, R_OK): err_msg = _("Unreadable file: {iso}").format(iso=path) if err_msg is None: # Should we also check source image before copying? # elif not check_iso(iso_name, path, release_url): # raise SystemError("ISO image is not valid for {iso}".format(iso=path)) print _("Copying {source} to {iso}").format(source=path, iso=iso_file) copy(path, iso_file) if not check_iso(iso_name, iso_file, release_url, proxy): # Copy was OK but check fails # Remove copied ISO as it may be corrupted and prevent # futur download clean_iso_dir() msg = _("Error checking ISO after copy, remove {iso}") err_msg = msg.format(iso=iso_file) else: # Try resuming download download_with_wget(iso_file, iso_url, proxy, args.limit_rate) # download_iso_with_zsync(iso_file, iso_url) if not check_iso(iso_name, iso_file, release_url, proxy): # Download was OK but check fails # Remove downloaded ISO as it may be fully downloaded but # corrupted and prevent futur download clean_iso_dir() msg = _(u"Error checking ISO image after download, remove {iso}") err_msg = msg.format(iso=iso_file) if err_msg: zephir('ERR', err_msg, z_proc) raise SystemError(err_msg) def main(): args_parser = ArgumentParser(description=_("EOLE distribution upgrade tool.")) args_parser.add_argument('--release', help=_(u"Target release number")) args_parser.add_argument('--download', action='store_true', help=_(u"Only download the ISO image")) args_parser.add_argument('--iso', metavar=u'PATH', help=_(u"Path to an ISO image")) args_parser.add_argument('--cdrom', action='store_true', help=_(u"Use CDROM device instead of downloading ISO image")) args_parser.add_argument('--limit-rate', metavar=u'BANDWIDTH', default='120k', help=_(u"Pass limit rate to wget. “0” to disable.")) args_parser.add_argument('-f', '--force', action='store_true', help=_(u"Do not ask confirmation")) args = args_parser.parse_args() try: locale.setlocale(locale.LC_ALL, "") except: pass print_red(_(u"This script will upgrade this server to a new release")) print_red(_(u"Modifications will be irreversible.")) init_proxy() zephir("INIT", _(u"Starting Upgrade-Auto ({})").format(" ".join(sys.argv[1:])), z_proc) # Ask for release if none provided on command line if args.release is not None: if args.release not in ALTERNATIVES: msg = _(u"Invalid release {version} use: {values}") err_msg = msg.format(version=args.release, values=', '.join(ALTERNATIVES)) zephir("ERR", err_msg, z_proc) print err_msg sys.exit(1) else: title = _(u"Choose which version you want to upgrade to\n") prompt = _(u"Which version do you want to upgrade to (or 'q' to quit)?") args.release = cli_choice(ALTERNATIVES, prompt, title=title, guess=False) if not args.force: confirmation_msg = _(u"Do you really want to upgrade to version {}?") if question_ouinon(confirmation_msg.format(args.release)) != 'oui': end_msg = _(u'Upgrade cancelled by user') zephir("FIN", end_msg, z_proc) print end_msg sys.exit(0) lock.acquire('upgrade-auto', valid=False, level='system') atexit.register(release_lock) if not args.download: print_title(_("Check update status")) PKGMGR = EolePkg('apt', ignore=False) PKGMGR.set_option('APT::Get::Simulate', 'true') _configure_sources_mirror(PKGMGR.pkgmgr) PKGMGR.update(silent=True) upgrades = PKGMGR.get_upgradable_list() for container, packages in upgrades.items(): if packages: err_msg = _(u"Some packages are not up-to-date!") zephir("ERR", err_msg, z_proc) print_red(err_msg) print_red(_(u"Update this server (Maj-Auto) before another attempt to upgrade")) sys.exit(1) print_green(_(u'Server is up-to-date')) if controle_kernel(): err_msg = _(u"In order to upgrade, most recent kernel endorsed for this release must be used") zephir("ERR", err_msg, z_proc) print_red(err_msg) sys.exit(1) print_green(_(u'This server uses most recent kernel')) download_iso(args) if args.download: end_msg = _(u"Download only detected, stop") print_green(end_msg) zephir("FIN", end_msg, z_proc) sys.exit(0) print_title(_("Copying upgrade scripts")) if isdir(tmp_dir): err_msg = _(u"Directory {0} already exists").format(tmp_dir) zephir("ERR", err_msg, z_proc) print_red(err_msg) sys.exit(1) copytree(UPGRADE_DIR, tmp_dir) pre_download = join(tmp_dir, PRE_DOWNLOAD) print_title(_("Configuring upgrade")) engine = CreoleTemplateEngine() rootctx = {u'name': u'root', u'path': u''} file_info = {'name': '/etc/update-manager/release-upgrades.d/eole.cfg', 'source': join(templatedir, 'eole.cfg'), 'full_name': '/etc/update-manager/release-upgrades.d/eole.cfg', 'activate' : True, 'del_comment': u'', 'mkdir' : False, 'rm' : False} engine.prepare_template(file_info) engine.process(file_info, rootctx) _configure_sources_mirror(PKGMGR.pkgmgr, eole_release=args.release) print_title(_("Module specific commands")) code = system(RUNPARTS_CMD.format(directory=pre_download)) if code != 0: err_msg = _(u'Error {0}').format(pre_download) print_red(err_msg) zephir("ERR", err_msg, z_proc) sys.exit(1) for container in (cont for cont in upgrades if cont != 'root'): upgrade_container_source(container) PKGMGR.dist_upgrade(container=container, silent=False) m = MetaReleaseCore(useDevelopmentRelease=False, useProposed=False) # this will timeout eventually while m.downloading: time.sleep(0.5) progress = apt.progress.text.AcquireProgress() fetcher = EOLEDistUpgradeFetcherCore(new_dist=m.new_dist, progress=progress) fetcher.run_options += ["--mode=server", "--frontend=DistUpgradeViewEOLE", ] print_title(_("Upgrading server")) fetcher.run() # all lines below will not be executed if __name__ == "__main__": try: main() except (KeyboardInterrupt, EOFError): print_red(_("\nUpgrade aborted by user")) exit(0)