scripts/gestion/wifi-update.py
bernat d47441f88f Corrections de bugs
darcs-hash:20041210181420-d1718-986a3ee42c88c314257ce89569bd5a61f3b79173.gz
2004-12-10 19:14:20 +01:00

276 lines
10 KiB
Python
Executable file

#! /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/nectaris.pem')
ctx.use_privatekey_file('/etc/ssl/private/nectaris.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' or ligne == 'ACK':
self.do_RESET()
else:
self.sendError("`%s' is an unknown command for this server." % ligne)
def sendError(self, error):
"""Renvoie une erreur sous forme de script."""
if error.__class__.__name__ != 'str':
error = error.getTraceback()
for ligne in error.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
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 reset_host(self,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()
remove_lock('gen_confs.wifi.conf_wifi')
return result
else:
# Il n'y a rien dans le répertoire retry
result = ""
getre = filter(lambda f: re.match(getre, f), os.listdir("."))
delre = 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 = os.fdopen(tempfile.mkstemp('','','retry')[0],'w')
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)
remove_lock('gen_confs.wifi.conf_wifi')
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))