
Ignore-this: 5a7ae8cbbb8368cbb2d376b785bda204 darcs-hash:20121106173911-3a55a-40830d547ded3ba9876f3fd1656025710b53f0f8.gz
3906 lines
142 KiB
Python
3906 lines
142 KiB
Python
#! /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 smtplib, re, os, random, string, time, sys, pwd
|
|
import ldap, ldap.modlist, ldap_passwd
|
|
import netaddr
|
|
|
|
import annuaires_pg as annuaires
|
|
import config, iptools, ip6tools, cPickle, config_mail
|
|
from chgpass import chgpass
|
|
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 midtools
|
|
|
|
date_format = '%d/%m/%Y %H:%M'
|
|
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()
|
|
|
|
cur_user = os.getenv('SUDO_USER') or pwd.getpwuid(os.getuid())[0]
|
|
|
|
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:
|
|
sys.path.append('/usr/scripts/gestion/secrets')
|
|
from secrets import ldap_password, ldap_auth_dn
|
|
except:
|
|
sys.stdout.write(coul('Warning : impossible de lire le fichier de secret !\n', 'jaune'))
|
|
sys.exit(1)
|
|
|
|
# uri pour les instances de CransLdap faisant de la lecture seule
|
|
if os.path.exists('/var/run/ldapi'):
|
|
ro_uri = 'ldapi://%2fvar%2frun%2fldapi/'
|
|
else:
|
|
ro_uri = uri
|
|
else:
|
|
# pour les autres on utilise ldap.adm.crans.org en rw
|
|
uri = 'ldap://ldap.adm.crans.org/'
|
|
|
|
# avec le secret
|
|
try:
|
|
sys.path.append('/usr/scripts/gestion/secrets')
|
|
from secrets import ldap_password, ldap_auth_dn
|
|
except:
|
|
sys.stdout.write(coul('Warning : impossible de lire le fichier de secret !\n', 'jaune'))
|
|
sys.exit(1)
|
|
|
|
# uri pour les instances de CransLdap faisant de la lecture seule
|
|
if os.path.exists('/var/run/ldapi'):
|
|
ro_uri = 'ldapi://%2fvar%2frun%2fldapi/'
|
|
else:
|
|
ro_uri = uri
|
|
|
|
##################################################################################
|
|
### Items de la blackliste
|
|
blacklist_items = { u'bloq': u'Bloquage total de tous les services',
|
|
u'virus': u'Passage en VLAN isolement',
|
|
u'upload': u"Bloquage total de l'accès à l'extérieur",
|
|
u'warez': u'Bloquage sur squid',
|
|
u'p2p': u"Bloquage total de l'accès à l'extérieur",
|
|
u'autodisc_virus': u'Autodisconnect pour virus',
|
|
u'autodisc_upload': u'Autodisconnect pour upload',
|
|
u'autodisc_p2p': u'Autodisconnect pour P2P',
|
|
u'ipv6_ra': u'Isolement pour RA',
|
|
u'mail_invalide': u'Bloquage sur squid',
|
|
}
|
|
|
|
##################################################################################
|
|
### Droits possibles
|
|
droits_possibles = [u'MultiMachines', u'Cableur', u'Imprimeur', u'Apprenti',
|
|
u'WebMaster', u'Moderateur', u'WebRadio',
|
|
u'Nounou', u'Tresorier', u'Bureau']
|
|
|
|
##################################################################################
|
|
### 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 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), unicode(val, 'iso-8859-1').encode('utf-8')]
|
|
elif isinstance(val, unicode):
|
|
val = val.strip()
|
|
return [len(val), val.encode('utf-8')]
|
|
else:
|
|
raise TypeError
|
|
|
|
|
|
def is_actif(sanction):
|
|
"""
|
|
Retourne True ou False suivant si la sanction fournie (chaîne
|
|
venant de blacklist) est active ou non
|
|
"""
|
|
bl = sanction.split('$')
|
|
now = time.time()
|
|
debut = int(bl[0])
|
|
if bl[1] == '-':
|
|
fin = now + 1
|
|
else:
|
|
fin = int(bl[1])
|
|
return debut < now and fin > now
|
|
|
|
|
|
def format_mac(mac):
|
|
"""
|
|
Formatage des adresses mac
|
|
Transforme une adresse pour obtenir la forme xx:xx:xx:xx:xx:xx
|
|
Le séparateur original peut être :, - ou rien
|
|
Retourne la mac formatée.
|
|
"""
|
|
l, mac = preattr(mac)
|
|
mac = mac.strip().replace("-", ":")
|
|
if mac.count(":") == 5:
|
|
# On a une adresse de la forme 0:01:02:18:d1:90
|
|
# On va compléter s'il manque des 0
|
|
mac = ":".join(map(lambda x: x.replace(' ', '0'),
|
|
map(lambda x: "%02s" % x, mac.split(":"))))
|
|
mac = mac.replace(':', '').lower()
|
|
if len(mac) != 12:
|
|
raise ValueError(u"Longueur de l'adresse mac incorrecte.")
|
|
for c in mac[:]:
|
|
if c not in string.hexdigits:
|
|
raise ValueError(u"Caractère interdit '%s' dans l'adresse mac." % c)
|
|
if mac == '000000000000':
|
|
raise ValueError(u"MAC nulle interdite\nIl doit être possible de modifier l'adresse de la carte.")
|
|
|
|
# Formatage
|
|
mac = "%s:%s:%s:%s:%s:%s" % (mac[:2], mac[2:4], mac[4:6], mac[6:8], mac[8:10], mac[10:])
|
|
|
|
return mac
|
|
|
|
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 <= 4) \
|
|
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(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': 'mail',
|
|
'hostname': 'host',
|
|
'mac': 'macAddress',
|
|
'ip': 'ipHostNumber',
|
|
'telephone': 'tel',
|
|
'position': 'positionBorne' }
|
|
|
|
# Les différentes classes LDAP de machines
|
|
ldap_machines_classes = ['machineFixe', 'machineWifi', 'machineCrans', 'borneWifi']
|
|
|
|
# Champs de recherche pour la recherche automatique
|
|
auto_search_machines_champs = \
|
|
['macAddress', 'host', 'ipHostNumber', 'hostAlias']
|
|
|
|
auto_search_champs = { \
|
|
'adherent': \
|
|
['nom', 'prenom', 'tel', 'mail', 'chbre', 'mailAlias', 'canonicalAlias', 'mailExt'],
|
|
'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']
|
|
|
|
non_auto_search_champs = { \
|
|
'adherent': \
|
|
['etudes', 'paiement', 'carteEtudiant', 'aid', 'postalAddress',
|
|
'historique', 'blacklist', 'droits', 'uidNumber', 'uid', 'info',
|
|
'solde', 'controle', 'contourneGreylist', 'rewriteMailHeaders',
|
|
'ablacklist', 'homepageAlias', 'charteMA',
|
|
'adherentPayant'], \
|
|
'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']}
|
|
|
|
# tous les champs de recherche
|
|
search_champs = {}
|
|
for i in auto_search_champs.keys():
|
|
search_champs[i] = auto_search_champs[i] + non_auto_search_champs[i]
|
|
|
|
# Profondeur des différentes recherches (scope)
|
|
scope = { 'adherent': 1,
|
|
'club': 1,
|
|
'machineFixe': 2,
|
|
'machineWifi': 2,
|
|
'machineCrans': 2,
|
|
'borneWifi': 2,
|
|
'facture': 2}
|
|
|
|
def __init__(self, readonly=False):
|
|
self.connect(readonly)
|
|
|
|
def __del__(self):
|
|
# Destruction des locks résiduels
|
|
if hasattr(self, '_locks'):
|
|
for lock in self._locks:
|
|
self.remove_lock(lock)
|
|
|
|
def connect(self, readonly=False):
|
|
""" Initialisation la connexion vers le serveur LDAP """
|
|
if readonly:
|
|
self.conn = ldap.initialize(ro_uri)
|
|
return
|
|
|
|
self.conn = ldap.initialize(uri)
|
|
nbessais = 0
|
|
while True:
|
|
try:
|
|
self.conn.bind_s(ldap_auth_dn, ldap_password, ldap.AUTH_SIMPLE)
|
|
break
|
|
except ldap.SERVER_DOWN:
|
|
nbessais += 1
|
|
if nbessais > 2:
|
|
sys.stderr.write("ERREUR : serveur LDAP injoignable\n")
|
|
sys.exit(1)
|
|
else:
|
|
time.sleep(0.3)
|
|
|
|
def exist(self, arg):
|
|
"""
|
|
Vérifie l'existence d'une entrée dans la base et que cette
|
|
entrée n'appartient pas à l'objet en cours.
|
|
|
|
La méthode prend en compte les locks.
|
|
|
|
arg doit être une expression de recherche ldap
|
|
|
|
Si il y a existence, on retourne la liste des dn correspondants
|
|
Sinon retourne une liste vide
|
|
|
|
Exemple : exist('chbre=Z345') vérifie si il y a un adhérent en Z345
|
|
"""
|
|
r = []
|
|
|
|
# Premier test: dans les objets déjà inscrits
|
|
ret = self.conn.search_s(self.base_dn, 2, arg)
|
|
for res in ret:
|
|
# C'est peut être l'objet courant
|
|
try: # Si ce n'est pas une classe fille avec l'attribut dn => erreur
|
|
if res[0] == self.dn:
|
|
continue
|
|
except:
|
|
pass
|
|
r.append(res[0])
|
|
|
|
# Deuxième test : lock ?
|
|
ret = self.conn.search_s(self.base_lock, 1, arg)
|
|
lockid = '%s-%s' % (hostname, os.getpid())
|
|
for res in ret:
|
|
# Lock encore actif ?
|
|
l = res[1]['lockid'][0]
|
|
if l == lockid: continue
|
|
# C'est locké par un autre process que le notre
|
|
# il tourne encore ?
|
|
try:
|
|
if l.split('-')[0] == hostname and os.system('ps %s > /dev/null 2>&1' % l.split('-')[1]):
|
|
# Il ne tourne plus
|
|
self.remove_lock(res[0]) # delock
|
|
continue
|
|
except:
|
|
pass
|
|
r.append(res[0])
|
|
|
|
return r
|
|
|
|
def lock(self, item, valeur):
|
|
"""
|
|
Lock un item avec la valeur valeur, les items possibles
|
|
peuvent être :
|
|
|
|
aid $ chbre $ mail $ mailAlias $ canonicalAlias $
|
|
mid $ macAddress $ host $ hostAlias $ ipHostNumber
|
|
|
|
Retourne le dn du lock
|
|
"""
|
|
|
|
valeur = valeur.encode('utf-8')
|
|
|
|
if not valeur:
|
|
# On le lock pas un truc vide
|
|
return True
|
|
|
|
lock_dn = '%s=%s,%s' % (item, valeur, self.base_lock)
|
|
lockid = '%s-%s' % (hostname, os.getpid())
|
|
modlist = ldap.modlist.addModlist({ 'objectClass': 'lock',
|
|
'lockid': lockid,
|
|
item: valeur })
|
|
|
|
try:
|
|
self.conn.add_s(lock_dn, modlist)
|
|
except ldap.ALREADY_EXISTS:
|
|
# Pas de chance, le lock est déja pris
|
|
try:
|
|
res = self.conn.search_s(lock_dn, 2, 'objectClass=lock')[0]
|
|
l = res[1]['lockid'][0]
|
|
except: l = '%s-1' % hostname
|
|
if l != lockid:
|
|
# C'est locké par un autre process que le notre
|
|
# il tourne encore ?
|
|
if l.split('-')[0] == hostname and os.system('ps %s > /dev/null 2>&1' % l.split('-')[1] ):
|
|
# Il ne tourne plus
|
|
self.remove_lock(res[0]) # delock
|
|
return self.lock(item, valeur) # relock
|
|
raise EnvironmentError(u'Objet (%s=%s) locké, patienter.' % (item, valeur), l)
|
|
else:
|
|
if not hasattr(self, '_locks'):
|
|
self._locks = [lock_dn]
|
|
else:
|
|
self._locks.append(lock_dn)
|
|
|
|
def remove_lock(self, lockdn):
|
|
"""
|
|
Destruction d'un lock
|
|
Destruction de tous les locks si lockdn=*
|
|
"""
|
|
# 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 os.getuid() != 0:
|
|
openlog('CransLdap.search')
|
|
syslog('(%s,%s) %s' % (script_utilisateur, mode, expression))
|
|
closelog()
|
|
|
|
if type(expression) == str:
|
|
# Transformation de l'expression en utf-8
|
|
expression = unicode(expression, 'iso-8859-15').encode('utf-8')
|
|
elif type(expression) == unicode:
|
|
expression = expression.encode('utf-8')
|
|
else:
|
|
raise TypeError(u'Chaîne attendue')
|
|
|
|
if not expression:
|
|
return []
|
|
|
|
# On échappe les caractères spéciaux
|
|
expression = expression.replace('\\', '\\\\').replace('(', '\\(').replace(')', '\\)')
|
|
|
|
# Il faut un filtre par type d'objet de la base
|
|
filtres = self.auto_search_champs.keys()
|
|
result = {}
|
|
for i in filtres:
|
|
result[i] = []
|
|
|
|
# Fonction utile
|
|
def build_filtre(champ, expr, neg=False):
|
|
"""
|
|
Retourne une chaine pour recherche dans la base LDAP
|
|
du style (champ=expr) en adaptant les valeurs de expr au champ.
|
|
Si neg = True, retourne le négatif : (!(champ=expr))
|
|
"""
|
|
el = ''
|
|
if champ in ['host', 'hostAlias']:
|
|
if expr[-1] == '*':
|
|
el = '(%s=%s)' % (champ, expr)
|
|
elif '.' not in expr:
|
|
el = '(%s=%s.*)' % (champ, expr)
|
|
else:
|
|
el = '(%s=%s*)' % (champ, expr)
|
|
elif champ == 'macAddress':
|
|
# Formatage adresse mac
|
|
try:
|
|
el = '(macAddress=%s)' % format_mac(expr)
|
|
except:
|
|
pass
|
|
elif champ == 'paiement' and expr == 'ok':
|
|
# Paiement donnant droit à une connexion maintenant ?
|
|
# (il doit avoir payé pour l'année en cours ou pour
|
|
# l'année précédente si on est en septembre
|
|
#
|
|
# Dans tous les cas, pour un adhérent, le paiement est
|
|
# considéré non ok s'il n'a pas fourni sa carte d'etudiant
|
|
# alors que l'on est desormais en periode de bloquage
|
|
# définifif (cf config.py).
|
|
if config.periode_transitoire:
|
|
# Pour la période transitoire année précédente ok
|
|
el = "(|(paiement=%d)(paiement=%d))" % (ann_scol, ann_scol-1)
|
|
else:
|
|
el = "(paiement=%s)" % ann_scol
|
|
# Doit-on bloquer en cas de manque de la carte d'etudiant ?
|
|
# (si période transitoire on ne bloque dans aucun cas)
|
|
if config.bl_carte_et_definitif:
|
|
el = "(&(|(carteEtudiant=%d)(objectClass=club))%s)" % (ann_scol, el)
|
|
elif champ[1:] == 'blacklist':
|
|
el = '(blacklist=%s)' % expr
|
|
else:
|
|
# Cas général
|
|
el = '(%s=%s)' % (champ, expr)
|
|
if neg: el = '(!%s)' % el
|
|
return el
|
|
|
|
if '=' in expression:
|
|
#### Recherche avec conditions explicites
|
|
## Construction des filtres
|
|
|
|
# Initialisation
|
|
filtre = {}
|
|
filtre_seul = {}
|
|
# filtre_seul[i] est True si la requête porte sur un champ
|
|
# qui n'est pas dans toutes les machines, mais dans i
|
|
ignore_filtre =[] # liste des filtres à ignorer
|
|
for i in filtres:
|
|
filtre[i] = ''
|
|
filtre_seul[i] = False
|
|
|
|
conds = expression.split('&')
|
|
|
|
# Test de l'expression de recherche et classement par filtres
|
|
for cond in conds:
|
|
neg = False
|
|
try:
|
|
champ, expr = cond.strip().split('=')
|
|
if champ[-1] == '!':
|
|
# Négation pour ce champ
|
|
champ = champ[:-1]
|
|
neg = True
|
|
except:
|
|
raise ValueError(u'Syntaxe de recherche invalide (%s)' % cond)
|
|
|
|
# Transformation de certains champs
|
|
champ = self.trans.get(champ, champ)
|
|
|
|
if expr == '': expr = '*'
|
|
|
|
ok = False
|
|
|
|
# si c'est un champ uniquement adherent (genre droit), on ignore les clubs et vice versa
|
|
if champ in self.search_champs['club'] and champ not in self.search_champs['adherent']:
|
|
if 'adherent' not in ignore_filtre: ignore_filtre.append('adherent')
|
|
if champ in self.search_champs['adherent'] and champ not in self.search_champs['club']:
|
|
if 'club' not in ignore_filtre: ignore_filtre.append('club')
|
|
|
|
# Construction du filtre
|
|
for i in filtres:
|
|
if champ in self.search_champs[i]:
|
|
filtre[i] += build_filtre(champ, expr, neg)
|
|
ok = True
|
|
if champ not in self.auto_search_machines_champs \
|
|
and champ not in self.non_auto_search_machines_champs:
|
|
filtre_seul[i] = True
|
|
|
|
if not ok:
|
|
raise ValueError(u'Champ de recherche inconnu (%s)' % champ)
|
|
|
|
## Recherche avec chacun des filtres
|
|
r = {} # contiendra les réponses par filtre
|
|
|
|
# On efface les bons filtres si la requête porte sur des
|
|
# champs spéciaux
|
|
ok = False
|
|
for i in self.ldap_machines_classes:
|
|
ok = ok or filtre_seul[i]
|
|
if ok:
|
|
# Il y a au moins un champ spécial
|
|
for i in self.ldap_machines_classes:
|
|
if not filtre_seul[i]:
|
|
filtre[i] = ''
|
|
|
|
for i in filtres:
|
|
if i in ignore_filtre or filtre[i] == '':
|
|
# Filtre vide ou à ignorer
|
|
filtre[i] = ''
|
|
r[i] = None
|
|
else:
|
|
# Filtre valide
|
|
filtre[i] = '(&(objectClass=%s)%s)' % (i, filtre[i])
|
|
r[i] = self.conn.search_s(self.base_dn, self.scope[i], filtre[i])
|
|
|
|
## On a alors une liste de résultats
|
|
## r = {categorie1: [(result1), (result2), ...], ...}
|
|
|
|
# Traitement
|
|
|
|
if (r['machineFixe'] != None or r['machineWifi'] != None) \
|
|
and (r['adherent'] != None or r['club'] != None) \
|
|
and len(conds) > 1:
|
|
|
|
# On renvoie toutes les machineCrans et borneWifi
|
|
for i in 'machineCrans', 'borneWifi':
|
|
if r[i] == None:
|
|
continue
|
|
for res in r[i]:
|
|
result[i].append(self.make(res, mode))
|
|
|
|
# On croise maintenant les résultats machine et propriétaire
|
|
|
|
# Traitement des machines
|
|
mach_adh = [] # liste de dn d'adhérents et de clubs
|
|
for res in r['machineFixe'] + r['machineWifi']:
|
|
dn = ','.join(res[0].split(',')[-4:])
|
|
if dn[:3] != 'aid' and dn[:3] != 'cid':
|
|
continue
|
|
if dn not in mach_adh:
|
|
mach_adh.append(dn)
|
|
|
|
# Croisement
|
|
bons_dn = [] # liste des dn d'adhérents qui correspondent aux critères
|
|
for i in 'adherent', 'club':
|
|
if r[i] == None:
|
|
continue
|
|
for a in r[i]:
|
|
if a[0] in mach_adh and not a[0] in bons_dn:
|
|
bons_dn.append(a[0])
|
|
result[i].append(self.make(a, mode))
|
|
|
|
# Maintenant c'est au tour des bonnes machines
|
|
bons_dn2 = []
|
|
for i in 'machineFixe', 'machineWifi':
|
|
if r[i] == None:
|
|
continue
|
|
for a in r[i]:
|
|
dn = ','.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 = base['adherent'] + base['club']
|
|
if graphic: a = anim('\tTri des machines', len(base))
|
|
for adh in base:
|
|
if graphic: a.cycle()
|
|
# Adhérent ayant payé l'année en cours
|
|
if 'bloq' in adh.blacklist_actif():
|
|
# Adhérent ignoré
|
|
continue
|
|
self.__machines += adh.machines()
|
|
if graphic: a.reinit()
|
|
if graphic: 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'attribu caractéristique de la classe (aid,mid,cid)"""
|
|
try:
|
|
s = self.dn.split(',')[0].split('=')
|
|
if s[0] == self.idn:
|
|
return s[1]
|
|
except:
|
|
return ''
|
|
|
|
def __str__(self):
|
|
""" Chainde identifiant l'objet de la forme 'uid=1245' """
|
|
return '%s=%s' % (self.idn, self.id())
|
|
|
|
def blacklist_actif(self):
|
|
"""
|
|
Vérifie si l'instance courante est blacklistée.
|
|
Retourne les sanctions en cours (liste).
|
|
Retourne une liste vide si aucune sanction en cours.
|
|
"""
|
|
return self.blacklist_all()[0].keys()
|
|
|
|
def blacklist_all(self):
|
|
"""
|
|
Vérifie si l'instance courante est blacklistée ou a été
|
|
blacklistée. Retourne les sanctions en cours sous la forme
|
|
d'un couple de deux dictionnaires (l'un pour les sanctions
|
|
actives, l'autre pour les inactive), chacun ayant comme
|
|
clef la sanction et comme valeur une liste de couple de
|
|
dates (en secondes depuis epoch) correspondant aux
|
|
différentes périodes de sanctions.
|
|
|
|
ex: {'upload': [(1143336210, 1143509010), ...]}
|
|
"""
|
|
bl_liste = self._data.get('blacklist', [])
|
|
|
|
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()):
|
|
# 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 (config.ann_scol in self.carteEtudiant()):
|
|
for h in self.historique()[::-1]:
|
|
x=re.match("(.*),.* : .*paiement\+%s.*" % config.ann_scol,h)
|
|
if x != None:
|
|
if (time.time()-time.mktime(time.strptime(x.group(1),'%d/%m/%Y %H:%M')))>config.sursis_carte:
|
|
actifs['carte_etudiant']=('-','-')
|
|
break
|
|
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()
|
|
"""
|
|
if not self._data.has_key('info'):
|
|
self._data['info'] = []
|
|
liste = list(self._data['info'])
|
|
if new == None: return map(decode, liste)
|
|
|
|
if type(new) == list:
|
|
# Modif
|
|
index = new[0]
|
|
l, new = preattr(new[1])
|
|
if not new:
|
|
# Supression remarque
|
|
liste.pop(index)
|
|
else:
|
|
# Modif remarque
|
|
liste[index] = new
|
|
elif type(new) == str:
|
|
# Remarque supplémentaire
|
|
l, new = preattr(new)
|
|
if not new:
|
|
# On ajoute pas de remarque vide
|
|
return liste
|
|
# Ajout à la liste
|
|
liste = liste + [new]
|
|
else:
|
|
raise TypeError
|
|
|
|
self._set('info', liste)
|
|
return liste
|
|
|
|
def _save(self):
|
|
""" Sauvegarde dans la base LDAP """
|
|
if not self.modifs:
|
|
# Rien à faire
|
|
return []
|
|
|
|
if not self.dn:
|
|
# Enregistrement à placer en tête de base
|
|
self.dn = self.base_dn
|
|
|
|
# Construction de l'historique
|
|
if not self._init_data:
|
|
if self.idn=='fid':
|
|
modif = preattr('création')[1]
|
|
else:
|
|
modif = 'inscription'
|
|
else:
|
|
### ON NE TOUCHE PAS A SELF.MODIFS, IL EST UTILISÉ PLUS LOIN !!!!!!!
|
|
# Dictionnaire local des modifs
|
|
modif = {}
|
|
|
|
# Cas spécial
|
|
if "solde" in self.modifs:
|
|
diff = float(self._init_data.get('solde', [0])[0]) - float(self._data.get('solde', [0])[0])
|
|
if diff > 0:
|
|
modif['solde'] = "debit %s Euros" % str(diff)
|
|
else:
|
|
modif['solde'] = "credit %s Euros" % str(-diff)
|
|
|
|
# Formate les entrées de l'historique de la forme champ (ancien -> nouveau)
|
|
# On suppose que le champ apparaît forcément dans l'enregistrement
|
|
for champ in ['chbre', 'nom', 'prenom', 'mail', 'tel',
|
|
'rewriteMailHeaders', 'contourneGreylist',
|
|
'puissance', 'canal', 'prise', 'responsable',
|
|
'macAddress', 'ipHostNumber', 'host', 'positionBorne',
|
|
'derniereConnexion', 'hotspot', 'dnsIpv6', 'machineAlias']:
|
|
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']:
|
|
if champ in self.modifs:
|
|
if champ == 'controle':
|
|
# Ce n'est pas pareil que self._init_data.get('controle', [''])
|
|
# qui peut renvoyer une liste vide (petite erreur de choix
|
|
# dans la première implémentation de controle)
|
|
ancien = (self._init_data.get('controle') or [''])[0]
|
|
nouveau = (self._data.get('controle') or [''])[0]
|
|
else:
|
|
# Là, on bosse directement sur les listes renvoyées par get
|
|
ancien = self._init_data.get(champ, [])
|
|
nouveau = self._data.get(champ, [])
|
|
|
|
# On établit le diff
|
|
diff = ''.join([ '+%s' % decode(d) for d in nouveau if d not in ancien ])
|
|
diff += ''.join([ '-%s' % decode(d) for d in ancien if d not in nouveau ])
|
|
modif[champ] = champ + diff
|
|
|
|
# On recolle tous les morceaux
|
|
liste_historique = []
|
|
for champ in self.modifs.keys():
|
|
# champs pour lesquels on ne veut pas d'historique
|
|
if champ in ['userPassword']:
|
|
continue
|
|
ligne = modif.get(champ, champ)
|
|
if self.modifs[champ] != None:
|
|
ligne += ' [%s]' % self.modifs[champ]
|
|
liste_historique.append(ligne)
|
|
modif = ', '.join(liste_historique)
|
|
|
|
timestamp = time.localtime()
|
|
hist = "%s, %s" % ( time.strftime(date_format, timestamp), script_utilisateur )
|
|
|
|
if self.modifs.has_key('derniereConnexion'):
|
|
# On nettoie l'historique pour ne garder que la dernière modification
|
|
# (celle en cours)
|
|
self._data['historique'] = [x
|
|
for x in self._data['historique']
|
|
if 'derniereConnexion' not in x]
|
|
|
|
# On loggue
|
|
try:
|
|
fd = file('%s/%s_%s_%s' % ("%s/logs" % config.cimetiere, str(self.__class__).split('.')[-1],
|
|
time.strftime('%Y-%m-%d-%H:%M', timestamp), self.nom()), 'wb')
|
|
fd.write("%s\n" % self._data)
|
|
fd.close()
|
|
except:
|
|
pass
|
|
|
|
# Suffit-t-il d'ajouter un item au dernier élément de l'historique ?
|
|
try:
|
|
if modif:
|
|
dern = self._data['historique'][-1].split(': ', 2)
|
|
if dern[0] == hist:
|
|
# Même date et même cableur
|
|
if modif not in dern[1].split(', '):
|
|
# Qqch de plus de modifié
|
|
self._data['historique'][-1] = self._data['historique'][-1] + ', ' +modif
|
|
else:
|
|
# Nouvelle entrée
|
|
# NE PAS UTILISER L'OPERATEUR += ICI sinon self._init_data aussi modififié
|
|
self._data['historique'] = self._data['historique'] + [ preattr("%s : %s" % ( hist, modif ))[1] ]
|
|
except:
|
|
# Nouvelle inscription
|
|
self._data['historique'] = [ "%s : %s" % ( hist, modif ) ]
|
|
|
|
if not self._init_data:
|
|
### Nouvel enregistrement
|
|
# Forcage de mid correspondant à l'ip.
|
|
if self.idn == 'mid':
|
|
my_mid = midtools.Mid(ipv4=self._data["ipHostNumber"][0])
|
|
self.dn = '%s=%d,%s' % (self.idn, my_mid, self.dn)
|
|
self._data[self.idn] = [ '%d' % my_mid ]
|
|
modlist = ldap.modlist.addModlist(self._data)
|
|
self.conn.add_s(self.dn, modlist)
|
|
else:
|
|
# 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, 2, self.filtre_idn)
|
|
vidn = 1
|
|
vidns = []
|
|
# Liste des dn pris
|
|
for r in res:
|
|
# r = ( dn, {} )
|
|
r = r[0].split(',')[0]
|
|
if r[:4] != '%s=' % self.idn: continue
|
|
vidns.append(int(r[4:]))
|
|
if self.idn=='fid':
|
|
# Pour une facture on prend un nouveau numéro
|
|
vidn = max([0]+vidns)+1
|
|
else:
|
|
# Sinon on prend le premier libre
|
|
while vidn in vidns:
|
|
vidn += 1
|
|
|
|
self.dn = '%s=%s,%s' % (self.idn, vidn, self.dn)
|
|
self._data[self.idn] = [ '%d' % vidn ]
|
|
|
|
try:
|
|
# Ecriture
|
|
modlist = ldap.modlist.addModlist(self._data)
|
|
self.conn.add_s(self.dn, modlist)
|
|
break
|
|
except Exception, 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(u'Entrée en double dans le champ %s' % champ)
|
|
|
|
# l'ip change, le mid aussi...
|
|
if 'ipHostNumber' in self.modifs:
|
|
try:
|
|
mid = midtools.Mid(ipv4 = self._data["ipHostNumber"][0])
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
self.conn.rename_s(self.dn, "%s=%d" % (self.idn, mid))
|
|
self.dn = "%s=%d,%s" % (self.idn, mid, self.dn.split(',', 1)[1])
|
|
self._data[self.idn] = [ '%d' % mid ]
|
|
|
|
|
|
### Génération de la liste de services à redémarrer
|
|
# Quasiement tout est traité dans les classes filles.
|
|
if hasattr(self, "_blacklist_restart"):
|
|
for n, t in self._blacklist_restart.items():
|
|
self.services_to_restart("blacklist_%s"%n, [], t)
|
|
|
|
# Reinitialisation
|
|
self._init_data = self._data.copy()
|
|
|
|
def _delete(self, dn, comment=''):
|
|
""" Sauvegarde puis destruction du dn (et des sous-dn) fourni """
|
|
# Commentaires
|
|
comment = preattr(comment)[1]
|
|
self.modifs.setdefault('destruction (%s)' % comment, None)
|
|
self._save()
|
|
|
|
# Sauvegarde
|
|
t = str(self.__class__).split('.')[-1]
|
|
fd = open('%s/%s/%s_%s' % (config.cimetiere, t,
|
|
time.strftime('%Y-%m-%d-%H:%M'),
|
|
self.nom()), 'wb')
|
|
self.conn = None # Fermeture des connexions à la base sinon cPickle ne marchera pas
|
|
cPickle.dump(self, fd, 2)
|
|
fd.close()
|
|
index = u"%s, %s : %s %s # %s\n" % (time.strftime(date_format),
|
|
script_utilisateur, t,
|
|
self.Nom(), decode(comment))
|
|
|
|
self.connect() # Reconnexion à la base
|
|
# Destruction
|
|
data = self.conn.search_s(dn, 2)
|
|
|
|
data.reverse() # Necessaire pour détruire d'abord les sous-dn
|
|
for r in data:
|
|
self.conn.delete_s(r[0])
|
|
|
|
try:
|
|
log = open(config.cimetiere + '/index', 'a')
|
|
log.write(index)
|
|
log.close()
|
|
except:
|
|
pass
|
|
|
|
def _set(self, champ, val, comment=None):
|
|
"""
|
|
Met à jour les données de _data et modifie modifs si besoin.
|
|
Si val=[] ou None, supprime le champ.
|
|
"""
|
|
# On convertit éventuellement en utf-8 avant
|
|
# On n'utilise pas preattr pour éviter de tout casser... mais
|
|
# il faudra à l'avenir que _set reçoive toujours en unicode
|
|
# et éviter l'utilisation de preattr autant que possible
|
|
def convert(x):
|
|
if type(x) is unicode: return x.encode('utf-8')
|
|
else: return x
|
|
val = map(convert, val)
|
|
comment = convert(comment)
|
|
# On fait le changement et on note s'il y a vraiment eu une modif
|
|
if self._data.get(champ, []) != val:
|
|
if val:
|
|
self._data[champ] = val
|
|
else:
|
|
self._data.pop(champ)
|
|
if self.modifs.get(champ) == None or comment == None:
|
|
self.modifs[champ] = comment
|
|
else:
|
|
# Ici, self.modifs[champ] et comment devraient être tous deux
|
|
# des chaînes de caractères
|
|
self.modifs[champ] += ', ' + comment
|
|
|
|
#############################################################################
|
|
|
|
class BaseProprietaire(BaseClasseCrans):
|
|
""" Méthodes de bases pour les classes Adherent et Club """
|
|
|
|
def __init__(self, data=(), mode='', conn=None):
|
|
"""
|
|
Si data est fourni initialise l'adhérent avec les valeurs données
|
|
Format de data : tuple comme retourné par une recherche dans la base ldap:
|
|
( dn, { donnée })
|
|
Si mode='w' : le propriétaire pourra être modifié
|
|
Attention, si mode ='w' mais si l'objet est déja locké il n'y a pas d'erreur
|
|
vérifier l'obtetion du lock grace à la valeur de _modifiable (si =w c'est bon)
|
|
Il est inutile de préciser le mode pour un nouveau proprietaire
|
|
|
|
conn est une instance de la classe de connexion à la base LDAP
|
|
"""
|
|
self.conn = conn
|
|
if not self.conn:
|
|
self.connect()
|
|
|
|
if type(data) != tuple:
|
|
raise TypeError
|
|
|
|
self.modifs = {}
|
|
if data:
|
|
self.dn = data[0]
|
|
if mode == 'w':
|
|
try:
|
|
self.lock(self.idn, self.id())
|
|
self._modifiable = 'w'
|
|
except EnvironmentError , c:
|
|
self._modifiable = 0
|
|
else:
|
|
self._modifiable = 0
|
|
# Utile pour construire l'instruction LDAP de modif
|
|
self._init_data = data[1].copy()
|
|
self._data = data[1]
|
|
else:
|
|
# Propriétaire vide
|
|
self.dn = '' # Génération du reste au moment de l'écriture
|
|
self._data = { 'objectClass': [ self.objectClass ] }
|
|
self._init_data = {}
|
|
self._modifiable = 'w'
|
|
|
|
def chsh(self, new=None):
|
|
""" Retourne ou change le shell de l'adhérent """
|
|
if new == None:
|
|
try: return decode(self._data.get('loginShell', [''])[0])
|
|
except: return ''
|
|
else:
|
|
new = preattr(new)[1]
|
|
self._set('loginShell', [new])
|
|
return new
|
|
|
|
def alias(self, new=None):
|
|
"""
|
|
Création ou visualisation des alias mail
|
|
Même sytème d'argument que la méthode info.
|
|
"""
|
|
if not self._data.has_key('mailAlias'):
|
|
self._data['mailAlias'] = []
|
|
liste = list(self._data['mailAlias'])
|
|
if new == None:
|
|
return map(decode, liste)
|
|
|
|
if type(new) == list:
|
|
# Modif
|
|
index = new[0]
|
|
new = new[1]
|
|
if new == '':
|
|
# Supression alias
|
|
liste.pop(index)
|
|
self._set('mailAlias', liste)
|
|
return liste
|
|
else:
|
|
new = new.replace('@crans.org', '')
|
|
index = -1
|
|
|
|
# Tests
|
|
l, new = preattr(new)
|
|
new = new.lower()
|
|
if l < 2:
|
|
raise ValueError(u"Alias trop court.")
|
|
for c in new[:]:
|
|
if not c in (string.letters + string.digits + '-_.'):
|
|
raise ValueError(u"Alias : seuls les caractères alphanumériques, le -, le _ et le . sont autorisés." )
|
|
if new[0] not in string.letters:
|
|
raise ValueError(u"Le premier caractère de l'alias doit être alphabétique.")
|
|
if mailexist(new):
|
|
raise ValueError(u"Alias existant ou correspondand à un compte.")
|
|
|
|
if index != -1:
|
|
liste[index] = new
|
|
else:
|
|
liste = liste + [ new ]
|
|
|
|
# Lock de mailAlias
|
|
self.lock('mailAlias', new)
|
|
|
|
self._set('mailAlias', liste)
|
|
return liste
|
|
|
|
def homepageAlias(self, new=None):
|
|
"""
|
|
Creation ou visualisation des alias de page perso.
|
|
Meme systeme d'arguments que pour info.
|
|
"""
|
|
liste = self._data.get('homepageAlias', [])[:]
|
|
if new == None: return liste
|
|
|
|
def validate(alias):
|
|
alias = alias.lower()
|
|
if not 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):
|
|
""" Retourne les factures (instances) appartenant à la classe """
|
|
# Le champ id n'est pas initialisé lorsque le proprio est en cours
|
|
# de création
|
|
if self.id():
|
|
res = []
|
|
for r in self.conn.search_s('%s=%s,%s' % (self.idn, self.id(), self.base_dn), 1, Facture.filtre_idn):
|
|
res.append(self.make(r, self._modifiable))
|
|
return res
|
|
else:
|
|
return []
|
|
|
|
def solde(self, operation=None, comment=None):
|
|
""" Retourne ou modifie le solde d'un propriétaire
|
|
operation doit être un nombre positif ou négatif
|
|
(string ou int ou float)
|
|
comment est un commentaire à rajouter dans l'historique
|
|
"""
|
|
solde = float(self._data.get('solde', [0])[0])
|
|
|
|
if operation == None:
|
|
return solde
|
|
|
|
# On effectue une opération
|
|
try:
|
|
new = solde + float(str(operation).replace(',', '.'))
|
|
except ValueError:
|
|
raise ValueError(u"Il faut donner un nombre en argument.")
|
|
|
|
# découvert accepté
|
|
if new < config.impression.decouvert:
|
|
raise ValueError(u"Solde minimal atteint, opération non effectuée.")
|
|
|
|
self._set('solde', [str(new)], comment)
|
|
return new
|
|
|
|
def controle(self, new=None):
|
|
"""
|
|
Controle du tresorier
|
|
New est de la forme [+-][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 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('iso-8859-15')], 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-dhcp')
|
|
if isinstance(m, MachineWifi):
|
|
self.services_to_restart('conf_wifi_ng')
|
|
self.services_to_restart('gordon-dhcp')
|
|
else:
|
|
self.services_to_restart('sable-dhcp')
|
|
self.services_to_restart('dyson-dhcp')
|
|
# TODO : le rendre plus propre
|
|
self.services_to_restart('titanic-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':
|
|
chgpass(self.dn)
|
|
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(str(e), '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:
|
|
return decode(self._data.get('mail', [''])[0])
|
|
|
|
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('iso-8859-15')])
|
|
|
|
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 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' ]:
|
|
try: self._data.pop(c)
|
|
except: pass
|
|
|
|
def etudes(self, index_or_new):
|
|
"""
|
|
Retourne l'un des 3 champs études (selon index_or_new si entier)
|
|
"""
|
|
if type(index_or_new) == int:
|
|
if self._data.has_key('etudes'):
|
|
return decode(self._data['etudes'][index_or_new])
|
|
else:
|
|
return ''
|
|
|
|
if type(index_or_new) != list:
|
|
raise TypeError
|
|
|
|
if not self._data.has_key('etudes'):
|
|
self._data['etudes'] = ['', '', '']
|
|
|
|
# Pas grand chose à faire à part vérifier que ce sont bien des chaines
|
|
if len(index_or_new) != 3:
|
|
raise ValueError(u"Format études non valides.")
|
|
|
|
new = ['', '', '']
|
|
for i in range(0, 3):
|
|
n = preattr(index_or_new[i])[1]
|
|
if n in new or n == '':
|
|
raise ValueError(u"Etudes non valides.")
|
|
new[i] = n
|
|
|
|
self._set('etudes', new)
|
|
|
|
return new
|
|
|
|
def carteEtudiant(self, action=None):
|
|
"""
|
|
Action est un entier représentant une année
|
|
si positif ajoute l'année à la liste
|
|
si négatif le supprime
|
|
"""
|
|
return self._an('carteEtudiant', action)
|
|
|
|
def checkPassword(self, password):
|
|
"""Vérifie le mot de passe de l'adhérent"""
|
|
anon = ldap.initialize(uri)
|
|
try:
|
|
r = anon.simple_bind(self.dn, password)
|
|
anon.result(r)
|
|
except ldap.INVALID_CREDENTIALS:
|
|
# A priori, le mot de passe est pas bon, il se peut aussi
|
|
# que l'utilisateur n'existe pas
|
|
return False
|
|
return True
|
|
|
|
|
|
def compte(self, login=None, uidNumber=0, hash_pass='', shell=config.login_shell):
|
|
"""
|
|
Création d'un compte à un adhérent
|
|
(la création se fait après l'écriture dans la base par la méthode save)
|
|
Si login = None, retourne le compte de l'adhérent
|
|
"""
|
|
if not login:
|
|
if self.mail().find('@') != -1:
|
|
return ''
|
|
else:
|
|
return self.mail()
|
|
|
|
# Supression des accents et espaces
|
|
login = strip_accents(login)
|
|
|
|
l, login = preattr(login)
|
|
login = login.lower()
|
|
|
|
if 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/' + login
|
|
if os.path.exists(home):
|
|
raise ValueError(u'Création du compte impossible : home existant', 1)
|
|
|
|
if os.path.exists("/var/mail/" + login):
|
|
raise ValueError(u'Création du compte impossible : /var/mail/%s existant' % login, 1)
|
|
|
|
# Lock du mail
|
|
self.lock('mail', login)
|
|
|
|
self._data['mail'] = [login]
|
|
if not 'compte' in self.modifs:
|
|
self.modifs.setdefault('compte', None)
|
|
|
|
# Création de l'alias canonique
|
|
if self.nom() and self.prenom():
|
|
a = '%s.%s' % (self.prenom().capitalize(), self.nom().capitalize())
|
|
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 canonical_alias(self, new=None):
|
|
""" Retourne ou défini l'alias canonique"""
|
|
if new == None:
|
|
try: return decode(self._data['canonicalAlias'][0])
|
|
except: return ''
|
|
else:
|
|
a = strip_accents(new)
|
|
a = preattr(a)[1]
|
|
|
|
if not mailexist(a):
|
|
# Attribution de l'alias, sinon on passe
|
|
|
|
# Lock de canonicalAlias
|
|
self.lock('canonicalAlias', a)
|
|
|
|
# Attribution
|
|
self._set('canonicalAlias', [a])
|
|
return a
|
|
|
|
def droits(self, droits=None, 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:
|
|
from user_tests import isadm
|
|
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(",")]
|
|
return time.mktime(time.strptime(h, date_format))
|
|
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/' + login.replace('-', '/', 1)
|
|
if os.path.exists(home):
|
|
raise ValueError(u'Création du compte impossible : home existant', 1)
|
|
|
|
if os.path.exists("/var/mail/"+login):
|
|
raise ValueError(u'Création du compte impossible : /var/mail/%s existant'%login, 1)
|
|
|
|
# Lock du mail
|
|
self.lock('mail', login)
|
|
|
|
if not 'compte' in self.modifs:
|
|
self.modifs.setdefault('compte', None)
|
|
|
|
self._data['objectClass'] = ['club', 'cransAccount', 'posixAccount', 'shadowAccount']
|
|
self._data['uid'] = [ login ]
|
|
self._data['cn'] = [ preattr(self.Nom())[1] ]
|
|
self._data['loginShell' ] = [ config.club_login_shell ]
|
|
|
|
# Détermination de l'uid
|
|
uidNumber = 1000
|
|
while self.exist('(uidNumber=%s)' % uidNumber):
|
|
uidNumber += 1
|
|
try:
|
|
self.lock('uidNumber', str(uidNumber))
|
|
except:
|
|
# Quelqu'un nous a piqué l'uid que l'on venait de choisir !
|
|
return self.compte(login)
|
|
|
|
self._data['uidNumber'] = [ str(uidNumber) ]
|
|
self._data['gidNumber'] = [ str(config.club_gid) ]
|
|
self._data['homeDirectory'] = [ preattr(home)[1] ]
|
|
|
|
return decode(login)
|
|
|
|
def email(self):
|
|
""" Retourne l'adresse mail du responsable """
|
|
return self.responsable().email()
|
|
|
|
class Machine(BaseClasseCrans):
|
|
""" Classe de définition d'une machine """
|
|
idn = 'mid'
|
|
filtre_idn = '(|(objectClass=machineFixe)(objectClass=machineWifi)'
|
|
filtre_idn += '(objectClass=machineCrans)(objectClass=borneWifi))'
|
|
|
|
def __init__(self, parent_or_tuple, mode='', conn=None):
|
|
"""
|
|
parent_or_tuple est :
|
|
* soit une instance d'une classe pouvant posséder une machine
|
|
(Adherent, Club ou AssociationCrans), la nouvelle machine lui
|
|
sera alors associée.
|
|
* soit directement le tuple définissant une machine (tel que
|
|
retourné par les fonctions de recherche ldap)
|
|
|
|
Pour l'édition d'une machine, mode devra être égal à 'w'
|
|
Attention, si mode='w' mais si l'objet est déja locké il n'y a
|
|
pas d'erreur, vérifier l'obtention du lock grâce à la valeur de
|
|
_modifiable (si ='w' c'est bon)
|
|
|
|
conn est une instance de la classe de connexion à la base LDAP
|
|
"""
|
|
# Définition de l'objectClass LDAP à partir du nom de la classe Python
|
|
self.objectClass = str(self.__class__).split('.')[-1]
|
|
self.objectClass = self.objectClass[0].lower() + self.objectClass[1:]
|
|
|
|
# Initialisation de la connexion
|
|
self.conn = conn
|
|
if not self.conn:
|
|
self.connect()
|
|
|
|
self.modifs = {}
|
|
t = parent_or_tuple.__class__
|
|
if t == tuple:
|
|
# Initialisation avec données fournies
|
|
self.dn = parent_or_tuple[0]
|
|
if mode == 'w':
|
|
try:
|
|
self.lock(self.idn, self.id())
|
|
self._modifiable = 'w'
|
|
except EnvironmentError , c:
|
|
self._modifiable = 0
|
|
else:
|
|
self._modifiable = 0
|
|
# Utile pour construire l'instruction LDAP de modif
|
|
self._init_data = parent_or_tuple[1].copy()
|
|
self._data = parent_or_tuple[1]
|
|
|
|
# Propriéraire inconnu mais ce n'est pas grave
|
|
self.__proprietaire = None
|
|
|
|
elif t in [Adherent, Club, AssociationCrans] and mode != 'w':
|
|
# Machine vide
|
|
self.__proprietaire = parent_or_tuple
|
|
self.dn = parent_or_tuple.dn
|
|
self._data = {'objectClass': [self.objectClass]}
|
|
self._init_data = {}
|
|
self._modifiable = 'w'
|
|
|
|
chbre = self.__proprietaire.chbre()
|
|
# if chbre == 'EXT' and mode == 'fixe':
|
|
# raise ValueError(u'Il faut une chambre pour pouvoir posséder une machine fixe')
|
|
|
|
if chbre == '????':
|
|
raise ValueError(u'ERREUR: la chambre du propriétaire est inconnue')
|
|
|
|
else:
|
|
raise TypeError(u'Arguments invalides')
|
|
|
|
def Nom(self):
|
|
""" Retourne le nom de la machine """
|
|
return self.nom()
|
|
|
|
def mac(self, mac=None, multi_ok=0):
|
|
"""
|
|
Défini ou retourne l'adresse mac de la machine
|
|
Adresse valide si:
|
|
12 caractères hexa avec - ou : comme séparateurs
|
|
non nulle
|
|
Stoque l'adresse sous la forme xx:xx:xx:xx:xx:xx
|
|
Si multi_ok = 1 permet d'avoir plusieur fois la même mac sur le réseau
|
|
"""
|
|
|
|
if mac == None:
|
|
return decode(self._data.get('macAddress', [''])[0])
|
|
|
|
mac = format_mac(mac)
|
|
|
|
# La MAC serait-elle 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
|
|
self.lock('macAddress', mac)
|
|
|
|
self._set('macAddress', [mac])
|
|
return mac
|
|
|
|
def __host_alias(self, champ, new):
|
|
""" Vérification de la validité d'un nom de machine """
|
|
# Supression des accents
|
|
new = strip_accents(unicode(new, 'iso-8859-15'))
|
|
|
|
l, new = preattr(new)
|
|
new = new.lower()
|
|
l = len(new.split('.')[0])
|
|
|
|
if l<2:
|
|
raise ValueError(u"%s trop court." % champ.capitalize())
|
|
|
|
if self.proprietaire().__class__ != AssociationCrans:
|
|
new = new.split('.', 1)[0]
|
|
for c in new[:]:
|
|
if not c in (string.letters + string.digits + '-'):
|
|
raise ValueError(u"Seuls les caractères alphabétiques minuscules et les - sont autorisés pour %s" % champ)
|
|
|
|
if l > 17:
|
|
raise ValueError(u"%s trop long." % champ.capitalize())
|
|
|
|
if 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
|
|
index = new[0]
|
|
new = new[1]
|
|
if new == '':
|
|
liste.pop(index)
|
|
self._set('hostAlias', liste)
|
|
return liste
|
|
else:
|
|
index = -1
|
|
|
|
# Test si valide
|
|
new = self.__host_alias('alias', new)
|
|
|
|
if index != -1:
|
|
liste[index] = new
|
|
else:
|
|
liste = liste + [new]
|
|
|
|
self._set('hostAlias', liste)
|
|
return liste
|
|
|
|
def ip(self, ip=None):
|
|
"""
|
|
Défini ou retourne l'IP de la machine.
|
|
Les IP sont stoquées sous forme xxx.xxx.xxx.xxx et doivent être fournies ainsi.
|
|
Si l'IP n'est pas définie retourne <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))
|
|
|
|
else:
|
|
# L'ip est elle dans le bon sous-réseau ?
|
|
# (accessoirement teste si l'IP est valide et ne correspond pas
|
|
# à l'adresse de broadcast ou de réseau)
|
|
if not iptools.AddrInNet(ip, net):
|
|
raise ValueError(u'IP invalide ou en dehors du sous-réseau alloué.', 1)
|
|
# Reformatage
|
|
ip = iptools.DecToQuad(iptools.QuadToDec(ip))
|
|
# L'ip est-elle déja allouée ?
|
|
if self.exist('ipHostNumber=%s' % ip):
|
|
raise ValueError(u'IP déjà prise.')
|
|
|
|
# Lock ip
|
|
self.lock('ipHostNumber', ip)
|
|
|
|
self._set('ipHostNumber', [ip])
|
|
return ip
|
|
|
|
def exempt(self, new=None):
|
|
"""
|
|
Liste des réseaux vers lesquels on ne compte pas l'upload
|
|
Cette liste est transférée dans la base postgres de komaz
|
|
Pour ajouter un réseau new doit être la chaîne
|
|
représentant le réseau à ajouter
|
|
Pour modifier new doit être une liste de la forme :
|
|
[ index du nouveau réseau , nouveau réseau ]
|
|
l'index est celui obtenu dans la liste retournée par exempt()
|
|
"""
|
|
if not self._data.has_key('exempt'):
|
|
self._data['exempt'] = []
|
|
liste = list(self._data['exempt'])
|
|
if new == None: return map(decode, liste)
|
|
|
|
if type(new) == list:
|
|
# Modif
|
|
index = new[0]
|
|
l, new = preattr(new[1])
|
|
if not new:
|
|
# Supression réseau
|
|
liste.pop(index)
|
|
else:
|
|
# Modif remarque
|
|
liste[index] = new
|
|
elif type(new) == str:
|
|
# Réseau supplémentaire
|
|
l, new = preattr(new)
|
|
if not new:
|
|
# On ajoute pas de réseau vide
|
|
return liste
|
|
# Ajout à la liste
|
|
liste = liste + [new]
|
|
else:
|
|
raise TypeError
|
|
|
|
self._set('exempt', liste)
|
|
return liste
|
|
|
|
def proprietaire(self):
|
|
"""
|
|
retroune le propriétaire de la machine (classe Adherent, Club ou AssociationCrans)
|
|
"""
|
|
if not self.__proprietaire:
|
|
res = self.conn.search_s(','.join(self.dn.split(',')[1:]), 0)
|
|
if 'adherent' in res[0][1]['objectClass']:
|
|
self.__proprietaire = Adherent(res[0], self._modifiable, self.conn)
|
|
elif 'club' in res[0][1]['objectClass']:
|
|
self.__proprietaire = Club(res[0], self._modifiable, self.conn)
|
|
else:
|
|
self.__proprietaire = AssociationCrans(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.
|
|
"""
|
|
from user_tests import isadm
|
|
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 = ''
|
|
|
|
# Besoin de redémarrer les firewalls ?
|
|
if 'ipHostNumber' in self.modifs or 'macAddress' in self.modifs:
|
|
reconf_ip = self._init_data.get('ipHostNumber', [])
|
|
reconf_ip += self._data.get('ipHostNumber', [])
|
|
else:
|
|
reconf_ip = []
|
|
|
|
# On vire les doublons dans reconf_ip
|
|
reconf_ip = list(dict(zip(reconf_ip, [None]*len(reconf_ip))))
|
|
|
|
# Enregistrement
|
|
self._save()
|
|
|
|
# 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')
|
|
self.services_to_restart('conf_wifi_ng')
|
|
|
|
# Reconfiguration firewalls et dhcps
|
|
if reconf_ip:
|
|
self.services_to_restart('macip', reconf_ip)
|
|
self.services_to_restart('classify', reconf_ip)
|
|
self.services_to_restart('dhcp-dhcp')
|
|
if isinstance(self, MachineWifi) or isinstance(self, BorneWifi):
|
|
self.services_to_restart('gordon-dhcp')
|
|
else:
|
|
self.services_to_restart('sable-dhcp')
|
|
# TODO : le rendre plus propre
|
|
self.services_to_restart('titanic-dhcp')
|
|
self.services_to_restart('dyson-dhcp')
|
|
if 'portTCPin' in self.modifs or 'portTCPout' in self.modifs or \
|
|
'portUDPin' in self.modifs or 'portUDPout' in self.modifs:
|
|
self.services_to_restart('komaz-ports', [self.ip()])
|
|
self.services_to_restart('mail_modif', ['ip=%s' % self.ip()])
|
|
|
|
# Reconfiguration DNS ?
|
|
if 'host' in self.modifs or 'ipHostNumber' in self.modifs or 'hostAlias' in self.modifs or 'dnsIpv6' in self.modifs:
|
|
self.services_to_restart('dns')
|
|
|
|
# Reconfiguration bornes wifi ?
|
|
if 'canal' in self.modifs or 'puissance' in self.modifs or 'nvram' in self.modifs or 'hotspot' in self.modifs:
|
|
self.services_to_restart('conf_wifi_ng')
|
|
|
|
# Reconfiguration clients wifi ?
|
|
if isinstance(self, MachineWifi) or isinstance(self, BorneWifi) \
|
|
and ('ipHostNumber' in self.modifs or 'host' in self.modifs or 'macAddress' in self.modifs):
|
|
self.services_to_restart('conf_wifi_ng')
|
|
|
|
# Regénération blackliste nécessaire ?
|
|
bl = self.blacklist_actif()
|
|
if bl and ('ipHostNumber' in self.modifs or 'host' in self.modifs):
|
|
for s in bl:
|
|
self.services_to_restart(s, [self.ip()])
|
|
|
|
# Regénération de l'autostatus et mail de changmement ?
|
|
if self.proprietaire().__class__ == AssociationCrans:
|
|
self.services_to_restart('autostatus')
|
|
self.services_to_restart('mail_modif', ['ip=%s' % self.ip()])
|
|
|
|
# Synchronisation avec la base pgsql pour les exemptions
|
|
if 'exempt' in self.modifs or ('ipHostNumber' in self.modifs and self.exempt()):
|
|
self.services_to_restart('surveillance_exemptions')
|
|
|
|
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"""
|
|
from user_tests import isadm
|
|
if self.proprietaire().__class__ == AssociationCrans and not isadm():
|
|
raise EnvironmentError(u'Il faut être administrateur pour effectuer cette opération.')
|
|
|
|
|
|
self.proprio = self.__proprietaire.Nom() # On met dans un coin le nom du proprio
|
|
self.__proprietaire = None # On oublie le propriétaire
|
|
self._delete(self.dn, comment)
|
|
|
|
# Services à redémarrer
|
|
self.services_to_restart('dhcp-dhcp')
|
|
if isinstance(self, MachineWifi):
|
|
self.services_to_restart('conf_wifi_ng')
|
|
self.services_to_restart('gordon-dhcp')
|
|
else:
|
|
self.services_to_restart('sable-dhcp')
|
|
self.services_to_restart('dyson-dhcp')
|
|
# TODO : le rendre plus propre
|
|
self.services_to_restart('titanic-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"""
|
|
classe = {
|
|
"crans.org": "fil",
|
|
"ferme.crans.org": "fil",
|
|
"adm.crans.org": "adm",
|
|
"wifi.crans.org": "wifi"
|
|
}.get(self.Nom().split('.', 1)[1], "fil")
|
|
return netaddr.IPNetwork(config.prefix[classe][0])
|
|
|
|
def ipv6(self):
|
|
"""Retourne l'adresse IPv6 correspondant à la machine"""
|
|
|
|
net = self.netv6()
|
|
|
|
if self.machineAlias():
|
|
return netaddr.IPAddress(net.first + int(self.id()))
|
|
else:
|
|
return ip6tools.mac_to_ipv6(net, netaddr.EUI(self.mac()))
|
|
|
|
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':['paypal']}
|
|
self._init_data = {}
|
|
self._modifiable = 'w'
|
|
|
|
else:
|
|
raise TypeError(u'Arguments invalides')
|
|
|
|
def numero(self):
|
|
""" Retourne le numéro de facture """
|
|
fid = self._data.get('fid', [None])[0]
|
|
if fid == None:
|
|
raise NotImplementedError, u'Il faut enregistrer une facture pour connaitre son numero'
|
|
return fid
|
|
|
|
def nom(self):
|
|
""" Utilisé pour la fonction delete() """
|
|
return "Facture%s" % self.numero()
|
|
|
|
Nom = nom
|
|
|
|
def proprietaire(self):
|
|
"""
|
|
retroune le propriétaire de la facture (classe Adherent ou Club)
|
|
"""
|
|
# si la facture est en mode w mais pas le proprio, on tente de prendre
|
|
# le proprio en w
|
|
if self.__proprietaire and self.__proprietaire._modifiable != self._modifiable:
|
|
self.__proprietaire = None
|
|
|
|
# récupère le proprio si ce n'est pas encore fait
|
|
if not self.__proprietaire:
|
|
res = self.conn.search_s(','.join(self.dn.split(',')[1:]), 0)
|
|
if 'adherent' in res[0][1]['objectClass']:
|
|
self.__proprietaire = Adherent(res[0], self._modifiable, self.conn)
|
|
elif 'club' in res[0][1]['objectClass']:
|
|
self.__proprietaire = Club(res[0], self._modifiable, self.conn)
|
|
else:
|
|
raise ValueError, u'Propriétaire inconnu'
|
|
|
|
return self.__proprietaire
|
|
|
|
def modePaiement(self, new=None):
|
|
"""
|
|
Définit ou retourne le mode de paiement.
|
|
Le mode de paiement doit être une chaine de caractère
|
|
"""
|
|
|
|
# modification du mode de paiement
|
|
if new != None:
|
|
if self.recuPaiement():
|
|
raise ValueError, u'Facture déja payée'
|
|
|
|
if not self._modifiable:
|
|
raise NotImplementedError, "La facture n'est pas modifiable"
|
|
|
|
if new not in ['liquide', 'cheque', 'paypal']:
|
|
raise ValueError, u'Mode de paiement non accepté'
|
|
|
|
self._set('modePaiement', [new])
|
|
|
|
return decode(self._data.get('modePaiement', [None])[0])
|
|
|
|
def recuPaiement(self, new=None):
|
|
"""
|
|
Définit ou retourne qui a recu le paiement
|
|
"""
|
|
|
|
# on vérifie que la facture n'est pas déja payéee
|
|
if new and self._data.get('recuPaiement', []):
|
|
raise ValueError, u'Facture déja payée'
|
|
|
|
# modification de la valeur
|
|
if new != None:
|
|
# on vérifie que la facture est modifiable
|
|
if not self._modifiable and new:
|
|
raise NotImplementedError, "La facture n'est pas modifiable"
|
|
|
|
# on crédite les articles, si c'est pas possible, la metode
|
|
# levera une exeption
|
|
self._crediter()
|
|
|
|
# ajout des frais à la liste d'articles
|
|
self.ajoute(self._frais())
|
|
|
|
# modifie la base ldap
|
|
self._set("recuPaiement", [new])
|
|
|
|
# renvoie la valeur trouvée dans la base
|
|
return self._data.get("recuPaiement", [None])[0]
|
|
|
|
def _del_recu_paiement(self):
|
|
""" Pour test """
|
|
self._set("recuPaiement", [])
|
|
|
|
def _crediter(self):
|
|
"""
|
|
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
|
|
if art["code"] == "SOLDE":
|
|
proprio = self.proprietaire()
|
|
proprio.solde(operation=art['nombre']*art["pu"], comment="Facture n°%s : %s" % (self.numero(), art['designation']))
|
|
proprio.save()
|
|
|
|
def _frais(self):
|
|
"""
|
|
Retourne une liste d'articles correspondants aux divers frais
|
|
"""
|
|
|
|
arts = []
|
|
|
|
# aucun frais pour une facture payée, ils sont intégrés aux articles
|
|
if self.recuPaiement():
|
|
return []
|
|
|
|
# frais de paiement par paypal
|
|
if self.modePaiement() == 'paypal':
|
|
# 25 centimes pour le paiement paypal
|
|
s = 0.25
|
|
|
|
# et on ajoute 3.5% du montant
|
|
for art in self._articles():
|
|
s += 0.035 * art['nombre'] * art['pu']
|
|
|
|
# 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):
|
|
"""Supprime un/des article(s) à la facture
|
|
arts est un article ou une liste d'articles
|
|
"""
|
|
# on ne eut pas modifier une facture payée
|
|
if self.recuPaiement():
|
|
raise ValueError, u'On ne peut pas modifier une facture payée'
|
|
|
|
# charge la liste des articles
|
|
arts = self._articles()
|
|
|
|
# on supprime les anciens articles
|
|
if type(supprime)==dict:
|
|
supprime = [supprime]
|
|
if type(supprime)==list:
|
|
for art in supprime:
|
|
arts.remove(art)
|
|
|
|
# enregistre la nouvelle liste
|
|
self._articles(arts)
|
|
|
|
def articles(self):
|
|
"""
|
|
Retourne la liste des articles.
|
|
Un article est un dictionnaire de la forme :
|
|
{ 'code' : string,
|
|
'designation' : string,
|
|
'nombre' : int,
|
|
'pu' : int/float }
|
|
"""
|
|
return self._articles() + self._frais()
|
|
|
|
def total(self):
|
|
"""
|
|
Calcule le total de la facture, frais compris
|
|
"""
|
|
s = 0
|
|
for art in self.articles():
|
|
s += art['nombre'] * art['pu']
|
|
return s
|
|
|
|
def urlPaypal(self, 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 += "¤cy_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
|
|
return []
|
|
def paiement(self):
|
|
return [ ann_scol ]
|
|
def carteEtudiant(self):
|
|
return [ ann_scol ]
|
|
def blacklist_actif(self):
|
|
return []
|
|
def mail(self, new=None):
|
|
return 'roots@crans.org'
|
|
def machines(self):
|
|
res = self.conn.search_s(self.dn, 1, Machine.filtre_idn)
|
|
m = []
|
|
for r in res:
|
|
m.append(self.make(r))
|
|
return m
|
|
|
|
class AssociationCrans(_FakeProprio):
|
|
""" Classe définissant l'assoce (pour affichage de ses machines) """
|
|
def __init__(self, conn=None):
|
|
_FakeProprio.__init__(self, conn)
|
|
def Nom(self):
|
|
return "Crans"
|
|
def chbre(self):
|
|
return "CRA"
|
|
def email(self):
|
|
return "roots@crans.org"
|
|
|
|
|
|
db = None
|
|
def crans_ldap(readonly=False):
|
|
""" Renvoie un objet CransLdap """
|
|
global db
|
|
if readonly:
|
|
return CransLdap(readonly)
|
|
if db == None:
|
|
db = CransLdap()
|
|
return db
|
|
|
|
|
|
if __name__ == '__main__':
|
|
import sys
|
|
|
|
usage = u"""Usage %s [ACTION]
|
|
--lock : donne la liste des locks actifs
|
|
--purgelock : supprime tous les locks de la base LDAP
|
|
--menage : supprime les machines des anciens adhérents""" % sys.argv[0]
|
|
|
|
if len(sys.argv) != 2:
|
|
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 '--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)
|