scripts/gestion/ldap_crans.py
2014-09-18 11:47:00 +02:00

4358 lines
158 KiB
Python
Executable file

#! /usr/bin/env python
# -*- coding: utf-8 -*-
"""
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 socket
import smtplib
import re
import os
import random
import string
import time
import datetime
import sys
import pwd
import errno
import ldap
import ldap.modlist
import ldap_passwd
import netaddr
import annuaires_pg as annuaires
import config
import config.impression
import config.cotisation as cotisation
import iptools
import ip6tools
import cPickle
import config_mail
from chgpass import change_password
from calendar import monthrange
from affich_tools import coul, prompt, cprint
from email_tools import send_email
from syslog import openlog, closelog, syslog
from numeros_disponibles import lister_ip_dispo
from unicodedata import normalize
import secrets_new as secrets
import ridtools
from user_tests import isadm
import getpass
cur_user = os.getenv("SUDO_USER") or os.getenv("USER") or os.getenv("LOGNAME") or getpass.getuser()
date_format = '%d/%m/%Y %H:%M'
date_format_new = '%d/%m/%Y %H:%M:%S'
hostname = gethostname().split(".")[0]
smtpserv = "smtp.crans.org"
random.seed() # On initialise le générateur aléatoire
##################################################################################
### paramètres de connexion à la base LDAP
test_hosts = tuple()
if __name__ == 'ldap_crans_test' or os.environ.get('crans_ldap', '') == 'test':
if hostname != "vo":
raise ImportError, coul("La base de test n'est accessible que depuis vo !", "rouge")
# 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 = 'ldapi://%2fvar%2frun%2fslapd%2fldapi/'
ldap_auth_dn = 'cn=admin,dc=crans,dc=org'
ldap_password = '75bdb64f32'
elif cur_user == 'freerad':
# Freeradius n'a pas accès au secret, donc accès que en local
uri = ''
ro_uri = 'ldapi://%2fvar%2frun%2fslapd%2fldapi/'
ldap_auth_dn = ldap_password = ''
elif hostname in test_hosts:
# pour les autres on utilise ldap.adm.crans.org en rw
uri = 'ldap://newldap.adm.crans.org/'
# avec le secret
try:
ldap_auth_dn = secrets.get('ldap_auth_dn')
ldap_password = secrets.get('ldap_password')
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
else:
# pour les autres on utilise ldap.adm.crans.org en rw
uri = 'ldap://ldap.adm.crans.org/'
# avec le secret
try:
ldap_auth_dn = secrets.get('ldap_auth_dn')
ldap_password = secrets.get('ldap_password')
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 = config.blacklist_items
##################################################################################
### Droits possibles
droits_possibles = [u'Multimachines', u'Cableur', u'Imprimeur', u'Apprenti',
u'Webmaster', u'Moderateur', u'Webradio',
u'Nounou', u'Tresorier', u'Bureau', u'Troll']
##################################################################################
### Droits critiques, ie que seules les nounous peuvent attribuer
droits_critiques = [u'Nounou', u'Apprenti', u'Webradio']
##################################################################################
### Droits vieux, ie qui permettent d'avoir des droits meme quand on ne
### cotise plus
droits_vieux = [u'Nounou', u'Bureau']
##################################################################################
### Variables internes diverses
#isadm = user_tests.isadm()
#isdeconnecteur = user_tests.isdeconnecteur()
ann_scol = config.ann_scol
#script_utilisateur = user_tests.getuser()
script_utilisateur = cur_user
##################################################################################
### 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
def tz(thetz):
abstz = 100*abs(thetz)
if thetz == 0:
return "Z"
else:
return "%s%04d" % ("+"*(thetz < 0) + "-"*(thetz > 0), abstz)
def generalizedTimeFormat(stamp):
"""Converts a timestamp (local) in a generalized time format
for LDAP
"""
return "%s%s" % (time.strftime("%Y%m%d%H%M%S", time.localtime(stamp)), tz(time.altzone/3600))
def fromGeneralizedTimeFormat(gtf):
"""Converts a GTF stamp to unix timestamp
"""
return time.mktime(time.strptime(gtf.split("-", 1)[0].split("+", 1)[0].split('Z', 1)[0], "%Y%m%d%H%M%S"))
def strip_accents(a, sois_un_porc_avec_les_espaces = True):
""" Supression des accents de la chaîne fournie """
res = normalize('NFKD', decode(a)).encode('ASCII', 'ignore')
if sois_un_porc_avec_les_espaces:
return res.replace(' ', '_').replace("'", '')
else:
return res
def mailexist(mail):
"""
Vérifie si une adresse mail existe ou non grace à la commande vrfy
du serveur mail
"""
mail = mail.split('@', 1)[0]
try:
s = smtplib.SMTP(smtpserv)
s.putcmd("vrfy", mail)
r = s.getreply()[0] in [250, 252]
s.close()
except:
raise ValueError(u'Serveur de mail injoignable')
return r
def preattr(val):
"""
val est :
* un entier
* une chaîne
* une liste avec un seul entier ou une seule chaîne
Retourne [ len(str(val).strip), str(val).strip en utf-8 ]
"""
if isinstance(val, list) and len(val) == 1:
return preattr(val[0])
elif isinstance(val, str) or isinstance(val, 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), val]
elif isinstance(val, unicode):
val = val.strip()
return [len(val), val.encode('utf-8')]
else:
raise TypeError(type(val))
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 log(contenu):
with open('/tmp/crans.log', 'a') as f:
f.writelines(contenu)
f.writelines('\n')
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.
"""
mac = mac.strip()
if mac == '<automatique>':
return mac
l, mac = preattr(mac)
mac = mac.strip().replace(' ','').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
def format_gpg_fingerprint(fpr):
"""
Formatage de fingerpring GPG pour les avoir de la forme
XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX
"""
# longueur + fpr sans espaces
l, fpr = preattr(fpr)
fpr = fpr.replace(':', '').replace('-', '').replace(' ', '')
if len(fpr) != 40:
raise ValueError(u"Longueur de la fingerprint GPG incorrecte, merci de respecter le format")
for c in fpr:
if c not in string.hexdigits:
raise ValueError(u"Une fingerpring GPG est exclusivement composée de caractères hexadécimaux, donc il faut virer %s." % c)
formattedfpr = ""
for i in range(0,10):
formattedfpr += fpr[i*4:(i+1)*4]
if i != 10:
formattedfpr += " "
if i == 4:
formattedfpr += " "
return formattedfpr
def validate_mail(mail, crans_allowed=False):
"""Valide une adresse e-mail"""
l, mail = preattr(mail)
mail = mail.lower()
#Emplacement de l'@
a = mail.find('@')
#Emplacement du . final
b = mail.rfind('.')
# Les tests :
# exactement un @
# 2 à 4 caractères après le . final
# @ pas en premier ni juste avant le dernier .
if mail.count('@') != 1 \
or not ( l-b >= 2 and l-b <= 6) \
or a < 1 or b-a < 2:
raise ValueError(u"Adresse mail incorrecte.")
# Pas de caractèrs bizarres
for l in mail[:]:
if not l in (string.lowercase + string.digits + '+-_.@'):
raise ValueError(u"Caractère interdit dans l'adresse mail (%s)." % l)
# Pour les vicieux
if not crans_allowed and mail.endswith(('@crans.org','@crans.ens-cachan.fr')):
raise ValueError(u"Adresse mail @crans interdite ici !")
# On enlève les blacklistes mail_invalide
return mail
##################################################################################
### 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 __unicode__(self):
starting = self.start
starting.sort()
dates = u' et '.join(map(lambda t: t < time.time() and \
u"maintenant" or time.strftime(date_format,
time.localtime(t)),
self.start))
dates = u" à partir d%s %s" % (dates.startswith(u"maintenant") and u"e" or u"u",
dates)
return (u"%s(%s)%s" % (self.nom,
u','.join([i.decode("UTF-8") for i in self.args]),
dates)).replace(u" et maintenant", u"")
def __str__(self):
return self.__unicode__().encode("utf-8", "ignore")
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': 'uid',
'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', 'ip6HostNumber', 'hostAlias']
auto_search_champs = { \
'adherent': \
['nom', 'prenom', 'tel', 'mail', 'chbre', 'mailAlias', 'canonicalAlias', 'mailExt', 'uid'],
'club': ['nom', 'chbre', 'uid'],
'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', 'dnsIpv6', 'machineAlias',
'sshFingerprint', 'rid']
non_auto_search_champs = { \
'adherent': \
['etudes', 'paiement', 'carteEtudiant', 'aid', 'postalAddress',
'historique', 'blacklist', 'droits', 'uidNumber', 'info',
'solde', 'controle', 'contourneGreylist', 'rewriteMailHeaders',
'ablacklist', 'homepageAlias', 'charteMA',
'adherentPayant', 'gpgFingerprint', 'debutConnexion', 'finConnexion',
'debutAdhesion', 'finAdhesion'], \
'club': \
['cid', 'responsable', 'paiement', 'historique', 'blacklist',
'mailAlias', 'info', 'controle', 'ablacklist', 'imprimeurClub'], \
'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', 'debutConnexion', 'finConnexion', 'debutAdhesion', 'finAdhesion']}
# 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]
del 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.cur_user = cur_user
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 $ rid $ macAddress $ host $ hostAlias $ ipHostNumber $
ip6HostNumber $ gpgFingerprint
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-%s' % (hostname, os.getpid(), time.time())
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] )) or time.time() - 600 >= l.split('-')[2]:
# 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=*
"""
# Ne tente de dertuire le lock que si l'on est connecté à la base
if self.conn:
# 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 sans 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:
mods = [{'start': serv_dates[new[1:]]}, { 'start': keep_date }]
self.conn.modify_s(remove_dn, ldap.modlist.modifyModlist(*mods))
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 isinstance(args, basestring):
args = [args]
args = map(lambda x:preattr(x)[1], args)
try:
start = [int(start)]
except TypeError:
pass
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 isinstance(expression, str):
pass
elif isinstance(expression, unicode):
expression = expression.encode('utf-8')
else:
raise TypeError(u'Chaîne attendue')
if os.getuid() != 0:
openlog('CransLdap.search')
syslog('(%s,%s) %s' % (script_utilisateur, mode, expression))
closelog()
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 config.periode_transitoire:
# Pour la période transitoire année précédente ok
el = "(|(paiement=%d)(paiement=%d)(finAdhesion>=%s))" % (ann_scol, ann_scol-1, generalizedTimeFormat(time.time()))
else:
el = "(|(paiement=%s)(finAdhesion>=%s))" % (ann_scol, generalizedTimeFormat(time.time()))
# Doit-on bloquer en cas de manque de la carte d'etudiant ?
# (si période transitoire on ne bloque dans aucun cas)
if config.bl_carte_et_definitif:
el = "(&(|(carteEtudiant=%d)(objectClass=club)(carteEtudiant=TRUE))%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 = ','.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
def getProprio(self, uid, mode=''):
"""
Renvoie un proprietaire ou un adherent correspondant au
login/ mot de passe (mot de passe facultatif)
"""
recherche = self.search(("uid=%s" % uid), mode)
proprio = None
if len(recherche['club']) > 0:
proprio = recherche['club'][0]
if len(recherche['adherent']) > 0:
proprio = recherche['adherent'][0]
if uid == "grosminet":
proprio = self.search("nom=grosminet", mode)['adherent'][0]
return proprio
__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(conn = self.conn).machines()
# Machines des adhérents et clubs de l'année en cours
#base = self.search('paiement=ok')
base = self.search('paiement=%s' % ann_scol)
base = base['adherent'] + base['club']
if config.periode_transitoire:
tmp=self.search('paiement=%s' % (ann_scol-1))
base.extend(tmp['adherent'] + tmp['club'])
del tmp
base=[a for a in base if a.paiement_ok()]
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: cprint(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'attribut 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', [])
actifs = {}
inactifs = {}
if isinstance(self, Machine):
# Il faut aussi regarder la blackliste du propriétaire
p = self.proprietaire()
bl_liste += p.blacklist()
elif isinstance(self, Adherent) and (config.ann_scol in self.paiement() or (self.adhesion() > time.time() and self.connexion() > time.time())):
# blacklistes virtuelle si on est un adhérent pour carte étudiant et chambre invalides
if not config.periode_transitoire and config.bl_carte_et_actif and not bool(self.carteEtudiant()) and not self.sursis_carte():
actifs['carte_etudiant']=('-','-')
if self.chbre() == '????':
actifs['chambre_invalide']=('-','-')
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.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)
# Si on édite une ancienne blackliste, on met aussi cette bl à jour
if index != -1:
old_restart = self._blacklist_restart.setdefault(self._data['blacklist'][index].split('$')[2], [])
now = int(time.time())
if now not in old_restart:
old_restart.append(now)
self._data['blacklist'] = liste
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()
"""
liste = list(self._data.get('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 sauf si
# facture.
if isinstance(self, Facture):
self.dn = self.proprietaire().dn
else:
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', 'ip6HostNumber',
'host', 'positionBorne', 'derniereConnexion',
'hotspot', 'dnsIpv6', 'machineAlias', 'finAdhesion',
'finConnexion', 'debutConnexion', 'debutAdhesion']:
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', 'imprimeurClub', 'gpgFingerprint',
'debutConnexion', 'finConnexion', 'debutAdhesion',
'finAdhesion']:
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
for champ in ['sshFingerprint']:
if champ in self.modifs:
modif[champ] = champ
# 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
# L'enregistrement peut échouer en cas de choix de dn concurrents
# (il n'y a pas de lock sur les dn)
for i in range(0, 5): # 5 tentatives
# Génération du dn
res = self.conn.search_s(self.base_dn, ldap.SCOPE_SUBTREE, 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' or self.idn == 'aid' or self.idn == 'mid':
# 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 ]
try:
# Ecriture
modlist = ldap.modlist.addModlist(self._data)
self.conn.add_s(self.dn, modlist)
break
except Exception as e:
# On logge
syslog("ldap_crans: exception: %s" % e)
raise
if i == 4:
raise
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('Entree en double dans le champ %r' % 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 adhesion(self, update=False, f=None):
"""
Gestion de l'adhésion d'un adhérent
La durée d'adhésion ne peut être choisie
* Si update vaut True, on ajoute un an
* f est une facture passée en référence.
"""
thetime = time.time()
# On récupère sur les factures l'ensemble de celles comportant une adhésion.
adh_factures = self.factures_adh()
finAdh = max([0.0] + [fromGeneralizedTimeFormat(facture._data.get('finAdhesion', ["19700101000000Z"])[0]) for facture in adh_factures if facture.controle() != "FALSE" and facture.recuPaiement() is not None])
if update == False:
return finAdh
else:
ftime = datetime.datetime.fromtimestamp(max(finAdh, thetime))
if not finAdh - thetime < cotisation.delai_readh:
raise EnvironmentError, u"On ne peut réadhérer que 15 jours avant l'expiration de l'adhésion précédente."
# Calcul de la nouvelle date de fin d'adhésion.
# le +86400 est une souplesse pour permettre au câblage de se passer sans warning
# quand le mec se fait câbler pour un an.
newFinAdh = time.mktime(ftime.replace(year=ftime.year + cotisation.duree_adh_an).timetuple()) + 86400
# Si aucune facture n'est passée en référence, on en crée une nouvelle.
if f is None:
f = Facture(self)
if isinstance(self, Adherent):
f.ajoute(cotisation.dico_adh)
elif isinstance(self, Club):
f.ajoute(cotisation.dico_adh_club)
f._set("finAdhesion", [generalizedTimeFormat(newFinAdh)])
f._set("debutAdhesion", [generalizedTimeFormat(thetime)])
self._set("finAdhesion", self._data.get("finAdhesion", []) + [generalizedTimeFormat(newFinAdh)])
self._set("debutAdhesion", self._data.get("debutAdhesion", []) + [generalizedTimeFormat(thetime)])
return f
def update_adhesion(self):
"""Récupère les dates de début et fin d'adhésion dans les factures
de l'adhérent, et met à jour ses données."""
debutAdh = []
finAdh = []
for facture in self.factures_adh():
debutAdh.append(generalizedTimeFormat(facture._data['debutAdhesion'][0]))
finAdh.append(generalizedTimeFormat(facture._data['finAdhesion'][0]))
self._set('debutAdhesion', debutAdh)
self._set('finAdhesion', finAdh)
self._save()
def droits(self, droits=None, light=False):
""" Renvoie les droits courants. Non modifiable (sauf si surchargée dans classe enfant)"""
if droits <> None:
raise NotImplementedError("Propriété non modifiable")
return []
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:
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 new.count('@') >= 2:
raise ValueError(u"Alias avec plus de un @ ?")
if '@' not in new:
new += '@crans.org'
if index != -1:
liste[index] = new
else:
liste = liste + [ new ]
# Lock de mailAlias
self.lock('mailAlias', new)
self._set('mailAlias', liste)
return liste
def gpgFingerprint(self, new=None):
"""
Fonction d'ajout d'empreinte GPG
"""
liste = list(self._data.get('gpgFingerprint', []))
if new == None:
return liste
if not isadm() and isadm(self.compte()):
raise EnvironmentError(u'Il faut être administrateur pour effectuer cette opération.')
else:
if type(new) == list:
# Modif
index = new[0]
new = new[1]
if new == '':
# Supression alias
liste.pop(index)
self._set('gpgFingerprint', liste)
return liste
else:
index = -1
new = format_gpg_fingerprint(new)
new = new.upper()
#if gpgexists(new):
# raise ValueError(u"Alias existant ou correspondand à un compte.")
if index != -1:
liste[index] = new
else:
liste = liste + [ new ]
self.lock('gpgFingerprint', new)
self._set('gpgFingerprint', 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 re.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, filtre=None):
""" Retourne les factures (instances) appartenant à la classe """
# Le champ id n'est pas initialisé lorsque le proprio est en cours
# de création
if filtre is None:
filtre = Facture.filtre_idn
else:
filtre = "(&%s%s)" % (filtre, Facture.filtre_idn)
if self.id():
res = []
for r in self.conn.search_s('%s=%s,%s' % (self.idn, self.id(), self.base_dn), 1, filtre):
res.append(self.make(r, self._modifiable))
return res
else:
return []
def factures_adh(self):
""" Retourne les factures pour adhésion """
return self.factures("(debutAdhesion=*)")
def factures_conn(self):
""" Retourne les factures pour connexion """
return self.factures("(debutConnexion=*)")
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 [+-][pc]
(p pour le paiement, c pour la carte)
Retourne une chaine contenant une combinaison de p, c.
"""
actuel = self._data.get('controle', [''])
if not actuel:
actuel = ''
else:
actuel = actuel[0]
if new == None:
return actuel
if not re.match(r'^[+-][pck]$', new):
raise ValueError('modification de controle incorrecte')
for c in 'pc':
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, new=None):
""" Retourne ou change l'uidNumber de l'adhérent """
if not self.compte():
raise NotImplementedError, u"L'adhérent na pas de compte"
if new == None:
return self._data['uidNumber'][0]
else:
new = preattr(new)[1]
self._set('uidNumber', [new])
return new
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 sursis_carte(self):
for h in self.historique()[::-1]:
x=re.match("(.*),.* : .*(paiement\+%s|inscription).*" % config.ann_scol,h)
if x != None:
return ((time.time()-time.mktime(time.strptime(x.group(1),'%d/%m/%Y %H:%M')))<=config.sursis_carte)
return False
def paiement_ok(self):
if isinstance(self, Adherent):
m_paiement = min(self.adhesion(), self.connexion())
else:
m_paiement = self.adhesion()
if config.ann_scol in self.paiement() or (config.periode_transitoire and (config.ann_scol-1) in self.paiement()) or (m_paiement > time.time()) or (config.periode_transitoire and config.debut_periode_transitoire <= m_paiement <= config.fin_periode_transitoire):
if config.periode_transitoire or not isinstance(self, Adherent) or not config.bl_carte_et_definitif or bool(self.carteEtudiant()):
return True
else:
return self.sursis_carte()
else:
return False
def delete(self, comment=''):
"""Destruction du propriétaire"""
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
else:
nouveau = 1
if 'chbre' in self.modifs and '????' in self._init_data.get("chbre", [''])[:2]:
self.services_to_restart('bl_chbre_invalide')
if ('chbre' in self.modifs or 'paiement' in self.modifs) and self._data['chbre'][0] not in ("????", "EXT"):
while True:
bat = self._data['chbre'][0][0]
ch = self._data['chbre'][0][1:]
if annuaires.is_crans(bat, ch):
break
else:
r = prompt(u"La chambre %s est câblée sur le réseau CROUS. Est-ce *bien* la chambre de l'adhérent ? [O/N]" % self._data['chbre'][0], "O")
if r == 'O' or r == 'o':
annuaires.crous_to_crans(bat, ch)
else:
while True:
new_ch = prompt(u"Chambre de l'adhérent ?")
try:
self.chbre(new_ch)
except ValueError, c:
if len(c.args) == 2:
old_adh = c.args[1]
r = prompt(u"Changer %s de chambre ? [O/N]" % old_adh.Nom(), "n")
if r == 'O' or r == 'o':
old_adh.chbre('????')
old_adh.save()
self.chbre(new_ch)
break
else:
break
# Enregistrement
self._save()
# Message de sortie
if nouveau:
ret += coul(u"%s inscrit avec succès." % self.Nom(), 'vert')
ret += coul('\n%s\n' % ('-'* 78), 'rouge')
ret += coul(u"Merci d'indiquer son aid (%s) en haut à gauche de la fiche d'adhésion" % self.id(), 'gras')
ret += coul('\n%s\n' % ('-'* 78), 'rouge')
if self.idn !='cid':
# Mail de bienvenue
self.services_to_restart('mail_bienvenue', [self.mail().encode('utf-8')], start = time.time() + 660)
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:
ret += coul('\n%s\n' % ('-'* 78), 'rouge')
ret += coul(u"Merci d'indiquer son aid (%s) en haut à gauche de la photocopie de la carte d'étudiant" % self.id(), 'gras')
ret += coul('\n%s\n' % ('-'* 78), 'rouge')
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')
self.services_to_restart('dhcp')
# Vérification si changement de bât, ce qui obligerai un changement d'IP
if 'adherentPayant' in self.modifs or '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('<automatique>') )
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]
r = prompt(u"Attribuer tout de suite un mot de passe ? [O/N]", "O")
if r == 'O' or r == 'o':
change_password(login=self.compte())
else:
ret += coul(u' Il faudra penser à attribuer un mot de passe\n', 'jaune')
r = prompt(u"Redirection mail ? [O/N]")
mail1 = mail2 = None
if r.lower().startswith('o'):
while True:
mail1 = prompt(u"Adresse mail ? (ANNUL pour annuler la redirection)")
if mail1 == "ANNUL":
mail1 = None
break
try:
validate_mail(mail1)
except ValueError, e:
print coul(e.message.encode('utf-8'), 'rouge')
continue
mail2 = prompt(u"Adresse mail (répéter) ?")
if mail1 == mail2 and mail1:
break
if mail1:
args += ',' + mail1
self.services_to_restart('home', [ args ])
# 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 strip_accents(new[:], False):
if c not in (string.letters + '- '):
raise ValueError(u"Seuls les caractères alphabétiques, l'espace et le - sont permis dans %s." % champ)
if l<2:
raise ValueError(u"%s trop court." % champ)
if new[0] not in string.letters:
raise ValueError(u"Le premier caractère du %s doit être une lettre" % champ)
self._set(champ, [new])
if self._data.has_key('gecos'):
gecos = '%s %s' % tuple(map(lambda x: strip_accents(x.capitalize(), False), (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éfinit 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.bat_switchs:
# 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'
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])
self._set('postalAddress', [])
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:
email = self._data.get('mail', [''])[0]
if not '@' in email:
return decode(email)+'@crans.org'
else:
return decode(email)
new = validate_mail(new)
# Il ne doit pas y avoir de compte
self.supprimer_compte()
self._set('mail', [new])
# On enlève les blacklistes mail_invalide
self.mail_invalide(False)
# on renvoie le mail de bienvenue
self.services_to_restart('mail_bienvenue', [new.encode('utf-8')])
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 email_exterieur(self, new=None):
"""Retourne ou paramètre l'adresse mail extérieure de l'adhérent"""
if not new:
return decode(self._data.get('mailExt', [''])[0])
new = validate_mail(new)
self._set('mailExt', [new])
# On enlève les blacklistes mail_invalide
self.mail_invalide(False)
return new
def mail_invalide(self, valeur=None):
"""
L'adresse est invalide.
Au niveau de la base, on considère l'absence ou la présence d'une
blackliste mail_invalide.
"""
if valeur == False:
# On enlève les blacklistes
bl_mail_list = [x for x in self.blacklist() if 'mail_invalide' in x]
now = int(time.time())
for bl_mail in bl_mail_list:
bl_data = bl_mail.split('$')
if int(bl_data[0]) <= now and bl_data[1] == '-':
self.blacklist((self.blacklist().index(bl_mail),
[bl_data[0], 'now', 'mail_invalide', bl_data[3]]))
elif int(bl_data[0]) >= now:
self.blacklist((self.blacklist().index(bl_mail),
[str(now-2), str(now-1), 'mail_invalide', bl_data[3]]))
# Sale, mais évite d'avoir plusieurs blacklistes sur les mêmes dates
now -= 2
elif valeur == None:
# On retourne l'existence d'une blackliste active ou future
bl_mail_list = [x for x in self.blacklist() if 'mail_invalide' in x]
now = time.time()
for bl_mail in bl_mail_list:
bl_data = bl_mail.split('$')
if (int(bl_data[0]) <= now and bl_data[1] == '-') or int(bl_data[0]) >= now:
return True
return False
else:
raise ValueError, u'mail_invalide ne peut prendre que None ou False en argument'
def charteMA(self, valeur=None):
"""
La charte des membres actifs est signee. False par defaut.
"""
# tente de modifier la valeur
if valeur == True:
self._set('charteMA', ['TRUE'])
elif valeur == False:
self._set('charteMA', [])
elif valeur != None:
raise ValueError, u"charteMA prend un booléen comme argument"
# renvoie la valeur trouvée dans la base
return bool(self._data.get('charteMA', []))
def connexion(self, mois=None, f=None):
"""
Gestion de la connexion d'un adhérent
* valeur est un entier définissant un nombre de mois
* f est une facture
"""
thetime = time.time()
# On récupère sur les factures l'ensemble de celles comportant une connexion.
conn_factures = self.factures_conn()
finConn = max([0.0] + [fromGeneralizedTimeFormat(facture._data.get('finConnexion', ["19700101000000Z"])[0]) for facture in conn_factures if facture.controle() != "FALSE" and facture.recuPaiement() is not None])
if mois is None:
return finConn
elif not isinstance(mois, int):
raise ValueError, u"Le nombre de mois doit être un entier"
else:
ftime = max(finConn, thetime)
# Calcul de la nouvelle date de fin d'adhésion.
curyear = datetime.datetime.now().year
curmonth = datetime.datetime.now().month
nbJours = 0
for i in xrange(1, mois+1):
nbJours += monthrange((curmonth + i - 1)/12 + curyear, (curmonth + i - 1)%12 + 12 * ((curmonth + i - 1) % 12 == 0))[1]
# On ajoute 3600 secondes sur suggestion de Raphaël Bonaque (<bonaque@crans.org>), pour tenir compte des malheureux qui
# pourraient subir le changement d'heure.
newFinConn = ftime + 86400 * nbJours + 3600
# Si aucune facture n'est passée en référence, on en crée une nouvelle.
if f is None:
f = Facture(self)
f.ajoute(cotisation.dico_cotis(mois))
f._set("finConnexion", [generalizedTimeFormat(newFinConn)])
f._set("debutConnexion", [generalizedTimeFormat(thetime)])
self._set("finConnexion", self._data.get("finConnexion", []) + [generalizedTimeFormat(newFinConn)])
self._set("debutConnexion", self._data.get("debutConnexion", []) + [generalizedTimeFormat(thetime)])
return f
def update_connexion(self):
"""Récupère les dates de début et fin de connexion dans les factures
de l'adhérent, et met à jour ses données."""
debutConn = []
finConn = []
for facture in self.factures_adh():
debutConn.append(generalizedTimeFormat(facture._data['debutConnexion'][0]))
finConn.append(generalizedTimeFormat(facture._data['finConnexion'][0]))
self._set('debutConnexion', debutConn)
self._set('finConnexion', finConn)
self._save()
def adherentPayant(self, valeur = None):
"""
L'adhérent paie sa cotisation (a droit au WiFi, à un compte Crans, ... True par défaut
"""
if isinstance(valeur, bool):
if valeur:
set_to = []
else:
set_to = ['FALSE']
self._set('adherentPayant', set_to)
elif valeur is not None:
raise ValueError, u"adherentPayant prend un booléen comme argument"
# bool d'une liste non vide est true, on inverse donc (la base ne peut contenir que FALSE)
return not bool(self._data.get('adherentPayant', []))
def supprimer_compte(self):
"""
Supprime le compte sur zamok. Penser à définir l'adresse mail après.
"""
self._set('mail', [''])
if abs(self.solde()) >= 0.01:
raise ValueError(u"Le solde d'un adhérent doit être nul pour supprimer son compte crans.")
self._data['objectClass'] = ['adherent']
for c in [ 'uid', 'cn', 'shadowLastChange', 'shadowMax',
'shadowWarning', 'loginShell', 'userPassword',
'uidNumber', 'gidNumber', 'homeDirectory', 'gecos',
'droits', 'mailAlias', 'canonicalAlias',
'rewriteMailHeaders', 'contourneGreylist',
'homepageAlias', 'derniereConnexion', 'solde',
'gpgFingerprint']:
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
"""
if action == True:
self._set('carteEtudiant', ['TRUE'])
elif action == False:
self._set('carteEtudiant', [])
return bool(self._data.get('carteEtudiant', []))
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:
return self._data.get('uid', [''])[0]
# Supression des accents et espaces
login = strip_accents(login)
l, login = preattr(login)
login = login.lower()
if login and not self.adherentPayant():
raise ValueError(u"L'adhérent ne paie pas de cotisation, il n'a pas droit à un compte.")
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/%s/%s' % (login[0], 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'] = ["%s@crans.org" % (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())
if self.canonical_alias(a) == None:
self.canonical_alias(login)
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)' % uidNumber):
# 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] + ',,,' ]
# On enleve les blacklistes mail_invalide
self.mail_invalide(False)
return decode(login)
def active(self, status=None):
"""Permet d'activer ou désactiver un compte"""
if self.compte() == '':
raise EnvironmentError("Can't deactivate non-existing account")
if status == None:
return not (self._data.get("shadowExpire", [""])[0] == "0")
elif status == False:
self._set('shadowExpire', ["0"])
else:
self._set('shadowExpire', [])
return not (self._data.get("shadowExpire", [""])[0] == "0")
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', ["%s@crans.org" % (a)])
return a
def droits(self, droits=None, light=False):
"""Modifie les droits d'un compte.
droits est la liste des droits à donner à l'utilisateur,
light permet de modifier les droits non critiques sans être Nounou."""
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', []))
# Si pas light, il faut être Nounou
if not light:
if not isadm():
raise EnvironmentError(u'Il faut être administrateur pour effectuer cette opération.')
# On vérifie la liste donnée
if type(droits) != list:
raise TypeError(u'Une liste est attendue')
new = []
for droit in droits:
droit = droit.strip()
if droit == '': continue
if droit not in droits_possibles:
raise ValueError(u'Droit %s incorrect' % droit)
new.append(droit)
ancien = self._data.get('droits', [])
# On envoie les mails de "bienvenue" pour chaque nouveau droit
for droit in new:
if droit not in ancien:
db.services_to_restart("mail_ajout_droits", self.compte().encode('latin-1') + ":" + droit)
# Si light, alors on n'a pas le droit de modifier les droits critiques
if light:
diff = [droit for droit in (ancien + new)
if (droit not in ancien or droit not in new) and (droit in droits_critiques)]
if len(diff) > 0:
raise ValueError("Droits critiques modifies (?) :: %s" % ','.join(diff))
# Sauvegarde
if new != self._data.get('droits', []):
self._set('droits', new)
return new
def droitsGeles(self):
reponse = False
if config.bl_vieux_cableurs:
l = self.droits()
if l != []:
if config.ann_scol not in self.paiement():
reponse = True
for d in droits_vieux:
if d in l:
reponse = False
return reponse
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
def dateInscription(self):
"""Renvoie la date d'inscription."""
# En théorie, c'est la date de la première entrée dans l'historique
if self.historique():
h = self.historique()[0]
h = h[:h.find(",")]
try:
dateInsc = time.mktime(time.strptime(h, date_format_new))
except:
dateInsc = time.mktime(time.strptime(h, date_format))
return dateInsc
else:
# Lors de l'inscription d'un nouvel adhérent, celui-ci n'a pas
# encore d'historique. On retourne alors la date en cours.
return time.time()
def is_nounou( self ):
return u"Nounou" in self.droits()
class Club(BaseProprietaire):
""" Classe de définition d'un club """
idn = 'cid'
filtre_idn = '(objectClass=club)'
objectClass = 'club'
def Nom(self, new=None):
""" Définit 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:
l = self.search('aid=%s' % aid)['adherent']
if l:
return l[0]
else:
raise ValueError("Responsable Invalide (%s)" % aid)
else: raise ValueError("Pas de responsable enregistré")
if adher.__class__ != Adherent:
raise ValueError
if not adher.id():
raise AttributeError(u'Adhérent invalide')
self._set('responsable', [adher.id()])
return adher
def imprimeurs(self, ajouter=None, retirer=None):
"""Retourne la liste des aid des personnes autorisees a imprimer avec
le compte *@club-*, ou ajoute/retire un aid a cette liste."""
if self._data.has_key('imprimeurClub'):
liste = list(self._data['imprimeurClub'])
else:
liste = []
if ajouter != None:
if len(db.search('aid=%s' % ajouter)['adherent']) > 0:
if ajouter not in liste:
liste.append(ajouter)
self._set('imprimeurClub', liste)
self.modifs['imprimeurClub'] = None
return True
return False
return False
elif retirer != None:
if retirer in liste:
liste.remove(retirer)
self._set('imprimeurClub', liste)
return True
return False
return liste
def chbre(self, new=None):
""" Définit 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 fourni>
login = login.lower()
if not login.startswith('club-'):
login = 'club-' + login
if not re.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/%s/%s' % (login[0], 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, lock=True):
"""
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)
if mac == '<automatique>':
self._set('macAddress', [mac])
self.ipv6('')
return mac
# 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 sort
raise RuntimeError(u"Fichier de fabriquants de MAC non trouvé !")
if not multi_ok and not vendor:
raise ValueError(
"""Le constructeur correspondant à cette adresse MAC ne peut être trouvé.
L'adresse MAC correspond peut-être à un pont réseau, désactivez ce pont réseau.
Contactez nounou si la MAC est bien celle d'une carte.""", 3)
# La mac serait-elle déjà connue ?
if not multi_ok and self.exist('macAddress=%s' % mac):
raise ValueError(u"Mac déjà utilisée sur le réseau.", 1)
# Lock de la mac
if lock:
self.lock('macAddress', mac)
net = self.netv6()
try:
self._set('macAddress', [mac])
if net.size > 1:
self.ipv6(ip6tools.mac_to_ipv6(net, mac), lock)
else:
self.ipv6(config.ipv6_machines_speciales[int(self.rid())], lock)
except Exception as e:
raise ValueError('La correspondance MAC <-> EUID64 a plante. Tapez PEB et son entourage. (net : %s, mac : %s, exception levee : %s)' % (net, mac, e))
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, 'utf-8'))
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 new.endswith('-'):
raise ValueError(u"Le dernier caractère du champ %s ne peut être un tiret" % 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.bat_switchs:
try:
return annuaires.chbre_prises(chbre[0], 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', [])
return
if not re.match('^[a-cg-jmopv][0-7][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
res = self.conn.search_s(self.dn, 1, "xid=*")
if res:
raise EnvironmentError("La machine possède des certificats, utilisez lc_ldap ou l'intranet2")
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 sshFingerprint(self, new=None):
"""
Fonction d'ajout d'empreinte SSH
"""
liste = list(self._data.get('sshFingerprint', []))
if new == None:
return liste
else:
if type(new) == list:
# Modif
index = new[0]
new = new[1]
if new == '':
# Supression alias
liste.pop(index)
self._set('sshFingerprint', liste)
return liste
else:
index = -1
#if sshexists(new):
# raise ValueError(u"FPR existant ou correspondand à un compte.")
if index != -1:
liste[index] = new
else:
liste = liste + [ new ]
self._set('sshFingerprint', liste)
return liste
def ip(self, ip=None, lock=True):
"""
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 <automatique>.
Si ip=<automatique> 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 '<automatique>'
l, ip = preattr(ip)
# Dans quel réseau la machine doit-elle être placée ?
if isinstance(self, MachineWifi):
net = config.NETs['wifi-adh']
pool_ip = lister_ip_dispo('wifi-adh')
elif isinstance(self, BorneWifi):
net = config.NETs['bornes']
pool_ip = lister_ip_dispo('bornes')
elif isinstance(self.proprietaire(), AssociationCrans):
net = [ '0.0.0.0/0' ]
pool_ip = lister_ip_dispo('all')
else:
proprio = self.proprietaire()
if proprio.etudes(0) == 'Personnel ENS':
net = config.NETs['personnel-ens']
pool_ip = lister_ip_dispo('personnel-ens')
elif not isinstance(proprio, Adherent) or proprio.adherentPayant():
proprio_subnet = 'adherents'
try:
net = config.NETs[proprio_subnet]
pool_ip = lister_ip_dispo(proprio_subnet)
except:
raise RuntimeError(u'Impossible de trouver le réseau où placer la machine.')
else:
net = config.NETs["gratuit"]
pool_ip = lister_ip_dispo("gratuit")
if ip == '<automatique>':
# 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." % ' et '.join(net))
elif ip == '':
self._set('ipHostNumber', [])
ip = None
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.')
if ip != None:
rid = ridtools.Rid(ipv4=ip)
else:
rid = self.rid()
if rid == '':
pass
# Lock ip
if lock and ip:
self.lock('ipHostNumber', ip)
self._set('ipHostNumber', [ip])
self.rid("%d" % rid, lock=lock)
return ip
def rid(self, rid=None, lock=True):
"""
Retourne le rid si rid == None, l'affecte sinon.
"""
if rid == None:
return self._data.get('rid', [''])[0]
rid_t = ridtools.Rid(ipv4=self.ip())
if int(rid_t) != int(rid):
raise ValueError('L\'ip et le rid doivent correspondre. rid : %s, ip : %s, rid_t : %s' % (int(rid), self.ip(), rid_t))
if lock:
self.lock('rid', '%s' % rid)
self._set('rid', ['%s' % rid])
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 du serveur de comptage
(currently odlyd)
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(conn = 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.')
ret = ''
# test si la machine a des certificats
if 'host' in self.modifs:
res = self.conn.search_s(self.dn, 1, "xid=*")
if res:
raise EnvironmentError("La machine possède des certificats, utilisez lc_ldap ou l'intranet2")
# 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))))
# Si la configuration ip à changer on met dynamiquement à jour le dhcp
if reconf_ip:
from gen_confs.dhcpd_new import dydhcp
for server in config.dhcp_servers:
try:
dhcp=dydhcp(server)
dhcp.del_host(self._init_data.get('ipHostNumber',[self.ip()])[0],self._init_data.get('macAddress',[self.mac()])[0])
dhcp.add_host(self._data.get('ipHostNumber',[self.ip()])[0],self._data.get('macAddress',[self.mac()])[0],self.nom())
except socket.error:
sys.stderr.write((u'Dhcp generate failed for %s\n' % server).encode('utf-8'))
# Enregistrement
self._save()
# Si le proprio ne paie pas de cotisation, il n'y a pas de services à
# redémarrer (config automatique pour le vlan inscriptions)
if not isinstance(self.proprietaire(), Adherent) or self.proprietaire().adherentPayant():
# Clef IPsec
if 'ipsec' in self.modifs:
ret += coul(u'Clef WiFi de la machine : %s\n' % self.ipsec(), 'cyan')
# Reconfiguration firewalls et dhcps
if reconf_ip:
self.services_to_restart('macip', reconf_ip)
#self.services_to_restart('classify', reconf_ip)
self.services_to_restart('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('odlyd-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 or 'dnsIpv6' in self.modifs or 'sshFingerprint' in self.modifs:
self.services_to_restart('dns')
# 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')
if 'exempt' in self.modifs:
self.services_to_restart('mail_modif', ['ip=%s' % self.ip()])
# 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 machine"""
if self.proprietaire().__class__ == AssociationCrans and not isadm():
raise EnvironmentError(u'Il faut être administrateur pour effectuer cette opération.')
res = self.conn.search_s(self.dn, 1, "xid=*")
if res:
raise EnvironmentError("La machine possède des certificats, utilisez lc_ldap ou l'intranet2")
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)
# On supprime la machine du dhcp
from gen_confs.dhcpd_new import dydhcp
for server in config.dhcp_servers:
try:
dhcp=dydhcp(server)
dhcp.del_host(self.ip(),self.mac())
except socket.error:
sys.stderr.write((u'Dhcp generate failed for %s\n' % server).encode('utf-8'))
# Services à redémarrer
self.services_to_restart('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 dnsIpv6(self, dnsIpv6 = None):
"""Accès au champ DNS IPv6"""
if dnsIpv6 == True:
self._set('dnsIpv6', ['TRUE'])
elif dnsIpv6 == False:
self._set('dnsIpv6', [])
elif dnsIpv6 != None:
raise ValueError, u"dnsIpv6 prend un booléen comme argument"
# renvoie la valeur trouvée dans la base
return bool(self._data.get('dnsIpv6', []))
def machineAlias(self, machineAlias = None):
"""Accès au champ DNS IPv6"""
if machineAlias == True:
self._set('machineAlias', ['TRUE'])
elif machineAlias == False:
self._set('machineAlias', [])
elif machineAlias != None:
raise ValueError, u"machineAlias prend un booléen comme argument"
# renvoie la valeur trouvée dans la base
return bool(self._data.get('machineAlias', []))
def netv6(self):
"""Retourne le réseau IPv6 Cr@ns associé à la machine"""
nettype, _ = ridtools.find_rid_plage(int(self.rid()))
if nettype != 'special':
return netaddr.IPNetwork(config.prefix[nettype][0])
else:
return netaddr.IPNetwork(config.ipv6_machines_speciales[int(self.rid())])
def ipv6(self, ipv6=None, lock=True):
"""Retourne l'adresse IPv6 correspondant à la machine"""
if ipv6 == None:
if self._data.get('ip6HostNumber', []) == []:
return None
else:
return netaddr.IPAddress(self._data.get('ip6HostNumber')[0])
if ipv6 == '':
self._set('ip6HostNumber', [])
return
ipv6 = str(ipv6)
net = self.netv6()
if net.size == 1:
ipv6_t = config.ipv6_machines_speciales[int(self.rid())]
else:
ipv6_t = str(ip6tools.mac_to_ipv6(net, netaddr.EUI(self.mac())))
if ipv6 != ipv6_t:
raise ValueError('L\'ipv6 n\'est pas celle EUID64 attendue. %s, %s' % (ipv6, ipv6_t))
if lock:
self.lock('ip6HostNumber', ipv6)
self._set('ip6HostNumber', [ipv6])
def nom6(self):
"""Retourne le nom "ipv6" de la machine"""
if self.dnsIpv6():
return self.nom()
else:
s = self.nom().split('.')
s.insert(1, 'v6')
return '.'.join(s)
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 WiFi (anciennement 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(10):
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)
def nombrePrises(self, new=None):
""" Nombre de prises, pour les switchs """
if not new:
return int(self._data.get('nombrePrises', [-1])[0])
try:
new = int(new)
except:
raise ValueError(u'Le nombre de prises doit être un entier')
vals = [24, 26, 28, 50, 52]
if new not in vals:
raise ValueError('Le nombre de prises doit etre %s ou %d' % (
', '.join(str(val) for val in vals[:-1]), vals[-1]))
self._set('nombrePrises', [str(new)])
return new
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':['']}
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 config.modePaiement:
raise ValueError, u'Mode de paiement non accepté'
self._set('modePaiement', [new])
return decode(self._data.get('modePaiement', [''])[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 is not None and new != False:
# 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])
elif new == False:
# 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._vider()
# modifie la base ldap
self._set("recuPaiement", [])
# renvoie la valeur trouvée dans la base
return self._data.get("recuPaiement", [None])[0]
def controle(self, action=None):
if action is None:
return self._data.get("controle", [''])[0]
else:
if action == True:
self._set("controle", ["TRUE"])
elif action == False:
self._set("controle", ["FALSE"])
elif action == "":
self._set("controle", [])
else:
raise ValueError("Mauvaise valeur pour l'attribut controle : %r" % (repr(action),))
def _del_recu_paiement(self):
""" Pour test """
self._set("recuPaiement", [])
def _crediter(self):
"""
Crédite 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 (on débite d'abord si jamais quelqu'un s'amuse à recharger son solde avec son solde)
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()
if self.modePaiement() == 'solde':
proprio = self.proprietaire()
proprio.solde(operation=0.0 - self.total(), comment="Facture n°%s" % self.numero())
proprio.save()
def _vider(self):
"""
Retire les articles à son propriétaire
"""
# 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 (on débite d'abord si jamais quelqu'un s'amuse à recharger son solde avec son solde)
if art["code"] == "SOLDE":
proprio = self.proprietaire()
proprio.solde(operation=-art['nombre']*art["pu"], comment="Revert facture n°%s : %s" % (self.numero(), art['designation']))
proprio.save()
if art["code"] == "FRAIS":
self.supprime(art)
if self.modePaiement() == 'solde':
proprio = self.proprietaire()
proprio.solde(operation=0.0 + self.total(), comment="Revert facture n°%s" % self.numero())
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']
# arrondissage-tronquage
s = float(int(s*100)/100.0)
# 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=None, pop=None):
"""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()
if pop is not None:
_ = arts.pop()
# 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, useSandbox = False, businessMail = "paypal@crans.org",
return_page=None, cancel_return_page=None):
"""
Retourne l'url paypal pour le paiement de cette facture
"""
if useSandbox:
url = "https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_cart"
else:
url = "https://www.paypal.com/cgi-bin/webscr?cmd=_cart"
url += "&upload=1"
url += "&business=%s" % businessMail
url += "&currency_code=EUR"
url += "&no_shipping=1"
url += "&no_note=1"
if return_page != None:
url += "&return=%s" % return_page
if cancel_return_page != None:
url += "&cancel_return=%s" % cancel_return_page
url += "&invoice="+self.numero() # 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, new=None):
if new is not None:
print >>sys.stderr, "Tentative d'ajout de blacklist à un propriétaire virtuel :"
try:
print >>sys.stderr, "Nom : %s" % self.Nom()
except AttributeError:
print >>sys.stderr, "Impossible de récupérer le nom..."
print >>sys.stderr, "Blacklist :"
print >>sys.stderr, new
raise ValueError("Impossible de blacklister %r" % str(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
def adhesion(self):
return time.time() + 86400
def connexion(self):
return time.time() + 86400
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 "Crans"
def chbre(self):
return "CRA"
def email(self):
return "roots@crans.org"
def paiement_ok(self):
"""b2moo: why not. Des fos gest_crans plante dessus"""
return True
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
def pid_exists(pid):
"""Check whether pid exists in the current process table.
Credits:
http://stackoverflow.com/questions/568271/how-to-check-if-there-exists-a-process-with-a-given-pid
"""
if pid < 0:
return False
try:
os.kill(pid, 0)
except OSError, e:
return e.errno == errno.EPERM
else:
return True
if __name__ == '__main__':
import sys
usage = u"""Usage %s [ACTION]
--lock : donne la liste des locks actifs
--purgelock : supprime tous les locks de la base LDAP
--zombielock : supprime les locks de processus plantés sur ce serveur""" % sys.argv[0]
if len(sys.argv) != 2:
cprint(usage)
sys.exit(1)
elif '--lock' in sys.argv:
cprint(u"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:
cprint(u"Suppression de tous les locks")
crans_ldap().remove_lock('*')
elif '--zombielock' in sys.argv:
cprint(u"Suppression des locks zombies")
for (dn, attrs) in crans_ldap().list_locks():
[lockhost, pid] = attrs['lockid'][0].split('-')[0:2]
if hostname == lockhost and not pid_exists(int(pid)):
cprint(u"Suppresion de %s" % repr(attrs))
crans_ldap().remove_lock(dn)
## CASSÉ SUPPRIME LES MACHINES CRANS !!!!!!!!!!
# elif '--menage' in sys.argv:
# cprint(u"Ménage des machines des adhérents partis...")
# machines = crans_ldap().search('paiement!=%s&host=*.crans.org' % ann_scol , 'w')['machine']
# cprint(u"Destruction de %i machines" % len(machines))
# for m in machines:
# cprint(u'Destruction de %s' % m.nom())
# m.delete('Ménage')
else:
cprint(usage)
sys.exit(1)