[ldap_locks] Mise en place des locks. Cf commentaires pour les détails.

* Malheureusement lc_ldap._create_entity et objet.create sont un peu
 sales, mais j'ai pas trouvé mieux.
 * L'historique contient désormais les secondes.
This commit is contained in:
Pierre-Elliott Bécue 2013-05-30 15:22:11 +02:00
parent 17efae121c
commit d6efff30de
7 changed files with 123 additions and 58 deletions

View file

@ -548,7 +548,7 @@ class etudes(Attr):
class chbre(Attr): class chbre(Attr):
singlevalue = True singlevalue = True
optional = False optional = False
unique = True unique = False
legend = u"Chambre sur le campus" legend = u"Chambre sur le campus"
can_modify = [soi, cableur, nounou] can_modify = [soi, cableur, nounou]
category = 'perso' category = 'perso'
@ -1125,6 +1125,14 @@ class gecos(Attr):
category = 'id' category = 'id'
ldap_name = "gecos" ldap_name = "gecos"
@crans_attribute
class userPassword(Attr):
singlevalue = True
optional = True
legend = "Le mot de passe"
category = ''
ldap_name = "userPassword"
@crans_attribute @crans_attribute
class sshFingerprint(Attr): class sshFingerprint(Attr):
singlevalue = False singlevalue = False

View file

@ -41,6 +41,7 @@ import ldap.filter
sys.path.append('/usr/scripts/gestion') sys.path.append('/usr/scripts/gestion')
import config import config
from unicodedata import normalize from unicodedata import normalize
import subprocess
DEVNULL = open(os.devnull, 'w') DEVNULL = open(os.devnull, 'w')
@ -208,7 +209,7 @@ def process_status(pid):
""" """
Vérifie l'état du processus pid Vérifie l'état du processus pid
""" """
cmd = subprocess.check(['ps', '%s' % pid], stdout=DEVNULL, stderr=subprocess.STDOUT) cmd = subprocess.call(['ps', '%s' % pid], stdout=DEVNULL, stderr=subprocess.STDOUT)
if cmd != 0: if cmd != 0:
return False return False
else: else:

View file

@ -85,6 +85,8 @@ class lc_ldap(ldap.ldapobject.LDAPObject, object):
""" """
ldap.ldapobject.LDAPObject.__init__(self, uri) ldap.ldapobject.LDAPObject.__init__(self, uri)
self.lockholder = ldap_locks.LdapLockHolder(self)
if user and not re.match('[a-z_][a-z0-9_-]*', user): if user and not re.match('[a-z_][a-z0-9_-]*', user):
raise ValueError('Invalid user name: %r' % user) raise ValueError('Invalid user name: %r' % user)
@ -295,6 +297,13 @@ class lc_ldap(ldap.ldapobject.LDAPObject, object):
'''Crée une nouvelle entité ldap avec le dn ``dn`` et les '''Crée une nouvelle entité ldap avec le dn ``dn`` et les
attributs de ``ldif``. Attention, ldif doit contenir des attributs de ``ldif``. Attention, ldif doit contenir des
données encodées.''' données encodées.'''
for key, values in uldif.iteritems():
if key.endswith('id'):
continue
attribs = [attributs.attrify(val, key, self) for val in values]
for attribut in attribs:
if attribut.unique:
self.lockholder.addlock(key, str(attribut))
return objets.new_cransldapobject(self, dn, 'rw', uldif) return objets.new_cransldapobject(self, dn, 'rw', uldif)
def _find_id(self, attr, plage=None): def _find_id(self, attr, plage=None):
@ -314,12 +323,17 @@ class lc_ldap(ldap.ldapobject.LDAPObject, object):
if my_id.value != i: if my_id.value != i:
continue continue
else: else:
try:
self.lockholder.addlock(attr, str(i))
break break
except:
continue
else: else:
raise EnvironmentError('Aucun %s libre dans la plage [%d, %d]' % raise EnvironmentError('Aucun %s libre dans la plage [%d, %d]' %
(attr, plage[0], i)) (attr, plage[0], i))
else: else:
i = nonfree[-1]+1 i = nonfree[-1]+1
self.lockholder.addlock(attr, str(i))
return i return i
def _check_parent(self, objdn): def _check_parent(self, objdn):

