597 lines
19 KiB
Python
Executable File
597 lines
19 KiB
Python
Executable File
#!/usr/bin/python -u
|
|
# -*- coding: utf-8 -*-
|
|
#
|
|
##########################################################################
|
|
# Maj-Auto - Manage automatique update of EOLE server
|
|
# Copyright © 2015 Pôle de compétences EOLE <eole@ac-dijon.fr>
|
|
#
|
|
# 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)
|
|
|