#!/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 subprocess import time class LockError(exceptions.StandardError): """ Erreur standard de lock """ pass class LdapLockedByYou(LockError): """ Classe d'erreur pour les locks par le process 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 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): """ On crée la connexion, et on crée un dico vide. """ self.locks = collections.defaultdict(dict, {}) self.host = socket.gethostname() self.pid = os.getpid() self.conn = conn def purge(self, Id=None): """ On essaye de détruire tous les verrous hébergés par l'objet. """ if Id == None: for key, subdict in self.locks['default'].keys(): for item, value in subdict: try: self.removelock(item, value, key) except: pass else: for item, value in self.locks[Id].items(): try: self.removelock(item, value, Id) except: pass def __del__(self): """ En cas de destruction du lockholder """ self.purge() 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: host, pid, begin = self.getlock(item, value) if time.time() - begin >= 600.0: self.removelock(item, value, Id, True) elif 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 un processus actif." % (item, value)) else: self.removelock(item, value, Id, True) else: raise LdapLockedByOther("La donnée %r=%r est lockée depuis une autre machine." % (item, value)) except ldap.NO_SUCH_OBJECT: pass except LockFormatError: self.removelock(item, value, Id) except Exception: raise dn = "%s=%s,%s" % (item, value, LOCKS_DN) lockid = "%s-%s-%s" % (self.host, self.pid, time.time()) modlist = ldap.modlist.addModlist({'objectClass' : 'lock', 'lockid' : lockid, item : value}) try: self.conn.add_s(dn, modlist) self.locks[Id][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 un processus actif." % (item, value)) else: self.removelock(item, value, Id) try: self.conn.add_s(dn, modlist) # Si on a un pointeur vers l'id de l'objet, on en tient compte self.locks[Id][item] = value except: raise StandardError("Quelque chose a planté durant la pose du lock %s=%s" % (item, value)) def removelock(self, item, value, Id='default', force=False): """ Libère le lock "$item=$value,$LOCKS_DN". """ if not force: if self.locks[Id].get(item, "") == str(value): _ = self.locks[Id].pop(item) self.conn.delete_s("%s=%s,%s" % (item, value, LOCKS_DN)) else: pass else: try: self.conn.delete_s("%s=%s,%s" % (item, value, LOCKS_DN)) except: pass def getlock(self, item, value): """ Trouve le lock item=value, et renvoie le contenu de lockinfo via un triplet host, pid, begin """ result = self.conn.search_s('%s=%s,%s' % (item, value, LOCKS_DN), 0) try: try: host, pid, begin = result[0][1]['lockid'][0].split('-') except: host, pid = result[0][1]['lockid'][0].split('-') begin = time.time() return host, int(pid), float(begin) except: raise LockFormatError