View file

@ -38,6 +38,8 @@ import os
import exceptions import exceptions
import socket import socket
import crans_utils import crans_utils
import collections
import subprocess
class LdapLockedByYou(exceptions.StandardError): class LdapLockedByYou(exceptions.StandardError):
""" """
@ -67,24 +69,37 @@ class LdapLockHolder:
""" """
On crée la connexion, et on crée un dico vide. On crée la connexion, et on crée un dico vide.
""" """
self.locks = {} self.locks = collections.defaultdict(dict, {})
self.host = socket.gethostbyname() self.host = socket.gethostname()
self.pid = os.getpid() self.pid = os.getpid()
self.conn = conn self.conn = conn
def __del__(self): def purge(self, Id=None):
""" """
On essaye de détruire tous les verrous hébergés par On essaye de détruire tous les verrous hébergés par
l'objet mourant. l'objet.
""" """
for item, value in self.locks: if Id == None:
for key, subdict in self.locks['default'].keys():
for item, value in subdict:
try: try:
self.removelock(item, value) self.removelock(item, value, key)
except:
pass
else:
for item, value in self.locks[Id].items():
try:
self.removelock(item, value, Id)
except: except:
pass pass
del self.conn
def addlock(self, item, value): 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", Applique un verrou sur "$item=$value,$LOCKS_DN",
si le précédent verrou était géré par la session si le précédent verrou était géré par la session
@ -97,21 +112,21 @@ class LdapLockHolder:
""" """
try: try:
host, pid = self.getlock(item, value) host, pid = self.getlock(item, value, Id)
if host == self.host and pid == self.pid(): if host == self.host and pid == self.pid:
raise LdapLockedByYou("La donnée %r=%r est lockée par vous-même." % (item, value)) raise LdapLockedByYou("La donnée %r=%r est lockée par vous-même." % (item, value))
elif host == self.host: elif host == self.host:
status = crans_utils.process_status(pid) status = crans_utils.process_status(pid)
if status: if status:
raise LdapLockedByOther("La donnée %r=%r est lockée par quelqu'un d'autre ou un processus." % (item, value)) raise LdapLockedByOther("La donnée %r=%r est lockée par quelqu'un d'autre ou un processus." % (item, value))
else: else:
self.removelock(item, value) self.removelock(item, value, Id, True)
else: else:
raise LdapLockedByOther("La donnée %r=%r est lockée par quelqu'un d'autre ou un processus." % (item, value)) raise LdapLockedByOther("La donnée %r=%r est lockée par quelqu'un d'autre ou un processus." % (item, value))
except ldap.NO_SUCH_OBJECT: except ldap.NO_SUCH_OBJECT:
pass pass
except LockFormatError: except LockFormatError:
self.removelock(item, value) self.removelock(item, value, Id)
dn = "%s=%s,%s" % (item, value, LOCKS_DN) dn = "%s=%s,%s" % (item, value, LOCKS_DN)
lockid = "%s-%s" % (self.host, self.pid) lockid = "%s-%s" % (self.host, self.pid)
@ -121,38 +136,49 @@ class LdapLockHolder:
try: try:
self.conn.add_s(dn, modlist) self.conn.add_s(dn, modlist)
self.locks[item] = value self.locks[Id][item] = value
except ldap.ALREADY_EXISTS: except ldap.ALREADY_EXISTS:
status = crans_utils.process_status(pid) status = crans_utils.process_status(pid)
if status: if status:
raise LdapLockedByOther("La donnée %r=%r est lockée par quelqu'un d'autre ou un processus." % (item, value)) raise LdapLockedByOther("La donnée %r=%r est lockée par quelqu'un d'autre ou un processus." % (item, value))
else: else:
self.removelock(item, value) self.removelock(item, value, Id)
try: try:
self.conn.add_s(dn, modlist) self.conn.add_s(dn, modlist)
self.locks[item] = value
# Si on a un pointeur vers l'id de l'objet, on en tient compte
self.locks[Id][item] = value
except: except:
raise StandardError("Quelque chose a planté durant la pose du lock %s=%s" % (item, value)) raise StandardError("Quelque chose a planté durant la pose du lock %s=%s" % (item, value))
def removelock(self, item, value): def removelock(self, item, value, Id='default', force=False):
""" """
Libère le lock "$item=$value,$LOCKS_DN". Libère le lock "$item=$value,$LOCKS_DN".
""" """
print "Deleting %s=%s,%s\n" % (item, value, LOCKS_DN) if not force:
if self.locks.get(item, "") == value: if self.locks[Id].get(item, "") == str(value):
self.locks.pop(item) _ = self.locks[Id].pop(item)
self.conn.delete_s("%s=%s,%s" % (item, value, LOCKS_DN)) self.conn.delete_s("%s=%s,%s" % (item, value, LOCKS_DN))
else: else:
print "Lock %s=%s,%s does not exist on this session" % (item, value, LOCKS_DN) pass
else:
try:
self.conn.delete_s("%s=%s,%s" % (item, value, LOCKS_DN))
except:
pass
def getlock(self, item, value): def getlock(self, item, value, Id):
""" """
Trouve le lock item=value, et renvoie le contenu de lockinfo Trouve le lock item=value, et renvoie le contenu de lockinfo
via un couple host, pid via un couple host, pid
""" """
result = self.conn.search_s('%s=%s,%s' % (item, value, LOCKS_DN), 0) result = self.conn.search_s('%s=%s,%s' % (item, value, LOCKS_DN), 0)
if not result:
_ = self.locks[Id].pop(item)
return None, 0
try: try:
return result[0][1]['lockid'][0].split('-') host, pid = result[0][1]['lockid'][0].split('-')
return host, int(pid)
except: except:
raise LockFormatError raise LockFormatError

