diff --git a/gestion/wifi-update.py b/gestion/wifi-update.py new file mode 100755 index 00000000..395dba4d --- /dev/null +++ b/gestion/wifi-update.py @@ -0,0 +1,272 @@ +#! /usr/bin/env python +# -*- coding: iso-8859-15 -*- + +# Ce script est un serveur SSL permettant aux bornes d'exécuter +# certains scripts. + +# Les scripts sont découpés en deux catégories : +# - ceux exécutés au démarrage de la borne +# - ceux à exécuter au plus tôt (pour une mise à jour) + +# Ils se trouvent dans /etc/wifi/wifi-update (ou dans le contenu de la +# variable ROOT). Ce répertoire contient plusieurs sous-répertoires. Chaque +# sous-répertoire correspond et est nommé selon le nom de la borne +# (ex : valhalla.wifi.crans.org). + +# Dans chacun de ces répertoires, il doit y avoir des scripts qui commencent +# par 0 et des scripts qui commencent par un autre chiffre. Les scripts +# qui commencent par 0 sont ceux exécutés au démarrage de la borne et les autres +# sont ceux devant être mis à jour. + +# Comme un certain nombre de bornes partagent les même scripts, ceux-ci pourront +# être des liens symboliques. Enfin, si une borne n'est pas présente dans +# l'arborescence, on utilise le répertoire "unknown". + +# Le serveur accepte trois commandes : +# BOOT qui permet d'obtenir le script de boot +# UPDATE qui permet d'avoir le script de mise à jour +# RESET qui permet de dire que l'on a bien reçu le script de mise à jour +# et qu'il peut donc être effacé sur le serveur + +# La transmission d'un script se termine par un "." isolé sur une ligne. + +# La création des scripts et leur gestion est laissée à un programme externe. + +# Lancement : twistd -n -y wifi-update.py --pidfile=/var/run/wifi-update.pid +# Pas de -n pour qu'il passe en fond + +# TODO: meilleure gestion des erreurs + +# On utilise twisted +# http://twistedmatrix.com/documents/current/howto/tutorial/intro +from twisted.internet import protocol, reactor, defer, utils +from twisted.internet.ssl import ContextFactory +from twisted.protocols import basic +from twisted.application import internet, service +from twisted.names import client +from twisted.python import log + +import sys +sys.path.append('/usr/scripts/gestion') + +# LDAP +from ldap_crans import crans_ldap +from lock import * + +# Divers +from iptools import AddrInNet +from OpenSSL import SSL +import re, errno, tempfile +from stat import * + +class ServerContextFactory(ContextFactory): + + def getContext(self): + """Création d'un contexte SSL côté serveur.""" + ctx = SSL.Context(SSL.SSLv23_METHOD) + ctx.use_certificate_file('/etc/ssl/certs/zamok.pem') + ctx.use_privatekey_file('/etc/ssl/private/zamok.pem') + return ctx + +class UpdateProtocol(basic.LineReceiver): + """Protocole de communication pour mettre à jour une borne wifi. + + Trois commandes sont possibles : + BOOT qui permet d'obtenir le script de boot + UPDATE qui permet d'obtenir le script de mise à jour + RESET qui permet de confirmer la bonne réception de ce dernier + """ + def lineReceived(self, ligne): + ligne = ligne.strip() + if ligne == 'BOOT': + self.do_BOOT() + elif ligne == 'UPDATE': + self.do_UPDATE() + elif ligne == 'RESET': + self.do_ACKNOWLEDGE() + else: + self.sendError("`%s' is an unknown command for this server." % ligne) + + def sendError(self, error): + """Renvoie une erreur sous forme de script.""" + for ligne in error.getTraceback().split("\n"): + self.sendLine("# %s" % ligne) + self.sendLine(".") + # Le plus simple est de couper la connexion + self.transport.loseConnection() + # On loggue aussi niveau serveur + print error.getTraceback() + + def do_BOOT(self): + """Répond à une commande de type BOOT""" + d = self.factory.getBootScripts(self.transport.getPeer().host) + d.addCallback(self.sendScript) + d.addErrback(self.sendError) + + def do_UPDATE(self): + """Répond à une commande de type UPDATE""" + d = self.factory.getUpdateScripts(self.transport.getPeer().host) + d.addCallback(self.sendScript) + d.addErrback(self.sendError) + + def do_RESET(self): + """Répond à une commande de type RESET""" + d = self.factory.reset(self.transport.getPeer().host) + d.addCallback(self.sendScript) + d.addErrback(self.sendError) + + def sendScript(self, script): + """Renvoie un script arbitraire.""" + if len(script) > 0: + for ligne in script.split("\n"): + if ligne != ".": + self.sendLine(ligne) + else: + self.sendLine("..") + self.sendLine(".") + +class UpdateFactory(protocol.ServerFactory): + """Backend du serveur. Fournit les scripts.""" + protocol = UpdateProtocol + + BASE = "/etc/wifi/wifi-update" + + def reset(self, host): + """Efface le contenu éventuel du répertoire retry. + + Il n'y a pas de lock mis en place pour cette opération : + aucun processus externe n'est censé écrire là-dedans.""" + def reset_host(self, host): + # ETAPE 1 + # On commence par résoudre "host". + d = client.lookupPointer("%s.in-addr.arpa" % '.'.join(host.split('.')[::-1])) + d.addCallback(lambda (ans, auth, add) : reset_del(self, ans[0].payload.name), + lambda _: reset_del(self, "unknown")) + return d + + def reset_del(self, host): + # ETAPE 2 + # On efface + try: + os.chdir('%s/%s' % (self.BASE, host)) + except OSError, e: + if e.errno != errno.ENOENT or host == "unknown": raise + os.chdir('%s/%s' % (self.BASE, "unknown")) + # On regarde si le répertoire retry existe + if not os.path.isdir('retry'): + os.mkdir('retry') + for f in os.listdir('retry'): + try: + os.remove('retry/%s', f) + except OSError: + pass + return "" # Aucun script à retourner + + return self.reset_host(host) + + def getBootScripts(self, host): + """Retourne les scripts de boot. + + Les scripts commençant par 0 sont sélectionnés. Lorsque l'on + accuse réception de la commande, ce seront les scripts de mise + à jour présents lors de la commande qui seront effacés car + ils sont considérés comme inclus dans les scripts de démarrage. + """ + return self.getScriptsAndDelete("^0.*[^~]$", "^[1-9].*[^~]$", host) + + def getUpdateScripts(self, host): + """Retourne les scripts de mise à jour.""" + return self.getScriptsAndDelete("^[1-9].*[^~]$", "^[1-9].*[^~]$", host) + + def getScriptsAndDelete(self, getre, delre, host): + """Retourne un ensemble de scripts et enregistre à la suppression un autre ensemble. + + Les scripts correspondant à l'IP `host' et à l'expression + régulière `getre' sont concatanés dans l'ordre et renvoyé + et ceux correspondant à l'expression régulière `delre' sont + marqués à la suppression. + + En pratique, c'est un peu plus compliqué. Les scripts communs + à delre et getre sont placés dans un sous-répertoire retry, + ceux qui sont uniquement dans delre sont effacés. + + Si le répertoire retry n'est pas vide, son contenu est envoyé + et rien d'autre n'est fait. + """ + def getSAD_host(self, getre, delre, host): + # ETAPE 1 + # On commence par résoudre "host". + d = client.lookupPointer("%s.in-addr.arpa" % '.'.join(host.split('.')[::-1])) + d.addCallback(lambda (ans, auth, add), _ : getSAD_lock(self, getre, delre, + ans[0].payload.name), + lambda _: getSAD_lock(self, getre, delre, "unknown")) + return d + + def getSAD_lock(self, getre, delre, host): + # ETAPE 2 + # On essaie d'obtenir le lock + def delLockAndRaise(f): + remove_lock('gen_confs.wifi.conf_wifi') + return f + + d = wait_lock('gen_confs.wifi.conf_wifi', 'locked by wifi-update') + d.addCallback(lambda _: getSAD_script(self, getre, delre, host)) + d.addErrback(delLockAndRaise) + return d + + def getSAD_script(self, getre, delre, host): + # ETAPE 3 + # On fait le reste du boulot et on retourne le script à exécuter + try: + os.chdir('%s/%s' % (self.BASE, host)) + except OSError, e: + if e.errno != errno.ENOENT or host == "unknown": raise + os.chdir('%s/%s' % (self.BASE, "unknown")) + # On regarde si le répertoire retry existe + if not os.path.isdir('retry'): + os.mkdir('retry') + if os.listdir('retry'): + # Le répertoire retry n'est pas vide + result = "" + # On va concaténer ces fichiers et en renvoyer le contenu + # Il ne devrait n'y en avoir qu'un, mais... + for f in os.listdir('retry'): + result = result + file('retry/%s' % f).read() + return result + else: + # Il n'y a rien dans le répertoire retry + result = "" + delre = filter(lambda f: re.match(getre, f), os.listdir(".")) + getre = filter(lambda f: re.match(delre, f), os.listdir(".")) + # On s'occupe de mettre ce qu'il faut dans retry + both = filter(lambda f: f in delre, getre) + both.sort() + for f in both: + result = result + file(f).read() + # On écrit result dans un fichier temporaire dans retry + retry = tempfile.mkstemp('','','retry')[0] + retry.write(result) + del result + # On s'occupe de construire le résultat + result = "" + getre.sort() + for f in getre: + result = result + file(f).read() + # On efface ce qui correspond à delre + for f in delre: + os.remove(f) + + return result + + + return getSAD_host(self, getre, delre, host) + + + +# Corps du programme +# On écoute sur le port 9999 +application = service.Application('wifi-update') +factory = UpdateFactory() +server = internet.SSLServer(9999, factory, + ServerContextFactory()) +server.setServiceParent(service.IServiceCollection(application))