diff --git a/crans_utils.py b/crans_utils.py index bad7078..480d275 100644 --- a/crans_utils.py +++ b/crans_utils.py @@ -29,11 +29,19 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import calendar, netaddr, re, time, smtplib, sys +import calendar +import netaddr +import re +import time +import smtplib +import sys +import os sys.path.append('/usr/scripts/gestion') import config from unicodedata import normalize +DEVNULL = open(os.devnull, 'w') + def ip4_of_rid(rid): """Convertit un rid en son IP associée""" # Au cas où @@ -180,6 +188,13 @@ def ldap_sanitize(s): try: return replace[c] except KeyError: return c return "".join([conv(c) for c in s]) - - +def process_status(pid): + """ + Vérifie l'état du processus pid + """ + cmd = subprocess.check(['ps', '%s' % pid], stdout=DEVNULL, stderr=subprocess.STDOUT) + if cmd != 0: + return False + else: + return True diff --git a/ldap_locks.py b/ldap_locks.py index c04a1da..24801b3 100644 --- a/ldap_locks.py +++ b/ldap_locks.py @@ -3,8 +3,11 @@ # # LDAP_LOCKS.PY-- Locks for lc_ldap # -## Copyright (C) 2010 Cr@ns -# Author: Antoine Durand-Gasselin +## Copyright (C) 2013 Cr@ns +# Authors: +# * Antoine Durand-Gasselin +# * Pierre-Elliott Bécue +# # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -30,115 +33,126 @@ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import ldap, os +import ldap +import os +import exceptions +import socket +import crans_utils -base_lock = 'ou=lock,dc=crans,dc=org' +class LdapLockedByYou(exceptions.StandardError): + """ + Classe d'erreur pour les locks par le process courant + """ + pass -class CransLock: +class LdapLockedByOther(exceptions.StandardError): + """ + Erreur car le lock est occupé par un autre process. + """ + pass +class LockFormatError(exceptions.StandardError): + """ + L'objet lock qu'on a récupéré n'est pas tel qu'on le voudrait + """ + pass + +LOCKS_DN = 'ou=lock,dc=crans,dc=org' + +class LdapLockHolder: + """ + Système de gestion des locks pour une instance de lc_ldap. + """ def __init__(self, conn): - self.conn = conn + """ + On crée la connexion, et on crée un dico vide. + """ self.locks = {} - self._active = [] + self.host = socket.gethostbyname() + self.pid = os.getpid() + self.conn = conn - def __enter__(self): - self.lock() - - def __exit__(self, *args): - # XXX - connecter correctement les tracebacks. - print "exiting with exception", args - self.release() - return True - - def add(self, item, valeur): - '''rajoute un lock, après avoir vérifié qu'il peut être posé''' - try: - locked = self._islocked(item, valeur) - if locked: - raise EnvironmentError(u'Object déjà locké', locked) - except ldap.NO_SUCH_OBJECT: - pass - - locked_values = self.locks.get(item, []) - if valeur not in locked_values: - locked_values.append(valeur) - self.locks[item] = locked_values - - def remove(self, item, valeur): - '''Enlève un lock''' - self.locks[item].remove(valeur) - - def lock(self): - '''Essaie de prendre tous les verrous''' - items = self.locks.items() - items.sort() - try: - for item, valeurs in items: - for valeur in valeurs: - self._lockitem(item, valeur) - except Exception, e: - # XXX - connecter proprement les traceback - self.release() - raise e - - def release(self): - '''Relâche tous les verrous''' - exceptions = [] - print "releasing", self._active - for item in self._active[:]: + def __del__(self): + """ + On essaye de détruire tous les verrous hébergés par + l'objet mourant. + """ + for item, value in self.locks: try: - self._releaseitem(item) - except Exception, e: - exceptions.append(e) - if len(exceptions) == 1: - # XXX - connecter proprement les tracebacks - raise exceptions[0] - elif len(exceptions) > 1: - raise Exception(exceptions) + self.removelock(item, value) + except: + pass + del self.conn - def _islocked(self, item, valeur): - # XXX - return self.conn.search_s(base_dn, 2, '%s=%s' % (item, valeur)) - return self.conn.search_s('%s=%s,%s' % (item, valeur, base_lock), 0) + def addlock(self, item, value): + """ + Applique un verrou sur "$item=$value,$LOCKS_DN", + si le précédent verrou était géré par la session + courante de lc_ldap, on prévient l'utilisateur + de la session, pour qu'il puisse éventuellement + libérer le lock. - def _lockitem(self, item, valeur): - u""" - Lock un item avec la valeur valeur, les items possibles - peuvent être : - aid $ chbre $ mail $ mailAlias $ canonicalAlias $ - mid $ macAddress $ host $ hostAlias $ ipHostNumber - Retourne le dn du lock + Sinon, on ne peut pas override le lock, et on laisse + tomber. """ - valeur = valeur.encode('utf-8') - - lock_dn = '%s=%s,%s' % (item, valeur, base_lock) - lockid = '%s-%s' % ('localhost', os.getpid()) - modlist = ldap.modlist.addModlist({ 'objectClass': 'lock', - 'lockid': lockid, - item: valeur }) - print "locking", lock_dn try: - self.conn.add_s(lock_dn, modlist) - except ldap.ALREADY_EXISTS: -# # Pas de chance, le lock est déja pris -# try: -# res = self.conn.search_s(lock_dn, 2, 'objectClass=lock')[0] -# l = res[1]['lockid'][0] -# except: l = '%s-1' % hostname -# if l != lockid: -# # C'est locké par un autre process que le notre -# # il tourne encore ? -# if l.split('-')[0] == hostname and os.system('ps %s > /dev/null 2>&1' % l.split('-')[1] ): -# # Il ne tourne plus -# self._releaseitem(res[0]) # delock -# return self._lockitem(item, valeur) # relock - raise EnvironmentError(u'Objet (%s=%s) locké, patienter.' % (item, valeur), lockid) - self._active.append(lock_dn) - return lock_dn + host, pid = self.getlock(item, value) + if host == self.host and pid == self.pid(): + raise LdapLockedByYou("La donnée %r=%r est lockée par vous-même." % (item, value)) + elif host == self.host: + status = crans_utils.process_status(pid) + if status: + raise LdapLockedByOther("La donnée %r=%r est lockée par quelqu'un d'autre ou un processus." % (item, value)) + else: + self.removelock(item, value) + else: + raise LdapLockedByOther("La donnée %r=%r est lockée par quelqu'un d'autre ou un processus." % (item, value)) + except ldap.NO_SUCH_OBJECT: + pass + except LockFormatError: + self.removelock(item, value) - def _releaseitem(self, lockdn): - u"""Destruction d'un lock""" - # Mettre des verifs ? - print "releasing", lockdn - self._active.remove(lockdn) - self.conn.delete_s(lockdn) + dn = "%s=%s,%s" % (item, value, LOCKS_DN) + lockid = "%s-%s" % (self.host, self.pid) + modlist = ldap.modlist.addModlist({'objectClass' : 'lock', + 'lockid' : lockid, + item : value}) + + try: + self.conn.add_s(dn, modlist) + self.locks[item] = value + except ldap.ALREADY_EXISTS: + status = crans_utils.process_status(pid) + if status: + raise LdapLockedByOther("La donnée %r=%r est lockée par quelqu'un d'autre ou un processus." % (item, value)) + else: + self.removelock(item, value) + try: + self.conn.add_s(dn, modlist) + self.locks[item] = value + except: + raise StandardError("Quelque chose a planté durant la pose du lock %s=%s" % (item, value)) + + def removelock(self, item, value): + """ + Libère le lock "$item=$value,$LOCKS_DN". + """ + print "Deleting %s=%s,%s\n" % (item, value, LOCKS_DN) + if self.locks.get(item, "") == value: + self.locks.pop(item) + self.conn.delete_s("%s=%s,%s" % (item, value, LOCKS_DN)) + else: + print "Lock %s=%s,%s does not exist on this session" % (item, value, LOCKS_DN) + + def getlock(self, item, value): + """ + Trouve le lock item=value, et renvoie le contenu de lockinfo + via un couple host, pid + """ + + result = self.conn.search_s('%s=%s,%s' % (item, value, LOCKS_DN), 0) + try: + return result[0][1]['lockid'][0].split('-') + except: + raise LockFormatError