View file

@ -63,6 +63,12 @@ from gestion.gen_confs.dhcpd_new import dydhcp
#: Champs à ignorer dans l'historique #: Champs à ignorer dans l'historique
HIST_IGNORE_FIELDS = ["modifiersName", "entryCSN", "modifyTimestamp", "historique"] HIST_IGNORE_FIELDS = ["modifiersName", "entryCSN", "modifyTimestamp", "historique"]
crans_account_attribs = [attributs.uid, attributs.canonicalAlias, attributs.solde,
attributs.contourneGreylist, attributs.derniereConnexion,
attributs.homepageAlias, attributs.loginShell, attributs.gecos,
attributs.uidNumber, attributs.homeDirectory,
attributs.gidNumber, attributs.userPassword,
attributs.mailAlias, attributs.cn]
def new_cransldapobject(conn, dn, mode='ro', uldif=None): def new_cransldapobject(conn, dn, mode='ro', uldif=None):
"""Crée un objet :py:class:`CransLdapObject` en utilisant la classe correspondant à """Crée un objet :py:class:`CransLdapObject` en utilisant la classe correspondant à
@ -159,7 +165,7 @@ class CransLdapObject(object):
assert isinstance(login, unicode) assert isinstance(login, unicode)
assert isinstance(chain, unicode) assert isinstance(chain, unicode)
new_line = u"%s, %s : %s" % (time.strftime("%d/%m/%Y %H:%M"), login, chain) new_line = u"%s, %s : %s" % (time.strftime("%d/%m/%Y %H:%M:%S"), login, chain)
# Attention, le __setitem__ est surchargé, mais pas .append sur l'historique # Attention, le __setitem__ est surchargé, mais pas .append sur l'historique
self["historique"] = self.get("historique", []) + [new_line] self["historique"] = self.get("historique", []) + [new_line]
@ -193,6 +199,12 @@ class CransLdapObject(object):
modlist = addModlist(self._modifs.to_ldif()) modlist = addModlist(self._modifs.to_ldif())
# Requête LDAP de création de l'objet # Requête LDAP de création de l'objet
self.conn.add_s(self.dn, modlist) self.conn.add_s(self.dn, modlist)
# On nettoie les locks
for key, values in self._modifs.to_ldif().iteritems():
for value in values:
self.conn.lockholder.removelock(key, value)
self.conn.lockholder.purge(id(self))
# Services à relancer
services.services_to_restart(self.conn, {}, self._modifs) services.services_to_restart(self.conn, {}, self._modifs)
self._post_creation() self._post_creation()
@ -221,6 +233,7 @@ class CransLdapObject(object):
raise EnvironmentError("Vous n'avez pas le droit de supprimer %s." % self.dn) raise EnvironmentError("Vous n'avez pas le droit de supprimer %s." % self.dn)
self.bury(comm, login) self.bury(comm, login)
self.conn.delete_s(self.dn) self.conn.delete_s(self.dn)
self.conn.lockholder.purge(id(self))
self._post_deletion() self._post_deletion()
services.services_to_restart(self.conn, self.attrs, {}) services.services_to_restart(self.conn, self.attrs, {})
@ -243,6 +256,9 @@ class CransLdapObject(object):
# On programme le redémarrage des services # On programme le redémarrage des services
services.services_to_restart(self.conn, self.attrs, self._modifs) services.services_to_restart(self.conn, self.attrs, self._modifs)
# On nettoie les locks
self.conn.lockholder.purge(id(self))
# Vérification des modifications # Vérification des modifications
old_ldif = self.conn.search_s(self.dn, ldap.SCOPE_BASE)[0][1] old_ldif = self.conn.search_s(self.dn, ldap.SCOPE_BASE)[0][1]
old_uldif = lc_ldap.ldif_to_uldif(old_ldif) old_uldif = lc_ldap.ldif_to_uldif(old_ldif)
@ -330,6 +346,9 @@ class CransLdapObject(object):
if not mixed_attrs[0].is_modifiable(self.conn.droits + self.conn._check_parent(self.dn) + self.conn._check_self(self.dn)): if not mixed_attrs[0].is_modifiable(self.conn.droits + self.conn._check_parent(self.dn) + self.conn._check_self(self.dn)):
raise EnvironmentError("Vous ne pouvez pas toucher aux attributs de type %r." % (attr)) raise EnvironmentError("Vous ne pouvez pas toucher aux attributs de type %r." % (attr))
self._modifs[attr] = attrs_before_verif self._modifs[attr] = attrs_before_verif
for attribut in attrs_before_verif:
if attribut.unique:
self.conn.lockholder.addlock(attr, str(attribut), id(self))
def search_historique(self, ign_fields=HIST_IGNORE_FIELDS): def search_historique(self, ign_fields=HIST_IGNORE_FIELDS):
u"""Récupère l'historique u"""Récupère l'historique
@ -617,15 +636,16 @@ class adherent(proprio):
attributs.mail, attributs.mailInvalide, attributs.charteMA, attributs.mail, attributs.mailInvalide, attributs.charteMA,
attributs.derniereConnexion, attributs.gpgFingerprint, attributs.derniereConnexion, attributs.gpgFingerprint,
attributs.carteEtudiant, attributs.droits, attributs.etudes, attributs.carteEtudiant, attributs.droits, attributs.etudes,
attributs.postalAddress, attributs.mailExt, attributs.compteWiki] attributs.postalAddress, attributs.mailExt, attributs.compteWiki,
]
ldap_name = "adherent" ldap_name = "adherent"
def __init__(self, conn, dn, mode='ro', ldif = None): def __init__(self, conn, dn, mode='ro', ldif = None):
super(adherent, self).__init__(conn, dn, mode, ldif) super(adherent, self).__init__(conn, dn, mode, ldif)
self.full = False
if u'cransAccount' in [ unicode(o) for o in self['objectClass']]: if u'cransAccount' in [ unicode(o) for o in self['objectClass']]:
self.attribs = self.attribs + [attributs.uid, attributs.canonicalAlias, attributs.solde, self.attribs = self.attribs + crans_account_attribs
attributs.contourneGreylist, attributs.derniereConnexion, self.full = True
attributs.homepageAlias, attributs.mailAlias, attributs.loginShell ]
def compte(self, login = None, uidNumber=0, hash_pass = '', shell=config.login_shell): def compte(self, login = None, uidNumber=0, hash_pass = '', shell=config.login_shell):
u"""Renvoie le nom du compte crans. S'il n'existe pas, et que uid u"""Renvoie le nom du compte crans. S'il n'existe pas, et que uid
@ -650,17 +670,20 @@ class adherent(proprio):
if os.path.exists("/var/mail/" + login): if os.path.exists("/var/mail/" + login):
raise ValueError('Création du compte impossible : /var/mail/%s existant' % login) raise ValueError('Création du compte impossible : /var/mail/%s existant' % login)
self._modifs['homeDirectory'] = [home] if not self.full:
self._modifs['mail'] = [login] self.attribs = self.attribs + crans_account_attribs
self._modifs['uid' ] = [login] self.full = True
self['homeDirectory'] = [home]
self['mail'] = [login + u"@crans.org"]
self['uid' ] = [login]
calias = crans_utils.strip_spaces(fn) + u'.' + crans_utils.strip_spaces(ln) calias = crans_utils.strip_spaces(fn) + u'.' + crans_utils.strip_spaces(ln)
if crans_utils.mailexist(calias): if crans_utils.mailexist(calias):
calias = login calias = login
self._modifs['canonicalAlias'] = [calias] self['canonicalAlias'] = [calias]
self._modifs['objectClass'] = [u'adherent', u'cransAccount', u'posixAccount', u'shadowAccount'] self['objectClass'] = [u'adherent', u'cransAccount', u'posixAccount', u'shadowAccount']
self._modifs['cn'] = [ fn + u' ' + ln ] self['cn'] = [ fn + u' ' + ln ]
self._modifs['loginShell'] = [unicode(shell)] self['loginShell'] = [unicode(shell)]
self._modifs['userPassword'] = [unicode(hash_pass)] self['userPassword'] = [unicode(hash_pass)]
if uidNumber: if uidNumber:
if self.conn.search('(uidNumber=%s)' % uidNumber): if self.conn.search('(uidNumber=%s)' % uidNumber):
@ -675,18 +698,11 @@ class adherent(proprio):
if not len(pool_uid): if not len(pool_uid):
raise ValueError("Plus d'uid disponibles !") raise ValueError("Plus d'uid disponibles !")
## try: self['uidNumber'] = [unicode(uidNumber)]
## self.lock('uidNumber', str(uidNumber)) self['gidNumber'] = [unicode(config.gid)]
## except: self['gecos'] = [self._modifs['cn'][0] + u',,,']
## # Quelqu'un nous a piqué l'uid que l'on venait de choisir !
## return self.compte(login, uidNumber, hash_pass, shell)
self._modifs['uidNumber'] = [unicode(uidNumber)]
self._modifs['gidNumber'] = [unicode(config.gid)]
self._modifs['gecos'] = [self._modifs['cn'][0] + u',,,']
self.save()
#self.save()
else: else:
raise EnvironmentError("L'adhérent n'a pas de compte crans") raise EnvironmentError("L'adhérent n'a pas de compte crans")

View file

@ -53,7 +53,7 @@ borne_ldif = {
'macAddress' : [randomMAC()], 'macAddress' : [randomMAC()],
'host' : ["autotest-%s.crans.org" % randomStr() ], 'host' : ["autotest-%s.crans.org" % randomStr() ],
'canal' : ["11"], 'canal' : ["11"],
'puissance' : ["52 khz"], 'puissance' : [u"52"],
} }
club_ldif = { club_ldif = {
@ -97,7 +97,6 @@ def test_list_of_dict(keys, list):
anim("\tTest de l'attribut %s" % key) anim("\tTest de l'attribut %s" % key)
ok = True ok = True
for item in list: for item in list:
if key == "chbre":
print item['aid'][0] print item['aid'][0]
try: item.get(key, []) try: item.get(key, [])
except psycopg2.OperationalError as error: except psycopg2.OperationalError as error:

View file

@ -33,3 +33,4 @@ deleted = 'deleted'
#: Mot de passe de la base de tests #: Mot de passe de la base de tests
ldap_test_password = '75bdb64f32' ldap_test_password = '75bdb64f32'