lc_ldap/ldap_locks.py

198 lines
6.9 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# LDAP_LOCKS.PY-- Locks for lc_ldap
#
## Copyright (C) 2013 Cr@ns <roots@crans.org>
# Authors:
# * Antoine Durand-Gasselin <adg@crans.org>
# * Pierre-Elliott Bécue <becue@crans.org>
#
# 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 <COPYRIGHT
# HOLDER> 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 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.
"""
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:
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 removelock(self, item, value, Id='default', force=False):
"""
Libère le lock "$item=$value,$LOCKS_DN".
"""
try:
if force or str(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
"""
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 ValueError as e:
self.removelock(item, value, Id, force=True)
raise LockNotFound()