lc_ldap/ldap_locks.py
2013-06-13 20:34:59 +02:00

195 lines
6.7 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 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