#! /usr/bin/env python # -*- coding: iso-8859-15 -*- """ Définitions des classes de base du système de gestion des machines et adhérents du crans Copyright (C) Frédéric Pauget Licence : GPLv2 """ from socket import gethostname import smtplib, sre, os, random, string, time, sys import ldap, ldap.modlist, ldap_passwd import config, annuaires, iptools, chgpass, user_tests, cPickle, config_mail from chgpass import chgpass from affich_tools import coul, prompt date_format = '%d/%m/%Y %H:%M' hostname = gethostname().split(".")[0] smtpserv = "rouge.crans.org" random.seed() # On initialise le générateur aléatoire ################################################################################## ### paramètres de connexion à la base LDAP if __name__ == 'ldap_crans_test' or os.environ.get('crans_ldap', '') == 'test': # Utilisation de la base de données de test (tests, séminaire...) # Il faut au choix : # - faire un import crans_ldap_test # ou - crans_ldap=test /le/script uri = ro_uri = 'ldap://egon.adm.crans.org/' ldap_auth_dn = 'cn=admin,dc=crans,dc=org' ldap_password = '75bdb64f32' elif user_tests.getuser() == 'freerad': # Freeradius n'a pas accès au secret, donc accès que en local uri = '' ro_uri = 'ldapi://%2fvar%2frun%2fldapi/' ldap_auth_dn = ldap_password = '' else: # pour les autres on utilise ldap.adm.crans.org en rw uri = 'ldap://ldap.adm.crans.org/' # avec le secret try: from secrets import ldap_password, ldap_auth_dn except: sys.stdout.write(coul('Warning : impossible de lire le fichier de secret !\n', 'jaune')) sys.exit(1) # uri pour les instances de CransLdap faisant de la lecture seule if os.path.exists('/var/run/ldapi'): ro_uri = 'ldapi://%2fvar%2frun%2fldapi/' else: ro_uri = uri ################################################################################## ### Items de la blackliste blacklist_items = { u'bloq': u'Bloquage total de tous les services', u'virus': u'Bloquage sur squid', u'upload': u"Bloquage total de l'accès à l'extérieur", u'warez': u'Bloquage sur squid', u'p2p': u"Bloquage total de l'accès à l'extérieur", u'autodisc_upload': u'Autodisconnect pour upload', u'autodisc_p2p': u'Autodisconnect pour P2P' } ################################################################################## ### Droits possibles droits_possibles = [ u'Nounou', u'Apprenti', u'Modérateur', u'Câbleur', u'Déconnecteur', u'WebRadio', u'Imprimeur', u'MultiMachines', u'Contrôleur' ] ################################################################################## ### Variables internes diverses isadm = user_tests.isadm() isdeconnecteur = user_tests.isdeconnecteur() ann_scol = config.ann_scol script_utilisateur = user_tests.getuser() ################################################################################## ### Fonctions utiles def decode(s): """ Retourne un unicode à partir de s s doit être en utf-8 """ if type(s) is unicode: # Si s est déjà un unicode, on ne décode pas return s else: return s.decode('utf-8', 'ignore') # On ignore les erreurs accents = "êëèéÉÈÀÙâäàûüôöÖÔîïÎÏ'çÇÿßæÆøØ" # Si modif ici modifier aussi la fonction def strip_accents(a): """ Supression des accents de la chaîne fournie """ a = a.replace(u'ê','e').replace(u'ë','e').replace(u'è','e').replace(u'é','e').replace(u'É','e').replace(u'È','e') a = a.replace(u'â','a').replace(u'ä','a').replace(u'à','a').replace(u'À','a') a = a.replace(u'û','u').replace(u'ü','u').replace(u'ù','u').replace(u'Ù','u') a = a.replace(u'ô','o').replace(u'ö','o').replace(u'Ö','o').replace(u'Ô','o') a = a.replace(u'î','i').replace(u'ï','i').replace(u'Ï','i').replace(u'Î','i') a = a.replace(' ','_').replace(u"'",'').replace(u'ç','c').replace(u'Ç','c') a = a.replace(u'ÿ','y').replace(u'ß','ss').replace(u'æ','ae').replace(u'Æ','ae').replace(u'ø','o').replace(u'Ø','o') return a def mailexist(mail): """ Vérifie si une adresse mail existe ou non grace à la commande vrfy du serveur mail """ try: s = smtplib.SMTP(smtpserv) r = s.vrfy(mail) s.close() except: raise ValueError(u'Serveur de mail injoignable') if r[0] in [250, 252]: return True else: return False def preattr(val): """ val est : * un entier * une chaîne * une liste avec un seul entier ou une seule cha-Aîne-b Retourne [ len(str(val).strip), str(val).strip en utf-8 ] """ t = type(val) if t == list and len(val) == 1: return preattr(val[0]) elif t == str or t == int: val = str(val).strip() # On passe tout en utf-8 pour ne pas avoir de problèmes # d'accents dans la base return [len(val), unicode(val, 'iso-8859-1').encode('utf-8')] elif t == unicode: val = val.strip() return [len(val), val.encode('utf-8')] else: raise TypeError 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 format_mac(mac): """ Formatage des adresses mac Transforme une adresse pour obtenir la forme xx:xx:xx:xx:xx:xx Le séparateur original peut être :, - ou rien Retourne la mac formatée. """ l, mac = preattr(mac) mac = mac.strip().replace("-", ":") if mac.count(":") == 5: # On a une adresse de la forme 0:01:02:18:d1:90 # On va compléter s'il manque des 0 mac = ":".join(map(lambda x: x.replace(' ', '0'), map(lambda x: "%02s" % x, mac.split(":")))) mac = mac.replace(':', '').lower() if len(mac) != 12: raise ValueError(u"Longueur de l'adresse mac incorrecte.") for c in mac[:]: if c not in string.hexdigits: raise ValueError(u"Caractère interdit '%s' dans l'adresse mac." % c) if mac == '000000000000': raise ValueError(u"MAC nulle interdite\nIl doit être possible de modifier l'adresse de la carte.") # Formatage mac = "%s:%s:%s:%s:%s:%s" % (mac[:2], mac[2:4], mac[4:6], mac[6:8], mac[8:10], mac[10:]) return mac ################################################################################## ### Définition des classes class Service: """ Définit un service à redémarrer """ def __init__(self, nom, args=[], start=[]): """ Nom du service Liste des arguments Liste d'horaires de démarrages """ self.nom = nom self.args = args self.start = map(int, start) def __str__(self): starting = self.start starting.sort() dates = ' et '.join(map(lambda t: t < time.time() and \ "maintenant" or time.strftime(date_format, time.localtime(t)), self.start)) dates = " à partir d%s %s" % (dates.startswith("maintenant") and "e" or "u", dates) return ("%s(%s)%s" % (self.nom, ','.join(self.args), dates)).replace(" et maintenant", "") class CransLdap: """ Classe de connexion à la base LDAP du crans. """ conn = None base_dn = 'ou=data,dc=crans,dc=org' base_lock = 'ou=lock,dc=crans,dc=org' base_services = 'ou=services,dc=crans,dc=org' ### Configuration de la recheche # Dictionnaire de transformation des champs trans = { 'prénom': 'prenom', 'chambre': 'chbre', 'login': 'mail', 'hostname': 'host', 'mac': 'macAddress', 'ip': 'ipHostNumber', 'telephone': 'tel', 'position': 'positionBorne' } # Les différentes classes LDAP de machines ldap_machines_classes = ['machineFixe', 'machineWifi', 'machineCrans', 'borneWifi'] # Champs de recherche pour la recherche automatique auto_search_machines_champs = \ ['macAddress', 'host', 'ipHostNumber', 'hostAlias'] auto_search_champs = { \ 'adherent': \ ['nom', 'prenom', 'tel', 'mail', 'chbre', 'mailAlias', 'canonicalAlias'], 'club': ['nom', 'chbre'], 'machineFixe': auto_search_machines_champs, 'machineWifi': auto_search_machines_champs, 'machineCrans': auto_search_machines_champs, 'borneWifi': auto_search_machines_champs, 'facture' : [] } # Champs de recherche pour la recherche manuelle (en plus de la recherche auto) non_auto_search_machines_champs = \ ['mid', 'historique', 'blacklist', 'info', 'exempt', 'mblacklist', 'portTCPin', 'portTCPout', 'portUDPin', 'portUDPout'] non_auto_search_champs = { \ 'adherent': \ ['etudes', 'paiement', 'carteEtudiant', 'aid', 'postalAddress', 'historique', 'blacklist', 'droits', 'uidNumber', 'uid', 'info', 'solde', 'controle', 'contourneGreylist', 'rewriteMailHeaders', 'ablacklist', 'homepageAlias'], \ 'club': \ ['cid', 'responsable', 'paiement', 'historique', 'blacklist', 'mailAlias', 'info', 'controle', 'ablacklist'], \ 'machineFixe': non_auto_search_machines_champs, 'machineCrans': non_auto_search_machines_champs + ['prise'], 'borneWifi': non_auto_search_machines_champs + \ ['prise', 'puissance', 'canal', 'hotspot', 'positionBorne', 'nvram'], 'machineWifi': non_auto_search_machines_champs + ['ipsec'], 'facture': ['fid']} # tous les champs de recherche search_champs = {} for i in auto_search_champs.keys(): search_champs[i] = auto_search_champs[i] + non_auto_search_champs[i] # Profondeur des différentes recherches (scope) scope = { 'adherent': 1, 'club': 1, 'machineFixe': 2, 'machineWifi': 2, 'machineCrans': 2, 'borneWifi': 2, 'facture': 2} def __init__(self, readonly=False): self.connect(readonly) def __del__(self): # Destruction des locks résiduels if hasattr(self, '_locks'): for lock in self._locks: self.remove_lock(lock) def connect(self, readonly=False): """ Initialisation la connexion vers le serveur LDAP """ if readonly: self.conn = ldap.initialize(ro_uri) return self.conn = ldap.initialize(uri) nbessais = 0 while True: try: self.conn.bind_s(ldap_auth_dn, ldap_password, ldap.AUTH_SIMPLE) break except ldap.SERVER_DOWN: nbessais += 1 if nbessais > 2: sys.stderr.write("ERREUR : serveur LDAP injoignable\n") sys.exit(1) else: time.sleep(0.3) def exist(self, arg): """ Vérifie l'existence d'une entrée dans la base et que cette entrée n'appartient pas à l'objet en cours. La méthode prend en compte les locks. arg doit être une expression de recherche ldap Si il y a existence, on retourne la liste des dn correspondants Sinon retourne une liste vide Exemple : exist('chbre=Z345') vérifie si il y a un adhérent en Z345 """ r = [] # Premier test: dans les objets déjà inscrits ret = self.conn.search_s(self.base_dn, 2, arg) for res in ret: # C'est peut être l'objet courant try: # Si ce n'est pas une classe fille avec l'attribut dn => erreur if res[0] == self.dn: continue except: pass r.append(res[0]) # Deuxième test : lock ? ret = self.conn.search_s(self.base_lock, 1, arg) lockid = '%s-%s' % (hostname, os.getpid()) for res in ret: # Lock encore actif ? l = res[1]['lockid'][0] if l == lockid: continue # C'est locké par un autre process que le notre # il tourne encore ? try: if l.split('-')[0] == hostname and os.system('ps %s > /dev/null 2>&1' % l.split('-')[1]): # Il ne tourne plus self.remove_lock(res[0]) # delock continue except: pass r.append(res[0]) return r def lock(self, item, valeur): """ 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') if not valeur: # On le lock pas un truc vide return True lock_dn = '%s=%s,%s' % (item, valeur, self.base_lock) lockid = '%s-%s' % (hostname, os.getpid()) modlist = ldap.modlist.addModlist({ 'objectClass': 'lock', 'lockid': lockid, item: valeur }) 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.remove_lock(res[0]) # delock return self.lock(item, valeur) # relock raise EnvironmentError(u'Objet (%s=%s) locké, patienter.' % (item, valeur), l) else: if not hasattr(self, '_locks'): self._locks = [lock_dn] else: self._locks.append(lock_dn) def remove_lock(self, lockdn): """ Destruction d'un lock Destruction de tous les locks si lockdn=* """ # Mettre des verifs ? if lockdn != '*': self.conn.delete_s(lockdn) try: self._locks.remove(lockdn) except: # Pas grave si ca foire, le lock n'y est plus pass else: locks = self.list_locks() for l in locks: self.conn.delete_s(l[0]) def list_locks(self): """ Liste les locks """ return self.conn.search_s(self.base_lock, 1, 'objectClass=lock') def services_to_restart(self, new=None, args=[], start=0): """ start indique la date (en secondes depuis epoch) à partir du moment où cette action sera effectuée. Si new = None retourne la liste des services à redémarrer. Si new est fourni, mais ne commence pas par '-', on ajoute le service à la liste avec les arguments args (args doit être une liste). Si new commence par '-', on supprime le service si son start est dans le futur. Si new commence par '--', on supprime le service de condition. """ if new: new = preattr(new)[1] # Quels services sont déjà à redémarrer ? serv = {} # { service: [ arguments ] } serv_dates = {} # { service: [ dates de restart ] } services = [] for s in self.conn.search_s(self.base_services, 1, 'objectClass=service'): s = s[1] serv[s['cn'][0]] = s.get('args', []) serv_dates[s['cn'][0]] = s.get('start', []) services.append(Service(s['cn'][0], s.get('args', []), s.get('start', []))) # Retourne la liste des services à redémarrer if not new: return services # Effacement d'un service if new[0] == '-': if new[1] == '-': # Double -- on enlève quelque soit la date remove_dn = 'cn=%s,%s' % (new[2:], self.base_services) else: # On enlève uniquement si la date est passée remove_dn = 'cn=%s,%s' % (new[1:], self.base_services) if not serv.has_key(new[1:]): # Existe pas => rien à faire return keep_date = [] for date in serv_dates[new[1:]]: if time.time() < int(date): keep_date.append(date) if keep_date: self.conn.modify_s(remove_dn, ldap.modlist.modifyModlist({'start': serv_dates[new[1:]]}, { 'start': keep_date })) remove_dn = None if remove_dn: # Suppression try: self.conn.delete_s(remove_dn) except: pass # Si n'existe pas => Erreur mais le résultat est là. return serv_dn = 'cn=%s,%s' % (new, self.base_services) # Conversion avant stockage dans la base if type(args) == str: args = [args] args = map(lambda x:preattr(x)[1], args) if type(start) == int: start = [start] start = map(lambda x:preattr(x)[1], start) if new in serv.keys(): modlist = [] new_args = [] # Nouveaux arguments ? for arg in args: if arg not in serv[new]: new_args.append(arg) if new_args: modlist += ldap.modlist.modifyModlist({'args': serv[new]}, {'args': serv[new] + new_args}) new_date = [] # Nouvelle date ? for date in start: if date not in serv_dates[new]: new_date.append(date) if new_date: modlist += ldap.modlist.modifyModlist({'start': serv_dates[new]}, {'start': serv_dates[new] + new_date}) if modlist: try: self.conn.modify_s(serv_dn, modlist) except ldap.TYPE_OR_VALUE_EXISTS: # Pas grave pass # else rien à faire else: # Entrée non présente -> ajout modlist = ldap.modlist.addModlist({ 'objectClass': 'service', 'cn': new, 'args': args, 'start': start }) try: self.conn.add_s(serv_dn, modlist) except ldap.ALREADY_EXISTS: # Existe déja => rien à faire pass def make(self, entry, mode=''): """ Crée le bon objet à partir de entry. mode a la même signification que dans search. """ # On récupère la bonne classe nom_classe = (entry[1].get('objectClass') or ['none'])[0] nom_classe = nom_classe[0].upper() + nom_classe[1:] try: classe = eval(nom_classe) # On crée l'objet return classe(entry, mode, self.conn) except: raise ValueError(u"Impossible de créer l'objet %s" % nom_classe) def search(self, expression, mode=''): """ Recherche dans la base LDAP, expression est une chaîne : * soit une expression : champ1=expr1&champ2=expr2&champ3!=expr3... * soit un seul terme, dans ce cas cherche sur les champs de auto_search_champs Si mode='w', les instances crées seront en mode d'écriture """ if type(expression) == str: # Transformation de l'expression en utf-8 expression = unicode(expression, 'iso-8859-15').encode('utf-8') elif type(expression) == unicode: expression = expression.encode('utf-8') else: raise TypeError(u'Chaîne attendue') if not expression: return [] # On échappe les caractères spéciaux expression = expression.replace('\\', '\\\\').replace('(', '\\(').replace(')', '\\)') # Il faut un filtre par type d'objet de la base filtres = self.auto_search_champs.keys() result = {} for i in filtres: result[i] = [] # Fonction utile def build_filtre(champ, expr, neg=False): """ Retourne une chaine pour recherche dans la base LDAP du style (champ=expr) en adaptant les valeurs de expr au champ. Si neg = True, retourne le négatif : (!(champ=expr)) """ el = '' if champ in ['host', 'hostAlias']: if expr[-1] == '*': el = '(%s=%s)' % (champ, expr) elif '.' not in expr: el = '(%s=%s.*)' % (champ, expr) else: el = '(%s=%s*)' % (champ, expr) elif champ == 'macAddress': # Formatage adresse mac try: el = '(macAddress=%s)' % format_mac(expr) except: pass elif champ == 'paiement' and expr == 'ok': # Paiement donnant droit à une connexion maintenant ? # (il doit avoir payé pour l'année en cours ou pour # l'année précédente si on est en septembre # # Dans tous les cas, pour un adhérent, le paiement est # considéré non ok s'il n'a pas fourni sa carte d'etudiant # alors que l'on est desormais en periode de bloquage # définifif (cf config.py). if time.localtime()[1] == 9: # Pour septembre paiement année précédente ok el = "(|(paiement=%d)(paiement=%d))" % (ann_scol, ann_scol-1) else: el = "(paiement=%s)" % ann_scol # Doit-on bloquer en cas de manque de la carte d'etudiant ? if config.bl_carte_et_definitif: el = "(&(|(carteEtudiant=%d)(objectClass=club))%s)" % (ann_scol, el) elif champ[1:] == 'blacklist': el = '(blacklist=%s)' % expr else: # Cas général el = '(%s=%s)' % (champ, expr) if neg: el = '(!%s)' % el return el if '=' in expression: #### Recherche avec conditions explicites ## Construction des filtres # Initialisation filtre = {} filtre_seul = {} # filtre_seul[i] est True si la requête porte sur un champ # qui n'est pas dans toutes les machines, mais dans i ignore_filtre =[] # liste des filtres à ignorer for i in filtres: filtre[i] = '' filtre_seul[i] = False conds = expression.split('&') # Test de l'expression de recherche et classement par filtres for cond in conds: neg = False try: champ, expr = cond.strip().split('=') if champ[-1] == '!': # Négation pour ce champ champ = champ[:-1] neg = True except: raise ValueError(u'Syntaxe de recherche invalide (%s)' % cond) # Transformation de certains champs champ = self.trans.get(champ, champ) if expr == '': expr = '*' ok = False # si c'est un champ uniquement adherent (genre droit), on ignore les clubs et vice versa if champ in self.search_champs['club'] and champ not in self.search_champs['adherent']: if 'adherent' not in ignore_filtre: ignore_filtre.append('adherent') if champ in self.search_champs['adherent'] and champ not in self.search_champs['club']: if 'club' not in ignore_filtre: ignore_filtre.append('club') # Construction du filtre for i in filtres: if champ in self.search_champs[i]: filtre[i] += build_filtre(champ, expr, neg) ok = True if champ not in self.auto_search_machines_champs \ and champ not in self.non_auto_search_machines_champs: filtre_seul[i] = True if not ok: raise ValueError(u'Champ de recherche inconnu (%s)' % champ) ## Recherche avec chacun des filtres r = {} # contiendra les réponses par filtre # On efface les bons filtres si la requête porte sur des # champs spéciaux ok = False for i in self.ldap_machines_classes: ok = ok or filtre_seul[i] if ok: # Il y a au moins un champ spécial for i in self.ldap_machines_classes: if not filtre_seul[i]: filtre[i] = '' for i in filtres: if i in ignore_filtre or filtre[i] == '': # Filtre vide ou à ignorer filtre[i] = '' r[i] = None else: # Filtre valide filtre[i] = '(&(objectClass=%s)%s)' % (i, filtre[i]) r[i] = self.conn.search_s(self.base_dn, self.scope[i], filtre[i]) ## On a alors une liste de résultats ## r = {categorie1: [(result1), (result2), ...], ...} # Traitement if (r['machineFixe'] != None or r['machineWifi'] != None) \ and (r['adherent'] != None or r['club'] != None) \ and len(conds) > 1: # On renvoie toutes les machineCrans et borneWifi for i in 'machineCrans', 'borneWifi': if r[i] == None: continue for res in r[i]: result[i].append(self.make(res, mode)) # On croise maintenant les résultats machine et propriétaire # Traitement des machines mach_adh = [] # liste de dn d'adhérents et de clubs for res in r['machineFixe'] + r['machineWifi']: dn = ','.join(res[0].split(',')[-4:]) if dn[:3] != 'aid' and dn[:3] != 'cid': continue if dn not in mach_adh: mach_adh.append(dn) # Croisement bons_dn = [] # liste des dn d'adhérents qui correspondent aux critères for i in 'adherent', 'club': if r[i] == None: continue for a in r[i]: if a[0] in mach_adh and not a[0] in bons_dn: bons_dn.append(a[0]) result[i].append(self.make(a, mode)) # Maintenant c'est au tour des bonnes machines bons_dn2 = [] for i in 'machineFixe', 'machineWifi': if r[i] == None: continue for a in r[i]: dn = string.join(a[0].split(',')[-4:], ',') if dn in bons_dn and not a[0] in bons_dn2: bons_dn2.append(dn) result[i].append(self.make(a, mode)) else: # On retourne tout for i in filtres: if r[i] == None: continue for res in r[i]: result[i].append(self.make(res, mode)) else: ### Recherche d'une chaine sur tous les champs for i in filtres: # Construction du filtre filtre = '' for champ in self.auto_search_champs[i]: filtre += build_filtre(champ, expression) filtre = '(&(|%s)(objectClass=%s))' % (filtre, i) # Recherche for r in self.conn.search_s(self.base_dn, self.scope[i], filtre): result[i].append(self.make(r, mode)) # Backward-compatibilité result['machine'] = [] for i in self.ldap_machines_classes: result['machine'] += result[i] return result __machines = () def all_machines(self, graphic=False): """ Renvoie toutes les machines autorisées. Cela affiche des trucs et des bidules si graphic est à True. """ if graphic: from affich_tools import anim, cprint, OK if not self.__machines: # Récolte des données if graphic: cprint(u'Lecture base LDAP', 'gras') # Machines de l'assoce self.__machines = AssociationCrans(self.conn).machines() # Machines des adhérents et clubs de l'année en cours base = self.search('paiement=ok') base = base['adherent'] + base['club'] if graphic: a = anim('\tTri des machines', len(base)) for adh in base: if graphic: a.cycle() # Adhérent ayant payé l'année en cours if 'bloq' in adh.blacklist_actif(): # Adhérent ignoré continue self.__machines += adh.machines() if graphic: a.reinit() if graphic: print OK return self.__machines ############################################################################# class BaseClasseCrans(CransLdap): """ Méthodes de base des classes machines, et BaseProprietaire """ def __eq__(self, autre): """ Test d'égalité de deux instances de club/adhérent/machine, retourne True s'il s'agit du même club/adhérent/machine, False sinon """ return self.__class__ == autre.__class__ and self.id() == autre.id() def id(self): """ Retourne la valeur de l'attribu caractéristique de la classe (aid,mid,cid)""" try: s = self.dn.split(',')[0].split('=') if s[0] == self.idn: return s[1] except: return '' def __str__(self): """ Chainde identifiant l'objet de la forme 'uid=1245' """ return '%s=%s' % (self.idn, self.id()) def blacklist_actif(self): """ 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): """ 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._data.get('blacklist', []) if isinstance(self, Machine): # Il faut aussi regarder la blackliste du propriétaire p = self.proprietaire() bl_liste += p.blacklist() actifs = {} inactifs = {} for sanction in bl_liste: champs = sanction.split('$') s = champs[2] if is_actif(sanction): actifs.setdefault(s, []).append((champs[0], champs[1])) else: inactifs.setdefault(s, []).append((champs[0], champs[1])) return (actifs, inactifs) def blacklist(self, new=None): """ 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(). """ liste = self._data.setdefault('blacklist', [])[:] if new == None: return map(decode, liste) if type(new) == tuple: # Modification index = new[0] new = new[1] if new == '': liste.pop(index) self._set('blacklist', liste) return liste 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 = '$'.join(map(str, new)) new_c = preattr(new_c)[1] if index != -1: liste[index] = new_c else: liste.append(new_c) if liste != self._data['blacklist']: self._data['blacklist'] = liste self.modifs.setdefault('blacklist_' + new[2], None) 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 liste def restore(self): """ Restore les données à l'état initial (ou pas) """ self._data = self._init_data.copy() self.modifs = {} def historique(self): """ Retourne l'historique de l'objet """ return map(decode, self._data.get('historique', [])) def info(self, new=None): """ Pour ajouter une remarque new doit être la chaîne représentant la remarque à ajouter Pour modifier new doit être une liste de la forme: [ index de la remarque à modifier, nouvelle remarque ] l'index est celui obtenu dans la liste retournée par info() """ if not self._data.has_key('info'): self._data['info'] = [] liste = list(self._data['info']) if new == None: return map(decode, liste) if type(new) == list: # Modif index = new[0] l, new = preattr(new[1]) if not new: # Supression remarque liste.pop(index) else: # Modif remarque liste[index] = new elif type(new) == str: # Remarque supplémentaire l, new = preattr(new) if not new: # On ajoute pas de remarque vide return liste # Ajout à la liste liste = liste + [new] else: raise TypeError self._set('info', liste) return liste def _save(self): """ Sauvegarde dans la base LDAP """ if not self.modifs: # Rien à faire return [] if not self.dn: # Enregistrement à placer en tête de base self.dn = self.base_dn # Construction de l'historique if not self._init_data: if self.idn=='fid': modif = preattr('création')[1] else: modif = 'inscription' else: ### ON NE TOUCHE PAS A SELF.MODIFS, IL EST UTILISÉ PLUS LOIN !!!!!!! # Dictionnaire local des modifs modif = {} # Cas spécial if "solde" in self.modifs: diff = float(self._init_data.get('solde', [0])[0]) - float(self._data.get('solde', [0])[0]) if diff > 0: modif['solde'] = "debit %s Euros" % str(diff) else: modif['solde'] = "credit %s Euros" % str(-diff) # Formate les entrées de l'historique de la forme champ (ancien -> nouveau) # On suppose que le champ apparaît forcément dans l'enregistrement for champ in ['chbre', 'nom', 'prenom', 'mail', 'tel', 'rewriteMailHeaders', 'contourneGreylist', 'puissance', 'canal', 'prise', 'responsable', 'macAddress', 'ipHostNumber', 'host', 'positionBorne', 'derniereConnexion', 'hotspot']: if champ in self.modifs: if champ not in self._init_data.keys(): valeur_initiale = 'N/A' else: valeur_initiale = self._init_data[champ][0] if not self._data.get(champ, []): valeur_finale = 'N/A' else: valeur_finale = self._data[champ][0] modif[champ] = '%s (%s -> %s)' % (champ, valeur_initiale, valeur_finale) # Formate les entrées de l'historique de la forme champ+diff-diff for champ in ['droits', 'controle', 'paiement', 'carteEtudiant', 'mailAlias', 'hostAlias', 'exempt', 'nvram', 'portTCPin', 'portTCPout', 'portUDPin', 'portUDPout', 'homepageAlias']: if champ in self.modifs: if champ == 'controle': # Ce n'est pas pareil que self._init_data.get('controle', ['']) # qui peut renvoyer une liste vide (petite erreur de choix # dans la première implémentation de controle) ancien = (self._init_data.get('controle') or [''])[0] nouveau = (self._data.get('controle') or [''])[0] else: # Là, on bosse directement sur les listes renvoyées par get ancien = self._init_data.get(champ, []) nouveau = self._data.get(champ, []) # On établit le diff diff = ''.join([ '+%s' % decode(d) for d in nouveau if d not in ancien ]) diff += ''.join([ '-%s' % decode(d) for d in ancien if d not in nouveau ]) modif[champ] = champ + diff # On recolle tous les morceaux liste_historique = [] for champ in self.modifs.keys(): # champs pour lesquels on ne veut pas d'historique if champ in ['userPassword']: continue ligne = modif.get(champ, champ) if self.modifs[champ] != None: ligne += ' [%s]' % self.modifs[champ] liste_historique.append(ligne) modif = ', '.join(liste_historique) timestamp = time.localtime() hist = "%s, %s" % ( time.strftime(date_format, timestamp), script_utilisateur ) if self.modifs.has_key('derniereConnexion'): # On nettoie l'historique pour ne garder que la dernière modification # (celle en cours) self._data['historique'] = [x for x in self._data['historique'] if 'derniereConnexion' not in x] # On loggue try: fd = file('%s/%s_%s_%s' % ("%s/logs" % config.cimetiere, str(self.__class__).split('.')[-1], time.strftime('%Y-%m-%d-%H:%M', timestamp), self.nom()), 'wb') fd.write("%s\n" % self._data) fd.close() except: pass # Suffit-t-il d'ajouter un item au dernier élément de l'historique ? try: if modif: dern = self._data['historique'][-1].split(': ', 2) if dern[0] == hist: # Même date et même cableur if modif not in dern[1].split(', '): # Qqch de plus de modifié self._data['historique'][-1] = self._data['historique'][-1] + ', ' +modif else: # Nouvelle entrée # NE PAS UTILISER L'OPERATEUR += ICI sinon self._init_data aussi modififié self._data['historique'] = self._data['historique'] + [ preattr("%s : %s" % ( hist, modif ))[1] ] except: # Nouvelle inscription self._data['historique'] = [ "%s : %s" % ( hist, modif ) ] if not self._init_data: ### Nouvel enregistrement # Génération du dn res = self.conn.search_s(self.base_dn, 2, self.filtre_idn) vidn = 1 vidns = [] # Liste des dn pris for r in res: # r = ( dn, {} ) r = r[0].split(',')[0] if r[:4] != '%s=' % self.idn: continue vidns.append(int(r[4:])) if self.idn=='fid': # Pour une facture on prend un nouveau numéro vidn = max([0]+vidns)+1 else: # Sinon on prend le premier libre while vidn in vidns: vidn += 1 self.dn = '%s=%s,%s' % (self.idn, vidn, self.dn) self._data[self.idn]= [ '%d' % vidn ] # Ecriture modlist = ldap.modlist.addModlist(self._data) self.conn.add_s(self.dn, modlist) else: ### Modification entrée if not self._modifiable: raise RuntimeError(u'Objet non modifiable : %s' % str(self)) modlist = ldap.modlist.modifyModlist(self._init_data, self._data) try: self.conn.modify_s(self.dn, modlist) except ldap.TYPE_OR_VALUE_EXISTS, c: champ = c.args[0]['info'].split(':')[0] raise RuntimeError(u'Entrée en double dans le champ %s' % champ) ### Génération de la liste de services à redémarrer # Quasiement tout est traité dans les classes filles. if hasattr(self, "_blacklist_restart"): for n, t in self._blacklist_restart.items(): self.services_to_restart("blacklist_%s"%n, [], t) # Reinitialisation self._init_data = self._data.copy() def _delete(self, dn, comment=''): """ Sauvegarde puis destruction du dn (et des sous-dn) fourni """ # Commentaires comment = preattr(comment)[1] self.modifs.setdefault('destruction (%s)' % comment, None) self._save() # Sauvegarde t = str(self.__class__).split('.')[-1] fd = open('%s/%s/%s_%s' % (config.cimetiere, t, time.strftime('%Y-%m-%d-%H:%M'), self.nom()), 'wb') self.conn = None # Fermeture des connexions à la base sinon cPickle ne marchera pas cPickle.dump(self, fd, 2) fd.close() index = u"%s, %s : %s %s # %s\n" % (time.strftime(date_format), script_utilisateur, t, self.Nom() , decode(comment)) self.connect() # Reconnexion à la base # Destruction data = self.conn.search_s(dn, 2) data.reverse() # Necessaire pour détruire d'abord les sous-dn for r in data: self.conn.delete_s(r[0]) try: log = open(config.cimetiere + '/index', 'a') log.write(index) log.close() except: pass def _set(self, champ, val, comment=None): """ Met à jour les données de _data et modifie modifs si besoin. Si val=[] ou None, supprime le champ. """ # On convertit éventuellement en utf-8 avant # On n'utilise pas preattr pour éviter de tout casser... mais # il faudra à l'avenir que _set reçoive toujours en unicode # et éviter l'utilisation de preattr autant que possible def convert(x): if type(x) is unicode: return x.encode('utf-8') else: return x val = map(convert, val) comment = convert(comment) # On fait le changement et on note s'il y a vraiment eu une modif if self._data.get(champ, []) != val: if val: self._data[champ] = val else: self._data.pop(champ) if self.modifs.get(champ) == None or comment == None: self.modifs[champ] = comment else: # Ici, self.modifs[champ] et comment devraient être tous deux # des chaînes de caractères self.modifs[champ] += ', ' + comment ############################################################################# class BaseProprietaire(BaseClasseCrans): """ Méthodes de bases pour les classes Adherent et Club """ def __init__(self, data=(), mode='', conn=None): """ Si data est fourni initialise l'adhérent avec les valeurs données Format de data : tuple comme retourné par une recherche dans la base ldap: ( dn, { donnée }) Si mode='w' : le propriétaire pourra être modifié Attention, si mode ='w' mais si l'objet est déja locké il n'y a pas d'erreur vérifier l'obtetion du lock grace à la valeur de _modifiable (si =w c'est bon) Il est inutile de préciser le mode pour un nouveau proprietaire conn est une instance de la classe de connexion à la base LDAP """ self.conn = conn if not self.conn: self.connect() if type(data) != tuple: raise TypeError self.modifs = {} if data: self.dn = data[0] if mode == 'w': try: self.lock(self.idn, self.id()) self._modifiable = 'w' except EnvironmentError , c: self._modifiable = 0 else: self._modifiable = 0 # Utile pour construire l'instruction LDAP de modif self._init_data = data[1].copy() self._data = data[1] else: # Propriétaire vide self.dn = '' # Génération du reste au moment de l'écriture self._data = { 'objectClass': [ self.objectClass ] } self._init_data = {} self._modifiable = 'w' def chsh(self, new=None): """ Retourne ou change le shell de l'adhérent """ if new == None: try: return decode(self._data.get('loginShell', [''])[0]) except: return '' else: new = preattr(new)[1] self._set('loginShell', [new]) return new def alias(self, new=None): """ Création ou visualisation des alias mail Même sytème d'argument que la méthode info. """ if not self._data.has_key('mailAlias'): self._data['mailAlias'] = [] liste = list(self._data['mailAlias']) if new == None: return map(decode, liste) if type(new) == list: # Modif index = new[0] new = new[1] if new == '': # Supression alias liste.pop(index) self._set('mailAlias', liste) return liste else: new = new.replace('@crans.org', '') index = -1 # Tests l, new = preattr(new) new = new.lower() if l<2: raise ValueError(u"Alias trop court.") for c in new[:]: if not c in (string.letters + string.digits + '-_.'): raise ValueError(u"Alias : seuls les caractères alphanumériques, le -, le _ et le . sont autorisés." ) if new[0] not in string.letters: raise ValueError(u"Le premier caractère de l'alias doit être alphabétique.") if mailexist(new): raise ValueError(u"Alias existant ou correspondand à un compte.") if index != -1: liste[index] = new else: liste = liste + [ new ] # Lock de mailAlias self.lock('mailAlias', new) self._set('mailAlias', liste) return liste def homepageAlias(self, new=None): """ Creation ou visualisation des alias de page perso. Meme systeme d'arguments que pour info. """ liste = self._data.get('homepageAlias', [])[:] if new == None: return liste def validate(alias): alias = alias.lower() if not sre.match(r"^[a-z0-9](\.?[-a-z0-9]+)*$", alias): raise ValueError("Alias incorrect : %s" % alias) if "." not in alias: alias += ".perso.crans.org" if alias.endswith(".crans.org") and not alias.endswith(".perso.crans.org"): raise ValueError("Les alias crans doivent se terminer par .perso.crans.org") # ...peut-etre faire une verification de domaine ici... # On convertit en str (il ne devrait plus y avoir de caractere Unicode) return str(alias) if type(new) == list: # Modif index = new[0] new = new[1] if not new: # Supression alias liste.pop(index) else: # Modification alias new = validate(new) liste[index] = new elif type(new) == str: # Alias supplémentaire if not new: # On n'ajoute pas d'alias vide return liste # Ajout à la liste new = validate(new) liste.append(new) else: raise TypeError # Lock de l'alias (a faire) # self.lock('homepageAlias', new) self._set('homepageAlias', liste) return liste def machines(self): """ Retourne les machines (instances) appartenant à la classe """ # Le champ id n'est pas initialisé lorsque le proprio est en cours # de création if self.id(): res = [] for r in self.conn.search_s('%s=%s,%s' % (self.idn, self.id(), self.base_dn), 1, Machine.filtre_idn): res.append(self.make(r, self._modifiable)) return res else: return [] def machines_fixes(self): """ Retourne les machines fixes appartenant à l'instance """ if self.id(): res = [] for r in self.conn.search_s('%s=%s,%s' % (self.idn, self.id(), self.base_dn), 1, 'objectClass=machineFixe'): res.append(self.make(r, self._modifiable)) return res else: return [] def machines_wifi(self): """ Retourne les machines wifi appartenant à l'instance """ if self.id(): res = [] for r in self.conn.search_s('%s=%s,%s' % (self.idn, self.id(), self.base_dn), 1, 'objectClass=machineWifi'): res.append(self.make(r, self._modifiable)) return res else: return [] def factures(self): """ Retourne les factures (instances) appartenant à la classe """ # Le champ id n'est pas initialisé lorsque le proprio est en cours # de création if self.id(): res = [] for r in self.conn.search_s('%s=%s,%s' % (self.idn, self.id(), self.base_dn), 1, Facture.filtre_idn): res.append(self.make(r, self._modifiable)) return res else: return [] def solde(self, operation=None, comment=None): """ Retourne ou modifie le solde d'un propriétaire operation doit être un nombre positif ou négatif (string ou int ou float) comment est un commentaire à rajouter dans l'historique """ solde = float(self._data.get('solde', [0])[0]) if operation == None: return solde # On effectue une opération try: new = solde + float(str(operation).replace(',', '.')) except ValueError: raise ValueError(u"Il faut donner un nombre en argument.") # découvert accepté if new < config.impression.decouvert: raise ValueError(u"Solde minimal atteint, opération non effectuée.") self._set('solde', [str(new)], comment) return new def controle(self, new=None): """ Controle du tresorier New est de la forme [+-][pck] (p pour le paiement, c pour la carte, k pour la caution) Retourne une chaine contenant une combinaison de p, c, k. """ actuel = self._data.get('controle', ['']) if not actuel: actuel = '' else: actuel = actuel[0] if new == None: return actuel if not sre.match(r'^[+-][pck]$', new): raise ValueError('modification de controle incorrecte') for c in 'pck': if new == '+%s' % c and c not in actuel: actuel += c if new == '-%s' % c: actuel = actuel.replace(c, '') if actuel == '': # La base LDAP n'accepte pas les chaînes vides. # On supprime directement le champ controle if self._data.has_key('controle'): if self._data.pop('controle') != []: # Il y avait vraiment qqch avant if 'controle' not in self.modifs: self.modifs.setdefault('controle', None) else: self._set('controle', [actuel]) return actuel def contourneGreylist(self, contourneGreylist=None): """ Retourne ou change la greylist pour le compte * True : contourne le GreyListing * False : ne contourne pas le greylisting Au niveau de la base, on considère la présence ou l'absence d'un champ contourneGreylist=OK. """ # Pour postfix il faut retourner : # OK : contourne la greyliste # cf. man 5 access # si l'adhérent n'a pas de compte, on lève une exception if not self.compte(): raise NotImplementedError, u"L'adhérent n'a pas de compte" # tente de modifier la valeur if contourneGreylist == True: self._set('contourneGreylist', ['OK']) elif contourneGreylist == False: self._set('contourneGreylist', []) elif contourneGreylist != None: raise ValueError, u"contourneGreylist prend un booléen comme argument" # renvoie la valeur trouvée dans la base return bool(self._data.get('contourneGreylist', [])) def verifyPasswd(self, passwd): """ Retourne True si le mot de passe est bon, ou False si le mot de passe n'est pas bon. """ if not self.compte(): raise NotImplementedError, u"L'adhérent n'a pas de compte" return ldap_passwd.checkpwd(passwd, self._data['userPassword'][0]) def changePasswd(self, passwd): """ Modifie le mot de passe de l'adhérent """ if not self.compte(): raise NotImplementedError, u"L'adhérent n'a pas de compte" self._set('userPassword', [ldap_passwd.mkpasswd(passwd)]) def forward(self, new = None): """ Modifie ou retourne l'adresse de forward de l'adhérent NB : il faut avoir un sudo sur /usr/scripts/gestion/mail_config.py sinon cette metode ne fonctionnera pas """ if not self.compte(): raise NotImplementedError, u"L'adhérent n'a pas de compte" return config_mail.MailConfig(uid=self._data['uid'][0], forward = new)['forward'] def spam(self, new = None): """ Modifie ou retourne le traitement des spams de l'adhérent new doit être 'accepte', 'marque' ou 'supprime' NB : il faut avoir un sudo sur /usr/scripts/gestion/mail_config.py sinon cette metode ne fonctionnera pas """ if not self.compte(): raise NotImplementedError, u"L'adhérent n'a pas de compte" return config_mail.MailConfig(uid=self._data['uid'][0], spam = new)['spam'] def home(self): """ Retourne le home de l'adhérent """ if not self.compte(): raise NotImplementedError, u"L'adhérent na pas de compte" return self._data['homeDirectory'][0] def uidNumber(self): """ Retourne l'uidNumber de l'adhérent """ if not self.compte(): raise NotImplementedError, u"L'adhérent na pas de compte" return self._data['uidNumber'][0] def paiement(self, action=None): """ Action est un entier représentant une année si positif ajoute l'année à la liste si négatif le supprime """ return self._an('paiement', action) def delete(self, comment=''): """Destruction du proprietaire""" for m in self.machines(): # Destruction machines m.delete(comment) for f in self.factures(): # Destruction factures f.delete(comment) self._delete(self.dn, comment) try: if self.compte(): args = self._data['uid'][0] + ',' args+= self._data['homeDirectory'][0] self.services_to_restart('del_user', [ args ] ) except: # Si ne peux avoir de compte pass def save(self): """ Enregistre l'adhérent ou le club courant dans la base LDAP Ajoute le mail de bienvenue à la liste des services à redémarrer Retourne une chaîne indiquant les opération effectuées. """ # Note: un peu trop de fonctions pour un club mais ce n'est pas génant ret = '' if self._init_data: nouveau = 0 # Reconfiguration switch si changement de chambre et si machine fixe if 'chbre' in self.modifs: if self.machines_fixes(): self.services_to_restart('switch', [self._data['chbre'][0]]) self.services_to_restart('switch', [self._init_data.get('chbre', '')[0]]) else: nouveau = 1 if 'chbre' in self.modifs and '????' in [ self._init_data.get("chbre", [''])[0] , self._init_data.get("chbre", [''])[0] ]: self.services_to_restart('bl_chbre_invalide') # Enregistrement self._save() # Message de sortie if nouveau: ret += coul(u"%s inscrit avec succès." % self.Nom(), 'vert') if self.idn !='cid' and self.etudes(1) != "Pers": # Mail de bienvenue self.services_to_restart('mail_bienvenue', [self.mail().encode('iso-8859-15')]) else: ret += coul(u"Modification %s effectuée avec succès." % self.Nom(), 'vert') # Changements administratifs test_carte = 'carteEtudiant' in self.modifs if test_carte and self.machines(): self.services_to_restart('bl_carte_etudiant') if 'paiement' in self.modifs or (config.bl_carte_et_definitif and test_carte): for m in self.machines(): self.services_to_restart('macip', [m.ip()] ) self.services_to_restart('classify', [m.ip()] ) self.services_to_restart('dns') if isinstance(m, MachineWifi): self.services_to_restart('conf_wifi_ng') self.services_to_restart('ragnarok-dhcp') else: self.services_to_restart('switch', [self.chbre()]) self.services_to_restart('rouge-dhcp') # Vérification si changement de bât, ce qui obligerai un changement d'IP if 'chbre' in self.modifs and self.chbre() != '????': # Verif si machines avec bonnes ip err = 0 for m in self.machines(): if isinstance(m, MachineWifi): # Machine Wifi continue # Machine fixe ip = m.ip() try: # Tentative de changement d'IP de la machine m.ip(ip) except ValueError: # IP invalide, on la change ret += "\nChangement d'IP machine %s : " % m.nom() try: ret += "%s -> %s" % ( ip, m.ip('') ) m.save() except Exception, c: ret += coul(u'ERREUR : %s' % c.args[0], rouge) err = 1 if err: ret += '\nEssayer de corriger les erreurs machines en éditant celles-ci.\n' # Faut-il créer un compte sur vert ? if 'compte' in self.modifs: ret += u'\nUn compte a été créé :\n login : %s\n' % self.compte() args = self._data['homeDirectory'][0] + ',' args+= self._data['uidNumber'][0] + ',' args+= self._data['uid'][0] self.services_to_restart('home', [ args ]) r = prompt(u"Attribuer tout de suite un mot de passe ? [O/N]", "O") if r == 'O' or r == 'o': chgpass(self.dn) else: ret += coul(u' Il faudra penser à attribuer un mot de passe\n', 'jaune') # Modif des droits ? if 'droits' in self.modifs: self.services_to_restart('droits') self.services_to_restart('mail_modif', ['uid=%s' % self._data['uid'][0]]) # Remise à zero self.modifs = {} return ret def _an(self, champ, action): """ Champ est un champ contenant une liste d'entiers Action est un entier représentant une année si positif ajoute l'année à la liste si négatif le supprime """ if not self._data.has_key(champ): trans = [] else: # On va travailler sur une liste d'entiers trans = map(int, self._data[champ]) if action == None: return trans if type(action) != int: raise TypeError touched = False if action>0 and action not in trans: trans.append(action) touched = True elif action<0 and -action in trans: trans.remove(-action) touched = True if touched and champ not in self.modifs: self.modifs.setdefault(champ, None) trans.sort() self._data[champ] = map(str, trans) return self._data[champ] ############################################################################# class Adherent(BaseProprietaire): """ Classe de définition d'un adhérent """ objectClass = 'adherent' idn = 'aid' filtre_idn = '(objectClass=adherent)' ### Méthodes Nom utilisée lors de l'affichage des propriétés ### (commune avec les classes AssociationCrans et Club) def Nom(self): """ Retourne prenom nom """ return "%s %s" % ( self.prenom() , self.nom() ) def nom(self, new=None): return self.__nom_prenom('nom', new) def prenom(self, new=None): return self.__nom_prenom('prenom', new) def __nom_prenom(self, champ, new): if new == None: return decode(self._data.get(champ, [''])[0]) l, new = preattr(new) new = new.capitalize() for c in new[:]: if c not in (string.letters + '- ' + preattr(accents)[1] ): raise ValueError(u"Seuls les caractères alphabétiques, l'espace et le - sont permis dans %s." % champ.replace(u'e', u'é') ) if l<2: raise ValueError(u"%s trop court." % champ.capitalize().replace(u'e', u'é')) if new[0] not in string.letters: raise ValueError(u"Le premier caractère du %s doit être une lettre" % champ.replace(u'e', u'é') ) self._set(champ, [new]) if self._data.has_key('gecos'): gecos = '%s %s' % tuple(map(lambda x: strip_accents(x.capitalize()), (self.prenom(), self.nom()))) self._data['gecos'] = [ preattr(gecos)[1] + ',,,' ] return new def tel(self, new=None): if new == None: return self._data.get('tel', [''])[0] if new != 'inconnu': l, new = preattr(new) if not new.isdigit() or l<6 or l>15: raise ValueError(u"Numéro de téléphone incorrect (il doit comporter uniquement des chiffres).") self._set('tel', [new]) return new def chbre(self, new=None): """ Défini la chambre d'un adhérent, EXT pour personne extérieure au campus """ if new == None: return decode(self._data.get('chbre', [''])[0]) l, new = preattr(new) if l == 0: raise ValueError(u"Chambre incorrecte.") if new.upper() == 'EXT': # N'est pas ou plus sur le campus # Machine fixe ? # for m in self.machines(): # if not isinstance(m, MachineWifi): # raise ValueError(u'Un adhérent en dehors du campus ne doit pas avoir de machine fixe.') self._set('chbre', ['EXT']) return 'EXT' elif new.upper() == '????': # On ne sait pas ou est l'adhérent self._set('chbre', ['????']) return '????' new = new.capitalize() bat = new[0].lower() if bat in annuaires.chbre_prises.keys(): # On a la liste des chambres chbres = annuaires.chbre_prises[bat].keys() if new[1:] not in chbres or len(new)<4 or not new[1:4].isdigit(): chbres.sort() aide = u"Chambre inconnue dans le batiment, les chambres valides sont :" a = 0 for c in chbres: if len(c)>=3 and not c[:3].isdigit(): # C'est un local club continue if int(a/14)>int((a-1)/14): aide += '\n ' if c[0] != 'X': aide += c.ljust(5) a = a+1 aide += u'\n' aide += u" " + annuaires.aide.get(bat, '') raise ValueError(aide) else: raise ValueError(u'Bâtiment inconnu.') # La chambre est valide, est-elle déja occupée ? test = self.exist('chbre=%s' % new) if test: search = test[0].split(',')[0] if search.split('=')[0] != 'aid': raise ValueError(u'Chambre déjà occupée.') adh = self.search(search, self._modifiable)['adherent'] if len(adh) != 1: raise ValueError(u'Chambre déjà occupée.') else: raise ValueError(u'Chambre déjà occupée.', adh[0]) # Lock de la chambre self.lock('chbre', new) self._set('chbre', [new]) return new def adresse(self, new=None): """ Défini l'adresse pour les personnes extérieures (dont la chambre = EXT) L'adresse est une liste de 4 éléments : numero, rue, code postal, ville """ if new == None: if self.chbre() != 'EXT': # Personne sur le campus return [u'', u'', u'', u''] else: addr = self._data.get('postalAddress', ['','', '', ''])[:4] if len(addr) < 4: addr = addr + ['']*(4-len(addr)) return map(decode, addr) if type(new) != list and len(new) != 4: raise TypeError l_min = [ 2, 0, 5, 2 ] for i in range(0, 4): l, new[i] = preattr(new[i]) if l < l_min[i]: raise ValueError(u"Adresse incorrecte.") # Correction si necessaire if not new[1]: new[1] = ' ' self._set('postalAddress', new) return new def mail(self, new=None): if new == None: return decode(self._data.get('mail', [''])[0]) l, new = preattr(new) new = new.lower() #Emplacement de l'@ a = new.find('@') #Emplacement du . final b = new.rfind('.') # Les tests : # exactement un @ # 2 ou 3 caractères après le . final # @ pas en premier ni juste avant le dernier . if new.count('@') != 1 \ or not ( l-b == 4 or l-b == 3) \ or a<1 or b-a<2: raise ValueError(u"Adresse mail incorrecte.") # Pas de caractèrs bizarres for l in new[:]: if not l in (string.lowercase + string.digits + '-_.@'): raise ValueError(u"Caractère interdits dans l'adresse mail (%s)." % l) # Pour les vicieux if sre.match('.*crans.(org|ens-cachan.fr)$', new): raise ValueError(u"Adresse mail @crans interdite ici") # Il ne doit pas y avoir de compte self.supprimer_compte() self._set('mail', [new]) return new def email(self, new=None): """ Retourne l'adresse mail, ajoute le @crans.org si nécessaire """ # pour la compatibilité entre les fonctions if new: self.mail(new) # ajout du @crans.org si nécessaire mail = self.mail() if not '@' in mail: mail += '@crans.org' return mail def supprimer_compte(self): u""" Supprime le compte sur zamok. Penser à définir l'adresse mail après. """ self._set('mail', ['']) self._data['objectClass'] = ['adherent'] for c in [ 'uid', 'cn', 'shadowLastChange', 'shadowMax', 'shadowWarning', 'loginShell', 'userPassword', 'uidNumber', 'gidNumber', 'homeDirectory', 'gecos', 'droits', 'mailAlias', 'canonicalAlias', 'rewriteMailHeaders', 'contourneGreylist', 'homepageAlias' ]: try: self._data.pop(c) except: pass def etudes(self, index_or_new): """ Retourne l'un des 3 champs études (selon index_or_new si entier) """ if type(index_or_new) == int: if self._data.has_key('etudes'): return decode(self._data['etudes'][index_or_new]) else: return '' if type(index_or_new) != list: raise TypeError if not self._data.has_key('etudes'): self._data['etudes'] = ['', '', ''] # Pas grand chose à faire à part vérifier que ce sont bien des chaines if len(index_or_new) != 3: raise ValueError(u"Format études non valides.") new = ['', '', ''] for i in range(0, 3): n = preattr(index_or_new[i])[1] if n in new or n == '': raise ValueError(u"Etudes non valides.") new[i] = n self._set('etudes', new) return new def carteEtudiant(self, action=None): """ Action est un entier représentant une année si positif ajoute l'année à la liste si négatif le supprime """ return self._an('carteEtudiant', action) def checkPassword(self, password): """Vérifie le mot de passe de l'adhérent""" anon = ldap.initialize(uri) try: r = anon.simple_bind(self.dn, password) anon.result(r) except ldap.INVALID_CREDENTIALS: # A priori, le mot de passe est pas bon, il se peut aussi # que l'utilisateur n'existe pas return False return True def compte(self, login=None, uidNumber=0, hash_pass='', shell=config.login_shell): """ Création d'un compte à un adhérent (la création se fait après l'écriture dans la base par la méthode save) Si login = None, retourne le compte de l'adhérent """ if not login: if self.mail().find('@') != -1: return '' else: return self.mail() # Supression des accents et espaces login = strip_accents(login) l, login = preattr(login) login = login.lower() if 'posixAccount' in self._data['objectClass']: if login != self._data['uid']: # A déja un compte raise ValueError(u"L'adhérent à déjà un compte. Login : %s" % self._data['uid'][0]) else: return login for c in login[:]: if not c in (string.letters + '-'): raise ValueError(u"Seuls les caractères alphabétiques non accentués et le - sont autorisés dans le login.") if l < 2: raise ValueError(u"Login trop court.") if l > config.maxlen_login: raise ValueError(u"Login trop long.") if login[0] == '-': raise ValueError(u"- interdit en première position.") if mailexist(login): raise ValueError(u"Login existant ou correspondant à un alias mail.", 1) home = '/home/' + login if os.path.exists(home): raise ValueError(u'Création du compte impossible : home existant', 1) if os.path.exists("/var/mail/" + login): raise ValueError(u'Création du compte impossible : /var/mail/%s existant' % login, 1) # Lock du mail self.lock('mail', login) self._data['mail'] = [login] if not 'compte' in self.modifs: self.modifs.setdefault('compte', None) # Création de l'alias canonique if self.nom() and self.prenom(): a = '%s.%s' % (self.prenom().capitalize(), self.nom().capitalize()) self.canonical_alias(a) self._data['objectClass'] = ['adherent', 'cransAccount', 'posixAccount', 'shadowAccount'] self._data['uid'] = [login] self._data['cn'] = [preattr(self.Nom())[1]] #self._data['shadowLastChange'] = [ '12632' ] #self._data['shadowMax'] = [ '99999'] #self._data['shadowWarning'] = [ '7' ] self._data['loginShell'] = [shell] if hash_pass: self._data['userPassword'] = [hash_pass] if uidNumber: if self.exist('(uidNumber=%s)' % uidNumber): raise ValueError(u'uidNumber pris') else: pool_uid = range(1001, 9999) random.shuffle(pool_uid) while len(pool_uid) > 0: uidNumber = pool_uid.pop() # On choisit une IP if not self.exist('(uidNumber=%s)'): # On a trouvé un uid libre pool_uid.append(uidNumber) break if not len(pool_uid): raise ValueError(u"Plus d'uid disponibles !") try: self.lock('uidNumber', str(uidNumber)) except: # Quelqu'un nous a piqué l'uid que l'on venait de choisir ! return self.compte(login, uidNumber, hash_pass, shell) self._data['uidNumber']= [str(uidNumber)] self._data['gidNumber'] = [str(config.gid)] self._data['homeDirectory'] = [ preattr(home)[1] ] gecos = '%s %s' % tuple(map(lambda x: strip_accents(x.capitalize()), (self.prenom(), self.nom()))) self._data['gecos'] = [ preattr(gecos)[1] + ',,,' ] return decode(login) def canonical_alias(self, new=None): """ Retourne ou défini l'alias canonique""" if new == None: try: return decode(self._data['canonicalAlias'][0]) except: return '' else: a = strip_accents(new) a = preattr(a)[1] if not mailexist(a): # Attribution de l'alias, sinon on passe # Lock de canonicalAlias self.lock('canonicalAlias', a) # Attribution self._set('canonicalAlias', [a]) return a def droits(self, droits=None): """ droits est la liste des droits à donner à l'utilisateur """ if droits != None and 'cransAccount' not in self._data.get('objectClass', []): raise EnvironmentError(u'Il faut avoir un compte pour avoir des droits.') if droits == None: return map(decode, self._data.get('droits', [])) if not isadm: raise EnvironmentError(u'Il faut être administrateur pour effectuer cette opération.') if type(droits) != list: raise TypeError(u'Une liste est attendue') new = [] for droit in droits: if droit == '': continue droit = unicode(droit.strip(), 'iso-8859-15') if droit not in droits_possibles: raise ValueError(u'Droit %s incorrect' % droit) new.append(droit.encode('utf-8')) if new != self._data.get('droits', []): self._set('droits', new) return new def rewriteMailHeaders(self, rewrite=None): """ Réécriture des entêtes mail avec l'alias canonique * True : réécrit les en-têtes * False : ne réécrit pas les en-têtes Au niveau de la base, on considère l'absence ou la présence de rewriteMailHeaders=TRUE. """ # si l'adhérent n'a pas de compte, on lève une exception if not self.compte(): raise NotImplementedError, u"L'adhérent n'a pas de compte" # tente de modifier la valeur if rewrite == True: self._set('rewriteMailHeaders', ['TRUE']) elif rewrite == False: self._set('rewriteMailHeaders', []) elif rewrite != None: raise ValueError, u"rewriteMailHeaders prend un booléen comme argument" # renvoie la valeur trouvée dans la base return bool(self._data.get('rewriteMailHeaders', [])) def derniereConnexion(self, new=False): """ Date de dernière connexion. Le format est le nombre de secondes écoulées depuis Epoch. Si new est donné, met à jour la valeur si elle est plus récente. """ # si l'adhérent n'a pas de compte, on lève une exception if not self.compte(): raise NotImplementedError, u"L'adhérent n'a pas de compte" current = int(self._data.get('derniereConnexion', ['0'])[0]) if new: # lève une exception si l'argument n'est pas valide new = int(new) if new > current: self._set('derniereConnexion', [str(new)]) current = new return current class Club(BaseProprietaire): """ Classe de définition d'un club """ idn = 'cid' filtre_idn = '(objectClass=club)' objectClass = 'club' def Nom(self, new=None): """ Défini ou retourne le nom du club """ if new == None: return decode(self._data.get('nom', [''])[0]) l, new = preattr(new) new = new.capitalize() if l<2: raise ValueError(u"Nom trop court.") self._set('nom', [new]) return new def etudes(*args): """ Etudes bidon pour club """ return u'N/A' def nom(self): """ Retourne le nom du club, utilisé lors de la destruction """ return strip_accents(self.Nom()) def carteEtudiant(self, pd=None): return [ ann_scol ] def responsable(self, adher=None): """ Responsable du club, adher doit être une instance de la classe adhérent """ if adher == None: aid = decode(self._data.get('responsable', [''])[0]) if aid: return self.search('aid=%s' % aid)['adherent'][0] else: return '' if adher.__class__ != Adherent: raise ValueError if not adher.id(): raise AttributeError(u'Adhérent invalide') self._set('responsable', [adher.id()]) return adher def chbre(self, new=None): """ Défini le local du club new doit être une des clefs de l'annuaire locaux_clubs""" if new == None: return decode(self._data.get('chbre', [''])[0]) annu = annuaires.locaux_clubs() if new not in annu.keys(): raise ValueError(u'Local invalide', annu) self._set('chbre', [new]) return new def local(self): """ Retourne le local à partir de la chambre enregistrée et de la conversion avec l'annuaire locaux_clubs """ annu = annuaires.locaux_clubs() return decode(annu.get(self.chbre(), '')) def compte(self, login=None): """ Créé un compte au club sur vert""" if login == None: return self._data.get('uid', [''])[0] # Génération du login : club- login = login.lower() if not sre.match('^club-', login): login = 'club-' + login if not sre.match('^[a-z0-9]*[a-z]+[a-z0-9-]*$', login): raise ValueError(u'Login incorrect') login = preattr(login)[1] if 'posixAccount' in self._data['objectClass']: if login != self._data['uid']: # A déja un compte raise ValueError(u"Le club à déjà un compte. Login : %s" % self._data['uid'][0]) else: return login if mailexist(login) and not os.system('/usr/sbin/list_lists | grep -qi %s' % login): # la 2ème vérif est pour vérifier que ce n'est pas la ML du club raise ValueError(u"Login existant ou correspondant à un alias mail.", 1) home = '/home/' + login.replace('-', '/', 1) if os.path.exists(home): raise ValueError(u'Création du compte impossible : home existant', 1) if os.path.exists("/var/mail/"+login): raise ValueError(u'Création du compte impossible : /var/mail/%s existant'%login, 1) # Lock du mail self.lock('mail', login) if not 'compte' in self.modifs: self.modifs.setdefault('compte', None) self._data['objectClass'] = ['club', 'cransAccount', 'posixAccount', 'shadowAccount'] self._data['uid'] = [ login ] self._data['cn'] = [ preattr(self.Nom())[1] ] self._data['loginShell' ] = [ config.club_login_shell ] # Détermination de l'uid uidNumber = 1000 while self.exist('(uidNumber=%s)' % uidNumber): uidNumber += 1 try: self.lock('uidNumber', str(uidNumber)) except: # Quelqu'un nous a piqué l'uid que l'on venait de choisir ! return self.compte(login) self._data['uidNumber']= [ str(uidNumber) ] self._data['gidNumber'] = [ str(config.club_gid) ] self._data['homeDirectory'] = [ preattr(home)[1] ] return decode(login) def email(self): """ Retourne l'adresse mail du responsable """ return self.responsable().email() class Machine(BaseClasseCrans): """ Classe de définition d'une machine """ idn = 'mid' filtre_idn = '(|(objectClass=machineFixe)(objectClass=machineWifi)' filtre_idn += '(objectClass=machineCrans)(objectClass=borneWifi))' def __init__(self, parent_or_tuple, mode='', conn=None): """ parent_or_tuple est : * soit une instance d'une classe pouvant posséder une machine (Adherent, Club ou AssociationCrans), la nouvelle machine lui sera alors associée. * soit directement le tuple définissant une machine (tel que retourné par les fonctions de recherche ldap) Pour l'édition d'une machine, mode devra être égal à 'w' Attention, si mode='w' mais si l'objet est déja locké il n'y a pas d'erreur, vérifier l'obtention du lock grâce à la valeur de _modifiable (si ='w' c'est bon) conn est une instance de la classe de connexion à la base LDAP """ # Définition de l'objectClass LDAP à partir du nom de la classe Python self.objectClass = str(self.__class__).split('.')[-1] self.objectClass = self.objectClass[0].lower() + self.objectClass[1:] # Initialisation de la connexion self.conn = conn if not self.conn: self.connect() self.modifs = {} t = parent_or_tuple.__class__ if t == tuple: # Initialisation avec données fournies self.dn = parent_or_tuple[0] if mode == 'w': try: self.lock(self.idn, self.id()) self._modifiable = 'w' except EnvironmentError , c: self._modifiable = 0 else: self._modifiable = 0 # Utile pour construire l'instruction LDAP de modif self._init_data = parent_or_tuple[1].copy() self._data = parent_or_tuple[1] # Propriéraire inconnu mais ce n'est pas grave self.__proprietaire = None elif t in [Adherent, Club, AssociationCrans] and mode != 'w': # Machine vide self.__proprietaire = parent_or_tuple self.dn = parent_or_tuple.dn self._data = {'objectClass': [self.objectClass]} self._init_data = {} self._modifiable = 'w' chbre = self.__proprietaire.chbre() # if chbre == 'EXT' and mode == 'fixe': # raise ValueError(u'Il faut une chambre pour pouvoir posséder une machine fixe') if chbre == '????': raise ValueError(u'ERREUR: la chambre du propriétaire est inconnue') else: raise TypeError(u'Arguments invalides') def Nom(self): """ Retourne le nom de la machine """ return self.nom() def mac(self, mac=None, multi_ok=0): """ Défini ou retourne l'adresse mac de la machine Adresse valide si: 12 caractères hexa avec - ou : comme séparateurs non nulle Stoque l'adresse sous la forme xx:xx:xx:xx:xx:xx Si multi_ok = 1 permet d'avoir plusieur fois la même mac sur le réseau """ if mac == None: return decode(self._data.get('macAddress', [''])[0]) mac = format_mac(mac) # La mac serait-elle déjà connue ? if not multi_ok and self.exist('macAddress=%s' % mac): raise ValueError(u'Mac déja utilisée sur le réseau.', 1) # La MAC serait-elle une MAC à la con ? if mac == "00:04:4b:80:80:03": raise ValueError(u"Il s'agit de l'unique adresse MAC achetée par nVidia pour ses cartes réseau. Il faut changer cette adresse.", 2) elif mac[0:11] == "44:45:53:54": raise ValueError(u"Il s'agit de l'adresse MAC d'une interface PPP.", 2) # Le test final : vendeur connu prefix = mac[:8].upper() + ' ' vendor = '' try: for line in open('/usr/scripts/gestion/ethercodes.dat').readlines(): if line.startswith(prefix): vendor = line.replace(prefix, '').replace('( )', '').strip() break except IOError: # Le fichier existe pas, on sors raise RuntimeError(u"Fichier de fabiquants de MAC non trouvé !") if not vendor: raise ValueError(u"Le constructeur correspondant à cette adresse MAC ne peut être trouvé.\nL'adresse MAC correspond peut-être à un pont réseau, désactivez ce pont réseau.\nContactez nounou si la MAC est bien celle d'une carte.", 2) # Lock de la mac self.lock('macAddress', mac) self._set('macAddress', [mac]) return mac def __host_alias(self, champ, new): """ Vérification de la validité d'un nom de machine """ # Supression des accents new = strip_accents(unicode(new, 'iso-8859-15')) l, new = preattr(new) new = new.lower() l = len(new.split('.')[0]) if l<2: raise ValueError(u"%s trop court." % champ.capitalize()) if self.proprietaire().__class__ != AssociationCrans: new = new.split('.', 1)[0] for c in new[:]: if not c in (string.letters + string.digits + '-'): raise ValueError(u"Seuls les caractères alphabétiques minuscules et les - sont autorisés pour %s" % champ) if l>17: raise ValueError(u"%s trop long." % champ.capitalize()) if not new[0].isalnum(): raise ValueError(u"Le premier caractère du champ %s doit être alphanumérique" % champ) # Ajout du domaine si necessaire if new.find('.') == -1: try: new += '.' + config.domains[self.objectClass] except: raise RuntimeError(u"%s : domaine non trouvé pour %s" % (champ.capitalize(), self.__class__)) # Pas déja pris ? if self.exist('(|(host=%s)(hostAlias=%s))' % (new, new)): raise ValueError(u"%s : nom déjà pris" % champ.capitalize()) # Lock host self.lock('host', new) return new def nom(self, new=None): """ Défini ou retourne le nom de machine. Un nom de machine valide ne comporte que des caractères alphabétiques minuscules et le - """ if new == None: return decode(self._data.get('host', [''])[0]) new = self.__host_alias('nom de machine', new) self._set('host', [new]) return new.split('.')[0] def prise(self, new=None): """ Retourne ou défini la prise associée à la machine La définition n'est possible que si la machine est une machine de l'assoce. Si la prise est inconne retourne N/A """ if new == None: if self.proprietaire().__class__ == AssociationCrans: return decode(self._data.get('prise', ['N/A'])[0]) else: chbre = self.proprietaire().chbre() if chbre and chbre[0].lower() in annuaires.chbre_prises.keys(): try: return annuaires.chbre_prises[chbre[0].lower()][chbre[1:]] except: return 'N/A' # Attribution de la prise new = preattr(new)[1] if new == 'N/A': if self._data.has_key('prise'): self._set('prise', []) self._data.pop('prise') return if not sre.match('^[a-cg-jmp][0-6][0-5][0-9]$', new.lower()): raise ValueError('Prise incorrecte') self._set('prise', [new.upper()]) return new def alias(self, new=None): """ Création ou visualisation des alias d'une machine. Même sytème d'argument que la méthode info. """ if not self._data.has_key('hostAlias'): self._data['hostAlias'] = [] liste = list(self._data['hostAlias']) if new == None: return map(decode, liste) if type(new) == list: # Modif index = new[0] new = new[1] if new == '': liste.pop(index) self._set('hostAlias', liste) return liste else: index = -1 # Test si valide new = self.__host_alias('alias', new) if index != -1: liste[index] = new else: liste = liste + [new] self._set('hostAlias', liste) return liste def ip(self, ip=None): """ Défini ou retourne l'IP de la machine. Les IP sont stoquées sous forme xxx.xxx.xxx.xxx et doivent être fournies ainsi. Si l'IP n'est pas définie retourne . Si ip= attribue la permière IP libre du sous-réseau. """ if ip == None: if self._data.has_key('ipHostNumber'): return decode(self._data['ipHostNumber'][0]) elif isinstance(self.proprietaire(), AssociationCrans) and not isinstance(self, BorneWifi): return '' else: return '' l, ip = preattr(ip) # Dans quel réseau la machine doit-elle être placée ? if isinstance(self, MachineWifi): if self.proprietaire().etudes(0) == "ENS" and self.proprietaire().etudes(1) == "Pers": # Personnel ENS net = config.NETs['wifi-ens'] else: net = config.NETs['wifi-adh'] elif isinstance(self, BorneWifi): net = config.NETs['bornes'] elif isinstance(self.proprietaire(), AssociationCrans): net = [ '0.0.0.0/0' ] else: try: net = config.NETs[self.proprietaire().chbre()[0].lower()] except: raise RuntimeError(u'Impossible de trouver le réseau où placer la machine.') if ip == '': pool_ip = [] # Pool d'IP à tester for ne in net: ip = ne.split('/')[0] ip = ip.split('.') n = [] for i in ip: n.append(int(i)) while 1: if n[3] < 255: n[3] += 1 else: n[2] += 1 n[3] = 0 if n[2] == 255: break ip = "%d.%d.%d.%d" % tuple(n) if not iptools.AddrInNet(ip, ne): # On est allé trop loin break pool_ip.append(ip) # On va prendre choisir une IP au hasard dans le pool des IP dispo random.shuffle(pool_ip) while len(pool_ip) > 0: ip = pool_ip.pop() # On choisit une IP if not self.exist('ipHostNumber=%s' % ip): # On a trouvé la première ip libre pool_ip.append(ip) break if not len(pool_ip): raise RuntimeError(u"Plus d'IP libres dans %s." % string.join(net, ' et ')) else: # L'ip est elle dans le bon sous-réseau ? # (accessoirement teste si l'IP est valide et ne correspond pas # à l'adresse de broadcast ou de réseau) if not iptools.AddrInNet(ip, net): raise ValueError(u'IP invalide ou en dehors du sous-réseau alloué.', 1) # Reformatage ip = iptools.DecToQuad(iptools.QuadToDec(ip)) # L'ip est-elle déja allouée ? if self.exist('ipHostNumber=%s' % ip): raise ValueError(u'IP déjà prise.') # Lock ip self.lock('ipHostNumber', ip) self._set('ipHostNumber', [ip]) return ip def exempt(self, new=None): """ Liste des réseaux vers lesquels on ne compte pas l'upload Cette liste est transférée dans la base postgres de komaz Pour ajouter un réseau new doit être la chaîne représentant le réseau à ajouter Pour modifier new doit être une liste de la forme : [ index du nouveau réseau , nouveau réseau ] l'index est celui obtenu dans la liste retournée par exempt() """ if not self._data.has_key('exempt'): self._data['exempt'] = [] liste = list(self._data['exempt']) if new == None: return map(decode, liste) if type(new) == list: # Modif index = new[0] l, new = preattr(new[1]) if not new: # Supression réseau liste.pop(index) else: # Modif remarque liste[index] = new elif type(new) == str: # Réseau supplémentaire l, new = preattr(new) if not new: # On ajoute pas de réseau vide return liste # Ajout à la liste liste = liste + [new] else: raise TypeError self._set('exempt', liste) return liste def proprietaire(self): """ retroune le propriétaire de la machine (classe Adherent, Club ou AssociationCrans) """ if not self.__proprietaire: res = self.conn.search_s(','.join(self.dn.split(',')[1:]), 0) if 'adherent' in res[0][1]['objectClass']: self.__proprietaire = Adherent(res[0], self._modifiable, self.conn) elif 'club' in res[0][1]['objectClass']: self.__proprietaire = Club(res[0], self._modifiable, self.conn) else: self.__proprietaire = AssociationCrans(self.conn) return self.__proprietaire def save(self): """ Enregistre la machine courante dans la base LDAP Retourne une chaîne indiquant les opération effectuées. """ if self.proprietaire().__class__ == AssociationCrans and not (isadm or user_tests.getuser() == 'www-data'): raise EnvironmentError(u'Il faut être administrateur pour effectuer cette opération.') if not self._init_data: # Nouvelle machine => configuration prise self.services_to_restart('switch', [self.proprietaire().chbre()]) ret = '' # Besoin de redémarrer les firewalls ? if 'ipHostNumber' in self.modifs or 'macAddress' in self.modifs: reconf_ip = self._init_data.get('ipHostNumber', []) reconf_ip += self._data.get('ipHostNumber', []) else: reconf_ip = [] # On vire les doublons dans reconf_ip reconf_ip = list(dict(zip(reconf_ip, [None]*len(reconf_ip)))) # Enregistrement self._save() # Clef IPsec if 'ipsec' in self.modifs: ret += coul(u'Clef IPsec de la machine : %s\n' % self.ipsec(), 'cyan') self.services_to_restart('conf_wifi_ng') # Reconfiguration firewalls et dhcps if reconf_ip: self.services_to_restart('macip', reconf_ip) self.services_to_restart('classify', reconf_ip) if isinstance(self, MachineWifi): self.services_to_restart('ragnarok-dhcp') else: self.services_to_restart('rouge-dhcp') if 'portTCPin' in self.modifs or 'portTCPout' in self.modifs or \ 'portUDPin' in self.modifs or 'portUDPout' in self.modifs: self.services_to_restart('komaz-ports', [self.ip()]) self.services_to_restart('mail_modif', ['ip=%s' % self.ip()]) # Reconfiguration DNS ? if 'host' in self.modifs or 'ipHostNumber' in self.modifs or 'hostAlias' in self.modifs: self.services_to_restart('dns') # Reconfiguration bornes wifi ? if 'canal' in self.modifs or 'puissance' in self.modifs or 'nvram' in self.modifs or 'hotspot' in self.modifs: self.services_to_restart('conf_wifi_ng') # Reconfiguration clients wifi ? if isinstance(self, MachineWifi) or isinstance(self, BorneWifi) \ and ('ipHostNumber' in self.modifs or 'host' in self.modifs or 'macAddress' in self.modifs): self.services_to_restart('conf_wifi_ng') # Regénération blackliste nécessaire ? bl = self.blacklist_actif() if bl and ('ipHostNumber' in self.modifs or 'host' in self.modifs): for s in bl: self.services_to_restart(s, [self.ip()]) # Regénération de l'autostatus et mail de changmement ? if self.proprietaire().__class__ == AssociationCrans: self.services_to_restart('autostatus') self.services_to_restart('mail_modif', ['ip=%s' % self.ip()]) # Synchronisation avec la base pgsql pour les exemptions if 'exempt' in self.modifs or ('ipHostNumber' in self.modifs and self.exempt()): self.services_to_restart('surveillance_exemptions') # Synchronisation avec la base pgsql pour la liste des machines if 'ipHostNumber' in self.modifs: self.services_to_restart('surveillance_machines') # Remise à zéro self.modifs = {} # Message de sortie ret += coul(u"Machine %s enregistrée avec succès." % self._data['host'][0], 'vert') return ret def delete(self, comment=''): """ Destruction de la machines """ if self.proprietaire().__class__ == AssociationCrans and not isadm: raise EnvironmentError(u'Il faut être administrateur pour effectuer cette opération.') self.proprio = self.__proprietaire.Nom() # On met dans un coin le nom du proprio self.__proprietaire = None # On oublie le propriétaire self._delete(self.dn, comment) # Services à redémarrer if isinstance(self, MachineWifi): self.services_to_restart('conf_wifi_ng') self.services_to_restart('ragnarok-dhcp') else: self.services_to_restart('rouge-dhcp') if self.exempt(): self.services_to_restart('surveillance_exemptions') self.services_to_restart('surveillance_machines') self.services_to_restart('dns') self.services_to_restart('macip', [self.ip()]) self.services_to_restart('classify', [self.ip()]) def portTCPin(self, ports=None): """ Ports TCP ouverts depuis l'extérieur pour la machine """ return self.__port(ports, 'portTCPin') def portTCPout(self, ports=None): """ Ports TCP ouverts vers l'extérieur pour la machine """ return self.__port(ports, 'portTCPout') def portUDPin(self, ports=None): """ Ports UDP ouverts vers l'extérieur pour la machine """ return self.__port(ports, 'portUDPin') def portUDPout(self, ports=None): """ Ports UDP ouverts vers l'extérieur pour la machine """ return self.__port(ports, 'portUDPout') def __port(self, ports, champ): if ports == None: return self._data.get(champ, []) # Les ports doivent être de la forme [port][:[port]]. On met # la spécification sous une forme qui donne un tri intéressant def parse(x): try: liste = x.split(':') assert(len(liste) == 1 or len(liste) == 2) return map(lambda x: x and int(x) or '', liste) except: raise ValueError(u'Spécification de ports incorrecte : %s' % x) ports = map(parse, ports) ports.sort() self._set(champ, map(lambda x: ':'.join(map(str, x)), ports)) def __hash__(self): """Retourne un hash de l'objet Machine""" return hash(self.nom()) class MachineFixe(Machine): """ Classe de définition d'une machine fixe """ def __init__(self, parent_or_tuple, typ='fixe', conn=None): Machine.__init__(self, parent_or_tuple, typ, conn) class MachineWifi(Machine): """ Classe de définition d'une machine wifi """ def __init__(self, parent_or_tuple, typ='wifi', conn=None): Machine.__init__(self, parent_or_tuple, typ, conn) if not isinstance(parent_or_tuple, tuple): # Initialisaton d'une nouvelle machine wifi self.ipsec(True) def ipsec(self, clef=None): """ Affichage (clef=None), génération (clef=True) ou définition de la clef IPsec de la machine. Si clef différent de True et None: prend la clef fournie. """ if clef == None: return decode(self._data.get('ipsec', [''])[0]) if clef == True: # Génération clef = '' for i in range(22): clef += random.choice(filter(lambda x: x != 'l' and x != 'o', string.lowercase) + filter(lambda x: x != '1' and x != '0', string.digits)) self._set('ipsec', [clef]) return clef class MachineCrans(Machine): """ Classe de définition d'une machine du Crans """ def __init__(self, parent_or_tuple, typ='fixe', conn=None): Machine.__init__(self, parent_or_tuple, typ, conn) class BorneWifi(Machine): """ Classe de définition d'une borne wifi """ def __init__(self, parent_or_tuple, typ='borne', conn=None): Machine.__init__(self, parent_or_tuple, typ, conn) if not isinstance(parent_or_tuple, tuple): # Initialisaton d'une nouvelle borne wifi self._data['canal'] = ['2047'] self._data['puissance'] = ['60'] self._data['hotspot'] = ['FALSE'] def mac2(self): """ Retourne l'adresse MAC + 2 """ mac = self.mac().split(":") mac[-1] = "%0.2x" % (int(mac[-1], 16) + 2) return ":".join(mac) def hotspot(self, new=None): """ Cette borne est-elle partagée avec l'ENS ? """ if new == None: # Le schéma nous assure que ce champ existe toujours return self._data['hotspot'][0] == "TRUE" else: if new: self._set('hotspot', ['TRUE']) else: self._set('hotspot', ['FALSE']) def canal(self, new=None, raw=False): """ Attribution ou visualisation du canal d'une borne wifi """ if not new: canaux = self._data.get('canal', [''])[0] if raw: return canaux else: canaux = int(canaux) if canaux < 14: # Compatibilité ascendante return str(canaux) lcanal1 = [] for i in range(1, 14): found = False if canaux & (1 << (i - 1)): lcanal2 = [] for c in lcanal1: if c[1] == i - 1: lcanal2.append((c[0], i)) found = True else: lcanal2.append(c) if not found: lcanal2.append((i, i)) lcanal1 = lcanal2 lcanal3 = [] for c in lcanal1: if c[0] == c[1]: lcanal3.append("%d" % c[0]) else: lcanal3.append("%d-%d" % (c[0], c[1])) return ",".join(lcanal3) try: new = int(new) if new < 0 or new > 13: raise except: # Nouveau système, il doit s'agir d'une liste de canaux try: lcanal3 = str(new).split(",") lcanal = [] for c in lcanal3: c2 = c.split("-") if len(c2) == 1: lcanal.append(int(c2[0])) else: for i in range(int(c2[0]), int(c2[1]) + 1): lcanal.append(i) new = 0 for c in lcanal: if c not in range(0, 14): raise new = new + (1 << (c - 1)) except: raise ValueError(u'Canal invalide : doit être entre 0 et 13 ou une liste de canaux') self._set('canal', [str(new)]) return new def puissance(self, new=None): """ Attribution ou visualisation de la puissance d'une borne wifi """ if not new: return self._data.get('puissance', [''])[0] try: new = int(new) if new < -99 or new > 99: raise except: raise ValueError(u'Puissance invalide : doit être entre -99 et 99') self._set('puissance', [str(new)]) return new def position(self, new=False): """ Attribution ou visualisation de la position d'une borne wifi. Renvoie un couple de coordonnées (en strings) si elles existent, None sinon. new doit être un couple de coordonnées (en strings), None (pour enlever les coordonnées) ou False (retourne les valeurs actuelles). On utilise des strings plutôt que des floats à cause de la précision. """ if new == False: valeur = self._data.get('positionBorne', [''])[0] if valeur: return tuple(valeur.split(' ')) else: return None elif new == None: self._set('positionBorne', []) return None else: self._set('positionBorne', [' '.join(new)]) return new def nvram(self, champ=None, new=False): """ Définit et renvoie un champ nvram d'une borne wifi. Si champ=None, renvoie sous forme de liste key=value tous les champs. On peut aussi définir tous les champs en passant dans new la liste. Sinon, définit et renvoie la valeur du champ indiqué. Le champ est juste retourné, supprimé ou modifié selon que new vaut False, None ou autre chose. """ current = self._data.get('nvram', [])[:] if champ == None: if type(new) is list: self._set('nvram', new) return new else: return current # Recherche l'index du champ index = None for i in range(len(current)): current_champ = current[i] if current_champ.startswith(champ): sep_index = current_champ.find('=') + 1 if sep_index > 0: current_champ = current_champ[sep_index:] index = i break if new == False: return index != None and current_champ or None elif new == None: if index != None: del current[index] self._set('nvram', current) return None else: if index == None: current.append("%s=%s" % (champ, new)) else: current[index] = "%s=%s" % (champ, new) self._set('nvram', current) return new class Facture(BaseClasseCrans): """ Classe de définition d'une facture """ objectClass = 'facture' idn = 'fid' filtre_idn = '(objectClass=facture)' def __init__(self, parent_or_tuple, mode='', conn=None): """ parent_or_tuple est : * soit une instance d'une classe pouvant posséder une facture (Adherent, Club), la nouvelle facture lui sera alors associée. * soit directement le tuple définissant une facture (tel que retourné par les fonctions de recherche ldap) Pour l'édition d'une facture, mode devra être égal à 'w' Attention, si mode='w' mais si l'objet est déja locké il n'y a pas d'erreur, vérifier l'obtention du lock grâce à la valeur de _modifiable (si ='w' c'est bon) conn est une instance de la classe de connexion à la base LDAP """ # Initialisation de la connexion self.conn = conn if not self.conn: self.connect() self.modifs = {} t = parent_or_tuple.__class__ if t == tuple: # Initialisation avec données fournies self.dn = parent_or_tuple[0] if mode == 'w': try: self.lock(self.idn, self.id()) self._modifiable = 'w' except EnvironmentError , c: self._modifiable = 0 else: self._modifiable = 0 # Utile pour construire l'instruction LDAP de modif self._init_data = parent_or_tuple[1].copy() self._data = parent_or_tuple[1] # Propriéraire inconnu mais ce n'est pas grave self.__proprietaire = None elif t in [Adherent, Club] and mode != 'w': # Facture vide self.__proprietaire = parent_or_tuple self.dn = parent_or_tuple.dn self._data = {'objectClass': [self.objectClass], 'modePaiement':['paypal']} self._init_data = {} self._modifiable = 'w' else: raise TypeError(u'Arguments invalides') def numero(self): """ Retourne le numéro de facture """ fid = self._data.get('fid',[None])[0] if fid == None: raise NotImplementedError, u'Il faut enregistrer une facture pour connaitre son numero' return fid def nom(self): """ Utilisé pour la fonction delete() """ return "Facture%s" % self.numero() Nom = nom def proprietaire(self): """ retroune le propriétaire de la facture (classe Adherent ou Club) """ # si la facture est en mode w mais pas le proprio, on tente de prendre # le proprio en w if self.__proprietaire and self.__proprietaire._modifiable != self._modifiable: self.__proprietaire = None # récupère le proprio si ce n'est pas encore fait if not self.__proprietaire: res = self.conn.search_s(','.join(self.dn.split(',')[1:]), 0) if 'adherent' in res[0][1]['objectClass']: self.__proprietaire = Adherent(res[0], self._modifiable, self.conn) elif 'club' in res[0][1]['objectClass']: self.__proprietaire = Club(res[0], self._modifiable, self.conn) else: raise ValueError, u'Propriétaire inconnu' return self.__proprietaire def modePaiement(self, new=None): """ Définit ou retourne le mode de paiement. Le mode de paiement doit être une chaine de caractère """ # modification du mode de paiement if new != None: if self.recuPaiement(): raise ValueError, u'Facture déja payée' if not self._modifiable: raise NotImplementedError, "La facture n'est pas modifiable" if new not in ['liquide','cheque','paypal']: raise ValueError, u'Mode de paiement non accepté' self._set('modePaiement', [new]) return decode(self._data.get('modePaiement', [None])[0]) def recuPaiement(self, new=None): """ Définit ou retourne qui a recu le paiement """ # on vérifie que la facture n'est pas déja payéee if new and self._data.get('recuPaiement', []): raise ValueError, u'Facture déja payée' # modification de la valeur if new != None: # on vérifie que la facture est modifiable if not self._modifiable and new: raise NotImplementedError, "La facture n'est pas modifiable" # on crédite les articles, si c'est pas possible, la metode # levera une exeption self._crediter() # ajout des frais à la liste d'articles self.ajoute(self._frais()) # modifie la base ldap self._set("recuPaiement",[new]) # renvoie la valeur trouvée dans la base return self._data.get("recuPaiement",[None])[0] def _del_recu_paiement(self): """ Pour test """ self._set("recuPaiement",[]) def _crediter(self): """ Credite les articles à son propriétaire """ # si la facture n'existe pas encore, on la sauve pour générer un numéro if not self._data.has_key('fid'): self.save() # on vérifie que le propriétaire est modifiable if not self.proprietaire()._modifiable: raise SystemError, u"Impossible de créditer les articles, le proprietaire n'est pas modifiable" # on crédite les articles for art in self._articles(): # solde impression if art["code"] == "SOLDE": proprio = self.proprietaire() proprio.solde(operation=art['nombre']*art["pu"], comment="Facture n°%s : %s" % (self.numero(),art['designation'])) proprio.save() def _frais(self): """ Retourne une liste d'articles correspondants aux divers frais """ arts = [] # aucun frais pour une facture payée, ils sont intégrés aux articles if self.recuPaiement(): return [] # frais de paiement par paypal if self.modePaiement() == 'paypal': # 25 centimes pour le paiement paypal s = 0.25 # et on ajoute 3.5% du montant for art in self._articles(): s += 0.035 * art['nombre'] * art['pu'] # ajoute à la liste d'articles de frais arts.append( {'code':'FRAIS','designation':'Frais de tansaction PayPal','nombre':1,'pu':round(s,2)} ) return arts def _articles(self, arts = None): """ Retourne ou modifie la liste des articles de la base """ # modifie la liste des articles if arts != None: self._set('article',['%s~~%s~~%s~~%s' % (art['code'],art['designation'],str(art['nombre']),str(art['pu'])) for art in arts]) # charge la liste des articles arts = [] for art in self._data.get("article",[]): art = art.split('~~') art = { 'code' : art[0], 'designation' : art[1], 'nombre' : int(art[2]), 'pu' : float(art[3]) } arts.append(art) return arts def ajoute(self, ajoute): """ Ajoute un/des article(s) à la facture ajoute est un article ou une liste d'articles """ # on ne eut pas modifier une facture payée if self.recuPaiement(): raise ValueError, u'On ne peut pas modifier une facture payée' # charge la liste des articles arts = self._articles() # ajoute les articles if type(ajoute)==dict: ajoute = [ajoute] if type(ajoute)==list: for art in ajoute: if int(art['nombre']) != float(art['nombre']): raise ValueError, u'nombre doit être un entier' if float(int(art['pu']*100)/100.0) != art['pu']: raise ValueError, u'pu ne doit pas avoir plus de 2 chiffres apres la virgule' art['nombre'] = int(art['nombre']) if '~~' in ' '.join([str(x) for x in art.values()]): raise ValueError, u'Ne pas mettre de ~~ dans les champs' arts.append(art) # enregistre la nouvelle liste self._articles(arts) def supprime(self, supprime): """ Supprime un/des article(s) à la facture arts est un article ou une liste d'articles """ # on ne eut pas modifier une facture payée if self.recuPaiement(): raise ValueError, u'On ne peut pas modifier une facture payée' # charge la liste des articles arts = self._articles() # on supprime les anciens articles if type(supprime)==dict: supprime = [supprime] if type(supprime)==list: for art in supprime: arts.remove(art) # enregistre la nouvelle liste self._articles(arts) def articles(self): """ Retourne la liste des articles. Un article est un dictionnaire de la forme : { 'code' : string, 'designation' : string, 'nombre' : int, 'pu' : int/float } """ return self._articles() + self._frais() def total(self): """ Calcule le total de la facture, frais compris """ s = 0 for art in self.articles(): s += art['nombre'] * art['pu'] return s def urlPaypal(self): """ Retourne l'url paypal pour le paiement de cette facture """ url = "https://www.paypal.com/cgi-bin/webscr?cmd=_cart" url += "&upload=1" url += "&business=paypal@crans.org" url += "¤cy_code=EUR" url += "&no_shipping=1" url += "&no_note=1" url += "&return=http://factures.crans.org/return" url += "&cancel_return=http://factures.crans.org/cancel" url += "&invoice=12345" # num de facture item_id = 0 for item in self.articles(): item_id += 1 url += "&item_name_%d=%s" % (item_id, item['designation']) url += "&amount_%d=%s" % (item_id, item['pu']) url += "&quantity_%d=%s" % (item_id, int(item['nombre'])) return url def save(self): """ Enregistre la facture dans la base LDAP Retourne une chaîne indiquant les opération effectuées. """ # Enregistrement self._save() # Remise à zéro self.modifs = {} # Message de sortie return coul(u"Facture n°%s enregistrée avec succès." % self.numero(), 'vert') def delete(self, comment=''): """ Suppression de la facture """ self.__proprietaire = None self._delete(self.dn, comment) class _FakeProprio(CransLdap): """ Définitions de base d'un propriétaire virtuel """ idn = '' def __init__(self, conn=None): self.conn = conn if not self.conn: self.connect() self.dn = self.base_dn def id(self): return '' def blacklist(self): return [] def paiement(self): return [ ann_scol ] def carteEtudiant(self): return [ ann_scol ] def blacklist_actif(self): return [] def mail(self, new=None): return 'roots@crans.org' def machines(self): res = self.conn.search_s(self.dn, 1, Machine.filtre_idn) m = [] for r in res: m.append(self.make(r)) return m class AssociationCrans(_FakeProprio): """ Classe définissant l'assoce (pour affichage de ses machines) """ def __init__(self, conn=None): _FakeProprio.__init__(self, conn) def Nom(self): return u"Crans" def chbre(self): return u"CRA" db = None def crans_ldap(readonly=False): """ Renvoie un objet CransLdap """ global db if readonly: return CransLdap(readonly) if db == None: db = CransLdap() return db if __name__ == '__main__': import sys usage = """Usage %s [ACTION] --lock : donne la liste des locks actifs --purgelock : supprime tous les locks de la base LDAP --menage : supprime les machines des anciens adhérents"""%sys.argv[0] if len(sys.argv) != 2: print usage sys.exit(1) elif '--lock' in sys.argv: print "Liste des locks" for lock in crans_ldap().list_locks(): print "%s\t %s" % (lock[1]["lockid"][0], lock[0].split(',')[0]) elif '--purgelock' in sys.argv: print "Suppression de tous les locks" crans_ldap().remove_lock('*') elif '--menage' in sys.argv: print "Ménage des machines des adhérents partis..." machines = crans_ldap().search('paiement!=%s&host=*.crans.org' % ann_scol , 'w')['machine'] print "Destruction de %i machines" % len(machines) for m in machines: print 'Destruction de %s' % m.nom() m.delete('Ménage') else: print usage sys.exit(1)