#!/usr/bin/env python # -*- coding: utf-8 -*- # # LDAP_LOCKS.PY-- Locks for lc_ldap # ## 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 # modification, are permitted provided that the following conditions are met: # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the Cr@ns nor the names of its contributors may # be used to endorse or promote products derived from this software # without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (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 ldap import os import exceptions import socket import crans_utils import collections import time class LockError(exceptions.StandardError): """ Erreur standard de lock """ pass class LockExpired(LockError): """ Classe d'erreur pour les locks non libéré avant la durée d'expiration du lock """ pass class LdapLockedByYou(LockError): """ Classe d'erreur pour les locks par le process courant """ pass class LdapLockedByMySelf(LockError): """ Classe d'erreur pour les locks par l'idlock courant """ pass class LdapLockedByOther(LockError): """ Erreur car le lock est occupé par un autre process. """ pass class LockFormatError(LockError): """ L'objet lock qu'on a récupéré n'est pas tel qu'on le voudrait """ pass class LockNotFound(LockError): """Le lock n'a pas été trouvé""" LOCKS_DN = 'ou=lock,dc=crans,dc=org' class LdapLockHolder: """ Système de gestion des locks pour une instance de lc_ldap. """ __slots__ = ("locks", "host", "pid", "conn", "timeout") def __init__(self, conn): """ On crée la connexion, et on crée un dico vide. """ # On crée un Id => (item => set()) self.locks = collections.defaultdict(lambda:collections.defaultdict(set)) self.host = socket.gethostname() self.pid = os.getpid() self.conn = conn self.timeout = 600.0 def newid(self): id = 'id_%s' % time.time() if id in self.locks: return self.newid() else: return id def purge(self, Id, purgeAll=False): """ On essaye de détruire tous les verrous hébergés par l'objet. """ if not purgeAll: for item, values in self.locks[Id].items(): for value in values.copy(): self.removelock(item, value, Id) else: for Id in self.locks: self.purge(Id) def __del__(self): """ En cas de destruction du lockholder """ self.purge(purgeAll=True) def addlock(self, item, value, Id='default'): """ 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. Sinon, on ne peut pas override le lock, et on laisse tomber. """ try: value = str(value) host, pid, begin = self.getlock(item, value) time_left = self.timeout - (time.time() - begin) if time_left <= 0: self.removelock(item, value, Id, True) elif Id!='default' and str(value) in self.locks[Id][item]: raise LdapLockedByMySelf("La donnée %r=%r est lockée par vous-même pour encore %ds." % (item, value, time_left)) elif host == self.host and pid == self.pid: raise LdapLockedByYou("La donnée %r=%r est lockée par vous-même pour encore %ds." % (item, value, time_left)) elif host == self.host: status = crans_utils.process_status(pid) if status: raise LdapLockedByOther("La donnée %r=%r est lockée par un processus actif pour encore %ds." % (item, value, time_left)) else: self.removelock(item, value, Id, True) else: raise LdapLockedByOther("La donnée %r=%r est lockée depuis une autre machine pour encore %ds." % (item, value, time_left)) except LockNotFound: pass dn = "%s=%s,%s" % (item, value, LOCKS_DN) lockid = "%s-%s-%s" % (self.host, self.pid, time.time()) lockv = "lc_ldap_201308" modlist = ldap.modlist.addModlist({'objectClass' : 'lock', 'lockid' : lockid, 'lockv' : lockv, item : value}) try: self.conn.add_s(dn, modlist) self.locks[Id][item].add(str(value)) except ldap.ALREADY_EXISTS: # Quelqu'un à eu le lock avant nous, on réessaye # S'il a été libéré, banzai, sinon, ça lèvera une exception return self.addlock(item, value, Id) def check(self, Id='default', delai=0): """Vérifie que l'on a toujours tous nos locks""" for item, values in self.locks[Id].items(): for value in values: host, pid, begin = self.getlock(item, value) time_left = self.timeout - (time.time() - begin) if time_left <= delai: raise LockExpired("Le lock sur la donnée %r=%r à expiré" % (item, value)) def removelock(self, item, value, Id='default', force=False): """ Libère le lock "$item=$value,$LOCKS_DN". """ value = str(value) try: if force or value in self.locks[Id][item]: self.conn.delete_s("%s=%s,%s" % (item, value, LOCKS_DN)) except ldap.NO_SUCH_OBJECT: pass finally: try: self.locks[Id][item].remove(value) except KeyError: pass if not self.locks[Id][item]: self.locks[Id].pop(item) def getlock(self, item, value): """ Trouve le lock item=value, et renvoie le contenu de lockinfo via un triplet host, pid, begin """ value = str(value) try: result = self.conn.search_s('%s=%s,%s' % (item, value, LOCKS_DN), 0) host, pid, begin = result[0][1]['lockid'][0].split('-') return host, int(pid), float(begin) except ldap.NO_SUCH_OBJECT: raise LockNotFound() except ldap.INVALID_DN_SYNTAX: print '%s=%s,%s' % (item, value, LOCKS_DN) raise