ldap_locks.py 7.75 KB
Newer Older
1 2 3 4 5
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# LDAP_LOCKS.PY-- Locks for lc_ldap
#
6 7
## Copyright (C) 2013 Cr@ns <roots@crans.org>
# Authors:
8
#    * Antoine Durand-Gasselin <adg@crans.org>
9 10
#    * Pierre-Elliott Bécue <becue@crans.org>
#
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
# 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.

36 37 38 39 40
import ldap
import os
import exceptions
import socket
import crans_utils
41
import collections
42
import time
43

44 45 46 47 48 49
class LockError(exceptions.StandardError):
    """
    Erreur standard de lock
    """
    pass

50 51 52 53 54 55
class LockExpired(LockError):
    """
    Classe d'erreur pour les locks non libéré avant
    la durée d'expiration du lock
    """
    pass
56
class LdapLockedByYou(LockError):
57 58 59 60 61
    """
    Classe d'erreur pour les locks par le process courant
    """
    pass

62 63 64 65 66 67
class LdapLockedByMySelf(LockError):
    """
    Classe d'erreur pour les locks par l'idlock courant
    """
    pass

68
class LdapLockedByOther(LockError):
69 70 71 72 73
    """
    Erreur car le lock est occupé par un autre process.
    """
    pass

74
class LockFormatError(LockError):
75 76 77 78 79
    """
    L'objet lock qu'on a récupéré n'est pas tel qu'on le voudrait
    """
    pass

80 81 82
class LockNotFound(LockError):
    """Le lock n'a pas été trouvé"""

83 84 85 86 87 88
LOCKS_DN = 'ou=lock,dc=crans,dc=org'

class LdapLockHolder:
    """
    Système de gestion des locks pour une instance de lc_ldap.
    """
89
    __slots__ = ("locks", "host", "pid", "conn", "timeout")
90
    def __init__(self, conn):
91 92 93
        """
        On crée la connexion, et on crée un dico vide.
        """
94 95
        # On crée un Id => (item => set())
        self.locks = collections.defaultdict(lambda:collections.defaultdict(set))
96
        self.host = socket.gethostname()
97 98
        self.pid = os.getpid()
        self.conn = conn
99
        self.timeout = 600.0
100

101 102 103 104 105 106 107 108
    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):
109 110
        """
        On essaye de détruire tous les verrous hébergés par
111
        l'objet.
112
        """
113 114 115
        if not purgeAll:
            for item, values in self.locks[Id].items():
                for value in values.copy():
116
                    self.removelock(item, value, Id)
117 118 119
        else:
            for Id in self.locks:
                self.purge(Id)
120

121 122 123 124
    def __del__(self):
        """
        En cas de destruction du lockholder
        """
125
        self.purge(purgeAll=True)
126 127

    def addlock(self, item, value, Id='default'):
128 129 130 131 132 133 134 135 136 137
        """
        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.
        """
138
        try:
139
            value = str(value)
140
            host, pid, begin = self.getlock(item, value)
141 142
            time_left = self.timeout - (time.time() - begin)
            if time_left <= 0:
143
                self.removelock(item, value, Id, True)
144 145
            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))
146
            elif host == self.host and pid == self.pid:
147
                raise LdapLockedByYou("La donnée %r=%r est lockée par vous-même pour encore %ds." % (item, value, time_left))
148 149 150
            elif host == self.host:
                status = crans_utils.process_status(pid)
                if status:
151
                    raise LdapLockedByOther("La donnée %r=%r est lockée par un processus actif pour encore %ds." % (item, value, time_left))
152
                else:
153
                    self.removelock(item, value, Id, True)
154
            else:
155
                raise LdapLockedByOther("La donnée %r=%r est lockée depuis une autre machine pour encore %ds." % (item, value, time_left))
156
        except LockNotFound:
157 158
            pass

159
        dn = "%s=%s,%s" % (item, value, LOCKS_DN)
160
        lockid = "%s-%s-%s" % (self.host, self.pid, time.time())
161
        lockv = "lc_ldap_201308"
162 163
        modlist = ldap.modlist.addModlist({'objectClass' : 'lock',
                                           'lockid' : lockid,
164
                                           'lockv' : lockv,
165
                                           item : value})
166 167

        try:
168
            self.conn.add_s(dn, modlist)
169
            self.locks[Id][item].add(str(value))
170
        except ldap.ALREADY_EXISTS:
171 172 173
            # 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)
174

175 176
    def check(self, Id='default', delai=0):
        """Vérifie que l'on a toujours tous nos locks"""
177 178 179 180 181 182
        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, time_left))
183

184
    def removelock(self, item, value, Id='default', force=False):
185 186 187
        """
        Libère le lock "$item=$value,$LOCKS_DN".
        """
188
        value = str(value)
189
        try:
190
            if force or value in self.locks[Id][item]:
191
                self.conn.delete_s("%s=%s,%s" % (item, value, LOCKS_DN))
192 193 194
        except ldap.NO_SUCH_OBJECT:
            pass
        finally:
195
            try:
196 197
                self.locks[Id][item].remove(value)
            except KeyError:
198
                pass
199 200
            if not self.locks[Id][item]:
                self.locks[Id].pop(item)
201

202
    def getlock(self, item, value):
203 204
        """
        Trouve le lock item=value, et renvoie le contenu de lockinfo
205
        via un triplet host, pid, begin
206
        """
207
        value = str(value)
208
        try:
209 210
            result = self.conn.search_s('%s=%s,%s' % (item, value, LOCKS_DN), 0)
            host, pid, begin = result[0][1]['lockid'][0].split('-')
211
            return host, int(pid), float(begin)
212 213
        except ldap.NO_SUCH_OBJECT:
            raise LockNotFound()
214 215 216
        except ldap.INVALID_DN_SYNTAX:
            print '%s=%s,%s' % (item, value, LOCKS_DN)
            raise
217 218 219
        except ValueError as e:
            self.removelock(item, value, Id, force=True)
            raise LockNotFound()