Disassembler

Artificial intelligence is no match for natural stupidity.
09září2012

Apt-get --without-annoying-questions


Mám blbuvzdorný pythonovský updatovací skriptík pro deb-based systémy. Jedná se v podstatě o jednoduchý wrapper nad apt-getem s několika málo možnostmi nastavení. Primárně je určen pro bezobslužné aktualizace systémů a pro vypisování zastaralých balíčků v reportech. Nedávno mi bylo nahlášeno, že při jednom updatu balíčku samby se skript nějak rozbil a nedoběhl do konce.

Opakování, matka moudrosti


Problémem bylo, že update samby nahrazoval již existující ručně modifikovaný konfigurační soubor. Apt-get byl sice spouštěn ve „dvojitě tichém“ režimu, implikujícím kladnou odpověď na jakoukoliv otázku, jenže tuhle otázku nepokládal apt-get, ale jím spouštěný dpkg, takže odpověď nebyla pokryta a dotaz na přepsání konfigurace zůstal viset na pozadí. Při zjišťování, jak apt-get přemluvit, aby se při updatu vůbec na nic neptal, jsem ale zjistil, že k plně bezobslužnému režimu je třeba říci „ano“ ve čtyřech různých parametrech! Opravdu user-friendly.

Absolutně bezobslužný upgrade oneliner, který si sám odpoví na všechny svoje dotazy, vypadá následovně

apt-get -qq --force-yes update >/dev/null 2>&1 && DEBIAN_FRONTEND=noninteractive && apt-get -qq --force-yes -o Dpkg::Options::="--force-confold" upgrade >/dev/null 2>&1

Nevím jak vám, ale mě přijde trošičku překombinovaný. Samozřejmě je možno výchozí chování změnit přímo v konfiguračních souborech apt-getu a dpkg, ale taková změna se nedá doporučit, pokud má k systému přístup více uživatelů, kteří očekávají výchozí chování komponent.

Upgrade skript


Můj upgradovací skript všechny čtyři výše uvedené parametry obsahuje a díky tomu umožňuje bezobslužný upgrade systému jediným příkazem.

#!/usr/bin/python

from argparse import ArgumentParser
from datetime import datetime, timedelta
from os import devnull, path, environ
from re import compile
from subprocess import Popen, PIPE
from time import sleep

def main(args):
    # Set some variables
    upgrade_method = 'dist-upgrade' if args.dist else 'upgrade'
    fnull = open(devnull, 'w')

    # Update package info
    last_update_time = path.getmtime('/var/cache/apt')
    last_update_delta = datetime.now() - datetime.fromtimestamp(last_update_time)
    if last_update_delta > timedelta(minutes=10):
        if not args.quiet:
            print '\nUpdating package info...'
        p = Popen(['apt-get', '-qq', '--force-yes', 'update'], stdout=fnull, stderr=fnull)
        p.wait()

    # Get names and versions of upgradable packages
    package_list = []
    regex = compile('Inst (.*) \[(.*)\] \((.*?) .*')
    p = Popen(['apt-get', '-s', upgrade_method], stdout=PIPE, stderr=fnull)
    for line in p.stdout:
        match = regex.match(line)
        if match:
            package_list.append(match.groups())

    # Print packages table
    if len(package_list) == 0:
        if not args.quiet:
            print '\nNo upgradable packages found.\n'
    else:
        if args.list_only or not args.quiet:
            if not args.quiet:
                print
            max_name_len = len(max([a[0] for a in package_list], key=len))
            max_from_len = len(max([a[1] for a in package_list], key=len))
            for package in package_list:
                print package[0].ljust(max_name_len), package[1].ljust(max_from_len), '->', package[2]
            if not args.quiet:
                print

        # Upgrade packages
        if not args.list_only:
            if not args.quiet:
                print 'Upgrading packages...\n'
            environ['DEBIAN_FRONTEND'] = 'noninteractive'
            p = Popen(['apt-get', '-qq', '--force-yes', '-o Dpkg::Options::="--force-confold"', upgrade_method], stdout=fnull, stderr=fnull)
            p.wait()
            if not args.quiet:
                print 'Done.\n'
            # Schedule reboot if requested
            if args.reboot:
                Popen(['shutdown', '-r', args.reboot], stdout=fnull, stderr=fnull)
                sleep(0.1)

    fnull.close()

if __name__ == '__main__':
    parser = ArgumentParser()
    parser.add_argument('-d', '--dist', action='store_true', help='Performs dist-upgrade instead of ordinary upgrade')
    parser.add_argument('-l', '--list-only', action='store_true', help='Just lists upgradable packages. Does not upgrade anything')
    parser.add_argument('-q', '--quiet', action='store_true', help='Hides all output. In conjunction with -l, list will be printed headless')
    parser.add_argument('-r', '--reboot', action='store', help='If some upgrades were installed, reboots machine at given time. Syntax is the same as for shutdown command.')
    args = parser.parse_args()
    main(args)

Parametry se samozřejmě dají kombinovat, takže například ./upgrade.py -dl vypíše seznam updatovatelných balíčků včetně kernelu a dalších věcí, které by se s obyčejným apt-get upgrade neupgradovaly.

root@lts:~# ./upgrade.py -l

libssl1.0.0    1.0.1-4ubuntu5.3 -> 1.0.1-4ubuntu5.5
openssl        1.0.1-4ubuntu5.3 -> 1.0.1-4ubuntu5.5
linux-firmware 1.79             -> 1.79.1
root@lts:~# ./upgrade.py -dl

libssl1.0.0          1.0.1-4ubuntu5.3 -> 1.0.1-4ubuntu5.5
openssl              1.0.1-4ubuntu5.3 -> 1.0.1-4ubuntu5.5
linux-firmware       1.79             -> 1.79.1
linux-headers-server 3.2.0.29.31      -> 3.2.0.30.32
linux-server         3.2.0.29.31      -> 3.2.0.30.32
linux-image-server   3.2.0.29.31      -> 3.2.0.30.32

A ./upgrade.py -dqr 23:30 spustí tichý apt-get dist-upgrade a pokud bude aktualizován alespoň jeden balíček, nastaví restart počítače na 23:30.

root@lts:~# ./upgrade.py -dqr 23:30

Broadcast message from root@lts.dasm.cz
        (/dev/pts/0) at 15:50 ...

The system is going down for reboot in 460 minutes!