commit c5ece03aa5f83cd5a64b8f1186bf5b51b7fb08ec Author: Antoine Durand-gasselin Date: Sat Jun 26 14:33:36 2010 +0200 Initial import (quite some code) diff --git a/crans_utils.py b/crans_utils.py new file mode 100644 index 0000000..2496451 --- /dev/null +++ b/crans_utils.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# CRANS_UTILS.PY-- Utils for Cr@ns gestion +# +# 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 netaddr, time, smtplib +import config + +def ip_of_mid(mid): + """Convertit un mid en son IP associée""" + for net, plage in config.mid.items(): + if mid >= plage[0] and mid <= plage[1]: + break + else: + raise ValueError("Mid dans aucune plage: %d" % mid) + + return netaddr.IPAddress(netaddr.IPNetwork(config.NETs[net]).first + mid - plage[0]) + +def strip_accents(a): + """ Supression des accents de la chaîne fournie""" + res = normalize('NFKD', decode(a)).encode('ASCII', 'ignore') + return res.replace(' ', '_').replace("'", '') + +def mailexist(mail): + """Vérifie si une adresse mail existe ou non grace à la commande + vrfy du serveur mail """ + + mail = mail.split('@', 1)[0] + try: + s = smtplib.SMTP(smtpserv) + s.putcmd("vrfy", mail) + r = s.getreply()[0] in [250, 252] + s.close() + except: + raise ValueError(u'Serveur de mail injoignable') + + return r + +def format_mac(mac): + u""" Formatage des adresses mac + Transforme une adresse pour obtenir la forme xx:xx:xx:xx:xx:xx + Retourne la mac formatée. + """ + netaddr.EUI(mac, dialect='mac_unix') + if not mac: + raise ValueError(u"MAC nulle interdite\nIl doit être possible de modifier l'adresse de la carte.") + return mac + diff --git a/lc_ldap.py b/lc_ldap.py new file mode 100644 index 0000000..b128e53 --- /dev/null +++ b/lc_ldap.py @@ -0,0 +1,683 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# LC_LDAP.PY-- LightWeight CransLdap +# +# 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. + +from __future__ import with_statement +import os, sys, ldap, ldap.modlist, re, netaddr, datetime, copy, time +sys.path.append('/usr/scripts/gestion') +import config, crans_utils + +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 + venant de blacklist) est active ou non + """ + bl = sanction.split('$') + now = time.time() + debut = int(bl[0]) + if bl[1] == '-': + fin = now + 1 + else: + fin = int(bl[1]) + return debut < now and fin > now + +def uldif_to_ldif(uldif): + """Prend en argument un dico ldif, et vérifie que toutes les + valeurs sont bien des unicodes, les converti alors en chaînes + utf-8, renvoie un ldif""" + ldif = {} + for attr, vals in uldif.items(): + ldif[attr] = [ unicode.encode(val, 'utf-8') for val in uldif[attr] ] + 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] ] + return uldif + +class lc_ldap(ldap.ldapobject.LDAPObject): + def __init__(self, dn=None, user=None, cred=None, uri=uri): + """Initialise la connexion ldap, + - En authentifiant avec dn et cred s'ils sont précisés + - Si dn n'est pas précisé, mais que user est précisé, récupère + le dn associé à l'uid user, et effectue l'authentification + avec ce dn et cred + - Sinon effectue une authentification anonyme + """ + + ldap.ldapobject.LDAPObject.__init__(self, uri) + + if user and not re.match('[a-z_][a-z0-9_-]*', user): + raise ValueError('Invalid user name: %s' % user) + + if user and not dn: + 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 }) + elif len(res) > 1: + raise ldap.INVALID_CREDENTIALS({'desc': 'Too many matches: uid=%s' %s }) + else: + dn = res[0][0] + if dn: + self.conn = self.bind_s(dn, cred) + else: + self.conn = self.simple_bind_s() + + def search(self, filter, mode='ro', dn= base_dn, scope= 2, sizelimit=400): + res = self.search_ext_s(dn, scope, filter, sizelimit=sizelimit) + return [ CransLdapObject(self, r[0], mode=mode) for r in res ] + + def allMachines(self): + """Renvoie la liste de toutes les machines, + Conçue pour s'éxécuter le plus rapidement possible""" + res = {} + machines = [] + for dn, attrs in self.search_s(base_dn, scope=2): + res[dn] = attrs + for dn, attrs in res.items(): + if dn.startswith('mid='): + m = CransLdapObject(self, dn, ldif = attrs) + parent_dn = dn.split(',', 1)[1] + m._proprio = CransLdapObject(self, parent_dn, res[parent_dn]) + machines.append(m) + return machines + + def newMachine(self, parent, realm, uldif): + """Crée une nouvelle machine""" + raise NotImplementedError() + + def newAdherent(self, uldif): + """Crée un nouvel adhérent""" + aid = uldif.setdefault('aid', [ unicode(self._find_id('aid')) ]) + # XXX - autres tests + return self._create_entity('aid=%s,%s' % (aid[0], base_dn), uldif) + + def newClub(self, uldif): + """Crée un nouveau club""" + raise NotImplementedError() + + def newFacture(self, uldif): + """Crée une nouvelle facture""" + raise NotImplementedError() + + def _create_entity(self, dn, uldif): + '''Crée une nouvelle entité ldap en dn, avec attributs ldif: + uniquement en unicode''' + lock = CransLock(self) + for item in ['aid', 'uid', 'chbre', 'mailAlias', 'canonicalAlias', + 'fid', 'cid', 'mid', 'macAddress', 'host', 'hostAlias' ]: + for val in uldif.get(item, []): + lock.add(item, val) + uldif['historique'] = [ self._hist('Création')] + ldif = uldif_to_ldif(uldif) + modlist = ldap.modlist.addModlist(ldif) + with lock: + print dn, modlist + self.add_s(dn, modlist) + return CransLdapObject(self, dn, mode='w') + + def _find_id(self, attr, plage = xrange(1, 32000)): + '''Trouve un id libre dans plage''' + res = self.search_s(base_dn, 2, '%s=*' % attr, attrlist = [attr]) + nonfree = [ int(r[1].get(attr)[0]) for r in res if r[1].get(attr) ] + nonfree.sort() + + for id in plage: + if nonfree and nonfree[0] <= id: + while nonfree and nonfree[0] <= id: + nonfree = nonfree[1:] + else: + break + else: + raise EnvironmentError(u'Aucun %s libre dans la plage [%d, %d]' % + (attr, plage[0], id)) + return id + + def _hist(self, msg): + now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M : ') + return unicode(now) + msg + + # ? 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' + + 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 + + def __init__(self, conn, dn, mode='ro', ldif = None): + '''Créé une instance d'un objet Crans (machine, adhérent, + etc...) à ce dn, si ldif est précisé, n'effectue pas de + recherche dans la base ldap. + ''' + if not isinstance(conn, lc_ldap): + raise TypeError(u"conn doit être une instance de lc_ldap") + self.conn = conn + + if ldif: + self.dn = dn + # /!\ attention, on a pas un uldif (rapidité...) + self.attrs = ldif + self.__class__ = eval(self.attrs['objectClass'][0]) + elif dn == base_dn: + self.__class__ = AssociationCrans + else: + self.mode = mode + res = conn.search_s(dn, 0) + if not res: + raise ValueError ('objet inexistant: %s' % dn) + 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) + + 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 = ldap.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)) + + def _gen_hist(self, modifs): + """Vérifie la correction des modifs et genère l'historique des + modifications apporté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]) + else: + msg = u"[%s] %s -> %s" % (field, self.attrs[field][0], modifs[field][0]) + histo.append(self.conn._hist(msg)) + + for field in self.xfields + self.ufields: + if modifs.get(field, []) != self.attrs.get(field, []): + msg = u"[%s] %s -> %s" % (field, u'; '.join(self.attrs[field]), u'; '.join(modifs[field])) + histo.append(self.conn._hist(msg)) + + for field in self.mfields: + oldvals = self.attrs.get(field, []) + newvals = modifs.get(field, []) + olds = set(oldvals) + news = set(newvals) + if oldvals == newvals: + continue + elif olds != news: + adds = ''.join([ '+' + val for val in news - olds]) + diff = ''.join([ '-' + val for val in olds - news]) + msg = u'[%s]%s%s' % ( field, adds, diff) + elif olds == news and len(oldvals) == len(newvals) == len(olds): + msg = u"[%s].shuffle()" % field + else: + raise ValueError(u"Les valeurs pour %s : %s -> %s ne semblent pas différentes" % (field, oldvals, newvals)) + histo.append(self.conn._hist(msg)) + + return histo + + def blacklist_actif(self): + u"""Vérifie si l'instance courante est blacklistée. + Retourne les sanctions en cours (liste). + Retourne une liste vide si aucune sanction en cours. + """ + return self.blacklist_all()[0].keys() + + def blacklist_all(self): + u"""Vérifie si l'instance courante est blacklistée ou a été + blacklistée. Retourne les sanctions en cours sous la forme + d'un couple de deux dictionnaires (l'un pour les sanctions + actives, l'autre pour les inactive), chacun ayant comme + clef la sanction et comme valeur une liste de couple de + dates (en secondes depuis epoch) correspondant aux + différentes périodes de sanctions. + + ex: {'upload': [(1143336210, 1143509010), ...]} + """ + bl_liste = self.attrs.get('blacklist', []) + if isinstance(self, machine): # Blist du propriétaire + bl_liste += proprio().blacklist() + + actifs = {}; inactifs = {} + for sanction in bl_liste: + f = sanction.split('$') + if is_actif(sanction): + actifs.setdefault(f[2], []).append((f[0], f[1])) + else: + inactifs.setdefault(f[2], []).append((f[0], f[1])) + return (actifs, inactifs) + + def blacklist(self, new=None): + u""" + Blacklistage de la ou de toutes la machines du propriétaire + * new est une liste de 4 termes : + [debut_sanction, fin_sanction, sanction, commentaire] + * début et fin sont le nombre de secondes depuis epoch + * pour un début ou fin immédiate mettre now + * pour une fin indéterminée mettre '-' + Les données sont stockées dans la base sous la forme : + debut$fin$sanction$commentaire + Pour modifier une entrée donner un tuple de deux termes : + (index dans blacklist à modifier, nouvelle liste), + l'index étant celui dans la liste retournée par blacklist(). + """ + Blist = self.attrs.setdefault('blacklist', [])[:] + if new == None: + return Blist + + if type(new) == tuple: + # Modification + index = new[0] + new = new[1] + if new == '': + Blist.pop(index) + return Blist + else: + index = -1 + + if type(new) != list or len(new) != 4: + raise TypeError + + # Verification que les dates sont OK + if new[0] == 'now': + debut = new[0] = int(time.time()) + else: + try: debut = new[0] = int(new[0]) + except: raise ValueError(u'Date de début blacklist invalide') + + if new[1] == 'now': + fin = new[1] = int(time.time()) + elif new[1] == '-': + fin = -1 + else: + try: fin = new[1] = int(new[1]) + except: raise ValueError(u'Date de fin blacklist invalide') + + if debut == fin: + raise ValueError(u'Dates de début et de fin identiques') + elif fin != -1 and debut > fin: + raise ValueError(u'Date de fin avant date de début') + + # On dépasse la fin de sanction d'1min pour être sûr qu'elle est périmée. + fin = fin + 60 + + new_c = u'$'.join(map(str, new)) + + if index != -1: + Blist[index] = new_c + else: + Blist.append(new_c) + + if Blist != self._modifs.get('blacklist'): + self._modifs['blacklist'] = Blist + if not hasattr(self, "_blacklist_restart"): + self._blacklist_restart = {} + restart = self._blacklist_restart.setdefault(new[2], []) + if debut not in restart: + restart.append(debut) + if fin != -1 and fin not in restart: + restart.append(fin) + + 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' ] + mfields = [ 'paiement', 'info', 'blacklist', 'controle'] + ofields = []; xfields = [] + _machines = None + def machines(self): + if self._machines == None: + self._machines = self.conn.search_s('mid=*', dn = self.dn, scope = 1) + for m in machines: + m._proprio = self + return self._machines + +class machine(CransLdapObject): + _proprio = None + ufields = ['mid', 'macAddress', 'host', 'midType'] + ofields = [] + mfields = ['info', 'blacklist', 'hostAlias', 'exempt', + 'portTCPout', 'portTCPin', 'portUDPout', 'portUDPin'] + xfields = ['ipHostNumber'] + def proprio(self): + parent_dn = self.dn.split(',', 1)[1] + self._proprio = CransLdapObject(self.conn, parent_dn, self.mode) + return self._proprio + + +class AssociationCrans(proprio): pass + +class adherent(proprio): + ufields = proprio.ufields + ['aid', 'prenom', 'tel', 'mail', 'mailInvalide'] + ofields = proprio.ofields + ['charteMA', 'adherentPayant', 'typeAdhesion', + 'canonicalAlias', 'solde', 'contourneGreylist', + 'rewriteMailHeaders', 'derniereConnexion', + 'homepageAlias'] + mfields = proprio.mfields + ['carteEtudiant', 'mailAlias', 'droits' ] + xfields = ['etudes', 'postalAddress'] + +class club(proprio): + ufields = ['cid', 'responsable'] + mfields = ['imprimeurClub'] + +class machineFixe(machine): pass + +class machineWifi(machine): + ufields = machine.ufields + ['ipsec'] + +class machineCrans(machine): + ufields = machine.ufields + ['prise'] + ofields = machine.ofields + ['nombrePrises'] + +class borneWifi(machine): + ufields = machine.ufields + ['canal', 'puissane', 'hotspot', + 'prise', 'positionBorne', 'nvram'] + +class facture(CransLdapObject): + ufields = ['fid', 'modePaiement', 'recuPaiement'] + +class service(CransLdapObject): pass + +class lock(CransLdapObject): pass + + +MODIFIABLE_ATTRS = [ 'tel', 'chbre', 'mailAlias', 'loginShell' ] + + + + +CRANS_ATTRIBUTES = { + 'nom' : { 'attr' : 'nom', + 'hname' : 'Nom', + 'isunique' : True }, + 'prenom' : { 'attr' : 'prenom', + 'hname' : u'Prénom', + 'isunique' : True }, + 'tel' : { 'attr' : 'tel', + 'hname' : 'Téléphone', + 'isunique' : True }, + 'paiement' : { 'attr' : 'paiement', + 'hname' : u'Années de cotisations', + 'isunique' : False }, + 'carteEtudiant' : { 'atttr' : 'carteEtudiant', + 'hname' : u'Carte fournie pour les années', + 'isunique' : False }, + 'mailAlias' : { 'attr' : 'mailAlias', + 'hname' : 'Alias mail', + 'isunique' : False }, + 'canonicalAlias' : { 'attr' : 'canonicalAlias', + 'hname' : 'Alias mail canonique', + 'isunique' : True }, + 'etudes' : { 'attr' : 'etudes', + 'hname' : u'Études suivies', + 'isunique' : False }, + 'chbre' : { 'attr' : 'chbre', + 'hname' : 'Chambre', + 'isunique' : True }, + 'droits' : { 'attr' : 'droits', + 'hname' : 'Droits', + 'isunique' : False }, + 'solde' : { 'attr' : 'solde', + 'hname' : "Solde sur le compte d'impression", + 'isunique' : True }, + + + 'mid' : { 'attr' : 'mid', + 'hname' : 'Identifiant de machine', + 'isunique' : True }, + 'hostAlias' : { 'attr' : 'hostAlias', + 'hname' : 'Alias de nom de machine', + 'isunique' : False }, + 'ipsec' : { 'attr' : 'ipsec', + 'hname' : 'Clef wifi', + 'isunique' : True }, + 'puissance' : { 'attr' : 'puissance', + 'hname' : u"Puissance d'émission de la borne wifi", + 'isunique' : True }, + 'canal' : { 'attr' : 'canal', + 'hname' : u"Canal d'émission de la borne wifi", + 'isunique' : True }, + 'portTCPout' : { 'attr' : 'portTCPout', + 'hname' : u"Port TCP ouvert vers l'extérieur", + 'isunique' : False }, + 'portTCPin' : { 'attr' : 'portTCPin', + 'hname' : u"Port TCP ouvert depuis l'extérieur", + 'isunique' : False }, + 'portUDPout' : { 'attr' : 'portUDPout', + 'hname' : u"Port UDP ouvert vers l'extérieur", + 'isunique' : False }, + 'portUDPin' : { 'attr' : 'portUDPin', + 'hname' : u"Port UDP ouvert depuis l'extérieur", + 'isunique' : False }, + 'prise' : { 'attr' : 'prise', + 'hname' : 'Prise sur laquelle est branchée la machine', + 'isunique' : True }, + + + 'cid' : { 'attr' : 'cid', + 'hname' : 'Identifiant de club', + 'isunique' : True }, + 'responsable' : { 'attr' : 'responsable', + 'hname' : 'Responsable du club', + 'isunique' : True }, + + 'blacklist' : {'attr' : 'blacklist', + 'hname' : 'Historique des sanctions', + 'isunique' : False }, + 'historique' : { 'attr' : 'historique', + 'hname' : 'Historique des modifications', + 'isunique' : False }, +} +