From a57f02d0b2c0b51b01a5a5778cb3b92caf0cbe7b Mon Sep 17 00:00:00 2001 From: Antoine Durand-gasselin Date: Sat, 3 Jul 2010 17:50:10 +0200 Subject: [PATCH] =?UTF-8?q?[lc=5Fldap]=20plus=20de=20propret=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lc_ldap.py | 335 ++++++++++++++++++++------------------------------ ldap_locks.py | 144 ++++++++++++++++++++++ 2 files changed, 279 insertions(+), 200 deletions(-) create mode 100644 ldap_locks.py diff --git a/lc_ldap.py b/lc_ldap.py index b128e53..3a5d296 100644 --- a/lc_ldap.py +++ b/lc_ldap.py @@ -18,25 +18,28 @@ # 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 +# 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. from __future__ import with_statement -import os, sys, ldap, ldap.modlist, re, netaddr, datetime, copy, time +import os, sys, ldap, re, netaddr, datetime, copy, time +from ldap.modlist import addModlist, modifyModlist sys.path.append('/usr/scripts/gestion') + import config, crans_utils +from ldap_locks import CransLock uri = 'ldapi:///' #'ldap://ldap.adm.crans.org/' base_dn = 'ou=data,dc=crans,dc=org' -base_lock = 'ou=lock,dc=crans,dc=org' def is_actif(sanction): """Retourne True ou False suivant si la sanction fournie (chaîne @@ -57,14 +60,14 @@ def uldif_to_ldif(uldif): utf-8, renvoie un ldif""" ldif = {} for attr, vals in uldif.items(): - ldif[attr] = [ unicode.encode(val, 'utf-8') for val in uldif[attr] ] + ldif[attr] = [ unicode.encode(val, 'utf-8') for val in vals ] return ldif def ldif_to_uldif(ldif): 'Prend en argument un dico ldif, et décode toutes les chaînes en utf-8' uldif = {} for attr, vals in ldif.items(): - uldif[attr] = [ unicode(val, 'utf-8') for val in ldif[attr] ] + uldif[attr] = [ unicode(val, 'utf-8') for val in vals ] return uldif class lc_ldap(ldap.ldapobject.LDAPObject): @@ -86,9 +89,9 @@ class lc_ldap(ldap.ldapobject.LDAPObject): self.simple_bind_s(base_dn) res = self.search_s('uid=%s' % user) if len(res) < 1: - raise ldap.INVALID_CREDENTIALS({'desc': 'No such user: %s' %s }) + raise ldap.INVALID_CREDENTIALS({'desc': 'No such user: %s' % user }) elif len(res) > 1: - raise ldap.INVALID_CREDENTIALS({'desc': 'Too many matches: uid=%s' %s }) + raise ldap.INVALID_CREDENTIALS({'desc': 'Too many matches: uid=%s' % user }) else: dn = res[0][0] if dn: @@ -143,9 +146,9 @@ class lc_ldap(ldap.ldapobject.LDAPObject): lock.add(item, val) uldif['historique'] = [ self._hist('Création')] ldif = uldif_to_ldif(uldif) - modlist = ldap.modlist.addModlist(ldif) + modlist = addModlist(ldif) with lock: - print dn, modlist + # print dn, modlist self.add_s(dn, modlist) return CransLdapObject(self, dn, mode='w') @@ -173,114 +176,6 @@ class lc_ldap(ldap.ldapobject.LDAPObject): # ? def reconnect(self, conn=None): -class CransLock: - - def __init__(self, conn): - self.conn = conn - self.locks = {} - self._active = [] - - def __enter__(self): - self.lock() - - def __exit__(self, *args): - # XXX - connecter correctement les tracebacks. - print "exiting with exception", args - self.release() - return True - - def add(self, item, valeur): - '''rajoute un lock, après avoir vérifié qu'il peut être posé''' - try: - locked = self._islocked(item, valeur) - if locked: - raise EnvironmentError(u'Object déjà locké', locked) - except ldap.NO_SUCH_OBJECT: - pass - - locked_values = self.locks.get(item, []) - if valeur not in locked_values: - locked_values.append(valeur) - self.locks[item] = locked_values - - def remove(self, item, valeur): - '''Enlève un lock''' - self.locks[item].remove(valeur) - - def lock(self): - '''Essaie de prendre tous les verrous''' - items = self.locks.items() - items.sort() - try: - for item, valeurs in items: - for valeur in valeurs: - self._lockitem(item, valeur) - except Exception, e: - # XXX - connecter proprement les traceback - self.release() - raise e - - def release(self): - '''Relâche tous les verrous''' - exceptions = [] - print "releasing", self._active - for item in self._active[:]: - try: - self._releaseitem(item) - except Exception, e: - exceptions.append(e) - if len(exceptions) == 1: - # XXX - connecter proprement les tracebacks - raise exceptions[0] - elif len(exceptions) > 1: - raise Exception(exceptions) - - def _islocked(self, item, valeur): - # XXX - return self.conn.search_s(base_dn, 2, '%s=%s' % (item, valeur)) - return self.conn.search_s('%s=%s,%s' % (item, valeur, base_lock), 0) - - def _lockitem(self, item, valeur): - u""" - Lock un item avec la valeur valeur, les items possibles - peuvent être : - aid $ chbre $ mail $ mailAlias $ canonicalAlias $ - mid $ macAddress $ host $ hostAlias $ ipHostNumber - Retourne le dn du lock - """ - - valeur = valeur.encode('utf-8') - - lock_dn = '%s=%s,%s' % (item, valeur, base_lock) - lockid = '%s-%s' % ('localhost', os.getpid()) - modlist = ldap.modlist.addModlist({ 'objectClass': 'lock', - 'lockid': lockid, - item: valeur }) - print "locking", lock_dn - try: - self.conn.add_s(lock_dn, modlist) - except ldap.ALREADY_EXISTS: -# # Pas de chance, le lock est déja pris -# try: -# res = self.conn.search_s(lock_dn, 2, 'objectClass=lock')[0] -# l = res[1]['lockid'][0] -# except: l = '%s-1' % hostname -# if l != lockid: -# # C'est locké par un autre process que le notre -# # il tourne encore ? -# if l.split('-')[0] == hostname and os.system('ps %s > /dev/null 2>&1' % l.split('-')[1] ): -# # Il ne tourne plus -# self._releaseitem(res[0]) # delock -# return self._lockitem(item, valeur) # relock - raise EnvironmentError(u'Objet (%s=%s) locké, patienter.' % (item, valeur), l) - self._active.append(lock_dn) - return lock_dn - - def _releaseitem(self, lockdn): - u"""Destruction d'un lock""" - # Mettre des verifs ? - print "releasing", lockdn - self._active.remove(lockdn) - self.conn.delete_s(lockdn) class CransLdapObject: mode = 'ro' @@ -288,7 +183,7 @@ class CransLdapObject: attrs = None # Contient un dico uldif qui doit représenter ce qui # est dans la base - _modifs = None # C'est là qu'on met les modifications + # _modifs = None # C'est là qu'on met les modifications def __init__(self, conn, dn, mode='ro', ldif = None): '''Créé une instance d'un objet Crans (machine, adhérent, @@ -314,46 +209,131 @@ class CransLdapObject: self.dn, self.attrs = res[0] self.attrs = ldif_to_uldif(self.attrs) self.__class__ = eval(self.attrs['objectClass'][0]) - self._modifs = copy.deepcopy(self.attrs) + # self._modifs = copy.deepcopy(self.attrs) - def save(self): - "Enregistre les modifications" - if self.mode != 'w': - raise EnvironmentError(u"Objet en lecture seule, réessayer en lecture/écriture") + # def save(self): + # "Enregistre les modifications" + # if self.mode != 'w': + # raise EnvironmentError(u"Objet en lecture seule, réessayer en lecture/écriture") + # + # # Vérifications et Historique + # histo = self._gen_hist(self._modifs) + # self._modifs['historique'] += histo + # + # # unicode -> utf-8 + # ldif = uldif_to_ldif(self._modifs) + # orig_ldif = uldif_to_ldif(self.attrs) + # + # # modifications + # modlist = modifyModlist(orig_ldif, ldif) + # self.conn.modify_s(self.dn, modlist) + # + # # Vérification des modifications + # self.attrs = ldif_to_uldif(self.conn.search_s(self.dn, 0)[0][1]) + # if self.attrs != self._modifs: + # raise EnvironmentError(u"Les modifications apportées à l'objet %s n'ont pas été correctement sauvegardées\nexpected = %s, found = %s" % (self.dn, self._modifs, self.attrs)) - # Vérifications et Historique - histo = self._gen_hist(self._modifs) - self._modifs['historique'] += histo + def get_values(self, attr): + """Renvoie les valeurs d'un attribut ldap de l'objet self""" + attrs = self.attrs.get(attr, []) + return attrs - # unicode -> utf-8 - ldif = uldif_to_ldif(self._modifs) - orig_ldif = uldif_to_ldif(self.attrs) + def get_value(self, attr): + """Renvoie la première valeur d'un attribut ldap de l'objet self""" + return self.get_values(attr)[0] - # modifications - modlist = ldap.modlist.modifyModlist(orig_ldif, ldif) + def set_ldapattr(self, attr, new_vals): + """Définit les nouvelles valeurs d'un attribut""" + if not isinstance(new_vals, list): + new_vals = [new_vals] + for val in new_vals: assert isinstance(val, unicode) + + # On vérifie les nouvelles valeurs données à l'attribut + self.check_vals(attr, new_vals) + + # Si ça passe, on effectue les modifications + old_vals = self.attrs.get(attr, []) + modlist = modifyModlist({attr : old_vals}, {attr : new_vals}) self.conn.modify_s(self.dn, modlist) - # Vérification des modifications - self.attrs = ldif_to_uldif(self.conn.search_s(self.dn, 0)[0][1]) - if self.attrs != self._modifs: - raise EnvironmentError(u"Les modifications apportées à l'objet %s n'ont pas été correctement sauvegardées\nexpected = %s, found = %s" % (self.dn, self._modifs, self.attrs)) + def mod_ldapattr(self, attr, new_val, old_val = None): + """Modifie l'attribut attr ayant la valeur oldVal en newVal. Si + l'attribut attr n'a qu'une seule valeur, il n'est pas nécessaire + de préciser oldVal.""" + assert isinstance(new_val, unicode) + new_vals = self.attrs.get(attr, [])[:] + if old_val: # and oldVal in attrs: + new_vals.remove(old_val) + new_vals.append(new_val) + elif len(new_vals) == 1: + new_vals = [ new_val ] + else: + raise ValueError(u"%s has multiple values, must specify old_val") + return self.set_ldapattr(attr, new_vals) + + def del_ldapattr(self, attr, val): + """Supprime la valeur val de l'attribut attr""" + new_vals = self.attrs.get(attr, [])[:] + new_vals.remove(val) + return self.set_ldapattr(attr, new_vals) + + def add_ldapattr(self, attr, new_val): + """Rajoute la valeur val à l'attribut attr""" + assert isinstance(new_val, unicode) + new_vals = self.attrs.get(attr, [])[:] + new_vals.append(new_val) + return self.set_ldapattr(attr, new_vals) + + def check_vals(self, attr, vals): + """Vérifie que attr peut se voir attribuer les valeurs vals""" + self.check_cardinality(attr, vals) + self.check_type(attr, vals) + self.check_uniqueness(attr, vals) + self.check_values(attr, vals) + self.check_users_restrictions(attr, vals) + + def check_cardinality(self, attr, vals): + """Vérifie qu'il y a un nombre correct de valeur =1, <=1, {0,1}, + etc...""" + if attr in self.ufields: + if len(vals) != 1: + raise ValueError('%s doit avoir exactement une valeur' % attr) + + if attr in self.ofields: + if len(vals) > 1: + raise ValueError('%s doit avoir au maximum une valeur' % attr) + + def check_type(self, attr, vals): + """Vérifie que les valeurs ont le bon type (nom est un mot, tel + est un nombre, etc...)""" + pass + + def check_uniqueness(self, attr, vals): + """Vérifie l'unicité dans la base de la valeur (mailAlias, chbre, + etc...)""" + pass + + def check_values(self, attr, vals): + """Vérifie que les valeurs sont valides (typiquement chbre)""" + pass + + def check_users_restrictions(self, attrs, vals): + """Vérifie les restrictions supplémentaires imposées selon les + niveaux de droits (<= 3 mailAlias, pas de mac identiques, + etc...)""" + pass def _gen_hist(self, modifs): - """Vérifie la correction des modifs et genère l'historique des - modifications apportées""" + """Genère l'historique des modifications apportées. Cette + fonction n'est là que pour de la rétro-compatibilité, + normalement les modifications sont automatiquement loggées.""" histo = [] - for field in self.ufields: - if len(modifs.get(field, [])) != 1: - raise ValueError('%s doit avoir exactement une valeur' % field) - for field in self.ofields: - if len(modifs.get(field, [])) > 1: - raise ValueError('%s doit avoir au maximum une valeur' % field) if modifs.get(field, []) != self.attrs.get(field, []): if modifs.get(field, []) == []: msg = u"[%s] %s -> RESET" % (field, self.attrs[field][0]) elif self.attrs.get(field, []) == []: - msg = u"[%s] := %s"(field, modifs[field][0]) + msg = u"[%s] := %s" % (field, modifs[field][0]) else: msg = u"[%s] %s -> %s" % (field, self.attrs[field][0], modifs[field][0]) histo.append(self.conn._hist(msg)) @@ -474,8 +454,8 @@ class CransLdapObject: else: Blist.append(new_c) - if Blist != self._modifs.get('blacklist'): - self._modifs['blacklist'] = Blist + if Blist != self.attrs.get('blacklist'): + self.set_ldapattr('blacklist', Blist) if not hasattr(self, "_blacklist_restart"): self._blacklist_restart = {} restart = self._blacklist_restart.setdefault(new[2], []) @@ -486,51 +466,6 @@ class CransLdapObject: return Blist - def __getattribute__(self, attr): - if self.__dict__.has_key(attr): - return self.__dict__[attr] - else: - if attr in self.ufields + self.ofields + self.mfields + self.xfields: - return _getormod_ldapattr(self, attr) - - def get_ldapattr(self, attr): - """Renvoie un attribut ldap de l'objet self""" - attrs = self._modifs.get(attr, self.attrs.get(attr,[])) - if len(attrs) == 1: - return attrs[0] - else: return attrs - - def mod_ldapattr(self, attr, newVal, oldVal = None): - """Modifie l'attribut attr ayant la valeur oldVal en newVal. Si - l'attribut attr n'a qu'une seule valeur, il n'est pas nécessaire - de préciser oldVal.""" - assert isinstance(newVal, unicode) - attrs = self._modifs.get(attr, self.attrs[attr])[:] - if oldVal: # and oldVal in attrs: - attrs.remove(oldVal) - attrs.append(newVal) - self._modifs[attr] = attrs - elif len(attrs) == 1: - self._modifs[attr] = [newVal] - else: - raise ValueError(u"%s has multiple values, must specify oldVal") - - def del_ldapattr(self, attr, val): - """Supprime la valeur val de l'attribut attr""" - self._modifs.setdefault(attr, self.attrs.get(attr, [])[:]) .remove(newVal) - - def set_ldapattr(self, attr, newVals): - """Définit les nouvelles valeurs d'un attribut""" - if not isinstance(newVals, list): - newVals = [newVals] - for val in newVals: assert isinstance(val, unicode) - self._modifs[attr] = newVals - - def add_ldapattr(self, attr, newVal): - """Rajoute la valeur val à l'attribut attr""" - assert isinstance(newVal, unicode) - self._modifs.setdefault(attr, self.attrs.get(attr, [])[:]).append(newVal) - class proprio(CransLdapObject): ufields = [ 'nom', 'chbre' ] @@ -540,7 +475,7 @@ class proprio(CransLdapObject): def machines(self): if self._machines == None: self._machines = self.conn.search_s('mid=*', dn = self.dn, scope = 1) - for m in machines: + for m in self._machines: m._proprio = self return self._machines diff --git a/ldap_locks.py b/ldap_locks.py new file mode 100644 index 0000000..c04a1da --- /dev/null +++ b/ldap_locks.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# LDAP_LOCKS.PY-- Locks for lc_ldap +# +## Copyright (C) 2010 Cr@ns +# Author: Antoine Durand-Gasselin +# 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, os + +base_lock = 'ou=lock,dc=crans,dc=org' + +class CransLock: + + def __init__(self, conn): + self.conn = conn + self.locks = {} + self._active = [] + + def __enter__(self): + self.lock() + + def __exit__(self, *args): + # XXX - connecter correctement les tracebacks. + print "exiting with exception", args + self.release() + return True + + def add(self, item, valeur): + '''rajoute un lock, après avoir vérifié qu'il peut être posé''' + try: + locked = self._islocked(item, valeur) + if locked: + raise EnvironmentError(u'Object déjà locké', locked) + except ldap.NO_SUCH_OBJECT: + pass + + locked_values = self.locks.get(item, []) + if valeur not in locked_values: + locked_values.append(valeur) + self.locks[item] = locked_values + + def remove(self, item, valeur): + '''Enlève un lock''' + self.locks[item].remove(valeur) + + def lock(self): + '''Essaie de prendre tous les verrous''' + items = self.locks.items() + items.sort() + try: + for item, valeurs in items: + for valeur in valeurs: + self._lockitem(item, valeur) + except Exception, e: + # XXX - connecter proprement les traceback + self.release() + raise e + + def release(self): + '''Relâche tous les verrous''' + exceptions = [] + print "releasing", self._active + for item in self._active[:]: + try: + self._releaseitem(item) + except Exception, e: + exceptions.append(e) + if len(exceptions) == 1: + # XXX - connecter proprement les tracebacks + raise exceptions[0] + elif len(exceptions) > 1: + raise Exception(exceptions) + + def _islocked(self, item, valeur): + # XXX - return self.conn.search_s(base_dn, 2, '%s=%s' % (item, valeur)) + return self.conn.search_s('%s=%s,%s' % (item, valeur, base_lock), 0) + + def _lockitem(self, item, valeur): + u""" + Lock un item avec la valeur valeur, les items possibles + peuvent être : + aid $ chbre $ mail $ mailAlias $ canonicalAlias $ + mid $ macAddress $ host $ hostAlias $ ipHostNumber + Retourne le dn du lock + """ + + valeur = valeur.encode('utf-8') + + lock_dn = '%s=%s,%s' % (item, valeur, base_lock) + lockid = '%s-%s' % ('localhost', os.getpid()) + modlist = ldap.modlist.addModlist({ 'objectClass': 'lock', + 'lockid': lockid, + item: valeur }) + print "locking", lock_dn + try: + self.conn.add_s(lock_dn, modlist) + except ldap.ALREADY_EXISTS: +# # Pas de chance, le lock est déja pris +# try: +# res = self.conn.search_s(lock_dn, 2, 'objectClass=lock')[0] +# l = res[1]['lockid'][0] +# except: l = '%s-1' % hostname +# if l != lockid: +# # C'est locké par un autre process que le notre +# # il tourne encore ? +# if l.split('-')[0] == hostname and os.system('ps %s > /dev/null 2>&1' % l.split('-')[1] ): +# # Il ne tourne plus +# self._releaseitem(res[0]) # delock +# return self._lockitem(item, valeur) # relock + raise EnvironmentError(u'Objet (%s=%s) locké, patienter.' % (item, valeur), lockid) + self._active.append(lock_dn) + return lock_dn + + def _releaseitem(self, lockdn): + u"""Destruction d'un lock""" + # Mettre des verifs ? + print "releasing", lockdn + self._active.remove(lockdn) + self.conn.delete_s(lockdn)